#!/usr/bin/env bash
# AskUserQuestion native-channel carrier gate.
#
# Blocks `AskUserQuestion` when the session has a native channel attached. The
# Claude Code channel protocol carries exactly two interactive methods
# (permission_request / permission); there is NO carrier for the AskUserQuestion
# elicitation. On a native-channel (rc-spawn) webchat/WhatsApp/Telegram session
# the harness blocks the turn awaiting a selection that can never arrive: the
# reader renders only delivered replies, never an elicitation card, so the turn
# wedges — the operator sees the last delivered reply and a live-looking but
# dead composer (see .tasks/1552, live SiteDesk incident session 1a085718
# 2026-07-11). This hook removes that wedge deterministically by steering the
# agent to ask the same question as prose via its channel reply tool, which
# rides the ordinary delivered-reply path that works on every surface.
#
# This is a SEPARATE concern from askuserquestion-investigate-gate.sh (which
# blocks a fabricated menu raised before any investigation). Both are registered
# as commands on the same PreToolUse AskUserQuestion matcher; either exit 2
# blocks. This hook guards the "surface can't render this" condition; the other
# guards the "no evidence yet" condition.
#
# Signal: the rc-spawn writes the channel MCP registration as a config file in
# os.tmpdir() keyed by session id (channel-mcp.ts / webchat-channel-mcp.ts /
# wa-channel-mcp.ts / telegram-channel-mcp.ts). Its presence at tool-call time
# is the authoritative "channel attached to this session" marker: a Pi channel
# process sees the file (block); a claude.ai/code process on another host never
# sees the Pi tmpfile (fail-open, allow — that surface can answer the card).
#
# Contract:
#   Input:  PreToolUse stdin JSON envelope from Claude Code
#   Output: exit 0 (allow) or exit 2 (block) per hook protocol
#   Logs:   one stderr line per decisive call, format
#           [ask-channel] decision=<allow|block> sessionId=<id8>
#                         channel=<attached|none>
#                         reason=<channel-attached|no-channel|fail-open-no-envelope|fail-open-no-session>
#
# Fail-open philosophy: any missing/blank marker (no channel file, no session
# id, no envelope) nudges the agent through, never bricks the UI. Block is
# reserved for the deterministic case "AskUserQuestion fired while a channel
# config file for this session exists in tmpdir".

set -uo pipefail

# ----- Read stdin, identify event ------------------------------------------
if [ -t 0 ]; then
  # No envelope — cannot enforce. Fail open per nudge-not-brick rule.
  echo "[ask-channel] decision=allow sessionId=- channel=none reason=fail-open-no-envelope" >&2
  exit 0
fi
INPUT=$(cat)

if [ -z "$INPUT" ]; then
  echo "[ask-channel] decision=allow sessionId=- channel=none reason=fail-open-no-envelope" >&2
  exit 0
fi

HOOK_EVENT=$(printf '%s' "$INPUT" | python3 -c '
import sys, json
try:
    d = json.load(sys.stdin)
    print(d.get("hook_event_name", "") or "")
except Exception:
    print("")
' 2>/dev/null)

TOOL_NAME=$(printf '%s' "$INPUT" | python3 -c '
import sys, json
try:
    d = json.load(sys.stdin)
    print(d.get("tool_name", "") or "")
except Exception:
    print("")
' 2>/dev/null)

# Only applies to PreToolUse on AskUserQuestion. Anything else: silent exit.
if [ "$HOOK_EVENT" != "PreToolUse" ] || [ "$TOOL_NAME" != "AskUserQuestion" ]; then
  exit 0
fi

SESSION_ID=$(printf '%s' "$INPUT" | python3 -c '
import sys, json
try:
    d = json.load(sys.stdin)
    print(d.get("session_id", "") or "")
except Exception:
    print("")
' 2>/dev/null)
SESSION_ID8="${SESSION_ID:0:8}"
if [ -z "$SESSION_ID8" ]; then SESSION_ID8="-"; fi

if [ -z "$SESSION_ID" ]; then
  echo "[ask-channel] decision=allow sessionId=- channel=none reason=fail-open-no-session" >&2
  exit 0
fi

# ----- Resolve the tmpdir the writers used ---------------------------------
# The channel MCP config path is join(os.tmpdir(), ...). Node's os.tmpdir() on
# POSIX is `process.env.TMPDIR` with a trailing separator stripped, else '/tmp'.
# Reproduce that exactly so the lookup lands on the same directory the rc-spawn
# wrote to. `ASK_CHANNEL_TMPDIR` overrides it for tests only — never set in
# production; the SDK-spawned child inherits the manager's TMPDIR.
if [ -n "${ASK_CHANNEL_TMPDIR:-}" ]; then
  TMP_DIR="$ASK_CHANNEL_TMPDIR"
else
  # Mirror Node's os.tmpdir() (libuv uv_os_tmpdir): TMPDIR, then TMP, then TEMP,
  # then /tmp; a trailing slash is stripped. The lookup must land on the exact
  # directory the rc-spawn writer used, so this resolution order matches the
  # writer's byte for byte. On the Pi (systemd user service) and macOS (launchd)
  # only TMPDIR is ever set, but honouring the full order removes a latent
  # false-allow if TMP/TEMP alone are present.
  TMP_DIR="${TMPDIR:-${TMP:-${TEMP:-/tmp}}}"
  TMP_DIR="${TMP_DIR%/}"
fi

# Sanitize the session id the same way the writers do:
# `sessionId.replace(/[^A-Za-z0-9_-]/g, '_')`.
SID_SAN=$(printf '%s' "$SESSION_ID" | sed 's/[^A-Za-z0-9_-]/_/g')

# ----- Channel attached? ---------------------------------------------------
# Any of the four channel MCP config filename shapes present in tmpdir for this
# session means a channel is bound (unified, webchat, whatsapp, or telegram).
CHANNEL_ATTACHED=0
for kind in channel webchat-channel wa-channel telegram-channel; do
  if [ -e "$TMP_DIR/maxy-${kind}-${SID_SAN}.json" ]; then
    CHANNEL_ATTACHED=1
    break
  fi
done

if [ "$CHANNEL_ATTACHED" -eq 1 ]; then
  echo "[ask-channel] decision=block sessionId=${SESSION_ID8} channel=attached reason=channel-attached" >&2
  echo "Blocked: this is a native-channel session and AskUserQuestion cannot be delivered over a channel (the channel protocol carries no elicitation card, so the turn would wedge). Ask the same question as prose in your channel reply tool and wait for the operator's next message." >&2
  exit 2
fi

echo "[ask-channel] decision=allow sessionId=${SESSION_ID8} channel=none reason=no-channel" >&2
exit 0
