Fix: Pin runtime and agent artifacts

This commit is contained in:
Gabriel Brown
2026-08-27 07:49:12 -04:00
parent eab4280969
commit 8264ca6320
3 changed files with 719 additions and 91 deletions
+273 -82
View File
@@ -107,72 +107,273 @@ install_list() {
fi
}
# --- Node and pnpm, through nvm ----------------------------------------------
#
# nvm is a shell function rather than a binary, so it has to be sourced before
# it can be used at all -- and its script reads variables that `set -u` above
# treats as fatal, so the strictness is lifted for exactly that source and put
# straight back.
#
# Deliberately not dnf's nodejs: config/bash/shell switches Node per project
# from .nvmrc, and a system Node earlier on PATH would win every switch, leaving
# `nvm use` looking like it did nothing.
#
# pnpm goes inside the nvm-managed Node rather than beside it as its own dnf
# package, so it travels with the version it belongs to instead of outliving it.
# --- Reviewed language runtimes and agent tools ------------------------------
_record_installer_failure() {
local component="$1"
log "$component install did not complete; continuing"
softly_failed+=("$component")
return 1
}
_set_artifact_arch() {
local machine_arch
machine_arch="$(uname -m)" || return 1
case "$machine_arch" in
x86_64) artifact_arch=X86_64 ;;
aarch64) artifact_arch=AARCH64 ;;
*) log "Unsupported architecture: $machine_arch"; return 1 ;;
esac
}
_archive_member_is_safe() {
local member="$1" expected_top="$2"
[[ -n "$member" && "$member" != /* && "$member" != *'//'*
&& ! "$member" =~ (^|/)\.\.?(/|$)
&& ( "$member" == "$expected_top" || "$member" == "$expected_top/" \
|| "$member" == "$expected_top/"* ) ]]
}
_tree_links_stay_inside() {
local root="$1" link resolved
while IFS= read -r -d '' link; do
resolved="$(realpath -m -- "$link")" || return 1
[[ "$resolved" == "$root" || "$resolved" == "$root/"* ]] || return 1
done < <(find "$root" -type l -print0)
}
_atomic_symlink() {
local target="$1" destination="$2" directory temporary
directory="$(dirname -- "$destination")"
mkdir -p -- "$directory" || return 1
temporary="$(mktemp "$directory/.$(basename -- "$destination").link.XXXXXX")" || return 1
rm -f -- "$temporary" || return 1
ln -s -- "$target" "$temporary" || return 1
if ! mv -Tf -- "$temporary" "$destination"; then
rm -f -- "$temporary"
return 1
fi
}
_load_nvm() {
local nvm_script="$PANAMA_SYSTEM_ETC/profile.d/nvm.sh"
[[ -s "$nvm_script" ]] || return 1
set +u
# shellcheck source=/dev/null
source "$nvm_script"
set -u
declare -F nvm >/dev/null
}
_install_node() {
local artifact_arch machine_arch archive_top parent target stage archive extract listing member
_set_artifact_arch || return 1
_load_nvm || return 1
case "$artifact_arch" in
X86_64) machine_arch=x64 ;;
AARCH64) machine_arch=arm64 ;;
esac
archive_top="node-v${INSTALLER_PROVENANCE[NODE_VERSION]}-linux-$machine_arch"
parent="${NVM_DIR:-$HOME/.nvm}/versions/node"
target="$parent/v${INSTALLER_PROVENANCE[NODE_VERSION]}"
if [[ -e "$target" || -L "$target" ]]; then
[[ -d "$target" && ! -L "$target" && -x "$target/bin/node"
&& "$($target/bin/node --version 2>/dev/null)" == "v${INSTALLER_PROVENANCE[NODE_VERSION]}" ]] \
|| return 1
nvm alias default "${INSTALLER_PROVENANCE[NODE_VERSION]}" >/dev/null 2>&1 || return 1
return 0
fi
mkdir -p -- "$parent" || return 1
stage="$(mktemp -d "$parent/.v${INSTALLER_PROVENANCE[NODE_VERSION]}.stage.XXXXXX")" \
|| return 1
chmod 0700 "$stage"
archive="$stage/artifact"
extract="$stage/extract"
mkdir -m 0700 "$extract" || { rm -rf -- "$stage"; return 1; }
if ! download_sha256 "${INSTALLER_PROVENANCE[NODE_${artifact_arch}_URL]}" \
"${INSTALLER_PROVENANCE[NODE_${artifact_arch}_SHA256]}" \
"${INSTALLER_PROVENANCE[NODE_${artifact_arch}_MAX_BYTES]}" "$archive"; then
rm -rf -- "$stage"
return 1
fi
listing="$(tar -tJf "$archive")" || { rm -rf -- "$stage"; return 1; }
[[ -n "$listing" ]] || { rm -rf -- "$stage"; return 1; }
while IFS= read -r member; do
_archive_member_is_safe "$member" "$archive_top" \
|| { rm -rf -- "$stage"; return 1; }
done <<<"$listing"
tar -xJf "$archive" --no-same-owner --no-same-permissions -C "$extract" \
|| { rm -rf -- "$stage"; return 1; }
[[ -d "$extract/$archive_top" && ! -L "$extract/$archive_top"
&& -x "$extract/$archive_top/bin/node"
&& "$($extract/$archive_top/bin/node --version 2>/dev/null)" \
== "v${INSTALLER_PROVENANCE[NODE_VERSION]}" ]] \
|| { rm -rf -- "$stage"; return 1; }
_tree_links_stay_inside "$extract/$archive_top" \
|| { rm -rf -- "$stage"; return 1; }
mv -- "$extract/$archive_top" "$target" || { rm -rf -- "$stage"; return 1; }
rm -rf -- "$stage"
nvm alias default "${INSTALLER_PROVENANCE[NODE_VERSION]}" >/dev/null 2>&1
}
install_node() {
_install_node || _record_installer_failure Node
}
# Kept as the call-site name used by the desktop-first ordering contract.
setup_node() {
if [[ -s /etc/profile.d/nvm.sh ]]; then
log "Installing the latest Node LTS through nvm"
set +u
# shellcheck source=/dev/null
source /etc/profile.d/nvm.sh
if nvm install --lts >/dev/null 2>&1; then
nvm alias default 'lts/*' >/dev/null 2>&1 || true
npm install -g pnpm >/dev/null 2>&1 || { log "pnpm did not install"; softly_failed+=("pnpm"); }
log "Node $(node --version 2>/dev/null) with pnpm $(pnpm --version 2>/dev/null)"
else
log "nvm could not install Node; skipping"; softly_failed+=("Node (nvm)")
fi
set -u
else
log "nvm is not installed, so Node was not set up"
fi
install_node
}
# --- Applications no repository packages -------------------------------------
#
# Everything else Panama installs comes from dnf or Flathub. These do not
# exist in either, so each is an explicit exception with a reason, and each is
# skipped when already present so a re-run costs nothing.
#
# None of them pins a version. sunhat pinned URLs -- upscayl 2.11.5, LACT 0.5.4,
# a fedora-40 RPM -- and every one of them was a 404 within a release cycle. An
# installer that resolves "latest" keeps working; one that names a version rots.
#
# A failure here is logged and stepped over rather than aborting: an
# unreachable third-party host should not cost the rest of the run.
install_pnpm() {
if sudo dnf install -y pnpm >/dev/null; then
return 0
fi
_record_installer_failure pnpm
}
_install_bun() {
local artifact_arch archive_top target bin_link parent stage archive listing
local staged_binary version_dir
_set_artifact_arch || return 1
case "$artifact_arch" in
X86_64) archive_top=bun-linux-x64 ;;
AARCH64) archive_top=bun-linux-aarch64 ;;
esac
version_dir="$HOME/.bun/versions/${INSTALLER_PROVENANCE[BUN_VERSION]}"
target="$version_dir/bin/bun"
bin_link="$HOME/.bun/bin/bun"
if [[ -e "$version_dir" || -L "$version_dir" ]]; then
[[ -d "$version_dir" && ! -L "$version_dir" && -x "$target"
&& "$($target --version 2>/dev/null)" == "${INSTALLER_PROVENANCE[BUN_VERSION]}" ]] \
|| return 1
[[ -L "$bin_link" && "$(readlink -- "$bin_link")" == "$target" ]] \
|| _atomic_symlink "$target" "$bin_link"
return
fi
parent="$HOME/.bun/versions"
mkdir -p -- "$parent" || return 1
stage="$(mktemp -d "$parent/.${INSTALLER_PROVENANCE[BUN_VERSION]}.stage.XXXXXX")" \
|| return 1
chmod 0700 "$stage"
archive="$stage/artifact"
if ! download_sha256 "${INSTALLER_PROVENANCE[BUN_${artifact_arch}_URL]}" \
"${INSTALLER_PROVENANCE[BUN_${artifact_arch}_SHA256]}" \
"${INSTALLER_PROVENANCE[BUN_${artifact_arch}_MAX_BYTES]}" "$archive"; then
rm -rf -- "$stage"
return 1
fi
listing="$(unzip -Z1 "$archive")" || { rm -rf -- "$stage"; return 1; }
[[ "$listing" == "$archive_top/bun" ]] || { rm -rf -- "$stage"; return 1; }
_archive_member_is_safe "$listing" "$archive_top" \
|| { rm -rf -- "$stage"; return 1; }
mkdir -m 0700 "$stage/extract" "$stage/version" "$stage/version/bin" \
|| { rm -rf -- "$stage"; return 1; }
unzip -q "$archive" -d "$stage/extract" || { rm -rf -- "$stage"; return 1; }
staged_binary="$stage/extract/$archive_top/bun"
[[ -f "$staged_binary" && ! -L "$staged_binary" && -x "$staged_binary"
&& "$($staged_binary --version 2>/dev/null)" == "${INSTALLER_PROVENANCE[BUN_VERSION]}" ]] \
|| { rm -rf -- "$stage"; return 1; }
mv -- "$staged_binary" "$stage/version/bin/bun" \
|| { rm -rf -- "$stage"; return 1; }
mv -- "$stage/version" "$version_dir" || { rm -rf -- "$stage"; return 1; }
rm -rf -- "$stage"
_atomic_symlink "$target" "$bin_link"
}
# Bun: the JavaScript runtime and package manager. No RPM, no flatpak.
install_bun() {
if [[ -x "$HOME/.bun/bin/bun" ]]; then
log "Bun already installed at \"$HOME/.bun/bin/bun\""
else
log "Installing Bun via curl..."
curl -fsSL https://bun.sh/install | bash > /dev/null 2>&1 || { log "Bun install failed; skipping"; softly_failed+=("Bun"); }
fi
_install_bun || _record_installer_failure Bun
}
# Codex: OpenAI's CLI. Distributed through npm, which is why this runs after
# setup_node -- the nvm-managed Node is the one it should land in.
install_codex() {
if command -v codex >/dev/null 2>&1; then
log "Codex already installed at \"$(command -v codex)\""
elif command -v npm >/dev/null 2>&1; then
log "Installing Codex via npm..."
npm install -g @openai/codex >/dev/null 2>&1 || { log "Codex install failed; skipping"; softly_failed+=("Codex"); }
else
log "npm is not available, so Codex was not installed"; softly_failed+=("Codex")
_codex_version_matches() {
local binary="$1" output version_pattern
output="$($binary --version 2>/dev/null)" || return 1
version_pattern="${INSTALLER_PROVENANCE[CODEX_VERSION]//./\\.}"
[[ "$output" =~ (^|[^0-9])${version_pattern}([^0-9]|$) ]]
}
_install_codex() {
local artifact_arch machine_arch archive_name version_dir target bin_link parent
local stage archive listing staged_binary
_set_artifact_arch || return 1
case "$artifact_arch" in
X86_64) machine_arch=x86_64 ;;
AARCH64) machine_arch=aarch64 ;;
esac
archive_name="codex-$machine_arch-unknown-linux-musl"
version_dir="$HOME/.local/lib/panama/codex/${INSTALLER_PROVENANCE[CODEX_VERSION]}"
target="$version_dir/codex"
bin_link="$HOME/.local/bin/codex"
if [[ -e "$version_dir" || -L "$version_dir" ]]; then
[[ -d "$version_dir" && ! -L "$version_dir" && -x "$target" ]] || return 1
_codex_version_matches "$target" || return 1
[[ -L "$bin_link" && "$(readlink -- "$bin_link")" == "$target" ]] \
|| _atomic_symlink "$target" "$bin_link"
return
fi
parent="$HOME/.local/lib/panama/codex"
mkdir -p -- "$parent" || return 1
stage="$(mktemp -d "$parent/.${INSTALLER_PROVENANCE[CODEX_VERSION]}.stage.XXXXXX")" \
|| return 1
chmod 0700 "$stage"
archive="$stage/artifact"
if ! download_sha256 "${INSTALLER_PROVENANCE[CODEX_${artifact_arch}_URL]}" \
"${INSTALLER_PROVENANCE[CODEX_${artifact_arch}_SHA256]}" \
"${INSTALLER_PROVENANCE[CODEX_${artifact_arch}_MAX_BYTES]}" "$archive"; then
rm -rf -- "$stage"
return 1
fi
listing="$(tar -tzf "$archive")" || { rm -rf -- "$stage"; return 1; }
[[ "$listing" == "$archive_name" ]] || { rm -rf -- "$stage"; return 1; }
[[ "$listing" != /* && ! "$listing" =~ (^|/)\.\.?(/|$) ]] \
|| { rm -rf -- "$stage"; return 1; }
mkdir -m 0700 "$stage/extract" "$stage/version" \
|| { rm -rf -- "$stage"; return 1; }
tar -xzf "$archive" --no-same-owner --no-same-permissions -C "$stage/extract" \
|| { rm -rf -- "$stage"; return 1; }
staged_binary="$stage/extract/$archive_name"
[[ -f "$staged_binary" && ! -L "$staged_binary" && -x "$staged_binary" ]] \
|| { rm -rf -- "$stage"; return 1; }
_codex_version_matches "$staged_binary" \
|| { rm -rf -- "$stage"; return 1; }
mv -- "$staged_binary" "$stage/version/codex" \
|| { rm -rf -- "$stage"; return 1; }
mv -- "$stage/version" "$version_dir" || { rm -rf -- "$stage"; return 1; }
rm -rf -- "$stage"
_atomic_symlink "$target" "$bin_link"
}
install_codex() {
_install_codex || _record_installer_failure Codex
}
_install_rustdesk() {
local artifact_arch installed_version="" work rpm_path status=0
_set_artifact_arch || return 1
if [[ "$artifact_arch" == AARCH64 ]]; then
log "RustDesk ${INSTALLER_PROVENANCE[RUSTDESK_VERSION]} has no reviewed aarch64 RPM"
return 1
fi
installed_version="$(rpm -q --queryformat '%{VERSION}' rustdesk 2>/dev/null)" || true
if [[ "$installed_version" == "${INSTALLER_PROVENANCE[RUSTDESK_VERSION]}" ]]; then
return 0
fi
work="$(mktemp -d)" || return 1
chmod 0700 "$work"
rpm_path="$work/rustdesk.rpm"
if ! download_sha256 "${INSTALLER_PROVENANCE[RUSTDESK_X86_64_URL]}" \
"${INSTALLER_PROVENANCE[RUSTDESK_X86_64_SHA256]}" \
"${INSTALLER_PROVENANCE[RUSTDESK_X86_64_MAX_BYTES]}" "$rpm_path"; then
rm -rf -- "$work"
return 1
fi
sudo dnf install -y --setopt=localpkg_gpgcheck=1 "$rpm_path" >/dev/null || status=$?
rm -rf -- "$work"
return "$status"
}
install_rustdesk() {
_install_rustdesk || _record_installer_failure RustDesk
}
# --- What was stepped over ---------------------------------------------------
@@ -713,7 +914,7 @@ ensure_flathub_remote() {
return "$status"
}
install_claude_code() {
_install_claude_code() {
local work staged_key staged_repo status
if command -v claude >/dev/null 2>&1; then
log "Claude Code already installed at \"$(command -v claude)\""
@@ -750,6 +951,10 @@ install_claude_code() {
return "$status"
}
install_claude_code() {
_install_claude_code || _record_installer_failure "Claude Code"
}
_claude_desktop_manual() {
log "Claude Desktop is optional; configure its reviewed local-key repository manually to install it"
}
@@ -811,10 +1016,13 @@ if [[ "$ROLE" == server ]]; then
sudo dnf update -y --refresh > /dev/null
install_list core-packages "Core"
install_list server-packages "Server"
set +e
setup_node
install_pnpm
install_bun
install_claude_code
install_codex
set -e
report_soft_failures
exit 0
fi
@@ -931,10 +1139,13 @@ else
log "Package list was not in specified path: $DEV_FILE"
fi
set +e
setup_node
install_pnpm
install_bun
install_claude_code
install_codex
set -e
# Claude Desktop remains optional. Panama never downloads its community setup
# script; only a repository an operator has already configured with the exact
@@ -944,29 +1155,9 @@ if ! install_claude_desktop_if_trusted; then
softly_failed+=("Claude Desktop")
fi
# RustDesk: remote desktop. The flatpak cannot register the root-owned system
# service that unattended access needs -- see panama-doctor's rustdesk check --
# so this takes the RPM. The download URL is resolved from the latest release
# rather than written down, so it does not go stale.
if rpm -q rustdesk >/dev/null 2>&1; then
log "RustDesk already installed"
else
log "Resolving the latest RustDesk release..."
# `|| true` because a failed curl -- unauthenticated GitHub API calls get
# rate-limited -- would otherwise trip set -e and kill the stage before the
# empty-result fallback below could do its job.
rustdesk_url="$(curl -fsSL https://api.github.com/repos/rustdesk/rustdesk/releases/latest 2>/dev/null \
| jq -r --arg arch "$(uname -m)" '.assets[].browser_download_url | select(test($arch + "\\.rpm$")) | select(test("suse") | not)' \
| head -1 || true)"
if [[ -n "$rustdesk_url" ]]; then
log "Installing RustDesk from $rustdesk_url"
# The RPM ships rustdesk.service already enabled, which is what provides
# unattended access; Panama deliberately does not start it a second time.
sudo dnf install -y "$rustdesk_url" > /dev/null || { log "RustDesk install failed; skipping"; softly_failed+=("RustDesk"); }
else
log "Could not resolve a RustDesk release; skipping"; softly_failed+=("RustDesk")
fi
fi
# The RPM ships rustdesk.service already enabled, which is what provides
# unattended access; Panama deliberately does not start it a second time.
install_rustdesk || true
# --- Install Flatpak Packages ---
FLATPAK_FILE="$PANAMA_PATH/setup/packages/flatpak-packages"