From 918a4a1937122ad00602d2a09c1ab2f9eed17e44 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Sun, 11 Oct 2026 16:16:28 -0500 Subject: [PATCH] ops(q81): mosaic on PATH runs the pinned mosaic-stack CLI (sage-0042) agents/sage/work/q81/mosaic-cli.sh does install, update, undo and status. ~/.npm-global/bin/mosaic is now a wrapper that execs scripts/mosaic in a detached worktree at ~/.local/share/mosaic-cli/stack, pinned to pushed ee129aa6. The estate CLI symlink was renamed in place to mosaic-old. Only commits on origin/refactor can be pinned. `mosaic queue` refuses from the worktree (the queue runs only in the canonical checkout), so queue work stays on scripts/mosaic. status flags a wrapper replaced by `npm i -g --force`. lease-gate-notes.md: no single environment location for MOSAIC_LEASE_VERSION_PROBE_COMMAND exists outside fleet state, so the old-path lease gate fails closed, as Mos ruled for that case. Co-Authored-By: Claude Opus 5.5 --- agents/sage/work/q81/lease-gate-notes.md | 14 +++ agents/sage/work/q81/mosaic-cli.sh | 138 +++++++++++++++++++++++ 2 files changed, 152 insertions(+) create mode 100644 agents/sage/work/q81/lease-gate-notes.md create mode 100755 agents/sage/work/q81/mosaic-cli.sh diff --git a/agents/sage/work/q81/lease-gate-notes.md b/agents/sage/work/q81/lease-gate-notes.md new file mode 100644 index 000000000..82294d87b --- /dev/null +++ b/agents/sage/work/q81/lease-gate-notes.md @@ -0,0 +1,14 @@ +# Q81 (2): lease gate env, read-only findings, 2026-10-11 + +- activation_version_gate.py:94-104 reads MOSAIC_LEASE_VERSION_PROBE_COMMAND from the + environment it is handed (shlex.split), else `shutil.which("mosaic", PATH)`. +- launch-runtime.py:129 uses os.environ unchanged; it inherits the old CLI's process env, + which inherits from whatever runs the old CLI (shell, provisioning, a unit). It reads no + env file of its own. +- So there is no single env location to set. Candidates and their problems: + - ~/.config/environment.d/*.conf: reaches systemd user services only, not shells. + - shell rc files: Jason's dotfiles, out of scope. + - fleet unit drop-ins or ~/.mosaic / ~/.config/mosaic files: fleet state, read-only for me. +- Alternative inside files Q81 owns: the new wrapper forwards only `__lease-capability` + to ~/.npm-global/bin/mosaic-old. Same effect as the env var (the real old CLI answers), + no env file, removed by undo. Needs Mos's ok, since it differs from the ruling's mechanism. diff --git a/agents/sage/work/q81/mosaic-cli.sh b/agents/sage/work/q81/mosaic-cli.sh new file mode 100755 index 000000000..383620d1e --- /dev/null +++ b/agents/sage/work/q81/mosaic-cli.sh @@ -0,0 +1,138 @@ +#!/usr/bin/env bash +# Q81 (sage-0042): put the mosaic-stack CLI on PATH as `mosaic`. +# +# mosaic-cli.sh install [REF] rename the npm-global `mosaic` to `mosaic-old`, +# add a pinned worktree and the wrapper +# mosaic-cli.sh update [REF] move the pinned worktree to REF +# mosaic-cli.sh undo remove the wrapper and the worktree, rename +# `mosaic-old` back to `mosaic` +# mosaic-cli.sh status what is installed, and whether it drifted +# +# REF defaults to origin/refactor after a fetch, and must already be on +# origin/refactor: the wrapper runs pushed code only, never the working +# checkout. The worktree is a detached linked worktree of the canonical +# checkout, so `mosaic queue` refuses there (the queue runs only in the +# canonical checkout); queue work stays on scripts/mosaic. +# +# Writes only: ~/.npm-global/bin/{mosaic,mosaic-old}, the worktree and +# state directory below, and the canonical checkout's worktree list. +set -euo pipefail + +CANON=/mnt/storage/src/mosaic-stack +BIN="$HOME/.npm-global/bin" +STATE_DIR="$HOME/.local/share/mosaic-cli" +WT="$STATE_DIR/stack" +LOG="$STATE_DIR/install.log" +OLD_TARGET=../lib/node_modules/@mosaicstack/mosaic/dist/cli.js +MARKER="# mosaic-cli wrapper (Q81, agents/sage/work/q81/mosaic-cli.sh)" + +die() { echo "mosaic-cli: $*" >&2; exit 1; } +now() { date -u +%FT%TZ; } +log() { mkdir -p "$STATE_DIR"; echo "$(now) $*" >>"$LOG"; } + +is_wrapper() { [ -f "$BIN/mosaic" ] && [ ! -L "$BIN/mosaic" ] && sed -n 2p "$BIN/mosaic" | grep -qxF "$MARKER"; } +is_old_link() { [ -L "$1" ] && [ "$(readlink "$1")" = "$OLD_TARGET" ]; } + +# Fetch, then resolve REF to a full sha that origin/refactor contains. +pinned_sha() { + local ref="${1:-origin/refactor}" sha + git -C "$CANON" fetch -q origin refactor || die "git fetch origin refactor failed" + sha="$(git -C "$CANON" rev-parse --verify -q "$ref^{commit}")" || die "$ref is not a commit" + git -C "$CANON" merge-base --is-ancestor "$sha" origin/refactor || die "$ref ($sha) is not on origin/refactor; only pushed code is pinned" + echo "$sha" +} + +# npm ci only when the lock differs from what node_modules was built from. +deps() { + if [ -f "$WT/node_modules/.package-lock.json" ] && [ -f "$STATE_DIR/lock.sha256" ] \ + && (cd "$WT" && sha256sum -c --status "$STATE_DIR/lock.sha256"); then + echo "node_modules: lock unchanged, kept" + return + fi + [ -f "$WT/package.json" ] || die "$WT/package.json missing; refusing npm ci (it would walk up)" + (cd "$WT" && npm ci --prefer-offline --no-audit --no-fund >/dev/null) || die "npm ci in $WT failed" + (cd "$WT" && sha256sum package-lock.json) >"$STATE_DIR/lock.sha256" + echo "node_modules: npm ci done" +} + +write_wrapper() { + local tmp + tmp="$(mktemp "$BIN/.mosaic.XXXXXX")" + cat >"$tmp" < $WT/scripts/mosaic at $sha" +} + +cmd_update() { + is_wrapper || die "not installed (no wrapper at $BIN/mosaic)" + local sha; sha="$(pinned_sha "${1:-}")" + [ -z "$(git -C "$WT" status --porcelain --untracked-files=no)" ] || die "$WT has tracked changes; refusing" + git -C "$WT" checkout -q --detach "$sha" + deps + log "update $sha" + echo "updated: $WT at $sha" +} + +cmd_undo() { + is_wrapper || die "$BIN/mosaic is not this wrapper; refusing (status says why)" + is_old_link "$BIN/mosaic-old" || die "$BIN/mosaic-old is not the expected symlink; refusing" + rm -f "$BIN/mosaic" + mv -n "$BIN/mosaic-old" "$BIN/mosaic" + is_old_link "$BIN/mosaic" || die "rename back to mosaic did not happen" + if [ -e "$WT" ]; then git -C "$CANON" worktree remove --force "$WT"; fi + rm -f "$STATE_DIR/lock.sha256" + log "undo" + echo "undone: $BIN/mosaic -> $OLD_TARGET again; worktree removed" +} + +cmd_status() { + local bad=0 sha origin + if is_wrapper; then echo "wrapper: $BIN/mosaic (marker ok)" + elif is_old_link "$BIN/mosaic"; then echo "wrapper: none; $BIN/mosaic is the estate symlink" + [ -e "$WT" ] && { echo "DRIFT: worktree exists but the wrapper is gone (an npm i -g --force?)"; bad=1; } + else echo "DRIFT: $BIN/mosaic is neither the wrapper nor the estate symlink"; bad=1; fi + if is_old_link "$BIN/mosaic-old"; then echo "mosaic-old: $BIN/mosaic-old -> $OLD_TARGET" + elif [ -e "$BIN/mosaic-old" ] || [ -L "$BIN/mosaic-old" ]; then echo "DRIFT: $BIN/mosaic-old is not the expected symlink"; bad=1 + else echo "mosaic-old: none"; fi + if [ -d "$WT" ]; then + sha="$(git -C "$WT" rev-parse HEAD)" + origin="$(git -C "$CANON" rev-parse origin/refactor)" + echo "worktree: $WT at $sha" + if [ "$sha" = "$origin" ]; then echo "pin: origin/refactor (as last fetched)" + else echo "pin: behind origin/refactor $origin by $(git -C "$CANON" rev-list --count "$sha..$origin") commits"; fi + [ -z "$(git -C "$WT" status --porcelain --untracked-files=no)" ] || { echo "DRIFT: worktree has tracked changes"; bad=1; } + else echo "worktree: none"; fi + echo "on PATH: $(command -v mosaic || echo none)" + [ -f "$LOG" ] && echo "last: $(tail -n 1 "$LOG")" + return "$bad" +} + +case "${1:-}" in + install) shift; cmd_install "$@" ;; + update) shift; cmd_update "$@" ;; + undo) cmd_undo ;; + status) cmd_status ;; + *) echo "usage: mosaic-cli.sh install [REF] | update [REF] | undo | status" >&2; exit 4 ;; +esac