#!/usr/bin/env bash
# Starts one ephemeral agent sandbox on this buildbox. Rootless, capability-free,
# resource-capped, discarded on exit.
set -euo pipefail

ROOT="${SANDBOX_HOST_ROOT:-$HOME/.local/share/overdeck-sandbox}"
SANDBOX_ROOT="${SANDBOX_ROOT:-$HOME/sandbox}"
IMAGE_TAG_FILE="$ROOT/agent-image-tag"
CRED_ROOT="$HOME/.local/state/overdeck-sandbox/creds"
PERMITTED_ROOTS=("$SANDBOX_ROOT" "$HOME/cdx-offload" "$CRED_ROOT")
PERMITTED_ROOTS_REAL=()
CRED_ROOTS=("$CRED_ROOT")

EXIT_USAGE=2
EXIT_CONTAINMENT=12

ID=""
MEMORY="${SANDBOX_MEMORY:-12g}"
PIDS="${SANDBOX_PIDS:-512}"
CPUS="${SANDBOX_CPUS:-4}"
CPU_SHARES="${SANDBOX_CPU_SHARES:-256}"
TTY=0
WORKSPACE=""
WORKSPACE_EXTERNAL=0
CMD=()
MOUNT_SOURCES=()
MOUNT_DESTS=()
MOUNT_MODES=()
MOUNT_VOLUMES=()
EGRESS_HOSTS=()
declare -A MOUNT_DEST_SEEN=()

usage() {
  cat >&2 <<'USAGE'
usage: sandbox-run --id <slug> [--workspace <abs-path>] [--mount <source>:<dest>:<ro|rw>]...
                   [--egress-host <host-or-ip>]...
                   [--memory 12g] [--pids 512] [--cpus 4] [--tty] [-- <cmd...>]

--mount may be repeated. Source must already exist; mode is literally ro or rw.
--workspace replaces $SANDBOX_ROOT/workspaces/<id> as the session workspace; it is
mounted at /sandbox/workspaces/<id> so the in-container path never changes.

exit 2:  usage or preflight failure
exit 12: workspace or mount source resolves outside the permitted roots, or a
         source under the credential staging root is not mounted ro
USAGE
  exit "${1:-$EXIT_USAGE}"
}

die_usage() { echo "sandbox-run: $1" >&2; usage; }

# Echoes the resolved path; aborts the whole script when it escapes the roots.
resolve_under_permitted_root() {
  local label="$1" path="$2" resolved root
  if ! resolved="$(realpath -- "$path" 2>/dev/null)"; then
    echo "sandbox-run: $label does not resolve: $path" >&2
    exit "$EXIT_CONTAINMENT"
  fi
  for root in ${PERMITTED_ROOTS_REAL[@]+"${PERMITTED_ROOTS_REAL[@]}"}; do
    if [ "$resolved" = "$root" ] || [[ "$resolved" == "$root"/* ]]; then
      printf '%s\n' "$resolved"
      return 0
    fi
  done
  echo "sandbox-run: $label outside permitted roots: $path -> $resolved" >&2
  exit "$EXIT_CONTAINMENT"
}

# Staged credentials are readable secrets, never a writable surface: a source under
# the credential root aborts the script unless the mount mode is ro. Both the literal
# and the resolved root are checked so a root that stops resolving still refuses.
refuse_writable_credential_source() {
  local label="$1" mode="$2" path root
  shift 2
  if [ "$mode" = ro ]; then return 0; fi
  for path in "$@"; do
    for root in ${CRED_ROOTS[@]+"${CRED_ROOTS[@]}"}; do
      if [ "$path" = "$root" ] || [[ "$path" == "$root"/* ]]; then
        echo "sandbox-run: $label under the credential root must be mounted ro: $path" >&2
        exit "$EXIT_CONTAINMENT"
      fi
    done
  done
  return 0
}

add_mount() {
  local spec="$1" source dest mode extra
  IFS=':' read -r source dest mode extra <<<"$spec"
  [ -n "${source:-}" ] && [ -n "${dest:-}" ] && [ -n "${mode:-}" ] && [ -z "${extra:-}" ] \
    || die_usage "--mount requires <source>:<dest>:<ro|rw>, got: $spec"
  [ "$mode" = ro ] || [ "$mode" = rw ] || die_usage "mount mode must be ro or rw: $mode"
  [[ "$source" = /* ]] || die_usage "mount source must be absolute: $source"
  [ -e "$source" ] || die_usage "mount source must already exist: $source"
  [[ "$dest" = /* ]] || die_usage "mount destination must be absolute: $dest"
  [ "$dest" != / ] || die_usage "mount destination must not be /"
  case "$dest/" in
    */./*|*/../*) die_usage "mount destination must not contain . or ..: $dest" ;;
  esac
  case "$dest" in
    /sandbox|/sandbox-secrets/e2e_key|/sandbox-secrets/e2e_known_hosts)
      die_usage "mount destination is reserved: $dest" ;;
  esac
  [ -z "${MOUNT_DEST_SEEN[$dest]+x}" ] || die_usage "duplicate mount destination: $dest"
  MOUNT_DEST_SEEN[$dest]=1
  MOUNT_SOURCES+=("$source")
  MOUNT_DESTS+=("$dest")
  MOUNT_MODES+=("$mode")
}

while [ $# -gt 0 ]; do
  case "$1" in
    --id)        ID="${2-}"; shift 2 ;;
    --memory)    MEMORY="${2-}"; shift 2 ;;
    --pids)      PIDS="${2-}"; shift 2 ;;
    --cpus)      CPUS="${2-}"; shift 2 ;;
    --workspace) WORKSPACE="${2-}"; WORKSPACE_EXTERNAL=1; shift 2 ;;
    --mount)     add_mount "${2-}"; shift 2 ;;
    --egress-host) EGRESS_HOSTS+=("${2-}"); shift 2 ;;
    --tty)       TTY=1; shift ;;
    --)          shift; CMD=("$@"); break ;;
    -h|--help)   usage 0 ;;
    *)           die_usage "unknown arg: $1" ;;
  esac
done

[[ "$ID" =~ ^[a-z0-9][a-z0-9._-]{0,63}$ ]] || { echo "sandbox-run: --id must match ^[a-z0-9][a-z0-9._-]{0,63}$" >&2; exit 2; }
[ -r "$IMAGE_TAG_FILE" ] || { echo "sandbox-run: no provisioned image; run sandbox-provision first" >&2; exit 2; }
IMAGE="$(cat "$IMAGE_TAG_FILE")"
podman image exists "$IMAGE" || { echo "sandbox-run: image $IMAGE is not present on $(hostname)" >&2; exit 2; }

if [ "$WORKSPACE_EXTERNAL" = 1 ]; then
  [ -n "$WORKSPACE" ] || die_usage "--workspace requires a path"
  [[ "$WORKSPACE" = /* ]] || die_usage "--workspace must be absolute: $WORKSPACE"
else
  WORKSPACE="$SANDBOX_ROOT/workspaces/$ID"
fi

for root in "${PERMITTED_ROOTS[@]}"; do
  resolved="$(realpath -- "$root" 2>/dev/null)" && PERMITTED_ROOTS_REAL+=("$resolved")
done
if resolved="$(realpath -- "$CRED_ROOT" 2>/dev/null)"; then
  [ "$resolved" = "$CRED_ROOT" ] || CRED_ROOTS+=("$resolved")
fi

[ -z "${MOUNT_DEST_SEEN[/sandbox/workspaces/$ID]+x}" ] || die_usage "mount destination is reserved: /sandbox/workspaces/$ID"

# Resolved before mkdir so an escaping workspace is never created, and again after.
ANCESTOR="$WORKSPACE"
while [ ! -e "$ANCESTOR" ] && [ "$ANCESTOR" != / ]; do ANCESTOR="$(dirname "$ANCESTOR")"; done
ANCESTOR_REAL="$(resolve_under_permitted_root "workspace ancestor" "$ANCESTOR")"
# The workspace is bound rw, so the credential root can never host one.
refuse_writable_credential_source workspace rw "$WORKSPACE" "$ANCESTOR_REAL"

mkdir -p "$WORKSPACE" "$SANDBOX_ROOT/home" "$SANDBOX_ROOT/store/pnpm" "$SANDBOX_ROOT/toolgap"
WORKSPACE="$(resolve_under_permitted_root workspace "$WORKSPACE")"
refuse_writable_credential_source workspace rw "$WORKSPACE"
[ -d "$WORKSPACE" ] || { echo "sandbox-run: workspace is not a directory: $WORKSPACE" >&2; exit "$EXIT_CONTAINMENT"; }

for i in ${MOUNT_SOURCES[@]+"${!MOUNT_SOURCES[@]}"}; do
  src="$(resolve_under_permitted_root "mount source" "${MOUNT_SOURCES[$i]}")"
  refuse_writable_credential_source "mount source" "${MOUNT_MODES[$i]}" "${MOUNT_SOURCES[$i]}" "$src"
  MOUNT_VOLUMES+=(--volume "$src:${MOUNT_DESTS[$i]}:${MOUNT_MODES[$i]}")
done
[ "$WORKSPACE_EXTERNAL" = 0 ] || MOUNT_VOLUMES+=(--volume "$WORKSPACE:/sandbox/workspaces/$ID:rw")

KEY="$ROOT/secrets/e2e_key"
KNOWN_HOSTS="$ROOT/secrets/e2e_known_hosts"
[ -r "$KEY" ] && [ -r "$KNOWN_HOSTS" ] || { echo "sandbox-run: e2e channel is not provisioned ($ROOT/secrets)" >&2; exit 2; }
[ -r "$ROOT/e2e-port" ] || { echo "sandbox-run: e2e port not provisioned ($ROOT/e2e-port)" >&2; exit 2; }
E2E_PORT="$(cat "$ROOT/e2e-port")"

# $ROOT is never mounted into the container, so the contained agent cannot forge these.
RUN_DIR="$ROOT/runs/$ID"
mkdir -p "$RUN_DIR"
CIDFILE="$RUN_DIR/cid"
rm -f "$CIDFILE" "$RUN_DIR/inspect.json" "$RUN_DIR/reason"

CONTAINER=""
cleanup_container() {
  if [ -z "$CONTAINER" ] && [ -r "$CIDFILE" ]; then CONTAINER="$(cat "$CIDFILE")"; fi
  if [ -n "$CONTAINER" ]; then podman rm -f "$CONTAINER" >/dev/null 2>&1 || true; fi
  return 0
}
trap cleanup_container EXIT
trap 'exit 130' INT
trap 'exit 143' TERM

RUN_FLAGS=(--replace --interactive --cidfile "$CIDFILE")
[ "$TTY" = 1 ] && RUN_FLAGS+=(--tty)

# Resolve every approved name before podman starts. Do not use systemd's
# IPAddressDeny=/IPAddressAllow= here: a rootless user scope can accept those
# properties without attaching a cgroup BPF program, and Podman's rootless network
# helper may live outside that scope. Both cases silently fail open. The rules below
# are instead installed in the container's own network namespace before the agent
# command starts; failure to install or verify them aborts the container entrypoint.
EGRESS_CONFIG="$ROOT/lib/egress-allowlist.json"
EGRESS_RESOLVER="$ROOT/lib/egress-policy.py"
[ -r "$EGRESS_CONFIG" ] && [ -x "$EGRESS_RESOLVER" ] || {
  echo "sandbox-run: egress policy is not provisioned; refusing launch" >&2; exit "$EXIT_CONTAINMENT";
}
if [ -n "${OD_SANDBOX_EXTRA_EGRESS_HOSTS:-}" ]; then
  read -r -a EXTRA_EGRESS <<<"$OD_SANDBOX_EXTRA_EGRESS_HOSTS"
  printf 'sandbox-run: explicit extra egress requested: %s\n' "${EXTRA_EGRESS[*]}" >&2
  EGRESS_HOSTS+=("${EXTRA_EGRESS[@]}")
fi
EGRESS_ARGS=()
for host in ${EGRESS_HOSTS[@]+"${EGRESS_HOSTS[@]}"}; do
  [[ "$host" =~ ^[A-Za-z0-9.:_-]+$ ]] || { echo "sandbox-run: invalid egress host: $host" >&2; exit "$EXIT_CONTAINMENT"; }
  EGRESS_ARGS+=(--host "$host")
done
# Permit the host-side receiver used by e2e-remote without hard-coding podman's
# gateway. An absent or malformed network record closes the launch.
PODMAN_GATEWAY="$(podman network inspect podman --format '{{range .Subnets}}{{.Gateway}}{{end}}' 2>/dev/null || true)"
[[ "$PODMAN_GATEWAY" =~ ^[0-9a-fA-F:.]+$ ]] || { echo "sandbox-run: podman gateway is unavailable; refusing launch" >&2; exit "$EXIT_CONTAINMENT"; }
EGRESS_ARGS+=(--host "$PODMAN_GATEWAY")
EGRESS_FILE="$RUN_DIR/egress-addresses"
if ! "$EGRESS_RESOLVER" --config "$EGRESS_CONFIG" "${EGRESS_ARGS[@]}" >"$EGRESS_FILE"; then
  echo "sandbox-run: egress allowlist could not be applied; refusing launch" >&2
  exit "$EXIT_CONTAINMENT"
fi
mapfile -t EGRESS_IPS <"$EGRESS_FILE"
[ "${#EGRESS_IPS[@]}" -gt 0 ] || { echo "sandbox-run: empty egress allowlist; refusing launch" >&2; exit "$EXIT_CONTAINMENT"; }
EGRESS_RULES="$RUN_DIR/egress-rules.nft"
{
  printf '%s\n' \
    'table inet overdeck_egress {' \
    '  chain output {' \
    '    type filter hook output priority filter; policy drop;' \
    '    ct state established,related accept' \
    '    oifname "lo" accept'
  for address in "${EGRESS_IPS[@]}"; do
    if [[ "$address" == *:* ]]; then
      printf '    ip6 daddr %s accept\n' "$address"
    else
      printf '    ip daddr %s accept\n' "$address"
    fi
  done
  printf '%s\n' '  }' '}'
} >"$EGRESS_RULES.tmp"
# $RUN_DIR is reused whenever an --id repeats, so the previous run left this file at
# 0444 and a plain redirect onto it fails EACCES — refusing to launch a container whose
# policy is perfectly valid. The 0444 exists to stop the agent rewriting the rules, not
# us: write a fresh temp and rename over it.
mv -f "$EGRESS_RULES.tmp" "$EGRESS_RULES"
chmod 0444 "$EGRESS_RULES"

rc=0
podman run "${RUN_FLAGS[@]}" \
  --name "overdeck-sandbox-$ID" \
  --hostname "sandbox-$ID" \
  --userns=keep-id \
  --cap-drop=ALL \
  --cap-add=NET_ADMIN \
  --cap-add=SETUID \
  --cap-add=SETGID \
  --cap-add=SETPCAP \
  --security-opt=no-new-privileges \
  --pids-limit "$PIDS" \
  --memory "$MEMORY" \
  --memory-swap "$MEMORY" \
  --cpus "$CPUS" \
  --cpu-shares "$CPU_SHARES" \
  --volume "$SANDBOX_ROOT:/sandbox:rw" \
  --volume "$KEY:/sandbox-secrets/e2e_key:ro" \
  --volume "$KNOWN_HOSTS:/sandbox-secrets/e2e_known_hosts:ro" \
  --volume "$EGRESS_RULES:/run/overdeck-egress/rules.nft:ro" \
  ${MOUNT_VOLUMES[@]+"${MOUNT_VOLUMES[@]}"} \
  --workdir "/sandbox/workspaces/$ID" \
  --env "HOME=/sandbox/home" \
  --env "SANDBOX_ID=$ID" \
  --env "SANDBOX_IMAGE=$IMAGE" \
  --env "SANDBOX_E2E_TARGET=$(id -un)@host.containers.internal" \
  --env "SANDBOX_E2E_PORT=$E2E_PORT" \
  --entrypoint /usr/local/bin/sandbox-egress-init \
  "$IMAGE" "${CMD[@]+"${CMD[@]}"}" || rc=$?

[ -r "$CIDFILE" ] && CONTAINER="$(cat "$CIDFILE")" || CONTAINER=""
STATE=""
if [ -n "$CONTAINER" ] && podman inspect "$CONTAINER" >"$RUN_DIR/inspect.json" 2>/dev/null; then
  STATE="$(podman inspect -f 'container_exit={{.State.ExitCode}} oomkilled={{.State.OOMKilled}} error={{.State.Error}}' "$CONTAINER" 2>/dev/null || true)"
fi
if [ -n "$STATE" ]; then
  printf 'exit=%s %s\n' "$rc" "$STATE" >"$RUN_DIR/reason"
else
  printf 'exit=%s inspect=unavailable\n' "$rc" >"$RUN_DIR/reason"
fi

cleanup_container
CONTAINER=""
exit "$rc"
