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.
29 KiB
Row 190 design: several Mosaic Stack instances on one host
Brief: docs/plans/2026-10-20_multi-instance.md, blob 4e8f1e0528 (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_CONFIGnames, or the default config when it is unset. This is how the code works today. No package learns an instance name. --instance NAMEexists 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 isdev, 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 keepsmosaic-bus@,mosaic-webuiandmosaic-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
-
Name rule:
^[a-z][a-z0-9]{1,15}$. No hyphen. With a hyphen, systemd's prefix drop-ins (mosaic-bus-.service.dand 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. -
Mapping: NAME's config is
<home>/.config/mosaic-NAME/config.json, where<home>isos.homedir()/$HOME, asmosaic-config.mjsuses today, notXDG_CONFIG_HOME.devis the default instance, so--instance devis the same as no flag, byte for byte. The mapping never produces~/.config/mosaic(v1 and scripts/foundation use that). -
Selection, in every shell entry point that takes it:
--instanceMOSAIC_CONFIGResult 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_CONFIGto everything it runsNAME set allowed only when both resolve (realpath) to the same file; otherwise exit 4, instance-config-mismatch -
Node code takes no flag.
scripts/mosaic, the bus host, the Console, the Discord connector, the task runner and the human CLI followMOSAIC_CONFIGas they do now. A human reaches a named instance through its wrappermosaic-NAME(D5) or by settingMOSAIC_CONFIG. -
The mapping lives in one place:
mosaic-config.mjs instance [--instance NAME]prints shell-safe exports. They areMOSAIC_INSTANCE,MOSAIC_CONFIG,MOSAIC_CONFIG_DIRandMOSAIC_UNIT_SUFFIX(empty for the default,-NAMEotherwise). A sourcedscripts/instance.shturns those intoBUS_TEMPLATE,BUS_GLOB,CONSOLE_UNIT,DISCORD_TEMPLATE,WRAPPER_NAMEandSESSION_ENV. Every entry point uses that file, and row 191 can too. -
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:
- creates
~/.config/mosaic-NAME/(mode 0700) and the config withwx, as today. It never overwrites; an existing config is validated and left alone; - writes the default
dataRoot~/.mosaic-NAME. The operator may change it before first use. The default instance keeps~/.mosaic-dev; - claims the data root. It creates the root if absent (0700) and writes
<dataRoot>/instance.json,{"instanceVersion":1,"name":"NAME"}, withwx, 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.jsonmust be a regular, non-symlink file naming NAME. Otherwise exit 3,instance-root-unclaimedorinstance-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--instanceflag to use. Row 2 is where this matters:MOSAIC_CONFIG=~/.config/mosaic-a/config.json scripts/stack-release.sh activate IDwith no flag would otherwise drivea'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:
- Configuration:
~/.config/mosaic-dev/config.jsonis the sole system config — created only byscripts/bootstrap.sh, never overwritten, fail-closed on any problem. Repo-scoped role authority lives inroles/*.json(versioned, reviewed commits only).
Proposed (O1, my recommendation):
- 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 configMOSAIC_CONFIGnames, or the default's when that is unset. Configs are created only byscripts/bootstrap.sh [--instance NAME], never overwritten, fail-closed on any problem. Repo-scoped role authority lives inroles/*.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_CONFIGonly, 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 becomesEnvironment=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@*.servicefor the default, as today, andmosaic-bus-NAME@*.servicefor 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 tomosaic-bus-NAME@<business>:mosaic-bus-NAME@<business>.service.d,[email protected]and the shared prefixmosaic-bus-.service.d. The existingmosaic-.service.dandservice.dscans stay. Sincemosaic-bus-.service.dapplies 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.dormosaic-discord*.service.d, and this row adds no scan for them. That includes the shared prefixesmosaic-webui-.service.dandmosaic-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 NAMEwithout--portexits 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 installandstack-release.sh activateread every installed Console unit inUNIT_DIR:mosaic-webui.service(no--portthere means 7332) andmosaic-webui-*.service. They take each unit's--portand--tester-port. If this instance's port or tester port equals any of them, the run exits 1 or 3 withport-claimed, naming the unit that holds the port.stack-release.sh rehearseadds 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.shtakes--instance NAME. ItsDATA,STATE,POINTER,LOG, backups, rollback sets, post-attempt directories and rehearsals all come from the loaded config'sdataRoot(:1473-1477). They are per instance once the config is.flags_driftreads 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 NAMEchecksmosaic-bus-NAME@<business>,mosaic-webui-NAMEand$BIN/mosaic-NAME.--businessstays required for a named instance.BUSINESS=mosaic-stackstays the default's default.- The named-instance wrapper.
stack-copy.sh wrapper ID --instance NAMEwrites$BIN/mosaic-NAMEthrough a temp file and a rename. Line 2 is a marker,# mosaic-cli wrapper, instance NAME (row 190). The body setsMOSAIC_CONFIGto NAME's config, then runsexec "<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
consolekey, 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 nosecrets/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.mdgets the--instancetext, 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/mosaicfrom 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.shis 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 baremosaic-bus@,mosaic-webuiand$BIN/mosaicand list each remaining one with its reason.
For row 191 (Rocko)
The resolution contract to build on:
- Inside processes,
MOSAIC_CONFIG, or the default when unset, through the existing loaders. - All state goes under that config's
dataRoot. - Unit names come from
scripts/instance.sh:BUS_TEMPLATE,BUS_GLOB,CONSOLE_UNIT,DISCORD_TEMPLATEandWRAPPER_NAME. Never from literals. - 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 usesos.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
bootstrapthe one exception. With no flag, the marker must be absent or namedev. T-I7 gains the D1 row 2 case (a's config with no flag, throughstack-release.sh activateandbus-service.sh install): exit 3instance-root-mismatch, andcalls.lognames 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.dandmosaic-discord-.service.dand checks them byte for byte. M14 (capture includesmosaic-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-NAMEand~/.config/mosaic-NAME/are protected whatever the config says. T-I10 gains the corrupt-config andmosaic.bakcases, 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-worktreescase. - 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.