#!/usr/bin/env bash
# Buildbox-side seat launcher. Runs ONE wrapper invocation inside ONE rootless podman
# container with kernel-enforced caps. Invoked by remote-seat.mjs through the existing
# remote-build transport (rb-<jobid>.service), with cwd = the repo mirror.
#
# Exit codes are the WRAPPER-CONTRACT codes:
#   pass-through  the container's rc (= the wrapper's rc: 0 / 2 / 3 / 75 / 124)
#   3             image missing, prompt missing, podman itself failed — NOTHING dispatched
set -uo pipefail

fail3() { printf '{"ok":false,"detail":"seat-run: %s"}\n' "$1" >&2; exit 3; }

RUN_ID="" WRAPPER="" REPO_ROOT="" WORKSPACE_REL="." PROMPT_FILE="" LOG_REL=""
TASK_SLUG="" MODEL="" TIMEOUT="" PROFILE="" PERMISSION_MODE="" RESUME="" THREAD_ID=""
IMAGE="" MEMORY="" CPUS="" PIDS="" CGROUP_PARENT="" TMPFS_SIZE="" GIT_NAME="" GIT_EMAIL="" MAX_SEATS=""
while [[ $# -gt 0 ]]; do
  case "$1" in
    --run-id)        RUN_ID="${2:-}";        shift 2;;
    --wrapper)       WRAPPER="${2:-}";       shift 2;;
    --repo-root)     REPO_ROOT="${2:-}";     shift 2;;
    --workspace-rel) WORKSPACE_REL="${2:-}"; shift 2;;
    --prompt-file)   PROMPT_FILE="${2:-}";   shift 2;;
    --log-rel)       LOG_REL="${2:-}";       shift 2;;
    --task-slug)     TASK_SLUG="${2:-}";     shift 2;;
    --model)         MODEL="${2:-}";         shift 2;;
    --timeout)       TIMEOUT="${2:-}";       shift 2;;
    --profile)       PROFILE="${2:-}";       shift 2;;
    --permission-mode)
      [[ $# -ge 2 && -n "${2:-}" && -z "$PERMISSION_MODE" ]] \
        || { printf '{"ok":false,"detail":"seat-run: --permission-mode requires one value"}\n' >&2; exit 2; }
      PERMISSION_MODE="$2"; shift 2;;
    --resume)        RESUME="${2:-}";        shift 2;;
    --thread-id)     THREAD_ID="${2:-}";     shift 2;;
    --image)         IMAGE="${2:-}";         shift 2;;
    --memory)        MEMORY="${2:-}";        shift 2;;
    --cpus)          CPUS="${2:-}";          shift 2;;
    --pids)          PIDS="${2:-}";          shift 2;;
    --cgroup-parent) CGROUP_PARENT="${2:-}"; shift 2;;
    --tmpfs-size)    TMPFS_SIZE="${2:-}";    shift 2;;
    --git-name)      GIT_NAME="${2:-}";      shift 2;;
    --git-email)     GIT_EMAIL="${2:-}";     shift 2;;
    --max-seats)     MAX_SEATS="${2:-}";     shift 2;;
    *) printf '{"ok":false,"detail":"seat-run: unknown arg %s"}\n' "$1" >&2; exit 2;;
  esac
done

[[ -n "$RUN_ID" && -n "$WRAPPER" && -n "$REPO_ROOT" && -n "$PROMPT_FILE" && -n "$LOG_REL" && -n "$TASK_SLUG" && -n "$IMAGE" ]] \
  || { printf '{"ok":false,"detail":"seat-run: missing required arg"}\n' >&2; exit 2; }
[[ -z "$PERMISSION_MODE" || "$PERMISSION_MODE" == "safe" ]] \
  || { printf '{"ok":false,"detail":"seat-run: --permission-mode safe required"}\n' >&2; exit 2; }
[[ "$TASK_SLUG" != incident-* || "$PERMISSION_MODE" == "safe" ]] \
  || { printf '{"ok":false,"detail":"seat-run: incident dispatch requires --permission-mode safe"}\n' >&2; exit 2; }

SEAT_DIR="$HOME/.rb/seats/$RUN_ID"
PROMPT_SOURCE="${BUILD_REMOTE_INPUT_FILE:-$REPO_ROOT/$PROMPT_FILE}"
cleanup() {
  rm -f "$SEAT_DIR/prompt.txt"
  [[ -z "${BUILD_REMOTE_INPUT_FILE:-}" ]] || rm -f "$BUILD_REMOTE_INPUT_FILE"
  tmux kill-session -t "harness-seat-$RUN_ID" >/dev/null 2>&1 || true
  podman rm -f "harness-seat-$RUN_ID" >/dev/null 2>&1 || true
}
trap cleanup EXIT
rm -rf "$SEAT_DIR"
mkdir -p "$SEAT_DIR" || fail3 "seat scratch dir create failed"
chmod 0700 "$SEAT_DIR"
[[ -f "$PROMPT_SOURCE" ]] || fail3 "private prompt input missing"
mv "$PROMPT_SOURCE" "$SEAT_DIR/prompt.txt" || fail3 "prompt move failed"
chmod 0600 "$SEAT_DIR/prompt.txt"

export PATH="$HOME/.local/bin:$PATH"
command -v podman >/dev/null 2>&1 || fail3 "podman not on PATH"

[[ -d "$REPO_ROOT" ]] || fail3 "repo mirror missing: $REPO_ROOT"
SEAT_SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BUNDLE_ROOT="$(cd "$SEAT_SOURCE_DIR/.." && pwd)"
[[ "$BUNDLE_ROOT" == "$REPO_ROOT/"* ]] || fail3 "seat bundle is outside the repo mirror"
BUNDLE_REL="${BUNDLE_ROOT#"$REPO_ROOT/"}"
WRAPPER_ABS="$BUNDLE_ROOT/wrappers/$WRAPPER"
[[ -f "$WRAPPER_ABS" ]] || fail3 "wrapper not in seat bundle: wrappers/$WRAPPER"
[[ -d "$REPO_ROOT/$WORKSPACE_REL" ]] || fail3 "workspace not in mirror: $WORKSPACE_REL"

# The credential set is declared once and read by all three sides of the seam: the workstation
# converger (seat-creds.mjs), this launcher's mounts, and seat-entrypoint.sh inside the container.
# It ships in the mirror alongside the wrappers, so it is resolved the same way they are.
CREDENTIALS_MANIFEST="$SEAT_SOURCE_DIR/credentials.json"
[[ -f "$CREDENTIALS_MANIFEST" ]] || fail3 "credential manifest not in seat bundle: seat/credentials.json"
command -v jq >/dev/null || fail3 "jq is not installed on $(hostname) — the credential manifest cannot be read"
# A seat whose adapter credential is absent on this box authenticates against nothing and burns
# the whole dispatch on "Authentication required". Refuse before podman is touched.
while IFS= read -r cred_home; do
  [[ -n "$cred_home" && ! -f "$HOME/$cred_home" ]] \
    && fail3 "$WRAPPER credential absent on $(hostname): ~/$cred_home — the workstation converges it on the next dispatch; nothing started"
done < <(jq -r --arg w "$WRAPPER" '.items[] | select((.required_by // []) | index($w)) | .home' "$CREDENTIALS_MANIFEST")

podman image exists "$IMAGE" \
  || fail3 "image $IMAGE absent — build it first: modules/harness/seat/build-image.sh $(hostname)"

# Every dispatch re-arms recovery, so a freshly provisioned box is covered by its first seat and a
# changed reconciler reaches the box without a separate install step. Non-fatal: a box that cannot
# arm it still runs seats, it just recovers them on the transport's mirror guard instead of at boot.
bash "$SEAT_SOURCE_DIR/seat-reconcile.sh" install "$SEAT_SOURCE_DIR" \
  || printf 'seat-run: could not arm the boot reconciler on %s\n' "$(hostname)" >&2

# The transport starts every job as `systemd-run --unit rb-<jobId>`, so our own cgroup names the
# job the laptop's watcher polls. Recovery needs it to write that job a terminal rc, and no other
# handle survives: the runner passes the job dir positionally without exporting it, and stdout is
# a supervisor pipe rather than the job log.
JOB_ID="$(sed -n 's|.*/rb-\([0-9a-f]\{1,\}\)\.service.*|\1|p' /proc/self/cgroup 2>/dev/null | head -1)"

# agent.slice caps the AGGREGATE, and it enforces by SIGKILL — a seat killed there reads to the
# orchestrator as an engine crash. Refuse the (N+1)th seat here instead, on the path every seat
# must cross, so the count is enforced rather than assumed.
#
# The lock is held from the count until `podman create` registers the name, so two seats
# dispatched at the same instant cannot both read N-1. Released before the container runs.
mkdir -p "$HOME/.rb/seats"
exec {ADMIT_FD}>"$HOME/.rb/seats/.admission.lock" || fail3 "admission lock not creatable"
# bounded: a wedged podman under the lock must refuse dispatches, never queue them forever
flock -w 30 "$ADMIT_FD" || fail3 "admission lock held 30s on $(hostname) — podman may be wedged"
if [[ -n "$MAX_SEATS" ]]; then
  # --rm reaps a seat when it EXITS. A job killed between create and start leaves one in `created`
  # for good, and three of those would refuse every future dispatch with nothing running. Only
  # seats older than the create->start window are phantoms; a sibling mid-dispatch is seconds old.
  podman ps -a --filter 'name=harness-seat-' --filter 'status=created' --filter 'status=exited' \
    --filter 'until=5m' -q 2>/dev/null | xargs -r podman rm -f >/dev/null 2>&1
  RUNNING="$(podman ps -a --filter 'name=harness-seat-' --format '{{.ID}}' 2>/dev/null | grep -c . )"
  [[ "$RUNNING" -lt "$MAX_SEATS" ]] \
    || fail3 "$(hostname) already runs $RUNNING/$MAX_SEATS seats — dispatch refused, nothing started"
fi

# --profile names a codex profile that must be DEFINED in the seeded config.toml. Dispatching
# without that section routes the run to whichever account the default config points at; a wrong
# account is worse than no run. (ca.sh documents that it ignores --profile.)
if [[ -n "$PROFILE" && "$WRAPPER" == codex.sh ]]; then
  grep -qE "^\[profiles\.\"?${PROFILE}\"?\]" "$HOME/.codex/config.toml" 2>/dev/null \
    || fail3 "--profile $PROFILE is not defined in ~/.codex/config.toml on $(hostname) — account routing undefined"
fi

STATE="$SEAT_DIR/state.json"
# Every field is known here, so the record is rewritten whole rather than merged — one line, no
# interpreter. seat-reconcile.sh reads it with sed for the same reason: it runs from a systemd
# unit that has neither node nor jq on its PATH.
seat_state() { # seat_state <phase> [rc]
  local boot; read -r boot < /proc/sys/kernel/random/boot_id
  printf '{"runId":"%s","jobId":"%s","mirrorPath":"%s","workspaceRel":"%s","wrapper":"%s","taskSlug":"%s","host":"%s","bootId":"%s","phase":"%s","rc":%s,"at":"%s"}\n' \
    "$RUN_ID" "$JOB_ID" "$REPO_ROOT" "$WORKSPACE_REL" "$WRAPPER" "$TASK_SLUG" "$(hostname)" "$boot" "$1" "${2:-null}" "$(date -Is)" \
    > "$STATE.tmp" && mv -f "$STATE.tmp" "$STATE"
}
seat_state starting

# seat log dirs are gitignored, so `git clean` never reaches them — prune our own
find "$REPO_ROOT/.harness-seat" -mindepth 1 -maxdepth 1 -type d -mtime +2 -exec rm -rf {} + 2>/dev/null
find "$HOME/.rb/seats" -mindepth 1 -maxdepth 1 -type d -mtime +2 -exec rm -rf {} + 2>/dev/null

# info/exclude is laptop-side and is never pushed. Without the same patterns HERE, a seat that runs
# `git add -A` commits its own log dir into the commit that gets replayed onto the user's branch.
EXCLUDE="$REPO_ROOT/.git/info/exclude"
if [[ -d "$REPO_ROOT/.git" ]] && ! grep -qx '\.harness-seat/' "$EXCLUDE" 2>/dev/null; then
  mkdir -p "$(dirname "$EXCLUDE")" \
    && printf '# harness seat dispatch (modules/harness/seat)\n/seat-prompt-*.txt\n.harness-seat/\n' >> "$EXCLUDE"
fi

mkdir -p "$REPO_ROOT/$LOG_REL" || fail3 "log dir create failed"
STATUS_FILE="$REPO_ROOT/$LOG_REL/status.json"

MOUNTS=(-v "$REPO_ROOT:/w:rw" -v "$SEAT_DIR/prompt.txt:/seed/prompt.txt:ro")
# The mirror's object store is an alternate outside the workspace (the transport's bare repo).
# git cannot read history — so cannot commit — without it. READ-ONLY at its own path: new objects
# still land in the writable /w/.git/objects, nothing outside the workspace can be modified.
ALTERNATES="$REPO_ROOT/.git/objects/info/alternates"
if [[ -f "$ALTERNATES" ]]; then
  while IFS= read -r alt; do
    [[ -n "$alt" && -d "$alt" ]] || continue
    MOUNTS+=(-v "$alt:$alt:ro")
  done < "$ALTERNATES"
fi
seed_ro() { [[ -f "$1" ]] && MOUNTS+=(-v "$1:$2:ro"); }
while IFS=$'\t' read -r cred_home cred_seed; do
  [[ -n "$cred_home" ]] && seed_ro "$HOME/$cred_home" "$cred_seed"
done < <(jq -r '.items[] | [.home, .seed] | @tsv' "$CREDENTIALS_MANIFEST")


RUN_ARGS=(
  podman run --rm --name "harness-seat-$RUN_ID"
  --memory "$MEMORY" --memory-swap "$MEMORY" --cpus "$CPUS" --pids-limit "$PIDS"
  --read-only --read-only-tmpfs=false
  --tmpfs "/tmp:rw,exec,mode=1777,size=$TMPFS_SIZE"
  --tmpfs "/run:rw,mode=0755,size=16m"
  --tmpfs "/seat-home:rw,exec,mode=0700,size=$TMPFS_SIZE"
  "${MOUNTS[@]}"
  --workdir /w
  --cap-drop=ALL --security-opt no-new-privileges
  --env HARNESS_SEAT_CONTAINER=1
  --env "HARNESS_SEAT_CREDENTIALS_MANIFEST=/w/$BUNDLE_REL/seat/credentials.json"
  --env "HARNESS_IDENTITY_FILE=/w/$LOG_REL/identity.json"
)
# no global git config reaches the container; without an identity a seat cannot commit
[[ -n "$GIT_NAME" ]]  && RUN_ARGS+=(--env "GIT_AUTHOR_NAME=$GIT_NAME"   --env "GIT_COMMITTER_NAME=$GIT_NAME")
[[ -n "$GIT_EMAIL" ]] && RUN_ARGS+=(--env "GIT_AUTHOR_EMAIL=$GIT_EMAIL" --env "GIT_COMMITTER_EMAIL=$GIT_EMAIL")
[[ -n "$CGROUP_PARENT" ]] && RUN_ARGS+=(--cgroup-parent "$CGROUP_PARENT")
RUN_ARGS+=(
  "$IMAGE"
  --wrapper "/w/$BUNDLE_REL/wrappers/$WRAPPER"
  --workspace "/w/$WORKSPACE_REL"
  --task-slug "$TASK_SLUG"
  --log-dir "/w/$LOG_REL"
)
[[ -n "$MODEL" ]]     && RUN_ARGS+=(--model "$MODEL")
[[ -n "$TIMEOUT" ]]   && RUN_ARGS+=(--timeout "$TIMEOUT")
[[ -n "$PROFILE" ]]   && RUN_ARGS+=(--profile "$PROFILE")
[[ -n "$PERMISSION_MODE" ]] && RUN_ARGS+=(--permission-mode "$PERMISSION_MODE")
[[ -n "$RESUME" ]]    && RUN_ARGS+=(--resume "$RESUME")
[[ -n "$THREAD_ID" ]] && RUN_ARGS+=(--thread-id "$THREAD_ID")

# create registers the name under the admission lock; start runs it after the lock is dropped.
CREATE_ARGS=(podman create "${RUN_ARGS[@]:2}")
printf '=== seat-run: %s\n' "$(printf '%q ' "${CREATE_ARGS[@]}")" >&2
"${CREATE_ARGS[@]}" >/dev/null 2>>"$REPO_ROOT/$LOG_REL/seat-run.stderr" \
  || fail3 "podman could not create the seat container"
exec {ADMIT_FD}>&-
seat_state running

# Attach view only. `podman start --attach` below owns stdout (the status JSON) and the wrapper
# rc, so the live seat can never be hosted inside tmux — this is a second reader over podman logs.
if command -v tmux >/dev/null 2>&1; then
  tmux new-session -d -s "harness-seat-$RUN_ID" \
    "podman wait --condition running harness-seat-$RUN_ID >/dev/null 2>&1; podman logs -f harness-seat-$RUN_ID" \
    >/dev/null 2>&1 || true
fi

# </dev/null is contract, not hygiene: `start --attach` proxies OUR stdin into the container, so a
# seat would otherwise eat the caller's remaining input (a script piped to bash disappears).
podman start --attach "harness-seat-$RUN_ID" \
  > "$STATUS_FILE.tmp" 2> >(tee -a "$REPO_ROOT/$LOG_REL/seat-run.stderr" >&2) < /dev/null
RC=$?
seat_state done "$RC"
mv "$STATUS_FILE.tmp" "$STATUS_FILE" 2>/dev/null || true

# podman's own failure codes are not wrapper rcs: nothing was dispatched.
case "$RC" in
  125|126|127) fail3 "podman failed to start the seat (rc=$RC)";;
esac
printf '=== seat-run exit=%s ===\n' "$RC" >&2
exit "$RC"
