#!/bin/bash
#
# Lane B — deterministic datastore policy grants (docs/MACOS_VM_POLICY_ALTERNATIVE.md).
#
# Runs INSIDE a macOS guest, as root, during a base build's dedicated SIP-OFF boot. It writes the
# in-guest agent's TCC grants directly into the system TCC.db (impossible with SIP on), disables the
# notification SOURCES the base must stay quiet about, and reads everything back so the caller can gate
# on verified state rather than an exit code. It is idempotent and safe to re-run.
#
# The host runs it over the vsock exec channel (no share mount, so it can't trip the very Network
# Volumes prompt it is granting): the whole file is base64'd on the host and piped into `sudo bash`,
# with parameters passed as environment variables — never on a command line an `ps` could read:
#
#   printf %s "<base64>" | base64 --decode \
#     | sudo NUCLEIC_VMAGENT_CSREQ_HEX=<hex> NUCLEIC_REQUIRE_CSREQ=1 bash
#
# Environment (all optional; sane defaults):
#   NUCLEIC_VMAGENT_CSREQ_HEX     hex of the agent's compiled designated-requirement blob (csreq), or
#                                 empty → write csreq NULL rows (still allowed by bundle id on current
#                                 builds, but not pinned to the code signature).
#   NUCLEIC_REQUIRE_CSREQ         1 → a non-NULL csreq is REQUIRED for the run to report OK (set by the
#                                 host only when it actually derived a blob, i.e. a signed build); 0/unset
#                                 → a NULL csreq is acceptable (ad-hoc dev build with no requirement).
#   NUCLEIC_AGENT_USER            the auto-login account (default: agent) — for the per-user launchctl UID.
#   NUCLEIC_POLICY_GRANTS_VERIFY_ONLY  1 → skip all writes; only read back + report (the post-SIP-re-enable
#                                 survival check).
#
# Machine-readable markers on stdout (the host parses these, never the exit code):
#   SIP=<0|1>                     0 = SIP enabled, 1 = disabled (as csrutil reports it this boot)
#   GRANT <service> auth=<n> csreq=<0|1>   one per expected service
#   NOTIF <label>=<disabled|present|error> one per notification source
#   POLICY_GRANTS_OK  /  POLICY_GRANTS_FAIL: <reason>

set -u

BUNDLE_ID="xyz.blakeslee.nucleic.vmagent"
TCC_DB="/Library/Application Support/com.apple.TCC/TCC.db"
AGENT_USER="${NUCLEIC_AGENT_USER:-agent}"
CSREQ_HEX="${NUCLEIC_VMAGENT_CSREQ_HEX:-}"
REQUIRE_CSREQ="${NUCLEIC_REQUIRE_CSREQ:-0}"
VERIFY_ONLY="${NUCLEIC_POLICY_GRANTS_VERIFY_ONLY:-0}"

# The full set Lane B grants the agent. FDA + Network Volumes remove the "Data Access Blocked" banner
# and the virtioFS custom-mount prompt (the "mac_vm_exec never returns" hang); Screen Recording is the
# one grant NO MDM PPPC payload can ever deliver silently; Accessibility + PostEvent are the semantic
# AX agent's control + input. All keyed by BUNDLE ID (client_type=0) with the agent's csreq blob.
SERVICES=(
  kTCCServiceSystemPolicyAllFiles
  kTCCServiceSystemPolicyNetworkVolumes
  kTCCServiceScreenCapture
  kTCCServiceAccessibility
  kTCCServicePostEvent
)

# SIP as this boot sees it. Reported for the transcript; the write below simply fails (and is caught by
# the read-back) if SIP is actually on, so this is diagnostic, not a gate.
if csrutil status 2>/dev/null | grep -qi disabled; then echo "SIP=1"; else echo "SIP=0"; fi

# ── Writes (skipped in verify-only) ───────────────────────────────────────────────────────────────
if [ "$VERIFY_ONLY" != "1" ]; then
  # tccd holds the db open; stop it so the direct write can't race its cache, then it relaunches and
  # reloads on next access. Both the system daemon and any per-user instance.
  killall tccd 2>/dev/null || true

  # Compile the csreq literal once: X'<hex>' when the host supplied a blob, else NULL.
  if [ -n "$CSREQ_HEX" ]; then CSREQ_SQL="X'${CSREQ_HEX}'"; else CSREQ_SQL="NULL"; fi

  # Columns are ALWAYS named: macOS keeps adding columns to `access` across releases, so a positional
  # INSERT breaks on the next OS. INSERT OR REPLACE so a re-run refreshes rather than duplicates.
  #   client_type = 0  → client identified by BUNDLE ID
  #   auth_value  = 2  → ALLOWED (0 denied, 1 unknown, 2 allowed, 3 limited)
  #   auth_reason = 3  → a plausible provenance for a pre-grant
  #   auth_version= 1  → current row schema version
  for svc in "${SERVICES[@]}"; do
    sqlite3 "$TCC_DB" \
      "INSERT OR REPLACE INTO access
       (service,client,client_type,auth_value,auth_reason,auth_version,csreq,policy_id,indirect_object_identifier_type,indirect_object_identifier,indirect_object_code_identity,flags,last_modified)
       VALUES('$svc','$BUNDLE_ID',0,2,3,1,$CSREQ_SQL,NULL,0,'UNUSED',NULL,0,strftime('%s','now'));" \
      2>/dev/null || echo "  (write of $svc reported an error — the read-back below is authoritative)" >&2
  done
  killall tccd 2>/dev/null || true

  # ── Notification sources (disable the SOURCE, not the alert — docs/MACOS_VM_POLICY_ALTERNATIVE.md).
  # `launchctl disable` writes a PERSISTENT override into the base image, so every clone inherits it.
  # Tips posts "Learn how to take a screenshot"; BTMNotificationAgent posts "Multiple Extensions
  # Added". Disabling these two agents stops those banners at the source without touching the
  # notification framework a tester's own app may use, and without the version-fragile ncprefs format.
  AGENT_UID="$(id -u "$AGENT_USER" 2>/dev/null || echo 0)"
  for label in com.apple.tipsd com.apple.BTMNotificationAgent; do
    launchctl disable "gui/$AGENT_UID/$label" 2>/dev/null || true
    launchctl bootout "gui/$AGENT_UID/$label" 2>/dev/null || true
  done
fi

# ── Read-back (always) — the authoritative verification the host gates on ─────────────────────────
ALL_OK=1
FAIL_REASON=""
for svc in "${SERVICES[@]}"; do
  auth="$(sqlite3 "$TCC_DB" \
    "SELECT auth_value FROM access WHERE service='$svc' AND client='$BUNDLE_ID' AND client_type=0;" \
    2>/dev/null)"
  hascsreq="$(sqlite3 "$TCC_DB" \
    "SELECT (csreq IS NOT NULL) FROM access WHERE service='$svc' AND client='$BUNDLE_ID' AND client_type=0;" \
    2>/dev/null)"
  auth="${auth:-none}"
  hascsreq="${hascsreq:-0}"
  echo "GRANT $svc auth=$auth csreq=$hascsreq"
  if [ "$auth" != "2" ]; then
    ALL_OK=0
    FAIL_REASON="${FAIL_REASON} $svc(auth=$auth)"
  elif [ "$REQUIRE_CSREQ" = "1" ] && [ "$hascsreq" != "1" ]; then
    ALL_OK=0
    FAIL_REASON="${FAIL_REASON} $svc(csreq-null)"
  fi
done

# Notification read-back (never gates — reported for the transcript only).
if [ "$VERIFY_ONLY" != "1" ]; then
  AGENT_UID="$(id -u "$AGENT_USER" 2>/dev/null || echo 0)"
  disabled_list="$(launchctl print-disabled "gui/$AGENT_UID" 2>/dev/null || true)"
  for label in com.apple.tipsd com.apple.BTMNotificationAgent; do
    if printf '%s' "$disabled_list" | grep -q "\"$label\" => \(disabled\|true\)"; then
      echo "NOTIF $label=disabled"
    else
      echo "NOTIF $label=present"
    fi
  done
fi

if [ "$ALL_OK" = "1" ]; then
  echo "POLICY_GRANTS_OK"
else
  echo "POLICY_GRANTS_FAIL:${FAIL_REASON}"
fi
exit 0
