diff --git a/scripts/ops/hermes_triage_demo.sh b/scripts/ops/hermes_triage_demo.sh index aa49322b2..f24296c05 100755 --- a/scripts/ops/hermes_triage_demo.sh +++ b/scripts/ops/hermes_triage_demo.sh @@ -6,7 +6,7 @@ # hermes_triage_demo.sh status # current incident/alert state, no changes # hermes_triage_demo.sh preflight # confirm the lab is ready to demo # hermes_triage_demo.sh reset # restore the demo to its pre-run state -# hermes_triage_demo.sh monitor # stream the flow chart stages live +# hermes_triage_demo.sh monitor [code] # stream the flow chart stages live # # FIRST RUN: copy hermes_triage_demo.env.example to hermes_triage_demo.env in # this directory and fill it in. That file is git-ignored precisely so it can @@ -78,7 +78,11 @@ for line in sys.stdin: print(" ", d["timestamp"][11:19], d.get("jobs"))' | tail -"${2:-5}" } +# `monitor` follows the fixture job; `monitor code` follows the code-proposal +# job, which takes the source-proposal branch of the chart instead. cmd_monitor() { + local which="${1:-fixture}" + [ "$which" = "code" ] && export MONITOR_JOB="$CODE_JOB" exec python3 "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hermes_triage_monitor.py" } @@ -285,6 +289,6 @@ case "${1:-}" in status) cmd_status ;; preflight) cmd_preflight ;; reset) cmd_reset ;; - monitor) cmd_monitor ;; + monitor) shift; cmd_monitor "$@" ;; *) sed -n '2,12p' "$0" | sed 's/^# \{0,1\}//' ; exit 1 ;; esac diff --git a/scripts/ops/hermes_triage_monitor.py b/scripts/ops/hermes_triage_monitor.py index d4e133313..170f155ed 100755 --- a/scripts/ops/hermes_triage_monitor.py +++ b/scripts/ops/hermes_triage_monitor.py @@ -288,14 +288,16 @@ def evidence_for(key: str, incident: str = "") -> None: print(f" {DIM}Hermes holds no Git or Kubernetes write access; this JSON is its" f" entire output{RESET}\n") elif key == "gates": - print(f" {DIM}authorized/authorize_reason above is the policy result: the verdict" - f" Ariadne reached on its own reading{RESET}") + print(f" {DIM}the authorization is a match between three separate things: an action" + f" Ariadne already has code to perform, an action id Hermes is permitted to" + f" request, and the action Hermes actually recommended. Ariadne holds the" + f" registry below; Hermes cannot add to it, and the recommendation only" + f" proceeds because it names something already in it.{RESET}") run(["kubectl", "-n", NS_ARIADNE, "exec", "deploy/ariadne", "-c", "ariadne", "--", "printenv", "ARIADNE_HERMES_ALLOWED_ACTIONS"], limit=2) run(["kubectl", "-n", NS_ARIADNE, "exec", "deploy/ariadne", "-c", "ariadne", "--", "printenv", "ARIADNE_HERMES_MIN_CONFIDENCE"], limit=2) - print(f" {DIM}only these two ids may ever be executed. Source fixes are not actions:" - f" they become pull requests and are never executed by Ariadne.{RESET}\n") + print() elif key == "route": print(f" {DIM}the fixture repair is an operational action, so the Optional source" f" proposal branch is not taken for this incident{RESET}\n") @@ -303,12 +305,20 @@ def evidence_for(key: str, incident: str = "") -> None: run(["kubectl", "-n", NS_DEMO, "get", "cm", "hermes-triage-demo-fixture", "-o", "jsonpath={.data.state}"]) if key == "response": - print(f" {DIM}Scoped ConfigMap repair on the chart: one merge patch to one" - f" ConfigMap, in process, no pod created{RESET}\n") + print(f" {DIM}that value read 'unhealthy' when the build failed - the seeded" + f" fault - and reads 'healthy' above because Ariadne has just patched it." + f" That single field changing is the repair.{RESET}\n") if key == "outputs": + print(f" {DIM}on the chart this is the 'records and artifacts' edge out of the" + f" whole Ariadne response box, not out of one branch. Every path ends" + f" here: an executed action, an escalation, or a pull request all record" + f" the same audit events and metrics.{RESET}") print(f" {YELLOW}issues filed by triage: {GITEA}/bstein/ariadne/issues{RESET}\n") elif key == "verify": - print(f" {DIM}exactly one rebuild is triggered; it never retries in a loop{RESET}\n") + print(f" {DIM}exactly one rebuild is triggered; it never retries in a loop. This" + f" completes the operational branch: because the action was authorized and" + f" performed, neither the human-required response nor the optional source" + f" proposal is entered for this incident.{RESET}\n") class Monitor: @@ -373,14 +383,40 @@ def main() -> None: ) monitor.mark("verify", "one rebuild triggered with seeding disabled") if status == "human_required": + reason = str(state.get("reason") or "") + proposed = reason == "code_fix_proposed" monitor.mark("evidence", "bundle collected") - monitor.mark("hermes", "diagnosis returned") - monitor.mark("gates", f"Ariadne refused: {state.get('reason', '')} — nothing ran") monitor.mark( - "route", - "policy result: refused -> Diagnosis and next checks -> Ariadne opens an issue", - ) - monitor.mark("response", "escalated; issue filed in the service repository") + "hermes", + "patch proposed as data: a path, an exact anchor and a replacement" + if proposed + else "diagnosis returned", + ) + monitor.mark( + "gates", + "no action was authorized; a source fix is not an action, so this takes" + " the proposal branch rather than the registry" + if proposed + else f"Ariadne refused: {reason} - nothing ran", + ) + monitor.mark( + "route", + "policy result: no action -> Optional source proposal -> Patch validator" + if proposed + else "policy result: refused -> Diagnosis and next checks -> opens an issue", + ) + monitor.mark( + "response", + "Ariadne opens a pull request; nothing merges without a human" + if proposed + else "escalated; issue filed in the service repository", + ) + if proposed: + monitor.mark( + "verify", + "the branch build validates the proposal; the incident stays" + " human-required either way", + ) if status == "healthy" and state.get("resolved") and not monitor.summarised: monitor.mark("verify", "rebuild finished green") monitor.mark("outputs", f"resolved: {', '.join(state['resolved'])}")