151 lines
6.1 KiB
Bash
Executable File
151 lines
6.1 KiB
Bash
Executable File
#!/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, and then only when the candidate is a trusted-shape
|
|
# absolute path: relative HOME and parent-escape (..) components
|
|
# are refused outright, and every remaining component must be a
|
|
# non-symlink (secure descriptor traversal). Refused candidates
|
|
# are never executed.
|
|
# 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
|
|
}
|
|
|
|
# True when any component of an ABSOLUTE candidate path is a symlink. Only
|
|
# ever called after fallback_candidate_usable's absolute-shape check.
|
|
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
|
|
}
|
|
|
|
# Reject the untrusted $HOME fallback candidate unless it is a trusted-shape
|
|
# absolute path: absolute, no parent-escape (..) components, and no symlink
|
|
# components anywhere on the path. Every rejection is named on stderr so the
|
|
# typed failure explains itself. This is the launcher's descriptor guard; the
|
|
# suite's mutation control (guard bypassed) must plant-exec, proving the guard
|
|
# is what stands between a hostile HOME and code execution.
|
|
fallback_candidate_usable() {
|
|
local candidate="$1"
|
|
case "$candidate" in
|
|
/*) ;;
|
|
*)
|
|
echo "mosaic: refusing \$HOME candidate $candidate: relative path is untrusted without a passwd home" >&2
|
|
return 1
|
|
;;
|
|
esac
|
|
if printf '%s' "$candidate" | grep -qE '(^|/)\.\.(/|$)'; then
|
|
echo "mosaic: refusing \$HOME candidate $candidate: parent-escape component" >&2
|
|
return 1
|
|
fi
|
|
if path_has_symlink_component "$candidate"; then
|
|
echo "mosaic: refusing \$HOME candidate $candidate: symlink component (untrusted without a passwd home)" >&2
|
|
return 1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
# 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 fallback_candidate_usable "$home_candidate"; then
|
|
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)"
|