From 58e89f010f8cb6ec481fdc1b9dfe27d8331d9964 Mon Sep 17 00:00:00 2001 From: homelab-runtime-publisher Date: Sun, 26 Jul 2026 15:54:53 +0000 Subject: [PATCH] runtime: publish RUN7P1-IMMUTABLE-RESULT-PLAN-20260726T154634Z --- ...MMUTABLE-RESULT-PLAN-20260726T154634Z.json | 30 +++++++++++++++++++ ...IMMUTABLE-RESULT-PLAN-20260726T154634Z.txt | 27 +++++++++++++++++ runtime/latest.json | 18 +++++------ runtime/latest.txt | 19 ++++++------ 4 files changed, 75 insertions(+), 19 deletions(-) create mode 100644 runtime/history/RUN7P1-IMMUTABLE-RESULT-PLAN-20260726T154634Z.json create mode 100644 runtime/history/RUN7P1-IMMUTABLE-RESULT-PLAN-20260726T154634Z.txt diff --git a/runtime/history/RUN7P1-IMMUTABLE-RESULT-PLAN-20260726T154634Z.json b/runtime/history/RUN7P1-IMMUTABLE-RESULT-PLAN-20260726T154634Z.json new file mode 100644 index 0000000..9c20400 --- /dev/null +++ b/runtime/history/RUN7P1-IMMUTABLE-RESULT-PLAN-20260726T154634Z.json @@ -0,0 +1,30 @@ +{ + "schema_version": 1, + "channel": "homelab-runtime", + "command_id": "RUN7P1-IMMUTABLE-RESULT-PLAN-20260726T154634Z", + "status": "FAIL", + "rc": 2, + "host": "pve01", + "mode": "verify", + "component": "evidence-delivery", + "started_at_utc": "2026-07-26T15:54:51Z", + "finished_at_utc": "2026-07-26T15:54:52Z", + "reference_register_checked": true, + "reference_sha256": "5763f2f8edc75fcf6f3951f8c95896d1cc112ceccb6cf5b64a383f540a4deb8e", + "error_register_checked": true, + "error_register_sha256": "24934a2c7fa5c26f6e828583c4aefac7f143b40c0171736ed7571e617d55ab83", + "command_sha256": "317b507e77cebd2da1bfa6929e277df3bc09ecfd115d5b15d32b9705445d03ee", + "duplicate_failed_command_blocked": false, + "block_reason": null, + "execution_started": true, + "changes_made": false, + "sanitized": true, + "secrets_included": false, + "private_addresses_included": false, + "raw_evidence_retained_locally": true, + "raw_evidence_sha256": "3e4c291c2612a19e2f249cc30480639c87a6d497643f07022f10dcec1ebe9800", + "sanitized_output_sha256": "3e4c291c2612a19e2f249cc30480639c87a6d497643f07022f10dcec1ebe9800", + "output_truncated_in_json": false, + "full_sanitized_output_url": "https://git.gram1.ru/.well-known/homelab-runtime/latest.txt", + "output": "RUN7P1_IMMUTABLE_RESULT_PLAN=BLOCKED\nPLAN_SHA256=40dee3f5a655b78fb38477b146c651f0c9a46a56111211876f56df41538c74ca\n{\"PLAN_SHA256\":\"40dee3f5a655b78fb38477b146c651f0c9a46a56111211876f56df41538c74ca\",\"acceptance_requires_separate_capsule\":true,\"anti_regression\":[\"AR-P1-001 stale latest cannot satisfy runner\",\"AR-P1-002 exact command_id and command_sha256 required\",\"AR-P1-003 exact command TXT SHA required\",\"AR-P1-004 context-auto cannot overwrite by-command result\",\"AR-P1-005 same ID same payload is idempotent\",\"AR-P1-006 same ID different payload is rejected\",\"AR-P1-007 JSON is readiness marker published after TXT\",\"AR-P1-008 sanitizer does not cross line boundaries\",\"AR-P1-009 public payload remains sanitized and address-free\",\"AR-P1-010 temporary files are cleaned\"],\"backup\":{\"core_apps\":\"/var/lib/homelab-runtime-receive/backups/homelab-runtime-receive.25b1d3ddb71ea76fa8a067c4ab97e5f2a17c6dd7657440e74517647ab25ae72c\",\"exact_sha_required\":true,\"pve01\":\"/var/lib/homelab-chat-run/state/backups/homelab-chat-run.2a88defef9517f89bc60934dce82097f8e316b08af9417e95167ea5d759a10d9\"},\"blockers\":[\"REMOTE_RECEIVER_SHA_DISAGREEMENT\",\"REMOTE_SOURCE_LOCK_MISMATCH:receiver\",\"TRANSFORMATION_FAILED:RuntimeError\"],\"cleanup\":[\"remove all remote /tmp rehearsal and apply staging files\",\"retain verified rollback copies until phase seal\",\"do not remove historical runtime results\"],\"component\":\"evidence-delivery\",\"created_at_utc\":\"2026-07-26T15:54:52.541976+00:00\",\"declared_change_flags_for_apply\":{\"backup\":false,\"cluster\":false,\"gitea\":true,\"production\":true},\"exact_diff\":[\"add immutable .well-known/by-command/.json and .txt\",\"write TXT first and JSON last as readiness marker\",\"idempotent same COMMAND_ID with identical JSON/TXT\",\"fail closed on same COMMAND_ID with differing JSON/TXT\",\"prevent Git history overwrite on COMMAND_ID conflict\",\"retain latest.json/latest.txt as non-authoritative convenience\",\"runner publishes own immutable TXT URL\",\"runner polls exact immutable JSON and TXT\",\"runner verifies command_id, command_sha256 and sanitized flags\",\"fix multiline sanitizer false-positive with horizontal whitespace\",\"add persistent --self-test anti-regression entrypoints\"],\"gates\":{\"G0_context_lock\":{\"source_evidence_sha256\":\"844bd57846acb2dcb939392fd22eeae005b348bd346f3aace0bc219f6bafa38a\",\"source_preplan_basis_sha256\":\"6ae59d4b7877710758ecb8e315a8322e71c475b6dcda680864eff8c1f7e7e0d9\",\"status\":\"FAIL\"},\"G1_read_only_discovery\":{\"source_hashes\":{\"adapter\":\"f2410cd328e2837b99e8ce4a28e25cb031a6bdd596d0286ac77a1822ef1cecd0\",\"adapter_unit\":\"74d0f37878652ed9d46b53ae8556bbb30ebd683741f293c3f59b3527876ed2d7\",\"portal\":\"8056faf4ef6348f0b06319ad3c49fbfeeadf25b6370f857ea19569bd27652ee2\",\"portal_unit\":\"8453eab30f5c3ccd285fc9f70fff18f564809bb27f7bf84f51f2ba593aa85263\",\"receiver\":\"25b1d3ddb71ea76fa8a067c4ab97e5f2a17c6dd7657440e74517647ab25ae72c\",\"runner\":\"2a88defef9517f89bc60934dce82097f8e316b08af9417e95167ea5d759a10d9\"},\"status\":\"PASS\",\"units\":{\"adapter\":{\"active\":\"active\",\"enabled\":\"enabled\",\"group\":\"homelab-command-portal\",\"restarts\":\"0\",\"user\":\"root\"},\"portal\":{\"active\":\"active\",\"enabled\":\"enabled\",\"group\":\"homelab-command-portal\",\"restarts\":\"0\",\"user\":\"homelab-command-portal\"}}},\"G2_static_validation\":{\"status\":\"FAIL\",\"target_hashes\":{}},\"G3_identity_equivalent\":{\"current_receiver_sudo_contract_rc\":64,\"remote_install_available\":true,\"remote_mkdir_available\":true,\"status\":\"PASS\"},\"G4_integration_rehearsal\":{\"production_mutation\":false,\"results\":{\"remote_cleanup_rc\":0},\"status\":\"PASS\"}},\"kind\":\"homelab-change-plan\",\"mutation_journal\":{\"core_apps\":\"/var/lib/homelab-runtime-receive/RUN7P1-immutable-result-mutation.json\",\"pve01\":\"/var/lib/homelab-chat-run/state/RUN7P1-immutable-result-mutation.json\"},\"plan_id\":\"RUN7P1-IMMUTABLE-PUBLIC-RESULT\",\"postconditions\":[\"installed files match planned target SHA-256\",\"installed runner --self-test returns PASS\",\"installed receiver --self-test via sudo returns PASS\",\"APPLY itself completes through backward-compatible latest channel\",\"separate acceptance capsule is fetched by exact by-command URL\",\"context-auto overwrite of latest does not change acceptance result\",\"same-ID same-content receiver test is idempotent\",\"same-ID different-content receiver test fails closed\"],\"preconditions\":[\"all source SHA-256 values exactly match this plan\",\"Portal and Adapter remain active/enabled with NRestarts=0\",\"no unresolved mutating failure or rollback\",\"exact PLAN_SHA256 is embedded in APPLY capsule\",\"APPLY capsule source evidence SHA matches RUN7P1 evidence\"],\"risk\":{\"backup_subsystem_touched\":false,\"level\":\"MEDIUM\",\"reason\":\"changes public sanitized-result transport on two hosts\",\"second_runner_created\":false},\"rollback\":[\"capture diagnostics before rollback\",\"record file SHA/mode/owner, runner and receiver self-tests\",\"record exact by-command HTTP status and response SHA\",\"restore both exact source files from verified backups\",\"verify restored SHA/mode/owner\",\"do not restart unrelated services\"],\"schema\":1,\"scope\":[{\"group\":\"root\",\"host\":\"pve01\",\"mode\":\"0750\",\"owner\":\"root\",\"path\":\"/usr/local/sbin/homelab-chat-run\",\"source_sha256\":\"2a88defef9517f89bc60934dce82097f8e316b08af9417e95167ea5d759a10d9\",\"target_sha256\":null},{\"group\":\"root\",\"host\":\"core-apps\",\"mode\":\"0750\",\"owner\":\"root\",\"path\":\"/usr/local/sbin/homelab-runtime-receive\",\"source_sha256\":\"25b1d3ddb71ea76fa8a067c4ab97e5f2a17c6dd7657440e74517647ab25ae72c\",\"target_sha256\":null}],\"service_identity_test\":{\"proposed_receiver_self_test\":\"PASS\",\"proposed_runner_self_test\":\"PASS\",\"receiver_current_sudo_usage_rc\":64},\"source\":{\"live_source_hashes\":{\"core_apps_receiver\":\"25b1d3ddb71ea76fa8a067c4ab97e5f2a17c6dd7657440e74517647ab25ae72c\",\"pve01_runner\":\"2a88defef9517f89bc60934dce82097f8e316b08af9417e95167ea5d759a10d9\"},\"run7a_capsule_sha256\":\"b45c17b1b5f4ac00868c84dd66ef8b59a6033b92c9886f7f3e6c5b48ea5da5a8\",\"run7a_evidence_sha256\":\"844bd57846acb2dcb939392fd22eeae005b348bd346f3aace0bc219f6bafa38a\",\"run7a_preplan_basis_sha256\":\"6ae59d4b7877710758ecb8e315a8322e71c475b6dcda680864eff8c1f7e7e0d9\",\"run7a_runner_json_sha256\":\"7c77a1aa9f229e7d67ab7a74769bcf4dee22b2fbd474c9c909fb796123b5a7bb\"},\"target\":{\"core_apps_receiver_sha256\":null,\"pve01_runner_sha256\":null},\"warnings\":[]}\n" +} diff --git a/runtime/history/RUN7P1-IMMUTABLE-RESULT-PLAN-20260726T154634Z.txt b/runtime/history/RUN7P1-IMMUTABLE-RESULT-PLAN-20260726T154634Z.txt new file mode 100644 index 0000000..3e1077a --- /dev/null +++ b/runtime/history/RUN7P1-IMMUTABLE-RESULT-PLAN-20260726T154634Z.txt @@ -0,0 +1,27 @@ +CHAT_OUTPUT_BEGIN +COMMAND_ID=RUN7P1-IMMUTABLE-RESULT-PLAN-20260726T154634Z +STATUS=FAIL +RC=2 +HOST=pve01 +MODE=verify +COMPONENT=evidence-delivery +REFERENCE_REGISTER_CHECK=OK +REFERENCE_SHA256=5763f2f8edc75fcf6f3951f8c95896d1cc112ceccb6cf5b64a383f540a4deb8e +ERROR_REGISTER_CHECK=OK +ERROR_REGISTER_SHA256=24934a2c7fa5c26f6e828583c4aefac7f143b40c0171736ed7571e617d55ab83 +COMMAND_SHA256=317b507e77cebd2da1bfa6929e277df3bc09ecfd115d5b15d32b9705445d03ee +DUPLICATE_FAILED_COMMAND_BLOCKED=false +EXECUTION_STARTED=true +CHANGES_MADE=false +SANITIZED=yes +SECRETS_INCLUDED=no +PRIVATE_ADDRESSES_INCLUDED=no +RAW_EVIDENCE_SHA256=3e4c291c2612a19e2f249cc30480639c87a6d497643f07022f10dcec1ebe9800 +SANITIZED_OUTPUT_SHA256=3e4c291c2612a19e2f249cc30480639c87a6d497643f07022f10dcec1ebe9800 +OUTPUT_BEGIN +RUN7P1_IMMUTABLE_RESULT_PLAN=BLOCKED +PLAN_SHA256=40dee3f5a655b78fb38477b146c651f0c9a46a56111211876f56df41538c74ca +{"PLAN_SHA256":"40dee3f5a655b78fb38477b146c651f0c9a46a56111211876f56df41538c74ca","acceptance_requires_separate_capsule":true,"anti_regression":["AR-P1-001 stale latest cannot satisfy runner","AR-P1-002 exact command_id and command_sha256 required","AR-P1-003 exact command TXT SHA required","AR-P1-004 context-auto cannot overwrite by-command result","AR-P1-005 same ID same payload is idempotent","AR-P1-006 same ID different payload is rejected","AR-P1-007 JSON is readiness marker published after TXT","AR-P1-008 sanitizer does not cross line boundaries","AR-P1-009 public payload remains sanitized and address-free","AR-P1-010 temporary files are cleaned"],"backup":{"core_apps":"/var/lib/homelab-runtime-receive/backups/homelab-runtime-receive.25b1d3ddb71ea76fa8a067c4ab97e5f2a17c6dd7657440e74517647ab25ae72c","exact_sha_required":true,"pve01":"/var/lib/homelab-chat-run/state/backups/homelab-chat-run.2a88defef9517f89bc60934dce82097f8e316b08af9417e95167ea5d759a10d9"},"blockers":["REMOTE_RECEIVER_SHA_DISAGREEMENT","REMOTE_SOURCE_LOCK_MISMATCH:receiver","TRANSFORMATION_FAILED:RuntimeError"],"cleanup":["remove all remote /tmp rehearsal and apply staging files","retain verified rollback copies until phase seal","do not remove historical runtime results"],"component":"evidence-delivery","created_at_utc":"2026-07-26T15:54:52.541976+00:00","declared_change_flags_for_apply":{"backup":false,"cluster":false,"gitea":true,"production":true},"exact_diff":["add immutable .well-known/by-command/.json and .txt","write TXT first and JSON last as readiness marker","idempotent same COMMAND_ID with identical JSON/TXT","fail closed on same COMMAND_ID with differing JSON/TXT","prevent Git history overwrite on COMMAND_ID conflict","retain latest.json/latest.txt as non-authoritative convenience","runner publishes own immutable TXT URL","runner polls exact immutable JSON and TXT","runner verifies command_id, command_sha256 and sanitized flags","fix multiline sanitizer false-positive with horizontal whitespace","add persistent --self-test anti-regression entrypoints"],"gates":{"G0_context_lock":{"source_evidence_sha256":"844bd57846acb2dcb939392fd22eeae005b348bd346f3aace0bc219f6bafa38a","source_preplan_basis_sha256":"6ae59d4b7877710758ecb8e315a8322e71c475b6dcda680864eff8c1f7e7e0d9","status":"FAIL"},"G1_read_only_discovery":{"source_hashes":{"adapter":"f2410cd328e2837b99e8ce4a28e25cb031a6bdd596d0286ac77a1822ef1cecd0","adapter_unit":"74d0f37878652ed9d46b53ae8556bbb30ebd683741f293c3f59b3527876ed2d7","portal":"8056faf4ef6348f0b06319ad3c49fbfeeadf25b6370f857ea19569bd27652ee2","portal_unit":"8453eab30f5c3ccd285fc9f70fff18f564809bb27f7bf84f51f2ba593aa85263","receiver":"25b1d3ddb71ea76fa8a067c4ab97e5f2a17c6dd7657440e74517647ab25ae72c","runner":"2a88defef9517f89bc60934dce82097f8e316b08af9417e95167ea5d759a10d9"},"status":"PASS","units":{"adapter":{"active":"active","enabled":"enabled","group":"homelab-command-portal","restarts":"0","user":"root"},"portal":{"active":"active","enabled":"enabled","group":"homelab-command-portal","restarts":"0","user":"homelab-command-portal"}}},"G2_static_validation":{"status":"FAIL","target_hashes":{}},"G3_identity_equivalent":{"current_receiver_sudo_contract_rc":64,"remote_install_available":true,"remote_mkdir_available":true,"status":"PASS"},"G4_integration_rehearsal":{"production_mutation":false,"results":{"remote_cleanup_rc":0},"status":"PASS"}},"kind":"homelab-change-plan","mutation_journal":{"core_apps":"/var/lib/homelab-runtime-receive/RUN7P1-immutable-result-mutation.json","pve01":"/var/lib/homelab-chat-run/state/RUN7P1-immutable-result-mutation.json"},"plan_id":"RUN7P1-IMMUTABLE-PUBLIC-RESULT","postconditions":["installed files match planned target SHA-256","installed runner --self-test returns PASS","installed receiver --self-test via sudo returns PASS","APPLY itself completes through backward-compatible latest channel","separate acceptance capsule is fetched by exact by-command URL","context-auto overwrite of latest does not change acceptance result","same-ID same-content receiver test is idempotent","same-ID different-content receiver test fails closed"],"preconditions":["all source SHA-256 values exactly match this plan","Portal and Adapter remain active/enabled with NRestarts=0","no unresolved mutating failure or rollback","exact PLAN_SHA256 is embedded in APPLY capsule","APPLY capsule source evidence SHA matches RUN7P1 evidence"],"risk":{"backup_subsystem_touched":false,"level":"MEDIUM","reason":"changes public sanitized-result transport on two hosts","second_runner_created":false},"rollback":["capture diagnostics before rollback","record file SHA/mode/owner, runner and receiver self-tests","record exact by-command HTTP status and response SHA","restore both exact source files from verified backups","verify restored SHA/mode/owner","do not restart unrelated services"],"schema":1,"scope":[{"group":"root","host":"pve01","mode":"0750","owner":"root","path":"/usr/local/sbin/homelab-chat-run","source_sha256":"2a88defef9517f89bc60934dce82097f8e316b08af9417e95167ea5d759a10d9","target_sha256":null},{"group":"root","host":"core-apps","mode":"0750","owner":"root","path":"/usr/local/sbin/homelab-runtime-receive","source_sha256":"25b1d3ddb71ea76fa8a067c4ab97e5f2a17c6dd7657440e74517647ab25ae72c","target_sha256":null}],"service_identity_test":{"proposed_receiver_self_test":"PASS","proposed_runner_self_test":"PASS","receiver_current_sudo_usage_rc":64},"source":{"live_source_hashes":{"core_apps_receiver":"25b1d3ddb71ea76fa8a067c4ab97e5f2a17c6dd7657440e74517647ab25ae72c","pve01_runner":"2a88defef9517f89bc60934dce82097f8e316b08af9417e95167ea5d759a10d9"},"run7a_capsule_sha256":"b45c17b1b5f4ac00868c84dd66ef8b59a6033b92c9886f7f3e6c5b48ea5da5a8","run7a_evidence_sha256":"844bd57846acb2dcb939392fd22eeae005b348bd346f3aace0bc219f6bafa38a","run7a_preplan_basis_sha256":"6ae59d4b7877710758ecb8e315a8322e71c475b6dcda680864eff8c1f7e7e0d9","run7a_runner_json_sha256":"7c77a1aa9f229e7d67ab7a74769bcf4dee22b2fbd474c9c909fb796123b5a7bb"},"target":{"core_apps_receiver_sha256":null,"pve01_runner_sha256":null},"warnings":[]} + +OUTPUT_END +CHAT_OUTPUT_END diff --git a/runtime/latest.json b/runtime/latest.json index 5f33a95..9c20400 100644 --- a/runtime/latest.json +++ b/runtime/latest.json @@ -1,19 +1,19 @@ { "schema_version": 1, "channel": "homelab-runtime", - "command_id": "RUN7A-DELIVERY-PREPLAN-DISCOVERY-20260726T153105Z", - "status": "OK", - "rc": 0, + "command_id": "RUN7P1-IMMUTABLE-RESULT-PLAN-20260726T154634Z", + "status": "FAIL", + "rc": 2, "host": "pve01", "mode": "verify", "component": "evidence-delivery", - "started_at_utc": "2026-07-26T15:32:54Z", - "finished_at_utc": "2026-07-26T15:33:10Z", + "started_at_utc": "2026-07-26T15:54:51Z", + "finished_at_utc": "2026-07-26T15:54:52Z", "reference_register_checked": true, "reference_sha256": "5763f2f8edc75fcf6f3951f8c95896d1cc112ceccb6cf5b64a383f540a4deb8e", "error_register_checked": true, "error_register_sha256": "24934a2c7fa5c26f6e828583c4aefac7f143b40c0171736ed7571e617d55ab83", - "command_sha256": "72e4d7cfc355fa26e684146bf7b62cece7b11d539ee4fae0ef84c613bcf36a6e", + "command_sha256": "317b507e77cebd2da1bfa6929e277df3bc09ecfd115d5b15d32b9705445d03ee", "duplicate_failed_command_blocked": false, "block_reason": null, "execution_started": true, @@ -22,9 +22,9 @@ "secrets_included": false, "private_addresses_included": false, "raw_evidence_retained_locally": true, - "raw_evidence_sha256": "80c0aa4ae374f18ac41e5c6df736a45e2f144cf5a23db6ccf7554c5a03844a98", - "sanitized_output_sha256": "edac687cd4d0ffb36c68d91fbc10d7554c37707b62c4d09b09b8e88db7ce13c3", + "raw_evidence_sha256": "3e4c291c2612a19e2f249cc30480639c87a6d497643f07022f10dcec1ebe9800", + "sanitized_output_sha256": "3e4c291c2612a19e2f249cc30480639c87a6d497643f07022f10dcec1ebe9800", "output_truncated_in_json": false, "full_sanitized_output_url": "https://git.gram1.ru/.well-known/homelab-runtime/latest.txt", - "output": "RUN7A_DELIVERY_PREPLAN_DISCOVERY=PASS\nPREPLAN_BASIS_SHA256=6ae59d4b7877710758ecb8e315a8322e71c475b6dcda680864eff8c1f7e7e0d9\nREPORT_SHA256=319d433bb77302d92977e66bec320130c69eca85e70e05f6f8350bba4f1a7d6b\n{\"anti_regression\":{\"RUN7A-AR-001-SANITIZER-LINE-BOUNDARY\":{\"checks\":{\"gateway_discovery_by_listener_not_name_guess\":true,\"multiline_false_positive_closed\":true,\"no_long_inline_remote_shell\":true,\"password_removed\":true,\"private_ip_removed\":true,\"remote_stat_single_argument_format\":true,\"userinfo_removed\":true},\"status\":\"PASS\"},\"RUN7A-AR-002-REMOTE-STAT-NO-SPACES\":{\"status\":\"PASS\"},\"RUN7A-AR-003-GATEWAY-BY-LISTENER-CGROUP\":{\"status\":\"PASS\"},\"RUN7A-AR-004-NO-INLINE-REMOTE-BODY\":{\"status\":\"PASS\"},\"RUN7A-AR-005-NO-MUTATION\":{\"backup_changed\":false,\"cluster_changed\":false,\"gitea_changed\":false,\"production_changed\":false,\"status\":\"PASS\"}},\"architecture\":{\"phase_1\":{\"name\":\"IMMUTABLE_PUBLIC_RESULT\",\"requirements\":[\"publish runtime/by-command/.json\",\"publish runtime/by-command/.txt\",\"fail closed on same COMMAND_ID with different SHA\",\"idempotent success on same COMMAND_ID and same SHA\",\"retain latest.json/latest.txt as non-authoritative convenience\",\"runner polls exact immutable by-command JSON\",\"runner verifies command_id and command_sha256\",\"full_sanitized_output_url points to immutable TXT\",\"atomic write, flock, git commit and git push\"],\"scope\":[\"/usr/local/sbin/homelab-chat-run\",\"/usr/local/sbin/homelab-runtime-receive\"]},\"phase_2\":{\"name\":\"DETERMINISTIC_FULL_EVIDENCE_RECEIPT\",\"requirements\":[\"deterministic receipt URL known before execution\",\"receipt capability derived from 256-bit capsule SHA\",\"receipt metadata uploaded only after evidence SHA verification\",\"uniform 404 for missing, invalid and expired receipt\",\"Cache-Control: no-store\",\"Content-Disposition contains COMMAND_ID and short SHA\",\"no ListBucket\",\"no permanent MinIO credentials in Gateway\",\"evidence SHA verified before HTTP 200\"],\"scope\":[\"/usr/local/sbin/homelab-command-adapter\",\"/opt/stacks/evidence-gw/gateway.py\",\"evidence-gw.service\"]},\"phase_3\":{\"name\":\"QUALITY_GATE_PLAN_APPLY_ENFORCEMENT\",\"requirements\":[\"schema-v1 read-only compatibility\",\"schema-v2 mandatory for change\",\"exact PLAN capsule/evidence/plan SHA binding\",\"no unresolved mutating failure\",\"Portal displays all gate fields\",\"Execute disabled on gate failure\",\"diagnostics before rollback\"],\"scope\":[\"/usr/local/sbin/homelab-command-adapter\",\"/usr/local/sbin/homelab-command-portal\"],\"source_plan_sha256\":\"99fac83369ee8d5098c07cf243398d8b6b144e4fbc9ef9b39ef75436b109d607\"}},\"blockers\":[],\"changes\":{\"backup\":false,\"cluster\":false,\"gitea\":false,\"production\":false},\"checked_at_utc\":\"2026-07-26T15:33:10.356156+00:00\",\"command_id\":\"RUN7A-DELIVERY-PREPLAN-DISCOVERY-20260726T153105Z\",\"edge_gateway\":{\"cgroup_rc\":0,\"cmdline\":[\"/usr/bin/python3\",\"/opt/stacks/evidence-gw/gateway.py\",\"--config\",\"/etc/evidence-gw/config.json\"],\"cmdline_rc\":0,\"exe\":\"/usr/bin/python3.13\",\"exe_rc\":0,\"install_help_rc\":0,\"listener_excerpt\":\"LISTEN 0 5 127.0.0.1:18093 0.0.0.0:* users:((\\\"python3\\\",pid=2384852,fd=3)) \",\"listener_found\":true,\"listener_rc\":0,\"nginx_rc\":125,\"nginx_relevant\":[],\"pid\":2384852,\"script_features\":{\"has_content_disposition\":true,\"has_expiry\":true,\"has_list_bucket\":false,\"has_no_store\":true,\"has_object_key\":false,\"has_receipt_route\":false,\"has_sha256\":true},\"script_path\":\"/opt/stacks/evidence-gw/gateway.py\",\"script_sha256\":\"27de57be0924fe8d86d38879b3bcdba8d4ade817b3a42219dd65d8aacc9601cb\",\"script_sha_rc\":0,\"script_source_bytes\":14158,\"script_source_rc\":0,\"script_source_redacted\":\"#!/usr/bin/env python3\\nfrom __future__ import annotations\\n\\nimport argparse\\nimport hashlib\\nimport hmac\\nimport http.server\\nimport json\\nimport os\\nimport re\\nimport secrets\\nimport tempfile\\nimport threading\\nimport time\\nimport urllib.error\\nimport urllib.parse\\nimport urllib.request\\nfrom pathlib import Path\\nfrom typing import Any\\n\\nKEY_SEGMENT = re.compile(r\\\"^[A-Za-z0-9._-]+$\\\")\\nSHA256_RE = re.compile(r\\\"^[0-9a-f]{64}$\\\")\\n\\n\\ndef load_json(path: Path) -> dict[str, Any]:\\n value = json.loads(path.read_text(encoding=\\\"utf-8\\\"))\\n if not isinstance(value, dict):\\n raise RuntimeError(\\\"configuration must be a JSON object\\\")\\n return value\\n\\n\\ndef validate_key(key: str) -> str:\\n if not key or len(key) > 512 or key.startswith(\\\"/\\\"):\\n raise ValueError(\\\"invalid object key\\\")\\n segments = key.split(\\\"/\\\")\\n if any(\\n not segment\\n or segment in {\\\".\\\", \\\"..\\\"}\\n or KEY_SEGMENT.fullmatch(segment) is None\\n for segment in segments\\n ):\\n raise ValueError(\\\"invalid object key\\\")\\n return key\\n\\n\\ndef canonical_path(key: str) -> str:\\n return \\\"/e/\\\" + urllib.parse.quote(\\n validate_key(key),\\n safe=\\\"/._-\\\",\\n )\\n\\n\\ndef signature(secret: bytes, path: str, expiry: int, sha256: str) -> str:\\n message = f\\\"{path}\\\\n{expiry}\\\\n{sha256}\\\".encode(\\\"utf-8\\\")\\n return hmac.new(secret, message, hashlib.sha256).hexdigest()\\n\\n\\ndef security_headers(handler: http.server.BaseHTTPRequestHandler) -> None:\\n handler.send_header(\\\"Cache-Control\\\", \\\"no-store, max-age=0\\\")\\n handler.send_header(\\\"Pragma\\\", \\\"no-cache\\\")\\n handler.send_header(\\\"X-Content-Type-Options\\\", \\\"nosniff\\\")\\n handler.send_header(\\\"Referrer-Policy\\\", \\\"no-referrer\\\")\\n handler.send_header(\\\"Content-Security-Policy\\\", \\\"default-src 'none'\\\")\\n handler.send_header(\\\"Cross-Origin-Resource-Policy\\\", \\\"same-origin\\\")\\n\\n\\nclass GatewayServer(http.server.ThreadingHTTPServer):\\n daemon_threads = True\\n allow_reuse_address = True\\n\\n def __init__(\\n self,\\n address: tuple[str, int],\\n handler: type[http.server.BaseHTTPRequestHandler],\\n *,\\n config: dict[str, Any],\\n secret: [REDACTED]\\n ) -> None:\\n super().__init__(address, handler)\\n self.config = config\\n self.secret = secret\\n self.slots = threading.BoundedSemaphore(\\n int(config[\\\"max_concurrent_downloads\\\"])\\n )\\n\\n\\nclass Handler(http.server.BaseHTTPRequestHandler):\\n server_version = \\\"EvidenceGateway/1\\\"\\n sys_version = \\\"\\\"\\n\\n @property\\n def gateway(self) -> GatewayServer:\\n return self.server # type: ignore[return-value]\\n\\n def log_message(self, fmt: str, *args: object) -> None:\\n return\\n\\n def _json(\\n self,\\n status: int,\\n payload: dict[str, Any],\\n ) -> None:\\n body = (\\n json.dumps(\\n payload,\\n separators=(\\\",\\\", \\\":\\\"),\\n sort_keys=True,\\n )\\n + \\\"\\\\n\\\"\\n ).encode()\\n self.send_response(status)\\n self.send_header(\\\"Content-Type\\\", \\\"application/json\\\")\\n self.send_header(\\\"Content-Length\\\", str(len(body)))\\n security_headers(self)\\n self.end_headers()\\n if self.command != \\\"HEAD\\\":\\n self.wfile.write(body)\\n\\n def _event(\\n self,\\n *,\\n status: int,\\n key: str | None,\\n size: int | None,\\n started: float,\\n ) -> None:\\n event = {\\n \\\"event\\\": \\\"download\\\",\\n \\\"status\\\": status,\\n \\\"key_sha256\\\": (\\n hashlib.sha256(key.encode()).hexdigest()\\n if key is not None\\n else None\\n ),\\n \\\"bytes\\\": size,\\n \\\"duration_ms\\\": int((time.monotonic() - started) * 1000),\\n \\\"remote\\\": self.client_address[0],\\n }\\n print(\\n json.dumps(event, separators=(\\\",\\\", \\\":\\\"), sort_keys=True),\\n flush=True,\\n )\\n\\n def do_HEAD(self) -> None:\\n self._handle_download()\\n\\n def do_GET(self) -> None:\\n parsed = urllib.parse.urlsplit(self.path)\\n if parsed.path == \\\"/healthz\\\":\\n self._json(\\n 200,\\n {\\n \\\"status\\\": \\\"ok\\\",\\n \\\"service\\\": \\\"evidence-gateway\\\",\\n },\\n )\\n return\\n self._handle_download()\\n\\n def do_POST(self) -> None:\\n self._json(405, {\\\"error\\\": \\\"method_not_allowed\\\"})\\n\\n do_PUT = do_POST\\n do_DELETE = do_POST\\n do_PATCH = do_POST\\n do_OPTIONS = do_POST\\n\\n def _handle_download(self) -> None:\\n started = time.monotonic()\\n key: str | None = None\\n size: int | None = None\\n status = 500\\n\\n if not self.gateway.slots.acquire(blocking=False):\\n status = 503\\n self._json(status, {\\\"error\\\": \\\"busy\\\"})\\n self._event(\\n status=status,\\n key=None,\\n size=None,\\n started=started,\\n )\\n return\\n\\n try:\\n parsed = urllib.parse.urlsplit(self.path)\\n if not parsed.path.startswith(\\\"/e/\\\"):\\n status = 404\\n self._json(status, {\\\"error\\\": \\\"not_found\\\"})\\n return\\n\\n try:\\n decoded_key = urllib.parse.unquote(\\n parsed.path[len(\\\"/e/\\\") :],\\n errors=\\\"strict\\\",\\n )\\n key = validate_key(decoded_key)\\n except Exception:\\n status = 400\\n self._json(status, {\\\"error\\\": \\\"invalid_key\\\"})\\n return\\n\\n if canonical_path(key) != parsed.path:\\n status = 400\\n self._json(status, {\\\"error\\\": \\\"noncanonical_path\\\"})\\n return\\n\\n query = urllib.parse.parse_qs(\\n parsed.query,\\n keep_blank_values=True,\\n strict_parsing=True,\\n )\\n if set(query) != {\\\"exp\\\", \\\"sha256\\\", \\\"sig\\\"} or any(\\n len(values) != 1 for values in query.values()\\n ):\\n status = 400\\n self._json(status, {\\\"error\\\": \\\"invalid_query\\\"})\\n return\\n\\n try:\\n expiry = int(query[\\\"exp\\\"][0])\\n except ValueError:\\n status = 400\\n self._json(status, {\\\"error\\\": \\\"invalid_expiry\\\"})\\n return\\n\\n expected_sha = query[\\\"sha256\\\"][0].lower()\\n provided_sig = query[\\\"sig\\\"][0].lower()\\n now = int(time.time())\\n max_ttl = int(self.gateway.config[\\\"max_ttl_seconds\\\"])\\n\\n if expiry < now:\\n status = 410\\n self._json(status, {\\\"error\\\": \\\"expired\\\"})\\n return\\n if expiry - now > max_ttl:\\n status = 403\\n self._json(status, {\\\"error\\\": \\\"ttl_exceeded\\\"})\\n return\\n if SHA256_RE.fullmatch(expected_sha) is None:\\n status = 400\\n self._json(status, {\\\"error\\\": \\\"invalid_sha256\\\"})\\n return\\n if SHA256_RE.fullmatch(provided_sig) is None:\\n status = 403\\n self._json(status, {\\\"error\\\": \\\"invalid_signature\\\"})\\n return\\n\\n wanted_sig = signature(\\n self.gateway.secret,\\n parsed.path,\\n expiry,\\n expected_sha,\\n )\\n if not secrets.compare_digest(provided_sig, wanted_sig):\\n status = 403\\n self._json(status, {\\\"error\\\": \\\"invalid_signature\\\"})\\n return\\n\\n minio_base = str(self.gateway.config[\\\"minio_base\\\"]).rstrip(\\\"/\\\")\\n bucket = str(self.gateway.config[\\\"bucket\\\"])\\n object_url = (\\n minio_base\\n + \\\"/\\\"\\n + urllib.parse.quote(bucket, safe=\\\"\\\")\\n + \\\"/\\\"\\n + urllib.parse.quote(key, safe=\\\"/._-\\\")\\n )\\n request = urllib.request.Request(\\n object_url,\\n method=\\\"GET\\\",\\n headers={\\n \\\"User-Agent\\\": \\\"evidence-gateway/1\\\",\\n \\\"Accept\\\": \\\"application/octet-stream\\\",\\n },\\n )\\n\\n max_bytes = int(self.gateway.config[\\\"max_object_bytes\\\"])\\n tmp_dir = str(self.gateway.config[\\\"tmp_dir\\\"])\\n digest = hashlib.sha256()\\n\\n try:\\n response = urllib.request.urlopen(\\n request,\\n timeout=float(\\n self.gateway.config[\\\"minio_timeout_seconds\\\"]\\n ),\\n )\\n except urllib.error.HTTPError as exc:\\n if exc.code == 404:\\n status = 404\\n self._json(status, {\\\"error\\\": \\\"not_found\\\"})\\n elif exc.code == 403:\\n status = 502\\n self._json(status, {\\\"error\\\": \\\"upstream_denied\\\"})\\n else:\\n status = 502\\n self._json(status, {\\\"error\\\": \\\"upstream_error\\\"})\\n return\\n except Exception:\\n status = 502\\n self._json(status, {\\\"error\\\": \\\"upstream_unavailable\\\"})\\n return\\n\\n content_length = response.headers.get(\\\"Content-Length\\\")\\n if content_length is not None:\\n try:\\n if int(content_length) > max_bytes:\\n response.close()\\n status = 413\\n self._json(status, {\\\"error\\\": \\\"object_too_large\\\"})\\n return\\n except ValueError:\\n response.close()\\n status = 502\\n self._json(status, {\\\"error\\\": \\\"invalid_upstream_length\\\"})\\n return\\n\\n with tempfile.NamedTemporaryFile(\\n mode=\\\"w+b\\\",\\n dir=tmp_dir,\\n prefix=\\\"download-\\\",\\n delete=True,\\n ) as temporary:\\n total = 0\\n while True:\\n block = response.read(1024 * 1024)\\n if not block:\\n break\\n total += len(block)\\n if total > max_bytes:\\n response.close()\\n status = 413\\n self._json(status, {\\\"error\\\": \\\"object_too_large\\\"})\\n return\\n digest.update(block)\\n temporary.write(block)\\n response.close()\\n\\n actual_sha = digest.hexdigest()\\n if not secrets.compare_digest(actual_sha, expected_sha):\\n status = 502\\n self._json(status, {\\\"error\\\": \\\"sha256_mismatch\\\"})\\n return\\n\\n size = total\\n temporary.flush()\\n temporary.seek(0)\\n\\n self.send_response(200)\\n self.send_header(\\n \\\"Content-Type\\\",\\n \\\"application/octet-stream\\\",\\n )\\n self.send_header(\\\"Content-Length\\\", str(total))\\n self.send_header(\\n \\\"Content-Disposition\\\",\\n 'attachment; filename=\\\"evidence.bin\\\"',\\n )\\n self.send_header(\\\"ETag\\\", f'\\\"sha256:{actual_sha}\\\"')\\n security_headers(self)\\n self.end_headers()\\n\\n if self.command != \\\"HEAD\\\":\\n while True:\\n block = temporary.read(1024 * 1024)\\n if not block:\\n break\\n self.wfile.write(block)\\n\\n status = 200\\n finally:\\n self.gateway.slots.release()\\n self._event(\\n status=status,\\n key=key,\\n size=size,\\n started=started,\\n )\\n\\n\\ndef validate_config(config: dict[str, Any]) -> None:\\n required = {\\n \\\"bind\\\",\\n \\\"port\\\",\\n \\\"minio_base\\\",\\n \\\"bucket\\\",\\n \\\"max_ttl_seconds\\\",\\n \\\"max_object_bytes\\\",\\n \\\"max_concurrent_downloads\\\",\\n \\\"minio_timeout_seconds\\\",\\n \\\"tmp_dir\\\",\\n \\\"secret_file\\\",\\n }\\n if set(config) != required:\\n raise RuntimeError(\\\"configuration keys mismatch\\\")\\n if config[\\\"bind\\\"] != \\\"127.0.0.1\\\":\\n raise RuntimeError(\\\"gateway must bind to loopback\\\")\\n if not (1 <= int(config[\\\"port\\\"]) <= 65535):\\n raise RuntimeError(\\\"invalid port\\\")\\n if not str(config[\\\"minio_base\\\"]).startswith(\\\"http://[PRIVATE_IP]:9002\\\"):\\n raise RuntimeError(\\\"unexpected MinIO endpoint\\\")\\n validate_key(\\\"selftest/object.txt\\\")\\n\\n\\ndef self_test() -> None:\\n secret = [REDACTED]\\n key = \\\"commands/CMD-123/evidence.tgz\\\"\\n path = canonical_path(key)\\n assert path == \\\"/e/commands/CMD-123/evidence.tgz\\\"\\n sig = signature(secret, path, 2000000000, \\\"a\\\" * 64)\\n assert len(sig) == 64\\n assert validate_key(key) == key\\n for bad in (\\\"\\\", \\\"../x\\\", \\\"a//b\\\", \\\"/a\\\", \\\"a/%2F\\\"):\\n try:\\n validate_key(bad)\\n except ValueError:\\n pass\\n else:\\n raise AssertionError(f\\\"invalid key accepted: {bad!r}\\\")\\n print(\\\"SELF_TEST=PASS\\\")\\n\\n\\ndef main() -> int:\\n parser = argparse.ArgumentParser()\\n parser.add_argument(\\n \\\"--config\\\",\\n default=\\\"/etc/evidence-gw/config.json\\\",\\n )\\n parser.add_argument(\\\"--self-test\\\", action=\\\"store_true\\\")\\n args = parser.parse_args()\\n\\n if args.self_test:\\n self_test()\\n return 0\\n\\n config = load_json(Path(args.config))\\n validate_config(config)\\n\\n secret_file = Path(str(config[\\\"secret_file\\\"]))\\n secret = [REDACTED]\\n if len(secret) != 32:\\n raise RuntimeError(\\\"gateway secret must be 32 bytes\\\")\\n\\n tmp_dir = Path(str(config[\\\"tmp_dir\\\"]))\\n tmp_dir.mkdir(parents=True, exist_ok=True)\\n\\n server = GatewayServer(\\n (str(config[\\\"bind\\\"]), int(config[\\\"port\\\"])),\\n Handler,\\n config=config,\\n secret=[REDACTED]\\n )\\n server.serve_forever(poll_interval=0.5)\\n return 0\\n\\n\\nif __name__ == \\\"__main__\\\":\\n raise SystemExit(main())\\n\",\"script_source_sha256\":\"27de57be0924fe8d86d38879b3bcdba8d4ade817b3a42219dd65d8aacc9601cb\",\"signer_sha256\":\"523490816d058eddf1273e73cc58ca41db1ad3da24092bc8f9831823ef27c6b1\",\"signer_sha_rc\":0,\"unit\":\"evidence-gw.service\",\"unit_cat_rc\":0,\"unit_show\":\"MainPID=2384852\\nResult=success\\nNRestarts=0\\nExecMainStatus=0\\nExecStart={ path=/usr/bin/python3 ; argv[]=/usr/bin/python3 /opt/stacks/evidence-gw/gateway.py --config /etc/evidence-gw/config.json ; ignore_errors=no ; start_time=[Sat 2026-07-25 22:06:01 MSK] ; stop_time=[n/a] ; pid=2384852 ; code=(null) ; status=0/0 }\\nUser=evidence-gw\\nGroup=evidence-gw\\nId=evidence-gw.service\\nLoadState=loaded\\nActiveState=active\\nSubState=running\\nFragmentPath=/etc/systemd/system/evidence-gw.service\\nUnitFileState=enabled\\n\",\"unit_show_rc\":0,\"unit_source_redacted\":\"# /etc/systemd/system/evidence-gw.service\\n[Unit]\\nDescription=Homelab Evidence Gateway\\nAfter=network-online.target\\nWants=network-online.target\\n\\n[Service]\\nType=simple\\nUser=evidence-gw\\nGroup=evidence-gw\\nExecStart=/usr/bin/python3 /opt/stacks/evidence-gw/gateway.py --config /etc/evidence-gw/config.json\\nRestart=on-failure\\nRestartSec=2s\\nTimeoutStartSec=30s\\nTimeoutStopSec=15s\\nUMask=0077\\nNoNewPrivileges=yes\\nPrivateTmp=yes\\nPrivateDevices=yes\\nProtectSystem=strict\\nProtectHome=yes\\nProtectKernelTunables=yes\\nProtectKernelModules=yes\\nProtectControlGroups=yes\\nLockPersonality=yes\\nMemoryDenyWriteExecute=yes\\nRestrictSUIDSGID=yes\\nRestrictRealtime=yes\\nRemoveIPC=yes\\nReadWritePaths=/var/lib/evidence-gw\\nCapabilityBoundingSet=\\nAmbientCapabilities=\\nLimitNOFILE=1024\\n\\n[Install]\\nWantedBy=multi-user.target\\n\",\"unit_source_sha256\":\"8af420c05d185181bee0d6847271b4947db2b1dddf510541ae40027c5ba34026\"},\"local_integrity\":{\"/etc/systemd/system/homelab-command-adapter.service\":{\"bytes\":665,\"exists\":true,\"gid\":0,\"is_symlink\":false,\"mode\":\"0644\",\"owner\":\"root\",\"path\":\"/etc/systemd/system/homelab-command-adapter.service\",\"resolved_path\":\"/etc/systemd/system/homelab-command-adapter.service\",\"sha256\":\"74d0f37878652ed9d46b53ae8556bbb30ebd683741f293c3f59b3527876ed2d7\",\"uid\":0},\"/etc/systemd/system/homelab-command-portal.service\":{\"bytes\":981,\"exists\":true,\"gid\":0,\"is_symlink\":false,\"mode\":\"0644\",\"owner\":\"root\",\"path\":\"/etc/systemd/system/homelab-command-portal.service\",\"resolved_path\":\"/etc/systemd/system/homelab-command-portal.service\",\"sha256\":\"8453eab30f5c3ccd285fc9f70fff18f564809bb27f7bf84f51f2ba593aa85263\",\"uid\":0},\"/usr/local/sbin/homelab-chat-run\":{\"bytes\":11864,\"exists\":true,\"gid\":0,\"is_symlink\":false,\"mode\":\"0750\",\"owner\":\"root\",\"path\":\"/usr/local/sbin/homelab-chat-run\",\"resolved_path\":\"/usr/local/sbin/homelab-chat-run\",\"sha256\":\"2a88defef9517f89bc60934dce82097f8e316b08af9417e95167ea5d759a10d9\",\"uid\":0},\"/usr/local/sbin/homelab-command-adapter\":{\"bytes\":77441,\"exists\":true,\"gid\":0,\"is_symlink\":false,\"mode\":\"0750\",\"owner\":\"root\",\"path\":\"/usr/local/sbin/homelab-command-adapter\",\"resolved_path\":\"/usr/local/sbin/homelab-command-adapter\",\"sha256\":\"f2410cd328e2837b99e8ce4a28e25cb031a6bdd596d0286ac77a1822ef1cecd0\",\"uid\":0},\"/usr/local/sbin/homelab-command-portal\":{\"bytes\":39362,\"exists\":true,\"gid\":990,\"is_symlink\":false,\"mode\":\"0750\",\"owner\":\"root\",\"path\":\"/usr/local/sbin/homelab-command-portal\",\"resolved_path\":\"/usr/local/sbin/homelab-command-portal\",\"sha256\":\"8056faf4ef6348f0b06319ad3c49fbfeeadf25b6370f857ea19569bd27652ee2\",\"uid\":0},\"/usr/local/sbin/homelab-vm130-exec\":{\"bytes\":1333,\"exists\":true,\"gid\":0,\"is_symlink\":false,\"mode\":\"0750\",\"owner\":\"root\",\"path\":\"/usr/local/sbin/homelab-vm130-exec\",\"resolved_path\":\"/usr/local/sbin/homelab-vm130-exec\",\"sha256\":\"1959773393723b013af7c93cd29a7b833fd607c903d044e2fcef6accaa29c275\",\"uid\":0}},\"preplan_basis_sha256\":\"6ae59d4b7877710758ecb8e315a8322e71c475b6dcda680864eff8c1f7e7e0d9\",\"receiver\":{\"anchors\":[{\"line\":45,\"text\":\"flock -w 60 9 || {\"},{\"line\":121,\"text\":\" \\\"$STAGE_DIR/latest.json\\\"\"},{\"line\":128,\"text\":\" \\\"$STAGE_DIR/latest.txt\\\"\"},{\"line\":170,\"text\":\" refs/remotes/origin/assistant-runtime\"},{\"line\":174,\"text\":\" -B assistant-runtime \\\\\"},{\"line\":175,\"text\":\" origin/assistant-runtime\"},{\"line\":179,\"text\":\" --orphan assistant-runtime\"},{\"line\":184,\"text\":\"mkdir -p runtime/history\"},{\"line\":186,\"text\":\"cp \\\"$STAGE/latest.json\\\" runtime/latest.json\"},{\"line\":187,\"text\":\"cp \\\"$STAGE/latest.txt\\\" runtime/latest.txt\"},{\"line\":188,\"text\":\"cp \\\"$STAGE/latest.json\\\" \\\"runtime/history/${COMMAND_ID}.json\\\"\"},{\"line\":189,\"text\":\"cp \\\"$STAGE/latest.txt\\\" \\\"runtime/history/${COMMAND_ID}.txt\\\"\"},{\"line\":192,\"text\":\"git add \\\\\"},{\"line\":194,\"text\":\" runtime/latest.json \\\\\"},{\"line\":195,\"text\":\" runtime/latest.txt \\\\\"},{\"line\":196,\"text\":\" \\\"runtime/history/${COMMAND_ID}.json\\\"\"},{\"line\":198,\"text\":\"git add \\\"runtime/history/${COMMAND_ID}.txt\\\"\"},{\"line\":204,\"text\":\" git commit \\\\\"},{\"line\":210,\"text\":\"GIT_ASKPASS=/data/gitea/runtime-auth/askpass GIT_TERMINAL_PROMPT=0 git push --quiet http://127.0.0.1:3000/homelab-admin/homelab-public-context.git HEAD:refs/heads/assistant-runtime\"},{\"line\":219,\"text\":\"JSON_TMP=\\\"$(mktemp \\\"${PUBLIC_DIR}/.latest.json.XXXXXX\\\")\\\"\"},{\"line\":220,\"text\":\"TXT_TMP=\\\"$(mktemp \\\"${PUBLIC_DIR}/.latest.txt.XXXXXX\\\")\\\"\"},{\"line\":238,\"text\":\" \\\"branch\\\": \\\"assistant-runtime\\\",\"},{\"line\":244,\"text\":\" \\\"homelab-runtime/latest.json\\\"\"},{\"line\":248,\"text\":\" \\\"homelab-runtime/latest.txt\\\"\"},{\"line\":255,\"text\":\"mv -f \\\"$JSON_TMP\\\" \\\"$PUBLIC_DIR/latest.json\\\"\"},{\"line\":256,\"text\":\"mv -f \\\"$TXT_TMP\\\" \\\"$PUBLIC_DIR/latest.txt\\\"\"},{\"line\":257,\"text\":\"mv -f \\\"$INDEX_TMP\\\" \\\"$PUBLIC_DIR/index.json\\\"\"},{\"line\":261,\"text\":\" http://127.0.0.1:3002/.well-known/homelab-runtime/latest.json |\"},{\"line\":266,\"text\":\" \\\"http://127.0.0.1:3002/homelab-admin/homelab-public-context/raw/branch/assistant-runtime/runtime/latest.json\\\" |\"}],\"features\":{\"has_atomic_replace\":true,\"has_by_command\":false,\"has_flock\":true,\"has_git_commit\":true,\"has_git_push\":true,\"has_latest_json\":true,\"has_latest_txt\":true},\"path\":\"/usr/local/sbin/homelab-runtime-receive\",\"sha256\":\"6f4eadda35241d984c30084430f60c456e943d3b17fea6b8c82cf3d5d6a318f4\",\"sha_rc\":0,\"source_bytes\":6549,\"source_rc\":0,\"source_redacted\":\"#!/usr/bin/env bash\\nset -Eeuo pipefail\\numask 027\\n\\nHOST_DATA='/opt/stacks/gitea/data'\\nBARE_CONTAINER='/data/git/repositories/homelab-admin/homelab-public-context.git'\\n\\nPUBLIC_DIR=\\\"${HOST_DATA}/gitea/public/.well-known/homelab-runtime\\\"\\nSTAGE_DIR=\\\"${HOST_DATA}/gitea/runtime-stage\\\"\\nWORK_ROOT=\\\"${HOST_DATA}/gitea/runtime-work\\\"\\n\\nLOCK='/run/lock/homelab-runtime-receive.lock'\\n\\n[ \\\"$#\\\" -eq 3 ] || {\\n echo 'RESULT=FAIL'\\n echo 'REASON=USAGE'\\n exit 64\\n}\\n\\nCOMMAND_ID=\\\"$1\\\"\\nJSON_FILE=\\\"$2\\\"\\nTXT_FILE=\\\"$3\\\"\\n\\ncase \\\"$COMMAND_ID\\\" in\\n ''|*[!A-Za-z0-9._-]*)\\n echo 'RESULT=FAIL'\\n echo 'REASON=INVALID_COMMAND_ID'\\n exit 65\\n ;;\\nesac\\n\\n[ -f \\\"$JSON_FILE\\\" ] || {\\n echo 'RESULT=FAIL'\\n echo 'REASON=JSON_FILE_MISSING'\\n exit 66\\n}\\n\\n[ -f \\\"$TXT_FILE\\\" ] || {\\n echo 'RESULT=FAIL'\\n echo 'REASON=TXT_FILE_MISSING'\\n exit 67\\n}\\n\\nexec 9>\\\"$LOCK\\\"\\nflock -w 60 9 || {\\n echo 'RESULT=FAIL'\\n echo 'REASON=RECEIVER_LOCK_TIMEOUT'\\n exit 68\\n}\\n\\npython3 - \\\"$JSON_FILE\\\" \\\"$TXT_FILE\\\" \\\"$COMMAND_ID\\\" <<'PY'\\nimport json\\nimport re\\nimport sys\\nfrom pathlib import Path\\n\\njson_path = Path(sys.argv[1])\\ntext_path = Path(sys.argv[2])\\nexpected_id = sys.argv[3]\\n\\nif json_path.stat().st_size > 524_288:\\n raise SystemExit(\\\"JSON_TOO_LARGE\\\")\\n\\nif text_path.stat().st_size > 1_048_576:\\n raise SystemExit(\\\"TEXT_TOO_LARGE\\\")\\n\\njson_text = json_path.read_text(encoding=\\\"utf-8\\\")\\ntext = text_path.read_text(encoding=\\\"utf-8\\\")\\n\\ndata = json.loads(json_text)\\n\\nif data.get(\\\"command_id\\\") != expected_id:\\n raise SystemExit(\\\"COMMAND_ID_MISMATCH\\\")\\n\\nif data.get(\\\"sanitized\\\") is not True:\\n raise SystemExit(\\\"SANITIZED_FLAG_REQUIRED\\\")\\n\\nif data.get(\\\"secrets_included\\\") is not False:\\n raise SystemExit(\\\"SECRETS_FLAG_INVALID\\\")\\n\\nif data.get(\\\"private_addresses_included\\\") is not False:\\n raise SystemExit(\\\"PRIVATE_ADDRESS_FLAG_INVALID\\\")\\n\\ncombined = json_text + \\\"\\\\n\\\" + text\\n\\npatterns = [\\n r\\\"-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----\\\",\\n r\\\"(?i)(password|passwd|passphrase|authorization|api[_-]?key|access[_-]?token|refresh[_-]?token|private[_-]?key)\\\\s*[:=]\\\\s*\\\\S+\\\",\\n r\\\"(?i)/root/\\\\.ssh\\\",\\n r\\\"(?i)[SENSITIVE_PATH]\\\",\\n r\\\"(?i)rclone\\\\.conf\\\",\\n r\\\"\\\\b10(?:\\\\.\\\\d{1,3}){3}\\\\b\\\",\\n r\\\"\\\\b192\\\\.168(?:\\\\.\\\\d{1,3}){2}\\\\b\\\",\\n r\\\"\\\\b172\\\\.(?:1[6-9]|2\\\\d|3[01])(?:\\\\.\\\\d{1,3}){2}\\\\b\\\",\\n]\\n\\nfor pattern in patterns:\\n if re.search(pattern, combined):\\n raise SystemExit(\\\"FORBIDDEN_PUBLIC_CONTENT\\\")\\n\\nprint(\\\"PAYLOAD_VALIDATION=OK\\\")\\nPY\\n\\nGIT_UID=\\\"$(docker exec gitea id -u git)\\\"\\nGIT_GID=\\\"$(docker exec gitea id -g git)\\\"\\n\\nmkdir -p \\\\\\n \\\"$PUBLIC_DIR\\\" \\\\\\n \\\"$STAGE_DIR\\\" \\\\\\n \\\"$WORK_ROOT\\\"\\n\\nchown \\\"$GIT_UID:$GIT_GID\\\" \\\\\\n \\\"$STAGE_DIR\\\" \\\\\\n \\\"$WORK_ROOT\\\"\\n\\ninstall \\\\\\n -o \\\"$GIT_UID\\\" \\\\\\n -g \\\"$GIT_GID\\\" \\\\\\n -m 0644 \\\\\\n \\\"$JSON_FILE\\\" \\\\\\n \\\"$STAGE_DIR/latest.json\\\"\\n\\ninstall \\\\\\n -o \\\"$GIT_UID\\\" \\\\\\n -g \\\"$GIT_GID\\\" \\\\\\n -m 0644 \\\\\\n \\\"$TXT_FILE\\\" \\\\\\n \\\"$STAGE_DIR/latest.txt\\\"\\n\\ncat > \\\"$STAGE_DIR/README.md\\\" <<'DOC'\\n# Homelab assistant runtime\\n\\nSanitized command results for read-only assistant inspection.\\n\\nCredentials, authentication data, private keys, private IP addresses and\\nunfiltered configuration files are prohibited.\\nDOC\\n\\nchown \\\"$GIT_UID:$GIT_GID\\\" \\\"$STAGE_DIR/README.md\\\"\\nchmod 0644 \\\"$STAGE_DIR/README.md\\\"\\n\\nPUBLISHED_AT=\\\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\\\"\\nWORK_CONTAINER=\\\"/data/gitea/runtime-work/${COMMAND_ID}.$$\\\"\\n\\nCOMMIT_SHA=\\\"$(\\n docker exec \\\\\\n -u \\\"${GIT_UID}:${GIT_GID}\\\" \\\\\\n -e COMMAND_ID=\\\"$COMMAND_ID\\\" \\\\\\n -e PUBLISHED_AT=\\\"$PUBLISHED_AT\\\" \\\\\\n -e WORK_CONTAINER=\\\"$WORK_CONTAINER\\\" \\\\\\n gitea sh -lc '\\nset -eu\\n\\nBARE=\\\"/data/git/repositories/homelab-admin/homelab-public-context.git\\\"\\nSTAGE=\\\"/data/gitea/runtime-stage\\\"\\nWORK=\\\"$WORK_CONTAINER\\\"\\n\\nrm -rf \\\"$WORK\\\"\\n\\ngit clone --quiet \\\"$BARE\\\" \\\"$WORK\\\"\\n\\ncd \\\"$WORK\\\"\\n\\ngit config user.name \\\"homelab-runtime-publisher\\\"\\ngit config user.email \\\"homelab-runtime@localhost\\\"\\n\\nif git show-ref \\\\\\n --verify \\\\\\n --quiet \\\\\\n refs/remotes/origin/assistant-runtime\\nthen\\n git checkout \\\\\\n --quiet \\\\\\n -B assistant-runtime \\\\\\n origin/assistant-runtime\\nelse\\n git checkout \\\\\\n --quiet \\\\\\n --orphan assistant-runtime\\n\\n git rm -rf . >/dev/null 2>&1 || true\\nfi\\n\\nmkdir -p runtime/history\\n\\ncp \\\"$STAGE/latest.json\\\" runtime/latest.json\\ncp \\\"$STAGE/latest.txt\\\" runtime/latest.txt\\ncp \\\"$STAGE/latest.json\\\" \\\"runtime/history/${COMMAND_ID}.json\\\"\\ncp \\\"$STAGE/latest.txt\\\" \\\"runtime/history/${COMMAND_ID}.txt\\\"\\ncp \\\"$STAGE/README.md\\\" README.md\\n\\ngit add \\\\\\n README.md \\\\\\n runtime/latest.json \\\\\\n runtime/latest.txt \\\\\\n \\\"runtime/history/${COMMAND_ID}.json\\\"\\n\\ngit add \\\"runtime/history/${COMMAND_ID}.txt\\\"\\n\\nif ! git diff --cached --quiet\\nthen\\n GIT_AUTHOR_DATE=\\\"$PUBLISHED_AT\\\" \\\\\\n GIT_COMMITTER_DATE=\\\"$PUBLISHED_AT\\\" \\\\\\n git commit \\\\\\n --quiet \\\\\\n -m \\\"runtime: publish ${COMMAND_ID}\\\"\\nfi\\n\\nCOMMIT=\\\"$(git rev-parse HEAD)\\\"\\nGIT_ASKPASS=/data/gitea/runtime-auth/askpass GIT_TERMINAL_PROMPT=0 git push --quiet http://127.0.0.1:3000/homelab-admin/homelab-public-context.git HEAD:refs/heads/assistant-runtime\\n\\n\\nrm -rf \\\"$WORK\\\"\\n\\nprintf \\\"%s\\\\n\\\" \\\"$COMMIT\\\"\\n'\\n)\\\"\\n\\nJSON_TMP=\\\"$(mktemp \\\"${PUBLIC_DIR}/.latest.json.XXXXXX\\\")\\\"\\nTXT_TMP=\\\"$(mktemp \\\"${PUBLIC_DIR}/.latest.txt.XXXXXX\\\")\\\"\\nINDEX_TMP=\\\"$(mktemp \\\"${PUBLIC_DIR}/.index.json.XXXXXX\\\")\\\"\\n\\ninstall -m 0644 \\\"$JSON_FILE\\\" \\\"$JSON_TMP\\\"\\ninstall -m 0644 \\\"$TXT_FILE\\\" \\\"$TXT_TMP\\\"\\n\\npython3 - \\\\\\n \\\"$COMMAND_ID\\\" \\\\\\n \\\"$PUBLISHED_AT\\\" \\\\\\n \\\"$COMMIT_SHA\\\" >\\\"$INDEX_TMP\\\" <<'PY'\\nimport json\\nimport sys\\n\\ncommand_id, published_at, commit_sha = sys.argv[1:4]\\n\\nprint(json.dumps({\\n \\\"schema_version\\\": 1,\\n \\\"channel\\\": \\\"homelab-runtime\\\",\\n \\\"branch\\\": \\\"assistant-runtime\\\",\\n \\\"latest_command_id\\\": command_id,\\n \\\"published_at_utc\\\": published_at,\\n \\\"commit_sha\\\": commit_sha,\\n \\\"latest_json_url\\\": (\\n \\\"https://git.gram1.ru/.well-known/\\\"\\n \\\"homelab-runtime/latest.json\\\"\\n ),\\n \\\"latest_text_url\\\": (\\n \\\"https://git.gram1.ru/.well-known/\\\"\\n \\\"homelab-runtime/latest.txt\\\"\\n )\\n}, ensure_ascii=False, indent=2))\\nPY\\n\\nchmod 0644 \\\"$INDEX_TMP\\\"\\n\\nmv -f \\\"$JSON_TMP\\\" \\\"$PUBLIC_DIR/latest.json\\\"\\nmv -f \\\"$TXT_TMP\\\" \\\"$PUBLIC_DIR/latest.txt\\\"\\nmv -f \\\"$INDEX_TMP\\\" \\\"$PUBLIC_DIR/index.json\\\"\\n\\ncurl -fsS \\\\\\n --max-time 10 \\\\\\n http://127.0.0.1:3002/.well-known/homelab-runtime/latest.json |\\ngrep -F \\\"$COMMAND_ID\\\" >/dev/null\\n\\ncurl -fsS \\\\\\n --max-time 10 \\\\\\n \\\"http://127.0.0.1:3002/homelab-admin/homelab-public-context/raw/branch/assistant-runtime/runtime/latest.json\\\" |\\ngrep -F \\\"$COMMAND_ID\\\" >/dev/null\\n\\necho 'RESULT=OK'\\necho \\\"COMMAND_ID=$COMMAND_ID\\\"\\necho \\\"COMMIT_SHA=$COMMIT_SHA\\\"\\necho 'STATIC_PUBLICATION=OK'\\necho 'RAW_BRANCH_PUBLICATION=OK'\\n\",\"source_sha256\":\"6f4eadda35241d984c30084430f60c456e943d3b17fea6b8c82cf3d5d6a318f4\",\"stat\":\"=root:root:750:6549\",\"stat_rc\":0,\"stat_stderr\":\"\",\"sudo_install_help_excerpt\":\"Usage: /usr/bin/install [OPTION]... [-T] SOURCE DEST\\n or: /usr/bin/install [OPTION]... SOURCE... DIRECTORY\\n or: /usr/bin/install [OPTION]... -t DIRECTORY SOURCE...\\n or: /usr/bin/install [OPTION]... -d DIRECTORY...\\n\\nThis install program copies files (often just compiled) into destination\\nlocations you choose. If you want to download and install a ready-to-use\\npackage on a GNU/Linux system, you should instead be using a package manager\\nlike yum(1) or apt-get(1).\\n\\nIn the first three forms, copy SOURCE to DEST or multiple SOURCE(s) to\\nthe existing DIRECTORY, while setting permission modes and owner/group.\\nIn the 4th form, create all components of the given DIRECTORY(ies).\\n\\nMandatory arguments to long options are mandatory for short options too.\\n --backup[=CONTROL] make a backup of each existing destination file\\n -b like --backup but does not accept an argument\\n -c (ignored)\\n -C, --compare compare content of source and destination file\",\"sudo_install_help_rc\":0},\"report_sha256\":\"319d433bb77302d92977e66bec320130c69eca85e70e05f6f8350bba4f1a7d6b\",\"rollback_started\":false,\"runner\":{\"anchors\":[{\"line\":117,\"text\":\"TXT_FILE=\\\"$PUBLIC_DIR/${COMMAND_ID}-${STAMP}-latest.txt\\\"\"},{\"line\":406,\"text\":\" \\\"homelab-runtime/latest.txt\\\"\"},{\"line\":482,\"text\":\" sudo -n /usr/local/sbin/homelab-runtime-receive \\\\\"},{\"line\":496,\"text\":\"PUBLIC_URL='https://git.gram1.ru/.well-known/homelab-runtime/latest.json'\"},{\"line\":503,\"text\":\" \\\"$PUBLIC_URL\\\" 2>/dev/null ||\"},{\"line\":540,\"text\":\"echo 'GITEA_PUBLICATION=OK'\"},{\"line\":541,\"text\":\"echo \\\"PUBLIC_URL=$PUBLIC_URL\\\"\"}],\"bytes\":11864,\"features\":{\"has_by_command\":false,\"latest_json_authoritative\":true,\"latest_txt_authoritative\":true,\"public_visibility_retry\":true,\"receiver_call\":true},\"sha256\":\"2a88defef9517f89bc60934dce82097f8e316b08af9417e95167ea5d759a10d9\"},\"schema\":1,\"source_lock\":{\"edge_signer_sha256\":\"523490816d058eddf1273e73cc58ca41db1ad3da24092bc8f9831823ef27c6b1\",\"gateway_script_path\":\"/opt/stacks/evidence-gw/gateway.py\",\"gateway_script_sha256\":\"27de57be0924fe8d86d38879b3bcdba8d4ade817b3a42219dd65d8aacc9601cb\",\"gateway_unit\":\"evidence-gw.service\",\"gateway_unit_sha256\":\"8af420c05d185181bee0d6847271b4947db2b1dddf510541ae40027c5ba34026\",\"local\":{\"/etc/systemd/system/homelab-command-adapter.service\":\"74d0f37878652ed9d46b53ae8556bbb30ebd683741f293c3f59b3527876ed2d7\",\"/etc/systemd/system/homelab-command-portal.service\":\"8453eab30f5c3ccd285fc9f70fff18f564809bb27f7bf84f51f2ba593aa85263\",\"/usr/local/sbin/homelab-chat-run\":\"2a88defef9517f89bc60934dce82097f8e316b08af9417e95167ea5d759a10d9\",\"/usr/local/sbin/homelab-command-adapter\":\"f2410cd328e2837b99e8ce4a28e25cb031a6bdd596d0286ac77a1822ef1cecd0\",\"/usr/local/sbin/homelab-command-portal\":\"8056faf4ef6348f0b06319ad3c49fbfeeadf25b6370f857ea19569bd27652ee2\",\"/usr/local/sbin/homelab-vm130-exec\":\"1959773393723b013af7c93cd29a7b833fd607c903d044e2fcef6accaa29c275\"},\"receiver_sha256\":\"6f4eadda35241d984c30084430f60c456e943d3b17fea6b8c82cf3d5d6a318f4\"},\"stage\":\"RUN7A_DELIVERY_PREPLAN_DISCOVERY\",\"state_inventory\":{\"counts\":{\"FAIL\":4,\"IMPORTED\":1,\"OK\":4,\"RUNNING\":1},\"diagnostic_failure_count\":4,\"duplicates\":{},\"stale_running\":[],\"unresolved_mutating\":[]},\"status\":\"PASS\",\"units\":{\"adapter\":{\"ActiveState\":\"active\",\"ExecMainStatus\":\"0\",\"ExecStart\":\"{ path=/usr/bin/python3 ; argv[]=/usr/bin/python3 -B /usr/local/sbin/homelab-command-adapter --serve ; ignore_errors=no ; start_time=[n/a] ; stop_time=[n/a] ; pid=364681 ; code=(null) ; status=0/0 }\",\"FragmentPath\":\"/etc/systemd/system/homelab-command-adapter.service\",\"Group\":\"homelab-command-portal\",\"Id\":\"homelab-command-adapter.service\",\"LoadState\":\"loaded\",\"MainPID\":\"364681\",\"NRestarts\":\"0\",\"Result\":\"success\",\"SubState\":\"running\",\"UnitFileState\":\"enabled\",\"User\":\"root\",\"show_rc\":0},\"portal\":{\"ActiveState\":\"active\",\"ExecMainStatus\":\"0\",\"ExecStart\":\"{ path=/usr/bin/python3 ; argv[]=/usr/bin/python3 -B /usr/local/sbin/homelab-command-portal ; ignore_errors=no ; start_time=[Sun 2026-07-26 09:18:49 MSK] ; stop_time=[n/a] ; pid=536586 ; code=(null) ; status=0/0 }\",\"FragmentPath\":\"/etc/systemd/system/homelab-command-portal.service\",\"Group\":\"homelab-command-portal\",\"Id\":\"homelab-command-portal.service\",\"LoadState\":\"loaded\",\"MainPID\":\"536586\",\"NRestarts\":\"0\",\"Result\":\"success\",\"SubState\":\"running\",\"UnitFileState\":\"enabled\",\"User\":\"homelab-command-portal\",\"show_rc\":0}},\"warnings\":[]}\n" + "output": "RUN7P1_IMMUTABLE_RESULT_PLAN=BLOCKED\nPLAN_SHA256=40dee3f5a655b78fb38477b146c651f0c9a46a56111211876f56df41538c74ca\n{\"PLAN_SHA256\":\"40dee3f5a655b78fb38477b146c651f0c9a46a56111211876f56df41538c74ca\",\"acceptance_requires_separate_capsule\":true,\"anti_regression\":[\"AR-P1-001 stale latest cannot satisfy runner\",\"AR-P1-002 exact command_id and command_sha256 required\",\"AR-P1-003 exact command TXT SHA required\",\"AR-P1-004 context-auto cannot overwrite by-command result\",\"AR-P1-005 same ID same payload is idempotent\",\"AR-P1-006 same ID different payload is rejected\",\"AR-P1-007 JSON is readiness marker published after TXT\",\"AR-P1-008 sanitizer does not cross line boundaries\",\"AR-P1-009 public payload remains sanitized and address-free\",\"AR-P1-010 temporary files are cleaned\"],\"backup\":{\"core_apps\":\"/var/lib/homelab-runtime-receive/backups/homelab-runtime-receive.25b1d3ddb71ea76fa8a067c4ab97e5f2a17c6dd7657440e74517647ab25ae72c\",\"exact_sha_required\":true,\"pve01\":\"/var/lib/homelab-chat-run/state/backups/homelab-chat-run.2a88defef9517f89bc60934dce82097f8e316b08af9417e95167ea5d759a10d9\"},\"blockers\":[\"REMOTE_RECEIVER_SHA_DISAGREEMENT\",\"REMOTE_SOURCE_LOCK_MISMATCH:receiver\",\"TRANSFORMATION_FAILED:RuntimeError\"],\"cleanup\":[\"remove all remote /tmp rehearsal and apply staging files\",\"retain verified rollback copies until phase seal\",\"do not remove historical runtime results\"],\"component\":\"evidence-delivery\",\"created_at_utc\":\"2026-07-26T15:54:52.541976+00:00\",\"declared_change_flags_for_apply\":{\"backup\":false,\"cluster\":false,\"gitea\":true,\"production\":true},\"exact_diff\":[\"add immutable .well-known/by-command/.json and .txt\",\"write TXT first and JSON last as readiness marker\",\"idempotent same COMMAND_ID with identical JSON/TXT\",\"fail closed on same COMMAND_ID with differing JSON/TXT\",\"prevent Git history overwrite on COMMAND_ID conflict\",\"retain latest.json/latest.txt as non-authoritative convenience\",\"runner publishes own immutable TXT URL\",\"runner polls exact immutable JSON and TXT\",\"runner verifies command_id, command_sha256 and sanitized flags\",\"fix multiline sanitizer false-positive with horizontal whitespace\",\"add persistent --self-test anti-regression entrypoints\"],\"gates\":{\"G0_context_lock\":{\"source_evidence_sha256\":\"844bd57846acb2dcb939392fd22eeae005b348bd346f3aace0bc219f6bafa38a\",\"source_preplan_basis_sha256\":\"6ae59d4b7877710758ecb8e315a8322e71c475b6dcda680864eff8c1f7e7e0d9\",\"status\":\"FAIL\"},\"G1_read_only_discovery\":{\"source_hashes\":{\"adapter\":\"f2410cd328e2837b99e8ce4a28e25cb031a6bdd596d0286ac77a1822ef1cecd0\",\"adapter_unit\":\"74d0f37878652ed9d46b53ae8556bbb30ebd683741f293c3f59b3527876ed2d7\",\"portal\":\"8056faf4ef6348f0b06319ad3c49fbfeeadf25b6370f857ea19569bd27652ee2\",\"portal_unit\":\"8453eab30f5c3ccd285fc9f70fff18f564809bb27f7bf84f51f2ba593aa85263\",\"receiver\":\"25b1d3ddb71ea76fa8a067c4ab97e5f2a17c6dd7657440e74517647ab25ae72c\",\"runner\":\"2a88defef9517f89bc60934dce82097f8e316b08af9417e95167ea5d759a10d9\"},\"status\":\"PASS\",\"units\":{\"adapter\":{\"active\":\"active\",\"enabled\":\"enabled\",\"group\":\"homelab-command-portal\",\"restarts\":\"0\",\"user\":\"root\"},\"portal\":{\"active\":\"active\",\"enabled\":\"enabled\",\"group\":\"homelab-command-portal\",\"restarts\":\"0\",\"user\":\"homelab-command-portal\"}}},\"G2_static_validation\":{\"status\":\"FAIL\",\"target_hashes\":{}},\"G3_identity_equivalent\":{\"current_receiver_sudo_contract_rc\":64,\"remote_install_available\":true,\"remote_mkdir_available\":true,\"status\":\"PASS\"},\"G4_integration_rehearsal\":{\"production_mutation\":false,\"results\":{\"remote_cleanup_rc\":0},\"status\":\"PASS\"}},\"kind\":\"homelab-change-plan\",\"mutation_journal\":{\"core_apps\":\"/var/lib/homelab-runtime-receive/RUN7P1-immutable-result-mutation.json\",\"pve01\":\"/var/lib/homelab-chat-run/state/RUN7P1-immutable-result-mutation.json\"},\"plan_id\":\"RUN7P1-IMMUTABLE-PUBLIC-RESULT\",\"postconditions\":[\"installed files match planned target SHA-256\",\"installed runner --self-test returns PASS\",\"installed receiver --self-test via sudo returns PASS\",\"APPLY itself completes through backward-compatible latest channel\",\"separate acceptance capsule is fetched by exact by-command URL\",\"context-auto overwrite of latest does not change acceptance result\",\"same-ID same-content receiver test is idempotent\",\"same-ID different-content receiver test fails closed\"],\"preconditions\":[\"all source SHA-256 values exactly match this plan\",\"Portal and Adapter remain active/enabled with NRestarts=0\",\"no unresolved mutating failure or rollback\",\"exact PLAN_SHA256 is embedded in APPLY capsule\",\"APPLY capsule source evidence SHA matches RUN7P1 evidence\"],\"risk\":{\"backup_subsystem_touched\":false,\"level\":\"MEDIUM\",\"reason\":\"changes public sanitized-result transport on two hosts\",\"second_runner_created\":false},\"rollback\":[\"capture diagnostics before rollback\",\"record file SHA/mode/owner, runner and receiver self-tests\",\"record exact by-command HTTP status and response SHA\",\"restore both exact source files from verified backups\",\"verify restored SHA/mode/owner\",\"do not restart unrelated services\"],\"schema\":1,\"scope\":[{\"group\":\"root\",\"host\":\"pve01\",\"mode\":\"0750\",\"owner\":\"root\",\"path\":\"/usr/local/sbin/homelab-chat-run\",\"source_sha256\":\"2a88defef9517f89bc60934dce82097f8e316b08af9417e95167ea5d759a10d9\",\"target_sha256\":null},{\"group\":\"root\",\"host\":\"core-apps\",\"mode\":\"0750\",\"owner\":\"root\",\"path\":\"/usr/local/sbin/homelab-runtime-receive\",\"source_sha256\":\"25b1d3ddb71ea76fa8a067c4ab97e5f2a17c6dd7657440e74517647ab25ae72c\",\"target_sha256\":null}],\"service_identity_test\":{\"proposed_receiver_self_test\":\"PASS\",\"proposed_runner_self_test\":\"PASS\",\"receiver_current_sudo_usage_rc\":64},\"source\":{\"live_source_hashes\":{\"core_apps_receiver\":\"25b1d3ddb71ea76fa8a067c4ab97e5f2a17c6dd7657440e74517647ab25ae72c\",\"pve01_runner\":\"2a88defef9517f89bc60934dce82097f8e316b08af9417e95167ea5d759a10d9\"},\"run7a_capsule_sha256\":\"b45c17b1b5f4ac00868c84dd66ef8b59a6033b92c9886f7f3e6c5b48ea5da5a8\",\"run7a_evidence_sha256\":\"844bd57846acb2dcb939392fd22eeae005b348bd346f3aace0bc219f6bafa38a\",\"run7a_preplan_basis_sha256\":\"6ae59d4b7877710758ecb8e315a8322e71c475b6dcda680864eff8c1f7e7e0d9\",\"run7a_runner_json_sha256\":\"7c77a1aa9f229e7d67ab7a74769bcf4dee22b2fbd474c9c909fb796123b5a7bb\"},\"target\":{\"core_apps_receiver_sha256\":null,\"pve01_runner_sha256\":null},\"warnings\":[]}\n" } diff --git a/runtime/latest.txt b/runtime/latest.txt index 986f395..3e1077a 100644 --- a/runtime/latest.txt +++ b/runtime/latest.txt @@ -1,7 +1,7 @@ CHAT_OUTPUT_BEGIN -COMMAND_ID=RUN7A-DELIVERY-PREPLAN-DISCOVERY-20260726T153105Z -STATUS=OK -RC=0 +COMMAND_ID=RUN7P1-IMMUTABLE-RESULT-PLAN-20260726T154634Z +STATUS=FAIL +RC=2 HOST=pve01 MODE=verify COMPONENT=evidence-delivery @@ -9,20 +9,19 @@ REFERENCE_REGISTER_CHECK=OK REFERENCE_SHA256=5763f2f8edc75fcf6f3951f8c95896d1cc112ceccb6cf5b64a383f540a4deb8e ERROR_REGISTER_CHECK=OK ERROR_REGISTER_SHA256=24934a2c7fa5c26f6e828583c4aefac7f143b40c0171736ed7571e617d55ab83 -COMMAND_SHA256=72e4d7cfc355fa26e684146bf7b62cece7b11d539ee4fae0ef84c613bcf36a6e +COMMAND_SHA256=317b507e77cebd2da1bfa6929e277df3bc09ecfd115d5b15d32b9705445d03ee DUPLICATE_FAILED_COMMAND_BLOCKED=false EXECUTION_STARTED=true CHANGES_MADE=false SANITIZED=yes SECRETS_INCLUDED=no PRIVATE_ADDRESSES_INCLUDED=no -RAW_EVIDENCE_SHA256=80c0aa4ae374f18ac41e5c6df736a45e2f144cf5a23db6ccf7554c5a03844a98 -SANITIZED_OUTPUT_SHA256=edac687cd4d0ffb36c68d91fbc10d7554c37707b62c4d09b09b8e88db7ce13c3 +RAW_EVIDENCE_SHA256=3e4c291c2612a19e2f249cc30480639c87a6d497643f07022f10dcec1ebe9800 +SANITIZED_OUTPUT_SHA256=3e4c291c2612a19e2f249cc30480639c87a6d497643f07022f10dcec1ebe9800 OUTPUT_BEGIN -RUN7A_DELIVERY_PREPLAN_DISCOVERY=PASS -PREPLAN_BASIS_SHA256=6ae59d4b7877710758ecb8e315a8322e71c475b6dcda680864eff8c1f7e7e0d9 -REPORT_SHA256=319d433bb77302d92977e66bec320130c69eca85e70e05f6f8350bba4f1a7d6b -{"anti_regression":{"RUN7A-AR-001-SANITIZER-LINE-BOUNDARY":{"checks":{"gateway_discovery_by_listener_not_name_guess":true,"multiline_false_positive_closed":true,"no_long_inline_remote_shell":true,"password_removed":true,"private_ip_removed":true,"remote_stat_single_argument_format":true,"userinfo_removed":true},"status":"PASS"},"RUN7A-AR-002-REMOTE-STAT-NO-SPACES":{"status":"PASS"},"RUN7A-AR-003-GATEWAY-BY-LISTENER-CGROUP":{"status":"PASS"},"RUN7A-AR-004-NO-INLINE-REMOTE-BODY":{"status":"PASS"},"RUN7A-AR-005-NO-MUTATION":{"backup_changed":false,"cluster_changed":false,"gitea_changed":false,"production_changed":false,"status":"PASS"}},"architecture":{"phase_1":{"name":"IMMUTABLE_PUBLIC_RESULT","requirements":["publish runtime/by-command/.json","publish runtime/by-command/.txt","fail closed on same COMMAND_ID with different SHA","idempotent success on same COMMAND_ID and same SHA","retain latest.json/latest.txt as non-authoritative convenience","runner polls exact immutable by-command JSON","runner verifies command_id and command_sha256","full_sanitized_output_url points to immutable TXT","atomic write, flock, git commit and git push"],"scope":["/usr/local/sbin/homelab-chat-run","/usr/local/sbin/homelab-runtime-receive"]},"phase_2":{"name":"DETERMINISTIC_FULL_EVIDENCE_RECEIPT","requirements":["deterministic receipt URL known before execution","receipt capability derived from 256-bit capsule SHA","receipt metadata uploaded only after evidence SHA verification","uniform 404 for missing, invalid and expired receipt","Cache-Control: no-store","Content-Disposition contains COMMAND_ID and short SHA","no ListBucket","no permanent MinIO credentials in Gateway","evidence SHA verified before HTTP 200"],"scope":["/usr/local/sbin/homelab-command-adapter","/opt/stacks/evidence-gw/gateway.py","evidence-gw.service"]},"phase_3":{"name":"QUALITY_GATE_PLAN_APPLY_ENFORCEMENT","requirements":["schema-v1 read-only compatibility","schema-v2 mandatory for change","exact PLAN capsule/evidence/plan SHA binding","no unresolved mutating failure","Portal displays all gate fields","Execute disabled on gate failure","diagnostics before rollback"],"scope":["/usr/local/sbin/homelab-command-adapter","/usr/local/sbin/homelab-command-portal"],"source_plan_sha256":"99fac83369ee8d5098c07cf243398d8b6b144e4fbc9ef9b39ef75436b109d607"}},"blockers":[],"changes":{"backup":false,"cluster":false,"gitea":false,"production":false},"checked_at_utc":"2026-07-26T15:33:10.356156+00:00","command_id":"RUN7A-DELIVERY-PREPLAN-DISCOVERY-20260726T153105Z","edge_gateway":{"cgroup_rc":0,"cmdline":["/usr/bin/python3","/opt/stacks/evidence-gw/gateway.py","--config","/etc/evidence-gw/config.json"],"cmdline_rc":0,"exe":"/usr/bin/python3.13","exe_rc":0,"install_help_rc":0,"listener_excerpt":"LISTEN 0 5 127.0.0.1:18093 0.0.0.0:* users:((\"python3\",pid=2384852,fd=3)) ","listener_found":true,"listener_rc":0,"nginx_rc":125,"nginx_relevant":[],"pid":2384852,"script_features":{"has_content_disposition":true,"has_expiry":true,"has_list_bucket":false,"has_no_store":true,"has_object_key":false,"has_receipt_route":false,"has_sha256":true},"script_path":"/opt/stacks/evidence-gw/gateway.py","script_sha256":"27de57be0924fe8d86d38879b3bcdba8d4ade817b3a42219dd65d8aacc9601cb","script_sha_rc":0,"script_source_bytes":14158,"script_source_rc":0,"script_source_redacted":"#!/usr/bin/env python3\nfrom __future__ import annotations\n\nimport argparse\nimport hashlib\nimport hmac\nimport http.server\nimport json\nimport os\nimport re\nimport secrets\nimport tempfile\nimport threading\nimport time\nimport urllib.error\nimport urllib.parse\nimport urllib.request\nfrom pathlib import Path\nfrom typing import Any\n\nKEY_SEGMENT = re.compile(r\"^[A-Za-z0-9._-]+$\")\nSHA256_RE = re.compile(r\"^[0-9a-f]{64}$\")\n\n\ndef load_json(path: Path) -> dict[str, Any]:\n value = json.loads(path.read_text(encoding=\"utf-8\"))\n if not isinstance(value, dict):\n raise RuntimeError(\"configuration must be a JSON object\")\n return value\n\n\ndef validate_key(key: str) -> str:\n if not key or len(key) > 512 or key.startswith(\"/\"):\n raise ValueError(\"invalid object key\")\n segments = key.split(\"/\")\n if any(\n not segment\n or segment in {\".\", \"..\"}\n or KEY_SEGMENT.fullmatch(segment) is None\n for segment in segments\n ):\n raise ValueError(\"invalid object key\")\n return key\n\n\ndef canonical_path(key: str) -> str:\n return \"/e/\" + urllib.parse.quote(\n validate_key(key),\n safe=\"/._-\",\n )\n\n\ndef signature(secret: bytes, path: str, expiry: int, sha256: str) -> str:\n message = f\"{path}\\n{expiry}\\n{sha256}\".encode(\"utf-8\")\n return hmac.new(secret, message, hashlib.sha256).hexdigest()\n\n\ndef security_headers(handler: http.server.BaseHTTPRequestHandler) -> None:\n handler.send_header(\"Cache-Control\", \"no-store, max-age=0\")\n handler.send_header(\"Pragma\", \"no-cache\")\n handler.send_header(\"X-Content-Type-Options\", \"nosniff\")\n handler.send_header(\"Referrer-Policy\", \"no-referrer\")\n handler.send_header(\"Content-Security-Policy\", \"default-src 'none'\")\n handler.send_header(\"Cross-Origin-Resource-Policy\", \"same-origin\")\n\n\nclass GatewayServer(http.server.ThreadingHTTPServer):\n daemon_threads = True\n allow_reuse_address = True\n\n def __init__(\n self,\n address: tuple[str, int],\n handler: type[http.server.BaseHTTPRequestHandler],\n *,\n config: dict[str, Any],\n secret: [REDACTED]\n ) -> None:\n super().__init__(address, handler)\n self.config = config\n self.secret = secret\n self.slots = threading.BoundedSemaphore(\n int(config[\"max_concurrent_downloads\"])\n )\n\n\nclass Handler(http.server.BaseHTTPRequestHandler):\n server_version = \"EvidenceGateway/1\"\n sys_version = \"\"\n\n @property\n def gateway(self) -> GatewayServer:\n return self.server # type: ignore[return-value]\n\n def log_message(self, fmt: str, *args: object) -> None:\n return\n\n def _json(\n self,\n status: int,\n payload: dict[str, Any],\n ) -> None:\n body = (\n json.dumps(\n payload,\n separators=(\",\", \":\"),\n sort_keys=True,\n )\n + \"\\n\"\n ).encode()\n self.send_response(status)\n self.send_header(\"Content-Type\", \"application/json\")\n self.send_header(\"Content-Length\", str(len(body)))\n security_headers(self)\n self.end_headers()\n if self.command != \"HEAD\":\n self.wfile.write(body)\n\n def _event(\n self,\n *,\n status: int,\n key: str | None,\n size: int | None,\n started: float,\n ) -> None:\n event = {\n \"event\": \"download\",\n \"status\": status,\n \"key_sha256\": (\n hashlib.sha256(key.encode()).hexdigest()\n if key is not None\n else None\n ),\n \"bytes\": size,\n \"duration_ms\": int((time.monotonic() - started) * 1000),\n \"remote\": self.client_address[0],\n }\n print(\n json.dumps(event, separators=(\",\", \":\"), sort_keys=True),\n flush=True,\n )\n\n def do_HEAD(self) -> None:\n self._handle_download()\n\n def do_GET(self) -> None:\n parsed = urllib.parse.urlsplit(self.path)\n if parsed.path == \"/healthz\":\n self._json(\n 200,\n {\n \"status\": \"ok\",\n \"service\": \"evidence-gateway\",\n },\n )\n return\n self._handle_download()\n\n def do_POST(self) -> None:\n self._json(405, {\"error\": \"method_not_allowed\"})\n\n do_PUT = do_POST\n do_DELETE = do_POST\n do_PATCH = do_POST\n do_OPTIONS = do_POST\n\n def _handle_download(self) -> None:\n started = time.monotonic()\n key: str | None = None\n size: int | None = None\n status = 500\n\n if not self.gateway.slots.acquire(blocking=False):\n status = 503\n self._json(status, {\"error\": \"busy\"})\n self._event(\n status=status,\n key=None,\n size=None,\n started=started,\n )\n return\n\n try:\n parsed = urllib.parse.urlsplit(self.path)\n if not parsed.path.startswith(\"/e/\"):\n status = 404\n self._json(status, {\"error\": \"not_found\"})\n return\n\n try:\n decoded_key = urllib.parse.unquote(\n parsed.path[len(\"/e/\") :],\n errors=\"strict\",\n )\n key = validate_key(decoded_key)\n except Exception:\n status = 400\n self._json(status, {\"error\": \"invalid_key\"})\n return\n\n if canonical_path(key) != parsed.path:\n status = 400\n self._json(status, {\"error\": \"noncanonical_path\"})\n return\n\n query = urllib.parse.parse_qs(\n parsed.query,\n keep_blank_values=True,\n strict_parsing=True,\n )\n if set(query) != {\"exp\", \"sha256\", \"sig\"} or any(\n len(values) != 1 for values in query.values()\n ):\n status = 400\n self._json(status, {\"error\": \"invalid_query\"})\n return\n\n try:\n expiry = int(query[\"exp\"][0])\n except ValueError:\n status = 400\n self._json(status, {\"error\": \"invalid_expiry\"})\n return\n\n expected_sha = query[\"sha256\"][0].lower()\n provided_sig = query[\"sig\"][0].lower()\n now = int(time.time())\n max_ttl = int(self.gateway.config[\"max_ttl_seconds\"])\n\n if expiry < now:\n status = 410\n self._json(status, {\"error\": \"expired\"})\n return\n if expiry - now > max_ttl:\n status = 403\n self._json(status, {\"error\": \"ttl_exceeded\"})\n return\n if SHA256_RE.fullmatch(expected_sha) is None:\n status = 400\n self._json(status, {\"error\": \"invalid_sha256\"})\n return\n if SHA256_RE.fullmatch(provided_sig) is None:\n status = 403\n self._json(status, {\"error\": \"invalid_signature\"})\n return\n\n wanted_sig = signature(\n self.gateway.secret,\n parsed.path,\n expiry,\n expected_sha,\n )\n if not secrets.compare_digest(provided_sig, wanted_sig):\n status = 403\n self._json(status, {\"error\": \"invalid_signature\"})\n return\n\n minio_base = str(self.gateway.config[\"minio_base\"]).rstrip(\"/\")\n bucket = str(self.gateway.config[\"bucket\"])\n object_url = (\n minio_base\n + \"/\"\n + urllib.parse.quote(bucket, safe=\"\")\n + \"/\"\n + urllib.parse.quote(key, safe=\"/._-\")\n )\n request = urllib.request.Request(\n object_url,\n method=\"GET\",\n headers={\n \"User-Agent\": \"evidence-gateway/1\",\n \"Accept\": \"application/octet-stream\",\n },\n )\n\n max_bytes = int(self.gateway.config[\"max_object_bytes\"])\n tmp_dir = str(self.gateway.config[\"tmp_dir\"])\n digest = hashlib.sha256()\n\n try:\n response = urllib.request.urlopen(\n request,\n timeout=float(\n self.gateway.config[\"minio_timeout_seconds\"]\n ),\n )\n except urllib.error.HTTPError as exc:\n if exc.code == 404:\n status = 404\n self._json(status, {\"error\": \"not_found\"})\n elif exc.code == 403:\n status = 502\n self._json(status, {\"error\": \"upstream_denied\"})\n else:\n status = 502\n self._json(status, {\"error\": \"upstream_error\"})\n return\n except Exception:\n status = 502\n self._json(status, {\"error\": \"upstream_unavailable\"})\n return\n\n content_length = response.headers.get(\"Content-Length\")\n if content_length is not None:\n try:\n if int(content_length) > max_bytes:\n response.close()\n status = 413\n self._json(status, {\"error\": \"object_too_large\"})\n return\n except ValueError:\n response.close()\n status = 502\n self._json(status, {\"error\": \"invalid_upstream_length\"})\n return\n\n with tempfile.NamedTemporaryFile(\n mode=\"w+b\",\n dir=tmp_dir,\n prefix=\"download-\",\n delete=True,\n ) as temporary:\n total = 0\n while True:\n block = response.read(1024 * 1024)\n if not block:\n break\n total += len(block)\n if total > max_bytes:\n response.close()\n status = 413\n self._json(status, {\"error\": \"object_too_large\"})\n return\n digest.update(block)\n temporary.write(block)\n response.close()\n\n actual_sha = digest.hexdigest()\n if not secrets.compare_digest(actual_sha, expected_sha):\n status = 502\n self._json(status, {\"error\": \"sha256_mismatch\"})\n return\n\n size = total\n temporary.flush()\n temporary.seek(0)\n\n self.send_response(200)\n self.send_header(\n \"Content-Type\",\n \"application/octet-stream\",\n )\n self.send_header(\"Content-Length\", str(total))\n self.send_header(\n \"Content-Disposition\",\n 'attachment; filename=\"evidence.bin\"',\n )\n self.send_header(\"ETag\", f'\"sha256:{actual_sha}\"')\n security_headers(self)\n self.end_headers()\n\n if self.command != \"HEAD\":\n while True:\n block = temporary.read(1024 * 1024)\n if not block:\n break\n self.wfile.write(block)\n\n status = 200\n finally:\n self.gateway.slots.release()\n self._event(\n status=status,\n key=key,\n size=size,\n started=started,\n )\n\n\ndef validate_config(config: dict[str, Any]) -> None:\n required = {\n \"bind\",\n \"port\",\n \"minio_base\",\n \"bucket\",\n \"max_ttl_seconds\",\n \"max_object_bytes\",\n \"max_concurrent_downloads\",\n \"minio_timeout_seconds\",\n \"tmp_dir\",\n \"secret_file\",\n }\n if set(config) != required:\n raise RuntimeError(\"configuration keys mismatch\")\n if config[\"bind\"] != \"127.0.0.1\":\n raise RuntimeError(\"gateway must bind to loopback\")\n if not (1 <= int(config[\"port\"]) <= 65535):\n raise RuntimeError(\"invalid port\")\n if not str(config[\"minio_base\"]).startswith(\"http://[PRIVATE_IP]:9002\"):\n raise RuntimeError(\"unexpected MinIO endpoint\")\n validate_key(\"selftest/object.txt\")\n\n\ndef self_test() -> None:\n secret = [REDACTED]\n key = \"commands/CMD-123/evidence.tgz\"\n path = canonical_path(key)\n assert path == \"/e/commands/CMD-123/evidence.tgz\"\n sig = signature(secret, path, 2000000000, \"a\" * 64)\n assert len(sig) == 64\n assert validate_key(key) == key\n for bad in (\"\", \"../x\", \"a//b\", \"/a\", \"a/%2F\"):\n try:\n validate_key(bad)\n except ValueError:\n pass\n else:\n raise AssertionError(f\"invalid key accepted: {bad!r}\")\n print(\"SELF_TEST=PASS\")\n\n\ndef main() -> int:\n parser = argparse.ArgumentParser()\n parser.add_argument(\n \"--config\",\n default=\"/etc/evidence-gw/config.json\",\n )\n parser.add_argument(\"--self-test\", action=\"store_true\")\n args = parser.parse_args()\n\n if args.self_test:\n self_test()\n return 0\n\n config = load_json(Path(args.config))\n validate_config(config)\n\n secret_file = Path(str(config[\"secret_file\"]))\n secret = [REDACTED]\n if len(secret) != 32:\n raise RuntimeError(\"gateway secret must be 32 bytes\")\n\n tmp_dir = Path(str(config[\"tmp_dir\"]))\n tmp_dir.mkdir(parents=True, exist_ok=True)\n\n server = GatewayServer(\n (str(config[\"bind\"]), int(config[\"port\"])),\n Handler,\n config=config,\n secret=[REDACTED]\n )\n server.serve_forever(poll_interval=0.5)\n return 0\n\n\nif __name__ == \"__main__\":\n raise SystemExit(main())\n","script_source_sha256":"27de57be0924fe8d86d38879b3bcdba8d4ade817b3a42219dd65d8aacc9601cb","signer_sha256":"523490816d058eddf1273e73cc58ca41db1ad3da24092bc8f9831823ef27c6b1","signer_sha_rc":0,"unit":"evidence-gw.service","unit_cat_rc":0,"unit_show":"MainPID=2384852\nResult=success\nNRestarts=0\nExecMainStatus=0\nExecStart={ path=/usr/bin/python3 ; argv[]=/usr/bin/python3 /opt/stacks/evidence-gw/gateway.py --config /etc/evidence-gw/config.json ; ignore_errors=no ; start_time=[Sat 2026-07-25 22:06:01 MSK] ; stop_time=[n/a] ; pid=2384852 ; code=(null) ; status=0/0 }\nUser=evidence-gw\nGroup=evidence-gw\nId=evidence-gw.service\nLoadState=loaded\nActiveState=active\nSubState=running\nFragmentPath=/etc/systemd/system/evidence-gw.service\nUnitFileState=enabled\n","unit_show_rc":0,"unit_source_redacted":"# /etc/systemd/system/evidence-gw.service\n[Unit]\nDescription=Homelab Evidence Gateway\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=simple\nUser=evidence-gw\nGroup=evidence-gw\nExecStart=/usr/bin/python3 /opt/stacks/evidence-gw/gateway.py --config /etc/evidence-gw/config.json\nRestart=on-failure\nRestartSec=2s\nTimeoutStartSec=30s\nTimeoutStopSec=15s\nUMask=0077\nNoNewPrivileges=yes\nPrivateTmp=yes\nPrivateDevices=yes\nProtectSystem=strict\nProtectHome=yes\nProtectKernelTunables=yes\nProtectKernelModules=yes\nProtectControlGroups=yes\nLockPersonality=yes\nMemoryDenyWriteExecute=yes\nRestrictSUIDSGID=yes\nRestrictRealtime=yes\nRemoveIPC=yes\nReadWritePaths=/var/lib/evidence-gw\nCapabilityBoundingSet=\nAmbientCapabilities=\nLimitNOFILE=1024\n\n[Install]\nWantedBy=multi-user.target\n","unit_source_sha256":"8af420c05d185181bee0d6847271b4947db2b1dddf510541ae40027c5ba34026"},"local_integrity":{"/etc/systemd/system/homelab-command-adapter.service":{"bytes":665,"exists":true,"gid":0,"is_symlink":false,"mode":"0644","owner":"root","path":"/etc/systemd/system/homelab-command-adapter.service","resolved_path":"/etc/systemd/system/homelab-command-adapter.service","sha256":"74d0f37878652ed9d46b53ae8556bbb30ebd683741f293c3f59b3527876ed2d7","uid":0},"/etc/systemd/system/homelab-command-portal.service":{"bytes":981,"exists":true,"gid":0,"is_symlink":false,"mode":"0644","owner":"root","path":"/etc/systemd/system/homelab-command-portal.service","resolved_path":"/etc/systemd/system/homelab-command-portal.service","sha256":"8453eab30f5c3ccd285fc9f70fff18f564809bb27f7bf84f51f2ba593aa85263","uid":0},"/usr/local/sbin/homelab-chat-run":{"bytes":11864,"exists":true,"gid":0,"is_symlink":false,"mode":"0750","owner":"root","path":"/usr/local/sbin/homelab-chat-run","resolved_path":"/usr/local/sbin/homelab-chat-run","sha256":"2a88defef9517f89bc60934dce82097f8e316b08af9417e95167ea5d759a10d9","uid":0},"/usr/local/sbin/homelab-command-adapter":{"bytes":77441,"exists":true,"gid":0,"is_symlink":false,"mode":"0750","owner":"root","path":"/usr/local/sbin/homelab-command-adapter","resolved_path":"/usr/local/sbin/homelab-command-adapter","sha256":"f2410cd328e2837b99e8ce4a28e25cb031a6bdd596d0286ac77a1822ef1cecd0","uid":0},"/usr/local/sbin/homelab-command-portal":{"bytes":39362,"exists":true,"gid":990,"is_symlink":false,"mode":"0750","owner":"root","path":"/usr/local/sbin/homelab-command-portal","resolved_path":"/usr/local/sbin/homelab-command-portal","sha256":"8056faf4ef6348f0b06319ad3c49fbfeeadf25b6370f857ea19569bd27652ee2","uid":0},"/usr/local/sbin/homelab-vm130-exec":{"bytes":1333,"exists":true,"gid":0,"is_symlink":false,"mode":"0750","owner":"root","path":"/usr/local/sbin/homelab-vm130-exec","resolved_path":"/usr/local/sbin/homelab-vm130-exec","sha256":"1959773393723b013af7c93cd29a7b833fd607c903d044e2fcef6accaa29c275","uid":0}},"preplan_basis_sha256":"6ae59d4b7877710758ecb8e315a8322e71c475b6dcda680864eff8c1f7e7e0d9","receiver":{"anchors":[{"line":45,"text":"flock -w 60 9 || {"},{"line":121,"text":" \"$STAGE_DIR/latest.json\""},{"line":128,"text":" \"$STAGE_DIR/latest.txt\""},{"line":170,"text":" refs/remotes/origin/assistant-runtime"},{"line":174,"text":" -B assistant-runtime \\"},{"line":175,"text":" origin/assistant-runtime"},{"line":179,"text":" --orphan assistant-runtime"},{"line":184,"text":"mkdir -p runtime/history"},{"line":186,"text":"cp \"$STAGE/latest.json\" runtime/latest.json"},{"line":187,"text":"cp \"$STAGE/latest.txt\" runtime/latest.txt"},{"line":188,"text":"cp \"$STAGE/latest.json\" \"runtime/history/${COMMAND_ID}.json\""},{"line":189,"text":"cp \"$STAGE/latest.txt\" \"runtime/history/${COMMAND_ID}.txt\""},{"line":192,"text":"git add \\"},{"line":194,"text":" runtime/latest.json \\"},{"line":195,"text":" runtime/latest.txt \\"},{"line":196,"text":" \"runtime/history/${COMMAND_ID}.json\""},{"line":198,"text":"git add \"runtime/history/${COMMAND_ID}.txt\""},{"line":204,"text":" git commit \\"},{"line":210,"text":"GIT_ASKPASS=/data/gitea/runtime-auth/askpass GIT_TERMINAL_PROMPT=0 git push --quiet http://127.0.0.1:3000/homelab-admin/homelab-public-context.git HEAD:refs/heads/assistant-runtime"},{"line":219,"text":"JSON_TMP=\"$(mktemp \"${PUBLIC_DIR}/.latest.json.XXXXXX\")\""},{"line":220,"text":"TXT_TMP=\"$(mktemp \"${PUBLIC_DIR}/.latest.txt.XXXXXX\")\""},{"line":238,"text":" \"branch\": \"assistant-runtime\","},{"line":244,"text":" \"homelab-runtime/latest.json\""},{"line":248,"text":" \"homelab-runtime/latest.txt\""},{"line":255,"text":"mv -f \"$JSON_TMP\" \"$PUBLIC_DIR/latest.json\""},{"line":256,"text":"mv -f \"$TXT_TMP\" \"$PUBLIC_DIR/latest.txt\""},{"line":257,"text":"mv -f \"$INDEX_TMP\" \"$PUBLIC_DIR/index.json\""},{"line":261,"text":" http://127.0.0.1:3002/.well-known/homelab-runtime/latest.json |"},{"line":266,"text":" \"http://127.0.0.1:3002/homelab-admin/homelab-public-context/raw/branch/assistant-runtime/runtime/latest.json\" |"}],"features":{"has_atomic_replace":true,"has_by_command":false,"has_flock":true,"has_git_commit":true,"has_git_push":true,"has_latest_json":true,"has_latest_txt":true},"path":"/usr/local/sbin/homelab-runtime-receive","sha256":"6f4eadda35241d984c30084430f60c456e943d3b17fea6b8c82cf3d5d6a318f4","sha_rc":0,"source_bytes":6549,"source_rc":0,"source_redacted":"#!/usr/bin/env bash\nset -Eeuo pipefail\numask 027\n\nHOST_DATA='/opt/stacks/gitea/data'\nBARE_CONTAINER='/data/git/repositories/homelab-admin/homelab-public-context.git'\n\nPUBLIC_DIR=\"${HOST_DATA}/gitea/public/.well-known/homelab-runtime\"\nSTAGE_DIR=\"${HOST_DATA}/gitea/runtime-stage\"\nWORK_ROOT=\"${HOST_DATA}/gitea/runtime-work\"\n\nLOCK='/run/lock/homelab-runtime-receive.lock'\n\n[ \"$#\" -eq 3 ] || {\n echo 'RESULT=FAIL'\n echo 'REASON=USAGE'\n exit 64\n}\n\nCOMMAND_ID=\"$1\"\nJSON_FILE=\"$2\"\nTXT_FILE=\"$3\"\n\ncase \"$COMMAND_ID\" in\n ''|*[!A-Za-z0-9._-]*)\n echo 'RESULT=FAIL'\n echo 'REASON=INVALID_COMMAND_ID'\n exit 65\n ;;\nesac\n\n[ -f \"$JSON_FILE\" ] || {\n echo 'RESULT=FAIL'\n echo 'REASON=JSON_FILE_MISSING'\n exit 66\n}\n\n[ -f \"$TXT_FILE\" ] || {\n echo 'RESULT=FAIL'\n echo 'REASON=TXT_FILE_MISSING'\n exit 67\n}\n\nexec 9>\"$LOCK\"\nflock -w 60 9 || {\n echo 'RESULT=FAIL'\n echo 'REASON=RECEIVER_LOCK_TIMEOUT'\n exit 68\n}\n\npython3 - \"$JSON_FILE\" \"$TXT_FILE\" \"$COMMAND_ID\" <<'PY'\nimport json\nimport re\nimport sys\nfrom pathlib import Path\n\njson_path = Path(sys.argv[1])\ntext_path = Path(sys.argv[2])\nexpected_id = sys.argv[3]\n\nif json_path.stat().st_size > 524_288:\n raise SystemExit(\"JSON_TOO_LARGE\")\n\nif text_path.stat().st_size > 1_048_576:\n raise SystemExit(\"TEXT_TOO_LARGE\")\n\njson_text = json_path.read_text(encoding=\"utf-8\")\ntext = text_path.read_text(encoding=\"utf-8\")\n\ndata = json.loads(json_text)\n\nif data.get(\"command_id\") != expected_id:\n raise SystemExit(\"COMMAND_ID_MISMATCH\")\n\nif data.get(\"sanitized\") is not True:\n raise SystemExit(\"SANITIZED_FLAG_REQUIRED\")\n\nif data.get(\"secrets_included\") is not False:\n raise SystemExit(\"SECRETS_FLAG_INVALID\")\n\nif data.get(\"private_addresses_included\") is not False:\n raise SystemExit(\"PRIVATE_ADDRESS_FLAG_INVALID\")\n\ncombined = json_text + \"\\n\" + text\n\npatterns = [\n r\"-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----\",\n r\"(?i)(password|passwd|passphrase|authorization|api[_-]?key|access[_-]?token|refresh[_-]?token|private[_-]?key)\\s*[:=]\\s*\\S+\",\n r\"(?i)/root/\\.ssh\",\n r\"(?i)[SENSITIVE_PATH]\",\n r\"(?i)rclone\\.conf\",\n r\"\\b10(?:\\.\\d{1,3}){3}\\b\",\n r\"\\b192\\.168(?:\\.\\d{1,3}){2}\\b\",\n r\"\\b172\\.(?:1[6-9]|2\\d|3[01])(?:\\.\\d{1,3}){2}\\b\",\n]\n\nfor pattern in patterns:\n if re.search(pattern, combined):\n raise SystemExit(\"FORBIDDEN_PUBLIC_CONTENT\")\n\nprint(\"PAYLOAD_VALIDATION=OK\")\nPY\n\nGIT_UID=\"$(docker exec gitea id -u git)\"\nGIT_GID=\"$(docker exec gitea id -g git)\"\n\nmkdir -p \\\n \"$PUBLIC_DIR\" \\\n \"$STAGE_DIR\" \\\n \"$WORK_ROOT\"\n\nchown \"$GIT_UID:$GIT_GID\" \\\n \"$STAGE_DIR\" \\\n \"$WORK_ROOT\"\n\ninstall \\\n -o \"$GIT_UID\" \\\n -g \"$GIT_GID\" \\\n -m 0644 \\\n \"$JSON_FILE\" \\\n \"$STAGE_DIR/latest.json\"\n\ninstall \\\n -o \"$GIT_UID\" \\\n -g \"$GIT_GID\" \\\n -m 0644 \\\n \"$TXT_FILE\" \\\n \"$STAGE_DIR/latest.txt\"\n\ncat > \"$STAGE_DIR/README.md\" <<'DOC'\n# Homelab assistant runtime\n\nSanitized command results for read-only assistant inspection.\n\nCredentials, authentication data, private keys, private IP addresses and\nunfiltered configuration files are prohibited.\nDOC\n\nchown \"$GIT_UID:$GIT_GID\" \"$STAGE_DIR/README.md\"\nchmod 0644 \"$STAGE_DIR/README.md\"\n\nPUBLISHED_AT=\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"\nWORK_CONTAINER=\"/data/gitea/runtime-work/${COMMAND_ID}.$$\"\n\nCOMMIT_SHA=\"$(\n docker exec \\\n -u \"${GIT_UID}:${GIT_GID}\" \\\n -e COMMAND_ID=\"$COMMAND_ID\" \\\n -e PUBLISHED_AT=\"$PUBLISHED_AT\" \\\n -e WORK_CONTAINER=\"$WORK_CONTAINER\" \\\n gitea sh -lc '\nset -eu\n\nBARE=\"/data/git/repositories/homelab-admin/homelab-public-context.git\"\nSTAGE=\"/data/gitea/runtime-stage\"\nWORK=\"$WORK_CONTAINER\"\n\nrm -rf \"$WORK\"\n\ngit clone --quiet \"$BARE\" \"$WORK\"\n\ncd \"$WORK\"\n\ngit config user.name \"homelab-runtime-publisher\"\ngit config user.email \"homelab-runtime@localhost\"\n\nif git show-ref \\\n --verify \\\n --quiet \\\n refs/remotes/origin/assistant-runtime\nthen\n git checkout \\\n --quiet \\\n -B assistant-runtime \\\n origin/assistant-runtime\nelse\n git checkout \\\n --quiet \\\n --orphan assistant-runtime\n\n git rm -rf . >/dev/null 2>&1 || true\nfi\n\nmkdir -p runtime/history\n\ncp \"$STAGE/latest.json\" runtime/latest.json\ncp \"$STAGE/latest.txt\" runtime/latest.txt\ncp \"$STAGE/latest.json\" \"runtime/history/${COMMAND_ID}.json\"\ncp \"$STAGE/latest.txt\" \"runtime/history/${COMMAND_ID}.txt\"\ncp \"$STAGE/README.md\" README.md\n\ngit add \\\n README.md \\\n runtime/latest.json \\\n runtime/latest.txt \\\n \"runtime/history/${COMMAND_ID}.json\"\n\ngit add \"runtime/history/${COMMAND_ID}.txt\"\n\nif ! git diff --cached --quiet\nthen\n GIT_AUTHOR_DATE=\"$PUBLISHED_AT\" \\\n GIT_COMMITTER_DATE=\"$PUBLISHED_AT\" \\\n git commit \\\n --quiet \\\n -m \"runtime: publish ${COMMAND_ID}\"\nfi\n\nCOMMIT=\"$(git rev-parse HEAD)\"\nGIT_ASKPASS=/data/gitea/runtime-auth/askpass GIT_TERMINAL_PROMPT=0 git push --quiet http://127.0.0.1:3000/homelab-admin/homelab-public-context.git HEAD:refs/heads/assistant-runtime\n\n\nrm -rf \"$WORK\"\n\nprintf \"%s\\n\" \"$COMMIT\"\n'\n)\"\n\nJSON_TMP=\"$(mktemp \"${PUBLIC_DIR}/.latest.json.XXXXXX\")\"\nTXT_TMP=\"$(mktemp \"${PUBLIC_DIR}/.latest.txt.XXXXXX\")\"\nINDEX_TMP=\"$(mktemp \"${PUBLIC_DIR}/.index.json.XXXXXX\")\"\n\ninstall -m 0644 \"$JSON_FILE\" \"$JSON_TMP\"\ninstall -m 0644 \"$TXT_FILE\" \"$TXT_TMP\"\n\npython3 - \\\n \"$COMMAND_ID\" \\\n \"$PUBLISHED_AT\" \\\n \"$COMMIT_SHA\" >\"$INDEX_TMP\" <<'PY'\nimport json\nimport sys\n\ncommand_id, published_at, commit_sha = sys.argv[1:4]\n\nprint(json.dumps({\n \"schema_version\": 1,\n \"channel\": \"homelab-runtime\",\n \"branch\": \"assistant-runtime\",\n \"latest_command_id\": command_id,\n \"published_at_utc\": published_at,\n \"commit_sha\": commit_sha,\n \"latest_json_url\": (\n \"https://git.gram1.ru/.well-known/\"\n \"homelab-runtime/latest.json\"\n ),\n \"latest_text_url\": (\n \"https://git.gram1.ru/.well-known/\"\n \"homelab-runtime/latest.txt\"\n )\n}, ensure_ascii=False, indent=2))\nPY\n\nchmod 0644 \"$INDEX_TMP\"\n\nmv -f \"$JSON_TMP\" \"$PUBLIC_DIR/latest.json\"\nmv -f \"$TXT_TMP\" \"$PUBLIC_DIR/latest.txt\"\nmv -f \"$INDEX_TMP\" \"$PUBLIC_DIR/index.json\"\n\ncurl -fsS \\\n --max-time 10 \\\n http://127.0.0.1:3002/.well-known/homelab-runtime/latest.json |\ngrep -F \"$COMMAND_ID\" >/dev/null\n\ncurl -fsS \\\n --max-time 10 \\\n \"http://127.0.0.1:3002/homelab-admin/homelab-public-context/raw/branch/assistant-runtime/runtime/latest.json\" |\ngrep -F \"$COMMAND_ID\" >/dev/null\n\necho 'RESULT=OK'\necho \"COMMAND_ID=$COMMAND_ID\"\necho \"COMMIT_SHA=$COMMIT_SHA\"\necho 'STATIC_PUBLICATION=OK'\necho 'RAW_BRANCH_PUBLICATION=OK'\n","source_sha256":"6f4eadda35241d984c30084430f60c456e943d3b17fea6b8c82cf3d5d6a318f4","stat":"=root:root:750:6549","stat_rc":0,"stat_stderr":"","sudo_install_help_excerpt":"Usage: /usr/bin/install [OPTION]... [-T] SOURCE DEST\n or: /usr/bin/install [OPTION]... SOURCE... DIRECTORY\n or: /usr/bin/install [OPTION]... -t DIRECTORY SOURCE...\n or: /usr/bin/install [OPTION]... -d DIRECTORY...\n\nThis install program copies files (often just compiled) into destination\nlocations you choose. If you want to download and install a ready-to-use\npackage on a GNU/Linux system, you should instead be using a package manager\nlike yum(1) or apt-get(1).\n\nIn the first three forms, copy SOURCE to DEST or multiple SOURCE(s) to\nthe existing DIRECTORY, while setting permission modes and owner/group.\nIn the 4th form, create all components of the given DIRECTORY(ies).\n\nMandatory arguments to long options are mandatory for short options too.\n --backup[=CONTROL] make a backup of each existing destination file\n -b like --backup but does not accept an argument\n -c (ignored)\n -C, --compare compare content of source and destination file","sudo_install_help_rc":0},"report_sha256":"319d433bb77302d92977e66bec320130c69eca85e70e05f6f8350bba4f1a7d6b","rollback_started":false,"runner":{"anchors":[{"line":117,"text":"TXT_FILE=\"$PUBLIC_DIR/${COMMAND_ID}-${STAMP}-latest.txt\""},{"line":406,"text":" \"homelab-runtime/latest.txt\""},{"line":482,"text":" sudo -n /usr/local/sbin/homelab-runtime-receive \\"},{"line":496,"text":"PUBLIC_URL='https://git.gram1.ru/.well-known/homelab-runtime/latest.json'"},{"line":503,"text":" \"$PUBLIC_URL\" 2>/dev/null ||"},{"line":540,"text":"echo 'GITEA_PUBLICATION=OK'"},{"line":541,"text":"echo \"PUBLIC_URL=$PUBLIC_URL\""}],"bytes":11864,"features":{"has_by_command":false,"latest_json_authoritative":true,"latest_txt_authoritative":true,"public_visibility_retry":true,"receiver_call":true},"sha256":"2a88defef9517f89bc60934dce82097f8e316b08af9417e95167ea5d759a10d9"},"schema":1,"source_lock":{"edge_signer_sha256":"523490816d058eddf1273e73cc58ca41db1ad3da24092bc8f9831823ef27c6b1","gateway_script_path":"/opt/stacks/evidence-gw/gateway.py","gateway_script_sha256":"27de57be0924fe8d86d38879b3bcdba8d4ade817b3a42219dd65d8aacc9601cb","gateway_unit":"evidence-gw.service","gateway_unit_sha256":"8af420c05d185181bee0d6847271b4947db2b1dddf510541ae40027c5ba34026","local":{"/etc/systemd/system/homelab-command-adapter.service":"74d0f37878652ed9d46b53ae8556bbb30ebd683741f293c3f59b3527876ed2d7","/etc/systemd/system/homelab-command-portal.service":"8453eab30f5c3ccd285fc9f70fff18f564809bb27f7bf84f51f2ba593aa85263","/usr/local/sbin/homelab-chat-run":"2a88defef9517f89bc60934dce82097f8e316b08af9417e95167ea5d759a10d9","/usr/local/sbin/homelab-command-adapter":"f2410cd328e2837b99e8ce4a28e25cb031a6bdd596d0286ac77a1822ef1cecd0","/usr/local/sbin/homelab-command-portal":"8056faf4ef6348f0b06319ad3c49fbfeeadf25b6370f857ea19569bd27652ee2","/usr/local/sbin/homelab-vm130-exec":"1959773393723b013af7c93cd29a7b833fd607c903d044e2fcef6accaa29c275"},"receiver_sha256":"6f4eadda35241d984c30084430f60c456e943d3b17fea6b8c82cf3d5d6a318f4"},"stage":"RUN7A_DELIVERY_PREPLAN_DISCOVERY","state_inventory":{"counts":{"FAIL":4,"IMPORTED":1,"OK":4,"RUNNING":1},"diagnostic_failure_count":4,"duplicates":{},"stale_running":[],"unresolved_mutating":[]},"status":"PASS","units":{"adapter":{"ActiveState":"active","ExecMainStatus":"0","ExecStart":"{ path=/usr/bin/python3 ; argv[]=/usr/bin/python3 -B /usr/local/sbin/homelab-command-adapter --serve ; ignore_errors=no ; start_time=[n/a] ; stop_time=[n/a] ; pid=364681 ; code=(null) ; status=0/0 }","FragmentPath":"/etc/systemd/system/homelab-command-adapter.service","Group":"homelab-command-portal","Id":"homelab-command-adapter.service","LoadState":"loaded","MainPID":"364681","NRestarts":"0","Result":"success","SubState":"running","UnitFileState":"enabled","User":"root","show_rc":0},"portal":{"ActiveState":"active","ExecMainStatus":"0","ExecStart":"{ path=/usr/bin/python3 ; argv[]=/usr/bin/python3 -B /usr/local/sbin/homelab-command-portal ; ignore_errors=no ; start_time=[Sun 2026-07-26 09:18:49 MSK] ; stop_time=[n/a] ; pid=536586 ; code=(null) ; status=0/0 }","FragmentPath":"/etc/systemd/system/homelab-command-portal.service","Group":"homelab-command-portal","Id":"homelab-command-portal.service","LoadState":"loaded","MainPID":"536586","NRestarts":"0","Result":"success","SubState":"running","UnitFileState":"enabled","User":"homelab-command-portal","show_rc":0}},"warnings":[]} +RUN7P1_IMMUTABLE_RESULT_PLAN=BLOCKED +PLAN_SHA256=40dee3f5a655b78fb38477b146c651f0c9a46a56111211876f56df41538c74ca +{"PLAN_SHA256":"40dee3f5a655b78fb38477b146c651f0c9a46a56111211876f56df41538c74ca","acceptance_requires_separate_capsule":true,"anti_regression":["AR-P1-001 stale latest cannot satisfy runner","AR-P1-002 exact command_id and command_sha256 required","AR-P1-003 exact command TXT SHA required","AR-P1-004 context-auto cannot overwrite by-command result","AR-P1-005 same ID same payload is idempotent","AR-P1-006 same ID different payload is rejected","AR-P1-007 JSON is readiness marker published after TXT","AR-P1-008 sanitizer does not cross line boundaries","AR-P1-009 public payload remains sanitized and address-free","AR-P1-010 temporary files are cleaned"],"backup":{"core_apps":"/var/lib/homelab-runtime-receive/backups/homelab-runtime-receive.25b1d3ddb71ea76fa8a067c4ab97e5f2a17c6dd7657440e74517647ab25ae72c","exact_sha_required":true,"pve01":"/var/lib/homelab-chat-run/state/backups/homelab-chat-run.2a88defef9517f89bc60934dce82097f8e316b08af9417e95167ea5d759a10d9"},"blockers":["REMOTE_RECEIVER_SHA_DISAGREEMENT","REMOTE_SOURCE_LOCK_MISMATCH:receiver","TRANSFORMATION_FAILED:RuntimeError"],"cleanup":["remove all remote /tmp rehearsal and apply staging files","retain verified rollback copies until phase seal","do not remove historical runtime results"],"component":"evidence-delivery","created_at_utc":"2026-07-26T15:54:52.541976+00:00","declared_change_flags_for_apply":{"backup":false,"cluster":false,"gitea":true,"production":true},"exact_diff":["add immutable .well-known/by-command/.json and .txt","write TXT first and JSON last as readiness marker","idempotent same COMMAND_ID with identical JSON/TXT","fail closed on same COMMAND_ID with differing JSON/TXT","prevent Git history overwrite on COMMAND_ID conflict","retain latest.json/latest.txt as non-authoritative convenience","runner publishes own immutable TXT URL","runner polls exact immutable JSON and TXT","runner verifies command_id, command_sha256 and sanitized flags","fix multiline sanitizer false-positive with horizontal whitespace","add persistent --self-test anti-regression entrypoints"],"gates":{"G0_context_lock":{"source_evidence_sha256":"844bd57846acb2dcb939392fd22eeae005b348bd346f3aace0bc219f6bafa38a","source_preplan_basis_sha256":"6ae59d4b7877710758ecb8e315a8322e71c475b6dcda680864eff8c1f7e7e0d9","status":"FAIL"},"G1_read_only_discovery":{"source_hashes":{"adapter":"f2410cd328e2837b99e8ce4a28e25cb031a6bdd596d0286ac77a1822ef1cecd0","adapter_unit":"74d0f37878652ed9d46b53ae8556bbb30ebd683741f293c3f59b3527876ed2d7","portal":"8056faf4ef6348f0b06319ad3c49fbfeeadf25b6370f857ea19569bd27652ee2","portal_unit":"8453eab30f5c3ccd285fc9f70fff18f564809bb27f7bf84f51f2ba593aa85263","receiver":"25b1d3ddb71ea76fa8a067c4ab97e5f2a17c6dd7657440e74517647ab25ae72c","runner":"2a88defef9517f89bc60934dce82097f8e316b08af9417e95167ea5d759a10d9"},"status":"PASS","units":{"adapter":{"active":"active","enabled":"enabled","group":"homelab-command-portal","restarts":"0","user":"root"},"portal":{"active":"active","enabled":"enabled","group":"homelab-command-portal","restarts":"0","user":"homelab-command-portal"}}},"G2_static_validation":{"status":"FAIL","target_hashes":{}},"G3_identity_equivalent":{"current_receiver_sudo_contract_rc":64,"remote_install_available":true,"remote_mkdir_available":true,"status":"PASS"},"G4_integration_rehearsal":{"production_mutation":false,"results":{"remote_cleanup_rc":0},"status":"PASS"}},"kind":"homelab-change-plan","mutation_journal":{"core_apps":"/var/lib/homelab-runtime-receive/RUN7P1-immutable-result-mutation.json","pve01":"/var/lib/homelab-chat-run/state/RUN7P1-immutable-result-mutation.json"},"plan_id":"RUN7P1-IMMUTABLE-PUBLIC-RESULT","postconditions":["installed files match planned target SHA-256","installed runner --self-test returns PASS","installed receiver --self-test via sudo returns PASS","APPLY itself completes through backward-compatible latest channel","separate acceptance capsule is fetched by exact by-command URL","context-auto overwrite of latest does not change acceptance result","same-ID same-content receiver test is idempotent","same-ID different-content receiver test fails closed"],"preconditions":["all source SHA-256 values exactly match this plan","Portal and Adapter remain active/enabled with NRestarts=0","no unresolved mutating failure or rollback","exact PLAN_SHA256 is embedded in APPLY capsule","APPLY capsule source evidence SHA matches RUN7P1 evidence"],"risk":{"backup_subsystem_touched":false,"level":"MEDIUM","reason":"changes public sanitized-result transport on two hosts","second_runner_created":false},"rollback":["capture diagnostics before rollback","record file SHA/mode/owner, runner and receiver self-tests","record exact by-command HTTP status and response SHA","restore both exact source files from verified backups","verify restored SHA/mode/owner","do not restart unrelated services"],"schema":1,"scope":[{"group":"root","host":"pve01","mode":"0750","owner":"root","path":"/usr/local/sbin/homelab-chat-run","source_sha256":"2a88defef9517f89bc60934dce82097f8e316b08af9417e95167ea5d759a10d9","target_sha256":null},{"group":"root","host":"core-apps","mode":"0750","owner":"root","path":"/usr/local/sbin/homelab-runtime-receive","source_sha256":"25b1d3ddb71ea76fa8a067c4ab97e5f2a17c6dd7657440e74517647ab25ae72c","target_sha256":null}],"service_identity_test":{"proposed_receiver_self_test":"PASS","proposed_runner_self_test":"PASS","receiver_current_sudo_usage_rc":64},"source":{"live_source_hashes":{"core_apps_receiver":"25b1d3ddb71ea76fa8a067c4ab97e5f2a17c6dd7657440e74517647ab25ae72c","pve01_runner":"2a88defef9517f89bc60934dce82097f8e316b08af9417e95167ea5d759a10d9"},"run7a_capsule_sha256":"b45c17b1b5f4ac00868c84dd66ef8b59a6033b92c9886f7f3e6c5b48ea5da5a8","run7a_evidence_sha256":"844bd57846acb2dcb939392fd22eeae005b348bd346f3aace0bc219f6bafa38a","run7a_preplan_basis_sha256":"6ae59d4b7877710758ecb8e315a8322e71c475b6dcda680864eff8c1f7e7e0d9","run7a_runner_json_sha256":"7c77a1aa9f229e7d67ab7a74769bcf4dee22b2fbd474c9c909fb796123b5a7bb"},"target":{"core_apps_receiver_sha256":null,"pve01_runner_sha256":null},"warnings":[]} OUTPUT_END CHAT_OUTPUT_END