#!/bin/sh
# Silicon Network engine installer for Apple Silicon Macs.
#
#   curl -fsSL https://www.siliconnetwork.io/install.sh | sh
#   curl -fsSL https://www.siliconnetwork.io/install.sh | sh -s -- --dry-run
#   curl -fsSL https://www.siliconnetwork.io/install.sh | sh -s -- --uninstall
#
# What it does, in order (docs/11-INTERNAL-CONTRACTS.md section 64.1):
#   1. checks this is macOS 14 or newer on Apple Silicon (not under Rosetta);
#   2. makes sure uv is there (installs the pinned official uv release when it
#      is missing; uv provides the Python the next step runs on);
#   3. downloads the newest release manifest from the website and verifies its
#      Ed25519 signature against the release key pinned below, the same key and
#      scheme the engine's updater trusts (section 19). Anything wrong stops the
#      install: a bad or missing signature, an unknown key, an expired or
#      future dated manifest, another channel, a malformed field;
#   4. checks what that release needs to build (Rust, the Xcode command line
#      tools and the Metal toolchain for a source release);
#   5. downloads the release files the manifest names, checks each against its
#      SHA-256 and size in the signed manifest, and installs the engine into
#      ~/.silicon/versions/<version> with `current` pointing at it, the layout
#      the updater uses;
#   6. sets up two launchd agents: com.silicon.engine (the engine, kept
#      running) and com.silicon.engine-updater (signed updates every 6 hours,
#      only after you set SILICON_AUTO_UPDATE=1 in ~/.silicon/engine.env).
#
# Options:
#   --dry-run     check and verify everything, download into a temporary
#                 directory, print what would change, change nothing
#   --uninstall   stop and remove the launchd agents this installer wrote,
#                 ~/.silicon/bin and ~/.silicon/versions; keeps engine.env,
#                 models, logs and the node identity (all under ~/.silicon)
#   --purge       with --uninstall: remove ~/.silicon entirely as well
#   --channel C   follow another release channel (default stable)
#   --force       replace a com.silicon.engine launchd agent that this
#                 installer did not write (a copy is kept in ~/.silicon)
#   -h, --help    this text
#
# Environment: SILICON_INSTALL_BASE_URL (default https://www.siliconnetwork.io).
# Read it before you run it: this file is the whole installer.
set -eu

# The release key the engine updater pins (engine/src/silicon/updater/keys.py,
# PINNED_RELEASE_KEYS). A test in scripts/release/test_install_sh.py fails if
# the two ever differ. One "key_id=base64 of the raw 32 byte key" per line.
PINNED_RELEASE_KEYS='release-2026-10=HKn1m5b5rXNF95yzQ4Qs9QuJCHjqvPIlstK0YRHGDp4='

UV_VERSION="0.11.29"
MIN_MACOS_MAJOR=14
MARKER="Written by the Silicon Network installer (install.sh)."
ENGINE_LABEL="com.silicon.engine"
UPDATER_LABEL="com.silicon.engine-updater"

BASE_URL="${SILICON_INSTALL_BASE_URL:-https://www.siliconnetwork.io}"
BASE_URL="${BASE_URL%/}"
CHANNEL="stable"
DRY_RUN=0
UNINSTALL=0
PURGE=0
FORCE=0
LAUNCHCTL="${SILICON_INSTALL_LAUNCHCTL:-launchctl}"

SILICON_DIR="$HOME/.silicon"
VERSIONS_DIR="$SILICON_DIR/versions"
BIN_DIR="$SILICON_DIR/bin"
AGENTS_DIR="$HOME/Library/LaunchAgents"
ENGINE_PLIST="$AGENTS_DIR/$ENGINE_LABEL.plist"
UPDATER_PLIST="$AGENTS_DIR/$UPDATER_LABEL.plist"

WORK=""
PY=""
UV=""

say() { printf '%s\n' "$*"; }
step() { printf '\n==> %s\n' "$*"; }
warn() { printf 'warning: %s\n' "$*" >&2; }
die() {
  printf 'error: %s\n' "$*" >&2
  exit 1
}
act() {
  # Print an action; run it unless this is a dry run.
  if [ "$DRY_RUN" = 1 ]; then
    printf '  would: %s\n' "$*"
    return 0
  fi
  "$@"
}

usage() {
  cat <<'USAGE'
Silicon Network engine installer for Apple Silicon Macs.

  curl -fsSL https://www.siliconnetwork.io/install.sh | sh -s -- [options]

  --dry-run     check and verify everything, download into a temporary
                directory, print what would change, change nothing
  --uninstall   remove the launchd agents this installer wrote,
                ~/.silicon/bin and ~/.silicon/versions (keeps engine.env,
                models, logs and the node identity)
  --purge       with --uninstall: remove ~/.silicon entirely as well
  --channel C   follow another release channel (default stable)
  --force       replace a com.silicon.engine launchd agent this installer
                did not write (a copy is kept in ~/.silicon)
  -h, --help    this text
USAGE
}

cleanup() {
  if [ -n "$WORK" ] && [ -d "$WORK" ]; then rm -rf "$WORK"; fi
}
trap cleanup EXIT INT TERM

# ---------------------------------------------------------------- arguments

while [ $# -gt 0 ]; do
  case "$1" in
    --dry-run) DRY_RUN=1 ;;
    --uninstall) UNINSTALL=1 ;;
    --purge) PURGE=1 ;;
    --force) FORCE=1 ;;
    --channel)
      [ $# -ge 2 ] || die "--channel needs a name"
      CHANNEL="$2"
      shift
      ;;
    --channel=*) CHANNEL="${1#--channel=}" ;;
    -h | --help)
      usage
      exit 0
      ;;
    *) die "unknown option $1 (see --help)" ;;
  esac
  shift
done

case "$CHANNEL" in
  "" | *[!A-Za-z0-9._-]*) die "a channel name is letters, digits, dot, dash or underscore" ;;
esac
if [ "$PURGE" = 1 ] && [ "$UNINSTALL" != 1 ]; then
  die "--purge goes with --uninstall"
fi

# Only https, except a loopback address (the installer's own tests).
LOOPBACK=0
case "$BASE_URL" in
  https://*) ;;
  http://127.0.0.1:* | http://127.0.0.1 | http://localhost:* | http://localhost) LOOPBACK=1 ;;
  *) die "SILICON_INSTALL_BASE_URL must be https ($BASE_URL)" ;;
esac
if [ "$LOOPBACK" = 1 ]; then
  CURL_PROTO="=http,https"
else
  CURL_PROTO="=https"
fi

fetch() {
  # fetch URL FILE [MAX_BYTES]
  if [ $# -ge 3 ]; then
    curl -fsSL --proto "$CURL_PROTO" --proto-redir "$CURL_PROTO" --max-filesize "$3" -o "$2" "$1"
  else
    curl -fsSL --proto "$CURL_PROTO" --proto-redir "$CURL_PROTO" -o "$2" "$1"
  fi
}

uid() { id -u; }

# ---------------------------------------------------------------- uninstall

has_marker() { [ -f "$1" ] && grep -q "$MARKER" "$1"; }

do_uninstall() {
  step "Uninstalling the Silicon Network engine"
  for label in "$ENGINE_LABEL" "$UPDATER_LABEL"; do
    plist="$AGENTS_DIR/$label.plist"
    if [ ! -f "$plist" ]; then
      continue
    fi
    if has_marker "$plist" || [ "$FORCE" = 1 ]; then
      act "$LAUNCHCTL" bootout "gui/$(uid)/$label" 2>/dev/null || true
      act rm -f "$plist"
    else
      warn "$plist was not written by this installer; left as it is (--force removes it)"
    fi
  done
  act rm -f "$BIN_DIR/run_engine.sh" "$BIN_DIR/run_updater.sh"
  if [ -d "$BIN_DIR" ]; then act rmdir "$BIN_DIR" 2>/dev/null || true; fi
  act rm -rf "$VERSIONS_DIR"
  if [ "$PURGE" = 1 ]; then
    act rm -rf "$SILICON_DIR"
    say "Removed $SILICON_DIR: engine.env, logs, models and the node identity are gone."
  else
    say "Kept $SILICON_DIR (engine.env, logs, downloaded models and this node's identity)."
    say "Remove them too with: curl -fsSL $BASE_URL/install.sh | sh -s -- --uninstall --purge"
  fi
  say "The engine is uninstalled."
}

if [ "$UNINSTALL" = 1 ]; then
  do_uninstall
  exit 0
fi

# ---------------------------------------------------------------- platform

step "Checking this Mac"
OS="$(uname -s)"
[ "$OS" = "Darwin" ] || die "this installer is for macOS on Apple Silicon (found $OS). For a Linux machine, use the node agent: $BASE_URL/download#linux"
ARCH="$(uname -m)"
TRANSLATED="$(sysctl -n sysctl.proc_translated 2>/dev/null || echo 0)"
if [ "$TRANSLATED" = 1 ]; then
  die "this shell runs under Rosetta; open a native Terminal (not 'Open using Rosetta') and run the installer again"
fi
[ "$ARCH" = "arm64" ] || die "the engine needs Apple Silicon (M1 or newer); this Mac is $ARCH"
MACOS="$(sw_vers -productVersion 2>/dev/null || echo 0)"
MACOS_MAJOR="${MACOS%%.*}"
case "$MACOS_MAJOR" in
  "" | *[!0-9]*) MACOS_MAJOR=0 ;;
esac
[ "$MACOS_MAJOR" -ge "$MIN_MACOS_MAJOR" ] || die "macOS $MIN_MACOS_MAJOR or newer is needed; this Mac runs $MACOS"
say "macOS $MACOS on $ARCH: ok"

# A com.silicon.engine agent set up by hand (from a repo checkout) is not
# replaced without --force: it may be serving right now.
for plist in "$ENGINE_PLIST" "$UPDATER_PLIST"; do
  if [ -f "$plist" ] && ! has_marker "$plist" && [ "$FORCE" != 1 ]; then
    die "$plist exists and was not written by this installer (an engine set up by hand?). Nothing was changed. Run again with --force to replace it."
  fi
done

WORK="$(mktemp -d "${TMPDIR:-/tmp}/silicon-install.XXXXXX")"

# ---------------------------------------------------------------- uv and Python

step "Checking uv"
find_uv() {
  if command -v uv >/dev/null 2>&1; then
    command -v uv
    return 0
  fi
  for candidate in "$HOME/.local/bin/uv" "$HOME/.cargo/bin/uv"; do
    if [ -x "$candidate" ]; then
      printf '%s\n' "$candidate"
      return 0
    fi
  done
  return 1
}
if UV="$(find_uv)"; then
  say "uv: $UV"
elif [ "$DRY_RUN" = 1 ]; then
  say "  would: install uv $UV_VERSION from https://astral.sh/uv/$UV_VERSION/install.sh"
  UV=""
else
  say "uv is missing: installing uv $UV_VERSION with its official installer (into ~/.local/bin)"
  fetch "https://astral.sh/uv/$UV_VERSION/install.sh" "$WORK/uv-install.sh"
  UV_NO_MODIFY_PATH=1 sh "$WORK/uv-install.sh" </dev/null
  UV="$(find_uv)" || die "uv did not install; see https://docs.astral.sh/uv/ and run this installer again"
fi

if [ -n "$UV" ]; then
  if ! PY="$("$UV" python find 3.13 2>/dev/null)"; then
    if [ "$DRY_RUN" = 1 ]; then
      say "  would: uv python install 3.13"
    else
      "$UV" python install 3.13 >/dev/null || die "uv could not install Python 3.13"
    fi
    PY="$("$UV" python find 3.13 2>/dev/null)" || PY=""
  fi
fi
if [ -z "$PY" ] && [ "$DRY_RUN" = 1 ] && command -v python3 >/dev/null 2>&1 && xcode-select -p >/dev/null 2>&1; then
  PY="$(command -v python3)"
fi
[ -n "$PY" ] || die "no Python to verify the release with (uv python find 3.13 found none)"

# The verifier and installer steps that need more than sh: standard library
# only, run isolated (-I) so nothing on this Mac's Python path is imported.
cat >"$WORK/silicon_install.py" <<'PYTHON'
import base64
import hashlib
import json
import os
import re
import secrets
import shutil
import sys
import tarfile
from datetime import datetime, timezone

# ---- Ed25519 verification, RFC 8032 section 5.1.7 (pure Python) ----

P = 2**255 - 19
L = 2**252 + 27742317777372353535851937790883648493
D = (-121665 * pow(121666, P - 2, P)) % P
SQRT_M1 = pow(2, (P - 1) // 4, P)


def _add(a, b):
    x1, y1, z1, t1 = a
    x2, y2, z2, t2 = b
    aa = (y1 - x1) * (y2 - x2) % P
    bb = (y1 + x1) * (y2 + x2) % P
    cc = 2 * t1 * t2 * D % P
    dd = 2 * z1 * z2 % P
    e, f, g, h = bb - aa, dd - cc, dd + cc, bb + aa
    return (e * f % P, g * h % P, f * g % P, e * h % P)


def _mul(s, point):
    q = (0, 1, 1, 0)
    while s > 0:
        if s & 1:
            q = _add(q, point)
        point = _add(point, point)
        s >>= 1
    return q


def _equal(a, b):
    x1, y1, z1, _ = a
    x2, y2, z2, _ = b
    return (x1 * z2 - x2 * z1) % P == 0 and (y1 * z2 - y2 * z1) % P == 0


def _recover_x(y, sign):
    if y >= P:
        return None
    x2 = (y * y - 1) * pow(D * y * y + 1, P - 2, P)
    if x2 == 0:
        return None if sign else 0
    x = pow(x2, (P + 3) // 8, P)
    if (x * x - x2) % P != 0:
        x = x * SQRT_M1 % P
    if (x * x - x2) % P != 0:
        return None
    if (x & 1) != sign:
        x = P - x
    return x


def _decompress(raw):
    if len(raw) != 32:
        return None
    y = int.from_bytes(raw, "little")
    sign = y >> 255
    y &= (1 << 255) - 1
    x = _recover_x(y, sign)
    if x is None:
        return None
    return (x, y, 1, x * y % P)


_GY = 4 * pow(5, P - 2, P) % P
_GX = _recover_x(_GY, 0)
G = (_GX, _GY, 1, _GX * _GY % P)


def ed25519_verify(public, message, signature):
    if len(public) != 32 or len(signature) != 64:
        return False
    a = _decompress(public)
    if a is None:
        return False
    r = _decompress(signature[:32])
    if r is None:
        return False
    s = int.from_bytes(signature[32:], "little")
    if s >= L:
        return False
    h = int.from_bytes(hashlib.sha512(signature[:32] + public + message).digest(), "little") % L
    return _equal(_mul(8 * s, G), _add(_mul(8, r), _mul(8 * h, a)))


# ---- the manifest (docs/11 section 19; silicon/updater/manifest.py) ----

ENVELOPE_FORMAT = "silicon-engine-release-envelope/1"
MANIFEST_SCHEMA = "silicon-engine-release/1"
SIGNING_CONTEXT = b"silicon-engine-release/1\n"
MAX_MANIFEST_BYTES = 256 * 1024
SHA256_RE = re.compile(r"^[0-9a-f]{64}$")
NAME_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._+-]{0,200}$")
VERSION_RE = re.compile(r"^[0-9]+(\.[0-9]+)*([a-z0-9.+-]*)$")
EXTRA_RE = re.compile(r"^[A-Za-z0-9._-]{1,64}$")


def fail(message):
    print(f"error: {message}", file=sys.stderr)
    sys.exit(1)


def parse_time(value, what):
    if not isinstance(value, str):
        fail(f"{what} is not a timestamp")
    try:
        parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
    except ValueError:
        fail(f"{what} is not an RFC 3339 timestamp")
    if parsed.utcoffset() is None or parsed.utcoffset().total_seconds() != 0:
        fail(f"{what} must be UTC")
    return parsed


def pinned_keys(text):
    keys = {}
    for line in text.split():
        key_id, sep, b64 = line.partition("=")
        if not sep or not key_id:
            fail("a pinned release key is not key_id=base64")
        try:
            raw = base64.b64decode(b64, validate=True)
        except ValueError:
            fail(f"pinned release key {key_id} is not base64")
        if len(raw) != 32:
            fail(f"pinned release key {key_id} is not 32 bytes")
        keys[key_id] = raw
    if not keys:
        fail("no release key is pinned; refusing every manifest")
    return keys


def verify(envelope_path, channel, keys_text, out_dir):
    keys = pinned_keys(keys_text)
    raw = open(envelope_path, "rb").read()
    if len(raw) > MAX_MANIFEST_BYTES:
        fail("the manifest is too large")
    try:
        envelope = json.loads(raw)
    except ValueError:
        fail("the manifest is not JSON")
    if not isinstance(envelope, dict) or envelope.get("format") != ENVELOPE_FORMAT:
        fail("not a release envelope")
    payload = envelope.get("payload")
    signatures = envelope.get("signatures")
    if not isinstance(payload, str) or not isinstance(signatures, list) or not signatures:
        fail("not a release envelope (payload or signatures)")
    message = SIGNING_CONTEXT + payload.encode("utf-8")
    verified_by = None
    for entry in signatures:
        if not isinstance(entry, dict) or entry.get("alg") != "ed25519":
            continue
        key = keys.get(entry.get("key_id"))
        sig = entry.get("sig")
        if key is None or not isinstance(sig, str):
            continue
        try:
            sig_raw = base64.b64decode(sig, validate=True)
        except ValueError:
            continue
        if ed25519_verify(key, message, sig_raw):
            verified_by = entry["key_id"]
            break
    if verified_by is None:
        fail("no valid signature from a pinned release key; nothing was installed")

    # Nothing in the payload is read before a signature holds.
    try:
        manifest = json.loads(payload)
    except ValueError:
        fail("the signed payload is not JSON")
    if not isinstance(manifest, dict) or manifest.get("schema") != MANIFEST_SCHEMA:
        fail("the manifest schema is not " + MANIFEST_SCHEMA)
    if manifest.get("channel") != channel:
        fail(f"the manifest is for channel {manifest.get('channel')!r}, not {channel!r}")
    version = manifest.get("version")
    if not isinstance(version, str) or not VERSION_RE.match(version):
        fail("the manifest version is not a release version")
    now = datetime.now(timezone.utc)
    published = parse_time(manifest.get("published_at"), "published_at")
    expires = parse_time(manifest.get("expires_at"), "expires_at")
    if expires <= now:
        fail(f"the release manifest expired at {manifest['expires_at']}; it must be re-signed by the release pipeline. Try again later")
    if (published - now).total_seconds() > 300:
        fail("the manifest is dated in the future")
    if expires <= published:
        fail("the manifest expires before it is published")
    install = manifest.get("install")
    if not isinstance(install, dict) or install.get("method") not in ("wheel", "source"):
        fail("the manifest install method is not wheel or source")
    python = install.get("python", "3.13")
    extras = install.get("extras", ["mlx"])
    if not isinstance(python, str) or not re.match(r"^3\.[0-9]+$", python):
        fail("the manifest python is not a version")
    if not isinstance(extras, list) or not all(isinstance(e, str) and EXTRA_RE.match(e) for e in extras):
        fail("the manifest extras are not names")
    artifacts = manifest.get("artifacts")
    if not isinstance(artifacts, list) or not artifacts:
        fail("the manifest lists no artifacts")
    release_files = manifest.get("release_files") or []
    if not isinstance(release_files, list):
        fail("release_files is not a list")
    listed = {}
    for f in release_files:
        if not isinstance(f, dict) or not isinstance(f.get("name"), str) or f["name"] in listed:
            fail("release_files names must be unique file names")
        listed[f["name"]] = f
    names = set()
    rows = []
    for a in artifacts:
        if not isinstance(a, dict):
            fail("an artifact is not an object")
        role, name, url, sha, size = (a.get(k) for k in ("role", "name", "url", "sha256", "size"))
        if role not in ("wheel", "requirements", "source"):
            fail("an artifact role is unknown")
        if not isinstance(name, str) or not NAME_RE.match(name) or name in names:
            fail("artifact names must be unique plain file names")
        names.add(name)
        if not isinstance(url, str) or not url.startswith("https://") or any(c.isspace() for c in url):
            fail(f"{name}: the artifact url must be https")
        if not isinstance(sha, str) or not SHA256_RE.match(sha):
            fail(f"{name}: sha256 must be 64 lowercase hex characters")
        if isinstance(size, bool) or not isinstance(size, int) or size <= 0:
            fail(f"{name}: size must be a positive integer")
        entry = listed.get(name)
        if entry is not None and (entry.get("sha256") != sha or entry.get("size") != size):
            fail(f"{name}: artifacts and release_files disagree on its sha256 or size")
        rows.append((role, name, url, sha, size))
    roles = [r[0] for r in rows]
    if install["method"] == "wheel":
        if roles.count("requirements") != 1 or "wheel" not in roles or "source" in roles:
            fail("a wheel install needs one requirements file, wheels and no source")
    elif roles != ["source"]:
        fail("a source install carries exactly one source artifact")

    os.makedirs(out_dir, exist_ok=True)
    with open(os.path.join(out_dir, "payload.json"), "w", encoding="utf-8") as fh:
        fh.write(payload)
    with open(os.path.join(out_dir, "artifacts.tsv"), "w", encoding="utf-8") as fh:
        for row in rows:
            fh.write("\t".join(str(v) for v in row) + "\n")
    with open(os.path.join(out_dir, "plan.env"), "w", encoding="utf-8") as fh:
        fh.write(f"REL_VERSION='{version}'\n")
        fh.write(f"REL_METHOD='{install['method']}'\n")
        fh.write(f"REL_PYTHON='{python}'\n")
        fh.write(f"REL_EXTRAS='{' '.join(extras)}'\n")
        fh.write(f"REL_KEY_ID='{verified_by}'\n")
        fh.write(f"REL_PUBLISHED_AT='{manifest['published_at']}'\n")
        fh.write(f"REL_EXPIRES_AT='{manifest['expires_at']}'\n")


def check_file(path, sha, size):
    actual = os.path.getsize(path)
    if actual != int(size):
        fail(f"{os.path.basename(path)}: {actual} bytes, the signed manifest says {size}")
    digest = hashlib.sha256()
    with open(path, "rb") as fh:
        for chunk in iter(lambda: fh.read(1 << 20), b""):
            digest.update(chunk)
    if digest.hexdigest() != sha:
        fail(f"{os.path.basename(path)}: sha256 does not match the signed manifest")


def extract(archive, dest):
    """The engine project from a source archive: 'data' refuses absolute
    paths, links out of the tree and devices (as the updater does)."""
    os.makedirs(dest, exist_ok=True)
    with tarfile.open(archive) as tar:
        tar.extractall(dest, filter="data")
    candidates = [dest, os.path.join(dest, "engine")]
    children = [os.path.join(dest, c) for c in os.listdir(dest) if os.path.isdir(os.path.join(dest, c))]
    if len(children) == 1:
        candidates += [children[0], os.path.join(children[0], "engine")]
    for candidate in candidates:
        pyproject = os.path.join(candidate, "pyproject.toml")
        if os.path.isfile(pyproject) and 'name = "silicon"' in open(pyproject, encoding="utf-8").read():
            print(candidate)
            return
    fail("the source archive holds no engine pyproject.toml")


def link(versions_dir, version):
    """versions/current -> <version>, replaced by rename (never half written)."""
    tmp = os.path.join(versions_dir, f".current.{secrets.token_hex(4)}")
    os.symlink(version, tmp)
    os.replace(tmp, os.path.join(versions_dir, "current"))


def state(versions_dir, version, published_at):
    """state.json in the updater's format (silicon/updater/updater.py State)."""
    path = os.path.join(versions_dir, "state.json")
    data = {}
    if os.path.exists(path):
        try:
            loaded = json.load(open(path, encoding="utf-8"))
            data = loaded if isinstance(loaded, dict) else {}
        except ValueError:
            data = {}
    previous = data.get("current") if data.get("current") != version else data.get("previous")
    out = {
        "install_id": data.get("install_id") if isinstance(data.get("install_id"), str) else secrets.token_hex(16),
        "current": version,
        "previous": previous if isinstance(previous, str) else None,
        "last_published_at": published_at,
        "failed_versions": [v for v in data.get("failed_versions", []) if isinstance(v, str) and v != version],
    }
    tmp = path + f".{secrets.token_hex(4)}"
    with open(tmp, "w", encoding="utf-8") as fh:
        fh.write(json.dumps(out, indent=2) + "\n")
    os.replace(tmp, path)


def release_json(payload_path, dest):
    manifest = json.load(open(payload_path, encoding="utf-8"))
    with open(dest, "w", encoding="utf-8") as fh:
        fh.write(json.dumps(manifest, indent=2) + "\n")


def api_key():
    print(secrets.token_hex(32))


def main(argv):
    command = argv[0] if argv else ""
    if command == "verify":
        verify(*argv[1:5])
    elif command == "check":
        check_file(*argv[1:4])
    elif command == "extract":
        extract(*argv[1:3])
    elif command == "link":
        link(*argv[1:3])
    elif command == "state":
        state(*argv[1:4])
    elif command == "release-json":
        release_json(*argv[1:3])
    elif command == "api-key":
        api_key()
    else:
        fail(f"unknown helper command {command!r}")


if __name__ == "__main__":
    main(sys.argv[1:])
PYTHON

helper() { "$PY" -I "$WORK/silicon_install.py" "$@"; }

# ---------------------------------------------------------------- the manifest

step "Verifying the newest $CHANNEL release"
TRUSTED="$PINNED_RELEASE_KEYS"
if [ -n "${SILICON_INSTALL_PUBLIC_KEY:-}" ]; then
  warn "SILICON_INSTALL_PUBLIC_KEY is set: trusting that key INSTEAD of the Silicon Network release key."
  warn "Only the installer's own tests should do this. Unset it unless you know why it is set."
  TRUSTED="$SILICON_INSTALL_PUBLIC_KEY"
fi
MANIFEST_URL="$BASE_URL/releases/$CHANNEL/manifest.json"
fetch "$MANIFEST_URL" "$WORK/manifest.json" 262144 || die "could not download $MANIFEST_URL (no release published yet, or no connection)"
helper verify "$WORK/manifest.json" "$CHANNEL" "$TRUSTED" "$WORK/plan" || die "the release manifest did not verify; nothing was installed"
# shellcheck disable=SC1091
. "$WORK/plan/plan.env"
say "release $REL_VERSION ($REL_METHOD install), signed by $REL_KEY_ID, published $REL_PUBLISHED_AT: ok"

# ---------------------------------------------------------------- build tools

step "Checking build tools"
MISSING=""
if [ "$REL_METHOD" = "source" ]; then
  if xcode-select -p >/dev/null 2>&1; then
    say "Xcode command line tools: ok"
  else
    MISSING="$MISSING
  - the Xcode command line tools: run  xcode-select --install"
  fi
  if command -v cargo >/dev/null 2>&1 || [ -x "$HOME/.cargo/bin/cargo" ]; then
    say "Rust (cargo): ok"
  else
    MISSING="$MISSING
  - Rust, which builds the engine's network bindings: run
      curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal"
  fi
  # MLX builds its Metal kernels: the Metal toolchain must answer.
  if xcrun -sdk macosx metal -v >/dev/null 2>&1; then
    say "Metal toolchain: ok"
  elif command -v xcodebuild >/dev/null 2>&1 && xcodebuild -version >/dev/null 2>&1; then
    if [ "$DRY_RUN" = 1 ]; then
      say "  would: xcodebuild -downloadComponent MetalToolchain"
    else
      say "Downloading Apple's Metal toolchain (xcodebuild -downloadComponent MetalToolchain)"
      xcodebuild -downloadComponent MetalToolchain || true
      xcrun -sdk macosx metal -v >/dev/null 2>&1 || MISSING="$MISSING
  - the Metal toolchain: run  xcodebuild -downloadComponent MetalToolchain"
    fi
  else
    MISSING="$MISSING
  - Xcode (from the App Store), then  xcodebuild -downloadComponent MetalToolchain"
  fi
fi
if [ -n "$MISSING" ]; then
  if [ "$DRY_RUN" = 1 ]; then
    say "Missing before a real install:$MISSING"
  else
    die "install these first, then run the installer again:$MISSING"
  fi
fi

# ---------------------------------------------------------------- download

step "Downloading release $REL_VERSION"
DL="$WORK/downloads"
mkdir -p "$DL"
TAB="$(printf '\t')"
while IFS="$TAB" read -r role name url sha size; do
  [ -n "$name" ] || continue
  if [ "$LOOPBACK" = 1 ]; then
    # The tests serve the release over plain http on a loopback address; the
    # manifest still names https (its signed rule). Never for another host.
    case "$url" in
      https://127.0.0.1:* | https://localhost:*) url="http://${url#https://}" ;;
    esac
  fi
  fetch "$url" "$DL/$name" </dev/null || die "could not download $name"
  helper check "$DL/$name" "$sha" "$size" </dev/null || die "$name does not match the signed manifest; nothing was installed"
  say "$name ($role, $size bytes): sha256 ok"
done <"$WORK/plan/artifacts.tsv"

# ---------------------------------------------------------------- install

TARGET="$VERSIONS_DIR/$REL_VERSION"
step "Installing into $TARGET"
EXTRA_ARGS=""
for extra in $REL_EXTRAS; do EXTRA_ARGS="$EXTRA_ARGS --extra $extra"; done

if [ "$DRY_RUN" = 1 ]; then
  if [ "$REL_METHOD" = "source" ]; then
    say "  would: unpack $(cut -f2 "$WORK/plan/artifacts.tsv") into $TARGET/src"
    say "  would: UV_PROJECT_ENVIRONMENT=$TARGET/.venv uv sync --frozen --no-dev$EXTRA_ARGS"
  else
    say "  would: uv venv --python $REL_PYTHON $TARGET/.venv, then uv pip install --require-hashes the requirements and the wheels"
  fi
  say "  would: point $VERSIONS_DIR/current at $REL_VERSION and write $VERSIONS_DIR/state.json"
elif [ -f "$TARGET/.complete" ]; then
  say "$REL_VERSION is already installed here; keeping it"
else
  rm -rf "$TARGET"
  mkdir -p "$TARGET/downloads"
  cp "$DL"/* "$TARGET/downloads/"
  VENV="$TARGET/.venv"
  if [ "$REL_METHOD" = "source" ]; then
    SOURCE_NAME="$(cut -f2 "$WORK/plan/artifacts.tsv")"
    PROJECT="$(helper extract "$TARGET/downloads/$SOURCE_NAME" "$TARGET/src")" || die "could not unpack the source archive"
    say "Building the engine (this takes several minutes the first time)"
    # shellcheck disable=SC2086 # EXTRA_ARGS is a list of flags
    UV_PROJECT_ENVIRONMENT="$VENV" UV_PYTHON="$REL_PYTHON" \
      "$UV" sync --frozen --no-dev --project "$PROJECT" $EXTRA_ARGS || {
      rm -rf "$TARGET"
      die "the engine build failed (above); nothing was switched"
    }
  else
    "$UV" venv --python "$REL_PYTHON" "$VENV" || die "uv venv failed"
    REQUIREMENTS=""
    WHEELS=""
    while IFS="$TAB" read -r role name _url _sha _size; do
      case "$role" in
        requirements) REQUIREMENTS="$TARGET/downloads/$name" ;;
        wheel) WHEELS="$WHEELS $TARGET/downloads/$name" ;;
      esac
    done <"$WORK/plan/artifacts.tsv"
    "$UV" pip install --python "$VENV/bin/python" --require-hashes -r "$REQUIREMENTS" || {
      rm -rf "$TARGET"
      die "installing the pinned requirements failed"
    }
    # shellcheck disable=SC2086 # WHEELS is a list of paths without spaces
    "$UV" pip install --python "$VENV/bin/python" --no-deps $WHEELS || {
      rm -rf "$TARGET"
      die "installing the engine wheels failed"
    }
  fi
  if [ ! -x "$VENV/bin/silicon" ]; then
    rm -rf "$TARGET"
    die "the install produced no $VENV/bin/silicon"
  fi
  rm -rf "$TARGET/downloads"
  helper release-json "$WORK/plan/payload.json" "$TARGET/release.json"
  printf '%s\n' "$REL_VERSION" >"$TARGET/.complete"
fi
if [ "$DRY_RUN" != 1 ]; then
  helper link "$VERSIONS_DIR" "$REL_VERSION"
  helper state "$VERSIONS_DIR" "$REL_VERSION" "$REL_PUBLISHED_AT"
  say "$VERSIONS_DIR/current -> $REL_VERSION"
fi

# ---------------------------------------------------------------- launchd

step "Setting up the launchd agents"
write_file() {
  # write_file PATH MODE (content on stdin)
  if [ "$DRY_RUN" = 1 ]; then
    cat >/dev/null
    printf '  would: write %s\n' "$1"
    return 0
  fi
  mkdir -p "$(dirname "$1")"
  tmp="$1.tmp.$$"
  cat >"$tmp"
  chmod "$2" "$tmp"
  mv -f "$tmp" "$1"
}

write_file "$BIN_DIR/run_engine.sh" 755 <<'RUNENGINE'
#!/bin/sh
# Silicon Network engine launcher for launchd (com.silicon.engine).
# Written by the Silicon Network installer (install.sh).
# Runs ~/.silicon/versions/current, the version the installer or the updater
# switched to, with the settings in ~/.silicon/engine.env, and keeps the Mac
# awake while it runs.
set -e
if [ -f "$HOME/.silicon/engine.env" ]; then
  set -a
  . "$HOME/.silicon/engine.env"
  set +a
fi
export PATH="$HOME/.local/bin:$HOME/.cargo/bin:$PATH"
# Keep the log bounded: launchd appends to it, so trim in place at start.
LOG="$HOME/.silicon/engine.log"
if [ -f "$LOG" ] && [ "$(wc -c <"$LOG")" -gt 104857600 ]; then
  tail -c 20971520 "$LOG" >"$LOG.1" && : >"$LOG"
fi
CURRENT="${SILICON_UPDATE_DIR:-$HOME/.silicon/versions}/current"
if [ ! -x "$CURRENT/.venv/bin/silicon" ] || [ ! -f "$CURRENT/.complete" ]; then
  echo "[run_engine] no complete install at $CURRENT; run the installer again" >&2
  sleep 60
  exit 1
fi
exec caffeinate -is "$CURRENT/.venv/bin/silicon"
RUNENGINE

write_file "$BIN_DIR/run_updater.sh" 755 <<'RUNUPDATER'
#!/bin/sh
# Silicon Network engine updater for launchd (com.silicon.engine-updater).
# Written by the Silicon Network installer (install.sh).
# Does nothing unless SILICON_AUTO_UPDATE=1 is set in ~/.silicon/engine.env.
# Then it runs the engine's own updater, which verifies the signed release
# manifest, installs the new version beside the old one, switches, restarts
# com.silicon.engine and rolls back by itself if the new version is unhealthy.
set -e
if [ -f "$HOME/.silicon/engine.env" ]; then
  set -a
  . "$HOME/.silicon/engine.env"
  set +a
fi
if [ "${SILICON_AUTO_UPDATE:-}" != "1" ]; then
  echo "[run_updater] SILICON_AUTO_UPDATE is not 1; nothing to do"
  exit 0
fi
export PATH="$HOME/.local/bin:$HOME/.cargo/bin:$PATH"
exec "${SILICON_UPDATE_DIR:-$HOME/.silicon/versions}/current/.venv/bin/python" -m silicon.updater apply
RUNUPDATER

plist() {
  # plist LABEL SCRIPT LOG EXTRA_XML
  cat <<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">
<!-- $MARKER
     Remove with: curl -fsSL $BASE_URL/install.sh | sh -s -- --uninstall -->
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>$1</string>
    <key>ProgramArguments</key>
    <array>
        <string>/bin/sh</string>
        <string>$2</string>
    </array>
    <key>WorkingDirectory</key>
    <string>$SILICON_DIR</string>
$4
    <key>StandardOutPath</key>
    <string>$3</string>
    <key>StandardErrorPath</key>
    <string>$3</string>
</dict>
</plist>
PLIST
}

for existing in "$ENGINE_PLIST" "$UPDATER_PLIST"; do
  if [ -f "$existing" ] && ! has_marker "$existing"; then
    # Only reached with --force (checked above): keep a copy.
    act mkdir -p "$SILICON_DIR"
    act cp "$existing" "$SILICON_DIR/$(basename "$existing").before-install"
    say "kept a copy of $existing in $SILICON_DIR"
  fi
done

plist "$ENGINE_LABEL" "$BIN_DIR/run_engine.sh" "$SILICON_DIR/engine.log" "    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <true/>
    <key>ThrottleInterval</key>
    <integer>15</integer>" | write_file "$ENGINE_PLIST" 644

plist "$UPDATER_LABEL" "$BIN_DIR/run_updater.sh" "$SILICON_DIR/updater.log" "    <key>StartInterval</key>
    <integer>21600</integer>
    <key>RunAtLoad</key>
    <false/>
    <key>LowPriorityIO</key>
    <true/>" | write_file "$UPDATER_PLIST" 644

ENV_FILE="$SILICON_DIR/engine.env"
if [ -f "$ENV_FILE" ]; then
  say "keeping $ENV_FILE"
elif [ "$DRY_RUN" = 1 ]; then
  say "  would: write $ENV_FILE (mode 600) with a new random SILICON_API_KEY"
else
  KEY="$(helper api-key)"
  (
    umask 077
    mkdir -p "$SILICON_DIR"
    {
      echo "# Silicon Network engine settings, read by ~/.silicon/bin/run_engine.sh."
      echo "# The engine key: the node UI and the API ask for it. Keep it secret."
      echo "SILICON_API_KEY=$KEY"
      echo "# Signed automatic updates (com.silicon.engine-updater): set to 1 to turn on."
      echo "SILICON_AUTO_UPDATE=0"
    } >"$ENV_FILE"
  )
  chmod 600 "$ENV_FILE"
  say "wrote $ENV_FILE with a new engine key"
fi

for label in "$ENGINE_LABEL" "$UPDATER_LABEL"; do
  act "$LAUNCHCTL" bootout "gui/$(uid)/$label" 2>/dev/null || true
  act "$LAUNCHCTL" bootstrap "gui/$(uid)" "$AGENTS_DIR/$label.plist"
done

# ---------------------------------------------------------------- done

if [ "$DRY_RUN" = 1 ]; then
  step "Dry run complete: release $REL_VERSION verified, nothing on this Mac was changed"
  exit 0
fi

step "Silicon Network engine $REL_VERSION is installed and starting"
cat <<NEXT

Next steps:
  1. Open http://localhost:52415 (the node UI). Your engine key is SILICON_API_KEY
     in ~/.silicon/engine.env.
  2. Check this Mac before you join:
       $VERSIONS_DIR/current/.venv/bin/silicon doctor
  3. Join the network on the node UI's Network page with your operator account.

Logs: ~/.silicon/engine.log. Signed automatic updates: set SILICON_AUTO_UPDATE=1
in ~/.silicon/engine.env. Uninstall: curl -fsSL $BASE_URL/install.sh | sh -s -- --uninstall
NEXT
