#!/usr/bin/env bash # Canonical fleet-pane PATH construction and executable reachability checks. # # This file is both sourceable by start-agent-session.sh and executable by the # fleet CLI preflight. Keep the pane PATH in one implementation: provisioning # checks and the eventual pane must answer the same question. mosaic_fleet_pane_home() { local mosaic_home="$1" local fallback_home="$2" case "$mosaic_home" in */.config/mosaic) printf '%s' "${mosaic_home%/.config/mosaic}" ;; *) printf '%s' "$fallback_home" ;; esac } mosaic_fleet_build_runtime_bin_prefix() { local pane_home="$1" local runtime_bin="${2:-}" local candidates=() if [ -n "$runtime_bin" ]; then candidates+=("$runtime_bin"); fi if command -v npm >/dev/null 2>&1; then local npm_prefix npm_prefix=$(npm config get prefix 2>/dev/null) || true if [ -n "$npm_prefix" ]; then candidates+=("${npm_prefix}/bin"); fi fi candidates+=("$pane_home/.npm-global/bin" "$pane_home/.local/bin") local prefix="" dir for dir in "${candidates[@]}"; do [ -d "$dir" ] || continue case ":${prefix}:" in *":${dir}:"*) ;; *) prefix="${prefix:+$prefix:}$dir" ;; esac done printf '%s' "$prefix" } mosaic_fleet_build_pane_path() { local pane_home="$1" local runtime_bin="${2:-}" local system_path="${3:-/usr/local/bin:/usr/bin:/bin}" local prefix prefix=$(mosaic_fleet_build_runtime_bin_prefix "$pane_home" "$runtime_bin") printf '%s' "${prefix:+${prefix}:}${system_path}" } mosaic_fleet_resolve_in_pane_path() { local pane_path="$1" local binary="$2" PATH="$pane_path" command -v -- "$binary" 2>/dev/null } # Sets executable evidence in MOSAIC_FLEET_EXECUTABLE_* and returns nonzero when # a resolved script's shebang interpreter cannot run in the pane. Native/ELF # binaries have no PATH-resolved interpreter dependency and pass the executable # bit check. Node receives an additional side-effect-free `node --version` # execution check; invoking `mosaic --version` itself is intentionally avoided # because Mosaic performs a cache-writing/network update check at CLI startup. mosaic_fleet_check_resolved_executable() { local pane_path="$1" local resolved="$2" MOSAIC_FLEET_EXECUTABLE_DEPENDENCY="" MOSAIC_FLEET_EXECUTABLE_PROBE="" MOSAIC_FLEET_EXECUTABLE_EXIT="" MOSAIC_FLEET_EXECUTABLE_OUTPUT="" [ -x "$resolved" ] || { MOSAIC_FLEET_EXECUTABLE_OUTPUT="resolved path is not executable" return 70 } local magic="" IFS= read -r -n 2 magic < "$resolved" || true [ "$magic" = '#!' ] || return 0 local shebang IFS= read -r shebang < "$resolved" || true shebang=${shebang%$'\r'} shebang=${shebang#\#!} local parts=() read -r -a parts <<< "$shebang" local interpreter="${parts[0]:-}" [[ "$interpreter" = /* ]] && [ -x "$interpreter" ] || { MOSAIC_FLEET_EXECUTABLE_DEPENDENCY="$interpreter" MOSAIC_FLEET_EXECUTABLE_OUTPUT="shebang interpreter is absent or not executable" return 70 } local dependency="$interpreter" local dependency_path="$interpreter" if [ "${interpreter##*/}" = env ]; then local index=1 if [ "${parts[$index]:-}" = -S ]; then index=$((index + 1)); fi dependency="${parts[$index]:-}" if [ -z "$dependency" ] || [[ "$dependency" = -* ]]; then MOSAIC_FLEET_EXECUTABLE_DEPENDENCY="$dependency" MOSAIC_FLEET_EXECUTABLE_OUTPUT="unsupported env shebang" return 70 fi fi MOSAIC_FLEET_EXECUTABLE_DEPENDENCY="$dependency" if [ "${dependency##*/}" = node ]; then MOSAIC_FLEET_EXECUTABLE_PROBE="node --version" fi if [ "${interpreter##*/}" = env ]; then if ! dependency_path=$(mosaic_fleet_resolve_in_pane_path "$pane_path" "$dependency"); then MOSAIC_FLEET_EXECUTABLE_OUTPUT="shebang command is not on the pane PATH" return 70 fi fi if [ "${dependency##*/}" = node ]; then if MOSAIC_FLEET_EXECUTABLE_OUTPUT=$(PATH="$pane_path" "$dependency_path" --version 2>&1); then MOSAIC_FLEET_EXECUTABLE_EXIT=0 else MOSAIC_FLEET_EXECUTABLE_EXIT=$? return 70 fi fi return 0 } mosaic_fleet_runtime_path_main() { local mosaic_home="" local runtime_bin="" local system_path="/usr/local/bin:/usr/bin:/bin" local binary="" local check_executable=0 while [ "$#" -gt 0 ]; do case "$1" in --mosaic-home) [ "$#" -ge 2 ] || return 64 mosaic_home="$2" shift 2 ;; --runtime-bin) [ "$#" -ge 2 ] || return 64 runtime_bin="$2" shift 2 ;; --binary) [ "$#" -ge 2 ] || return 64 binary="$2" shift 2 ;; --check-executable) check_executable=1 shift ;; # Test seam for measuring a greenfield host with no system Node. The # launcher and production CLI omit it and retain the fixed system suffix. --system-path) [ "$#" -ge 2 ] || return 64 system_path="$2" shift 2 ;; *) return 64 ;; esac done [ -n "$mosaic_home" ] && [ -n "$binary" ] || return 64 local pane_home pane_path resolved pane_home=$(mosaic_fleet_pane_home "$mosaic_home" "${HOME:-}") # npm config is HOME-sensitive. Pin it to the derived pane home before asking # for its prefix so an operator's unrelated npmrc cannot influence preflight. HOME=$pane_home export HOME pane_path=$(mosaic_fleet_build_pane_path "$pane_home" "$runtime_bin" "$system_path") if ! resolved=$(mosaic_fleet_resolve_in_pane_path "$pane_path" "$binary"); then printf 'pane_path\0%s\0status\0missing\0binary_path\0\0dependency\0\0probe_command\0\0probe_exit\0\0probe_output\0\0' \ "$pane_path" return 69 fi if [ "$check_executable" -eq 1 ]; then if mosaic_fleet_check_resolved_executable "$pane_path" "$resolved"; then printf 'pane_path\0%s\0status\0present\0binary_path\0%s\0dependency\0%s\0probe_command\0%s\0probe_exit\0%s\0probe_output\0%s\0' \ "$pane_path" "$resolved" "$MOSAIC_FLEET_EXECUTABLE_DEPENDENCY" \ "$MOSAIC_FLEET_EXECUTABLE_PROBE" "$MOSAIC_FLEET_EXECUTABLE_EXIT" \ "$MOSAIC_FLEET_EXECUTABLE_OUTPUT" return 0 fi printf 'pane_path\0%s\0status\0unexecutable\0binary_path\0%s\0dependency\0%s\0probe_command\0%s\0probe_exit\0%s\0probe_output\0%s\0' \ "$pane_path" "$resolved" "$MOSAIC_FLEET_EXECUTABLE_DEPENDENCY" \ "$MOSAIC_FLEET_EXECUTABLE_PROBE" "$MOSAIC_FLEET_EXECUTABLE_EXIT" \ "$MOSAIC_FLEET_EXECUTABLE_OUTPUT" return 70 fi printf 'pane_path\0%s\0status\0present\0binary_path\0%s\0dependency\0\0probe_command\0\0probe_exit\0\0probe_output\0\0' \ "$pane_path" "$resolved" return 0 } if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then set -euo pipefail mosaic_fleet_runtime_path_main "$@" fi