Compare commits
22
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4ec998cce | ||
|
|
e3fd9f3a08 | ||
|
|
d163cfccb5 | ||
|
|
39b52b71f5 | ||
|
|
5dc0b5534b | ||
|
|
4d487dfdc4 | ||
|
|
48359dbfaf | ||
|
|
aa1bd209e3 | ||
|
|
ace27dc5eb | ||
|
|
a4a42dbac2 | ||
|
|
a31f1a0f8b | ||
|
|
992650232f | ||
|
|
a7661ada06 | ||
|
|
f871cb3096 | ||
|
|
0bbd23c619 | ||
|
|
0217b71c8f | ||
|
|
c6f528a32f | ||
|
|
96f243fafc | ||
|
|
c862699352 | ||
|
|
48ebee0ffe | ||
|
|
66bdd84b8f | ||
|
|
6966164f0c |
@@ -19,6 +19,10 @@ not fleet seats. Keep changes in `/mnt/storage/src/mosaic-stack`; do not modify
|
||||
| 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 |
|
||||
| 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` |
|
||||
|
||||
@@ -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.
|
||||
@@ -3,5 +3,5 @@
|
||||
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 row 195 settles per-agent identity, it pushes only its own `duckworth/*` branches
|
||||
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,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.
|
||||
@@ -3,5 +3,5 @@
|
||||
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 row 195 settles per-agent identity, it pushes only its own `honker/*` branches
|
||||
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.
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
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 row 195 settles per-agent identity, it pushes only its own `launchpad/*` branches
|
||||
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.
|
||||
|
||||
@@ -103,7 +103,7 @@ checked by sha256. Final run on the final tree: `/mnt/storage/scratch/launchpad-
|
||||
| 20 | CSP form-action without the end-session origins | sso 13 CSP | KILLED |
|
||||
| 21 | import oauth4webapi at module load | pinned-copy | KILLED |
|
||||
| 22 | restore the r1 relative returnPath (round 2, B1) | sso 14 | KILLED |
|
||||
| 23 | drop the second origin check (round 2, sage-1403) | sso 14 | SURVIVED, equivalent, guard kept (sage-1407) |
|
||||
| 23 | drop the second origin check (round 2, sage-1403) | sso 14 | SURVIVED, equivalent (see Round 2) |
|
||||
|
||||
Mutant 16 survived its first run: test 1 had no `getJSON` path. The History click on
|
||||
`/api/conversations` was added and it is now killed.
|
||||
@@ -158,9 +158,8 @@ Mutant 16 survived its first run: test 1 had no `getJSON` path. The History clic
|
||||
- **Mutant 22** restores the r1 relative `returnPath`; test 14 kills it.
|
||||
- **Mutant 23** drops the second origin check. It SURVIVES, and I read it as equivalent: `checkConsole`
|
||||
holds `publicOrigin` to a bare https origin, and a parsed https pathname always starts with `/`, so
|
||||
`publicOrigin + pathname` can't parse to another origin. Webby agrees (webby-0022). Sage's ruling
|
||||
(sage-1407): the check stays as an unreachable guard, and 23 is listed here as equivalent. That is a packet
|
||||
note, not a candidate change.
|
||||
`publicOrigin + pathname` can't parse to another origin. The check stays because sage-1403 asks for it.
|
||||
Either the check goes, or it stays as a guard no test can reach. That is Sage's call.
|
||||
- **N1.** The design cite is now blob 9e3eadb15c80 (`git rev-parse 83b32d8d8:agents/sage/work/q175-sso/DESIGN.md`).
|
||||
- **N2.** Listed under Deviations.
|
||||
- **N3.** `SIGN_IN_COOKIE` has its own comment, above the row 177 comment and `TESTER_CHAT_WRITE`.
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
# Row 200: `stack-release.sh activate --sso` (Q175 phase 2 prerequisite)
|
||||
|
||||
Launchpad, 2026-10-20. Author Launchpad, reviewer Webby, gate owner Sage. Issue #1698. Brief
|
||||
`docs/plans/2026-10-20_stack-release-sso.md` (blob 426336b177f6). Rulings sage-1389 (R1 uses the activation onto
|
||||
the current copy, plus a same-copy test and a latest-set mutant), sage-1403 and sage-1407 (N4), mos-1056 item 5
|
||||
(N4 is a window acceptance item), sage-1432 (round 2 for webby-0028 N1 and N2).
|
||||
|
||||
Base 4d487dfdc (`base.txt`): aa1bd209e plus row 192's two commits picked. 48359dbfa is 83b32d8d8 (patch-id
|
||||
8d11f48a9e03) and 4d487dfdc is r2 37582e820 (patch-id 0e32af8aa2a4 on both). The webui tree at the base matches
|
||||
`launchpad/row192` at 37582e820 path for path. Candidate: r1 5dc0b5534 and the round 2 commit on `launchpad/row200`.
|
||||
`candidate-manifest.sha256` lists its 3 paths; the packet files are not in it. Drift check at commit time:
|
||||
`git log aa1bd209e..origin/refactor` (origin/refactor 9ba31b5b3 at round 2) touches none of the 3 paths.
|
||||
|
||||
No test touches `~/.mosaic-dev`, `~/.config/mosaic-dev`, `~/.mosaic`, a live unit or a live slot. T56 runs on
|
||||
the scratch roots and seeded pointers rows 187 and 189 use. Its config and slot sit in the test's temp dir,
|
||||
and the slot carries the `proofFile` read watch, so a run that opened it would fail the test.
|
||||
|
||||
## Files
|
||||
|
||||
| Path | Change |
|
||||
| --- | --- |
|
||||
| `scripts/stack-release.sh` | `--sso` on activate (usage, parse, `sso_for_target`), `sso_checks` and `slot_problem` (item 2), `sso_base` (rollback flags, item 3), `config_checks` (item 3 and N4), the `sso:` plan line and step lines. |
|
||||
| `packages/cli/tests/stack-release.test.mjs` | Fixture copies gain row 192's `--sso` marker, an old loader without `console`, and a C copy whose `ALGS` lack RS256. T56, 10 subtests (2 in round 2). |
|
||||
| `docs/TOOLS.md` | Usage line and two paragraphs after row 189's `--no-send`. |
|
||||
|
||||
## What it does
|
||||
|
||||
1. **Item 1.** `activate ID BUSINESS --sso` drops `--public-host`, `--auth-header`, `--allow-user` and any proof
|
||||
pair, and puts `--sso` after the other flags, before `--send` and `--tester-port`, as `webui-service.sh`
|
||||
renders it (`sso_for_target`, `:775`). It may name the live copy once. After that, both a plain activate and
|
||||
an `--sso` activate refuse `already-active`. Later activates carry `--sso` (`sso: carried`; T56 covers
|
||||
activate, not `pm-on`, which carries the pointer's flags as it does for every flag).
|
||||
`--plan` prints `sso: on; drops ...; carries ...`, plus the proof line's `dropped; --sso drops header ...`.
|
||||
`--sso` with a pair, with `--no-proof` or on another verb is a usage error.
|
||||
2. **Item 2** (`sso_checks`, `:817`). These checks run whenever the run's Console flags hold `--sso`, before
|
||||
anything stops:
|
||||
- `sso-target`: the target's `cli.mjs` has no `--sso`.
|
||||
- `sso-config`: the target copy's own `checkConsole` refuses `console`, or there is no `console.sso`. The
|
||||
config is parsed in node, and only the message's first line is kept. A JSON parse error never quotes
|
||||
the text.
|
||||
- `sso-slot`: one per provider, from `lstat`, `stat` and `realpath` in `readSlot`'s order (`slot_problem`,
|
||||
`:847`). The slot is never opened.
|
||||
3. **Item 3.**
|
||||
- **Rollback** (`sso_base`, `:728`): from an `--sso` pointer to a copy without `--sso`, the Console flags
|
||||
come from the rollback set of the activation that moved the stack onto the pointer's copy from the
|
||||
target (`from_log arrival`). That set's `show-mosaic-webui.service.txt` is checked against its
|
||||
`SHA256SUMS`. `--send` and `--tester-port` follow the pointer.
|
||||
- **`sso-rollback-flags`** refuses when that set is missing, fails its sums, holds no readable argv, or
|
||||
already ran `--sso`.
|
||||
- **`config-console-key`** (`config_checks`, `:866`): any activate or rollback refuses before anything
|
||||
stops when the config has a `console` key that the target's `scripts/mosaic-config.mjs validate` refuses.
|
||||
The message says to remove the key first (restore step 1's backup), then run the verb again.
|
||||
4. **N4, the first half** (sage-1403, sage-1407). With no `console.sso` and the stack on another copy, the
|
||||
`sso-config` refusal names the first step: "activate ID without --sso first, then write console.sso, then
|
||||
run activate ID --sso". A copy before row 192 refuses the key, so the re-pin comes first. The `--sso`
|
||||
activate is then the allowed same-copy re-pin.
|
||||
5. **TOOLS.md.** The flag, its refusals, the rollback rule and the N4 order.
|
||||
|
||||
## Window plan acceptance item (N4, mos-1056 item 5, sage-1407)
|
||||
|
||||
For the row 200 window plan, word for word:
|
||||
|
||||
- **Before the `console` key is written, the window re-pins to row 192 or later.** Check line, run before the
|
||||
key is written: `git -C /mnt/storage/src/mosaic-stack merge-base --is-ancestor <row 192 landing sha>
|
||||
<pointer copy sha from scripts/stack-release.sh status acme> && echo REPIN_OK`. It must print `REPIN_OK`.
|
||||
If the window instead runs `activate <ID> acme --sso --plan` before the key is written, the
|
||||
`refuses sso-config:` line must end with "activate <ID> without --sso first, then write console.sso, then run
|
||||
activate <ID> --sso". It names that step only when the pointer is on another copy.
|
||||
- **A rollback past row 192 removes the key first.** Check line, run before the rollback:
|
||||
`scripts/stack-release.sh rollback acme --plan` must print no `refuses config-console-key:` line. If it
|
||||
prints one, restore `config.json` from the window's step 1 backup and run the plan again. The rollback itself
|
||||
refuses on the same check, so a skipped step stops it before anything stops.
|
||||
- **Back-out line (webby-0028 N4, sage-1432).** Once a copy with `--sso` support is live, no verb turns `--sso`
|
||||
off. Only a rollback to a copy before row 192 gets the preview set back, after the `console` key is removed
|
||||
(the check line above).
|
||||
|
||||
## Tests run (this worktree, final tree)
|
||||
|
||||
- `node --test --test-name-pattern='^T56' tests/stack-release.test.mjs`: 10/10 on the round 2 tree
|
||||
(`/mnt/storage/scratch/launchpad-r200/t56-r2c.log`). Round 1 was 8/8 (`t56-6.log`).
|
||||
- The whole file, the cli suite and every `scripts/test-*.sh` go to Sage's gate. T1 to T55 have not run on
|
||||
this tree.
|
||||
|
||||
## Mutants (`mutants.py`, run as `python3 mutants.py <tree> [N ...]`, each against T56)
|
||||
|
||||
| # | Mutant | Result |
|
||||
| --- | --- | --- |
|
||||
| 1 | no `sso-target` refusal (item 2) | KILLED |
|
||||
| 2 | no `sso-config` refusal (item 2) | KILLED |
|
||||
| 3 | no `sso-slot` refusal (item 2) | KILLED |
|
||||
| 4 | no `sso-rollback-flags` refusal (item 3) | KILLED |
|
||||
| 5 | no `config-console-key` refusal (item 3) | KILLED |
|
||||
| 6 | keep `--auth-header` under `--sso` (item 1) | KILLED |
|
||||
| 7 | keep `--public-host` under `--sso` (item 1) | KILLED |
|
||||
| 8 | rollback takes the latest activation onto the copy, not the arrival (sage-1389) | KILLED |
|
||||
| 9 | `sso-config` names no first step (N4) | KILLED |
|
||||
| 10 | `slot_problem` compares the unresolved `REPO` (round 2, N1) | not run, Sage's gate |
|
||||
| 11 | `proof_file_problem` compares the unresolved `REPO` (round 2, N1) | not run, Sage's gate |
|
||||
| 12 | `config_checks` passes a config that doesn't parse (round 2, N2) | not run; equivalent if it survives, guard kept (sage-1446) |
|
||||
|
||||
9/9 killed (`/mnt/storage/scratch/launchpad-r200/mutants.log`). The script restored `stack-release.sh`, and it
|
||||
matches its pre-run sha256. No mutant hit the 300 s timeout. This batch ran without Sage's slot (sage-1414),
|
||||
while Darkwing's gate was running, so Sage reruns all 9 on the row 200 gate and doesn't take these results
|
||||
(sage-1423). Mutants 10 to 12 are new in round 2 and have not run. Sage runs 1 to 12 on the gate (sage-1446). Mutant 5's
|
||||
pattern now names the `holds` message, because the round 2 message also starts with `config-console-key "$cfg`.
|
||||
Every pattern matches once on the round 2 tree.
|
||||
|
||||
## Choices
|
||||
|
||||
- **R1, `--send` and `--tester-port` follow the pointer.** These two flags come from the pointer, not from the
|
||||
rollback set, so a `--no-send` or a tester port set after the arrival stays. This is row 189's rule that
|
||||
rollback carries `--send` off. Tradeoff: the rolled-back Console differs from the set's unit in those two
|
||||
flags, and the plan line says so.
|
||||
- **`config_checks` runs for activate too**, not only rollback. An activate onto a pre-192 copy with the key
|
||||
in place breaks that copy's loader the same way.
|
||||
- **The inside-repository slot test** symlinks the secrets directory to a temp dir under the fixture repo. The
|
||||
slot file is then not a link, but its real path is inside the repository.
|
||||
|
||||
## Not tested
|
||||
|
||||
- The slot owner-uid refusal: it needs a file owned by another uid, which needs privilege.
|
||||
|
||||
## Review r1 notes (webby-0028, non-blocking)
|
||||
|
||||
- **N1.** `slot_problem` (`:859`) and row 187's `proof_file_problem` (`:988`) compare a resolved path to
|
||||
`REPO="$(pwd)"` (`:26`), which is not resolved. Run through the `~/src/mosaic-stack-dev-test` symlink, a slot
|
||||
inside the repository passes here, and the Console refuses it after the stop. Confirmed by reading; Sage
|
||||
rules on a round 2 or a follow-up.
|
||||
- **N2.** `config_checks` (`:868`) returns 0 when config.json doesn't parse. Same ruling.
|
||||
- **N3.** A check this script doesn't make: `slot_problem` refuses only a size of 0. A slot holding only a
|
||||
newline, or two lines, passes here, and the Console's one-non-empty-line rule (`readSlot`) refuses it after
|
||||
the stop. Reading the content would break the rule that this script never opens the slot.
|
||||
- **N4, for the window's back-out step.** Once the live copy supports `--sso`, no verb turns `--sso` off on it.
|
||||
The only way back to the preview set is a rollback to a copy before row 192, which removes the `console` key
|
||||
first (the second check line above).
|
||||
|
||||
## Round 2 (sage-1432: N1 and N2; N3 and N4 stay notes)
|
||||
|
||||
- **N1.** `slot_problem` (`:847`) and `proof_file_problem` (`:989`) now compare the slot's real path with
|
||||
`realpath -m` of both `REPO` and `COPIES`, and refuse "cannot be resolved" if either fails. T56's
|
||||
"round 2 (webby-0028 N1)" subtest runs the script through a symlink to the fixture repo
|
||||
(`repo-link.200/scripts/stack-release.sh`). A proof file inside the repository is refused `proof-file-invalid`,
|
||||
and a slot whose secrets directory links into the repository is refused `sso-slot` "is inside the repository
|
||||
or the copies". Mutants 10 and 11 undo each check.
|
||||
- **N2, with a deviation.** `config_checks` (`:867`) now tells "no key" (node exits 1) from "doesn't parse"
|
||||
(exits 2) and refuses `config-console-key` "cannot be read as JSON" on the second. But webby-0028's case
|
||||
doesn't reach it: `load_config` (common.sh, through `scripts/mosaic-config.mjs env`) exits 1 on a config that
|
||||
doesn't parse, before anything is read or stopped. So the refusal is a guard for a config that changes after
|
||||
`load_config` ran, and no test reaches it without a race. The T56 subtest "round 2 (webby-0028 N2)" checks
|
||||
what does happen: activate, rollback and `activate --plan` each exit 1 with "common: configuration load
|
||||
failed", the pointer, the start/stop log and the activation log are unchanged, and activate works after the
|
||||
config is restored. Mutant 12 drops the guard and is expected to survive as equivalent for that reason. sage-1446:
|
||||
the guard stays, and mutant 12 is equivalent if it survives, the same call as row 192's mutant 23.
|
||||
- **N4.** The back-out line is in the window plan section above.
|
||||
|
||||
## Not done here
|
||||
|
||||
- No change to the live pointer, units or config. The phase 2 window is Sage's and Mos's.
|
||||
@@ -0,0 +1 @@
|
||||
4d487dfdc468c58e3790976c3d4276fe60816fbe
|
||||
@@ -0,0 +1,3 @@
|
||||
6ee1adb3951aeb1e3862cb119c12b600b88641f325912c09840b841761aa12dd docs/TOOLS.md
|
||||
9b231532d14b21d0d39ca89035d9067e827e18cc1cf3dfe298bef840ae87ff0c packages/cli/tests/stack-release.test.mjs
|
||||
d24e75e9927623ad106b55bce82e40ddf7677d483626fb46923c3b3c7c1148ad scripts/stack-release.sh
|
||||
@@ -0,0 +1,48 @@
|
||||
# Row 200 (#1698) mutants: `python3 mutants.py <tree> [N ...]`. Each applies to the tree's scripts/stack-release.sh,
|
||||
# runs T56 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', '^T56'
|
||||
M = [
|
||||
('no sso-target refusal (item 2)', [(" sso_supported || { check_refuse sso-target", " true || { check_refuse sso-target")]),
|
||||
('no sso-config refusal (item 2)', [(' check_refuse sso-config "$TO', ' : "$TO')]),
|
||||
('no sso-slot refusal (item 2)', [('[ -z "$why" ] || check_refuse sso-slot', '[ -z "$why" ] || : sso-slot')]),
|
||||
('no sso-rollback-flags refusal (item 3)', [(' check_refuse sso-rollback-flags "$TO', ' : "$TO')]),
|
||||
('no config-console-key refusal (item 3)', [(' check_refuse config-console-key "$cfg holds', ' : "$cfg holds')]),
|
||||
('keep --auth-header under --sso (item 1)', [(" --public-host | --auth-header | --allow-user)", " --public-host | --allow-user)")]),
|
||||
('keep --public-host under --sso (item 1)', [(" --public-host | --auth-header | --allow-user)", " --auth-header | --allow-user)")]),
|
||||
('rollback takes the latest activation onto the copy, not the arrival (sage-1389)', [('o.to===current&&o.from!==current){process.stdout.write(`${o.activation', 'o.to===current){process.stdout.write(`${o.activation')]),
|
||||
('sso-config names no first step (N4)', [(' first="; activate $TO without --sso first, then write console.sso, then run activate $TO --sso"', ' first=""')]),
|
||||
('slot_problem compares the unresolved REPO (round 2, webby-0028 N1)', [(' case "$real" in "$repo" | "$repo"/* | "$copies" | "$copies"/*) echo "is inside the repository or the copies"; return ;; esac\n [ "$size" != 0 ]', ' case "$real" in "$REPO" | "$REPO"/* | "$copies" | "$copies"/*) echo "is inside the repository or the copies"; return ;; esac\n [ "$size" != 0 ]')]),
|
||||
('proof_file_problem compares the unresolved REPO (round 2, webby-0028 N1)', [(' case "$real" in "$repo" | "$repo"/* | "$copies" | "$copies"/*) echo "is inside the repository or the copies"; return ;; esac\n [ "$mode" = 600 ]', ' case "$real" in "$REPO" | "$REPO"/* | "$copies" | "$copies"/*) echo "is inside the repository or the copies"; return ;; esac\n [ "$mode" = 600 ]')]),
|
||||
('config_checks passes a config that does not parse (round 2, webby-0028 N2; expected equivalent)', [(' [ "$key" != 1 ] || return 0', ' [ "$key" = 0 ] || return 0')]),
|
||||
]
|
||||
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.
|
||||
@@ -3,5 +3,5 @@
|
||||
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 row 195 settles per-agent identity, it pushes only its own `morgana/*` branches
|
||||
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,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,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,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
|
||||
@@ -3,5 +3,5 @@
|
||||
Reviewer seat, Claude Opus 5.5, started by Mos (mos-0981, thread 03086006). Reviews Case 2 designs and MVP T1 rows 192 and 194.
|
||||
|
||||
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 row 195 settles per-agent identity, it pushes only its own `webby/*` branches
|
||||
credentials of its own; until the credentials row settles per-agent identity, it pushes only its own `webby/*` branches
|
||||
through the repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch.
|
||||
|
||||
@@ -1361,3 +1361,5 @@ are never rewritten or removed; corrections are new entries.
|
||||
2026-10-20T18:34:53Z | Filbert (Opus 5.5, 048f3c63) | Q175 SSO design review r2 (DESIGN.md blob 3eb718b5e131 at 1a268249e), static | changes: C1 config.json console key fails old loaders (mosaic-config.mjs:113 rejects unknown keys; key add inside phase 2 window, rollback removes it, loader test), C2 app.js signs out only on opaqueredirect so 401 never shows sign-in; notes on tests 15/7/9a, Q-B 200-HTML classification, scratch Console flags; Q-E keep iss/sub escaped and capped; verdict sent to Sage, no Gitea/queue record
|
||||
2026-10-20T18:40:20Z | Filbert (Opus 5.5, 048f3c63) | Q175 SSO design review r3 (DESIGN.md blob a152fd58e18e at 817e8f51b), static | approve on condition D1 (slot file checks stay out of shared loader, shape-only there) as a line edit; D2 notes: slot root from configDir, scratch dataRoot, name discovery GETs for Mos; C1/C2 and notes verified; Mos C1 sequence confirm still open; no Gitea/queue record
|
||||
2026-10-20T18:41:20Z | Filbert (Opus 5.5, 048f3c63) | Q175 SSO design D1/D2 check (DESIGN.md blob eba861f5d2bb at d55d86bb0), static git diff 817e8f51b..d55d86bb0 | ack: D1 and D2 match; design approved; Mos C1 sequence confirm still open; no Gitea/queue record
|
||||
2026-10-20T18:57:10Z | Filbert (Opus 5.5, 048f3c63) | row 190 design review r2 (#1688, DESIGN.md sha256 5493c9df3ec0) | CHANGES to Darkwing, cc Sage: B1 claim check on MOSAIC_CONFIG-only selection, B2 shared prefix drop-in dirs; N1-N7 notes; static read, no slot, no Gitea or queue record
|
||||
2026-10-20T19:00:04Z | Filbert (Opus 5.5, 048f3c63) | row 190 design review r3 (#1688, DESIGN.md sha256 98236a1aff98) | APPROVE to Darkwing, cc Sage: B1 and B2 closed with M13 and M14, N7 settled by Sage's 48ebee0ff; static read, no slot; manifest round gets a queue review record (sage-1337)
|
||||
|
||||
+39
-1
@@ -368,7 +368,7 @@ reports checkout or copy identity and refactor drift, and writes nothing.
|
||||
```
|
||||
scripts/stack-release.sh adopt BUSINESS
|
||||
scripts/stack-release.sh activate ID BUSINESS [--pm | --pm-after-harness] [--migrate] [--tester-port N]
|
||||
[--proof-header NAME --proof-file PATH | --no-proof] [--timeout S] [--plan]
|
||||
[--proof-header NAME --proof-file PATH | --no-proof | --sso] [--no-send] [--timeout S] [--plan]
|
||||
scripts/stack-release.sh pm-on BUSINESS [--timeout S]
|
||||
scripts/stack-release.sh rollback BUSINESS [--restore ACTIVATION [--events N]] [--timeout S] [--plan]
|
||||
scripts/stack-release.sh status BUSINESS
|
||||
@@ -480,6 +480,44 @@ copies. It checks the file with `stat` and `realpath` and never reads the value.
|
||||
or `off`. On the live copy it also notes that the run re-pins the pointer's copy
|
||||
and only the Console flags change.
|
||||
|
||||
`activate ... --no-send` (row 189, mos-0972) drops `--send` from the pointer's
|
||||
Console flags and the unit, so the Console serves reads and no human sends. It
|
||||
may name the live copy while the pointer holds `--send`; once `--send` is gone a
|
||||
same-copy `--no-send` refuses `already-active`. Later activates, `pm-on` and
|
||||
rollbacks carry the pointer, so send stays off. No flag puts `--send` back.
|
||||
`--plan` prints one `send:` line: `dropped`, `off` with or without the
|
||||
`--no-send` note, or `carried`.
|
||||
|
||||
`activate ... --sso` (row 200, #1698) starts the Console with Q175 sign-in
|
||||
(row 192). It drops `--public-host`, `--auth-header`, `--allow-user` and any
|
||||
proof pair from the pointer's Console flags and the unit, and puts `--sso` after
|
||||
the other flags, before `--send` and `--tester-port`. It may name the live copy
|
||||
once, so a copy already at row 192 or later turns sign-in on without a copy
|
||||
change. Later activates, `pm-on` and rollbacks to a copy with `--sso` carry it.
|
||||
`--sso` with a proof pair or `--no-proof`, or on any other verb, is a usage
|
||||
error. Before anything stops, a run whose Console flags hold `--sso` refuses
|
||||
`sso-target` for a target Console without it, `sso-config` when the target's own
|
||||
`checkConsole` refuses the config's `console` key (or there is none, in which
|
||||
case, from another copy, it names the first step: activate the target without
|
||||
`--sso`, then write `console.sso`), and `sso-slot` for each provider's slot file
|
||||
that is missing, a symlink, not a regular file, not owned by this uid, has group
|
||||
or other bits, resolves inside the repository or the copies, or is empty. It
|
||||
uses `lstat`, `stat` and `realpath` on the slot and never reads it. `--plan`
|
||||
prints one `sso:` line: `on` with what it drops and carries, `carried`, or
|
||||
`dropped` on a rollback.
|
||||
|
||||
A rollback from an `--sso` pointer to a copy whose Console lacks `--sso` takes
|
||||
the Console flags from the rollback set of the activation that moved the stack
|
||||
onto the pointer's copy from the target, past any same-copy re-pin, checked
|
||||
against the set's `SHA256SUMS`. `--send` and `--tester-port` follow the pointer.
|
||||
When that set is missing, fails its sums, holds no readable argv or already ran
|
||||
`--sso`, the run refuses `sso-rollback-flags`. Any activate or rollback also
|
||||
refuses `config-console-key` before anything stops when the config holds a
|
||||
`console` key the target's `scripts/mosaic-config.mjs validate` refuses (a copy
|
||||
before row 192 refuses the key). The message says to remove the key first. So a
|
||||
window re-pins to row 192 or later before it writes the key, and a rollback past
|
||||
row 192 removes the key first.
|
||||
|
||||
#### A target past c216f47db: the PM after the harness kind
|
||||
|
||||
From c216f47db on, the bus launches the PM only with a `harness` kind in the
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# Brief: activate --no-send drops the Console's --send
|
||||
|
||||
Sage, 2026-10-20, for Mos's ruling mos-0972 (Q175 interim, Vikunja #412). Owner Sage, reviewer Dewey, gate owner
|
||||
Sage. Base: refactor 9ae8c73ac (row 163 landed). Line numbers are at that commit.
|
||||
Sage. Base: refactor 9ae8c73ac (row 163 landed). Line numbers are at that commit. The candidate corrects them;
|
||||
the first draft took them before row 163.
|
||||
|
||||
This row builds the tool only. The live restart without `--send` is a separate step afterwards, a same-copy
|
||||
activate under Q173 with a backup first and a STATE report after. No test here touches `~/.mosaic-dev`, the
|
||||
@@ -14,11 +15,11 @@ Mos ruled that the Console runs without `--send` until Q175 phase 2, because wit
|
||||
process can forge `X-authentik-username` on 127.0.0.1:7332 and send as Jason (design section 8). On the pinned
|
||||
stack, the Console flags live in the pointer (`~/.mosaic-dev/state/stack-active.json`):
|
||||
|
||||
- `console_for_target` (scripts/stack-release.sh:658-687) carries the pointer's flags unchanged. Only
|
||||
- `console_for_target` (scripts/stack-release.sh:664-693) carries the pointer's flags unchanged. Only
|
||||
`--tester-port` and the row 187 proof pair can change.
|
||||
- `check_target` (:247-249) lets an activate name the pointer's copy only for a proof-pair change.
|
||||
- `adopt` refuses once a pointer exists (:822).
|
||||
- A hand `webui-service.sh install` without `--send` makes `flags_drift` (:640-651) refuse
|
||||
- `check_target` (:249-256) lets an activate name the pointer's copy only for a proof-pair change.
|
||||
- `adopt` refuses once a pointer exists (:828).
|
||||
- A hand `webui-service.sh install` without `--send` makes `flags_drift` (:646-657) refuse
|
||||
`unit-flags-drift` on every later activate, pm-on and rollback.
|
||||
|
||||
So the tool can't drop `--send` today. This row adds that one path.
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# Brief: a credential store per agent, own git identity, pushes to a remote
|
||||
|
||||
Sage, 2026-10-20, for mos-0974 (MVP T1 item 6) and the MVP plan
|
||||
(`jason-master-restart/projects/mosaic-mvp/PLAN.md` item 6, GAP-MAP G11 and G13; TARGET.md: "credentials store
|
||||
available for all agents; secrets protected with RLS equivalent" and "Agent uses own credentials for git and
|
||||
other actions"). Gitea #1697. Owner Megavolt (Opus 5.5, a new builder seat), reviewer Duckworth, gate owner
|
||||
Sage. Base: refactor at the brief commit. Design first. The first deliverable is
|
||||
`agents/megavolt/work/queue-ROW/DESIGN.md`.
|
||||
|
||||
This row draws a security boundary. Sage reviews the design's authority model, and Mos rules on it before any
|
||||
code is written.
|
||||
|
||||
## What exists today (GAP-MAP, measured at 9ae8c73ac)
|
||||
|
||||
- G11. Credential references exist per instance for Gitea and Vikunja only
|
||||
(`packages/business/src/credentials.mjs`, `packages/bus/src/credentials.mjs`, `vocabulary.mjs:103`). One
|
||||
Claude env file is shared by every seat.
|
||||
- G13. Agents use their own Vikunja token (`packages/tasks/src/adapter.mjs`). Git goes through the host
|
||||
socket (`packages/cli/src/git-socket.mjs`). Gitea tokens are loaded but never used.
|
||||
- The development repository's helper (`scripts/git-credential-mosaic.mjs`, wired in `.git/config` for
|
||||
git.mosaicstack.dev) pushes as one identity. Until this row lands, seats push only their own `<seat>/*`
|
||||
branches through it (mos-0981).
|
||||
|
||||
## What the design must settle
|
||||
|
||||
1. The store. Where each agent's credentials live, per instance, under the row 190 config canon. File modes,
|
||||
ownership and the read path. Which process may read which entry, and how the bus enforces that (the "RLS
|
||||
equivalent"). A process asking for another agent's entry is refused and logged.
|
||||
2. Kinds. Gitea (git over HTTPS, and the API), Vikunja, and the model provider. Say which kinds 10-27 needs
|
||||
and which wait. The model provider entry must not copy jwoltje's `claude-session.env`. If it needs a new
|
||||
provider account or subscription, flag it for Jason and don't pick one.
|
||||
3. Git identity and push. Each agent commits with its own name and email and pushes with its own token to a
|
||||
remote it is granted. Which refs an agent may push (its own branches, never the default branch or another
|
||||
agent's), and where that's enforced: the Gitea side (branch protection, token scopes) and our side. How
|
||||
this fits the host git socket, or replaces it.
|
||||
4. Lifecycle. Issue, rotate (`rotateBy`, `expires`), revoke and delete, and what row 194 (agent lifecycle) calls
|
||||
when an agent is hired or fired. A fired agent's credentials stop working at once.
|
||||
5. Who does what. Creating a Gitea user or token, or any account outside the fleet, is Jason's (Q173
|
||||
exclusions). The design lists each such step as a command or UI action for Jason. Agents never mint
|
||||
accounts.
|
||||
6. Migration. Today's seats (jarvis helper, seat tokens) keep working until each moves. The mos-0981 push
|
||||
rule ends only when Mos says so.
|
||||
7. What this row closes in G11 and G13, and what stays open.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- No credential value is read, printed, logged or committed. Tests use fake tokens in scratch roots and a
|
||||
fake Gitea.
|
||||
- No change to `~/.mosaic-dev`, `~/.config/mosaic-dev`, the live units, slot files, `~/.mosaic` or the live
|
||||
`.git/config` of the canonical checkout.
|
||||
- No request to git.mosaicstack.dev, tasks.woltje.com or any provider from a test.
|
||||
|
||||
## Gate
|
||||
|
||||
Duckworth approves DESIGN.md, Mos rules on the authority model, then Duckworth approves the manifest. The bus,
|
||||
business, cli and tasks suites and every `scripts/test-*.sh` are green on Sage's gate, and the design's
|
||||
mutants are killed.
|
||||
@@ -0,0 +1,77 @@
|
||||
# Brief: Melanie's hub instance on its own host (row 195, was 190b)
|
||||
|
||||
Sage, 2026-10-20. Rewritten for Mos's rulings mos-0998 and mos-1005 (Vikunja #418, #419). The first version
|
||||
put the hub under its own Linux user on dragon-lin. That form can't pass Duckworth's B7 there, because the
|
||||
factory uid's `sudo -n` succeeds on dragon-lin (Mos's recon, brain-redux
|
||||
`projects/mosaic-mvp/MELANIE-HOST-RECON.md`). Melanie's own server is ruled out too: it has 1.2 GiB RAM free
|
||||
with her two Hermes bots running, and co-tenancy would share her user and her passwordless sudo. The hub runs
|
||||
on a separate host. Which host is Jason's decision (#419): hardware outside the cluster, a cluster VM with
|
||||
factory PVE root removed, or a cluster VM with that residual accepted. This row is written so the host choice
|
||||
doesn't change it. Gitea #1693. On the 10-27 MVP path. Federation R8 (Gizmoduck's design r4, 04a6112be) needs
|
||||
this row or nothing.
|
||||
|
||||
Owner Darkwing (Opus 5.5), reviewer Duckworth (author of B5 and B7), gate owner Sage. Chained on row 190
|
||||
(Q177): the hub runs under row 190's config canon. Design first. The first deliverable is
|
||||
`agents/darkwing/work/queue-190b/DESIGN.md`.
|
||||
|
||||
## Goal
|
||||
|
||||
Melanie's hub instance (Case 2, C2-13) runs on its own host. No factory agent can read or write the hub's
|
||||
config, data root or secrets, reach its bus socket, or log in to the hub host. Her writing reaches the hub only
|
||||
through the existing git.mosaicstack.dev remotes, books repo only.
|
||||
|
||||
## Provision (blank until Jason answers #419)
|
||||
|
||||
Who provisions the host and how is left open. Sizing from Mos (mos-0998): 4 vCPU, 8 GB RAM, 64 GB disk,
|
||||
internal network only. Each agent process measures 300 to 390 MB RSS on dragon-lin. The design takes the host
|
||||
as given: a fresh Linux host with a named admin path, reachable by the deploying seat while the row runs.
|
||||
|
||||
## The design must state
|
||||
|
||||
1. The deploy. Exactly what Darkwing installs on the hub host and how: the stack copy, node, the units, the
|
||||
config dir and data root under row 190's canon (O1: `~/.config/mosaic-NAME`, and "`~` is the home of the OS
|
||||
user that runs the instance"). Which commands run as which user on the hub host, and which need its root.
|
||||
2. Re-pin, rollback and backup on the hub host, and who runs them after the deploy key is gone.
|
||||
3. How the hub gets model access without a copy of jwoltje's `claude-session.env` or any factory credential.
|
||||
If that needs the credentials row (T1 item 6), say so and name the interim. If it needs a new subscription
|
||||
or a new provider account, flag it for Jason and don't pick one (mos-1008).
|
||||
4. The books repo path. Which git.mosaicstack.dev remote, which credential the hub holds for it, and that it
|
||||
can't read or push any other repository.
|
||||
5. The admin path and the end state. The host ends with no key the factory uid can read. The design names the
|
||||
admin path that remains (for example the hypervisor console, or a key kept off the factory uid) and who
|
||||
holds it.
|
||||
6. The acceptance script, as code Duckworth can read. It takes Gizmoduck's list of infrastructure gate items
|
||||
as its input (mos-1011), not a fixed set of checks. Each item names a path, its closed form and its
|
||||
accepted form, and the script reports which form holds for each, or fails. The list covers at least:
|
||||
- federation R8's separate-host form: the hub's bus socket, data root and config dir don't exist on the
|
||||
factory host, the factory host mounts nothing of the hub's, and no factory agent holds a login or key
|
||||
for the hub host
|
||||
- B7: from the factory uid, `sudo -n`, `doas -n`, `su`, polkit, and `ssh -o BatchMode=yes` to the hub
|
||||
host all fail
|
||||
- the hypervisor and backup paths ops-01 measured (mos-1011: one PVE node the factory key has root on,
|
||||
PBS on that node with unencrypted backups, a flat /16 LAN), each closed or accepted by Jason
|
||||
7. The identity provider and TLS front end (B8, mos-1008, mos-1011). Either the factory holds no credential
|
||||
that can create users, change groups, issue tokens or impersonate in the issuer the hub trusts, or the hub
|
||||
uses an issuer the factory can't administer. A brand, tenant or app inside the same Authentik does not
|
||||
count as separate. The same holds for the TLS front end that serves the hub. The proof is a failed admin
|
||||
call from every token the factory's root can read. No credential value is printed in that proof.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- Provisioning, hypervisor access and any infra or k3s change are Jason's (Q173 exclusions). Mos takes the
|
||||
hypervisor residual (factory root on the PVE nodes, Q172) to Jason. It doesn't block this row.
|
||||
- Melanie's server (`[email protected]`) is reference only, read only, under Jason's authorization in
|
||||
mos-0995. Nothing on it changes, and her Hermes bots keep running.
|
||||
- No tunnel, Traefik or other exposure outside the internal network. That waits for Jason, after Q175 SSO.
|
||||
- No credential value is printed, logged or committed. Keys made for the deploy are removed at the end, and
|
||||
the acceptance script proves it.
|
||||
- The admin-call proof in point 7 and any call to the issuer or the TLS front end run only in a window Mos
|
||||
approves, never as a test or an anonymous probe (Q175).
|
||||
- Tests in the repository use scratch roots and stand-ins. The live deploy and the acceptance run on the hub
|
||||
host are a window with a backup first (where there is state) and a STATE report after.
|
||||
|
||||
## Gate
|
||||
|
||||
Duckworth approves DESIGN.md, then the manifest. Any repository change is green on Sage's gate (cli suite and
|
||||
every `scripts/test-*.sh`), with the design's mutants killed. The row is done when the acceptance script passes
|
||||
on the hub host from the factory uid, Duckworth reads its output, and Mos confirms.
|
||||
@@ -0,0 +1,62 @@
|
||||
# Brief: the human boundary across uids and humans (P19 333)
|
||||
|
||||
Sage, 2026-10-20, for Mos's order mos-0989 (P19 333, MOSAICSTA-173, is now on the 10-27 path: federation R3 and
|
||||
row 193 need it). Gitea #1694. Owner Gander (Codex gpt-6.1-sol, a new builder seat under mos-0977 item 5),
|
||||
reviewer Duckworth, gate owner Sage. Latest-by: DESIGN.md approved by 2026-10-22, landed by 2026-10-24, and in
|
||||
any case before federation R3 lands. R3 and row 193 may chain on this row's candidate (Q177). Base: refactor at
|
||||
the brief commit. Design first. The first deliverable is `agents/gander/work/queue-ROW/DESIGN.md`.
|
||||
|
||||
## The problem, as filed and as it now matters
|
||||
|
||||
P19 333 as filed (row 87, mos-0657): the Console listens on a loopback TCP port, and any host process that
|
||||
reaches it, under any uid, acts as Jason. Jason accepted that as a documented limit for a one-user host
|
||||
(mos-0787, row 161). Two things change that:
|
||||
|
||||
- Row 190b (#1693, mos-0986) puts Melanie's hub under its own Linux user. Factory processes then sit on another
|
||||
uid, on the same host, next to the hub's ports and socket. The other-uid reach is now the Case 2 threat.
|
||||
- Federation r2 (gizmoduck-0004, 60b9b112a) binds a bus user only from a user-cli whose proof passes the human
|
||||
walk, whose direct parent is the bus's own Console, and which holds a live grant. That rule rests on the
|
||||
path-bound human proof (#225): today a same-uid process that runs the tree's `human-cli.mjs` acts as the
|
||||
human, without touching the Console. The Q175 design (section 8) names that limit and leaves it here.
|
||||
|
||||
## What already settles part of it
|
||||
|
||||
- Q175 phase 2 (#412) settles the Console-ingress part on the main port. Under `--sso`, 7332 answers any
|
||||
non-public Host with 403 except `GET /healthz`, and every other request needs a verified OIDC session. A
|
||||
process on another uid that reaches the port gets only `/healthz`. Phase 1 (row 192) builds it; it is live
|
||||
only after the phase 2 window.
|
||||
- Row 189 (`--no-send`) is interim only. Until phase 2 the Console takes no human writes, so the reach is
|
||||
read-only. It settles nothing.
|
||||
- Row 190b separates files and units by uid. Whether the hub's `broker.sock` refuses the factory uid depends on
|
||||
its directory and socket modes, which this row checks and tests rather than assumes.
|
||||
|
||||
## What the design must settle
|
||||
|
||||
1. Inventory every listener and entry point per instance: the Console port, the tester listener (7333 today),
|
||||
`broker.sock`, the human CLI wrapper, the Discord connector and anything row 190 adds. For each: who can
|
||||
reach it today from the same uid and from another uid, with and without `--sso`.
|
||||
2. The tester listener. It serves reads and the row 175 tester writes to anything that reaches it, on any uid.
|
||||
Close the other-uid reach (for example a unix socket with 0600 modes, or a peer-uid check on loopback), or
|
||||
bring Mos a concrete reason it must stay open. bus.tester stays Jason's switch.
|
||||
3. The bus human boundary. How the bus tells the human's own verified call from a same-uid process that runs
|
||||
`human-cli.mjs`. State what federation R3's direct-parent rule needs from the bus (peer credentials on
|
||||
`broker.sock`, the parent check, how it can't be forged by a child of an agent), and what stays possible for
|
||||
a same-uid process after this row. Say it plainly where a residual limit remains.
|
||||
4. Fit with row 193 (Morgana, human users and roles; Mos's B1 rule: a principal comes only from something the
|
||||
bus verifies) and with federation R3. One mechanism, not three.
|
||||
5. Migration: the live single-user Console and bus keep working until each instance re-pins.
|
||||
6. Which parts of P19 333 this row closes, which parts Q175 phase 2 closes, and what P19 333 says when it is
|
||||
closed.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- Who can reach the Console from outside stays Jason's. This row changes what a host process can do once it
|
||||
reaches a local port or socket.
|
||||
- No test touches `~/.mosaic-dev`, `~/.config/mosaic-dev`, the live units or a slot file. No test creates a real
|
||||
second uid; use stand-ins as rows 190 and 190b do, and name any part only a Q173 window can prove.
|
||||
- No request to the live 127.0.0.1:7332 or 7333, mosaic.woltje.com or auth.diversecanvas.com.
|
||||
|
||||
## Gate
|
||||
|
||||
Duckworth approves DESIGN.md, then the manifest. The bus, webui and cli suites and every `scripts/test-*.sh`
|
||||
are green on Sage's gate, and the design's mutants are killed.
|
||||
@@ -12,8 +12,10 @@ before any code, as Q175 did. Then the build follows the design, with tests and
|
||||
|
||||
Two or more instances run side by side on dragon-lin. Each one has its own config file, data root (store,
|
||||
broker.sock, host.lock, state, runs, sessions, workspaces), businesses, bus unit, Console unit and port,
|
||||
tester port, Discord unit, Claude session env file, stack-active pointer and activation log. Nothing one
|
||||
instance does can read or change another's state. The current instance keeps working unchanged: its
|
||||
tester port, Discord unit, Claude session env file, stack-active pointer and activation log. No instance's
|
||||
commands, units or tools address another instance's state by default or by mistake. On one uid this is
|
||||
addressing only, not a security boundary: a same-uid process can still read another instance's files.
|
||||
Isolation against that is row 190b (#1693, mos-0986), one Linux user per instance. The current instance keeps working unchanged: its
|
||||
config path, data root, unit names and ports stay as they are, and no re-pin is needed for it.
|
||||
|
||||
## Known single-instance points (GAP-MAP.md, measured at 9ae8c73ac)
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# Brief: retire the control board writer on 7331
|
||||
|
||||
Sage, 2026-10-20, for Mos's ruling mos-1008 item 2. Gitea #1696. MVP 10-27 work. Owner Bushroot (Haiku 5.5, a
|
||||
low-level model under Q177), reviewer Webby, gate owner Sage. Base: refactor at the brief commit.
|
||||
|
||||
## Why
|
||||
|
||||
`packages/control-board/src/serve.mjs` is a loopback HTTP service on 7331 with no authentication. It reads
|
||||
transcripts and writes through `/api/reply`. Any host process that reaches the port can do both. That fails
|
||||
Q175 ("zero trust with verification"). Gander found it in the row 196 inventory (gander-0004). Nothing listens
|
||||
on 7331 now (`ss -ltnp`, 2026-10-20T19:05:09Z), so retiring it costs the least.
|
||||
|
||||
## Steps, in order
|
||||
|
||||
1. Inventory first. List every reference to the control board and to 7331: scripts, units, `scripts/mosaic`
|
||||
and other CLI dispatch, docs (AGENTS.md, READMEs, TOOLS.md), package.json scripts, tests, and the Console
|
||||
(`packages/webui`). For each, say whether it starts the writer, reads its code, or only mentions it.
|
||||
Commit the list as `agents/bushroot/work/queue-ROW/INVENTORY.md` and send it to Sage and Webby.
|
||||
2. Stop if something depends on it. If a reference shows a live dependency (the Console or another package
|
||||
imports code that only the control board has, or a unit or script still starts it), stop. Sage takes it to
|
||||
Mos before anything is removed.
|
||||
3. Otherwise retire the writer. Either remove the `serve` entry point, or make it refuse to start with a
|
||||
message that points to the Console. Keep any read-only code another package imports, and say which.
|
||||
Update the docs that tell people to start it. The AGENTS.md text about the control-board reply convention
|
||||
is a separate matter. Change it only if it tells someone to start 7331.
|
||||
|
||||
## Mos's call on the Console dependency (mos-1019)
|
||||
|
||||
The inventory (004f133bd) found that the Console proxies `/api/board`, `/api/seen`, `/api/reply` and the
|
||||
conversation routes to `DEFAULT_BOARD` on 7331 (`packages/webui/src/serve.mjs:12`), so the board view returns
|
||||
502 today. Mos took Sage's recommendation: retire the 7331 listener and its README start commands, and have
|
||||
the Console call the board's read code in-process, behind the Console's own auth (Q175 SSO on 7332).
|
||||
`/api/reply` stays only under `--send`. The DESIGN.md for that change comes before code, and Webby approves
|
||||
it.
|
||||
|
||||
## Acceptance
|
||||
|
||||
Nothing in the stack can start a writer on 7331. A test proves it: the entry point refuses (or is gone), no
|
||||
script or unit starts it, and a grep check finds no starter. Each change gets a mutant that brings the writer
|
||||
back and the test kills it. Mos added three items (mos-1019):
|
||||
|
||||
- The board view answers from the Console with nothing listening on 7331.
|
||||
- Every board route sits behind the Console's auth.
|
||||
- A test proves `/api/reply` refuses without `--send`.
|
||||
|
||||
Test fixtures that bind port 0 may stay if they only test the code in-process.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- No change to the live units, `~/.mosaic-dev`, `~/.config/mosaic-dev`, slot files or `~/.mosaic`.
|
||||
- No request to any live port.
|
||||
- Single test files run freely. Suites need Sage's slot.
|
||||
|
||||
## Gate
|
||||
|
||||
Webby approves INVENTORY.md, then DESIGN.md, then the manifest. The control-board and webui package suites, the cli suite and
|
||||
every `scripts/test-*.sh` are green on Sage's gate, and the mutants are killed.
|
||||
@@ -0,0 +1,42 @@
|
||||
# Brief: a second gate lane, two full gates at once on dragon-lin
|
||||
|
||||
Sage, 2026-10-20, for Mos's order mos-0977 item 6 ("put the design for a second gate lane on the list now")
|
||||
and the MVP push (mos-0974). Gitea #1695. Owner Herb (Codex gpt-6.1-sol, a new builder seat), reviewer
|
||||
Webby, gate owner Sage. Base: refactor at the brief commit. Design first. The first deliverable is
|
||||
`agents/herb/work/queue-ROW/DESIGN.md`.
|
||||
|
||||
## Why
|
||||
|
||||
Every full run shares one slot today. A full gate (cli suite plus every `scripts/test-*.sh`) takes about 13
|
||||
minutes or more, reviewers' runs and mutants take the same slot, and with eight or more seats building for
|
||||
10-27 the slot is the bottleneck. The slot exists because concurrent runs broke each other. The busy check is
|
||||
`agents/sage/work/gate-lane/busy-now.sh` (a copy of Sage's), and the gate is
|
||||
`agents/sage/work/gate-lane/gate-template.sh` (a copy of Sage's template). The host has 16 cores and 62 GB.
|
||||
|
||||
## What the design must settle
|
||||
|
||||
1. Every shared resource two concurrent runs can collide on, found from the code and tests, not guessed:
|
||||
Docker compose project names and networks (`scripts/test-task.sh` and others), fixed TCP ports in tests,
|
||||
unix socket paths and their 107-byte limit under TMPDIR, the sandbox's private `/tmp`, shared caches,
|
||||
scratch and worktree paths, the queue lock, and any test that reads host-wide state (process lists,
|
||||
`pgrep`, `/proc`). For each: how a lane gets its own copy, or why it is safe to share.
|
||||
2. Timing. Which tests have time limits that a doubled CPU load could break, measured, and what changes (a
|
||||
bound, or a lane rule that keeps those suites apart).
|
||||
3. The lane mechanism: lane ids, a lock per lane that a run takes and releases even on a crash, the busy
|
||||
check that reports per lane, and how a seat asks for a lane. Sage still grants it.
|
||||
4. The gate template takes a lane id and isolates everything from point 1 by it.
|
||||
5. Proof: two full gates on different candidates at once, three times, with zero cross-failures, plus the
|
||||
wall time of each against a single-lane run. Any flake found is named with its cause.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- No change to the live units, `~/.mosaic-dev`, `~/.config/mosaic-dev` or slot files. The proof runs use
|
||||
scratch roots only, and it needs Sage's grant of the whole slot while it runs.
|
||||
- Test code changes that only isolate a resource are in scope. A change to what a test checks is not, and
|
||||
goes to its owner.
|
||||
- Docker networks and containers a run makes are removed by the run (trap), and nothing else is touched.
|
||||
|
||||
## Gate
|
||||
|
||||
Webby approves DESIGN.md, then the manifest. The cli suite and every `scripts/test-*.sh` are green on Sage's
|
||||
gate, both singly and in the two-lane proof.
|
||||
@@ -0,0 +1,39 @@
|
||||
# Brief: stack-release.sh activates a Console with --sso (Q175 phase 2 prerequisite)
|
||||
|
||||
Sage, 2026-10-20, for Launchpad's finding F3 on row 192 (launchpad-0001) and Q175 (#412). Gitea #1698. Owner
|
||||
Launchpad, reviewer Webby, gate owner Sage. Chained on row 192's candidate (Q177): it lands after row 192 and
|
||||
before the phase 2 window. Base: row 192's candidate.
|
||||
|
||||
## Why
|
||||
|
||||
The phase 2 window re-pins the Console to the phase 1 commit with `--sso --send --tester-port 7333`
|
||||
(`agents/sage/work/q175-sso/DESIGN.md`, section 5). `scripts/stack-release.sh` doesn't know `--sso` today.
|
||||
Its `proof_checks` (`:751`) requires `--public-host`, `--auth-header` and `--allow-user` whenever a proof pair is
|
||||
carried, and the pointer's Console flags still hold the forward-auth pair (`--auth-header X-authentik-username
|
||||
--allow-user jason.woltje`) that `--sso` refuses to start with. So the window can't run through the script, and
|
||||
a hand-edited pointer is not an option.
|
||||
|
||||
## What the row does
|
||||
|
||||
1. `activate ID BUSINESS --sso` writes Console flags with `--sso` and without `--auth-header`, `--allow-user`
|
||||
and any proof pair, and `--plan` names each flag it drops and carries, in the style of row 189's `send:`
|
||||
line. The design names the full flag set the phase 2 Console starts with and checks it against row 192's
|
||||
refusals.
|
||||
2. Before anything stops, the script refuses a target copy whose Console doesn't support `--sso`, and refuses
|
||||
when the config's `console` key or its slot file fails the checks row 192's `--sso` start runs. It reads
|
||||
neither value and only uses the same stat and shape checks.
|
||||
3. Rollback from an `--sso` pointer to a pre-phase-1 copy follows the design's phase 2 rollback order (remove
|
||||
the `console` key first). The script refuses a rollback that would start a Console on a config it can't
|
||||
read, and says which step comes first.
|
||||
4. `docs/TOOLS.md` gets the flag.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- No change to the live pointer, units, `~/.mosaic-dev` or `~/.config/mosaic-dev`. Tests use scratch roots
|
||||
and seeded pointers, as rows 187 and 189 do.
|
||||
- No request to the live Console, mosaic.woltje.com or auth.diversecanvas.com.
|
||||
|
||||
## Gate
|
||||
|
||||
Webby approves the manifest. The cli suite and every `scripts/test-*.sh` are green on Sage's gate, and the
|
||||
mutants are killed (one per refusal, and one that keeps `--auth-header` in the written flags).
|
||||
@@ -12,6 +12,7 @@ import {tmpdir} from 'node:os';
|
||||
import {join,dirname,basename} from 'node:path';
|
||||
import {fileURLToPath,pathToFileURL} from 'node:url';
|
||||
import {once} from 'node:events';
|
||||
import {ALGS} from '../../webui/src/sso-config.mjs';
|
||||
|
||||
const ROOT=fileURLToPath(new URL('../../..',import.meta.url)).replace(/\/$/,'');
|
||||
const NODE=process.execPath;
|
||||
@@ -260,9 +261,17 @@ const storeText=(v,froms,to=true)=>`export const SCHEMA_VERSION = '${v}';\nexpor
|
||||
const KINDS_NEW=readFileSync(join(repo,'packages','plugins','src','kinds.mjs'),'utf8');
|
||||
const KINDS_OLD=KINDS_NEW.replace('\n harness: Object.freeze({','\n "harness-before-c216f47db": Object.freeze({');
|
||||
assert.notEqual(KINDS_OLD,KINDS_NEW);
|
||||
// Row 200 (#1698): C1 and C take --sso and have the row 192 loader and checkConsole; C's checker
|
||||
// refuses RS256, a stand-in for a target whose rules differ from the repository's. B has the loader
|
||||
// from before row 192, which refuses a `console` key. The other copies have no loader.
|
||||
const LOADER=readFileSync(join(repo,'scripts','mosaic-config.mjs'),'utf8'),SSO_CONFIG=readFileSync(join(repo,'packages','webui','src','sso-config.mjs'),'utf8');
|
||||
const LOADER_OLD=LOADER.replace('["configVersion", "environment", "dataRoot", "execution", "console"]','["configVersion", "environment", "dataRoot", "execution"]');
|
||||
const SSO_CONFIG_C=SSO_CONFIG.replace("'ES256', 'RS256',","'ES256',");
|
||||
assert.notEqual(LOADER_OLD,LOADER);assert.notEqual(SSO_CONFIG_C,SSO_CONFIG);
|
||||
const ssoFiles=(id)=>id===B?[['scripts/mosaic-config.mjs',LOADER_OLD]]:[C1,C].includes(id)?[['scripts/mosaic-config.mjs',LOADER],['packages/webui/src/sso-config.mjs',id===C?SSO_CONFIG_C:SSO_CONFIG]]:[];
|
||||
for(const [id,store] of [[F,'// a copy from before SCHEMA_VERSION\n'],[A,storeText('3g',['3f'],'3x')],[B,storeText('3g',['3f'])],[C1,storeText('3g',['3f'],false)],[C,storeText('3h',['3f','3g'])],[S,storeText('3g',['3f'])],[U,storeText('3g',['3f'])]]){
|
||||
const d=join(COPIES,id);
|
||||
for(const [f,text,mode] of [['scripts/mosaic',script(mosaicStub),0o755],['packages/webui/src/cli.mjs',[B,C1,C].includes(id)?"// fixture Console, never run\n// serve accepts '--tester-port'\n// serve accepts '--proof-header' (row 178)\n":'// fixture Console before tester listener, never run\n',0o644],['packages/bus/src/store.mjs',store,0o644],['packages/bus/src/migrate-schema.mjs',`(${migrateStub})();\n`,0o644],['packages/plugins/src/kinds.mjs',[C1,C].includes(id)?KINDS_NEW:KINDS_OLD,0o644]]){
|
||||
for(const [f,text,mode] of [['scripts/mosaic',script(mosaicStub),0o755],['packages/webui/src/cli.mjs',[B,C1,C].includes(id)?`// fixture Console, never run\n// serve accepts '--tester-port'\n// serve accepts '--proof-header' (row 178)\n${[C1,C].includes(id)?"// serve accepts '--sso' (row 192)\n":''}`:'// fixture Console before tester listener, never run\n',0o644],['packages/bus/src/store.mjs',store,0o644],['packages/bus/src/migrate-schema.mjs',`(${migrateStub})();\n`,0o644],['packages/plugins/src/kinds.mjs',[C1,C].includes(id)?KINDS_NEW:KINDS_OLD,0o644],...ssoFiles(id).map(([f,text])=>[f,text,0o644])]){
|
||||
mkdirSync(dirname(join(d,f)),{recursive:true});writeFileSync(join(d,f),text,{mode});
|
||||
}
|
||||
for(const p of ['packages/business/src','roles'])cpSync(join(repo,p),join(d,p),{recursive:true});
|
||||
@@ -381,7 +390,7 @@ async function seed(e,{copy=B,pm=false,store='3g',pointer:how='adopt'}={}){
|
||||
const rolledBack=(code,to=B)=>`stack-release: ${code}; rolled back to ${to}, which passed the gate\n`;
|
||||
const planBackup=(e)=>`backup: the run backs up the stopped store to ${e.data}/backups/stack/<activation>/ before any migration`;
|
||||
const planNoBackup='backup: none; the run rolls back before the backup';
|
||||
const planTail=(e,to,pm=false)=>[`bus: scripts/bus-service.sh install --copy ${to} --copies ${COPIES}${pm?' --pm':''} --claude ${e.claude}`,'proof: off',`Console: scripts/webui-service.sh install --copy ${to} --copies ${COPIES} --port ${e.port} --business acme`,`CLI: scripts/stack-copy.sh wrapper ${to} --copies ${COPIES} --bin ${e.bin}`];
|
||||
const planTail=(e,to,pm=false)=>[`bus: scripts/bus-service.sh install --copy ${to} --copies ${COPIES}${pm?' --pm':''} --claude ${e.claude}`,'proof: off','send: off',`Console: scripts/webui-service.sh install --copy ${to} --copies ${COPIES} --port ${e.port} --business acme`,`CLI: scripts/stack-copy.sh wrapper ${to} --copies ${COPIES} --bin ${e.bin}`];
|
||||
|
||||
// --- The T table ---
|
||||
|
||||
@@ -1030,17 +1039,17 @@ test('T36 --plan shows the pending state and lists the row 169 refusals it can s
|
||||
return r.out.split('\n');
|
||||
};
|
||||
let out=await quiet(0,'activate',C1,'acme','--pm-after-harness','--plan');
|
||||
assert.deepEqual(out.slice(-6),['pm: the bus starts without --pm and the PM stays pending; next: Jason\'s plugins set of harness, then pm-on',...planTail(e,C1),'']);
|
||||
assert.deepEqual(out.slice(-7),['pm: the bus starts without --pm and the PM stays pending; next: Jason\'s plugins set of harness, then pm-on',...planTail(e,C1),'']);
|
||||
biz(e,HARNESS);
|
||||
out=await quiet(3,'activate',A,'acme','--pm-after-harness','--plan');
|
||||
assert.deepEqual(out.slice(-7),['pm: the bus starts without --pm and the PM stays pending; next: Jason\'s plugins set of harness, then pm-on',...planTail(e,A),`refuses unknown-kind-for-target: the business file names plug-in kind(s) harness, which ${A} does not know`,'']);
|
||||
assert.deepEqual(out.slice(-8),['pm: the bus starts without --pm and the PM stays pending; next: Jason\'s plugins set of harness, then pm-on',...planTail(e,A),`refuses unknown-kind-for-target: the business file names plug-in kind(s) harness, which ${A} does not know`,'']);
|
||||
out=await quiet(3,'activate',C1,'acme','--pm-after-harness','--plan');
|
||||
assert.equal(out.at(-2),'refuses pm-after-harness-set: the business file already sets harness ["claude-code"]; use --pm');
|
||||
biz(e);assert.equal((await run(e,'activate',C1,'acme','--pm-after-harness')).code,0);
|
||||
out=await quiet(3,'activate',C,'acme','--plan');
|
||||
assert.deepEqual(out.slice(-7),['pm: the pointer is pending (activate --pm-after-harness); the bus runs without its PM',...planTail(e,C,true),`refuses pm-pending: ${PENDING_MSG}`,'']);
|
||||
assert.deepEqual(out.slice(-8),['pm: the pointer is pending (activate --pm-after-harness); the bus runs without its PM',...planTail(e,C,true),`refuses pm-pending: ${PENDING_MSG}`,'']);
|
||||
out=await quiet(0,'rollback','acme','--plan');
|
||||
assert.deepEqual(out.slice(-6),['pm: the pointer is pending (activate --pm-after-harness); the bus runs without its PM',...planTail(e,B,true),'']);
|
||||
assert.deepEqual(out.slice(-7),['pm: the pointer is pending (activate --pm-after-harness); the bus runs without its PM',...planTail(e,B,true),'']);
|
||||
});
|
||||
|
||||
// Row 176 test 11: supported and older Console copies deliberately differ from the checkout.
|
||||
@@ -1706,6 +1715,327 @@ test('T54 row 188: a carried pair gets the proof file checks, and a proof file i
|
||||
});
|
||||
});
|
||||
|
||||
// Row 189 (#1687, mos-0972/0973): activate --no-send drops the pointer's --send; nothing turns it back on.
|
||||
const sendLines=(out)=>out.filter((l)=>l.startsWith('send:'));
|
||||
const noSend=(c)=>c.filter((x)=>x!=='--send');
|
||||
const LIVE_NOTE=`note: ${B} is the pointer's copy; the run re-pins it and only the Console flags change`;
|
||||
test('T55 row 189: activate --no-send drops --send, and rollback and pm-on carry it off',async(t)=>{
|
||||
await t.test('test 1: a live-copy --no-send drops --send from the pointer and the unit, keeps the rest in order, logs the step and backs up the stopped store',async(t)=>{
|
||||
const e=await mk(t),base=await seedPreview(e,undefined,['--send','--tester-port','7333']),want=noSend(base);
|
||||
assert.deepEqual(want.slice(-2),['--tester-port','7333']);
|
||||
const sum=createHash('sha256').update(readFileSync(e.store)).digest('hex'),n=calls(e).length;
|
||||
let r=await prun(e,'activate',B,'acme','--no-send');assert.equal(r.code,0,r.out+r.err);
|
||||
assert.equal(pointer(e).copy,B);assert.deepEqual(pointer(e).console,want);
|
||||
assert.ok(conExec(e).endsWith(` --checkout ${repo} ${want.join(' ')}`),conExec(e));assert.doesNotMatch(conExec(e),/ --send( |$)/);
|
||||
r=await prun(e,'status','acme');assert.equal(r.code,0,r.err);assert.match(r.out,/unit flags: match the pointer/);
|
||||
assert.match(stepText(e),/Z drop --send; --no-send was given\n/);
|
||||
const act=pointer(e).activation,l=logLines(e).at(-1);
|
||||
assert.deepEqual([l.op,l.from,l.to,l.result,l.backup],['activate',B,B,'ok',act]);assert.equal(backupSum(e,act),sum);
|
||||
assert.deepEqual(startsStops(e,n),[`stop ${CON}`,`stop ${BUS}`,`start ${BUS}`,`start ${CON}`]);
|
||||
await noLeak(e);
|
||||
});
|
||||
await t.test('test 2: a plain live-copy activate still refuses already-active, and so does --no-send once --send is gone',async(t)=>{
|
||||
const e=await mk(t);await seedPreview(e,undefined,['--send']);
|
||||
await refused(e,['activate',B,'acme'],'already-active',`${B} is the pointer's copy`);
|
||||
await refused(e,['activate',B,'acme','--tester-port','7333'],'already-active',`${B} is the pointer's copy`);
|
||||
await refused(e,['activate',B,'acme','--no-proof'],'already-active',`${B} is the pointer's copy`);
|
||||
const r=await prun(e,'activate',B,'acme','--no-send');assert.equal(r.code,0,r.out+r.err);
|
||||
await refused(e,['activate',B,'acme','--no-send'],'already-active',`${B} is the pointer's copy`);
|
||||
const g=await mk(t);await seedPreview(g);
|
||||
await refused(g,['activate',B,'acme','--no-send'],'already-active',`${B} is the pointer's copy`);
|
||||
for(const x of [e,g])await noLeak(x);
|
||||
});
|
||||
await t.test('test 3: --plan prints one send: line in each case, with the live-copy note on a same-copy run',async(t)=>{
|
||||
const e=await mk(t),base=await seedPreview(e,undefined,['--send']);
|
||||
let out=await planOnly(e,0,'activate',B,'acme','--no-send');
|
||||
assert.deepEqual(sendLines(out),['send: dropped; --no-send was given']);
|
||||
assert.deepEqual(out.filter((l)=>l.startsWith('note: ')),[LIVE_NOTE]);
|
||||
assert.equal(out[out.indexOf('send: dropped; --no-send was given')+1],`Console: scripts/webui-service.sh install --copy ${B} --copies ${COPIES} ${noSend(base).join(' ')}`);
|
||||
out=await planOnly(e,0,'activate',C1,'acme');
|
||||
assert.deepEqual(sendLines(out),['send: carried']);assert.deepEqual(out.filter((l)=>l.startsWith('note: ')),[]);
|
||||
assert.ok(out.find((l)=>l.startsWith('Console: ')).endsWith(' --send'));
|
||||
assert.deepEqual(sendLines(await planOnly(e,0,'activate',C1,'acme','--no-send')),['send: dropped; --no-send was given']);
|
||||
let r=await prun(e,'activate',C1,'acme','--no-send');assert.equal(r.code,0,r.out+r.err);
|
||||
assert.deepEqual(pointer(e).console,noSend(base));assert.match(stepText(e),/Z drop --send; --no-send was given\n/);
|
||||
assert.deepEqual(sendLines(await planOnly(e,0,'activate',B,'acme','--no-send')),['send: off; --no-send was given and the pointer holds no --send']);
|
||||
assert.deepEqual(sendLines(await planOnly(e,0,'activate',B,'acme')),['send: off']);
|
||||
assert.deepEqual(sendLines(await planOnly(e,0,'rollback','acme')),['send: off']);
|
||||
r=await prun(e,'activate',B,'acme','--no-send');assert.equal(r.code,0,r.out+r.err);
|
||||
assert.match(stepText(e),/Z --no-send was given and the pointer holds no --send, so there is nothing to drop\n/);
|
||||
assert.doesNotMatch(stepText(e),/drop --send/);
|
||||
await noLeak(e);
|
||||
});
|
||||
await t.test('test 4: after --no-send, a rollback to the earlier copy leaves --send out of the pointer and the unit',async(t)=>{
|
||||
const e=await mk(t),base=await seedPreview(e,undefined,['--send']);
|
||||
let r=await prun(e,'activate',C1,'acme','--no-send');assert.equal(r.code,0,r.out+r.err);
|
||||
r=await prun(e,'rollback','acme');assert.equal(r.code,0,r.out+r.err);
|
||||
assert.equal(pointer(e).copy,B);assert.deepEqual(pointer(e).console,noSend(base));assert.doesNotMatch(conExec(e),/ --send( |$)/);
|
||||
assert.doesNotMatch(stepText(e),/--send/);
|
||||
r=await prun(e,'status','acme');assert.equal(r.code,0,r.err);assert.match(r.out,/unit flags: match the pointer/);
|
||||
await noLeak(e);
|
||||
});
|
||||
await t.test('test 4: pm-on after --no-send keeps --send off',async(t)=>{
|
||||
const e=await mk(t),base=await seedPreview(e,{pm:true},['--send']);
|
||||
let r=await prun(e,'activate',C1,'acme','--pm-after-harness','--no-send');assert.equal(r.code,0,r.out+r.err);
|
||||
biz(e,HARNESS);
|
||||
r=await prun(e,'pm-on','acme');assert.equal(r.code,0,r.out+r.err);
|
||||
assert.equal(pointer(e).pm,true);assert.deepEqual(pointer(e).console,noSend(base));assert.doesNotMatch(conExec(e),/ --send( |$)/);
|
||||
r=await prun(e,'status','acme');assert.equal(r.code,0,r.err);assert.match(r.out,/unit flags: match the pointer/);
|
||||
await noLeak(e);
|
||||
});
|
||||
await t.test('test 5: --no-send on rollback, pm-on, status, adopt or rehearse is a usage error',async(t)=>{
|
||||
const e=await mk(t);await seedPreview(e,undefined,['--send']);
|
||||
const usageText=(await prun(e,'--help')).err,n=calls(e).length,logs=logLines(e).length,before=fp(e);
|
||||
for(const args of [['rollback','acme','--no-send'],['rollback','acme','--plan','--no-send'],['pm-on','acme','--no-send'],['status','acme','--no-send'],['adopt','acme','--no-send'],
|
||||
['rehearse','acme','--backup','/tmp/x','--port','7400','--no-send']]){
|
||||
const r=await prun(e,...args);assert.equal(r.code,4,args.join(' '));assert.equal(r.out,'');assert.equal(r.err,usageText);
|
||||
}
|
||||
assert.match(usageText,/\| --no-proof\] \[--no-send\]/);
|
||||
assert.deepEqual(fp(e),before);assert.deepEqual(startsStops(e,n),[]);assert.equal(logLines(e).length,logs);
|
||||
await noLeak(e);
|
||||
});
|
||||
await t.test('test 6: with both a pair and --send in the pointer, --no-send and --no-proof each drop what they name and keep the rest',async(t)=>{
|
||||
const e=await mk(t),base=await seedPreview(e,undefined,['--send']),pre=noSend(base),f=await proofFile(e),g=await proofFile(e,'proof2.189',0o400);
|
||||
let r=await prun(e,'activate',B,'acme',...pair(f));assert.equal(r.code,0,r.out+r.err);
|
||||
assert.deepEqual(pointer(e).console,[...pre,...pair(f),'--send']);
|
||||
let out=await planOnly(e,0,'activate',C1,'acme','--no-proof');
|
||||
assert.deepEqual([...proofLines(out),...sendLines(out)],['proof: dropped; --no-proof drops header x-console-proof','send: carried']);
|
||||
out=await planOnly(e,0,'activate',C1,'acme','--no-send');
|
||||
assert.deepEqual([...proofLines(out),...sendLines(out)],['proof: carried, header x-console-proof','send: dropped; --no-send was given']);
|
||||
out=await planOnly(e,0,'activate',B,'acme','--no-send','--no-proof');
|
||||
assert.deepEqual([...proofLines(out),...sendLines(out)],['proof: dropped; --no-proof drops header x-console-proof','send: dropped; --no-send was given']);
|
||||
r=await prun(e,'activate',C1,'acme','--no-send');assert.equal(r.code,0,r.out+r.err);
|
||||
assert.deepEqual(pointer(e).console,[...pre,...pair(f)]);
|
||||
r=await prun(e,'activate',C1,'acme','--no-send',...pair(g,'X-Proof-Two'));assert.equal(r.code,0,r.out+r.err);
|
||||
assert.deepEqual(pointer(e).console,[...pre,...pair(g,'X-Proof-Two')]);
|
||||
r=await prun(e,'status','acme');assert.equal(r.code,0,r.err);assert.match(r.out,/unit flags: match the pointer/);
|
||||
const h=await mk(t),hbase=await seedPreview(h,undefined,['--send']),hpre=noSend(hbase),k=await proofFile(h);
|
||||
r=await prun(h,'activate',B,'acme',...pair(k));assert.equal(r.code,0,r.out+r.err);
|
||||
r=await prun(h,'activate',C1,'acme','--no-proof');assert.equal(r.code,0,r.out+r.err);
|
||||
assert.deepEqual(pointer(h).console,[...hpre,'--send']);
|
||||
r=await prun(h,'activate',C1,'acme','--no-send','--no-proof');assert.equal(r.code,0,r.out+r.err);
|
||||
assert.deepEqual(pointer(h).console,hpre);assert.doesNotMatch(conExec(h),/--proof-| --send( |$)/);
|
||||
const steps=stepText(h);
|
||||
assert.match(steps,/Z --no-proof was given and the pointer holds no proof pair, so there is nothing to drop\n/);assert.match(steps,/Z drop --send; --no-send was given\n/);
|
||||
r=await prun(h,'status','acme');assert.equal(r.code,0,r.err);assert.match(r.out,/unit flags: match the pointer/);
|
||||
for(const x of [e,h])await noLeak(x);
|
||||
});
|
||||
});
|
||||
|
||||
// Row 200 (#1698): activate --sso. The config gets console.sso with one provider `a`, and its slot
|
||||
// file sits where cli.mjs's readSlot looks, beside the config, with a read watch: the run may stat
|
||||
// it but never open it. ES256 passes every copy's checker; RS256 passes C1's and fails C's.
|
||||
const SLOT='console-sso-a';
|
||||
const slotPath=(e)=>join(dirname(e.cfg),'secrets','mosaic-stack',SLOT);
|
||||
const SSO_KEY={sso:{publicOrigin:'https://console.example.test',providers:[{id:'a',issuer:'https://op.example.test/',clientId:'mosaic-console',clientSecret:{slot:SLOT},algs:['ES256'],subjects:[{sub:'subject-1',name:'jason'}]}]}};
|
||||
function setKey(e,key){const c=JSON.parse(readFileSync(e.cfg,'utf8'));if(key===undefined)delete c.console;else c.console=key;writeFileSync(e.cfg,JSON.stringify(c))}
|
||||
async function ssoConfig(e,{key=SSO_KEY,slot=true}={}){
|
||||
setKey(e,key);mkdirSync(dirname(slotPath(e)),{recursive:true,mode:0o700});
|
||||
return slot?proofFile(e,SLOT,0o600,dirname(slotPath(e))):slotPath(e);
|
||||
}
|
||||
const ssoLines=(out)=>out.filter((l)=>l.startsWith('sso:'));
|
||||
const DROPS='--public-host console.example --auth-header x-user --allow-user jason';
|
||||
const SHOW=`show-${CON}.txt`;
|
||||
const setOf=(e,act)=>join(e.state,'stack-rollback',act);
|
||||
const arrivalSet=(e,to)=>setOf(e,logLines(e).findLast((l)=>l.result==='ok'&&l.to===to&&l.from!==to).activation);
|
||||
const ROLLBACK_PLAN=(set)=>`sso: dropped; ${B}'s Console does not support --sso, so the rollback takes the Console flags from ${set}/${SHOW}, with --send and --tester-port as the pointer has them`;
|
||||
const KEY_MSG=(e,to,op,why)=>`${e.cfg} holds a console key, and ${to}'s loader refuses it (${why}); remove the key first (restore the config.json backup from the window's step 1), then run the ${op} again`;
|
||||
const RB_MSG=(why)=>`${B}'s Console does not support --sso, and the Console flags it ran with cannot be read: ${why}`;
|
||||
// Rewrites a file in a rollback set (0700, files kept by cp -p) and, unless sums is false, its SHA256SUMS line.
|
||||
function setEdit(set,name,from,to,sums=true){
|
||||
const f=join(set,name);chmodSync(set,0o700);chmodSync(f,0o600);edit(f,from,to);
|
||||
if(sums){const s=join(set,'SHA256SUMS');chmodSync(s,0o600);writeFileSync(s,readFileSync(s,'utf8').replace(new RegExp(`^[0-9a-f]{64}( \\./${name.replace(/[.@]/g,'\\$&')})$`,'m'),`${sha(f)}$1`))}
|
||||
}
|
||||
|
||||
test('T56 row 200: activate --sso, its refusals before any stop, and the rollback to a Console without --sso',async(t)=>{
|
||||
await t.test('item 1: --sso drops the preview set and the pair, puts --sso before --send and the tester port, and the plan names what it drops and carries',async(t)=>{
|
||||
const e=await mk(t),base=await seedPreview(e,undefined,['--send']),f=await proofFile(e),pre=noSend(base);
|
||||
let r=await prun(e,'activate',B,'acme','--tester-port','7333',...pair(f));assert.equal(r.code,0,r.out+r.err);
|
||||
assert.deepEqual(pointer(e).console,[...pre,...pair(f),'--send','--tester-port','7333']);
|
||||
await ssoConfig(e);
|
||||
const want=['--port',e.port,'--business','acme','--sso','--send','--tester-port','7333'];
|
||||
const out=await planOnly(e,0,'activate',C1,'acme','--sso');
|
||||
assert.deepEqual([...proofLines(out),...sendLines(out),...ssoLines(out)],['proof: dropped; --sso drops header x-console-proof','send: carried',
|
||||
`sso: on; drops ${DROPS}; carries --port ${e.port} --business acme --send --tester-port 7333`]);
|
||||
assert.equal(out[out.findIndex((l)=>l.startsWith('sso:'))+1],`Console: scripts/webui-service.sh install --copy ${C1} --copies ${COPIES} ${want.join(' ')}`);
|
||||
r=await prun(e,'activate',C1,'acme','--sso');assert.equal(r.code,0,r.out+r.err);
|
||||
assert.equal(pointer(e).copy,C1);assert.deepEqual(pointer(e).console,want);
|
||||
assert.ok(conExec(e).endsWith(` --checkout ${repo} ${want.join(' ')}`),conExec(e));
|
||||
r=await prun(e,'status','acme');assert.equal(r.code,0,r.err);assert.match(r.out,/unit flags: match the pointer/);
|
||||
const steps=stepText(e);
|
||||
assert.ok(steps.includes(`Z drop --proof-header x-console-proof --proof-file ${f}; --sso was given\n`),steps);
|
||||
assert.ok(steps.includes(`Z turn on --sso; drop ${DROPS}\n`),steps);
|
||||
await noLeak(e);
|
||||
});
|
||||
await t.test('item 1: --sso may re-pin the live copy once; later activates carry it; --no-send still drops --send',async(t)=>{
|
||||
const e=await mk(t),base=await seedPreview(e,undefined,['--send']);await ssoConfig(e);
|
||||
let r=await prun(e,'activate',C1,'acme');assert.equal(r.code,0,r.out+r.err);
|
||||
let out=await planOnly(e,0,'activate',C1,'acme','--sso');
|
||||
assert.deepEqual([...proofLines(out),...ssoLines(out)],['proof: off',`sso: on; drops ${DROPS}; carries --port ${e.port} --business acme --send`]);
|
||||
r=await prun(e,'activate',C1,'acme','--sso');assert.equal(r.code,0,r.out+r.err);
|
||||
const l=logLines(e).at(-1);assert.deepEqual([l.op,l.from,l.to,l.result],['activate',C1,C1,'ok']);
|
||||
assert.deepEqual(pointer(e).console,['--port',e.port,'--business','acme','--sso','--send']);
|
||||
assert.doesNotMatch(stepText(e),/proof/);
|
||||
await refused(e,['activate',C1,'acme','--sso'],'already-active',`${C1} is the pointer's copy`);
|
||||
await refused(e,['activate',C1,'acme'],'already-active',`${C1} is the pointer's copy`);
|
||||
out=await planOnly(e,0,'activate',C,'acme');assert.deepEqual(ssoLines(out),['sso: carried']);
|
||||
r=await prun(e,'activate',C,'acme','--migrate');assert.equal(r.code,0,r.out+r.err);
|
||||
// Carried, not turned on: no stamped step names sso (the install output's serve line still shows --sso).
|
||||
assert.deepEqual(pointer(e).console,['--port',e.port,'--business','acme','--sso','--send']);assert.doesNotMatch(stepText(e),/^\S+Z .*sso/m);
|
||||
assert.match(stepText(e),/^serve: .* --sso --send$/m);
|
||||
r=await prun(e,'activate',C,'acme','--no-send');assert.equal(r.code,0,r.out+r.err);
|
||||
assert.deepEqual(pointer(e).console,['--port',e.port,'--business','acme','--sso']);
|
||||
r=await prun(e,'status','acme');assert.equal(r.code,0,r.err);assert.match(r.out,/unit flags: match the pointer/);
|
||||
const g=await mk(t);await seedPreview(g,undefined,['--send']);await ssoConfig(g);
|
||||
r=await prun(g,'activate',C1,'acme','--sso','--no-send');assert.equal(r.code,0,r.out+r.err);
|
||||
assert.deepEqual(pointer(g).console,['--port',g.port,'--business','acme','--sso']);
|
||||
assert.ok(stepText(g).includes('Z drop --send; --no-send was given\n'));
|
||||
for(const x of [e,g])await noLeak(x);
|
||||
});
|
||||
await t.test('item 1: --sso with a pair or --no-proof, or on any verb but activate, is a usage error',async(t)=>{
|
||||
const e=await mk(t);await seedPreview(e);const f=await proofFile(e);
|
||||
const usageText=(await prun(e,'--help')).err,n=calls(e).length,logs=logLines(e).length,before=fp(e);
|
||||
for(const args of [['activate',C1,'acme','--sso',...pair(f)],['activate',C1,'acme','--sso','--no-proof'],['activate',C1,'acme','--no-proof','--sso','--plan'],
|
||||
['rollback','acme','--sso'],['pm-on','acme','--sso'],['status','acme','--sso'],['adopt','acme','--sso']]){
|
||||
const r=await prun(e,...args);assert.equal(r.code,4,args.join(' '));assert.equal(r.out,'');assert.equal(r.err,usageText);
|
||||
}
|
||||
assert.match(usageText,/\[--proof-header NAME --proof-file PATH \| --no-proof \| --sso\] \[--no-send\]/);
|
||||
assert.deepEqual(fp(e),before);assert.deepEqual(startsStops(e,n),[]);assert.equal(logLines(e).length,logs);
|
||||
await noLeak(e);
|
||||
});
|
||||
await t.test('item 2: a target without --sso, a missing or bad console.sso, and each slot problem refuse before any stop',async(t)=>{
|
||||
const e=await mk(t);await seedPreview(e);await ssoConfig(e,{slot:false});
|
||||
const slot=slotPath(e),put=(mode=0o600,text='x\n')=>{rmSync(slot,{force:true,recursive:true});writeFileSync(slot,text,{mode});chmodSync(slot,mode)};
|
||||
put();
|
||||
await refused(e,['activate',B,'acme','--sso'],'sso-target',`${B}'s Console does not support --sso`);
|
||||
setKey(e,undefined);
|
||||
// N4: from another copy, the refusal names the re-pin without --sso as the first step.
|
||||
await refused(e,['activate',C1,'acme','--sso'],'sso-config',`${C1}'s Console would refuse its --sso start: there is no console.sso in the system config; activate ${C1} without --sso first, then write console.sso, then run activate ${C1} --sso`);
|
||||
let r=await prun(e,'activate',C1,'acme');assert.equal(r.code,0,r.out+r.err);
|
||||
await refused(e,['activate',C1,'acme','--sso'],'sso-config',`${C1}'s Console would refuse its --sso start: there is no console.sso in the system config`);
|
||||
setKey(e,{sso:{...SSO_KEY.sso,providers:[{...SSO_KEY.sso.providers[0],algs:['RS256']}]}});
|
||||
r=await prun(e,'activate',C1,'acme','--sso','--plan');assert.equal(r.code,0,r.out+r.err);
|
||||
await refused(e,['activate',C,'acme','--sso'],'sso-config',`${C}'s Console would refuse its --sso start: console.sso.providers[0].algs[0] must be one of ${ALGS.filter((a)=>a!=='RS256').join(', ')}; HS* and none are refused`);
|
||||
setKey(e,SSO_KEY);
|
||||
const bad=(why)=>`console.sso provider a client secret slot ${slot} ${why}`;
|
||||
rmSync(slot);
|
||||
await refused(e,['activate',C,'acme','--sso'],'sso-slot',bad('is missing'));
|
||||
const elsewhere=join(e.root,'elsewhere');writeFileSync(elsewhere,'x\n',{mode:0o600});symlinkSync(elsewhere,slot);
|
||||
await refused(e,['activate',C,'acme','--sso'],'sso-slot',bad('is a symbolic link'));
|
||||
rmSync(slot);mkdirSync(slot,{mode:0o700});
|
||||
await refused(e,['activate',C,'acme','--sso'],'sso-slot',bad('is not a regular file'));
|
||||
for(const mode of [0o644,0o640,0o604]){put(mode);await refused(e,['activate',C,'acme','--sso'],'sso-slot',bad(`has mode ${mode.toString(8)}, which gives group or other permission bits`))}
|
||||
put(0o600,'');
|
||||
await refused(e,['activate',C,'acme','--sso'],'sso-slot',bad('is empty'));
|
||||
// The slot directory resolving into the repository: the file is no link, but its real path is.
|
||||
const inRepo=mkdtempSync(join(repo,'t56-slot-'));t.after(()=>rmSync(inRepo,{recursive:true,force:true}));
|
||||
writeFileSync(join(inRepo,SLOT),'x\n',{mode:0o600});chmodSync(join(inRepo,SLOT),0o600);
|
||||
const secrets=dirname(slot);rmSync(secrets,{recursive:true});symlinkSync(inRepo,secrets);
|
||||
await refused(e,['activate',C,'acme','--sso'],'sso-slot',bad('is inside the repository or the copies'));
|
||||
// --plan lists the sso refusal with the rest and writes nothing.
|
||||
const out=await planOnly(e,3,'activate',C,'acme','--sso','--tester-port','80');
|
||||
assert.deepEqual(out.filter((l)=>l.startsWith('refuses ')),[`refuses tester-port-invalid: --tester-port 80 is not a whole port from 1024 to 65535 other than 7332 and the Console's --port ${e.port}`,`refuses sso-slot: ${bad('is inside the repository or the copies')}`]);
|
||||
rmSync(secrets);mkdirSync(secrets,{mode:0o700});put();
|
||||
r=await prun(e,'activate',C,'acme','--sso','--migrate');assert.equal(r.code,0,r.out+r.err);
|
||||
await noLeak(e);
|
||||
});
|
||||
await t.test('item 3: a config whose console key the target loader refuses stops an activate or a rollback before any stop, naming the first step',async(t)=>{
|
||||
const e=await mk(t);await seedPreview(e);await ssoConfig(e);
|
||||
let r=await prun(e,'activate',C1,'acme');assert.equal(r.code,0,r.out+r.err);
|
||||
const why='mosaic-config: unsupported configuration key: "console"';
|
||||
await refused(e,['activate',B,'acme'],'config-console-key',KEY_MSG(e,B,'activate',why));
|
||||
await refused(e,['rollback','acme'],'config-console-key',KEY_MSG(e,B,'rollback',why));
|
||||
const out=await planOnly(e,3,'rollback','acme');
|
||||
assert.deepEqual(out.filter((l)=>l.startsWith('refuses ')),[`refuses config-console-key: ${KEY_MSG(e,B,'rollback',why)}`]);
|
||||
setKey(e,{sso:{...SSO_KEY.sso,providers:[{...SSO_KEY.sso.providers[0],algs:['RS256']}]}});
|
||||
await refused(e,['activate',C,'acme'],'config-console-key',KEY_MSG(e,C,'activate',`mosaic-config: console.sso.providers[0].algs[0] must be one of ${ALGS.filter((a)=>a!=='RS256').join(', ')}; HS* and none are refused`));
|
||||
setKey(e,undefined);
|
||||
r=await prun(e,'rollback','acme');assert.equal(r.code,0,r.out+r.err);assert.equal(pointer(e).copy,B);
|
||||
await noLeak(e);
|
||||
});
|
||||
await t.test('item 3: a rollback from --sso to a Console without it takes the flags from the set of the activation onto the copy, past a same-copy re-pin (sage-1389)',async(t)=>{
|
||||
const e=await mk(t),base=await seedPreview(e,undefined,['--send','--tester-port','7333']);await ssoConfig(e);
|
||||
let r=await prun(e,'activate',C1,'acme');assert.equal(r.code,0,r.out+r.err);
|
||||
const set=arrivalSet(e,C1);
|
||||
r=await prun(e,'activate',C1,'acme','--sso');assert.equal(r.code,0,r.out+r.err);
|
||||
r=await prun(e,'activate',C1,'acme','--no-send');assert.equal(r.code,0,r.out+r.err);
|
||||
// The latest set holds the --sso Console; the arrival set holds B's.
|
||||
assert.match(readFileSync(join(setOf(e,pointer(e).activation),SHOW),'utf8'),/ --sso /);
|
||||
assert.doesNotMatch(readFileSync(join(set,SHOW),'utf8'),/ --sso /);
|
||||
await refused(e,['rollback','acme'],'config-console-key',KEY_MSG(e,B,'rollback','mosaic-config: unsupported configuration key: "console"'));
|
||||
setKey(e,undefined);
|
||||
const want=[...noSend(base.slice(0,-2)),'--tester-port','7333'];
|
||||
const out=await planOnly(e,0,'rollback','acme');
|
||||
assert.deepEqual(ssoLines(out),[ROLLBACK_PLAN(set)]);
|
||||
assert.equal(out.find((l)=>l.startsWith('Console: ')),`Console: scripts/webui-service.sh install --copy ${B} --copies ${COPIES} ${want.join(' ')}`);
|
||||
r=await prun(e,'rollback','acme');assert.equal(r.code,0,r.out+r.err);
|
||||
assert.equal(pointer(e).copy,B);assert.deepEqual(pointer(e).console,want);
|
||||
assert.ok(conExec(e).endsWith(` --checkout ${repo} ${want.join(' ')}`),conExec(e));
|
||||
assert.ok(stepText(e).includes(`Z drop --sso; ${B}'s Console does not support it, so the Console flags come from ${set}/${SHOW}\n`));
|
||||
r=await prun(e,'status','acme');assert.equal(r.code,0,r.err);assert.match(r.out,/unit flags: match the pointer/);
|
||||
// The plain case: --send and the preview set and pair come back as they were.
|
||||
const g=await mk(t),gbase=await seedPreview(g,undefined,['--send']),f=await proofFile(g);
|
||||
r=await prun(g,'activate',B,'acme',...pair(f));assert.equal(r.code,0,r.out+r.err);
|
||||
await ssoConfig(g);
|
||||
r=await prun(g,'activate',C1,'acme','--sso');assert.equal(r.code,0,r.out+r.err);
|
||||
setKey(g,undefined);
|
||||
r=await prun(g,'rollback','acme');assert.equal(r.code,0,r.out+r.err);
|
||||
assert.deepEqual(pointer(g).console,[...noSend(gbase),...pair(f),'--send']);
|
||||
for(const x of [e,g])await noLeak(x);
|
||||
});
|
||||
await t.test('item 3: a rollback set that is missing, fails its sums, holds no argv or already ran --sso refuses sso-rollback-flags',async(t)=>{
|
||||
const e=await mk(t);await seedPreview(e);await ssoConfig(e);
|
||||
let r=await prun(e,'activate',C1,'acme','--sso');assert.equal(r.code,0,r.out+r.err);
|
||||
setKey(e,undefined);
|
||||
const set=arrivalSet(e,C1),show=join(set,SHOW),text=readFileSync(show,'utf8');
|
||||
setEdit(set,SHOW,' --public-host ',' --sso --public-host ');
|
||||
await refused(e,['rollback','acme'],'sso-rollback-flags',RB_MSG(`the Console in ${show} already ran with --sso`));
|
||||
let out=await planOnly(e,3,'rollback','acme');
|
||||
assert.deepEqual(out.filter((l)=>l.startsWith('refuses ')),[`refuses sso-rollback-flags: ${RB_MSG(`the Console in ${show} already ran with --sso`)}`]);
|
||||
setEdit(set,SHOW,' --sso --public-host ',' --public-host ',false);
|
||||
await refused(e,['rollback','acme'],'sso-rollback-flags',RB_MSG(`${show} does not match the set's SHA256SUMS`));
|
||||
setEdit(set,SHOW,' serve ',' ',true);
|
||||
await refused(e,['rollback','acme'],'sso-rollback-flags',RB_MSG(`${show} holds no Console argv this script can read`));
|
||||
rmSync(show);
|
||||
await refused(e,['rollback','acme'],'sso-rollback-flags',RB_MSG(`${set} has no ${SHOW} and SHA256SUMS`));
|
||||
writeFileSync(show,text);setEdit(set,SHOW,'\n','\n',true);
|
||||
// A log whose arrival line came from another copy names no set for B.
|
||||
const log=readFileSync(e.log,'utf8');chmodSync(e.log,0o600);
|
||||
writeFileSync(e.log,log.replaceAll(`"from":"${B}","to":"${C1}"`,`"from":"${A}","to":"${C1}"`));
|
||||
await refused(e,['rollback','acme'],'sso-rollback-flags',RB_MSG(`the log names no activation that moved the stack from ${B} onto ${C1}`));
|
||||
writeFileSync(e.log,readFileSync(e.log,'utf8').replaceAll(`"from":"${A}","to":"${C1}"`,`"from":"${B}","to":"${C1}"`));
|
||||
r=await prun(e,'rollback','acme');assert.equal(r.code,0,r.out+r.err);assert.equal(pointer(e).copy,B);
|
||||
await noLeak(e);
|
||||
});
|
||||
await t.test('round 2 (webby-0028 N1): run through a symlinked repository path, a slot or a proof file inside the repository still refuses',async(t)=>{
|
||||
const link=join(base,'repo-link.200');if(!existsSync(link))symlinkSync(repo,link);
|
||||
const e=await mk(t);e.sr=join(link,'scripts','stack-release.sh');await seedPreview(e);
|
||||
const f=await proofFile(e,'proof.200',0o600,mkdtempSync(join(repo,'t56-proof-')));t.after(()=>rmSync(dirname(f),{recursive:true,force:true}));
|
||||
await refused(e,['activate',C1,'acme',...pair(f)],'proof-file-invalid',`--proof-file ${f} is inside the repository or the copies`);
|
||||
await ssoConfig(e,{slot:false});
|
||||
let r=await prun(e,'activate',C1,'acme');assert.equal(r.code,0,r.out+r.err);
|
||||
const inRepo=mkdtempSync(join(repo,'t56-slot-'));t.after(()=>rmSync(inRepo,{recursive:true,force:true}));
|
||||
writeFileSync(join(inRepo,SLOT),'x\n',{mode:0o600});chmodSync(join(inRepo,SLOT),0o600);
|
||||
const secrets=dirname(slotPath(e));rmSync(secrets,{recursive:true});symlinkSync(inRepo,secrets);
|
||||
await refused(e,['activate',C1,'acme','--sso'],'sso-slot',`console.sso provider a client secret slot ${slotPath(e)} is inside the repository or the copies`);
|
||||
await noLeak(e);
|
||||
});
|
||||
await t.test('round 2 (webby-0028 N2): a config.json that does not parse stops the run before anything is read or stopped',async(t)=>{
|
||||
const e=await mk(t);await seedPreview(e);
|
||||
// load_config exits first, before the pointer, the log or a unit is read; config_checks' own
|
||||
// refusal is the guard for a file that changes after that (BUILD.md, round 2).
|
||||
const good=readFileSync(e.cfg,'utf8');writeFileSync(e.cfg,good.slice(0,-1));
|
||||
const before=fp(e),n=calls(e).length,logs=logLines(e).length;
|
||||
for(const args of [['activate',C1,'acme'],['rollback','acme'],['activate',C1,'acme','--plan']]){
|
||||
const r=await prun(e,...args);assert.equal(r.code,1,args.join(' '));assert.match(r.err,/common: configuration load failed\n$/);
|
||||
}
|
||||
assert.deepEqual(fp(e),before);assert.deepEqual(startsStops(e,n),[]);assert.equal(logLines(e).length,logs);
|
||||
writeFileSync(e.cfg,good);
|
||||
const r=await prun(e,'activate',C1,'acme');assert.equal(r.code,0,r.out+r.err);
|
||||
await noLeak(e);
|
||||
});
|
||||
});
|
||||
|
||||
// --- rehearse (D2, row 163; the brief's "Stack rehearsal on a scratch root") ---
|
||||
// Copies of their own in RCOPIES: the real scripts/mosaic, config loader, business loader, roles and
|
||||
// mosaic-config.mjs, with stand-ins for the bus host (cli.mjs), the Console, the Store and
|
||||
|
||||
+248
-23
@@ -3,7 +3,8 @@
|
||||
# Moves the bus, the Console and the human CLI from the pointer's copy to another copy together,
|
||||
# behind a health gate, and puts the old stack back inside the same run when the gate fails.
|
||||
# Console tester listener flags follow target support (row 176, #1674); so does the row 178 proof
|
||||
# pair, which activate sets or drops (row 187, #1685).
|
||||
# pair, which activate sets or drops (row 187, #1685). activate --no-send drops --send (row 189, #1687).
|
||||
# activate --sso swaps the preview set and the pair for the Console's own sign-in (row 200, #1698).
|
||||
# Design and tests: agents/darkwing/work/queue-162/BUILD.md.
|
||||
#
|
||||
# Who runs it. Decision 96 (Mos, mos-0716) lets this script stop and start the live units under
|
||||
@@ -30,13 +31,13 @@ BIN="$HOME/.npm-global/bin"
|
||||
UNIT_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
|
||||
CONSOLE_UNIT=mosaic-webui.service
|
||||
TIMEOUT=60 WANT_PM=0 AFTER_HARNESS=0 MIGRATE=0 PLAN=0 RESTORE="" EVENTS="" TESTER_PORT=""
|
||||
PROOF_HEADER="" PROOF_FILE="" NO_PROOF=0 R_BACKUP="" R_PORT=""
|
||||
PROOF_HEADER="" PROOF_FILE="" NO_PROOF=0 NO_SEND=0 SSO=0 R_BACKUP="" R_PORT=""
|
||||
|
||||
usage() {
|
||||
cat >&2 <<'USAGE'
|
||||
usage: stack-release.sh adopt BUSINESS
|
||||
stack-release.sh activate ID BUSINESS [--pm | --pm-after-harness] [--migrate] [--tester-port N]
|
||||
[--proof-header NAME --proof-file PATH | --no-proof] [--timeout S] [--plan]
|
||||
[--proof-header NAME --proof-file PATH | --no-proof | --sso] [--no-send] [--timeout S] [--plan]
|
||||
stack-release.sh pm-on BUSINESS [--timeout S]
|
||||
stack-release.sh rollback BUSINESS [--restore ACTIVATION [--events N]] [--timeout S] [--plan]
|
||||
stack-release.sh status BUSINESS
|
||||
@@ -85,6 +86,8 @@ parse_opts() {
|
||||
--proof-header) [ "$OP" = activate ] && [ $# -ge 2 ] && [ -n "$2" ] || usage; PROOF_HEADER="$2"; shift 2 ;;
|
||||
--proof-file) [ "$OP" = activate ] && [ $# -ge 2 ] && [ -n "$2" ] || usage; PROOF_FILE="$2"; shift 2 ;;
|
||||
--no-proof) [ "$OP" = activate ] || usage; NO_PROOF=1; shift ;;
|
||||
--no-send) [ "$OP" = activate ] || usage; NO_SEND=1; shift ;;
|
||||
--sso) [ "$OP" = activate ] || usage; SSO=1; shift ;;
|
||||
--migrate) [ "$OP" = activate ] || usage; MIGRATE=1; shift ;;
|
||||
--restore) [ "$OP" = rollback ] && [ $# -ge 2 ] && [[ "$2" =~ ^[0-9]{8}T[0-9]{6}Z-[0-9a-f]{12}(-[1-9])?$ ]] || usage; RESTORE="$2"; shift 2 ;;
|
||||
--backup) [ "$OP" = rehearse ] && [ $# -ge 2 ] && { [[ "$2" =~ ^[0-9]{8}T[0-9]{6}Z-[0-9a-f]{12}(-[1-9])?$ ]] || [[ "$2" =~ ^/[A-Za-z0-9/._+-]*$ ]]; } || usage; R_BACKUP="$2"; shift 2 ;;
|
||||
@@ -102,6 +105,8 @@ parse_opts() {
|
||||
# The proof pair goes together, and --no-proof goes alone (row 187).
|
||||
[ "${PROOF_HEADER:+1}" = "${PROOF_FILE:+1}" ] || usage
|
||||
[ "$NO_PROOF" = 0 ] || [ -z "$PROOF_HEADER" ] || usage
|
||||
# --sso drops the pair itself, so it takes neither a pair nor --no-proof (row 200).
|
||||
[ "$SSO" = 0 ] || { [ -z "$PROOF_HEADER" ] && [ "$NO_PROOF" = 0 ]; } || usage
|
||||
local d
|
||||
for d in "$COPIES" "$BIN"; do
|
||||
case "$d" in /*) ;; *) usage ;; esac
|
||||
@@ -157,7 +162,9 @@ for(const k of open.keys())process.stdout.write(k+"\n");' -- "$LOG"
|
||||
|
||||
# Reads the log's ok lines, newest first. `previous` prints rollback's target: the first `to`
|
||||
# that is not the pointer's copy (release.sh's rule). `backup` prints the backup field of the
|
||||
# line that made the pointer's copy live, the backup a rollback would restore.
|
||||
# line that made the pointer's copy live, the backup a rollback would restore. `arrival` prints the
|
||||
# activation and `from` of the line that moved the stack onto the pointer's copy from another one,
|
||||
# past any same-copy re-pin after it (row 200, sage-1389).
|
||||
from_log() {
|
||||
[ -f "$LOG" ] || return 1
|
||||
node -e '
|
||||
@@ -168,6 +175,7 @@ for(const l of lines.reverse()){
|
||||
if(o.result!=="ok"||!["adopt","activate","rollback"].includes(o.op)||!o.to)continue;
|
||||
if(mode==="previous"&&o.to!==current){process.stdout.write(o.to+"\n");process.exit(0)}
|
||||
if(mode==="backup"&&o.to===current){if(o.backup)process.stdout.write(o.backup+"\n");process.exit(o.backup?0:1)}
|
||||
if(mode==="arrival"&&o.to===current&&o.from!==current){process.stdout.write(`${o.activation??""} ${o.from??""}\n`);process.exit(0)}
|
||||
}
|
||||
process.exit(1);' -- "$LOG" "$1" "$P_COPY"
|
||||
}
|
||||
@@ -195,15 +203,23 @@ read_units() {
|
||||
U_CLAUDE=""
|
||||
read -r -a argv <<< "$env"
|
||||
for tok in "${argv[@]}"; do case "$tok" in MOSAIC_CLAUDE_BIN=*) U_CLAUDE="${tok#MOSAIC_CLAUDE_BIN=}" ;; esac; done
|
||||
read -r -a argv <<< "$console_argv"
|
||||
U_CONSOLE=()
|
||||
serve_flags "$console_argv" || return 1
|
||||
U_CONSOLE=("${SERVE_FLAGS[@]}")
|
||||
}
|
||||
|
||||
# Sets SERVE_FLAGS (array) to the Console argv $1 after `serve`, without --checkout and its value;
|
||||
# returns 1 with no `serve`.
|
||||
serve_flags() {
|
||||
local -a argv
|
||||
read -r -a argv <<< "$1"
|
||||
SERVE_FLAGS=()
|
||||
local i serve=0
|
||||
for ((i = 0; i < ${#argv[@]}; i++)); do
|
||||
if [ "$serve" = 0 ]; then [ "${argv[i]}" != serve ] || serve=1; continue; fi
|
||||
if [ "${argv[i]}" = --checkout ]; then i=$((i + 1)); continue; fi
|
||||
U_CONSOLE+=("${argv[i]}")
|
||||
SERVE_FLAGS+=("${argv[i]}")
|
||||
done
|
||||
[ "$serve" = 1 ] || return 1
|
||||
[ "$serve" = 1 ]
|
||||
}
|
||||
|
||||
console_port() {
|
||||
@@ -244,14 +260,17 @@ target_schema() {
|
||||
in_chain() { local v; for v in "${T_CHAIN[@]}"; do [ "$v" != "$1" ] || return 0; done; return 1; }
|
||||
|
||||
# The item 3 checks on the target copy, in order: copy, schema, already-active. An activate that
|
||||
# sets a proof pair, or drops the pointer's pair with --no-proof, may name the pointer's copy: it
|
||||
# re-pins the live copy with new Console flags (row 187, Part B's B3 and B-R1).
|
||||
# sets a proof pair, drops the pointer's pair with --no-proof, drops the pointer's --send with
|
||||
# --no-send, or turns on --sso, may name the pointer's copy: it re-pins the live copy with new
|
||||
# Console flags (row 187, Part B's B3 and B-R1; row 189, mos-0972; row 200, the phase 2 window).
|
||||
check_target() {
|
||||
copy_exists "$TO" 2>/dev/null || refuse copy-missing "no copy $TO in $COPIES"
|
||||
copy_ok "$TO" 2>/dev/null || refuse copy-unreachable "$TO is not reachable from refactor and origin/refactor"
|
||||
target_schema "$TO" || refuse copy-schema-unknown "cannot read SCHEMA_VERSION and its chain from $COPIES/$TO/packages/bus/src/store.mjs"
|
||||
[ "$TO" != "$P_COPY" ] || [ -n "$PROOF_HEADER" ] ||
|
||||
{ [ "$NO_PROOF" = 1 ] && [[ " ${P_CONSOLE[*]} " == *" --proof-header "* ]]; } ||
|
||||
{ [ "$NO_SEND" = 1 ] && [[ " ${P_CONSOLE[*]} " == *" --send "* ]]; } ||
|
||||
{ [ "$SSO" = 1 ] && [[ " ${P_CONSOLE[*]} " != *" --sso "* ]]; } ||
|
||||
refuse already-active "$TO is the pointer's copy"
|
||||
}
|
||||
|
||||
@@ -664,16 +683,17 @@ flags_drift() {
|
||||
console_for_target() {
|
||||
local supported=0 main=7332 inherited="" i
|
||||
local -a without_tester=()
|
||||
sso_base
|
||||
grep -q -- '--tester-port' "$COPIES/$TO/packages/webui/src/cli.mjs" && supported=1
|
||||
for ((i = 0; i < ${#P_CONSOLE[@]}; i++)); do
|
||||
if [ "${P_CONSOLE[i]}" = --tester-port ]; then
|
||||
i=$((i + 1)); inherited="${P_CONSOLE[i]:-}"
|
||||
for ((i = 0; i < ${#BASE_CONSOLE[@]}; i++)); do
|
||||
if [ "${BASE_CONSOLE[i]}" = --tester-port ]; then
|
||||
i=$((i + 1)); inherited="${BASE_CONSOLE[i]:-}"
|
||||
else
|
||||
without_tester+=("${P_CONSOLE[i]}")
|
||||
if [ "${P_CONSOLE[i]}" = --port ]; then main="${P_CONSOLE[i+1]:-7332}"; fi
|
||||
without_tester+=("${BASE_CONSOLE[i]}")
|
||||
if [ "${BASE_CONSOLE[i]}" = --port ]; then main="${BASE_CONSOLE[i+1]:-7332}"; fi
|
||||
fi
|
||||
done
|
||||
RUN_CONSOLE=("${P_CONSOLE[@]}")
|
||||
RUN_CONSOLE=("${BASE_CONSOLE[@]}")
|
||||
DROP_TESTER=""
|
||||
if [ -n "$TESTER_PORT" ]; then
|
||||
# --plan lists both with its other refusals (row 180); outside it they end the run.
|
||||
@@ -689,7 +709,205 @@ console_for_target() {
|
||||
RUN_CONSOLE=("${without_tester[@]}")
|
||||
DROP_TESTER="--tester-port $inherited"
|
||||
fi
|
||||
proof_for_target
|
||||
if [ "$SSO" = 1 ]; then sso_for_target; else proof_for_target; fi
|
||||
send_for_target
|
||||
sso_checks
|
||||
}
|
||||
|
||||
sso_supported() { grep -q -- '--sso' "$COPIES/$TO/packages/webui/src/cli.mjs"; }
|
||||
has_flag() { local f="$1"; shift; [[ " $* " == *" $f "* ]]; }
|
||||
|
||||
# Row 200 (#1698, R1): a rollback from an --sso pointer to a copy whose Console has no --sso. The
|
||||
# pointer no longer holds the preview set that Console ran with, so the flags come from the rollback
|
||||
# set of the activation that moved the stack onto the pointer's copy from the rollback target. A
|
||||
# same-copy re-pin after it has a set of its own, holding the same copy's earlier flags, so the
|
||||
# pointer's own activation is the wrong set (sage-1389). The set's show-mosaic-webui.service.txt is
|
||||
# the Console unit as it was before that run, checked against the set's SHA256SUMS. Sets
|
||||
# BASE_CONSOLE (the flags console_for_target starts from), SSO_PLAN and SSO_STEP. SSO_NO_TARGET
|
||||
# skips sso_checks after a refusal here, so --plan lists that refusal alone.
|
||||
sso_base() {
|
||||
BASE_CONSOLE=("${P_CONSOLE[@]}") SSO_PLAN="" SSO_STEP="" SSO_NO_TARGET=""
|
||||
has_flag --sso "${P_CONSOLE[@]}" || return 0
|
||||
[ "$SSO" = 1 ] || SSO_PLAN="sso: carried"
|
||||
[ "$OP" = rollback ] && ! sso_supported || return 0
|
||||
local f="show-$CONSOLE_UNIT.txt" argv="" why="" act="" from="" set
|
||||
read -r act from <<< "$(from_log arrival || true)"
|
||||
set="$STATE/stack-rollback/$act"
|
||||
if [ "$from" != "$TO" ]; then why="the log names no activation that moved the stack from $TO onto $P_COPY"
|
||||
elif ! [[ "$act" =~ ^[0-9]{8}T[0-9]{6}Z-[0-9a-f]{12}(-[1-9])?$ ]]; then why="the activation $act that moved the stack onto $P_COPY names no rollback set"
|
||||
elif [ ! -f "$set/$f" ] || [ ! -f "$set/SHA256SUMS" ]; then why="$set has no $f and SHA256SUMS"
|
||||
elif [ "$(sum_of "$set" "$f")" != "$(sha256sum < "$set/$f" | cut -d' ' -f1)" ]; then why="$set/$f does not match the set's SHA256SUMS"
|
||||
else
|
||||
argv="$(argv_of "$(cat "$set/$f")")"
|
||||
if ! unquoted "$argv" || ! serve_flags "$argv"; then why="$set/$f holds no Console argv this script can read"
|
||||
elif has_flag --sso "${SERVE_FLAGS[@]}"; then why="the Console in $set/$f already ran with --sso"
|
||||
fi
|
||||
fi
|
||||
if [ -n "$why" ]; then
|
||||
SSO_NO_TARGET=1
|
||||
check_refuse sso-rollback-flags "$TO's Console does not support --sso, and the Console flags it ran with cannot be read: $why"
|
||||
return 0
|
||||
fi
|
||||
# --send and --tester-port follow the pointer, so a --no-send or a tester port set after that
|
||||
# activation stays (row 189's rule that rollback carries --send off).
|
||||
local i
|
||||
BASE_CONSOLE=()
|
||||
for ((i = 0; i < ${#SERVE_FLAGS[@]}; i++)); do
|
||||
case "${SERVE_FLAGS[i]}" in
|
||||
--send) ;;
|
||||
--tester-port) i=$((i + 1)) ;;
|
||||
*) BASE_CONSOLE+=("${SERVE_FLAGS[i]}") ;;
|
||||
esac
|
||||
done
|
||||
for ((i = 0; i < ${#P_CONSOLE[@]}; i++)); do
|
||||
case "${P_CONSOLE[i]}" in
|
||||
--send) BASE_CONSOLE+=(--send) ;;
|
||||
--tester-port) i=$((i + 1)); BASE_CONSOLE+=(--tester-port "${P_CONSOLE[i]:-}") ;;
|
||||
esac
|
||||
done
|
||||
SSO_PLAN="sso: dropped; $TO's Console does not support --sso, so the rollback takes the Console flags from $set/$f, with --send and --tester-port as the pointer has them"
|
||||
SSO_STEP="drop --sso; $TO's Console does not support it, so the Console flags come from $set/$f"
|
||||
}
|
||||
|
||||
# Row 200 (#1698): activate --sso drops the preview set and the proof pair, which serve refuses beside
|
||||
# it, and puts --sso where webui-service.sh renders it: after the other flags, before --send and
|
||||
# --tester-port. Sets SSO_DROP (the plan's dropped preview flags), PROOF_PLAN and PROOF_STEP.
|
||||
sso_for_target() {
|
||||
local header="" file="" i
|
||||
local -a kept=()
|
||||
SSO_DROP=""
|
||||
for ((i = 0; i < ${#RUN_CONSOLE[@]}; i++)); do
|
||||
case "${RUN_CONSOLE[i]}" in
|
||||
--public-host | --auth-header | --allow-user) SSO_DROP+=" ${RUN_CONSOLE[i]} ${RUN_CONSOLE[i + 1]:-}"; i=$((i + 1)) ;;
|
||||
--proof-header) i=$((i + 1)); header="${RUN_CONSOLE[i]:-}" ;;
|
||||
--proof-file) i=$((i + 1)); file="${RUN_CONSOLE[i]:-}" ;;
|
||||
--sso) ;;
|
||||
*) kept+=("${RUN_CONSOLE[i]}") ;;
|
||||
esac
|
||||
done
|
||||
RUN_CONSOLE=()
|
||||
for ((i = 0; i < ${#kept[@]}; i++)); do
|
||||
case "${kept[i]}" in --send | --tester-port) break ;; esac
|
||||
RUN_CONSOLE+=("${kept[i]}")
|
||||
done
|
||||
RUN_CONSOLE+=(--sso "${kept[@]:i}")
|
||||
PROOF_PLAN="proof: off" PROOF_STEP=""
|
||||
if [ -n "$header" ]; then
|
||||
PROOF_PLAN="proof: dropped; --sso drops header $header"
|
||||
PROOF_STEP="drop --proof-header $header --proof-file $file; --sso was given"
|
||||
fi
|
||||
}
|
||||
|
||||
# Row 200 item 2: whenever the run's Console flags hold --sso, the checks row 192's --sso start runs,
|
||||
# before anything stops: a target Console with --sso, console.sso in config.json as the target's own
|
||||
# checkConsole reads it, and each provider's slot file. The slot gets lstat, stat and realpath
|
||||
# only; this script never opens it. Also sets the plan's sso: line for activate --sso.
|
||||
SSO_JS='
|
||||
const {readFileSync}=require("node:fs");const {pathToFileURL}=require("node:url");
|
||||
const [copy,file]=process.argv.slice(1);
|
||||
(async()=>{
|
||||
const {checkConsole}=await import(pathToFileURL(`${copy}/packages/webui/src/sso-config.mjs`).href);
|
||||
let doc;
|
||||
// A JSON.parse message can quote the text, so it is not passed on.
|
||||
try{doc=JSON.parse(readFileSync(file,"utf8"))}catch{throw new Error(`cannot read ${file} as JSON`)}
|
||||
if(!doc?.console?.sso)throw new Error("there is no console.sso in the system config");
|
||||
checkConsole(doc.console);
|
||||
for(const p of doc.console.sso.providers)process.stdout.write(`${p.id} ${p.clientSecret.slot}\n`);
|
||||
})().catch((e)=>{process.stdout.write(String(e?.message??e).split("\n")[0]);process.exit(1)});'
|
||||
sso_checks() {
|
||||
has_flag --sso "${RUN_CONSOLE[@]}" || return 0
|
||||
if [ "$SSO" = 1 ]; then
|
||||
local t carried="" dropped="${SSO_DROP# }"
|
||||
for t in "${RUN_CONSOLE[@]}"; do [ "$t" = --sso ] || carried+="${carried:+ }$t"; done
|
||||
SSO_PLAN="sso: on; drops ${dropped:-nothing}; carries ${carried:-nothing}"
|
||||
SSO_STEP="turn on --sso${SSO_DROP:+; drop$SSO_DROP}"
|
||||
fi
|
||||
[ -z "$SSO_NO_TARGET" ] || return 0
|
||||
sso_supported || { check_refuse sso-target "$TO's Console does not support --sso"; return 0; }
|
||||
local cfg="${MOSAIC_CONFIG:-$HOME/.config/mosaic-dev/config.json}" out id slot path why
|
||||
# N4 (webby-0019, sage-1403): a pre-phase-1 loader refuses a console key, so with the key not yet
|
||||
# written and the stack on another copy, the first step is the re-pin without --sso.
|
||||
local first=""
|
||||
if ! out="$(node -e "$SSO_JS" -- "$COPIES/$TO" "$cfg" 2>&1)"; then
|
||||
out="${out%%$'\n'*}"
|
||||
[ "$out" != "there is no console.sso in the system config" ] || [ "$P_COPY" = "$TO" ] ||
|
||||
first="; activate $TO without --sso first, then write console.sso, then run activate $TO --sso"
|
||||
check_refuse sso-config "$TO's Console would refuse its --sso start: $out$first"
|
||||
return 0
|
||||
fi
|
||||
while read -r id slot; do
|
||||
path="$(dirname "$cfg")/secrets/mosaic-stack/$slot"
|
||||
why="$(slot_problem "$path")"
|
||||
[ -z "$why" ] || check_refuse sso-slot "console.sso provider $id client secret slot $path $why"
|
||||
done <<< "$out"
|
||||
}
|
||||
|
||||
# Why a slot file can't serve, or nothing: readSlot's checks in packages/webui/src/sso.mjs, in its
|
||||
# order, from lstat, stat and realpath only, then a size of 0 for its one-non-empty-line rule.
|
||||
slot_problem() {
|
||||
local st uid mode size type real copies repo
|
||||
[ -e "$1" ] || [ -L "$1" ] || { echo "is missing"; return; }
|
||||
[ ! -L "$1" ] || { echo "is a symbolic link"; return; }
|
||||
st="$(stat -c '%u:%a:%s:%F' -- "$1" 2>/dev/null)" || { echo "cannot be read with stat"; return; }
|
||||
uid="${st%%:*}" st="${st#*:}"
|
||||
mode="${st%%:*}" st="${st#*:}"
|
||||
size="${st%%:*}" type="${st#*:}"
|
||||
case "$type" in "regular file" | "regular empty file") ;; *) echo "is not a regular file"; return ;; esac
|
||||
[ "$uid" = "$(id -u)" ] || { echo "is owned by uid $uid, not $(id -u), the uid this script runs as"; return; }
|
||||
(( (8#$mode & 8#077) == 0 )) || { echo "has mode $mode, which gives group or other permission bits"; return; }
|
||||
# REPO is $(pwd), which keeps a symlink in the path the script ran by (webby-0028 N1).
|
||||
real="$(realpath -e -- "$1")" && copies="$(realpath -m -- "$COPIES")" && repo="$(realpath -m -- "$REPO")" || { echo "cannot be resolved"; return; }
|
||||
case "$real" in "$repo" | "$repo"/* | "$copies" | "$copies"/*) echo "is inside the repository or the copies"; return ;; esac
|
||||
[ "$size" != 0 ] || echo "is empty"
|
||||
}
|
||||
|
||||
# Row 200 item 3 and the design's phase 2 rollback order: a config.json with a `console` key must be
|
||||
# one the target's loader reads, or its bus, Console and seats fail on their next read. Runs the
|
||||
# target's own scripts/mosaic-config.mjs validate; its output, the resolved config, is not kept.
|
||||
config_checks() {
|
||||
local cfg="${MOSAIC_CONFIG:-$HOME/.config/mosaic-dev/config.json}" out loader="$COPIES/$TO/scripts/mosaic-config.mjs" first
|
||||
# 0: a console key; 1: no key, or no file; 2: the file doesn't parse, so whether it holds the key
|
||||
# is unknown, and the run refuses rather than pass it (webby-0028 N2). load_config already exits on
|
||||
# such a file before anything is read or stopped, so this is reached only if the file changes after it.
|
||||
local key=0
|
||||
node -e 'const fs=require("fs"),f=process.argv[1];if(!fs.existsSync(f))process.exit(1);let d;try{d=JSON.parse(fs.readFileSync(f,"utf8"))}catch{process.exit(2)}
|
||||
process.exit(d!==null&&typeof d==="object"&&Object.hasOwn(d,"console")?0:1)' -- "$cfg" 2>/dev/null || key=$?
|
||||
[ "$key" != 1 ] || return 0
|
||||
if [ "$key" != 0 ]; then
|
||||
check_refuse config-console-key "$cfg cannot be read as JSON, so whether $TO's loader reads it is unknown; fix the config first, then run the $OP again"
|
||||
return 0
|
||||
fi
|
||||
if [ ! -f "$loader" ]; then out="it has no scripts/mosaic-config.mjs"
|
||||
elif out="$(MOSAIC_CONFIG="$cfg" node "$loader" validate 2>&1 >/dev/null)"; then return 0
|
||||
fi
|
||||
first="${out%%$'\n'*}"
|
||||
check_refuse config-console-key "$cfg holds a console key, and $TO's loader refuses it ($first); remove the key first (restore the config.json backup from the window's step 1), then run the $OP again"
|
||||
}
|
||||
|
||||
# Row 189 (mos-0972): --no-send drops --send from RUN_CONSOLE, so the unit and the pointer both lose
|
||||
# it; without the flag the pointer's --send goes on unchanged. Rollback and pm-on never take it, so
|
||||
# they carry the pointer as it is. Sets SEND_PLAN (the plan's send: line) and SEND_STEP.
|
||||
send_for_target() {
|
||||
local had=0 i
|
||||
local -a without_send=()
|
||||
for ((i = 0; i < ${#RUN_CONSOLE[@]}; i++)); do
|
||||
if [ "${RUN_CONSOLE[i]}" = --send ]; then had=1; else without_send+=("${RUN_CONSOLE[i]}"); fi
|
||||
done
|
||||
SEND_STEP=""
|
||||
if [ "$NO_SEND" = 1 ]; then
|
||||
RUN_CONSOLE=("${without_send[@]}")
|
||||
if [ "$had" = 1 ]; then
|
||||
SEND_PLAN="send: dropped; --no-send was given"
|
||||
SEND_STEP="drop --send; --no-send was given"
|
||||
else
|
||||
SEND_PLAN="send: off; --no-send was given and the pointer holds no --send"
|
||||
SEND_STEP="--no-send was given and the pointer holds no --send, so there is nothing to drop"
|
||||
fi
|
||||
elif [ "$had" = 1 ]; then
|
||||
SEND_PLAN="send: carried"
|
||||
else
|
||||
SEND_PLAN="send: off"
|
||||
fi
|
||||
}
|
||||
|
||||
# Row 187: the row 178 proof pair in RUN_CONSOLE. A new pair replaces the pointer's and goes last;
|
||||
@@ -769,7 +987,7 @@ proof_checks() {
|
||||
# Why the proof file can't serve, or nothing. lstat, stat and realpath only; the order follows
|
||||
# webui-service.sh render, which refuses the same files.
|
||||
proof_file_problem() {
|
||||
local st uid mode type real copies
|
||||
local st uid mode type real copies repo
|
||||
case "$1" in /*) ;; *) echo "is not an absolute path"; return ;; esac
|
||||
case "$1" in *[!A-Za-z0-9/._+-]*) echo "holds a character outside [A-Za-z0-9/._+-]"; return ;; esac
|
||||
[ ! -L "$1" ] || { echo "is a symlink"; return; }
|
||||
@@ -777,8 +995,8 @@ proof_file_problem() {
|
||||
uid="${st%%:*}" st="${st#*:}"
|
||||
mode="${st%%:*}" type="${st#*:}"
|
||||
case "$type" in "regular file" | "regular empty file") ;; *) echo "is not a regular file"; return ;; esac
|
||||
real="$(realpath -e -- "$1")" && copies="$(realpath -m -- "$COPIES")" || { echo "cannot be resolved"; return; }
|
||||
case "$real" in "$REPO" | "$REPO"/* | "$copies" | "$copies"/*) echo "is inside the repository or the copies"; return ;; esac
|
||||
real="$(realpath -e -- "$1")" && copies="$(realpath -m -- "$COPIES")" && repo="$(realpath -m -- "$REPO")" || { echo "cannot be resolved"; return; }
|
||||
case "$real" in "$repo" | "$repo"/* | "$copies" | "$copies"/*) echo "is inside the repository or the copies"; return ;; esac
|
||||
[ "$mode" = 600 ] || [ "$mode" = 400 ] || { echo "has mode $mode, not 600 or 400"; return; }
|
||||
[ "$uid" = "$(id -u)" ] || echo "is owned by uid $uid, not $(id -u), the uid this script runs as"
|
||||
}
|
||||
@@ -943,6 +1161,8 @@ plan() {
|
||||
echo "bus: scripts/bus-service.sh install --copy $TO --copies $COPIES$([ "$RUN_PM" = 0 ] || echo ' --pm') --claude $P_CLAUDE"
|
||||
[ -z "$DROP_TESTER" ] || echo "drop $DROP_TESTER; the target Console does not support it"
|
||||
echo "$PROOF_PLAN"
|
||||
echo "$SEND_PLAN"
|
||||
[ -z "$SSO_PLAN" ] || echo "$SSO_PLAN"
|
||||
echo "Console: scripts/webui-service.sh install --copy $TO --copies $COPIES${RUN_CONSOLE[*]:+ ${RUN_CONSOLE[*]}}"
|
||||
echo "CLI: scripts/stack-copy.sh wrapper $TO --copies $COPIES --bin $BIN"
|
||||
local r
|
||||
@@ -956,6 +1176,8 @@ run_steps() {
|
||||
capture || refuse capture-failed "could not write the rollback set $RS; nothing was stopped"
|
||||
[ -z "$DROP_TESTER" ] || say "drop $DROP_TESTER; the target Console does not support it"
|
||||
[ -z "$PROOF_STEP" ] || say "$PROOF_STEP"
|
||||
[ -z "$SEND_STEP" ] || say "$SEND_STEP"
|
||||
[ -z "$SSO_STEP" ] || say "$SSO_STEP"
|
||||
trap 'after_fail signal' INT TERM HUP
|
||||
stop_both || rollback_in_run stop-timeout
|
||||
if [ -e "$STORE" ]; then
|
||||
@@ -1015,9 +1237,10 @@ do_activate() {
|
||||
pointer_for_business
|
||||
run_pm
|
||||
[ "$P_PENDING" = 0 ] || check_refuse pm-pending "the pointer is pending (activate --pm-after-harness); run pm-on or rollback first"
|
||||
if [ "$PLAN" = 1 ]; then check_target; console_for_target; plugin_checks; plan; exit 0; fi
|
||||
if [ "$PLAN" = 1 ]; then check_target; console_for_target; plugin_checks; config_checks; plan; exit 0; fi
|
||||
precheck
|
||||
plugin_checks
|
||||
config_checks
|
||||
new_activation
|
||||
run_steps migrate
|
||||
}
|
||||
@@ -1027,9 +1250,10 @@ do_rollback() {
|
||||
pointer_for_business
|
||||
TO="$(from_log previous)" || refuse no-previous "the log names no earlier copy than $P_COPY"
|
||||
run_pm
|
||||
if [ "$PLAN" = 1 ]; then check_target; console_for_target; plugin_checks; plan; exit 0; fi
|
||||
if [ "$PLAN" = 1 ]; then check_target; console_for_target; plugin_checks; config_checks; plan; exit 0; fi
|
||||
precheck
|
||||
plugin_checks
|
||||
config_checks
|
||||
[ "$P_SCHEMA" = "$T_SCHEMA" ] || [ -n "$RESTORE" ] || refuse schema-differs "the store is at $P_SCHEMA and $TO expects $T_SCHEMA; pass --restore ACTIVATION"
|
||||
new_activation
|
||||
if [ -n "$RESTORE" ]; then
|
||||
@@ -1478,7 +1702,8 @@ STORE="$DATA/bus/bus.sqlite"
|
||||
BUS_UNIT="mosaic-bus@$BUSINESS.service"
|
||||
FROM="" TO="" ACT="" RS="" POST="" BACKUP="" TAKEN="" RESTORED="" STEPS="" COUNT="" UNCLEAN="" FAIL=""
|
||||
STORE_SCHEMA="" START_EPOCH="" START_MS=0 GATE="" GATE_WHY="" S_CHANGED=""
|
||||
P_COPY="" P_PM=0 P_PENDING=0 P_SCHEMA="" P_CLAUDE="" P_CONSOLE=() RUN_PM=0 RUN_PENDING=0 RUN_CONSOLE=() DROP_TESTER="" PROOF_PLAN="" PROOF_STEP="" PLAN_REFUSE=()
|
||||
P_COPY="" P_PM=0 P_PENDING=0 P_SCHEMA="" P_CLAUDE="" P_CONSOLE=() RUN_PM=0 RUN_PENDING=0 RUN_CONSOLE=() DROP_TESTER="" PROOF_PLAN="" PROOF_STEP="" SEND_PLAN="" SEND_STEP="" PLAN_REFUSE=()
|
||||
BASE_CONSOLE=() SERVE_FLAGS=() SSO_PLAN="" SSO_STEP="" SSO_DROP="" SSO_NO_TARGET=""
|
||||
R="" R_RC=0 R_EARLY=() R_BK="" R_HOST_PID="" R_CONSOLE_PID="" R_STARTED="" LIVE_CFG="" LIVE_BIZ=""
|
||||
case "$OP" in
|
||||
adopt) do_adopt ;;
|
||||
|
||||
Reference in New Issue
Block a user