#!/bin/bash # AutoScholar worker installer for a MacLustr node (macOS Apple Silicon). Run by the operator with sudo ONCE: # sudo bash install-macos.sh --control-plane http://192.168.2.85:8470 --name M4M64c --enrollment-secret --anthropic-key-file /path/to/keyfile # Creates the dedicated non-admin user `asworker`, /var/autoscholar (owned by asworker), /etc/autoscholar/worker.env (root:asworker 0640), # installs toolchain (tectonic, poppler, uv) via Homebrew *as the operator* (brew refuses root), deploys the worker bundle, # loads the LaunchDaemon io.autoscholar.worker (UserName=asworker) and a pf anchor blocking LAN egress for asworker except the control plane. set -euo pipefail CONTROL_PLANE=""; NAME="$(scutil --get ComputerName 2>/dev/null || hostname)"; SECRET=""; KEYFILE=""; BUNDLE_DIR="$(cd "$(dirname "$0")/.." && pwd)"; OPERATOR="${SUDO_USER:-$(whoami)}" while [ $# -gt 0 ]; do case "$1" in --control-plane) CONTROL_PLANE="$2"; shift 2;; --name) NAME="$2"; shift 2;; --enrollment-secret) SECRET="$2"; shift 2;; --anthropic-key-file) KEYFILE="$2"; shift 2;; --bundle) BUNDLE_DIR="$2"; shift 2;; *) echo "unknown arg $1"; exit 2;; esac; done [ "$(id -u)" = 0 ] || { echo "run with sudo"; exit 1; } [ -n "$CONTROL_PLANE" ] || { echo "--control-plane required"; exit 1; } BREW=/opt/homebrew/bin/brew echo "== 1. dedicated user asworker" if ! dscl . -read /Users/asworker >/dev/null 2>&1; then sysadminctl -addUser asworker -fullName "AutoScholar Worker" -password "$(openssl rand -base64 24)" -home /var/autoscholar/home -shell /bin/zsh >/dev/null 2>&1 || true dscl . -create /Users/asworker IsHidden 1 fi dseditgroup -o edit -d asworker -t user admin 2>/dev/null || true # never admin dseditgroup -o read asworker >/dev/null 2>&1 || dseditgroup -o create -r "AutoScholar Worker" asworker dseditgroup -o edit -a asworker -t user asworker 2>/dev/null || true id asworker echo "== 2. directories" mkdir -p /var/autoscholar/{runs,state,cache/tectonic,home,templates/latex,logs,bundle} chown -R asworker:asworker /var/autoscholar chmod 750 /var/autoscholar chmod 700 /var/autoscholar/state /var/autoscholar/home mkdir -p /etc/autoscholar echo "== 3. toolchain via Homebrew (as $OPERATOR)" sudo -u "$OPERATOR" $BREW list tectonic >/dev/null 2>&1 || sudo -u "$OPERATOR" $BREW install tectonic sudo -u "$OPERATOR" $BREW list poppler >/dev/null 2>&1 || sudo -u "$OPERATOR" $BREW install poppler sudo -u "$OPERATOR" $BREW list uv >/dev/null 2>&1 || sudo -u "$OPERATOR" $BREW install uv sudo -u "$OPERATOR" $BREW list python@3.13 >/dev/null 2>&1 || sudo -u "$OPERATOR" $BREW install python@3.13 echo "== 4. worker bundle → /var/autoscholar/bundle" rsync -a --delete --exclude ".git" "$BUNDLE_DIR/" /var/autoscholar/bundle/ cp "$BUNDLE_DIR"/../../research/templates/latex/* /var/autoscholar/templates/latex/ 2>/dev/null || cp /var/autoscholar/bundle/templates/latex/* /var/autoscholar/templates/latex/ 2>/dev/null || true chown -R asworker:asworker /var/autoscholar/bundle /var/autoscholar/templates echo "== 5. environment file (root:asworker 0640; secrets never in git or logs)" ENVF=/etc/autoscholar/worker.env if [ ! -f "$ENVF" ]; then { echo "CONTROL_PLANE_URL=$CONTROL_PLANE"; echo "WORKER_NAME=$NAME"; echo "WORKER_ROOT=/var/autoscholar"; echo "WORKER_MODE=real"; echo "WORKER_MAX_CONCURRENT=1"; echo "TECTONIC_BIN=/opt/homebrew/bin/tectonic"; echo "LATEX_TEMPLATE_DIR=/var/autoscholar/templates/latex"; echo "LOG_LEVEL=info"; [ -n "$SECRET" ] && echo "WORKER_ENROLLMENT_SECRET=$SECRET"; [ -n "$KEYFILE" ] && echo "ANTHROPIC_API_KEY=$(tr -d '\n' < "$KEYFILE")"; } > "$ENVF" else echo " $ENVF exists; not overwritten (edit by hand to rotate secrets)" fi chown root:asworker "$ENVF"; chmod 640 "$ENVF" echo "== 6. pre-warm tectonic bundle cache as asworker" cat > /var/autoscholar/cache/warm.tex <<'TEX' \documentclass{article} \usepackage{amsmath,amsthm,mathtools,booktabs,array,longtable,multirow,siunitx,graphicx,float,caption,subcaption,xcolor,fancyhdr,titlesec,enumitem,etoolbox,setspace,natbib,url,hyperref,cleveref,microtype,fontspec,unicode-math,iftex} \setmainfont{LibertinusSerif}[Extension=.otf,UprightFont=*-Regular,BoldFont=*-Bold,ItalicFont=*-Italic,BoldItalicFont=*-BoldItalic] \setsansfont{LibertinusSans}[Extension=.otf,UprightFont=*-Regular,BoldFont=*-Bold,ItalicFont=*-Italic] \setmonofont{lmmonolt10-regular}[Extension=.otf,BoldFont=lmmonolt10-bold,ItalicFont=lmmonolt10-oblique] \setmathfont{LibertinusMath-Regular.otf} \begin{document}warm $x^2$ \cite{a}\bibliographystyle{plainnat}\bibliography{warm}\end{document} TEX echo '@misc{a, title={Warm}, author={A}, year={2026}}' > /var/autoscholar/cache/warm.bib chown asworker:asworker /var/autoscholar/cache/warm.tex /var/autoscholar/cache/warm.bib sudo -u asworker env HOME=/var/autoscholar/home TECTONIC_CACHE_DIR=/var/autoscholar/cache/tectonic PATH=/opt/homebrew/bin:/usr/bin:/bin bash -c 'cd /var/autoscholar/cache && /opt/homebrew/bin/tectonic -X compile warm.tex >/var/autoscholar/logs/tectonic-warm.log 2>&1 && echo " bundle cached" || echo " warm-up failed (see logs/tectonic-warm.log; will retry at first run)"' echo "== 7. LaunchDaemon io.autoscholar.worker" cat > /Library/LaunchDaemons/io.autoscholar.worker.plist < Labelio.autoscholar.worker UserNameasworker GroupNameasworker ProgramArguments/bin/bash/var/autoscholar/bundle/install/run-worker.sh WorkingDirectory/var/autoscholar RunAtLoad KeepAlive ThrottleInterval15 StandardOutPath/var/autoscholar/logs/worker.out.log StandardErrorPath/var/autoscholar/logs/worker.err.log EnvironmentVariablesHOME/var/autoscholar/homePATH/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin SoftResourceLimitsNumberOfFiles8192 PLIST chown root:wheel /Library/LaunchDaemons/io.autoscholar.worker.plist; chmod 644 /Library/LaunchDaemons/io.autoscholar.worker.plist launchctl bootout system/io.autoscholar.worker 2>/dev/null || true launchctl bootstrap system /Library/LaunchDaemons/io.autoscholar.worker.plist launchctl kickstart -k system/io.autoscholar.worker echo "== 8. pf egress rules for uid asworker (LAN/Tailscale/loopback blocked except control plane + DNS)" CP_HOST=$(echo "$CONTROL_PLANE" | sed -E 's#https?://##; s#[:/].*##'); CP_PORT=$(echo "$CONTROL_PLANE" | sed -nE 's#https?://[^:/]+:([0-9]+).*#\1#p'); CP_PORT=${CP_PORT:-80} cat > /etc/pf.anchors/io.autoscholar <> /etc/pf.conf cat > /Library/LaunchDaemons/io.autoscholar.pf.plist < Labelio.autoscholar.pf ProgramArguments/bin/sh-c/sbin/pfctl -E; /sbin/pfctl -f /etc/pf.conf RunAtLoad PLIST launchctl bootout system/io.autoscholar.pf 2>/dev/null || true launchctl bootstrap system /Library/LaunchDaemons/io.autoscholar.pf.plist pfctl -E 2>/dev/null || true; pfctl -f /etc/pf.conf pfctl -a io.autoscholar -s rules echo "== done. Logs: /var/autoscholar/logs/worker.*.log ; status: sudo launchctl print system/io.autoscholar.worker | head"