#!/usr/bin/env bash
# Session-start coordination brief (FR-5) — OPT-IN, default OFF.
#
# Runs `mx-agent coordination brief` at session start and forwards its output
# (the fenced cross-team coordination brief) into the session context. This is
# DISABLED by default so no team's session behavior changes before the Gate 3
# migration cutover. Enable per worktree with:
#
#   mx-agent config set coordination-brief-enabled true
#
# Token distribution (dev-cluster only; fleet-wide-prod is separately gated on
# per-worktree Azure identities). Each team's per-team bearer token is issued by
# platform and stored in Azure Key Vault mx-dev-eastus2-kv-001 as secret
# `coordination-token-<team>`. This hook derives NOTHING: it reads an
# authoritative, explicitly-assigned team slug from the worktree marker file and
# fetches that team's token exactly once, caching it per-worktree.
#
# Fail policy:
#   - Fail-CLOSED (skip the brief, exit 0) for: missing marker, unknown/invalid
#     slug, or any team-identity ambiguity. Never fetch a default token.
#   - Fail-OPEN (skip the brief, exit 0) for: az not logged in, KV unreachable,
#     no token, brief hang/timeout/error. A coordination hiccup must NEVER block
#     session start.
# Both cases exit 0. The distinction is intent: fail-closed refuses to act on an
# untrustworthy identity; fail-open tolerates infra flakiness.
#
# LOUD EXCEPTIONS to silent fail-open (KI-COORDCLI-SF1-SILENT-DISARM cousins).
# Three failures are announced on stderr because each is indistinguishable from
# an empty queue when it is silent, and each has a different remedy:
#   1. TIMEOUT (exit 124, #4846 SF-25) — drain did not complete; queue state unknown.
#   2. CLI TOO OLD — the installed global mx-agent predates the `coordination`
#      command; one actionable line telling the operator to update.
#   3. TOKEN REJECTED (HTTP 401, #4970) — the service refused this worktree's
#      token, so nothing was drained. Before this branch existed a 401 was
#      discarded and the hook exited 0 with no output, which after a token
#      rotation is a fleet-wide silent coordination outage with no self-heal.
# All THREE still exit 0 — a coordination failure must not block session start.
# All OTHER brief failures stay silently fail-open as before.
# On SUCCESS, any CLI warnings (the SF-1 wrong-team interlock
# and the INFO-1 "service reported no team" note — both carry team slugs ONLY,
# never tokens or message bodies) are forwarded for visibility.
#
# The brief itself is provenance-fenced (MF-1) and confirms its batch only after
# a successful render. The SF-1 interlock (MX_COORD_EXPECT_TEAM) makes the CLI
# skip confirm if the service-reported team != the marker slug — defense in depth
# against a wrong-token accident.

set -euo pipefail
# NB: `set -x` is deliberately NOT enabled. A trace would echo the fetched token.

# ── Opt-in gate: only run when explicitly enabled. ───────────────────────────
ENABLED="$(mx-agent config get coordination-brief-enabled 2>/dev/null || echo '')"
[ "$ENABLED" = "true" ] || exit 0

# ── Non-secret constants ─────────────────────────────────────────────────────
COORD_URL_DEFAULT="https://mxagent.dev.memnexus.ai"
KEYVAULT_NAME="mx-dev-eastus2-kv-001"
# Minimum global mx-agent CLI that has the `coordination` command. Below this the
# brief cannot run; the hook says so out loud instead of fail-opening silently.
MIN_CLI_VERSION="0.1.172"
CLI_UPDATE_CMD="npm install -g @memnexus-ai/mx-agent-cli@latest"

# MULTI-TENANT (Phase 1). Identity is (project, team). `project` is the committed
# project-config signal (resolveProductSlug precedence: MX_PRODUCT_SLUG →
# mx-agent.config.json projectSlug → memnexus). `team` is the marker slug. The
# roster is resolved from the OFFLINE generated file (ND-2) — NEVER a live
# GET /roster at session start, so a coordination-service outage (the shared
# SPOF) can't break session start. For the DEFAULT project (memnexus) behavior is
# IDENTICAL to before: same roster membership, same token (via the legacy KV
# name fallback below).
DEFAULT_PROJECT="memnexus"
# BUILTIN memnexus roster — OFFLINE fallback used only when the generated file is
# unreadable (or node is unavailable). Mirrors PROVISIONED_ROSTERS in
# coordination-service/src/config/rosters.ts.
BUILTIN_MEMNEXUS_ROSTER="platform retrieval mcp pipeline marketing customer-portal eval product"

# ── MF-2: authoritative marker, hook NEVER derives the slug. ─────────────────
# The slug is an explicit assignment written into the worktree by an onboarding
# step (the provisioning runbook instructs each team to write its own marker;
# platform can't write other worktrees). We do NOT fall back to CLAUDE_TEAM_NAME
# or the worktree name — those are the unreliable heuristics this design rejects.
WORKTREE="${CLAUDE_WORKTREE_PATH:-}"
if [ -z "$WORKTREE" ]; then
  # No worktree path → cannot locate the marker → fail-closed.
  exit 0
fi
MARKER="${WORKTREE}/.mx-coordination-team"
if [ ! -f "$MARKER" ]; then
  # Missing marker → fail-closed (no derivation, no default).
  exit 0
fi
# Read first line, strip surrounding whitespace. Treat the file as immutable input.
SLUG="$(head -n1 "$MARKER" 2>/dev/null | tr -d '[:space:]')"
if [ -z "$SLUG" ]; then
  exit 0
fi

# ── Resolve the PROJECT (resolveProductSlug precedence, offline). ────────────
# 1) MX_PRODUCT_SLUG env; 2) mx-agent.config.json projectSlug at the worktree
# root; 3) default memnexus. No HTTP — reads local config only.
PROJECT="${MX_PRODUCT_SLUG:-}"
if [ -z "$PROJECT" ] && [ -f "${WORKTREE}/mx-agent.config.json" ]; then
  PROJECT="$(node -e 'try{const c=require(process.argv[1]);const s=(c.projectSlug||(c.project&&c.project.slug)||"");process.stdout.write(String(s).trim())}catch(e){}' "${WORKTREE}/mx-agent.config.json" 2>/dev/null || true)"
fi
# Unset/empty → default memnexus (unchanged behavior). A set-but-INVALID slug is
# NEVER coerced to memnexus — that would silently run the wrong tenant's brief.
[ -z "$PROJECT" ] && PROJECT="$DEFAULT_PROJECT"

# ── KI-COORDMT-HOOK-EXPORT-ROBUSTNESS: validate the resolved slug fail-closed. ─
# Mirror coordination-service isValidSlug (types.ts): base regex
# ^[a-z][a-z0-9-]{0,48}[a-z0-9]$, NO '--' substring (KV name parse-safety), and
# NOT the reserved word 'all'. A resolved-but-invalid PROJECT skips the brief with
# a one-line stderr warning (fail-closed) — it is never coerced to a default and
# never flows into the az secret-name / roster lookup below. "$PROJECT" is quoted.
is_valid_slug() {
  local s="$1"
  case "$s" in
    *--*) return 1 ;;      # no '--' (double-hyphen is the KV name separator)
    all) return 1 ;;       # reserved broadcast slug
  esac
  # Base regex, WHOLE-STRING: bash `[[ =~ ]]` anchors ^/$ to the string ends, so a
  # multiline value (e.g. $'memnexus\nevil') fails — unlike `grep -Eq`, which
  # matches line-by-line and would accept an embedded newline. Starts lowercase
  # alpha, then [a-z0-9-], ends alnum; 2..50 chars.
  [[ "$s" =~ ^[a-z][a-z0-9-]{0,48}[a-z0-9]$ ]]
}
if ! is_valid_slug "$PROJECT"; then
  echo "coordination brief: resolved project slug \"$PROJECT\" is invalid — skipping (fail-closed)" >&2
  exit 0
fi

# ── SF-2: per-project roster-membership validation before any az call. ───────
# The roster is read from the OFFLINE generated file (ND-2), with the builtin
# memnexus roster as an offline fallback. NEVER a live GET /roster.
ROSTER_FILE="${MX_COORD_ROSTER_FILE:-${WORKTREE}/mx-agent-system/agent-config/coordination-rosters.generated.json}"
ROSTER_TEAMS=""
if [ -f "$ROSTER_FILE" ]; then
  ROSTER_TEAMS="$(node -e 'try{const r=require(process.argv[1]);const t=(r.projects&&r.projects[process.argv[2]])||[];process.stdout.write(t.join(" "))}catch(e){}' "$ROSTER_FILE" "$PROJECT" 2>/dev/null || true)"
fi
if [ -z "$ROSTER_TEAMS" ] && [ "$PROJECT" = "$DEFAULT_PROJECT" ]; then
  # Offline fallback: file missing/unparseable/node absent → builtin memnexus roster.
  ROSTER_TEAMS="$BUILTIN_MEMNEXUS_ROSTER"
fi
in_roster=""
for t in $ROSTER_TEAMS; do
  if [ "$SLUG" = "$t" ]; then in_roster="yes"; break; fi
done
if [ -z "$in_roster" ]; then
  # Unknown/invalid (project, team) → fail-closed. Never fetch a default token.
  echo "coordination brief: (project \"$PROJECT\", team \"$SLUG\") not in roster — skipping" >&2
  exit 0
fi

# ── MF-1: (project, team)-bound per-worktree token cache. ────────────────────
# The cache filename embeds BOTH slugs, so a worktree recycled to another
# team/project can NEVER reuse the prior tenant's cached token.
CACHE="${WORKTREE}/.mx-coordination-token--${PROJECT}--${SLUG}"
TOKEN=""
if [ -s "$CACHE" ]; then
  TOKEN="$(head -n1 "$CACHE" 2>/dev/null || true)"
fi

# ── Fetch from Key Vault once if not cached (SF-3 born-0600 write). ───────────
if [ -z "$TOKEN" ]; then
  # Fail-OPEN on any fetch problem (not logged in, no access, missing secret).
  # PROJECT/SLUG are roster-validated above; each is a single quoted argv element.
  # ND-1: primary name is the parse-safe double-hyphen coordination-token--<p>--<t>.
  fetched="$(az keyvault secret show \
    --vault-name "$KEYVAULT_NAME" \
    --name "coordination-token--${PROJECT}--${SLUG}" \
    --query value -o tsv 2>/dev/null || true)"
  # ZERO-ORPHAN dual-read (memnexus only): the un-migrated live vault still holds
  # the legacy single-hyphen secret. Fall back to it so memnexus session-start is
  # identical to today. Phase 2 renames the secret and removes this fallback.
  if [ -z "$fetched" ] && [ "$PROJECT" = "$DEFAULT_PROJECT" ]; then
    fetched="$(az keyvault secret show \
      --vault-name "$KEYVAULT_NAME" \
      --name "coordination-token-${SLUG}" \
      --query value -o tsv 2>/dev/null || true)"
  fi
  if [ -z "$fetched" ]; then
    # No token available — tolerate and skip (fail-open).
    exit 0
  fi
  # SF-3: create the cache born 0600 (umask BEFORE creation — no 0644 window).
  ( umask 077; printf '%s' "$fetched" > "$CACHE" ) 2>/dev/null || true
  TOKEN="$fetched"
fi

if [ -z "$TOKEN" ]; then
  exit 0
fi

# ── Run the brief with the token injected via env (never via shared config). ─
# The CLI reads MX_COORD_SERVICE_URL / MX_COORD_SERVICE_TOKEN in preference to
# the global ~/.mx-agent/config.json, so we avoid the cross-worktree clobber that
# `mx-agent config set` would cause (the config store is a single shared file).
# MX_COORD_EXPECT_TEAM arms the SF-1 interlock in runBrief.
# Capture the brief's stderr so we can distinguish a CLI-too-old failure (which
# gets a visible one-line note) from every other failure (silent fail-open), and
# so success-path CLI warnings can be surfaced. stdout still flows straight into
# the session context — only stderr is intercepted.
#
# INFO-2: mktemp gives an unpredictable, private path. Do NOT fall back to a
# predictable /tmp name ($TMPDIR/...$$) — a co-tenant could pre-plant a symlink
# there. On the rare mktemp failure we fall back to plain 2>/dev/null (the old
# silent-discard behavior): we lose the too-old/warning visibility in that one
# case, never write a capture to a world-writable predictable path.
STDERR_CAP="$(mktemp 2>/dev/null || true)"
status=0
if [ -n "$STDERR_CAP" ]; then
  CAP_REDIR="$STDERR_CAP"
else
  CAP_REDIR="/dev/null"
fi
timeout "${MX_COORD_BRIEF_TIMEOUT_SECS:-60}" \
  env \
    MX_COORD_SERVICE_URL="${MX_COORD_SERVICE_URL:-$COORD_URL_DEFAULT}" \
    MX_COORD_SERVICE_TOKEN="$TOKEN" \
    MX_COORD_EXPECT_TEAM="$SLUG" \
    mx-agent coordination brief 2>"$CAP_REDIR" || status=$?
if [ "$status" -eq 124 ]; then
  # LOUD BY DESIGN (#4846 SF-25). Everything else in this hook is quietly
  # fail-open, and this branch must not be: a silent timeout is indistinguishable
  # from an empty queue, so the agent reads "no escalations" when the truth is
  # "delivery failed". The drain is destructive server-side, so messages may have
  # been marked delivered without ever being shown. Say so, and say what to run.
  {
    echo "coordination brief: DRAIN DID NOT COMPLETE within ${MX_COORD_BRIEF_TIMEOUT_SECS:-60}s."
    echo "  QUEUE STATE UNKNOWN — this is NOT the same as an empty queue."
    echo "  Escalations addressed to this team may be undelivered, and because the"
    echo "  drain consumes messages server-side, some may already be marked"
    echo "  delivered without having been shown to you."
    echo "  Before relying on 'no escalations', run:  mx-agent coordination history"
    echo "  Raise the bound with MX_COORD_BRIEF_TIMEOUT_SECS if the queue is deep."
  } >&2
elif [ "$status" -ne 0 ]; then
  # Non-timeout failure. Only two cases are announced — CLI-too-old and a
  # REJECTED TOKEN; every other failure stays silently fail-open (a coordination
  # hiccup must not be noise). Commander in an old CLI prints e.g.
  # `error: unknown command 'coordination'`.
  # (When mktemp failed, CAP is empty → grep no-ops and we stay silent.)
  if [ -n "$STDERR_CAP" ] && grep -qiE "unknown command[ '\"]*coordination" "$STDERR_CAP" 2>/dev/null; then
    echo "coordination brief: installed mx-agent CLI too old (need >=${MIN_CLI_VERSION}) — skipping; run: ${CLI_UPDATE_CMD}" >&2
  # The 401 match must NOT be a bare "401" anywhere in the capture. The CLI prints
  # ServiceHttpError's message, and that message is the SERVICE-SUPPLIED `error`
  # field: a service returning HTTP 500 with {"error":"upstream 401 timeout"} would
  # otherwise send the operator to re-issue a token for a problem that is not a
  # token problem. This block's whole value is that it is rare and trusted, so it
  # requires our own marker or an adjacent HTTP context. The bare `invalid token`
  # / `unauthorized` alternatives stay: an OLD CLI (pre-#4970) prints only the raw
  # service error on a real 401, and that operator has the same problem.
  #
  # THE OLD-CLI ALTERNATIVES ARE THE SERVICE'S ACTUAL 401 BODIES, ALL OF THEM.
  # coordination-service returns exactly four 401 reasons (auth/auth.ts
  # authenticate(); router.ts surfaces `reason` verbatim as the `error` field):
  # `missing bearer token`, `invalid token`, `token revoked`,
  # `token grace window expired`. Only the second was listed here, so an old CLI
  # hitting a REVOKED or grace-expired token printed a line this pattern did not
  # match and the hook stayed silent — the #4970 outage, on the three rotation
  # paths most likely to cause it. All four are enumerated now. They are full
  # phrases from our own service, not the bare digits, so they do not re-open the
  # over-broad direction that test 24 pins.
  #
  # EVERY ALTERNATIVE BELOW IS PINNED by a case in coordination-brief.test.sh
  # (tests 21-24 and 24a-24f). Deleting one must turn a test red — that is the
  # point: this pattern is load-bearing in the SILENCE direction, so a future tidy
  # of an "unused" alternative must not be able to pass a green suite.
  elif [ -n "$STDERR_CAP" ] && grep -qiE "TOKEN REJECTED \(HTTP 401\)|http[ /][0-9.]* ?401|status[ :=]*401|invalid token|unauthorized|missing bearer token|token revoked|token grace window expired" "$STDERR_CAP" 2>/dev/null; then
    # LOUD BY DESIGN (#4970), same reasoning as the timeout branch above. A 401
    # means the service refused this worktree's token: nothing was drained, and
    # the previous behavior discarded the error and exited 0 with no output —
    # indistinguishable from a quiet queue. Post-rotation that is a fleet-wide
    # silent coordination outage. Say it, and say what to run. Still exit 0: a
    # dead token must not block session start.
    {
      echo "coordination brief: TOKEN REJECTED — the coordination service refused this worktree's token (HTTP 401)."
      echo "  QUEUE STATE UNKNOWN — this is NOT the same as an empty queue. Nothing was drained;"
      echo "  escalations addressed to this team are still queued and unread."
      echo "  A current CLI refetches the token and retries once (replacing the cached copy only"
      echo "  if the vault had a new one), so a message here means the retry was refused too,"
      echo "  or no replacement could be fetched (or the CLI predates that fix:"
      echo "  ${CLI_UPDATE_CMD})."
      echo "  Before relying on 'no escalations', run:"
      echo "    1. mx-agent coordination status   # confirm this worktree's identity"
      echo "    2. mx-agent coordination brief    # run it by hand and read the full error"
      echo "    3. Still rejected? This team's token needs re-issuing — tell the platform team (P1)."
    } >&2
  fi
else
  # Success: surface any CLI warnings for visibility. These are non-secret: the
  # SF-1 wrong-team interlock and the INFO-1 no-team note (team slugs only), and
  # an opportunistic-flush WARNING (a server error field / fs errno). None carry
  # a token or a message body.
  if [ -n "$STDERR_CAP" ] && [ -s "$STDERR_CAP" ]; then
    cat "$STDERR_CAP" >&2
  fi
fi
if [ -n "$STDERR_CAP" ]; then
  rm -f "$STDERR_CAP" 2>/dev/null || true
fi
exit 0
