fleet: split-home-safe mosaic launcher (T110, P5-RM-009 stack side)
ci/woodpecker/pr/ci Pipeline was canceled

Ships fleet/bin/mosaic in the framework so the T106 shipped-first launcher
contract survives split-home layouts. The shipped npm mosaic resolves from the
passwd real home instead of $HOME: under a seat home (HOME=seat dir) the
operator's npm prefix is still found, so 'HOME=<seat-home> fleet/bin/mosaic
--version' returns the shipped version. The $HOME/.npm-global candidate is
consulted only when the passwd lookup fails, and then every path component
must be a non-symlink (secure descriptor traversal; a planted descriptor is
refused without execution).

T106 behavior preserved verbatim: worktree builds are MOSAIC_CLI_WORKTREE
opt-in only, health-checked, shipped fallback on doubt; MOSAIC_FLEET_CLI_OFF
forces pure pass-through; no runnable candidate exits 127 with the documented
stderr line; stale worktree candidates never regain default precedence
(without the opt-in env the worktree path is never read).

Manifest: fleet/bin/** added to the [framework] section so the updater and
installer both own the subtree (manifest-parity 17/17 green). Hermetic suite
test-mosaic-launcher.sh covers the split-home positive, typed failure,
stale-worktree non-precedence, opt-in healthy/unhealthy, OFF pass-through, and
descriptor-refusal arms. Marker T110-DISPATCH-TOPHER-1N2O.
This commit is contained in:
topher
2026-08-29 19:14:28 -05:00
parent 94d626dff9
commit 7e9e013f92
3 changed files with 239 additions and 0 deletions
+123
View File
@@ -0,0 +1,123 @@
#!/usr/bin/env bash
# mosaic — fleet launcher (shipped-first, split-home safe).
#
# T110 / P5-RM-009 stack side. Carries the T106 brain launcher contract
# (shipped-first, worktree dev opt-in, OFF pass-through, typed failure) with
# one split-home correction: the SHIPPED npm mosaic is resolved from the real
# user's home (passwd database), never from $HOME. Under split-home seat
# layouts HOME is a seat home: it carries no npm prefix, and a
# $HOME/.npm-global there would be a plantable descriptor, so the $HOME
# candidate is consulted only when the passwd lookup itself fails, and then
# only with a full symlink-component refusal (secure descriptor traversal).
#
# Contract:
# 1. SHIPPED npm mosaic is the default. Candidate order:
# a. <real-home>/.npm-global/bin/mosaic — real home from the passwd
# database. The final component may be npm's own bin symlink into
# lib/node_modules; that indirection is npm's layout, not a plant.
# b. $HOME/.npm-global/bin/mosaic — ONLY when the passwd lookup
# fails. Untrusted: every path component must be a non-symlink or
# the candidate is refused without execution.
# 2. Worktree build is DEV OPT-IN: used only when MOSAIC_CLI_WORKTREE is
# explicitly set. Health-checked via --version; ANY doubt (absent,
# unreadable, or failing) falls back to the shipped npm mosaic with a
# warning on stderr. With no environment set, worktree candidates are
# never consulted — stale worktree builds cannot regain precedence.
# 3. MOSAIC_FLEET_CLI_OFF keeps its pass-through semantics: set (any
# value) forces pure pass-through. The dev path is not consulted even
# when MOSAIC_CLI_WORKTREE is also set.
# 4. Typed failure: with no runnable candidate the launcher prints one
# stderr line naming what was checked and exits 127.
# 5. NEVER writes to the mosaic home or the npm prefix. Deployment to the
# fleet goes through the real channel (PR to next -> mosaic update).
#
# Env:
# MOSAIC_CLI_WORKTREE dev opt-in: path to a stack worktree whose
# packages/mosaic/dist/cli.js is used (health-checked,
# shipped fallback on doubt)
# MOSAIC_FLEET_CLI_OFF set (any value) to force pure pass-through
#
# Component walk note: the descriptor guard splits on "/" without quoting so
# multi-byte HOME paths with spaces are not supported for the FALLBACK
# candidate; the passwd candidate needs no walk (trusted derivation).
set -u
fail() {
echo "mosaic: $*" >&2
exit 127
}
# Real user home from the passwd database (HOME-independent).
real_home() {
getent passwd "$(id -u)" 2>/dev/null | cut -d: -f6
}
# Refuse when any component of the candidate path is a symlink. Used only for
# the untrusted $HOME fallback candidate (contract 1b).
path_has_symlink_component() {
local path="$1" dir base acc="" part
dir="$(dirname -- "$path")"
base="$(basename -- "$path")"
local IFS='/'
for part in $dir; do
acc="$acc/$part"
[ -L "$acc" ] && return 0
done
[ -L "$dir/$base" ] && return 0
return 1
}
# Print shipped candidates in contract order. Refusals are reported on stderr
# so the typed failure names the cause.
shipped_candidates() {
local rh home_candidate
rh="$(real_home)"
if [ -n "$rh" ]; then
printf '%s\n' "$rh/.npm-global/bin/mosaic"
return 0
fi
# passwd lookup failed: the only fallback is $HOME, descriptor-guarded.
if [ -n "${HOME:-}" ]; then
home_candidate="$HOME/.npm-global/bin/mosaic"
if path_has_symlink_component "$home_candidate"; then
echo "mosaic: refusing \$HOME candidate $home_candidate: symlink component (untrusted without a passwd home)" >&2
else
printf '%s\n' "$home_candidate"
fi
fi
return 0
}
resolve_shipped() {
local candidate
while IFS= read -r candidate; do
[ -n "$candidate" ] || continue
if [ -x "$candidate" ]; then
printf '%s\n' "$candidate"
return 0
fi
done < <(shipped_candidates)
return 1
}
# Dev opt-in only: an explicit MOSAIC_CLI_WORKTREE reaches the worktree build,
# and pure pass-through (MOSAIC_FLEET_CLI_OFF) outranks it.
if [ -n "${MOSAIC_CLI_WORKTREE:-}" ] && [ -z "${MOSAIC_FLEET_CLI_OFF:-}" ]; then
CLI="$MOSAIC_CLI_WORKTREE/packages/mosaic/dist/cli.js"
if [ -r "$CLI" ]; then
if v="$(node "$CLI" --version 2>/dev/null)" && [ -n "$v" ]; then
exec node "$CLI" "$@"
fi
echo "mosaic: worktree build at $CLI failed its health check; using shipped npm mosaic" >&2
else
echo "mosaic: worktree build at $CLI absent or unreadable; using shipped npm mosaic" >&2
fi
fi
SHIPPED="$(resolve_shipped)" || true
if [ -n "${SHIPPED:-}" ]; then
exec "$SHIPPED" "$@"
fi
fail "no runnable CLI (shipped npm mosaic absent from the passwd-home npm prefix and \$HOME; worktree build requires MOSAIC_CLI_WORKTREE)"
+115
View File
@@ -0,0 +1,115 @@
#!/usr/bin/env bash
# Hermetic suite for the fleet/bin/mosaic launcher (T110 / P5-RM-009).
#
# Arms cover the plan acceptance: split-home shipped-first positive, typed
# failure on missing candidates, stale-worktree non-precedence, OFF
# pass-through, and secure-descriptor refusal on the untrusted $HOME
# fallback. No network, no real npm install, no node package build: the
# "shipped mosaic" is a stub script and getent is PATH-stubbed (set
# GETENT_STUB=fail to make the passwd lookup fail, exercising the guarded
# $HOME fallback).
set -euo pipefail
SCRIPT_DIR=$(cd -- "$(dirname -- "$0")" && pwd)
LAUNCHER="$SCRIPT_DIR/mosaic"
fail() {
echo "FAIL: $*" >&2
exit 1
}
[ -f "$LAUNCHER" ] || fail "missing launcher"
[ -x "$LAUNCHER" ] || fail "launcher is not executable"
bash -n "$LAUNCHER" || fail "launcher fails bash -n"
WORK=$(mktemp -d)
cleanup() { rm -rf "$WORK"; }
trap cleanup EXIT
REAL_HOME="$WORK/real-home"
SEAT_HOME="$WORK/seat-home"
STUB_BIN="$WORK/stub-bin"
mkdir -p "$REAL_HOME/.npm-global/bin" "$SEAT_HOME" "$STUB_BIN"
cat >"$REAL_HOME/.npm-global/bin/mosaic" <<'SH'
#!/bin/sh
echo "0.0.0-shipped-stub"
SH
chmod +x "$REAL_HOME/.npm-global/bin/mosaic"
# PATH-stubbed getent: reports the real home for the current uid, unless
# GETENT_STUB=fail is in the launcher environment (exercises the guarded
# $HOME fallback path).
cat >"$STUB_BIN/getent" <<SH
#!/bin/sh
if [ "\${GETENT_STUB:-}" = "fail" ]; then exit 2; fi
if [ "\$1" = "passwd" ]; then
echo "stub:x:$(id -u):$(id -g):stub:$REAL_HOME:/bin/sh"
exit 0
fi
exit 2
SH
chmod +x "$STUB_BIN/getent"
run_launcher() { # run_launcher <home> [VAR=value ...] -- [args...]
local home="$1"; shift
[ "${1:-}" = "--" ] && shift
env -i PATH="$STUB_BIN:/usr/bin:/bin" HOME="$home" TERM="${TERM:-dumb}" "$LAUNCHER" "$@"
}
# A1 — acceptance 1: split-home positive. HOME is an empty seat home; the
# shipped mosaic resolves through the passwd real home.
out="$(printf '' | run_launcher "$SEAT_HOME" -- --version)"
[ "$out" = "0.0.0-shipped-stub" ] || fail "A1 split-home positive: got '$out', want shipped stub version"
# A3 — acceptance 3: a stale worktree build is NEVER consulted without the
# explicit opt-in, even when a worktree exists on disk.
WT="$WORK/stale-wt"
mkdir -p "$WT/packages/mosaic/dist"
printf 'console.log("0.0.0-stale-worktree")\n' >"$WT/packages/mosaic/dist/cli.js"
out="$(printf '' | run_launcher "$SEAT_HOME" -- --version)"
[ "$out" = "0.0.0-shipped-stub" ] || fail "A3 stale worktree regained precedence without opt-in: got '$out'"
# A2 (opt-in healthy) — explicit MOSAIC_CLI_WORKTREE reaches the worktree.
out="$(printf '' | env MOSAIC_CLI_WORKTREE="$WT" HOME="$SEAT_HOME" PATH="$STUB_BIN:/usr/bin:/bin" "$LAUNCHER" --version)"
[ "$out" = "0.0.0-stale-worktree" ] || fail "A2 opt-in worktree not used: got '$out'"
# A2b (opt-in unhealthy) — absent dist falls back to shipped with a warning.
out2="$(printf '' | env MOSAIC_CLI_WORKTREE="$WORK/empty-wt" HOME="$SEAT_HOME" PATH="$STUB_BIN:/usr/bin:/bin" "$LAUNCHER" --version 2>/dev/null)"
[ "$out2" = "0.0.0-shipped-stub" ] || fail "A2b unhealthy worktree fallback output: '$out2'"
err2="$(printf '' | env MOSAIC_CLI_WORKTREE="$WORK/empty-wt" HOME="$SEAT_HOME" PATH="$STUB_BIN:/usr/bin:/bin" "$LAUNCHER" --version 2>&1 >/dev/null)"
case "$err2" in *"absent or unreadable"*|*"health check"*) ;; *) fail "A2b unhealthy worktree fallback warning missing: '$err2'" ;; esac
# A4 — OFF pass-through: worktree opt-in is ignored when OFF is set.
out="$(printf '' | env MOSAIC_FLEET_CLI_OFF=1 MOSAIC_CLI_WORKTREE="$WT" HOME="$SEAT_HOME" PATH="$STUB_BIN:/usr/bin:/bin" "$LAUNCHER" --version)"
[ "$out" = "0.0.0-shipped-stub" ] || fail "A4 OFF did not force pass-through: got '$out'"
# A5 — acceptance 4: typed failure when no candidate exists (passwd lookup
# fails, seat home carries no npm prefix). Expect 127 + documented message.
set +e
err="$(printf '' | env GETENT_STUB=fail HOME="$SEAT_HOME" PATH="$STUB_BIN:/usr/bin:/bin" "$LAUNCHER" --version 2>&1 >/dev/null)"
rc=$?
set -e
[ "$rc" = "127" ] || fail "A5 typed failure rc: got $rc, want 127"
case "$err" in *"no runnable CLI"*) ;; *) fail "A5 typed failure message missing: '$err'" ;; esac
# A6 — secure descriptor traversal: passwd lookup fails and a symlink-planted
# $HOME/.npm-global is refused without execution.
PLANT="$WORK/planted-target"
mkdir -p "$PLANT/.npm-global/bin"
cat >"$PLANT/.npm-global/bin/mosaic" <<SH
#!/bin/sh
touch "$WORK/planted-sentinel"
echo "0.0.0-planted"
SH
chmod +x "$PLANT/.npm-global/bin/mosaic"
ln -s "$PLANT" "$SEAT_HOME/.npm-global"
set +e
err="$(printf '' | env GETENT_STUB=fail HOME="$SEAT_HOME" PATH="$STUB_BIN:/usr/bin:/bin" "$LAUNCHER" --version 2>&1)"
rc=$?
set -e
[ "$rc" = "127" ] || fail "A6 planted descriptor was followed (rc $rc, out '$err')"
case "$err" in *"symlink component"*) ;; *) fail "A6 refusal diagnostic missing: '$err'" ;; esac
[ ! -e "$WORK/planted-sentinel" ] || fail "A6 planted mosaic EXECUTED"
echo "mosaic launcher suite: all arms passed"
@@ -47,6 +47,7 @@ templates/**
tools/** tools/**
# Fleet: only the framework-seeded fleet subtrees are framework-owned. # Fleet: only the framework-seeded fleet subtrees are framework-owned.
fleet/README.md fleet/README.md
fleet/bin/**
fleet/examples/** fleet/examples/**
fleet/profiles/** fleet/profiles/**
fleet/roles/** fleet/roles/**