Files
stack/agents/sage/work/queue-68/build.md
T

6.5 KiB

Row 68 (#1561): the bus host runs an explicit Claude Code binary, round 1

Sage, 2026-10-11. Brief: docs/plans/2026-10-11_bus-claude-binary.md, "Bus host runs an explicit Claude Code binary" (e1b90ed6). Mos's Q18 ask: the bus finds /usr/bin/claude 2.1.259 (pacman claude-code), which refuses claude-opus-5-5. Jason runs the native ~/.local/bin/claude (2.1.296). Filbert and Dewey review. Sage commits after the gate; nothing is committed, staged or pushed yet.

Base a86de1f1. build.patch applies there with git apply --index; candidate-manifest.sha256 pins all 13 files after the patch.

Design

The binary is named, not found. MOSAIC_CLAUDE_BIN is an absolute path:

  1. The unit sets it. scripts/bus-service.sh render|install --claude PATH; without --claude, command -v claude in the installing shell. The path is kept as given, not resolved: ~/.local/bin/claude is a symlink the native installer moves to each new version, so the unit follows updates without a reinstall.
  2. ENV_ALLOW passes it to the session. It's a path, not a credential.
  3. adapters/claude/adapter.sh runs $MOSAIC_CLAUDE_BIN when it is set and nonempty, else claude as before.
  4. claudeVersion, which fills the manifest's claudeVersion, asks the same binary, so the manifest records what actually ran.

Rejected: prepending ~/.local/bin to the unit's PATH. Every binary there (pip and npm installs, personal scripts) would then shadow the system's, for the host and for every session. Also rejected: a field in the business file or system config. Which binary runs is a property of the host install, like node's directory, which the unit already carries.

Refusals

Where Input Result
bus-service.sh --claude relative exit 1, "must be absolute", nothing written
bus-service.sh not an executable file, or missing exit 1, "not an executable file"
bus-service.sh a character outside A-Za-z0-9/._+- (%, space, ...) exit 1, "cannot carry". systemd expands % and splits Environment= on spaces.
bus-service.sh no --claude and no claude on PATH exit 0; the unit gets # MOSAIC_CLAUDE_BIN unset: ... and stderr says so. A pi-only host installs as before.
bus-service.sh uninstall --claude, render --dir exit 4
adapter MOSAIC_CLAUDE_BIN relative exit 2 before claude runs
adapter not an executable file, or missing exit 2 before claude runs
adapter unset or empty claude from PATH, as today

install now renders into a variable before mktemp, so a refused pin leaves no temp file in the unit directory. Calling render inside || { ... } would have switched set -e off in the function and written a unit with an empty line. claude_line failing is handled with an explicit || exit 1.

install prints claude: <path> (<--version>) or the unset line. status adds -p Environment to its systemctl show, which shows PATH and the pin. The unit holds no secret.

Files

  • packages/cli/systemd/mosaic-bus.service.in: a comment and an @CLAUDE_ENV@ line after Environment=PATH=. The header names "the claude line below" rather than the token, because header tokens render into the comment (as @REPO@ and @PATH@ already do) and the claude substitution only matches its own line.
  • scripts/bus-service.sh: claude_line, --claude for render and install, the install print, status shows Environment, usage.
  • adapters/claude/adapter.sh: the CLAUDE variable and its checks.
  • packages/seat/src/session.mjs: MOSAIC_CLAUDE_BIN in ENV_ALLOW.
  • packages/cli/src/launcher.mjs: claudeVersion asks env.MOSAIC_CLAUDE_BIN || "claude".
  • Docs: packages/cli/README.md ("Unit" and the manifest note), adapters/README.md (adapter list and variable table), packages/seat/README.md (sessionEnv), docs/TOOLS.md (usage line and one paragraph). The brief didn't list docs/TOOLS.md. I added it because its usage line would otherwise be wrong, and it is my file.
  • Tests:
    • host.test.mjs, new test "bus-service.sh pins the claude binary...":
      • the pin found on PATH, from --claude, and through a symlink kept unresolved
      • five refusals with exit 1 and empty stdout
      • install printing the pin and the stand-in's version
      • a refused install writing nothing
      • uninstall --claude and render --dir exiting 4
      • no claude anywhere: PATH is a directory holding only node, dirname and sed, and the unit gets the comment line
    • The existing render test also asserts no @CLAUDE_ENV@.
    • launcher.test.mjs: claudeVersion with the pin, from PATH, and with none (null).
    • session.test.mjs: sessionEnv passes MOSAIC_CLAUDE_BIN.
    • claude-session.test.mjs, new test:
      • the pin wins over a claude on PATH
      • unset or empty uses PATH
      • relative, missing and non-executable pins exit 2 without running anything

Each new test fails against base: unpinned claudeVersion returns the PATH stand-in's 1.0.0, the base adapter runs the PATH stand-in, base sessionEnv drops the variable, and base render rejects --claude with exit 4.

Gate (my run on the candidate)

Suite Result
packages/cli 85/0
packages/seat 27/0
packages/harness 57/0, nothing skipped. The live claude tests ran against the 2.1.296 on my PATH.
scripts/test-*.sh all nine exit 0: auth 15, conductor 17, config 24, discord 66, extension-package 18, foundation 44, queue 27, release 14, task 98 (Docker on)

Logs: /tmp/sage-r68-out/ (not kept).

After landing (not in the candidate)

  1. scripts/bus-service.sh install from my shell, which resolves /home/jwoltje/.local/bin/claude. install prints the pin and its version.
  2. The opus probe: the adapter's flags with MOSAIC_CLAUDE_BIN and PATH taken from the installed unit, as decision 89's probe did.
  3. Report the path and version to Mos.

The running host (MainPID 1242258) keeps its old environment until it restarts. That restart is the Q18 --pm start, which waits for Mos's watched window. Nothing here restarts it.

Questions for reviewers

  • Is keeping the symlink, not its target, right? The other choice pins an exact version and needs a reinstall per update. I chose to follow updates because that's what Jason's own sessions do.
  • With no claude found, install succeeds with a comment rather than refusing, because a pi-only business needs no claude. Is that the right default, or should install refuse unless the business's roles are all pi?