fix(installer): harden greenfield detector contracts

This commit is contained in:
2026-08-05 17:46:58 -05:00
parent 99e28d4100
commit 3edde464b3
18 changed files with 924 additions and 190 deletions
+199 -70
View File
@@ -510,27 +510,77 @@ state_action_failed() {
fi
}
state_redact_stream() {
python3 /dev/fd/3 3<<'PY'
import os, re, sys
text = sys.stdin.read()
secret_name = re.compile(r"(?:TOKEN|PASSWORD|PASSWD|SECRET|API_KEY|AUTH|CREDENTIAL|CANARY)", re.I)
secrets = {value for name, value in os.environ.items() if secret_name.search(name) and len(value) >= 4}
for value in sorted(secrets, key=len, reverse=True):
text = text.replace(value, "[REDACTED]")
patterns = (
(re.compile(r"(?im)^(\s*(?:proxy-)?authorization\s*:\s*)[^\r\n]+"), r"\1[REDACTED]"),
(re.compile(r"(?im)^(\s*(?:set-)?cookie\s*:\s*)[^\r\n]+"), r"\1[REDACTED]"),
(re.compile(r"(?i)(Bearer\s+)[^\s'\"]+"), r"\1[REDACTED]"),
(re.compile(r"(?i)((?:[_-]?auth(?:Token)?|token|password|passwd|secret|api[_-]?key)\s*[=:]\s*)[^\s'\"]+"), r"\1[REDACTED]"),
)
for pattern, replacement in patterns:
text = pattern.sub(replacement, text)
url_pattern = re.compile(r"https?://[^\s'\"<>]+", re.I)
def redact_url(match):
url = match.group(0)
scheme_end = url.find("://") + 3
authority_end = len(url)
for separator in "/?#":
position = url.find(separator, scheme_end)
if position != -1:
authority_end = min(authority_end, position)
authority = url[scheme_end:authority_end]
at = authority.rfind("@")
if at != -1:
return url[:scheme_end] + "[REDACTED]@" + authority[at + 1:] + url[authority_end:]
return url
sys.stdout.write(url_pattern.sub(redact_url, text))
PY
}
state_redaction_probe() {
printf '[REDACTION-PROBE] emitted=%s\n' "${MOSAIC_INSTALL_SECRET_CANARY:?redaction probe requires canary}"
}
state_run_captured() {
local label="$1" output status=0
local label="$1" redacted redactor_pid capture_fd status=0 redact_status=0
shift
output="$(mktemp "${TMPDIR:-/tmp}/mosaic-phase-command.XXXXXX.log")" || return
# The command is deliberately called in a conditional so its status can be
# journaled before the caller's ERR trap rolls back. Bash disables errexit in
# functions invoked this way, so every multi-command phase helper below must
# explicitly return on each required command failure.
if "$@" >"$output" 2>&1; then status=0; else status=$?; fi
cat "$output" || { rm -f "$output"; return 1; }
if ! { printf '\n=== %s (exit=%s) ===\n' "$label" "$status"; cat "$output"; } >> "$STATE_COMMAND_LOG"; then
rm -f "$output"
redacted="$(mktemp "${TMPDIR:-/tmp}/mosaic-phase-redacted.XXXXXX")" || return
chmod 0600 "$redacted" || { rm -f "$redacted"; return 1; }
# Process substitution preserves in-shell phase side effects while ensuring
# plaintext diagnostics exist only in a pipe, never in a filesystem body.
exec {capture_fd}> >(state_redact_stream > "$redacted")
redactor_pid=$!
set +e
"$@" >&"$capture_fd" 2>&1
status=$?
exec {capture_fd}>&-
wait "$redactor_pid"
redact_status=$?
set -e
if [[ "$redact_status" -ne 0 ]]; then
rm -f "$redacted"
fail "Could not redact '$label' diagnostics; refusing to expose or persist raw command output."
return 1
fi
cat "$redacted" || { rm -f "$redacted"; return 1; }
if ! { printf '\n=== %s (exit=%s) ===\n' "$label" "$status"; cat "$redacted"; } >> "$STATE_COMMAND_LOG"; then
rm -f "$redacted"
fail "Could not append '$label' output to $STATE_COMMAND_LOG; refusing to continue."
return 1
fi
if ! sync "$STATE_COMMAND_LOG"; then
rm -f "$output"
rm -f "$redacted"
fail "Could not sync '$label' output in $STATE_COMMAND_LOG; refusing to continue."
return 1
fi
rm -f "$output"
rm -f "$redacted"
state_json_line command "$STATE_CURRENT_PHASE" "$([[ "$status" -eq 0 ]] && echo committed || echo failed)" "label=$label output_log=$STATE_COMMAND_LOG exit=$status"
return "$status"
}
@@ -569,6 +619,55 @@ state_target_shell() {
printf '%s' "${shell:-${SHELL:-}}"
}
STATE_POLICY_REASON=""
state_path_owner_mode_ok() {
local path="$1" policy="${2:-shared-read}" uid gid mode mode_value original resolved
original="$path"
[[ -e "$path" ]] || { STATE_POLICY_REASON="$path missing"; return 1; }
if [[ -L "$path" ]]; then
resolved="$(realpath -e -- "$path" 2>/dev/null)" \
|| { STATE_POLICY_REASON="$path has an unresolved symlink target"; return 1; }
if [[ "$resolved" != "$HOME"/* && "$resolved" != "$PREFIX"/* ]]; then
STATE_POLICY_REASON="$path escapes target-owned roots via symlink to $resolved"
return 1
fi
path="$resolved"
fi
uid="$(stat -c '%u' "$path" 2>/dev/null)" || { STATE_POLICY_REASON="$original owner unreadable"; return 1; }
gid="$(stat -c '%g' "$path" 2>/dev/null)" || { STATE_POLICY_REASON="$path group unreadable"; return 1; }
mode="$(stat -c '%a' "$path" 2>/dev/null)" || { STATE_POLICY_REASON="$path mode unreadable"; return 1; }
[[ "$uid" == "$(id -u)" && "$gid" == "$(id -g)" && "$mode" =~ ^[0-7]{3,4}$ ]] \
|| { STATE_POLICY_REASON="$original owner=$uid group=$gid mode=$mode expected=$(id -u):$(id -g)"; return 1; }
mode_value=$((8#$mode))
case "$policy" in
private)
(( (mode_value & 077) == 0 )) \
|| { STATE_POLICY_REASON="$original mode=$mode exposes private material to group/other"; return 1; }
;;
executable)
(( (mode_value & 0111) != 0 && (mode_value & 022) == 0 )) \
|| { STATE_POLICY_REASON="$original mode=$mode is not executable or is group/world-writable"; return 1; }
;;
shared-read)
(( (mode_value & 022) == 0 )) \
|| { STATE_POLICY_REASON="$original mode=$mode is group/world-writable"; return 1; }
;;
*) STATE_POLICY_REASON="unknown owner/mode policy=$policy for $path"; return 1 ;;
esac
}
state_tree_owner_mode_ok() {
local root="$1" path policy
[[ -e "$root" ]] || return 0
while IFS= read -r -d '' path; do
policy=shared-read
case "$path" in
"$MOSAIC_HOME/credentials"|"$MOSAIC_HOME/credentials"/*|"$MOSAIC_HOME/SOUL.md"|"$MOSAIC_HOME/USER.md") policy=private ;;
esac
state_path_owner_mode_ok "$path" "$policy" || return
done < <(find "$root" -xdev -print0)
}
state_resolved_version() {
local cli gateway
if [[ "$FLAG_DEV" == "true" ]]; then
@@ -598,21 +697,37 @@ state_predicate() {
local phase="$1" shell node_major installed expected
local missing=() login_path nonlogin_path broker=false dead_hooks=0
local prefix_parent disk_kb inode_count min_disk_kb min_inodes npm_major privilege_mode
local passwd_row passwd_user passwd_uid passwd_home passwd_shell actual_user actual_uid
STATE_REASON=""
case "$phase" in
P0)
shell="$(state_target_shell)"
actual_uid="$(id -u 2>/dev/null || true)"
actual_user="$(id -un 2>/dev/null || true)"
passwd_row="$(getent passwd "$actual_uid" 2>/dev/null || true)"
IFS=: read -r passwd_user _ passwd_uid _ _ passwd_home passwd_shell <<<"$passwd_row"
shell="$passwd_shell"
node_major="$(node -p 'Number(process.versions.node.split(".")[0])' 2>/dev/null || echo 0)"
npm_major="$(npm --version 2>/dev/null | cut -d. -f1 || echo 0)"
privilege_mode="$([[ "$(id -u)" -eq 0 ]] && echo root-without-explicit-target || echo user)"
if [[ -n "$HOME" && -n "$shell" && "$privilege_mode" == "user" && "$(uname -s)" == "Linux" ]] \
if [[ "$actual_uid" == 0 && -n "${SUDO_USER:-}" ]]; then
privilege_mode="sudo-with-inherited-home"
elif [[ "$actual_uid" == 0 ]]; then
privilege_mode="root-without-explicit-target"
else
privilege_mode="user"
fi
if [[ -z "$passwd_row" || "$actual_uid" != "$passwd_uid" || "$actual_user" != "$passwd_user" \
|| -z "$passwd_home" || "$HOME" != "$passwd_home" ]]; then
STATE_REASON="unsupported or unresolved target account: HOME mismatch or passwd identity mismatch (target=${actual_user:-unknown} uid=${actual_uid:-unknown} HOME=${HOME:-unset} passwd_user=${passwd_user:-unset} passwd_uid=${passwd_uid:-unset} passwd_HOME=${passwd_home:-unset} shell=${passwd_shell:-unset} privilege=$privilege_mode)"
return 1
fi
if [[ -n "$shell" && "$privilege_mode" == "user" && "$(uname -s)" == "Linux" ]] \
&& ldd --version 2>&1 | grep -i 'glibc\|gnu libc' >/dev/null \
&& [[ "$(uname -m)" == "x86_64" ]] && [[ "$node_major" -ge 20 ]] && [[ "$npm_major" -ge 9 ]] \
&& state_validate_target_paths; then
STATE_REASON="target=$(id -un) uid=$(id -u) HOME=$HOME shell=$shell privilege=$privilege_mode arch=x86_64 libc=glibc node=$(node --version) npm=$(npm --version)"
STATE_REASON="target=$actual_user uid=$actual_uid HOME=$HOME passwd_HOME=$passwd_home shell=$shell privilege=$privilege_mode arch=x86_64 libc=glibc node=$(node --version) npm=$(npm --version)"
return 0
fi
STATE_REASON="unsupported, unresolved, or unsafe context (target=$(id -un 2>/dev/null || echo unknown) uid=$(id -u) HOME=${HOME:-unset} shell=${shell:-unset} privilege=$privilege_mode arch=$(uname -m 2>/dev/null || echo unknown) node_major=$node_major npm_major=$npm_major path_check=${STATE_PATH_REASON:-not-reached})"
STATE_REASON="unsupported, unresolved, or unsafe context (target=${actual_user:-unknown} uid=${actual_uid:-unknown} HOME=${HOME:-unset} passwd_HOME=${passwd_home:-unset} shell=${shell:-unset} privilege=$privilege_mode arch=$(uname -m 2>/dev/null || echo unknown) node_major=$node_major npm_major=$npm_major path_check=${STATE_PATH_REASON:-not-reached})"
return 1
;;
P1)
@@ -684,11 +799,12 @@ state_predicate() {
expected="$(state_expected_cli_version)"
installed=""
[[ -x "$PREFIX/bin/mosaic" ]] && installed="$("$PREFIX/bin/mosaic" --version 2>&1 | tail -n 1 | tr -d '\r' || true)"
if [[ -n "$expected" && -x "$PREFIX/bin/mosaic" && "$installed" == "$expected" ]]; then
STATE_REASON="absolute_path=$PREFIX/bin/mosaic version=$installed equals resolved lane version"
if [[ -n "$expected" && -x "$PREFIX/bin/mosaic" && "$installed" == "$expected" ]] \
&& state_path_owner_mode_ok "$PREFIX/bin/mosaic" executable; then
STATE_REASON="absolute_path=$PREFIX/bin/mosaic version=$installed equals resolved lane version; owner/mode policy satisfied"
return 0
fi
STATE_REASON="absolute_path=$PREFIX/bin/mosaic executable=$([[ -x "$PREFIX/bin/mosaic" ]] && echo yes || echo no) got=${installed:-missing} expected=${expected:-unresolved}"
STATE_REASON="absolute_path=$PREFIX/bin/mosaic executable=$([[ -x "$PREFIX/bin/mosaic" ]] && echo yes || echo no) got=${installed:-missing} expected=${expected:-unresolved}; unsafe owner/group/mode=${STATE_POLICY_REASON:-not-evaluated}"
return 1
;;
P4)
@@ -699,6 +815,10 @@ state_predicate() {
local expected_lane expected_version
expected_lane="$([[ "$FLAG_NEXT" == true ]] && echo next || echo latest)"
expected_version="$(state_expected_cli_version)"
if [[ -e "$MOSAIC_HOME" ]] && ! state_tree_owner_mode_ok "$MOSAIC_HOME"; then
STATE_REASON="framework created-path owner/mode policy failed: $STATE_POLICY_REASON"
return 1
fi
if [[ ! -s "$declared_set" ]]; then
STATE_REASON="NOT-MEASURED / UNDECLARED: installer published no checkout-free, lane/versioned shipped-set artifact at $declared_set"
return 1
@@ -738,12 +858,14 @@ NODE
for skill in SOUL.md USER.md; do
local path="$MOSAIC_HOME/$skill"
if [[ ! -s "$path" ]] || ! grep -q '^# ' "$path" 2>/dev/null \
|| [[ "$(stat -c '%u' "$path" 2>/dev/null || echo -1)" != "$(id -u)" ]] \
|| [[ "$(stat -c '%a' "$path" 2>/dev/null || echo 777)" =~ [2367]$ ]]; then
|| ! state_path_owner_mode_ok "$path" private; then
missing+=("$skill")
fi
done
if [[ "${#missing[@]}" -eq 0 ]]; then STATE_REASON="SOUL.md and USER.md parse and have target owner/mode"; return 0; fi
if [[ -e "$MOSAIC_HOME/credentials" ]] && ! state_tree_owner_mode_ok "$MOSAIC_HOME/credentials"; then
missing+=("credentials(owner/mode=$STATE_POLICY_REASON)")
fi
if [[ "${#missing[@]}" -eq 0 ]]; then STATE_REASON="SOUL.md and USER.md parse and have private target owner/mode; credential paths are private"; return 0; fi
STATE_REASON="identity missing, empty, malformed, wrong-owner, or unsafe-mode: ${missing[*]}"
return 1
;;
@@ -863,7 +985,8 @@ state_validate_target_paths() {
}
state_snapshot_create() {
local dst list path key index=0
local dst list path key index=0 parent parent_list parent_status
local -A recorded_parents=()
if ! state_validate_target_paths; then
fail "P1 Preflight refused snapshot creation: $STATE_PATH_REASON"
return 1
@@ -871,7 +994,9 @@ state_snapshot_create() {
STATE_SNAPSHOT_DIR="$STATE_RUN_DIR/snapshot"
mkdir -p "$STATE_SNAPSHOT_DIR/data"
list="$STATE_SNAPSHOT_DIR/paths.tsv"
parent_list="$STATE_SNAPSHOT_DIR/parents.tsv"
: > "$list"
: > "$parent_list"
for path in "$MOSAIC_HOME" "$PREFIX" "$HOME/.npmrc" "$HOME/.bashrc" "$HOME/.bash_profile" \
"$HOME/.profile" "$HOME/.zshrc" "$HOME/.config/fish/config.fish" "$HOME/.claude" \
"$HOME/.pi" "$HOME/.codex" "$HOME/.config/opencode" "$HOME/.config/mosaic-gateway" \
@@ -886,12 +1011,22 @@ state_snapshot_create() {
else
printf 'absent\t%s\t%s\n' "$path" "$key" >> "$list"
fi
parent="$(dirname "$path")"
while [[ "$parent" != "$HOME" && "$parent" == "$HOME"/* ]]; do
if [[ -z "${recorded_parents[$parent]:-}" ]]; then
recorded_parents[$parent]=1
parent_status=absent
[[ -d "$parent" ]] && parent_status=present
printf '%s\t%s\n' "$parent_status" "$parent" >> "$parent_list"
fi
parent="$(dirname "$parent")"
done
done
state_json_line snapshot P1 committed "pre-install snapshot=$STATE_SNAPSHOT_DIR"
}
state_snapshot_restore() {
local status target key saved
local status target key saved parent
[[ -s "$STATE_SNAPSHOT_DIR/paths.tsv" ]] || return 1
while IFS=$'\t' read -r status target key; do
[[ -n "$target" ]] || continue
@@ -906,6 +1041,19 @@ state_snapshot_restore() {
cp -a "$saved" "$target" || return
fi
done < "$STATE_SNAPSHOT_DIR/paths.tsv"
# Mutating a previously absent nested target can leave empty parents behind
# after the target itself is restored. Remove only parents proven absent in
# the pre-install snapshot; repeated passes handle arbitrary nesting without
# depending on GNU tac/sort behavior.
if [[ -s "$STATE_SNAPSHOT_DIR/parents.tsv" ]]; then
for _ in {1..16}; do
while IFS=$'\t' read -r status parent; do
[[ "$status" == absent ]] || continue
[[ "$parent" != "$HOME" && "$parent" == "$HOME"/* ]] || return 1
rmdir "$parent" 2>/dev/null || true
done < "$STATE_SNAPSHOT_DIR/parents.tsv"
done
fi
}
state_begin_install() {
@@ -980,44 +1128,19 @@ state_mark_resumable_failure() {
echo " Remediation: fix each named phase, then run this installer with --check; journal: $STATE_JOURNAL" >&2
}
state_self_test() {
local phase path
state_begin_install
state_snapshot_create
trap 'state_handle_unexpected_failure "$?" "$STATE_CURRENT_PHASE"' ERR INT TERM
for phase in P2 P3 P4 P5 P6 P7 P8; do
state_phase_begin "$phase"
case "$phase" in
P2) path="$MOSAIC_HOME/.selftest-artifact" ;;
P3) path="$PREFIX/bin/mosaic" ;;
P4) path="$MOSAIC_HOME/.selftest-framework" ;;
P5) path="$MOSAIC_HOME/SOUL.md" ;;
P6) path="$HOME/.claude/settings.json" ;;
P7) path="$MOSAIC_HOME/.selftest-service" ;;
P8) path="$HOME/.bashrc" ;;
esac
state_record_mutation "$phase" "$path" "restore representative path from $STATE_SNAPSHOT_DIR"
mkdir -p "$(dirname "$path")"
printf 'mutated-by-%s\n' "$phase" > "$path"
state_phase_finish "$phase" committed "representative mutation committed"
if [[ "${MOSAIC_INSTALL_FAULT_AFTER:-}" == "$phase" ]]; then
state_json_line fault "$phase" injected "phase=$phase"
echo "Injected installer fault: phase=$phase" >&2
state_snapshot_restore
state_json_line install "$phase" rolled-back "fault injection restored pre-install snapshot"
state_write_active "$(printf '{\"status\":\"rolled-back\",\"phase\":\"%s\",\"journal\":\"%s\"}' "$phase" "$STATE_JOURNAL")"
exit 97
fi
done
fail "self-test requires MOSAIC_INSTALL_FAULT_AFTER=P2..P8"
exit 2
state_maybe_inject_fault() {
local phase="$1"
[[ "${MOSAIC_INSTALL_FAULT_AFTER:-}" == "$phase" ]] || return 0
state_json_line fault "$phase" injected "phase=$phase after real phase action"
echo "Injected installer fault after real action: phase=$phase" >&2
state_handle_unexpected_failure 97 "$phase"
}
resolve_source_commit() {
local encoded_ref body headers content_type
encoded_ref="$(node -p 'encodeURIComponent(process.argv[1])' "$GIT_REF")"
body="$(mktemp "${TMPDIR:-/tmp}/mosaic-ref.XXXXXX.json")" || return
headers="$(mktemp "${TMPDIR:-/tmp}/mosaic-ref.XXXXXX.headers")" || { rm -f "$body"; return 1; }
body="$(mktemp "${TMPDIR:-/tmp}/mosaic-ref-body.XXXXXX")" || return
headers="$(mktemp "${TMPDIR:-/tmp}/mosaic-ref-headers.XXXXXX")" || { rm -f "$body"; return 1; }
if ! curl -fsSL -D "$headers" -o "$body" \
"https://git.mosaicstack.dev/api/v1/repos/mosaicstack/stack/commits?sha=${encoded_ref}&limit=1"; then
rm -f "$body" "$headers"
@@ -1252,9 +1375,10 @@ install_next_cli_from_registry() {
# ─── preflight / state-machine dispatch ──────────────────────────────────────
if [[ "$FLAG_STATE_SELF_TEST" == "true" ]]; then
require_cmd node
require_cmd flock
state_self_test
if [[ "${MOSAIC_INSTALL_SELF_TEST_ALLOW:-0}" != 1 || ! "${MOSAIC_INSTALL_FAULT_AFTER:-}" =~ ^P[2-8]$ ]]; then
fail "state-machine self-test requires MOSAIC_INSTALL_SELF_TEST_ALLOW=1 and MOSAIC_INSTALL_FAULT_AFTER=P2..P8"
exit 2
fi
fi
# `--check` exits before mkdir, npm-prefix setup, locks, snapshots, downloads, or
@@ -1306,6 +1430,9 @@ state_phase_finish P0 committed "$P0_REASON"
state_phase_finish P1 committed "$P1_REASON; exclusive lock acquired; journal opened"
state_snapshot_create
trap 'state_handle_unexpected_failure "$?" "$STATE_CURRENT_PHASE"' ERR INT TERM
if [[ "${MOSAIC_INSTALL_REDACTION_PROBE:-0}" == 1 ]]; then
state_run_captured "credential redaction acceptance probe" state_redaction_probe
fi
state_phase_begin P2
state_record_mutation P2 "$STATE_RUN_DIR/work" "discard acquired temporary artifacts"
@@ -1322,6 +1449,7 @@ if [[ "$FLAG_FRAMEWORK" == "true" || "$FLAG_DEV" == "true" ]]; then
state_run_captured "P2 acquire pinned source archive" ensure_monorepo
fi
state_phase_finish P2 committed "lane=$([[ "$FLAG_NEXT" == true ]] && echo next || echo latest) cli_version=${RESOLVED_CLI_VERSION:-pending-source-package-build} source_commit=${RESOLVED_SOURCE_COMMIT:-deferred-until-source-fallback} sha256=${RESOLVED_SOURCE_DIGEST:-deferred-until-source-fallback}"
state_maybe_inject_fault P2
# ═══════════════════════════════════════════════════════════════════════════════
# PART 1: Framework (bash launcher + guides + runtime configs + tools)
@@ -1366,6 +1494,7 @@ if [[ "$FLAG_FRAMEWORK" == "true" ]]; then
# Run the framework's own install.sh (handles keep/overwrite for SOUL.md etc.)
info "Installing framework to ${MOSAIC_HOME}"
MOSAIC_INSTALL_MODE="${MOSAIC_INSTALL_MODE:-keep}" \
MOSAIC_CLI_PATH="$PREFIX/bin/mosaic" \
MOSAIC_ALLOW_MISSING_SEQUENTIAL_THINKING=1 \
MOSAIC_SKIP_SKILLS_SYNC="${MOSAIC_SKIP_SKILLS_SYNC:-0}" \
bash "$FRAMEWORK_SRC/install.sh" || return
@@ -1539,6 +1668,7 @@ else
fail "P3 Install CLI failed: $STATE_REASON"
false
fi
state_maybe_inject_fault P3
state_phase_begin P4
if [[ "$FLAG_FRAMEWORK" == "true" ]]; then
@@ -1550,6 +1680,7 @@ if [[ "$FLAG_FRAMEWORK" == "true" ]]; then
state_record_mutation P4 "$HOME/.local/state/mosaic/backups" "restore framework backup state from $STATE_SNAPSHOT_DIR"
state_record_mutation P6 "$HOME/.claude/settings.json" "restore activation settings from $STATE_SNAPSHOT_DIR"
fi
export MOSAIC_CLI_PATH="$PREFIX/bin/mosaic"
state_run_captured "P4 install framework and skills; P6 evaluate activation" install_phase_p4_action
if [[ "$FLAG_FRAMEWORK" == "false" ]]; then
state_phase_finish P4 not-requested "framework component excluded by --cli"
@@ -1560,6 +1691,7 @@ else
# absent. Keep the partial state for P5-P8 diagnostics; P9 fails non-zero.
state_phase_finish P4 failed-resumable "$STATE_REASON"
fi
state_maybe_inject_fault P4
# P5/P7 actions (wizard/service requests) live in the summary flow below and
# bind their mutation records immediately before the wizard executes. P8 is
@@ -1579,7 +1711,7 @@ if [[ "$FLAG_CHECK" == "false" ]]; then
# First install guidance / auto-launch
if [[ ! -f "$MOSAIC_HOME/SOUL.md" ]]; then
echo ""
if [[ "$FLAG_NO_AUTO_LAUNCH" == "false" ]] && [[ -t 0 ]] && [[ -t 1 ]]; then
if [[ "$FLAG_NO_AUTO_LAUNCH" == "false" ]] && { { [[ -t 0 ]] && [[ -t 1 ]]; } || [[ "$FLAG_STATE_SELF_TEST" == true ]]; }; then
# Interactive TTY and auto-launch not suppressed: run the unified wizard.
# `mosaic wizard` now runs the full first-run flow end-to-end: identity
# setup → runtimes → hooks preview → skills → finalize → gateway
@@ -1589,15 +1721,11 @@ if [[ "$FLAG_CHECK" == "false" ]]; then
MOSAIC_BIN="$PREFIX/bin/mosaic"
if ! command -v "$MOSAIC_BIN" &>/dev/null && ! command -v mosaic &>/dev/null; then
warn "mosaic binary not found on PATH — skipping auto-launch."
warn "Add $PREFIX/bin to PATH and run: mosaic wizard"
if [[ ! -x "$MOSAIC_BIN" ]]; then
warn "P3 absolute mosaic binary is unavailable — skipping auto-launch."
warn "Repair $MOSAIC_BIN and run it with: $MOSAIC_BIN wizard"
else
# Prefer the absolute path from the prefix we just installed to
MOSAIC_CMD="mosaic"
if [[ -x "$MOSAIC_BIN" ]]; then
MOSAIC_CMD="$MOSAIC_BIN"
fi
MOSAIC_CMD="$MOSAIC_BIN"
state_record_mutation P5 "$MOSAIC_HOME/SOUL.md" "restore identity from $STATE_SNAPSHOT_DIR"
state_record_mutation P5 "$MOSAIC_HOME/USER.md" "restore identity from $STATE_SNAPSHOT_DIR"
@@ -1741,6 +1869,7 @@ if [[ "$FLAG_CHECK" == "false" ]]; then
else
state_phase_finish "$phase" failed-resumable "$STATE_REASON"
fi
state_maybe_inject_fault "$phase"
done
echo ""