Files
hermes-agent/setup-hermes.sh
ethernet 712734436e fix(pm): make bootstrap and bundle ownership explicit
Finish bootstrap uv before PM replaces its store entry. Keep failure
receipts stdlib-only and align the cryptography requirement and override
with the locked version.

Let bundle builders declare launch paths and update ownership. Remove
payload discovery, Store probing, and the unused develop command.
Derive Nix Python from the PM lock and share its provenance stamp.

Document setup, activation, optional dependencies, and distribution
ownership. Targeted Windows tests, relocated runtime launches, Electron
bundling, and bilingual docs builds pass. Native Nix and signed-package
acceptance remain CI gates.
2026-09-08 00:24:51 -04:00

260 lines
10 KiB
Bash
Executable File

#!/bin/bash
# ============================================================================
# Hermes Agent Setup Script — THE dev-environment entry point.
# ============================================================================
# Sets up the pm-managed development environment from a fresh clone:
# 1. Stage the pinned uv from pm/lock.json (sha256-verified, into the pm
# store slot) — pm needs uv to bootstrap, so it cannot stage uv itself.
# 2. Use uv to install and locate bootstrap Python, then let uv exit.
# Run `python -m pm.cli install` directly so PM can safely replace uv.
# PM owns the final interpreter, tool store, and dependency generation.
# 3. Point you at `source ./activate` — the venv-style way to put the pm
# env (PATH + tool vars) into your current shell.
# There is no pip fallback tier here on purpose.
# ============================================================================
set -e
# Colors
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
CYAN='\033[0;36m'
RED='\033[0;31m'
NC='\033[0m'
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
# Prevent uv from discovering config files (uv.toml, pyproject.toml) from the
# wrong user's home directory when running under sudo -u <user>. See #21269.
export UV_NO_CONFIG=1
echo ""
echo -e "${CYAN}⚕ Hermes Agent Setup${NC}"
echo ""
# ============================================================================
# Install / locate uv — staged from pm/lock.json (the lockfile is the only
# authority; no astral-latest, no curl|sh).
# ============================================================================
echo -e "${CYAN}→${NC} Checking for uv..."
lock="$SCRIPT_DIR/pm/lock.json"
[ -f "$lock" ] || { echo -e "${RED}✗${NC} pm/lock.json not found" >&2; exit 1; }
case "$(uname -s)" in
Linux) os=linux ;;
Darwin) os=darwin ;;
MINGW*|MSYS*|CYGWIN*) os=win32 ;;
*) echo -e "${RED}✗${NC} unsupported OS $(uname -s)" >&2; exit 1 ;;
esac
if [ "$os" = win32 ]; then
# PROCESSOR_ARCHITECTURE lies under an emulated shell (x64 msys on a
# WoA box reports AMD64); the registry carries the machine's truth.
winarch="$(reg.exe query 'HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment' /v PROCESSOR_ARCHITECTURE 2>/dev/null | tr -d '\r' | awk '/PROCESSOR_ARCHITECTURE/ {print $NF}')"
case "${winarch:-${PROCESSOR_ARCHITECTURE:-}}" in
ARM64) arch=arm64 ;;
*) arch=x64 ;;
esac
else
case "$(uname -m)" in
arm64|aarch64) arch=arm64 ;;
x86_64|amd64) arch=x64 ;;
*) echo -e "${RED}✗${NC} unsupported arch $(uname -m)" >&2; exit 1 ;;
esac
fi
target="$os-$arch"
# lock.json is machine-written (sorted keys, 2-space indent): read the uv
# pin's version + this target's url/sha256 with awk — no python yet.
pin() { # $1 = field (url | sha256)
awk -v target="$target" -v field="$1" '
/^ "uv": \{/ { in_uv = 1 }
in_uv && $0 ~ "^ \"" target "\": \\{" { in_t = 1 }
in_t && $0 ~ "^ \"" field "\":" {
gsub(/.*: "|,?$/, ""); print; exit
}' "$lock"
}
uv_version="$(awk '
/^ "uv": \{/ { in_uv = 1 }
in_uv && /^ "version":/ { gsub(/.*: "|,?$/, ""); print; exit }' "$lock")"
py_version="$(awk '
/^ "python": \{/ { in_py = 1 }
in_py && /^ "version":/ { gsub(/.*: "|"$|",$/, ""); print; exit }' "$lock" \
| cut -d+ -f1 | cut -d. -f1,2)"
[ -n "$uv_version" ] || { echo -e "${RED}✗${NC} no uv pin in pm/lock.json" >&2; exit 1; }
store="${HERMES_RUNTIME_DIR:-$HOME/.hermes/tools}"
entry="$store/uv-$uv_version-$target"
uv="$entry/uv"; [ "$os" = win32 ] && uv="$entry/uv.exe"
if [ -x "$uv" ]; then
echo -e "${GREEN}✓${NC} pinned uv found ($("$uv" --version 2>/dev/null))"
else
url="$(pin url)"; sha="$(pin sha256)"
[ -n "$url" ] && [ -n "$sha" ] || { echo -e "${RED}✗${NC} no uv artifact for $target" >&2; exit 1; }
echo -e "${CYAN}→${NC} Staging pinned uv $uv_version ($target) into the pm store..."
mkdir -p "$store"
tmp="$(mktemp -d "$store/.bootstrap-XXXXXX")"; trap 'rm -rf "$tmp"' EXIT
archive="$tmp/${url##*/}"
curl -fsSL -o "$archive" "$url"
got="$( (sha256sum "$archive" 2>/dev/null || shasum -a 256 "$archive") | cut -d' ' -f1 | tr -d '\\')"
[ "$got" = "$sha" ] || { echo -e "${RED}✗${NC} sha256 mismatch for uv (got $got, pinned $sha)" >&2; exit 1; }
mkdir -p "$tmp/tree"
case "$archive" in
*.zip) unzip -q "$archive" -d "$tmp/tree" ;;
*) tar -xzf "$archive" -C "$tmp/tree" ;;
esac
# flatten a single wrapping dir (uv tarballs ship uv-<triple>/uv)
inner="$(find "$tmp/tree" -mindepth 1 -maxdepth 1)"
if [ "$(printf '%s\n' "$inner" | wc -l)" = 1 ] && [ -d "$inner" ]; then
mv "$inner" "$tmp/entry"
else
mv "$tmp/tree" "$tmp/entry"
fi
rm -rf "$entry"
mv "$tmp/entry" "$entry"
echo -e "${GREEN}✓${NC} uv installed ($("$uv" --version 2>/dev/null))"
fi
# ============================================================================
# Delegate to pm: python + venv + tool store + hash-verified venv sync
# ============================================================================
echo -e "${CYAN}→${NC} Installing python + tools + dependencies via pm (hash-verified via uv.lock)..."
echo -e "${CYAN}→${NC} (first run on a fresh checkout can take 1-5 minutes)"
# PM can replace its uv entry only after the bootstrap uv has exited.
"$uv" python install --no-bin "$py_version"
boot_py="$("$uv" python find --managed-python "$py_version")"
boot_py="${boot_py%$'\r'}"
if ! "$boot_py" -m pm.cli install; then
echo -e "${RED}✗${NC} pm install failed — see output above."
exit 1
fi
echo -e "${GREEN}✓${NC} Tools + dependencies installed (hash-verified via pm + uv.lock)"
# ============================================================================
# Environment file
# ============================================================================
if [ ! -f ".env" ]; then
if [ -f ".env.example" ]; then
cp .env.example .env
# .env holds API keys — restrict to owner-only access (matches
# scripts/install.sh which already chmods 600 after creation).
chmod 600 .env 2>/dev/null || true
echo -e "${GREEN}✓${NC} Created .env from template"
fi
else
# Tighten an existing .env's perms in case it was created elsewhere
# under a permissive umask.
chmod 600 .env 2>/dev/null || true
echo -e "${GREEN}✓${NC} .env exists"
fi
# ============================================================================
# PATH setup — symlink hermes into a user-facing bin dir
# ============================================================================
echo -e "${CYAN}→${NC} Setting up hermes command..."
# pm installs the venv; find its python across layouts (posix venv vs win).
PYBIN=""
for candidate in "$SCRIPT_DIR/venv/bin/python" "$SCRIPT_DIR/venv/Scripts/python.exe"; do
[ -x "$candidate" ] && { PYBIN="$candidate"; break; }
done
HERMES_BIN=""
for candidate in "$SCRIPT_DIR/venv/bin/hermes" "$SCRIPT_DIR/venv/Scripts/hermes.exe"; do
[ -e "$candidate" ] && { HERMES_BIN="$candidate"; break; }
done
if [ -n "$HERMES_BIN" ] && [ "$os" != win32 ]; then
mkdir -p "$HOME/.local/bin"
ln -sf "$HERMES_BIN" "$HOME/.local/bin/hermes"
echo -e "${GREEN}✓${NC} Symlinked hermes → ~/.local/bin/hermes"
fi
if [ "$os" != win32 ]; then
# Determine the appropriate shell config file
SHELL_CONFIG=""
if [[ "$SHELL" == *"zsh"* ]]; then
SHELL_CONFIG="$HOME/.zshrc"
elif [[ "$SHELL" == *"bash"* ]]; then
SHELL_CONFIG="$HOME/.bashrc"
[ ! -f "$SHELL_CONFIG" ] && SHELL_CONFIG="$HOME/.bash_profile"
else
# Fallback to checking existing files
if [ -f "$HOME/.zshrc" ]; then
SHELL_CONFIG="$HOME/.zshrc"
elif [ -f "$HOME/.bashrc" ]; then
SHELL_CONFIG="$HOME/.bashrc"
elif [ -f "$HOME/.bash_profile" ]; then
SHELL_CONFIG="$HOME/.bash_profile"
fi
fi
if [ -n "$SHELL_CONFIG" ]; then
# Touch the file just in case it doesn't exist yet but was selected
touch "$SHELL_CONFIG" 2>/dev/null || true
if ! echo "$PATH" | tr ':' '\n' | grep -q "^$HOME/.local/bin$"; then
if ! grep -q '\.local/bin' "$SHELL_CONFIG" 2>/dev/null; then
echo "" >> "$SHELL_CONFIG"
echo "# Hermes Agent — ensure ~/.local/bin is on PATH" >> "$SHELL_CONFIG"
echo 'export PATH="$HOME/.local/bin:$PATH"' >> "$SHELL_CONFIG"
echo -e "${GREEN}✓${NC} Added ~/.local/bin to PATH in $SHELL_CONFIG"
else
echo -e "${GREEN}✓${NC} ~/.local/bin already in $SHELL_CONFIG"
fi
else
echo -e "${GREEN}✓${NC} ~/.local/bin already on PATH"
fi
fi
fi
# ============================================================================
# Seed bundled skills into ~/.hermes/skills/
# ============================================================================
HERMES_SKILLS_DIR="${HERMES_HOME:-$HOME/.hermes}/skills"
mkdir -p "$HERMES_SKILLS_DIR"
echo ""
echo "Syncing bundled skills to ~/.hermes/skills/ ..."
if [ -n "$PYBIN" ] && "$PYBIN" "$SCRIPT_DIR/tools/skills_sync.py" 2>/dev/null; then
echo -e "${GREEN}✓${NC} Skills synced"
else
# Fallback: copy if sync script fails (missing deps, etc.)
if [ -d "$SCRIPT_DIR/skills" ]; then
cp -rn "$SCRIPT_DIR/skills/"* "$HERMES_SKILLS_DIR/" 2>/dev/null || true
echo -e "${GREEN}✓${NC} Skills copied"
fi
fi
# ============================================================================
# Done
# ============================================================================
echo ""
echo -e "${GREEN}✓ Setup complete!${NC}"
echo ""
echo "Next steps:"
echo ""
echo " 1. Activate the dev environment (venv-style, in THIS shell):"
echo " source ./activate"
echo ""
echo " 2. Run the setup wizard to configure API keys:"
echo " hermes setup"
echo ""
echo " 3. Start chatting:"
echo " hermes"
echo ""
echo "Other commands:"
echo " hermes pm install # Re-run the tool + dependency install"
echo " hermes status # Check configuration"
echo " hermes doctor # Diagnose issues"
echo " deactivate # Undo the activation (restore PATH etc.)"
echo ""