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
