10 KiB
Bus, Console and mosaic CLI on one pinned copy (2026-10-12)
Status: written by Sage, lead. It follows docs/plans/BRIEF-TEMPLATE.md.
It is the fix plan in my comment 52 on Vikunja #225, which Mos asked for
in sage-0094 ("Tell me the plan before anything touches the wrapper or
the unit") and sage-0095 ("Keep the one-copy row brief going"). It
follows queue row 77 (#1571), whose round 2 makes webui-service.sh install --copy refuse unless the bus runs from the same copy. Gitea
#1579.
Bus, Console and mosaic CLI on one pinned copy (#225)
Problem
On 2026-10-12 Jason ran mosaic talk pm "Can you hear me now?" and got
mosaic: human-required (#225). The bus accepts a human proof only from
its own packages/bus/src/human-cli.mjs: verifyHuman compares the
proof process's argv[1] with resolve(cliPath)
(packages/bus/src/human.mjs:52-102), and cliPath comes from the bus's
own tree (packages/bus/src/runtime.mjs:68). The CLI spawns the
human-cli of its own tree (packages/cli/src/transport.mjs:12), and so
does the Console (packages/webui/src/bus.mjs). Today three things run
from three trees:
- the bus unit,
ExecStart=/mnt/storage/src/mosaic-stack/scripts/mosaic bus start %i, rendered byscripts/bus-service.shfrompackages/cli/systemd/mosaic-bus.service.inwith REPO set to the script's own checkout; - the
mosaicwrapper (Q81,~/.npm-global/bin/mosaic), which runs a linked worktree at~/.local/share/mosaic-cli/stack, pinned atd3ca2c22, six bus and CLI commits behind; - the Console, which runs from the checkout today and from a row 77 copy
under
~/.local/share/mosaic-console/<id>/once row 77 lands and is installed.
So the proof works only from one tree at a time. The stopgap is the full
path, /mnt/storage/src/mosaic-stack/scripts/mosaic talk pm ..., which
Jason confirmed works (6b5b7aef). The bus also runs code from the moving
checkout, so every landing in packages/bus or packages/cli is live at
its next restart, the same problem row 77 fixes for the Console (lead
decision 109). And the unit template has no --pm
(packages/cli/src/cli.mjs:265-279), so no PM session ran until Mos
added a temporary drop-in that sets it (sage-0096; the S7 run log,
2026-10-12T18:49Z).
Accepting a second path in the proof (#225 option B) is rejected: it adds a trusted path and keeps the three trees on different code.
Owner and reviewer
Owner: Rocko. Reviewers: Darkwing and Filbert. Rocko has no Gitea token; Sage runs the review request as jarvis. This row starts when row 77 is done, and it goes ahead of row 84, since S7 comes before M1 (Q97).
Files owned
scripts/stack-copy.sh, newscripts/webui-service.sh, only thecopyverb, the copies directory and the same-copy check row 77 round 2 addsscripts/bus-service.shpackages/cli/systemd/mosaic-bus.service.in- tests for the above, under
packages/cli/tests/andpackages/webui/tests/, or a newscripts/test-stack-copy.sh packages/cli/README.md, the "Bus host" sectionpackages/webui/README.md, the service sectiondocs/TOOLS.md, the entries for the three scripts onlyagents/rocko/work/queue-<row>/
What ships
One copy for all three:
scripts/stack-copy.sh copy COMMIT [--copies DIR]makes the copy. It takes over row 77's copy code and rules: canonical checkout only (the--allow-worktreetest override stays), the full 40-character id, a temp directory beside the target and a rename, write permission removed, and an existing copy left as it is (exists:). COMMIT must be reachable from bothrefs/heads/refactorandrefs/remotes/origin/refactor, since the wrapper runs pushed code only (Q81).- The default directory is
${XDG_DATA_HOME:-$HOME/.local/share}/mosaic-stack/copies/<full id>/.webui-service.sh copystays as a call intostack-copy.sh copy, so row 77's documented steps still work, and both scripts read the same directory. - The path list grows to what the bus host, the CLI and the Console load:
row 77's list, plus
scripts/mosaic,adapters/,packages/queue/srcif any copied path imports it,package.jsonandpackage-lock.json. The script runsnpm ciin the temp directory before the rename, so the copy carries the pi pin'snode_modulesandpi-pin.mjs's lock check passes there (packages/conversation/src/pi-pin.mjs:52-57). Row 61 addspackages/tasks/srcandpackages/tasks/fields.jsonto what the Console loads (Darkwing, 2026-10-12), so the list carries them. The script refuses ifpackage.jsonis missing from the temp directory, sonpm cican never walk up. Rocko derives the final list from the imports and records how in the packet. - Tests build a copy from a fixture repository, then from that copy:
bus startreachesbus host upwith a stand-in config,cli.mjs serveanswers/healthz200, andscripts/mosaic talkgets past the human proof against a broker started from the same copy. A path missing from the list fails the suite.
The proof across copies, as tests (with the agent-environment check
stubbed as packages/bus/tests/human.test.mjs already does, never
bypassed in the code):
- a human-cli from the same copy as the broker is accepted;
- a human-cli from another copy, or from the checkout, is refused with
human-required.
The bus unit:
bus-service.sh renderandinstalltake--copy IDand--pm.--copy IDnames an existing copy by its full id, and the template's@REPO@becomes the copy's path, soWorkingDirectory,ExecStartandDocumentationpoint at it. Without--copythe unit renders as today, so a fresh install from the checkout still works, andstatussays the unit runs the checkout.--pmrendersExecStart=<copy>/scripts/mosaic bus start %i --pm. A PM refusal at start exits 3, which the unit doesn't retry (RestartPreventExitStatus=2 3 4), so the host stays down and its journal names the refusal. The README says so.installrefuses while anymosaic-bus@instance is active, activating or deactivating, with row 77's N1 wording (Jason or Mos stops it first), and with row 77's N4 rule for a failedsystemctlread.statusprints the copy id the unit runs, read from itsWorkingDirectory, whether--pmis in itsExecStart, and whetherrefactorpoints at another commit. It changes nothing.- The
--claude PATHpin (lead decision 91) is unchanged and is rendered next to--copyand--pm. - Mos's temporary drop-in,
~/.config/systemd/user/[email protected]/pm.conf(sage-0096, 2026-10-12T18:53:34Z), resetsExecStartto add--pmuntil this row lands. A drop-in overrides the installed unit, sostatusandcheckread the effectiveExecStartandWorkingDirectoryfromsystemctl --user show, not from the unit file, andstatuslists any drop-in files.installrefuses while a drop-in formosaic-bus@setsExecStartorWorkingDirectory, naming the file. The update step removespm.confbefore the install.
The check and the wrapper:
stack-copy.sh checkprints the tree each of the three runs: the bus unit'sWorkingDirectory, the Console unit'sWorkingDirectory, and the copy themosaicwrapper execs. It exits 0 only when all three name the same copy, and 1 otherwise, naming each path. It changes nothing. A missing unit or wrapper counts as a mismatch, named as absent.stack-copy.sh wrapper IDrewrites~/.npm-global/bin/mosaicto exec<copy>/scripts/mosaic. It keeps the Q81 lease line that sends__lease-capabilitytomosaic-old, and keeps the Q81 marker on line 2. It refuses unless the current file carries that marker and isn't a symlink, and it writes a temp file, then renames it.--bin DIRpoints it at a test directory. Tests cover the refusal on a foreign file and the lease line.
The update step, documented once in docs/TOOLS.md and pointed to from
both READMEs: stack-copy.sh copy COMMIT; stop the Console, then the
bus; remove any mosaic-bus@ drop-in that sets ExecStart (the first
time, Mos's pm.conf) and run systemctl --user daemon-reload;
bus-service.sh install --copy ID --pm with its flags;
webui-service.sh install --copy ID with its flags; stack-copy.sh wrapper ID; start the bus, then the Console; stack-copy.sh check,
webui-service.sh status, /healthz and scripts/mosaic launches list
showing the PM. Rollback is the same steps with the earlier id. A bus
restart ends the running sessions (packages/cli/src/launcher.mjs:304-320),
so the docs say to do it when no PM turn is in flight. The stops, starts,
installs and the wrapper rewrite are Jason's or Mos's (lead decisions 94,
102 and 103, and Mos's sage-0094: nothing touches the wrapper or the
units before the row lands and Mos says go).
Suites: the cli, bus and webui node suites, and every
scripts/test-*.sh.
Out of scope
Running any step of the update on the live system: stopping, starting,
installing or enabling either unit, and rewriting the real wrapper.
Removing the Q81 worktree at ~/.local/share/mosaic-cli/stack or
mosaic-old; that's Mos's or Jason's after the first update. Pruning old
copies. A health-gated activate and rollback like scripts/release.sh,
which is D1. Any change to the human proof itself
(packages/bus/src/human.mjs). Any change to packages/queue; mosaic queue from a copy refuses as it does from the Q81 worktree today, and
queue work stays on the checkout's scripts/mosaic. Relaunching a PM
whose session ended while the host runs.
Gate
Darkwing and Filbert approve on the row's issue, each naming the
candidate manifest. The cli, bus and webui node suites and every
scripts/test-*.sh pass on Sage's gate rerun. Before the landing Sage
notifies Mos (decision 109); after it, Sage sends Mos the commit and
scripts/webui-service.sh status, scripts/bus-service.sh status mosaic-stack and /healthz. #225 closes when Mos's first update
step at the landed commit ends with stack-copy.sh check exiting 0, and
Jason's plain mosaic talk pm "..." printing sent and then a reply.