#!/bin/sh
set -e

export TZ="${TZ:-America/New_York}"
export HIVE_API_PORT="${HIVE_API_PORT:-3002}"
export HIVE_PROXY_PORT="${HIVE_PROXY_PORT:-3001}"
export HIVE_STATIC_DIR="${HIVE_STATIC_DIR:-/opt/hive/proxy/public}"

# Packet mark (fwmark / SO_MARK) used to exempt the MITM proxy's OWN upstream
# :443 dials from the forced-egress redirect, so its traffic is not looped back
# into itself. Exported here so the iptables `-m mark --mark` exemption below
# stays in lockstep with the Go proxy (which reads HIVE_PROXY_EGRESS_MARK,
# defaulting to this same 0x1112). Unlike `-m owner --uid-owner`, a packet mark
# needs no xt_owner kernel module, so the exemption also works on OpenShift/OVN
# (where xt_owner is absent). On OKE the owner-match is the reliable exemption;
# the mark is the OpenShift-compatible fallback. Value must match the Go default.
export HIVE_PROXY_EGRESS_MARK="${HIVE_PROXY_EGRESS_MARK:-0x1112}"

# ── Config backup/restore across container recreation ─────────────────
# When Watchtower recreates the container (pull new image, stop old, start
# new), the Go binary's config.Save() may write an empty/default config to
# the bind-mounted hive.yaml during shutdown or early startup, wiping the
# host file. /data/ is a Docker named volume that persists across container
# recreations, so we keep a rolling backup there.
#
# In Kubernetes, the ConfigMap is the source of truth — admins update it to
# change settings like acmm_level, is_public, etc. The PVC copy is only
# used for disaster recovery if the ConfigMap volume is missing or empty.
#
# In Docker/LXC there is no ConfigMap and no overlay: the PVC copy IS the
# boot-time source of truth, restored over the config path (or read directly
# when that path is read-only) on every boot. See the Docker branch below.
#
# ── Naming: hive.yaml.runtime, formerly hive.yaml.bak ─────────────────
# The old .bak name implied "the restorable backup", which is only half
# true and cost real debugging time: on K8s it is a post-merge SNAPSHOT
# written after the merge, while on Docker/LXC it is a live boot INPUT.
# ".runtime" is honest for both — it is the runtime config as this hive
# last had it.
#
# MIGRATION (one release): ~51 live hives have only the old-named file on
# their PVC. We WRITE the new name and READ the old one when the new is
# absent. We deliberately do NOT rename on the PVC: on Docker/LXC that
# file is the only copy of the live config, and mutating it at boot risks
# losing owner customisations with no warning. The old name is picked up
# read-only until the first save writes the new one, after which both
# exist and the new one wins.
HIVE_CONFIG_PATH="${HIVE_CONFIG:-/etc/hive/hive.yaml}"
HIVE_CONFIG_RUNTIME="/data/hive.yaml.runtime"
HIVE_CONFIG_RUNTIME_LEGACY="/data/hive.yaml.bak"

# hive_runtime_config_read echoes the path to read the persisted runtime
# config from: the new name when it is present and non-empty, else the
# legacy name when that is, else empty. Read-only — it never creates,
# renames or removes anything on the PVC.
hive_runtime_config_read() {
  if [ -f "$HIVE_CONFIG_RUNTIME" ] && [ -s "$HIVE_CONFIG_RUNTIME" ]; then
    echo "$HIVE_CONFIG_RUNTIME"
  elif [ -f "$HIVE_CONFIG_RUNTIME_LEGACY" ] && [ -s "$HIVE_CONFIG_RUNTIME_LEGACY" ]; then
    echo "$HIVE_CONFIG_RUNTIME_LEGACY"
  fi
}

# Detect Kubernetes vs Docker environment
IS_KUBERNETES=false
if [ -n "${KUBERNETES_SERVICE_HOST:-}" ] || [ -f /var/run/secrets/kubernetes.io/serviceaccount/token ]; then
  IS_KUBERNETES=true
fi

if [ "$IS_KUBERNETES" = "true" ]; then
  # ── Kubernetes mode: the PVC runtime config is the boot input ──────
  #
  # PHASE 2 OF THE LAYER COLLAPSE. The ConfigMap is now a SEED — consulted
  # on first boot, when the PVC has no runtime config yet — and not a
  # per-boot input that has to be merged over.
  #
  # Why this is the right way round: the ConfigMap is frozen at provision
  # time and cannot be written by anything at runtime (the hub has only
  # `get` on ConfigMaps, spoke RBAC omits them entirely, and the hub has no
  # kubectl path to vllm-d at all). The runtime config on the PVC is the
  # only layer any component can actually write. Treating the writable
  # layer as the input — instead of merging it over a frozen one on every
  # boot — is what removes the "which layer wins" question that cost about
  # an hour during the 2026-07-31 GHE incident, when three read-only layers
  # were patched before the writable one was found.
  #
  # Measured before the change: on 50 readable spokes all three layers held
  # identical values for every identity field. The merge was recomputing a
  # result equal to its own input on every boot.
  HIVE_CONFIG_BOOT="$(hive_runtime_config_read)"
  if [ -n "$HIVE_CONFIG_BOOT" ]; then
    # Steady state: boot from what this hive last had. The ConfigMap seed is
    # deliberately NOT merged — it is frozen at provision and every field it
    # could contribute is either already here or re-asserted by the hub on
    # the next heartbeat (~30s), including hub.is_public.
    if cp "$HIVE_CONFIG_BOOT" "$HIVE_CONFIG_PATH" 2>/dev/null; then
      echo "[entrypoint] K8s mode — booting from runtime config $HIVE_CONFIG_BOOT (ConfigMap is a seed, not merged)"
    else
      export HIVE_CONFIG="$HIVE_CONFIG_BOOT"
      echo "[entrypoint] K8s mode — config path read-only, using $HIVE_CONFIG_BOOT directly"
    fi
  elif [ -f "$HIVE_CONFIG_PATH" ] && [ -s "$HIVE_CONFIG_PATH" ]; then
    # FIRST BOOT ONLY. The copy-config init container now seeds the config path
    # solely when the PVC has no runtime config, so reaching here means this
    # hive has never written one.
    #
    # The overlay merge below is kept for one case that is real: a hive
    # REPROVISIONED onto an existing PVC has a dashboard overlay but no runtime
    # config, and dropping its overlay would silently discard the owner's agent
    # roster and level. On a genuinely new hive there is no overlay and the
    # merge is a no-op.
    # The copy-config init container re-seeded $HIVE_CONFIG_PATH from the
    # ConfigMap on this boot. Config saved via the dashboard (Config.Save
    # writes /etc/hive/hive.yaml, an emptyDir) would be silently lost —
    # so the Go binary also persists a secret-free overlay to the PVC
    # (/data/hive.yaml.dashboard) on every save, and we merge it over the
    # seed here, BEFORE the backup copy and before the agent-UID
    # enumeration below reads hive.yaml.
    #
    # Precedence: the overlay (last dashboard save) wins for everything
    # EXCEPT the hub/admin-managed keys, which the ConfigMap owns:
    #   - acmm_level     (set by the hub at provision / level change)
    #   - hub.is_public  (hub-managed visibility)
    # A missing, empty, unparsable, or implausible overlay (no
    # project.org / agents) leaves the ConfigMap seed untouched.
    #
    # github.{app_id,installation_id,key_file}: a dashboard-installed (or
    # hub-pushed, post App-install-webhook) GitHub App is PVC-overlay-only
    # state — the ConfigMap seed is never updated after install and keeps
    # whatever placeholder it was provisioned with. The overlay must win
    # here too, and a seed that already shows a real installed App must
    # never be silently downgraded by a missing/placeholder overlay.
    HIVE_DASHBOARD_OVERLAY="/data/hive.yaml.dashboard"
    if [ -f "$HIVE_DASHBOARD_OVERLAY" ] && [ -s "$HIVE_DASHBOARD_OVERLAY" ]; then
      if python3 - "$HIVE_CONFIG_PATH" "$HIVE_DASHBOARD_OVERLAY" <<'PYEOF'
import os, sys, yaml

seed_path, overlay_path = sys.argv[1], sys.argv[2]
with open(seed_path) as f:
    seed = yaml.safe_load(f) or {}
with open(overlay_path) as f:
    overlay = yaml.safe_load(f) or {}

# Sanity: the overlay must look like a full hive config. Save()'s
# validateSaveGuard enforces this on write; re-check before trusting it.
if not isinstance(overlay, dict):
    sys.exit(1)
if not (overlay.get('project') or {}).get('org') or not overlay.get('agents'):
    sys.exit(1)

# GitHub App credentials installed at runtime via the dashboard (or pushed
# by the hub after the App install webhook fires — see
# pkg/dashboard/api.go handleConfigGitHub) are persisted ONLY to this
# overlay, never back to the ConfigMap. The ConfigMap seed for a freshly
# claimed/placeholder hive still carries the placeholder app_id/
# installation_id/key_file, so if the overlay is missing, truncated, or
# otherwise reverted to a placeholder-looking github block, letting the
# seed win here would silently wipe a live App installation on every
# restart (observed: a hive's installed App reverted to placeholder
# creds after a pod restart, 401-looping on every GitHub call). Treat a
# seed that already has a real (non-placeholder) app_id/installation_id
# as a signal this hive has an installed App — refuse to let a
# github-less or placeholder-looking overlay silently downgrade it.
seed_gh = seed.get('github') or {}
overlay_gh = overlay.get('github') or {}


def _looks_placeholder(gh):
    key_file = gh.get('key_file') or ''
    app_id = gh.get('app_id')
    installation_id = gh.get('installation_id')
    if not app_id or not installation_id:
        return True
    if isinstance(key_file, str) and 'PLACEHOLDER' in key_file.upper():
        return True
    return False


seed_has_real_app = isinstance(seed_gh, dict) and not _looks_placeholder(seed_gh)
overlay_has_real_app = isinstance(overlay_gh, dict) and not _looks_placeholder(overlay_gh)
if seed_has_real_app and not overlay_has_real_app:
    sys.stderr.write(
        "[entrypoint] github: overlay missing/placeholder app credentials but "
        "ConfigMap seed has a real app_id/installation_id — keeping the seed's "
        "github block to avoid wiping an installed GitHub App\n"
    )
    overlay['github'] = seed_gh
elif overlay_has_real_app:
    # The overlay's installed-App credentials win over the seed, per the
    # standard overlay-wins precedence — but only trust key_file if the
    # PEM it names actually exists on this PVC. A dangling reference
    # (e.g. an interrupted key write) must not silently authenticate with
    # a missing/placeholder file; fail loud instead of 401-looping quietly.
    key_file = overlay_gh.get('key_file') or ''
    if isinstance(key_file, str) and key_file.startswith('/data/') and not os.path.isfile(key_file):
        sys.stderr.write(
            "[entrypoint] WARNING: overlay github.key_file=%s does not exist on "
            "the PVC — GitHub App auth will fail until it is re-installed\n" % key_file
        )

# The dashboard OVERLAY wins for acmm_level: the ConfigMap seed only ever
# carries the provision-time level and is NOT updated when the level is
# changed via the dashboard/apply-pack (which persists to the overlay). So
# the seed value is stale after any level change, and letting it win here
# silently reverted the level on every restart (issue #1856: a hive raised
# to L5 dropped back to its provisioned L3 on the next pod restart). Only
# fall back to the seed's acmm_level when the overlay has none.
if seed.get('acmm_level') is not None and overlay.get('acmm_level') is not None and seed['acmm_level'] != overlay['acmm_level']:
    sys.stderr.write("[entrypoint] acmm_level: overlay=%s wins over stale ConfigMap seed=%s (issue #1856)\n" % (overlay['acmm_level'], seed['acmm_level']))
if 'acmm_level' not in overlay and 'acmm_level' in seed:
    overlay['acmm_level'] = seed['acmm_level']
# hub.is_public is genuinely hub-managed (updated live via heartbeat), so
# the seed still wins for it when set.
if isinstance(seed.get('hub'), dict) and 'is_public' in seed['hub']:
    overlay.setdefault('hub', {})['is_public'] = seed['hub']['is_public']

# Atomic replace so a failure mid-write can never corrupt the seed.
tmp_path = seed_path + '.merged'
with open(tmp_path, 'w') as f:
    yaml.safe_dump(overlay, f, default_flow_style=False, sort_keys=False)
os.replace(tmp_path, seed_path)
PYEOF
      then
        echo "[entrypoint] K8s mode — dashboard overlay merged over ConfigMap seed (overlay wins for acmm_level and github app creds; ConfigMap wins for hub.is_public)"
      else
        echo "[entrypoint] K8s mode — dashboard overlay invalid, using ConfigMap seed as-is"
      fi
    fi
    # Write the (merged) config as the disaster-recovery snapshot. Always
    # under the new name; the legacy file is left untouched on the PVC.
    cp "$HIVE_CONFIG_PATH" "$HIVE_CONFIG_RUNTIME" 2>/dev/null || true
    echo "[entrypoint] K8s mode — ConfigMap is the seed, runtime config written to $HIVE_CONFIG_RUNTIME"
  else
    # Neither source exists: no runtime config on the PVC (checked first,
    # above) and no ConfigMap seed. There is nothing to boot from.
    echo "[entrypoint] ERROR: no runtime config at $HIVE_CONFIG_RUNTIME (or legacy $HIVE_CONFIG_RUNTIME_LEGACY) and no ConfigMap seed at $HIVE_CONFIG_PATH."
    echo "[entrypoint] Ensure the hive ConfigMap is created before deploying."
    exit 1
  fi
else
  # ── Docker mode: the PVC runtime config is the source of truth ─────
  # Unlike K8s there is no ConfigMap and no overlay here, so this file is a
  # boot INPUT, not a snapshot: it is restored over the config path (or read
  # directly when that path is read-only) on every boot, and it is the only
  # thing that makes a dashboard save survive a container recreation.
  #
  # During the migration a hive may still have only the legacy name. We read
  # whichever exists (new preferred) and always WRITE the new one, so the
  # first boot on new code seeds hive.yaml.runtime from the legacy content
  # without touching the legacy file itself.
  HIVE_CONFIG_SOURCE="$(hive_runtime_config_read)"
  if [ -f "$HIVE_CONFIG_PATH" ] && [ -s "$HIVE_CONFIG_PATH" ] && [ -z "$HIVE_CONFIG_SOURCE" ]; then
    # First boot: config exists but no PVC runtime config yet — seed it
    cp "$HIVE_CONFIG_PATH" "$HIVE_CONFIG_RUNTIME"
    echo "[entrypoint] First boot — config seeded to PVC: $HIVE_CONFIG_RUNTIME"
  elif [ -f "$HIVE_CONFIG_PATH" ] && [ -s "$HIVE_CONFIG_PATH" ] && [ -n "$HIVE_CONFIG_SOURCE" ]; then
    # The PVC runtime config is the source of truth (updated by Save()).
    # Try to copy it over the config path; if read-only (Docker bind mount),
    # override HIVE_CONFIG so the Go binary reads from the PVC directly.
    if cp "$HIVE_CONFIG_SOURCE" "$HIVE_CONFIG_PATH" 2>/dev/null; then
      echo "[entrypoint] PVC runtime config restored to config path (from $HIVE_CONFIG_SOURCE)"
    else
      export HIVE_CONFIG="$HIVE_CONFIG_SOURCE"
      echo "[entrypoint] Config path is read-only — using $HIVE_CONFIG_SOURCE directly"
    fi
    # Migration: if we booted from the legacy name, also seed the new name so
    # the next boot finds it. Copy, never rename — the legacy file stays put
    # as the untouched fallback until Save() takes over writing the new one.
    if [ "$HIVE_CONFIG_SOURCE" = "$HIVE_CONFIG_RUNTIME_LEGACY" ]; then
      if cp "$HIVE_CONFIG_RUNTIME_LEGACY" "$HIVE_CONFIG_RUNTIME" 2>/dev/null; then
        echo "[entrypoint] Migration — seeded $HIVE_CONFIG_RUNTIME from legacy $HIVE_CONFIG_RUNTIME_LEGACY (legacy left in place)"
      fi
    fi
  elif [ -f "$HIVE_CONFIG_PATH" ] && [ ! -s "$HIVE_CONFIG_PATH" ] && [ -n "$HIVE_CONFIG_SOURCE" ]; then
    # Config was wiped to 0 bytes (Watchtower recreation) but the PVC copy exists — restore
    cp "$HIVE_CONFIG_SOURCE" "$HIVE_CONFIG_PATH"
    echo "[entrypoint] RECOVERED: $HIVE_CONFIG_PATH was empty (0 bytes), restored from $HIVE_CONFIG_SOURCE"
  elif [ -f "$HIVE_CONFIG_PATH" ] && [ ! -s "$HIVE_CONFIG_PATH" ]; then
    # Config is empty and no PVC copy exists — fatal, cannot recover
    echo "[entrypoint] ERROR: $HIVE_CONFIG_PATH exists but is empty (0 bytes)."
    echo "[entrypoint] No runtime config found at $HIVE_CONFIG_RUNTIME (or legacy $HIVE_CONFIG_RUNTIME_LEGACY)."
    echo "[entrypoint] This usually happens after 'docker compose down -v' wipes the data volume."
    echo "[entrypoint] Restore your hive.yaml from backup or version control and restart."
    exit 1
  fi
fi

# ── Cleanup stale stderr logs from previous agent launches ────────────
STDERR_LOG_MAX_AGE_MINUTES=60
find /tmp -maxdepth 1 -name '.hive-launch-stderr-*.log' -mmin +"$STDERR_LOG_MAX_AGE_MINUTES" -delete 2>/dev/null || true

# ── Root-only setup (runs once, then re-execs as dev) ──────────────────
if [ "$(id -u)" = "0" ]; then
  # Fix ownership of mounted volumes (may be root-owned from host bind mounts).
  # Skip recursive chown if /data is already owned by dev — critical for NFS
  # where recursive chown over thousands of files causes multi-minute delays.
  DATA_OWNER=$(stat -c '%u' /data 2>/dev/null || echo "0")
  if [ "$DATA_OWNER" != "1001" ]; then
    echo "[entrypoint] Fixing /data ownership (currently uid=$DATA_OWNER)..."
    chown -R dev:node /data 2>/dev/null || true
  fi
  chown dev:node /home/dev 2>/dev/null || true
  chown dev:node /etc/hive/hive.yaml 2>/dev/null || true

  # Ensure the PVC secrets dir the dashboard writes API keys into
  # (/data/secrets/litellm_api_key) exists and is owned by the dev user.
  # The Go binary runs as non-root (uid 1001) and CANNOT chown, so if this
  # dir ever ends up root-owned the key save fails with EACCES. We create
  # and chown it here (as root, before dropping to dev) UNCONDITIONALLY —
  # not gated on DATA_OWNER — because the recursive /data chown above is
  # skipped when /data is already dev-owned, which would leave a
  # root-owned /data/secrets uncorrected.
  #
  # Mode 710, not 700: the directory needs group EXECUTE so agent UIDs (2001+,
  # group node) can TRAVERSE it to open /data/secrets/bob_api_key, which the
  # bobshell CLI reads as the agent (see the bob_api_key chmod 440 below).
  # Execute-without-read means agents can open that one path by name but
  # cannot LIST the directory, so other secrets in here are not enumerable.
  # Per-file modes remain the actual access control.
  mkdir -p /data/secrets && chown dev:node /data/secrets 2>/dev/null || true
  chmod 710 /data/secrets 2>/dev/null || true
  # The dashboard writes this file as dev with mode 600 (bobKeyFileMode); the
  # agent UID must be able to read it. Re-apply on every boot so a key saved
  # before this fix shipped is corrected without re-entering it.
  if [ -f /data/secrets/bob_api_key ]; then
    chown dev:node /data/secrets/bob_api_key 2>/dev/null || true
    chmod 440 /data/secrets/bob_api_key 2>/dev/null || true
  fi

  mkdir -p /var/run/hive-metrics && chown dev:node /var/run/hive-metrics 2>/dev/null || true
  mkdir -p /var/run/hive-metrics/agent-tokens && chown dev:node /var/run/hive-metrics/agent-tokens 2>/dev/null || true

  # Fix permissions on bind-mounted secret files (host may own them as
  # a different UID with mode 600, making them unreadable by dev/UID 1001)
  chown dev:node /secrets/*.pem 2>/dev/null || true
  chmod 644 /secrets/*.pem 2>/dev/null || true
  # API-key files are not .pem, so they need the same treatment. Without this
  # a host-side mode-600 file owned by a foreign UID reads as EACCES from the
  # Go binary (uid 1001), and every key-file read swallows the error — the key
  # silently resolves to "" and the backend looks unconfigured. Mode 400:
  # owner-read only (stricter than the .pem files, which ttyd/git also read).
  #
  # bob_api_key is the ONE exception and must be 440 (group node), not 400.
  # Every other key here is consumed IN-PROCESS by the Go binary running as
  # dev, so owner-read is sufficient. bob is different: the bobshell CLI reads
  # BOBSHELL_API_KEY itself while running AS THE AGENT UID (2001+, group node),
  # so a 400 dev-owned file is EACCES for it. The key then resolves empty and
  # bob silently falls back to the W3ID browser SSO flow that cannot complete
  # in a pod — the fleet-wide "stuck at the auth prompt" bug. Group-read only;
  # never world-readable.
  for key_file in /secrets/litellm_api_key; do
    [ -f "$key_file" ] || continue
    chown dev:node "$key_file" 2>/dev/null || true
    chmod 400 "$key_file" 2>/dev/null || true
  done
  if [ -f /secrets/bob_api_key ]; then
    chown dev:node /secrets/bob_api_key 2>/dev/null || true
    chmod 440 /secrets/bob_api_key 2>/dev/null || true
  fi

  # Copy read-only mounted secrets so dev user can read them
  if [ -f /etc/hive/gh-app-key.pem ]; then
    cp /etc/hive/gh-app-key.pem /var/run/hive-metrics/gh-app-key.pem
    chown dev:node /var/run/hive-metrics/gh-app-key.pem
    chmod 400 /var/run/hive-metrics/gh-app-key.pem
    export GH_APP_KEY_FILE=/var/run/hive-metrics/gh-app-key.pem
  fi

  # Seed data files from image into /data if they don't already exist
  if [ -d /opt/hive/seed-data ]; then
    echo "[entrypoint] Seeding data files..."
    cp -rn /opt/hive/seed-data/* /data/ 2>/dev/null || true
  fi

  # Create beads symlinks: /home/dev/<agent>-beads -> /data/beads/<agent>
  if [ -d /etc/hive/agents ] || [ -d /data/beads ]; then
    mkdir -p /home/dev /data/beads
    if [ -d /etc/hive/agents ]; then
      for envfile in /etc/hive/agents/*.env; do
        [ -f "$envfile" ] || continue
        agent="$(basename "$envfile" .env)"
        mkdir -p "/data/beads/${agent}"
        ln -sfn "/data/beads/${agent}" "/home/dev/${agent}-beads"
        echo "[entrypoint] Beads symlink: /home/dev/${agent}-beads -> /data/beads/${agent}"
      done
    fi
    for beaddir in /data/beads/*/; do
      [ -d "$beaddir" ] || continue
      agent="$(basename "$beaddir")"
      if [ ! -L "/home/dev/${agent}-beads" ]; then
        ln -sfn "/data/beads/${agent}" "/home/dev/${agent}-beads"
        echo "[entrypoint] Beads symlink: /home/dev/${agent}-beads -> /data/beads/${agent}"
      fi
    done
    chown dev:node /home/dev 2>/dev/null || true
    if [ "$DATA_OWNER" != "1001" ]; then
      chown -R dev:node /data/beads 2>/dev/null || true
      chmod -R g+rwX /data/beads 2>/dev/null || true
    fi
  fi

  # Shared CLI auth/cache lives in /data/home (persistent volume).
  # Make it group-writable so all agent UIDs (node group) can use it.
  # The manager sets HOME=/data/home for agent tmux sessions.
  mkdir -p /data/home/.config /data/home/.copilot /data/home/.claude/session-env /data/home/.codex /data/home/.bob/settings /data/config/github-copilot /home/dev/.config
  # $HOME itself must be group-writable, not just its children. bob calls
  # mkdirSync('$HOME/.bob') on first run, which needs write on /data/home — a
  # 0755 root-owned $HOME makes that EACCES even though every child dir below
  # is perfectly writable. 2775 = rwxrwxr-x + setgid (new entries inherit node).
  chmod 2775 /data/home 2>/dev/null || true
  chown dev:node /data/home 2>/dev/null || true
  chmod 2770 /data/home/.copilot 2>/dev/null || true
  chown dev:node /data/home/.copilot 2>/dev/null || true
  chmod 2775 /data/home/.claude /data/home/.claude/session-env 2>/dev/null || true
  chown -R dev:node /data/home/.claude 2>/dev/null || true
  # Codex (newer method) writes a local sqlite state db under $HOME/.codex —
  # pre-create it group-writable + setgid so every agent UID (node group) can
  # init it, matching .claude/.copilot. Without this, switching an agent to the
  # codex backend fails with "Permission denied" initializing state_N.sqlite.
  chmod 2775 /data/home/.codex 2>/dev/null || true
  chown -R dev:node /data/home/.codex 2>/dev/null || true
  # bob writes installation_id, settings.json, trustedFolders.json and tmp/ under
  # $HOME/.bob, plus custom modes under $HOME/.bob/settings. Pre-create both
  # group-writable + setgid so the FIRST agent to launch (a 2001+ UID in group
  # node) never has to mkdir them itself — that mkdir is what failed with EACCES
  # in #2284/#2253. Pre-creating also removes the ordering dependency on which
  # process touches .bob first. 2770: no world access, these hold auth settings.
  chmod 2770 /data/home/.bob /data/home/.bob/settings 2>/dev/null || true
  chown -R dev:node /data/home/.bob 2>/dev/null || true
  ln -sfn /data/config/github-copilot /home/dev/.config/github-copilot
  ln -sfn /data/config/github-copilot /data/home/.config/github-copilot
  ln -sfn /data/home/.copilot /home/dev/.copilot
  # Set group-write + setgid on shared dirs — skip if already done (saves 100s+ on NFS).
  # The polling perm guard handles ongoing config.json fixes regardless.
  # The sampled set MUST include every dir the repair below is relied on to fix.
  # It previously sampled only .copilot and .claude, so a hive whose those two
  # were already correct skipped the whole-tree repair forever — leaving
  # /data/home itself at its original root-owned 0755 and making bob's
  # mkdir('$HOME/.bob') fail with EACCES (#2284). /data/home and .bob are sampled
  # too so that gap cannot reopen.
  NEED_PERM_FIX=false
  if [ -d "/data/home/.copilot" ] && [ -d "/data/home/.claude" ]; then
    for perm_dir in /data/home /data/home/.copilot /data/home/.claude /data/home/.bob; do
      # Missing dir → repair. Default 755 → repair (no group-write/setgid).
      DIR_PERMS=$(stat -c '%a' "$perm_dir" 2>/dev/null || echo "755")
      case "$DIR_PERMS" in
        27[0-9][0-9]|37[0-9][0-9]) ;; # already has group-write + setgid
        *) NEED_PERM_FIX=true ;;
      esac
    done
  else
    NEED_PERM_FIX=true
  fi
  if [ "$NEED_PERM_FIX" = "true" ]; then
    # Run synchronously — on fresh provisions /data/home is nearly empty so
    # this completes in <1s. Running in background caused a race where agents
    # started before perms were fixed, hitting EACCES on config.json.
    echo "[entrypoint] Fixing /data/home perms..."
    chmod -R g+rwX /data/home 2>/dev/null
    find /data/home -type d -exec chmod g+s {} + 2>/dev/null
    if [ "$DATA_OWNER" != "1001" ]; then
      chown -R dev:node /data/config /data/home 2>/dev/null
    fi
    echo "[entrypoint] perm fix complete"
  else
    echo "[entrypoint] /data/home perms OK — skipping"
  fi
  chown dev:node /home/dev/.config 2>/dev/null || true
  # Copilot CLI rewrites config.json with 0600 on every token refresh,
  # locking out other agent UIDs. Run inotify (if available) AND polling
  # as belt-and-suspenders — inotify is unreliable on NFS but instant on
  # local storage; polling is reliable everywhere but has a 5s delay.
  if command -v inotifywait >/dev/null 2>&1; then
    (
      while inotifywait -qq -e close_write,moved_to /data/home/.copilot/ 2>/dev/null; do
        chmod 660 /data/home/.copilot/config.json 2>/dev/null
        chown dev:node /data/home/.copilot/config.json 2>/dev/null
      done
    ) &
    (
      while inotifywait -qq -e close_write,moved_to,create /data/home/.claude/ 2>/dev/null; do
        chmod -R g+rwX /data/home/.claude 2>/dev/null
        find /data/home/.claude -type d -exec chmod g+s {} + 2>/dev/null
        chown -R dev:node /data/home/.claude 2>/dev/null
      done
    ) &
    (
      while inotifywait -qq -e close_write,moved_to,create /data/home/.codex/ 2>/dev/null; do
        chmod -R g+rwX /data/home/.codex 2>/dev/null
        find /data/home/.codex -type d -exec chmod g+s {} + 2>/dev/null
        chown -R dev:node /data/home/.codex 2>/dev/null
      done
    ) &
    echo "[entrypoint] inotify perm guard active (copilot + claude + codex)"
  fi
  (
    CYCLE=0
    while true; do
      # Fast cycle: fix config.json every 5s (copilot rewrites it with 600)
      chmod 660 /data/home/.copilot/config.json 2>/dev/null
      chown dev:node /data/home/.copilot/config.json 2>/dev/null
      # Slow cycle: fix entire /data/home tree every 5 min (new dirs from agents)
      CYCLE=$((CYCLE + 1))
      if [ "$CYCLE" -ge 60 ]; then
        chmod -R g+rwX /data/home/.cache /data/home/.copilot /data/home/.claude /data/home/.codex /data/home/.bob 2>/dev/null
        find /data/home/.cache /data/home/.claude /data/home/.codex /data/home/.bob -type d -exec chmod g+s {} + 2>/dev/null
        CYCLE=0
      fi
      sleep 5
    done
  ) &
  echo "[entrypoint] polling perm guard active (config.json 5s, cache 5m)"
  echo "[entrypoint] CLI config: /data/home (shared, group-writable for agent UIDs)"

  # Write .bashrc for agent shells. GH_TOKEN is NOT exported here — gh-wrapper.sh
  # injects the token at call time, avoiding exposure in shell env / transcripts.
  # Point agent shells (git/curl, which also go through the MITM proxy) at the
  # COMBINED bundle (public roots + proxy CA) the entrypoint builds before the Go
  # launch. Falling back to the proxy CA alone if the combined bundle isn't there
  # yet keeps proxied HTTPS working; the combined bundle is strictly safer because
  # it also trusts real public roots for any non-proxied endpoint (advisory mode).
  cat > /data/home/.bashrc <<'BASHRC' 2>/dev/null || true
# Hive agent shell environment
if [ -f /data/proxy-ca-bundle.pem ]; then
  export SSL_CERT_FILE=/data/proxy-ca-bundle.pem
else
  export SSL_CERT_FILE=/data/proxy-ca.pem
fi
BASHRC
  chmod 644 /data/home/.bashrc 2>/dev/null || true

  # ── Per-agent UID isolation ──────────────────────────────────────────
  # Extract agent names from config + pack YAML, create system users,
  # write UID map, and set up iptables to force all outbound :443
  # through the MITM proxy (so agents can't bypass it via unset HTTPS_PROXY).
  HIVE_CONFIG="${HIVE_CONFIG:-/etc/hive/hive.yaml}"
  HIVE_UID_BASE=2001
  PROXY_UID=1001

  # Collect agent names from hive.yaml (map keys) and pack YAML (list items)
  AGENT_NAMES=""
  if [ -f "$HIVE_CONFIG" ]; then
    AGENT_NAMES=$(python3 -c "
import yaml, sys, os
names = set()
with open('$HIVE_CONFIG') as f:
    cfg = yaml.safe_load(f) or {}
agents = cfg.get('agents', {})
if isinstance(agents, dict):
    names.update(agents.keys())
elif isinstance(agents, list):
    for a in agents:
        if isinstance(a, dict) and 'name' in a:
            names.add(a['name'])
# Also check pack YAML if HIVE_LEVEL is set
level = os.environ.get('HIVE_LEVEL', '')
if level:
    import glob
    for p in glob.glob('/opt/hive/packs/level-*.yaml') + glob.glob('/data/packs/level-*.yaml'):
        try:
            with open(p) as pf:
                pack = yaml.safe_load(pf) or {}
            pack_agents = pack.get('agents', [])
            if isinstance(pack_agents, list):
                for a in pack_agents:
                    if isinstance(a, dict) and 'name' in a:
                        names.add(a['name'])
            elif isinstance(pack_agents, dict):
                names.update(pack_agents.keys())
        except Exception:
            pass
print('\n'.join(sorted(names)))
" 2>/dev/null) || true
  fi

  # Pre-cleanup: remove stale workspace artifacts from agent dirs.
  # Runs in BACKGROUND to avoid blocking startup — on slow CephFS with many
  # dirs, sequential rm -rf can exceed the startup probe timeout (10 min),
  # causing a livelock where the container restarts before cleanup finishes.
  # The Go binary's workspace_cleanup.go handles ongoing cleanup at runtime.
  WORKSPACE_MAX_AGE_SECS=7200
  if [ -d "$agentWorkspaceRoot" ] 2>/dev/null || [ -d /data/agents ]; then
    _CLEANUP_ROOT="${agentWorkspaceRoot:-/data/agents}"
    (
      _CLEANUP_DIRS=0
      _CLEANUP_FILES=0
      _NOW=$(date +%s)
      for _agent_dir in "$_CLEANUP_ROOT"/*/; do
        [ -d "$_agent_dir" ] || continue
        _agent_base=$(basename "$_agent_dir")
        for _entry in "$_agent_dir"*; do
          [ -e "$_entry" ] || continue
          _entry_name=$(basename "$_entry")
          case "$_entry_name" in .cache|.config|.npm-cache|bin|beads.json|stats.json|.*) continue ;; esac
          _MTIME=$(stat -c '%Y' "$_entry" 2>/dev/null || echo "$_NOW")
          _AGE=$((_NOW - _MTIME))
          if [ "$_AGE" -gt "$WORKSPACE_MAX_AGE_SECS" ]; then
            if [ -d "$_entry" ]; then
              echo "[entrypoint] bg-cleanup: removing dir ${_agent_base}/${_entry_name}"
              rm -rf "$_entry" 2>/dev/null && _CLEANUP_DIRS=$((_CLEANUP_DIRS + 1))
            else
              rm -f "$_entry" 2>/dev/null && _CLEANUP_FILES=$((_CLEANUP_FILES + 1))
            fi
          fi
        done
      done
      echo "[entrypoint] bg-cleanup: done — removed $_CLEANUP_DIRS dirs, $_CLEANUP_FILES files"
    ) &
    echo "[entrypoint] Pre-cleanup: started in background (PID $!)"
  fi

  if [ -n "$AGENT_NAMES" ]; then
    echo "[entrypoint] Creating per-agent users for UID isolation..."
    mkdir -p /var/run/hive
    UID_OFFSET=0
    UID_JSON='{"agents":{'
    FIRST=true
    echo "$AGENT_NAMES" | while read -r agent_name; do
      [ -z "$agent_name" ] && continue
      AGENT_UID=$((HIVE_UID_BASE + UID_OFFSET))
      if ! id "hive-${agent_name}" >/dev/null 2>&1; then
        useradd --system -u "$AGENT_UID" -g node -d /data/home -M -s /bin/bash "hive-${agent_name}" 2>/dev/null || true
      fi
      mkdir -p "/data/agents/${agent_name}"
      # Skip recursive chown if already owned by this agent — avoids NFS
      # contention during rolling updates when the old pod is still writing.
      AGENT_DIR_OWNER=$(stat -c '%u' "/data/agents/${agent_name}" 2>/dev/null || echo "0")
      if [ "$AGENT_DIR_OWNER" != "$AGENT_UID" ]; then
        chown -R "hive-${agent_name}:node" "/data/agents/${agent_name}" 2>/dev/null || true
      fi
      # Ensure agent dirs are group-writable so the dev user (same node group)
      # can clean up stale workspaces at runtime without root privileges.
      chmod g+rwX "/data/agents/${agent_name}" 2>/dev/null || true
      if [ -d "/data/beads/${agent_name}" ]; then
        BEAD_DIR_OWNER=$(stat -c '%u' "/data/beads/${agent_name}" 2>/dev/null || echo "0")
        if [ "$BEAD_DIR_OWNER" != "$AGENT_UID" ]; then
          chown -R "hive-${agent_name}:node" "/data/beads/${agent_name}" 2>/dev/null || true
        fi
      fi
      # ── Per-agent GitHub App token cache file ────────────────────────
      # The hive binary runs as dev (UID 1001) after privilege drop and CANNOT
      # chown — an unprivileged process cannot give a file away to another UID
      # without CAP_CHOWN, and capabilities are deliberately restricted here
      # (the same container already fails NET_ADMIN for iptables above). So the
      # runtime write path must never call chown. We pre-create the cache file
      # HERE, as root, with the final ownership and mode:
      #
      #   owner = dev  (UID 1001)      -> hive can rewrite it in place, no chown
      #   group = hive-<agent>         -> ONLY this agent can read it
      #   mode  = 0640                 -> owner rw, group r, world none
      #
      # The group MUST be a per-agent private group. The agent users are all in
      # the shared "node" group (gid 1000), so group-node would let EVERY agent
      # read EVERY other agent's scoped token — exactly the cross-agent leak the
      # per-agent token exists to prevent. groupadd + usermod -aG gives each
      # agent a private group it is the only member of, while leaving "node" as
      # its primary group so nothing else about the agent's access changes.
      #
      # dev deliberately does NOT join these groups: it writes as the file
      # OWNER, so it needs no group membership. That keeps dev out of every
      # agent's private group and avoids N usermod calls.
      #
      # gids are auto-allocated by `groupadd --system` rather than derived from
      # the agent UID. A derived gid (e.g. uid+1000) can collide with an
      # existing group; letting groupadd pick from the system range cannot.
      #
      # Verified in ghcr.io/kubestellar/hive:v2-latest: dev writes in place OK,
      # the owning agent reads its own token OK, and a second agent gets
      # EACCES on the first agent's token.
      AGENT_TOKEN_GROUP="hive-${agent_name}"
      if ! getent group "$AGENT_TOKEN_GROUP" >/dev/null 2>&1; then
        groupadd --system "$AGENT_TOKEN_GROUP" 2>/dev/null || true
      fi
      usermod -aG "$AGENT_TOKEN_GROUP" "hive-${agent_name}" 2>/dev/null || true
      AGENT_TOKEN_FILE="/var/run/hive-metrics/agent-tokens/gh-token-${agent_name}.cache"
      # Create empty (never seeded with a value) — hive fills it at launch.
      # touch is idempotent across restarts; chown/chmod re-assert the contract
      # unconditionally in case a previous image left the file mis-owned.
      touch "$AGENT_TOKEN_FILE" 2>/dev/null || true
      chown "dev:${AGENT_TOKEN_GROUP}" "$AGENT_TOKEN_FILE" 2>/dev/null || true
      chmod 640 "$AGENT_TOKEN_FILE" 2>/dev/null || true
      echo "[entrypoint] Agent token cache: ${AGENT_TOKEN_FILE} (dev:${AGENT_TOKEN_GROUP} 0640)"
      echo "[entrypoint] Agent user: hive-${agent_name} (UID ${AGENT_UID})"
      UID_OFFSET=$((UID_OFFSET + 1))
    done

    # Write uid-map.json using python for proper JSON
    python3 -c "
import json, os
names = '''$AGENT_NAMES'''.strip().split('\n')
names = [n for n in names if n]
agents = {}
for i, name in enumerate(sorted(names)):
    agents[name] = $HIVE_UID_BASE + i
uid_map = {
    'agents': agents,
    'proxy_uid': $PROXY_UID,
    'base_uid': $HIVE_UID_BASE,
    'iptables_active': False
}
os.makedirs('/var/run/hive', exist_ok=True)
with open('/var/run/hive/uid-map.json', 'w') as f:
    json.dump(uid_map, f, indent=2)
print('[entrypoint] UID map written to /var/run/hive/uid-map.json')
" 2>/dev/null || echo "[entrypoint] WARN: Failed to write UID map"

    # Set up iptables: redirect all outbound :443 to the MITM proxy port,
    # except traffic from the proxy itself (UID 1001 / dev user).
    PROXY_PORT=18443
    if command -v iptables >/dev/null 2>&1; then
      if iptables -t nat -N HIVE_PROXY 2>/dev/null; then
        # Self-exemption uses BOTH owner-UID and packet-mark RETURNs so each
        # platform is covered by whichever mechanism works there:
        #   - OKE: `-m owner --uid-owner` works (xt_owner present) and is the
        #     RELIABLE exemption for the proxy's own upstream :443 dials.
        #   - OpenShift/OVN: xt_owner is ABSENT so the owner lines fail to load,
        #     but the PACKET MARK (SO_MARK / HIVE_PROXY_EGRESS_MARK, exported
        #     above) exempts the proxy's traffic there.
        #
        # A mark-ONLY exemption (as PR #2678 shipped on the v4 line) regressed
        # the LIVE OKE console hive: the SO_MARK did not reliably stick on the
        # proxy's outbound sockets, so its OWN :443 to api.github.com hit the
        # REDIRECT rule, looped back into itself (:18443) → EPERM/ECONNREFUSED/
        # EOF → no GitHub App token minting → no repo enumeration → dead L6 loop
        # (refs #2678, #2674). Keeping the owner-match RETURNs is what fixes OKE.
        #
        # The owner `-A` lines are appended DEFENSIVELY with `|| true` so that on
        # a host WITHOUT xt_owner their failure does NOT abort the ruleset under
        # `set -e`; the mark exemption + redirect still establish.
        # OKE: owner-match exemption (reliable where xt_owner is present).
        iptables -t nat -A HIVE_PROXY -m owner --uid-owner 0 -j RETURN || true
        iptables -t nat -A HIVE_PROXY -m owner --uid-owner "$PROXY_UID" -j RETURN || true
        # OpenShift/OVN: packet-mark exemption (works with no xt_owner).
        iptables -t nat -A HIVE_PROXY -m mark --mark "$HIVE_PROXY_EGRESS_MARK" -j RETURN
        iptables -t nat -A HIVE_PROXY -p tcp --dport 443 -j REDIRECT --to-ports "$PROXY_PORT"
        iptables -t nat -A OUTPUT -j HIVE_PROXY
        echo "[entrypoint] iptables: outbound :443 -> :${PROXY_PORT} (proxy UID ${PROXY_UID} + egress mark ${HIVE_PROXY_EGRESS_MARK} exempt)"
        # Update uid-map to record iptables active
        python3 -c "
import json
with open('/var/run/hive/uid-map.json') as f:
    m = json.load(f)
m['iptables_active'] = True
with open('/var/run/hive/uid-map.json', 'w') as f:
    json.dump(m, f, indent=2)
" 2>/dev/null || true
      else
        echo "[entrypoint] WARN: iptables chain creation failed (need NET_ADMIN capability)"
      fi
    else
      echo "[entrypoint] WARN: iptables not found, proxy enforcement is advisory-only"
    fi
  fi

  # Drop to non-root user for all runtime processes.
  # Claude Code refuses --dangerously-skip-permissions as root.
  # Test gosu first — on some NFS/container combos it fails with EPERM.
  if command -v gosu >/dev/null 2>&1 && gosu dev true 2>/dev/null; then
    echo "[entrypoint] Dropping to dev user"
    exec gosu dev "$0" "$@"
  else
    echo "[entrypoint] WARN: gosu unavailable or failed, continuing as root"
  fi
fi

# ── Non-root setup and process launch (runs as dev) ────────────────────

# Ensure vault directories exist
mkdir -p /data/vaults
if [ -n "${HIVE_WIKI_GIT_URL:-}" ] && [ ! -d /data/vaults/hive-wiki/.git ]; then
  echo "[entrypoint] Cloning wiki vault from ${HIVE_WIKI_GIT_URL}..."
  git clone "${HIVE_WIKI_GIT_URL}" /data/vaults/hive-wiki 2>/dev/null || \
    echo "[entrypoint] Git clone failed — vault will be initialized empty"
fi
mkdir -p /data/vaults/hive-wiki

# Configure git identity and credential helper for GitHub App token
git config --global user.name "kubestellar-hive"
git config --global user.email "hive-bot@kubestellar.io"
git config --global --replace-all credential.helper ""
git config --global --replace-all "credential.https://github.com.helper" "/usr/local/bin/git-credential-hive.sh"

# Also wire the credential helper for this hive's ACTUAL GitHub host when it
# is NOT public github.com (a GitHub Enterprise instance, e.g. github.ibm.com).
#
# Without this, a hive whose repos/App live on GHE only ever gets the helper
# registered for credential.https://github.com.helper (above). git matches
# credential helpers by exact host, so `git clone https://github.ibm.com/...`
# never invokes ANY helper for that host, falls through to an interactive
# prompt, and fails in this non-interactive shell with "fatal: could not read
# Username for 'https://github.ibm.com': terminal prompts disabled" — this was
# reported live from a GHE hive (devx-prod/epx-vscode-ext-poc) where scanner
# and quality (which talk to the GitHub API, not git-over-HTTPS) worked fine
# while guide's `git clone` could not authenticate at all.
#
# The host is derived the same way the Go binary's GitHubConfig.HostLabel()
# derives it (pkg/config/config.go): prefer github.base_url, fall back to the
# host portion of github.api_url, strip scheme and a trailing /api/v3, default
# to github.com. Reading it here (from the same hive.yaml the Go binary reads)
# rather than hardcoding "github.ibm.com" keeps this general for ANY configured
# GHE host, and a no-op for a plain github.com hive (GHE_GIT_HOST resolves to
# "github.com", which already has its helper wired above).
GHE_GIT_HOST=""
if [ -f "${HIVE_CONFIG:-/etc/hive/hive.yaml}" ]; then
  GHE_GIT_HOST=$(python3 -c "
import sys, yaml
try:
    with open(sys.argv[1]) as f:
        cfg = yaml.safe_load(f) or {}
except Exception:
    sys.exit(0)
gh = cfg.get('github') or {}
pick = (gh.get('base_url') or gh.get('api_url') or '').strip()
if pick.startswith('https://'):
    pick = pick[len('https://'):]
elif pick.startswith('http://'):
    pick = pick[len('http://'):]
pick = pick.rstrip('/')
if pick.endswith('/api/v3'):
    pick = pick[: -len('/api/v3')]
host = pick.split('/', 1)[0]
if host and host.lower() != 'api.github.com' and host.lower() != 'github.com':
    print(host)
" "${HIVE_CONFIG:-/etc/hive/hive.yaml}" 2>/dev/null) || true
fi
if [ -n "$GHE_GIT_HOST" ]; then
  git config --global --replace-all "credential.https://${GHE_GIT_HOST}.helper" "/usr/local/bin/git-credential-hive.sh"
  echo "[entrypoint] git credential helper wired for GitHub Enterprise host: ${GHE_GIT_HOST}"
fi

# Generate initial GitHub App token if credentials are available
if [ -x /usr/local/bin/hive-config.sh ]; then
  . /usr/local/bin/hive-config.sh 2>/dev/null || true
fi
# Use the dev-readable copy if the configured key file isn't readable
if [ -n "${GH_APP_KEY_FILE:-}" ] && [ ! -r "$GH_APP_KEY_FILE" ]; then
  if [ -r /var/run/hive-metrics/gh-app-key.pem ]; then
    export GH_APP_KEY_FILE=/var/run/hive-metrics/gh-app-key.pem
  fi
fi
if [ -n "${GH_APP_ID:-}" ] && [ -n "${GH_APP_INSTALLATION_ID:-}" ]; then
  echo "[entrypoint] Generating GitHub App token..."
  /usr/local/bin/gh-app-token.sh >/dev/null 2>&1 && \
    echo "[entrypoint] Token cached at /var/run/hive-metrics/gh-app-token.cache" || \
    echo "[entrypoint] WARN: GitHub App token generation failed"
  export HIVE_GITHUB_TOKEN="$(cat /var/run/hive-metrics/gh-app-token.cache 2>/dev/null || true)"
fi

# Load Copilot PAT from persistent volume so the Go binary can inject it
# into agent tmux sessions via COPILOT_GITHUB_TOKEN env var.
COPILOT_PAT_FILE="/data/copilot-token-pat"
if [ -f "$COPILOT_PAT_FILE" ] && [ -s "$COPILOT_PAT_FILE" ]; then
  export COPILOT_GITHUB_TOKEN
  COPILOT_GITHUB_TOKEN="$(cat "$COPILOT_PAT_FILE")"
  echo "[entrypoint] Copilot PAT loaded from $COPILOT_PAT_FILE"
fi

# ── Proxy CA trust for the MAIN Go hive process (race-free) ──────────────
#
# CHICKEN-AND-EGG (fixed in the Go binary; this block is now belt-and-suspenders):
# /data/proxy-ca.pem is GENERATED BY the Go proxy at runtime
# (proxy.NewGitHubProxy → loadOrGenerateCA). On a fresh-PVC boot it does NOT
# exist when the entrypoint reaches the `[ -f /data/proxy-ca.pem ]` gate below,
# so SSL_CERT_FILE was never exported and the Go process launched trusting only
# the (still-installing) system store → a multi-minute post-roll window where
# every token mint failed "x509: certificate signed by unknown authority".
# The Go binary now establishes its OWN proxy-CA trust: the token-mint HTTP
# client (pkg/github/proxytrust.go) carries a RootCAs pool = system roots +
# /data/proxy-ca.pem, LAZILY reloaded from disk, so the first mint after the
# proxy writes the CA trusts it immediately, with no dependency on this export.
# The block below is kept because it still helps Node/sub-processes and any Go
# restart, and it is now correct whether or not the CA exists yet at this point.
#
# The Go hive process makes GitHub App token calls
# (POST /app/installations/{id}/access_tokens). With F5 forced egress enabled
# (the iptables REDIRECT of all outbound :443 to the MITM proxy, set up above),
# those calls hit the proxy, which presents a cert forged by /data/proxy-ca.pem.
# Go's crypto/x509 must trust that CA or every token mint fails with
# "x509: certificate signed by unknown authority" → "all N repos failed" → the
# agent loop is dead (verified live on the console hive).
#
# We CANNOT rely on the system-trust install below (the gosu/update-ca-certificates
# block after the launch) for the Go process, for two reasons:
#   1. RACE: the Go process starts here, BEFORE that install runs, and Go caches
#      the system cert pool at first TLS handshake. A boot-time gosu failure
#      ("WARN: proxy CA install via gosu failed", seen live) then leaves the
#      already-running Go process permanently unable to verify the proxy cert —
#      the later CA-watch re-install loop cannot fix a pool Go already cached.
#   2. Go honors SSL_CERT_FILE at handshake time (NOT cached like the system
#      bundle), so pointing it at a file we control is robust and race-free.
#
# We point SSL_CERT_FILE at a COMBINED bundle = public roots + proxy CA, NOT the
# proxy CA alone. Rationale: SSL_CERT_FILE REPLACES the trust set, so proxy-CA-only
# would make Go trust ONLY the proxy CA and break any HTTPS that does NOT traverse
# the MITM proxy — which happens in advisory mode (HIVE_PROXY_ADVISORY_OK=true, no
# iptables redirect: the Go process then reaches api.github.com / a GHE host
# directly with a REAL public cert). The combined bundle trusts BOTH the real
# public roots AND the proxy CA, so it is correct whether or not egress is forced.
SSL_CERT_BUNDLE="/data/proxy-ca-bundle.pem"
SYSTEM_CA_BUNDLE="/etc/ssl/certs/ca-certificates.crt"
if [ -f /data/proxy-ca.pem ] && [ -f "$SYSTEM_CA_BUNDLE" ]; then
  if cat "$SYSTEM_CA_BUNDLE" /data/proxy-ca.pem > "$SSL_CERT_BUNDLE" 2>/dev/null; then
    export SSL_CERT_FILE="$SSL_CERT_BUNDLE"
    echo "[entrypoint] SSL_CERT_FILE=$SSL_CERT_BUNDLE (public roots + proxy CA) for Go hive process"
  else
    echo "[entrypoint] WARN: could not build combined CA bundle $SSL_CERT_BUNDLE; Go hive will rely on system trust store (racy)"
  fi
elif [ -f /data/proxy-ca.pem ]; then
  # No system bundle on disk (unusual): fall back to proxy CA alone so at least
  # proxied HTTPS verifies. Direct (non-proxied) HTTPS would fail here, but with
  # forced egress every :443 is proxied anyway.
  export SSL_CERT_FILE="/data/proxy-ca.pem"
  echo "[entrypoint] WARN: no system CA bundle at $SYSTEM_CA_BUNDLE; SSL_CERT_FILE=/data/proxy-ca.pem (proxy CA only)"
fi

echo "[entrypoint] Starting Go binary on :${HIVE_API_PORT} (uid=$(id -u))"
hive "$@" &
HIVE_PID=$!

sleep 1

# Install the MITM proxy CA into the system trust store so that
# agent sub-processes (git, curl) trust the forged certificates.
# Also set NODE_EXTRA_CA_CERTS for Node.js (Copilot, etc.).
#
# NOTE: this system-bundle install is NO LONGER the trust path for the Go hive
# process — that now uses SSL_CERT_FILE (the combined bundle built before the
# launch above), which is race-free and survives a gosu failure here. So unlike
# the F5 egress enforcement (fatal on failure), a failure of THIS install is
# recoverable and must NOT be fatal: it only affects sub-processes that read the
# system store, and even those have the combined-bundle fallback via .bashrc /
# NODE_EXTRA_CA_CERTS. We log clearly but continue.
if [ -f /data/proxy-ca.pem ]; then
  if command -v gosu >/dev/null 2>&1; then
    gosu root sh -c 'cp /data/proxy-ca.pem /usr/local/share/ca-certificates/hive-proxy-ca.crt && update-ca-certificates' 2>/dev/null \
      && echo "[entrypoint] proxy CA installed to system trust store" \
      || echo "[entrypoint] WARN: proxy CA install via gosu failed (recoverable: Go hive uses SSL_CERT_FILE combined bundle; sub-processes use NODE_EXTRA_CA_CERTS / .bashrc SSL_CERT_FILE)"
  elif cp /data/proxy-ca.pem /usr/local/share/ca-certificates/hive-proxy-ca.crt 2>/dev/null; then
    update-ca-certificates 2>/dev/null && echo "[entrypoint] proxy CA installed to system trust store"
  else
    echo "[entrypoint] WARN: could not install proxy CA to system store (non-root; recoverable via SSL_CERT_FILE / NODE_EXTRA_CA_CERTS)"
  fi
  # Prefer the combined bundle (public roots + proxy CA) for Node.js too, for the
  # same advisory-mode safety as the Go process; fall back to the proxy CA alone.
  if [ -f /data/proxy-ca-bundle.pem ]; then
    export NODE_EXTRA_CA_CERTS=/data/proxy-ca-bundle.pem
  else
    export NODE_EXTRA_CA_CERTS=/data/proxy-ca.pem
  fi
  echo "[entrypoint] NODE_EXTRA_CA_CERTS=$NODE_EXTRA_CA_CERTS set for Node.js agents"
fi
# Watch for CA cert changes (Go binary may regenerate it) and re-install into
# the system store AND rebuild the combined SSL_CERT_FILE bundle so sub-processes
# and any future Go restart pick up the new proxy CA. (A restart is required for
# the already-running Go process to re-read SSL_CERT_FILE — Go caches it — but the
# supervisor restarts the process, which then reads the freshly-rebuilt bundle.)
(
  PREV_HASH=""
  while true; do
    sleep 10
    [ -f /data/proxy-ca.pem ] || continue
    CUR_HASH=$(sha256sum /data/proxy-ca.pem 2>/dev/null | cut -d' ' -f1)
    if [ -n "$CUR_HASH" ] && [ "$CUR_HASH" != "$PREV_HASH" ]; then
      cp /data/proxy-ca.pem /usr/local/share/ca-certificates/hive-proxy-ca.crt 2>/dev/null \
        && update-ca-certificates 2>/dev/null \
        && echo "[entrypoint] proxy CA re-installed to system trust store (hash changed)"
      if [ -f "$SYSTEM_CA_BUNDLE" ]; then
        cat "$SYSTEM_CA_BUNDLE" /data/proxy-ca.pem > "$SSL_CERT_BUNDLE" 2>/dev/null \
          && echo "[entrypoint] combined CA bundle rebuilt at $SSL_CERT_BUNDLE (proxy CA hash changed)"
      fi
      PREV_HASH="$CUR_HASH"
    fi
  done
) &

echo "[entrypoint] Starting Node.js proxy on :${HIVE_PROXY_PORT} → :${HIVE_API_PORT}"
cd /opt/hive/proxy && node server.js &
PROXY_PID=$!

TTYD_PORT="${HIVE_TTYD_PORT:-7681}"
echo "[entrypoint] Starting ttyd on :${TTYD_PORT}"
# Wrap ttyd in a respawn loop: ttyd exits on SIGHUP (its close signal),
# and orphaned LISTEN sockets block rebind, so we wait before retrying.
TTYD_RESPAWN_DELAY_SECS=5
(
  trap '' HUP
  while true; do
    ttyd -W -a -p "${TTYD_PORT}" -t fontSize=14 -t disableLeaveAlert=true /usr/local/bin/ttyd-tmux.sh
    echo "[entrypoint] ttyd exited (rc=$?), respawning in ${TTYD_RESPAWN_DELAY_SECS}s..."
    sleep "$TTYD_RESPAWN_DELAY_SECS"
  done
) &
TTYD_PID=$!

cleanup() {
  echo "[entrypoint] Shutting down..."
  # PVC backup is managed by Save() — no shutdown backup needed
  kill "$TTYD_PID" 2>/dev/null || true
  kill "$PROXY_PID" 2>/dev/null || true
  kill "$HIVE_PID" 2>/dev/null || true
  wait "$HIVE_PID" 2>/dev/null || true
  wait "$PROXY_PID" 2>/dev/null || true
}
trap cleanup INT TERM

wait "$HIVE_PID"
