#!/usr/bin/env bash
# PreToolUse gate — block AGENT-INITIATED background dispatch. OFF by default;
# the background-jobs-blocker hook control turns it on.
# Policy when on: the agent never backgrounds. It runs foreground; the USER presses Ctrl+B
# on the running foreground process to background it. Ctrl+B acts on an already-running
# process, so this gate (which only inspects the tool INPUT) never interferes with it.
# That Ctrl+B is the ONLY approval path. No bypass file, no prompt.
# Scope: Bash run_in_background flag, context-mode background execution, and inline shell backgrounding in Bash (MAIN AGENT only).
# Agent run_in_background is OUT OF SCOPE — subagent dispatch is allowed to background.
# Subagents (transcript_path contains /subagents/) are exempt — all bg forms allowed.
# Out of scope (own opt-in elsewhere): Workflow, ScheduleWakeup, Cron.
#
# HARNESS AUTO-BG (out of scope for this hook — config-controlled only):
#   Two CC-core mechanisms fire AFTER this hook has already passed the foreground input:
#   1. Bash path: onTimeout fires at min(D||BASH_DEFAULT_TIMEOUT_MS, BASH_MAX_TIMEOUT_MS).
#      Mitigated by setting BASH_DEFAULT_TIMEOUT_MS=86400000 in settings.json env block.
#   2. Agent path: qLf() returns 120000ms when server flag tengu_auto_background_agents is ON
#      (no local override; DISABLE_BACKGROUND_TASKS kills Ctrl+B too — not applied).
#   Neither is catchable by a PreToolUse hook.
set -euo pipefail

INPUT=""; IFS= read -rd '' INPUT || true

# Fast path: hook_control_is_enabled only returns true (enabled) when the config
# file parses as valid hook-controls/v1 JSON AND the background-jobs-blocker key
# is literally `true`. Any config file missing/unreadable, or lacking that exact
# key/value pair, resolves to the same disabled exit 0 the full lookup would
# reach — so this skips sourcing hook-control.sh and its jq call entirely for
# the common (disabled) case, without ever exiting 0 on a payload that would
# otherwise be gated.
CONFIG_FILE="${OVERDECK_CONFIG_DIR:-${HOME:-}/.config/overdeck}/hook-controls.json"
CONFIG_CONTENT=""
[[ -r "$CONFIG_FILE" ]] && CONFIG_CONTENT=$(<"$CONFIG_FILE")
case "$CONFIG_CONTENT" in
  *background-jobs-blocker*true*) ;;
  *) exit 0 ;;
esac

_hook_dir="${BASH_SOURCE[0]%/*}"
[[ "$_hook_dir" == "${BASH_SOURCE[0]}" ]] && _hook_dir='.'
HOOK_CONTROL_LIB="${_hook_dir}/lib/hook-control.sh"
if [[ -r "$HOOK_CONTROL_LIB" ]]; then
  # shellcheck source=lib/hook-control.sh
  source "$HOOK_CONTROL_LIB" || hook_control_is_enabled() { [[ "$2" == true ]]; }
else
  hook_control_is_enabled() { [[ "$2" == true ]]; }
fi

if ! hook_control_is_enabled background-jobs-blocker false; then exit 0; fi

deny() {
  local reason=$1
  if jq -n --arg r "$reason" '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "deny",
      permissionDecisionReason: $r
    }
  }' 2>/dev/null; then
    :
  else
    local escaped=${reason//\\/\\\\}
    escaped=${escaped//\"/\\\"}
    printf '%s\n' "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"$escaped\"}}"
  fi
  exit 0
}

PARSE_REASON='bg-gate: unable to parse hook input'
if ! command -v jq >/dev/null 2>&1; then deny "$PARSE_REASON"; fi
parsed=$(printf '%s' "$INPUT" | jq -ec '
  def okstr($v): ($v == null or ($v | type) == "string");
  def okobj($v): ($v == null or ($v | type) == "object");
  def okbool($v): ($v == null or ($v | type) == "boolean");
  if type != "object" then error("parse") else . end |
  if okstr(.tool_name) and okstr(.transcript_path) and okobj(.tool_input)
     and okbool((.tool_input // {}).run_in_background)
     and okbool((.tool_input // {}).background)
     and okstr((.tool_input // {}).command) then
    {
      tool_name: (.tool_name // ""),
      bg: ((.tool_input // {}).run_in_background // false),
      ctx_bg: ((.tool_input // {}).background // false),
      cmd: ((.tool_input // {}).command // ""),
      transcript: (.transcript_path // "")
    }
  else error("parse") end
' 2>/dev/null) || deny "$PARSE_REASON"
TOOL=$(printf '%s' "$parsed" | jq -r '.tool_name')
BG=$(printf '%s' "$parsed" | jq -r '.bg')
CTX_BG=$(printf '%s' "$parsed" | jq -r '.ctx_bg')
CMD=$(printf '%s' "$parsed" | jq -r '.cmd')
TRANSCRIPT=$(printf '%s' "$parsed" | jq -r '.transcript')

# Subagents run inside .../subagents/agent-*.jsonl — exempt from all bg rules.
[[ "$TRANSCRIPT" == */subagents/* ]] && exit 0
# QuietContext owns its background execution lifecycle.
[[ "$TOOL" == mcp__quietcontext__* ]] && exit 0

REASON='bg-gate: agent-initiated background dispatch is blocked. Re-run in FOREGROUND (drop run_in_background / inline &/nohup/setsid/disown). To background it, the USER presses Ctrl+B on the running command — that is the only approval path.'

# 1. Explicit run_in_background flag — Bash only (Agent subagent backgrounding is allowed)
[[ "$TOOL" == "Bash" && "$BG" == "true" ]] && deny "$REASON"

# 2. Context-mode execution backgrounding.
[[ "$TOOL" == "mcp__plugin_context-mode_context-mode__ctx_execute" && "$CTX_BG" == "true" ]] && deny "$REASON"

# 3. Inline shell backgrounding inside a Bash command.
#    - lone & at statement end or before a separator (NOT &&, NOT fd-redirect 2>&1 / &>)
#    - nohup / setsid / disown / coproc keywords
if [[ -n "$CMD" ]]; then
  if printf '%s' "$CMD" | perl -ne '
      exit 0 if /(?<![&>])&(?=[\s;]|$)/;    # lone backgrounding & (end, or before space/;/newline/next cmd) — NOT &&, NOT 2>&1, NOT &>file
      exit 0 if /\b(?:nohup|setsid|disown|coproc)\b/;
      exit 1'; then
    deny "$REASON"
  fi
fi

# Passthrough — foreground command, allowed.
exit 0
