Files
PFVCluster/hooks/ticket-gate.sh
T
mrcharles a024b78cde feat(hooks): CHANGE GATE — prod remote ops require .crush/active-cr [#767]
Q3 ruling 2026-09-04: Tier 2 of the t/325 ladder is mechanical. Prod-
target remote.sh/remote-dns.sh invocations block without a filed GLPI
Change id; dev lanes (sectestbed/preprod/test/tsys5/sandbox/kali) exempt;
CR tooling exempt (chicken-and-egg). 7-case matrix green, shellcheck clean.

https://projects.knownelement.com/issues/767#note-4191
2026-09-04 07:16:24 -05:00

128 lines
4.3 KiB
Bash
Executable File

#!/usr/bin/env bash
# hooks/ticket-gate.sh — enforce ticket-first work policy
#
# Blocks modifying operations until an active ticket is established.
# The agent sets the active ticket via: echo '#NNN' > .crush/active-ticket
# And clears it when done: > .crush/active-ticket
#
# Exempts read-only and management commands (so you can create tickets,
# run audits, check status, etc.).
set -euo pipefail
TOOL="${CRUSH_TOOL_NAME:-}"
CMD="${CRUSH_TOOL_INPUT_COMMAND:-}"
# Read-only tools — always allowed
case "$TOOL" in
view|ls|grep|glob|agent|sourcegraph|fetch|agentic_fetch|download|lsp_diagnostics|lsp_symbols|lsp_definition|lsp_references|lsp_call_hierarchy|crush_info|crush_logs|question|todos)
exit 0
;;
esac
# For bash tool: exempt read-only and management commands
if [ "$TOOL" = "bash" ]; then
# Ticket/doc/dns management — always allowed
case "$CMD" in
*"redmine-cli"*|*"discourse-cli"*|*"dns-cli"*|*"technitium"*) exit 0 ;;
esac
# Read-only git
case "$CMD" in
*"git status"*|*"git log"*|*"git diff"*|*"git show"*|*"git branch"*) exit 0 ;;
esac
# Repo hygiene scripts
case "$CMD" in
*"check-rules"*|*"setup-hooks"*|*"shellcheck"*|*"run-tests"*) exit 0 ;;
esac
# Monitoring/probe commands
case "$CMD" in
*"tailscale status"*|*"access-matrix"*) exit 0 ;;
esac
# Setting/clearing the active ticket
case "$CMD" in
*active-ticket*) exit 0 ;;
esac
# Filing/managing GLPI CRs (chicken-and-egg: CR tooling must always run)
case "$CMD" in
*active-cr*|*"glpi-change"*) exit 0 ;;
esac
fi
# For edit/write: exempt policy/hook files (these ARE the policy)
FILE_PATH="${CRUSH_TOOL_INPUT_FILE_PATH:-}"
case "$FILE_PATH" in
*/AGENTS.md|*/check-rules.sh|*/crush.json|*/hooks/*)
if [ "$TOOL" = "write" ] || [ "$TOOL" = "edit" ] || [ "$TOOL" = "multiedit" ]; then
exit 0
fi
;;
esac
# Check for active ticket. Parallel sessions each use their OWN file:
# .crush/active-ticket (default / single session)
# .crush/active-ticket-<name> (suffixed per session; any non-empty one
# satisfies the gate — sessions never fight
# over one file)
GATE_OK=0
for f in "$CRUSH_PROJECT_DIR"/.crush/active-ticket*; do
[ -f "$f" ] && [ -s "$f" ] || continue
GATE_OK=1
printf '{"context":"Active ticket(s): %s -> %s"}\n' "$(basename "$f")" "$(cat "$f")"
done
if [ "$GATE_OK" -ne 1 ]; then
cat >&2 <<'MSG'
TICKET GATE: No active ticket set.
This project requires ticket-governed work (AGENTS.md Agent Authority).
Before modifying systems or code, set the active ticket:
echo '#NNN' > .crush/active-ticket
If no ticket exists yet, create one first (redmine-cli create), then set it.
Clear the ticket when work is complete:
> .crush/active-ticket
MSG
exit 2
fi
# CHANGE GATE [#767] (t/325 §5 ladder; Q3 ruling 2026-09-04): prod-target
# remote operations additionally require a filed GLPI Change — its id in
# .crush/active-cr (clear when the CR is closed: > .crush/active-cr)
# Dev-lane targets are exempt (sectestbed/preprod/test/sandbox = Tier 0/1).
# File a CR in one line (tooling lives in KNEL/inventory):
# echo "why/what/where" | scripts/glpi-change.sh create --agent --title "[#NNN] summary"
if [ "$TOOL" = "bash" ]; then
case "$CMD" in
*"remote.sh vm-file"*|*"remote.sh prox-file"*|*"remote.sh vm-copy"*|*"remote.sh prox-copy"*|*"remote.sh vm"*|*"remote.sh prox"*|*"remote-dns.sh "*)
PROD=1
case "$CMD" in
*sectestbed*|*preprod*|*glpi-test*|*kali*|*tsys5*|*sandbox*) PROD=0 ;;
esac
if [ "$PROD" -eq 1 ]; then
CR_OK=0
for f in "$CRUSH_PROJECT_DIR"/.crush/active-cr*; do
[ -f "$f" ] && [ -s "$f" ] || continue
CR_OK=1
printf '{"context":"Active CR: %s -> %s"}\n' "$(basename "$f")" "$(cat "$f")"
done
if [ "$CR_OK" -ne 1 ]; then
cat >&2 <<'MSG'
CHANGE GATE: prod-target remote operation requires a filed GLPI Change.
File one, then set its id (Tier 2 of the t/325 escalation ladder):
echo "what/why/where" | scripts/glpi-change.sh create --agent --title "[#NNN] summary"
echo '<change-id>' > .crush/active-cr
Dev-lane targets (sectestbed/preprod/test/tsys5) are exempt.
Clear when the CR closes: > .crush/active-cr
MSG
exit 2
fi
fi
;;
esac
fi
exit 0