#!/usr/bin/env bash
# preference-consult-gate — PreToolUse gate on customer-facing document deliverables.
#
# A customer document (a PDF/file to the customer documents scope, or an email/
# Outlook send carrying an attachment) must not go out until the account's saved
# layer-2 preferences were consulted this turn, proven by a profile-read tool
# call after the last user message in the session transcript. Layer-2 records
# hold the signature policy, header, naming and styling rules the fixed layer-1
# block does not; a send with no profile-read this turn is the miss that dropped
# a customer signature.
#
# Scope (document-deliverable shape only; every other tool/shape exits 0):
#   *browser-pdf-save  path under memory/users/<phone>/documents/
#   SendUserFile       path under memory/users/<phone>/documents/ or .pdf/.html/.docx
#   *email-send|reply|draft-send / *outlook-mail-send|reply|draft-send  non-empty attachments
#
# Exit: 0 allow, 2 block. Fail OPEN on tty/empty stdin/no python3/transcript
# unreadable. Block log: [preference-gate] op=bypass tool=<name> detail=no profile-read this turn
set -uo pipefail

if [ -t 0 ]; then exit 0; fi
INPUT=$(cat)
[ -z "$INPUT" ] && exit 0
command -v python3 >/dev/null 2>&1 || exit 0

DECISION=$(INPUT="$INPUT" python3 - <<'PY' 2>/dev/null || true
import os, json, re, sys
try:
    d = json.loads(os.environ["INPUT"])
except Exception:
    sys.exit(0)
tool = d.get("tool_name", "") or ""
ti = d.get("tool_input", {}) or {}

def under_customer_docs(p):
    return bool(re.search(r'(^|/)memory/users/[^/]+/documents/', os.path.normpath(p or "")))

gated = False
if tool.endswith("browser-pdf-save"):
    gated = under_customer_docs(ti.get("path", ""))
elif tool == "SendUserFile" or tool.endswith("__SendUserFile"):
    p = ti.get("path", "") or ti.get("file_path", "")
    gated = under_customer_docs(p) or p.lower().endswith((".pdf", ".html", ".docx"))
elif re.search(r'(email-send|email-reply|email-draft-send|outlook-mail-send|outlook-mail-reply|outlook-draft-send)$', tool):
    atts = ti.get("attachments")
    gated = isinstance(atts, list) and len(atts) > 0

if not gated:
    print("")  # not a document deliverable
    sys.exit(0)

transcript = d.get("transcript_path", "") or ""
if not transcript or not os.access(transcript, os.R_OK):
    print("")  # cannot inspect consultation -> fail open
    sys.exit(0)

try:
    with open(transcript) as f:
        lines = f.readlines()
except Exception:
    print("")
    sys.exit(0)

def is_genuine_user_turn(e):
    is_user = (e.get("type") == "user") or (e.get("role") == "user") or (isinstance(e.get("message"), dict) and e["message"].get("role") == "user")
    if not is_user:
        return False
    msg = e.get("message", e)
    content = msg.get("content") if isinstance(msg, dict) else None
    if isinstance(content, str):
        return True
    if isinstance(content, list):
        # A tool_result delivery is recorded as a user entry whose content is
        # composed only of tool_result blocks. It is not a human turn; counting
        # it would push the boundary past the profile-read that ran this turn and
        # false-block a compliant send. A genuine human turn carries at least one
        # non-tool_result block (text/image).
        return any(not (isinstance(b, dict) and b.get("type") == "tool_result") for b in content)
    # Unknown shape: do not advance the boundary, biasing toward a wider consulted
    # window (toward allow) — fail-open is the correct direction here.
    return False

last_user = -1
for i, ln in enumerate(lines):
    try:
        e = json.loads(ln)
    except Exception:
        continue
    if is_genuine_user_turn(e):
        last_user = i

consulted = False
for ln in (lines[last_user + 1:] if last_user >= 0 else lines):
    try:
        e = json.loads(ln)
    except Exception:
        continue
    msg = e.get("message", e)
    content = msg.get("content") if isinstance(msg, dict) else None
    if isinstance(content, list):
        for block in content:
            if isinstance(block, dict) and block.get("type") == "tool_use" and (block.get("name", "") or "").endswith("profile-read"):
                consulted = True

print("%s|%s" % (tool, "yes" if consulted else "no"))
PY
)

[ -z "$DECISION" ] && exit 0
TOOL="${DECISION%%|*}"
CONSULTED="${DECISION##*|}"

if [ "$CONSULTED" = "yes" ]; then
  echo "[preference-gate] op=allow tool=$TOOL consulted=true" >&2
  exit 0
fi

echo "[preference-gate] op=bypass tool=$TOOL detail=no profile-read this turn" >&2
echo "Blocked: this customer document is about to go out without the account's saved preferences being checked this turn. Those records hold the signature, header, naming and styling rules. Run profile-read for this account, apply anything relevant, then send again." >&2
exit 2
