Compare commits
118
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c90099ea9 | ||
|
|
c515273db7 | ||
|
|
da3051ccc6 | ||
|
|
060a17b549 | ||
|
|
ffd90e7fc4 | ||
|
|
02709125a6 | ||
|
|
c771a36f4b | ||
|
|
caa4b59440 | ||
|
|
57f59e0c39 | ||
|
|
5465fd52d5 | ||
|
|
b00d7d94d0 | ||
|
|
642a9b87de | ||
|
|
16019b80e6 | ||
|
|
9b53620185 | ||
|
|
7662b04902 | ||
|
|
04ce8dfd56 | ||
|
|
fc1cb856b0 | ||
|
|
c0c4907af9 | ||
|
|
5fb736b92d | ||
|
|
96bac588fb | ||
|
|
5d4a617762 | ||
|
|
e65a3ab4d2 | ||
|
|
0444acd084 | ||
|
|
052ba95c7c | ||
|
|
a52d64ed41 | ||
|
|
8fede5fc6a | ||
|
|
c084d8d423 | ||
|
|
f5753c0e8d | ||
|
|
9d165be8c7 | ||
|
|
c25785d3d5 | ||
|
|
ad95c888c3 | ||
|
|
584e2bd827 | ||
|
|
0adf8ddfe2 | ||
|
|
d7d9431990 | ||
|
|
a0e3d7cfdd | ||
|
|
9d24ff9b28 | ||
|
|
6c04b89dae | ||
|
|
6f7dacb417 | ||
|
|
d21b2615c1 | ||
|
|
816d48b03e | ||
|
|
011d41ba6a | ||
|
|
c8bc5cf4cb | ||
|
|
390ea5261c | ||
|
|
cb5fdb064f | ||
|
|
76134dfde1 | ||
|
|
9e10858160 | ||
|
|
7221367728 | ||
|
|
a304dad947 | ||
|
|
919a84738b | ||
|
|
2d8687bcfd | ||
|
|
1c46d0abf6 | ||
|
|
f519fb1d26 | ||
|
|
b8236a7e13 | ||
|
|
c7675476b1 | ||
|
|
9de7ad8292 | ||
|
|
c40d846cb8 | ||
|
|
01b40d3576 | ||
|
|
ad6f0be7aa | ||
|
|
ffc124bf68 | ||
|
|
8a14931fa4 | ||
|
|
0272383f58 | ||
|
|
00d4cc1f69 | ||
|
|
99a406ed3c | ||
|
|
d0bbc5c1cb | ||
|
|
c9f374284b | ||
|
|
4385b9ec18 | ||
|
|
afad4993f6 | ||
|
|
f41729178e | ||
|
|
305857c8f9 | ||
|
|
176a6bf4b9 | ||
|
|
5b8def2f14 | ||
|
|
4429a104f6 | ||
|
|
32b54ac3b0 | ||
|
|
6025ddd9cf | ||
|
|
ecbfd6deb6 | ||
|
|
0498c0613f | ||
|
|
49d1f3a231 | ||
|
|
628e2ca5cd | ||
|
|
088087ac96 | ||
|
|
a0fbefd48d | ||
|
|
f24f2cd40d | ||
|
|
9ba31b5b3f | ||
|
|
9db578ff54 | ||
|
|
8eb9ddd12c | ||
|
|
ae3c36f248 | ||
|
|
a2af7ab495 | ||
|
|
aa1bd209e3 | ||
|
|
ace27dc5eb | ||
|
|
a4a42dbac2 | ||
|
|
a31f1a0f8b | ||
|
|
992650232f | ||
|
|
a7661ada06 | ||
|
|
f871cb3096 | ||
|
|
0bbd23c619 | ||
|
|
0217b71c8f | ||
|
|
c6f528a32f | ||
|
|
96f243fafc | ||
|
|
c862699352 | ||
|
|
48ebee0ffe | ||
|
|
66bdd84b8f | ||
|
|
6966164f0c | ||
|
|
53bb594621 | ||
|
|
5e01c36833 | ||
|
|
9f56d2098d | ||
|
|
0599b119f4 | ||
|
|
b416d2ede9 | ||
|
|
5fc73a729c | ||
|
|
99772421ab | ||
|
|
da230ca489 | ||
|
|
4f2d1be68a | ||
|
|
d55d86bb04 | ||
|
|
c56ccf916e | ||
|
|
817e8f51b0 | ||
|
|
5d019d8768 | ||
|
|
59475fc5a8 | ||
|
|
1a268249e6 | ||
|
|
def2e79327 | ||
|
|
c30b36fffb |
@@ -25,10 +25,15 @@ green at every step. Not production software — a proven foundation.
|
||||
1. **Root is bootstrap-only.** First-class system configuration lives at the
|
||||
repository root; everything else gets a dedicated directory (`roles/`,
|
||||
`contracts/`, `missions/`, `tasks/`, `docs/`). Do not add new files to root.
|
||||
2. **Configuration**: `~/.config/mosaic-dev/config.json` is the sole system
|
||||
config — created only by `scripts/bootstrap.sh`, never overwritten,
|
||||
fail-closed on any problem. Repo-scoped role authority lives in
|
||||
`roles/*.json` (versioned, reviewed commits only).
|
||||
2. **Configuration**: each stack instance has exactly one system config. The
|
||||
default instance's is `~/.config/mosaic-dev/config.json`. Instance NAME's
|
||||
is `~/.config/mosaic-NAME/config.json`, and its data root carries a marker
|
||||
that names it. `~` is the home of the OS user that runs the instance. A
|
||||
process reads only the config `MOSAIC_CONFIG` names, or the default's when
|
||||
that is unset. Configs are created only by
|
||||
`scripts/bootstrap.sh [--instance NAME]`, never overwritten, fail-closed on
|
||||
any problem. Repo-scoped role authority lives in `roles/*.json`
|
||||
(versioned, reviewed commits only).
|
||||
3. **Secrets** never enter the repository or container images; auth is
|
||||
runtime-only (read-only mount or environment variable).
|
||||
4. **Contracts** (`contracts/`) are immutable and image-baked. Missions and
|
||||
@@ -177,8 +182,8 @@ Full reference — usage, fields, exit codes, safety notes:
|
||||
|
||||
## Data map (canon)
|
||||
|
||||
- `~/.config/mosaic-dev/config.json` — system config (user-authored; never
|
||||
auto-written).
|
||||
- `~/.config/mosaic-dev/config.json`, or `~/.config/mosaic-NAME/config.json`
|
||||
per named instance: system config (user-authored; never auto-written).
|
||||
- `<dataRoot>` (from config; default `~/.mosaic-dev`):
|
||||
- `runs/` — write-once run evidence (`result.json`, snapshots, `stderr.txt`)
|
||||
- `sessions/` — pi JSONL session trees, one directory per named session
|
||||
|
||||
@@ -5174,3 +5174,6 @@ Before: managed project/thread verbs had no Console conversation area. After: fi
|
||||
2026-10-20 04:44:54 UTC | Row 178 round 2 preparation | Before: Dewey C1 five render guards lacked killing tests. Added independent refusal fixtures, a temporary uid+1 id stub with cleanup, and empty loopback proof case; activation ordering recorded in r2 BUILD. Product unchanged; syntax/text preflight only, no test or mutant execution. Pending row 181 rebase and Sage slot.
|
||||
|
||||
2026-10-20 05:10:34 UTC | Row 178 #1676 round 2 verified | Before: C1 render refusals lacked killing tests and row 181 base was pending. Rebased onto 931651f2d, kept all COUNTED/count code and tests, appended T178 tail; added independent owner/collision/absolute/charset/header and empty-loopback proof tests. Four files singly 156/156, all 24 mutants killed, metadata and sandbox fixture probes pass; scratch sweep empty. Packet agents/rocko/work/queue-178/r2/, candidate 9300a670, inventory d0dcde7b (59/59), scan 58 files no findings; in-review revs 2166-2167, expected no-token request failure. No live actions or commits.
|
||||
2026-10-21T01:28:05Z | Darkwing | row 232 #1734 installer I1 build, round 1 in review | Before: stack-release.sh managed only user units (systemctl --user), and stack-copy.sh had no bundle copy. After: --units external (adopt, activate, rollback --to, status, confirm, revert, gate --since, AWAITING-UNITS, stale-start, external-pm, --plan --expect-from) and stack-copy check --units external, copy --bundle FILE SHA, wrapper ID --create; user mode byte-identical to the base by test. First build 0af0c73c6 on 00d4cc1f6; rebased onto row 200 (cb5fdb064) as a0ffabc3f on darkwing/row232-r1; --sso refused under external (D9), row 200 config_checks run under external (D10). Gate on Sage's slot (sage-1618): cli 561/0, every scripts/test-*.sh green; 17/17 mutants killed. Results commit 84f1a91e6 (pushed, seat branch only). Manifest 7a3b4ea1 (9 paths), queue revs 2595-2596, request comment 29825, reviewer Filbert. Packet agents/darkwing/work/queue-232/BUILD.md.
|
||||
2026-10-21T04:50:44Z | Darkwing | row 233 #1735 installer I3 build, round 1 in review | Before: root had no upgrade driver for root-owned units and no host run sheet; stack-release.sh had no rehearse --backup last and no external plan refusals. After: scripts/stack-upgrade.sh upgrade and rollback (r6 A1-A5, A2f, R1-R4) with 20 tests, docs/guides/host-run-sheet.md, TOOLS.md lines, and on Sage's rulings stack-release.sh --backup last, BACKUP_STORE and the --plan refusals open-activation and migrate-required (3 tests). Base e65a3ab4d (row 232's landing). Gate on Sage's slot (sage-1704) on d99166ade: cli 616/0, every scripts/test-*.sh green, 1201/0 summed by awk; m1-m8 8/8 killed. Correction: the process sweep after the gate found an orphaned sleep 60 from the lock test's flock holder (N1); 08db6ac3f fixes it test only, Sage ruled the gate stands (sage-1707), rerun c0 20/0 and m1 killed with 0 left (sage-1708). Tip 1a1dc80ca (pushed, seat branch only). Manifest 464ce9eb (7 paths), queue revs 2644 and 2647, request comment 29901, reviewer Filbert. Packet agents/darkwing/work/queue-233/BUILD.md.
|
||||
2026-10-21T06:37:41Z | Darkwing | row 240 #1746 installer I4 build, round 1 in review | Before: root had no host check for the bwrap AppArmor profile, its pins or a boot unit, and stack-upgrade.sh exited 1 args-failed when the args move after A5 failed, while exit 1 means the old copy runs. After: scripts/case1-hostcheck.sh (seven items, --check, --enforce, --upstream), scripts/case1-pins.sh, mosaic-hostcheck.service and 16 tests; the args move failure exits 5 args-failed, documented in TOOLS.md and run sheet section 6 and tested; run sheet section 3 puts the pinned npm on PATH and names the bin/claude.exe --claude path; sections 8 and 10 have precheck lines. Verification: slot run r1 on 47ec9fe2f failed at the singles (the new N1 test's after hook threw ENOENT after the fixture cleanup removed the root); b45852daa drops the hook; r2 on b45852daa green: singles 16/0 and 21/0, c0 plus M1-M8 8/8 killed, cli 639/0, scripts 585/0. Candidate darkwing/row240-r3 ad1b23ab2 on c771a36f4, manifest 573ece95 (9 paths), revs 2694 and 2695, comment 29943.
|
||||
|
||||
@@ -16,6 +16,17 @@ not fleet seats. Keep changes in `/mnt/storage/src/mosaic-stack`; do not modify
|
||||
| Rocko | General development, investigation, testing, and review | T3 session (Codex, gpt-6.1-sol, thread d99326cc, lead decision 122; Claude thread b84bb264 retired 2026-10-12); launcher Claude Code, Sonnet model | `agents/rocko/launch.sh` |
|
||||
| Filbert | General development, investigation, testing, and review | T3 session (Claude, Opus 5.5, thread 048f3c63, non-sandbox reviews only, lead decisions 130 and 134; Codex threads f712d8af and 50342f6a retired 2026-10-13, Claude thread 9cb9731e retired 2026-10-12); launcher Pi, `openai-codex/gpt-6-astra:low`, not to be used under R26 until it changes (lead decision 69) | `agents/filbert/launch.sh` |
|
||||
| Researcher | Source-grounded technical research and evidence | T3 session (Claude, Sonnet 5.5, thread 9bac0794, rows 97 and 98, lead decision 124); Haiku rows (71, 80, 84, 88, later 96) run as subagents Sage dispatches with model haiku, effort high and a worktree (lead decision 129); Haiku thread c98aaf4a retired 2026-10-13 unused; launcher Pi, configured Mosaic model | `agents/researcher/launch.sh` |
|
||||
| Launchpad | Builder, MVP T1 row 192 (Q175 SSO phase 1) | T3 session (Claude, Opus 5.5), started by Sage under mos-0974/0977 | none, T3 only |
|
||||
| Morgana | Builder, MVP T1 row 193 (human users and RBAC) | T3 session (Claude, Opus 5.5), started by Sage under mos-0974/0977 | none, T3 only |
|
||||
| Honker | Builder, MVP T1 row 194 (agent lifecycle) | T3 session (Claude, Opus 5.5), started by Sage under mos-0974/0977 | none, T3 only |
|
||||
| Gander | Builder, the human boundary across uids (P19 333, #1694) | T3 session (Codex, gpt-6.1-sol), started by Sage under mos-0977 | none, T3 only |
|
||||
| Herb | Builder, a second gate lane (#1695) | T3 session (Codex, gpt-6.1-sol), started by Sage under mos-0977 | none, T3 only |
|
||||
| Bushroot | Builder, retire the control board writer on 7331 (#1696) | T3 session (Claude, claude-haiku-5-5), started by Sage under mos-1008 | none, T3 only |
|
||||
| Megavolt | Builder, MVP T1 item 6 (agent credentials and git identity, #1697) | T3 session (Claude, Opus 5.5), started by Sage under mos-0974/0977 | none, T3 only |
|
||||
| Quackerjack | Builder, federation R5 and R2 (#414) | T3 session (Claude, claude-haiku-5-5), started by Sage under mos-1023 | none, T3 only |
|
||||
| Gizmoduck | Federation design (#414) and builder of FED R1 | T3 session (Claude, Opus 5.5, thread 9312a896) | none, T3 only |
|
||||
| Webby | Reviewer: Case 2 designs, rows 192 and 194 | T3 session (Claude, Opus 5.5, thread 03086006), started by Mos (mos-0981) | none, T3 only |
|
||||
| Duckworth | Reviewer: federation designs, row 193 | T3 session (Claude, Opus 5.5, thread ffceafec), started by Mos (mos-0977) | none, T3 only |
|
||||
| Sage | Project lead: coordination, review, integration; earlier DYOR strategy records retained | T3 session (Claude Code); Pi launcher `zai/glm-5.3:high` retained | `agents/sage/launch.sh` |
|
||||
|
||||
Each script supports `--check` and `--fresh`. Normal launches resume the agent's
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# Bushroot
|
||||
|
||||
Builder seat, Claude Haiku 5.5, started by Sage under mos-1008 item 2 (a low-level model under Q177) for the row
|
||||
that retires the control board writer on 7331 (#1696).
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no
|
||||
credentials of its own; until the credentials row settles per-agent identity, it pushes only its own `bushroot/*`
|
||||
branches through the repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch.
|
||||
@@ -0,0 +1,287 @@
|
||||
# Row 232 (I1): stack-release external units and bundle copies
|
||||
|
||||
Brief: `docs/plans/2026-10-20_installer-rows.md` section I1, blob `f35e7e57daee` (commit ad6f0be7a). Gitea
|
||||
#1734, Vikunja P19 #432. Owner Darkwing, reviewer Filbert, gate owner Sage. Design input: row 195 DESIGN r6,
|
||||
`agents/darkwing/work/queue-195/DESIGN.md` at b7239bb66 (approved in duckworth-0055). Where the brief and
|
||||
r6 differ, the brief wins. sage-1561 ruled where the stack user's `stack-release.sh` runs: from its own
|
||||
clone of the release bundle, so `check_target`, the plan's commit list and status drift are unchanged and
|
||||
nothing adds `safe.directory`.
|
||||
|
||||
## Files
|
||||
|
||||
Base: cb5fdb064 (row 199's landing, on top of row 200's 9e1085816). Branch `darkwing/row232-r1`. The first
|
||||
build, 0af0c73c6 on `darkwing/row232`, sat on 00d4cc1f6 (row 190); row 200 landed first, so this one is
|
||||
rebased onto it (see D9 and D10).
|
||||
|
||||
- `scripts/stack-release.sh`: `--units user|external`, the `sc` and `jctl` backend, `confirm`, `revert`,
|
||||
`gate --since`, `rollback --to`, `--plan --expect-from`, the external gate (start floor, PM check from
|
||||
`launches list`), `external-pm`, and the `awaiting units:` status line.
|
||||
- `scripts/stack-copy.sh`: `check --units external`, `copy --bundle FILE SHA`, `wrapper ID --create`.
|
||||
- `packages/cli/tests/stack-release-external.test.mjs` (new, 25 tests).
|
||||
- `packages/cli/tests/stack-release-fixture.mjs` (new): the helpers `stack-release.test.mjs` defined, moved
|
||||
so both files share them. See deviation D1.
|
||||
- `packages/cli/tests/stack-release.test.mjs`: the helpers leave and one import line comes in. The 74 tests
|
||||
have the same names and the same bodies as on the base.
|
||||
- `packages/cli/tests/stack-copy.test.mjs`: three new `row 232:` tests and a wider `node:fs` import.
|
||||
- `docs/TOOLS.md`: the stack-copy usage block and three paragraphs, the stack-release usage block, and a new
|
||||
subsection "Root-owned units (`--units external`, row 232)".
|
||||
- `packages/cli/README.md`: one paragraph.
|
||||
- This file.
|
||||
|
||||
The worktree's `node_modules` symlink is not part of the candidate.
|
||||
|
||||
## User mode is unchanged
|
||||
|
||||
Every `systemctl` call goes through `sc` and every `journalctl` call through `jctl`. Under `user` they
|
||||
are `systemctl --user "$@"` and `journalctl --user "$@"`. Under `external`, `sc` allows only `show`,
|
||||
`is-active` and `list-units`, and `jctl` returns 1, so an external run can't start, stop, install or read
|
||||
the journal even through a path the tests miss.
|
||||
|
||||
Two tests hold the brief's "every command line and every byte of output":
|
||||
|
||||
- `user mode: every systemctl call carries --user, and activate, rollback and check make the base's
|
||||
calls`. A fixture repo gets the base's two scripts from `git show cb5fdb064:`. The same activate,
|
||||
rollback and `stack-copy.sh check` run from both trees, and the recorded argv (with the fixture root and
|
||||
port replaced by placeholders) must match line for line. Every call carries `--user`.
|
||||
- `user mode: a default activate --plan prints the base's bytes`, for four plans.
|
||||
|
||||
The base output is produced live from the base blobs, not stored as a fixture file. See deviation D3.
|
||||
|
||||
## r6 section 7, test by test
|
||||
|
||||
All in `stack-release-external.test.mjs` unless named.
|
||||
|
||||
| r6 :742-812 item | Test |
|
||||
|---|---|
|
||||
| `units-running` (N4), store, log and TAKEN left | `units-running (N4) refuses while a unit is active...`; also checks that `failed` counts as down and that the Console is checked first |
|
||||
| activate leaves the pointer, writes `AWAITING-UNITS` and a backup | `activate stages: ...`, which also checks the set's file list, `UNITS.sha256`, no `--user`, no journal call and `status` |
|
||||
| `open-activation` with zero, two, the wrong copy, one not open | `confirm and revert refuse open-activation ...` |
|
||||
| `confirm` polls: 3 s fails at `--timeout 1`, passes at `--timeout 10` | `confirm polls: bus status good after 3 s fails ...` and `confirm polls to a pass ...` |
|
||||
| `stale-start` floor with `started` at .900 | `stale-start: @1792540799 fails ...` and `stale-start is a gate code ...` |
|
||||
| `confirm` pass writes pointer, ok, removes marker; timeout writes `FAILED` and exits 1 | the two polling tests |
|
||||
| `activation-failed` when `FAILED` exists | `confirm polls: ... FAILED then refuses activation-failed` |
|
||||
| lines carry the started op, from, to, activation and backup | `sameAsStarted` in every confirm test, and sequence 7 |
|
||||
| `gate` (N2) with and without `--since` | `gate (N2, A2f): ...` and the usage test |
|
||||
| `--plan --expect-from` (N5), activate and `rollback --to` | sequence 7 |
|
||||
| A3 order: capture-failed, marker-failed, wrapper failure after migrate | `A3 order: ...` and sequence 2 |
|
||||
| refusals log no activation; `interrupted` unchanged | `open-activation` test and sequence 5 |
|
||||
| PM check: one, none, two, early, not live, other business, other instance; no journal call | `the external PM check reads launches list ...` (modes '', coder, none, twolive, early, dead, other) and `journalCalls` in every external test |
|
||||
| `external-pm` | `pm-on and activate --pm-after-harness refuse external-pm` |
|
||||
| `revert` restores TAKEN only after a migration | `revert puts the wrapper back and leaves the store ...` and sequences 2 and 8 |
|
||||
| `confirm --rollback` per cause, `unrecorded`, `rollback-failed`, mismatch | sequences 1, 2, 3, 5 and 8, and `confirm --rollback refuses rollback-mismatch, and logs rollback-failed ...` |
|
||||
| sequences 1-8 | `sequence 1:` to `sequence 8:`, each ending with the `closed` check (one started and one result line, empty `interrupted`, no marker or `FAILED`, a following `--plan` that doesn't refuse `interrupted-run`) |
|
||||
| `rollback --to` refuses not-previous; `rollback --restore` finds an external backup | sequence 7, and `confirm --rollback after a --restore swap ...` |
|
||||
| `gate` writes nothing | the `gate (N2, A2f)` test and sequence 4 compare the log, the pointer and the state tree |
|
||||
| `confirm`, `revert`, `gate`, `--to` refuse without `--units external` | `confirm, revert, gate, --to and --expect-from need --units external ...` |
|
||||
| default path: every call carries `--user`; default `--plan` bytes | the two user-mode tests above |
|
||||
|
||||
The brief's additions, in `stack-copy.test.mjs`:
|
||||
|
||||
- `row 232: copy --bundle copies a SHA the bundle's refactor reaches, byte-identical to copy SHA, and
|
||||
refuses the rest`: a short SHA, a missing file, a symlink, a bundle that fails `git bundle verify`, a
|
||||
bundle with no `refs/heads/refactor`, a SHA not in the bundle, and a SHA the bundle carries on another
|
||||
branch that refactor doesn't reach. The bundle copy's tree matches `copy SHA`'s byte for byte, and the
|
||||
`release:` and `bundle sha256:` lines are checked.
|
||||
- `row 232: wrapper --create writes the first wrapper only, the same bytes wrapper ID writes, and refuses
|
||||
over any file`, a symlink included. Without `--create` a missing wrapper still refuses.
|
||||
- `row 232: check --units external reads the system units, the default reads the user units`.
|
||||
|
||||
## Mutants
|
||||
|
||||
`mutants.sh` in my scratch (not committed, it edits the worktree), 17 mutants: r6's re-pin list (:817-838)
|
||||
minus the three `case1-repin.sh` ones, plus the brief's two. Each one applies exact replacements after a
|
||||
checked backup, runs `bash -n`, runs its named killers, restores the file and checks its sha256. Two
|
||||
controls run first with no change: the whole external file, and stack-copy's `row 232:` tests.
|
||||
|
||||
| Mutant | Killers |
|
||||
|---|---|
|
||||
| m01 `confirm` writes the pointer on a gate failure | `confirm polls: bus status` |
|
||||
| m02 `gate_once` in place of `gate` | `confirm polls to a pass`, `stale-start is a gate code` |
|
||||
| m03 PM check counts any instance | `external PM check` |
|
||||
| m04 PM check drops the `startedAt` bound | `external PM check` |
|
||||
| m05 `confirm` takes the newest `AWAITING-UNITS` | `open-activation` |
|
||||
| m06 `rollback --to` skips the comparison | `sequence 7:` |
|
||||
| m07 the default path drops `--user` from one call (`is-active`) | `user mode: every systemctl` |
|
||||
| m08 the timeout writes r4's interim `rollback pending` line | `confirm polls: bus status`, `sequence 1:`, `sequence 5:` |
|
||||
| m09 `AWAITING-UNITS` last, after the wrapper | `sequence 2:` |
|
||||
| m10 a refusal names the open activation | `sequence 5:`, `open-activation` |
|
||||
| m11 `stale-start` is a refusal | `stale-start is a gate code` |
|
||||
| m12 `revert` restores without `MIGRATING` | `revert puts the wrapper back` |
|
||||
| m13 `confirm` logs op `confirm` | `sequence 7:`, `confirm polls to a pass` |
|
||||
| m14 `MIGRATING` after the migration | `sequence 8:` |
|
||||
| m15 `gate` takes its floor from the log without `--since` | `gate (N2` |
|
||||
| m16 `copy --bundle` skips the reachability check | stack-copy `row 232:` |
|
||||
| m17 `wrapper --create` overwrites | stack-copy `row 232:` |
|
||||
|
||||
## Deviations and choices
|
||||
|
||||
D1. The shared helpers moved to a new file, `packages/cli/tests/stack-release-fixture.mjs`. The brief's
|
||||
files list names `stack-release.test.mjs` "where a shared helper moves" but not a destination. Importing
|
||||
from the other test file would run its 74 tests twice, so the helpers need a module of their own.
|
||||
|
||||
D2. The fixture's T17 check (what a run may never remove) now exempts a rollback set's `AWAITING-UNITS` and
|
||||
`FAILED`. `confirm` removes both when it closes the activation (r6 A5 and R4). User mode writes neither, so
|
||||
user-mode runs check exactly what they did.
|
||||
|
||||
D3. The user-mode argv and `--plan` tests read the base's scripts with `git show cb5fdb064:` rather than
|
||||
storing their output as a fixture file. A later row that changes user-mode output on purpose moves the
|
||||
`BASE` constant in the test to its own base, or the test fails. I chose this over a stored fixture because
|
||||
a stored copy can drift from the base without anyone noticing.
|
||||
|
||||
D4. `marker-failed` is reached by making `state/stack-rollback` unwritable. r6 says "an existing `$RS`", but
|
||||
`next_activation` never picks a taken name, so no test can make the run meet an existing `$RS`. The
|
||||
exclusive `mkdir` is still what refuses.
|
||||
|
||||
D5. The stale-start polling case turns the enter time to `@1792540800`, not r6's `@1792540805`, with the
|
||||
started line at 1792540800.900. That also checks that the floor is the whole second, so .900 doesn't push it
|
||||
to 1792540801.
|
||||
|
||||
D6. No fake `sudo` or `case2-unit.sh`. r6's sequences ran under them because r6 had `case1-repin.sh` drive
|
||||
the steps, and that script is I3's. Here the test plays root: it stops and starts the fake system units and
|
||||
installs with the existing `bus-service.sh` and `webui-service.sh` stand-ins, between the real
|
||||
`stack-release.sh` calls, on one log.
|
||||
|
||||
D7. The fake marker in the `open-activation` test is named `20000101T000000Z-<C1 prefix>` so it sorts
|
||||
before the real one. A marker that sorted after it would leave m05 (newest marker) alive.
|
||||
|
||||
D8. m10 sets `ACT` inside `open_activation`, where r6's wording ("ACT set before the checks") lands in this
|
||||
code. `ACT` is set only after every check in `confirm`.
|
||||
|
||||
D9. Row 200's `--sso` is a usage error under `--units external`, with the proof flags, `--no-proof` and
|
||||
`--no-send`. It is a Console flag, and root's Console unit carries the Console flags; the script under
|
||||
external renders no Console line and never calls `console_for_target`, so `sso_checks` doesn't run there
|
||||
either. The usage text, the TOOLS.md bullet and the external usage test name it.
|
||||
|
||||
D10. Row 200's `config_checks` runs under `--units external` too, after `plugin_checks`, in both the
|
||||
`--plan` and run paths of `activate` and `rollback --to`. It checks the target's loader against the stack
|
||||
user's `config.json`, which the bus on a VM still reads, so a `console` key the target refuses would break
|
||||
root's bus start the same way. The cost is one more refusal (`config-console-key`) an external run can
|
||||
print; I took it over a bus that fails after root's start. The rebase changed no user-mode line: the
|
||||
conflicts were the `SSO=0` globals line, the `plan` body (row 200's `SSO_PLAN` line sits in the user
|
||||
branch) and the two `--plan` lines that gained `config_checks`. In the tests, row 200's fixture lines
|
||||
(the loaders, `sso-config.mjs` and the `--sso` cli.mjs line for C1 and C) moved into
|
||||
`stack-release-fixture.mjs` with the rest of the copy setup, and the `ALGS` import stays in
|
||||
`stack-release.test.mjs`, which T57 uses.
|
||||
|
||||
## duckworth-0055's notes on r6 (brief line 169)
|
||||
|
||||
Duckworth's r6 approval (REVIEW-r5.md at 23f5520b5) carried two notes.
|
||||
|
||||
- N1, A2's stop timeout under `gate --since`. A unit that never stopped keeps its old `ActiveEnterTimestamp`,
|
||||
so `gate --since` fails it with `stale-start`, and the run ends at exit 2 with root stopping the units.
|
||||
That is intended: a unit that won't stop is a fair reason to stop. In I1 this is the gate code and its
|
||||
polling (`stale-start: @1792540799 fails ...`, `gate (N2, A2f)`, m11). The run sheet's A2f line and the
|
||||
re-pin test that points "exits 1 with OLD's gate passing" at an A3 exit 3 belong to I3, which owns
|
||||
`scripts/stack-upgrade.sh` and `docs/guides/host-run-sheet.md`. I'll name both in the I3 build.
|
||||
- N2, the claude path under `/opt/mosaic-stack` (gosalyn-0058, sage-1522). I1 doesn't touch it. Under
|
||||
`--units external` the script checks no claude pin of its own (`no-claude-pin` applies only to user
|
||||
mode), because root's unit carries it. The `--claude` value, the managed-settings reasoning, F6 and the
|
||||
run sheet's claude step go to I2 and I3.
|
||||
|
||||
## Run environment
|
||||
|
||||
Worktree `/mnt/storage/scratch/darkwing-r232`, `TMPDIR=/mnt/storage/scratch/tmp/r232`, node from PATH.
|
||||
One external file run takes about 94 s.
|
||||
|
||||
## Results
|
||||
|
||||
Singles on the rebased candidate, before the gate:
|
||||
|
||||
| File | Tests | Pass | Fail |
|
||||
|---|---|---|---|
|
||||
| `packages/cli/tests/stack-release-external.test.mjs` | 25 | 25 | 0 |
|
||||
| `packages/cli/tests/stack-copy.test.mjs` | 16 | 16 | 0 |
|
||||
| `packages/cli/tests/stack-release.test.mjs` (row 200's T57 included) | 192 | 192 | 0 |
|
||||
| `packages/cli/tests/stack-instances.test.mjs` | 23 | 23 | 0 |
|
||||
| `packages/webui/tests/pinned-copy.test.mjs` | 10 | 10 | 0 |
|
||||
| `packages/cli/tests/channel-recovery.test.mjs` (imports `channel-fixture.mjs`, which names the scripts) | 1 | 1 | 0 |
|
||||
| `packages/cli/tests/host.test.mjs` (reads `packages/cli/README.md`) | 42 | 42 | 0 |
|
||||
|
||||
These are every test file that reads `stack-release.sh` or `stack-copy.sh` (a grep of `packages/*/tests` and
|
||||
`scripts/`). None of them pins a sha256 of a changed file. `packages/cli/tests/host.test.mjs` reads
|
||||
`packages/cli/README.md` for a unit file name; this row only adds a paragraph there. The grep and the runs
|
||||
were repeated on the rebased tree on 2026-10-21; the reader set is the same. A dry run of `mutants.sh`
|
||||
matches each of the 17 replacements once on the rebased scripts.
|
||||
|
||||
### Gate, 2026-10-21, Sage's slot (sage-1618)
|
||||
|
||||
Candidate a0ffabc3f on `darkwing/row232-r1`, run by `~/darkwing-scratch/r232/gate.sh`: the cli suite, then
|
||||
every `scripts/test-*.sh`, one after another, stopping at the first failure. busy.sh passed at 01:07:49Z
|
||||
after Launchpad's single `stack-release.test.mjs` run ended. The gate stopped at the first one, so the
|
||||
chain waited for that PID to exit and checked again. The EXIT trap removed the compose network; no
|
||||
container was left.
|
||||
|
||||
| Step | Time (UTC) | Result |
|
||||
|---|---|---|
|
||||
| `packages/cli`, `node --test tests/*.test.mjs` | 01:07:50 to 01:17:53 | 561 pass, 0 fail, 0 skipped |
|
||||
| `scripts/test-auth.sh` | 01:17:53 to 01:17:54 | 15 passed, 0 failed |
|
||||
| `scripts/test-conductor.sh` | 01:17:54 to 01:18:00 | 17 passed, 0 failed |
|
||||
| `scripts/test-config.sh` | 01:18:00 to 01:18:02 | 31 passed, 0 failed |
|
||||
| `scripts/test-discord.sh` | 01:18:02 to 01:18:12 | 78 passed, 0 failed |
|
||||
| `scripts/test-extension-package.sh` | 01:18:12 to 01:18:13 | 18 passed, 0 failed |
|
||||
| `scripts/test-foundation.sh` | 01:18:13 to 01:19:01 | 44 passed, 0 failed |
|
||||
| `scripts/test-land-work.sh` | 01:19:01 to 01:19:14 | 86 passed, 0 failed |
|
||||
| `scripts/test-queue.sh` | 01:19:14 to 01:19:37 | 156 node tests pass, queue suite 28 passed, 0 failed |
|
||||
| `scripts/test-release.sh` | 01:19:37 to 01:19:51 | 14 passed, 0 failed |
|
||||
| `scripts/test-task.sh` | 01:19:51 to 01:20:44 | 98 passed, 0 failed |
|
||||
|
||||
### Mutants, 2026-10-21, same slot
|
||||
|
||||
`~/darkwing-scratch/r232/mutants.sh`, right after the gate. Both controls (the external file whole, and
|
||||
stack-copy's `^row 232: ` tests) exited 0 on the unmutated tree. Each mutant was applied after a checked
|
||||
backup and a syntax check, ran its killers from the table above, and was put back with its sha256
|
||||
checked. The script stops on any restore mismatch, and it ran to its end.
|
||||
|
||||
| Mutant | Killer exit | Result |
|
||||
|---|---|---|
|
||||
| m01 pointer on gate failure | 1 | killed |
|
||||
| m02 gate once | 1 | killed |
|
||||
| m03 PM from any instance | 1 | killed |
|
||||
| m04 no startedAt check | 1 | killed |
|
||||
| m05 newest marker | 1 | killed |
|
||||
| m06 --to not compared | 1 | killed |
|
||||
| m07 one call without --user | 1 | killed |
|
||||
| m08 interim pending line | 1 | killed |
|
||||
| m09 marker written last | 1 | killed |
|
||||
| m10 refusal names the open activation | 1 | killed |
|
||||
| m11 stale-start refuses | 1 | killed |
|
||||
| m12 revert always restores | 1 | killed |
|
||||
| m13 confirm op | 1 | killed |
|
||||
| m14 MIGRATING after migrate | 1 | killed |
|
||||
| m15 gate floor from the log | 1 | killed |
|
||||
| m16 bundle reachability unchecked | 1 | killed |
|
||||
| m17 --create overwrites | 1 | killed |
|
||||
|
||||
17 of 17 killed, 01:22:16 to 01:24:32Z. I read the logs for m01 and m09, two of the quick kills: each failed on
|
||||
its named test's assertion (m09 on `AWAITING-UNITS` in sequence 2), not on a load error.
|
||||
|
||||
## Round 2: rebase onto 8fede5fc6 (sage-1673)
|
||||
|
||||
Rebase only. Before the landing gate, a check of BASE..HEAD on the manifest paths found that row 230
|
||||
(c8bc5cf4c) had changed one of them after the round 1 base, cb5fdb064. Sage ruled a re-round with a
|
||||
scoped review, the same shape as row 210 round 3.
|
||||
|
||||
Branch `darkwing/row232-r2`: the two round 1 commits, a0ffabc3f and 84f1a91e6, cherry-picked onto
|
||||
8fede5fc6 with no conflict, then this section. No code or test changed.
|
||||
|
||||
- `git merge-tree --write-tree origin/refactor darkwing/row232-r1` (refactor at 8fede5fc6) gives tree
|
||||
837bde79236c with no conflict. The cherry-picked tip before this section has the same tree.
|
||||
- `packages/cli/README.md` is a clean 3-way merge. Row 230's hunk is at lines 710-717 (the session credential
|
||||
paragraph); this row's is a new paragraph at 773. With `git patch-id --stable`, the README diff from r1 to
|
||||
r2 is 02b679e2, the same as row 230's README diff `cb5fdb064..8fede5fc6`. This row's README diff is
|
||||
c395ecd2 on both bases (`cb5fdb064..r1` and `8fede5fc6..r2`).
|
||||
- The seven code, test and doc paths other than the README hash the same as in manifest 7a3b4ea1, which
|
||||
Filbert approved in round 1: `docs/TOOLS.md`, `packages/cli/tests/stack-copy.test.mjs`,
|
||||
`packages/cli/tests/stack-release-external.test.mjs`, `packages/cli/tests/stack-release-fixture.mjs`,
|
||||
`packages/cli/tests/stack-release.test.mjs`, `scripts/stack-copy.sh`, `scripts/stack-release.sh`.
|
||||
This file also matched until this section was appended. Its other lines are unchanged.
|
||||
- The one test that reads the README, `host.test.mjs:855`, looks for the unit's `EnvironmentFile=` line.
|
||||
That line is unchanged context in row 230's hunk.
|
||||
- No row 230 test reads `stack-release.sh` or `stack-copy.sh`, and no test of this row reads a row 230 file.
|
||||
|
||||
Nothing was run for this round. The landing gate runs on the r2 tip on Sage's slot.
|
||||
|
||||
Follow-up, doc-only (sage-1673): `scripts/case2-unit.sh:108` and `scripts/case2-precheck.sh:157` cite
|
||||
`scripts/stack-copy.sh:8` and `:22` for `COPIES` and `copy_root`. With this row those lines are `:10` and
|
||||
`:26`. Only comments are affected and no test reads them. Row 233 doesn't touch those scripts, so the fix
|
||||
goes on the #1732 carry list.
|
||||
@@ -0,0 +1,249 @@
|
||||
# Row 233 (I3): root's upgrade driver and the host run sheet
|
||||
|
||||
Brief: `docs/plans/2026-10-20_installer-rows.md` section I3, blob `e61291bcf1c6` (commit a0e3d7cfd), re-pinned
|
||||
by Sage (sage-1685, rev 2631) from `f35e7e57daee` (ad6f0be7a). The later blob only appends I4; the I3 text
|
||||
is the same. Gitea #1735, Vikunja P19
|
||||
#432. Owner Darkwing, reviewer Filbert, gate owner Sage. Design input: row 195 DESIGN r6,
|
||||
`agents/darkwing/work/queue-195/DESIGN.md` at b7239bb66, sections 2, 4 and 7. Where the brief and r6
|
||||
differ, the brief wins. Sage's rulings during the build: sage-1650 and sage-1652 (`--backup last`, the
|
||||
`--no-rehearsal` mapping, `--rehearse-port`, hostcheck), and sage-1653 (`from_log newest`, the two external
|
||||
plan refusals, the `BACKUP_STORE` line).
|
||||
|
||||
## Files
|
||||
|
||||
Base: e65a3ab4d, row 232's landing, on `darkwing/row233-r1`. The build started on ea1e85c7b
|
||||
(`darkwing/row233`, row 232's candidate e6ca230d2 and its BUILD.md) before row 232 landed. The three row 232
|
||||
files this row changes are identical on the two bases, so the uncommitted diff moved across unchanged.
|
||||
|
||||
- `scripts/stack-upgrade.sh` (new): `upgrade` and `rollback`, root's side of r6 A1-A5, A2f and R1-R4.
|
||||
- `packages/cli/tests/stack-upgrade.test.mjs` (new, 20 tests).
|
||||
- `docs/guides/host-run-sheet.md` (new).
|
||||
- `docs/TOOLS.md`: a new subsection "Root's upgrade driver (`scripts/stack-upgrade.sh`, row 233)", plus the
|
||||
row 232 lines below.
|
||||
- Row 232 files, changed on Sage's rulings (the brief's files list doesn't name them):
|
||||
- `scripts/stack-release.sh`:
|
||||
- `rehearse --backup last` takes the newest backup the log records (`from_log newest`) and prints
|
||||
`PASS backup-last`. With none it fails `no-backup`, exits 3 and makes no rehearsal directory.
|
||||
A newest backup that isn't an activation id aborts `backup-unreadable`, exit 1.
|
||||
- The rehearsal prints `BACKUP_STORE <activation id> schema <version>` before its migration step.
|
||||
- `--plan` under `--units external` refuses `open-activation` when any rollback set holds
|
||||
`AWAITING-UNITS`, and `migrate-required` for a migrating target without `--migrate`. Root reads
|
||||
only the plan's exit, so these two have to be refusals there. User mode prints the same bytes.
|
||||
- `packages/cli/tests/stack-release.test.mjs`: three new `row 233:` tests for `--backup last`.
|
||||
- `docs/TOOLS.md`: `--backup ACTIVATION|PATH|last` in the rehearse usage, a paragraph on `last`, and a
|
||||
paragraph on the two external plan refusals.
|
||||
- This file.
|
||||
|
||||
## The brief, item by item
|
||||
|
||||
| Brief item | Where |
|
||||
|---|---|
|
||||
| Root runs from a root-owned checkout, never a copy | `head-mismatch` (A1); TOOLS.md; run sheet section 6 |
|
||||
| I2's render with `/etc/mosaic-stack/<instance>.args`, `--copy` the one changed value | `read_args`, `args_with`; the args test |
|
||||
| The args file changes only after a confirmed run | `prepare_args` before A2, `mv` after A5; sequences 1 and 7, m6 |
|
||||
| `copy --bundle` as the stack's user before A1 | the `fetch`, `ff`, `copy` steps; sequence 1's sudo keys |
|
||||
| A rehearsal before A2, a failed one refuses with nothing stopped | `rehearse`; three rehearsal tests, m4 |
|
||||
| The root lock, held to the end, rollback included | `flock -n 9`; the lock test and sequence 3's probe, m1, m2 |
|
||||
| A1 refuses `open-activation` | stack-release.sh's external plan; the AWAITING-UNITS test, m3 |
|
||||
| `migrate-required` without `--migrate` | stack-release.sh's external plan; its test, m5 |
|
||||
| The step log, 0640, one line per step | `logl`, `step`; every test checks lines against `STEP` |
|
||||
| Exit codes 0-3 | `finish`, `refuse`; exit 4 is usage |
|
||||
| duckworth-0055's notes | the next section |
|
||||
| Root parses no stack user output beyond exits | the step log test; the one exception is sage-1653's `BACKUP_STORE` |
|
||||
| The host run sheet | `docs/guides/host-run-sheet.md` |
|
||||
|
||||
## duckworth-0055's notes on r6 (brief line 169)
|
||||
|
||||
- N1, A2's stop timeout under `gate --since`. Row 232's BUILD.md left two pieces to I3. The run sheet's
|
||||
A2f line is in section 6 ("When a run doesn't exit 0", exit 2). The test is `N1: a bus that outlives
|
||||
A2's timeout keeps its old start`: a bus that ignores `stop` keeps its old `ActiveEnterTimestamp`,
|
||||
`wait-down` logs `rc=1 ... state: active`, A2f restarts and gates, the gate fails `stale-start`, and the
|
||||
driver stops both units and exits 2. Sequence 4 is the other half: an A3 exit 3 goes to A2f and exits
|
||||
1 with the old copy's gate passing.
|
||||
- N2, the claude path under `/opt/mosaic-stack`. Run sheet section 3 installs it there as root with
|
||||
managed settings. The args file's `--claude` carries it, and `case2-unit.sh` already refuses a binary
|
||||
that isn't root's or sits under the home. The driver passes the args file through and adds no claude
|
||||
check of its own. F6 is I2's.
|
||||
|
||||
## Sage's rulings during the build
|
||||
|
||||
- sage-1650 and sage-1652. Root always runs the rehearsal and maps its exit:
|
||||
- 0 goes on, or refuses `rehearsal-required` with `--no-rehearsal`.
|
||||
- 3 refuses `no-backup`, or with `--no-rehearsal` logs `rehearsal-skipped` and goes on.
|
||||
- Anything else refuses `rehearsal-failed`.
|
||||
|
||||
`--rehearse-port N`, default 7339. Hostcheck: a missing file or a nonzero `--check` refuses
|
||||
`profile-drift`.
|
||||
- sage-1653. `from_log newest`, and the external plan refusals `open-activation` and `migrate-required`.
|
||||
Residual: the `BACKUP_STORE` id and schema land in root's step log, but nothing ties that backup to the
|
||||
one the activation's own backup step takes later. A stopped backup before the upgrade would close that
|
||||
gap; it's a later row, not this one.
|
||||
|
||||
## Tests
|
||||
|
||||
`stack-upgrade.test.mjs` runs the driver as the test's user against a scratch root, with the test seams
|
||||
`STACK_UPGRADE_*`. The stand-ins are a fake `sudo` (logs its argv, checks the driver's form, then runs the
|
||||
real command as the test's user, r6's B1 pattern), a fake `case2-unit.sh` render that installs and starts
|
||||
the fixture units, a fake `systemctl` for root's system units, and a shell `hostcheck.sh`. The stack's
|
||||
user's steps run the real `stack-release.sh` and `stack-copy.sh` from a clone made from a real bundle. The
|
||||
units, copies and store are `stack-release-fixture.mjs`'s.
|
||||
|
||||
| Test | Covers |
|
||||
|---|---|
|
||||
| sequence 1 | confirm fails healthz, rollback with `--cause A5`, exit 1; every sudo call in the driver's form |
|
||||
| sequence 2 | `activate --migrate` fails at the wrapper, store restored, `--cause A3`, exit 1 |
|
||||
| sequence 3 | render fails, `--cause A4`; a probe run during `render-old` refuses `upgrade-running` |
|
||||
| sequence 4 | stage exits 3, A2f restarts and gates the old units, exit 1 |
|
||||
| N1 | a unit that won't stop: A2f's gate fails `stale-start`, exit 2, units stopped |
|
||||
| sequence 5 | confirm of the wrong copy refused, `--cause A5`, exit 1 |
|
||||
| sequence 6 | revert fails, exit 2; the next run refuses `interrupted-run` at its plan |
|
||||
| sequence 7 | two upgrades and a `rollback --to` with `--restore`; the args file after each confirm; `to-not-previous` |
|
||||
| sequence 8 | a migration that writes and fails, store back byte for byte, exit 1 |
|
||||
| lock | a second run while the lock is held: one log line, no call |
|
||||
| open-activation | any `AWAITING-UNITS` refuses at A1, nothing stopped |
|
||||
| migrate-required | refused without `--migrate`, no rehearsal, nothing stopped |
|
||||
| rehearsal-failed | the rehearsal's argv, exit 3, nothing stopped |
|
||||
| `BACKUP_STORE` | the line after a match; four malformed lines log `BACKUP_STORE none` |
|
||||
| no-backup | the real rehearsal with no backup refuses; `--no-rehearsal` logs the skip and goes on |
|
||||
| backup-last | the real rehearsal names the newest backup; `--no-rehearsal` with one refuses `rehearsal-required` |
|
||||
| args file | group write, other write, two `--copy`, an unknown flag, a symlink, `args-differ` |
|
||||
| hostcheck | nonzero `--check` and a missing file refuse `profile-drift` before the plan |
|
||||
| head-mismatch | checkout not at `--sha`; a commit the clone can't fast-forward to refuses `fetch-failed` |
|
||||
| usage | twelve bad argvs exit 4 with no log and no lock |
|
||||
|
||||
The step log check (`STEP`) runs in every test that reads the log: each line must be a step line or the
|
||||
result line, and none may hold the stack user's output (`stack-release:`, `PASS `, `FAIL `, `REHEARSAL`,
|
||||
the fake rehearsal's text).
|
||||
|
||||
## Mutants
|
||||
|
||||
`~/darkwing-scratch/r233/mutants.sh`, not committed because it edits the worktree. It has the brief's six
|
||||
mutants and two for sage-1650's mapping. The method is row 232's: a checked backup, exact replacements, a
|
||||
syntax check, the named killers, then the file put back with its sha256 checked. One control runs first:
|
||||
the whole `stack-upgrade.test.mjs` with no change. `DRY=1` matched each replacement once on the candidate
|
||||
(2026-10-21).
|
||||
|
||||
| Mutant | File | Killers |
|
||||
|---|---|---|
|
||||
| m1 drop the flock | stack-upgrade.sh | `a second run while the lock is held`, `sequence 3:` |
|
||||
| m2 release the lock when the rollback starts | stack-upgrade.sh | `sequence 3:` |
|
||||
| m3 skip A1's open-activation check | stack-release.sh | `A1 refuses open-activation` |
|
||||
| m4 skip the rehearsal | stack-upgrade.sh | `the rehearsal runs after the plan`, `sequence 1:` |
|
||||
| m5 accept a migrating target without `--migrate` | stack-release.sh | `migrate-required` |
|
||||
| m6 write the args file before confirm | stack-upgrade.sh | `sequence 7:`, `sequence 1:` |
|
||||
| m7 `--no-rehearsal` accepted with a backup | stack-upgrade.sh | `refuses rehearsal-required` |
|
||||
| m8 drop the `rehearsal-skipped` line | stack-upgrade.sh | `a real rehearsal with no backup` |
|
||||
|
||||
## Deviations
|
||||
|
||||
D1. The brief has A1 refuse `open-activation` for an open `AWAITING-UNITS` or a non-empty `interrupted`
|
||||
set. The interrupted case refuses with row 232's existing code, `interrupted-run`; only `AWAITING-UNITS`
|
||||
gives `open-activation`. Both reach root as `plan-refused`, exit 3 (sequence 6, the open-activation test).
|
||||
|
||||
D2. Root copies one line of the stack user's output into its log, `BACKUP_STORE`, on sage-1653. It
|
||||
matches a fixed pattern first, takes the last match, logs `BACKUP_STORE none` otherwise, and decides
|
||||
nothing on it.
|
||||
|
||||
D3. The stack user's steps carry no `--instance`. The driver takes `--instance dev` only, row 190's default
|
||||
instance, which the stack user's commands select with no flag. A named instance's units are a later row.
|
||||
|
||||
D4. Most tests fake the rehearsal in the fake `sudo`: the fixture's stub copies can't pass a real one. Two
|
||||
tests run the real rehearsal (`faults/real-rehearse`): no-backup and backup-last. `stack-release.test.mjs`
|
||||
covers `--backup last` itself.
|
||||
|
||||
D5. Any nonzero `--plan` exit refuses `plan-refused`, exit 3, whatever the code. The stack user's output
|
||||
above it names the refusal; root doesn't parse it.
|
||||
|
||||
D6. `rollback --restore ACT` is accepted without `--events`, as `stack-release.sh rollback` accepts it.
|
||||
The brief's usage pairs them. `--events` without `--restore` is a usage error.
|
||||
|
||||
D7. The refusal of any `STACK_UPGRADE_*` seam when run as root has no test, because the tests don't run
|
||||
as root. It is three lines at the top of the script.
|
||||
|
||||
D8. The fake `sudo` keeps the test's environment, where real `sudo` resets it. The driver passes
|
||||
everything the stack user needs through `env PATH=...` and `-H`, and the fake checks that form.
|
||||
|
||||
D9. Exit 3 means nothing stopped, not nothing changed. A refusal after `fetch` leaves the stack user's
|
||||
clone fast-forwarded and the new copy made. Both are the user's own state, and the next run reuses them.
|
||||
TOOLS.md and the run sheet say so.
|
||||
|
||||
D10. The brief's usage has neither `--no-rehearsal` nor `--rehearse-port`. Both come from sage-1650 and
|
||||
sage-1652.
|
||||
|
||||
## Decisions
|
||||
|
||||
- `rollback` has no HEAD check. Its target is an older copy than root's checkout, which already exists.
|
||||
It fetches, copies and rehearses nothing (sequence 7).
|
||||
- The new args file is written next to the old one (`.dev.args.XXXXXX`, same mode) before A2, so a full
|
||||
`/etc` refuses before anything stops. It moves into place after A5. A2f and the rollback remove it; a failed move keeps it and names it.
|
||||
- Every stack user step gets `--copies` explicitly, so the copies directory comes from the args file's
|
||||
`--home`, never from the user's environment.
|
||||
- The stack's user does the fetch, the fast-forward and the copy, so root never runs git on a tree that
|
||||
user can write.
|
||||
- A4 is the render, and `case2-unit.sh` starts the bus, then the Console. The forward path has no
|
||||
`systemctl start` of its own; only A2f does.
|
||||
|
||||
## Run environment
|
||||
|
||||
Worktree `/mnt/storage/scratch/darkwing-r233`, `TMPDIR=/mnt/storage/scratch/tmp/r233`, node from PATH. One
|
||||
whole `stack-upgrade.test.mjs` run takes about 56 s.
|
||||
|
||||
## Results
|
||||
|
||||
### Singles, run 1, 2026-10-21, Sage's slot (sage-1689)
|
||||
|
||||
`~/darkwing-scratch/r233/singles.sh` on `darkwing/row233-r1` (base e65a3ab4d, uncommitted tree), 03:48:20Z
|
||||
to 03:59:00Z. These are the test files that read a changed script. `stack-instances.test.mjs` reads
|
||||
`stack-release.sh`, so it joined the three named in the plan.
|
||||
|
||||
| File | Result |
|
||||
|---|---|
|
||||
| `stack-upgrade.test.mjs` | 20 pass, 0 fail |
|
||||
| `stack-release.test.mjs` | 194 pass, 1 fail |
|
||||
| `stack-release-external.test.mjs` | 25 pass, 0 fail |
|
||||
| `stack-instances.test.mjs` | 23 pass, 0 fail |
|
||||
|
||||
The failure was my test, not the script. "row 233: --backup last takes the newest backup..." compares
|
||||
`untouched(e)` before and after the run, and `untouched` includes `existsSync(e.log)`. The test took its
|
||||
before snapshot ahead of `rlog()`, which writes the log, so that flag went from false to true. Every hash in
|
||||
the snapshot matched. The fix takes the snapshot after `rlog()`. No other row 233 test calls `untouched`.
|
||||
|
||||
### Singles, run 2
|
||||
|
||||
`~/darkwing-scratch/r233/rerun.sh` on aacfa2d7a, Sage's slot (sage-1695), 04:13:05Z to 04:21:28Z:
|
||||
`stack-release.test.mjs` whole, 195 pass, 0 fail, 0 skipped. The fixed test passed. The other three files
|
||||
did not change after run 1. The gate and the mutants run next, in Sage's slot.
|
||||
|
||||
### Gate and mutants, 2026-10-21, Sage's slot (sage-1704)
|
||||
|
||||
`~/darkwing-scratch/r233/gate.sh` then `mutants.sh`, chained behind `busy.sh`, on d99166ade (code
|
||||
aacfa2d7a; d99166ade changes only this file). Counts are an awk sum over `gate-r1.out`.
|
||||
|
||||
| Part | Time (UTC) | Result |
|
||||
|---|---|---|
|
||||
| cli suite (`node --test tests/*.test.mjs`) | 04:29:01-04:37:27 | 616 pass, 0 fail, 0 skipped |
|
||||
| the 10 `scripts/test-*.sh` | 04:37:27-04:39:38 | selftests 429 passed, 0 failed; `test-queue`'s node part 156 pass, 0 fail |
|
||||
| gate total | | 1201 pass, 0 fail, 11 suites, `GATE_PASSED` |
|
||||
| c0 control | 04:39:38-04:40:35 | exit 0 |
|
||||
| m1-m8 | 04:40:35-04:41:25 | 8 of 8 killed, every one exit 1 |
|
||||
|
||||
Every restore passed its sha256 check, and `git diff` was clean at d99166ade afterwards. The EXIT trap
|
||||
removed the `darkwing-r233_default` network.
|
||||
|
||||
N1, found in the process sweep after the run: a `sleep 60` was left with its cwd in `packages/cli`.
|
||||
`stack-upgrade.test.mjs:312` spawns `flock LOCK sleep 60`, and its `t.after` SIGKILLs only the flock, so
|
||||
flock's child outlives the test by up to 60 s. It exited on its own. Row 232's T20
|
||||
(`stack-release.test.mjs:370`) spawns its holder `detached` and kills the group with `process.kill(-pid)`.
|
||||
The fix is the same two lines. That test is one of m1's killers.
|
||||
|
||||
Sage ruled it a fix now (sage-1707), and 08db6ac3f makes it, test only. After the gate's d99166ade
|
||||
the only change outside this file is that test:
|
||||
|
||||
```
|
||||
$ git diff --stat d99166ade 08db6ac3f
|
||||
packages/cli/tests/stack-upgrade.test.mjs | 4 ++--
|
||||
1 file changed, 2 insertions(+), 2 deletions(-)
|
||||
```
|
||||
|
||||
The gate stands on Sage's ruling. The rerun is the whole `stack-upgrade.test.mjs` singly plus m1, in
|
||||
Sage's slot, with a sweep for a leftover `sleep` afterwards.
|
||||
@@ -0,0 +1,173 @@
|
||||
# Row 240 (I4): root's host check, its pins and the boot unit
|
||||
|
||||
Brief: `docs/plans/2026-10-20_installer-rows.md` section I4, blob `4a5a65020f00` (commit b00d7d94d, rev 2665,
|
||||
the pin row 240 carries; the I3 carry items came in with it). Sage split it out of I3 (sage-1650). Gitea #1746, Vikunja P19 #432. Owner Darkwing, reviewer Filbert, gate owner Sage.
|
||||
Design: row 195 DESIGN r6 B2, `agents/darkwing/work/queue-195/DESIGN.md` at b7239bb66, :262-303 (the script,
|
||||
the three callers) and :839-860 (files, test cases, mutants). Where the brief and r6 differ, the brief wins.
|
||||
|
||||
## Files
|
||||
|
||||
Drafted on `darkwing/row240-r1` from d99166ade (row 233's candidate) while row 233's gate ran, then applied
|
||||
as one patch onto row 233's landing, 57f59e0c3, on `darkwing/row240-r2` (sage-1753). It applied cleanly; between
|
||||
the two bases the only change on this row's paths is 233's N1 test fix in `stack-upgrade.test.mjs`. Row 234
|
||||
then landed as c771a36f4 on 57f59e0c3. Sage preferred that the gate run with it in the tree (sage-1773), so both
|
||||
commits were picked onto c771a36f4 as `darkwing/row240-r3`. That was a clean pick: the 42 paths changed from
|
||||
57f59e0c3 to c771a36f4 share none with this row's 9.
|
||||
|
||||
- `scripts/case1-pins.sh` (new, 0644): `PIN_PROFILES_VERSION`, `PIN_PROFILE_SHA256`, `PIN_BUBBLEWRAP_VERSION`,
|
||||
r6 section 0's values.
|
||||
- `scripts/case1-hostcheck.sh` (new, 0755): the seven items and `--check`, `--enforce`, `--upstream`.
|
||||
- `packages/cli/systemd/mosaic-hostcheck.service` (new): the boot oneshot.
|
||||
- `packages/cli/tests/case1-hostcheck.test.mjs` (new, 16 tests). The cli suite's `tests/*.test.mjs` glob picks
|
||||
it up.
|
||||
- `docs/guides/host-run-sheet.md` section 4: the install lines that replace "Install them from row 240's run
|
||||
sheet lines once it lands", plus rollback, drift recovery and `--upstream` before a re-pin window. Sections
|
||||
3, 6, 8 and 10 for the I3 carry items below.
|
||||
- `scripts/stack-upgrade.sh` and `packages/cli/tests/stack-upgrade.test.mjs`: I3 N1 below.
|
||||
- `docs/TOOLS.md`: a new subsection "Root's host check (`scripts/case1-hostcheck.sh`, row 240)" after the
|
||||
upgrade driver's.
|
||||
|
||||
Brief line 257 said I3's `stack-upgrade.sh` doesn't change. Sage's ruling sage-1727 widened the row to it for
|
||||
N1 alone. Its default `STACK_UPGRADE_HOSTCHECK=/usr/local/lib/mosaic-stack/hostcheck.sh` is where the run
|
||||
sheet installs this script.
|
||||
|
||||
## Carried from I3
|
||||
|
||||
Filbert's row 233 approve (Gitea 29922) left five notes. Sage ruled N1, N4 and N5 into this row and N2 and N3
|
||||
to DEFERRED.md (sage-1727).
|
||||
|
||||
| Note | Change |
|
||||
|---|---|
|
||||
| N1: `stack-upgrade.sh` exits 1 `args-failed` when the args move after A5 fails, but exit 1 means the old copy runs | The move's failure exits 5 `args-failed`. The header, TOOLS.md's step 5 and exit line, and run sheet section 6's exit list name exit 5 and what root does: move the new file the result line names into place. Test `row 240: I3 N1: a failed args move after A5 exits 5 ...` makes `/etc/mosaic-stack` read-only at confirm and checks exit 5, the step names, `args rc=1`, the result line, both units on the new copy, the old args file's bytes and the new file beside it. The step log regex takes `exit=5`. Mutant M8. |
|
||||
| N4: section 3's `npm install` has no npm on root's PATH, and the `--claude` path is unnamed while `node_modules/.bin/claude` is a symlink `case2-unit.sh:140` refuses | The install line runs with section 2's pinned node first on PATH (the postinstall runs `node` too). The `--claude` path is `node_modules/@anthropic-ai/claude-code/bin/claude.exe`, and a `--version` line catches the postinstall's 500-byte stub. |
|
||||
| N5: sections 8 and 10 lack precheck lines | Each gets "Precheck line: none" and its missing line. |
|
||||
|
||||
Exit 5 is new rather than 2 because 2 means both units are stopped, and here both run. Exit 4 stays usage.
|
||||
|
||||
The `claude.exe` path comes from the 2.1.304 package itself: `npm view` gives `bin.claude` as `bin/claude.exe`,
|
||||
and its `install.cjs` hardlinks the platform binary over that placeholder, or copies it across devices, then
|
||||
sets 0755. I downloaded the tarball into `/mnt/storage/scratch/tmp/r240/cc304` and read it; nothing ran. The
|
||||
run sheet says a newer pin needs the same check.
|
||||
|
||||
## The brief, item by item
|
||||
|
||||
| Brief line | Where |
|
||||
|---|---|
|
||||
| 247 `case1-pins.sh` holds the three pins | `scripts/case1-pins.sh`; the pins test checks the values against r6's |
|
||||
| 248-249 seven items, `--check` exits 0 or 1 | `items()` and the final dispatch; the check tests |
|
||||
| 250-251 `--enforce`: `apparmor_parser -R`, `root:root 0700`, `DRIFT`, a log line, exit 0 | `enforce()` and `stop_bwrap()`; the four enforce tests |
|
||||
| 252 `--upstream` compares the current deb to the pin | `upstream()`; the upstream test |
|
||||
| 253 the boot oneshot | `mosaic-hostcheck.service`; the boot unit test |
|
||||
| 254-255 fakes for every host tool, r6's seven case groups, seven mutants | the test file; Tests and Mutants below |
|
||||
| 257 the I3 run sheet item points here | run sheet section 4 |
|
||||
|
||||
## Tests
|
||||
|
||||
Fakes on PATH, each logging its argv to `$HC/calls.log`: `aa-status`, `dpkg`, `dpkg-statoverride`,
|
||||
`apparmor_parser`, `systemd-run`, `stat`, `chown`, `chmod`, `apt-get`, `dpkg-deb`. `host(t)` builds a scratch
|
||||
root (`/etc`, `/proc/sys`, `/usr/bin/bwrap`, `/var/log`) and a lib dir with a copy of the script and fixture
|
||||
pins that hold the fixture profile's sha256. Every run passes `--root`.
|
||||
|
||||
r6's case groups (:843-856) against the tests:
|
||||
|
||||
| r6 case | Tests |
|
||||
|---|---|
|
||||
| `--check` exits 0 with all seven, 1 with each failing alone, its line naming it | `check: exits 0 ...`, `check: exits 1 with each item failing alone ...`, plus one test per item for the item's edge cases |
|
||||
| item 3: a second file naming `bwrap` as a word, `/{usr/,}bin/bwrap` included; passes with the profile alone | `check: item 3 ...` |
|
||||
| item 4: `unpriv_bwrap (enforce)` or a trailing space fails; the argv carries `-p NoNewPrivileges=yes` and the 2a' properties | `check: item 4 ...` checks the whole argv |
|
||||
| item 7: the floor at 732 fails | `check: item 6 ...; item 7 ...` (733 passes as the control) |
|
||||
| `--enforce` exits 0 on a pass and every failure; unload, close, `DRIFT`, one log line; `DRIFT` present with every item passing still closes | `enforce: a pass ...`, `enforce: each failure ...`, `enforce: with DRIFT present ...` |
|
||||
| `--enforce` never calls `dpkg-statoverride` with `--update`, `--add` or `--remove` | `enforce: no unload ...; never writes the statoverride` (every call is `--list /usr/bin/bwrap`) |
|
||||
| a bubblewrap version other than the pin is a line, not a failure | `check: a bubblewrap version other than the install pin ...` |
|
||||
| `--upstream` against a differing deb prints the difference, exits 0 | `upstream: ...` (also: a match, a deb with no profile, a failed download) |
|
||||
| the unit: `Type=oneshot`, `After=apparmor.service`, `[email protected]`, runs `--enforce` | `boot unit: ...` |
|
||||
|
||||
The last test also checks that the repo script reads `scripts/case1-pins.sh` beside it, that bad usage exits
|
||||
2, and that a non-root run without `--root` refuses.
|
||||
|
||||
Singles are the two files that run a changed script: `case1-hostcheck.test.mjs` and `stack-upgrade.test.mjs`.
|
||||
Sage asked (sage-1759) whether `stack-release.test.mjs` or `stack-release-external.test.mjs`, which share
|
||||
`stack-release-fixture.mjs`, run `stack-upgrade.sh` or `case1-pins.sh`. They don't. `grep -cF` for
|
||||
`stack-upgrade`, `case1-pins`, `case1-hostcheck` and `hostcheck` gives 0 in all three files; the control
|
||||
`stack-release.sh` gives 2 in the fixture and 8 in `stack-release.test.mjs`. `scripts/stack-release.sh` names
|
||||
`stack-upgrade.sh` once, in a comment (:2061). The gate's cli suite runs both files anyway.
|
||||
|
||||
## Mutants
|
||||
|
||||
r6's seven (:858-860) and one for I3 N1, scripted in `~/darkwing-scratch/r240/mutants.sh` with r233's method: a checked backup,
|
||||
an exact replacement that must match once, `bash -n`, the named killer by `--test-name-pattern`, the restore
|
||||
and a sha256 check. A dry run matched all eight once each. Results below.
|
||||
|
||||
| Mutant | Change | Killer |
|
||||
|---|---|---|
|
||||
| M1 | `--enforce` exits 1 after the stop | `enforce: each failure exits 0 ...` |
|
||||
| M2 | `[ ! -e "$DRIFT" ] \|\| had=1` becomes `true \|\| had=1` | `enforce: with DRIFT present ...` |
|
||||
| M3 | item 1's `dpkg -S` becomes `owner=""; rc=1` | `check: item 1 fails when a package owns ...` |
|
||||
| M4 | item 3's `grep -rlw bwrap` becomes `grep -rlF /usr/bin/bwrap` | `check: item 3 ...` |
|
||||
| M5 | item 4's `[ "$label" = "$LABEL" ]` becomes a substring match | `check: item 4 ...` |
|
||||
| M6 | item 4 drops `-p NoNewPrivileges=yes` | `check: item 4 ...` (argv) |
|
||||
| M7 | item 7's `-gt` becomes `-ge` | `check: item 6 ...; item 7 fails with the floor at 732 ...` |
|
||||
| M8 | `stack-upgrade.sh`'s `finish 5 args-failed` becomes `finish 1 args-failed` (I3 N1) | `row 240: I3 N1: a failed args move ...` |
|
||||
|
||||
## Deviations
|
||||
|
||||
- Repo name `scripts/case1-hostcheck.sh` (r6), installed as `/usr/local/lib/mosaic-stack/hostcheck.sh`, the
|
||||
name in brief line 239 and I3's `stack-upgrade.sh` default. r6's apt hook line says `case1-hostcheck.sh`;
|
||||
the run sheet's hook uses the installed name.
|
||||
- `stat` is a fake too. r6 lists seven fakes; a test can't make a root-owned file, so item 5's `stat` reads a
|
||||
fixture.
|
||||
- Item 7 with no Console unit installed reads the port as 1023 and says so ("1023, because no Console unit
|
||||
is installed"). Until I2 installs the unit the floor must be above 1023. A unit with no `--port` fails.
|
||||
- Item 4 runs under `timeout 60`. A hung `systemd-run` would otherwise hang the apt hook and the boot unit.
|
||||
|
||||
## Decisions
|
||||
|
||||
- Fail closed where r6 is silent:
|
||||
- pins with no 64-hex sha fail item 1;
|
||||
- `dpkg -S` exit 1 (no owner) passes, exit 0 fails with the owner, any other exit fails with the code;
|
||||
- a failed `aa-status` fails item 2 and counts as loaded, so `--enforce` tries the unload.
|
||||
- `--root DIR` is the test seam (r6 :840). Root refuses it; a non-root user needs it. Both exit 2. Root runs
|
||||
with `PATH=/usr/sbin:/usr/bin:/sbin:/bin`.
|
||||
- `DRIFT` is append-only (`<stamp> <failed line>`), and `--enforce` never removes it. The log line is written
|
||||
only when the stop runs: `<stamp> enforce stop items=<ids or none> drift-before=<0|1> unload=<rc=N|no-file|skipped> chown=N chmod=N`.
|
||||
- `--upstream` exits 1 when the download or the unpack fails, since it then compared nothing. A deb without
|
||||
the profile file is a difference (exit 0), per r6 :302-303.
|
||||
- The bubblewrap note reads the version from `dpkg -s bubblewrap`.
|
||||
- The run sheet's recovery steps (`chown root:mosaic`, `chmod 0750`, `apparmor_parser -r`, then `--check`
|
||||
before root removes `DRIFT`) are mine. r6 says only that root removes `DRIFT` in a reviewed step.
|
||||
|
||||
## Run environment
|
||||
|
||||
Worktree `/mnt/storage/scratch/darkwing-r240b`, branch `darkwing/row240-r3` on c771a36f4, with `node_modules`
|
||||
linked to the canonical checkout's. Node v26.8.1, kernel 7.2.2-1-cachyos, 16 CPUs, TMPDIR
|
||||
`/mnt/storage/scratch/tmp/r240`, compose project `darkwing-r240b`. Sage's slot (sage-1773, 06:16Z). Each run is
|
||||
one chain behind `busy.sh`, launched from `/`: `~/darkwing-scratch/r240/run-all.sh` checks the HEAD pin and a
|
||||
clean tree, runs the singles, then the mutants (8 killed, none surviving, the tree clean after), then the gate
|
||||
(the cli suite and every `scripts/test-*.sh`), each stopping the chain on failure, then sweeps `/proc` cwd links
|
||||
for anything left in the r240 trees.
|
||||
|
||||
## Results
|
||||
|
||||
Run r1, 06:17:10Z on 47ec9fe2f, failed at the singles. `case1-hostcheck.test.mjs` passed 16/0/0, and
|
||||
`stack-upgrade.test.mjs` went 20 pass, 1 fail. The failure was the N1 test's own `t.after` hook, which ran
|
||||
`chmodSync(e.etc, 0o755)` after the fixture's cleanup had already removed the scratch root, so it threw ENOENT.
|
||||
Every assertion in the test body had passed, exit 5 included. The fixture's cleanup runs `chmod -R u+w` on the
|
||||
root before `rmSync`, so the 0555 dir never needed the hook. b45852daa drops it and changes nothing else. The
|
||||
mutants and the gate didn't run. The sweep found nothing left. Sage counted r1 as this slot's one failure
|
||||
(sage-1788).
|
||||
|
||||
Run r2, 06:20:41Z to 06:33:28Z on b45852daa, passed. The counts come from awk over the .out files
|
||||
(`~/darkwing-scratch/r233/sum.sh`):
|
||||
|
||||
| Step | Result |
|
||||
|---|---|
|
||||
| `case1-hostcheck.test.mjs` | 16/0/0 |
|
||||
| `stack-upgrade.test.mjs` | 21/0/0 |
|
||||
| c0 control | exit 0 |
|
||||
| M1-M8 | 8/8 killed, none survived, every restore's sha256 matched, tree clean after |
|
||||
| `packages/cli` suite | 639/0/0 |
|
||||
| `scripts/test-*.sh` | auth 15, conductor 17, config 31, discord 78, extension-package 18, foundation 44, land-work 86, queue 156 node + 28 selftest, release 14, task 98; 585 pass, 0 fail |
|
||||
| Gate total | 11 suites, 1224 pass, 0 fail |
|
||||
| Sweep | nothing left in the r240 trees; the compose network was removed |
|
||||
|
||||
This commit adds only these BUILD.md lines. No test reads BUILD.md.
|
||||
@@ -0,0 +1,7 @@
|
||||
# Duckworth
|
||||
|
||||
Reviewer seat, Claude Opus 5.5, started by Mos (mos-0977, thread ffceafec). Reviews federation designs and MVP T1 row 193.
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no
|
||||
credentials of its own; until the credentials row settles per-agent identity, it pushes only its own `duckworth/*` branches
|
||||
through the repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch.
|
||||
@@ -0,0 +1,8 @@
|
||||
# Gander
|
||||
|
||||
Builder seat, Codex gpt-6.1-sol, started by Sage under mos-0977 item 5 (more Codex Sol rows) for the human
|
||||
boundary row (P19 333, #1694, mos-0989).
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no
|
||||
credentials of its own; until the credentials row settles per-agent identity, it pushes only its own `gander/*`
|
||||
branches through the repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch.
|
||||
@@ -0,0 +1,9 @@
|
||||
# Gizmoduck
|
||||
|
||||
Design and builder seat, Claude Opus 5.5, a T3 thread (9312a896). Author of the federation design
|
||||
(`agents/gizmoduck/work/federation/DESIGN.md` on `gizmoduck/federation-design`, #414). Builds FED R1, the
|
||||
access.json loader and validator, under Mos's order mos-1023 item 1.
|
||||
|
||||
No launcher in this directory. The seat works in its own worktree and has no credentials of its own; until
|
||||
the credentials row settles per-agent identity, it pushes only its own `gizmoduck/*` branches through the
|
||||
repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch.
|
||||
@@ -1,655 +0,0 @@
|
||||
# T3 federation design: factory and hub (Vikunja #414, G3, G4, C1.1)
|
||||
|
||||
Author Gizmoduck (Opus 5.5), 2026-10-20, r4.11. Design only, no product code. Reviewer Duckworth (Opus 5.5,
|
||||
mos-0978), r1 verdict changes B1-B5 (duckworth-0001, REVIEW.md at b402197ff), r2 verdict changes B6 (duckworth-0002,
|
||||
REVIEW-r2.md at 03a663cd7), r3 verdict changes B7 (duckworth-0003, REVIEW-r3.md at 45cbe5896), r4 verdict changes B8 (duckworth-0005, REVIEW-r4.md at
|
||||
4966136d4), r4.1 verdict changes, B8's issuer host (duckworth-0007, REVIEW-r4.1.md at b14f11136), r4.4 verdict changes B9 (duckworth-0010, REVIEW-r4.4.md at 4cf3ce232), r4.5 approved (duckworth-0012, REVIEW-r4.5.md at 912b1d325). R1, R2 and R5 go to Sage's
|
||||
queue once Duckworth approves, and Sage assigns owners. Code lines are at origin/refactor 9ae8c73ac. The
|
||||
Q175 SSO design is cited at Sage's local commit 817e8f51b (agents/sage/work/q175-sso/DESIGN.md), which was
|
||||
not on origin when this was written.
|
||||
|
||||
Binding inputs: TARGET.md addendum 2026-10-20, DECISIONS Q175 to Q178, projects/mosaic-mvp/GAP-MAP.md and
|
||||
PLAN.md T3 (all in jason-master-restart).
|
||||
|
||||
## Changes from r1
|
||||
|
||||
- B1. A user principal now comes only from the Console's verified OIDC session. The user CLI takes the
|
||||
human proof's walk, which refuses agent markers and binaries, and its direct parent must be the
|
||||
Console from the bus's own tree. This needs the path-bound human proof (P19 333). Residual risk in 3.6.
|
||||
- B2. 3.6 names the ownership changes: `#owner`, `#threadFor`, the `#human` gates and `thread.send`'s
|
||||
sender fields. `message.send` is on no list, and `#threadFor` refuses users as a second wall.
|
||||
- B3. One level for 10-27, `collaborator`, with an exact verb list and a filter per read (3.5). Viewer and
|
||||
every board read wait.
|
||||
- B4. Revocation has three triggers: the next user request, a 30 s broker timer, and a reload verb that
|
||||
R5 calls. The launcher refuses to start a revoked thread, because the broker reports it on every
|
||||
`threadsState`. It also SIGTERMs a running one (3.7). R4 now depends on R3 and R5.
|
||||
- B5. A separate uid per instance is a hard entry gate of R8, with checks (mos-0984 point 2).
|
||||
- N1 to N9 are handled where they apply: v1 count (2), `configDir` at business.mjs:25-28 (5), R1 takes
|
||||
providers and peers as arguments (7), "human-only" (3.5), file checks (3.5), `instances` as the real limit
|
||||
(3.5), provider removal needs a Console restart (3.7), Q1 is with Jason (9), client ids in R8 (7).
|
||||
- r3 (duckworth-0002): B6, the reply path to a user (3.6 item 7, runner header, R3 test and mutant, R7
|
||||
asserts a reply). C1, the walk doesn't stop an unsandboxed same-uid agent (3.6). N10, `names` gates
|
||||
through `#agent` (3.6 item 4). mos-0991: R8's gate accepts a separate host or a separate uid, and 3.8
|
||||
checks the federation path for one-host assumptions. Q1 holds Jason's answer. morgana-0002 settles Q2.
|
||||
- r4 (duckworth-0003, mos-0996): B7, R8's gate now proves there is no privilege path from the factory to
|
||||
the hub. Per mos-0996 (Vikunja #418) the hub runs on its own host, and for 10-27 the gate is the host form
|
||||
only. The uid form (row 190b) is a later option. The checks run as the factory uid with a clean
|
||||
environment and pass only when the call fails: `sudo -n`, `doas -n`, and `ssh -o BatchMode=yes` to the
|
||||
hub host with and without the agent socket. Factory root on the PVE nodes defeats the host form
|
||||
(duckworth-0004, mos-1004), so it is an open R8 gate item with options O1 to O3 for Jason (Vikunja
|
||||
#419, section 8). N11, `#waiting` returns null for `to` `user` (3.6 item 7, R3).
|
||||
N12, R3 assumes nothing about how shares end (section 9 Q2). Row 196 (#1694, Gander) owns the shared
|
||||
principal verifier before R3 (section 5).
|
||||
- r4.11 (mos-1063): the hub's host is settled, so the "own host, not dragon-lin or Melanie's server"
|
||||
lines and the #418 dependency are gone. The hub runs on .54, VM 1012, as user `mosaic` (Q181), and R8
|
||||
depends on the Case 2 install on .54 (#416). #418 is closed. #419 carries only the wording for
|
||||
Melanie. Mos ruled the four functional checks stay blocking.
|
||||
- r4.10 (mos-1055, DECISIONS Q182 and Q183): R8 is a recorded acceptance. It starts with Jason's
|
||||
acceptance of the factory paths to .54 on record, quoted in section 8. Every privilege and host check
|
||||
still runs, and its result goes into the R8 record with who ran it. They report and don't refuse.
|
||||
Agents may run the .54 checks as localadmin, which replaces r4.9's line. The mos-1042 order is now in
|
||||
the R8 row. The wording still reaches Melanie only through Jason (#419).
|
||||
Two lines that said R8 proves the factory has no login on the hub (sections 3.5 and 3.6) now say
|
||||
factory agents keep ssh to .54 under Q183, recorded as accepted.
|
||||
- r4.9 (mos-1037): R8 names who runs the root checks on 10.1.1.54.
|
||||
- r4.8 (duckworth-0020 N22): R8 checks the hub user's limits on 10.1.1.54 instead of taking #421's plan
|
||||
as done.
|
||||
- r4.7 (mos-1030, DECISIONS Q181): the hosts are settled. Case 1 is W-jarvis, 10.1.1.53 (VM 1008), the
|
||||
same host as the Hermes agents, so section 8 lists them as one factory host. The hub, Case 2, is
|
||||
W-Melanie-OC, 10.1.1.54 (VM 1012). Its OS user is `mosaic`, a system user with no sudo, no groups
|
||||
and no keys (ops-01's tightening plan section 3, Vikunja #421).
|
||||
- r4.6 (duckworth-0012 N21, morgana-0011): R6's notice is row 193's final user sentence, word for word
|
||||
(row 193 design r5 section 5.7, morgana/row193 e2d161e2e). The start-order line now matches the
|
||||
section 7 table: R2 waits on #412 phase 1, and R5 lands after R1.
|
||||
- r4.5 (duckworth-0010): B9, a user-thread run can't message the owner, but its decisions do reach the
|
||||
owner's inbox, as Sage ruled for row 193 (sage-1368, morgana-0008)
|
||||
and Mos kept (mos-1022). 3.6 item 7 says so, R6 tells
|
||||
the user, and R3 tests both sides. N20, the start-order line waits for row 193's viewer level. R8
|
||||
can't start while any host variable is unresolved.
|
||||
- r4.4 (mos-1020, mos-1019): N17, R4 is chained after row 193's viewer level and keeps the level-loss
|
||||
test, which has to kill its mutant before R4 is done.
|
||||
- r4.3 (duckworth-0007): B8, an issuer that isn't on the hub host must pass the host-access, hypervisor
|
||||
and backups rows against its own host, and a hosted issuer's admin accounts each pass the failed-call
|
||||
check (section 8, sign-in row). N16, the dependency note covers every secret on a PVE guest. N17, R4's
|
||||
level-loss test moves to row 193's viewer level. N18, 3.6 names `verifyPrincipal`. N19, the TLS check
|
||||
runs through the public route and from the LAN.
|
||||
- r4.2 (mos-1013, morgana-0007): "factory" in R8 and section 8 means every principal outside the hub
|
||||
instance, not only the dragon-lin uid: the dragon-lin dev uid, the Case 1 instance's VM and its agents,
|
||||
and Jason's Hermes agents on W-jarvis. Every R8 check runs from each of their hosts. Hosts are variables
|
||||
until Mos confirms them with Jason. The table rows don't change. Q1 is settled by Q180: P2P is
|
||||
person-to-person. Row 193 calls `covers` with no level for share reads and with
|
||||
`level: 'collaborator'` for create, open, send and share, so `covers` needs no other argument
|
||||
(morgana-0007, row 193 DESIGN.md at 19c015741).
|
||||
- r4.1 (mos-1006, gander-0007, morgana-0005): R8's gate adds the identity provider path. A factory
|
||||
credential that can administer the issuer the hub trusts gets in without the host (the B8 bullet below
|
||||
has the rule). Mos accepted the sudo deviation (root ssh
|
||||
and the `authorized_keys` check). Row 196's interface is settled: one verify per one-request
|
||||
connection, `s.user` for that request only, and the walk mutants move to row 196. morgana-0005: grants
|
||||
name bus project ids, and one pure `covers` in R1 matches a grant to a thread (3.5). R4's loss
|
||||
predicate counts only collaborator grants, so a downgrade to viewer ends the run.
|
||||
- r4.1 (duckworth-0005): B8, the hub's issuer and TLS front end are in its trust base. The hub trusts
|
||||
only an issuer no factory credential can administer, and TLS ends where the factory can't reach
|
||||
(3.4, R8). A brand, tenant or application in the factory's Authentik doesn't count. N13, the key check
|
||||
is a procedure. N14, R3 consumes row 196's verifier and depends on it. N15, DNS control. mos-1009:
|
||||
section 8 has a table of every privilege path (hypervisor, backups, sign-in provider, TLS front end,
|
||||
certificate authority, DNS, network), each closed by a check or accepted by Jason with set wording.
|
||||
- r2.1 (mos-0988): the separate uid is row 190b. R8's gate points at 190b, R7 at row 190 (one uid,
|
||||
scratch). P19 333 is on the 10-27 path (section 8).
|
||||
- Mos's rulings in mos-0978 and mos-0984 replace the r1 questions (section 9). The push credential note is
|
||||
in section 10.
|
||||
|
||||
## 1. Summary
|
||||
|
||||
By 10-27, federation means a person from one instance can sign in to the other instance's Console and use
|
||||
exactly what its owner granted: one business, some projects and its thread instance, at the collaborator
|
||||
level. The receiving instance verifies that person itself through OIDC (Q175) and its broker enforces the
|
||||
grant on every request. Revoking a grant refuses the next request and stops that user's thread runs within
|
||||
about 30 s, or at once when the grant CLI makes the change.
|
||||
|
||||
Instance-to-instance traffic, where an agent on the factory talks to a role on the hub, waits until after
|
||||
10-27 unless Jason rules otherwise (mos-0984 point 3). Section 6 sketches it (Ed25519 instance keys and
|
||||
signed HTTP requests) so the 10-27 slice doesn't block it.
|
||||
|
||||
## 2. What exists today (measured)
|
||||
|
||||
- F1. The bus is a local Unix socket per data root: `socketPath` at packages/cli/src/config.mjs:19, opened
|
||||
at packages/bus/src/runtime.mjs:169. Nothing listens on a network port. No live federation code exists.
|
||||
The PRD lists REQ-FED-1 as post-v1 (packages/tasks/requirements.json:61).
|
||||
- F2. A business has exactly one human: `human` at packages/business/src/business.mjs:160.
|
||||
`bindHuman` refuses any other name (packages/bus/src/broker.mjs:532-538).
|
||||
- F3. The human proof is a cooperative same-uid process check, not a wall. `verifyHuman` compares the
|
||||
caller's uid, start time, nonce and CLI path (packages/bus/src/human.mjs:52-72), then walks the parents
|
||||
and refuses agent markers, launch records and agent binaries (:73-88). The tester proof refuses only
|
||||
managed runs and allows agents by design (packages/bus/src/tester.mjs:5-9, :56-60). The server takes
|
||||
exactly one of `cap`, `human` or `tester` per request (packages/bus/src/server.mjs:52-70).
|
||||
- F4. Authority for agents is checked in the broker: `#checkAuthority` and `#authorityFor`
|
||||
(broker.mjs:763-799), with classification at :596-609. Every one of these is about a role run. Humans
|
||||
get no roles. `#human` (broker.mjs:375-378) is the only human check.
|
||||
- F5. Threads have a single `owner` column (packages/bus/schema.sql:247). `#thread` refuses a thread whose
|
||||
owner isn't the caller (broker.mjs:356-362). `#owner` returns `tester` or the human (broker.mjs:364-366).
|
||||
`#threadFor` special-cases the human (:326) and checks reply chains for the tester only (`#testerOwns`,
|
||||
:352-355, called at :337 and :342). `#threadHolders(business, instance, owner)` finds the held runs of
|
||||
one owner's threads (broker.mjs:298-306).
|
||||
- F6. The Console reaches the bus as the one human by spawning `human-cli.mjs` per call
|
||||
(packages/webui/src/bus.mjs:1-9, 27-31). Its entry is `packages/webui/src/cli.mjs serve`
|
||||
(scripts/webui-service.sh:3). Its sign-in today trusts a proxy header (Sage's design, section "What exists
|
||||
today"). Q175 replaces that with an OIDC relying party that takes a `subjects` list from a `console.sso`
|
||||
key in config.json (Sage's design sections 1 and 4).
|
||||
- F7. config.json is read by one loader that refuses unknown top-level keys
|
||||
(scripts/mosaic-config.mjs:112-116). The business file is writable by agents through
|
||||
`organizationWriter` (packages/business/src/organization.mjs:83), which is why Sage put SSO config in
|
||||
config.json and not the business file.
|
||||
- F8. Bus, Discord and Console run as `systemctl --user` units (scripts/bus-service.sh:200), so every
|
||||
instance on a host runs under Jason's uid today.
|
||||
- F9. Board reads are per business, not per project or instance: `inbox`, `agents`, `tasks`, `trail` at
|
||||
broker.mjs:2143-2146, `decisions` :2174, `credentials` :2195, `messages` :2217 (the Console's set is
|
||||
views.mjs:3-13). `project.list` returns every project in the business (:2318-2322).
|
||||
- F10. The launcher restarts a thread that has pending messages and no holder (packages/cli/src/launcher.mjs:533),
|
||||
launched as the business human (:542), every 2 s (:133). Its `startedFor` guard is in memory. A claim end
|
||||
stops a run only at its next bus call (broker.mjs:2273-2275). The idle stop is the one place that
|
||||
SIGTERMs a thread runner (launcher.mjs:527).
|
||||
|
||||
### The archived v1 federation (v1/apps/gateway/src/federation)
|
||||
|
||||
What it does: mTLS between gateways with certificates from step-ca. The grant id and subject user id sit
|
||||
in custom certificate OIDs (ca.service.ts:22-24). A peer enrolls with a single-use, 15-minute bearer
|
||||
token. The guard reads the client certificate off the TLS socket (server/federation-auth.guard.ts:93-106)
|
||||
and re-checks the grant row on every request. Scopes are zod-validated resource lists
|
||||
(scope-schema.ts:15-99). Shared data is read-only list and get queries.
|
||||
|
||||
Verdict: none of the code is reusable here.
|
||||
|
||||
1. It is about 4,470 lines of NestJS, Drizzle and Postgres code without tests (9,198 with them), plus a
|
||||
step-ca dependency. The new stack has none of those.
|
||||
2. Inbound federation never worked. The guard depends on `requestCert: true`, but the server is built as
|
||||
`new FastifyAdapter({ bodyLimit })` with no TLS options (v1/apps/gateway/src/main.ts:50-53). Every
|
||||
inbound call would get 401.
|
||||
3. mTLS is the wrong fit for the homelab path. Traefik terminates TLS in front of the Console (Sage's
|
||||
design, public path). A client certificate would reach the app as a forwarded header, which is the
|
||||
header trust Q175 point 1 rules out.
|
||||
|
||||
Ideas worth keeping: re-check the grant on every request, never cache it in the credential. Keep
|
||||
enrollment short-lived and single-use. Tag every federated result with its source instance.
|
||||
|
||||
## 3. The 10-27 slice
|
||||
|
||||
### 3.1 What it must do
|
||||
|
||||
1. D1. Each instance names itself and its peers in config.json, so the Console can show "you are on the
|
||||
hub" and link to the factory. Peer entries carry no trust in the 10-27 slice. They are labels and URLs.
|
||||
2. D2. The owner of an instance grants a person access to one business, with a project list and an
|
||||
instance list. Grants live in a human-authored access file on the receiving instance.
|
||||
3. D3. That person signs in to the receiving instance's Console with OIDC. The Console verifies the token
|
||||
itself (Q175) and matches `(provider, sub)` to an access-file user with a live grant.
|
||||
4. D4. The broker, not the Console, enforces the grant on every request: which verbs, which projects,
|
||||
which instances, and which threads (own threads only).
|
||||
5. D5. Revoking a grant refuses that user's next request on the Console and on the bus, and ends the runs
|
||||
of that user's threads in that business.
|
||||
|
||||
### 3.2 What waits
|
||||
|
||||
- W1. Agent-to-agent traffic between instances (section 6).
|
||||
- W2. Shared threads between several users (C1.4), and a viewer level. Both are T1.3's. A granted user gets
|
||||
private threads only.
|
||||
- W3. Remote owners. A grant never gives owner verbs: organization, instances, routes, plugins, limits,
|
||||
decision resolution, launch revoke and the kill switch stay with the local human.
|
||||
- W4. A registry, `mosaic comms`, enrollment tokens and key rotation tooling. With two instances,
|
||||
out-of-band setup by Jason is cheaper.
|
||||
- W5. Federated reads that merge results from both instances into one view, and any board read for a
|
||||
granted user (F9).
|
||||
|
||||
### 3.3 Instance identity in the slice
|
||||
|
||||
An instance is its public HTTPS origin plus its own OIDC client at the provider. The factory and the hub
|
||||
are two clients, so their ID tokens carry different `aud` values. The Q175 audience check refuses a
|
||||
factory token presented at the hub, which makes tokens instance-bound by signature, not by a header. This
|
||||
holds only if the two client ids differ, which R8 checks. No instance key is needed until instances talk
|
||||
to each other (W1).
|
||||
|
||||
### 3.4 Remote user identity
|
||||
|
||||
A user is `(provider, sub)` where `provider` is an id from that instance's `console.sso.providers`. Q175
|
||||
already verifies signature, issuer, audience, expiry and nonce, and allows several providers. The hub
|
||||
trusts only an issuer that no factory credential can administer (B8, duckworth-0005), for example a
|
||||
Pocket ID on the hub's side. Jason then has his own account at that issuer, with a `sub` of its own. A
|
||||
separate brand, tenant or application in the factory's Authentik doesn't count: factory agents hold an
|
||||
Authentik API token, and whoever administers the issuer can impersonate any user, reset a login or
|
||||
repoint a redirect. If Jason wants the hub on the factory's Authentik anyway, that is his decision, of
|
||||
the same kind as #419 (section 8). Either way the receiving instance checks the token. It never takes the
|
||||
home instance's word for who the user is.
|
||||
|
||||
The owner path stays as Sage designed it: `console.sso` subjects sign in as the business human. Everyone
|
||||
else comes from the access file. The two lists are disjoint, and the loader refuses a subject in both.
|
||||
|
||||
### 3.5 The access file and the collaborator level
|
||||
|
||||
`<configDir>/access.json`, next to config.json, so it is per instance (`configDir` at business.mjs:25-28
|
||||
reads `MOSAIC_CONFIG`). It is not in config.json for two reasons. Revocation has to take effect without a
|
||||
restart, and a typo in config.json stops every tool on the instance (Sage's design, section 4). It is not
|
||||
in the business file because agents can write that (F7).
|
||||
|
||||
```json
|
||||
{
|
||||
"accessVersion": 1,
|
||||
"revision": 3,
|
||||
"users": [
|
||||
{ "id": "jason", "home": "factory", "subjects": [{ "provider": "authentik", "sub": "<sub>" }] }
|
||||
],
|
||||
"grants": [
|
||||
{
|
||||
"id": "g-hub-1",
|
||||
"user": "jason",
|
||||
"business": "creative-hub",
|
||||
"projects": ["*"],
|
||||
"instances": ["researcher"],
|
||||
"level": "collaborator",
|
||||
"expires": "2026-12-31T00:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Rules the loader enforces, refusing the whole file on any failure (fail closed). The loader takes the
|
||||
provider ids, peer ids and declared businesses as arguments, so it stays pure:
|
||||
|
||||
- The file is a regular file, not a symlink, owned by the instance's uid, with no group or other write.
|
||||
These are the checks Sage's loader runs on the secret slot.
|
||||
- Unknown keys refuse. `accessVersion` is 1. `revision` is a positive integer.
|
||||
- A user id matches the business id rule and is never `human`, `tester`, or any business's `human`.
|
||||
- `subjects[].provider` names a given provider. A `(provider, sub)` pair appears once in the file and
|
||||
never in `console.sso.subjects`.
|
||||
- `home` names a given peer, or is `local` for a non-federated extra user.
|
||||
- A grant names a declared business. `projects` is `["*"]` or bus project ids, the UUIDs
|
||||
`project.create` makes (broker.mjs:2306). `instances` is
|
||||
`["*"]` or declared instances. `level` is `collaborator`, the only level in 10-27. `expires` is optional
|
||||
ISO time.
|
||||
- The loader checks a project entry's shape only (a UUID). It can't see the bus, so an id with no bus
|
||||
project covers nothing.
|
||||
- A grant past `expires` is treated as absent.
|
||||
- A file that fails any rule leaves every user with no grant, which revokes everything. That is the safe
|
||||
direction.
|
||||
|
||||
Matching a grant to a thread (morgana-0005). A thread's `project` is a bus project id
|
||||
(`threads.project REFERENCES projects(id)`, packages/bus/schema.sql:245), set by `thread.create`
|
||||
(broker.mjs:2329-2345). A bus project may have no repo in the business file, so grants can't name
|
||||
business-file projects. They name bus project ids, not names. A name could be taken later: the tester,
|
||||
which runs from an agent, can call `project.create` (:2300), and nothing renames or deletes a project
|
||||
today. R1 exports one pure function, `covers(grant, { business, project, instance }, now, { level })`.
|
||||
It is true when the grant is unexpired, the business matches, `projects` is `["*"]` or holds the
|
||||
project id, `instances` is `["*"]` or holds the instance, and the grant's level is the one asked for,
|
||||
when one is asked for. The broker passes the thread row. `["*"]` also covers projects made after the
|
||||
grant, and the R5 help text says so. R5's `grant --project <name>` resolves the name to the id through
|
||||
`project.list` at grant time and writes the id. `mosaic access list` shows both. Row 193 uses the same
|
||||
`covers`.
|
||||
|
||||
The collaborator level is an exact verb list. Every other verb fails `access-refused` before the verb's own
|
||||
checks. That covers all owner verbs (W3), every board read (F9), `message.*`, `decision.*`, project files,
|
||||
`spec.read`, `assistant.stop`, `launch.*`, `name.set` and `usage.*`.
|
||||
|
||||
| Verb | Today | For a user |
|
||||
|---|---|---|
|
||||
| `project.list` | every project in the business (:2318-2322) | granted projects only |
|
||||
| `thread.create` | the business's one thread instance (:2331-2334) | project granted, and that instance in the grant's `instances`, else `access-refused` |
|
||||
| `thread.list` | owner filter (:2351-2362) | owner `user:<id>`, and only threads whose project and instance are still granted |
|
||||
| `thread.open` | `#thread` owner check (:2365-2371) | same, plus the thread's project and instance still granted |
|
||||
| `thread.send` | `#thread`, sender `human`, writes `human.input` (:2374-2398) | same grant check. Sender `from_role` `user`, `from_run` `user:<id>`, no `human.input` |
|
||||
| `thread.messages` | `#thread` (:2401-2420) | same grant check |
|
||||
| `names` | business-wide (:2288-2295) | granted instances only |
|
||||
|
||||
`human.input` events are the owner's authority evidence (the `input` view, broker.mjs:1203-1207). A user's
|
||||
message must never write one, the same rule the tester follows at :2398.
|
||||
|
||||
`instances` is the real limit of a grant. The thread instance runs with its full role authority and is
|
||||
launched as the business human (F10), so a collaborator's message drives that instance with the owner's
|
||||
role. A grant gives a user exactly what the granted instance can do. The grant docs and the R5 help text
|
||||
say so.
|
||||
|
||||
Writes go through `mosaic access grant` and `revoke` (row R5), atomically with a revision bump and one
|
||||
receipt line in `<dataRoot>/state/access-log.jsonl` (append-only, as `activation-log.jsonl` is). The
|
||||
CLI checks its caller with the human proof, but a direct file write by the instance's uid skips both the
|
||||
CLI and the receipt. The separate host keeps other instances' processes from making that write, but
|
||||
factory agents keep ssh to .54 (Q183), so one with a login there can make it. R8 records that as
|
||||
accepted (section 8). Jason may also edit the file by hand.
|
||||
|
||||
### 3.6 The user principal in the broker
|
||||
|
||||
The bar (B1, mos-0984 point 1): a bus user principal comes only from something the bus can verify. Here
|
||||
that is the Console's verified OIDC session, reached through a proof the bus checks itself. The calling
|
||||
process's claim alone isn't enough, and neither is a proof that admits agent ancestry.
|
||||
|
||||
The proof. Row 196 (#1694, Gander) builds this check as the `user` kind of its shared `verifyPrincipal`,
|
||||
and R3 consumes it (section 5). The checks are the ones below. A new path-bound CLI, `packages/bus/src/user-cli.mjs`, with its own nonce variable
|
||||
(`MOSAIC_BUS_USER_NONCE`), so neither of the other CLIs' nonces proves it. The bus checks it with
|
||||
the `user` kind of row 196's `verifyPrincipal`, which:
|
||||
|
||||
1. Runs `verifyHuman`'s checks with `cliPath` set to the bus's own `user-cli.mjs`: same uid as the bus,
|
||||
start time, nonce, CLI path, and the parent walk that refuses agent markers, launch records and agent
|
||||
binaries (human.mjs:64-96).
|
||||
2. Also requires the user CLI's direct parent to be the Console from the bus's own tree: `argv[1]` equals
|
||||
the bus's resolved `packages/webui/src/cli.mjs`, and `argv[2]` is `serve`. This binds the user CLI to
|
||||
the Console's tree, as #225 binds `human-cli` to its literal path.
|
||||
3. Takes `user` from the proof and binds it only if the access file gives that user a live grant in the
|
||||
proof's business. Otherwise `access-revoked`.
|
||||
|
||||
So the claim "I am user jason" comes from a process the bus has verified as its own Console, outside any
|
||||
agent. The Console makes that claim only after Q175 verified the ID token's signature, issuer, audience,
|
||||
expiry and nonce and mapped `(provider, sub)` to the user.
|
||||
|
||||
This needs the path-bound human proof, P19 333. Residual risk, stated as Sage's design section 8 states it
|
||||
for the owner. The proof is a cooperative same-uid check. A non-agent process on the instance's uid, a
|
||||
shell Jason opens for example, can start its own copy of the Console's tree from the bus's path and claim
|
||||
any granted user. So can anything that can ptrace or write files as that uid. The walk refuses an agent
|
||||
in its normal process tree, but an unsandboxed agent on the same uid can get past it by starting the
|
||||
Console through `systemd-run --user` or by daemonizing so its parent chain ends at pid 1. That is the same
|
||||
hole the owner proof has (P19 333), not a new one. Other instances can't: the 10-27 hub runs on its own host, so its bus
|
||||
isn't on their host at all. Factory agents keep ssh to .54 and root on pve (Q183), so the host form doesn't stop them, and R8 records that as accepted (section 8). Federation adds no weaker path than the owner's.
|
||||
|
||||
The broker changes, each with an R3 test:
|
||||
|
||||
1. `bindUser({ business, user })` makes a session with `s.user` set and `s.human` and `s.tester` unset.
|
||||
2. `#owner(s)` (:364-366) returns `user:<id>` for a user session. Today it would return undefined.
|
||||
3. `#threadFor` (:318-345) refuses a user session with `thread-required-use-thread-send`, as it refuses
|
||||
the human at :326. `message.send` is also off the verb list. Two walls, so a reply-chain send into
|
||||
another user's thread fails even if one is removed (R3 mutant).
|
||||
4. The `if (!s.tester) this.#human(s)` gates on the six project and thread verbs above become a check
|
||||
that admits the human, the tester, or a user with a live grant. `names` gates differently, with
|
||||
`if (!s.human && !s.tester) this.#agent(s)` (:2290), so there the user branch goes before the `#agent`
|
||||
fall-through, or a user session fails `#agent`.
|
||||
5. Every other `s.human` and `s.tester` branch (48 and 41 in broker.mjs) gets an audit line in R3's
|
||||
packet: user refused, user treated as human, or not reachable for a user. Since every non-list verb
|
||||
fails first, most are "not reachable", and the audit shows it.
|
||||
6. `refuse()` records `action.refused` for every refusal (broker.mjs:526-530), with `by` = `user:<id>`.
|
||||
7. The reply path (B6). The runner answers each message with `message.send { to: m.from_role }`
|
||||
(packages/harness/src/runner.mjs:450), and today the broker accepts only `human`, `tester` or a role as
|
||||
`to` (broker.mjs:1862). A user's message has `from_role` `user`, so without this every reply fails
|
||||
`unknown-role`. The broker accepts `to: 'user'` from a run only when all of these hold, else
|
||||
`user-reply-required`:
|
||||
- `in_reply_to` names a message with `from_role` `user` in this business,
|
||||
- that message's `from_run` is `user:<id>` and `user:<id>` owns the run's thread,
|
||||
- the message is in that thread (`message_threads`).
|
||||
|
||||
This is the tester rule at :1852-1857, narrowed to one user and one thread. A `#userThread(s)` beside
|
||||
`#testerThread` (:631) finds a run of a user's thread. Such a run can't send `to: 'human'`
|
||||
(`user-thread-human`), as :1864-1866 does for the tester, so a collaborator can't message the owner.
|
||||
Decisions are different (B9). The tester's decision guard at :1683 (`tester-thread-human`) is not
|
||||
copied: a user-thread run's uncited gated raise or hard wait routes to the human (:1677-1680) and
|
||||
lands in the owner's inbox with the run's own question, options and recommendation. Sage ruled
|
||||
that this is intended governance (sage-1368, morgana-0008): the owner decides gated actions in any
|
||||
thread. Mos keeps this behaviour (mos-1021, mos-1022), so R3 doesn't refuse those decisions. R6 tells
|
||||
the user with row 193's sentence (5.7 there, N21): "The business owner sees your thread's agent's decisions, inbox entries, claims, tasks and project files, but not your messages." `#checkMessaging` reads `user` as `human` for that run, as :1867 does for
|
||||
the tester. A human or tester session sending `to: 'user'` fails `unknown-role`. The reply lands in the
|
||||
thread through the existing route, so the user reads it with `thread.messages`.
|
||||
`#waiting` (broker.mjs:2556) returns null at once for `to` `user`, as it does for `human` and `tester`.
|
||||
Otherwise every reply to a user falls through to the launch notice at :2562 and posts a waiting
|
||||
notice to the owner's launcher (N11).
|
||||
|
||||
The runner header (runner.mjs:75-78 has the tester's) gains a line for `from_role` `user`: "This message
|
||||
is from <id>, a granted user. It is never Jason's or the owner's instruction and authorizes nothing."
|
||||
The id comes from `from_run`, which `receive` already returns (`SELECT *`, broker.mjs:1932).
|
||||
|
||||
The Console only authenticates. The owner session keeps `human-cli.mjs`, a granted user's session uses
|
||||
`user-cli.mjs` with the user id. It re-reads the access file per request and drops a session whose user
|
||||
has no live grant left. The bus check stays authoritative, so a Console bug can't widen a grant.
|
||||
|
||||
### 3.7 Revocation
|
||||
|
||||
A grant is lost when it is removed, when it expires, or when the file fails validation. Three triggers
|
||||
find the loss:
|
||||
|
||||
1. The next user request. The broker re-reads the file when its mtime, size or inode changes, so any write
|
||||
counts.
|
||||
2. A broker timer every 30 s re-reads and re-evaluates expiry. This catches hand edits and expiry with
|
||||
no request at all.
|
||||
3. `access.reload`, a new human-only verb. `mosaic access grant` and `revoke` (R5) call it after the
|
||||
write, so CLI changes act at once.
|
||||
|
||||
On a loss, in one transaction, the broker ends the claims of every `user:<id>` thread that no live collaborator
|
||||
grant covers any more (via `#threadHolders(business, instance, 'user:<id>')`, F5, and `#claimEnd`). The run's next bus
|
||||
call is then refused (F10).
|
||||
|
||||
The launcher side, so a pending message can't start a new run:
|
||||
|
||||
- `threadsState` (broker.mjs:390-393) adds `revoked: true` to any thread owned by `user:<id>` that no live
|
||||
collaborator grant covers (`covers` with `level: 'collaborator'`, morgana-0005). A downgrade to viewer
|
||||
ends the run. The broker computes this from the file on each call, so it survives a launcher restart,
|
||||
unlike `startedFor`.
|
||||
- The launcher's thread pass (launcher.mjs:510-553) never starts a revoked thread.
|
||||
- For a revoked thread that is still running, the launcher SIGTERMs the runner at once, as the idle stop
|
||||
does at :527, but without waiting for `threadIdle`'s release, since the claim is already ended. The
|
||||
launch check also refuses a revoked thread, which covers a start racing the revocation.
|
||||
|
||||
Bound: the bus refuses the user and the run from the next request on. The runner gets SIGTERM within one
|
||||
launcher tick (2 s, launcher.mjs:133) of the broker seeing the loss. A model turn already in flight ends
|
||||
when its runner exits. Its last bus call is refused, so it can't deliver a reply into the thread.
|
||||
|
||||
Removing a provider from `console.sso` is different. config.json is read at start, so that takes effect
|
||||
only after a Console restart. Removing the users from access.json acts at once, so the R5 help text says to
|
||||
do that first.
|
||||
|
||||
### 3.8 One host or two (mos-0991)
|
||||
|
||||
For 10-27 the hub runs on .54, VM 1012, as user `mosaic` (Q181), a host of its own (mos-0996).
|
||||
Jason (mos-1013): dragon-lin is his workstation and won't host the final instances, and both cases run
|
||||
as VMs on the PVE node. Jason confirmed the hosts (DECISIONS Q181, mos-1030). The Case 1 instance is on W-jarvis,
|
||||
10.1.1.53 (VM 1008), and the hub, Case 2, is W-Melanie-OC, 10.1.1.54 (VM 1012). The hub's OS user is
|
||||
`mosaic`, a system user with no sudo, no groups and no keys (Vikunja #421).
|
||||
It is a PVE VM, and Jason accepted the factory's paths to it (Q182, Q183, section 8). The federation path
|
||||
assumes nothing about sharing a host:
|
||||
|
||||
- Nothing crosses between the instances' buses or filesystems. Each Console talks only to its own bus
|
||||
through `user-cli` or `human-cli`, spawned on its own host. access.json lives in the receiving
|
||||
instance's config dir. `federation.peers` holds URLs and labels, never a path or a socket. No row reads
|
||||
a peer's file or socket.
|
||||
- In the 10-27 slice the two Consoles never call each other. The user's browser reaches each origin, and a
|
||||
peer link is a plain link. Section 6 does need each Console's origin reachable from the other host, so
|
||||
R8 checks that now and the later rows add no infra.
|
||||
- Each instance has its own OIDC client with its own redirect URI on its own origin (3.3). Each Console
|
||||
must reach its provider's issuer and JWKS from its own host. The issuer is a privilege path too. Whoever administers it can mint a hub user, so
|
||||
the hub's issuer is one the factory can't administer (3.4, R8). The factory's Console reaches its own
|
||||
Authentik, and the hub's reaches its own issuer.
|
||||
- Q175 checks token expiry, and section 6 has a 60 s window on signed requests, so both hosts need a synced
|
||||
clock.
|
||||
|
||||
The only one-host piece is R7, which is scratch on one host by design (row 190). The uid form of R8's gate
|
||||
(row 190b) is a later option, outside the 10-27 gate.
|
||||
|
||||
## 4. Fit with the current code
|
||||
|
||||
- The bus stays a local socket. Federation reaches it only through the receiving instance's Console, which
|
||||
is the one network-facing piece. No row opens a bus port.
|
||||
- The broker's agent authority path (broker.mjs:763-799) is untouched. User checks are a separate path,
|
||||
next to `#human`, because humans have no role claim and `#agent` would refuse them (broker.mjs:367-374).
|
||||
- New ownership code is small but real: `#owner`, `#threadFor`, the seven verb gates, `thread.send`'s
|
||||
sender fields and `threadsState` (3.6, 3.7).
|
||||
- The business file keeps one `human`. Granted users sit outside it, which keeps every existing
|
||||
business-file test and the organization writer unchanged.
|
||||
- The config loader gains one optional key, `federation`, in the same file Sage's phase 1 changes
|
||||
(mosaic-config.mjs:112-116). Row R2 chains on that candidate and repeats its blast-radius rule: the key
|
||||
goes in only after a copy that accepts it is pinned.
|
||||
|
||||
```json
|
||||
"federation": {
|
||||
"self": "hub",
|
||||
"peers": [{ "id": "factory", "origin": "https://mosaic.woltje.com", "label": "Software factory" }]
|
||||
}
|
||||
```
|
||||
|
||||
## 5. Dependencies on T1
|
||||
|
||||
- T1.1, several instances per host (mos-0988). Row 190 (Darkwing) lands several instances on one uid,
|
||||
for scratch tests. R7 runs on that. Row 190b (Darkwing, Opus, design first, chained on 190) puts the hub
|
||||
under its own Linux user, with no factory process able to touch it (mos-0986). Rows R1 to R7 don't need
|
||||
190b: they use separate `MOSAIC_CONFIG` files and data roots, which the code already honours
|
||||
(business.mjs:25-28). For 10-27, R8's gate is the host form only (mos-0996), so 190b is a later
|
||||
option, not an R8 dependency. With per-uid instances,
|
||||
a factory-uid process can't read or write the hub's access.json or data root, and it can't pass the
|
||||
hub's human or user proof, because `verifyHuman` requires the caller's uid to equal the bus's
|
||||
(human.mjs:67). Inside one instance the proofs stay same-uid cooperative (3.6, P19 333).
|
||||
The design doesn't plan a single-uid live run.
|
||||
- A separate host (mos-0991, mos-0996). The hub runs on its own host, which R8 checks and records (section 7,
|
||||
3.8). It is a PVE VM, with the factory paths accepted by Jason (Q182, Q183, section 8).
|
||||
mos-0996 measured why the other hosts fail: on dragon-lin `sudo -n true` succeeds for jwoltje, and
|
||||
Melanie's server takes `ssh -o BatchMode=yes` from here with passwordless sudo behind it.
|
||||
- Row 196 (#1694, Gander) owns the bus human boundary before R3 (gander-0006): one shared verifier,
|
||||
`verifyPrincipal(request, connection)` returning `{kind, business, id}`, with `SO_PEERCRED` binding
|
||||
`proof.pid` to the connection. R3 consumes it for the `user` kind instead of writing its own
|
||||
`verifyUser`, with the same checks as 3.6 (human walk, direct parent the bus tree's Console `serve`,
|
||||
own nonce variable). It runs once for each one-request connection, and `s.user` holds for that
|
||||
request only, with no role or grant cached (gander-0007). R3 owns every live grant check, the
|
||||
`(provider, sub)` to user mapping and the user-name policy. Row 196 kills the agent-parent,
|
||||
non-Console-parent and kind-confusion mutants. R3 keeps the reserved user-name mutant.
|
||||
- T1.1 live also needs two Consoles, two public origins, two reverse-proxy routes and two provider
|
||||
clients, which are Jason-gated infra.
|
||||
- T1.2, #412 SSO. Hard dependency. Row R6 builds on Sage's phase 1 candidate (Q177 merge-chaining) and
|
||||
goes live only after the phase 2 window.
|
||||
- T1.3, human users and RBAC. One user and grant model for local and federated humans (mos-0978 Q2).
|
||||
Row 193 (Morgana) confirmed access.json as that store (morgana-0002, section 9 Q2). T1.3 adds `local`
|
||||
users, the viewer level and `thread_shares`, and federation adds `home`. R1 is the shared loader, so row
|
||||
193 builds on R1's candidate.
|
||||
|
||||
## 6. After 10-27: instance-to-instance
|
||||
|
||||
Sketch only, so the slice doesn't block it. If Jason puts agent-to-agent inside 10-27 (mos-0984 point 3),
|
||||
rows for it are added after r2.
|
||||
|
||||
- Each instance gets an Ed25519 key in a secrets slot (`~/.config/mosaic-dev/secrets/<instance>/`, 0600,
|
||||
per the Q173 slot rule). Peers pin each other's public keys in `federation.peers[].keys`. Public keys are
|
||||
not secrets.
|
||||
- Requests between instances are signed with HTTP Message Signatures (RFC 9421) over method, target URI,
|
||||
authority, content digest, created time and a nonce. The receiving Console verifies the signature
|
||||
itself, so the proof survives Traefik. A created time outside 60 s or a reused nonce refuses. No shared
|
||||
secret, no bearer token.
|
||||
- A peer agent becomes a grant subject of kind `peer` (`factory/researcher`), in the same access file, so
|
||||
grant and revoke work the same way. Removing a peer key refuses all of that peer's traffic at once.
|
||||
- First use: a factory agent sends a message into a hub thread it was granted, owned by `peer:factory`.
|
||||
|
||||
## 7. Rows
|
||||
|
||||
Owner model per Q177 and Q178. Opus 5.5 for hard design and builds, Haiku 5.5 at high effort for mid work,
|
||||
Codex gpt-6.1-sol for parallel builds. Every row has its own independent review (Filbert or Dewey) and
|
||||
gate. Chains follow Q177: a dependent row builds on its parent's candidate.
|
||||
|
||||
| Row | Work | Owner model | Depends on | Test gate |
|
||||
|---|---|---|---|---|
|
||||
| R1 | `packages/business/src/access.mjs`: access.json loader and validator. Providers, peers and businesses come in as arguments. One read plus the file checks | Codex Sol | none | business tests: every rule in 3.5 refuses with the key path named, symlink, non-regular file, other owner, group or other write, expiry, `*`, disjoint from the given subjects, `human`/`tester` ids, a level other than `collaborator`, a project entry that isn't a UUID. `covers`: `*` and listed ids match, an unlisted project, instance or business doesn't, an expired grant doesn't, a level filter rejects another level |
|
||||
| R2 | `federation` key in scripts/mosaic-config.mjs | Haiku 5.5 high | Sage #412 phase 1 candidate | test-config.sh: valid key loads, unknown sub-key and bad origin refuse, config without it loads |
|
||||
| R5 | `mosaic access list, grant, revoke`: human proof, atomic write, revision bump, receipt line. Help text states the `instances` limit and the provider-removal order | Haiku 5.5 high | R1 | cli tests: tester and agent callers refused, concurrent write refuses on revision conflict, receipt appended, bad grant refused before write |
|
||||
| R3 | Broker user principal: consumes row 196's `verifyPrincipal` (`user` kind, `user-cli.mjs`), `bindUser`, the 3.6 changes, the collaborator verb list and its filters, per-request grant read, audit lines for every `s.human`/`s.tester` branch | Opus 5.5 | R1, row 196 | bus tests: each listed verb allowed, every other verb refused, a leak test per listed read (other projects, other instances, other users' threads), a reply-chain `message.send` into another user's thread refused, `thread.send` writes no `human.input`, the run's reply to a user's message reaches that user's `thread.messages`, a reply `to: 'user'` without a matching `in_reply_to` refused, a user-thread run sending `to: 'human'` refused, a reply to a user returns `waiting` null and posts no notice message, a user-thread run's uncited gated raise reaches the owner's inbox and no other inbox or role, while its `message.send` `to: 'human'` is still refused (B9, mos-1022), revoked and expired grant refused on next request, refusals recorded. Mutants (the agent-parent, non-Console-parent and kind-confusion mutants are row 196's): the grant read skipped, a read filter dropped, `#threadFor`'s user refusal removed, the reply-to check dropped (a run answers another user's message or one outside its thread), a user named like the human accepted |
|
||||
| R4 | Revocation: 30 s timer, `access.reload` verb, claim end on loss, `threadsState` `revoked`, launcher skip and SIGTERM, launch check refusal, R5 calls `access.reload` | Codex Sol | R3, R5, row 193's viewer level (mos-1019) | bus and launcher tests, each with a mutant: a hand edit removing the grant ends the run within one timer period, an expiry with no request ends it, a pending message followed by a launcher restart starts no run, a running revoked thread gets SIGTERM, a grant downgraded to viewer counts as a loss, another user's runs stay. R4 starts after row 193's viewer level lands, because before it R1 refuses any other level, the whole file fails, and the downgrade test can't kill its mutant (N17, mos-1020) |
|
||||
| R6 | Console: granted-user sign-in, `user-cli` transport per session, per-request grant re-read, owner-only controls hidden, a notice on the user's thread page with row 193's sentence word for word (B9, N21): "The business owner sees your thread's agent's decisions, inbox entries, claims, tasks and project files, but not your messages.", peer links | Opus 5.5 | Sage #412 phase 1, R1, R2, R3 | webui tests on Sage's fake OP: granted user signs in, ungranted `sub` refused, revoked mid-session gets 401 on next call, a granted user's owner-verb call refused by the bus even with the UI check removed (mutant), the thread page's notice equals row 193's sentence exactly (N21) |
|
||||
| R7 | Scratch two-instance acceptance: two configs, two data roots, two Consoles, one fake OP. The Consoles start outside agent ancestry (a transient user unit), as the user proof requires. They never use the live stack's units or ports | Haiku 5.5 high | R1-R6, row 190 (several instances on one uid) | script test: Jason granted collaborator on one hub project signs in to the hub, makes a thread, sends to the granted instance, gets the instance's reply in the thread, is refused on another project and on board reads, is revoked and gets refused with the run ended. The factory shows nothing of the hub |
|
||||
| R8 | Live window, not a code row: hub provider client, hub proxy route, access.json written by Jason, re-pins | Jason, Mos | the Case 2 install on .54 (#416), Bean and Chet live and both Hermes gateways on .54 stopped (mos-1042), #412 phase 2, R7 | Host form only for 10-27 (mos-0996): the hub runs on .54, VM 1012, as user `mosaic` (Q181). R8 is a recorded acceptance (mos-1055). It starts with Jason's acceptance of the factory paths to .54 on record, DECISIONS Q182 and Q183, quoted in section 8. Every privilege and host check below still runs, and its result goes into the R8 record with the name of whoever ran it. They report and don't refuse. R8 runs after Bean and Chet are live and both Hermes gateways on .54 have stopped (mos-1042). Case 2 go-live doesn't wait on R8. The hub is W-Melanie-OC, 10.1.1.54 (VM 1012), and its OS user is `mosaic`, with no sudo, no groups and no keys (Q181, Q183). R8 checks that on 10.1.1.54 as root and records whether each holds: `sudo -l -U mosaic` grants nothing, `id mosaic` shows only its own group, `~mosaic/.ssh/authorized_keys` is absent or empty, and no file under `/etc/sudoers.d` names it (N22). An agent or Jason runs these as localadmin (mos-1055), and the output goes into the R8 record with who ran them. The privilege checks run on every factory host in section 8's list (the dragon-lin dev uid, and W-jarvis at 10.1.1.53, which is the Case 1 VM with its agents and the Hermes agents), as each factory uid there under `env -i`, and each is recorded as passing only when the call fails: `sudo -n true`, `doas -n true`, and `ssh -o BatchMode=yes 10.1.1.54 true` as the hub user `mosaic`, as Jason's user and as root, each with and without the session's `SSH_AUTH_SOCK`. If `sudo -n` succeeds, as it does for jwoltje on dragon-lin (mos-0996 point 1), the factory counts as root on its own host. Then, as root through `sudo -n`: fingerprint every private key under every home and `/root`, and every key loaded in any agent socket found on the host (`ssh-add -l` per socket). Compare them with `ssh-keygen -lf` over every `authorized_keys` on the hub host, root's included, and no fingerprint matches. Run the ssh checks as root, once with no agent socket and once with each socket found, and record each result (N13). Every other privilege path is a named item in section 8's table (mos-1009): hypervisor, backups, sign-in provider, TLS front end, certificate authority, DNS and network. The record marks each one closed, with its check passing, or accepted under Q182 and Q183 with the table's wording. That wording goes to Melanie through Jason (#419), never to her directly. R8 also records whether the hub host's bus socket, data root and config dir don't exist on any factory host, no factory host mounts anything of the hub's, and whether the hub host runs no other instance or agent. These must work, or R8 stops and Mos takes it to Jason, because federation fails without them: the two provider client ids differ, each origin answers over HTTPS from the other host, each Console reaches its issuer's discovery URL, and both clocks are synced. The record holds Jason's acceptance and the check results side by side, so a passing check is a measured fact and an accepted path is named as accepted. The uid form (row 190b) is a later option, outside this gate. Then Jason signs in to the hub from his account and sends one message, Melanie revokes, and his next request is refused |
|
||||
|
||||
R1 can start now. R5 can be briefed now against R1's exported interface and lands after R1. R2 starts
|
||||
when Sage's #412 phase 1 candidate exists. R3 starts when R1 has a candidate, R4 when R3, R5 and row 193's viewer level do (mos-1019).
|
||||
R6 waits on Sage's phase 1 candidate.
|
||||
|
||||
## 8. Risk to 10-27
|
||||
|
||||
The slice lands only if three things hold. #412 phase 1 has a reviewed candidate by about 10-23. P19 333,
|
||||
the path-bound human proof R3 depends on, is settled in time for R3 (Mos told Sage, mos-0988). The Case 2
|
||||
install on .54 (#416) is done in time for R8. Jason's answer on the factory paths is Q182 and Q183. R6 and R7 chain on #412 phase 1. R8 needs all three, plus
|
||||
two Jason-gated windows (#412 phase 2 and the federation re-pin), plus infra work only Jason does: a
|
||||
second public origin, a proxy route and a provider client. A failed privilege check
|
||||
doesn't stop R8. It goes into the record, and Mos takes it to Jason.
|
||||
|
||||
The hypervisor path (Vikunja #419, mos-1004), accepted by Jason under Q182 and Q183. Factory agents hold root
|
||||
ssh to the PVE nodes (Q172). Root on a node can read a running VM's memory and disk, open its console,
|
||||
and change its config or boot. So it defeats the host form completely when the hub is a cluster VM
|
||||
(duckworth-0004). LUKS inside the VM doesn't close it alone: the key sits in the running VM's memory,
|
||||
which the node can read, and a node that can change the boot can capture the passphrase. Options:
|
||||
|
||||
- O1. The hub runs on hardware outside the PVE cluster, where the factory has no root.
|
||||
- O2. A cluster VM, with factory root ssh removed on every node, no factory PVE token with console,
|
||||
monitor, config, backup or snapshot rights on the VM, VM storage the factory can't read, and PBS
|
||||
backups the factory can't read or that are encrypted with a key it doesn't hold.
|
||||
- O3. Jason accepts it as a residual. Then this section and R8 say plainly that the host form doesn't
|
||||
stop a factory agent using PVE root, and Melanie is told.
|
||||
|
||||
Jason's answer is O3, accepted (DECISIONS Q183 confirms Q4). His lines, verbatim:
|
||||
|
||||
Q182 (DECISIONS.md:1514-1519, jason-master-restart):
|
||||
|
||||
> "I am not pursuing a tightening of key and agent security on my workstation. I need my agents to be able to get onto the other workstations and servers."
|
||||
|
||||
> "We will eventually have my agents running on my VM on PVE, but currently I accept the risk of agents running on my workstation."
|
||||
|
||||
Q183 (DECISIONS.md:1535-1541):
|
||||
|
||||
> "Security tightening on .53 and .54 is fine. I will still want to allow infra agent(s) on my .53 instance to access other servers, etc. However, not all agents will need to have that access."
|
||||
|
||||
> "Q4 appears to properly reflect my statements."
|
||||
|
||||
Q4, as Mos asked it and Jason confirmed it:
|
||||
|
||||
> "The #421 plan stops. Your key stays, and agents keep root on pve and PBS and ssh to .53 and .54. Each VM still gets a mosaic user without sudo to run its instance. The federation gate records your acceptance instead of refusing to start."
|
||||
|
||||
So this section and R8 say it plainly: the host form doesn't stop a factory agent using PVE root, and
|
||||
R8 records that as accepted. Melanie is told through Jason (#419).
|
||||
|
||||
### R8 privilege paths (mos-1009, duckworth-0005, Vikunja #419)
|
||||
|
||||
Here and in R8, "factory" means every principal outside the hub instance (mos-1013), and "factory
|
||||
host" means each of their hosts:
|
||||
|
||||
- the dragon-lin dev uid, on Jason's workstation
|
||||
- W-jarvis, 10.1.1.53 (VM 1008): the Case 1 instance's VM and its agents, and Jason's Hermes agents.
|
||||
ops-01 reports the `jarvis@w-jarvis` key has root on `pve`.
|
||||
|
||||
Case 1 and the Hermes agents share W-jarvis, so they are one factory host (Q181, mos-1030). The hub is
|
||||
W-Melanie-OC, 10.1.1.54 (VM 1012). Every check in the
|
||||
table runs on each factory host, and a path is closed only when it passes on all of them.
|
||||
|
||||
ops-01 measured the estate read-only at 19:10Z (relayed in mos-1009). There is one PVE node, `pve`,
|
||||
standalone, and its root `authorized_keys` holds the factory's key (dragon-lin's, and per mos-1013
|
||||
the `jarvis@w-jarvis` key), so the factory has root on the
|
||||
hypervisor without any API token. The only PBS is VM 1021 on that node, the factory has root on it, and
|
||||
its backups are unencrypted. The LAN is flat (10.1.0.0/16) with dragon-lin on it and no internal VLAN.
|
||||
The factory has used the jarvis Authentik token for admin changes. No hardware outside PVE without
|
||||
factory root has been confirmed. So today the factory administers every path below.
|
||||
|
||||
Each path is a record item. Its check runs and the result is recorded. A path is closed when its check
|
||||
passes. Otherwise it is accepted under Q182 and Q183, and its wording goes into the R8 record and to
|
||||
Melanie through Jason (#419). Every check runs from the factory
|
||||
host, as root through `sudo -n` where the factory has it, and passes only when the call fails.
|
||||
|
||||
| Path | Closed: the check | Accepted: the wording | Hub-local option |
|
||||
|---|---|---|---|
|
||||
| Host access | The R8 sudo, doas, ssh and key checks (section 7, N13) | "Factory agents can log in to the hub host." | none needed, the checks can pass today |
|
||||
| Hypervisor | O1 or O2 above: no factory key in any PVE node's root `authorized_keys`, and a PVE API call on the hub VM fails with each token the factory holds | "Factory agents have root on the machine the hub runs on and can read its memory and disk." | O1, hardware outside PVE |
|
||||
| Backups | The hub's backups are encrypted with a key only Jason or Melanie holds: the PBS datastore shows the hub's snapshots encrypted, and the key's fingerprint matches no file on the factory host | "Factory agents can read the hub's backups in full." | client-side encrypted backups, key held by Jason or Melanie |
|
||||
| Sign-in provider (B8, mos-1006) | The hub's `console.sso` trusts only an issuer no factory credential administers, never a brand, tenant or application in the factory's Authentik. An admin API call on it fails with each identity provider token found on the factory host, and the issuer's list of admin tokens and service accounts (names only, never values) shows none held on the factory. An issuer that isn't on the hub host passes the host-access, hypervisor and backups rows itself, run against its own host, and a hosted issuer's admin accounts each pass the failed-call check (duckworth-0007) | "Anyone with admin on the factory's sign-in provider can sign in to the hub as any user, Melanie included." | an issuer on the hub host, such as Pocket ID (Q175 supports several providers) |
|
||||
| TLS front end (B8) | TLS for the hub's origin ends on the hub host: the certificate the origin serves, fetched both through the public route and from the LAN (N19), matches the key on the hub host, and no factory proxy, tunnel or overlay route names the hub's origin | "The factory's proxy sees every hub sign-in, cookie and message in clear." | TLS ending on the hub host |
|
||||
| Certificate authority (mos-1009) | The hub's certificate comes from a public CA through an ACME account whose key is only on the hub host, and the zone's CAA record names only that CA | "The factory can get a valid certificate for the hub's name." | ACME run on the hub host |
|
||||
| DNS (N15) | A zone edit for the hub's name fails with each DNS credential found on the factory host, and the zone's owner account is one the factory can't sign in to | "The factory can point the hub's name at a host it runs." | a zone or registrar account only Jason or Melanie holds |
|
||||
| Network | From the factory host, the hub host answers only on the ports R8 needs (HTTPS, and ssh where it is used), and nothing else is open to the LAN | "The hub host sits on the same flat LAN as the factory, and only its logins protect it." | a host firewall on the hub, a separate VLAN |
|
||||
|
||||
Some paths depend on others. While the hypervisor row is open, every secret on a PVE guest is exposed to
|
||||
node root: the TLS key, the ACME account key, an issuer's database and signing key on a guest, and the
|
||||
backup key whenever a restore loads it (N16). So closing those rows doesn't help until the hypervisor
|
||||
row is closed too, or they sit on hardware outside PVE. The R8 record names every path that stays accepted.
|
||||
|
||||
## 9. Rulings (mos-0978, mos-0984, mos-0991, mos-0996, mos-1004, mos-1006, mos-1009, mos-1013)
|
||||
|
||||
1. Q1: human cross-access is the 10-27 slice (mos-0978). Duckworth's N8 put agent-to-agent's timing to
|
||||
Jason through Mos. Jason answered "P2P federation is acceptable for 10-27" (mos-0991), and then "P2P is
|
||||
person-to-person in this case" (DECISIONS Q180, mos-1013). The slice is human access only, with no
|
||||
agent-to-agent rows.
|
||||
2. Q2, settled by row 193 (morgana-0002, to Sage too): access.json is the one user and grant store for
|
||||
humans. Row 193 adds `home: "local"` users, uses the grant level as the human role, and keeps viewer and
|
||||
collaborator. The owner stays outside the file. Thread shares go in a new append-only bus table,
|
||||
`thread_shares`, not in access.json, and a share counts only while the named user's live grant covers
|
||||
the thread's project and instance. The 10-27 slice ships collaborator only (W2), so when row 193 adds
|
||||
viewer, R1's level check widens with it. `thread_shares` is append-only, so an unshare is an end row in
|
||||
row 193's design. R3 assumes nothing about shares (N12).
|
||||
3. Q3 and B5: R8 never runs on a single uid (mos-0984 point 2). For 10-27 its isolation is the hub's own host,
|
||||
.54 (Q181), the host form only (mos-0996). The uid form (row 190b) is a later option. Jason accepted
|
||||
the factory paths, the hypervisor's included (Q182, Q183), so R8 records them (section 8).
|
||||
|
||||
## 10. Notes
|
||||
|
||||
- The r1 push used the repo-local credential helper `scripts/git-credential-mosaic.mjs` (set in the
|
||||
canonical checkout's .git/config for git.mosaicstack.dev) with its default credential file,
|
||||
`~/secrets/mosaic.gitea.json`, and no `MOSAIC_GITEA_CREDENTIAL_FILE` override. That file's user is
|
||||
jason.woltje. The r2 push uses the same. Gizmoduck has no seat identity until T1 row 195 adds per-agent
|
||||
git identity.
|
||||
@@ -0,0 +1,310 @@
|
||||
# Row 234 (#436a row a, Gitea #1740): host enrollment, store records and bus schema 3i to 3j
|
||||
|
||||
Gizmoduck, 2026-10-21. Reviewer Duckworth, gate owner Sage. Lands by 10-25, before the .53 and .54 installs
|
||||
(mos-1134).
|
||||
|
||||
- Brief: `docs/plans/2026-10-20_host-credentials.md`, section "Machine grant row a: enrollment, store records
|
||||
and migration 3i-3j", blob 64451be5 (queue rev 2617).
|
||||
- Design: `agents/gizmoduck/work/machine-grant/DESIGN.md` r6, f9039682e on `gizmoduck/machine-grant-design`,
|
||||
approved by Duckworth (duckworth-0064, REVIEW-r6.md at 8ea792256). Not reopened here. Cites "r6:" are lines
|
||||
of that file.
|
||||
- Grant event rule: #436b section 7, fa5658941 on `megavolt/row199`,
|
||||
`agents/megavolt/work/436b/DESIGN.md:431-490`, file sha256
|
||||
96510b83778e79a71b33d0d5f6fd0c806dfe74f6de315ff4a9f1fbe5620f6c69, approved by Duckworth (duckworth-0067).
|
||||
- Base: origin/refactor 8fede5fc6, with rows 199 (cb5fdb064) and 210 (f5753c0e8) landed. Branch
|
||||
`gizmoduck/row234`. Code cites are `path:line` at 8fede5fc6 unless they say otherwise.
|
||||
|
||||
## Names: r6 against section 7
|
||||
|
||||
r6's `grant.*` and `host.enroll.*` names are `body.action` values on `action.allowed` events, not event
|
||||
kinds. The `events` kind CHECK isn't rebuilt. Four r6 names move under `grant.`:
|
||||
|
||||
| r6 | This row |
|
||||
|---|---|
|
||||
| `host.enroll.step` | `grant.host.enroll.step` |
|
||||
| `host.enrolled` | `grant.host.enrolled` |
|
||||
| `host.checked` | `grant.host.checked` |
|
||||
| `host.removed` | `grant.host.removed` |
|
||||
|
||||
Only the consume record carries `decision`: `{operation: 'grant.host-enroll', decision}`. Every other
|
||||
guarded body names the decision `grantDecision`, so r6's `decision` field in step, enrolled and failed bodies
|
||||
is `grantDecision` here. The actor of every guarded event is `actor_role` NULL with `actor_run` 'grant',
|
||||
and `body.by` names the caller the broker checked.
|
||||
|
||||
## Split (r6 section 12 Q-S4)
|
||||
|
||||
Row a builds:
|
||||
|
||||
1. **Schema 3i to 3j.** `migrations/3i-3j.sql` adds `hosts` and `host_grants` (r6 S1, primary keys
|
||||
(business, host) and (business, agent, host)) and the guard trigger: an `action.allowed` whose
|
||||
`body.action` or `body.operation` GLOBs `'grant.*'` aborts unless the actor is (NULL, 'grant').
|
||||
`SCHEMA_VERSION` 3j, a migration test with a step that fails partway.
|
||||
2. **Grant event writer.** A private session-less broker writer for guarded events. `#event` doesn't change.
|
||||
`recordEvent` refuses `reserved-event` for the guarded namespace.
|
||||
3. **The `grant` raise frame.** `decision.raise` takes `grant` only with `credential.mint`, only from the chief
|
||||
(`grant-chief-only`), and in row a only with `method: 'host-enroll'`. Any other method refuses
|
||||
`invalid-request` until row b adds `host`. E1's checks, the `ssh-keyscan` set compare before the raise
|
||||
transaction, and the rendered block.
|
||||
4. **Enrollment executor.** `#consumeGrant`, the start on the human's cli yes (X1), E2 steps 1 to 4, the
|
||||
installer key at `<configDir>/secrets/hosts/<business>/<hostId>/installer`, the failure cleanup, and the
|
||||
start reconcile for enrollments: the `registry` step undo, and a resolved-yes unspent enrollment ending
|
||||
`grant.failed interrupted` (X3).
|
||||
5. **`hosts.check` and `hosts.remove`** (E4), with `mosaic hosts status|check|remove`.
|
||||
6. **`tools/host/`**: `mosaic-keys`, `mosaic-keys-write`, `mosaic-keys.sudoers`, `mosaic-keys.sshd.conf` (E3).
|
||||
7. **`host.run` on the store** (X4 first and third bullets): it reads the `hosts` and `host_grants` records
|
||||
and the Approve record check, and at start the runtime gives `Credentials` a reference per `host_grants`
|
||||
record. The loader refuses `hosts` at the top level and in a role (S1). Row 210's tests move to store
|
||||
fixtures.
|
||||
8. **Reader filter** (duckworth-0067 N1): every grant-evidence reader in this row (status, the start
|
||||
reconcile, `openGrantDecisions`) filters `kind = 'action.allowed' AND actor_role IS NULL AND actor_run =
|
||||
'grant'`, with a test that an `action.refused` look-alike and a session-written one are ignored.
|
||||
`hosts.remove`'s `hostLineLeft` reader is one of them (duckworth-0079 M4).
|
||||
|
||||
Row b keeps the `host` method's raise binding (R4), the grant executor (X2), the grant rows of X3, X5,
|
||||
`Credentials.register`/`claim`/`abortClaim`, revoke (section 6) and `sweep`. So row a has no way to make a
|
||||
`host_grants` record outside a test fixture, and from its landing to row b's no agent holds a host grant
|
||||
(r6 N9, the run sheet strips row 210's keys).
|
||||
|
||||
Tests in row a (r6 section 10): G1 and G5 for the `host-enroll` raise, G6 and G10 for the enrollment
|
||||
executor, G11's enrollment cases, G14, G15, G16, G17, G18, G20, G21 (its `host-line-left` case reads an
|
||||
event fixture until row b writes `hostLineLeft`), G25, the migration test, the trigger cases, the reader
|
||||
filter test, and row 210's T-tests on store fixtures. Row b: G2 to G4, G7 to G9, G11's grant cases, G12,
|
||||
G13, G19, G22 to G24, G26 to G28.
|
||||
|
||||
## Early check items (duckworth-0079, REVIEW-build-split.md 10672772e on `duckworth/reviews`)
|
||||
|
||||
Moves into row a:
|
||||
- M1. G4 for the `host-enroll` block (r6:240-242): a question can't forge the block, and the 4096-character
|
||||
limit holds with the block appended. G4 for the `host` block stays in row b.
|
||||
- M2. The tag function (r6:265-266), because E2 step 2 tags the installer line. G24's first sentence moves
|
||||
here: names of 64 characters with `.` and `_` give distinct tags of the fixed length. The `sweep` half
|
||||
stays in row b.
|
||||
- M3. A store-fixture test that `host.run` refuses `host-not-granted` for no record, an unresolved decision,
|
||||
a decision resolved by an arbiter, the tester or an agent or not via cli, a consume record with operation
|
||||
`grant.host-enroll` in place of `grant.host`, and an unspent decision (X4 first bullet, r6:593-596). A
|
||||
restart test that a `host_grants` fixture gives `Credentials` a reference (X4 third bullet).
|
||||
|
||||
G21's `host-line-left` case:
|
||||
- F1. The fixture goes through this row's private grant writer in the real shape: `action.allowed`, actor
|
||||
(NULL, 'grant'), `body.action` 'grant.failed', `body.host`, `body.hostLineLeft`. No raw SQL around the
|
||||
trigger.
|
||||
- F2. `hosts.remove` reads `body.hostLineLeft` from any guarded body, not only `grant.failed`.
|
||||
- F3. Decided: a guarded body that carries `hostLineLeft` also carries `host`. Row b adds `host` to
|
||||
`grant.revoke.closed`, so its body is `{tag, host, hostLineLeft}` (r6:716 has `{tag, hostLineLeft}`).
|
||||
The reader matches on `body.host`, and doesn't map tags back to hosts through `grant.step`. A direct
|
||||
field is one equality in SQL, and a tag lookup would add a second reader that the N1 filter also has to
|
||||
cover.
|
||||
- F4. Row b carry: "no later `sweep` cleared it" (r6:403) needs `grant.sweep`, which only row b writes. Row
|
||||
b adds the sweep clear to `hosts.remove` with a G21 case, and its G8 or G13 shows `hosts.remove`
|
||||
refusing after a real `hostLineLeft` write, not only after the fixture. Until then row a's check is "any
|
||||
guarded `hostLineLeft` for this host refuses".
|
||||
|
||||
## Mutants (r6:838-883, numbered here)
|
||||
|
||||
Row a, killed on this row's tree:
|
||||
|
||||
| # | Mutant |
|
||||
|---|---|
|
||||
| A1 | drop the chief check |
|
||||
| A2 | start the executor on a `credential.mint` with no `grant` |
|
||||
| A3 | start it on an arbiter resolve |
|
||||
| A3b | start it on a `cli` yes by someone other than the human (duckworth-0083 N8) |
|
||||
| A4 | start it on `no` |
|
||||
| A5 | skip the keyscan compare |
|
||||
| A6 | compare only one key |
|
||||
| A7 | write the private key into the event body |
|
||||
| A8 | drop the "not yet enrolled" check in E2 step 1 |
|
||||
| A9 | drop the `hosts` primary key |
|
||||
| A10 | skip the start check for a resolved, unspent enrollment decision |
|
||||
| A11 | `host.run` reads the business file instead of the grant record |
|
||||
| A12 | build the tag without the business |
|
||||
| A13 | drop `from=` in the writer |
|
||||
| A14 | let the writer touch a line outside the tag pattern |
|
||||
| A15 | drop `O_NOFOLLOW` on the user file |
|
||||
| A16 | drop one group from the writer's refused list |
|
||||
| A17 | drop the user pattern check before the path use |
|
||||
| A18 | treat a `privileged` entry as enough without `users` |
|
||||
| A19 | let the writer accept a sudo user not in `privileged` |
|
||||
| A20 | trigger: drop the `body.operation` arm, so the consume record passes from a session |
|
||||
| A21 | trigger: accept actor_role NULL with any actor_run |
|
||||
| A22 | a grant reader drops the `kind = 'action.allowed'` or actor filter. The runner splits it: A22a drops the kind, and G21's `action.refused` look-alikes kill it. A22b drops the actor, which the trigger can't stand in for, because 3i-3j adds the trigger without checking old rows. Its killer is the grant-migration test "A22b, sage-1703": a session-written `grant.host.removed` with `hostLineLeft` and a `grant.host-enroll` row from 3i, migrated to 3j, where `hosts.remove` still removes the host and `reconcileGrants` ends nothing |
|
||||
| A23 | `host.run` skips the Approve record check (any `host_grants` record is enough) |
|
||||
|
||||
Row b keeps the rest of r6's list: the `ready` check, the three hash compares, the definition check, the
|
||||
install and record order, `remove` on a verify failure, both start sweeps, the reconcile's remote `remove`,
|
||||
the `grant.step` before install, revoke leaving the record, `register` before `issue`, the abort check and
|
||||
the `await` before `issue`, revoke and an unreferenced claim, `sweep` and any `mosaic-` line, revoke's host
|
||||
step order, claim before consume, the resolved unspent grant decision, an async `commit`, `hosts.revoke`
|
||||
through `credential-verbs.mjs`, the revoke marker reconcile, the upsert, the undo past `grant.revoked`, the
|
||||
abort after step 5 and a missing `bus.tester` as dev.
|
||||
|
||||
## Broker functions this row touches (sage-1677)
|
||||
|
||||
For row 236 (Megavolt), which rebuilds on this landing. Old lines are `packages/bus/src/broker.mjs` at
|
||||
8fede5fc6, new lines are at the last commit that changed this file (they moved by 10 after `runEnrollment`
|
||||
went in). The planned list is kept in this file's history (827b89ea6).
|
||||
|
||||
Changed:
|
||||
- `constructor` (:279): one more option, `hostGrant`, the object `host-grant.mjs` builds.
|
||||
- `recordEvent` (old :1243-1251, new :1606): one more refusal. An `action.allowed` whose `body.action` or
|
||||
`body.operation` is a `grant.*` name refuses `reserved-event`.
|
||||
- `request()` (old :1362, new :1726): two routes ahead of the sync path, a `decision.raise` with `grant` to
|
||||
`#raiseGrant` and the `hosts.*` verbs to `#requestHosts`. Both are async.
|
||||
- `requestHost` (old :1712-1795, new :2079): the business-file lookups (old :1730-1736) become one
|
||||
`#hostAccess` call, in the same transaction as `#agent`. `commands` and `entry` come from it.
|
||||
- `#dispatch` (old :1796, new :2160) gets a fifth parameter, `enroll`, which only `#raiseGrant` passes.
|
||||
- `decision.raise` (new :2244): `grant` joins the allowed keys, and `grant` without `enroll` (or the
|
||||
reverse) refuses `invalid-request`. The `host.sudo` block reads `#hostAccess` in place of `b.hosts`
|
||||
and the role's `hosts`, and an undefined `target` still refuses `host-unknown`. `validArgv` and
|
||||
`validStdin` now run after the access check, so a malformed argv for an ungranted host refuses
|
||||
`host-not-granted` first. The enrollment block is appended to the question before `string(question)`,
|
||||
so the 4096 cap counts it (M1). The decision's raise body carries `grant: enroll`.
|
||||
- `decision.resolve` (new :2409): when `op` is `resolved`, `#approvedGrant` reads the decision, and a
|
||||
`host-enroll` grant starts `#runEnrollment` from `setImmediate`, after the transaction commits. The
|
||||
result adds `grant: 'started'`.
|
||||
|
||||
New, all private but two:
|
||||
- `#grantEvent` (:657), the writer of guarded events. It runs the secret check, then `writeGrantEvent`.
|
||||
- `#approvedGrant` (:667), `#consumeGrant` (:678). `#consumeGrant` sits after `#approvedGrant`, not next
|
||||
to `#consumeAuthority` as planned.
|
||||
- `#hostRecord` (:685), `#hostAccess` (:692), `#grantNotice` (:708), `#enrollFailed` (:736),
|
||||
`#runEnrollment` (:750), `#openGrantDecisions` (:823).
|
||||
- `runEnrollment` (:745), public, a trusted entry to `#runEnrollment` for G10. No verb reaches it.
|
||||
- `reconcileGrants` (:833), public, which `runtime.mjs` calls once at start.
|
||||
- `#raiseGrant` (:866), `#hostsCaller` (:886), `#requestHosts` (:900), `#hostLinesLeft` (:964).
|
||||
|
||||
Module level, new (:184-230): `GRANT_RUN`, `grantName`, `GRANT_EVIDENCE` (the N1 filter as SQL),
|
||||
`registrySha256`, `writeGrantEvent` (exported, the one writer of guarded events, which test fixtures use
|
||||
for F1), `HOSTS_VERBS`, `fingerprintOf`, `INSTALLER_COMMENT`.
|
||||
|
||||
Unchanged: `#event`, `#spent`, `#consumeAuthority`.
|
||||
|
||||
Other `src` files:
|
||||
- `bus/src/host-grant.mjs`, new: the keyscan, the installer key pair, and `hosts.check`'s `list`.
|
||||
- `bus/src/runtime.mjs`: builds `hostGrant`, calls `reconcileGrants` before the server starts, and adds
|
||||
the `host_grants` references before `new Credentials` (X4 third bullet).
|
||||
- `bus/src/host-run.mjs`: `runHost` takes `command`, sent as is in place of the sudo argv, for the
|
||||
installer's `list`.
|
||||
- `bus/src/server.mjs`: the `hosts.*` verbs and a raise with `grant` go down the async path.
|
||||
- `bus/src/store.mjs`: the 3i-3j step, `SCHEMA_VERSION` 3j. `bus/src/credentials.mjs` exports
|
||||
`privateDir`.
|
||||
- `business/src/business.mjs` and `business/src/hosts.mjs`: the loader refuses `hosts` (S1).
|
||||
`checkHosts` and `checkHostGrants` are gone.
|
||||
- `cli/src/hosts.mjs`, new, routed from `cli.mjs` and `scripts/mosaic`.
|
||||
|
||||
## Decisions taken in the build
|
||||
|
||||
- **`#hostAccess` binds the agent and the host.** X4 says the Approve record check passes when the
|
||||
decision is an approved `host` grant with a `grant.host` consume. The check also requires the grant's
|
||||
`agent` to equal the session's role and its `host` to equal the requested host. Without that, an
|
||||
approved grant for one agent and host would vouch for any `host_grants` row that names its decision.
|
||||
Cost: none for row b, whose executor writes the row from the same grant.
|
||||
- **Store fixtures are written the real way.** `bus/tests/host-fixtures.mjs` inserts the decision rows by
|
||||
SQL, but every guarded event goes through `writeGrantEvent`, so the trigger sees it (F1). An enrollment
|
||||
fixture writes `grant.host.enrolled`, or the start reconcile reads it as interrupted.
|
||||
- **The resolver test for host kinds is gone.** `business/tests/hosts.test.mjs` lost its D5 case, because
|
||||
the loader now refuses `host.<id>` credential refs. The `isHostKind` filter in `business/src/resolve.mjs`
|
||||
can't be reached now. I left it in, since row b may want it back, and it's one line.
|
||||
- **Store before Credentials.** The runtime already opened the store before `Credentials`. The
|
||||
`host_grants` references depend on that order, and the comment there says so.
|
||||
- **CLI.** `mosaic hosts` prints tab lines, and `--json` prints the bus rows. Usage errors exit 4 and bus
|
||||
refusals exit 2, as `mosaic credentials` does. There is no enroll command: enrollment is the chief's raise.
|
||||
- **Host tools tests live in the bus suite.** `bus/tests/host-tools.test.mjs` loads the writer by path
|
||||
through a Python harness under `python3 -B -I`. The harness passes its own key dir, config dir, owner
|
||||
uid and user, group and sudo lookups, and runs `mosaic-keys` from a copy with stub `sudo` and `logger`.
|
||||
The gate runs the bus suite, so G16 and G25 run there. The real `pwd`, `grp` and `sudo -l` paths are
|
||||
checked on the #433 VM, not here. The test also runs `visudo -c` on the sudoers file and `sshd -T` on
|
||||
the drop-in.
|
||||
- **Writer details the design left open.** The `from` file must hold exactly one value matching
|
||||
`[0-9A-Za-z.:/*?!_-]+(,[0-9A-Za-z.:/*?!_-]+)*`, so it can't close the quote. `users`, `privileged` and `from` must be
|
||||
regular, owned by the owner uid and not group or world writable, and are opened without following a
|
||||
symlink. Lines starting with `#` are comments. A refusal exits 1 with its code on stderr and in syslog.
|
||||
`add` of a tag that is already there exits 0 and writes nothing.
|
||||
- **`runEnrollment` is public for G10 (duckworth-0081 B1).** G10 needs a second start for a spent
|
||||
decision, and the resolve path can't give one: a second resolve refuses `decision-closed`
|
||||
(broker.mjs:2416) before the executor runs. So
|
||||
`#runEnrollment` returns its code (`enrolled`, `host-enrolled` or the refusal), and a public
|
||||
`runEnrollment` calls it, as `reconcileGrants` is public. The test checks a spent
|
||||
decision gives `decision-consumed`, and an open or agent-resolved one gives `not-authorized`, with
|
||||
nothing written. That second case also reaches the `via` and `by` checks in `#approvedGrant`, so A3 is
|
||||
no longer vacuous (N1).
|
||||
Who can call it (sage-1692): only code holding the `Broker` object, which is the bus process and the
|
||||
tests. The one `new Broker` in `src` is `runtime.mjs:126`, and no `src` file calls `runEnrollment`;
|
||||
the resolve path calls the private `#runEnrollment` from `setImmediate` (broker.mjs:2445). A seat
|
||||
holds no `Broker`. It reaches the bus only over `broker.sock`, where `server.mjs` calls a fixed set of
|
||||
methods by name (`request` at :134 and :140, `identity`, `disconnect`). `request()` routes on the verb
|
||||
through the `#dispatch` switch, and neither file looks a method up by name (no `this[` in either), so no
|
||||
verb string can reach `runEnrollment`.
|
||||
- **A `sudo -l` that times out refuses `user-sudo` (N2).** `_sudo` returns an empty answer on
|
||||
`TimeoutExpired`, which isn't the expected line, so `main` logs the refusal and exits 1.
|
||||
- **Keyscan comment (N3).** A scan that can't start refuses `host-failed`. One that runs and finds no key
|
||||
returns no lines, and the raise refuses `host-key-mismatch`. The comment in `host-grant.mjs` now says so.
|
||||
- **No tag on in-run `grant.failed` (N4).** r6:578 tags only the `interrupted` one, so the code follows it.
|
||||
- **Run sheet (N5).** The writer refuses `config-privileged` when the file is missing, so each host needs an
|
||||
empty, root-owned `/etc/mosaic-keys/privileged` (0644) before the first enrollment.
|
||||
|
||||
## Mutant results (Sage's slot, 2026-10-21)
|
||||
|
||||
All 25 killed: A1 to A23, with A3b added for duckworth-0083 N8 and A22 split into A22a and A22b. The
|
||||
runner is `mutants.py` outside the repo. Each mutant gets its own session, a 420 s limit with a group kill,
|
||||
and a byte restore checked against git status. None hung. Run 1 (04:24Z, on 95f966b1c) killed A1 to A22a
|
||||
and stopped at A22b, which survived. Run 2 (04:43Z, on 097892741) killed A22b and A23.
|
||||
|
||||
- Each kill comes from a named assertion in the mutant's own test files, except A9. Dropping the `hosts`
|
||||
primary key leaves `host_grants`' foreign key with no parent key, so the store refuses to open
|
||||
("foreign key mismatch") and 28 of 29 tests fail at setup. That's a structural kill, not the S1 test.
|
||||
The schema can't exist without the key (sage-1709).
|
||||
- A22b drops the actor filter from `GRANT_EVIDENCE`. The trigger can't stand in for it, because a 3i store
|
||||
can hold a session-written grant row that the 3i-3j step keeps. The grant-migration test "A22b,
|
||||
sage-1703" kills it (see the A22 row above).
|
||||
|
||||
Follow-up, not in row a: G18's control calls `runHost` directly, so nothing runs `hg.check` end to end
|
||||
with a good pin (duckworth-0083 N9). The #433 VM run covers it.
|
||||
|
||||
## Status at 097892741
|
||||
|
||||
Done: every Split item, 1 to 8, duckworth-0081 B1 to B3, duckworth-0083 N8, and all 25 mutants killed.
|
||||
|
||||
Single files in the slot, all exit 0, 178 pass and 0 fail by an awk sum over 23 logs: bus grant-enroll
|
||||
24, host-run 24, grant-migration 6, host-tools 6, account-migration 3, chief-of-staff 18,
|
||||
comment-migration 5, hard-waits 13, name-migration 1, project-runtime 3, spec-migration 1,
|
||||
thread-migration 3, usage-migration 1, project-files 8, spec-socket 6, spec-review 12, land 17, spec 4;
|
||||
harness spec-tools 2, raise-domain 2; business hosts 4; cli hosts 5; seat sandbox 10.
|
||||
|
||||
Left: Sage runs the whole bus, business and cli suites and every `scripts/test-*.sh` at the landing gate,
|
||||
after Duckworth approves.
|
||||
|
||||
## Round 3 (sage-1742): two tests that count the schema
|
||||
|
||||
The landing gate failed `packages/bus/tests/schema.test.mjs:522`. It counts the tables and triggers that
|
||||
`schema.sql` creates, and row a adds two tables and a trigger, so the counts go from 16 to 18 tables and
|
||||
from 70 to 71 triggers. My singles didn't run it because I took it for a test of other rows' tables.
|
||||
|
||||
I grepped `packages/*/tests/*` for `sqlite_master` and `schema.sql`, with `schema.test.mjs:517` as the
|
||||
positive control. That turned up one more test that would fail: T46 in
|
||||
`packages/cli/tests/stack-release.test.mjs:910`. It puts every schema table in exactly one of four lists, and
|
||||
hosts and host_grants were in none. I'd left the stack-release tests out of the r2 singles on the claim that
|
||||
they don't load the bus, but T46 reads `schema.sql`. Both tables go in its untimed list (Sage's ruling,
|
||||
sage-1745: the message_threads pattern). Neither has an `at` column. Row a writes hosts at five lines,
|
||||
and each runs in one store transaction with a `grant.*` events row, which the restore guard counts.
|
||||
`#grantEvent` (broker.mjs:657) writes that row through `writeGrantEvent` (:201, INSERT INTO events at
|
||||
:212).
|
||||
|
||||
- :737 DELETE, in `#enrollFailed`, with grant.failed at :738. Both callers wrap it in a transaction,
|
||||
:816 and :849.
|
||||
- :763 INSERT, in the step 1 transaction (:754), with grant.host.enroll.step at :761.
|
||||
- :783 UPDATE installer_fingerprint, in the step 4 transaction (:780), with grant.host.enrolled at :784.
|
||||
- :932 DELETE, in the hosts.remove transaction (:927), with grant.host.removed at :933.
|
||||
- :950 UPDATE state ready, in the hosts.check transaction (:944), with grant.host.checked at :951.
|
||||
|
||||
Row a writes no host_grants rows. It only reads them (broker.mjs:695, :930, runtime.mjs:103). Row b adds
|
||||
the writes, and each must share a transaction with a `grant.*` event the same way. That goes on row
|
||||
235's carry list. The other hits pin an older schema
|
||||
(account, comment, name, spec, thread and usage migrations, chief-of-staff 3c) or count only their own
|
||||
tables (state.test.mjs:106 reads every table but pins no list, threads.test.mjs:129).
|
||||
|
||||
hosts and host_grants are mutable by design, so the refused-labels loop above `schema.test.mjs:517` gets no
|
||||
entry for them. hosts.check updates a host to ready. hosts.remove and a failed enrollment delete the
|
||||
record. Row b updates and deletes grant records. Neither table has append-only triggers. The one new
|
||||
trigger, events_grant_actor, guards events. grant-migration.test.mjs tests it, and it killed mutants
|
||||
A20 and A21.
|
||||
@@ -0,0 +1,11 @@
|
||||
# Gosalyn
|
||||
|
||||
Worker seat for Mosaic Stack, MVP track T2 (Case 2, the creative writing hub, Vikunja #416).
|
||||
|
||||
- T3 thread: 7604438a-0ded-41c8-bd3f-26f57fdb5cd2
|
||||
- Model: Opus 5.5 (Claude Code, T3)
|
||||
- Launched by Mos (Jason's main agent, T3 38bc4c81-6270-4021-b00c-6a15c43b9b0e), who briefs the seat
|
||||
- Repository lead and queue: Sage (T3 1ef1e4f8-3ead-4208-beca-38f9f1add079)
|
||||
- Branch: gosalyn/case2-design, pushed only by this seat, landed on refactor by Sage
|
||||
|
||||
Work lives in `work/`. The Case 2 design is `work/case2/DESIGN.md`.
|
||||
@@ -0,0 +1,104 @@
|
||||
# Row 230: C2-13a unit render and host precheck
|
||||
|
||||
Gitea #1732. Vikunja #416 (track T2). Brief: docs/plans/2026-10-20_case2-rows.md, section "C2-13a",
|
||||
blob f6b4933f0f47 (01b40d357, sage-1562). Base: 00d4cc1f6 (row 190). Reviewer: Webby. Gate: the cli
|
||||
suite plus every scripts/test-*.sh.
|
||||
|
||||
## What ships
|
||||
|
||||
`scripts/case2-unit.sh` renders `[email protected]` and `mosaic-webui.service` from the two existing
|
||||
templates into the system unit directory, for the default instance only. It writes each unit through
|
||||
a temp file at mode 0644, runs `daemon-reload`, enables both units, starts the bus and then the
|
||||
Console. If the bus fails to start, the script refuses and leaves the Console unstarted. Both units
|
||||
get `WantedBy=multi-user.target`, and neither gets `After=` or `Wants=`. The script never links a unit
|
||||
file into place. A re-install refuses unless each installed unit passes the same checks, both units
|
||||
name one copy, and each equals the render apart from the copy id.
|
||||
|
||||
`--profile library` (the default) refuses `--pm`, `--claude`, `--tester-port` and `--sso`, and needs
|
||||
the preview proxy flags plus the proof pair. `--profile factory` refuses the six proxy flags and takes
|
||||
`--pm` and `--claude`. Both refuse `--checkout` and `--board`.
|
||||
|
||||
`scripts/case2-precheck.sh --business ID [--profile factory|library]` runs as `<user>`. It prints one
|
||||
`ok` or `REFUSED` line per check and exits 1 if any check refused. It prints variable names only,
|
||||
never values.
|
||||
|
||||
`packages/cli/README.md:713` now says nothing creates the session env file, only the Case 2 precheck
|
||||
reads it, and the precheck prints only variable names (webby-0034 N2, webby-0035).
|
||||
|
||||
## Deviations for review
|
||||
|
||||
Sage accepted all six for the build (sage-1570). Webby rules on each.
|
||||
|
||||
1. The precheck takes `--business ID`. The brief names no other way to find the business file.
|
||||
2. A run root `<dataRoot>/launches/<business>` that doesn't exist passes, with a note that the first
|
||||
launch creates it at 0700 (session.mjs:47 and newRun). A symlink refuses, and so does an existing
|
||||
entry that isn't a 0700 directory.
|
||||
3. The claude marker check passes when the reply holds the marker and input plus cache tokens are at
|
||||
most 2000. **The 2000 ceiling is unmeasured.** The tests use fakes at 40 and 9000 tokens. Measuring
|
||||
it on a real claude belongs to row 195's install run, not this row.
|
||||
4. A non-root test can't create a root-owned proof file. For the "proof not owned by the user" case,
|
||||
the fake getent reports another uid for the user, so the user's own file reads as someone else's.
|
||||
5. The unit script refuses `%h` left in any unit, rendered or installed. A system unit expands `%h`
|
||||
to root's home. The render replaces every `%h`, so the test edits an installed unit to reach it.
|
||||
6. With `--auth-header X-Authentik-Uid`, the allowed user must be 64 lower-case hex characters (lead
|
||||
decision 101). The test covers both the refusal and the pass.
|
||||
|
||||
## Tests (packages/cli/tests/case2-unit.test.mjs, 30 cases after round 2)
|
||||
|
||||
The tests use a scratch unit directory and a scratch home, plus fake systemctl, getent, sudo, id,
|
||||
node, bwrap and claude. They never touch /etc/systemd, ~/.mosaic-dev, ~/.config/mosaic-dev or a
|
||||
live unit. Every precheck case asserts its output doesn't hold the fixture key.
|
||||
|
||||
- Unit: the clean library render, PATH for a node outside the default list and for /usr/bin/node, the
|
||||
systemctl order, a failed bus start, no `systemctl link`, factory `--pm` and `--claude` rendered and
|
||||
absent, re-install to another copy, edits to an installed unit, template restrictions,
|
||||
EnvironmentFile placement, unknown tokens, PermissionsStartOnly and `+`/`-+` Exec prefixes,
|
||||
symlinked unit paths, bad `--node`, re-install drift, a split copy pair, bad copy ids, every library
|
||||
and factory flag refusal, and bad `--claude`.
|
||||
- Precheck: a good host, node older than 24, more than one group, sudo succeeding, XDG_DATA_HOME,
|
||||
HOME mismatch, a run root mode, a bad config, bwrap failing, the port floor at or below the Console
|
||||
port, a missing Console unit, bus.tester per profile, profile-library-business, the claude check
|
||||
(no route, replaces, appends, missing, the unit's MOSAIC_CLAUDE_BIN beating PATH), and the session
|
||||
env file (missing, 0644, symlink, empty, emptied by a later line, quoted).
|
||||
- Bind (mos-1100): inside `binds()` plus `sandboxArgv()`, a bind on port 980 gets EACCES, and a bind
|
||||
on a free port of 1024 or above succeeds. The test fails if the host's floor is at or below 980.
|
||||
|
||||
## Verification
|
||||
|
||||
Round 1 singles, run before the gate: case2-unit.test.mjs passes 28/28 with no skips, and host.test.mjs (it
|
||||
reads the cli README) passes 42/42. No test pins a hash of the README. No other test reads the two
|
||||
scripts.
|
||||
|
||||
## Round 2 (webby-0046, sage-1589, sage-1590, sage-1591)
|
||||
|
||||
Base: origin/refactor (rebased from 00d4cc1f6; the 15 commits between touch none of the five paths
|
||||
or the two unit templates). Branch gosalyn/c2-13a-r2.
|
||||
|
||||
B1. The precheck now has a case for each refusal Webby named: the user missing from passwd (:77),
|
||||
a business file that is unparsable, missing or fails the schema (:137), a run root that is a symlink
|
||||
to a 0700 directory (:151), a Console ExecStart with no --port (:179), an unreadable port floor
|
||||
(:180), a claude reply without the marker (:223) and a claude reply that isn't JSON (:224). The fake
|
||||
claude gained `nomarker` and `text` kinds. The no-claude-code case and the missing-claude case share
|
||||
a PATH without `/usr/bin/claude` (`pathWithoutClaude`), so the no-claude-code case now shows a host
|
||||
with no claude passes.
|
||||
|
||||
N1 (ruled in for this round). Before writing, `case2-unit.sh` asks `systemctl is-active` for both
|
||||
units. For each unit that was running, it prints that the unit is still on its old copy, never
|
||||
"started", and then that the bus and Console must restart together to reach the new copy. A unit
|
||||
that wasn't running gets "started ... on copy". A fresh install prints the round 1 line. The script
|
||||
still restarts nothing. The new test covers both units running and only the Console running.
|
||||
|
||||
N4. The README line at :713 is rewrapped.
|
||||
|
||||
Mutants, run in Sage's slot on a scratch worktree, one branch each: :77, :137, :151, :179, :180,
|
||||
:223, :224, a precheck that refuses with no claude on PATH (for the no-claude-code case), and an
|
||||
install that prints "started" for a running unit (sage-1591).
|
||||
|
||||
## Carry items for row 195
|
||||
|
||||
- N1's restart. Row 195 restarts the bus and Console together after a re-install over running
|
||||
units, as the install line now asks.
|
||||
- N2. A system drop-in check like `bus-service.sh:122-145`, so a drop-in under
|
||||
`[email protected]/` or `mosaic-webui.service.d/` can't change what the checked unit runs.
|
||||
- N3. The precheck finds claude on the shell PATH. The bus unit's `PATH=` is what a session uses
|
||||
when `MOSAIC_CLAUDE_BIN` is unset, so the check should search that PATH.
|
||||
@@ -0,0 +1,8 @@
|
||||
# Herb
|
||||
|
||||
Builder seat, Codex gpt-6.1-sol, started by Sage under mos-0977 item 5 (more Codex Sol rows) for a second gate lane
|
||||
design row (#1695, mos-0977 item 6).
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no
|
||||
credentials of its own; until the credentials row settles per-agent identity, it pushes only its own `herb/*`
|
||||
branches through the repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch.
|
||||
@@ -0,0 +1,7 @@
|
||||
# Honker
|
||||
|
||||
Builder seat, Claude Opus 5.5, started by Sage for MVP T1 row 194 (agent lifecycle, #1692) under mos-0974 and mos-0977.
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no
|
||||
credentials of its own; until the credentials row settles per-agent identity, it pushes only its own `honker/*` branches
|
||||
through the repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch.
|
||||
@@ -0,0 +1,158 @@
|
||||
# PRD 1.7 draft: the chief of staff's hire and fire authority, the coder's push to its fork, and agent
|
||||
# host access through the credential store
|
||||
|
||||
Draft by Honker for row 194 (#1692), under Mos's ruling mos-1017 (through Sage, sage-1365). Not approved.
|
||||
It changes nothing until Jason approves it as a `prd.approve` decision in the Console. Sage opens that
|
||||
decision and sends Mos its id. Base: `docs/prd/mosaic-stack.md` version 1.6 at 53bb59462.
|
||||
|
||||
## What it changes
|
||||
|
||||
Row 194 adds `role.hire` and `role.fire` to the vocabulary, and row 199 adds `git.push.remote`. Under
|
||||
REQ-WAIT-3 all three are hard waits, so today Jason resolves every hire, every fire and every push to a
|
||||
remote. This version routes hire and fire for the chief of staff within fixed bounds, so the chief hires
|
||||
and fires as needed and the user's per-agent forbid still holds. It routes `git.push.remote` for the coder
|
||||
only (Mos, mos-1028 item 3). It adds agent host access with sudo through the credential store for
|
||||
agents with a grant (Q183, Q184, Vikunja #424, due 2026-10-27). One `prd.approve` decision covers it all
|
||||
(Vikunja #420).
|
||||
|
||||
When this version lands, a reviewed commit:
|
||||
|
||||
1. adds `role.hire`, `role.fire` and `git.push.remote` to `ROUTED` in
|
||||
`packages/business/src/vocabulary.mjs` and in `packages/bus/src/broker.mjs`.
|
||||
2. adds `role.hire` and `role.fire` to `roles/chief-of-staff.json` withinRole, and `git.push.remote` to
|
||||
`roles/coder.json` withinRole. No other role file gets `git.push.remote`.
|
||||
3. switches the broker's check on `instance.hire` and `instance.fire` from a cited decision to the
|
||||
`reason` form, as `role.end` works for REQ-ROLE-2, and makes `publish`'s `decision` optional for a
|
||||
within-role class (row 199 D9 step 1).
|
||||
4. states, for a `git.push.remote` with no decision, where the source commit comes from: the latest work
|
||||
commit the bus recorded for the coder's task. The check still matches it against the work ref tip and
|
||||
the push sends it by oid (row 199, duckworth-0022 N9).
|
||||
5. adds the host credential grant check and the host access it allows (REQ-CRED-3, Vikunja #424).
|
||||
|
||||
Every other check stays as rows 194 and 199 build it. Filling `hireable` stays a separate reviewed role
|
||||
file commit in `roles/`.
|
||||
|
||||
## Text for the PRD
|
||||
|
||||
Status line, appended to the current paragraph:
|
||||
|
||||
> Version 1.7 routes the chief of staff's hire and fire actions and the coder's push to its own fork,
|
||||
> and adds agent host access through the credential store, approved by Jason in decision
|
||||
> <prd.approve decision id>.
|
||||
|
||||
Source, a new bullet above "Version 1.6 source":
|
||||
|
||||
> - Version 1.7 source, TARGET.md line 66: "Chief of staff agent has hire/fire authority for agents as
|
||||
> needed; unless user forbids per agent". And Jason through Mos (mos-1067), DECISIONS Q183, in part: "I will
|
||||
> still want to allow infra agent(s) on my .53 instance to access other servers, etc. However, not all
|
||||
> agents will need to have that access. I envision a credential store within Mosaic Stack that works
|
||||
> as some sort of variable for the agent to call. The system will enforce access to the credentials to
|
||||
> keep the agents sandboxed with proper security. I need to be able to trust my agents with assisting
|
||||
> me. This will require sudo access to several systems. If we can utilize the credential store in
|
||||
> Mosaic Stack and allow agents to perform their jobs with NOPASSWD sudo on various machines, I am a
|
||||
> happy man." DECISIONS Q184, on whether that is in place by 10-27: "Q5: by 10.27"
|
||||
|
||||
Under "Added in version 1.2", after REQ-COS-2:
|
||||
|
||||
> Added in version 1.7:
|
||||
>
|
||||
> - **REQ-COS-3.** The chief of staff hires and fires role instances without a decision each time. Hiring
|
||||
> adds a role instance to the organization file and lists it for launch. Firing ends the instance's
|
||||
> runs and removes it from the launch list. It stays in the organization file. Only the user deletes
|
||||
> an instance. These actions are `role.hire` and `role.fire`. They leave the hard waits and route
|
||||
> under this requirement, within these bounds:
|
||||
> - The chief acts only in its own organization.
|
||||
> - The chief hires only role definitions that need no credentials, until a later version adds
|
||||
> credentialed roles.
|
||||
> - The chief hires only the role kinds its role file lists. Changing that list is a reviewed commit.
|
||||
> - The chief hires only a role whose every action is on a fixed list of plain work actions: sending
|
||||
> messages, writing records, asking for review, updating its assigned tasks, and the scope and
|
||||
> working-push actions of an assigned task. A role holding any other action, such as launch, end,
|
||||
> organization, naming, hire, review verdict or merge authority, or any action added later, is never
|
||||
> hired. So the chief never hires a chief, never picks its own reviewers or merge gate, and the author
|
||||
> is never the reviewer. This holds whatever the role file's list says. Adding to that list is a PRD
|
||||
> change.
|
||||
> - The chief may set a hired instance's model and narrow its limits, and nothing else about it.
|
||||
> - The user can forbid per agent. The user marks an instance so the chief can't fire it, end its runs
|
||||
> or change its model, and marks role kinds the chief can't hire. Only the user changes these marks, through the
|
||||
> WebUI or the CLI.
|
||||
> - The kill switch stops every hire and fire while it is on.
|
||||
> - Every hire and fire is logged with the chief's instance, its run and the reason.
|
||||
|
||||
Under "Landing", after REQ-LAND-1:
|
||||
|
||||
> Added in version 1.7:
|
||||
>
|
||||
> - **REQ-LAND-2.** A coder pushes its own work to its own fork. The action is `git.push.remote`.
|
||||
> - What it pushes: one commit, by its oid, never a ref name. That commit is the latest work commit the
|
||||
> bus recorded for the coder's task, and the coder made that push. A decision that authorizes a push
|
||||
> names that commit, and it doesn't authorize any other.
|
||||
> - Where it lands: only the branch `refs/heads/<its Gitea user>/<branch>`. The host builds that
|
||||
> name, the agent never does. A push never forces.
|
||||
> - Which remote: the agent's own fork of the project's upstream. The business file names the upstream,
|
||||
> and the host never contacts it. For each push the host builds the fork's address over https from
|
||||
> the upstream's host, port and repository name and the agent's own Gitea user. The history check,
|
||||
> the push and the credential match all use that address. So the agent's own user owns the receiving
|
||||
> repository on every push, and the agent never names it.
|
||||
> - The host refuses an upstream on a host or port other than the agent's Gitea, and an upstream the
|
||||
> agent's own user owns. It refuses a fork that is missing, empty or doesn't share the project's
|
||||
> history. Gitea's push-to-create stays off, so a push never creates a repository.
|
||||
> - Who may: the action routes under REQ-DEC-2 and is granted within-role to the coder role only. No
|
||||
> other role file grants it.
|
||||
> - Before version 1.7 the action was a hard wait under REQ-WAIT-3, so each push needed a decision
|
||||
> the human resolved, and that decision named the commit.
|
||||
>
|
||||
> Every push is logged with the remote, the ref, the commit and, when one was given, the decision.
|
||||
|
||||
Under "Credentials", after REQ-CRED-2:
|
||||
|
||||
> Added in version 1.7:
|
||||
>
|
||||
> - **REQ-CRED-3.** An agent with a grant can run commands with sudo on another host. The agent asks
|
||||
> for the host credential by name. The stack checks that a grant exists for that agent and that host
|
||||
> before the credential is used. The agent never sees the key or any other secret value (REQ-CRED-1,
|
||||
> REQ-VAR-2). An agent without a grant for that host is refused. Not every agent gets a grant. Who
|
||||
> grants a host credential, and how, is set by the #424 design, which Jason approves.
|
||||
|
||||
REQ-WAIT-3, appended:
|
||||
|
||||
> `role.hire` and `role.fire` were added as hard waits and route under REQ-COS-3 since version 1.7.
|
||||
> `git.push.remote` was added as a hard wait and routes under REQ-LAND-2 since version 1.7.
|
||||
|
||||
## Notes for the reviewer and for Mos
|
||||
|
||||
- REQ-COS-1 (1.2) already says the chief "hires and terminates role instances". REQ-COS-3 gives that
|
||||
sentence its actions and bounds. It doesn't widen REQ-COS-1.
|
||||
- The forbid now names what r3 called "replace": ending a kept instance's run with a reason and changing
|
||||
its model (webby-0010 N2). A user-resolved decision still ends a kept run (row 194 D2.4).
|
||||
- The fixed list is `HIRE_ALLOWED` in row 194 D2.3, an allowlist since r4 (webby-0010 B1). r3's text
|
||||
named the refused authorities instead, which let merge-gate and any later action through.
|
||||
- The kill switch is row 191's. If row 191 hasn't landed when this version is approved, the bound still
|
||||
holds through row 194's chain on its candidate.
|
||||
- REQ-LAND-2 follows row 199's design r6 (4d3007ec9 on megavolt/row199, approved by Duckworth in
|
||||
duckworth-0027: D9 to D11, D9a, D9c, Q-M1 and Q-M2, and the oid binding from mos-1034 item 5) and
|
||||
Megavolt's wording in megavolt-0005, megavolt-0008 and megavolt-0014, through Sage (sage-1377,
|
||||
sage-1380, sage-1387). The fork bullet answers duckworth B5 and B6 and webby-0011 N1. Commit-email
|
||||
checks, the refusal codes and the URL parse rules (D9c) are host checks with tests in row 199, not
|
||||
bounds, so they stay out of the text (megavolt-0008).
|
||||
- Once the routing lands and the decision is optional for the within-role class, the commit is the task's
|
||||
latest recorded push at check time. It is still checked against the work ref tip and pushed by oid, so
|
||||
the binding holds without a decision (megavolt-0008). Step 4 of "When this version lands" puts that in
|
||||
the routing commit (duckworth-0022 N9, sage-1387).
|
||||
- REQ-LAND-2 and REQ-COS-3 meet at the coder. Coder declares Gitea credentials, so REQ-COS-3's
|
||||
credentials bound keeps the chief from hiring one until a later version lifts it. `git.push.remote` isn't
|
||||
on REQ-COS-3's fixed list either, so a coder holding it stays unhireable until a PRD version adds it
|
||||
(row 194 D2.3).
|
||||
- Adding to the fixed action list is a PRD change (mos-1035 item 3, sage-1395). The role file's
|
||||
`hireable` list stays a reviewed commit in `roles/`.
|
||||
- The credentials bound names no row number, so the text doesn't go stale when row 199 lands. A later
|
||||
version lifts it.
|
||||
- REQ-CRED-3 states only what Jason said in Q183 and Q184 (sage-1422). It names the outcome and the
|
||||
check, not how the stack reaches the host. Proxying the ssh and a signing socket are both designs, and
|
||||
the #424 design picks one (duckworth-0038 B1).
|
||||
- Open point: Q183 doesn't say who grants host access or how, so the text leaves it open and defines no
|
||||
mechanism. The #424 design settles it, Jason approves that design, and Duckworth reviews it because it
|
||||
gives agents sudo on hosts (Q184, Mos's reading, duckworth-0038 B2). The build is row 210 (#1707,
|
||||
owner Gizmoduck), a new row on top of row 199 (megavolt-0019, sage-1430).
|
||||
- This draft follows the PRD's own style: plain words, no semicolons in new sentences, and the status
|
||||
paragraph never edited in place except to append.
|
||||
@@ -0,0 +1,7 @@
|
||||
# Launchpad
|
||||
|
||||
Builder seat, Claude Opus 5.5, started by Sage for MVP T1 row 192 (Q175 SSO phase 1, #1690) under mos-0974 and mos-0977.
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no
|
||||
credentials of its own; until the credentials row settles per-agent identity, it pushes only its own `launchpad/*` branches
|
||||
through the repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch.
|
||||
@@ -0,0 +1,67 @@
|
||||
# Row 238: the sso-config hint names the instance (row 200 follow-up N1)
|
||||
|
||||
Launchpad, 2026-10-21. Author Launchpad, reviewer Webby, gate owner Sage. Issue #1744. Brief "Brief: row 200
|
||||
follow-up N1, the sso-config hint names the instance" in `docs/plans/2026-10-20_stack-release-sso.md` (76134dfde),
|
||||
from Webby's row 200 round 3 note N1 (comment 29772), assigned in sage-1597.
|
||||
|
||||
Round 4 base c515273db, row 240's landing (`base.txt`). Branch `launchpad/row238-r4`, one commit.
|
||||
|
||||
- Round 1 was `launchpad/row238` bec7e499e on 011d41ba6, approved by Webby (webby-0049). It was built on
|
||||
76134dfde and rebased before its first push, after row 199 (cb5fdb064) added 6 lines to `docs/TOOLS.md`.
|
||||
- Rows 232 (e65a3ab4d) and 233 (57f59e0c3) then landed on all 3 paths (sage-1710, sage-1755). Round 2 was a
|
||||
local rebase onto 57f59e0c3 (ca3d30e5c, never pushed). The merge was clean. `sso_checks` moved from about
|
||||
:838 to :920 with its body unchanged except for this row's hunk. 233 moved the shared test helpers into
|
||||
`stack-release-fixture.mjs`, and the new subtest is still under T57.
|
||||
- Row 234 (c771a36f4) then added two lines to T46's `untimed` map (`host_grants`, `hosts`, now :919-920).
|
||||
Round 3 is the same patch on c771a36f4 with those lines kept. The code diff on the 3 paths has the same
|
||||
`git patch-id --stable` on both bases (b9d0bb0ee3c7). Pushed as `launchpad/row238-r3` a33597c33 and
|
||||
approved by Webby in round 2 (webby-0059, rev 2707).
|
||||
- Row 240 (c515273db) then changed `docs/TOOLS.md` at :837 and :854, away from this row's hunk at :599
|
||||
(sage-1803). Round 4 is the same patch on c515273db: `docs/TOOLS.md` is the landed blob plus the one
|
||||
hunk, and `scripts/stack-release.sh` (100755) and `stack-release.test.mjs` keep round 3's blobs. The
|
||||
patch-id is still b9d0bb0ee3c7. No test run for this doc-only merge; Sage's landing gate covers it.
|
||||
Row 240 also changed `stack-upgrade.test.mjs`, so the round 2 result for that file is from the older blob.
|
||||
`candidate-manifest.sha256` lists the 3 changed paths; the packet files are not in it.
|
||||
|
||||
No test touches `~/.mosaic-dev`, `~/.config/mosaic-dev`, `~/.mosaic`, a live unit or a live slot. The new
|
||||
subtest runs on row 190's `named()` scratch instance under the test's temp HOME.
|
||||
|
||||
## Files
|
||||
|
||||
| Path | Change |
|
||||
| --- | --- |
|
||||
| `scripts/stack-release.sh` | `sso_checks`: the N4 first step puts `${INSTANCE_FLAGS[*]}` after `activate $TO` in both commands. |
|
||||
| `packages/cli/tests/stack-release.test.mjs` | T57 subtest "row 238 (#1744)": the sso-config refusal and the `--plan` line on instance `alpha`. |
|
||||
| `docs/TOOLS.md` | The `sso-config` paragraph says both commands carry `--instance NAME` on a named instance. |
|
||||
|
||||
## What it does
|
||||
|
||||
On a named instance the hint now reads `activate ID --instance NAME without --sso first, then write
|
||||
console.sso, then run activate ID --instance NAME --sso`. `INSTANCE_FLAGS` is empty for the default instance,
|
||||
so `inst` is empty and the default text is unchanged byte for byte. T57's item 2 subtest still asserts the
|
||||
default text exactly. The flag goes right after `ID`, the same place the other entry points take it from
|
||||
`INSTANCE_FLAGS`. BUSINESS is not in the hint before or after this row.
|
||||
|
||||
## Tests run (this tree)
|
||||
|
||||
Logs are in `/mnt/storage/scratch/launchpad-r238/`.
|
||||
|
||||
- Round 1: `stack-release.test.mjs` whole, 193/193 on 76134dfde (`sr-whole.log`) and on 011d41ba6
|
||||
(`sr-whole-2.log`). `stack-instances.test.mjs` whole, 23/23 (`si.log`).
|
||||
- Round 2, on ca3d30e5c (57f59e0c3), each file singly on Sage's slot (sage-1761): `stack-release.test.mjs`
|
||||
196/196, `stack-release-external.test.mjs` 25/25, `stack-upgrade.test.mjs` 20/20 (`r2-*.log`, 241/0 by
|
||||
awk). Sage counts these as pre-merge (sage-1769).
|
||||
- Round 3, on this blob singly on Sage's slot (sage-1794): `stack-release.test.mjs` 196/196, 06:34:22Z to
|
||||
06:42:57Z (`r3-stack-release.log`). The other two files and
|
||||
`stack-release-fixture.mjs` are the same on 57f59e0c3 and c771a36f4, so their round 2 results stand.
|
||||
- A grep of `packages/*/tests/*` and `scripts/test-*.sh` finds the hint text only in `stack-release.test.mjs`
|
||||
(`:1538` default, `:1689` and `:1691` named) and no 64-hex pin on the 3 paths.
|
||||
- The cli suite, every `scripts/test-*.sh` and the mutant go to Sage's slot.
|
||||
|
||||
## Mutants (`mutants.py`, run as `python3 mutants.py <tree> [N ...]`, against T57)
|
||||
|
||||
| # | Mutant | Result |
|
||||
| --- | --- | --- |
|
||||
| 1 | the first step drops `--instance NAME` (`inst=""`) | not run, Sage's slot |
|
||||
|
||||
The pattern matches once on this tree.
|
||||
@@ -0,0 +1 @@
|
||||
c515273db7b364736527493ed170d0f088eb506e
|
||||
@@ -0,0 +1,3 @@
|
||||
abdd641ea5a411347e3644286821c6485a6220e2a3de0f5ee95dcc37ba2c75ca docs/TOOLS.md
|
||||
4eeae9f672aecccdf853c377fb10847ce2656dbc566720bddfece435c1755d3b packages/cli/tests/stack-release.test.mjs
|
||||
1896d6518a502d340dfe30d402102b8c5cd5d8520757387b018b636755eb5d36 scripts/stack-release.sh
|
||||
@@ -0,0 +1,37 @@
|
||||
# Row 238 (#1744) mutant: `python3 mutants.py <tree> [N ...]`. Each applies to the tree's scripts/stack-release.sh,
|
||||
# runs T57 in packages/cli/tests/stack-release.test.mjs and restores the file from a saved copy, checked by sha256.
|
||||
# Killed = test exit != 0.
|
||||
import hashlib, os, subprocess, sys, signal
|
||||
R = os.path.abspath(sys.argv[1])
|
||||
S = os.path.join(R, 'scripts', 'stack-release.sh')
|
||||
if not os.path.isfile(S): sys.exit(f'ABORT no scripts/stack-release.sh under {sys.argv[1]}')
|
||||
T, PAT = 'tests/stack-release.test.mjs', '^T57'
|
||||
M = [
|
||||
('the sso-config first step drops --instance NAME (row 238)', [(' local first="" inst="${INSTANCE_FLAGS[*]:+ ${INSTANCE_FLAGS[*]}}"', ' local first="" inst=""')]),
|
||||
]
|
||||
sha = lambda b: hashlib.sha256(b).hexdigest()
|
||||
only = sys.argv[2:]
|
||||
os.chdir(os.path.join(R, 'packages', 'cli'))
|
||||
survived = []
|
||||
for i, (name, pairs) in enumerate(M, 1):
|
||||
if only and str(i) not in only: continue
|
||||
orig = open(S, 'rb').read(); h = sha(orig)
|
||||
src = orig.decode()
|
||||
for a, b in pairs:
|
||||
if src.count(a) != 1: sys.exit(f'ABORT {i} {name}: pattern found {src.count(a)} times')
|
||||
src = src.replace(a, b)
|
||||
try:
|
||||
open(S, 'w').write(src); os.chmod(S, 0o755)
|
||||
p = subprocess.Popen(['node', '--test', f'--test-name-pattern={PAT}', T], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, start_new_session=True)
|
||||
try: out, _ = p.communicate(timeout=300)
|
||||
except subprocess.TimeoutExpired:
|
||||
os.killpg(p.pid, signal.SIGKILL); out, _ = p.communicate(); out += b'\nTIMEOUT'
|
||||
text = out.decode(errors='replace')
|
||||
ran = [l for l in text.splitlines() if l.startswith('ℹ fail ')]
|
||||
verdict = 'KILLED' if p.returncode != 0 else 'SURVIVED'
|
||||
if verdict == 'SURVIVED': survived.append(name)
|
||||
print(f'{i:2} {verdict} {name} [{ran[0] if ran else "no count"}]', flush=True)
|
||||
finally:
|
||||
open(S, 'wb').write(orig); os.chmod(S, 0o755)
|
||||
if sha(open(S, 'rb').read()) != h: sys.exit(f'ABORT restore failed for {S}')
|
||||
print('survived:', survived or 'none')
|
||||
@@ -0,0 +1,8 @@
|
||||
# Megavolt
|
||||
|
||||
Builder seat, Claude Opus 5.5, started by Sage under mos-0974 and mos-0977 item 5 for MVP T1 item 6, the
|
||||
credential store per agent with its own git identity (#1697).
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no
|
||||
credentials of its own; until the credentials row settles per-agent identity, it pushes only its own `megavolt/*`
|
||||
branches through the repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch.
|
||||
@@ -0,0 +1,216 @@
|
||||
# Row 199 (#1697): per-agent credential store, each agent's own git identity, publish to its fork
|
||||
|
||||
Builder: Megavolt. Brief: `docs/plans/2026-10-20_agent-credentials.md`. Design: `DESIGN.md` r6 at
|
||||
4d3007ec9, approved by Duckworth in duckworth-0027 (REVIEW-r6.md at 640011518). Authority model ruled
|
||||
by Mos in mos-1027. Branch: megavolt/row199. Reviewer: Duckworth.
|
||||
|
||||
Status: candidate on megavolt/row199-on-190, rebased on origin/refactor 00d4cc1f6 (sage-1555). The
|
||||
full gate and the section 11 mutants ran in Sage's slot; results under "Runs".
|
||||
|
||||
## Commits so far
|
||||
|
||||
- 286bf598c business: agent identity, the publish grant, the project upstream with the D9c parse, store
|
||||
credential references; git.push.remote as a hard wait.
|
||||
- a2c387dba bus: broker publish authority (checkPublish, recordPublish, publishRefused, pushCommit on
|
||||
the raise); the D9c parse moves to `business/src/vocabulary.mjs`; pm.md and chief-of-staff.md list
|
||||
git.push.remote among the hard waits.
|
||||
- 609dc9364 bus: the per-agent credential store in `credentials.mjs`.
|
||||
- 504336797 bus: runtime wiring, the store root and the four publish entries in the op table.
|
||||
- a18fc6cf8 tasks: request calls use `useFor` keyed from the session; the start probe keeps `use`. The
|
||||
D5 test in `credential-store.test.mjs` pins the `use(` call sites to `credentials.mjs` (useFor's own
|
||||
call) and `tasks/src/adapter.mjs` (the host's @sync and start probe).
|
||||
- aa740cc71 bus: the lifecycle verbs `credentials.issue`, `credentials.revoke` and `credentials.delete`
|
||||
in `credential-verbs.mjs`, the broker branch, the runtime wiring; `organizationWriter` gets a
|
||||
`detail` on `write` and a receipt-only `record`.
|
||||
- 0cdace415 cli: `mosaic credentials status|issue|rotate|revoke|delete`, the scripts/mosaic route, docs.
|
||||
|
||||
## Deviations from DESIGN.md r6
|
||||
|
||||
1. **Identity is optional on an agent.** D8 reads as identity on every agent. Today's business files
|
||||
carry none, so identity stays optional and `publish` requires it (N10's load refusal). An agent
|
||||
without identity commits as E7 (T6).
|
||||
2. **Revoke of a `{file}` or `{env}` reference.** Section 6 describes revoke for store references.
|
||||
For a reference outside the store, revoke forgets the token in memory and never unlinks the file;
|
||||
the lifecycle verb then turns the business file's reference into `store`, so a restart finds
|
||||
nothing. The test: `credential-store.test.mjs`, "revoke of a {file} reference".
|
||||
3. **Issue order.** Issue writes the token file (tmp, fsync, rename, dir fsync), then runs the
|
||||
caller's commit (the business file's reference becomes `store`), then sets memory. A failed commit
|
||||
leaves the file on disk and the token out of memory, so the agent stays `not-issued` until the
|
||||
issue is run again. The test: "a failed commit leaves the token on disk but not in memory".
|
||||
4. **The D9c parse lives in `business/src/vocabulary.mjs`, not `business.mjs`.** The broker reads
|
||||
`forkProblem` and `forkUrl`, and `project-runtime.test.mjs` pins the broker's import closure to
|
||||
files that import nothing. `business.mjs` wraps `parseGiteaUrl` with the loader's refusal
|
||||
messages. The pin test names the two new imports.
|
||||
5. **`withGitea` takes the publish ticket, not a cap (D9b).** The key is the session checkPublish
|
||||
checked with `#agent` (a run holding its claim; never a tester, reader or human), read through
|
||||
`broker.publishSession(ticket)`. The token is usable only between checkPublish and recordPublish
|
||||
or publishRefused. Its refusals are filed once, by publishRefused on the task's trail, and not
|
||||
also by `refusing()`. The test: `publish.test.mjs`, "runtime: withGitea keys from the open ticket
|
||||
only".
|
||||
6. **Role contracts.** `workflow-roles.test.mjs` requires the hard waits listed in `roles/pm.md` and
|
||||
`roles/chief-of-staff.md` to equal GATED_ONLY. Adding git.push.remote to the vocabulary made it
|
||||
fail, so both lists now name git.push.remote. Prose only; no hash pin reads either file. Duckworth
|
||||
should review this hunk because `roles/` is reviewed authority.
|
||||
|
||||
7. **Delete takes a reason.** Section 6 lists `delete AGENT` with no reason. The broker requires
|
||||
`reason` on delete as on revoke, so every receipt that ends a credential says why.
|
||||
8. **Rotate is `credentials.issue`.** There are three bus verbs, not four. An issue over an entry the
|
||||
bus holds is recorded as `rotate` in the receipt and the answer. The CLI's `rotate` reads the
|
||||
status first and refuses before reading stdin when nothing is issued.
|
||||
9. **Issue covers entries the bus loaded.** The business file must already name the kind for the
|
||||
agent, and the bus must have loaded that reference at start; otherwise `credential-unknown`. An
|
||||
agent added after the bus started needs the restart row 165 already asks for. Row 194's human
|
||||
`create` is where a new agent's `store` references come from.
|
||||
10. **No Console page yet.** Section 6 puts the verbs in the Console's agent page too. This branch has
|
||||
the bus verbs and the CLI. The Console page is a separate piece (packages/webui, its `BUS_VERBS`
|
||||
and `STATUS` pins); I raise the split with Sage before the manifest. Ruled in sage-1421: the
|
||||
Console page is out of row 199 and becomes a follow-up row with Webby as reviewer. The manifest
|
||||
holds bus, business, tasks, cli and seat.
|
||||
11. **The push runs in the bus child, and the git socket polls for it (D9b).** The git socket runs in
|
||||
the host process. The broker, the runtime and the token are in the bus child that
|
||||
`packages/cli/src/host.mjs` forks, and host ops cross that IPC one at a time with a 10 s answer
|
||||
limit. ls-remote and the push can run longer than that, so `withGitea(ticket, fn)` can't wrap a
|
||||
host-side git. The split: the socket runs D9 steps 3 and 4 (tip and identity under the lock,
|
||||
`working.publish`), then op `publishPush`, which starts `publishRemote` in the bus child under
|
||||
`withGitea` and answers `{state: 'running'}`. The socket polls op `publishResult` every 200 ms
|
||||
until it reads `pushed` or `refused`. Fork, user, source and destination come from the ticket
|
||||
(`broker.publishTarget`), never from the host's message. `recordPublish` also refuses `ref-moved`
|
||||
when the pushed ref isn't the ticket's destination. A second `publishPush` on a running ticket
|
||||
refuses `publish-busy`. The token reaches git on fd 3 only (`runGit` option `pass`), read once
|
||||
by a one-shot credential helper that answers `get` for https on the fork's exact host and port.
|
||||
On close, a running publish's git isn't awaited; git's own time limit ends it, and the ticket
|
||||
goes with the process. The 10 s IPC limit stays as it is (sage-1440). The poll has a bound
|
||||
(sage-1440): `publishDeadlineMs`, by default four times git's limit and grace (publishRemote runs
|
||||
at most four git commands) plus 30 s. Past it the socket refuses `publish-timeout`, filed once by
|
||||
publishRefused, and logs that the bus process's push may still reach the fork. A failure that is not a refusal is filed as `git-failed` and logged on
|
||||
the bus's stderr by error name and code only. Tests: `seat/tests/publish.test.mjs` (11),
|
||||
`bus/tests/publish.test.mjs` (the process ops), `cli/tests/publish-socket.test.mjs` (7, end to
|
||||
end through the socket, the in-process runtime, the store and a TLS fake Gitea; one is the
|
||||
publish-timeout outcome).
|
||||
12. **No harness tool for publish.** `packages/harness/src/git-tools.mjs` has push, fetch and land
|
||||
clients. A runner tool that calls the socket's `publish` op is in packages/harness, outside the
|
||||
manifest scope sage-1421 set. Today a publish needs a socket client; the tool is a follow-up.
|
||||
13. **The bind rule's `git.push.remote` clause has no test of its own.** D11 adds it and the
|
||||
launcher has it. A role can publish only a push its own role made (`work-not-yours`), and
|
||||
`recordPush` needs `git.push.working`, which already binds the socket. No role reaches the
|
||||
clause alone, so dropping it changes no outcome today. It matters once a role can publish work
|
||||
it didn't push. Ruled in sage-1440: keep it; its mutant goes in the section 11 list as
|
||||
equivalent, with this reasoning, and Duckworth judges it.
|
||||
14. **A push between checkPublish and the git push refuses; it never lands a different commit.**
|
||||
T3 says such a push "doesn't change the commit that lands". Under the hard wait it refuses:
|
||||
`working.publish` checks the work ref's tip against the ticket's source under the lock
|
||||
(`ref-moved`, `seat/tests/publish.test.mjs`), and `recordPublish` checks `pushCommit` again
|
||||
(`decision-mismatch`, `bus/tests/publish.test.mjs` "r4 B4"). The fork gets only the ticket's
|
||||
oid either way. N9's routing-commit test still applies. Ruled in sage-1440: T3's wording in
|
||||
DESIGN.md now reads "refuses with `ref-moved` or `decision-mismatch`", and an end-to-end test
|
||||
asserts each code mid-flight in `cli/tests/publish-socket.test.mjs`: a push after checkPublish
|
||||
refuses `ref-moved` with no request to the fork; a push recorded while the bus process pushes
|
||||
refuses `decision-mismatch`, filed once, and the fork holds only the ticket's oid.
|
||||
|
||||
15. **Fix: rotate on a due gitea key (gizmoduck-0050, sage-1494).** The CLI's `ISSUED` set
|
||||
(`packages/cli/src/credentials.mjs`) listed valid, expiring and expired, but the bus's `status()`
|
||||
gives a gitea key past its rotate-by date `rotation-due`. So `credentials rotate` refused the one
|
||||
case it exists for. `ISSUED` now names every state status() gives a held token. The test:
|
||||
`cli/tests/credentials.test.mjs`, "rotate goes ahead on each held state". Its mutant (the old set)
|
||||
fails that test and goes in the section 11 list.
|
||||
|
||||
16. **The bus process refuses a boot config carrying `publishTls` (M26, sage-1563).** process.mjs
|
||||
used to drop `publishTls` from the boot config before `startBroker`, and no test could see the
|
||||
drop. The host's boot config (`packages/cli/src/config.mjs`) never carries it; it is the tests'
|
||||
scratch CA for an in-process runtime only. A boot that carries it now refuses
|
||||
`invalid-host-request` and the process exits 2, rather than trusting it or dropping it quietly.
|
||||
The test: `bus/tests/publish.test.mjs`, "the bus process answers the publish ops from their own
|
||||
lines". Every test file that reads process.mjs ran singly, green (see "Runs").
|
||||
17. **M17 is equivalent: `#authorityFor`'s pushCommit comparison is also made by `#publishFacts`.**
|
||||
A git.push.remote raise with no recorded push refuses `no-push` (`broker.mjs` raise,
|
||||
`pushCommit = ... ?? fail('no-push')`), so every publish decision carries a pushCommit.
|
||||
`#publishFacts` takes `source = result.pushCommit` and refuses `decision-mismatch` when it isn't
|
||||
`#latestPush(task_ref).commit`, the same two values `#authorityFor` compares. The check stays as
|
||||
defense in depth, for a decision whose context lacks pushCommit, which no code path writes today.
|
||||
Duckworth judges it, as with M27.
|
||||
|
||||
## N9, for the routing commit (sage-1388)
|
||||
|
||||
Today git.push.remote is a hard wait, so every publish carries a decision, and the decision's
|
||||
`pushCommit` fixes the commit. If a later commit routes git.push.remote within role, a publish has
|
||||
no decision and no `pushCommit`. `#publishFacts` then takes the commit from `#latestPush`, and the
|
||||
git socket's tip check under the lock is the only thing that holds it. That commit needs a test
|
||||
that a push between checkPublish and the git push doesn't change what lands.
|
||||
|
||||
## R1, the backup set
|
||||
|
||||
Checked at 504336797. Row 185's `backup_store` (`scripts/stack-release.sh:372-392`) copies only
|
||||
`<dataRoot>/bus/bus.sqlite`, plus its sums, modes, listing and stamps, into
|
||||
`<dataRoot>/backups/stack/<activation>/`. Nothing under `<configDir>` is copied, so
|
||||
`<configDir>/secrets/agents` isn't in the backup set. `rehearse` copies the live business file
|
||||
through `R_BUSINESS_JS` (`:1268-1288`). Its `swap` replaces every credential reference, a `store`
|
||||
one included, with a placeholder file in the rehearsal's own dir. The guard then refuses
|
||||
`credential-ref-live` unless every reference is such a placeholder (`:1329-1337`), so a rehearsal
|
||||
never reads a store token. A backup tool outside this repository that copies `<configDir>` would
|
||||
copy tokens. That is R1's open half, and it is Jason's to check on each VM.
|
||||
|
||||
## T1 limit
|
||||
|
||||
A wrong owner on a store dir or file can't be set up without root, so T1 covers modes, symlinks and
|
||||
dirs. The owner check (`st.uid !== process.getuid()`) is in `privateDir` and `#read`, and is untested.
|
||||
|
||||
## Runs
|
||||
|
||||
Single files, each green on its commit: bus broker, decision-close, decision-route, decisions,
|
||||
hard-waits, land, project-runtime, launch-notice, review, publish, credential-store, credentials,
|
||||
credential-status, limits, tester, plugins-verb, routes-verb, limits-runtime, instances-verb, web,
|
||||
usage-runtime, limit-checks, daily-quota-runtime, chief-of-staff, tasks, tester-writes; every
|
||||
business test file. Since: credential-verbs (2/2), credential-store (11/11, with D5), each tasks file,
|
||||
tester, credential-status, plugins-verb, routes-verb, instances-verb; business launch-settings,
|
||||
plugin-settings, instance-settings, token-limit-settings; cli credentials (3/3), cli, app,
|
||||
tester-reads, tester, other-tree, transport, instances, limits, verbs. Hash pins: none on a changed
|
||||
file (`thread-migration.test.mjs:46` pins DDL only).
|
||||
|
||||
Publish, single files, each green before its commit: seat
|
||||
checkout (77), working (26), publish (11), landing (8); bus process (8), land (17), publish (12),
|
||||
credential-verbs (2); cli publish-socket (4), git-socket (24), launcher (29); harness tools (22).
|
||||
Hash pins: none on a changed file.
|
||||
|
||||
sage-1440 changes (the poll bound, the mid-flight tests), single files, green before the commit: cli
|
||||
publish-socket (7), git-socket (24), launcher (29). Hash pins: none on a changed file.
|
||||
|
||||
The rotation-due fix, single files: cli credentials (4), green; the old-set mutant fails it 3/1.
|
||||
Hash pins: none on a changed file.
|
||||
|
||||
Slip, 2026-10-20 about 19:51Z: I ran `node --test packages/tasks/tests/*.test.mjs` in one command,
|
||||
which is a package suite, without asking Sage for the slot. It passed, 97 pass and 0 fail, in under a
|
||||
minute. Reported to Sage in megavolt-0017.
|
||||
|
||||
Rebase (sage-1555): the 21 commits from megavolt/row199 sit on origin/refactor 00d4cc1f6 as the new
|
||||
branch megavolt/row199-on-190 (no force-push). `git range-diff` shows all 21 patches as `=`; the
|
||||
merge of 12c85ada1 flattened with an empty remerge-diff.
|
||||
|
||||
Full gate, Sage's slot, on c6868ba41 (base 00d4cc1f6), logs started 2026-10-20T22:55:44Z: bus 476
|
||||
pass 0 fail; business 191/0; tasks 97/0; cli 522/0 (472 s); seat 163/0; scripts/test-queue.sh
|
||||
156/0; test-auth, test-conductor, test-config, test-discord, test-extension-package,
|
||||
test-foundation, test-land-work, test-release and test-task each exit 0. The test-task compose run
|
||||
left the network megavolt-row199_default with 0 containers; I removed it.
|
||||
|
||||
Section 11 mutants, batch 1, on c6868ba41, 2026-10-20T23:09:03Z to 23:09:56Z, restores verified and
|
||||
packages/ clean: 22 of 28 killed; M27 survived, expected equivalent (deviation 13). Five expected
|
||||
kills survived, each a test gap: M05 (the token in the push URL), M06 (revoke unlinks before it drops
|
||||
memory), M12 (http.followRedirects=false dropped), M17 (#authorityFor's pushCommit check dropped)
|
||||
and M26 (publishTls reaches startBroker). Launchpad's stack-release single file ran alongside from
|
||||
23:09:28Z, so batch 2 reruns all 28 (sage-1563).
|
||||
|
||||
Fixes, each checked by its mutant alone: 5fa7b2553 seat publish, a test reads git's argv through a
|
||||
PATH shim and finds no token and no userinfo (M05 killed); the 302 test's target shares the CA git
|
||||
trusts, so only http.followRedirects=false keeps git off it (M12 killed). 116eeec48 bus,
|
||||
credential-store: revoke's unlink fails (dir 0500) and the token is already out of memory (M06
|
||||
killed); process.mjs refuses publishTls, deviation 16 (M26 killed, its anchor is now the refusal
|
||||
line). M17 is equivalent, deviation 17. Single files after the fixes, each green: seat publish (12),
|
||||
bus credential-store (12), bus publish (12), bus process (8), and every other file that reads
|
||||
process.mjs: bus credential-status (4), limit-checks (16), land (17), tasks (9), end-launch (7); cli
|
||||
trackers-boot (1), thread-launch (11), publish-socket (7). Hash pins: none on a changed file.
|
||||
|
||||
Section 11 mutants, batch 2, all 28, on 7c3cdd940 in Sage's slot (sage-1565), 2026-10-20T23:17:59Z to
|
||||
23:18:48Z, restores verified and packages/ clean: 26 killed; M17 and M27 survived, both expected
|
||||
equivalent (deviations 17 and 13). Each of the five batch 1 survivors but M17 now fails its test. Then
|
||||
pkg-bus 477 pass 0 fail, pkg-seat 164/0, pkg-cli 522/0 (461 s), ending 23:26:40Z. No other seat's run
|
||||
was active: the busy check said CLEAR before the batch started. Logs:
|
||||
/mnt/storage/scratch/megavolt-r199/batch2-20261020T231650Z/.
|
||||
@@ -0,0 +1,416 @@
|
||||
# Row 199 design: a credential store per agent, own git identity, pushes to a remote
|
||||
|
||||
Megavolt, 2026-10-20. Gitea #1697, MVP T1 item 6. Brief: `docs/plans/2026-10-20_agent-credentials.md`.
|
||||
Reviewer Duckworth. Mos rules on the authority model (section 9) before any code. Base: refactor at 992650232.
|
||||
No credential value was read to write this. Evidence is code and docs only.
|
||||
|
||||
r2 answers duckworth-0013 (review at 6ab9cd927 on duckworth/reviews): B1 in D3a, T1 and T5, B2 in D9 step 5
|
||||
and T3, N1 in section 6 and Q-M3, N2 in section 6, N3 in D9 step 3, N4 in D9a and T3, N5 in E5 and D9b.
|
||||
|
||||
r3 records Mos's rulings (mos-1027) in section 9. The Q-M2 hard wait changes D9 step 1, D9b and T3. Q-M1
|
||||
drops O2 (D10, A3, R2).
|
||||
|
||||
r4 answers duckworth-0017 (REVIEW-r3.md at 4065ed59f): B3 in T3 and D9 step 5, B4 in D9 steps 1, 3 and 5, N6
|
||||
and N7 in D9a, N8 in D9 step 1.
|
||||
|
||||
r5 takes webby-0011 N1 (row 194 review, through honker-0009): the host checks at each push that the
|
||||
remote's owner is the agent's own Gitea user. r4 named one `remote` per project, which can't be a fork per
|
||||
agent (O1) once two agents publish the same project. So `projects.<p>.remote` now names the upstream
|
||||
repository, and the host builds each agent's fork URL from it (D9 step 2, D10, T3). It also fixes a conflict
|
||||
found while building: D8 made `identity` required for every agent whose definition declares `gitea`, while D9
|
||||
step 2 says today's files load unchanged. Coder declares `gitea`, so r5 makes `identity` optional and
|
||||
`publish` require it (D8).
|
||||
|
||||
r6 answers duckworth-0026 (REVIEW-r5.md at 3cb6888e1), with Mos's acceptance (mos-1043). B5: the derived URL
|
||||
has one name, `fork`, and D9a's `ls-remote`, step 5's push, the helper's host match, `checkPublish`'s result
|
||||
and step 6's event all use it (D9, D9a, D9b, T3). B6: the URL parse is written down (D9c) and tested (T3).
|
||||
N10 in D8 and T3. N11 in D9a and A3. N9 is carried to BUILD.md for the routing commit.
|
||||
|
||||
## 0. Terms
|
||||
|
||||
- **Stack instance**: one Mosaic Stack deployment (row 190). It has one OS user, one config dir
|
||||
`~/.config/mosaic-NAME` (here `<configDir>`), one data root and one bus. Case 1 and Case 2 are two stack
|
||||
instances on two VMs (DECISIONS Q180).
|
||||
- **Agent**: a role instance in a business file (`roles.<agent>`). Today's code calls it "instance". This
|
||||
design says agent.
|
||||
- **Kind**: a credential type: `gitea`, `vikunja`, `model`.
|
||||
- **Host**: the bus process and its git socket, running as the stack instance's OS user, outside any sandbox.
|
||||
|
||||
## 1. What exists (measured at 992650232)
|
||||
|
||||
| Code | Fact | Where |
|
||||
|---|---|---|
|
||||
| E1 | The business file references credentials per agent: `roles.<agent>.credentials.<kind> = {file or env, rotateBy or expires}`. The kinds an agent may hold are the ones its role definition lists | `business/src/business.mjs:89-111`, `credentials.mjs:18-38`, `role.mjs:105-118` |
|
||||
| E2 | The bus reads each token once at start (O_NOFOLLOW, own uid, 0600, at most 16 KiB, outside the repos and data root) into a private map keyed `<business>/<agent>:<kind>` | `bus/src/credentials.mjs:18-89`, `bus/src/runtime.mjs:78-89` |
|
||||
| E3 | Only Vikunja uses a token: `credentials.use("<business>/<agent>", "vikunja", ...)`. Gitea tokens are loaded and never used. `assertClean` refuses any event that contains a token | `tasks/src/adapter.mjs:70`, `runtime.mjs:134`, `broker.mjs:560` |
|
||||
| E4 | Agents run in bwrap as the bus's own uid. `~/.config`, `~/.mosaic` and `~/.ssh` are not bound. The git socket is bound only for `git.push.working`, `review.verdict` or a landing | `seat/src/sandbox.mjs:21-30, 104-124`, `cli/src/launcher.mjs:401` |
|
||||
| E5 | A session proves who it is with a 64-hex cap the broker mints and maps to a frozen session record. The git socket resolves the cap to (business, role) before anything else | `broker.mjs:280, 379-383`, `packages/cli/src/git-socket.mjs:398-407` |
|
||||
| E6 | The model credential is one env file per host (`EnvironmentFile=-%h/.config/mosaic-dev/secrets/claude-session.env`). `ENV_ALLOW` passes it into every sandbox, so every agent holds the same model token | `cli/systemd/mosaic-bus.service.in:24`, `seat/src/session.mjs:83-94` |
|
||||
| E7 | Commits in a session's clone use `<agent>@<business>.mosaic.invalid`. Landed squashes use "Mosaic bus host" | `sandbox.mjs:151-157`, `working.mjs:280-281` |
|
||||
| E8 | No code pushes to a remote. The git socket pushes bundles into a local bare working repo with no remote and no credential | `working.mjs:1-14, 198-238` |
|
||||
| E9 | Adding or changing an agent refuses a definition that declares credentials (`organization-credentials-out-of-scope`). Binding them is a hand edit. Row 194's design keeps that refusal for create and hire "until the credentials row" | `business/src/instance-settings.mjs:20-29`, row 194 DESIGN.md D1 |
|
||||
| E10 | The development repository's helper pushes as one identity from one JSON file | `scripts/git-credential-mosaic.mjs` |
|
||||
|
||||
So per-agent references and a host-only reader already exist for two kinds. What's missing: a fixed place for
|
||||
the files, an agent identity, any remote push, lifecycle verbs, and a model credential that agents don't
|
||||
share.
|
||||
|
||||
## 2. The store (brief item 1)
|
||||
|
||||
D1. **Layout.** Each stack instance keeps its agents' secrets under its own config dir:
|
||||
|
||||
```
|
||||
<configDir>/secrets/agents/ 0700, owner = the stack instance's OS user
|
||||
<configDir>/secrets/agents/<business>/<agent>/ 0700
|
||||
<configDir>/secrets/agents/<business>/<agent>/<kind> 0600, the token only, one line
|
||||
```
|
||||
|
||||
This is row 190's secrets root (`<configDir>/secrets/...`, as Q175's slot uses). The token file keeps E2's
|
||||
format, so the bus reader doesn't change. Nothing in a token file is a name, an email or a URL. Those are not
|
||||
secret and go in the business file (D6), where review sees them.
|
||||
|
||||
D2. **The business file reference.** A new reference form, `{"store": true, "rotateBy" | "expires": DATE}`,
|
||||
means "the file at the layout path for this business, agent and kind". The path is derived, never written,
|
||||
so an agent's entry can't name another agent's file. The old `{file}` and `{env}` forms stay valid for
|
||||
migration (section 7). A `{file}` reference that points inside `<configDir>/secrets/agents/` but not at its
|
||||
own agent's path refuses `credential-not-yours` when the business file loads.
|
||||
|
||||
D3. **Read path.** Unchanged in shape: the bus reads every entry once at start (E2) and on a store change
|
||||
(section 6), and holds the values in memory. No sandbox binds `<configDir>` (E4 already holds). No token goes
|
||||
into a sandbox for `gitea` or `vikunja`. The agent asks the bus to act, and the bus uses the token.
|
||||
|
||||
D3a. **A `store` reference with no file.** ENOENT on a `store` path loads as an absent entry. `status` shows
|
||||
`not-issued`, and `useFor` refuses `credential-unavailable`. The bus still starts. This is the state after a
|
||||
create without `issue` (D13) and after `revoke` (section 6). Every other defect still refuses start: wrong
|
||||
owner or mode on the file or a dir, a symlink, a non-regular file, bad format, a missing or wrong-mode
|
||||
`secrets/agents/` dir. `{file}` and `{env}` keep E2's behavior, where a missing file refuses start.
|
||||
|
||||
## 3. The "RLS equivalent" (brief item 1)
|
||||
|
||||
Three layers. Each one is enforced by code or the OS, not by prose.
|
||||
|
||||
D4. **OS, between stack instances.** Files are 0600 and dirs 0700, owned by the stack instance's own OS user.
|
||||
Case 1's user can't read Case 2's store (Q180: each instance under its own user without sudo). The bus refuses
|
||||
to start when any file or dir under `secrets/agents/` has the wrong owner or mode (`credential-file`, as E2
|
||||
does for files today, extended to the dirs).
|
||||
|
||||
D5. **Bus, between agents of one stack instance.** The principal for every credential use comes only from the
|
||||
verified cap (E5). The agent-facing API is `credentials.useFor(session, kind, op)`: `session` is the broker's
|
||||
frozen record, and the key is built from `session.business` and `session.role`. It has no agent argument, so
|
||||
there is nothing in a request to point at another agent. The existing `use(name, kind, op)` stays for the
|
||||
host's own `@sync` entry and is not reachable from a request. The tasks adapter's per-role calls (`ctx.as`,
|
||||
`verbs.mjs:126`) move to `useFor`. Any request field that names an agent other than the caller (`as`,
|
||||
`agent`, `instance`, `role` on a credential path) is refused `credential-not-yours` and recorded through
|
||||
`broker.refuse()` as `action.refused`, with the caller's cap and the task ref. A build test greps every
|
||||
`credentials.use(` call site and fails on a new one outside the allowlist.
|
||||
|
||||
D6. **What stays open.** A process of the same OS user outside the sandbox can read every agent's file. That
|
||||
is the same limit as the human proof (P19 333). Q180's per-user VMs move the boundary to "one stack instance
|
||||
per OS user". Agents of one instance are kept apart by the sandbox and D5, not by the OS.
|
||||
|
||||
## 4. Kinds (brief item 2)
|
||||
|
||||
| Kind | 10-27 | Use |
|
||||
|---|---|---|
|
||||
| `gitea` | yes | Git push to a granted remote (section 5). Gitea API verbs (issues, pull requests) wait. The factory's review flow runs in the working repo today, and no 10-27 case needs an agent to open a pull request |
|
||||
| `vikunja` | yes, exists | Moves to `store` references and `useFor`. No behavior change |
|
||||
| `model` | per stack instance only | See D7 |
|
||||
|
||||
D7. **Model provider.** A model token has to sit inside the sandbox today, because the harness calls the
|
||||
provider itself (E6). Per-agent model tokens need a proxy that holds the real token and gives each run its own
|
||||
short-lived one, which is the CLIProxyAPI line in TARGET.md. That waits for its own row. For 10-27 the model
|
||||
credential is per stack instance: row 190's `<configDir>/secrets/claude-session.env`, owned by that
|
||||
instance's OS user. It is never a copy of jwoltje's file. Jason signs in on each VM user himself (A6). Whether
|
||||
Case 1 and Case 2 run on Jason's existing subscription or need a new account is Jason's call (Q-J1). This row
|
||||
doesn't pick. A pi model credential also waits for P19 #383.
|
||||
|
||||
## 5. Git identity and push (brief item 3)
|
||||
|
||||
D8. **Identity.** A new business-file key, `roles.<agent>.identity = {name, email, gitea: {url, user}}`. It is
|
||||
optional, because today's business files with a `gitea` agent (coder) must load unchanged. `publish` (D9
|
||||
step 2) requires it, and requires that the definition declare `gitea`. A business file with a `publish`
|
||||
grant on an agent that lacks either refuses at load, and the error names `roles.<agent>.publish` and says
|
||||
"needs identity and a gitea definition" (N10). Load is the only place this is checked: a business file that
|
||||
fails it never reaches the bus. The sandbox sets `GIT_AUTHOR_*` and `GIT_COMMITTER_*` from
|
||||
it in place of E7's invalid domain. The email is the one Jason gives the agent's Gitea user, so Gitea links the
|
||||
commits to that user. Agents without `gitea` keep E7.
|
||||
|
||||
D9. **Push to a remote: a new git socket op, `publish`.** No new socket, no bundle, and no token in the sandbox.
|
||||
|
||||
```
|
||||
publish {"cap", "op": "publish", "task_ref", "project", "branch", "decision"}
|
||||
-> {"ok": true, "result": {"fork", "ref", "commit", "previous"}} or a refusal
|
||||
```
|
||||
|
||||
The host checks, in order, and each refusal goes through `broker.refuse()`:
|
||||
|
||||
1. Identity from the cap (E5), then the broker's `#checkAuthority(s, "git.push.remote", {decision, target:
|
||||
task_ref})`, as `checkLand` does for a merge (`broker.mjs:907-913`). `git.push.remote` is a new action in
|
||||
ACTIONS and is not in ROUTED (REQ-WAIT-3, `packages/business/src/vocabulary.mjs:52-58`,
|
||||
`broker.mjs:42-69`), so it is a hard wait and classifies `gated` for every role. Every publish therefore
|
||||
needs a decision the human resolved for this task ref. A decision is spent once (`decision-consumed`).
|
||||
|
||||
**The decision binds one commit.** `#authorityFor` binds action, role, thread and target only
|
||||
(`broker.mjs:769-798`), so the commit needs its own carrier. The raise records it the same way it records
|
||||
`holderRun` for `role.revoke` (`broker.mjs:1733`). When the action is `git.push.remote`, the
|
||||
`decision.raise` event body gets `pushCommit`. That is the commit of `#latestPush(business, target)` at
|
||||
raise time, read by the broker and never taken from the agent. A raise with no recorded push for the task
|
||||
refuses `no-push`. The decision's question shown to the human ends with that commit. `#authorityFor` then
|
||||
refuses `decision-mismatch` when `context.pushCommit` differs from the commit of `#latestPush` now, which
|
||||
is the same check `holderRun` gets at `broker.mjs:796`. So the sequence approve at A, push B, publish B
|
||||
refuses.
|
||||
No role definition grants the action yet. When Jason's PRD version routes it (Sage, Vikunja #420), a
|
||||
reviewed commit adds it to both ROUTED lists and to coder's `withinRole`. From then on a coder publishes
|
||||
without a decision, and `decision` is optional for a within-role class only.
|
||||
2. The project is in the agent's grant, `roles.<agent>.publish: [project]`, and the business file names the
|
||||
project's upstream repository, `projects.<project>.remote: "https://HOST/OWNER/REPO"`
|
||||
(`remote-not-granted`). Both URLs pass D9c's parse at load. The upstream's host and port equal
|
||||
`identity.gitea.url`'s (`remote-host-mismatch`). The host never contacts that URL. It builds the agent's
|
||||
fork, `fork = https://HOST[:PORT]/<gitea.user>/REPO`, from the upstream's origin and repository name and
|
||||
the agent's own `gitea.user`. `fork` is the only URL the rest of `publish` uses: D9a's `ls-remote`, step
|
||||
5's push, the helper's host match, `checkPublish`'s result and step 6's event. So the owner of the
|
||||
repository that receives a push is the agent's own Gitea user on every push, by construction, and the
|
||||
agent can't name it (webby-0011 N1). D9c is what makes "by construction" hold. Gitea's refusal of a
|
||||
push to a repository the user doesn't own or collaborate on is the second layer. An upstream whose owner
|
||||
equals the agent's `gitea.user`, compared without case, refuses `remote-not-fork`, at load and at push. `remote` is a new key
|
||||
beside `root` and `repo` (`business.mjs:186`), and `identity` and `publish` are new keys on a role entry
|
||||
(`business.mjs:89`). All three are optional, so today's business files load unchanged.
|
||||
3. The source is one commit, `C = context.pushCommit`. It is checked three ways, so all three are equal:
|
||||
the decision's commit, the commit of `#latestPush`, and the work ref's tip in the working repo,
|
||||
`work/vikunja-<project>-<task>` (`working.mjs:110-114`). The tip is read under the repository's lock, and
|
||||
if they differ the push refuses `ref-moved`. The work ref is per task, not per agent. Two checks make it the
|
||||
caller's: `#latestPush` was recorded by the caller's role (`work-not-yours`), and step 4. The destination is always `refs/heads/<gitea.user>/<branch>`, built by the host.
|
||||
`validBranch(branch)` holds (`invalid-request`). The agent never names the destination ref.
|
||||
4. Every commit in `main..<work ref>` has author and committer email equal to `identity.email`
|
||||
(`identity-mismatch`).
|
||||
5. The push runs through `credentials.useFor(session, "gitea", ...)`: `git push <fork> <C>:<dst>`, by oid,
|
||||
with no `+` and no `--force`. It never pushes by ref name, so a push to the working repo after
|
||||
`checkPublish` can't change what goes out (`non-fast-forward` from git stays a refusal). Git runs with E8's SAFE flags
|
||||
(`working.mjs:92-98`), which set `protocol.allow=never`. `publish` adds exactly two flags,
|
||||
`protocol.https.allow=always` and `http.followRedirects=false`, plus `GIT_TERMINAL_PROMPT=0` and no global
|
||||
or system config. The token reaches git through a one-shot credential helper that reads it from an
|
||||
inherited pipe, never through argv, env, the URL or a file. Seat `gitEnv` (`checkout.mjs:79-89`) takes only
|
||||
PATH from the host and sets the rest itself, so no proxy or `GIT_SSL_*` variable reaches git. `publish` takes a `tls` parameter that
|
||||
defaults to `[]`, the same way `push` takes `fsck = FSCK` (`working.mjs:100`). Tests pass
|
||||
`["-c", "http.sslCAInfo=<scratch CA>"]`. The host never sets it, and nothing in config or env reaches it. The helper answers `get` only when `protocol`
|
||||
is `https` and `host`, port included, equals `fork`'s. It writes nothing for any other `get` and for
|
||||
`store` and `erase`. `assertClean` checks git's output, so a fork that echoes the token gets
|
||||
`credential-leak`.
|
||||
6. The bus records `action.allowed` for `git.push.remote` with fork, ref and commit.
|
||||
|
||||
D9a. **The agent's fork must share root's history.** A push sends every object the fork lacks. The working
|
||||
repo's `main` is the project root's history (`working.mjs`, seed), so a fork that doesn't share it would
|
||||
receive all of root, landing squashes included. The check runs on `fork`, never on the upstream: a related
|
||||
upstream says nothing about what the agent's `<user>/REPO` holds.
|
||||
`publish` runs `seed` first (`working.mjs:164-176`), under the repository's lock as `push` does, so `main`
|
||||
catches up with root before the check below. That means a fork also receives any landed root commits that
|
||||
upstream doesn't have yet, because they sit under the agent's work. That is expected: the agent's branch is
|
||||
built on them.
|
||||
Before the push, the host runs `git ls-remote --symref <fork> HEAD` (same flags and helper). It refuses
|
||||
`remote-unrelated` when the fork is empty or when that tip isn't a commit the working repo already has and an
|
||||
ancestor of `main`. When `ls-remote` exits non-zero, for a fork that doesn't exist, a refused token or a
|
||||
network failure, `publish` refuses `fork-unavailable` and the push doesn't run (N11). Git's stderr isn't
|
||||
parsed to tell those apart and isn't returned, since it can carry the URL. Only `ls-remote` crosses the
|
||||
network for this, so nothing is fetched from the fork. If the fork has moved past root, the refusal stands
|
||||
until root catches up. That is a human pull into root, not an agent's.
|
||||
|
||||
D9c. **The URL parse (B6).** `projects.<p>.remote` and `identity.gitea.url` are parsed with `new URL()` at load,
|
||||
and the business file refuses to load unless all of these hold:
|
||||
|
||||
- The scheme is `https:`. There is no username, password, query or fragment.
|
||||
- The string equals its own normalized form: for the upstream, `url.origin + url.pathname`; for
|
||||
`gitea.url`, `url.origin + "/"` or `url.origin`. So a dot segment (`/<user>/../OWNER/REPO`), a
|
||||
percent-encoded dot, an upper-case host, an explicit `:443`, a backslash, userinfo, a query and a fragment
|
||||
each refuse, because `new URL()` would change them (checked with node: each differs from its normalized
|
||||
form). A trailing slash survives normalization and is refused by the two-segment rule below. The checks below run on that path, so the owner that is checked is
|
||||
the owner that is used.
|
||||
- The upstream's path is exactly `/OWNER/REPO`: two non-empty segments, no more and no fewer. One trailing `.git` on
|
||||
REPO is allowed and stripped. `fork` is built from the stripped REPO.
|
||||
- `OWNER`, `REPO` and `gitea.user` each match Gitea's name rule, `^[A-Za-z0-9_.-]{1,100}$` and not `.` or
|
||||
`..`. REPO after stripping must still match.
|
||||
- `gitea.url`'s path is `/`. A Gitea served under a sub-path isn't supported by this row and refuses at load.
|
||||
- Host and port are compared after `new URL()` normalized them, and an omitted port counts as 443 on both
|
||||
sides.
|
||||
- Gitea compares user and repository names without case (`lower_name`), so `remote-not-fork` compares the
|
||||
upstream's OWNER with `gitea.user` without case. An upstream owned by `Coder` and a `gitea.user` of `coder`
|
||||
would otherwise build a "fork" that is the upstream itself.
|
||||
|
||||
D9b. **How `publish` reaches the broker and the store.** The git socket runs in the bus process
|
||||
(`cli.mjs:504-506`) and reaches the broker only through the host's op table (`bus/src/runtime.mjs:222-246`).
|
||||
`publish` adds three entries there, each wrapped in `refusing()`:
|
||||
|
||||
- `checkPublish(cap, args)`: steps 1 to 3, including the decision check. It returns `{ticket, fork, user,
|
||||
email, source}` and no token. The ticket holds the session and the decision until `recordPublish`, the
|
||||
same way `checkLand` holds a landing.
|
||||
- `withGitea(cap, fn)`: `credentials.useFor(broker.session(cap), "gitea", fn)`. `fn` gets the token and runs
|
||||
step 5. The token never leaves that call.
|
||||
- `recordPublish(ticket, result)`: step 6, and it spends the decision.
|
||||
|
||||
The op table gets no entry that returns a token.
|
||||
|
||||
D10. **Which refs.** Our side: only `refs/heads/<gitea.user>/*`, set by the host (D9 step 3). The default
|
||||
branch and other agents' prefixes can't be named. Gitea side, two options (Q-M1):
|
||||
|
||||
- **O1. A fork per agent. Ruled by Mos (mos-1027).** The agent's Gitea user has read on the upstream repo and owns a fork
|
||||
named like it. The host derives the fork's URL from the upstream and the agent's user (D9 step 2).
|
||||
Its token can write only repos the user owns or collaborates on, so Gitea itself stops any push to upstream
|
||||
or to another agent's fork. No branch rules are needed per agent, so none can drift. The cost: work reaches
|
||||
upstream as a pull request from the fork, opened by a human or a later API verb.
|
||||
- **O2. One shared repo. Not chosen.** The agent is a Write collaborator. Upstream's default and release branches get
|
||||
branch protection with no agent on the push list, plus a rule `<user>/*` per agent. Gitea doesn't stop agent
|
||||
A from creating an unprotected name outside `A/*` unless a catch-all rule outranks the per-agent ones. That
|
||||
depends on Gitea's rule priority, which I haven't verified on our version (probe P1). Every hire adds a rule.
|
||||
|
||||
Token scopes for either: `write:repository` and `read:user`, nothing else, per vocabulary.mjs rules (no
|
||||
`all`, no admin).
|
||||
|
||||
D11. **The host git socket.** `publish` is added to the socket. `push`, `fetch` and `land` don't change. The
|
||||
socket's bind rule (E4) adds `git.push.remote`. The working repo stays credential-free and remote-free. The
|
||||
`fork` URL is passed per call and never written into its config. A landing still squashes into the project
|
||||
root as "Mosaic bus host". Landings don't push anywhere.
|
||||
|
||||
## 6. Lifecycle (brief item 4)
|
||||
|
||||
All verbs are on the bus, in the CLI (`mosaic credentials ...`) and in the Console's agent page. Every verb is
|
||||
human-only. Under Q173 the tester may run `status` and `revoke` on the dev stack instance. `issue` and
|
||||
`rotate` stay with Jason, because the tester is an agent and the token would pass through its session
|
||||
(duckworth N1). Each one appends to the organization
|
||||
writer's `changes.jsonl` (actor, agent, kind, verb, date; never a value). No schema migration, no new event
|
||||
kind. A refusal uses `action.refused`.
|
||||
|
||||
| Verb | What it does |
|
||||
|---|---|
|
||||
| `issue AGENT KIND --rotate-by DATE` (gitea) or `--expires DATE` (vikunja) | Reads the token on stdin (never argv), checks E2's format, writes `<kind>.tmp` 0600, fsyncs, renames over `<kind>`, and sets the business file reference to `{"store": true, date}`. The bus reloads that one entry, with no restart |
|
||||
| `rotate` | Same as `issue` over an existing entry. The old value is gone from disk and memory when the call returns. Jason then deletes the old token in Gitea (A5) |
|
||||
| `revoke AGENT [KIND] --reason` | Drops the entry from the bus's memory first, then unlinks the file. A use in flight finishes. Every later use refuses `credential-unavailable` |
|
||||
| `delete AGENT` | `revoke` for every kind, then removes the agent's dir. Refused while the agent has a running claim |
|
||||
| `status [AGENT]` | Kind, date and state per entry (E3's `status()`), no values |
|
||||
|
||||
D12. **"Stops working at once."** On our side this holds without a timing window. A fired agent never holds a
|
||||
`gitea` or `vikunja` token (D3), so revoking means the bus no longer has it. On the Gitea and Vikunja side the
|
||||
token still exists until Jason deletes it (A5). The status line for a revoked entry says so until Jason
|
||||
confirms. The model token is the exception: it sits in the running sandbox (D7), so a fired agent's runs must
|
||||
end. Row 194's fire ends them.
|
||||
|
||||
D13. **What row 194 calls.**
|
||||
|
||||
- `credentials.issue` is never called by the chief. Row 194's hire stays uncredentialed (its D1).
|
||||
- A human `create` of a credentialed definition: row 194 lifts E9's refusal for the human path only. The agent
|
||||
is created with `store` references and no files. `launch` refuses `credential-unavailable` until Jason runs
|
||||
`issue`.
|
||||
- `fire` and `delete`: row 194 calls `credentials.revoke(business, agent, {reason, actor})` before it changes
|
||||
the business file. A failed revoke stops the fire (`credential-revoke-failed`), so an agent can't be fired
|
||||
with its credentials still live on our side.
|
||||
- `regenerate`: no credential change.
|
||||
|
||||
## 7. Who does what (brief item 5)
|
||||
|
||||
Agents never mint accounts or tokens. Jason's steps, per agent that needs `gitea`:
|
||||
|
||||
- A1. Create the Gitea user: Site Administration, User Accounts, Create User Account. The name is the agent's
|
||||
`gitea.user`. The email is the agent's `identity.email`.
|
||||
- A2. Create its token: sign in as that user, Settings, Applications, Generate New Token, scopes
|
||||
`write:repository` and `read:user`, nothing else.
|
||||
- A3. Create the fork of each granted repo as that user (O1). Gitea's push-to-create
|
||||
(`ENABLE_PUSH_CREATE_USER`, `ENABLE_PUSH_CREATE_ORG`) stays off, so a push to a missing `<user>/REPO`
|
||||
fails rather than creating a plain repository (N11). `publish` refuses `fork-unavailable` before that.
|
||||
- A4. On the stack instance's VM, as its OS user: `mosaic credentials issue AGENT gitea --rotate-by DATE`, with
|
||||
the token pasted into stdin.
|
||||
- A5. On fire or rotate: delete the token in Gitea (Settings, Applications), or disable the user.
|
||||
- A6. Model: sign in to the provider on each VM user to write `<configDir>/secrets/claude-session.env`.
|
||||
|
||||
Vikunja bot accounts and tokens are already Jason's (tracker bots). Only A4 changes for them.
|
||||
|
||||
## 8. Migration (brief item 6)
|
||||
|
||||
- M1. Existing `{file}` and `{env}` references keep working. A business moves one agent at a time with
|
||||
`issue`, which rewrites that agent's reference to `store`.
|
||||
- M2. Nothing here touches the dev repository's helper, its `.git/config`, `~/.mosaic` or `~/.mosaic-dev`.
|
||||
Dev seats (jarvis, seat tokens) keep pushing as today. The mos-0981 rule ends only when Mos says so.
|
||||
- M3. The live dev stack instance needs no change at re-pin: no reference uses `store` until someone issues
|
||||
one.
|
||||
|
||||
## 9. Questions
|
||||
|
||||
Mos ruled on the authority model in mos-1027 (2026-10-20T19:27Z):
|
||||
|
||||
- Q-M1. O1, one fork per agent.
|
||||
- Q-M2. `git.push.remote` and its bounds in D9 to D11 are approved. It lands as a hard wait: out of ROUTED,
|
||||
and no role grants it within-role (D9 step 1). Sage puts its routing in the PRD version draft going to
|
||||
Jason with `role.hire` and `role.fire` (Vikunja #420). After his approval, a reviewed commit routes it and
|
||||
grants it to coder.
|
||||
- Q-M3. Issue, rotate and delete are human-only, and the chief gets none of them. Under Q173 the dev
|
||||
instance's tester gets `status` and `revoke` only.
|
||||
- Q-M4. The model credential stays per stack instance for 10-27. Per-agent model tokens wait for a proxy
|
||||
row.
|
||||
|
||||
Open for Jason. Mos takes these to him one at a time:
|
||||
|
||||
- Q-J1. The model account for Case 1 and Case 2: existing subscription or a new account.
|
||||
- Q-J2. Gitea users and tokens for the agents that need them (A1 to A3).
|
||||
|
||||
## 10. What this closes (brief item 7)
|
||||
|
||||
- G11: closes the store (D1 to D6) and lifecycle (section 6) for `gitea` and `vikunja`. Still open: per-agent
|
||||
model credentials (D7), and the same-uid limit inside one stack instance (D6).
|
||||
- G13: closes "own credentials for git" (D8 to D10). Still open: Gitea API verbs (section 4).
|
||||
- C2.5 (git remote): Case 2's books repo can use `publish` once its project names a remote.
|
||||
|
||||
## 11. Tests and mutants
|
||||
|
||||
All tests use scratch roots, fake tokens and a fake Gitea: `git http-backend` behind a node http server on
|
||||
127.0.0.1 that checks basic auth against a fake token and records each ref update. No request leaves the host.
|
||||
|
||||
- T1. Layout: wrong mode or owner on a dir or file refuses start. A `{file}` reference into another agent's
|
||||
path refuses `credential-not-yours`. A bus restart after a create without `issue` starts, and the agent's
|
||||
status is `not-issued`. A missing `{file}` still refuses start.
|
||||
- T2. `useFor` keys from the cap only. A request naming another agent is refused and leaves one
|
||||
`action.refused`.
|
||||
- T3. The fake Gitea serves https on 127.0.0.1 with a cert from a scratch CA made by the test. It reaches git
|
||||
only through the `tls` parameter (D9 step 5). Each refusal test that should reach the network asserts that
|
||||
the first port received the request, so no test passes because git refused http first. `publish`: the
|
||||
happy path lands `refs/heads/<user>/<branch>` on the agent's fork in the fake, which shares root's history, at
|
||||
exactly the decision's commit. Refusals for each step in D9, one test each, including `work-not-yours`. Until routing, a publish with
|
||||
no decision refuses, one with a decision for another task or commit refuses, and a second publish on a
|
||||
spent decision refuses `decision-consumed`. Approve while the latest push is A, push B, publish: refuses
|
||||
`decision-mismatch`, and the fork receives nothing. A push to the working repo between `checkPublish` and
|
||||
the git push refuses with `ref-moved` or `decision-mismatch` (sage-1440, BUILD.md deviation 14). A raise with no recorded push refuses `no-push`. A test pins that `git.push.remote` is in neither ROUTED list
|
||||
and that no `roles/*.json` grants it. A force-push attempt is
|
||||
refused by git's non-fast-forward. A fake fork that answers 302 to a second 127.0.0.1 port: the push
|
||||
fails, and the second port receives no request with an Authorization header. A remote on the right host
|
||||
and a different port is refused at step 2. Two agents granted one project each land on their own fork:
|
||||
agent A's push reaches `/<A's user>/REPO` only and B's reaches `/<B's user>/REPO` only, and the fake's
|
||||
upstream repository receives nothing. An upstream owned by the agent's own user refuses `remote-not-fork`,
|
||||
and so does one whose owner differs from `gitea.user` only in case. With an upstream that shares root's
|
||||
history, a first publish to an empty fork and to an unrelated fork each refuse `remote-unrelated`, and
|
||||
neither fork nor upstream receives objects. A fork that doesn't exist refuses `fork-unavailable`, and the
|
||||
fake records no push. `ls-remote` and the push each reach the fork's path only (the fake records every
|
||||
path). Load refusals (D9c), one test each: a dot segment, an extra path segment, a missing segment,
|
||||
a trailing slash, userinfo, a query, a fragment, an explicit `:443`, a name outside Gitea's rule, and a `gitea.url` with a
|
||||
sub-path. A `.git` suffix loads and builds the fork without it. A `publish` grant on an agent without
|
||||
`identity`, and on one whose definition lacks `gitea`, each refuse at load with the N10 message.
|
||||
- T4. Token handoff: the token never appears in argv, env, the fork URL, the working repo config,
|
||||
`changes.jsonl`, events or logs (scan every file under the scratch roots).
|
||||
- T5. Lifecycle: issue, rotate, revoke and delete, each live with no restart. After revoke, `publish` and the
|
||||
Vikunja path refuse `credential-unavailable`, and so they do after a bus restart that follows the revoke.
|
||||
The tester can run `status` and `revoke`, and `issue` and `rotate` refuse it.
|
||||
- T6. Default: an agent without `identity` commits as E7. Existing `{file}` references load unchanged.
|
||||
|
||||
Mutants to kill: drop the cap-derived key in `useFor`, let the request name the destination ref, add `+` to
|
||||
the refspec, skip the identity check, put the token in the URL, revoke file before memory, skip the dir mode
|
||||
check, allow a `{file}` reference into another agent's dir, make ENOENT on a `store` path fatal again, make
|
||||
ENOENT on a `{file}` path silent, a helper that answers any host, drop `http.followRedirects=false`, drop the
|
||||
`work-not-yours` check, drop the `remote-unrelated` check, add `git.push.remote` to ROUTED, skip the spent
|
||||
check on the decision, drop the `pushCommit` comparison in `#authorityFor`, push by ref name in place of the
|
||||
oid, push to the upstream URL in place of the derived fork, run `ls-remote` against the upstream in place of
|
||||
the fork, take the fork's owner from anywhere but the agent's `gitea.user`, compare the owner with case, run the
|
||||
D9c checks on the raw path in place of the normalized one, read a failed `ls-remote` as empty, skip the tip
|
||||
check under the lock, and pass a CA flag in production.
|
||||
|
||||
## 12. Risks
|
||||
|
||||
- R1. Backups: if a backup tool copies `<configDir>`, it copies tokens. The build checks row 185's backup set
|
||||
and says what it includes.
|
||||
- R2. Withdrawn. O1 was ruled, so probe P1 isn't needed.
|
||||
- R3. Outbound HTTPS from the bus host to Gitea is new network reach for the host process. On the VMs this is
|
||||
Jason's network to confirm.
|
||||
@@ -0,0 +1,75 @@
|
||||
# Row 237: one decision publishes once (#1743)
|
||||
|
||||
Megavolt, 2026-10-21 (from `date -u`). Brief: `docs/plans/2026-10-20_agent-credentials.md`
|
||||
§ "row 199 follow-up N1", blob dca67d310e1b. Source: Duckworth's row 199 round 1 note N1
|
||||
(duckworth-0068). Base: `cb5fdb064` (row 199's landing). Reviewer Duckworth, gate owner Sage.
|
||||
|
||||
## The defect
|
||||
|
||||
`checkPublish` opens a ticket after checking the decision is unspent. The decision is spent only at
|
||||
`recordPublish`, which runs after the bus process has pushed. Two `checkPublish` calls on one decision
|
||||
therefore both get tickets, and both push. With different branches, the commit lands on two fork refs:
|
||||
`refs/heads/<user>/feat` and `refs/heads/<user>/other`. The second `recordPublish` then refuses
|
||||
`decision-consumed`, but by then its push has already reached the fork.
|
||||
|
||||
## The fix, and why this option
|
||||
|
||||
`checkPublish` refuses `publish-busy` when an open ticket in `#publishes` holds the same business and
|
||||
decision (`packages/bus/src/broker.mjs:1038-1040`). It runs after the authority and fact checks, so a
|
||||
spent decision still refuses `decision-consumed` first.
|
||||
|
||||
The brief's other option, rechecking `#spent` in `publishTarget`, doesn't close the race. Both tickets
|
||||
reach `publishTarget` before either push ends, and nothing is spent until `recordPublish`. So both
|
||||
rechecks pass and both pushes run. The only state that exists while the first push runs is the open
|
||||
ticket, so the check reads that.
|
||||
|
||||
A refusal (`publishRefused`) drops the ticket and leaves the decision unspent, so a retry on the same
|
||||
decision gets a new ticket. That is row 199's behaviour, unchanged. A within-role publish has no
|
||||
decision and isn't affected.
|
||||
|
||||
## Tests
|
||||
|
||||
- `packages/bus/tests/publish.test.mjs:186`. A second ticket refuses `publish-busy`, on the same branch
|
||||
and on another. After `publishRefused` the retry gets a ticket and a third refuses busy. After
|
||||
`recordPublish` there is one `action.allowed` for git.push.remote and the next call refuses
|
||||
`decision-consumed`. Another agent's decision on the same task isn't held.
|
||||
- `packages/cli/tests/publish-socket.test.mjs:269`. End to end through the git socket, the bus runtime
|
||||
and the TLS fake Gitea. While the first publish holds its ticket, a second publish on the same
|
||||
decision to branch `other` gets `publish-busy`. The fake Gitea gets exactly one `git-receive-pack`,
|
||||
the fork has only `refs/heads/coder/feat`, and there is one `action.allowed`.
|
||||
|
||||
Runs on this branch, single files:
|
||||
- publish.test.mjs: 13/13.
|
||||
- publish-socket.test.mjs: 8/8.
|
||||
|
||||
## Mutant
|
||||
|
||||
M1 removes the `publish-busy` line. Both tests fail:
|
||||
- The bus test fails at the first busy assert.
|
||||
- The cli test fails because the second publish answers `ok: true` with `ref: refs/heads/coder/other`.
|
||||
That is the defect: two refs on the fork.
|
||||
|
||||
Logs are in `/mnt/storage/scratch/megavolt-r237/` (m1-bus.log, m1b-cli.log). `cmp` confirmed
|
||||
broker.mjs matched the saved copy after the restore. The first cli mutant run (m1-cli.log) was against
|
||||
a hook that recursed on every checkPublish. I fixed the hook to fire only for branch `feat` and reran
|
||||
it (m1b-cli.log).
|
||||
|
||||
## Residuals, not fixed here
|
||||
|
||||
1. **A ticket that never ends holds its decision until the bus restarts.** `#publishes` is in memory.
|
||||
That happens when the host dies between checkPublish and recordPublish or publishRefused. It fails
|
||||
closed (`publish-busy`), and a restart clears it.
|
||||
2. **publish-timeout (sage-1440).** The socket files `publishRefused` and drops the ticket, but the bus
|
||||
process's push may still be running. A retry on the same decision can then open a ticket while that
|
||||
push is live. This is the same residual the socket already logs ("the bus process's push may still
|
||||
reach the fork"). Closing it means the runtime's `remotes` must keep the decision busy until its push
|
||||
settles, which reaches into runtime.mjs. That is beyond the brief's two options.
|
||||
3. **checkLand has the same shape.** `#landings` takes a ticket per call and the decision is spent at
|
||||
`recordLand`. Whether two landings on one decision can both reach main isn't checked here. That's a
|
||||
follow-up for Sage to row or dismiss.
|
||||
|
||||
## Gate
|
||||
|
||||
Per the brief: Duckworth approves the manifest, the bus suite and every `scripts/test-*.sh` are green
|
||||
on Sage's gate, and M1 is killed. This row also changes a cli test file, so the cli suite (or that file
|
||||
alone) should run on the gate too.
|
||||
@@ -0,0 +1,71 @@
|
||||
# Row 241: one decision lands once (#1747, P19 441)
|
||||
|
||||
Megavolt, 2026-10-21 (from `date -u`). Brief: `docs/plans/2026-10-20_agent-credentials.md`
|
||||
§ "Brief: row 237 follow-up, one decision lands once, with tests at the broker", c25785d3d. Source:
|
||||
row 237 residual 3, megavolt-0068, megavolt-0069 and sage-1651. Base: origin/refactor f5753c0e8, row 210's landing.
|
||||
Reviewer Duckworth, gate owner Sage. Tests only; no source changes.
|
||||
|
||||
## Why landing has no row 237 race
|
||||
|
||||
Row 237 fixed publish: two tickets on one decision both pushed, because the decision is spent only
|
||||
after the push, and a push to a remote fork can't be taken back. Landing has the same ticket shape
|
||||
(`checkLand` opens a ticket, `recordLand` spends the decision), but three guards keep one decision
|
||||
from landing twice:
|
||||
|
||||
1. **land-busy, the outer guard.** `doLand` in `packages/cli/src/git-socket.mjs:351` refuses
|
||||
`land-busy` while another landing runs. The slot is taken before the first await, and freed only
|
||||
when the landing ends. There is one git socket per launcher, and one host per dataRoot (host.lock).
|
||||
So through the socket two landings never overlap, and a later one stops at `checkLand` with
|
||||
`decision-consumed`. Even past that, applying the same reviewed patch to the new main fails
|
||||
`base-conflict` in `prepareLand`.
|
||||
2. **The CAS on main.** `finishLand` (`packages/seat/src/working.mjs:391`) moves main with
|
||||
`update-ref main <commit> <old>`, so a landing prepared on a tip that has since moved refuses
|
||||
`base-moved` and writes nothing.
|
||||
3. **The recordLand recheck.** `finishLand` calls `record()`, which is `recordLand`, under the same
|
||||
repository lock. `recordLand` runs `#landFacts` again (`packages/bus/src/broker.mjs:963`), which
|
||||
refuses `decision-consumed` once the decision is spent. When `record()` throws, `finishLand` moves
|
||||
main back.
|
||||
|
||||
Guards 2 and 3 can't be reached end to end, because guard 1 stops a second landing first. So they are
|
||||
pinned below the socket, and no test bypasses land-busy.
|
||||
|
||||
### The unrecorded-commit window
|
||||
|
||||
Between the `update-ref` and the end of `record()`, main names the squash, which isn't recorded yet.
|
||||
That window is under the repository's lock, so no other push, review or landing in this host sees
|
||||
it. If `record()` refuses, main moves back. If moving back fails, `finishLand` refuses
|
||||
`rollback-failed` and main keeps naming an unrecorded commit (tested at `working.test.mjs:555`, the
|
||||
main.lock case). That is existing behaviour, unchanged here.
|
||||
|
||||
## The test
|
||||
|
||||
`packages/bus/tests/land.test.mjs:312`. Two tickets pass `checkLand` on one decision before either
|
||||
records. `recordLand` on the first lands. `recordLand` on the second refuses `decision-consumed`
|
||||
(:319), and leaves one merge `action.allowed`, one RESULT and no `action.refused`. `landRefused`, as
|
||||
the socket would call it, then files the refusal and ends the ticket.
|
||||
|
||||
**The assert that pins the recordLand recheck is :319.** The `#spent` line is shared with
|
||||
`checkLand`, so dropping it also fails :144 (a `checkLand` after the decision is spent). Only :319
|
||||
shows the recheck at record time.
|
||||
|
||||
Single file on this branch: land.test.mjs 18/18.
|
||||
Whole suites on this branch before in-review: bus 500/500, seat 164/164, cli 565/565 (02:52 to 03:01Z).
|
||||
|
||||
## Mutants
|
||||
|
||||
| Mutant | Change | Killed by |
|
||||
|---|---|---|
|
||||
| M1 | drop the `#spent` line in `#landFacts` (broker.mjs:963) | land.test.mjs: :312 at :319, and :94 at :144 (16/2) |
|
||||
| M2 | drop `land.old` from the `update-ref` in `finishLand` (working.mjs:391) | working.test.mjs:555 at :567, the `base-moved` assert (25/1) |
|
||||
| M3 | drop the `land-busy` throw (git-socket.mjs:351) | git-socket.test.mjs:812 at :825, the `land-busy` assert (23/1) |
|
||||
|
||||
Each run was one test file on base f5753c0e8 (row 210's landing, which moved the `#spent` line from
|
||||
:945 to :963), 2026-10-21 02:51Z. Each assert failed with "Missing expected exception" or "rejection".
|
||||
`cmp` confirmed each source matched its saved copy after the restore. M2 is killed by the existing
|
||||
working.test:555, so per the brief working.test.mjs isn't changed. Logs: `/mnt/storage/scratch/megavolt-r241/`
|
||||
(m1.log to m3.log, with each diff).
|
||||
|
||||
## Gate
|
||||
|
||||
Per the brief: Duckworth approves the manifest; the bus, seat and cli suites and every
|
||||
`scripts/test-*.sh` are green on Sage's gate; the three mutants are killed.
|
||||
@@ -0,0 +1,108 @@
|
||||
# Row 242 build: a retry after publish-timeout pushes once
|
||||
|
||||
Row 242, #1748, P19 440. Brief: docs/plans/2026-10-20_agent-credentials.md, "Brief: row 237
|
||||
follow-up N1, a retry after publish-timeout pushes once" (9d165be8c). Base: origin/refactor
|
||||
8fede5fc6 (row 210 landed f5753c0e8).
|
||||
|
||||
## The defect
|
||||
|
||||
The git socket polls publishResult until publishDeadlineMs, then refuses publish-timeout
|
||||
(packages/cli/src/git-socket.mjs:433) while the bus process's push (runtime publishPush) can
|
||||
still be running. The socket then calls publishRefused, which went straight to
|
||||
broker.publishRefused: it filed the refusal and deleted the ticket. checkPublish no longer saw an
|
||||
open ticket on the decision, so a retry got a new ticket and pushed a second ref while the first
|
||||
push was still on its way to the fork.
|
||||
|
||||
## Fix chosen: hold the decision until the push ends, then record a push that reached the fork
|
||||
|
||||
packages/bus/src/runtime.mjs, `publishRefused` and `settle`:
|
||||
|
||||
- If the bus has no push for the ticket, publishRefused goes to the broker as before.
|
||||
- If the push is still running, the refusal code is held on the entry and the ticket stays open.
|
||||
checkPublish refuses another publish on the decision with publish-busy (row 237's check),
|
||||
the same as when the socket is still polling.
|
||||
- When the push ends, `settle` runs:
|
||||
- A push that reached the fork is recorded with broker.recordPublish, which spends the
|
||||
decision. The bus logs `pushed after the git socket refused <code>; recorded`.
|
||||
- A push that failed is filed as publishRefused with the socket's code.
|
||||
- If recordPublish refuses with a BusError, that code is filed instead.
|
||||
- If the push has already ended when the refusal arrives, it's settled at once. The sage-1440 test
|
||||
relies on this: its push fails fast, so publish-timeout is filed once, immediately, as before.
|
||||
|
||||
Why hold rather than kill. Killing the git client doesn't undo a push the fork has already
|
||||
received: receive-pack can update the ref after the client dies. After a kill the bus would have
|
||||
to ls-remote the fork to learn what landed, and the result could still race the server. Holding
|
||||
waits for the push's own outcome. Each git run in publishRemote already has its own limit
|
||||
(publishRemote defaults to checkout.mjs GIT_LIMITS), so a hold is bounded. Holding also reuses the existing publish-busy
|
||||
state, and the refusal text is unchanged.
|
||||
|
||||
Why record and not only hold. If the hold just filed publish-timeout once the push ended, the
|
||||
ticket would close and the decision would be free again with a ref on the fork. A retry would then
|
||||
push a second ref. Recording the late push spends the decision, so the retry gets
|
||||
decision-consumed.
|
||||
|
||||
The publish-timeout refusal text is unchanged (brief condition).
|
||||
|
||||
## Test
|
||||
|
||||
packages/cli/tests/publish-socket.test.mjs:302, "row 242: a retry after publish-timeout refuses
|
||||
publish-busy while the bus process's push runs; the late push is recorded and the decision pushes
|
||||
once".
|
||||
|
||||
- publishDeadlineMs 150 and a store token. The fork's pre-receive hook writes `held`, then loops
|
||||
until the test writes `release`.
|
||||
- The first publish gives publish-timeout. The test waits for `held`.
|
||||
- A retry to branch `other` gives publish-busy (:322). Refused events: ["publish-busy"]. No
|
||||
git.push.remote allowed event.
|
||||
- The test writes `release` and waits until there is exactly one allowed event, with ref
|
||||
refs/heads/coder/feat and the work commit.
|
||||
- The next retry gives decision-consumed (:330).
|
||||
- Exactly one git-receive-pack request reached the fake Gitea, and the fork has only
|
||||
refs/heads/coder/feat. Refused events: ["publish-busy", "decision-consumed"]. No TOKEN appears in
|
||||
events or logs.
|
||||
|
||||
Fail fast (sage-1681): the body is wrapped in try/finally, and the finally writes `release`. Under
|
||||
the first mutant run, the assert at the retry failed and the hook was never released. setup's
|
||||
teardown waits for the fake Gitea, which waited for the held push, so the file hung until I stopped
|
||||
it. Now a failed assert ends the hook and the run. Each wait is bounded at 10 s with an assert.fail.
|
||||
|
||||
## Mutants
|
||||
|
||||
Script: /mnt/storage/scratch/megavolt-r242/mutants.sh. Each mutant: save, one-line sed, run
|
||||
publish-socket.test.mjs whole, restore, cmp.
|
||||
|
||||
| Mutant | Change | Expected kill |
|
||||
|---|---|---|
|
||||
| M1 | export `publishRefused: (ticket, args) => broker.publishRefused(ticket, args)` again (removes the fix) | :302 at :322: the retry gets a new ticket and its own push, so publish-timeout, not publish-busy (seen in the stopped 03:06Z run) |
|
||||
| M2 | `settle` files publishRefused without trying recordPublish (hold only) | :302 at the wait for one allowed event, then :330 |
|
||||
|
||||
Results (Sage's slot, sage-1686, 2026-10-21T03:35:00Z to 03:35:15Z): both killed, both restored
|
||||
(cmp ok).
|
||||
|
||||
- M1: publish-socket 8/1, the row 242 test fails at :322 in 0.5 s.
|
||||
- M2: publish-socket 8/1, the row 242 test fails at :327, the wait for one allowed event, through
|
||||
the assert.fail inside `until` (:311) after its 10 s bound, in 10.3 s.
|
||||
|
||||
## Suites
|
||||
|
||||
On Sage's slot (sage-1686), on base 8fede5fc6 plus this row:
|
||||
|
||||
- bus `node --test tests/*.test.mjs`: 499/499, 03:35:20Z to 03:35:34Z. This is one fewer than
|
||||
Sage's 241 gate (500) because the base predates row 241's land.test case.
|
||||
- cli `node --test tests/*.test.mjs`: 566/566, 03:35:34Z to 03:43:47Z. That's 565 plus the
|
||||
row 242 test.
|
||||
|
||||
Singles before the slot: publish-socket.test.mjs 9/9, bus publish.test.mjs 13/13. The seat suite
|
||||
and scripts/test-*.sh are left for Sage's landing gate.
|
||||
|
||||
## Residuals
|
||||
|
||||
- The agent was told publish-timeout, but the push is recorded later and the run isn't
|
||||
notified. The allowed event and the bus log line are the only trace. A later publish on the
|
||||
decision gets decision-consumed, which tells the agent the decision was used. Whether the run
|
||||
should get a RESULT for a late record is a follow-up question for Sage.
|
||||
- A late push whose recordPublish refuses (Duckworth N2) is filed with that code: the ref stays on
|
||||
the fork and the decision is not spent, the same residual as an in-band push whose record
|
||||
refuses.
|
||||
- The hold lives in the bus process's memory, like the broker's open tickets. A bus restart during
|
||||
a hold drops both, the same as today for any publish in flight.
|
||||
@@ -0,0 +1,7 @@
|
||||
# Morgana
|
||||
|
||||
Builder seat, Claude Opus 5.5, started by Sage for MVP T1 row 193 (human users and RBAC, #1691) under mos-0974 and mos-0977.
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no
|
||||
credentials of its own; until the credentials row settles per-agent identity, it pushes only its own `morgana/*` branches
|
||||
through the repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch.
|
||||
@@ -0,0 +1,10 @@
|
||||
# Quackerjack
|
||||
|
||||
Builder seat, Claude Haiku 5.5 at high effort, started by Sage under mos-1023 item 1 and Q177 for federation
|
||||
rows R5 (`mosaic access list, grant, revoke`) and R2 (the `federation` config key), per Gizmoduck's design
|
||||
section 7 (#414).
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no
|
||||
credentials of its own; until the credentials row settles per-agent identity, it pushes only its own
|
||||
`quackerjack/*` branches through the repository credential helper (mos-0981), never `refactor`, `main` or
|
||||
another seat's branch.
|
||||
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
n=0
|
||||
for p in /proc/[0-9]*; do
|
||||
c=$(tr '\0' ' ' < $p/cmdline 2>/dev/null) || continue
|
||||
case "$c" in *shell-snapshots*|*chrome-devtools-mcp*|*sage-busy-now*) continue;; esac
|
||||
if echo "$c" | grep -qE 'node --test|gate\.sh|mutant|scripts/test-[a-z-]+\.sh|docker compose'; then echo "BUSY ${p#/proc/}: ${c:0:160}"; n=$((n+1)); fi
|
||||
done
|
||||
[ $n -eq 0 ] && echo CLEAR
|
||||
Executable
+24
@@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
set -o pipefail
|
||||
# Landing gate template (2026-10-14): sed the G= and OUT= lines and this comment.
|
||||
# Sequential suites, each teed to its own log; stops at the first failure.
|
||||
G=/mnt/storage/scratch/SET-ME-wt
|
||||
OUT=/mnt/storage/scratch/SET-ME-out
|
||||
cd "$G" || exit 9
|
||||
mkdir -p "$OUT" || exit 9 # a missing OUT failed the row 142 gate at step 1 (2026-10-15)
|
||||
# test-task.sh live cases run `docker compose` from $G, so without a name the project is "wt"
|
||||
# and leaves wt_default behind (row 142 gate, 2026-10-15). Name it per gate dir and remove it on exit.
|
||||
export COMPOSE_PROJECT_NAME="$(basename "$(dirname "$G")")"
|
||||
trap 'docker network rm "${COMPOSE_PROJECT_NAME}_default" >/dev/null 2>&1 && echo "removed network ${COMPOSE_PROJECT_NAME}_default"' EXIT
|
||||
# A gate tree without the node_modules link fails cli tests with ENOENT (2026-10-13).
|
||||
[ -d "$G/node_modules" ] || { echo "STOP no node_modules in $G"; exit 8; }
|
||||
echo "base $(git rev-parse --short HEAD) dirty $(git status --short | grep -vc node_modules) DOCKER_HOST=${DOCKER_HOST:-unset}"
|
||||
run() { local name="$1" dir="$2" cmd="$3"
|
||||
(cd "$G/$dir" && bash -c "$cmd") > "$OUT/$name.log" 2>&1; local rc=$?
|
||||
echo "$name rc=$rc $(grep -E '^ℹ (pass|fail) [0-9]+' "$OUT/$name.log" | tr '\n' ' ') | $(tail -n1 "$OUT/$name.log")"
|
||||
[ $rc -eq 0 ] || { echo "STOP $name"; exit $rc; }; }
|
||||
# No pkg-queue step: scripts/test-queue.sh runs packages/queue/tests/ itself (same 156 tests; CI debate item 2a, Vikunja #274).
|
||||
for d in packages/*/; do p=$(basename "$d"); [ -d "$d/tests" ] || continue; [ "$p" = queue ] && continue
|
||||
run "pkg-$p" "packages/$p" 'node --test tests/*.test.mjs'; done
|
||||
for s in scripts/test-*.sh; do n=$(basename "$s" .sh); run "script-${n#test-}" . "bash $s"; done
|
||||
echo GATE_PASSED
|
||||
@@ -0,0 +1,36 @@
|
||||
# Row 189 gate timing (for row 197, Herb)
|
||||
|
||||
Sage, 2026-10-20T19:25:04Z. Gate on 0c177e9ef (base 0217b71c8), one lane. The slot was mine. Seats may have run single test files at the same time, which the slot rules allow, and I did not record that.
|
||||
Start 2026-10-20T19:09:37Z, end 2026-10-20T19:20:36Z. Total wall time: 659 s.
|
||||
|
||||
Wall time per suite is the gap between one suite's log mtime and the one before it (the first suite starts at start.txt), so it includes node start-up. node's duration_ms is listed where the suite prints it. Scripts print no duration.
|
||||
|
||||
| Suite | Wall s | duration_ms s | Ended (UTC) |
|
||||
|---|---|---|---|
|
||||
| pkg-bus | 3 | 3.8 | 19:09:40 |
|
||||
| pkg-business | 3 | 2.2 | 19:09:43 |
|
||||
| pkg-cli | 388 | 388.5 | 19:16:11 |
|
||||
| pkg-control-board | 1 | 0.8 | 19:16:12 |
|
||||
| pkg-conversation | 35 | 35.4 | 19:16:47 |
|
||||
| pkg-discord | 3 | 2.6 | 19:16:50 |
|
||||
| pkg-harness | 12 | 11.6 | 19:17:02 |
|
||||
| pkg-ledger | 13 | 13.5 | 19:17:15 |
|
||||
| pkg-mosaic | 3 | 2.3 | 19:17:18 |
|
||||
| pkg-plugins | 0 | 0.3 | 19:17:18 |
|
||||
| pkg-project | 1 | 1.2 | 19:17:19 |
|
||||
| pkg-runs | 1 | 0.8 | 19:17:20 |
|
||||
| pkg-seat | 6 | 6.2 | 19:17:26 |
|
||||
| pkg-tasks | 1 | 1.2 | 19:17:27 |
|
||||
| pkg-webui | 58 | 57.9 | 19:18:25 |
|
||||
| script-auth | 1 | - | 19:18:26 |
|
||||
| script-conductor | 5 | - | 19:18:31 |
|
||||
| script-config | 2 | - | 19:18:33 |
|
||||
| script-discord | 8 | - | 19:18:41 |
|
||||
| script-extension-package | 1 | - | 19:18:42 |
|
||||
| script-foundation | 40 | - | 19:19:22 |
|
||||
| script-land-work | 11 | - | 19:19:33 |
|
||||
| script-queue | 21 | - | 19:19:54 |
|
||||
| script-release | 11 | - | 19:20:05 |
|
||||
| script-task | 31 | - | 19:20:36 |
|
||||
|
||||
Source: /mnt/storage/scratch/sage-g189/out/*.log mtimes and gate.log. The cli suite is about 59% of the run, and webui plus conversation add about 14%.
|
||||
@@ -0,0 +1,8 @@
|
||||
# PRD 1.8 r7: no in-Stack raise (mos-1154)
|
||||
|
||||
Sage, 2026-10-21T01:20:06Z. Jason approved PRD 1.8 in T3 (thread 38bc4c81, before 2026-10-21T01:16:55Z), verbatim:
|
||||
"PRD 1.8: I am preemptively approving it. Do not surface in the Mosaic Stack board. Just proceed."
|
||||
Mos records it as DECISIONS Q192 (mos-1154). The [mos test] raise request drafted here was dropped
|
||||
unsent, and the template is deleted. Jason closed 3a6c8fb9 himself at 01:05:45Z (no, "superseded by r7").
|
||||
Scope (Mos's reading): r7 at 47c2cb33c plus 82df17347, 342 lines, sha256
|
||||
de079281ff2760911039276379ec4b190287b2ce2e4e8a79c9459939727440cd, Duckworth duckworth-0073 (pass, no findings).
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"seq": 22,
|
||||
"id": "3a6c8fb9-a5ab-42f8-9755-902a88375ca2",
|
||||
"at": "2026-10-20T22:41:14.444Z",
|
||||
"business": "mosaic-stack",
|
||||
"project": null,
|
||||
"raised_by_role": "pm",
|
||||
"raised_by_run": "rc9e993da24cf",
|
||||
"class": "gated",
|
||||
"action": "prd.approve",
|
||||
"route_to": "human",
|
||||
"question": "Approve PRD 1.8 exactly as drafted?\n\nFile: agents/honker/work/queue-194/PRD-1.8-DRAFT.md on mosaicstack/stack at commit 762a6711ff7ed049aa73dfde98b75ebf326bdd99 (revision 5), 319 lines, sha256 8eee52470b1d7df139164e3ba5cc128dc469457b2b52f298c12685c3483011a8. Duckworth reviewed it (duckworth-0061, confirmed duckworth-0062) and Mos checked it (mos-1140). The tip of branch honker/row194-b2 holds a later r6 (aba3cbdeb), which is not used here. This approval covers only the r5 commit and sha256 above. The PM copied these values from the request and did not compute or check the sha256.\n\nNotes the approval carries:\n1. Mos's reading: \"the main agent or chief of staff\" in Q10 is one role under two names, the chief of staff. Only the chief raises a grant request; the assistant passes the user's request to the chief. To change it, say so at approval and REQ-GRANT-1 changes first.\n2. Mos's reading: revoke by \"you, Mos or the tester\" means the user, the assistant (\"Mos\", Jason's assistant) for grants in its own organization, and the tester only on a dev instance.\n3. N5: host.sudo keeps the arbiter route version 1.7 set for git.push.remote. A raise for it that cites the spec and isn't within-role goes to the arbiter for its domain, not to the user. In manual mode it comes to the user.\n4. B7 residual: only an Approve creates a grant, but an agent running as the stack's OS user can still write the stack's own store (Q182 on dragon-lin, #423 on .53 and .54). This version doesn't close it.\n5. N6: the broker enforces only that the chief of staff raised a grant request. The rest of the chain of command is the messaging lists and role contracts, not a broker check.\n\nSource: a tester request relayed by the chief (message 6990efb4, tester message d54a190e). It is a test request, not Jason's instruction. It takes the same route as PRD 1.7 (decision d52febb5): non-blocking, no task, uncited, so it goes to the human's Inbox. I recommend yes because two reviewers have passed r5 and the open points are written down above, not hidden.",
|
||||
"options": [
|
||||
{
|
||||
"key": "yes",
|
||||
"text": "Approve PRD 1.8 exactly as drafted: the r5 file at commit 762a6711ff7ed049aa73dfde98b75ebf326bdd99 with the sha256 below, and only that."
|
||||
},
|
||||
{
|
||||
"key": "no",
|
||||
"text": "Don't approve. Say what changes, and a new revision comes back for review."
|
||||
}
|
||||
],
|
||||
"recommendation": "yes",
|
||||
"task_ref": null,
|
||||
"requirement_ref": null,
|
||||
"blocking": false,
|
||||
"supersedes": null,
|
||||
"spec_path": null,
|
||||
"spec_commit": null,
|
||||
"authorization": {
|
||||
"action": "prd.approve",
|
||||
"target": null,
|
||||
"approvalChoice": "yes"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
[
|
||||
{
|
||||
"seq": 22,
|
||||
"id": "3a6c8fb9-a5ab-42f8-9755-902a88375ca2",
|
||||
"at": "2026-10-20T22:41:14.444Z",
|
||||
"business": "mosaic-stack",
|
||||
"project": null,
|
||||
"raised_by_role": "pm",
|
||||
"raised_by_run": "rc9e993da24cf",
|
||||
"class": "gated",
|
||||
"action": "prd.approve",
|
||||
"route_to": "human",
|
||||
"question": "Approve PRD 1.8 exactly as drafted?\n\nFile: agents/honker/work/queue-194/PRD-1.8-DRAFT.md on mosaicstack/stack at commit 762a6711ff7ed049aa73dfde98b75ebf326bdd99 (revision 5), 319 lines, sha256 8eee52470b1d7df139164e3ba5cc128dc469457b2b52f298c12685c3483011a8. Duckworth reviewed it (duckworth-0061, confirmed duckworth-0062) and Mos checked it (mos-1140). The tip of branch honker/row194-b2 holds a later r6 (aba3cbdeb), which is not used here. This approval covers only the r5 commit and sha256 above. The PM copied these values from the request and did not compute or check the sha256.\n\nNotes the approval carries:\n1. Mos's reading: \"the main agent or chief of staff\" in Q10 is one role under two names, the chief of staff. Only the chief raises a grant request; the assistant passes the user's request to the chief. To change it, say so at approval and REQ-GRANT-1 changes first.\n2. Mos's reading: revoke by \"you, Mos or the tester\" means the user, the assistant (\"Mos\", Jason's assistant) for grants in its own organization, and the tester only on a dev instance.\n3. N5: host.sudo keeps the arbiter route version 1.7 set for git.push.remote. A raise for it that cites the spec and isn't within-role goes to the arbiter for its domain, not to the user. In manual mode it comes to the user.\n4. B7 residual: only an Approve creates a grant, but an agent running as the stack's OS user can still write the stack's own store (Q182 on dragon-lin, #423 on .53 and .54). This version doesn't close it.\n5. N6: the broker enforces only that the chief of staff raised a grant request. The rest of the chain of command is the messaging lists and role contracts, not a broker check.\n\nSource: a tester request relayed by the chief (message 6990efb4, tester message d54a190e). It is a test request, not Jason's instruction. It takes the same route as PRD 1.7 (decision d52febb5): non-blocking, no task, uncited, so it goes to the human's Inbox. I recommend yes because two reviewers have passed r5 and the open points are written down above, not hidden.",
|
||||
"options": [
|
||||
{
|
||||
"key": "yes",
|
||||
"text": "Approve PRD 1.8 exactly as drafted: the r5 file at commit 762a6711ff7ed049aa73dfde98b75ebf326bdd99 with the sha256 below, and only that."
|
||||
},
|
||||
{
|
||||
"key": "no",
|
||||
"text": "Don't approve. Say what changes, and a new revision comes back for review."
|
||||
}
|
||||
],
|
||||
"recommendation": "yes",
|
||||
"task_ref": null,
|
||||
"requirement_ref": null,
|
||||
"blocking": false,
|
||||
"supersedes": null,
|
||||
"spec_path": null,
|
||||
"spec_commit": null,
|
||||
"authorization": {
|
||||
"action": "prd.approve",
|
||||
"target": null,
|
||||
"approvalChoice": "yes"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,13 @@
|
||||
[mos test] Please have the PM raise a prd.approve decision for PRD 1.8, the same route as PRD 1.7 (decision d52febb5): blocking false, no task_ref, recommendation yes, gated to the human. The PM must not compute or change the sha256; it copies the values below.
|
||||
|
||||
Question: "Approve PRD 1.8 exactly as drafted?"
|
||||
File: agents/honker/work/queue-194/PRD-1.8-DRAFT.md on mosaicstack/stack at commit 762a6711ff7ed049aa73dfde98b75ebf326bdd99 (revision 5), 319 lines, sha256 8eee52470b1d7df139164e3ba5cc128dc469457b2b52f298c12685c3483011a8. Reviewed by Duckworth (duckworth-0061, confirmed duckworth-0062) and checked by Mos (mos-1140). The branch tip of honker/row194-b2 holds a later r6 (aba3cbdeb) that is not used; the approval covers only the r5 commit and sha256 above.
|
||||
|
||||
The decision notes must carry these five items:
|
||||
1. Mos's reading: "the main agent or chief of staff" in Q10 is one role under two names, the chief of staff. Only the chief raises a grant request; the assistant passes the user's request to the chief. To change it, say so at approval and REQ-GRANT-1 changes first.
|
||||
2. Mos's reading: revoke by "you, Mos or the tester" means the user, the assistant ("Mos", Jason's assistant) for grants in its own organization, and the tester only on a dev instance.
|
||||
3. N5: host.sudo keeps the arbiter route version 1.7 set for git.push.remote. A raise for it that cites the spec and isn't within-role goes to the arbiter for its domain, not to the user. In manual mode it comes to the user.
|
||||
4. B7 residual: only an Approve creates a grant, but an agent running as the stack's OS user can still write the stack's own store (Q182 on dragon-lin, #423 on .53 and .54). This version doesn't close it.
|
||||
5. N6: the broker enforces only that the chief of staff raised a grant request. The rest of the chain of command is the messaging lists and role contracts, not a broker check.
|
||||
|
||||
Reply with the decision id once raised. Do not resolve it.
|
||||
@@ -0,0 +1,8 @@
|
||||
sent d54a190e to chief
|
||||
chief (RESULT, re d54a190e) cc46a7b4:
|
||||
I passed your request to the pm without changing any of it, and launched the pm so it can act on it. No decision id yet, because the pm hasn't raised the decision.
|
||||
|
||||
- **Message to pm:** 6990efb4-8321-4d0e-9b8a-771f65625b83. It says the request comes from a tester and authorizes nothing, and it carries your PRD 1.8 values and the five notes as you wrote them.
|
||||
- **pm launch:** run rc9e993da24cf.
|
||||
|
||||
When the pm answers with the decision id, I'll send it to you.
|
||||
@@ -0,0 +1,5 @@
|
||||
trail 3a6c8fb9-a5ab-42f8-9755-902a88375ca2: 4 row(s)
|
||||
2026-10-20T22:40:47.949Z events#1550 session.launched pm
|
||||
2026-10-20T22:40:47.969Z role_claims#102 claim pm by rc9e993da24cf
|
||||
2026-10-20T22:41:14.444Z decisions#22 gated prd.approve → human: Approve PRD 1.8 exactly as drafted? File: agents/honker/work/queue-194/PRD-1.8-DRAFT.md on mosaicstack/stack at commit …
|
||||
2026-10-20T22:41:14.445Z events#1553 action.allowed pm decision.raise decision 3a6c8fb9
|
||||
@@ -1,8 +1,42 @@
|
||||
# Q175 design: the Console verifies SSO itself (Vikunja #412)
|
||||
|
||||
Author Sage, 2026-10-20. Reviewer Filbert, and the review comes before any code. Line numbers are at refactor
|
||||
4ca8d6e94. This replaces Part B (row 178 proxy proof, #385 comments 345 and 353). #385 stays open until the
|
||||
Console stops trusting `X-authentik-username`.
|
||||
Author Sage, 2026-10-20, r3. Reviewer Filbert, and the review comes before any code. Line numbers are at
|
||||
refactor 4ca8d6e94 unless a line says otherwise. This replaces Part B (row 178 proxy proof, #385 comments 345
|
||||
and 353). #385 stays open until the Console stops trusting `X-authentik-username`.
|
||||
|
||||
## Changes from r2 (Filbert's r2 review, CHANGES)
|
||||
|
||||
- C1: the `console` key goes into the shared loader, scripts/mosaic-config.mjs, which today refuses any
|
||||
unknown top-level key (:113). Section 4 names the blast radius. Jason step 4 moves inside the phase 2
|
||||
window, after the pin and before the Console starts. The phase 2 rollback removes `console` first. A
|
||||
test-config.sh case covers it (test 25). Mos confirms the sequence before phase 1 (section 5).
|
||||
- C2: under `--sso` the page treats a 401 `sign-in required` as signed out and links to
|
||||
`/auth/login?return=<path>` (section 5 phase 1, test 24).
|
||||
- Notes: tests 7 and 15 are built so that only the record rule refuses; test 9a is an alg test; Q-B's two
|
||||
classes are defined, and test 23 adds a 200 HTML page; the phase 2 scratch Console uses a scratch config,
|
||||
a dummy slot, no `--send` and no `--tester-port`.
|
||||
- r3 fix (Filbert r3 approve): D1, the loader checks the slot reference's shape only, and the file checks
|
||||
run in the Console's `--sso` start; D2, the slot resolves from the config dir, and the scratch Console has
|
||||
a scratch dataRoot and an unreachable issuer (section 5).
|
||||
- Q-E: settled as Filbert ruled. The `subject-unknown` line keeps iss and sub, JSON-escaped and capped
|
||||
(section 1, tests 10 and 19).
|
||||
|
||||
## Changes from r1 (Filbert's r1 review, CHANGES)
|
||||
|
||||
- B1: the provider config moves to `~/.config/mosaic-dev/config.json` (section 4). Mos agreed (mos-0973). The
|
||||
Console prints provider ids, issuers and subject counts at start. The rehearse-strip follow-up is dropped.
|
||||
- B2: one return-path rule, `new URL(ret, publicOrigin).origin === publicOrigin` (section 1 step 7), with
|
||||
tests for `/\evil.example` and `/%09/evil.example`.
|
||||
- B3: refusals log a fixed code and the provider id, never the error or its cause (section 1).
|
||||
- B4: login records are capped at 32 (section 1 step 1), with a test.
|
||||
- B5: test 9 splits into a forged payload under a real kid and a kid missing from the JWKS, and a try/catch
|
||||
mutant joins the list. Test 2 no longer claims the signature mutant (section 9).
|
||||
- B6: the phase 2 forged-header check runs on a scratch Console, or Mos or Jason runs it on live (section 5).
|
||||
- Rulings: Q-B start degraded (section 4), Q-C only the public Host on 7332 (section 3), Q-D keep 1 h idle
|
||||
but polls don't count as activity (section 3).
|
||||
- Section 6: `startServer` throws on `tester && sso`, and `human` joins `tester` as a refused subject label.
|
||||
- The missing tests and nits N1-N5 are in sections 1, 3, 7 and 9.
|
||||
- Section 8: the human-cli statement stays as written (mos-0972). The recommendation is now Mos's ruling.
|
||||
|
||||
## The ruling this implements
|
||||
|
||||
@@ -22,7 +56,9 @@ trust with verification" and confirmed five points (mos-0970):
|
||||
## What exists today
|
||||
|
||||
The live Console runs from copies/490e2771d3d0 with `--public-host mosaic.woltje.com --auth-header
|
||||
X-authentik-username --allow-user jason.woltje --send --tester-port 7333`.
|
||||
X-authentik-username --allow-user jason.woltje --send --tester-port 7333`. Row 189 (#1687, P19 #413) adds
|
||||
`stack-release.sh activate --no-send`, and Mos's ruling mos-0972 takes `--send` off the live Console until
|
||||
phase 2.
|
||||
|
||||
- `previewOptions` (packages/webui/src/serve.mjs:205) builds `{ host, header, user }`.
|
||||
- A request is `named` when its Host equals the public name (:493). A named request passes sign-in when it
|
||||
@@ -45,41 +81,73 @@ New module `packages/webui/src/sso.mjs`, used only by the human listener (7332).
|
||||
|
||||
| Route | Method | What it does |
|
||||
|---|---|---|
|
||||
| `/auth/login` | GET | One provider configured: start its flow. Several: a plain page listing them, each a link to `/auth/login?provider=ID`. |
|
||||
| `/auth/login` | GET | One provider configured: start its flow. Several: a plain page listing them, each a link to `/auth/login?provider=ID`. A provider whose discovery hasn't succeeded gets 503 (section 4). |
|
||||
| `/auth/callback` | GET | Finish the flow, create the session, 303 to the saved return path. |
|
||||
| `/auth/logout` | POST | Same-origin checks, delete the session, 303 to the provider's `end_session_endpoint`. |
|
||||
| `/auth/signed-out` | GET | Static "signed out" page with a sign-in link. |
|
||||
|
||||
Every URL the Console hands a provider is built from the configured `publicOrigin`: `redirect_uri` is
|
||||
`publicOrigin + '/auth/callback'` and `post_logout_redirect_uri` is `publicOrigin + '/auth/signed-out'`. No
|
||||
code path reads the request's Host to build them (N2).
|
||||
|
||||
The flow per provider:
|
||||
|
||||
1. Start. Generate a state, a nonce and a PKCE verifier (`generateRandomState`, `generateRandomNonce`,
|
||||
`generateRandomCodeVerifier`), and a challenge with `calculatePKCECodeChallenge` (S256 only). Put them, the
|
||||
provider id and the return path in a server-side login record keyed by a random id. Set
|
||||
`__Host-mosaic_login` to that id. 303 to `authorization_endpoint` with `response_type=code`,
|
||||
`scope=openid profile email`, `code_challenge_method=S256`, `redirect_uri`, `state` and `nonce`.
|
||||
provider id, the return path and an expiry 10 minutes out in a server-side login record keyed by a random
|
||||
id. The store holds at most 32 login records; a new one evicts the oldest (B4). Set `__Host-mosaic_login` to
|
||||
that id. 303 to `authorization_endpoint` with `response_type=code`, `scope=openid profile email`,
|
||||
`code_challenge_method=S256`, `redirect_uri`, `state` and `nonce`.
|
||||
2. Callback. Read exactly one `__Host-mosaic_login` cookie, then take and delete its record (single use, so a
|
||||
replayed callback finds nothing). Expire the cookie in the response. The provider comes from the record,
|
||||
never from the query, which stops a mix-up between providers.
|
||||
never from the query, which stops a mix-up between providers. Two tabs signing in at once each set the
|
||||
cookie, so the later one wins and the earlier tab's callback finds no record and gets the 403 page with a
|
||||
sign-in link (N5). That costs one retry and keeps one cookie per browser.
|
||||
3. `validateAuthResponse(as, client, url, record.state)` checks state, refuses an error response, and checks
|
||||
the `iss` parameter when the provider advertises RFC 9207.
|
||||
4. `authorizationCodeGrantRequest(as, client, clientAuth, params, redirectUri, record.verifier)`, then
|
||||
`processAuthorizationCodeResponse(as, client, response, { expectedNonce: record.nonce, requireIdToken: true })`.
|
||||
That checks iss, aud (and azp when aud has several values), exp, iat and nonce, and checks `alg` against
|
||||
`client.id_token_signed_response_alg`.
|
||||
4. `authorizationCodeGrantRequest(as, client, clientAuth, params, redirectUri, record.verifier)` returns a
|
||||
`Response`. Keep that object. `processAuthorizationCodeResponse(as, client, response, { expectedNonce:
|
||||
record.nonce, requireIdToken: true })` checks iss, aud (and azp when aud has several values), exp, iat and
|
||||
nonce, and checks `alg` against `client.id_token_signed_response_alg`. That field gets the provider's whole
|
||||
`algs` array, which the library's `checkSigningAlgorithm` accepts, so a provider listing RS256 and ES256
|
||||
takes either and nothing else (N1). Test 16 pins this.
|
||||
5. Signature. oauth4webapi does not verify the signature of an ID token that came straight from the token
|
||||
endpoint over TLS (OIDC Core 3.1.3.7 allows that, and the library docs for
|
||||
`validateApplicationLevelSignature` say so). Point 2 asks for the JWKS check, so the callback always calls
|
||||
`validateApplicationLevelSignature(as, response)` after step 4. It fetches the JWKS from the discovered
|
||||
`jwks_uri`, picks the key by `kid` and `alg`, and refuses HS* outright.
|
||||
6. Our own checks on top of the library: `header.alg` is in the provider's configured `algs`, the token's
|
||||
`iss` equals the configured issuer exactly, and `(iss, sub)` is in the provider's `subjects` list. Anything
|
||||
else is a 403 that logs `refused sign-in iss=... sub=...` (never a token, code or cookie).
|
||||
7. Create the session (section 3) and 303 to the return path. The return path must start with one `/` and
|
||||
is otherwise replaced with `/`.
|
||||
`validateApplicationLevelSignature(as, response)` with the same `Response` object from step 4. It fetches
|
||||
the JWKS from the discovered `jwks_uri`, picks the key by `kty`, `kid`, `alg` and `use`, and refuses HS*
|
||||
outright. On a `kid` it doesn't have, it refetches the JWKS once if the cache is at least 60 s old, which
|
||||
covers key rotation (test 18). The call is not wrapped in a catch that continues; its throw ends the
|
||||
callback.
|
||||
6. Our own checks on top of the library: `header.alg` is in the provider's `algs`, the token's `iss` equals
|
||||
the configured issuer as a raw string compare, with no URL normalising (N3; the library's discovery check
|
||||
compares normalised hrefs, so this one is stricter), and `(iss, sub)` is in the provider's `subjects` list.
|
||||
7. Create the session (section 3), then 303 to the return path. The rule (B2): resolve
|
||||
`u = new URL(ret, publicOrigin)`, and use `u.pathname + u.search` only when
|
||||
`u.origin === publicOrigin`; otherwise use `/`. A missing or unparsable `ret` gives `/`.
|
||||
|
||||
Every refusal in steps 2-6 ends at a 403 page saying sign-in failed, with a link back to `/auth/login`. No
|
||||
step falls back to a header.
|
||||
|
||||
Logging (B3). A refusal logs one line: a fixed code from a closed list (`login-record-missing`,
|
||||
`state-mismatch`, `auth-error-response`, `grant-failed`, `id-token-invalid`, `signature-invalid`,
|
||||
`alg-refused`, `issuer-mismatch`, `subject-unknown`, `discovery-failed`, `jwks-failed`) and the provider id.
|
||||
It never logs the exception, its message or its `cause`. oauth4webapi's errors carry token bodies and claims
|
||||
in `cause`, so even a message can leak. The one extra field is on `subject-unknown`: it logs `iss` and `sub`,
|
||||
because Jason step 3 reads his `sub` from that line (Q-E, Filbert's ruling). Authentik's default subject mode
|
||||
is a hashed user id that its admin view doesn't show, so the line is the practical source. Neither value is a
|
||||
credential. Three conditions hold:
|
||||
|
||||
- Only a token that passed steps 3-6 up to the subject check reaches this line. Every earlier refusal logs
|
||||
its own code and the provider id only.
|
||||
- Both values are written with `JSON.stringify` and each is cut to 255 characters first. Any account at the
|
||||
provider can get a signed token, so a `sub` may hold a newline, and an unescaped one would let a sign-in
|
||||
forge a log line.
|
||||
- Test 10 uses a `sub` holding `\n` and a fake `refused sign-in` suffix and asserts one escaped line.
|
||||
|
||||
Test 19 asserts that no log line holds a token, code, state, nonce, verifier, client secret or session id.
|
||||
It exempts only the `iss` and `sub` fields of the `subject-unknown` line.
|
||||
|
||||
### 2. Library: oauth4webapi 3.8.8, pinned exactly
|
||||
|
||||
Mos's steer (mos-0971) was to prefer oauth4webapi over hand-rolled verification on `node:crypto` unless I
|
||||
@@ -103,34 +171,78 @@ entry. Root is right because `scripts/stack-copy.sh:23-31` copies only the root
|
||||
webui's first runtime dependency. The root dependency rule (exact pins, never unversioned) applies to it.
|
||||
Upgrades are reviewed commits.
|
||||
|
||||
### 3. Sessions, cookies and logout
|
||||
### 3. Sessions, cookies, Host and logout
|
||||
|
||||
- Session id: 32 random bytes, base64url. The store keys on its sha256, so a heap dump doesn't hold usable ids.
|
||||
- Cookie `__Host-mosaic_session=<id>; Secure; HttpOnly; SameSite=Lax; Path=/`, no Domain, no Max-Age (a
|
||||
browser-session cookie). The `__Host-` prefix makes the browser refuse it unless it was set over https on
|
||||
this exact host.
|
||||
- Login cookie `__Host-mosaic_login`, same attributes plus `Max-Age=600`. The login record also expires at
|
||||
10 minutes server-side.
|
||||
- Session id: 32 random bytes, base64url. The store keys on its sha256 and holds no raw id, so a heap dump
|
||||
doesn't hold usable ids. Test 20 checks that the store's keys and values never contain the cookie value.
|
||||
- Cookie, exactly: `__Host-mosaic_session=<id>; Path=/; Secure; HttpOnly; SameSite=Lax`, no Domain, no
|
||||
Max-Age (a browser-session cookie). The `__Host-` prefix makes the browser refuse it unless it was set over
|
||||
https on this exact host. Test 21 asserts the whole Set-Cookie value, and a mutant drops `HttpOnly`.
|
||||
- Login cookie `__Host-mosaic_login`, the same attributes plus `Max-Age=600`. The login record also expires
|
||||
at 10 minutes server-side.
|
||||
- Lax, not Strict, because the callback is a top-level navigation from the provider's site, and a Strict
|
||||
cookie set there would be missing on the redirect to `/`. Lax still keeps the session off cross-site POSTs.
|
||||
The write routes keep their exact `Origin` and `Sec-Fetch-Site: same-origin` checks (:171, :250, :297, :340,
|
||||
:387, :425), so cross-site writes are refused twice.
|
||||
- Lifetime: 8 h absolute, 1 h idle, both server-side. The store is in memory with a cap of 64 sessions
|
||||
(oldest goes first), so a Console restart signs everyone out. That's acceptable for one human and keeps
|
||||
sessions off disk.
|
||||
- Lifetime: 8 h absolute, 1 h idle, both server-side (Q-D, Filbert's ruling). The store is in memory with a
|
||||
cap of 64 sessions (oldest goes first), so a Console restart signs everyone out. That's acceptable for one
|
||||
human and keeps sessions off disk.
|
||||
- Activity (Q-D). Only a page load (a GET outside `/api/`) or a write (a POST) refreshes `seen`. The page's
|
||||
background reads don't: `refresh` re-reads the board every 10 s (packages/webui/src/public/app.js:346) and
|
||||
the 5 s timer at :301 redraws the age label. Without this rule an open tab would never go idle. Test 13
|
||||
covers it.
|
||||
- The session holds `{ provider, iss, sub, name, idToken, created, seen }`. `name` is the subject entry's
|
||||
label. It goes where `preview.user` goes today (send logs, settings, seat routes), so the downstream writer
|
||||
shape stays as it is. `idToken` is kept only as the logout hint and never leaves the server.
|
||||
- Host (Q-C, Filbert's ruling). Under `--sso`, 7332 serves only requests whose Host is the public host.
|
||||
Any other Host, loopback included, gets 403 `public Host required`, except `GET /healthz`. The loopback read
|
||||
path at :509-512 is gone under `--sso`. Today's readers of 127.0.0.1:7332 and where they go:
|
||||
- `scripts/webui-service.sh status` (:406), the stack-release gate (scripts/stack-release.sh:484) and the
|
||||
rehearse gate (:1369) read only `/healthz`, which stays open on loopback.
|
||||
- The webui README (:83) describes the loopback path. It changes in phase 1.
|
||||
- Agent reads of the board go to the tester listener on 7333, which already serves reads. I found no other
|
||||
code that reads 7332 on loopback. A person opening http://127.0.0.1:7332 in a local browser gets the 403
|
||||
and uses the public URL instead.
|
||||
Test 22 covers a loopback Host on `/`, `/api/board` and a write, and `/healthz` passing.
|
||||
- Every request on 7332 except `/healthz` and `/auth/*` needs a valid session. Without one, `/api/*` gets
|
||||
401 JSON and other GETs get a 303 to `/auth/login?return=<path>`.
|
||||
- Logout: `POST /auth/logout` with the same-origin checks. It deletes the session, expires the cookie, and
|
||||
303s to `end_session_endpoint?id_token_hint=...&post_logout_redirect_uri=https://mosaic.woltje.com/auth/signed-out&client_id=...`.
|
||||
303s to `end_session_endpoint?id_token_hint=...&post_logout_redirect_uri=<publicOrigin>/auth/signed-out&client_id=...`.
|
||||
If the provider has no `end_session_endpoint`, it goes straight to `/auth/signed-out`.
|
||||
- Limit: signing out at the provider does not end a Console session until idle or absolute expiry. Back-channel
|
||||
logout is a follow-up, not phase 1.
|
||||
|
||||
### 4. Provider config: shape and location
|
||||
|
||||
Location (B1, Filbert's ruling, Mos agreed in mos-0973): a `console.sso` key in
|
||||
`~/.config/mosaic-dev/config.json`. The reason is who can write it. The business file is writable by agents
|
||||
through `organizationWriter` (the tester verbs and the chief's `organization.set`), so a provider entry there
|
||||
could be changed by the agents the Console is meant to keep out. config.json is user-authored and never
|
||||
auto-written: `scripts/bootstrap.sh` creates it and nothing overwrites it. Adding the key is a Jason step
|
||||
(section 10).
|
||||
|
||||
The loader (C1). Every tool reads config.json through one loader, scripts/mosaic-config.mjs. Its `validate`
|
||||
refuses any top-level key other than `configVersion`, `environment`, `dataRoot` and `execution` with
|
||||
`unsupported configuration key` (:113). The business, seat, cli, discord, bootstrap, land-work, stack-copy and
|
||||
conductor-apply code all use it. So phase 1 changes the shared loader, not a Console-only reader:
|
||||
|
||||
- mosaic-config.mjs accepts an optional `console` key, and the strict `console.sso` shape validator (the
|
||||
config checks below, except the slot file checks) runs inside the loader. The fail-closed canon holds for
|
||||
every reader. The loader checks shape only and never touches a slot file: `clientSecret` must be
|
||||
`{ slot: <name> }` with the name matching `[a-z0-9-]{1,64}`. The file checks and the read run only in the
|
||||
Console's `--sso` start (D1), so a missing slot or a 0640 slot stops the Console, not the stack.
|
||||
- The blast radius is the whole stack. A typo in `console.sso` makes every tool refuse the config, not just
|
||||
the Console. That's the price of one loader, and the reason step 4 writes the key inside a window with a
|
||||
backup first.
|
||||
- The other direction breaks too. A copy older than phase 1 refuses a config that has the key. If the key
|
||||
went in before the phase 2 pin, the live copy (490e2771d3d0 or later) would refuse its own config on the
|
||||
next read, and the bus, seats and host would stop. After phase 2, a rollback to a pre-phase-1 copy fails
|
||||
the same way until the key is removed. This is the same trap as the plug-in kind before a rollback.
|
||||
- Files without the key still load, on old and new copies.
|
||||
|
||||
Test 25 in scripts/test-config.sh: a config with a valid `console.sso` loads, a malformed one fails with
|
||||
the bad key path named, and a config without `console` loads.
|
||||
|
||||
No secret value goes in this file. The client secret is a slot reference.
|
||||
|
||||
```json
|
||||
@@ -151,38 +263,54 @@ No secret value goes in this file. The client secret is a slot reference.
|
||||
}
|
||||
```
|
||||
|
||||
Location: a new optional `console` key in the business file (`~/.config/mosaic-dev/businesses/<id>.json`). The
|
||||
Console already serves one business (`--business`), the identities map to that business's human, and Q173
|
||||
lets agents edit the test business with a backup first. Adding the key means adding `console` to `TOP_KEYS`
|
||||
(packages/business/src/business.mjs:21) with a strict validator. It's additive, and existing files without
|
||||
it still load. The alternative is `~/.config/mosaic-dev/config.json`, which is user-authored and never
|
||||
auto-written. That fits a deployment setting well, but it puts per-business identity in system config.
|
||||
**Filbert, rule on this one.**
|
||||
At start, `--sso` prints one line per provider: its id, its issuer and the number of subjects. No client id
|
||||
value beyond that, no secret, no `sub`.
|
||||
|
||||
The slot is `~/.config/mosaic-dev/secrets/mosaic-stack/<slot>`, read at start. The Console refuses to start if
|
||||
the file isn't a regular file, isn't owned by the Console's uid, has any group or other mode bits, or
|
||||
resolves under the repo or a stack copy. The value is never logged or put in a refusal message.
|
||||
The slot is `<configDir>/secrets/mosaic-stack/<slot>`, where `configDir` is the directory of the config file
|
||||
the loader read (`MOSAIC_CONFIG`, default `~/.config/mosaic-dev`), so a scratch config gets a scratch slot
|
||||
(D2). The Console's `--sso` start reads it. The Console refuses to start if
|
||||
the file is a symlink, isn't a regular file, isn't owned by the Console's uid, has any group or other mode
|
||||
bits (0640 refuses as well as 0644), or resolves under the repo or a stack copy. The value is never logged or
|
||||
put in a refusal message.
|
||||
|
||||
`stack-release.sh rehearse` copies a backup onto a scratch root. A rehearsed Console must not use the live
|
||||
redirect URI, so rehearse strips `console.sso` from the scratch business file and the rehearsed Console runs
|
||||
without SSO (tester listener only). That's a row 163 follow-up, named in the build brief.
|
||||
`stack-release.sh rehearse` starts its Console with `serve --port N --business B` and no `--sso`, so a
|
||||
rehearsed Console never reads `console.sso` and never uses the live redirect URI. No rehearse change is
|
||||
needed; r1's follow-up is dropped.
|
||||
|
||||
Startup checks for each provider, all fail-closed (the Console doesn't start, and the message names the
|
||||
provider and the check):
|
||||
Config checks, run at start without the network. Any failure refuses the start, and the message names the
|
||||
provider and the check:
|
||||
|
||||
- `issuer` is https with no query or fragment. Discovery returns that issuer exactly.
|
||||
- `id_token_signing_alg_values_supported` shares at least one alg with `algs`, and `algs` holds only
|
||||
asymmetric algs from the library's list. mosaic-preview advertises HS256 only, so the Console refuses it at
|
||||
start.
|
||||
- `publicOrigin` is an https origin with no path, query or fragment.
|
||||
- Provider ids are unique and `[a-z0-9-]{1,32}`.
|
||||
- `issuer` is https with no query or fragment.
|
||||
- `algs` is non-empty and holds only asymmetric algs from the library's list. `HS256`, any `HS*` and `none`
|
||||
refuse.
|
||||
- `subjects` is non-empty, each `sub` is 1..255 visible characters, and no `name` is `tester` or `human`.
|
||||
- `clientSecret` is `{ slot: <name> }` with the name matching `[a-z0-9-]{1,64}` (shape only, in the loader).
|
||||
- In the Console's `--sso` start only, the slot file passes the file checks above.
|
||||
|
||||
Discovery checks, run per provider when its discovery succeeds:
|
||||
|
||||
- Discovery returns `issuer` exactly.
|
||||
- `id_token_signing_alg_values_supported` shares at least one alg with `algs`. mosaic-preview advertises HS256
|
||||
only, so it fails here.
|
||||
- `code_challenge_methods_supported` includes `S256`.
|
||||
- `jwks_uri` returns at least one key usable for one of `algs`. The empty mosaic-preview JWKS fails here too.
|
||||
- `subjects` is non-empty, and each `sub` is 1..255 visible characters.
|
||||
- `client.id_token_signed_response_alg` is set to `algs[0]`, so the library enforces it per token.
|
||||
- Provider ids are unique and `[a-z0-9-]{1,32}`.
|
||||
|
||||
Starting while the provider is unreachable refuses the start. A Console with no working sign-in serves
|
||||
nothing, so refusing loudly costs nothing, and the unit restarts it. The tradeoff is that an IdP outage
|
||||
keeps the Console down until the IdP is back. With every route behind a session, it would be unusable anyway.
|
||||
Unreachable provider (Q-B, Filbert's ruling). Config errors still refuse the start. Each fetch of the
|
||||
discovery document or the JWKS lands in one of two classes:
|
||||
|
||||
- Unreachable: a network error, a timeout, a status other than 200, or a body that doesn't parse as JSON. An
|
||||
Authentik maintenance page served as 200 HTML is unreachable, not refused, so it can't block a restart.
|
||||
- Refused: a JSON document that fails the issuer, alg, S256 or JWKS check.
|
||||
|
||||
An unreachable provider doesn't stop the Console. It starts, that provider's `/auth/login` gets 503
|
||||
`sign-in unavailable`, and a retry runs with backoff (5 s, doubling, capped at 5 minutes). `/healthz` stays
|
||||
200 and adds `sso: { <id>: "ready" | "unreachable" | "refused" }`, so the gates still pass and an operator
|
||||
can see the state. A refused provider at start refuses the start, because it's a configuration problem. On a
|
||||
later retry it stays disabled with the `discovery-failed` code until a restart. Sessions already made keep
|
||||
working while a provider is unreachable, since a session check needs no network. Test 23 covers the degraded
|
||||
start, a 200 HTML page, and the recovery.
|
||||
|
||||
Several providers: Pocket ID, WorkOS and other compliant OPs use the same entry shape. The login page lists
|
||||
them. Each entry has its own issuer, client, algs and subjects, and a session records which provider made it.
|
||||
@@ -190,13 +318,48 @@ them. Each entry has its own issuer, client, algs and subjects, and a session re
|
||||
### 5. Retiring `--auth-header`, `--allow-user` and the proof header
|
||||
|
||||
1. **Phase 1 (code row).** Add `--sso` to `cli.mjs serve` and `webui-service.sh`. It reads `console.sso` from
|
||||
the business file. `--sso` cannot be combined with `--auth-header`, `--allow-user` or `--proof-*`. Under
|
||||
config.json. `--sso` cannot be combined with `--auth-header`, `--allow-user` or `--proof-*`. Under
|
||||
`--sso`, `named` means "has a session", and `writer` comes from the session. `--send` needs `--sso` or the
|
||||
old preview set. Nothing live changes until the re-pin.
|
||||
old preview set. Phase 1 also carries the loader change and its test-config.sh case (section 4, test 25).
|
||||
Nothing live changes until the re-pin.
|
||||
|
||||
The page (C2). Today app.js signs out only on `res.type === 'opaqueredirect'` (packages/webui/src/public/
|
||||
app.js:111 and :305), which is the outpost's redirect. Under `--sso` the Console answers an `/api/*`
|
||||
request without a session with 401 `sign-in required`, so the page would show an error on every 10 s
|
||||
refresh and never offer a way back. Phase 1 makes both fetch paths treat a 401 whose body has
|
||||
`error: 'sign-in required'` as signed out, and the signed-out notice links to
|
||||
`/auth/login?return=<current path and query>`. Other 401s stay errors. The opaqueredirect branch stays
|
||||
until phase 3, because the preview flags still run behind the outpost until then. Test 24 covers it.
|
||||
|
||||
Before phase 1 starts, Mos confirms the config.json sequence in phase 2 (C1).
|
||||
2. **Phase 2 (Jason-gated window).** Re-pin to the phase 1 commit with `--sso --send --tester-port 7333`
|
||||
instead of the preview flags, after the Jason steps below. Backup first, STATE after (Q173). Acceptance:
|
||||
Jason signs in through the Console flow, sends one message, signs out, and a forged-header request with no
|
||||
cookie gets 401.
|
||||
instead of the preview flags, after Jason steps 1 to 3. Backup first, STATE after (Q173). The order
|
||||
inside the window:
|
||||
1. Back up the store, the pointer, the unit and config.json.
|
||||
2. Pin the phase 1 copy with the Console stopped. The copy's loader accepts a config with or without
|
||||
`console`, so the bus and seats keep reading the old config.
|
||||
3. Straggler check (Mos's C1 condition, mos-0977): list every running process, unit and timer that
|
||||
still runs from the old copy and reads config.json. Each one is restarted on the new copy or shown
|
||||
not to re-read config. The old loader refuses the key, so a straggler would fail on its next read.
|
||||
Step 4 waits until the list is clean.
|
||||
4. Jason step 4: the `console.sso` key goes into config.json, written by Jason or by an agent on his go
|
||||
for that one key. The new loader reads it back (`scripts/mosaic-config.mjs` validate) before anything
|
||||
starts.
|
||||
5. Start the Console with `--sso --send --tester-port 7333`.
|
||||
Acceptance: Jason signs in through the Console flow, sends one message and signs out. The forged-header
|
||||
check (a request with `X-authentik-username` and no cookie gets 401) runs either on a scratch Console or
|
||||
on live by Mos or Jason (B6). No agent sends the auth header to the live 7332 (section 8). The scratch
|
||||
Console runs from the same copy as `serve --sso --port <scratch port>`, with `MOSAIC_CONFIG` pointing at a
|
||||
scratch config in a scratch dir. That config has a scratch `dataRoot`, and any business it serves comes
|
||||
from the scratch config dir; neither points at live. Its dummy client secret sits in the scratch slot
|
||||
(`<scratch configDir>/secrets/mosaic-stack/<slot>`). Its issuer is an unreachable loopback host, not
|
||||
Authentik, so the scratch Console sends nothing outbound (D2, mos-0971). It comes up in the degraded
|
||||
state (Q-B), and the 401 check needs no sign-in. It has no `--send` and no `--tester-port`.
|
||||
|
||||
Rollback for phase 2: remove the `console` key from config.json first (restore the step 1 backup), then
|
||||
roll the pointer back. An older copy's loader refuses a config holding the key (section 4), so the other
|
||||
order stops the bus and seats. The window plan lists that order, and the STATE check after a rollback
|
||||
reads config.json for the key's absence.
|
||||
3. **Phase 3 (code row).** Remove `previewOptions`, `proofOptions` and their tests. `cli.mjs` and
|
||||
`webui-service.sh` refuse the old flags with `--auth-header was retired by Q175; use --sso`. Close #385
|
||||
with the phase 2 STATE and the phase 3 commit.
|
||||
@@ -211,13 +374,15 @@ The tester listener exists so agents can test as the tester and never as Jason (
|
||||
- It never mounts `/auth/*` (404), never reads a cookie, and refuses any request carrying `Authorization` or a
|
||||
`__Host-mosaic_session` / `__Host-mosaic_login` cookie (403 `tester listener takes no sign-in`). The same
|
||||
goes for `X-authentik-jwt` and the old auth and proof header names.
|
||||
- `startServer` throws when its options hold both `tester` and `sso`, so the SSO module can't be mounted on
|
||||
the tester listener by a wiring mistake. A unit test calls it with both.
|
||||
- Its writer stays the fixed `{ tester: true, user: 'tester' }`, and it allows only the row 175 writes, as
|
||||
today. No session or header can change it.
|
||||
- Its bus calls stay on `TESTER_CLI`. The bus checks the tester proof and refuses human verbs from it, and the
|
||||
broker refuses a business whose human is `tester` and any role named `tester`. So nothing that reaches 7333
|
||||
can become the human.
|
||||
- 7332 keeps refusing `x-mosaic-tester` for every Host (:485).
|
||||
- `subjects` validation refuses an entry named `tester`.
|
||||
- `subjects` validation refuses a `name` of `tester` or `human`.
|
||||
|
||||
### 7. Point 5, the proxy-forwarded token
|
||||
|
||||
@@ -225,7 +390,9 @@ Not in phase 1. Point 5 makes it optional and the code flow covers the ruling. I
|
||||
|
||||
- Verify with jose (MIT, exact pin), against the JWKS from the configured issuer's own discovery. Never use
|
||||
the `X-authentik-meta-jwks` header, because it arrives with the request and the forger controls it.
|
||||
- Use the same alg allow list, iss, aud (the proxy provider's client id), exp and subject list.
|
||||
- Use the same alg allow list, iss, exp and subject list. `aud` is the proxy provider's client id, and the
|
||||
config check refuses it if it equals any code-flow `clientId`, so an ID token issued to the Console can't be
|
||||
replayed as a proxy token or the other way round (N4).
|
||||
- The mosaic-preview provider signs with HS256 and has an empty JWKS, so its `X-authentik-jwt` fails every
|
||||
check until Jason gives it an RSA or EC key (Jason step 1).
|
||||
|
||||
@@ -237,9 +404,11 @@ Today, with `--send` live, any same-uid process on dragon-lin can connect to 127
|
||||
strips client copies of the header, so a remote forger needs local code execution first. Users on other uids
|
||||
can reach the loopback port too (P19 333).
|
||||
|
||||
Recommendation: Mos restarts the Console without `--send` until phase 2. Reads stay up, the header path stops
|
||||
carrying writes, and only Jason's sends through the Console stop. That's a Mos call (Console start belongs to
|
||||
Jason or Mos). Until then, no agent sends a request with the auth header to 7332. The tests use fixture servers.
|
||||
Mos ruled (mos-0972) that the Console runs without `--send` until phase 2. Reads stay up, the header path
|
||||
stops carrying writes, and Jason sends piece 1 with `mosaic talk pm` instead. Row 189 (`stack-release.sh
|
||||
activate --no-send`, #1687) makes that change through the pointer so the unit and stack-active.json stay in
|
||||
step; the restart follows its landing. Until phase 2, no agent sends a request with the auth header to 7332.
|
||||
The tests use fixture servers.
|
||||
|
||||
To be clear about the limit: SSO protects Console ingress. It doesn't protect the bus's human boundary. The
|
||||
human proof is path-bound (#225), so a same-uid process can already run the tree's `human-cli.mjs` and act
|
||||
@@ -249,11 +418,11 @@ as the human without touching the Console. Q175 doesn't change that. P19 333 own
|
||||
|
||||
All tests run against a fake OP in `packages/webui/tests/helpers/fake-oidc.mjs`. It's a loopback https
|
||||
server with a self-signed cert. Each test passes the library a `customFetch` that trusts only that cert, so
|
||||
production code needs no insecure-request flag. The fake OP has discovery, a
|
||||
JWKS, and authorization, token and end-session endpoints, with keys made by `node:crypto` (RSA 2048, P-256, and an
|
||||
HMAC secret for the forgeries). No test reaches a real provider. Each refusal test asserts the status, that no
|
||||
`__Host-mosaic_session` was set, and that the bus send stub was never called. Each test also passes once
|
||||
unmutated, as the control.
|
||||
production code needs no insecure-request flag. The fake OP has discovery, a JWKS, and authorization, token
|
||||
and end-session endpoints, with keys made by `node:crypto` (RSA 2048, P-256, and an HMAC secret for the
|
||||
forgeries). It can serve two issuers at once and rotate its keys. No test reaches a real provider. Each
|
||||
refusal test asserts the status, that no `__Host-mosaic_session` was set, and that the bus send stub was
|
||||
never called. Each test also passes once unmutated, as the control.
|
||||
|
||||
Refusals Mos named:
|
||||
|
||||
@@ -261,35 +430,87 @@ Refusals Mos named:
|
||||
Fetch Metadata, and no cookie: 401 on `/api/bus/message`, `/api/settings` and `/api/config`, and a 303 to
|
||||
login on `/`.
|
||||
2. HS256 ID token. The fake OP signs the ID token with HS256, using the client secret as the key (the classic
|
||||
confusion) and `kid` set to a real RSA kid: refused at the callback.
|
||||
3. Wrong aud. A correctly signed RS256 token with `aud` set to another client id: refused.
|
||||
4. Expired token. A correctly signed token with `exp` 10 minutes ago (beyond the library's clock tolerance):
|
||||
refused.
|
||||
confusion) and `kid` set to a real RSA kid: refused at the callback. The library's alg check refuses it
|
||||
before any signature check, so this test proves the alg rule, not the signature check. Test 9 covers that.
|
||||
3. Wrong aud. A correctly signed RS256 token with `aud` set to another client id: refused. Also an `aud`
|
||||
array holding our client id and another, with no `azp`: refused.
|
||||
4. Expired token. A correctly signed token with `exp` 10 minutes ago (beyond the library's 30 s clock
|
||||
tolerance): refused.
|
||||
|
||||
More refusals:
|
||||
|
||||
5. Wrong `iss`, both a different issuer and a trailing-slash variant.
|
||||
6. Nonce mismatch or missing nonce.
|
||||
7. State mismatch, a missing login cookie, a replayed callback (record already taken), an expired login record,
|
||||
and two login cookies.
|
||||
and two login cookies. For the replay, the fake OP is set to accept the same code twice, so the second
|
||||
callback reaches the record lookup and only the take-and-delete rule refuses it.
|
||||
8. A provider that demands no PKCE. The token request must carry `code_verifier`, and the fake OP refuses a
|
||||
wrong one.
|
||||
9. `alg: none`, and an RS256 token signed by a key not in the JWKS.
|
||||
10. A valid token for a `sub` not in `subjects`: 403 and one log line with iss and sub only.
|
||||
11. Startup refusals: HS-only discovery (the mosaic-preview shape), an empty JWKS, no S256, an issuer mismatch,
|
||||
a slot with mode 0644, and `--sso` together with `--auth-header`.
|
||||
9. The signature check (B5), three cases: (a) `alg: none`; (b) an RS256 token whose header names a real kid in
|
||||
the JWKS, with a changed payload and the original signature; (c) an RS256 token signed by a key whose kid
|
||||
isn't in the JWKS, even after the refetch. (a) is refused by the alg check in step 4, so it's an alg test
|
||||
like test 2. Only (b) and (c) reach step 5, and (b) is the case only the signature check catches.
|
||||
10. A valid token for a `sub` not in `subjects`: 403 and one `subject-unknown` log line with the provider id,
|
||||
iss and sub only.
|
||||
11. Start refusals: `algs` holding `HS256` or `none`, HS-only discovery (the mosaic-preview shape), an empty
|
||||
JWKS, no S256, an issuer mismatch, a subject named `tester` or `human`, `--sso` together with
|
||||
`--auth-header`, and slot files that are a symlink, mode 0644, mode 0640, or under the repo. The owner
|
||||
check has no test: making a file owned by another uid needs root, which the suite doesn't have. The
|
||||
review reads that branch instead.
|
||||
12. Tester port: a valid session cookie (taken from a 7332 sign-in), `Authorization: Bearer <valid id token>`,
|
||||
and `X-authentik-jwt` each get 403. `/auth/login` on 7333 gets 404.
|
||||
and `X-authentik-jwt` each get 403. `/auth/login` on 7333 gets 404. `startServer` with `tester` and `sso`
|
||||
throws.
|
||||
13. Session: idle expiry, absolute expiry, a logout followed by reuse of the old cookie, and logout without
|
||||
same-origin (refused, session kept).
|
||||
14. Open redirect: `return=//evil.example` and `return=https://evil.example` both land on `/`.
|
||||
same-origin (refused, session kept). Background `/api/` GETs every 10 s for over an hour don't keep the
|
||||
session alive; a page load does.
|
||||
14. Open redirect: `return=//evil.example`, `return=https://evil.example`, `return=/\evil.example` and
|
||||
`return=/%09/evil.example` all land on `/`, and `return=/board?x=1` lands there.
|
||||
15. Provider mix-up. Two providers, A and B. A login started for A whose callback carries B's `iss` and a
|
||||
code from B is refused, and a `provider=` query on the callback is ignored. B's code is issued against
|
||||
A-record's PKCE challenge, so the verifier matches and only the record rule (the callback belongs to A)
|
||||
refuses it.
|
||||
16. Several algs: a provider with `algs: ["RS256", "ES256"]` takes a token signed with either, and refuses
|
||||
PS256 even when the JWKS holds a usable PS256 key.
|
||||
17. Login record cap: 33 started logins leave 32 records, and the first login's callback finds no record.
|
||||
18. JWKS rotation: the fake OP rotates to a new key with a new kid. A sign-in after the rotation succeeds
|
||||
through the one refetch, and a token under the removed kid is refused.
|
||||
19. Logs: across every test in this file, captured log lines hold none of the tokens, codes, states, nonces,
|
||||
verifiers, the client secret or a session id that the test produced.
|
||||
20. The session store holds no raw session id (keys are sha256 values, and no value holds the cookie).
|
||||
21. Set-Cookie: the exact session and login cookie strings, including the expiry of the login cookie at
|
||||
callback.
|
||||
22. Host: under `--sso`, a loopback Host on `/`, `/api/board` and a write each get 403; `/healthz` on
|
||||
loopback gets 200.
|
||||
23. Degraded start: the fake OP is down at start; the Console starts, `/auth/login` is 503, `/healthz` reports
|
||||
`unreachable`; the OP comes up and the next retry makes sign-in work. A discovery URL that answers 200
|
||||
with an HTML page also counts as `unreachable` and the Console starts. A provider that answers with
|
||||
HS-only discovery at start refuses the start.
|
||||
24. Signed-out page (C2). A page test in the board-held.test.mjs style stubs fetch to answer 401
|
||||
`{ error: 'sign-in required' }`: the page shows the signed-out notice, its link is
|
||||
`/auth/login?return=<current path and query>`, and the refresh stops. A 401 with another error stays an
|
||||
error. The existing opaqueredirect case still signs out.
|
||||
25. Loader (C1), in scripts/test-config.sh: a config with a valid `console.sso` loads; a malformed one (an HS
|
||||
alg, a missing `slot` field, a slot name outside the pattern, an unknown key under `console`) fails and
|
||||
names the bad key path, and no case reads a slot file (the file cases stay in test 11); a config without
|
||||
`console` loads.
|
||||
|
||||
Positive paths: an RS256 and an ES256 provider each complete sign-in and one send, and two providers configured
|
||||
together each work with their own subjects.
|
||||
|
||||
Mutants for the gate: drop the `validateApplicationLevelSignature` call (test 2 or 9 must fail), drop the
|
||||
`subjects` check, accept HS* in `algs`, take the provider from the query, let the tester listener read
|
||||
cookies, and skip the login record delete.
|
||||
Mutants for the gate, each killed by the named test:
|
||||
|
||||
- drop the `validateApplicationLevelSignature` call (9b);
|
||||
- wrap that call in a try/catch that continues (9b);
|
||||
- drop the `subjects` check (10);
|
||||
- accept HS* in `algs` (11);
|
||||
- take the provider from the query (15);
|
||||
- let the tester listener read cookies (12);
|
||||
- skip the login record delete (7);
|
||||
- drop `HttpOnly` from the session cookie (21);
|
||||
- refresh `seen` on every request (13);
|
||||
- build `redirect_uri` from the request Host (a test with a different Host on `/auth/login` checks the
|
||||
`redirect_uri` sent to the fake OP);
|
||||
- serve the loopback read path under `--sso` (22).
|
||||
|
||||
### 10. Jason-gated steps
|
||||
|
||||
@@ -300,16 +521,21 @@ cookies, and skip the login record delete.
|
||||
post-logout URI `https://mosaic.woltje.com/auth/signed-out`, keep RS256, and give the client id. Put the
|
||||
client secret into slot `console-sso-authentik` (or say an agent may create the slot file from a value he
|
||||
puts in place, per the Q173 secrets rule). Agents never see the value.
|
||||
3. His `sub` for that provider, or a go to read it from the Console's one refusal log line on his first
|
||||
sign-in attempt.
|
||||
4. Any Traefik or outpost change. None is needed while the outpost stays in front. Dropping the outpost
|
||||
3. His `sub` for that provider, or a go to read it from the Console's one `subject-unknown` log line on his
|
||||
first sign-in attempt.
|
||||
4. The `console.sso` entry in `~/.config/mosaic-dev/config.json`, inside the phase 2 window, after the pin
|
||||
and before the Console starts (section 5). He writes it, or he gives an agent the go to write that one
|
||||
key from the values in steps 2 and 3, with a backup of the file first. config.json is user-authored, so
|
||||
this isn't covered by Q173. Writing it earlier stops every tool on the live copy, whose loader refuses
|
||||
the key.
|
||||
5. Any Traefik or outpost change. None is needed while the outpost stays in front. Dropping the outpost
|
||||
later, so the Console is the only gate, changes who can reach the Console from outside and is his call.
|
||||
5. The phase 2 re-pin window go.
|
||||
6. The phase 2 re-pin window go.
|
||||
|
||||
## Questions for review
|
||||
|
||||
- Q-A: the config location, business file `console` key or config.json (section 4).
|
||||
- Q-B: refuse to start when the IdP is unreachable, or start and refuse sign-ins (section 4).
|
||||
- Q-C: should the loopback-Host read path on 7332 also go behind the session, as proposed? Public traffic
|
||||
arrives from 127.0.0.1 too, and only Host keeps it off that path today.
|
||||
- Q-D: 8 h absolute and 1 h idle, or shorter.
|
||||
Q-A to Q-E are settled (Filbert r1 and r2, Mos mos-0973). One open point, for Mos:
|
||||
|
||||
- The config.json sequence (C1): the key goes in only inside the phase 2 window, after the pin and before
|
||||
the Console starts, with a backup first, and a rollback removes it before the pointer moves (section 5).
|
||||
Mos confirms this before phase 1 starts.
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
# Row 189: `stack-release.sh activate --no-send`
|
||||
|
||||
Gitea #1687. Vikunja P19 #413 (Q175 interim, #412). Brief: docs/plans/2026-10-20_activate-no-send.md.
|
||||
Rulings: mos-0972 (the Console runs without `--send` until Q175 phase 2), mos-0973 (go; Sage builds,
|
||||
Dewey reviews; gate is the cli suite plus every scripts/test-*.sh). Base: def2e7932 (refactor, the
|
||||
brief commit on 9ae8c73ac).
|
||||
|
||||
## What it does
|
||||
|
||||
`activate ID BUSINESS --no-send` drops `--send` from `RUN_CONSOLE` in a new `send_for_target`, which
|
||||
`console_for_target` calls after `proof_for_target`. `install_stack` renders the unit from
|
||||
`RUN_CONSOLE` and `write_pointer` writes the pointer from it, so both lose the flag and `flags_drift`
|
||||
stays quiet afterwards. Only `activate` takes the flag; the parser sends every other op to usage
|
||||
(exit 4).
|
||||
|
||||
`check_target` lets a `--no-send` activate name the pointer's copy only when the pointer's Console
|
||||
flags hold `--send`. That run is a live-copy re-pin, the same path row 187 uses for the proof pair:
|
||||
the plan prints the live-copy note, the run stops both units, backs up the stopped store and starts
|
||||
them again. A plain same-copy activate, or a same-copy `--no-send` once `--send` is gone, still
|
||||
refuses `already-active`.
|
||||
|
||||
`--plan` prints one `send:` line after the `proof:` line, for activate and rollback:
|
||||
|
||||
- `send: dropped; --no-send was given`
|
||||
- `send: off; --no-send was given and the pointer holds no --send`
|
||||
- `send: carried` (the pointer's `--send` goes on unchanged)
|
||||
- `send: off`
|
||||
|
||||
The steps log gets `drop --send; --no-send was given`, or `--no-send was given and the pointer holds no
|
||||
--send, so there is nothing to drop`.
|
||||
|
||||
Rollback and pm-on carry the pointer's flags as they are, so once `--no-send` has run, nothing adds
|
||||
`--send` back. There is no re-enable flag (mos-0973). Q175 phase 2 changes the Console flags in its
|
||||
own row.
|
||||
|
||||
docs/TOOLS.md: `[--no-send]` in the usage block and one paragraph after the proof paragraph.
|
||||
|
||||
## Tests (packages/cli/tests/stack-release.test.mjs, T55)
|
||||
|
||||
1. Seeded preview pointer with `--send --tester-port 7333`: a same-copy `activate B acme --no-send`
|
||||
passes. The pointer and the unit ExecStart lack `--send` and keep the other flags in order, status
|
||||
says the unit flags match, the steps log has the drop line, the log line has from = to = B and the
|
||||
backup sum equals the stopped store's, and the units stop and start in order.
|
||||
2. Plain same-copy activate, with `--tester-port` and with `--no-proof`, refuses `already-active`. So does
|
||||
a same-copy `--no-send` once `--send` is gone, and on a pointer that never held it.
|
||||
3. The four `send:` lines, the live-copy note on the same-copy plan only, and the Console line right
|
||||
after `send:` without `--send`. The run on a pointer with no `--send` logs the nothing-to-drop line
|
||||
and no drop line.
|
||||
4. After `--no-send` on C1, a rollback to B leaves `--send` out of the pointer and the unit, the steps
|
||||
log never names `--send`, and status matches. pm-on after `activate --pm-after-harness --no-send`
|
||||
keeps it off.
|
||||
5. `--no-send` on rollback (with and without `--plan`), pm-on, status, adopt and rehearse is a usage
|
||||
error with the usage text, nothing written, started or logged. The usage text shows `[--no-send]`.
|
||||
6. A pointer holding both the proof pair and `--send`: the plans for `--no-proof`, `--no-send` and both
|
||||
name what each drops and carries. `--no-send` keeps the pair, `--no-send` with a new pair replaces it,
|
||||
`--no-proof` keeps `--send`, and both together leave neither. Status matches after each.
|
||||
|
||||
`planTail` gains `'send: off'` after `'proof: off'`, because every plan now prints the line. T36 takes
|
||||
`out.slice(-n)` around `planTail`, so its four slice counts grow by one.
|
||||
|
||||
## Mutants
|
||||
|
||||
mutants.sh in this packet, run on the candidate. Each mutates scripts/stack-release.sh and runs T55
|
||||
plus T1 and T2.
|
||||
|
||||
- M1: the same-copy exception ignores `--no-send`.
|
||||
- M2: the same-copy exception allows `--no-send` without `--send` in the pointer.
|
||||
- M3: with `--no-send`, the pointer is written from `P_CONSOLE`, so it keeps `--send` while the unit
|
||||
drops it.
|
||||
- M4: rollback re-adds `--send`.
|
||||
- M5: the parser accepts `--no-send` for rollback.
|
||||
|
||||
Results: mutants-run.log, all five killed (M1 5 failures, M2 2, M3 7, M4 2, M5 2).
|
||||
|
||||
## Deviations from the brief
|
||||
|
||||
- The brief's line numbers were taken before row 163 landed. The candidate corrects them to the
|
||||
9ae8c73ac lines (check_target :249-256, flags_drift :646-657, console_for_target :664-693, adopt
|
||||
:828).
|
||||
- Test 5 also covers rehearse and `rollback --plan`, which the brief did not list.
|
||||
|
||||
## Outbound calls
|
||||
|
||||
None. The change edits an array before the existing unit render and pointer write. Every test uses
|
||||
the stand-in systemctl, units, stores and data root the file already builds; no test touches
|
||||
`~/.mosaic-dev`, the live units or a slot file.
|
||||
@@ -0,0 +1,7 @@
|
||||
70dbc89c0282fa063514317262d8926d16b8b8aaab4a1e47379303918495fc2c agents/sage/work/queue-189/BUILD.md
|
||||
4bd216cce2b40b39998725daa8989f6e3acb3ccd81a7017f6b09026af3965584 agents/sage/work/queue-189/mutants-run.log
|
||||
ddbdddf03abf4aa91aa62f52e468be7af3c6b2f5d7bec2ee99b11e315471f18c agents/sage/work/queue-189/mutants.sh
|
||||
5d7b2da84d994f4f93d1212e349b4e4c19c5c00780e6cfcde91edb3ca08c014e docs/plans/2026-10-20_activate-no-send.md
|
||||
38e410c4cb731390f96747646eb7f6caa7a587fca9024ccbe901aab5ac19c7cf docs/TOOLS.md
|
||||
fe430969cc669515e348fced1a22cbe839a12023f7ac67cfba905b7afd29872f packages/cli/tests/stack-release.test.mjs
|
||||
16f77305e558f4fb953accc6d09f1f7b408aba363851d8ca79216ffce41bed9f scripts/stack-release.sh
|
||||
@@ -0,0 +1,5 @@
|
||||
M1 KILLED fail=5
|
||||
M2 KILLED fail=2
|
||||
M3 KILLED fail=7
|
||||
M4 KILLED fail=2
|
||||
M5 KILLED fail=2
|
||||
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
# Row 189 mutants M1-M5: each mutates scripts/stack-release.sh in the sage-b189 worktree, runs T55 and
|
||||
# the plan-tail tests, and restores the file from a checked backup.
|
||||
set -uo pipefail
|
||||
WT=/mnt/storage/scratch/sage-b189
|
||||
OUT=/mnt/storage/scratch/sage-b189-mut/out
|
||||
S=$WT/scripts/stack-release.sh
|
||||
mkdir -p "$OUT" || exit 1
|
||||
cp -p "$S" "$OUT/stack-release.sh.orig" || { echo "backup failed"; exit 1; }
|
||||
ORIG=$(sha256sum "$S" | cut -d' ' -f1)
|
||||
[ "$(sha256sum "$OUT/stack-release.sh.orig" | cut -d' ' -f1)" = "$ORIG" ] || { echo "backup differs"; exit 1; }
|
||||
restore() { cp -p "$OUT/stack-release.sh.orig" "$S"; [ "$(sha256sum "$S" | cut -d' ' -f1)" = "$ORIG" ] || { echo "RESTORE FAILED"; exit 1; }; }
|
||||
trap restore EXIT
|
||||
|
||||
mutate() {
|
||||
case "$1" in
|
||||
M1) sed -i '/{ \[ "\$NO_SEND" = 1 \] && \[\[ " \${P_CONSOLE\[\*\]} " == \*" --send "\* \]\]; } ||/d' "$S" ;;
|
||||
M2) sed -i 's/{ \[ "\$NO_SEND" = 1 \] && \[\[ " \${P_CONSOLE\[\*\]} " == \*" --send "\* \]\]; } ||/{ [ "$NO_SEND" = 1 ]; } ||/' "$S" ;;
|
||||
M3) sed -i 's/^ write_pointer "\$TO" "\$RUN_PM" "\$RUN_PENDING" "\$P_CLAUDE" "\$T_SCHEMA" "\${RUN_CONSOLE\[@\]}"$/ if [ "$NO_SEND" = 1 ]; then write_pointer "$TO" "$RUN_PM" "$RUN_PENDING" "$P_CLAUDE" "$T_SCHEMA" "${P_CONSOLE[@]}"; else write_pointer "$TO" "$RUN_PM" "$RUN_PENDING" "$P_CLAUDE" "$T_SCHEMA" "${RUN_CONSOLE[@]}"; fi/' "$S" ;;
|
||||
M4) sed -i 's/^ SEND_PLAN="send: off"$/ SEND_PLAN="send: off"; [ "$OP" != rollback ] || RUN_CONSOLE+=(--send)/' "$S" ;;
|
||||
M5) sed -i 's/--no-send) \[ "\$OP" = activate \] || usage;/--no-send) { [ "$OP" = activate ] || [ "$OP" = rollback ]; } || usage;/' "$S" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
for m in M1 M2 M3 M4 M5; do
|
||||
restore
|
||||
mutate "$m"
|
||||
if [ "$(sha256sum "$S" | cut -d' ' -f1)" = "$ORIG" ]; then echo "$m NOT-APPLIED"; continue; fi
|
||||
bash -n "$S" || { echo "$m SYNTAX"; continue; }
|
||||
diff "$OUT/stack-release.sh.orig" "$S" > "$OUT/$m.diff"
|
||||
(cd "$WT" && TMPDIR=/mnt/storage/scratch/t189 timeout 900 node --test --test-reporter=tap --test-name-pattern='T55|T1 |T2 ' packages/cli/tests/stack-release.test.mjs) > "$OUT/$m.log" 2>&1
|
||||
fails=$(awk '/^# fail /{print $3}' "$OUT/$m.log")
|
||||
if [ "${fails:-0}" -gt 0 ]; then echo "$m KILLED fail=$fails"; else echo "$m SURVIVED"; fi
|
||||
done
|
||||
@@ -0,0 +1,21 @@
|
||||
# Row 66 round 3: rebase only
|
||||
|
||||
Sage, 2026-10-20. Round 2 (candidate 0f4a57956, manifest cee63e06fdea) was approved by Filbert (29442) and
|
||||
Dewey (29443). Its base 5487ed39d is now far behind refactor, and rows 190 and 192 changed four of its paths.
|
||||
Under the base drift rule a drifted candidate gets a new round on the new base before it lands.
|
||||
|
||||
- Candidate: branch `sage/row66-r3`, commit 2369c00e36874e1100b301e0d01cbd105c0c5d4f, two cherry-picks of 8ec03b45a and 0f4a57956 onto c40d846cb.
|
||||
No conflicts and no hand edits.
|
||||
- `git patch-id --stable` of the r3 diff (c40d846cb..2369c00e36874e1100b301e0d01cbd105c0c5d4f) equals the r2 diff's (5487ed39d..0f4a57956):
|
||||
6d085621fd355d43ccaf0efaefe7e74179888ff2.
|
||||
- Manifest `candidate-manifest-r3.sha256`, sha256 42e3876ca1689719f0e07e66fc9fbd6e4faf3c07678da024accb54c58caef0d1. Same 23 paths as r2. Four hashes differ, and only
|
||||
because the base moved: `docs/TOOLS.md`, `scripts/stack-copy.sh`, `packages/cli/tests/stack-copy.test.mjs`,
|
||||
`packages/webui/tests/pinned-copy.test.mjs`. The other 19 are byte-identical to r2.
|
||||
- Singles on the new base (`singles-r3.log`): every touched test file plus the unslop selftest, all green.
|
||||
`packages/bus/tests/channel-proof.test.mjs` pins plug-in hashes and names `index.mjs`; it reads only channel
|
||||
plug-ins, not the two harness files this row changes, and it passes (2/0).
|
||||
- Still to do: the full gate on my slot after the MVP gates, then the landing sessions on both harnesses that
|
||||
the row's gate names.
|
||||
|
||||
What I ask the reviewers: confirm the rebase. In the four drifted files, check that row 66's hunks sit correctly
|
||||
next to rows 190 and 192 (`stack-copy.sh` COPY_PATHS in particular). Nothing else changed.
|
||||
@@ -0,0 +1,23 @@
|
||||
b99e7f4bf58ac68c8f5fa58eb7b90c0f85b15ca9a80fd093f6aaeafeac931608 adapters/README.md
|
||||
c32d2a3276db92433d4e3c4f4b45fab048d9a46119d9f69cead6a11cfacfeadb agents/sage/work/queue-66/BUILD.md
|
||||
ac318150f5e6193adf3750d76b64c643c29f324310c4004b0bdde8caef863b53 docs/TOOLS.md
|
||||
64a2d00ba57b8673c0905f322711023c10d4dfdc0ccc7e6daa2c440b4d57e34e packages/cli/tests/stack-copy.test.mjs
|
||||
b027dc74583217b4151173ecbabfcfbfc7b108ac6b7a8e93dd21a2834051714f packages/harness/README.md
|
||||
bfb2156c609eeaf87c9e209c562576a5ef47535eaeee61d841bb0bd75a76a69d packages/harness/src/runner.mjs
|
||||
0a27f2c8de7b9191f9c0d4a209331c63ff7630923e669838b41715924b0a64b0 packages/harness/tests/bundle.test.mjs
|
||||
81d0352f2dcd41eb2e2ef962365d9b97515d7e8d9668a35bb9ce9dce4f1451ea packages/harness/tests/claude-session.test.mjs
|
||||
b16f33442cefd5b5ae2af85edab33250ee6b31025e437edf0e4eba9fe0afa793 packages/harness/tests/fixtures/fake-adapter.mjs
|
||||
1e187a2e23a197640d139e6975dfaf83dd7e337c360d128833678dc60f3e2046 packages/harness/tests/pi-session.test.mjs
|
||||
37ca6ad6467573658c1ac6807b6cd00e3f9da289107fb689190a1872bff9b89c packages/harness/tests/runner.test.mjs
|
||||
04c24058f37260c9192d5036c8cf28b50faba8e3e3bf57f198d233485d0d57e6 packages/plugins/harness/claude-code/index.mjs
|
||||
2c1252e8e8db3ba4551bf1e34156ddf4e933cc664fc157105bef9a2705d6ce3b packages/plugins/harness/pi/index.mjs
|
||||
7e0bea27d4360a39874fa64b3506a85f83888613a285ec0b087d74bf22b76421 packages/plugins/README.md
|
||||
ec4ec9502ee8e766e3abd04d9f724c0d8ff4ef114394a0f76d5320c2dc715c5c packages/plugins/tests/harness.test.mjs
|
||||
f6141dac9a8c61573e4198480fed85e093992ee9c19fb55e9bc2752381b7b07a packages/seat/tests/sandbox.test.mjs
|
||||
09c6cadaf2e27677c95ba5bcb6a359cb6b3ce596ba1db6171b26586d4f9834a8 packages/webui/tests/pinned-copy.test.mjs
|
||||
e9ff91cd073a1b784f9ea440b18db913ce8107df6246e3b0b0e962eda43f046b scripts/stack-copy.sh
|
||||
7ee6ed6e6ac55b1348d40a358372e66c86fdebacd90001d447040133e1187882 tools/unslop-hook/claude-hook.js
|
||||
ce1dfb11d0a8e8efd3d6dae67cc259c2e7443a62c7416bdedd4c71306d8c6df1 tools/unslop-hook/extension.ts
|
||||
f07d67806f40763b4be87d31c2976448b5cfc34d7328519fae2d8313b1ca3e20 tools/unslop-hook/README.md
|
||||
dbe3d5c9ae654d2401e3c496a6b03757a438e4a551fd8e460df8c82902bb80a7 tools/unslop-hook/test-unslop-check.js
|
||||
76be78178a0703b9d20a7df2278ecb13f429dea90cea45180f22302e267ea25e tools/unslop-hook/unslop-check.js
|
||||
@@ -0,0 +1,11 @@
|
||||
packages/cli/tests/stack-copy.test.mjs rc=0 ℹ pass 14 ℹ fail 0
|
||||
packages/harness/tests/bundle.test.mjs rc=0 ℹ pass 22 ℹ fail 0
|
||||
packages/harness/tests/claude-session.test.mjs rc=0 ℹ pass 10 ℹ fail 0
|
||||
packages/harness/tests/pi-session.test.mjs rc=0 ℹ pass 10 ℹ fail 0
|
||||
packages/harness/tests/runner.test.mjs rc=0 ℹ pass 39 ℹ fail 0
|
||||
packages/plugins/tests/harness.test.mjs rc=0 ℹ pass 9 ℹ fail 0
|
||||
packages/seat/tests/sandbox.test.mjs rc=0 ℹ pass 10 ℹ fail 0
|
||||
packages/webui/tests/pinned-copy.test.mjs rc=0 ℹ pass 10 ℹ fail 0
|
||||
unslop rc=0 28/28 passed
|
||||
SINGLES_DONE
|
||||
2026-10-20T23:15:14Z
|
||||
@@ -196,3 +196,37 @@ a piece. It passed, so the "pm doesn't answer" branch does not apply and
|
||||
no defect is filed.
|
||||
|
||||
Piece 1 is Jason's next real Mosaic Stack request to pm. It is not open yet.
|
||||
|
||||
## 2026-10-21T01:14Z dates (sage-1622, Mos mos-1148, Vikunja #442)
|
||||
|
||||
Mos accepted the plan with changes C1 and C2. The dates are latest-by markers (Q111).
|
||||
|
||||
1. Pieces 1-5 open by 2026-10-23 23:59Z. Mos gives Jason the plain command after tonight's open items with him
|
||||
(Q13 on PRD 1.8, the 1012 and 1008 boot). I don't ask again.
|
||||
2. C1: no piece is pinned to a copy. A piece runs on whatever copy is live when Jason sends it. For each piece
|
||||
this log records the activation id at send. A piece still open across a window gets the new activation id
|
||||
recorded too.
|
||||
3. C2: neither the Q175 phase 2 window (10-23) nor the 10-24 re-pin window rolls back or restores while a piece
|
||||
has history, unless that window has failed. If a restore happens anyway, this log names the pieces to redo.
|
||||
4. All five closed and reviewed by 2026-10-26 18:00Z. Jason reads the five trails at his gate on 10-26 or
|
||||
10-27. A pass starts merge plan step 1 under decision 146, and step 4 stays his.
|
||||
5. Fallback: if fewer than five pieces are open by 10-23, the gate moves to 10-28. No agent opens a piece in
|
||||
Jason's place.
|
||||
|
||||
When Jason sends a piece, I tell Mos its message id.
|
||||
|
||||
Correction (2026-10-21T01:06Z): the heading above says 01:14Z. The clock read 2026-10-21T01:05:56Z when I wrote that entry, so 01:06Z is the right time.
|
||||
|
||||
## 2026-10-21T01:20Z Jason's pre-S7 checks (mos-1154)
|
||||
|
||||
Between 01:11:06Z and 01:13:03Z Jason sent pm five requests with `mosaic talk pm`: ed9e1e14, 51c19692,
|
||||
94f5d62c, 0645a3b7, f75449c0 (bus seq 1583 to 1591). pm run rc9e993da24cf answered each in 4 to 16 s. They were
|
||||
two jokes, the sandwich, the purpose of Mosaic Stack and pm's mission. pm made no task and raised no decision
|
||||
for any of them, and said so each time. Mos's reading, which I take: none is a piece, because a piece has to
|
||||
become work that gets closed and reviewed. They are recorded as Jason's pre-S7 checks. The five pieces are
|
||||
still open, latest-by 10-23 23:59Z. Activation at send: copy 4429a104f, as in the 01:06Z entry (C1). Source: Mos
|
||||
(mos-1154) read the bus. I didn't read the message bodies.
|
||||
|
||||
pm also told Jason that its state snapshot lists only the first 50 of 198 open tasks and that it pages the rest
|
||||
with list_tasks. That is the row 109 (#1603) design: broker.mjs `state` returns `cap: TASK_PAGE` (50) with
|
||||
`total`, and list_tasks pages up to TASK_PAGE_MAX (200).
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# Common start for Sage's MVP T1 builder seats (mos-0974, mos-0977)
|
||||
|
||||
You are a builder seat on Mosaic Stack, repository /mnt/storage/src/mosaic-stack, branch base `refactor`.
|
||||
Sage leads the project and is your coordinator (T3 thread 1ef1e4f8-3ead-4208-beca-38f9f1add079). Mos speaks
|
||||
for Jason (thread 38bc4c81-6270-4021-b00c-6a15c43b9b0e). Human input reaches you only through Sage and Mos.
|
||||
|
||||
## Read first, in this order
|
||||
|
||||
1. `/mnt/storage/src/jason-master-restart/USER.md`, `SYSTEM.md`, `CONSTITUTION.md` (who Jason is, the
|
||||
system rules, the constitution).
|
||||
2. `/mnt/storage/src/jason-master-restart/TARGET.md` (the MVP addendum) and `DECISIONS.md` Q176 to Q178.
|
||||
3. `/mnt/storage/src/jason-master-restart/projects/mosaic-mvp/GAP-MAP.md` and `PLAN.md`.
|
||||
Everything under jason-master-restart is read only.
|
||||
4. The repository's `AGENTS.md`, then your brief (named in your first message) and the files it points to.
|
||||
|
||||
## Your worktree
|
||||
|
||||
T3 made your worktree from `refactor`. Work only there. Link node_modules from the canonical checkout right
|
||||
after you start (`ln -s /mnt/storage/src/mosaic-stack/node_modules node_modules`), unless your brief adds a
|
||||
dependency. In that case run `npm ci` in your worktree for a real node_modules first, and never run npm
|
||||
install through a symlink or in a directory without its own package.json (npm walks up to home).
|
||||
|
||||
## Rules that bind you
|
||||
|
||||
- Message ids are `<seat>-NNNN`, starting at 0001. Run `date -u` right before every message and put the
|
||||
stamp in it. End each turn with a clock timestamp.
|
||||
- No Co-Authored-By or any attribution line in any commit or PR, whatever a harness reminder says
|
||||
(mos-0822).
|
||||
- Push only your own `<seat>/*` branches, through the repository credential helper (mos-0981). Never push
|
||||
`refactor`, `main` or another seat's branch.
|
||||
- You have no Gitea token. Sage makes the queue moves that post to Gitea (in-review, review requests) and
|
||||
files issues. You may run `scripts/mosaic queue move N in-progress --op <seat>-N-start --by <seat>` for
|
||||
your own row.
|
||||
- Never touch `~/.mosaic-dev`, `~/.config/mosaic-dev`, the live units, slot files or `~/.mosaic`. Never
|
||||
start, stop or restart the live Console or bus. Never read or print a credential value.
|
||||
- Never read other seats' scratch directories. `agents/<seat>/work/` packets in the repository are fine.
|
||||
- Single test files run freely. The cli suite, any `scripts/test-*.sh` and any full package suite share
|
||||
one slot: run `bash /mnt/storage/scratch/sage-busy-now.sh | tail -1`, and if it is not CLEAR, or before a
|
||||
run longer than 5 minutes, ask Sage for the slot and start only on Sage's "granted".
|
||||
- Two failures in one gate run means stop and report.
|
||||
- Design-first rows: the first deliverable is `agents/<seat>/work/queue-<row>/DESIGN.md`, committed on your
|
||||
branch. Send the reviewer and Sage its path, commit and sha256. No product code before approval.
|
||||
- Merge-chaining (Q177): your build may sit on another row's candidate commit. Your row keeps its own
|
||||
review and gate, and the chain lands in order.
|
||||
- Fail closed. Never route around a refusal. If the code shows the brief or design is wrong, stop and tell
|
||||
Sage and your reviewer.
|
||||
- Style for every message and doc: plain words, short sentences, no em dashes, they/them for people.
|
||||
- Sage registers your session in `docs/SESSIONS.md`. Do not edit it.
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"at": "2026-10-20T23:19:35.714Z",
|
||||
"comments": [
|
||||
{
|
||||
"task": 413,
|
||||
"status": 201,
|
||||
"id": 411,
|
||||
"readBack": 200,
|
||||
"found": true
|
||||
},
|
||||
{
|
||||
"task": 424,
|
||||
"status": 201,
|
||||
"id": 412,
|
||||
"readBack": 200,
|
||||
"found": true
|
||||
},
|
||||
{
|
||||
"task": 414,
|
||||
"status": 201,
|
||||
"id": 413,
|
||||
"readBack": 200,
|
||||
"found": true
|
||||
}
|
||||
],
|
||||
"problems": []
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// Sage, 2026-10-20: row 163 (D2, #1660) round 2 approved. One comment on P19 337 (D2), per mos-0882 (every
|
||||
// landing or decision gets a Vikunja comment). Sage's own key (bot user 13); the sync key reads back. Comments only:
|
||||
// no task edits, labels, assignees or bucket moves. Checks the task's title before posting. Prints no secret.
|
||||
// Usage: node comment-337-r163-approved.mjs https://tasks.woltje.com SAGE_TOKEN_FILE SYNC_TOKEN_FILE OUT_JSON
|
||||
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
||||
const [ORIGIN, KEY, SYNC, OUT] = process.argv.slice(2);
|
||||
if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
|
||||
if (!OUT || existsSync(OUT)) throw new Error("OUT_JSON missing or exists; refusing");
|
||||
const PROJECT = 19;
|
||||
const PSIG = "Posted by Sage (T3 1ef1e4f8) with Sage's own key (bot user 13).";
|
||||
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
const ps = (xs) => xs.map((x) => `<p>${esc(x)}</p>`).join("") + `<p>${esc(PSIG)}</p>`;
|
||||
const POSTS = [
|
||||
{ id: 337, title: /^D2: stack rehearsal on a scratch root/, body: [
|
||||
"Row 163 (Gitea #1660) landed and pushed. Landing commit 8ecc7a239 (Sage; Filbert approve 29447), queue revs 2235-2236 (landing note and done) in 9ae8c73ac, range 4ca8d6e94..9ae8c73ac on refactor. Patch-id b8d5f8a37c24 matches candidate c6f4f4c12596. #1660 is closed with comment 29450.",
|
||||
"Sage gate passed at 18:16Z on eaa0f2666: cli 472/472 and every package and scripts/test-*.sh green. The pushed commit is a cherry-pick of eaa0f2666 onto 7f57419b3, which adds only the Q175 design doc, and no test reads that file. Mos dropped the Part B wait when Q175 replaced Part B (mos-0971).",
|
||||
"Not live. The live stack still runs copies/490e2771d3d0. Rehearse and bus start --no-launch go live at the next re-pin, where the Q173 backup and STATE report apply.",
|
||||
] },
|
||||
{ id: 412, title: /^Console SSO: zero-trust OIDC verification/, body: [
|
||||
"Design r1 is committed: agents/sage/work/q175-sso/DESIGN.md in 7f57419b3 (blob 91e5b1b5e737), pushed in 9ae8c73ac. The Console becomes an OIDC relying party on oauth4webapi 3.8.8, pinned exactly. It always calls validateApplicationLevelSignature, because the library skips the signature check on a token-endpoint ID token. Identity is an (iss, sub) pair, and only asymmetric algs are accepted.",
|
||||
"Filbert's r1 review asks for changes in the doc before any code (B1-B6). B1 moves the provider config to config.json, because agents have write paths into the business file. B2 closes an open-redirect hole. B3 keeps the library's error causes out of logs. B4 caps login records. B5 fixes the signature mutant test. B6 removes a live forged-header probe from the phase 2 acceptance. Their rulings: Q-B start degraded when the IdP is unreachable, Q-C yes (7332 accepts only the public Host under --sso), Q-D keep idle expiry but don't refresh it on poll GETs. r2 follows.",
|
||||
"Interim (mos-0972): the Console runs without --send until phase 2. stack-release.sh has no way to drop --send that keeps stack-active.json and the unit in step, so a small tool row comes first, through review.",
|
||||
] },
|
||||
];
|
||||
const key = readFileSync(KEY, "utf8").trim(), sync = readFileSync(SYNC, "utf8").trim();
|
||||
async function api(method, path, body, auth) {
|
||||
const r = await fetch(ORIGIN + path, { method, headers: { Authorization: `Bearer ${auth}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), redirect: "error" });
|
||||
const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {}
|
||||
return { status: r.status, json };
|
||||
}
|
||||
const list = (j) => j?.items ?? (Array.isArray(j) ? j : []);
|
||||
const rec = { at: new Date().toISOString(), comments: [], problems: [] };
|
||||
const save = () => {
|
||||
const out = JSON.stringify(rec, null, 1) + "\n";
|
||||
if (out.includes(key) || out.includes(sync)) throw new Error("secret in the record; not written");
|
||||
writeFileSync(OUT, out);
|
||||
};
|
||||
for (const P of POSTS) {
|
||||
const t = await api("GET", `/api/v2/tasks/${P.id}`, undefined, sync);
|
||||
if (t.status !== 200 || t.json?.project_id !== PROJECT || !P.title.test(t.json?.title ?? "")) throw new Error(`task ${P.id}: ${t.status}, title check failed; nothing posted`);
|
||||
}
|
||||
writeFileSync(OUT, "", { flag: "wx" });
|
||||
for (const P of POSTS) {
|
||||
const c = await api("PUT", `/api/v1/tasks/${P.id}/comments`, { comment: ps(P.body) }, key);
|
||||
const e = { task: P.id, status: c.status, id: c.json?.id ?? null };
|
||||
rec.comments.push(e);
|
||||
if (c.status >= 300 || !Number.isSafeInteger(c.json?.id)) { rec.problems.push(`comment ${P.id}: ${c.status} ${c.json?.message ?? ""}`); save(); continue; }
|
||||
const back = await api("GET", `/api/v2/tasks/${P.id}/comments?per_page=50`, undefined, sync);
|
||||
const found = list(back.json).find((x) => x.id === c.json.id);
|
||||
Object.assign(e, { readBack: back.status, found: Boolean(found) });
|
||||
if (back.status !== 200 || !found) rec.problems.push(`comment ${P.id} read-back: ${back.status}`);
|
||||
save();
|
||||
}
|
||||
save();
|
||||
process.stdout.write(JSON.stringify(rec, null, 1) + "\n");
|
||||
if (rec.problems.length) process.exit(2);
|
||||
@@ -0,0 +1,51 @@
|
||||
// Sage, 2026-10-20: row 192 (Q175 phase 1, #1690) landed. One comment on P19 412, per mos-0882 (every
|
||||
// landing or decision gets a Vikunja comment). Sage's own key (bot user 13); the sync key reads back. Comments only:
|
||||
// no task edits, labels, assignees or bucket moves. Checks the task's title before posting. Prints no secret.
|
||||
// Usage: node comment-412-r192-landed.mjs https://tasks.woltje.com SAGE_TOKEN_FILE SYNC_TOKEN_FILE OUT_JSON
|
||||
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
||||
const [ORIGIN, KEY, SYNC, OUT] = process.argv.slice(2);
|
||||
if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
|
||||
if (!OUT || existsSync(OUT)) throw new Error("OUT_JSON missing or exists; refusing");
|
||||
const PROJECT = 19;
|
||||
const PSIG = "Posted by Sage (T3 1ef1e4f8) with Sage's own key (bot user 13).";
|
||||
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
const ps = (xs) => xs.map((x) => `<p>${esc(x)}</p>`).join("") + `<p>${esc(PSIG)}</p>`;
|
||||
const POSTS = [
|
||||
{ id: 412, title: /^Console SSO: zero-trust OIDC verification/, body: [
|
||||
"Row 192 (Q175 phase 1, Gitea #1690) landed and pushed. Landing commit 4429a104f (Launchpad's launchpad/row192 53bb59462..37582e820, manifest round 2, 18 paths; Webby approved). Patch-id 4a486902614d matches the reviewed diff. Queue revs 2429-2430 (landing note and done) are in 305857c8f, range 32b54ac3b..305857c8f on refactor. #1690 is closed.",
|
||||
"Sage gate on 088087ac9: package suites 2758 pass 0 fail, scripts/test-*.sh 429 pass 0 fail, mutants 22 of 23 killed (#23, the second origin check, ruled equivalent). The Console now verifies SSO itself under --sso as an OIDC relying party on oauth4webapi 3.8.8, pinned exactly. mosaic-config.mjs load is now async, so in-flight rows that call it rebase and await it (row 190 round 3 is doing this).",
|
||||
"Not live. The live stack still runs copies/490e2771d3d0. The new dependency needs npm ci in a new stack copy, so the live re-pin is a window with Mos (mos-1099 item 3); Sage sends Mos the plan. Row 200 (phase 2) follows.",
|
||||
] },
|
||||
];
|
||||
const key = readFileSync(KEY, "utf8").trim(), sync = readFileSync(SYNC, "utf8").trim();
|
||||
async function api(method, path, body, auth) {
|
||||
const r = await fetch(ORIGIN + path, { method, headers: { Authorization: `Bearer ${auth}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), redirect: "error" });
|
||||
const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {}
|
||||
return { status: r.status, json };
|
||||
}
|
||||
const list = (j) => j?.items ?? (Array.isArray(j) ? j : []);
|
||||
const rec = { at: new Date().toISOString(), comments: [], problems: [] };
|
||||
const save = () => {
|
||||
const out = JSON.stringify(rec, null, 1) + "\n";
|
||||
if (out.includes(key) || out.includes(sync)) throw new Error("secret in the record; not written");
|
||||
writeFileSync(OUT, out);
|
||||
};
|
||||
for (const P of POSTS) {
|
||||
const t = await api("GET", `/api/v2/tasks/${P.id}`, undefined, sync);
|
||||
if (t.status !== 200 || t.json?.project_id !== PROJECT || !P.title.test(t.json?.title ?? "")) throw new Error(`task ${P.id}: ${t.status}, title check failed; nothing posted`);
|
||||
}
|
||||
writeFileSync(OUT, "", { flag: "wx" });
|
||||
for (const P of POSTS) {
|
||||
const c = await api("PUT", `/api/v1/tasks/${P.id}/comments`, { comment: ps(P.body) }, key);
|
||||
const e = { task: P.id, status: c.status, id: c.json?.id ?? null };
|
||||
rec.comments.push(e);
|
||||
if (c.status >= 300 || !Number.isSafeInteger(c.json?.id)) { rec.problems.push(`comment ${P.id}: ${c.status} ${c.json?.message ?? ""}`); save(); continue; }
|
||||
const back = await api("GET", `/api/v2/tasks/${P.id}/comments?per_page=50`, undefined, sync);
|
||||
const found = list(back.json).find((x) => x.id === c.json.id);
|
||||
Object.assign(e, { readBack: back.status, found: Boolean(found) });
|
||||
if (back.status !== 200 || !found) rec.problems.push(`comment ${P.id} read-back: ${back.status}`);
|
||||
save();
|
||||
}
|
||||
save();
|
||||
process.stdout.write(JSON.stringify(rec, null, 1) + "\n");
|
||||
if (rec.problems.length) process.exit(2);
|
||||
@@ -0,0 +1,59 @@
|
||||
// Sage, 2026-10-20: catch-up landing and status comments on P19 413 (row 189), 424 (row 210) and 414 (rows 201
|
||||
// and 204), per mos-0882. Sage's own key (bot user 13); the sync key reads back. Comments only: no task edits.
|
||||
// Checks each task's title before posting. Prints no secret.
|
||||
// Usage: node comment-catchup-2026-10-20.mjs https://tasks.woltje.com SAGE_TOKEN_FILE SYNC_TOKEN_FILE OUT_JSON
|
||||
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
||||
const [ORIGIN, KEY, SYNC, OUT] = process.argv.slice(2);
|
||||
if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
|
||||
if (!OUT || existsSync(OUT)) throw new Error("OUT_JSON missing or exists; refusing");
|
||||
const PROJECT = 19;
|
||||
const PSIG = "Posted by Sage (T3 1ef1e4f8) with Sage's own key (bot user 13).";
|
||||
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
const ps = (xs) => xs.map((x) => `<p>${esc(x)}</p>`).join("") + `<p>${esc(PSIG)}</p>`;
|
||||
const POSTS = [
|
||||
{ id: 413, title: /^stack-release\.sh activate --no-send/, body: [
|
||||
"Row 189 landed on refactor as ace27dc5e (Gitea #1687): stack-release.sh activate --no-send drops the Console's --send and keeps the pointer and the unit in step. Dewey approved (comment 29485), and Sage's gate passed.",
|
||||
"The stack activation log shows the same-copy activate on 490e2771d3d0 after the landing: activation 20261020T192956Z-490e2771d3d0, result ok at 19:30:00Z, with backup 20261020T192956Z-490e2771d3d0. Since then the live Console runs without --send. The re-pin to 4429a104f at 22:02Z (row 192) carried the pointer's flags, no --send and no --sso, as its W3 plan check required.",
|
||||
] },
|
||||
{ id: 424, title: /^Credential store: per-agent host access/, body: [
|
||||
"Status, 2026-10-20 23:20Z. Row 210 (Gizmoduck) builds this task. Its design r3 is approved, and the machine-grant design final r6 f9039682e is approved for code once row 210 lands (mos-1141, mos-1143). Every grant name sits under grant.* behind one guard (mos-1144). No agent creates a grant: the chief requests, Jason approves, and protected code creates it (Q188).",
|
||||
"Order (mos-1135, mos-1137): row 210 lands only after row 199, the per-agent credential store. Row 199's gate is green on c6868ba41. Megavolt fixed five surviving mutants and is rerunning all 28 now on 7c3cdd940, before the Duckworth review. Rows 190, 199 and 210 go live together in the one 10-24 window with the 3i-3j migration (mos-1145 line 9), backup first, under Q173.",
|
||||
] },
|
||||
{ id: 414, title: /^MVP platform: the Given list for Cases 1 and 2/, body: [
|
||||
"RBAC rows landed (Given list, RBAC enforcement). Row 201, FED R1: access.json loader and validator with covers, afad4993f (Gitea #1699), Gizmoduck, Duckworth approved (comment 29581). Gate: business 173/0, scripts/test-*.sh 422/0, mutants 44 of 44 killed.",
|
||||
"Row 204, 193a: the access level check widened to viewer, d0bbc5c1c (Gitea #1702), Morgana, Duckworth approved (comment 29595). LEVELS gains collaborator and viewer, covers() takes a required least level, and an unknown grant level covers nothing. Gate: packages and scripts 2789/0, mutants 7 of 7 killed.",
|
||||
"Neither changes the live stack. Rows 205-208 (193b1 to 193d) follow, and 206 adds a schema step, so it goes live only in a --migrate window.",
|
||||
] },
|
||||
];
|
||||
const key = readFileSync(KEY, "utf8").trim(), sync = readFileSync(SYNC, "utf8").trim();
|
||||
async function api(method, path, body, auth) {
|
||||
const r = await fetch(ORIGIN + path, { method, headers: { Authorization: `Bearer ${auth}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), redirect: "error" });
|
||||
const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {}
|
||||
return { status: r.status, json };
|
||||
}
|
||||
const list = (j) => j?.items ?? (Array.isArray(j) ? j : []);
|
||||
const rec = { at: new Date().toISOString(), comments: [], problems: [] };
|
||||
const save = () => {
|
||||
const out = JSON.stringify(rec, null, 1) + "\n";
|
||||
if (out.includes(key) || out.includes(sync)) throw new Error("secret in the record; not written");
|
||||
writeFileSync(OUT, out);
|
||||
};
|
||||
for (const P of POSTS) {
|
||||
const t = await api("GET", `/api/v2/tasks/${P.id}`, undefined, sync);
|
||||
if (t.status !== 200 || t.json?.project_id !== PROJECT || !P.title.test(t.json?.title ?? "")) throw new Error(`task ${P.id}: ${t.status}, title check failed; nothing posted`);
|
||||
}
|
||||
writeFileSync(OUT, "", { flag: "wx" });
|
||||
for (const P of POSTS) {
|
||||
const c = await api("PUT", `/api/v1/tasks/${P.id}/comments`, { comment: ps(P.body) }, key);
|
||||
const e = { task: P.id, status: c.status, id: c.json?.id ?? null };
|
||||
rec.comments.push(e);
|
||||
if (c.status >= 300 || !Number.isSafeInteger(c.json?.id)) { rec.problems.push(`comment ${P.id}: ${c.status} ${c.json?.message ?? ""}`); save(); continue; }
|
||||
const back = await api("GET", `/api/v2/tasks/${P.id}/comments?per_page=50`, undefined, sync);
|
||||
const found = list(back.json).find((x) => x.id === c.json.id);
|
||||
Object.assign(e, { readBack: back.status, found: Boolean(found) });
|
||||
if (back.status !== 200 || !found) rec.problems.push(`comment ${P.id} read-back: ${back.status}`);
|
||||
save();
|
||||
}
|
||||
save();
|
||||
process.stdout.write(JSON.stringify(rec, null, 1) + "\n");
|
||||
if (rec.problems.length) process.exit(2);
|
||||
@@ -0,0 +1,60 @@
|
||||
// Sage, 2026-10-21: re-plan comments on P19 405-408 and 411 (due 10-22). Sage's own key (bot user 13); the sync key reads back. Comments only: no task edits.
|
||||
// Checks each task's title before posting. Prints no secret.
|
||||
// Usage: node comment-p19-405-411-replan.mjs https://tasks.woltje.com SAGE_TOKEN_FILE SYNC_TOKEN_FILE OUT_JSON
|
||||
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
||||
const [ORIGIN, KEY, SYNC, OUT] = process.argv.slice(2);
|
||||
if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
|
||||
if (!OUT || existsSync(OUT)) throw new Error("OUT_JSON missing or exists; refusing");
|
||||
const PROJECT = 19;
|
||||
const PSIG = "Posted by Sage (T3 1ef1e4f8) with Sage's own key (bot user 13).";
|
||||
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
const ps = (xs) => xs.map((x) => `<p>${esc(x)}</p>`).join("") + `<p>${esc(PSIG)}</p>`;
|
||||
const POSTS = [
|
||||
{ id: 405, title: /^stack-release --plan: no test plans a missing store/, body: [
|
||||
"Re-plan (Sage, 2026-10-21, due 10-22). Tasks 405, 406, 407, 408 and 411 become one queue row, a stack-release follow-up row. All five touch scripts/stack-release.sh, scripts/webui-service.sh or docs/TOOLS.md, and rows 232 (Darkwing) and 238 (Launchpad) are changing stack-release.sh now. So I brief the row after 232 and 238 land, from their landing, which avoids base drift (the row 68 rule). It lands after the 10-24 window, because that window holds the four service scripts at NEW (P5). 411's hold was the Part B window, and Part B is superseded (Q175, mos-0970), so that hold is gone. Each item keeps the test and mutant its task names. I'll name the owner when I brief it, either the seat that frees up first of Darkwing and Launchpad, or me.",
|
||||
] },
|
||||
{ id: 406, title: /^stack-release --plan: backup line printed/, body: [
|
||||
"Re-plan (Sage, 2026-10-21, due 10-22). Tasks 405, 406, 407, 408 and 411 become one queue row, a stack-release follow-up row. All five touch scripts/stack-release.sh, scripts/webui-service.sh or docs/TOOLS.md, and rows 232 (Darkwing) and 238 (Launchpad) are changing stack-release.sh now. So I brief the row after 232 and 238 land, from their landing, which avoids base drift (the row 68 rule). It lands after the 10-24 window, because that window holds the four service scripts at NEW (P5). 411's hold was the Part B window, and Part B is superseded (Q175, mos-0970), so that hold is gone. Each item keeps the test and mutant its task names. I'll name the owner when I brief it, either the seat that frees up first of Darkwing and Launchpad, or me.",
|
||||
] },
|
||||
{ id: 407, title: /^stack-release\.sh: the proof_for_target comment/, body: [
|
||||
"Re-plan (Sage, 2026-10-21, due 10-22). Tasks 405, 406, 407, 408 and 411 become one queue row, a stack-release follow-up row. All five touch scripts/stack-release.sh, scripts/webui-service.sh or docs/TOOLS.md, and rows 232 (Darkwing) and 238 (Launchpad) are changing stack-release.sh now. So I brief the row after 232 and 238 land, from their landing, which avoids base drift (the row 68 rule). It lands after the 10-24 window, because that window holds the four service scripts at NEW (P5). 411's hold was the Part B window, and Part B is superseded (Q175, mos-0970), so that hold is gone. Each item keeps the test and mutant its task names. I'll name the owner when I brief it, either the seat that frees up first of Darkwing and Launchpad, or me.",
|
||||
] },
|
||||
{ id: 408, title: /^docs\/TOOLS\.md: the row 178 paragraph/, body: [
|
||||
"Re-plan (Sage, 2026-10-21, due 10-22). Tasks 405, 406, 407, 408 and 411 become one queue row, a stack-release follow-up row. All five touch scripts/stack-release.sh, scripts/webui-service.sh or docs/TOOLS.md, and rows 232 (Darkwing) and 238 (Launchpad) are changing stack-release.sh now. So I brief the row after 232 and 238 land, from their landing, which avoids base drift (the row 68 rule). It lands after the 10-24 window, because that window holds the four service scripts at NEW (P5). 411's hold was the Part B window, and Part B is superseded (Q175, mos-0970), so that hold is gone. Each item keeps the test and mutant its task names. I'll name the owner when I brief it, either the seat that frees up first of Darkwing and Launchpad, or me.",
|
||||
] },
|
||||
{ id: 411, title: /^stack-release\.sh and webui-service\.sh: the proof file repository check/, body: [
|
||||
"Re-plan (Sage, 2026-10-21, due 10-22). Tasks 405, 406, 407, 408 and 411 become one queue row, a stack-release follow-up row. All five touch scripts/stack-release.sh, scripts/webui-service.sh or docs/TOOLS.md, and rows 232 (Darkwing) and 238 (Launchpad) are changing stack-release.sh now. So I brief the row after 232 and 238 land, from their landing, which avoids base drift (the row 68 rule). It lands after the 10-24 window, because that window holds the four service scripts at NEW (P5). 411's hold was the Part B window, and Part B is superseded (Q175, mos-0970), so that hold is gone. Each item keeps the test and mutant its task names. I'll name the owner when I brief it, either the seat that frees up first of Darkwing and Launchpad, or me.",
|
||||
] },
|
||||
];
|
||||
const key = readFileSync(KEY, "utf8").trim(), sync = readFileSync(SYNC, "utf8").trim();
|
||||
async function api(method, path, body, auth) {
|
||||
const r = await fetch(ORIGIN + path, { method, headers: { Authorization: `Bearer ${auth}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), redirect: "error" });
|
||||
const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {}
|
||||
return { status: r.status, json };
|
||||
}
|
||||
const list = (j) => j?.items ?? (Array.isArray(j) ? j : []);
|
||||
const rec = { at: new Date().toISOString(), comments: [], problems: [] };
|
||||
const save = () => {
|
||||
const out = JSON.stringify(rec, null, 1) + "\n";
|
||||
if (out.includes(key) || out.includes(sync)) throw new Error("secret in the record; not written");
|
||||
writeFileSync(OUT, out);
|
||||
};
|
||||
for (const P of POSTS) {
|
||||
const t = await api("GET", `/api/v2/tasks/${P.id}`, undefined, sync);
|
||||
if (t.status !== 200 || t.json?.project_id !== PROJECT || !P.title.test(t.json?.title ?? "")) throw new Error(`task ${P.id}: ${t.status}, title check failed; nothing posted`);
|
||||
}
|
||||
writeFileSync(OUT, "", { flag: "wx" });
|
||||
for (const P of POSTS) {
|
||||
const c = await api("PUT", `/api/v1/tasks/${P.id}/comments`, { comment: ps(P.body) }, key);
|
||||
const e = { task: P.id, status: c.status, id: c.json?.id ?? null };
|
||||
rec.comments.push(e);
|
||||
if (c.status >= 300 || !Number.isSafeInteger(c.json?.id)) { rec.problems.push(`comment ${P.id}: ${c.status} ${c.json?.message ?? ""}`); save(); continue; }
|
||||
const back = await api("GET", `/api/v2/tasks/${P.id}/comments?per_page=50`, undefined, sync);
|
||||
const found = list(back.json).find((x) => x.id === c.json.id);
|
||||
Object.assign(e, { readBack: back.status, found: Boolean(found) });
|
||||
if (back.status !== 200 || !found) rec.problems.push(`comment ${P.id} read-back: ${back.status}`);
|
||||
save();
|
||||
}
|
||||
save();
|
||||
process.stdout.write(JSON.stringify(rec, null, 1) + "\n");
|
||||
if (rec.problems.length) process.exit(2);
|
||||
@@ -0,0 +1,53 @@
|
||||
// Sage, 2026-10-21: landing comments on P19 412 (row 200) and 424 (row 199), per mos-0882. Sage's own key (bot user 13); the sync key reads back. Comments only: no task edits.
|
||||
// Checks each task's title before posting. Prints no secret.
|
||||
// Usage: node comment-r200-r199-landed.mjs https://tasks.woltje.com SAGE_TOKEN_FILE SYNC_TOKEN_FILE OUT_JSON
|
||||
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
||||
const [ORIGIN, KEY, SYNC, OUT] = process.argv.slice(2);
|
||||
if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
|
||||
if (!OUT || existsSync(OUT)) throw new Error("OUT_JSON missing or exists; refusing");
|
||||
const PROJECT = 19;
|
||||
const PSIG = "Posted by Sage (T3 1ef1e4f8) with Sage's own key (bot user 13).";
|
||||
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
const ps = (xs) => xs.map((x) => `<p>${esc(x)}</p>`).join("") + `<p>${esc(PSIG)}</p>`;
|
||||
const POSTS = [
|
||||
{ id: 412, title: /^Console SSO: zero-trust OIDC verification/, body: [
|
||||
"Row 200 landed on refactor as 9e1085816 (Gitea #1698, closed): stack-release.sh activate --sso, the phase 2 prerequisite. Launchpad built it and Webby approved r3. Sage's run killed 13 of 14 mutants (M12 is the equivalent ruled in round 2). Gate: 15 package suites and 10 scripts/test-*.sh suites, all green, at 00:07:57Z.",
|
||||
"Follow-up N1 is row 238 (Gitea #1744, Launchpad, Webby). The sso-config hint will carry --instance NAME for a named instance. Nothing is live until a window activates a copy that includes 9e1085816.",
|
||||
] },
|
||||
{ id: 424, title: /^Credential store: per-agent host access/, body: [
|
||||
"Row 199 landed on refactor as cb5fdb064 (Gitea #1697, closed). It adds the per-agent credential store, the agent's own git identity and pushes to a remote. Megavolt built it and Duckworth approved r1 (comment 29779). Gate on 9e1085816 plus the patch: packages 2735/0, scripts 429/0.",
|
||||
"Row 210 (Gizmoduck) now rebases onto cb5fdb064 on a new branch (mos-1135, mos-1137). Rows 234 and 235 follow it. Rows 190, 199 and 210 go live together in the 10-24 window with the 3i-3j migration, backup first, under Q173 (mos-1145).",
|
||||
] },
|
||||
];
|
||||
const key = readFileSync(KEY, "utf8").trim(), sync = readFileSync(SYNC, "utf8").trim();
|
||||
async function api(method, path, body, auth) {
|
||||
const r = await fetch(ORIGIN + path, { method, headers: { Authorization: `Bearer ${auth}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), redirect: "error" });
|
||||
const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {}
|
||||
return { status: r.status, json };
|
||||
}
|
||||
const list = (j) => j?.items ?? (Array.isArray(j) ? j : []);
|
||||
const rec = { at: new Date().toISOString(), comments: [], problems: [] };
|
||||
const save = () => {
|
||||
const out = JSON.stringify(rec, null, 1) + "\n";
|
||||
if (out.includes(key) || out.includes(sync)) throw new Error("secret in the record; not written");
|
||||
writeFileSync(OUT, out);
|
||||
};
|
||||
for (const P of POSTS) {
|
||||
const t = await api("GET", `/api/v2/tasks/${P.id}`, undefined, sync);
|
||||
if (t.status !== 200 || t.json?.project_id !== PROJECT || !P.title.test(t.json?.title ?? "")) throw new Error(`task ${P.id}: ${t.status}, title check failed; nothing posted`);
|
||||
}
|
||||
writeFileSync(OUT, "", { flag: "wx" });
|
||||
for (const P of POSTS) {
|
||||
const c = await api("PUT", `/api/v1/tasks/${P.id}/comments`, { comment: ps(P.body) }, key);
|
||||
const e = { task: P.id, status: c.status, id: c.json?.id ?? null };
|
||||
rec.comments.push(e);
|
||||
if (c.status >= 300 || !Number.isSafeInteger(c.json?.id)) { rec.problems.push(`comment ${P.id}: ${c.status} ${c.json?.message ?? ""}`); save(); continue; }
|
||||
const back = await api("GET", `/api/v2/tasks/${P.id}/comments?per_page=50`, undefined, sync);
|
||||
const found = list(back.json).find((x) => x.id === c.json.id);
|
||||
Object.assign(e, { readBack: back.status, found: Boolean(found) });
|
||||
if (back.status !== 200 || !found) rec.problems.push(`comment ${P.id} read-back: ${back.status}`);
|
||||
save();
|
||||
}
|
||||
save();
|
||||
process.stdout.write(JSON.stringify(rec, null, 1) + "\n");
|
||||
if (rec.problems.length) process.exit(2);
|
||||
@@ -0,0 +1,66 @@
|
||||
// Sage, 2026-10-20: W11 of the re-pin window 490e2771d to 4429a104f (STATE.md in commit 99a406ed3) on P19 386, 412
|
||||
// and 393, and the row 190 landing (00d4cc1f6, #1688) on P19 414, per mos-0882. Sage's own key (bot user 13); the
|
||||
// sync key reads back. Comments only: no task edits. Checks each task's title before posting. Prints no secret.
|
||||
// Usage: node comment-w11-r190-2026-10-20.mjs https://tasks.woltje.com SAGE_TOKEN_FILE SYNC_TOKEN_FILE OUT_JSON
|
||||
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
||||
const [ORIGIN, KEY, SYNC, OUT] = process.argv.slice(2);
|
||||
if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
|
||||
if (!OUT || existsSync(OUT)) throw new Error("OUT_JSON missing or exists; refusing");
|
||||
const PROJECT = 19;
|
||||
const PSIG = "Posted by Sage (T3 1ef1e4f8) with Sage's own key (bot user 13).";
|
||||
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
const ps = (xs) => xs.map((x) => `<p>${esc(x)}</p>`).join("") + `<p>${esc(PSIG)}</p>`;
|
||||
const STATE = "agents/sage/work/window-repin-4429a104f/STATE.md in commit 99a406ed3 (pushed), sha256 a4d0a7b12f431f15";
|
||||
const POSTS = [
|
||||
{ id: 386, title: /^Q173 test system: tester writes/, body: [
|
||||
"Q173 re-pin window, 490e2771d to 4429a104f (row 192), done. STATE report: " + STATE + ".",
|
||||
"Activation 20261020T220206Z-4429a104f6d6 with --pm, exit 0, every gate check including pm_launched. Store stays on 3i, migration none. Backup ~/.mosaic-dev/backups/stack/20261020T220206Z-4429a104f6d6 (SHA256SUMS b61d6484), rollback set beside it; rollback would go to 490e2771d. Business file unchanged (sha256 1e49bdf0). Console flags unchanged: no --send, no --sso, --tester-port 7333.",
|
||||
"W0 to W10 passed, nothing rolled back. W10 stopped once on two browser checks; both were checks written before row 192 (the hidden #sign-out link, and a chat check that read only the first project list). Mos passed them (mos-1135) and the corrected checks are in the plan. Matrix line g is 6575 bytes, not 6454, because row 192 grows index.html by 121 bytes; Mos passed it (mos-1137).",
|
||||
"S1 to S2 store deltas include task_snapshots +7 and task_comment_cursors +5. The writer is the bus's Vikunja poll (packages/tasks/src/adapter.mjs tick, sync.mjs recordTask), source 'poll', not the window. Nobody acted as Jason: no --by jason, no pi, no Save, no mosaic name, bus.tester untouched.",
|
||||
] },
|
||||
{ id: 412, title: /^Console SSO: zero-trust OIDC verification/, body: [
|
||||
"Row 192 (Q175 phase 1) is live in copy 4429a104f since activation 20261020T220206Z (Q173 window with Mos). The sign-in code is in the copy but dormant: the Console runs without --sso, and the proof pair stays unset. The new oauth4webapi 3.8.8 dependency came in through npm ci in the new copy and imports cleanly from it.",
|
||||
"STATE report: " + STATE + ". The webui journal since activation shows one 'Proxy: proxy proof off' line and no 'Sign-in:' line, as expected without --sso.",
|
||||
] },
|
||||
{ id: 393, title: /^dragon-lin: seven legacy user units in failed state/, body: [
|
||||
"Current failure times read during the Q173 re-pin window W11 (systemctl --user show, CDT with UTC): mosaic-standup-pm.service failed 2026-10-19 19:00:28 CDT (10-20 00:00:28Z), exit 1. mosaic-install-drift.service failed 2026-10-20 00:12:54 CDT (05:12:54Z), exit 9. [email protected] failed 2026-10-20 13:16:12 CDT (18:16:12Z), exit 4.",
|
||||
"The marker's journal at 18:16:12Z says its tmux send to jarvis failed ('tmux target not found', socket mosaic-fleet), so that page never arrived; the record is in ~/.local/state/mosaic-unit-failures/20261020.jsonl. mosaic-reconcile.service itself last ran OK at 17:16:27 CDT (22:16:27Z), so the marker is stale until reset.",
|
||||
"All three predate the re-pin at 22:02Z and come from the legacy ~/.mosaic and jarvis-brain setup. The window changed none of them. Disabling or resetting any still needs Jason or ops-01's charter. Details: " + STATE + ", section 'Failed user units'.",
|
||||
] },
|
||||
{ id: 414, title: /^MVP platform: the Given list for Cases 1 and 2/, body: [
|
||||
"Row 190 (several Mosaic Stack instances on one host, MVP T1.1, Gitea #1688) landed and pushed: 00d4cc1f6 on refactor (99a406ed3..00d4cc1f6). Squash of Darkwing's darkwing/row190-r3 176a6bf4b..df65a3d4c, 25 paths, manifest cc0ea9e8, Filbert approved round 3; patch-id matches the branch diff.",
|
||||
"Sage gate on d0bbc5c1c with the patch: 15 package suites 2664 pass 0 fail, 10 scripts/test-*.sh suites 429 pass 0 fail. Mutants 20 of 20 killed. Not live: the stack runs copy 4429a104f until a re-pin. Row 230 (C2-13a) now carries row 190's @CONFIG_ENV@ and @SESSION_ENV@ tokens (gosalyn-0058 item 4).",
|
||||
] },
|
||||
];
|
||||
const key = readFileSync(KEY, "utf8").trim(), sync = readFileSync(SYNC, "utf8").trim();
|
||||
async function api(method, path, body, auth) {
|
||||
const r = await fetch(ORIGIN + path, { method, headers: { Authorization: `Bearer ${auth}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), redirect: "error" });
|
||||
const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {}
|
||||
return { status: r.status, json };
|
||||
}
|
||||
const list = (j) => j?.items ?? (Array.isArray(j) ? j : []);
|
||||
const rec = { at: new Date().toISOString(), comments: [], problems: [] };
|
||||
const save = () => {
|
||||
const out = JSON.stringify(rec, null, 1) + "\n";
|
||||
if (out.includes(key) || out.includes(sync)) throw new Error("secret in the record; not written");
|
||||
writeFileSync(OUT, out);
|
||||
};
|
||||
for (const P of POSTS) {
|
||||
const t = await api("GET", `/api/v2/tasks/${P.id}`, undefined, sync);
|
||||
if (t.status !== 200 || t.json?.project_id !== PROJECT || !P.title.test(t.json?.title ?? "")) throw new Error(`task ${P.id}: ${t.status}, title check failed; nothing posted`);
|
||||
}
|
||||
writeFileSync(OUT, "", { flag: "wx" });
|
||||
for (const P of POSTS) {
|
||||
const c = await api("PUT", `/api/v1/tasks/${P.id}/comments`, { comment: ps(P.body) }, key);
|
||||
const e = { task: P.id, status: c.status, id: c.json?.id ?? null };
|
||||
rec.comments.push(e);
|
||||
if (c.status >= 300 || !Number.isSafeInteger(c.json?.id)) { rec.problems.push(`comment ${P.id}: ${c.status} ${c.json?.message ?? ""}`); save(); continue; }
|
||||
const back = await api("GET", `/api/v2/tasks/${P.id}/comments?per_page=50`, undefined, sync);
|
||||
const found = list(back.json).find((x) => x.id === c.json.id);
|
||||
Object.assign(e, { readBack: back.status, found: Boolean(found) });
|
||||
if (back.status !== 200 || !found) rec.problems.push(`comment ${P.id} read-back: ${back.status}`);
|
||||
save();
|
||||
}
|
||||
save();
|
||||
process.stdout.write(JSON.stringify(rec, null, 1) + "\n");
|
||||
if (rec.problems.length) process.exit(2);
|
||||
@@ -0,0 +1,56 @@
|
||||
// Sage, 2026-10-20: files the activate --no-send row (mos-0972, mos-0973) as one P19 task, owner Sage, no due date.
|
||||
// Sage's own key (bot user 13); the sync key reads back. No labels, assignees, due dates or bucket moves. Refuses if
|
||||
// the title exists. Prints no secret.
|
||||
// Usage: node file-no-send-row.mjs https://tasks.woltje.com SAGE_TOKEN_FILE SYNC_TOKEN_FILE OUT_JSON
|
||||
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
||||
const [ORIGIN, KEY, SYNC, OUT] = process.argv.slice(2);
|
||||
if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
|
||||
if (!OUT || existsSync(OUT)) throw new Error("OUT_JSON missing or exists; refusing");
|
||||
const PROJECT = 19;
|
||||
const SIG = "Created by Sage (T3 1ef1e4f8) with Sage's own key (bot user 13).";
|
||||
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
const ps = (sig, xs) => xs.map((x) => `<p>${esc(x)}</p>`).join("") + `<p>${esc(sig)}</p>`;
|
||||
const T = {
|
||||
title: "stack-release.sh activate --no-send: drop the Console's --send and keep the pointer in step (Q175 interim, mos-0972)",
|
||||
description: ps(SIG, [
|
||||
"Mos ruled (mos-0972) that the Console runs without --send until Q175 phase 2 (P19 #412), because with --send live any same-uid process can forge X-authentik-username on 127.0.0.1:7332 and send as Jason.",
|
||||
"stack-release.sh has no path that drops --send and keeps ~/.mosaic-dev/state/stack-active.json and the unit in step: console_for_target (:658-687) carries the pointer's flags, check_target (:247-249) allows a same-copy activate only for the proof pair, and adopt refuses once a pointer exists. A hand reinstall makes flags_drift refuse every later activate, pm-on and rollback.",
|
||||
"The row adds activate --no-send, modeled on row 187's --no-proof. It drops --send from the pointer and the unit. A same-copy activate is allowed when the pointer holds --send. Rollback and pm-on carry the pointer as they do now. There is no re-enable flag. Brief: docs/plans/2026-10-20_activate-no-send.md. Owner Sage, reviewer Dewey, gate the cli suite plus every scripts/test-*.sh (mos-0973 go).",
|
||||
"After landing: a same-copy activate on 490e2771d3d0 with --no-send under Q173, with the plan output sent to Mos first, a backup first and a STATE report after. Then row 66 lands.",
|
||||
]),
|
||||
};
|
||||
const key = readFileSync(KEY, "utf8").trim(), sync = readFileSync(SYNC, "utf8").trim();
|
||||
async function api(method, path, body, auth) {
|
||||
const r = await fetch(ORIGIN + path, { method, headers: { Authorization: `Bearer ${auth}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), redirect: "error" });
|
||||
const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {}
|
||||
return { status: r.status, json };
|
||||
}
|
||||
const list = (j) => j?.items ?? (Array.isArray(j) ? j : []);
|
||||
async function existing() {
|
||||
const out = [];
|
||||
for (let page = 1; page < 20; page++) {
|
||||
const r = await api("GET", `/api/v2/projects/${PROJECT}/tasks?per_page=50&page=${page}`, undefined, sync);
|
||||
if (r.status !== 200) throw new Error(`list tasks: ${r.status}`);
|
||||
out.push(...list(r.json));
|
||||
if (list(r.json).length < 50) return out;
|
||||
}
|
||||
throw new Error("more than 19 pages");
|
||||
}
|
||||
const rec = { at: new Date().toISOString(), project: PROJECT, created: null, problems: [] };
|
||||
const save = () => {
|
||||
const out = JSON.stringify(rec, null, 1) + "\n";
|
||||
if (out.includes(key) || out.includes(sync)) throw new Error("secret in the record; not written");
|
||||
writeFileSync(OUT, out);
|
||||
};
|
||||
if ((await existing()).some((t) => t.title === T.title)) throw new Error("title exists; refusing");
|
||||
writeFileSync(OUT, "", { flag: "wx" });
|
||||
const r = await api("PUT", `/api/v1/projects/${PROJECT}/tasks`, { title: T.title, description: T.description }, key);
|
||||
rec.created = { status: r.status, id: r.json?.id ?? null };
|
||||
if (r.status >= 300 || !Number.isSafeInteger(r.json?.id)) rec.problems.push(`create: ${r.status} ${r.json?.message ?? ""}`);
|
||||
else {
|
||||
const back = await api("GET", `/api/v2/tasks/${r.json.id}`, undefined, sync);
|
||||
Object.assign(rec.created, { readBack: back.status, done: back.json?.done, inverse: `mark done: POST /api/v1/tasks/${r.json.id} with the whole task, done true (sage key); delete is Jason's or ops-01's` });
|
||||
if (back.status !== 200 || back.json?.title !== T.title || back.json?.done) rec.problems.push("create read-back differs");
|
||||
}
|
||||
save();
|
||||
console.log(JSON.stringify(rec));
|
||||
@@ -0,0 +1,66 @@
|
||||
// Sage, 2026-10-21: files row 237's review follow-ups N1 and N3 (duckworth-0069, #1743 comment 29810) as two P19 tasks.
|
||||
// Sage's own key (bot user 13); the sync key reads back. No labels, assignees, due dates or bucket moves. Refuses if
|
||||
// the title exists. Prints no secret.
|
||||
// Usage: node file-r237-followups.mjs https://tasks.woltje.com SAGE_TOKEN_FILE SYNC_TOKEN_FILE OUT_JSON
|
||||
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
||||
const [ORIGIN, KEY, SYNC, OUT] = process.argv.slice(2);
|
||||
if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
|
||||
if (!OUT || existsSync(OUT)) throw new Error("OUT_JSON missing or exists; refusing");
|
||||
const PROJECT = 19;
|
||||
const SIG = "Created by Sage (T3 1ef1e4f8) with Sage's own key (bot user 13).";
|
||||
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
const ps = (sig, xs) => xs.map((x) => `<p>${esc(x)}</p>`).join("") + `<p>${esc(sig)}</p>`;
|
||||
const TASKS = [
|
||||
{
|
||||
title: "Bus publish: a retry after publish-timeout can push a second fork ref while the first push still runs (row 237 N1)",
|
||||
description: ps(SIG, [
|
||||
"Duckworth's row 237 review N1 (#1743 comment 29810, duckworth-0069). Row 237 makes checkPublish refuse publish-busy while an open ticket holds the same business and decision. publishRefused drops the ticket on publish-timeout, but the bus process's push may still be running, so a retry to another branch can push a second ref to the fork.",
|
||||
"The fix belongs in packages/bus/src/runtime.mjs, in `remotes`: either keep the ticket until the push process exits, or kill the push on timeout and wait for it. A test should start a slow push, time it out, retry to another branch, and expect one ref on the fork. Not in row 237's scope. Megavolt's BUILD.md residual 2 (agents/megavolt/work/queue-237/BUILD.md). Needs a queue row once 237 lands.",
|
||||
]),
|
||||
},
|
||||
{
|
||||
title: "Bus land: check whether checkLand has the same open-ticket race that row 237 fixed in checkPublish (row 237 N3)",
|
||||
description: ps(SIG, [
|
||||
"Row 237 residual 3 (Megavolt's BUILD.md) and Duckworth's N3 (#1743 comment 29810): checkLand has the same ticket shape as checkPublish had before row 237. Two lands on the same decision may both pass the check before either records. Duckworth didn't check it.",
|
||||
"First step: read checkLand and its ticket code in packages/bus/src/broker.mjs against row 237's fix (broker.mjs:1038-1043). If the race exists, file a queue row with a race test like publish-socket.test.mjs's; if not, record why and close this task.",
|
||||
]),
|
||||
},
|
||||
];
|
||||
const key = readFileSync(KEY, "utf8").trim(), sync = readFileSync(SYNC, "utf8").trim();
|
||||
async function api(method, path, body, auth) {
|
||||
const r = await fetch(ORIGIN + path, { method, headers: { Authorization: `Bearer ${auth}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), redirect: "error" });
|
||||
const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {}
|
||||
return { status: r.status, json };
|
||||
}
|
||||
const list = (j) => j?.items ?? (Array.isArray(j) ? j : []);
|
||||
async function existing() {
|
||||
const out = [];
|
||||
for (let page = 1; page < 20; page++) {
|
||||
const r = await api("GET", `/api/v2/projects/${PROJECT}/tasks?per_page=50&page=${page}`, undefined, sync);
|
||||
if (r.status !== 200) throw new Error(`list tasks: ${r.status}`);
|
||||
out.push(...list(r.json));
|
||||
if (list(r.json).length < 50) return out;
|
||||
}
|
||||
throw new Error("more than 19 pages");
|
||||
}
|
||||
const rec = { at: new Date().toISOString(), project: PROJECT, created: null, problems: [] };
|
||||
const save = () => {
|
||||
const out = JSON.stringify(rec, null, 1) + "\n";
|
||||
if (out.includes(key) || out.includes(sync)) throw new Error("secret in the record; not written");
|
||||
writeFileSync(OUT, out);
|
||||
};
|
||||
const have = await existing();
|
||||
for (const T of TASKS) if (have.some((t) => t.title === T.title)) throw new Error("title exists; refusing");
|
||||
writeFileSync(OUT, "", { flag: "wx" });
|
||||
rec.created = [];
|
||||
for (const T of TASKS) {
|
||||
const r = await api("PUT", `/api/v1/projects/${PROJECT}/tasks`, { title: T.title, description: T.description }, key);
|
||||
const c = { status: r.status, id: r.json?.id ?? null };
|
||||
rec.created.push(c);
|
||||
if (r.status >= 300 || !Number.isSafeInteger(r.json?.id)) { rec.problems.push(`create: ${r.status} ${r.json?.message ?? ""}`); continue; }
|
||||
const back = await api("GET", `/api/v2/tasks/${r.json.id}`, undefined, sync);
|
||||
Object.assign(c, { readBack: back.status, done: back.json?.done });
|
||||
if (back.status !== 200 || back.json?.title !== T.title || back.json?.done) rec.problems.push(`read-back differs for ${r.json.id}`);
|
||||
}
|
||||
save();
|
||||
console.log(JSON.stringify(rec));
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"at": "2026-10-20T18:21:15.323Z",
|
||||
"project": 19,
|
||||
"created": {
|
||||
"status": 201,
|
||||
"id": 413,
|
||||
"readBack": 200,
|
||||
"done": false,
|
||||
"inverse": "mark done: POST /api/v1/tasks/413 with the whole task, done true (sage key); delete is Jason's or ops-01's"
|
||||
},
|
||||
"problems": []
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"at": "2026-10-21T01:06:25.071Z",
|
||||
"comments": [
|
||||
{
|
||||
"task": 405,
|
||||
"status": 201,
|
||||
"id": 416,
|
||||
"readBack": 200,
|
||||
"found": true
|
||||
},
|
||||
{
|
||||
"task": 406,
|
||||
"status": 201,
|
||||
"id": 417,
|
||||
"readBack": 200,
|
||||
"found": true
|
||||
},
|
||||
{
|
||||
"task": 407,
|
||||
"status": 201,
|
||||
"id": 418,
|
||||
"readBack": 200,
|
||||
"found": true
|
||||
},
|
||||
{
|
||||
"task": 408,
|
||||
"status": 201,
|
||||
"id": 419,
|
||||
"readBack": 200,
|
||||
"found": true
|
||||
},
|
||||
{
|
||||
"task": 411,
|
||||
"status": 201,
|
||||
"id": 420,
|
||||
"readBack": 200,
|
||||
"found": true
|
||||
}
|
||||
],
|
||||
"problems": []
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"at": "2026-10-20T18:19:36.544Z",
|
||||
"comments": [
|
||||
{
|
||||
"task": 337,
|
||||
"status": 201,
|
||||
"id": 372,
|
||||
"readBack": 200,
|
||||
"found": true
|
||||
},
|
||||
{
|
||||
"task": 412,
|
||||
"status": 201,
|
||||
"id": 373,
|
||||
"readBack": 200,
|
||||
"found": true
|
||||
}
|
||||
],
|
||||
"problems": []
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"at": "2026-10-20T21:35:49.240Z",
|
||||
"comments": [
|
||||
{
|
||||
"task": 412,
|
||||
"status": 201,
|
||||
"id": 395,
|
||||
"readBack": 200,
|
||||
"found": true
|
||||
}
|
||||
],
|
||||
"problems": []
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"at": "2026-10-21T00:28:32.038Z",
|
||||
"comments": [
|
||||
{
|
||||
"task": 412,
|
||||
"status": 201,
|
||||
"id": 414,
|
||||
"readBack": 200,
|
||||
"found": true
|
||||
},
|
||||
{
|
||||
"task": 424,
|
||||
"status": 201,
|
||||
"id": 415,
|
||||
"readBack": 200,
|
||||
"found": true
|
||||
}
|
||||
],
|
||||
"problems": []
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"at": "2026-10-21T00:38:47.170Z",
|
||||
"project": 19,
|
||||
"created": [
|
||||
{
|
||||
"status": 201,
|
||||
"id": 440,
|
||||
"readBack": 200,
|
||||
"done": false
|
||||
},
|
||||
{
|
||||
"status": 201,
|
||||
"id": 441,
|
||||
"readBack": 200,
|
||||
"done": false
|
||||
}
|
||||
],
|
||||
"problems": []
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// Sage, 2026-10-21: read-only. Prints id, done, title, the last update and the description as plain text for the
|
||||
// given P19 tasks, with the sync key. Writes nothing. Prints no secret.
|
||||
// Usage: node read-tasks.mjs https://tasks.woltje.com SYNC_TOKEN_FILE ID...
|
||||
import { readFileSync } from "node:fs";
|
||||
const [ORIGIN, SYNC, ...IDS] = process.argv.slice(2);
|
||||
if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
|
||||
const sync = readFileSync(SYNC, "utf8").trim();
|
||||
const text = (h) => (h ?? "").replace(/<\/p>|<br\s*\/?>|<\/li>/g, "\n").replace(/<[^>]+>/g, "").replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&").trim();
|
||||
for (const id of IDS) {
|
||||
const r = await fetch(`${ORIGIN}/api/v2/tasks/${id}`, { headers: { Authorization: `Bearer ${sync}` }, redirect: "error" });
|
||||
const j = await r.json().catch(() => null);
|
||||
if (r.status !== 200 || j?.project_id !== 19) { console.log(`== ${id}: status ${r.status}, not P19`); continue; }
|
||||
const out = `== ${id} done=${j.done} updated=${j.updated}\n${j.title}\n${text(j.description)}\n`;
|
||||
if (out.includes(sync)) throw new Error("secret in output");
|
||||
console.log(out);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"at": "2026-10-20T22:51:44.218Z",
|
||||
"comments": [
|
||||
{
|
||||
"task": 386,
|
||||
"status": 201,
|
||||
"id": 406,
|
||||
"readBack": 200,
|
||||
"found": true
|
||||
},
|
||||
{
|
||||
"task": 412,
|
||||
"status": 201,
|
||||
"id": 407,
|
||||
"readBack": 200,
|
||||
"found": true
|
||||
},
|
||||
{
|
||||
"task": 393,
|
||||
"status": 201,
|
||||
"id": 408,
|
||||
"readBack": 200,
|
||||
"found": true
|
||||
},
|
||||
{
|
||||
"task": 414,
|
||||
"status": 201,
|
||||
"id": 409,
|
||||
"readBack": 200,
|
||||
"found": true
|
||||
}
|
||||
],
|
||||
"problems": []
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
# 10-24 window: re-pin 4429a104f to the row 234 landing, with the 3i-3j migration (draft 0)
|
||||
|
||||
Sage, 2026-10-21T01:05Z, draft 0, for mos-1145 under DECISIONS Q173. This is the skeleton. NEW isn't fixed yet,
|
||||
so Mos doesn't check this draft. Draft 1 names NEW and goes to Mos for the P0 check. The step layout follows
|
||||
`repin-4429a104f.md`. Steps that match are named, not copied.
|
||||
|
||||
## Scope (mos-1145, mos-1134)
|
||||
|
||||
- One window carries the code of rows 190, 199 and 210, plus row 234 (row a) with its schema step 3i to 3j
|
||||
through `activate --migrate`, agent-run under Q173, backup first.
|
||||
- Row a lands before the .53 and .54 installs, by 10-25 (mos-1134). If row 234 hasn't landed by 10-24 12:00Z,
|
||||
I ask Mos whether to run the window without it (no migration) or to move it.
|
||||
- Jason has no step. Nobody acts as Jason. No pi (P19 383). No Save, no `mosaic name`. Nothing is pruned.
|
||||
- `--sso` stays off. Row 200's `activate --sso` is in NEW as a tool. Turning it on changes who reaches the Console
|
||||
from outside, so that's Jason's (Q173 exclusion). It has its own window, the Q175 phase 2 window on 10-23
|
||||
(mos-1148). That window runs first, so this window's OLD may be the phase 2 copy, and P1 reads it from
|
||||
`status`.
|
||||
|
||||
## Names
|
||||
|
||||
- OLD, live: `4429a104f6d65ccf165f6c83b5d1dce3147dfde0`, schema 3i, pointer `pm: true` (record 99a406ed3).
|
||||
P1 re-reads the activation id and the Console flags from `status`. I don't copy them from the last window.
|
||||
- NEW: `c771a36f4bd0b6c359282425c668996a90072f93`, the row 234 landing (2026-10-21 06:16Z), unless Mos rules a
|
||||
later code landing. 194 is the open candidate. 210, 237, 232, 241, 242, 233 and 234 have landed.
|
||||
- REPO, `P`, `WIN=agents/sage/work/window-repin-<NEW12>/`, `SNAP`: as in `repin-4429a104f.md`, with the
|
||||
sha checks at P0 and before S1, S2 and any D3.
|
||||
|
||||
## What NEW carries over OLD (refactor c771a36f4)
|
||||
|
||||
`git log --first-parent 4429a104f..HEAD`, path-limited to packages, scripts, roles, contracts, adapters, skills
|
||||
and the package files:
|
||||
|
||||
| Row | Commit | Live effect to check |
|
||||
|---|---|---|
|
||||
| 201 | afad4993f | business: access.json loader. Check: the live business has no access.json, and load stays LOAD_OK (P1). |
|
||||
| 204 | d0bbc5c1c | business: 193a viewer level. Same check. |
|
||||
| 190 | 00d4cc1f6 | Several instances per host. Touches `stack-release.sh`, `webui-service.sh`, `bus-service.sh`, `stack-copy.sh`. The default instance must render the same units: P2 diffs the rendered units OLD vs NEW for `mosaic-stack`, and any difference stops the window until it's explained. |
|
||||
| 200 | 9e1085816 | Tool: `activate --sso`. Off here. `--plan` must show no `--sso` line. |
|
||||
| 199 | cb5fdb064 | Credential store per agent. Check: bus start with no store entries for the live agents changes nothing, and the chief's and pm's launches still pass. The P3 rehearsal covers start. |
|
||||
| N3 | 390ea5261 | README only. |
|
||||
| 230 | c8bc5cf4c | Case 2 scripts, tools only. Not run live. |
|
||||
| 210 | f5753c0e8 | Host access. The live business has no `hosts`, so nothing is granted. Bus start runs the host-run sweep (M34d). P3 must show it removes nothing. |
|
||||
| 237 | d7d943199 | Publish busy check (199 N1). |
|
||||
| 232 | e65a3ab4d | Installer I1: `stack-release.sh --units external` and `stack-copy.sh copy --bundle`, `wrapper --create`. Both are opt-in, and this window uses neither. The default units path must render the same units, so the P2 unit diff covers it with row 190. P0 checks that no window command line carries `--units external`. |
|
||||
| 241 | 5d4a61776 | Test only (bus land.test.mjs). No live effect. |
|
||||
| 242 | 5fb736b92 | Bus publish hold: after publish-timeout the bus holds the decision until its push ends, then records a late push. The live business has no publish in flight at W1, and P3 shows none open. Its follow-up fc1cb856b is comments only (git-socket.mjs). |
|
||||
| 233 | 57f59e0c3 | Installer I3: new tool `scripts/stack-upgrade.sh` and a `stack-release.sh` change. This window doesn't run `stack-upgrade.sh`. The P2 unit diff covers the `stack-release.sh` change with rows 190 and 232. |
|
||||
| 234 | c771a36f4 | Machine grant row a and the 3i to 3j schema step (new tables `hosts` and `host_grants`). `--plan` must say `migration: 3i -> 3j`. |
|
||||
| PRD 1.7+1.8 | 0adf8ddfe | `docs/prd/mosaic-stack.md` and `packages/tasks/requirements.json` move to version 1.8 (mos-1154, mos-1156, DECISIONS Q192). The live pm cites 1.6 until this window. Check after: the pm's requirements read answers version 1.8. |
|
||||
|
||||
Service script drift OLD to HEAD is 415+/91- over the four scripts, mostly row 190. P5 holds them at NEW.
|
||||
|
||||
## Backups and the migration
|
||||
|
||||
- W5: the stopped store backup that every activate takes (row 185), with SHA256SUMS, SCHEMA 3i and STAMP,
|
||||
plus the rollback set. Before `--migrate` writes, I check that SCHEMA reads 3i and the backup SHA256SUMS hash.
|
||||
- W0b: the business file copy, as in the last window.
|
||||
- Rollback after a migration isn't the plain rollback. OLD is 3i and the store is 3j, so going back needs
|
||||
`--restore <W5 activation>`, and that loses the events written after W5. The row 181 restore guard counts
|
||||
11 tables, and the untimed ones need the D3 snapshot. Row 234 adds `hosts` and `host_grants` to the untimed
|
||||
list (T46), so at 3j the D3 snapshot covers them too. Draft 1 recounts the list from NEW. Mos decides any restore past W5, as before.
|
||||
- Grant look-alikes (row 234 A22b, gizmoduck-0107). `3i-3j.sql` adds `events_grant_actor` but doesn't check
|
||||
rows already in the store, so after the migration only the actor filter in `GRANT_EVIDENCE` (actor_role NULL,
|
||||
actor_run 'grant') ignores an old session-written row. P3, and again just before `--migrate`: on the
|
||||
backup copy, count the `action.allowed` events whose body.action or body.operation starts with `grant.`, or
|
||||
whose body has `hostLineLeft`. Expect 0, since no grant code has run live. Any nonzero count stops the window
|
||||
until I've read those rows.
|
||||
- P3 is the rehearsal with `--migrate` on a copy of the live backup. It must show the 3j step and the row
|
||||
counts before and after.
|
||||
|
||||
## Row 42 (S7) interaction
|
||||
|
||||
Row 42's pieces may be in flight at window start (sage-1622, mos-1148 C1 and C2). W1 adds: list open pm tasks, and either pm
|
||||
finishes or parks them before W5, or I record which piece crosses the window. No restore while a piece has
|
||||
history unless the window fails.
|
||||
|
||||
## Open before draft 1
|
||||
|
||||
- Q1, ruled by mos-1161: draft 1 doesn't wait for 194. NEW is the latest code landing when I cut draft 1. If
|
||||
194 has landed with a green full gate (15 packages, ten script suites), NEW is the 194 landing and P3 also runs
|
||||
the lifecycle verbs once on the backup copy and gets one pm reply under the new roles/chief-of-staff files. A
|
||||
P3 failure that traces to 194 drops NEW back to c771a36f4 before anything touches live, and the window goes on.
|
||||
Otherwise NEW is c771a36f4 and 194 rides a later window.
|
||||
- Q2, settled by mos-1148: phase 2 is its own window on 10-23. Draft 1 takes OLD and the Console flags,
|
||||
`--sso` included, from `status` after that window.
|
||||
- Q3: who operates. If Darkwing is free, Darkwing operates and I check. Otherwise I operate as in the last
|
||||
window.
|
||||
- The P-step evidence list for rows 199 and 210 at bus start. Both have landed (cb5fdb064, f5753c0e8), so
|
||||
it's due in draft 1. I take each log line and exit code from a P3 rehearsal run, not from the code.
|
||||
- Rows 241 and 242 (bus): P3 shows no publish or land decision open before W5.
|
||||
@@ -0,0 +1,125 @@
|
||||
# Re-pin 490e2771d to 4429a104f (row 192, Q175 phase 1), with npm ci in the new copy
|
||||
|
||||
Sage, 2026-10-20, draft 1, for mos-1099 item 3 under DECISIONS Q173. Mos checks this file at its commit before
|
||||
P0. Any change to this file or to NEW after the check means Mos checks again (mos-0849/0850).
|
||||
|
||||
This is a smaller window than `repin-rows-177-185.md` (draft 2, which Mos checked in mos-0929). Where a step is
|
||||
the same, this file names that file's step and says what differs. The 7332 matrix, the snapshot, the W9 list and
|
||||
the rollback compare are reused unchanged.
|
||||
|
||||
## Who, and what nobody does
|
||||
|
||||
- Sage operates, as the tester. Darkwing holds the gate lane for row 190 round 3 tonight, so I take the
|
||||
operator seat. Mos verifies each step's output before the next one and runs `rehearse` (P3), which reads the
|
||||
live config and a live backup (`docs/TOOLS.md` `rehearse`: Jason or Mos runs it).
|
||||
- Jason has no step. Q173 covers a re-pin of this test system with a backup first and a STATE report after.
|
||||
Nothing is pruned.
|
||||
- Nobody acts as Jason: no `--by jason`, no human shell or tmux pane, every bus read and write carries
|
||||
`--tester`, and no request carries `jason.woltje`. `bus.tester` stays as it is; if it isn't `true`, stop at P1.
|
||||
- No pi (mos-0865 D2, P19 #383). No Save in the human Console, no `mosaic name`.
|
||||
- Outside reach doesn't change. The Console keeps its current flags. `--sso` stays off: row 192's sign-in code is
|
||||
dormant without the flag, and `cli.mjs:57` refuses `--sso` with the preview flags the pointer carries anyway.
|
||||
The proof pair stays unset. The tunnel still forwards 7332 only (P4 and W10 check it).
|
||||
|
||||
## Names
|
||||
|
||||
- OLD, live: `490e2771d3d057ab76991e2c7859cd251857e64c`, activation `20261020T192956Z-490e2771d3d0`, pointer
|
||||
`pm: true`, schema 3i, Console `--port 7332 --public-host mosaic.woltje.com --auth-header X-authentik-username
|
||||
--allow-user jason.woltje --tester-port 7333` (no `--send`, read from `stack-release.sh status` between 21:36Z and 21:39Z).
|
||||
- NEW: `4429a104f6d65ccf165f6c83b5d1dce3147dfde0`, row 192's landing, 103 commits past OLD (`git rev-list --count 490e2771d..4429a104f`).
|
||||
The 106 that `status` showed is OLD to HEAD 305857c8f, so it also counts the three record commits below.
|
||||
The commits after it (5b8def2f1 plans, 176a6bf4b sessions, 305857c8f queue) are records and don't move NEW. If
|
||||
another code row lands before W0 (row 190 is the likely one), NEW stays here unless Mos rules otherwise.
|
||||
- REPO: the canonical checkout. `activate` runs REPO's `webui-service.sh`, so the four service scripts are held
|
||||
at NEW for the window (P5).
|
||||
- `P=/mnt/storage/scratch/sage-pin-de352dfaf` (`business-check.mjs`, `console-check.sh`, checked against
|
||||
`agents/sage/work/window-plan/repin-de352dfaf/SCRIPTS.sha256`).
|
||||
- `WIN=agents/sage/work/window-repin-4429a104f/`, where every transcript goes.
|
||||
- `SNAP=$WIN/snapshot.mjs`, a copy of `agents/darkwing/work/window-rows-177-185/snapshot.mjs`, sha256
|
||||
`639fd28a2674aaa03ce824e3394b4052f21cdef7d6fa49bae61a9927ed341e67` (mos-0899). I check the sha before S1, S2
|
||||
and any D3.
|
||||
|
||||
## What NEW carries over OLD
|
||||
|
||||
The path-limited log over `packages`, `scripts`, `roles`, `contracts`, `adapters`, `skills` and the package files:
|
||||
|
||||
| Row | Commit | Live effect |
|
||||
|---|---|---|
|
||||
| 186 | 269134d8d | Tool only: the activate plan's backup line says what the run will do. |
|
||||
| 187 | 9a71e4e80 | Tool: activate sets, carries and drops the proof pair. The pointer has no pair, so nothing changes. |
|
||||
| 188 | 8fc0ee216 | Tool: proof file checks on a carried pair. No pair, no effect. |
|
||||
| 163 | 8ecc7a239 | Tool: `rehearse`, which P3 uses, and `bus start --no-launch`. CLI `cli.mjs` and `launcher.mjs` gain the `--no-launch` path; the default start is unchanged. |
|
||||
| 189 | ace27dc5e | Tool: `activate --no-send`. Live already runs without `--send`; the pointer carries that. |
|
||||
| 192 | 4429a104f | Console `--sso` (off here), `scripts/mosaic-config.mjs` load becomes async, `webui-service.sh` renders `--sso` only when given, and a new dependency, oauth4webapi 3.8.8. `cli.mjs` imports `sso.mjs` at the top, so the Console won't start from a copy without that module. |
|
||||
|
||||
No schema step: `SCHEMA_VERSION` (`packages/bus/src/store.mjs:17`) is 3i at OLD and NEW, and no migration was
|
||||
added. `--plan` must say `migration: none`. No roles, skills, contracts, adapters or plug-in kinds changed. So: no
|
||||
harness set, no skill carry, no `pm-on`. One `activate --pm`.
|
||||
|
||||
## npm ci in the new copy
|
||||
|
||||
mos-1099 asks for npm ci in the new copy. `stack-copy.sh copy` already does it: it extracts the archive to a temp
|
||||
dir, refuses without `package.json` and `package-lock.json` (:55), runs `npm ci --no-audit --no-fund` there and
|
||||
writes no copy if it fails (:56). W0 checks the result: `node_modules/oauth4webapi/package.json` in the copy reads
|
||||
3.8.8, and `node --input-type=module -e "await import('oauth4webapi')"` from the copy exits 0. The live copy is
|
||||
163M; `/home` has 30G free (94% used), enough for one more.
|
||||
|
||||
## Backups
|
||||
|
||||
| Before | Backup | Taken by |
|
||||
|---|---|---|
|
||||
| W5 | the stopped store, `$DATA/backups/stack/<W5 activation id>/` with SHA256SUMS, DATA_ROOT, SCHEMA 3i, STAMP | `stack-release.sh` (row 185: every activate) |
|
||||
| W5 | the rollback set (units, wrapper, pointer) | `stack-release.sh` |
|
||||
| W9 | the business file, `cp -p` to `~/.config/mosaic-dev/businesses/mosaic-stack.json.pre-window-<stamp>` with its sha256, as evidence nothing wrote it | Sage, in W0b |
|
||||
|
||||
## Before the window (nothing stopped)
|
||||
|
||||
| # | Who | Step |
|
||||
|---|-----|------|
|
||||
| P0 | Mos | The `$P` sha check, as in the 177-185 plan P0. |
|
||||
| P1 | Sage | The business file, read only, the same values as the 177-185 plan P1. I report its sha256 now rather than assume the 06:15Z value. `business-check.mjs` for OLD now and for NEW after W0: LOAD_OK both. |
|
||||
| W0 | Sage | `cd $REPO && scripts/stack-copy.sh copy $NEW`, in a gap between gate-lane runs, since npm ci is heavy. Then the two npm checks above, and `diff <(git -C $REPO show $NEW:package-lock.json) $COPIES/$NEW/package-lock.json` empty. W0 writes only under `copies/` and changes nothing live, so it moves before P2 and P3, which need the copy. |
|
||||
| P2 | Sage | `console-check.sh $NEW` against a stub Console of NEW on a scratch port, once without and once with `--tester-port` on a second scratch port. CONSOLE_PASS both. |
|
||||
| P3 | Mos | `scripts/stack-release.sh rehearse $NEW mosaic-stack --backup 20261020T192956Z-490e2771d3d0 --port <scratch>`. It must pass every gate check but the PM's. It stops no unit and writes no pointer. Transcript in `$WIN/p3/`. Correction (mos-1107): `rehearse` starts NEW's Console with only `--port` and `--business`, not the pointer's flags. So Mos also starts NEW's Console by hand with the pointer's flags on two scratch ports and reads healthz and `/api/config` on both, then stops it (`$WIN/p3/flags-check.txt`, `flags-console.log`). |
|
||||
| P4 | Sage | The tunnel read, as in the 177-185 plan P4. |
|
||||
| P5 | Sage | `git -C $REPO diff --quiet $NEW HEAD -- scripts/stack-release.sh scripts/webui-service.sh scripts/bus-service.sh scripts/stack-copy.sh` and an empty `git status --porcelain` on the same four. Row 190 round 3 touches `webui-service.sh`; if it lands before W5, P5 fails and the window waits for Mos's ruling. |
|
||||
| P6 | Sage | The 7332 matrix (a)-(g) on OLD, as in the 177-185 plan, saved as `$WIN/matrix-old.txt`. |
|
||||
|
||||
## The window
|
||||
|
||||
| # | Who | Step |
|
||||
|---|-----|------|
|
||||
| W0b | Sage | The business file copy. |
|
||||
| W1 | Sage | Idle check (`mosaic agents --business mosaic-stack --tester`, `mosaic launches list`), the start stamp from `date -u`, then S1 `node $SNAP --store "$DATA/bus/bus.sqlite" --held`. |
|
||||
| W2 | Sage | `stack-release.sh status mosaic-stack`: pointer on OLD, `pm: true`, 3i, last line ok, unit flags match. |
|
||||
| W3 | Sage | `stack-release.sh activate $NEW mosaic-stack --pm --plan`. Must show from OLD to NEW, `migration: none`, the backup line, and the Console line with OLD's flags exactly: no `--send`, no `--sso`, no proof pair, `--tester-port 7333`. Any other difference stops here. |
|
||||
| W4 | Sage | The unit reads, as in the 177-185 plan W4. |
|
||||
| W5 | Sage, on Mos's go | `stack-release.sh activate $NEW mosaic-stack --pm`. Exit 0, `result ok`, every gate check including `pm_launched`. Record the activation id, the backup path and STAMP, and the backup SHA256SUMS sha256. Then `status`. |
|
||||
| W9 | Sage, after Mos verifies W5 | The 177-185 plan's W9 list unchanged, the chief turn and the one `assistant` thread turn included (mos-0929 Q3), plus two lines: the webui journal since the W5 stamp (with `UTC`) has one `Proxy: proxy proof off` and no `Sign-in:` line, and `ls -l /proc/<Console pid>/cwd` names the NEW copy. |
|
||||
| W10 | Sage | The browser check on 127.0.0.1:7333 and the matrix again as `$WIN/matrix-new.txt`, identical to `matrix-old.txt`. Then P4 again. Corrections after the window (mos-1135, mos-1137): the matrix line for a file the pin changes isn't "identical". Line (g) serves `index.html`, which row 192 grows by 121 bytes (6470 to 6591), so (g) is 200 non-json at 6575 bytes, not 6454. The two browser checks come from the 177-185 window, before row 192 and before a second thread project. "Only tester forms" now allows `#sign-out` while it is `hidden` and `/api/config` has no `sso: true`. "Chat lists the W9 thread" now reads every `.chat-projects section.panel` list (chat.js:93 renders one per project), not the first. |
|
||||
| W11 | Sage | STATE report in `$WIN/STATE.md`, posted on P19 386 and P19 412 and on Gitea #1690. |
|
||||
|
||||
## Stop conditions and rollback
|
||||
|
||||
The same as the 177-185 plan. Two failures in one window: stop and report (mos-0823).
|
||||
|
||||
Rollback: `stack-release.sh rollback mosaic-stack --plan`, then the real run. The previous distinct copy in the
|
||||
log is 490e2771d, both are 3i, so no `--restore` and no data loss. OLD takes the same Console flags. The R1 and
|
||||
R1post compares are as in the 177-185 plan. A restore past W5 follows that plan's D3 steps, with Mos deciding.
|
||||
|
||||
Keep every copy. Prune nothing.
|
||||
|
||||
## Questions for the plan check
|
||||
|
||||
- Q1: NEW is row 192 alone, not bundled with row 190. Row 190 (several instances) changes `stack-release.sh`
|
||||
and `webui-service.sh` and deserves its own window. I recommend running this one first.
|
||||
- Q2: Sage operates instead of Darkwing, who is on the row 190 gate. OK?
|
||||
- Q3: W0 before P2 and P3. It writes a copy only. OK?
|
||||
|
||||
## What Sage ran for this plan
|
||||
|
||||
Read only, 2026-10-20 between 21:36Z and 21:39Z (clock read 21:39:50Z at the commit): `stack-release.sh status mosaic-stack` (pointer, last five log
|
||||
lines, unit flags match, 106 commits of drift), `git rev-list --count`, `SCHEMA_VERSION` at both ends, the
|
||||
path-limited log, `git diff --stat` on the service scripts, CLI and config loader, `stack-copy.sh:23-56`,
|
||||
`webui/src/cli.mjs:15-120`, `docs/TOOLS.md` on `rehearse`, `du -sh` of the live copy and `df -h /home`. Nothing
|
||||
live was changed.
|
||||
@@ -0,0 +1,3 @@
|
||||
chief rbb708846b605 claude-code since 2026-10-20T19:29:59.053Z
|
||||
pm rc36b17ef3788 claude-code since 2026-10-20T20:20:56.278Z
|
||||
# exit 0
|
||||
@@ -0,0 +1,256 @@
|
||||
{
|
||||
"settings loaded": {
|
||||
"pass": true
|
||||
},
|
||||
"settings": {
|
||||
"banner": "Tester Console. Changes are recorded as tester, never as the human.",
|
||||
"h1": "Settings",
|
||||
"forms": [
|
||||
"sign-out",
|
||||
"conv-form",
|
||||
"routes-form",
|
||||
"instance-add-form",
|
||||
"instance-change-form",
|
||||
"tester-name-form",
|
||||
"tester-name-form",
|
||||
"tester-name-form",
|
||||
"tester-name-form",
|
||||
"tester-name-form",
|
||||
"tester-name-form"
|
||||
],
|
||||
"convFormHidden": true,
|
||||
"panels": [
|
||||
"Routing",
|
||||
"Role instances",
|
||||
"Seat names"
|
||||
],
|
||||
"instancesTable": true,
|
||||
"nameForms": 6,
|
||||
"routesForm": true,
|
||||
"routesSubmitEnabled": true,
|
||||
"forbidden": [],
|
||||
"tokenLimitsPanel": false
|
||||
},
|
||||
"tester banner": {
|
||||
"pass": true,
|
||||
"detail": "Tester Console. Changes are recorded as tester, never as the human."
|
||||
},
|
||||
"instance, routes and name editors present": {
|
||||
"pass": true
|
||||
},
|
||||
"only tester forms (conv-form is index.html:21, global and hidden)": {
|
||||
"pass": false,
|
||||
"detail": [
|
||||
"sign-out",
|
||||
"conv-form",
|
||||
"routes-form",
|
||||
"instance-add-form",
|
||||
"instance-change-form",
|
||||
"tester-name-form",
|
||||
"tester-name-form",
|
||||
"tester-name-form",
|
||||
"tester-name-form",
|
||||
"tester-name-form",
|
||||
"tester-name-form"
|
||||
]
|
||||
},
|
||||
"no launch settings, assistant, limit reset or send": {
|
||||
"pass": true,
|
||||
"detail": []
|
||||
},
|
||||
"no Token limits panel (plan W10 item 2, amended)": {
|
||||
"pass": true
|
||||
},
|
||||
"board": {
|
||||
"busViewHidden": true,
|
||||
"banner": true,
|
||||
"headings": [
|
||||
"Waiting on you –",
|
||||
"All sessions –"
|
||||
],
|
||||
"unavailable": false,
|
||||
"length": 179,
|
||||
"forms": [
|
||||
"sign-out",
|
||||
"conv-form"
|
||||
]
|
||||
},
|
||||
"board reads": {
|
||||
"pass": true,
|
||||
"detail": {
|
||||
"busViewHidden": true,
|
||||
"banner": true,
|
||||
"headings": [
|
||||
"Waiting on you –",
|
||||
"All sessions –"
|
||||
],
|
||||
"unavailable": false,
|
||||
"length": 179,
|
||||
"forms": [
|
||||
"sign-out",
|
||||
"conv-form"
|
||||
]
|
||||
}
|
||||
},
|
||||
"chat": {
|
||||
"hash": "#/chat",
|
||||
"banner": true,
|
||||
"threads": [
|
||||
{
|
||||
"href": "#/chat/threads/d0ba7a66-aa3f-4918-9518-94132bce6b28",
|
||||
"name": "w9",
|
||||
"instance": "assistant"
|
||||
}
|
||||
],
|
||||
"conversationOpen": false,
|
||||
"forms": [
|
||||
"sign-out",
|
||||
"conv-form",
|
||||
"chat-project-form"
|
||||
]
|
||||
},
|
||||
"chat lists the W9 thread": {
|
||||
"pass": false,
|
||||
"detail": [
|
||||
{
|
||||
"href": "#/chat/threads/d0ba7a66-aa3f-4918-9518-94132bce6b28",
|
||||
"name": "w9",
|
||||
"instance": "assistant"
|
||||
}
|
||||
]
|
||||
},
|
||||
"W9 thread page not opened": {
|
||||
"pass": true,
|
||||
"detail": "#/chat"
|
||||
},
|
||||
"tester banner on chat": {
|
||||
"pass": true
|
||||
},
|
||||
"requests": [
|
||||
{
|
||||
"method": "GET",
|
||||
"url": "/api/config",
|
||||
"refused": false
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"url": "/api/config",
|
||||
"refused": false
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"url": "/api/bus/messages",
|
||||
"refused": false
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"url": "/api/bus-status",
|
||||
"refused": false
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"url": "/api/bus/messages",
|
||||
"refused": false
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"url": "/api/settings",
|
||||
"refused": false
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"url": "/api/bus/usage",
|
||||
"refused": false
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"url": "/api/settings",
|
||||
"refused": false
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"url": "/api/bus/usage",
|
||||
"refused": false
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"url": "/api/bus/names",
|
||||
"refused": false
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"url": "/api/bus/names",
|
||||
"refused": false
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"url": "/api/bus/instances",
|
||||
"refused": false
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"url": "/api/bus/instances",
|
||||
"refused": false
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"url": "/api/bus/routes",
|
||||
"refused": false
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"url": "/api/bus/routes",
|
||||
"refused": false
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"url": "/api/bus/limits",
|
||||
"refused": false
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"url": "/api/bus/limits",
|
||||
"refused": false
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"url": "/api/bus/inbox",
|
||||
"refused": false
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"url": "/api/bus/agents",
|
||||
"refused": false
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"url": "/api/bus/messages",
|
||||
"refused": false
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"url": "/api/chat/projects",
|
||||
"refused": false
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"url": "/api/chat/threads",
|
||||
"refused": false
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"url": "/api/business",
|
||||
"refused": false
|
||||
}
|
||||
],
|
||||
"no write through the listener": {
|
||||
"pass": true,
|
||||
"detail": []
|
||||
},
|
||||
"no request to the W9 thread messages or open": {
|
||||
"pass": true,
|
||||
"detail": []
|
||||
}
|
||||
}
|
||||
W10 browser: FAIL
|
||||
# exit 1
|
||||
@@ -0,0 +1,200 @@
|
||||
{
|
||||
"config": {
|
||||
"sso": null
|
||||
},
|
||||
"forms": [
|
||||
{
|
||||
"id": "sign-out",
|
||||
"hidden": true,
|
||||
"action": "/auth/logout",
|
||||
"method": "post"
|
||||
},
|
||||
{
|
||||
"id": "conv-form",
|
||||
"hidden": true,
|
||||
"action": null,
|
||||
"method": null
|
||||
},
|
||||
{
|
||||
"id": "routes-form",
|
||||
"hidden": false,
|
||||
"action": null,
|
||||
"method": null
|
||||
},
|
||||
{
|
||||
"id": "instance-add-form",
|
||||
"hidden": false,
|
||||
"action": null,
|
||||
"method": null
|
||||
},
|
||||
{
|
||||
"id": "instance-change-form",
|
||||
"hidden": false,
|
||||
"action": null,
|
||||
"method": null
|
||||
},
|
||||
{
|
||||
"id": "tester-name-form",
|
||||
"hidden": false,
|
||||
"action": null,
|
||||
"method": null
|
||||
},
|
||||
{
|
||||
"id": "tester-name-form",
|
||||
"hidden": false,
|
||||
"action": null,
|
||||
"method": null
|
||||
},
|
||||
{
|
||||
"id": "tester-name-form",
|
||||
"hidden": false,
|
||||
"action": null,
|
||||
"method": null
|
||||
},
|
||||
{
|
||||
"id": "tester-name-form",
|
||||
"hidden": false,
|
||||
"action": null,
|
||||
"method": null
|
||||
},
|
||||
{
|
||||
"id": "tester-name-form",
|
||||
"hidden": false,
|
||||
"action": null,
|
||||
"method": null
|
||||
},
|
||||
{
|
||||
"id": "tester-name-form",
|
||||
"hidden": false,
|
||||
"action": null,
|
||||
"method": null
|
||||
}
|
||||
],
|
||||
"chat": {
|
||||
"hash": "#/chat",
|
||||
"open": false,
|
||||
"projects": [
|
||||
{
|
||||
"project": "window-20261020-0801",
|
||||
"threads": [
|
||||
"#/chat/threads/d0ba7a66-aa3f-4918-9518-94132bce6b28"
|
||||
]
|
||||
},
|
||||
{
|
||||
"project": "window-20261020-2205",
|
||||
"threads": [
|
||||
"#/chat/threads/446201de-e8b5-4df3-9356-d0d5422cc899"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"requests": [
|
||||
{
|
||||
"method": "GET",
|
||||
"url": "/api/config",
|
||||
"refused": false
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"url": "/api/config",
|
||||
"refused": false
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"url": "/api/bus/messages",
|
||||
"refused": false
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"url": "/api/bus-status",
|
||||
"refused": false
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"url": "/api/bus/messages",
|
||||
"refused": false
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"url": "/api/settings",
|
||||
"refused": false
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"url": "/api/bus/usage",
|
||||
"refused": false
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"url": "/api/settings",
|
||||
"refused": false
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"url": "/api/bus/usage",
|
||||
"refused": false
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"url": "/api/bus/names",
|
||||
"refused": false
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"url": "/api/bus/names",
|
||||
"refused": false
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"url": "/api/bus/instances",
|
||||
"refused": false
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"url": "/api/bus/instances",
|
||||
"refused": false
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"url": "/api/bus/routes",
|
||||
"refused": false
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"url": "/api/bus/routes",
|
||||
"refused": false
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"url": "/api/bus/limits",
|
||||
"refused": false
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"url": "/api/bus/limits",
|
||||
"refused": false
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"url": "/api/config",
|
||||
"refused": false
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"url": "/api/chat/projects",
|
||||
"refused": false
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"url": "/api/chat/threads",
|
||||
"refused": false
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"url": "/api/business",
|
||||
"refused": false
|
||||
}
|
||||
]
|
||||
}
|
||||
sign-out hidden true, sso null; W9 thread listed true; thread page open false; writes 0
|
||||
# exit 0
|
||||
@@ -0,0 +1,8 @@
|
||||
a 200 ok=true
|
||||
b 401 error="sign-in required"
|
||||
c 401 error="sign-in required"
|
||||
d 403 error="proxy Host refused"
|
||||
e 403 error="tester listener required"
|
||||
f 403 error="proxy header refused"
|
||||
g 200 non-json 6575 bytes
|
||||
# exit 0
|
||||
@@ -0,0 +1,7 @@
|
||||
# P4 2026-10-20T22:31:29Z
|
||||
DropInPaths=
|
||||
ExecStart={ path=/home/jwoltje/.local/bin/chisel ; argv[]=/home/jwoltje/.local/bin/chisel client --fingerprint ce+ISFvMAhuoCRDuc7riIf+hYz87TLHk/C9C/H8QsUg= --keepalive 25s --max-retry-interval 10s http://10.1.1.227:8080 R:0.0.0.0:7332:127.0.0.1:7332 ; ignore_errors=no ; start_time=[n/a] ; stop_time=[n/a] ; pid=0 ; code=(null) ; status=0/0 }
|
||||
# exit 0
|
||||
# R: specs (1): R:0.0.0.0:7332:127.0.0.1:7332
|
||||
P4 PASS: one R: spec R:0.0.0.0:7332:127.0.0.1:7332, DropInPaths empty
|
||||
# exit 0
|
||||
@@ -0,0 +1,6 @@
|
||||
ActiveState=active
|
||||
DropInPaths=
|
||||
MainPID=2950923
|
||||
ExecStart={ path=/home/jwoltje/.local/share/mosaic-stack/copies/4429a104f6d65ccf165f6c83b5d1dce3147dfde0/scripts/mosaic ; argv[]=/home/jwoltje/.local/share/mosaic-stack/copies/4429a104f6d65ccf165f6c83b5d1dce3147dfde0/scripts/mosaic bus start mosaic-stack --pm ; ignore_errors=no ; start_time=[Tue 2026-10-20 17:02:08 CDT] ; stop_time=[n/a] ; pid=2950923 ; code=(null) ; status=0/0 }
|
||||
WorkingDirectory=/home/jwoltje/.local/share/mosaic-stack/copies/4429a104f6d65ccf165f6c83b5d1dce3147dfde0
|
||||
# exit 0
|
||||
@@ -0,0 +1,12 @@
|
||||
● jarvis-curation-gate.service loaded failed failed Jarvis curation-gate liveness check (wake the agent if staged pulls sit uncurated)
|
||||
● jarvis-retro-check.service loaded failed failed Jarvis weekly retrospective ARTIFACT check (did Sunday's tick produce a retro?)
|
||||
● jarvis-retro.service loaded failed failed Jarvis weekly retrospective tick
|
||||
● mosaic-install-drift.service loaded failed failed Assert the installed Mosaic binaries still match the committed source (drift, not failure)
|
||||
● mosaic-standup-pm.service loaded failed failed Coordinator standup trigger (evening)
|
||||
● [email protected] loaded failed failed Escalate failed Mosaic unit jarvis-curation-gate.service to the local agent seat
|
||||
● [email protected] loaded failed failed Escalate failed Mosaic unit jarvis-retro-check.service to the local agent seat
|
||||
● [email protected] loaded failed failed Escalate failed Mosaic unit jarvis-retro.service to the local agent seat
|
||||
● [email protected] loaded failed failed Escalate failed Mosaic unit mosaic-install-drift.service to the local agent seat
|
||||
● [email protected] loaded failed failed Escalate failed Mosaic unit mosaic-reconcile.service to the local agent seat
|
||||
● setspark-tmp-guard.service loaded failed failed SetSpark: /tmp and scratch guard
|
||||
# exit 0
|
||||
@@ -0,0 +1,5 @@
|
||||
ActiveState=active
|
||||
SubState=running
|
||||
ActiveEnterTimestamp=Tue 2026-10-13 08:40:19 CDT
|
||||
MainPID=1317
|
||||
# exit 0
|
||||
@@ -0,0 +1,6 @@
|
||||
ActiveState=active
|
||||
DropInPaths=
|
||||
MainPID=2950925
|
||||
ExecStart={ path=/usr/bin/node ; argv[]=/usr/bin/node /home/jwoltje/.local/share/mosaic-stack/copies/4429a104f6d65ccf165f6c83b5d1dce3147dfde0/packages/webui/src/cli.mjs serve --checkout /mnt/storage/src/mosaic-stack --port 7332 --public-host mosaic.woltje.com --auth-header X-authentik-username --allow-user jason.woltje --tester-port 7333 ; ignore_errors=no ; start_time=[Tue 2026-10-20 17:02:08 CDT] ; stop_time=[n/a] ; pid=2950925 ; code=(null) ; status=0/0 }
|
||||
WorkingDirectory=/home/jwoltje/.local/share/mosaic-stack/copies/4429a104f6d65ccf165f6c83b5d1dce3147dfde0
|
||||
# exit 0
|
||||
@@ -0,0 +1,2 @@
|
||||
chief r097436c91816 claude-code since 2026-10-20T22:02:08.951Z
|
||||
# exit 0
|
||||
@@ -0,0 +1,7 @@
|
||||
PASS markdown-preview.js is served and equals the copy's
|
||||
PASS /api/config on loopback: send=false edit=false
|
||||
PASS project.files through the Console: head=ae0b17c9586e51da7bc1b7e548c6acf913a55665 files=2 spec=true error=none
|
||||
PASS project.file spec/north-star.md through the Console: commit=ae0b17c9586e51da7bc1b7e548c6acf913a55665 length=99 error=none
|
||||
PASS loopback save refused: HTTP 403 error=read-only sent=false
|
||||
CONSOLE_PASS
|
||||
# exit 0
|
||||
@@ -0,0 +1,19 @@
|
||||
[
|
||||
{
|
||||
"business": "mosaic-stack",
|
||||
"instance": "chief",
|
||||
"run": "r097436c91816",
|
||||
"launchedBy": "jason",
|
||||
"harness": "claude-code",
|
||||
"model": "claude-opus-5-5",
|
||||
"provider": "anthropic",
|
||||
"modelKey": "opus",
|
||||
"pid": 2951160,
|
||||
"startTime": "63491912",
|
||||
"runnerPid": 2951161,
|
||||
"runnerStartTime": "63491912",
|
||||
"runDir": "/home/jwoltje/.mosaic-dev/launches/mosaic-stack/r097436c91816",
|
||||
"startedAt": "2026-10-20T22:02:08.941Z",
|
||||
"live": true
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,2 @@
|
||||
r097436c91816 mosaic-stack/chief claude-code claude-opus-5-5 by jason since 2026-10-20T22:02:08.941Z running
|
||||
# exit 0
|
||||
@@ -0,0 +1,2 @@
|
||||
[]
|
||||
# exit 0
|
||||
@@ -0,0 +1,2 @@
|
||||
created project window-20261020-2205 id 77e7b45f-8c34-49a3-b2fa-9cf565177c30
|
||||
# exit 0
|
||||
@@ -0,0 +1 @@
|
||||
1792533965
|
||||
@@ -0,0 +1,21 @@
|
||||
pointer: /home/jwoltje/.mosaic-dev/state/stack-active.json
|
||||
{"copy":"4429a104f6d65ccf165f6c83b5d1dce3147dfde0","business":"mosaic-stack","pm":true,"claude":"/home/jwoltje/.local/bin/claude","console":["--port","7332","--public-host","mosaic.woltje.com","--auth-header","X-authentik-username","--allow-user","jason.woltje","--tester-port","7333"],"schema":"3i","activation":"20261020T220206Z-4429a104f6d6","at":"2026-10-20T22:02:09Z"}
|
||||
log (last five lines):
|
||||
{"at":"2026-10-20T07:58:02Z","op":"activate","from":"89af38a83afa22a8a4f0d368a0a4030ad15a0efa","to":"490e2771d3d057ab76991e2c7859cd251857e64c","backup":"20261020T075800Z-490e2771d3d0","activation":"20261020T075800Z-490e2771d3d0","result":"ok"}
|
||||
{"at":"2026-10-20T19:29:56Z","op":"activate","from":"490e2771d3d057ab76991e2c7859cd251857e64c","to":"490e2771d3d057ab76991e2c7859cd251857e64c","backup":null,"activation":"20261020T192956Z-490e2771d3d0","result":"started"}
|
||||
{"at":"2026-10-20T19:30:00Z","op":"activate","from":"490e2771d3d057ab76991e2c7859cd251857e64c","to":"490e2771d3d057ab76991e2c7859cd251857e64c","backup":"20261020T192956Z-490e2771d3d0","activation":"20261020T192956Z-490e2771d3d0","result":"ok"}
|
||||
{"at":"2026-10-20T22:02:06Z","op":"activate","from":"490e2771d3d057ab76991e2c7859cd251857e64c","to":"4429a104f6d65ccf165f6c83b5d1dce3147dfde0","backup":null,"activation":"20261020T220206Z-4429a104f6d6","result":"started"}
|
||||
{"at":"2026-10-20T22:02:09Z","op":"activate","from":"490e2771d3d057ab76991e2c7859cd251857e64c","to":"4429a104f6d65ccf165f6c83b5d1dce3147dfde0","backup":"20261020T220206Z-4429a104f6d6","activation":"20261020T220206Z-4429a104f6d6","result":"ok"}
|
||||
check:
|
||||
bus: /home/jwoltje/.local/share/mosaic-stack/copies/4429a104f6d65ccf165f6c83b5d1dce3147dfde0
|
||||
Console: /home/jwoltje/.local/share/mosaic-stack/copies/4429a104f6d65ccf165f6c83b5d1dce3147dfde0
|
||||
CLI: /home/jwoltje/.local/share/mosaic-stack/copies/4429a104f6d65ccf165f6c83b5d1dce3147dfde0
|
||||
result: pass
|
||||
bus status:
|
||||
host: mosaic-stack, pid 2950923, running, since 2026-10-20T22:02:08.133Z, notifier off
|
||||
socket: present
|
||||
writer.lock: pid 2950981
|
||||
boot: organization file loaded at 2026-10-20T22:02:08.125Z
|
||||
unit flags: match the pointer
|
||||
drift: 6 commit(s) on refactor since the pointer's copy
|
||||
# exit 0
|
||||
@@ -0,0 +1,4 @@
|
||||
sent c5b43f04 to chief
|
||||
chief (RESULT, re c5b43f04) c3e236d7:
|
||||
Got the re-pin of 4429a104f6d65ccf165f6c83b5d1dce3147dfde0 from the tester; I took no action because a tester message authorizes nothing, and I launched and changed nothing.
|
||||
# exit 0
|
||||
@@ -0,0 +1,4 @@
|
||||
sent b4b66ced to window-20261020-2205/w9
|
||||
assistant (RESULT, re b4b66ced) cfa7ea99:
|
||||
Assistant (run r497f1c817d4d) got the [mos test] message and is replying in one line as asked, without treating it as Jason's instruction or acting on it.
|
||||
# exit 0
|
||||
@@ -0,0 +1,32 @@
|
||||
[
|
||||
{
|
||||
"seq": 570,
|
||||
"id": "b4b66ced-d088-4b9c-a944-306f164028f9",
|
||||
"at": "2026-10-20T22:05:58.283Z",
|
||||
"business": "mosaic-stack",
|
||||
"from_role": "tester",
|
||||
"from_run": "tester-cli",
|
||||
"to_role": "assistant",
|
||||
"class": "REQUEST",
|
||||
"in_reply_to": null,
|
||||
"decision": null,
|
||||
"corrects": null,
|
||||
"body": "[mos test] reply with one line, then stop",
|
||||
"to_name": null
|
||||
},
|
||||
{
|
||||
"seq": 571,
|
||||
"id": "cfa7ea99-7ccb-4225-80a6-9a903540e337",
|
||||
"at": "2026-10-20T22:06:04.247Z",
|
||||
"business": "mosaic-stack",
|
||||
"from_role": "assistant",
|
||||
"from_run": "r497f1c817d4d",
|
||||
"to_role": "tester",
|
||||
"class": "RESULT",
|
||||
"in_reply_to": "b4b66ced-d088-4b9c-a944-306f164028f9",
|
||||
"decision": null,
|
||||
"corrects": null,
|
||||
"body": "Assistant (run r497f1c817d4d) got the [mos test] message and is replying in one line as asked, without treating it as Jason's instruction or acting on it.",
|
||||
"to_name": null
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,2 @@
|
||||
created window-20261020-2205/w9 id 446201de-e8b5-4df3-9356-d0d5422cc899 instance assistant
|
||||
# exit 0
|
||||
@@ -0,0 +1,3 @@
|
||||
window-20261020-0801/w9 assistant d0ba7a66
|
||||
window-20261020-2205/w9 assistant 446201de
|
||||
# exit 0
|
||||
@@ -0,0 +1 @@
|
||||
446201de-e8b5-4df3-9356-d0d5422cc899
|
||||
@@ -0,0 +1,165 @@
|
||||
{
|
||||
"total": {
|
||||
"input": 62,
|
||||
"output": 9623,
|
||||
"cacheWrite": 186650,
|
||||
"cacheRead": 404132,
|
||||
"unknownTurns": 1
|
||||
},
|
||||
"byRun": [
|
||||
{
|
||||
"run": "r8fba7e598156",
|
||||
"input": 0,
|
||||
"output": 0,
|
||||
"cacheWrite": 0,
|
||||
"cacheRead": 0,
|
||||
"unknownTurns": 1
|
||||
},
|
||||
{
|
||||
"run": "r4a96e2142e4d",
|
||||
"input": 2,
|
||||
"output": 52,
|
||||
"cacheWrite": 11750,
|
||||
"cacheRead": 0,
|
||||
"unknownTurns": 0
|
||||
},
|
||||
{
|
||||
"run": "rc78c9700c8e9",
|
||||
"input": 14,
|
||||
"output": 1519,
|
||||
"cacheWrite": 18187,
|
||||
"cacheRead": 89559,
|
||||
"unknownTurns": 0
|
||||
},
|
||||
{
|
||||
"run": "r293f33bcbc63",
|
||||
"input": 2,
|
||||
"output": 44,
|
||||
"cacheWrite": 18595,
|
||||
"cacheRead": 0,
|
||||
"unknownTurns": 0
|
||||
},
|
||||
{
|
||||
"run": "r34520a581318",
|
||||
"input": 6,
|
||||
"output": 323,
|
||||
"cacheWrite": 26349,
|
||||
"cacheRead": 14307,
|
||||
"unknownTurns": 0
|
||||
},
|
||||
{
|
||||
"run": "r7e99838f9f93",
|
||||
"input": 2,
|
||||
"output": 44,
|
||||
"cacheWrite": 9858,
|
||||
"cacheRead": 0,
|
||||
"unknownTurns": 0
|
||||
},
|
||||
{
|
||||
"run": "rc964611b2092",
|
||||
"input": 4,
|
||||
"output": 120,
|
||||
"cacheWrite": 26138,
|
||||
"cacheRead": 23870,
|
||||
"unknownTurns": 0
|
||||
},
|
||||
{
|
||||
"run": "rbb708846b605",
|
||||
"input": 12,
|
||||
"output": 2089,
|
||||
"cacheWrite": 17285,
|
||||
"cacheRead": 73823,
|
||||
"unknownTurns": 0
|
||||
},
|
||||
{
|
||||
"run": "rc36b17ef3788",
|
||||
"input": 16,
|
||||
"output": 5221,
|
||||
"cacheWrite": 36717,
|
||||
"cacheRead": 202573,
|
||||
"unknownTurns": 0
|
||||
},
|
||||
{
|
||||
"run": "r097436c91816",
|
||||
"input": 2,
|
||||
"output": 131,
|
||||
"cacheWrite": 11915,
|
||||
"cacheRead": 0,
|
||||
"unknownTurns": 0
|
||||
},
|
||||
{
|
||||
"run": "r497f1c817d4d",
|
||||
"input": 2,
|
||||
"output": 80,
|
||||
"cacheWrite": 9856,
|
||||
"cacheRead": 0,
|
||||
"unknownTurns": 0
|
||||
}
|
||||
],
|
||||
"byInstance": [
|
||||
{
|
||||
"instance": "chief",
|
||||
"input": 36,
|
||||
"output": 4114,
|
||||
"cacheWrite": 85486,
|
||||
"cacheRead": 177689,
|
||||
"unknownTurns": 1
|
||||
},
|
||||
{
|
||||
"instance": "coder",
|
||||
"input": 2,
|
||||
"output": 44,
|
||||
"cacheWrite": 18595,
|
||||
"cacheRead": 0,
|
||||
"unknownTurns": 0
|
||||
},
|
||||
{
|
||||
"instance": "assistant",
|
||||
"input": 4,
|
||||
"output": 124,
|
||||
"cacheWrite": 19714,
|
||||
"cacheRead": 0,
|
||||
"unknownTurns": 0
|
||||
},
|
||||
{
|
||||
"instance": "pm",
|
||||
"input": 20,
|
||||
"output": 5341,
|
||||
"cacheWrite": 62855,
|
||||
"cacheRead": 226443,
|
||||
"unknownTurns": 0
|
||||
}
|
||||
],
|
||||
"byProvider": [
|
||||
{
|
||||
"provider": "anthropic",
|
||||
"input": 62,
|
||||
"output": 9623,
|
||||
"cacheWrite": 186650,
|
||||
"cacheRead": 404132,
|
||||
"unknownTurns": 1
|
||||
}
|
||||
],
|
||||
"byModel": [
|
||||
{
|
||||
"provider": "anthropic",
|
||||
"model": "claude-opus-5-5",
|
||||
"input": 62,
|
||||
"output": 9623,
|
||||
"cacheWrite": 186650,
|
||||
"cacheRead": 404132,
|
||||
"unknownTurns": 1
|
||||
}
|
||||
],
|
||||
"byAccount": [
|
||||
{
|
||||
"account": null,
|
||||
"input": 62,
|
||||
"output": 9623,
|
||||
"cacheWrite": 186650,
|
||||
"cacheRead": 404132,
|
||||
"unknownTurns": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
# exit 0
|
||||
@@ -0,0 +1,2 @@
|
||||
r097436c91816 mosaic-stack/chief claude-code claude-opus-5-5 by jason since 2026-10-20T22:02:08.941Z running
|
||||
# exit 0
|
||||
@@ -0,0 +1,19 @@
|
||||
[
|
||||
{
|
||||
"business": "mosaic-stack",
|
||||
"instance": "chief",
|
||||
"run": "r097436c91816",
|
||||
"launchedBy": "jason",
|
||||
"harness": "claude-code",
|
||||
"model": "claude-opus-5-5",
|
||||
"provider": "anthropic",
|
||||
"modelKey": "opus",
|
||||
"pid": 2951160,
|
||||
"startTime": "63491912",
|
||||
"runnerPid": 2951161,
|
||||
"runnerStartTime": "63491912",
|
||||
"runDir": "/home/jwoltje/.mosaic-dev/launches/mosaic-stack/r097436c91816",
|
||||
"startedAt": "2026-10-20T22:02:08.941Z",
|
||||
"live": true
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,153 @@
|
||||
{
|
||||
"schema": "3i",
|
||||
"tables": {
|
||||
"decision_events": {
|
||||
"rows": 21,
|
||||
"maxSeq": 21,
|
||||
"sha256": "28dd95056663dd5eb7442a3d815b73a4a3cd311a0e854fd82dc54f7dc3e6ef26"
|
||||
},
|
||||
"decisions": {
|
||||
"rows": 21,
|
||||
"maxSeq": 21,
|
||||
"sha256": "fef1058660b0d128ef21089dda5e0fba7f71763d8325148fe521de0d3f57e4ad"
|
||||
},
|
||||
"deliveries": {
|
||||
"rows": 1726,
|
||||
"maxSeq": 1726,
|
||||
"sha256": "8667571f4a05ce3621b04cf2dc5eb0311bac3d3aff1858584c78445c5c4c2fb4"
|
||||
},
|
||||
"events": {
|
||||
"rows": 1525,
|
||||
"maxSeq": 1525,
|
||||
"sha256": "e51827e31f2fa5a9a50993e5a10f37f1dee383b7d673f793006bd2ba1e9a5573"
|
||||
},
|
||||
"instance_names": {
|
||||
"rows": 0,
|
||||
"maxSeq": null,
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
"message_threads": {
|
||||
"rows": 2,
|
||||
"maxSeq": null
|
||||
},
|
||||
"messages": {
|
||||
"rows": 567,
|
||||
"maxSeq": 567
|
||||
},
|
||||
"meta": {
|
||||
"rows": 3,
|
||||
"maxSeq": null,
|
||||
"sha256": "89fec56e7293d19e0b2f6cd8c0ef745ebf99e4115abeaea7fbdbc98a261332d9"
|
||||
},
|
||||
"projects": {
|
||||
"rows": 1,
|
||||
"maxSeq": 1,
|
||||
"sha256": "a3945f7462895e0e302029c542d384a6d33e559da900e6461d78bbcd7cc28b16"
|
||||
},
|
||||
"role_claims": {
|
||||
"rows": 98,
|
||||
"maxSeq": 98,
|
||||
"sha256": "ac6694fd0b365469f7486ae1f31486cd52b31be7bc0399938b7361f22454362e"
|
||||
},
|
||||
"task_comment_cursors": {
|
||||
"rows": 215,
|
||||
"maxSeq": 215,
|
||||
"sha256": "7f80df8efb90b502a9530a7ed5068d59be59b2eb318aa4e64ce6bf481762a5e9"
|
||||
},
|
||||
"task_snapshots": {
|
||||
"rows": 401,
|
||||
"maxSeq": 401,
|
||||
"sha256": "cced8c7f90ecaa60eee3010d987013d8c0301563cb888860fd91b8c5a02f0a00"
|
||||
},
|
||||
"thread_claims": {
|
||||
"rows": 2,
|
||||
"maxSeq": 2,
|
||||
"sha256": "6046c7088779aa2f6e9b3fd2c7520404b1678a4fcae71c2b9157a008bba22a5f"
|
||||
},
|
||||
"thread_opens": {
|
||||
"rows": 0,
|
||||
"maxSeq": null,
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
"threads": {
|
||||
"rows": 1,
|
||||
"maxSeq": 1,
|
||||
"sha256": "3043721efd386f23dfdaad4acf41e0d553ff6182f0068c2133a615141bcf772a"
|
||||
},
|
||||
"usage_records": {
|
||||
"rows": 30,
|
||||
"maxSeq": 30,
|
||||
"sha256": "7ecbc0606273043b017d778bb23880377bfe367f788d234289b7c132caa588f2",
|
||||
"byRun": [
|
||||
{
|
||||
"business": "mosaic-stack",
|
||||
"run": "r8fba7e598156",
|
||||
"input": "0",
|
||||
"output": "0",
|
||||
"unknownTurns": 1
|
||||
},
|
||||
{
|
||||
"business": "mosaic-stack",
|
||||
"run": "r4a96e2142e4d",
|
||||
"input": "2",
|
||||
"output": "52",
|
||||
"unknownTurns": 0
|
||||
},
|
||||
{
|
||||
"business": "mosaic-stack",
|
||||
"run": "rc78c9700c8e9",
|
||||
"input": "14",
|
||||
"output": "1519",
|
||||
"unknownTurns": 0
|
||||
},
|
||||
{
|
||||
"business": "mosaic-stack",
|
||||
"run": "r293f33bcbc63",
|
||||
"input": "2",
|
||||
"output": "44",
|
||||
"unknownTurns": 0
|
||||
},
|
||||
{
|
||||
"business": "mosaic-stack",
|
||||
"run": "r34520a581318",
|
||||
"input": "6",
|
||||
"output": "323",
|
||||
"unknownTurns": 0
|
||||
},
|
||||
{
|
||||
"business": "mosaic-stack",
|
||||
"run": "r7e99838f9f93",
|
||||
"input": "2",
|
||||
"output": "44",
|
||||
"unknownTurns": 0
|
||||
},
|
||||
{
|
||||
"business": "mosaic-stack",
|
||||
"run": "rc964611b2092",
|
||||
"input": "4",
|
||||
"output": "120",
|
||||
"unknownTurns": 0
|
||||
},
|
||||
{
|
||||
"business": "mosaic-stack",
|
||||
"run": "rbb708846b605",
|
||||
"input": "12",
|
||||
"output": "2089",
|
||||
"unknownTurns": 0
|
||||
},
|
||||
{
|
||||
"business": "mosaic-stack",
|
||||
"run": "rc36b17ef3788",
|
||||
"input": "16",
|
||||
"output": "5221",
|
||||
"unknownTurns": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"untimed": [
|
||||
"message_threads",
|
||||
"meta",
|
||||
"task_comment_cursors"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
{
|
||||
"schema": "3i",
|
||||
"tables": {
|
||||
"decision_events": {
|
||||
"rows": 21,
|
||||
"maxSeq": 21,
|
||||
"sha256": "28dd95056663dd5eb7442a3d815b73a4a3cd311a0e854fd82dc54f7dc3e6ef26"
|
||||
},
|
||||
"decisions": {
|
||||
"rows": 21,
|
||||
"maxSeq": 21,
|
||||
"sha256": "fef1058660b0d128ef21089dda5e0fba7f71763d8325148fe521de0d3f57e4ad"
|
||||
},
|
||||
"deliveries": {
|
||||
"rows": 1740,
|
||||
"maxSeq": 1740,
|
||||
"sha256": "554ce5b377ffacc72bfde11c99c920cd5d30337c8bfa217275d0073b587e2169"
|
||||
},
|
||||
"events": {
|
||||
"rows": 1543,
|
||||
"maxSeq": 1543,
|
||||
"sha256": "0151673193d0286ce8f3b234d7d21fa2b0f43f2ceec55644cccaa5cbd77603dd"
|
||||
},
|
||||
"instance_names": {
|
||||
"rows": 0,
|
||||
"maxSeq": null,
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
"message_threads": {
|
||||
"rows": 4,
|
||||
"maxSeq": null
|
||||
},
|
||||
"messages": {
|
||||
"rows": 571,
|
||||
"maxSeq": 571
|
||||
},
|
||||
"meta": {
|
||||
"rows": 3,
|
||||
"maxSeq": null,
|
||||
"sha256": "89fec56e7293d19e0b2f6cd8c0ef745ebf99e4115abeaea7fbdbc98a261332d9"
|
||||
},
|
||||
"projects": {
|
||||
"rows": 2,
|
||||
"maxSeq": 2,
|
||||
"sha256": "3435f574ab94bc2fcc65fb8605251f30d4e9e63baf38e6fdf3c39c126531aa63"
|
||||
},
|
||||
"role_claims": {
|
||||
"rows": 101,
|
||||
"maxSeq": 101,
|
||||
"sha256": "fb47114defbb8a57dd148a06fbaf61b30f791a81f14b1510b8c32690506b4308"
|
||||
},
|
||||
"task_comment_cursors": {
|
||||
"rows": 220,
|
||||
"maxSeq": 220,
|
||||
"sha256": "df03c84094999299499de01107ddd08636df6156f5c9ccd2ff6e8c43e459f7e4"
|
||||
},
|
||||
"task_snapshots": {
|
||||
"rows": 408,
|
||||
"maxSeq": 408,
|
||||
"sha256": "e2243a89388afba07e8d096b4961deeb2d17c656cdcace54b46eac7001393814"
|
||||
},
|
||||
"thread_claims": {
|
||||
"rows": 4,
|
||||
"maxSeq": 4,
|
||||
"sha256": "088b8236570d8afe03364b85803fdc5034cce2cef6ed7b16f8b99bf6366524ad"
|
||||
},
|
||||
"thread_opens": {
|
||||
"rows": 0,
|
||||
"maxSeq": null,
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
"threads": {
|
||||
"rows": 2,
|
||||
"maxSeq": 2,
|
||||
"sha256": "819fd24c64e279d0debad5ab5dbbe533d8e6f26de58277c69510b9cbbaadbc3a"
|
||||
},
|
||||
"usage_records": {
|
||||
"rows": 34,
|
||||
"maxSeq": 34,
|
||||
"sha256": "de614f3d62c9b3819757c5659bee40085cb86cc873b7bfe381b13e254ddb7cad",
|
||||
"byRun": [
|
||||
{
|
||||
"business": "mosaic-stack",
|
||||
"run": "r8fba7e598156",
|
||||
"input": "0",
|
||||
"output": "0",
|
||||
"unknownTurns": 1
|
||||
},
|
||||
{
|
||||
"business": "mosaic-stack",
|
||||
"run": "r4a96e2142e4d",
|
||||
"input": "2",
|
||||
"output": "52",
|
||||
"unknownTurns": 0
|
||||
},
|
||||
{
|
||||
"business": "mosaic-stack",
|
||||
"run": "rc78c9700c8e9",
|
||||
"input": "14",
|
||||
"output": "1519",
|
||||
"unknownTurns": 0
|
||||
},
|
||||
{
|
||||
"business": "mosaic-stack",
|
||||
"run": "r293f33bcbc63",
|
||||
"input": "2",
|
||||
"output": "44",
|
||||
"unknownTurns": 0
|
||||
},
|
||||
{
|
||||
"business": "mosaic-stack",
|
||||
"run": "r34520a581318",
|
||||
"input": "6",
|
||||
"output": "323",
|
||||
"unknownTurns": 0
|
||||
},
|
||||
{
|
||||
"business": "mosaic-stack",
|
||||
"run": "r7e99838f9f93",
|
||||
"input": "2",
|
||||
"output": "44",
|
||||
"unknownTurns": 0
|
||||
},
|
||||
{
|
||||
"business": "mosaic-stack",
|
||||
"run": "rc964611b2092",
|
||||
"input": "4",
|
||||
"output": "120",
|
||||
"unknownTurns": 0
|
||||
},
|
||||
{
|
||||
"business": "mosaic-stack",
|
||||
"run": "rbb708846b605",
|
||||
"input": "12",
|
||||
"output": "2089",
|
||||
"unknownTurns": 0
|
||||
},
|
||||
{
|
||||
"business": "mosaic-stack",
|
||||
"run": "rc36b17ef3788",
|
||||
"input": "16",
|
||||
"output": "5221",
|
||||
"unknownTurns": 0
|
||||
},
|
||||
{
|
||||
"business": "mosaic-stack",
|
||||
"run": "r097436c91816",
|
||||
"input": "2",
|
||||
"output": "131",
|
||||
"unknownTurns": 0
|
||||
},
|
||||
{
|
||||
"business": "mosaic-stack",
|
||||
"run": "r497f1c817d4d",
|
||||
"input": "2",
|
||||
"output": "80",
|
||||
"unknownTurns": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"untimed": [
|
||||
"message_threads",
|
||||
"meta",
|
||||
"task_comment_cursors"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
# Re-pin window 490e2771d to 4429a104f (row 192, Q175 phase 1), STATE report (W11)
|
||||
|
||||
Operator and tester: Sage. Plan: `agents/sage/work/window-plan/repin-4429a104f.md` at f41729178, sha256
|
||||
e1272d1a391e00fb1291d47da9973c0ba342d65fcd42f583ea007e883c1f32b9 (checked before W0b, W9). The plan file in
|
||||
this commit carries three corrections made after the window. They are listed at the end, so its sha256 differs
|
||||
now. Tool blob 3b068b1546d4 (`scripts/stack-release.sh`). OLD 490e2771d3d057ab76991e2c7859cd251857e64c, NEW
|
||||
4429a104f6d65ccf165f6c83b5d1dce3147dfde0. Every stamp below comes from `date -u` or from the transcript `W.txt`
|
||||
in this directory. Mos's gos: mos-1113 (W5), mos-1114 (W9 to W11), mos-1128 (W9b accepted), mos-1135 (W10
|
||||
browser part), mos-1137 (line g, post W11).
|
||||
|
||||
## Outcome
|
||||
|
||||
NEW is live for mosaic-stack with its PM, and the store stays on 3i. Row 192's sign-in code is in the copy but
|
||||
dormant, because the Console runs without `--sso`. W0 to W10 passed, and nothing was rolled back. W10 stopped
|
||||
once on two failed browser checks. I diagnosed both read-only as checks written before row 192, not faults in
|
||||
NEW, and Mos passed them (mos-1135). Matrix line (g) isn't byte-identical, for a known cause, and Mos passed it
|
||||
(mos-1137). Nobody acted as Jason. There was no `--by jason`, no pi, no Save and no `mosaic name`, and
|
||||
`bus.tester` was not touched.
|
||||
|
||||
## Stamps and activation
|
||||
|
||||
| Step | Start (UTC) | Result |
|
||||
| --- | --- | --- |
|
||||
| P1 values, business-check OLD | file written 21:43:13Z | values as planned, LOAD_OK (`p1.txt`) |
|
||||
| W0 copy NEW with npm ci, business-check NEW | 21:45:57Z | copied, lockfile diff empty, LOAD_OK (`w0.txt`) |
|
||||
| P2 console-check on a stub of NEW | 21:46:09Z | CONSOLE_PASS without and with `--tester-port` (`p2.txt`) |
|
||||
| P3 rehearsal (Mos) | 21:52:31Z to 21:52:34Z | REHEARSAL_PASS, rc 0 (`p3/`) |
|
||||
| P3 flags check (Mos) | 21:53:52Z to 21:53:56Z | NEW Console with the pointer flags on 7341/7342: healthz 200 both, then gone, 0 listeners |
|
||||
| P4, P5, P6 | 22:00:17Z | P4 PASS, P5 PASS (HEAD 4385b9ec1), `matrix-old.txt` |
|
||||
| W0b business file copy | 22:00:49Z | sha256 1e49bdf0 |
|
||||
| W1 idle check, S1 | 22:00:49.758Z | chief idle; S1 a7649760 |
|
||||
| W2 status | 22:00:49.819Z | pointer OLD, pm true, 3i, unit flags match |
|
||||
| W3 plan | 22:00:50.068Z | exit 0 (`W3-plan.txt` 45de68d4) |
|
||||
| W4 unit read | 22:00:50.238Z | OLD units, no drop-ins, bus `--pm`, no quote or backslash |
|
||||
| W5 activate (mos-1113) | 22:02:06.776Z, end 22:02:09.556Z | exit 0, gate passed, activate ok |
|
||||
| W5 status | 22:02:13.885Z | pointer NEW, pm true, 3i, unit flags match |
|
||||
| W9 checks and turns (mos-1114) | 22:05:51Z to 22:06:06Z | pass |
|
||||
| W9b run end and S2 | 22:21:05.171Z, S2 22:21:07Z | ended by the launcher (thread-idle); S2 1a444f5a |
|
||||
| W10 browser (stop), diagnosis | 22:29:14Z, 22:30:02Z | 10 of 12, then both failures traced to the checks |
|
||||
| W10 matrix, P4 (mos-1135) | 22:31:29Z | a-f identical, g +121 bytes; P4 PASS |
|
||||
| W11 reads | 22:44:33Z to 22:44:34Z | units, tunnel, business file, failed units |
|
||||
|
||||
Activation id 20261020T220206Z-4429a104f6d6. Pointer `at` 2026-10-20T22:02:09Z.
|
||||
|
||||
## Backups and hashes
|
||||
|
||||
- Store backup: `~/.mosaic-dev/backups/stack/20261020T220206Z-4429a104f6d6`, SHA256SUMS sha256
|
||||
b61d64843c9e2a153842606353450838da9cb778c8f3b9c27cc9366dd72ec0c9.
|
||||
- Rollback set: `~/.mosaic-dev/state/stack-rollback/20261020T220206Z-4429a104f6d6`. Rollback goes to 490e2771d.
|
||||
- Business file: 1e49bdf05e62607972958f5b7298cfd600c7825812370e26d7c2a842f2af9b93 at P1, at W0b and again at
|
||||
W11. The W0b copy is `~/.config/mosaic-dev/businesses/mosaic-stack.json.pre-window-20261020T220049Z` with the
|
||||
same hash. Nothing in the window wrote it. `backups.txt` holds the W0b and W5 lines.
|
||||
|
||||
## Pointer before and after
|
||||
|
||||
Before (W2): copy 490e2771d3d0, pm true, schema 3i, activation 20261020T192956Z-490e2771d3d0. Console flags
|
||||
`--port 7332 --public-host mosaic.woltje.com --auth-header X-authentik-username --allow-user jason.woltje
|
||||
--tester-port 7333`.
|
||||
|
||||
After (W5, W9 and W11): copy 4429a104f6d6, pm true, schema 3i, activation 20261020T220206Z-4429a104f6d6. The
|
||||
Console flags are the same, with no `--send`, no `--sso` and no proof pair. Bus pid 2950923 and Console pid
|
||||
2950925, both since 22:02:08Z. Status shows drift of 6 commits on refactor since the pointer's copy (records).
|
||||
|
||||
## Unit ExecStart lines (no Environment values)
|
||||
|
||||
W11, after. Both units active, DropInPaths empty, WorkingDirectory the NEW copy:
|
||||
|
||||
```
|
||||
bus: ~/.local/share/mosaic-stack/copies/4429a104f6d65ccf165f6c83b5d1dce3147dfde0/scripts/mosaic bus start mosaic-stack --pm (MainPID 2950923)
|
||||
webui: /usr/bin/node ~/.local/share/mosaic-stack/copies/4429a104f6d65ccf165f6c83b5d1dce3147dfde0/packages/webui/src/cli.mjs serve --checkout /mnt/storage/src/mosaic-stack --port 7332 --public-host mosaic.woltje.com --auth-header X-authentik-username --allow-user jason.woltje --tester-port 7333 (MainPID 2950925)
|
||||
```
|
||||
|
||||
W4 read the same two lines on OLD's copy (W.txt 329-344). The bus Environment has 2 assignments with no quote or
|
||||
backslash. The values are not recorded. The Console's cwd is the NEW copy (W9). The webui journal since W5 has one
|
||||
"Proxy: proxy proof off" line and no "Sign-in:" line.
|
||||
|
||||
Tunnel: `mosaic-preview-tunnel.service` active, running, MainPID 1317, since 2026-10-13 08:40:19 CDT. The window
|
||||
didn't touch it. P4 before and after shows one R: spec, `R:0.0.0.0:7332:127.0.0.1:7332`.
|
||||
|
||||
## S1, S2 and the store
|
||||
|
||||
S1 (`S1.json`, sha256 a7649760c3439705c8b5f3af26d0c21aea1081c1eb295fd02aef3a642064194b) is W1's held read at
|
||||
22:00:49Z. S2 (`agents/sage/work/window-repin-4429a104f/S2.json`, sha256
|
||||
1a444f5ac0049bc78e397b6928e1008cfe8cbc30989bbf2042b4ed27f56a4173) is the held read after the assistant run
|
||||
ended, at 22:21:07Z. Both are 3i. SNAP (`snapshot.mjs`, 639fd28a) was checked before each.
|
||||
|
||||
| Table | S1 rows | S2 rows | S2 vs S1 |
|
||||
| --- | --- | --- | --- |
|
||||
| decisions, decision_events | 21, 21 | 21, 21 | same sha256 |
|
||||
| meta, instance_names, thread_opens | 3, 0, 0 | 3, 0, 0 | same sha256 |
|
||||
| deliveries | 1726 | 1740 | +14 |
|
||||
| events | 1525 | 1543 | +18 |
|
||||
| role_claims | 98 | 101 | +3 |
|
||||
| messages | 567 | 571 | +4 (counts only) |
|
||||
| message_threads | 2 | 4 | +2 (counts only) |
|
||||
| usage_records | 30 | 34 | +4 |
|
||||
| projects, threads, thread_claims | 1, 1, 2 | 2, 2, 4 | +1, +1, +2 |
|
||||
| task_snapshots | 401 | 408 | +7 |
|
||||
| task_comment_cursors | 215 | 220 | +5 |
|
||||
|
||||
For every grown table with a seq, maxSeq grew by exactly the row delta.
|
||||
|
||||
The task_snapshots and task_comment_cursors rows (mos-1128 item 2). The writer is the bus's own Vikunja poll, not
|
||||
an agent. `packages/bus/src/process.mjs:104` loads the tasks adapter. Its timer runs `tick` every `pollSeconds`
|
||||
(`packages/tasks/src/adapter.mjs:192-197`), and `sync.mjs:169` calls `broker.recordTask` with no cap
|
||||
(`packages/bus/src/broker.mjs:1123`). That writes `source 'poll'` with role and run null (`broker.mjs:1159-1174`)
|
||||
and the comment cursors (`:1131-1138`). The adapter logs only on an error (`adapter.mjs:115`), so the bus journal
|
||||
has no line for a good tick. That's why there's no sync line between 22:05Z and 22:22Z. The rows, read
|
||||
read-only after the window:
|
||||
|
||||
- task_snapshots 402-404 at 22:05:08Z: P19 436, 435 and 434, all updated 22:04:55-56Z. 405-406 at 22:12:38Z: 438
|
||||
and 437. 407 at 22:17:08Z: 438 again. 408 at 22:19:08Z: 431. All have `source poll`, `via cursor`, role null
|
||||
and run null. These are tracker edits made by seats during the window, which the poll picked up.
|
||||
- task_comment_cursors 216-220: tasks 424, 432, 438, 431 and 436, last comments 397 to 401. These are the
|
||||
comments seats posted on P19 in the same minutes (398 is Mos's comment on #432).
|
||||
|
||||
Rows after S2 (task_snapshots 409 at 22:33Z, cursors 221-223) are the same poll and are outside the window.
|
||||
|
||||
## W9 output
|
||||
|
||||
- Status, console-check (CONSOLE_PASS), the journal counts, the Console cwd and one running chief from this
|
||||
start (r097436c91816) all passed.
|
||||
- `talk chief --tester` (c5b43f04): the chief replied that a tester message authorizes nothing and it took no
|
||||
action.
|
||||
- Project `window-20261020-2205`, thread 446201de-e8b5-4df3-9356-d0d5422cc899. `talk --thread` (b4b66ced) got one
|
||||
assistant reply from run r497f1c817d4d. `thread read` shows the tester REQUEST and the assistant RESULT.
|
||||
- `threads`, `names` (empty) and `usage` read fine. The human-thread probe was skipped: S1's one thread is the
|
||||
177-185 window's tester thread d0ba7a66, so no human thread exists.
|
||||
- W9b: the launcher logged launch at 22:06:00.766Z, thread-idle at 22:21:05.149Z and end at 22:21:05.171Z
|
||||
(reason stopped, exit 0). Nobody stopped it by hand.
|
||||
|
||||
## W10
|
||||
|
||||
The browser check (`w10-browser.mjs`, dda57ccf) ran against 127.0.0.1:7333 with fetch and XHR limited to GET and
|
||||
HEAD. It made no writes and sent no W9 thread request. 10 of 12 checks passed. The two that failed:
|
||||
|
||||
1. "Only tester forms": `#sign-out` was in the list. It is row 192's form at `packages/webui/src/public/index.html:12`,
|
||||
`hidden`. `app.js:479` shows it only when `/api/config` has `sso: true`. The live config has no `sso`
|
||||
key, and the form stayed hidden.
|
||||
2. "Chat lists the W9 thread": the check read only the first `ul.chat-thread-list`. `chat.js:93` renders one list
|
||||
per project, and W9 made the second project. The diagnosis (`w10-diag.mjs`, 42d0844b) read every panel and
|
||||
found 446201de listed.
|
||||
|
||||
Both checks come from the 177-185 window (Darkwing's `window-rows-177-185/w10-browser.mjs`), written before row
|
||||
192 and before a second thread project existed. Corrected, they now read:
|
||||
|
||||
- Only tester forms, allowing `#sign-out` when it is `hidden` and `/api/config` has no `sso: true`.
|
||||
- The W9 thread is in some `.chat-projects section.panel` thread list, not only the first.
|
||||
|
||||
Both corrections are in the plan's W10 line, for the next window that copies this one.
|
||||
|
||||
The matrix (`matrix-new.txt` against `matrix-old.txt`). Lines a to f are byte-identical. Line g is 200 non-json
|
||||
both times, but 6454 bytes on OLD and 6575 on NEW. `index.html` is 6470 bytes at 490e2771d and 6591 at 4429a104f,
|
||||
+121, and the only hunk is the line 12 form. The served page grew by the same 121. The plan's W10 line now says a
|
||||
line for a file the pin changes isn't "identical", with this case as its example. P4 again at 22:31:29Z: PASS.
|
||||
|
||||
## Failed user units (mos-1128 item 3)
|
||||
|
||||
All three predate the re-pin at 22:02Z and come from the legacy `~/.mosaic` and jarvis-brain setup, not the
|
||||
stack. P19 #393, "dragon-lin: seven legacy user units in failed state", covers all three (filed by Mos on
|
||||
10-20 at about 01:42Z). It has no comments yet. The current failure times are later than the ones #393 lists,
|
||||
so I added a comment with them. Times are from `systemctl --user show` (CDT) with UTC alongside.
|
||||
|
||||
| Unit | Runs | Failed | #393 |
|
||||
| --- | --- | --- | --- |
|
||||
| mosaic-standup-pm.service | `~/.mosaic/tools/coordinator/standup_trigger.py pm` | 2026-10-19 19:00:28 CDT (10-20 00:00:28Z), exit 1 | listed |
|
||||
| mosaic-install-drift.service | `~/src/jarvis-brain/scripts/alerting/mosaic-install-drift` | 2026-10-20 00:12:54 CDT (05:12:54Z), exit 9 | listed, with an older time |
|
||||
| mosaic-unit-failed@mosaic-reconcile.service | `~/.local/bin/mosaic-unit-failed mosaic-reconcile.service` | 2026-10-20 13:16:12 CDT (18:16:12Z), exit 4 | listed, with an older time |
|
||||
|
||||
The marker's journal at 18:16:12Z says the escalation's tmux send to jarvis failed ("tmux target not found",
|
||||
socket mosaic-fleet), so the page wasn't delivered. The record is on disk in
|
||||
`~/.local/state/mosaic-unit-failures/20261020.jsonl`. `mosaic-reconcile.service` failed at that time (exit 7)
|
||||
but last ran OK at 17:16:27 CDT (22:16:27Z). The marker stays failed until something resets it. The journal no
|
||||
longer has lines for the other two units. The window changed none of these units, and touching them is
|
||||
outside it (`~/.mosaic` is off limits, and #393 needs Jason or ops-01's charter to disable any). W11's list also
|
||||
shows `setspark-tmp-guard.service` failed. It belongs to another project, and I've left it.
|
||||
|
||||
## Corrections to the plan, made after the window
|
||||
|
||||
- NEW is 103 commits past OLD (`git rev-list --count 490e2771d..4429a104f`), not 106. The 106 in `status` was OLD
|
||||
to HEAD 305857c8f, so it also counted the three record commits.
|
||||
- P3: `rehearse` starts NEW's Console with only `--port` and `--business`, not the pointer's flags. Mos's
|
||||
separate flags check covers that (`p3/flags-check.txt`, `p3/flags-console.log`). The line was wrong as
|
||||
written (mos-1107).
|
||||
- W10: the matrix line g size change and the two corrected browser checks, as above.
|
||||
|
||||
## Files
|
||||
|
||||
`W.txt` (transcript), `S1.json`, `S2.json`, `matrix-old.txt`, `matrix-new.txt`, `backups.txt`, `W3-plan.txt`,
|
||||
`W5.txt`, `W5-status.txt`, `w0.txt`, `p1.txt`, `p2.txt`, `p3/` (Mos's rehearsal and flags check), `p456.sh`,
|
||||
`w9.sh`, `w9b.sh`, `w9.out`, `w9b.out`, `w10-browser.mjs`, `w10-diag.mjs` with their outputs (the three
|
||||
screenshots stay on disk in this directory, uncommitted, since the push check refuses binaries),
|
||||
and the `.w9*`, `.w10*` and `.w11*` reads. The store snapshot tool is `snapshot.mjs` (639fd28a).
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,116 @@
|
||||
plan: activate for mosaic-stack
|
||||
from: 490e2771d3d057ab76991e2c7859cd251857e64c
|
||||
to: 4429a104f6d65ccf165f6c83b5d1dce3147dfde0
|
||||
store schema: 3i (read from the store the running bus holds)
|
||||
target schema: 3i
|
||||
migration: none; the store is at the target's version
|
||||
backup: the run backs up the stopped store to /home/jwoltje/.mosaic-dev/backups/stack/<activation>/ before any migration
|
||||
commits: the run adds 103 (newest first, at most 200):
|
||||
4429a104f webui: Q175 phase 1, the Console verifies SSO itself under --sso, OIDC RP with oauth4webapi 3.8.8 (row 192, #1690)
|
||||
32b54ac3b plans: Case 2 row briefs from Gosalyn's 51006710a (webby-0034 N1/N2 and webby-0035: row A README :711 one line per variable, C2-13 owns README :713 and says the precheck prints only variable names)
|
||||
6025ddd9c plans: Case 2 row briefs from Gosalyn's a2d94b2c2 (Webby r9 notes, webby-0032: row A README line on claude-session.env and the dist/providers/data/zai.json path, C2-13 XDG_DATA_HOME precheck and last-assignment-wins key file parse)
|
||||
ecbfd6deb plans: Case 2 row briefs from Gosalyn's 29e85196b (design r9 for mos-1090: row A out of scope points at design 10 step 2d, C2-13 precheck checks the hub key file by name and mode, claude marker check only for claude-code harness, EnvironmentFile= fixed in case2-unit.sh)
|
||||
0498c0613 agents: Gosalyn seat dir, README from gosalyn/case2-design, so Case 2 rows can name the seat as owner (#416)
|
||||
49d1f3a23 plans: Case 2 slice row briefs plus rows A and B, from Gosalyn's f325657c7 on gosalyn/case2-design (#416, design r8, C2-13 copy id and profile asks for row 195, C2-20 count-only note, row A ZAI_API_KEY per mos-1086/mos-1089, row B #428 after 10-27)
|
||||
628e2ca5c sessions: Filbert row 190 manifest round 1 CHANGES (base drift on row 189)
|
||||
088087ac9 queue: revs 2248-2361+. Sage: rows 210 (#424 host access, Gizmoduck) and 211 (#426 force-push guard, Rocko) added and briefed; row 195 re-briefed to the Case 1 install on .53; review requests for 200 r1/r2 and 202; plus earlier Sage revs. Other seats' revs: Launchpad 2254, 2283-2284, 2286, 2311-2313, 2328-2329, 2344, 2347-2348 (rows 192/200); Honker 2255; Morgana 2257, 2320, 2326-2327 (row 204); Gander 2262; Herb 2265; Dewey 2268; Bushroot 2273; Megavolt 2276; Webby 2303, 2316, 2339, 2355 (row 192/200 verdicts); Gizmoduck 2304, 2318-2319 and the row 210 claim (rows 201/210); Quackerjack 2317, 2341-2342 (row 202); Duckworth 2323, 2335, 2353 (rows 201/204/202 verdicts); Darkwing 2354, 2357 (row 190 in-review)
|
||||
a0fbefd48 plans: pre-push force-push hook and failing busy check brief (Vikunja #426, mos-1078, mos-1083; owner Rocko, reviewer Dewey)
|
||||
f24f2cd40 plans: row 195 brief becomes the Case 1 install on 10.1.1.53 (Q181 hosts, Q182/Q183 recorded acceptance, Q185 existing account, chained on rows 190 and C2-13; ops-01 root steps under #423 step 1; sage-1439)
|
||||
9ba31b5b3 agents: Honker's PRD 1.7 draft r2 on refactor for the prd.approve raise (11cb0423b, sha256 e896746df595d1b5155130ca163fbf63b0dc38a87a8dd154af77f1a6d1328647, Duckworth approved duckworth-0039, Vikunja #420)
|
||||
9db578ff5 plans: agent host access with sudo through the credential store brief (#1707, Vikunja #424, Q183/Q184, chained on row 199)
|
||||
8eb9ddd12 plans: FED R8 brief to r4.11 (b93f098f0, recorded acceptance, Duckworth N1/N2, #416 date), R5 context source (sage-1419), 193a makes covers level required (duckworth-0033 N1)
|
||||
ae3c36f24 agents: Gizmoduck seat dir and roster row (FED R1 owner, #414)
|
||||
a2af7ab49 plans: FED R1, R5, R2 and R8 briefs (#414, r4.9 a1daa3c07) and row 193a-d build briefs (#1691); Quackerjack seat dir and roster row
|
||||
aa1bd209e gate-lane: row 189 gate timing table for row 197 (Herb)
|
||||
ace27dc5e row 189 (#1687): stack-release.sh activate --no-send drops the Console's --send (Sage; Dewey approve 29485; Sage gate passed)
|
||||
a4a42dbac plans: stack-release --sso brief (#1698, Launchpad, reviewer Webby, row 192 F3; chained on 192)
|
||||
a31f1a0f8 plans: row 198 brief records Mos's call, in-process board reads behind the Console's auth (mos-1019)
|
||||
992650232 plans: agent credentials and git identity brief (MVP T1 item 6, #1697, Megavolt, reviewer Duckworth); Megavolt seat dir
|
||||
a7661ada0 plans: row 195 acceptance script takes Gizmoduck's gate item list; B8 issuer and TLS front end (mos-1011)
|
||||
f871cb309 plans: retire the control board writer on 7331 brief (#1696, Bushroot, reviewer Webby, mos-1008 item 2); Bushroot seat dir
|
||||
0bbd23c61 plans: row 195 brief adds the identity provider check and flags any new model account for Jason (mos-1008)
|
||||
0217b71c8 plans: row 195 brief rewritten as the hub on its own host, host left to Jason (#1693, mos-0998, mos-1005)
|
||||
c6f528a32 plans: second gate lane brief (#1695, Herb, reviewer Webby, mos-0977 item 6); Herb seat dir; gate template and busy check copies for the row
|
||||
96f243faf plans: human boundary row brief (P19 333, #1694, Gander, reviewer Duckworth, mos-0989); Gander seat dir; seat READMEs name the credentials row, not row 195 (now 190b)
|
||||
c86269935 sessions: Filbert row 190 design review r3, approved
|
||||
48ebee0ff plans: row 190 brief scopes isolation to addressing on one uid, points to row 190b (Filbert N7, mos-0986)
|
||||
66bdd84b8 sessions: Filbert row 190 design review r2, changes requested
|
||||
6966164f0 plans: row 190b brief, Melanie's hub under its own Linux user (#1693, Darkwing, reviewer Duckworth), mos-0986
|
||||
53bb59462 sage: common start for MVP T1 builder seats (mos-0974, mos-0977, mos-0981)
|
||||
5e01c3683 agents: seat dirs and roster rows for Launchpad, Morgana, Honker (Sage's T1 builders) and Webby, Duckworth (Mos's reviewers); row 193 brief takes Mos's B1 starting rule (mos-0985)
|
||||
9f56d2098 plans: rows 192 and 194 review by Webby, not Filbert or Dewey (mos-0981)
|
||||
0599b119f plans: MVP T1 briefs, SSO phase 1 (#1690, Launchpad), human users (#1691, Morgana), agent lifecycle (#1692, Honker), mos-0974/0977
|
||||
b416d2ede sage: Q175 SSO design, Mos C1 condition (mos-0977): straggler check before Jason step 4 in the phase 2 window
|
||||
5fc73a729 revs 2237-2247: row 189 added, briefed, started, in-review round 1 (Dewey, comment 29457); rows 190 (Darkwing) and 191 (Rocko) added and briefed for MVP T1 (mos-0974); darkwing-190-start (rev 2245, Darkwing's) and rocko-191-start (rev 2244, made for Rocko)
|
||||
99772421a sage: row 189 candidate manifest for Dewey's review (d6116771f)
|
||||
da230ca48 plans: MVP T1 briefs, several instances per host (#1688, Darkwing) and the kill switch (#1689, Rocko), mos-0974
|
||||
4f2d1be68 sessions: Filbert Q175 SSO design D1/D2 ack
|
||||
d55d86bb0 sage: Q175 SSO design r3 fix, Filbert r3 D1 (loader checks slot shape only) and D2 (slot from config dir, scratch Console dataRoot and unreachable issuer)
|
||||
c56ccf916 sessions: Filbert Q175 SSO design review r3, approve with D1
|
||||
817e8f51b sage: Q175 SSO design r3 for #412, Filbert r2 C1 (shared loader, config.json inside the phase 2 window, rollback order, test 25) and C2 (401 signed-out page, test 24), notes and Q-E
|
||||
5d019d876 sessions: Filbert Q175 SSO design review r2, changes
|
||||
59475fc5a sage: row 163 landing record, P19 comments 372/373 on 337/412, and P19 #413 filed for the --no-send row
|
||||
1a268249e sage: Q175 SSO design r2 for #412, Filbert r1 B1-B6, rulings Q-B/Q-C/Q-D, config in config.json (mos-0973)
|
||||
def2e7932 plans: brief for stack-release activate --no-send (Q175 interim, mos-0972/0973, #1687, P19 413)
|
||||
c30b36fff sessions: Filbert Q175 SSO design review r1, changes
|
||||
9ae8c73ac queue: revs 2235-2236: row 163 Sage landing note (8ecc7a239, patch-id b8d5f8a37c24, gate passed on eaa0f2666) and done
|
||||
8ecc7a239 row 163 (#1660): stack-release.sh rehearse, the D2 rehearsal on a scratch root from a backup, and bus start --no-launch (Sage; Filbert approve 29447; Sage gate passed)
|
||||
7f57419b3 sage: Q175 SSO design for #412, OIDC relying party on oauth4webapi 3.8.8, for Filbert review (mos-0970, mos-0971)
|
||||
4ca8d6e94 sage: row 42 run log, S7 reachability pass (51cebff7 answered by cff1e9ad after a chief launch, mos-0969)
|
||||
22e5e825b sage: row 42 run log, plain mosaic on a pinned host supersedes the 10-12 full-path line (mos-0968)
|
||||
e6dea74b2 sessions: Sage row 163 r2 approval intake; P19 337 comment 368 script and record
|
||||
cd2b377bb queue: rev 2234: row 163 r2 approve from Filbert (op filbert-163-r2, comment 29447, candidate c6f4f4c12596)
|
||||
ceccb0ef2 sessions: Filbert row 163 r2 review, approve
|
||||
10ab8ea62 sessions: Sage row 163 r2 in-review; P19 337 comment 367 script and record
|
||||
c539995cf queue: revs 2231-2233: row 163 r2 in-review from Sage (ops sage-163-r2-start, sage-163-inreview-2, .outcome; request comment 29446, reviewer Filbert, manifest c6f4f4c12596)
|
||||
92cc6011c queue: rev 2230: row 163 r1 changes from Filbert (op filbert-163-r1, comment 29445, candidate 430212299beb)
|
||||
7af166574 sage: row 163 r2 packet (candidate 2c090eefd on code 2a5f3a3a0, base 4f02486d5: diff, manifest c6f4f4c12596, gate passed, mutants K34-K41 killed)
|
||||
2244e8baf sessions: Filbert row 163 r1 review, changes
|
||||
bde0fdec9 sessions: Sage row 163 r1 in-review; P19 337 comment 366 script and record
|
||||
def677bc7 queue: revs 2228-2229: row 163 r1 in-review from Sage (ops sage-163-inreview-1, .outcome; request comment 29444, reviewer Filbert, manifest 430212299beb)
|
||||
51f67f1f8 sage: row 163 packet (candidate 4be3f6aec on 580500699: diff, manifest 430212299beb, gate passed, mutants K1-K33 killed after four survivors got tests)
|
||||
5b298bd00 sessions: Dewey row 66 r1 (changes) and r2 (approve) reviews
|
||||
565bc32c2 queue: revs 2223-2227: row 66 r2 round from Sage (2223-2225, ops sage-r66-r2-start, sage-r66-inreview-2); r2 verdicts from Filbert (2226, op filbert-66-r2, approve) and Dewey (2227, op dewey-66-r2, approve)
|
||||
5140aa9e8 sessions: Filbert row 66 r2 review, approve
|
||||
973cb5a87 queue: revs 2220-2222: row 163 Sage start (2220); row 66 r1 verdicts from Filbert (2221, op filbert-66-r1) and Dewey (2222, op dewey-66-r1)
|
||||
b82ec9353 sage: row 66 r2 packet (candidate 0f4a57956: diff, manifest cee63e06fdea, gate, mutants K1-K4 killed, singles rerun)
|
||||
3560c450a sessions: Filbert row 66 r1 review, changes
|
||||
4f02486d5 sage: row 66 r1 packet (candidate 8ec03b45a: diff, manifest e68c6bce5ec4, gate r2), row 42 run log (trigger (a), mos-0960), SESSIONS line
|
||||
263ea8a9a queue: revs 2215-2219: row 66 Sage start (2215), in-review round 1 on candidate manifest e68c6bce5ec4 (8ec03b45a) and request comment 29437 (2216-2217); row 163 brief re-pinned to b1500d96ad27 (6020bb43a) and briefed (2218-2219)
|
||||
6020bb43a plans: D2 brief owner is sage (row 163, #1660), and D2 lands after the Part B window (mos-0958, mos-0959)
|
||||
5487ed39d sage: row 188 landing record: P19 411 (N1) and comments 361-363 script and record, 409/410 description restore after a partial done update, SESSIONS line
|
||||
31e328958 queue: revs 2209-2214: row 188 Darkwing start, in-review and request 29432 (2209-2211); Filbert approve 29433 (2212); Sage landing note (8fc0ee216, patch-id c9a56b167c07, stack-release.sh blob 2586a2a37cd2, gate passed) and done (2213-2214)
|
||||
8fc0ee216 row 188 (#1686, P19 409, 410): stack-release T54, the proof file checks on a carried pair and a proof file inside the repository (Darkwing; Filbert approve 29433; Sage gate passed)
|
||||
23a609c5c sessions: Filbert row 188 r1 review, approve
|
||||
b9c1c9fe2 sage: Part B draft 2 A2 passed (mos-0958), row 188 launched, Vikunja comments 358-360 script and record, SESSIONS line
|
||||
525990ecb queue: revs 2207-2208: row 188 add and briefed (Sage), #1686, P19 409/410, gates Part B (mos-0956)
|
||||
148a9503d sage: Part B window file draft 2 for mos-0957 (F1: B6 and B-R1 name the stop's endLaunch rows, PB5 counts per step; F2: PB6, PB7 and B-R1 matrices run (a)-(h)); Q2 answered, Q1 to Jason's list
|
||||
240f21650 brief: stack-release tests for a carried proof pair and a proof file inside the repository (P19 409, 410, #1686, mos-0956)
|
||||
742e98976 sage: Part B window file, the read window's end stamp from the clock (10:29Z, not 10:40Z)
|
||||
288ab1b5a sage: Part B window file for Mos's A2 check (live-copy B3 and B-R1, Q1 on J2 order), SESSIONS lines for Darkwing (row 187, 10:25:22Z) and Sage
|
||||
a843ae284 sage: row 187 landing, P19 407-410 filed and P19 385 comment 357, #1685 closed (comment 29425), SESSIONS line
|
||||
652d6f3a8 queue: revs 2201-2206: row 187 Darkwing start, in-review and request comment 29423 (2201-2203); Filbert approve comment 29424 (2204); landing note (9a71e4e80, patch-id 6b57c47bc661, gate passed) and done (Sage 2205-2206)
|
||||
9a71e4e80 row 187 (#1685): activate sets, carries and drops the Console's proof pair
|
||||
d17328a55 sessions: Filbert row 187 r1 review, approve
|
||||
f81ebfbae sage: row 186 follow-ups filed as P19 405 and 406, P19 404 closed (mos-0955)
|
||||
9760d4967 sage: row 186 landing, P19 404 comment 354 and SESSIONS line, with Darkwing's row 186 SESSIONS line (09:13:21Z)
|
||||
16a936c79 queue: revs 2198-2200: row 186 Filbert approve comment 29419 (2198); landing note (269134d8d, patch-id c97b9f6e25cd, gate passed) and done (Sage 2199-2200)
|
||||
269134d8d row 186 (#1684): the activate plan's backup line says what the run will do
|
||||
3c5da3a91 sessions: Filbert row 186 r1 review, approve
|
||||
b367f5730 sage: rows 186-187 Vikunja comments (P19 404 351, P19 385 352-353) and SESSIONS line (mos-0954)
|
||||
5a768d82c queue: row 187 added and briefed (Part B A0, #1685), with Darkwing's row 186 start (rev 2193)
|
||||
6de3c86be plans: brief for row 187, activate carries the row 178 proof pair (mos-0928 Part B A0, #1685)
|
||||
1d7fd75e2 queue: row 186 added and briefed (P19 404, #1684)
|
||||
f5c72a5c3 plans: brief for row 186, the activate plan's backup line (P19 404, #1684)
|
||||
b724b7b0b sessions: Sage Part A window line, with Darkwing's 06:53:53Z and 08:28:01Z lines
|
||||
e24a13ad8 sage: Vikunja scripts and read-back records 2026-10-13 to 10-20 (decision 110, mos-0882)
|
||||
21aa1cab0 darkwing: re-pin window rows 177-185 packet, STATE.md (sha256 b586f11c9b6f1329), W.txt (edd4622c5c45ef3f), S1/S2/S2b/B snapshots, matrices, W10 browser check and P3 rehearsals (#1683; P19 386)
|
||||
9ddd8a92c plans: repin-rows-177-185 draft 4, R1 snapshot after the plain rollback (mos-0943)
|
||||
e7637b99c plans: repin-rows-177-185 draft 3, R1post expects one tester action.refused per refused read (mos-0935, mos-0942)
|
||||
2ea05700b plans: mos-0928 plan draft 2, NEW is the row 185 landing 490e2771d
|
||||
010666074 queue: revs 2185-2190: row 185 start and in-review on bf87dbba with request comment 29403 (Darkwing 2185-2187); Filbert approve comment 29404 (2188); landing note (490e2771d, patch-id 774cd23186d5, gate passed) and done (Sage 2189-2190)
|
||||
bus: scripts/bus-service.sh install --copy 4429a104f6d65ccf165f6c83b5d1dce3147dfde0 --copies /home/jwoltje/.local/share/mosaic-stack/copies --pm --claude /home/jwoltje/.local/bin/claude
|
||||
proof: off
|
||||
send: off
|
||||
Console: scripts/webui-service.sh install --copy 4429a104f6d65ccf165f6c83b5d1dce3147dfde0 --copies /home/jwoltje/.local/share/mosaic-stack/copies --port 7332 --public-host mosaic.woltje.com --auth-header X-authentik-username --allow-user jason.woltje --tester-port 7333
|
||||
CLI: scripts/stack-copy.sh wrapper 4429a104f6d65ccf165f6c83b5d1dce3147dfde0 --copies /home/jwoltje/.local/share/mosaic-stack/copies --bin /home/jwoltje/.npm-global/bin
|
||||
@@ -0,0 +1,21 @@
|
||||
pointer: /home/jwoltje/.mosaic-dev/state/stack-active.json
|
||||
{"copy":"4429a104f6d65ccf165f6c83b5d1dce3147dfde0","business":"mosaic-stack","pm":true,"claude":"/home/jwoltje/.local/bin/claude","console":["--port","7332","--public-host","mosaic.woltje.com","--auth-header","X-authentik-username","--allow-user","jason.woltje","--tester-port","7333"],"schema":"3i","activation":"20261020T220206Z-4429a104f6d6","at":"2026-10-20T22:02:09Z"}
|
||||
log (last five lines):
|
||||
{"at":"2026-10-20T07:58:02Z","op":"activate","from":"89af38a83afa22a8a4f0d368a0a4030ad15a0efa","to":"490e2771d3d057ab76991e2c7859cd251857e64c","backup":"20261020T075800Z-490e2771d3d0","activation":"20261020T075800Z-490e2771d3d0","result":"ok"}
|
||||
{"at":"2026-10-20T19:29:56Z","op":"activate","from":"490e2771d3d057ab76991e2c7859cd251857e64c","to":"490e2771d3d057ab76991e2c7859cd251857e64c","backup":null,"activation":"20261020T192956Z-490e2771d3d0","result":"started"}
|
||||
{"at":"2026-10-20T19:30:00Z","op":"activate","from":"490e2771d3d057ab76991e2c7859cd251857e64c","to":"490e2771d3d057ab76991e2c7859cd251857e64c","backup":"20261020T192956Z-490e2771d3d0","activation":"20261020T192956Z-490e2771d3d0","result":"ok"}
|
||||
{"at":"2026-10-20T22:02:06Z","op":"activate","from":"490e2771d3d057ab76991e2c7859cd251857e64c","to":"4429a104f6d65ccf165f6c83b5d1dce3147dfde0","backup":null,"activation":"20261020T220206Z-4429a104f6d6","result":"started"}
|
||||
{"at":"2026-10-20T22:02:09Z","op":"activate","from":"490e2771d3d057ab76991e2c7859cd251857e64c","to":"4429a104f6d65ccf165f6c83b5d1dce3147dfde0","backup":"20261020T220206Z-4429a104f6d6","activation":"20261020T220206Z-4429a104f6d6","result":"ok"}
|
||||
check:
|
||||
bus: /home/jwoltje/.local/share/mosaic-stack/copies/4429a104f6d65ccf165f6c83b5d1dce3147dfde0
|
||||
Console: /home/jwoltje/.local/share/mosaic-stack/copies/4429a104f6d65ccf165f6c83b5d1dce3147dfde0
|
||||
CLI: /home/jwoltje/.local/share/mosaic-stack/copies/4429a104f6d65ccf165f6c83b5d1dce3147dfde0
|
||||
result: pass
|
||||
bus status:
|
||||
host: mosaic-stack, pid 2950923, running, since 2026-10-20T22:02:08.133Z, notifier off
|
||||
socket: present
|
||||
writer.lock: pid 2950981
|
||||
boot: organization file loaded at 2026-10-20T22:02:08.125Z
|
||||
unit flags: match the pointer
|
||||
drift: 6 commit(s) on refactor since the pointer's copy
|
||||
# exit 0
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user