CHAT_OUTPUT_BEGIN COMMAND_ID=CLUSTER-REFERENCE-MODE-SEMANTICS-153 STATUS=OK RC=0 HOST=pve01 MODE=read-only COMPONENT=cluster-audit REFERENCE_REGISTER_CHECK=OK REFERENCE_SHA256=5763f2f8edc75fcf6f3951f8c95896d1cc112ceccb6cf5b64a383f540a4deb8e ERROR_REGISTER_CHECK=OK ERROR_REGISTER_SHA256=08b3a769b8e38eaa1acec915352fb328180d70caa7ff6cd9d1ec0da426262752 COMMAND_SHA256=8dc27e444049919a8436dfda6dbbf1e6f925075388cb73ecf483b2944f0dcf2e DUPLICATE_FAILED_COMMAND_BLOCKED=false EXECUTION_STARTED=true CHANGES_MADE=false SANITIZED=yes SECRETS_INCLUDED=no PRIVATE_ADDRESSES_INCLUDED=no RAW_EVIDENCE_SHA256=5a67f7767c4daff72ce91f626619b8d4251dbd9378ec9d757183506a3e9345d0 SANITIZED_OUTPUT_SHA256=1d71af6fb931c9d83bc82e594516212e45cd585f292d30486e0939ba5708acd0 OUTPUT_BEGIN PREVIOUS_RESULT=VERIFIED PREVIOUS_IMMUTABLE_COMMIT=fa3878ea8aeec87002c4c8f179fb88e25184e924 === RUNNER_HELP === Usage: homelab-chat-run COMMAND_ID MODE COMPONENT -- command [arguments...] MODE: read-only verify change transport-test Environment: HOMELAB_ALLOW_REPEAT=1 Allow exact repeat of a previously failed command. HOMELAB_TIMEOUT=SECONDS Optional command timeout. === MODE_VALIDATION_AND_EFFECTS === 1-#!/usr/bin/env bash 2-set -Eeuo pipefail 3-umask 077 4- 5-usage() { 6- cat >&2 <<'USAGE' 7-Usage: 8- homelab-chat-run COMMAND_ID MODE COMPONENT -- command [arguments...] 9- 10-MODE: 11: read-only 12: verify 13: change 14: transport-test 15- 16-Environment: 17- HOMELAB_ALLOW_REPEAT=1 Allow exact repeat of a previously failed command. 18- HOMELAB_TIMEOUT=SECONDS Optional command timeout. 19-USAGE 20- exit 64 21-} 22- 23-[ "$#" -ge 5 ] || usage 24- 25-COMMAND_ID="$1" 26-MODE="$2" 27-COMPONENT="$3" 28-shift 3 29- 30-[ "${1:-}" = '--' ] || usage 31-shift 32-[ "$#" -ge 1 ] || usage 33- 34-case "$COMMAND_ID" in 35- ''|*[!A-Za-z0-9._-]*) 36- echo 'RESULT=FAIL' 37- echo 'REASON=INVALID_COMMAND_ID' 38- exit 65 39- ;; 40-esac 41- 42-case "$COMPONENT" in 43- ''|*[!A-Za-z0-9._-]*) 44- echo 'RESULT=FAIL' 45- echo 'REASON=INVALID_COMPONENT' 46- exit 66 47- ;; 48-esac 49- 50:case "$MODE" in 51: read-only|verify|change|transport-test) 52- ;; 53- *) 54- echo 'RESULT=FAIL' 55: echo 'REASON=INVALID_MODE' 56- exit 67 57- ;; 58-esac 59- 60-for TOOL in \ 61- awk \ 62- curl \ 63- flock \ 64- python3 \ 65- scp \ 66- sha256sum \ 67- ssh 68-do 69- command -v "$TOOL" >/dev/null 2>&1 || { 70- echo 'RESULT=FAIL' 71- echo "REASON=REQUIRED_TOOL_MISSING:$TOOL" 72- exit 68 73- } 74-done 75- 76-REFERENCE=/etc/pve/31_HOMELAB_REFERENCE.md 77-ERROR_REGISTER=/etc/pve/HOMELAB_ASSISTANT_ERROR_REGISTER.md 78- 79-[ -r "$REFERENCE" ] || { 80- echo 'RESULT=FAIL' 81- echo 'REASON=REFERENCE_NOT_READABLE' 82- exit 69 83-} 84- 85-[ -r "$ERROR_REGISTER" ] || { 86- echo 'RESULT=FAIL' 87- echo 'REASON=ERROR_REGISTER_NOT_READABLE' 88- exit 70 89-} 90- 91-STATE_DIR=/var/lib/homelab-chat-run 92-RAW_DIR="$STATE_DIR/raw" 93-PUBLIC_DIR="$STATE_DIR/public" 94-LEDGER="$STATE_DIR/state/command-ledger.tsv" 95-LOCK="$STATE_DIR/state/runner.lock" -- 131- 132-PREVIOUS_FAILED_RC="$( 133- awk -F '\t' -v hash="$COMMAND_SHA256" ' 134- $3 == hash && $4 != "0" { 135- value=$4 136- } 137- 138- END { 139- print value 140- } 141- ' "$LEDGER" 142-)" 143- 144-RUN_STARTED=true 145-REPEAT_BLOCKED=false 146:BLOCK_REASON='' 147:CHANGES_MADE=false 148- 149:case "$MODE" in 150: change) 151: CHANGES_MADE=true 152- ;; 153-esac 154- 155-if [ -n "$PREVIOUS_FAILED_RC" ] && 156- [ "${HOMELAB_ALLOW_REPEAT:-0}" != 1 ]; then 157- 158- RUN_STARTED=false 159- REPEAT_BLOCKED=true 160: BLOCK_REASON='EXACT_FAILED_COMMAND_REPEAT' 161- RC=73 162- STATUS='BLOCKED' 163- 164- { 165- echo 'COMMAND_EXECUTION_BLOCKED=yes' 166- echo 'REASON=EXACT_FAILED_COMMAND_REPEAT' 167- echo "PREVIOUS_RC=$PREVIOUS_FAILED_RC" 168- echo "COMMAND_SHA256=$COMMAND_SHA256" 169- echo 170- echo 'Set HOMELAB_ALLOW_REPEAT=1 only after reviewing the previous failure.' 171- } >"$RAW_FILE" 172-else 173- set +e 174- 175- if [ -n "${HOMELAB_TIMEOUT:-}" ]; then 176- case "$HOMELAB_TIMEOUT" in 177- *[!0-9]*|'') 178- echo 'RESULT=FAIL' 179- echo 'REASON=INVALID_HOMELAB_TIMEOUT' 180- exit 74 181- ;; 182- esac 183- 184- timeout \ 185- --foreground \ 186- --preserve-status \ 187- "$HOMELAB_TIMEOUT" \ 188- "$@" >"$RAW_FILE" 2>&1 189- RC=$? 190- else 191- "$@" >"$RAW_FILE" 2>&1 192- RC=$? 193- fi 194- 195- set -e 196- 197- if [ "$RC" -eq 0 ]; then 198- STATUS='OK' 199- else 200- STATUS='FAIL' -- 317- "$COMMAND_ID" \ 318- "$STATUS" \ 319- "$RC" \ 320- "$HOST_SHORT" \ 321- "$MODE" \ 322- "$COMPONENT" \ 323- "$STARTED_AT" \ 324- "$FINISHED_AT" \ 325- "$REFERENCE_SHA256" \ 326- "$ERROR_REGISTER_SHA256" \ 327- "$COMMAND_SHA256" \ 328- "$RAW_SHA256" \ 329- "$SANITIZED_SHA256" \ 330- "$RUN_STARTED" \ 331- "$REPEAT_BLOCKED" \ 332: "$BLOCK_REASON" \ 333: "$CHANGES_MADE" <<'PY' 334-import json 335-import sys 336-from pathlib import Path 337- 338-( 339- json_path, 340- output_path, 341- command_id, 342- status, 343- rc, 344- host, 345- mode, 346- component, 347- started, 348- finished, 349- reference_sha, 350- error_sha, 351- command_sha, 352- raw_sha, 353- sanitized_sha, 354- run_started, 355- repeat_blocked, 356: block_reason, 357: changes_made, 358-) = sys.argv[1:] 359- 360-output = Path(output_path).read_text( 361- encoding="utf-8", 362- errors="replace", 363-) 364- 365-encoded = output.encode("utf-8") 366-json_limit = 300_000 367-output_truncated = len(encoded) > json_limit 368- 369-if output_truncated: 370- output_preview = encoded[:json_limit].decode( 371- "utf-8", 372- errors="ignore", 373- ) 374-else: 375- output_preview = output 376- 377-document = { 378- "schema_version": 1, 379- "channel": "homelab-runtime", 380- "command_id": command_id, 381- "status": status, 382- "rc": int(rc), 383- "host": host, 384- "mode": mode, 385- "component": component, 386- "started_at_utc": started, 387- "finished_at_utc": finished, 388- "reference_register_checked": True, 389- "reference_sha256": reference_sha, 390- "error_register_checked": True, 391- "error_register_sha256": error_sha, 392- "command_sha256": command_sha, 393- "duplicate_failed_command_blocked": repeat_blocked == "true", 394: "block_reason": block_reason or None, 395: "execution_started": run_started == "true", 396: "changes_made": changes_made == "true", 397- "sanitized": True, 398- "secrets_included": False, 399- "private_addresses_included": False, 400- "raw_evidence_retained_locally": True, 401- "raw_evidence_sha256": raw_sha, 402- "sanitized_output_sha256": sanitized_sha, 403- "output_truncated_in_json": output_truncated, 404- "full_sanitized_output_url": ( 405- "https://git.gram1.ru/.well-known/" 406- "homelab-runtime/latest.txt" 407- ), 408- "output": output_preview, 409-} 410- 411-Path(json_path).write_text( 412- json.dumps( 413- document, 414- ensure_ascii=False, 415- indent=2, 416- ) 417- + "\n", 418- encoding="utf-8", 419-) 420-PY 421- 422-{ 423- echo 'CHAT_OUTPUT_BEGIN' 424- echo "COMMAND_ID=$COMMAND_ID" 425- echo "STATUS=$STATUS" 426- echo "RC=$RC" 427- echo "HOST=$HOST_SHORT" 428- echo "MODE=$MODE" 429- echo "COMPONENT=$COMPONENT" 430- echo 'REFERENCE_REGISTER_CHECK=OK' 431- echo "REFERENCE_SHA256=$REFERENCE_SHA256" 432- echo 'ERROR_REGISTER_CHECK=OK' 433- echo "ERROR_REGISTER_SHA256=$ERROR_REGISTER_SHA256" 434- echo "COMMAND_SHA256=$COMMAND_SHA256" 435- echo "DUPLICATE_FAILED_COMMAND_BLOCKED=$REPEAT_BLOCKED" 436: echo "EXECUTION_STARTED=$RUN_STARTED" 437: echo "CHANGES_MADE=$CHANGES_MADE" 438- echo 'SANITIZED=yes' 439- echo 'SECRETS_INCLUDED=no' 440- echo 'PRIVATE_ADDRESSES_INCLUDED=no' 441- echo "RAW_EVIDENCE_SHA256=$RAW_SHA256" 442- echo "SANITIZED_OUTPUT_SHA256=$SANITIZED_SHA256" 443- echo 'OUTPUT_BEGIN' 444- cat "$SANITIZED_FILE" 445- echo 446- echo 'OUTPUT_END' 447- echo 'CHAT_OUTPUT_END' 448-} >"$TXT_FILE" 449- 450-chmod 0600 \ 451- "$RAW_FILE" \ 452- "$SANITIZED_FILE" \ 453- "$JSON_FILE" \ 454- "$TXT_FILE" 455- 456-TARGET=debian@[PRIVATE_IP] 457-REMOTE_PREFIX="/tmp/homelab-runtime-${COMMAND_ID}-$$" 458- 459-SSH_OPTS=( 460- -o BatchMode=yes 461- -o ConnectTimeout=10 462- -o ConnectionAttempts=1 463- -o StrictHostKeyChecking=yes 464- -o UserKnownHostsFile=[SENSITIVE_PATH] 465-) 466- 467-scp \ 468- -q \ 469- "${SSH_OPTS[@]}" \ 470- "$JSON_FILE" \ 471- "${TARGET}:${REMOTE_PREFIX}.json" 472- 473-scp \ 474- -q \ 475- "${SSH_OPTS[@]}" \ 476- "$TXT_FILE" \ 477- "${TARGET}:${REMOTE_PREFIX}.txt" === MODE_REFERENCES === 11: read-only 12: verify 13: change 14: transport-test 51: read-only|verify|change|transport-test) 150: change) 357: changes_made, 396: "changes_made": changes_made == "true", CLUSTER_REFERENCE_MODE_SEMANTICS=COMPLETE OUTPUT_END CHAT_OUTPUT_END