#!/usr/bin/env bash
#
# Turn a freshly-installed macOS guest into Nucleic's GOLDEN base image.
#
#   ./provision-macos-guest.sh [/path/to/nucleic-id_ed25519.pub] [AGENT_PASSWORD]
#
# The optional 2nd arg (or $NUCLEIC_AGENT_PW) is the `agent` account password, needed ONLY to enable
# auto-login for the computer-use phase (Phase 7). Omit it to skip auto-login/computer-use setup.
#
# THIS RUNS INSIDE THE VM, not on the host. It's the second half of the "install + provision locally"
# base flow — scripts/build-macos-base.sh installs a clean macOS and drops you at Setup Assistant;
# after you create the `agent` account by hand, you copy THIS script (plus Nucleic's host PUBLIC key)
# into the guest and run it as `agent`. It:
#
#   1. Enables Remote Login (sshd) so Nucleic can reach the guest as agent@<ip>.
#   2. Authorizes Nucleic's host public key in ~agent/.ssh/authorized_keys.
#   3. Installs the Xcode Command Line Tools (the bootstrap toolchain). A full Xcode staged from the
#      host is required and verified near the end before the base can be published.
#   4. Installs Homebrew + the major dev toolchains (node, python/pip, rust, go), build systems
#      (cmake/make/pkg-config/autotools), git, and common CLI dev tools — baked into the base.
#   5. CRITICAL: appends the toolchain PATH to /etc/zshenv so NON-login, non-interactive SSH shells
#      (how the engine runs commands) can see node/npm/python/xcodebuild. .zprofile/.zshrc are NOT
#      sourced by those shells; /etc/zshenv always is.
#   6. Warms the iOS simulator.
#   7. COMPUTER USE: installs cliclick at /usr/local/bin and enables auto-login for the agent (so a
#      headless boot has an Aqua session), and installs the NATIVE IN-GUEST AGENT (NucleicVMAgent.app
#      + its LaunchAgent) when the app is staged alongside this script — see
#      docs/MACOS_VM_NATIVE_AGENT.md. It grants NO TCC: the default computer-use path is host-side
#      virtual IO (framebuffer + virtual HID), which needs nothing from the guest, and SIP stays on.
#   8. Verifies the promised CLI tools plus a real SwiftUI/Metal compile, then shuts the guest DOWN
#      cleanly so only a build-ready base can be cloned.
#
# Run it as the `agent` user (it will `sudo` for the system-level bits). It's idempotent — safe to
# re-run. Some steps (Remote Login, first-launch) may prompt or need Full Disk Access for Terminal.

set -euo pipefail

# The account Nucleic logs in as. Must match MacVMSettings.defaultSSHUser; also the account you
# created in Setup Assistant. Provision under this user even if invoked via sudo.
AGENT_USER="agent"
AGENT_HOME="$(/usr/bin/dscl . -read "/Users/$AGENT_USER" NFSHomeDirectory 2>/dev/null | awk '{print $2}' || true)"
AGENT_HOME="${AGENT_HOME:-/Users/$AGENT_USER}"

# Where this script itself is staged. Everything the host stages for us (nash, NucleicVMAgent.app,
# install-packages.sh, user-apps/) sits beside it, so this is the anchor for those lookups.
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"

# Nucleic's host PUBLIC key to authorize. Prefer the script argument; else a copy dropped next to the
# script or into the agent home; else the shared workspace (virtiofs) if the host staged it there.
PUBKEY_ARG="${1:-}"
SHARED_WORKSPACE="/Volumes/My Shared Files/workspace"
# The host-provisioning share (STATUS/STARTED/provision.log live here). Used to signal the host — e.g.
# to auto-confirm the Command Line Tools install-on-demand dialog via HID (see Phase 3). Only present
# during a host-driven base build; harmless when absent.
#
# A host-driven build exports NUCLEIC_PROVISION_SHARE (and runs us from it) pointing at the share's
# CUSTOM, SPACE-FREE mountpoint — see MacVMEngine.provisionMountPoint. Apple's automount root
# ("/Volumes/My Shared Files") is only the cold-entry path and remains the fallback here for a
# hands-on run, or one where the custom mount didn't take.
PROVISION_SHARE="${NUCLEIC_PROVISION_SHARE:-}"
if [ -z "$PROVISION_SHARE" ]; then
  if [ -f "$SCRIPT_DIR/password" ] || [ -f "$SCRIPT_DIR/STARTED" ]; then
    PROVISION_SHARE="$SCRIPT_DIR"
  else
    PROVISION_SHARE="/Volumes/My Shared Files/nucleic-provision"
  fi
fi

echo "▸ Provisioning this macOS guest as Nucleic's golden base (user: $AGENT_USER, home: $AGENT_HOME)"
echo "▸ Staged assets: $SCRIPT_DIR · provisioning share: $PROVISION_SHARE"

# ── Phase 0: sanity ──────────────────────────────────────────────────────────────────────────────
if [ "$(uname -s)" != "Darwin" ]; then
  echo "✗ this script runs INSIDE a macOS guest, not on Linux/other." >&2
  exit 1
fi
if ! /usr/bin/dscl . -read "/Users/$AGENT_USER" >/dev/null 2>&1; then
  echo "✗ account '$AGENT_USER' does not exist. Create it in Setup Assistant first (short name: $AGENT_USER)." >&2
  exit 1
fi
# We sudo repeatedly. Under a host-driven base build the bootstrap has already primed passwordless
# sudo (a NOPASSWD sudoers drop-in), so `sudo -v` returns without a prompt; a hands-on run may be
# asked for the password once here.
if sudo -n true 2>/dev/null; then
  echo "▸ sudo is already primed (passwordless) — continuing without a prompt."
else
  echo "▸ This uses sudo for system settings — you may be asked for the $AGENT_USER password once."
  sudo -v
fi

# Keep passwordless sudo after the base is sealed. Agent execs have no TTY in which to answer a
# password prompt, and this also makes hands-on/manual base provisioning match the host bootstrap.
if ! sudo grep -Eq '^[[:space:]]*@includedir[[:space:]]+/private/etc/sudoers.d' /etc/sudoers; then
  printf '%s\n' '@includedir /private/etc/sudoers.d' | sudo tee -a /etc/sudoers >/dev/null
fi
sudo mkdir -p /private/etc/sudoers.d
printf '%s ALL=(ALL) NOPASSWD: ALL\n' "$AGENT_USER" \
  | sudo tee /private/etc/sudoers.d/nucleic-agent >/dev/null
sudo chmod 0440 /private/etc/sudoers.d/nucleic-agent

# ── Phases 1–2: legacy SSH access (OFF by default — the control plane is vsock) ───────────────────
# Nucleic reaches the guest entirely over vsock now (exec + computer-use via the native in-guest
# agent installed in Phase 7d); Remote Login and an authorized SSH key are no longer part of the
# control plane and are NOT enabled. Set NUCLEIC_PROVISION_ENABLE_SSH=1 to opt back into the old
# SSH-over-NAT surface (e.g. for manual debugging) — then a public key may be passed as $1.
if [ "${NUCLEIC_PROVISION_ENABLE_SSH:-0}" = "1" ]; then
  echo "▸ [1/8] Enabling Remote Login (sshd) [opt-in] …"
  if sudo systemsetup -setremotelogin on 2>/dev/null; then
    echo "  ✓ Remote Login on."
  else
    echo "  ⚠ Could not toggle Remote Login via systemsetup (needs Full Disk Access for Terminal)." >&2
  fi

  echo "▸ [2/8] Installing Nucleic's host public key into $AGENT_HOME/.ssh/authorized_keys …"
  PUBKEY_SRC=""
  if [ -n "$PUBKEY_ARG" ] && [ -f "$PUBKEY_ARG" ]; then
    PUBKEY_SRC="$PUBKEY_ARG"
  else
    for cand in \
      "$SCRIPT_DIR/id_ed25519.pub" \
      "$AGENT_HOME/id_ed25519.pub" \
      "$SHARED_WORKSPACE/id_ed25519.pub"; do
      [ -f "$cand" ] && { PUBKEY_SRC="$cand"; break; }
    done
  fi
  if [ -n "$PUBKEY_SRC" ] && grep -Eq '^(ssh-ed25519|ssh-rsa|ecdsa-|sk-) ' "$PUBKEY_SRC"; then
    SSH_DIR="$AGENT_HOME/.ssh"; AUTH_KEYS="$SSH_DIR/authorized_keys"
    mkdir -p "$SSH_DIR"; touch "$AUTH_KEYS"
    KEY_LINE="$(tr -d '\r' < "$PUBKEY_SRC")"
    grep -qxF "$KEY_LINE" "$AUTH_KEYS" || printf '%s\n' "$KEY_LINE" >> "$AUTH_KEYS"
    chmod 700 "$SSH_DIR"; chmod 600 "$AUTH_KEYS"; sudo chown -R "$AGENT_USER":staff "$SSH_DIR"
    echo "  ✓ key authorized."
  else
    echo "  ⚠ NUCLEIC_PROVISION_ENABLE_SSH=1 but no valid public key found — skipping key authorization." >&2
  fi
else
  echo "▸ [1–2/8] Skipping Remote Login + SSH key (vsock-only base; set NUCLEIC_PROVISION_ENABLE_SSH=1 to opt in)."
fi

# ── Phase 3: Xcode Command Line Tools (the minimum toolchain) ────────────────────────────────────
# The CLT bootstrap clang/git/xcrun before the full staged Xcode is copied into place. Full Xcode is
# required later for xcodebuild, Apple SDKs, Metal, and SwiftUI; iOS simulator runtimes remain an
# optional, Apple-ID-gated download.
echo "▸ [3/8] Installing the Xcode Command Line Tools …"
# CLT are "present" only when the developer dir is set AND clang is actually resolvable. `xcode-select
# -p` alone can succeed against a half-registered dir, so check `xcrun --find clang` too.
clt_present() { /usr/bin/xcode-select -p >/dev/null 2>&1 && /usr/bin/xcrun --find clang >/dev/null 2>&1; }
# macOS ships no `timeout(1)`, and `softwareupdate -l` routinely WEDGES at "Finding available software"
# on beta / VM guests. Bound any command with perl's alarm (perl is always present) so one hung network
# call can't stall the whole build. `alarm` survives the exec, so the wrapped process is killed on time.
run_timeout() { perl -e 'alarm shift; exec @ARGV' "$@"; }

if clt_present; then
  echo "  ✓ CLT already installed at $(xcode-select -p)."
else
  # Install HEADLESSLY via softwareupdate — never a bare `xcode-select --install`, which pops a GUI
  # dialog that would block this unattended build. The on-demand trigger file makes the CLT package
  # appear in `softwareupdate -l`; it MUST exist *before* we query.
  TRIGGER=/tmp/.com.apple.dt.CommandLineTools.installondemand.in-progress
  sudo touch "$TRIGGER" 2>/dev/null || touch "$TRIGGER" 2>/dev/null || true

  # Find the CLT label — each query bounded so a wedged softwareupdate can't hang us. Match the
  # "* Label: Command Line Tools …" line specifically (NOT the "Title:" line), newest version last.
  # The trailing `|| true` is load-bearing under `set -euo pipefail`: on a beta/VM guest whose SU
  # catalog lists no CLT at all, `grep` exits 1 and would otherwise abort the WHOLE provision run
  # right here — before the on-demand-installer fallback below ever gets a chance.
  CLT_LABEL=""
  for attempt in 1 2 3; do
    CLT_LABEL="$(run_timeout 180 softwareupdate -l 2>/dev/null \
      | grep -E 'Label:.*Command Line Tools' \
      | sed -E 's/^.*Label: *//' | sort -V | tail -1 || true)"
    [ -n "$CLT_LABEL" ] && break
    echo "  (softwareupdate hasn't listed the CLT package yet — attempt $attempt) …"
  done

  if [ -z "$CLT_LABEL" ]; then
    sudo rm -f "$TRIGGER" 2>/dev/null || rm -f "$TRIGGER" 2>/dev/null || true
    echo "  ✗ Command Line Tools are unavailable through the headless Software Update catalog." >&2
    echo "    Refusing to open the graphical xcode-select installer during unattended provisioning." >&2
    echo "    Seed a base with CLT or make the CLT product visible to softwareupdate, then retry." >&2
    exit 1
  fi

  echo "  installing via softwareupdate: $CLT_LABEL"
  if ! run_timeout 1800 sudo softwareupdate -i "$CLT_LABEL" --verbose; then
    sudo rm -f "$TRIGGER" 2>/dev/null || rm -f "$TRIGGER" 2>/dev/null || true
    echo "  ✗ Headless Command Line Tools installation failed." >&2
    exit 1
  fi
  sudo rm -f "$TRIGGER" 2>/dev/null || rm -f "$TRIGGER" 2>/dev/null || true

  # Block (bounded, ≤20 min) until the tools actually land, BEFORE anything invokes xcodebuild/xcrun —
  # calling those while the CLT are absent is what pops the interactive installer.
  for i in $(seq 1 80); do
    clt_present && { echo "  ✓ CLT installed at $(xcode-select -p)."; break; }
    sleep 15
  done
fi
if ! clt_present; then
  echo "  ✗ Command Line Tools did not become usable after the headless installation." >&2
  exit 1
fi

# Accept license + run first-launch ONLY once the CLT are present — invoking `xcodebuild` without them
# triggers the interactive "install the command line developer tools" dialog that stalls the build.
# (With CLT-only and no full Xcode these just no-op on stderr, which we swallow.)
if clt_present; then
  sudo xcodebuild -runFirstLaunch 2>/dev/null || true
  sudo xcodebuild -license accept 2>/dev/null || true
else
  echo "  ⚠ CLT still not present after waiting — skipping xcodebuild first-launch to avoid a GUI prompt." >&2
fi

# ── Phase 4: Homebrew + dev tools ────────────────────────────────────────────────────────────────
echo "▸ [4/8] Installing Homebrew + dev tools …"
# Apple silicon Homebrew lives in /opt/homebrew. Install non-interactively if absent.
if [ -x /opt/homebrew/bin/brew ]; then
  BREW=/opt/homebrew/bin/brew
elif [ -x /usr/local/bin/brew ]; then
  BREW=/usr/local/bin/brew
else
  echo "  installing Homebrew (non-interactive) …"
  NONINTERACTIVE=1 /bin/bash -c \
    "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
  BREW=/opt/homebrew/bin/brew
  [ -x "$BREW" ] || BREW=/usr/local/bin/brew
fi
eval "$("$BREW" shellenv)"

echo "  installing packages via brew …"
# Bake the MAJOR dev toolchains into the golden base so every session clone is born ready to build
# JS/TS, Python, Rust, Go, and C/C++ projects without a first-run install. `brew install` is
# idempotent, and one big install call lets Homebrew resolve/download in parallel.
#   • Languages/runtimes:
#       node   → node / npm / npx / corepack (JS/TS; corepack fronts yarn + pnpm)
#       python → python3 / pip3 (the `pip` toolkit ships with the python formula)
#       rust   → rustc / cargo / rustup (Rust toolchain; `rustup` manages channels/targets)
#       go     → the Go toolchain (go build/test/mod)
#   • Build systems + VCS: git, cmake, make (GNU make as `gmake`), pkg-config, autoconf, automake, libtool
#   • Common CLI dev tools: jq, ripgrep, fd, fzf, tree, wget, curl, coreutils, gnu-sed, gh
# Kept as one list so a single `brew install` maximizes Homebrew's parallel fetch; `|| true` so one
# unbottled/failed formula never aborts the whole base build (the rest still install).
"$BREW" install \
  node python rust rustup go \
  git cmake make pkg-config autoconf automake libtool \
  jq ripgrep fd fzf tree wget curl coreutils gnu-sed gh \
  || true

# Rust: `rustup` needs a default toolchain selected before `rustc`/`cargo` resolve; do it here (idempotent,
# offline-safe once the toolchain is present) so the base ships a working Rust out of the box.
if command -v rustup >/dev/null 2>&1; then
  rustup default stable 2>/dev/null || rustup toolchain install stable 2>/dev/null || true
fi
# OPTIONAL tooling for extra Xcode simulator runtimes: `xcodes` + `aria2` for fast, resumable
# downloads. The full Xcode app itself is already staged from the host and is a required invariant.
# Purely best-effort — `xcodes` has no prebuilt bottle on beta macOS, so Homebrew may try to build it
# from source and fail (harmless `make` error); we don't need it for the base to work.
# Keep it fully quiet so a build-from-source failure never looks like a provisioning error.
if "$BREW" install --quiet aria2 xcodesorg/made/xcodes >/dev/null 2>&1; then
  echo "  ✓ installed xcodes + aria2 (optional simulator-runtime helpers)."
else
  echo "  (skipped xcodes — optional simulator-runtime helper; no beta bottle. aria2 …)"
  "$BREW" install --quiet aria2 >/dev/null 2>&1 || true
fi

cat <<'EOF'
  ── iOS simulator runtimes (optional, needs an Apple ID) ──────────────────────────────────────
  Full Xcode is staged from the host and verified later in this provisioning pass. To run iOS apps
  in this guest you also need a simulator runtime; that download is Apple-ID-gated. If xcodes was
  installed above, run (inside the guest, as agent):
        xcodes runtimes --include-unreleased | grep iOS
        xcodes runtimes install "iOS 18.0"   # or whatever simulator runtime you need
  ──────────────────────────────────────────────────────────────────────────────────────────────
EOF

# ── Phase 5: CRITICAL — toolchain PATH into /etc/zshenv ──────────────────────────────────────────
# Nucleic runs commands over SSH via the guest's default shell (zsh) in NON-login, non-interactive
# mode. Those shells source ONLY /etc/zshenv (not /etc/zprofile, ~/.zprofile, or ~/.zshrc), so PATH
# additions MUST live in /etc/zshenv or the engine won't find node/npm/python/xcodebuild. This is the
# single most load-bearing step in provisioning — get it wrong and remote builds fail with "command
# not found" despite the tools being installed.
echo "▸ [5/8] Appending toolchain PATH to /etc/zshenv (for non-login SSH shells) …"
BREW_PREFIX="$("$BREW" --prefix)"                       # /opt/homebrew (arm64) or /usr/local
ZSHENV=/etc/zshenv
MARKER="# >>> nucleic macvm toolchain PATH >>>"
END_MARKER="# <<< nucleic macvm toolchain PATH <<<"
# Idempotent: strip any existing Nucleic block first, then append a fresh one — so re-runs never
# duplicate the block or leave a stale PATH. awk matches the markers as FIXED strings (index()),
# avoiding all the regex-escaping hazards of doing this with sed.
if [ -f "$ZSHENV" ] && grep -qF "$MARKER" "$ZSHENV"; then
  echo "  ✓ /etc/zshenv already has the Nucleic PATH block (refreshing it)."
  TMP_ZSHENV="$(mktemp)"
  awk -v start="$MARKER" -v end="$END_MARKER" '
    index($0, start) { skip = 1 }
    !skip           { print }
    index($0, end)  { skip = 0 }
  ' "$ZSHENV" > "$TMP_ZSHENV"
  sudo cp "$TMP_ZSHENV" "$ZSHENV"
  rm -f "$TMP_ZSHENV"
fi
sudo tee -a "$ZSHENV" >/dev/null <<EOF
$MARKER
# Added by scripts/provision-macos-guest.sh so Nucleic's non-login SSH zsh shells find the toolchain.
# Homebrew bin/sbin first, then the per-user toolchain bin dirs (\$HOME evaluated by the shell at runtime)
# so \`cargo install\` (~/.cargo/bin) and \`go install\` (~/go/bin) binaries also resolve in agent shells.
export PATH="$BREW_PREFIX/bin:$BREW_PREFIX/sbin:\$HOME/.cargo/bin:\$HOME/go/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:\$PATH"
$END_MARKER
EOF
echo "  ✓ PATH written to $ZSHENV (prefix: $BREW_PREFIX)."

# Homebrew installation is intentionally one parallel transaction, but `brew install … || true`
# above can return after one failed formula while leaving the rest installed. Do not turn that partial
# result into a golden base whose clones discover missing compilers one session at a time. These are
# the core tools Phase 4 promises; optional helpers such as xcodes/aria2 are deliberately excluded.
echo "  verifying required Homebrew toolchains …"
MISSING_TOOLS=()
for tool in node npm python3 pip3 rustc cargo go git cmake pkg-config; do
  command -v "$tool" >/dev/null 2>&1 || MISSING_TOOLS+=("$tool")
done
if [ "${#MISSING_TOOLS[@]}" -ne 0 ]; then
  echo "  ✗ required development tools are missing: ${MISSING_TOOLS[*]}" >&2
  echo "    Homebrew provisioning was incomplete; retry the base build instead of publishing it." >&2
  exit 1
fi
echo "  ✓ required Homebrew toolchains resolve in the provisioning shell."

# ── Phase 5⅓: nash — the Nucleic agent shell (docs/NASH.md §7.5) ─────────────────────────────────
# nash is the ONLY shell agent commands run under in a virtualized guest (locked decision: no zsh
# fallback on the exec surface — the in-guest agent refuses to exec when nash is missing). On macOS
# the system shell can't be diverted (SIP), so we install nash to /usr/local/bin and the agent runs
# every command under `nash -lc`. Because a non-login `nash -l` sources /etc/profile + /etc/profile.d
# (not /etc/zshenv), we MIRROR the same toolchain PATH into a profile.d drop-in so
# node/npm/python/xcodebuild resolve there too. A base with no staged nash is therefore NOT a usable
# base: this phase HARD-FAILS the provision rather than quietly producing a guest whose exec surface
# is dead. The universal nash binary is a build artifact (NASH.md §8); stage it next to this script
# the way NucleicVMAgent.app is staged.
echo "▸ [5⅓] Installing nash (the Nucleic agent shell) …"
NASH_SRC=""
for cand in \
  "$SCRIPT_DIR/nash" \
  "$AGENT_HOME/nash" \
  "${SHARED_WORKSPACE:-}/nash"; do
  if [ -n "$cand" ] && [ -f "$cand" ]; then NASH_SRC="$cand"; break; fi
done
if [ -n "$NASH_SRC" ]; then
  sudo mkdir -p /usr/local/bin
  sudo cp "$NASH_SRC" /usr/local/bin/nash
  sudo chmod 0755 /usr/local/bin/nash
  # Mirror the toolchain PATH into a profile.d drop-in that `nash -l` sources (sh syntax; same value
  # as the /etc/zshenv block above). Idempotent — overwrite the file each run.
  sudo mkdir -p /etc/profile.d
  sudo tee /etc/profile.d/nucleic-nash-path.sh >/dev/null <<EOF
# Added by scripts/provision-macos-guest.sh so \`nash -l\` shells find the toolchain (NASH.md §7.5).
# Mirrors the /etc/zshenv PATH block used by the guest's zsh shells.
export PATH="$BREW_PREFIX/bin:$BREW_PREFIX/sbin:\$HOME/.cargo/bin:\$HOME/go/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:\$PATH"
EOF
  # Set the agent account's login shell to nash (dscl). The vsock agent spawns nash directly, so this
  # is belt-and-suspenders for any interactive/SSH shell; re-provisioning (or pointing it back to
  # /bin/zsh) fully reverts it.
  sudo /usr/bin/dscl . -create "/Users/$AGENT_USER" UserShell /usr/local/bin/nash || \
    echo "    ⚠ could not set login shell to nash (non-fatal; exec still uses nash directly)." >&2
  echo "  ✓ nash installed → /usr/local/bin/nash ($("/usr/local/bin/nash" --version 2>/dev/null || echo 'version unknown')); PATH mirrored to /etc/profile.d."
else
  echo "  ✗ no nash binary staged — ABORTING. nash is the required agent shell in every virtualized" >&2
  echo "    environment (NASH.md §7.5): without it the guest's exec surface refuses to run. Build the" >&2
  echo "    darwin binary (scripts/build-nash-macos.sh) or use a packaged app that embeds it, then" >&2
  echo "    rebuild the base." >&2
  exit 1
fi

# ── Phase 5½: disable sleep / display-blanking (the guest must never nod off) ─────────────────────
# A session clone is a headless, auto-login worker that the host drives and SCREENSHOTS over vsock.
# With Apple's default energy-saver timers, after ~10 idle minutes macOS blanks the (virtual) display
# and can idle-sleep the whole guest — so the host's captures come back BLACK and computer-use no-ops,
# exactly the "monitors go black / VM seems asleep" symptom. The Linux base already pins this off via
# dconf (idle-delay=0, sleep-inactive-*-type='nothing', screensaver off); this is the macOS parallel,
# baked into the golden base so every clone is born awake.
#
# pmset writes /Library/Preferences/SystemConfiguration/com.apple.PowerManagement.plist (persists into
# the base image, hence into every clone). `-a` = all power sources; a VM has no real battery, but -a
# covers whatever source VZ reports. Some keys (e.g. powernap) may be rejected on a VM — tolerated.
echo "▸ [5½/8] Disabling display/system sleep so headless captures never go black …"
sudo pmset -a displaysleep 0 sleep 0 disksleep 0 2>/dev/null || \
  echo "  ⚠ 'pmset -a displaysleep 0 sleep 0 disksleep 0' reported an error (continuing)." >&2
# powernap is best-effort — not all VM builds accept it; never fail provisioning over it.
sudo pmset -a powernap 0 2>/dev/null || true
# The SCREENSAVER blanks the screen independently of display-sleep, so pin its idle timer to 0 (off)
# for the auto-login `agent` (ByHost pref). This script runs AS agent, so no sudo — target agent's db.
defaults -currentHost write com.apple.screensaver idleTime 0 2>/dev/null || \
  echo "  ⚠ could not disable the screensaver idle timer for $AGENT_USER (continuing)." >&2
echo "  ✓ sleep disabled. Current pmset:"
pmset -g custom 2>/dev/null | grep -Ei 'displaysleep|[^k] sleep|disksleep|powernap' | sed 's/^/      /' || true

# ── Phase 5¾: disable the "Tips" app (don't let it auto-launch in the clones) ─────────────────────
# macOS auto-launches the Tips app (via the com.apple.tipsd launch agent) on early logins to show its
# "what's new" window. On a headless, auto-login session clone that window just steals focus and clutters
# the screen we screenshot — so pin tipsd OFF in the golden base. `launchctl disable gui/<uid>/<svc>`
# writes a PERSISTENT override (/var/db/com.apple.xpc.launchd/disabled.<uid>.plist) that lives in the
# base image, hence in every clone. Target the `agent` uid explicitly (not `id -u`) so this is correct
# even if the provisioner is ever invoked via sudo. Best effort — never fail the base over a cosmetic tweak.
echo "▸ [5¾/8] Disabling the Tips app (com.apple.tipsd) so it never auto-launches in clones …"
TIPS_UID="$(id -u "$AGENT_USER" 2>/dev/null || id -u)"
if sudo launchctl disable "gui/$TIPS_UID/com.apple.tipsd" 2>/dev/null \
  || launchctl disable "gui/$TIPS_UID/com.apple.tipsd" 2>/dev/null; then
  echo "  ✓ tipsd disabled for uid $TIPS_UID."
else
  echo "  ⚠ could not disable com.apple.tipsd for uid $TIPS_UID (continuing)." >&2
fi

# ── Phase 5⅞: disable window restoration (don't reopen the build's Terminal in clones) ─────────────
# The base is built by DRIVING a Terminal window to run the host bootstrap (nucleic-bootstrap.sh, which
# runs THIS script); that window is still open when the build powers the guest off. macOS's "Resume"
# feature saves open windows at logout/shutdown and RE-OPENS them on the next login — so without this,
# every session clone boots with the leftover `nucleic-bootstrap.sh` Terminal window restored onto the
# very desktop we screenshot. Pin window restoration OFF (per the `agent` user, baked into the base) so
# clones boot to a clean desktop and no stale Terminal window lingers after the bootstrap completes:
#   • TALLogoutSavesState=false          → loginwindow doesn't save open windows at logout/shutdown
#   • NSQuitAlwaysKeepsWindows=false     → apps don't reopen their windows on relaunch (global + Terminal)
# Runs as `agent`, so these land in the auto-login user's domains that loginwindow/Terminal actually read.
echo "▸ [5⅞/8] Disabling window restoration (so clones don't reopen the build's Terminal window) …"
defaults write com.apple.loginwindow TALLogoutSavesState -bool false 2>/dev/null \
  || echo "  ⚠ could not disable loginwindow state saving (continuing)." >&2
defaults write NSGlobalDomain NSQuitAlwaysKeepsWindows -bool false 2>/dev/null || true
defaults write com.apple.Terminal NSQuitAlwaysKeepsWindows -bool false 2>/dev/null || true
# Drop any Terminal window state already saved during this build, so nothing lingers to restore.
rm -rf "$AGENT_HOME/Library/Saved Application State/com.apple.Terminal.savedState" 2>/dev/null || true
echo "  ✓ window restoration disabled for $AGENT_USER."

# ── Phase 5⅞+: ensure NO notification-sweeping LaunchAgent is installed (removed) ─────────────────
# An earlier base installed a LaunchAgent that ran `killall NotificationCenter` at login and every 60s
# to clear banners off the screenshotted desktop. That is REMOVED — proven wrong on a live macOS 27
# clone for two reasons:
#   1. It does not work. The recurring boot banners (Data Access Blocked / Multiple Extensions Added /
#      Tips) are persistent ALERT-style notifications; NotificationCenter is only the UI, has launchd
#      KeepAlive, and RE-DRAWS them the instant it relaunches (`killall usernoted` doesn't clear them
#      either). They are actually REPLAYED from the sealed base snapshot — the real fix is to seal the
#      base with them dismissed and to kill their SOURCES (see scripts/macos-notification-fix/README.md).
#   2. It is HARMFUL. A session may be exercising an app's OWN notifications; a blanket 60s sweep would
#      dismiss those too. The base must leave the notification framework intact and remove only the
#      specific noise sources (Full Disk Access for the agent → no "Data Access Blocked"; Tips
#      notifications off → no "Tips").
# So this step now GUARANTEES the sweeper is gone: boot out any live instance and delete the plist.
echo "▸ [5⅞+/8] Removing the old NotificationCenter-clearing LaunchAgent (ineffective + harmful) …"
KILLNC_PLIST="/Library/LaunchAgents/xyz.blakeslee.nucleic.killnotificationcenter.plist"
KILLNC_UID="$(id -u "$AGENT_USER" 2>/dev/null || id -u)"
sudo launchctl bootout "gui/$KILLNC_UID/xyz.blakeslee.nucleic.killnotificationcenter" 2>/dev/null || true
if [ -f "$KILLNC_PLIST" ]; then
  sudo rm -f "$KILLNC_PLIST" && echo "  ✓ removed $KILLNC_PLIST." || echo "  ⚠ could not remove $KILLNC_PLIST." >&2
else
  echo "  ✓ no sweeper LaunchAgent present (nothing to remove)."
fi

# ── Phase 6: warm the simulator + shut down clean ────────────────────────────────────────────────
# Pre-accept / warm the iOS simulator first-launch so the first real agent turn isn't slowed by it.
# Harmless (and skipped) if only the CLT are installed and no runtimes exist yet.
echo "▸ [6/8] Warming the iOS simulator (best effort) …"
xcrun simctl list >/dev/null 2>&1 && echo "  ✓ simctl responsive." || echo "  (simctl not available yet — full Xcode/runtimes not installed.)"

# ══════════════════════════════════════════════════════════════════════════════════════════════════
# ── Phase 7: COMPUTER USE (GUI automation) ────────────────────────────────────────────────────────
# ══════════════════════════════════════════════════════════════════════════════════════════════════
# This phase makes the base drivable by Nucleic's `mac_vm_computer` MCP tool, which SSHes in and
# dispatches screen/input commands into the console user's Aqua GUI session:
#
#     launchctl asuser $(id -u) <ABSOLUTE-PATH> …
#
# using screencapture for screenshots (base64'd back), cliclick for mouse/keyboard, and `open -a` for
# apps. Three things must be true in the guest for that to work; this phase sets up all three,
# IDEMPOTENTLY:
#
#   (a) cliclick lives at the CANONICAL path /usr/local/bin/cliclick (the engine calls it there, and
#       TCC attributes Accessibility to that exact path);
#   (b) `agent` AUTO-LOGS-IN, so a headless boot has an active Aqua session (screencapture and
#       cliclick have no GUI session to talk to otherwise);
#   (c) Screen Recording + Accessibility are PRE-GRANTED in the SYSTEM TCC.db — the only unattended
#       way to do which is SIP-off + a direct sqlite3 write (see the honest caveats below).
#
# See docs/MACOS_VM.md §12 (Computer use) for the full picture and the SIP/TCC reality.
echo "▸ [7/8] Setting up computer use (cliclick + auto-login + TCC pre-grant) …"

# ── 7a: cliclick at the canonical /usr/local/bin/cliclick ─────────────────────────────────────────
# Homebrew on Apple silicon installs to /opt/homebrew/bin, but the Swift engine calls cliclick at the
# EXACT path /usr/local/bin/cliclick, and TCC must attribute Accessibility to that exact path. So we
# `brew install` it, then COPY it (NOT a symlink — a copy preserves the code signature and gives a
# real file at the TCC-granted path; a symlink's target path can confuse TCC attribution).
echo "  ▸ [7a] Installing cliclick and copying it to /usr/local/bin/cliclick …"
"$BREW" install cliclick 2>/dev/null || "$BREW" install cliclick || true
CLICLICK_SRC="$("$BREW" --prefix)/bin/cliclick"
if [ -x "$CLICLICK_SRC" ]; then
  sudo mkdir -p /usr/local/bin
  sudo cp "$CLICLICK_SRC" /usr/local/bin/cliclick
  if /usr/local/bin/cliclick p >/dev/null 2>&1; then
    echo "    ✓ /usr/local/bin/cliclick installed (from $CLICLICK_SRC) and responsive."
  else
    echo "    ⚠ /usr/local/bin/cliclick exists but 'cliclick p' failed — it may need Accessibility (7c)." >&2
  fi
else
  echo "    ✗ brew did not produce cliclick at $CLICLICK_SRC — install it by hand, then re-run." >&2
fi

# ── 7b: auto-login for agent (so a headless boot has an Aqua session) ─────────────────────────────
# screencapture and cliclick need a LIVE console GUI (Aqua) session. A headless clone that boots to
# the login window has none, so screenshots come back black and clicks no-op. Auto-login lands `agent`
# straight into the desktop on every boot.
#
#   ⚠ FileVault MUST be OFF for this to work — auto-login is fundamentally incompatible with FileVault
#     (FV requires the password at pre-boot to unlock the disk, which defeats auto-login). The base is
#     a disposable clone, not a device holding secrets, so FileVault-off is the right trade here.
#
# The password comes from $NUCLEIC_AGENT_PW, else the 2nd script arg ($2). We do NOT hardcode it.
echo "  ▸ [7b] Enabling auto-login for '$AGENT_USER' …"
AGENT_PW="${NUCLEIC_AGENT_PW:-${2:-}}"
if [ -z "$AGENT_PW" ]; then
  echo "    ⚠ No agent password provided (set \$NUCLEIC_AGENT_PW or pass it as the 2nd arg) — SKIPPING"
  echo "      auto-login. Without it, a headless boot has no Aqua session and computer use won't work."
  echo "      Re-run as:  ./provision-macos-guest.sh /path/to/id_ed25519.pub 'AGENT_PASSWORD'" >&2
else
  if sudo sysadminctl -autologin set -userName "$AGENT_USER" -password "$AGENT_PW" 2>/dev/null; then
    echo "    ✓ auto-login set for $AGENT_USER."
    sudo sysadminctl -autologin status 2>&1 | sed 's/^/      /' || true
  else
    echo "    ⚠ sysadminctl could not set auto-login (is FileVault ON? it must be OFF)." >&2
  fi
fi

# ── 7c: TCC pre-grants — NOT DONE IN THIS SCRIPT (Lane B owns them, in a separate boot) ────────────
# This toolchain provisioner deliberately writes NO TCC rows and touches SIP not at all — and the
# guard test in Tests/NucleicCoreTests/MacVMTests.swift enforces that (no `TCC.db` / `csrutil` /
# `kTCCServiceSystemPolicy` in its executable lines).
#
# The DEFAULT computer-use path is host-side virtual IO (docs/MACOS_VM.md §12.2): the host captures
# the guest's framebuffer from its VZVirtualMachineView and injects synthesized NSEvents into the
# guest's virtual HID. That is ordinary virtual hardware, so the guest grants NOTHING — no TCC, no
# SIP — for ordinary computer use.
#
# The other grants the base bakes in — Full Disk Access, Network Volumes, Screen Recording,
# Accessibility, PostEvent — are delivered by **Lane B** (docs/MACOS_VM_POLICY_ALTERNATIVE.md), NOT
# here. The engine runs a dedicated pass BEFORE this toolchain boot: it boots the base to recoveryOS
# to `csrutil disable`, writes those rows straight into the system TCC.db keyed to the agent's compiled
# designated requirement (scripts/macvm-policy-grants.sh, run over vsock), then re-enables SIP and
# confirms the rows survived. So by the time this script runs, SIP is back ON and the grants are
# already baked in — nothing for this phase to do. (Lane A/MDM, behind nucleic.macvm.policyLane=mdm,
# delivers FDA/Network Volumes via a PPPC profile instead; it can never grant Screen Recording.)
echo "  ▸ [7c] TCC pre-grants: handled by the Lane B policy-grants pass, not this toolchain script."

# ── 7d: the NATIVE IN-GUEST AGENT (NucleicVMAgent.app + LaunchAgent + TCC) ────────────────────────
# The preferred computer-use path (docs/MACOS_VM_NATIVE_AGENT.md): a small signed .app, run as a
# per-user LaunchAgent in the auto-login Aqua session, that the host reaches over VSOCK (no SSH) and
# that drives the guest with Apple's frameworks directly — AXUIElement (semantic observe/control,
# and the ONLY observation channel on a macOS 26 guest whose framebuffer captures come back blank),
# ScreenCaptureKit (capture), CGEvent (raw input). Entirely OPTIONAL: when the app isn't staged, the
# base still works via the SSH + cliclick path above (Nucleic probes and falls back automatically).
#
# Build it on the HOST with scripts/build-vm-agent.sh and stage dist/* (the .app + the LaunchAgent
# plist) next to this script, in the agent home, or in the shared workspace.
echo "  ▸ [7d] Installing the native in-guest agent (NucleicVMAgent.app) …"
AGENT_APP_DEST="/Applications/NucleicVMAgent.app"
AGENT_LA_PLIST="/Library/LaunchAgents/xyz.blakeslee.nucleic.vmagent.plist"
AGENT_BUNDLE_ID="xyz.blakeslee.nucleic.vmagent"
AGENT_APP_SRC=""
for cand in \
  "$SCRIPT_DIR/NucleicVMAgent.app" \
  "$AGENT_HOME/NucleicVMAgent.app" \
  "$SHARED_WORKSPACE/NucleicVMAgent.app"; do
  [ -d "$cand" ] && { AGENT_APP_SRC="$cand"; break; }
done

if [ -z "$AGENT_APP_SRC" ]; then
  echo "    ⚠ NucleicVMAgent.app not staged — SKIPPING the native agent. Computer use will fall back"
  echo "      to the SSH + cliclick path (no ax_* semantic actions, and NO computer-use at all on a"
  echo "      macOS 26 guest whose screenshots are blank). To add it: on the host run"
  echo "      scripts/build-vm-agent.sh, stage its dist/* next to this script, and re-run."
else
  # STOP the running agent before replacing its bundle. It's a KeepAlive LaunchAgent, so on an
  # auto-login session it is live during this pass — and if it (or launchd's KeepAlive respawn) races
  # the `rm -rf`/copy, the result is a MALFORMED bundle: `codesign` reports "bundle format
  # unrecognized, invalid, or unsuitable" and launchd then refuses to spawn it (EX_CONFIG / spawn
  # failed), so the in-guest agent never comes up on the next boot. (The minimal exec-agent pass boots
  # it out first for exactly this reason; the full pass must too.)
  AGENT_UID="$(id -u "$AGENT_USER" 2>/dev/null || id -u)"
  sudo /bin/launchctl bootout "gui/$AGENT_UID/$AGENT_BUNDLE_ID" 2>/dev/null || true
  /bin/launchctl bootout "gui/$AGENT_UID/$AGENT_BUNDLE_ID" 2>/dev/null || true
  sleep 1

  # Copy the bundle with `ditto`, not `cp -R`: ditto is bundle-aware and reliably preserves an .app's
  # structure + code signature (cp -R can mangle a bundle copied off a virtiofs share), then strip any
  # quarantine xattr.
  sudo rm -rf "$AGENT_APP_DEST"
  sudo /usr/bin/ditto "$AGENT_APP_SRC" "$AGENT_APP_DEST"
  sudo xattr -dr com.apple.quarantine "$AGENT_APP_DEST" 2>/dev/null || true

  # VERIFY the installed bundle before trusting it — an invalid bundle here is the difference between a
  # working base and a clone whose agent silently never launches. One bounded re-copy on failure.
  if ! sudo /usr/bin/codesign -v "$AGENT_APP_DEST" 2>/dev/null; then
    echo "    ⚠ installed agent bundle failed codesign verification — re-copying with ditto …" >&2
    sudo rm -rf "$AGENT_APP_DEST"
    sudo /usr/bin/ditto "$AGENT_APP_SRC" "$AGENT_APP_DEST"
    sudo xattr -dr com.apple.quarantine "$AGENT_APP_DEST" 2>/dev/null || true
  fi
  if sudo /usr/bin/codesign -v "$AGENT_APP_DEST" 2>/dev/null; then
    echo "    ✓ installed $AGENT_APP_DEST (from $AGENT_APP_SRC; codesign valid)."
  else
    echo "    ✗ agent bundle at $AGENT_APP_DEST is INVALID (codesign rejects it) — the in-guest agent" >&2
    echo "      will not launch. Details:" >&2
    sudo /usr/bin/codesign -vvv "$AGENT_APP_DEST" 2>&1 | sed 's/^/        /' >&2 || true
  fi

  # LaunchAgent: Aqua-session-only, RunAtLoad + KeepAlive (staged copy preferred, generated else).
  AGENT_LA_SRC="$(dirname "$AGENT_APP_SRC")/xyz.blakeslee.nucleic.vmagent.plist"
  if [ -f "$AGENT_LA_SRC" ]; then
    sudo cp "$AGENT_LA_SRC" "$AGENT_LA_PLIST"
  else
    sudo tee "$AGENT_LA_PLIST" >/dev/null <<'PLIST'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>xyz.blakeslee.nucleic.vmagent</string>
    <key>ProgramArguments</key>
    <array>
        <string>/Applications/NucleicVMAgent.app/Contents/MacOS/NucleicVMAgent</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <true/>
    <key>LimitLoadToSessionType</key>
    <string>Aqua</string>
    <key>AssociatedBundleIdentifiers</key>
    <string>xyz.blakeslee.nucleic.vmagent</string>
    <key>StandardOutPath</key>
    <string>/tmp/nucleic-vmagent.log</string>
    <key>StandardErrorPath</key>
    <string>/tmp/nucleic-vmagent.log</string>
</dict>
</plist>
PLIST
  fi
  sudo chown root:wheel "$AGENT_LA_PLIST"
  sudo chmod 644 "$AGENT_LA_PLIST"
  echo "    ✓ LaunchAgent at $AGENT_LA_PLIST (Aqua session, RunAtLoad, KeepAlive)."

  # No TCC rows are written for the agent here, for the reason given in 7c: the system TCC.db is not
  # writable with SIP on, and SIP stays on. The agent's vsock exec channel — which is what
  # provisioning and `mac_vm_exec` actually use — needs no TCC at all. Its AX/screen-capture ops do,
  # and they stay unavailable until an MDM PPPC payload grants Accessibility.
fi

echo ""
echo "  ── Computer use runs HOST-SIDE (framebuffer + virtual HID) and needs nothing from the guest ──"
echo "  If the in-guest agent is unreachable after this base is live, check, in the guest:"
echo "    • agent is auto-logged-in:    an Aqua/desktop session is active (not the login window)"
echo "    • agent is running:           tail /tmp/nucleic-vmagent.log"
echo "    • native agent alive (7d):    cat /tmp/nucleic-vmagent.log — should show 'starting on"
echo "                                  vsock port 2035' and readiness true×3 (if the app was staged)"
echo "  ──────────────────────────────────────────────────────────────────────────────────────────────"

# ══════════════════════════════════════════════════════════════════════════════════════════════════
# ── Phase 7½: user-provided apps → /Applications ────────────────────────────────────────────────────
# ══════════════════════════════════════════════════════════════════════════════════════════════════
# Nucleic stages any .app bundles the operator picked in Settings ("Base image" → "Included apps")
# into a `user-apps/` subdir of the provisioning share; bake each into the base's /Applications so
# every session clone has them. Optional apps remain best effort, but Xcode is a required base
# invariant: a failed or incomplete Xcode copy aborts provisioning. `ditto` is bundle-aware and
# preserves symlinks, executable bits, and signatures across virtiofs. Quarantine is stripped so
# copied apps launch without Gatekeeper prompting inside the (disposable, NAT-isolated) guest.
USER_APPS_DIR="$PROVISION_SHARE/user-apps"
if [ -d "$USER_APPS_DIR" ]; then
  echo "▸ Installing user-provided apps into /Applications …"
  shopt -s nullglob
  for app in "$USER_APPS_DIR"/*.app; do
    name="$(basename "$app")"
    dest="/Applications/$name"
    required=0
    case "$name" in Xcode*.app) required=1 ;; esac
    if sudo rm -rf "$dest" && sudo /usr/bin/ditto "$app" "$dest"; then
      sudo xattr -dr com.apple.quarantine "$dest" 2>/dev/null || true
      if [ "$required" -eq 1 ] && [ ! -x "$dest/Contents/Developer/usr/bin/xcodebuild" ]; then
        echo "  ✗ installed $name is incomplete (xcodebuild is missing) — aborting." >&2
        exit 1
      fi
      echo "  ✓ installed $dest"
    else
      if [ "$required" -eq 1 ]; then
        echo "  ✗ failed to install required $name — aborting instead of publishing a broken base." >&2
        exit 1
      fi
      echo "  ⚠ failed to install optional $name — skipping." >&2
    fi
  done
  shopt -u nullglob
fi

# ══════════════════════════════════════════════════════════════════════════════════════════════════
# ── Phase 7⅝: Chrome for Testing (default browser target) → /Applications ───────────────────────────
# ══════════════════════════════════════════════════════════════════════════════════════════════════
# Every base gets **Chrome for Testing** — this is not an operator opt-in. It is what the `chrome`
# key of `open_browser` / the computer-use browser target resolves to (see `knownBrowsers` in
# Sources/NucleicCore/MacVM/MacVMEngine+Computer.swift).
#
# Chrome for Testing rather than stock Chrome because CfT is a *pinned* build with auto-update
# machinery stripped out: a disposable clone can't have Chrome silently updating (or nagging) under
# an agent mid-session, and a run is reproducible against a known version. Downloads are public and
# unauthenticated, so the guest fetches it unattended over its NAT network.
#
# Best effort throughout: under `set -e` every step is guarded so a network hiccup logs and moves on
# rather than aborting the base build.
echo "▸ [7⅝/8] Installing Chrome for Testing (the 'chrome' browser target) …"
case "$(uname -m)" in
  arm64) CFT_PLATFORM="mac-arm64" ;;
  *)     CFT_PLATFORM="mac-x64" ;;
esac
CFT_APP="Google Chrome for Testing.app"
if CFT_VERSION="$(run_timeout 120 curl -fsSL --retry 3 \
      https://googlechromelabs.github.io/chrome-for-testing/LATEST_RELEASE_STABLE 2>/dev/null)" \
   && [ -n "$CFT_VERSION" ]; then
  CFT_TMP="$(mktemp -d)"
  CFT_URL="https://storage.googleapis.com/chrome-for-testing-public/$CFT_VERSION/$CFT_PLATFORM/chrome-$CFT_PLATFORM.zip"
  echo "  ↓ downloading Chrome for Testing $CFT_VERSION ($CFT_PLATFORM) …"
  if run_timeout 900 curl -fL --retry 3 -o "$CFT_TMP/chrome.zip" "$CFT_URL" \
     && run_timeout 300 /usr/bin/ditto -x -k "$CFT_TMP/chrome.zip" "$CFT_TMP/x" 2>/dev/null; then
    # `ditto -x -k` (not `unzip`) so the bundle's internal symlinks + exec bits survive extraction —
    # an unzipped Chrome framework is subtly broken and won't launch.
    if [ -d "$CFT_TMP/x/chrome-$CFT_PLATFORM/$CFT_APP" ]; then
      sudo rm -rf "/Applications/$CFT_APP"
      if sudo cp -R "$CFT_TMP/x/chrome-$CFT_PLATFORM/$CFT_APP" "/Applications/$CFT_APP"; then
        sudo xattr -dr com.apple.quarantine "/Applications/$CFT_APP" 2>/dev/null || true
        # Register with LaunchServices so `open -a "Google Chrome for Testing"` resolves immediately,
        # without waiting for the periodic /Applications rescan.
        /System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister \
          -f "/Applications/$CFT_APP" >/dev/null 2>&1 || true
        echo "  ✓ Chrome for Testing $CFT_VERSION installed."
      else
        echo "  ⚠ could not copy $CFT_APP into /Applications (continuing)." >&2
      fi
    else
      echo "  ⚠ $CFT_APP not found in the downloaded archive (continuing)." >&2
    fi
  else
    echo "  ⚠ Chrome for Testing download/extract failed (continuing)." >&2
  fi
  rm -rf "$CFT_TMP"
else
  echo "  ⚠ could not resolve the latest Chrome for Testing version (continuing)." >&2
fi

# Pre-complete first-run so a computer-use clone opens straight to a usable window instead of the
# welcome tab + "make Chrome your default" / import / sign-in prompts. The "First Run" sentinel in the
# user-data dir tells Chromium first-run already happened, so it skips that flow; the seeded
# Default/Preferences keeps the recurring "make default" bubble off and marks a clean prior exit (no
# "restore pages?" bubble). Seeds ONLY setup state — sign-in stays available for tasks that need it.
# CfT keeps its profile under "Google/Chrome for Testing", NOT the stock "Google/Chrome".
CFT_DIR="$AGENT_HOME/Library/Application Support/Google/Chrome for Testing"
if mkdir -p "$CFT_DIR/Default" 2>/dev/null; then
  touch "$CFT_DIR/First Run" 2>/dev/null || true
  cat > "$CFT_DIR/Default/Preferences" <<'PREFS' 2>/dev/null || true
{"browser":{"check_default_browser":false,"has_seen_welcome_page":true},"profile":{"exit_type":"Normal","exited_cleanly":true}}
PREFS
  sudo chown -R "$AGENT_USER":staff "$AGENT_HOME/Library/Application Support/Google" 2>/dev/null || true
  echo "  ✓ Chrome for Testing first-run pre-completed for $AGENT_HOME."
else
  echo "  ⚠ could not seed Chrome for Testing first-run state under $AGENT_HOME (continuing)." >&2
fi

# ══════════════════════════════════════════════════════════════════════════════════════════════════
# ── Phase 7¾: common packages (operator-selected) → install ─────────────────────────────────────────
# ══════════════════════════════════════════════════════════════════════════════════════════════════
# When the operator opts into any "Common packages" in Settings, the host stages a generated
# `install-packages.sh` (from Sources/NucleicCore/MacVM/MacVMPackage.swift) into the provisioning
# share; each package fetches + installs itself into the guest over the guest's NAT network —
# provisioning has network up by this point. (Chrome is NOT one of these: every base installs Chrome
# for Testing unconditionally in Phase 7⅝.) Best effort + idempotent: no
# staged installer (a hands-on run, or nothing selected) just skips this. Runs as `agent` (admin, so
# /Applications is writable) — the same script the after-the-fact vsock path runs into a live guest.
PKG_INSTALLER="$PROVISION_SHARE/install-packages.sh"
[ -f "$PKG_INSTALLER" ] || PKG_INSTALLER="$SCRIPT_DIR/install-packages.sh"
if [ -f "$PKG_INSTALLER" ]; then
  echo "▸ Installing operator-selected common packages …"
  /bin/bash "$PKG_INSTALLER" || echo "  ⚠ common-package install reported errors (continuing)." >&2
fi

# ══════════════════════════════════════════════════════════════════════════════════════════════════
# ── Phase 7⅞: FULL Xcode license + first-launch + Metal toolchain (bake in, not per-run) ────────────
# ══════════════════════════════════════════════════════════════════════════════════════════════════
# Every base gets a FULL Xcode: the host's own /Applications/Xcode[-beta].app is staged automatically
# alongside the operator's "Included apps" (MacVMEngine+Provision.swift → `addingHostXcode`) and lands
# in Phase 7½. This phase finishes its one-time setup so every disposable session clone is born
# build-ready. Without this, the first agent turn that builds a full-Xcode / Metal target pays for the
# first-launch component install AND the Metal toolchain download itself — on EVERY clone, since clones
# are thrown away. That's the "Xcode[-beta] needs its first-launch component install, then retry the
# Metal toolchain download" churn this phase eliminates.
#
# It is also the ONLY way this guest can have a `metal` compiler at all. The Command Line Tools ship no
# `xcodebuild`, so `-downloadComponent MetalToolchain` — Apple's only supported Metal installer — is
# unreachable without a full Xcode; and even with the toolchain asset installed, `xcrun` refuses to
# resolve `metal` unless a full Xcode is the selected developer dir. Hence the verification at the end:
# a base that can't compile a shader should say so HERE, not inside some agent's build an hour later.
#
# Runs AFTER Phase 7½/7¾ (that's when Xcode.app lands) and is idempotent. Every verification here is
# a publication gate: a base without a working Apple SDK is not a successful macOS development base.
# `run_timeout` bounds network/toolchain steps so a wedged download fails the build rather than hanging.
echo "▸ [7⅞/8] Finalizing and verifying required full Xcode (license, SDK, SwiftUI, Metal) …"
# Locate a FULL Xcode by BUNDLE, not by a fixed name — it may be "Xcode.app" (stable) or "Xcode-beta.app"
# (beta channel), and the operator could stage either depending on their OS version/channel. Accept any
# /Applications/Xcode*.app that carries a real xcodebuild; prefer stable, then beta, then any other.
XCODE_APP=""
for cand in /Applications/Xcode.app /Applications/Xcode-beta.app /Applications/Xcode*.app; do
  if [ -x "$cand/Contents/Developer/usr/bin/xcodebuild" ]; then XCODE_APP="$cand"; break; fi
done

if [ -z "$XCODE_APP" ]; then
  echo "  ✗ no full Xcode in /Applications — refusing to publish a base without Apple SDKs." >&2
  echo "    Install Xcode on the host (or select a full Xcode.app under Included apps) and rebuild." >&2
  exit 1
else
  XCODE_DEVDIR="$XCODE_APP/Contents/Developer"
  echo "  ▸ Using $XCODE_APP"
  # Point the toolchain at full Xcode (overrides the CLT selected in Phase 3) so xcodebuild/xcrun and
  # every agent shell resolve into it — and so first-launch/Metal act on THIS Xcode, not the CLT.
  if sudo /usr/bin/xcode-select -s "$XCODE_DEVDIR"; then
    echo "    ✓ xcode-select → $XCODE_DEVDIR"
  else
    echo "    ✗ could not select $XCODE_DEVDIR — agent shells would use the wrong SDK." >&2
    exit 1
  fi
  # Make the copied Xcode immediately usable to LaunchServices too (`open -a Xcode`, `xed`, and the
  # computer-use path) instead of waiting for the periodic /Applications rescan — same treatment
  # Chrome for Testing gets in Phase 7⅝. Quarantine was already stripped when it was copied in 7½.
  /System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister \
    -f "$XCODE_APP" >/dev/null 2>&1 || true

  # ── License agreement ───────────────────────────────────────────────────────────────────────────
  # A just-copied Xcode.app is NOT usable: until the agreement is recorded (by root, system-wide in
  # /Library/Preferences/com.apple.dt.Xcode), EVERY xcodebuild/xcrun call — and the GUI — stops at
  # "You have not agreed to the Xcode license agreements", and an agent exec has no TTY to answer
  # that prompt in. So accept it HERE, once per base, and PROVE it took.
  #
  # The proof is functional, not an exit code: `xcodebuild -version` run as the agent (NOT root) is
  # exactly what a build hits, and it fails while the license is outstanding. Probe with it after
  # every attempt — `-license accept` can exit 0 having recorded nothing.
  xcode_license_ok() { "$XCODE_DEVDIR/usr/bin/xcodebuild" -version >/dev/null 2>&1; }

  if xcode_license_ok; then
    echo "    ✓ Xcode license already accepted."
  else
    # 1. The documented non-interactive verb.
    run_timeout 300 sudo "$XCODE_DEVDIR/usr/bin/xcodebuild" -license accept >/dev/null 2>&1 || true
    if xcode_license_ok; then
      echo "    ✓ Xcode license accepted (-license accept)."
    else
      # 2. Fall back to driving the INTERACTIVE `sudo xcodebuild -license` exactly as a human does:
      #    page out of the agreement pager ("q"), then type the "agree" confirmation (and a "y" for
      #    any further yes/no confirmation). Stdin is PRE-FED, so it can never block on a keystroke
      #    nobody is there to type, and `run_timeout` bounds a pager that ignores the feed. `</dev/null`
      #    is deliberately NOT used: the point is to answer the prompts, not to EOF them.
      printf 'q\nagree\nagree\ny\n' \
        | run_timeout 300 sudo "$XCODE_DEVDIR/usr/bin/xcodebuild" -license >/dev/null 2>&1 || true
      if xcode_license_ok; then
        echo "    ✓ Xcode license accepted (interactive 'xcodebuild -license')."
      else
        # Every clone of this base will refuse to build until someone accepts it by hand — that is
        # worth shouting about here rather than surfacing inside an agent's build an hour later.
        echo "    ✗ Xcode license is STILL not accepted — builds in clones of this base will fail." >&2
        echo "      Run 'sudo xcodebuild -license accept' in the guest and rebuild the base." >&2
        exit 1
      fi
    fi
  fi

  # ── Further one-time confirmations, so a clone is born build-ready ──────────────────────────────
  # First-launch component install — the exact step the runtime message calls out. Installs Xcode's
  # bundled packages (device support, dsym services, etc.). Bounded so it can't hang the base build.
  # `-checkFirstLaunchStatus` exits 0 when nothing is pending, which keeps re-runs cheap.
  if "$XCODE_DEVDIR/usr/bin/xcodebuild" -checkFirstLaunchStatus >/dev/null 2>&1; then
    echo "    ✓ first-launch components already installed."
  else
    echo "    installing first-launch components (xcodebuild -runFirstLaunch) …"
    run_timeout 1200 sudo "$XCODE_DEVDIR/usr/bin/xcodebuild" -runFirstLaunch \
      && echo "    ✓ first-launch components installed" \
      || {
        echo "    ✗ -runFirstLaunch failed/timed out — refusing to publish an incomplete Xcode." >&2
        exit 1
      }
    if ! "$XCODE_DEVDIR/usr/bin/xcodebuild" -checkFirstLaunchStatus >/dev/null 2>&1; then
      echo "    ✗ Xcode still reports pending first-launch components after installation." >&2
      exit 1
    fi
  fi
  # Developer-tools access: without these, the FIRST debug/test run pops the "Developer Tools Access
  # needs to take control of another process" authentication panel — another GUI prompt an exec-only
  # agent cannot answer. `DevToolsSecurity -enable` answers it system-wide; membership in
  # `_developer` is what that panel is actually gating on for the agent account.
  sudo /usr/sbin/DevToolsSecurity -enable >/dev/null 2>&1 \
    && echo "    ✓ developer-tools access enabled (no taskgated auth prompt)" \
    || echo "    ⚠ DevToolsSecurity -enable reported an error (continuing)." >&2
  sudo /usr/sbin/dseditgroup -o edit -a "$AGENT_USER" -t user _developer >/dev/null 2>&1 || true
  # Pre-download the Metal toolchain. In Xcode 16+ it's a SEPARATE downloadable component
  # (`-downloadComponent MetalToolchain`); older Xcodes bundle it and lack that verb. Probe first so a
  # base that already has `metal` skips the (network) download and stays idempotent; then try the
  # modern verb, falling back to the legacy `-downloadPlatform macOS`. Needs the guest's NAT network,
  # which is up by this point (the common-packages phase above relies on it too).
  if "$XCODE_DEVDIR/usr/bin/xcrun" --find metal >/dev/null 2>&1; then
    echo "    ✓ Metal toolchain already present (xcrun found 'metal') — skipping download."
  else
    echo "    downloading the Metal toolchain …"
    if run_timeout 1800 sudo "$XCODE_DEVDIR/usr/bin/xcodebuild" -downloadComponent MetalToolchain; then
      echo "    ✓ Metal toolchain downloaded (-downloadComponent)."
    elif run_timeout 1800 sudo "$XCODE_DEVDIR/usr/bin/xcodebuild" -downloadPlatform macOS; then
      echo "    ✓ Metal toolchain downloaded (-downloadPlatform macOS fallback)."
    else
      echo "    ✗ Metal toolchain download failed/timed out — refusing to defer this to every clone." >&2
      exit 1
    fi
  fi
  # VERIFY, don't assume. `-downloadComponent` can exit 0 while `metal` still isn't usable, and the
  # failure would otherwise surface an hour later inside an agent's build. Resolve the tool the way a
  # build system does (`xcrun`) and actually compile a trivial kernel — that's the only proof the
  # toolchain is really installed and matched to this Xcode.
  METAL_BIN="$("$XCODE_DEVDIR/usr/bin/xcrun" --find metal 2>/dev/null || true)"
  if [ -z "$METAL_BIN" ]; then
    echo "    ✗ 'metal' is STILL unresolvable via xcrun — this base cannot compile shaders." >&2
    exit 1
  else
    METAL_PROBE="$(mktemp -d)"
    printf '#include <metal_stdlib>\nkernel void nucleic_probe() {}\n' > "$METAL_PROBE/probe.metal"
    if run_timeout 300 "$XCODE_DEVDIR/usr/bin/xcrun" -sdk macosx metal \
         -c "$METAL_PROBE/probe.metal" -o "$METAL_PROBE/probe.air"; then
      echo "    ✓ metal verified — $METAL_BIN compiled a probe kernel."
    else
      echo "    ✗ 'metal' resolves ($METAL_BIN) but failed to compile a trivial kernel." >&2
      rm -rf "$METAL_PROBE"
      exit 1
    fi
    rm -rf "$METAL_PROBE"
  fi

  # A working xcodebuild and a Metal compiler still do not prove the Swift frontend can see the
  # macOS SDK. Typecheck a minimal SwiftUI view through this exact selected Xcode; this catches a
  # missing/corrupt SDK, an incompatible copied Xcode, and a broken developer-dir selection before
  # the base is sealed.
  SWIFTUI_PROBE="$(mktemp -d)"
  cat > "$SWIFTUI_PROBE/Probe.swift" <<'SWIFT'
import SwiftUI
struct NucleicSwiftUIProbe: View {
    var body: some View { Text("Nucleic") }
}
SWIFT
  if run_timeout 300 "$XCODE_DEVDIR/usr/bin/xcrun" -sdk macosx swiftc \
       -typecheck "$SWIFTUI_PROBE/Probe.swift"; then
    echo "    ✓ SwiftUI verified — the selected Xcode typechecked a frontend probe."
  else
    echo "    ✗ the selected Xcode could not typecheck a SwiftUI frontend probe." >&2
    rm -rf "$SWIFTUI_PROBE"
    exit 1
  fi
  rm -rf "$SWIFTUI_PROBE"
fi

echo ""
echo "────────────────────────────────────────────────────────────────────────────────────────────────"
echo "✓ Provisioning complete. This guest is now Nucleic's golden base."
echo "  Shutting DOWN cleanly so the host can clone the base bundle (a running/dirty guest can't be)."
echo "  After it powers off, verify from the host with the default macvm-spike round-trip."
echo "────────────────────────────────────────────────────────────────────────────────────────────────"
# When Nucleic drives this over SSH it wants to read back the provisioning result (SIP/TCC/agent
# state) BEFORE the guest powers off, so it sets NUCLEIC_PROVISION_NO_SHUTDOWN=1 and owns the
# shutdown itself. A hands-on run leaves it unset and shuts down here as before.
if [ "${NUCLEIC_PROVISION_NO_SHUTDOWN:-0}" = "1" ]; then
  echo "▸ [8/8] Skipping shutdown (NUCLEIC_PROVISION_NO_SHUTDOWN=1 — the caller will power off)."
  exit 0
fi
echo "▸ [8/8] Shutting down …"
# `shutdown -h now` requires root; run last so nothing after it matters.
sudo shutdown -h now
