Files
stack/docs/guides/host-run-sheet.md
jason.woltje 13c75b699f cli, organization: org config and org instances commands, old business spelling kept (rename r2 stage B2)
mosaic org config validate|resolve runs runConfigCommand
(packages/organization/src/config-command.mjs), the callable service the
old mosaic business validate|resolve executable now calls too. The service
returns the exit code, never exits the process and does nothing on import.
The spelling sets only the stderr prefix and the usage text; status and
stdout are byte-identical, and the v1 output keeps the business key.

mosaic org instances|instance|veto takes the same arguments as
mosaic business instances|instance|veto and runs the same proof-bearing
instances command. It is dispatched before orgCommand, so it never builds
the dev-local-owner context. mosaic org validate FILE is unchanged.

Help, the UI labels for agent actions and instance reasons, and the
current docs now use the org spelling and the packages/organization paths.
Two webui test regexes follow the label text. packages/cli/tests/
rename-b-commands.test.mjs compares the two spellings golden for golden
on status, stdout, stderr and authorization outcomes, and covers a draft
file named like an installed ID.
2026-11-09 12:00:29 -06:00

507 lines
28 KiB
Markdown

# Host run sheet: preparing a host for the stack
ops-01 runs this on a fresh host before install (I2), and once per host for enrollment. Jason places the
credentials in section 9. Brief: `docs/plans/2026-10-20_installer-rows.md`, I3 (row 233, #1735). The
host design is Darkwing's r6 (`agents/darkwing/work/queue-195/DESIGN.md`, section 2) as the brief
changes it.
The hosts are .53 (profile `factory`) and .54 (profile `library`). Both get every section unless a
section says otherwise. Every command runs as root unless it starts with `sudo -u mosaic`.
Each item names the `scripts/case2-precheck.sh` line that verifies it. Install's precheck runs as the
stack's user with `--business ID --profile factory|library`. Where no precheck line checks an item, the
item says "precheck line missing", and Gosalyn adds the line to I2. Today's precheck (C2-13a) has these
gaps; C2-13b adds the library lines (`library.sync`, persona modes, no Discord binding).
## 1. The stack's user and its home
```sh
useradd --system --user-group --home-dir /srv/mosaic --create-home --shell /usr/sbin/nologin mosaic
chmod 0700 /srv/mosaic
install -d -o root -g root -m 0755 /etc/mosaic-stack
install -d -o root -g root -m 0750 /var/log/mosaic-stack
```
`mosaic` gets no sudo rule and no supplementary group. Check with `sudo -l -U mosaic` (expect "not allowed
to run sudo") and `id -G mosaic` (one number).
Precheck lines:
- `ok HOME is /srv/mosaic`
- `ok sudo -n true fails as mosaic`
- `ok id -G prints one group (<gid>)`
- `/srv/mosaic` at 0700: precheck line missing.
## 2. node, pinned under /opt
The pin is the version and sha256 the window names. `<v>` below is that version, for example `v24.x.y`.
```sh
install -d -o root -g root -m 0755 /etc/mosaic-stack/node-keys /opt/mosaic-stack
cd "$(mktemp -d)"
curl -fsSLO https://nodejs.org/dist/<v>/node-<v>-linux-x64.tar.xz
curl -fsSLO https://nodejs.org/dist/<v>/SHASUMS256.txt
curl -fsSLO https://nodejs.org/dist/<v>/SHASUMS256.txt.sig
gpg --no-default-keyring --keyring /etc/mosaic-stack/node-keys/pubring.kbx --verify SHASUMS256.txt.sig SHASUMS256.txt
grep " node-<v>-linux-x64.tar.xz\$" SHASUMS256.txt | sha256sum -c -
echo "<pinned sha256> node-<v>-linux-x64.tar.xz" | sha256sum -c -
tar -xJf node-<v>-linux-x64.tar.xz --no-same-owner
mv node-<v>-linux-x64 /opt/mosaic-stack/node-<v>
chown -R root:root /opt/mosaic-stack/node-<v>
chmod -R go-w /opt/mosaic-stack/node-<v>
```
The keyring holds the Node release keys from the nodejs/release-keys repository, nothing else. Stop on any
failed check; don't unpack. Rollback: `rm -r /opt/mosaic-stack/node-<v>`.
Precheck lines:
- `ok node <v>` (version 24 or later only).
- The `--node` path in the args file must be a root-owned regular file outside the home. `case2-unit.sh`
refuses otherwise at render, which is not a precheck line.
- The installed version equals the pin: precheck line missing.
## 3. claude, factory only (.53)
Root installs it under `/opt`, never under the stack's home (this replaces r6's `/srv/mosaic/.local/bin`).
The pin is 2.1.304 unless the window names a newer one.
```sh
install -d -o root -g root -m 0755 /opt/mosaic-stack/claude-<pin> /etc/claude-code
PATH=/opt/mosaic-stack/node-<v>/bin:/usr/sbin:/usr/bin:/sbin:/bin \
npm install --prefix /opt/mosaic-stack/claude-<pin> @anthropic-ai/claude-code@<pin>
chown -R root:root /opt/mosaic-stack/claude-<pin>
/opt/mosaic-stack/claude-<pin>/node_modules/@anthropic-ai/claude-code/bin/claude.exe --version
printf '%s\n' '{"env":{"DISABLE_AUTOUPDATER":"1"}}' > /etc/claude-code/managed-settings.json
chown root:root /etc/claude-code/managed-settings.json
chmod 0644 /etc/claude-code/managed-settings.json
```
Rollback: `rm -r /opt/mosaic-stack/claude-<pin> /etc/claude-code/managed-settings.json`.
Root's PATH has no npm, so the install line puts section 2's pinned node first; the package's postinstall
also runs `node`. The `--version` line must print `<pin> (Claude Code)`. Anything else means the postinstall
left its 500-byte stub in `bin/claude.exe` (no native package for the platform, or scripts were off): stop and
roll back.
The args file's `--claude` is
`/opt/mosaic-stack/claude-<pin>/node_modules/@anthropic-ai/claude-code/bin/claude.exe`, the regular file the
postinstall hardlinks or copies the native binary to. Not `node_modules/.bin/claude`: that is npm's symlink
to it, and `case2-unit.sh` refuses a symlink, as it does anything that isn't a root-owned regular file outside
the home. Checked against the 2.1.304 package's `install.cjs`; a newer pin needs the same check.
Precheck lines:
- `ok claude replaces its prompt with --system-prompt-file (<n> input tokens)`. It runs the claude the bus
unit's `MOSAIC_CLAUDE_BIN` names, so it needs the model credential from section 9.
- Managed settings present with the autoupdater off: precheck line missing.
## 4. bubblewrap, AppArmor and the drift stop
Both hosts. These are r6's items, unchanged.
```sh
apt-get install bubblewrap=0.9.0-1ubuntu0.3
stat -c '%U:%G %a' /usr/bin/bwrap > /etc/mosaic-stack/bwrap.orig # expect root:root 755
cd "$(mktemp -d)"
apt-get download apparmor-profiles=4.0.1really4.0.1-0ubuntu0.24.04.9
dpkg-deb --fsys-tarfile apparmor-profiles_*.deb | tar -x ./usr/share/apparmor/extra-profiles/bwrap-userns-restrict
echo "11d39094f044f0cda0febb3ad517b830301da6b2ce929664af09ee9e4dd264f9 usr/share/apparmor/extra-profiles/bwrap-userns-restrict" | sha256sum -c -
install -o root -g root -m 0644 usr/share/apparmor/extra-profiles/bwrap-userns-restrict /etc/apparmor.d/bwrap-userns-restrict
echo "11d39094f044f0cda0febb3ad517b830301da6b2ce929664af09ee9e4dd264f9 /etc/apparmor.d/bwrap-userns-restrict" | sha256sum -c -
apparmor_parser -r /etc/apparmor.d/bwrap-userns-restrict
dpkg-statoverride --update --add root mosaic 0750 /usr/bin/bwrap
```
Run as root in a `mktemp -d` dir, `apt-get download` prints a line starting `W: Download is performed
unsandboxed as root`, because the `_apt` user can't write that dir. The line is expected and the
download still succeeds. Each `sha256sum -c -` line must print `OK`. The second one checks the copy in
`/etc/apparmor.d`, the file that loads. Stop on any other result.
`aa-status` then lists `bwrap` and `unpriv_bwrap` in enforce mode. Install the one profile file, never the
whole `apparmor-profiles` package, whose 20 other profiles would load at the next boot. Rollback, in
reverse order: `dpkg-statoverride --remove /usr/bin/bwrap`, then `chown` and `chmod` to the values in
`bwrap.orig`; `apparmor_parser -R /etc/apparmor.d/bwrap-userns-restrict`, then `rm` it;
`apt-get remove bubblewrap`.
The no-new-privs test must print `bwrap//&unpriv_bwrap (enforce)`:
```sh
systemd-run --wait --pipe --uid=mosaic --gid=mosaic -p NoNewPrivileges=yes -p ProtectSystem=strict \
-p ProtectHome=yes -p PrivateTmp=yes \
/usr/bin/bwrap --unshare-user --unshare-pid --ro-bind / / cat /proc/self/attr/current
```
The P9 checks: another user's `/usr/bin/bwrap --version` fails with Permission denied, `getent group mosaic`
lists no member, and `sysctl fs.protected_hardlinks` prints 1. `sysctl
kernel.apparmor_restrict_unprivileged_userns` prints 1 too (Ubuntu 24.04's default); leave it, so nothing
else on the host gains user namespaces.
The drift stop (row 240, I4, #1746) goes in last, after the profile and the statoverride. As root, from
the root-owned checkout of the reviewed commit:
```sh
install -d -o root -g root -m 0755 /usr/local/lib/mosaic-stack
install -o root -g root -m 0755 scripts/case1-hostcheck.sh /usr/local/lib/mosaic-stack/hostcheck.sh
install -o root -g root -m 0644 scripts/case1-pins.sh /usr/local/lib/mosaic-stack/case1-pins.sh
printf '%s\n' 'DPkg::Post-Invoke { "/usr/local/lib/mosaic-stack/hostcheck.sh --enforce || true"; };' \
> /etc/apt/apt.conf.d/80mosaic-hostcheck
install -o root -g root -m 0644 packages/cli/systemd/mosaic-hostcheck.service /etc/systemd/system/
systemctl daemon-reload && systemctl enable mosaic-hostcheck.service
/usr/local/lib/mosaic-stack/hostcheck.sh --check
```
`--check` prints one `ok` line for each of its seven items and exits 0. Item 7 reads 1023 as the port
until I2 installs the Console unit (section 10), then the unit's `--port`. `stack-upgrade.sh` runs `--check`
before its plan and refuses `profile-drift` on any exit but 0, a missing file included. Rollback:
`systemctl disable mosaic-hostcheck.service`, then `rm` the unit, the hook file and
`/usr/local/lib/mosaic-stack`.
On drift, `--enforce` (the apt hook after every package run, and the unit at boot) unloads the profile,
closes `/usr/bin/bwrap` to `root:root` 0700, appends the failed items to `/etc/mosaic-stack/DRIFT` and logs
a line in `/var/log/mosaic-stack/hostcheck.log`. Every role session then refuses to start. ops-01 reads the
log in each STATE report and sends any `DRIFT` to Mos. Only root removes `DRIFT`, in a reviewed step with
a new pin or a Mos go. After that, `chown root:mosaic /usr/bin/bwrap`, `chmod 0750 /usr/bin/bwrap` and
`apparmor_parser -r /etc/apparmor.d/bwrap-userns-restrict` put bwrap back, and `--check` must exit 0.
Before each re-pin window, ops-01 runs `/usr/local/lib/mosaic-stack/hostcheck.sh --upstream`. It
downloads the `apparmor-profiles` candidate and compares its `bwrap-userns-restrict` to the pin. A
`DIFFERS` line goes to Mos as a new-pin request; it refuses nothing.
Precheck lines:
- `ok bwrap --unshare-all runs`
- The AppArmor profile, the statoverride, the no-new-privs result and the P9 checks: precheck line missing.
`/usr/local/lib/mosaic-stack/hostcheck.sh --check` covers them (row 240).
## 5. The port floor
Leave `net.ipv4.ip_unprivileged_port_start` at its default (1024) or higher. The Console's port is below
it and the unit gets `CAP_NET_BIND_SERVICE`, so no other `mosaic` process can bind that port.
Precheck line: `ok net.ipv4.ip_unprivileged_port_start <n> is above the Console's port <port>`.
## 6. The release transport
A release is a git bundle of `refs/heads/refactor` plus the 40-character commit id (I1, mos-1118 choice 2).
Mos's go names both, with the bundle's sha256. `<sha>` below is that id.
First install:
```sh
# Copy the bundle in, then check it before anything reads it.
echo "<bundle sha256> /root/<sha>.bundle" | sha256sum -c -
(
bundle_check="$(mktemp -d)" || exit 1
trap 'rm -rf "$bundle_check"' EXIT
git init -q --bare "$bundle_check" &&
git -C "$bundle_check" bundle verify /root/<sha>.bundle
) || echo "STOP: bundle verify failed; do not continue"
# Root's checkout. stack-upgrade.sh and case2-unit.sh run from here, never from a copy.
git clone -q -b refactor /root/<sha>.bundle /root/mosaic-stack
git -C /root/mosaic-stack checkout -q --detach <sha>
git -C /root/mosaic-stack rev-parse HEAD # must print <sha>
# The stack's user's copy of the bundle, and its own clone.
install -d -o mosaic -g mosaic -m 0700 /srv/mosaic/incoming /srv/mosaic/src
install -o mosaic -g mosaic -m 0600 /root/<sha>.bundle /srv/mosaic/incoming/<sha>.bundle
sudo -u mosaic -H git clone -q -b refactor /srv/mosaic/incoming/<sha>.bundle /srv/mosaic/src/mosaic-stack
```
If the first install refuses, use Mos's retry path (mos-1565, ops-0291). Bootstrap can already have
written `/srv/mosaic/.config/mosaic-dev/config.json` (`scripts/case2-install.sh:174`,
`scripts/bootstrap.sh:13`, `scripts/mosaic-config.mjs:412-424`). The config dir can also hold
`businesses/`, the business file, and library persona files and slot dirs
(`scripts/case2-install.sh:200-211`, `scripts/case2-persona-import.mjs:300-308`). A validation refusal
removes only the business file this run created; its directory remains (`scripts/case2-install.sh:217-219`).
The copy may already exist under `/srv/mosaic/.local/share/mosaic-stack/copies/<sha>`
(`scripts/case2-install.sh:80`, `scripts/stack-copy.sh:68`).
As root, move these three paths aside with a UTC suffix. Move, never delete, so their owner and mode
stay with them (mos-1565). This is the default-instance retry for the paths in this sheet:
```sh
(
cd / || exit 1
retry_stamp="$(date -u +%Y%m%dT%H%M%SZ)" || exit 1
for retry_path in /root/mosaic-stack /srv/mosaic/src/mosaic-stack /srv/mosaic/.config/mosaic-dev; do
if [ -e "$retry_path" ] || [ -L "$retry_path" ]; then
retry_saved="$retry_path.$retry_stamp"
if [ -e "$retry_saved" ] || [ -L "$retry_saved" ]; then
echo "STOP: retry destination already exists: $retry_saved" >&2
exit 1
fi
mv -T -- "$retry_path" "$retry_saved" || exit 1
fi
done
) || echo "STOP: could not move the first-install paths aside; do not continue"
```
Then rerun the first-install block from its first line. Never use the upgrade fetch below on clones
whose first install has not finished: it updates `refs/remotes/origin/refactor`, while copy requires
the commit to be reachable from both that ref and `refs/heads/refactor` (`scripts/stack-copy.sh:44-48`; mos-1565).
Recheck section 9 before starting units. If Jason already filled `claude-session.env`, the library
proof file or `library-gitea`, have him re-place those files under the new config dir with section 9's
owner and mode. Per-agent slots still use `mosaic credentials issue` or Approve, never a hand copy
(section 9; mos-1565). The moved config dir preserves the earlier files.
After a first install, once both units are running (section 10), adopt the installed copy before
any activate or upgrade. This step runs only on Mos's go, with Jason or Mos running it
(`scripts/stack-release.sh:14`):
```sh
sudo -u mosaic -H env PATH=/opt/mosaic-stack/node-<v>/bin:/usr/sbin:/usr/bin:/sbin:/bin \
/srv/mosaic/src/mosaic-stack/scripts/stack-release.sh adopt <business> --instance dev --units external
```
Each upgrade, before `stack-upgrade.sh upgrade`:
Only the first upgrade after adopt takes `--no-rehearsal`: append it to the upgrade command below.
Adopt writes no backup, so omitting the flag then refuses `no-backup`. Subsequent upgrades omit it;
when a backup exists, supplying it refuses `rehearsal-required`.
```sh
echo "<bundle sha256> /root/<sha>.bundle" | sha256sum -c -
(
bundle_check="$(mktemp -d)" || exit 1
trap 'rm -rf "$bundle_check"' EXIT
git init -q --bare "$bundle_check" &&
git -C "$bundle_check" bundle verify /root/<sha>.bundle
) || echo "STOP: bundle verify failed; do not continue"
git -C /root/mosaic-stack fetch -q /root/<sha>.bundle refs/heads/refactor:refs/remotes/origin/refactor
git -C /root/mosaic-stack checkout -q --detach <sha>
install -o mosaic -g mosaic -m 0600 /root/<sha>.bundle /srv/mosaic/incoming/<sha>.bundle
/root/mosaic-stack/scripts/stack-upgrade.sh upgrade --instance dev --bundle /srv/mosaic/incoming/<sha>.bundle --sha <sha>
```
The driver fetches the bundle into the stack's user's clone and makes the copy itself. It refuses
`head-mismatch` when root's checkout isn't at `<sha>`. Usage, exits and the step log are in
`docs/TOOLS.md`, under "Root's upgrade driver". Each run needs Mos's go, and `--migrate` needs Jason present
(decision 96).
When a run doesn't exit 0, send Mos the exit and the last lines of `/var/log/mosaic-stack/upgrade-dev.log`:
- Exit 3: refused before anything stopped. The last line names the code. The stack's user's clone and the
new copy may already hold the new commit; the units still run the old one.
- Exit 1: the old copy runs and passed its gate. Either the driver restarted it after A2 or a refused A3
(A2f), or it rolled back after A3, A4 or A5.
- Exit 2: both units are stopped and the activation stays open, so the next run refuses at its plan until
it is finished with `confirm` or `revert`. One way here: a unit that doesn't stop within `--timeout`
keeps its old start time, A2f's gate fails it `stale-start`, and the driver stops both (duckworth-0055
N1). That is on purpose; a unit that won't stop is a reason to stop.
- Exit 5: `args-failed`. The new copy is live and passed confirm, but `/etc/mosaic-stack/dev.args` still names
the old copy. The next run refuses `args-differ`, and a render from that file would put the old copy back.
Move the file the result line names into place with `mv -f`. The two differ only in the `--copy` value.
To check a backup by hand before rollback or restore, run as the stack's user from its `bus/` directory:
```sh
sudo -u mosaic -H /bin/sh -c \
'cd "<dataRoot>/backups/stack/<activation>/bus" && sha256sum -c ../SHA256SUMS'
```
Expect `bus.sqlite: OK`; stop on a failed check. The set holds `bus/bus.sqlite`, and `SHA256SUMS` names
`bus.sqlite` relative to `bus/` (`scripts/stack-release.sh:475-482`). Running `sha256sum -c SHA256SUMS`
from the backup set's directory looks for `bus.sqlite` one directory too high (ops-0292). Restore and
rehearsal use the same `bus/` form
(`scripts/stack-release.sh:502`, `scripts/stack-release.sh:2244`).
After `case2-install.sh` succeeds, create the human CLI wrapper before running
`stack-copy.sh check --units external`. Creating this wrapper is a separate live
step requiring Mos's go (`scripts/stack-copy.sh:3`); the install does not create it. Run only on that go:
The step runs under sudo's `secure_path`, so it sets its own PATH and umask.
```sh
sudo -u mosaic -H /bin/sh <<'MOSAIC_WRAPPER'
cd / || exit 1
umask 077
PATH=/opt/mosaic-stack/node-<v>/bin:/usr/sbin:/usr/bin:/sbin:/bin; export PATH
mkdir -p -- "$HOME/.npm-global/bin" || exit 1
"$HOME/src/mosaic-stack/scripts/stack-copy.sh" wrapper <sha> --create
MOSAIC_WRAPPER
```
`--create` refuses an existing wrapper; stop and review it rather than replacing
it. The wrapper must name the same `<sha>` copy as the units.
Precheck lines: none. Root's checkout at `<sha>` and the stack's user's clone: precheck line missing.
After install, `stack-copy.sh check --units external` reads the copy the units run.
Before running `case2-install.sh`, check the exact file you will pass as `--business-file`. Each
non-library `projects` key must equal the `id` in `<root>/.mosaic/project.json`, and that file must exist
in the stack's user's clone. Library entries are skipped by validate (`packages/organization/src/config-command.mjs:95`).
Run this read-only check as `mosaic` with section 2's pinned node:
```sh
sudo -u mosaic -H /opt/mosaic-stack/node-<v>/bin/node --input-type=module - '<business-file>' <<'MOSAIC_PROJECT_KEYS'
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
try {
const business = JSON.parse(readFileSync(process.argv[2], 'utf8'));
const projects = Object.entries(business.projects);
if (!projects.length) throw new Error('business file declares no projects');
for (const [id, entry] of projects) {
if (entry.kind === 'library') continue;
const file = join(entry.root, '.mosaic', 'project.json');
const project = JSON.parse(readFileSync(file, 'utf8'));
if (project.id !== id)
throw new Error(`project file ${file} has id ${project.id}, but business ${business.id} declares it as ${id}`);
console.log(`ok project ${id}: ${file} has id ${project.id}`);
}
} catch (error) {
console.error(`STOP: ${error.message}; do not install`);
process.exitCode = 1;
}
MOSAIC_PROJECT_KEYS
```
For .53 expect `ok project stack: /srv/mosaic/src/mosaic-stack/.mosaic/project.json has id stack`
(`.mosaic/project.json:3`, `docs/guides/case1-business.mosaic-stack.json:10-11`). Any STOP or nonzero exit
stops the install. Fix a mismatched business key, not the project id (mos-1563). The install otherwise
reaches that refusal at step 7, after making the copy (`packages/organization/src/config-command.mjs:103`,
`scripts/case2-install.sh:178`, `scripts/case2-install.sh:217`).
The stack's user's `scripts/case2-install.sh` (C2-13b, design 10 step 2b) makes the first copy with
`stack-copy.sh copy <sha>`. The copy runs `npm ci` from `package-lock.json`, which installs the pinned
harness, so the host needs to reach the npm registry during the install. The install runs no
`release.sh package` or `activate`.
A missing `.mosaic/project.json` only warns: `project <id>: no project file at <path>; it has no project
variables`. Validate reports that project `absent` and continues (`packages/organization/src/config-command.mjs:97-101`).
An install can therefore pass step 7 without the project's variables. It captures validate's output
and prints `business <id> validates` (`scripts/case2-install.sh:217-221`). Treat the missing-file warning as a stop,
place the correct project file and repeat the check above before continuing (Duckworth 323 N2;
`scripts/case2-install.sh:217-221`).
## 7. The args file
`/etc/mosaic-stack/dev.args` holds the `case2-unit.sh` arguments, one per line, with the serve flags after a
line holding `--`. Root writes it at the unit render, from the values the go names. The stack's user's
install never writes it: that user can't write `/etc`, and root reads nothing that user wrote. It must be root's, with no group
or other write (`chmod 0644` or tighter). Before `--` it takes only `--user`, `--home`, `--business`,
`--copy` (exactly once), `--node`, `--profile`, `--claude`, `--pm` and `--proxy`. `stack-upgrade.sh` changes only the
`--copy` line, and only after confirm passes.
For a hub installed with `--proxy`, keep that bare flag on its own line before `--`; it takes no value.
Upgrade and rollback require the proxy, bus and Console units to name the same copy. They stop the Console,
then the bus, then the proxy, re-render all three onto the selected copy, and start the proxy, then the bus,
then the Console. Recovery after a failed upgrade restores all three to the old copy. A missing or
disagreeing proxy unit, or an installed proxy without `--proxy` in the args file, refuses before anything stops.
For the `library` profile, the serve argv after `--` keeps the proxy-preview trio and required proof pair
but omits `--send`. That Console is read-only until Case 2 gains real OIDC in #412; `case2-unit.sh`
explicitly refuses `--send`. Proxy preview and proof-protected reads remain available. The `factory`
profile and its private tester socket are unchanged.
A library host installed before #385 has `--send` in two places, because the earlier `case2-unit.sh`
required it: a `--send` line after `--` in the args file, and `--send` in the installed Console unit's
`ExecStart`. `stack-upgrade.sh upgrade` and `rollback` check both before anything stops, when the
profile is `library` (no `--profile` line, or the last one says `library`):
- `--send` after `--` in the args file refuses `args-unreadable`.
- A `--send` token after `serve` in `/etc/systemd/system/mosaic-webui.service`'s `ExecStart` refuses
`console-unit-legacy`. The current `case2-unit.sh` refuses that installed unit, so an upgrade or
rollback would stop both units and then fail both renders.
Both refusals leave the units running, and the script rewrites neither file. Check the installed unit
read-only, as root:
```sh
grep -n '^ExecStart=' /etc/systemd/system/mosaic-webui.service | grep -c -- ' --send'
```
- `0`, and no installed unit carries `--send`: removing the args line is enough. Open
`/etc/mosaic-stack/dev.args` and delete the one line that reads `--send`, changing nothing else. Check
that `stat -c '%U %a' /etc/mosaic-stack/dev.args` still gives `root` and a mode with no group or other
write, and that `grep -cx -- --send /etc/mosaic-stack/dev.args` prints `0`. Then rerun the same
`stack-upgrade.sh` command.
- `1` or more: the host needs a reviewed migration of the unit, the args file and the release pointer
together, and none exists yet. Stop. Leave the units running, change neither file and report to the
lead with the command's output. Removing only the args line does not help: the run then refuses
`console-unit-legacy`.
The Console stays read-only after the upgrade, until #412 (#385).
Precheck line: none. `stack-upgrade.sh` refuses `args-unreadable` or `console-unit-legacy` on any of these
rules, before it stops a unit.
## 8. Enrollment, once per host
The machine-grant enrollment is Gizmoduck's design r6, commit f9039682e, section 3, steps E1-E4. ops-01's
Q-O1 is answered (mos-1125). The host side is the `tools/host/` files: `mosaic-keys`, `mosaic-keys-write`,
their sudoers line and the sshd drop-in `/etc/ssh/sshd_config.d/<nn>-mosaic-keys.conf`. The verbs that run
it (`hosts.enroll`, the `host-enroll` decision) are rows 234 and 235, not yet on refactor. This item has no
commands until they land.
Precheck line: none. The enrolled host's key and its `mosaic-keys` sudoers line: precheck line missing.
## 9. Credential slots
Jason places these, all in one sitting per host. No value appears here or in any message. Check a placed
file with `stat -c '%U %a %n' PATH`, never `cat`. `<business>` is the host's business id.
### .53, factory
| Path | File | Mode | Owner | Profile | Precheck line |
|---|---|---|---|---|---|
| `/srv/mosaic/.config/mosaic-dev/secrets/` | `claude-session.env`, one line `CLAUDE_CODE_OAUTH_TOKEN=...` (Q185) | 0600 | mosaic | factory | `claude replaces its prompt with --system-prompt-file` runs claude with it. The file's mode and owner: precheck line missing. |
| `/srv/mosaic/.config/mosaic-dev/secrets/agents/<business>/<agent>/` | one file per service | 0600, dirs 0700 | mosaic | both | none. Filled only by `mosaic credentials issue`, or Approve after #436. Never by hand. |
### .54, library
| Path | File | Mode | Owner | Profile | Precheck line |
|---|---|---|---|---|---|
| `/srv/mosaic/.config/mosaic-dev/secrets/` | `claude-session.env`, one line `ZAI_API_KEY=...` | 0600 | mosaic | library | `<path> sets ZAI_API_KEY` |
| `/srv/mosaic/.config/mosaic-dev/secrets/` | the Console's proof file, the path the args file's `--proof-file` names: one base64url line, at least 32 `[A-Za-z0-9_-]` characters, with one optional trailing LF (`packages/webui/src/serve.mjs:263-264`) | 0600 or 0400 | mosaic | library | none. Render checks metadata only and never opens the file (`scripts/case2-unit.sh:397-408`). Bad bytes refuse at Console start with exit 2 (`packages/webui/src/serve.mjs:267`, `packages/webui/src/cli.mjs:138-140`). Precheck line missing. |
| `/srv/mosaic/.config/mosaic-dev/secrets/<business>/` | `library-gitea` | 0600 | mosaic | library | After a unit start, wait for loopback `/healthz` with the bounded retry in section 10 before `case2-precheck.sh --business <business> --library-status`: `library-last-error library-credential-missing` until it's placed, then `library <project> last fetched ...` |
| `/srv/mosaic/.config/mosaic-dev/secrets/agents/<business>/<agent>/` | one file per service | 0600, dirs 0700 | mosaic | both | none. Filled only by `mosaic credentials issue`, or Approve after #436. Never by hand. |
The installer key, `<configDir>/secrets/hosts/<business>/<hostId>/installer`, is not a slot anyone places.
Enrollment (section 8) writes it under the enrolling stack's config directory and deletes it when the host
leaves.
## 9a. Notifier decision, before starting units
Make the explicit per-host decision before section 10: the default below is
`binding: null`, so the bus runs without DMs. A named binding is a later per-host
choice that needs Mos's go. The installer does not make this decision or write
this file. Replace `<dataRoot>` with the configured data root and `<business>`
with this host's business id. If the file already exists, stop and review the
existing decision; this command refuses to overwrite it.
```sh
sudo -u mosaic -H /bin/sh <<'MOSAIC_NOTIFY'
cd / || exit 1
umask 077
notify_root='<dataRoot>/notify'
notify_dir="$notify_root/<business>"
mkdir -p -m 0700 -- "$notify_root" && chmod 0700 -- "$notify_root" || exit 1
mkdir -p -m 0700 -- "$notify_dir" && chmod 0700 -- "$notify_dir" || exit 1
(set -C; printf '%s\n' '{"notifyVersion": 1, "binding": null}' > "$notify_dir/notify.json") || exit 1
chmod 0600 -- "$notify_dir/notify.json"
MOSAIC_NOTIFY
```
Precheck line: `ok notifier config <dataRoot>/notify/<business>/notify.json validates (directory 0700, file 0600)`.
## 10. Console start
None by hand. I2's `case2-unit.sh` enables both units, starts the bus, then the Console (Q189, mos-1122).
`stack-upgrade.sh` stops and starts both units itself.
After a unit start, wait for the Console's loopback `/healthz` to succeed before running
`case2-precheck.sh`, including `--library-status` in section 9. Replace `<port>` with the installed
Console unit's `--port`. This retry is bounded to 30 attempts, each with a 2-second timeout, with
1 second between failed attempts. If it prints STOP, stop here; do not run the precheck or adopt.
```sh
(
attempts=0
until curl -fsS --max-time 2 -o /dev/null 'http://127.0.0.1:<port>/healthz'; do
attempts=$((attempts + 1))
[ "$attempts" -lt 30 ] || { echo "STOP: loopback healthz did not become ready; do not continue" >&2; exit 1; }
sleep 1
done
)
```
Precheck line: none. Both units active after the render: precheck line missing.