#!/usr/bin/env bash
# _agent-unsafe.sh — shared body for the deliberate host escape hatches.
# cld-unsafe, cdx-unsafe and ca-unsafe are symlinks to this file; the basename
# selects the runtime, exactly like _cpu-guard-shim.sh and _tmpjail-shim.sh.
#
# These hatches run an agent runtime ON THE HOST with no capability restriction,
# so the sandbox that will contain normal agent execution can never lock the
# human out of their own machine. They are for a HUMAN AT A TTY ONLY: an agent
# that can invoke one has escaped the sandbox, so every gate below fails closed.
#
# Blast radius, not privilege, is bounded: each invocation gets its own transient
# scope inside unsafe.slice (MemorySwapMax=0, MemoryMax, TasksMax, low CPUWeight,
# no CPUQuota), so one runaway invocation can be killed atomically without
# touching the desktop, other terminals, or another invocation.
set -uo pipefail

UNSAFE_LOG_DIR="$HOME/.local/state/agent-unsafe"
UNSAFE_LOG="$UNSAFE_LOG_DIR/invocations.log"
UNSAFE_SLICE="unsafe.slice"

UNSAFE_SELF_DIR="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)"
# shellcheck source=../lib/session-class.sh
source "$UNSAFE_SELF_DIR/../lib/session-class.sh"

unsafe_log() { # $1=hatch $2=outcome $3...=argv
  local hatch="$1" outcome="$2"
  shift 2
  local quoted="" a
  for a in "$@"; do quoted+="$(printf '%q ' "$a")"; done
  mkdir -p "$UNSAFE_LOG_DIR" 2>/dev/null || return 1
  printf '%s\t%s\t%s\t%s\n' \
    "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$hatch" "$outcome" "${quoted% }" >>"$UNSAFE_LOG"
}

unsafe_refuse() { # $1=hatch $2=reason $3=human detail $4...=argv
  local hatch="$1" reason="$2" detail="$3"
  shift 3
  unsafe_log "$hatch" "refused:$reason" "$@"
  printf '%s: REFUSED (%s)\n%s\n' "$hatch" "$reason" "$detail" >&2
  printf 'These hatches run an agent on the host with no restrictions and are for a human at a terminal only. There is no override flag.\n' >&2
  exit 77
}

unsafe_resolve_runtime() { # $1=hatch → prints the command to exec
  case "$1" in
    cld-unsafe) printf '%s' "$HOME/.claude/bin/claude" ;;
    cdx-unsafe) printf '%s' "$HOME/.claude/bin/codex" ;;
    ca-unsafe)
      local cur engine
      cur="$(cat "$HOME/.harness/engine/CURRENT" 2>/dev/null)" || return 1
      [[ -n "$cur" ]] || return 1
      engine="$HOME/.harness/engine/versions/$cur/wrappers/ca.sh"
      [[ -x "$engine" ]] || return 1
      printf '%s' "$engine"
      ;;
    *) return 1 ;;
  esac
}

# systemd-run silently invents an unconfigured slice when the unit file is absent,
# which would place the invocation in an unbounded cgroup.
unsafe_slice_ready() {
  [[ "$(systemctl --user show "$UNSAFE_SLICE" -p MemorySwapMax --value 2>/dev/null)" == "0" ]]
}

unsafe_main() {
  local hatch="$1"
  shift
  local marker detail runtime

  if marker="$(session_agent_marker)"; then
    unsafe_refuse "$hatch" "agent-env-marker:$marker" \
      "\$$marker is set, so this call comes from inside an agent runtime, not from a human shell." "$@"
  fi

  if detail="$(session_container_signal)"; then
    unsafe_refuse "$hatch" "in-container" \
      "Container marker present ($detail). The hatch exists to run OUTSIDE the sandbox." "$@"
  fi

  if [[ ! -t 0 ]]; then
    unsafe_refuse "$hatch" "no-tty" \
      "stdin is not a TTY. A process shelling out has no controlling terminal; a human typing this does." "$@"
  fi

  if ! runtime="$(unsafe_resolve_runtime "$hatch")"; then
    unsafe_refuse "$hatch" "runtime-unresolved" \
      "Cannot resolve the target runtime for $hatch on this machine." "$@"
  fi

  if ! unsafe_slice_ready; then
    unsafe_refuse "$hatch" "slice-unconfigured" \
      "$UNSAFE_SLICE is missing or its MemorySwapMax is not 0; refusing to run uncontained. Install modules/monitor/systemd/user/unsafe.slice and run: systemctl --user daemon-reload" "$@"
  fi

  unsafe_log "$hatch" allowed "$@" ||
    unsafe_refuse "$hatch" "audit-log-unwritable" "Cannot append to $UNSAFE_LOG." "$@"

  # Keeps ~/.claude/bin (the cpu-guard shims for node/pnpm/cargo/...) ahead of the
  # real toolchain for everything the session later runs.
  export PATH="$HOME/.claude/bin:$PATH"
  # Marks the process tree as an agent tree so the git main-checkout guard stays live.
  export AGENT_BUILD_SCOPE_ACTIVE=1
  # Declares the hatch exempt from the /tmp jail and from agent-class re-confinement:
  # re-confinement would move the process into agent.slice and out of its own scope.
  export TMPJAIL_ACTIVE=1
  export CONFINE_ACTIVE="agent:cgroup"

  # systemd-run expands $VAR / ${VAR} in the command line it is given; "$$" is its
  # escape for a literal "$". Without this an agent prompt containing a dollar sign
  # reaches the runtime silently truncated.
  local -a exec_argv=()
  local a
  for a in "$runtime" "$@"; do exec_argv+=("${a//\$/\$\$}"); done

  exec systemd-run --user --scope --quiet --collect \
    --slice="$UNSAFE_SLICE" \
    --unit="unsafe-${hatch%-unsafe}-$(date +%s%N)-$$" \
    --property=OOMPolicy=continue \
    -- "${exec_argv[@]}"
}

# Sourcing this file (the test harness does) must not run the gate.
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
  unsafe_main "$(basename -- "$0")" "$@"
fi
