#!/usr/bin/env bash
# _agent-unsafe.sh — shared body for the deliberate host escape hatches.
# cld-unsafe, cdx-unsafe, ca-unsafe, opencode-unsafe, kiro-cli-unsafe and
# agy-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. Cgroup ancestry identifies humans; agent and dispatch
# scopes are refused.
#
# 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" ;;
    opencode-unsafe) printf '%s' "$HOME/.claude/bin/opencode" ;;
    kiro-cli-unsafe) printf '%s' "$HOME/.claude/bin/kiro-cli" ;;
    agy-unsafe)
      local agy="$HOME/.claude/bin/agy"
      [[ -x "$agy" ]] || return 1
      printf '%s' "$agy"
      ;;
    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 detail runtime

  if ! detail="$(session_is_human)"; then
    unsafe_refuse "$hatch" "$detail" \
      "This call does not classify as an owner's terminal (cgroup ancestry, container signal and controlling TTY decide; env markers never do)." "$@"
  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"
  # Read by _tmpjail-shim.sh: this invocation already has its own unsafe.slice scope, so it
  # must not also queue behind the agent-session concurrency cap.
  export UNSAFE_HATCH_ACTIVE=1

  # The hatch's whole contract is "no restrictions", so the runtime's own permission prompt
  # is off by default rather than something to remember to type.
  if [[ "$hatch" == cld-unsafe && " $* " != *" --dangerously-skip-permissions "* ]]; then
    set -- --dangerously-skip-permissions "$@"
  fi

  # 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
