Unclaimed files left in the canonical checkout. Inventory: canonical-cleanup-inventory.md (commit-as-evidence groups). detect-secrets scan clean on 2026-11-02: hits are sha256 pins, git object ids, test sentinels already in tracked tests and unit-file lines.
383 lines
29 KiB
Markdown
383 lines
29 KiB
Markdown
# Row 190 design: several Mosaic Stack instances on one host
|
|
|
|
Brief: `docs/plans/2026-10-20_multi-instance.md`, blob 4e8f1e052894 (commit 48ebee0ff, rev 2259). Line 16
|
|
now limits isolation to addressing on one uid and points to row 195 (190b). Gitea #1688. MVP T1 item 1 (mos-0974, DECISIONS
|
|
Q176-Q178). Owner Darkwing, reviewer Filbert, gate owner Sage. Base: da230ca48. Worktree:
|
|
`/mnt/storage/scratch/darkwing-r190`, branch `darkwing/row190`.
|
|
|
|
Mos ruled Q1 in mos-0986 (2026-10-20T18:53:48Z). O1 is approved with one added sentence, and R1 is ranked
|
|
required, as row 190b. See "Rulings" at the end. Revision 2 of this file adds that sentence and the section.
|
|
Revision 3 answers Filbert's review of revision 2 (B1, B2, N1-N6). "Review rounds" at the end lists each
|
|
change.
|
|
|
|
## Summary
|
|
|
|
- One carrier. Inside every process, the instance is the config file that `MOSAIC_CONFIG` names, or the
|
|
default config when it is unset. This is how the code works today. No package learns an instance name.
|
|
- `--instance NAME` exists only on the shell entry points that name units, ports or wrappers, or that
|
|
create a config. NAME maps to `~/.config/mosaic-NAME/config.json`. The default instance is `dev`, which
|
|
maps to today's `~/.config/mosaic-dev/config.json`. With no flag, everything is byte-identical to today.
|
|
- Units: `mosaic-bus-NAME@<business>`, `mosaic-webui-NAME`, `mosaic-discord-NAME@<binding>`. The default
|
|
keeps `mosaic-bus@`, `mosaic-webui` and `mosaic-discord@`.
|
|
- A data root is claimed by a marker file that bootstrap writes. A tool aimed at one instance refuses a data
|
|
root that carries another instance's marker, or no marker at all for a named instance.
|
|
- Copies stay shared. The pointer, activation log, backups, rollback sets and rehearsals already live under
|
|
`$DATA`, so they become per instance with no new code.
|
|
- One limit holds through all of this. Every instance runs as the same Unix user. That separates instances by
|
|
addressing: no Mosaic command aimed at one names a file, unit or port of another. It is not an OS
|
|
permission boundary. R1 covers what that means for Melanie's instance.
|
|
|
|
## Inventory
|
|
|
|
Every single-instance point I found in `scripts/` and `packages/*/src`, outside tests, at da230ca48. The grep
|
|
covered `.config/mosaic-dev`, `mosaic-dev/config.json`, `MOSAIC_CONFIG`, `.mosaic-dev`, `dataRoot`, the
|
|
unit names, 7330/7332/7333, `claude-session.env`, `BUSINESS=`, `COPIES` and `secrets`. About 55 files read
|
|
`dataRoot` from the loaded config. Those are already per instance and are not listed.
|
|
|
|
| # | Where | What is single-instance | Change |
|
|
|---|---|---|---|
|
|
| F1 | `scripts/mosaic-config.mjs:41-45, :183` | default config path and default `dataRoot` `~/.mosaic-dev` | `--instance NAME` on `bootstrap`, `validate`, `env`; new op `instance` (D1) |
|
|
| F2 | `scripts/bootstrap.sh` | runs `mosaic-config.mjs bootstrap` with no instance | passes `--instance NAME` through (D2) |
|
|
| F3 | `packages/cli/systemd/mosaic-bus.service.in:24` | fixed `EnvironmentFile=-%h/.config/mosaic-dev/secrets/claude-session.env`, no `MOSAIC_CONFIG` | two placeholders that render to today's bytes for the default (D3) |
|
|
| F4 | `scripts/bus-service.sh:25-26, :102-118, :177` | unit name `[email protected]`; busy and drop-in scans glob `mosaic-bus@*` | `--instance`; names and globs from D3 |
|
|
| F5 | `packages/webui/systemd/mosaic-webui.service.in`, `scripts/webui-service.sh:68-69, :85, :274-279` | one unit `mosaic-webui.service`; port default 7332; `bus_on_copy` globs `mosaic-bus@*` | `--instance`; a named instance must pass `--port`; port refusal (D4) |
|
|
| F6 | `packages/discord/systemd/[email protected]`, `scripts/discord-service.sh:20-21, :153` | one template; `status` reads `${XDG_CONFIG_HOME}/mosaic-dev/config.json` and ignores `MOSAIC_CONFIG` | `--instance`; `status` reads the instance's config |
|
|
| F7 | `scripts/stack-copy.sh:8, :95` | `BUSINESS=mosaic-stack`; `check` reads `mosaic-webui.service` and `$BIN/mosaic` | `check --instance`; `wrapper --instance` (D5) |
|
|
| F8 | `scripts/stack-release.sh:31, :416-425, :533-546, :607-628, :1478` | `CONSOLE_UNIT`, `mosaic-bus@` in capture, restore, busy check and drop-in scan; wrapper `$BIN/mosaic` | names from D3 and D5 |
|
|
| F9 | `scripts/stack-release.sh:72, :99, :1405-1414` | tester port refuses 7332 only; rehearse reserves 7332, 7333 and the pointer's port; `LIVE_CFG` default | reserved set from D4; `LIVE_CFG` follows `--instance` |
|
|
| F10 | `packages/conversation/src/guard.mjs:47-55, :111` | the live-session guard protects `~/.mosaic-dev` and the default config's `dataRoot` only | also protects each named instance's config dir, `~/.mosaic-NAME` and its config's `dataRoot`, for names that pass the D1 rule (D7) |
|
|
| F11 | `packages/business/src/business.mjs:26`, `packages/seat/src/seat.mjs:47`, `packages/discord/src/binding.mjs:54` | `MOSAIC_CONFIG` or the default | none: already follow the carrier |
|
|
| F12 | `packages/control-board/src/scan.mjs:30` | default config only, `--config PATH` overrides | none: the control board is dev tooling, and it already takes `--config` |
|
|
| F13 | `scripts/reset.sh:2-4` | refuses any target but `/home/jwoltje/.mosaic-dev` | none: reset stays default-only, and refuses a named instance |
|
|
| F14 | `scripts/land-work.sh:99`, `scripts/conductor-apply.sh` | the conductor pipeline's config dir | none: dev pipeline, not an instance |
|
|
| F15 | `packages/business/src/business.mjs:23-27` (configDir) | the business files and secrets sit beside the config | none: per instance once the config is |
|
|
| F16 | copies `${XDG_DATA_HOME}/mosaic-stack/copies` in four scripts | one copies directory | shared on purpose (D5) |
|
|
|
|
One naming collision to know about. "Instance" already means a role instance in `packages/business`
|
|
(`resolveInstance`, `b.roles` keys). That word stays out of the packages. The new flag lives only in shell
|
|
scripts and `mosaic-config.mjs`. The docs say "stack instance" wherever a reader could confuse the two.
|
|
|
|
## D1. Instance identity
|
|
|
|
1. Name rule: `^[a-z][a-z0-9]{1,15}$`. No hyphen. With a hyphen, systemd's prefix drop-ins
|
|
(`mosaic-bus-.service.d` and so on) would gain a level per hyphen, and the guard scans would no longer be
|
|
a fixed list. A hyphen would also let one name be a prefix of another's unit.
|
|
2. Mapping: NAME's config is `<home>/.config/mosaic-NAME/config.json`, where `<home>` is `os.homedir()` /
|
|
`$HOME`, as `mosaic-config.mjs` uses today, not `XDG_CONFIG_HOME`. `dev` is the default instance, so
|
|
`--instance dev` is the same as no flag, byte for byte. The mapping never produces `~/.config/mosaic`
|
|
(v1 and scripts/foundation use that).
|
|
3. Selection, in every shell entry point that takes it:
|
|
|
|
| `--instance` | `MOSAIC_CONFIG` | Result |
|
|
|---|---|---|
|
|
| absent | absent | default, exactly today |
|
|
| absent | set | that config, with default unit and wrapper names: today's behavior, and what every existing test does |
|
|
| NAME | absent | NAME's config, and the entry point exports `MOSAIC_CONFIG` to everything it runs |
|
|
| NAME | set | allowed only when both resolve (realpath) to the same file; otherwise exit 4, `instance-config-mismatch` |
|
|
|
|
4. Node code takes no flag. `scripts/mosaic`, the bus host, the Console, the Discord connector, the task
|
|
runner and the human CLI follow `MOSAIC_CONFIG` as they do now. A human reaches a named instance through
|
|
its wrapper `mosaic-NAME` (D5) or by setting `MOSAIC_CONFIG`.
|
|
5. The mapping lives in one place: `mosaic-config.mjs instance [--instance NAME]` prints shell-safe exports.
|
|
They are `MOSAIC_INSTANCE`, `MOSAIC_CONFIG`, `MOSAIC_CONFIG_DIR` and `MOSAIC_UNIT_SUFFIX` (empty for the
|
|
default, `-NAME` otherwise). A sourced `scripts/instance.sh` turns those into `BUS_TEMPLATE`, `BUS_GLOB`,
|
|
`CONSOLE_UNIT`, `DISCORD_TEMPLATE`, `WRAPPER_NAME` and `SESSION_ENV`. Every entry point uses that file,
|
|
and row 191 can too.
|
|
6. Refusals: a bad name exits 4 (`instance-name-invalid`). For a named instance, a missing config exits 3
|
|
with the bootstrap hint, as today. Data-root claims are D2.
|
|
|
|
## D2. Config canon and bootstrap
|
|
|
|
Bootstrap: `scripts/bootstrap.sh --instance NAME` runs `mosaic-config.mjs bootstrap --instance NAME`. It:
|
|
|
|
1. creates `~/.config/mosaic-NAME/` (mode 0700) and the config with `wx`, as today. It never overwrites; an
|
|
existing config is validated and left alone;
|
|
2. writes the default `dataRoot` `~/.mosaic-NAME`. The operator may change it before first use. The default
|
|
instance keeps `~/.mosaic-dev`;
|
|
3. claims the data root. It creates the root if absent (0700) and writes `<dataRoot>/instance.json`,
|
|
`{"instanceVersion":1,"name":"NAME"}`, with `wx`, mode 0644. It refuses if the root exists and is not
|
|
empty and holds no marker of its own name. An existing config whose root lacks the marker is a refusal,
|
|
not a repair.
|
|
|
|
The claim check runs in `scripts/instance.sh`. Every entry point that takes `--instance` sources that file
|
|
on every run, flag or no flag, so the check covers all four rows of the D1 table. It runs once the config
|
|
has loaded, before the entry point reads or writes anything else. A missing config keeps today's refusal.
|
|
`bootstrap` is the one exception, because it makes the claim (step 3 above).
|
|
|
|
- With `--instance NAME` (D1 rows 3 and 4), `<dataRoot>/instance.json` must be a regular, non-symlink file
|
|
naming NAME. Otherwise exit 3, `instance-root-unclaimed` or `instance-root-mismatch`.
|
|
- With no flag (D1 rows 1 and 2), the selection takes the default's unit and wrapper names, so the marker
|
|
must be absent or name `dev`. Otherwise exit 3, `instance-root-mismatch`, naming the marker's instance and
|
|
the `--instance` flag to use. Row 2 is where this matters:
|
|
`MOSAIC_CONFIG=~/.config/mosaic-a/config.json scripts/stack-release.sh activate ID` with no flag would
|
|
otherwise drive `a`'s data root through the default's units and `$BIN/mosaic`. With the check, it refuses
|
|
before any unit is read or called.
|
|
- No marker exists anywhere today, and the existing tests' scratch configs carry none, so rows 1 and 2 behave
|
|
exactly as they do now.
|
|
|
|
This blocks two configs from sharing one data root through the shell tools. It does not stop a hand-run
|
|
`MOSAIC_CONFIG=... scripts/mosaic bus start` from a copy, but the bus store's writer lock
|
|
(`<dataRoot>/bus/writer.lock`) still refuses a second broker on one root.
|
|
|
|
### Q1, for Mos: the AGENTS.md invariant 2 wording
|
|
|
|
Today:
|
|
|
|
> 2. **Configuration**: `~/.config/mosaic-dev/config.json` is the sole system config — created only by
|
|
> `scripts/bootstrap.sh`, never overwritten, fail-closed on any problem. Repo-scoped role authority lives in
|
|
> `roles/*.json` (versioned, reviewed commits only).
|
|
|
|
Proposed (O1, my recommendation):
|
|
|
|
> 2. **Configuration**: each stack instance has exactly one system config. The default instance's is
|
|
> `~/.config/mosaic-dev/config.json`. Instance NAME's is `~/.config/mosaic-NAME/config.json`, and its data
|
|
> root carries a marker that names it. `~` is the home of the OS user that runs the instance. A process reads only the config `MOSAIC_CONFIG` names, or the
|
|
> default's when that is unset. Configs are created only by `scripts/bootstrap.sh [--instance NAME]`, never
|
|
> overwritten, fail-closed on any problem. Repo-scoped role authority lives in `roles/*.json` (versioned,
|
|
> reviewed commits only).
|
|
|
|
The "Data map (canon)" line in AGENTS.md changes to match: "`~/.config/mosaic-dev/config.json`, or
|
|
`~/.config/mosaic-NAME/config.json` per named instance: system config (user-authored; never auto-written)".
|
|
|
|
Alternatives:
|
|
|
|
- O2: named instances under the default's directory, `~/.config/mosaic-dev/instances/NAME/config.json`. That
|
|
keeps one config root, but the default's config dir would then contain every other instance's businesses
|
|
and secrets. Anything allowed into the default's config dir reaches all of them. I recommend against it.
|
|
- O3: no canon change, `MOSAIC_CONFIG` only, no names. Unit names, ports and wrappers then have nothing to
|
|
key on, so the brief's units point can't be met.
|
|
|
|
AGENTS.md is Sage's file. The row edits only those two lines, and only with the wording Mos rules.
|
|
|
|
## D3. Units
|
|
|
|
| | Default (unchanged) | Instance NAME |
|
|
|---|---|---|
|
|
| bus template | `[email protected]`, `%i` = business | `[email protected]`, `%i` = business |
|
|
| Console | `mosaic-webui.service` | `mosaic-webui-NAME.service` |
|
|
| Discord template | `[email protected]`, `%i` = binding | `[email protected]`, `%i` = binding |
|
|
| wrapper | `$BIN/mosaic` | `$BIN/mosaic-NAME` |
|
|
|
|
The templates gain two placeholder lines:
|
|
|
|
- `@CONFIG_ENV@`. For the default, render deletes the line. For NAME, it becomes
|
|
`Environment=MOSAIC_CONFIG=<abs config path>`, held to the plain character set the scripts already
|
|
enforce.
|
|
- In the bus template, `EnvironmentFile=-@SESSION_ENV@`. For the default this renders as
|
|
`%h/.config/mosaic-dev/secrets/claude-session.env`, today's text. For NAME it is
|
|
`<abs config dir>/secrets/claude-session.env`.
|
|
|
|
Default renders therefore stay byte-identical to the base template's. A test (T-I8) compares them. The
|
|
Console unit's comment "The unit sets no MOSAIC_* ... variable" gains "except `MOSAIC_CONFIG` for a named
|
|
instance". The Description lines get ` [NAME]` for named instances only.
|
|
|
|
The scans follow the names:
|
|
|
|
- Busy checks glob `mosaic-bus@*.service` for the default, as today, and `mosaic-bus-NAME@*.service` for
|
|
NAME. Neither glob matches the other's units, so a running bus in one instance never blocks or reaches
|
|
the other.
|
|
- The drop-in scan (`bus-service.sh:118`, `stack-release.sh:628`) covers bus units only, today and in this
|
|
row. For a named instance it adds the bus directories that systemd applies to `mosaic-bus-NAME@<business>`:
|
|
`mosaic-bus-NAME@<business>.service.d`, `[email protected]` and the shared prefix
|
|
`mosaic-bus-.service.d`. The existing `mosaic-.service.d` and `service.d` scans stay. Since
|
|
`mosaic-bus-.service.d` applies to every named bus, each named instance's scan refuses an ExecStart
|
|
override there.
|
|
- Console and Discord drop-in scans stay out of scope, as they are today: no script scans
|
|
`mosaic-webui*.service.d` or `mosaic-discord*.service.d`, and this row adds no scan for them. That includes
|
|
the shared prefixes `mosaic-webui-.service.d` and `mosaic-discord-.service.d`, which systemd applies to
|
|
every named Console and Discord unit. I'll record it as a follow-up.
|
|
|
|
Shared prefix directories are never instance state. The directories systemd applies to more than one
|
|
instance are `mosaic-bus-.service.d`, `mosaic-webui-.service.d`, `mosaic-discord-.service.d`,
|
|
`mosaic-.service.d` and `service.d`. Capture and restore never save or rewrite any of them. For NAME, the
|
|
rollback set holds `[email protected]`, `mosaic-webui-NAME.service`, `[email protected]`,
|
|
`mosaic-bus-NAME@<business>.service.d`, `mosaic-webui-NAME.service.d` and `$BIN/mosaic-NAME`, and nothing
|
|
else. The default's set stays as it is (`stack-release.sh:416-425`). None of its directories applies to a
|
|
named unit: systemd derives `[email protected]`'s prefixes by cutting at hyphens, which never yields
|
|
`[email protected]` or `mosaic-webui.service.d`. So one instance's rollback can't rewrite a directory
|
|
another instance's units read.
|
|
|
|
The human-proof rule still holds per instance. Jason or Mos starts each Console. Nothing here adds a start
|
|
verb.
|
|
|
|
## D4. Ports
|
|
|
|
- A named instance's Console has no default port. `webui-service.sh install|render --instance NAME` without
|
|
`--port` exits 4. The default keeps 7332.
|
|
- Reserved for named instances: 7330 (the operator's bare Console), 7332 (the tunnel) and 7333 (the
|
|
default's tester). The existing rule that no tester takes 7332 stays for every instance.
|
|
- Collision refusal. `webui-service.sh install` and `stack-release.sh activate` read every installed Console
|
|
unit in `UNIT_DIR`: `mosaic-webui.service` (no `--port` there means 7332) and `mosaic-webui-*.service`.
|
|
They take each unit's `--port` and `--tester-port`. If this instance's port or tester port equals any of
|
|
them, the run exits 1 or 3 with `port-claimed`, naming the unit that holds the port.
|
|
`stack-release.sh rehearse` adds those ports to its reserved list.
|
|
- Unit files are host configuration, not another instance's state, so reading them keeps the D7 rule that
|
|
each instance reads only its own config and data root. No instance reads another's pointer.
|
|
- The claim covers installed units only. An instance that is bootstrapped but has no Console unit yet holds no
|
|
port, so a second instance can take the port first. The loser's install then refuses `port-claimed`, which
|
|
is safe. Nothing reserves a port at bootstrap.
|
|
- A port another process already holds still fails at bind, as today (serve exits 2).
|
|
|
|
## D5. Copies, pointer and wrapper
|
|
|
|
- Copies stay shared, one `${XDG_DATA_HOME}/mosaic-stack/copies`. They are read-only trees named by full
|
|
commit id, and nothing prunes them (decision 96 condition 4). Any future prune must read every instance's
|
|
pointer and log. This row doesn't add one.
|
|
- `stack-release.sh` takes `--instance NAME`. Its `DATA`, `STATE`, `POINTER`, `LOG`, backups, rollback sets,
|
|
post-attempt directories and rehearsals all come from the loaded config's `dataRoot` (`:1473-1477`). They
|
|
are per instance once the config is. `flags_drift` reads the pointer and this instance's unit only. Capture
|
|
and restore (`:416-425`, `:533-552`) use the D3 names and `$BIN/$WRAPPER_NAME`.
|
|
- `stack-copy.sh check --instance NAME` checks `mosaic-bus-NAME@<business>`, `mosaic-webui-NAME` and
|
|
`$BIN/mosaic-NAME`. `--business` stays required for a named instance. `BUSINESS=mosaic-stack` stays the
|
|
default's default.
|
|
- The named-instance wrapper. `stack-copy.sh wrapper ID --instance NAME` writes `$BIN/mosaic-NAME` through
|
|
a temp file and a rename. Line 2 is a marker, `# mosaic-cli wrapper, instance NAME (row 190)`. The body
|
|
sets `MOSAIC_CONFIG` to NAME's config, then runs `exec "<copy>/scripts/mosaic" "$@"`. It has no Q81 lease
|
|
line: that probe belongs to the default. It creates the file when absent, replaces a file with its own
|
|
marker, and refuses any other file. The default wrapper code is unchanged.
|
|
- Q175 C1 applies per instance. A named instance's pointer can roll back to a copy from before Q175, and that
|
|
copy refuses a config with a `console` key, the same trap the default has. Nothing new handles it here.
|
|
|
|
## D6. Secrets
|
|
|
|
- The Claude session env file is `<configDir>/secrets/claude-session.env`, set by the bus unit (D3). The
|
|
default's stays `%h/.config/mosaic-dev/secrets/claude-session.env`.
|
|
- The secrets slot root is `<configDir>/secrets/mosaic-stack/`, the same rule Q175 uses. Business credential
|
|
files are absolute paths in the business file, which lives in `<configDir>/businesses/`. The operator puts
|
|
each instance's tokens under its own config dir.
|
|
- Nothing in this row reads, creates or lists a secret file. Bootstrap creates `<configDir>` at 0700 and no
|
|
`secrets/` directory.
|
|
|
|
## D7. Isolation proof (tests)
|
|
|
|
All tests use scratch roots under `TMPDIR` and the stand-in `systemctl`, `journalctl` and `systemd-analyze`
|
|
from `packages/cli/tests/stack-release.test.mjs`. `HOME` is a scratch directory, so `~/.config/mosaic-*`
|
|
resolves inside it. None of them touches `~/.mosaic-dev`, `~/.config/mosaic-dev`, a live unit or a slot
|
|
file. New tests go in `stack-release.test.mjs` (T55, so they share `mk`), plus a new
|
|
`packages/cli/tests/instances.test.mjs` for the config and bootstrap cases and the two real hosts.
|
|
|
|
T-I5 binds two bus sockets under the scratch roots. The run keeps TMPDIR under `~/darkwing-scratch`, within
|
|
the 107-byte socket path limit (the row 177 lesson), and BUILD.md states the scratch path's length.
|
|
|
|
The guard (F10) is the one stated exception to "each instance reads only its own config and data root". It
|
|
reads, and never writes, `dataRoot` from each `~/.config/mosaic-NAME/config.json` whose NAME passes the D1
|
|
rule, so that a session can't write into another instance's root. The glob `mosaic-*` is filtered by the
|
|
name rule. On this host it matches only `mosaic-dev`: `mosaic`, `mosaic.bak` and `mosaic.bak1` don't match.
|
|
An unreadable config adds nothing, as at `guard.mjs:49-54`. So, as with `.mosaic-dev` at `:111`, the guard
|
|
also protects `~/.mosaic-NAME` and `~/.config/mosaic-NAME/` for each name it finds, whatever that config
|
|
says. A broken config for `b` can't drop `b`'s default root from the guard.
|
|
|
|
| Test | Shows |
|
|
|---|---|
|
|
| T-I1 bootstrap | `bootstrap --instance a` and `--instance b` create `mosaic-a` and `mosaic-b` configs, roots and markers. A second bootstrap changes no byte. A non-empty unmarked root refuses, including an existing non-empty `~/.mosaic-worktrees` under `--instance worktrees` (the name passes the rule, so the claim is what stops it). Names `A`, `a-b`, `x`, a 17-character name and `../x` exit 4. `--instance dev` writes exactly what no flag writes |
|
|
| T-I2 selection | the four rows of the D1 table, including the mismatch refusal and the realpath case that agrees |
|
|
| T-I3 render | bus, Console and Discord renders for `a` and `b` carry their own `MOSAIC_CONFIG`, `EnvironmentFile` and unit names, and no other instance's path |
|
|
| T-I4 ports | `a --port 7340`, then `b --port 7340`, refuses `port-claimed`. So do `b --tester-port 7340`, `b` with no `--port`, and `b --port 7332`. `b --port 7341` passes |
|
|
| T-I5 side by side | two real bus hosts (`scripts/mosaic bus start --no-launch`) from the worktree, one per scratch config, both up at once. Each `bus status` answers from its own socket under its own root |
|
|
| T-I6 no cross-reach | activate and rollback on `a`, with `b` fully installed: `b`'s units, drop-ins, wrapper, pointer, log, store and backups are byte-identical before and after, and so are the shared prefix directories `mosaic-bus-.service.d`, `mosaic-webui-.service.d` and `mosaic-discord-.service.d`, each seeded with a `.conf` file. `a`'s rollback set holds none of them. `calls.log` names no `b` unit. An inotify read watch (the row 187 T53 watch) on `b`'s config dir and data root counts zero events. Then the same with `a` and `b` swapped |
|
|
| T-I7 claims | `a`'s config pointing at `b`'s root refuses `instance-root-mismatch`. A named root with no marker refuses `instance-root-unclaimed`. A default config whose root carries `a`'s marker refuses. D1 row 2: `MOSAIC_CONFIG=<a's config> stack-release.sh activate C1` and `bus-service.sh install`, both with no flag, refuse `instance-root-mismatch` with exit 3. `calls.log` names no unit, and no unit, wrapper, pointer or store changes. A scratch config with no marker, run with no flag, passes as today |
|
|
| T-I8 default unchanged | default renders of all three units equal the da230ca48 templates' renders byte for byte. `stack-release.sh --plan`, `status` and `stack-copy.sh check` print the same text from the base script and the candidate in one fixture. The existing stack-release tests pass unchanged |
|
|
| T-I9 busy scoping | `b`'s bus active does not refuse `a`'s activate, and `a`'s own bus active does |
|
|
| T-I10 guard | `LiveSessionGuard` refuses a path inside a named instance's config dir or data root. With `b`'s config corrupt, it still refuses a path in `~/.mosaic-b` and in `~/.config/mosaic-b/`. A directory `~/.config/mosaic.bak` adds nothing |
|
|
|
|
### Mutants
|
|
|
|
Each one must fail at least one test above. They are run against the candidate, the script restored and its
|
|
sha256 checked after each, as in rows 187 and 188.
|
|
|
|
| # | Change | Expected killer |
|
|
|---|---|---|
|
|
| M1 | `instance.sh` ignores NAME and returns the default names | T-I3, T-I6 |
|
|
| M2 | the port-claimed check skipped | T-I4 |
|
|
| M3 | the marker check skipped | T-I7 |
|
|
| M4 | the bus render drops the `MOSAIC_CONFIG` line for a named instance | T-I3 |
|
|
| M5 | `SESSION_ENV` always the default path | T-I3 |
|
|
| M6 | the named busy glob widened to `mosaic-bus*@*.service` | T-I9 |
|
|
| M7 | the default render keeps an empty line where `@CONFIG_ENV@` was | T-I8 |
|
|
| M8 | capture saves `$BIN/mosaic` for a named instance | T-I6 |
|
|
| M9 | `--instance` and a different `MOSAIC_CONFIG` accepted | T-I2 |
|
|
| M10 | the name pattern accepts a hyphen | T-I1 |
|
|
| M11 | `bootstrap --instance` opens the config without `wx` | T-I1 |
|
|
| M12 | the guard drops the named instances' roots | T-I10 |
|
|
| M13 | the claim check runs only when `--instance` is given | T-I7 (D1 row 2 case) |
|
|
| M14 | capture and put_back include `mosaic-bus-.service.d` for a named instance | T-I6 |
|
|
| M15 | the guard drops the unconditional `~/.mosaic-NAME` and keeps only the config's `dataRoot` | T-I10 (corrupt-config case) |
|
|
|
|
## Scope and boundaries
|
|
|
|
- In the row: F1-F10 code, the three templates, `scripts/instance.sh`, the tests, the package READMEs, and the
|
|
two AGENTS.md lines in Mos's ruled wording (O1 plus the `~` sentence), nothing else in that file.
|
|
`docs/TOOLS.md` gets the `--instance` text, written by me as part of the manifest (sage-1335).
|
|
- Not in the row:
|
|
- standing up a real second instance (a later Q173 window, with a backup first and a STATE report after);
|
|
- any tunnel or Traefik exposure (Jason, after Q175 SSO);
|
|
- SSO, users and RBAC (T1 items 2 and 3);
|
|
- the kill switch (row 191).
|
|
- No re-pin needed. The default's units, paths and ports don't change, and the live copy keeps running its
|
|
own templates until a later activate. At that activate the default renders are byte-identical (T-I8).
|
|
|
|
## Risks
|
|
|
|
- R1. Same Unix user. Every instance runs as jwoltje. A role session or a hand-run process in one instance
|
|
can read another's files if it names them. This row only makes sure no Mosaic tool aimed at one instance
|
|
names another's files, units or ports. For Melanie's instance, the real boundary is SSO plus RBAC (T1
|
|
items 2-3) for the Console. Mos ranked the stronger separation as required (mos-0986): row 190b, chained on
|
|
this row, puts Melanie's hub under its own Linux user. This row stays on one uid.
|
|
- R2. The marker check guards the shell tools only (D2). A hand-run `MOSAIC_CONFIG=... scripts/mosaic` from
|
|
a copy skips it. The writer lock still stops two brokers on one root. Filbert's N5 offered a fix: the
|
|
shared loader could check the marker whenever the config path is a canonical
|
|
`~/.config/mosaic-NAME/config.json`. I'm leaving it out. Q175 C1 showed the cost of a shared-loader change,
|
|
since every old copy refuses what it doesn't know, and a rollback lands on old copies. Row 190b, with its
|
|
own uid, is the real boundary for the hub.
|
|
- R3. `stack-release.sh` is 1490 lines with many name literals. M1 and M8 plus T-I6 are there to catch a
|
|
missed literal. Before in-review I'll grep the candidate for every bare `mosaic-bus@`, `mosaic-webui` and
|
|
`$BIN/mosaic` and list each remaining one with its reason.
|
|
|
|
## For row 191 (Rocko)
|
|
|
|
The resolution contract to build on:
|
|
|
|
1. Inside processes, `MOSAIC_CONFIG`, or the default when unset, through the existing loaders.
|
|
2. All state goes under that config's `dataRoot`.
|
|
3. Unit names come from `scripts/instance.sh`: `BUS_TEMPLATE`, `BUS_GLOB`, `CONSOLE_UNIT`,
|
|
`DISCORD_TEMPLATE` and `WRAPPER_NAME`. Never from literals.
|
|
4. Shell entry points take `--instance NAME`. That name may change if Mos rules O2 or O3 on Q1.
|
|
|
|
## Rulings
|
|
|
|
mos-0986 (2026-10-20T18:53:48Z), on revision 1 (sha256 3a6479764283...):
|
|
|
|
- Q1: O1 is approved, with one sentence added to the invariant: "`~` is the home of the OS user that runs the
|
|
instance." The proposed wording in D2 now carries it. The D1 mapping already uses `os.homedir()`, so an
|
|
instance under another uid needs no more code in this row. The roles line and the Data map line change as
|
|
proposed. O2 and O3 are rejected.
|
|
- R1: Mos ranks it required, not a follow-up (federation review B5, duckworth-0001; mos-0984 and mos-0985). A
|
|
separate uid per instance is a hard precondition of federation R8 and of Case 2 row C2-13. This row lands
|
|
as designed, on one uid, so scratch two-instance tests run without root. Row 190b (queue row 195, Gitea #1693, reviewer Duckworth), chained on this one and
|
|
owned by Darkwing, designs Melanie's hub under its own Linux user.
|
|
|
|
## Review rounds
|
|
|
|
Filbert, on revision 2 (sha256 5493c9df3ec0..., 2026-10-20T18:56:34Z): CHANGES, B1 and B2. Revision 3:
|
|
|
|
- B1. D2 now says the claim check runs on every row of the D1 table, flag or no flag, with `bootstrap` the
|
|
one exception. With no flag, the marker must be absent or name `dev`. T-I7 gains the D1 row 2 case
|
|
(`a`'s config with no flag, through `stack-release.sh activate` and `bus-service.sh install`): exit 3
|
|
`instance-root-mismatch`, and `calls.log` names no unit. A marker-less scratch config still passes. M13
|
|
(the check runs only with `--instance`) dies on that case.
|
|
- B2 (1). D3 now says the drop-in scan covers bus units only, today and in this row. Console and Discord
|
|
drop-in scans stay out of scope, including their shared prefixes, as a recorded follow-up.
|
|
- B2 (2). D3 lists the shared prefix directories and says capture and restore never save or rewrite them. It
|
|
also gives the exact rollback set for NAME. T-I6 seeds `mosaic-bus-.service.d`, `mosaic-webui-.service.d`
|
|
and `mosaic-discord-.service.d` and checks them byte for byte. M14 (capture includes
|
|
`mosaic-bus-.service.d`) dies there.
|
|
- N1. D7 states the guard as the one read-only exception. The glob is filtered by the name rule. `~/.mosaic-NAME`
|
|
and `~/.config/mosaic-NAME/` are protected whatever the config says. T-I10 gains the corrupt-config and
|
|
`mosaic.bak` cases, and M15 checks the unconditional root.
|
|
- N2. D4 says installed units are the only claim.
|
|
- N3. D7 states the TMPDIR rule, and BUILD.md will give the path length.
|
|
- N4. T-I1 gains the `~/.mosaic-worktrees` case.
|
|
- N5. R2 records the loader option and why I'm leaving it out.
|
|
- N6. D5 has the one line on Q175 C1 per instance.
|
|
- N7 is Sage's brief line. Nothing changes here.
|