diff --git a/runtime/history/RUN7A-DELIVERY-PREPLAN-DISCOVERY-20260726T153105Z.json b/runtime/history/RUN7A-DELIVERY-PREPLAN-DISCOVERY-20260726T153105Z.json new file mode 100644 index 0000000..5f33a95 --- /dev/null +++ b/runtime/history/RUN7A-DELIVERY-PREPLAN-DISCOVERY-20260726T153105Z.json @@ -0,0 +1,30 @@ +{ + "schema_version": 1, + "channel": "homelab-runtime", + "command_id": "RUN7A-DELIVERY-PREPLAN-DISCOVERY-20260726T153105Z", + "status": "OK", + "rc": 0, + "host": "pve01", + "mode": "verify", + "component": "evidence-delivery", + "started_at_utc": "2026-07-26T15:32:54Z", + "finished_at_utc": "2026-07-26T15:33:10Z", + "reference_register_checked": true, + "reference_sha256": "5763f2f8edc75fcf6f3951f8c95896d1cc112ceccb6cf5b64a383f540a4deb8e", + "error_register_checked": true, + "error_register_sha256": "24934a2c7fa5c26f6e828583c4aefac7f143b40c0171736ed7571e617d55ab83", + "command_sha256": "72e4d7cfc355fa26e684146bf7b62cece7b11d539ee4fae0ef84c613bcf36a6e", + "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": "80c0aa4ae374f18ac41e5c6df736a45e2f144cf5a23db6ccf7554c5a03844a98", + "sanitized_output_sha256": "edac687cd4d0ffb36c68d91fbc10d7554c37707b62c4d09b09b8e88db7ce13c3", + "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" +} diff --git a/runtime/history/RUN7A-DELIVERY-PREPLAN-DISCOVERY-20260726T153105Z.txt b/runtime/history/RUN7A-DELIVERY-PREPLAN-DISCOVERY-20260726T153105Z.txt new file mode 100644 index 0000000..986f395 --- /dev/null +++ b/runtime/history/RUN7A-DELIVERY-PREPLAN-DISCOVERY-20260726T153105Z.txt @@ -0,0 +1,28 @@ +CHAT_OUTPUT_BEGIN +COMMAND_ID=RUN7A-DELIVERY-PREPLAN-DISCOVERY-20260726T153105Z +STATUS=OK +RC=0 +HOST=pve01 +MODE=verify +COMPONENT=evidence-delivery +REFERENCE_REGISTER_CHECK=OK +REFERENCE_SHA256=5763f2f8edc75fcf6f3951f8c95896d1cc112ceccb6cf5b64a383f540a4deb8e +ERROR_REGISTER_CHECK=OK +ERROR_REGISTER_SHA256=24934a2c7fa5c26f6e828583c4aefac7f143b40c0171736ed7571e617d55ab83 +COMMAND_SHA256=72e4d7cfc355fa26e684146bf7b62cece7b11d539ee4fae0ef84c613bcf36a6e +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 +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":[]} + +OUTPUT_END +CHAT_OUTPUT_END diff --git a/runtime/latest.json b/runtime/latest.json index ae97adc..5f33a95 100644 --- a/runtime/latest.json +++ b/runtime/latest.json @@ -1 +1,30 @@ -{"schema_version":1,"channel":"homelab-runtime","command_id":"CONTEXT-AUTO-20260726T151701Z","status":"OK","rc":0,"host":"pve01","mode":"read-only","component":"cluster-context","started_at_utc":"2026-07-26T15:17:01Z","finished_at_utc":"2026-07-26T15:17:02Z","reference_register_checked":true,"reference_sha256":"5763f2f8edc75fcf6f3951f8c95896d1cc112ceccb6cf5b64a383f540a4deb8e","error_register_checked":true,"error_register_sha256":"24934a2c7fa5c26f6e828583c4aefac7f143b40c0171736ed7571e617d55ab83","changes_made":false,"sanitized":true,"secrets_included":false,"private_addresses_included":false,"output":"AUTOMATIC_CLUSTER_CONTEXT_REFRESH=OK"} +{ + "schema_version": 1, + "channel": "homelab-runtime", + "command_id": "RUN7A-DELIVERY-PREPLAN-DISCOVERY-20260726T153105Z", + "status": "OK", + "rc": 0, + "host": "pve01", + "mode": "verify", + "component": "evidence-delivery", + "started_at_utc": "2026-07-26T15:32:54Z", + "finished_at_utc": "2026-07-26T15:33:10Z", + "reference_register_checked": true, + "reference_sha256": "5763f2f8edc75fcf6f3951f8c95896d1cc112ceccb6cf5b64a383f540a4deb8e", + "error_register_checked": true, + "error_register_sha256": "24934a2c7fa5c26f6e828583c4aefac7f143b40c0171736ed7571e617d55ab83", + "command_sha256": "72e4d7cfc355fa26e684146bf7b62cece7b11d539ee4fae0ef84c613bcf36a6e", + "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": "80c0aa4ae374f18ac41e5c6df736a45e2f144cf5a23db6ccf7554c5a03844a98", + "sanitized_output_sha256": "edac687cd4d0ffb36c68d91fbc10d7554c37707b62c4d09b09b8e88db7ce13c3", + "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" +} diff --git a/runtime/latest.txt b/runtime/latest.txt index a3960f0..986f395 100644 --- a/runtime/latest.txt +++ b/runtime/latest.txt @@ -1,162 +1,28 @@ CHAT_OUTPUT_BEGIN -COMMAND_ID=CONTEXT-AUTO-[PRIVATE_IP]701Z +COMMAND_ID=RUN7A-DELIVERY-PREPLAN-DISCOVERY-20260726T153105Z STATUS=OK RC=0 HOST=pve01 -COMPONENT=cluster-context -REFERENCE_SHA[PRIVATE_IP]f8edc75fcf[PRIVATE_IP]c[PRIVATE_IP]cc112ceccb6cf[PRIVATE_IP]a4deb8e -ERROR_REGISTER_SHA[PRIVATE_IP]c7fa[PRIVATE_IP]583c4aefac[PRIVATE_IP]1736ed[PRIVATE_IP]ab83 +MODE=verify +COMPONENT=evidence-delivery +REFERENCE_REGISTER_CHECK=OK +REFERENCE_SHA256=5763f2f8edc75fcf6f3951f8c95896d1cc112ceccb6cf5b64a383f540a4deb8e +ERROR_REGISTER_CHECK=OK +ERROR_REGISTER_SHA256=24934a2c7fa5c26f6e828583c4aefac7f143b40c0171736ed7571e617d55ab83 +COMMAND_SHA256=72e4d7cfc355fa26e684146bf7b62cece7b11d539ee4fae0ef84c613bcf36a6e +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 OUTPUT_BEGIN -GENERATED_AT_UTC=[PRIVATE_IP]T15:17:01Z -Cluster information -------------------- -Name: homelab -Config Version: 3 -Transport: knet -Secure auth: on +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":[]} -Quorum information ------------------- -Date: Sun Jul [PRIVATE_IP] 2026 -Quorum provider: corosync_votequorum -Nodes: 3 -Node ID: [PRIVATE_IP] -Ring ID: 1.c6 -Quorate: Yes - -Votequorum information ----------------------- -Expected votes: 3 -Highest expected: 3 -Total votes: 3 -Quorum: 2 -Flags: Quorate - -Membership information ----------------------- - Nodeid Votes Name -[PRIVATE_IP] [PRIVATE_IP].11 (local) -[PRIVATE_IP] [PRIVATE_IP].13 -[PRIVATE_IP] [PRIVATE_IP].12 - -Membership information ----------------------- - Nodeid Votes Name - 1 1 pve01 (local) - 2 1 pve03 - 3 1 pve02 -[{"cpu":[PRIVATE_IP]087e-05,"disk":[PRIVATE_IP],"diskread":[PRIVATE_IP],"diskwrite":[PRIVATE_IP],"id":"lxc/110","maxcpu":1,"maxdisk":[PRIVATE_IP],"maxmem":[PRIVATE_IP],"mem":[PRIVATE_IP],"memhost":0,"name":"dns1","netin":[PRIVATE_IP],"netout":[PRIVATE_IP],"node":"pve01","status":"running","template":0,"type":"lxc","uptime":266149,"vmid":110},{"cpu":[PRIVATE_IP][PRIVATE_IP],"disk":[PRIVATE_IP],"diskread":[PRIVATE_IP],"diskwrite":[PRIVATE_IP],"id":"lxc/111","maxcpu":1,"maxdisk":[PRIVATE_IP],"maxmem":[PRIVATE_IP],"mem":[PRIVATE_IP],"memhost":0,"name":"dns2","netin":[PRIVATE_IP],"netout":[PRIVATE_IP],"node":"pve02","status":"running","template":0,"type":"lxc","uptime":265992,"vmid":111},{"cpu":[PRIVATE_IP]087e-05,"disk":[PRIVATE_IP],"diskread":[PRIVATE_IP],"diskwrite":[PRIVATE_IP],"id":"lxc/112","maxcpu":1,"maxdisk":[PRIVATE_IP],"maxmem":[PRIVATE_IP],"mem":[PRIVATE_IP],"memhost":0,"name":"unbound1","netin":[PRIVATE_IP],"netout":[PRIVATE_IP],"node":"pve01","status":"running","template":0,"type":"lxc","uptime":266152,"vmid":112},{"cpu":[PRIVATE_IP]109e-05,"disk":[PRIVATE_IP],"diskread":[PRIVATE_IP],"diskwrite":[PRIVATE_IP],"id":"lxc/113","maxcpu":1,"maxdisk":[PRIVATE_IP],"maxmem":[PRIVATE_IP],"mem":[PRIVATE_IP],"memhost":0,"name":"unbound2","netin":[PRIVATE_IP],"netout":[PRIVATE_IP],"node":"pve02","status":"running","template":0,"type":"lxc","uptime":265995,"vmid":113},{"cpu":[PRIVATE_IP]47839,"disk":0,"diskread":[PRIVATE_IP],"diskwrite":[PRIVATE_IP],"id":"qemu/130","maxcpu":4,"maxdisk":[PRIVATE_IP],"maxmem":[PRIVATE_IP],"mem":[PRIVATE_IP],"memhost":[PRIVATE_IP],"name":"edge-vm","netin":[PRIVATE_IP],"netout":[PRIVATE_IP],"node":"pve03","status":"running","template":0,"type":"qemu","uptime":266089,"vmid":130},{"cpu":[PRIVATE_IP]61993,"disk":0,"diskread":[PRIVATE_IP],"diskwrite":[PRIVATE_IP],"id":"qemu/150","maxcpu":4,"maxdisk":[PRIVATE_IP],"maxmem":[PRIVATE_IP],"mem":[PRIVATE_IP],"memhost":[PRIVATE_IP],"name":"nextcloud","netin":[PRIVATE_IP],"netout":[PRIVATE_IP],"node":"pve01","status":"running","template":0,"type":"qemu","uptime":266146,"vmid":150},{"cpu":[PRIVATE_IP]924456,"disk":0,"diskread":[PRIVATE_IP],"diskwrite":[PRIVATE_IP],"id":"qemu/160","maxcpu":4,"maxdisk":[PRIVATE_IP],"maxmem":[PRIVATE_IP],"mem":[PRIVATE_IP],"memhost":[PRIVATE_IP],"name":"forum-prod","netin":[PRIVATE_IP],"netout":[PRIVATE_IP],"node":"pve02","status":"running","template":0,"type":"qemu","uptime":265989,"vmid":160},{"cpu":[PRIVATE_IP]25638,"disk":0,"diskread":[PRIVATE_IP],"diskwrite":[PRIVATE_IP],"id":"qemu/170","maxcpu":4,"maxdisk":[PRIVATE_IP],"maxmem":[PRIVATE_IP],"mem":[PRIVATE_IP],"memhost":[PRIVATE_IP],"name":"core-apps","netin":[PRIVATE_IP],"netout":[PRIVATE_IP],"node":"pve01","status":"running","template":0,"type":"qemu","uptime":266142,"vmid":170},{"cpu":[PRIVATE_IP]18189,"disk":0,"diskread":[PRIVATE_IP],"diskwrite":[PRIVATE_IP],"id":"qemu/171","maxcpu":4,"maxdisk":[PRIVATE_IP],"maxmem":[PRIVATE_IP],"mem":[PRIVATE_IP],"memhost":[PRIVATE_IP],"name":"monitoring","netin":[PRIVATE_IP],"netout":[PRIVATE_IP],"node":"pve01","status":"running","template":0,"type":"qemu","uptime":266139,"vmid":171},{"cpu":[PRIVATE_IP]862648,"disk":0,"diskread":[PRIVATE_IP],"diskwrite":[PRIVATE_IP],"id":"qemu/180","maxcpu":2,"maxdisk":[PRIVATE_IP],"maxmem":[PRIVATE_IP],"mem":[PRIVATE_IP],"memhost":[PRIVATE_IP],"name":"cluster-admin","netin":[PRIVATE_IP],"netout":[PRIVATE_IP],"node":"pve02","status":"running","template":0,"type":"qemu","uptime":265986,"vmid":180},{"cpu":[PRIVATE_IP]216696,"disk":0,"diskread":[PRIVATE_IP],"diskwrite":[PRIVATE_IP],"id":"qemu/190","maxcpu":4,"maxdisk":[PRIVATE_IP],"maxmem":[PRIVATE_IP],"mem":[PRIVATE_IP],"memhost":[PRIVATE_IP],"name":"homelab-ops-worker","netin":[PRIVATE_IP],"netout":[PRIVATE_IP],"node":"pve03","status":"running","template":0,"type":"qemu","uptime":266085,"vmid":190},{"cpu":0,"disk":0,"diskread":0,"diskwrite":0,"id":"qemu/9130","maxcpu":4,"maxdisk":[PRIVATE_IP],"maxmem":[PRIVATE_IP],"mem":0,"memhost":0,"name":"edge-cold-standby","netin":0,"netout":0,"node":"pve02","status":"stopped","template":0,"type":"qemu","uptime":0,"vmid":9130},{"cgroup-mode":2,"cpu":[PRIVATE_IP]02484,"disk":[PRIVATE_IP],"id":"node/pve03","level":"","maxcpu":8,"maxdisk":[PRIVATE_IP],"maxmem":[PRIVATE_IP],"mem":[PRIVATE_IP],"node":"pve03","status":"online","type":"node","uptime":266107},{"cgroup-mode":2,"cpu":[PRIVATE_IP]29079,"disk":[PRIVATE_IP],"id":"node/pve02","level":"","maxcpu":4,"maxdisk":[PRIVATE_IP],"maxmem":[PRIVATE_IP],"mem":[PRIVATE_IP],"node":"pve02","status":"online","type":"node","uptime":266056},{"cgroup-mode":2,"cpu":[PRIVATE_IP]67316,"disk":[PRIVATE_IP],"id":"node/pve01","level":"","maxcpu":24,"maxdisk":[PRIVATE_IP],"maxmem":[PRIVATE_IP],"mem":[PRIVATE_IP],"node":"pve01","status":"online","type":"node","uptime":266179},{"content":"vztmpl,import,iso,backup","disk":[PRIVATE_IP],"id":"storage/pve03/local","maxdisk":[PRIVATE_IP],"node":"pve03","plugintype":"dir","shared":0,"status":"available","storage":"local","type":"storage"},{"content":"vztmpl,import,iso,backup","disk":[PRIVATE_IP],"id":"storage/pve02/local","maxdisk":[PRIVATE_IP],"node":"pve02","plugintype":"dir","shared":0,"status":"available","storage":"local","type":"storage"},{"content":"vztmpl,import,iso,backup","disk":[PRIVATE_IP],"id":"storage/pve01/local","maxdisk":[PRIVATE_IP],"node":"pve01","plugintype":"dir","shared":0,"status":"available","storage":"local","type":"storage"},{"content":"rootdir,images","disk":[PRIVATE_IP],"id":"storage/pve03/local-lvm","maxdisk":[PRIVATE_IP],"node":"pve03","plugintype":"lvmthin","shared":0,"status":"available","storage":"local-lvm","type":"storage"},{"content":"rootdir,images","disk":[PRIVATE_IP],"id":"storage/pve02/local-lvm","maxdisk":[PRIVATE_IP],"node":"pve02","plugintype":"lvmthin","shared":0,"status":"available","storage":"local-lvm","type":"storage"},{"content":"rootdir,images","disk":[PRIVATE_IP],"id":"storage/pve01/local-lvm","maxdisk":[PRIVATE_IP],"node":"pve01","plugintype":"lvmthin","shared":0,"status":"available","storage":"local-lvm","type":"storage"},{"id":"network/pve03/zone/localnetwork","network":"localnetwork","network-type":"zone","node":"pve03","status":"ok","type":"network"},{"id":"network/pve02/zone/localnetwork","network":"localnetwork","network-type":"zone","node":"pve02","status":"ok","type":"network"},{"id":"network/pve01/zone/localnetwork","network":"localnetwork","network-type":"zone","node":"pve01","status":"ok","type":"network"}] -Name Type Status Total (KiB) Used (KiB) Available (KiB) % -local dir active [PRIVATE_IP] [PRIVATE_IP] [PRIVATE_IP] 12.69% -local-lvm lvmthin active [PRIVATE_IP] [PRIVATE_IP] [PRIVATE_IP] 38.43% - UNIT LOAD ACTIVE SUB DESCRIPTION -● homelab-backup-matrix-refresh.service loaded failed failed Generate unified homelab backup matrix state - -Legend: LOAD → Reflects whether the unit definition was properly loaded. - ACTIVE → The high-level unit activation state, i.e. generalization of SUB. - SUB → The low-level unit activation state, values depend on unit type. - -1 loaded units listed. -NEXT LEFT LAST PASSED UNIT ACTIVATES -Sun [PRIVATE_IP] 18:17:07 MSK 4s Sun [PRIVATE_IP] 18:14:58 MSK 2min 3s ago homelab-cluster-admin-inventory-sync.timer homelab-cluster-admin-inventory-sync.service -Sun [PRIVATE_IP] 18:17:20 MSK 17s Sun [PRIVATE_IP] 18:16:20 MSK 42s ago homelab-cluster-admin-webpanel-sync.timer homelab-cluster-admin-webpanel-sync.service -Sun [PRIVATE_IP] 18:17:20 MSK 17s Sun [PRIVATE_IP] 18:16:20 MSK 42s ago homelab-private-vpn-hosts-health.timer homelab-private-vpn-hosts-health.service -Sun [PRIVATE_IP] 18:17:20 MSK 18s Sun [PRIVATE_IP] 18:16:20 MSK 42s ago homelab-router-watchdog.timer homelab-router-watchdog.service -Sun [PRIVATE_IP] 18:17:42 MSK 39s Sun [PRIVATE_IP] 18:12:29 MSK 4min 33s ago homelab-crypto-portfolio-chart-health.timer homelab-crypto-portfolio-chart-health.service -Sun [PRIVATE_IP] 18:18:23 MSK 1min 20s Sun [PRIVATE_IP] 18:13:15 MSK 3min 46s ago homelab-health-metrics.timer homelab-health-metrics.service -Sun [PRIVATE_IP] 18:19:29 MSK 2min 26s Sun [PRIVATE_IP] 18:14:29 MSK 2min 33s ago homelab-smartctl-textfile.timer homelab-smartctl-textfile.service -Sun [PRIVATE_IP] 18:19:29 MSK 2min 26s Sun [PRIVATE_IP] 18:14:29 MSK 2min 33s ago netbird-peers-health.timer netbird-peers-health.service -Sun [PRIVATE_IP] 18:28:15 MSK 11min Sun [PRIVATE_IP] 18:13:15 MSK 3min 47s ago homelab-disk-space-health.timer homelab-disk-space-health.service -Sun [PRIVATE_IP] 18:28:15 MSK 11min Sun [PRIVATE_IP] 18:13:15 MSK 3min 47s ago homelab-pve03-staging-capacity-health.timer homelab-pve03-staging-capacity-health.service -Sun [PRIVATE_IP] 18:28:29 MSK 11min Sun [PRIVATE_IP] 18:13:29 MSK 3min 33s ago prometheus-node-exporter-apt.timer prometheus-node-exporter-apt.service -Sun [PRIVATE_IP] 18:28:29 MSK 11min Sun [PRIVATE_IP] 18:13:29 MSK 3min 33s ago prometheus-node-exporter-nvme.timer prometheus-node-exporter-nvme.service -Sun [PRIVATE_IP] 18:30:00 MSK 12min Sun [PRIVATE_IP] 18:15:01 MSK 2min 1s ago homelab-incident-journal.timer homelab-incident-journal.service -Sun [PRIVATE_IP] 18:30:13 MSK 13min Sun [PRIVATE_IP] 18:16:04 MSK 58s ago homelab-external-probe-vps-health.timer homelab-external-probe-vps-health.service -Sun [PRIVATE_IP] 18:30:23 MSK 13min Sun [PRIVATE_IP] 18:16:15 MSK 47s ago homelab-forum-snuffleupagus-health.timer homelab-forum-snuffleupagus-health.service -Sun [PRIVATE_IP] 18:30:36 MSK 13min Sun [PRIVATE_IP] 18:16:04 MSK 58s ago homelab-duty-admin-v2.timer homelab-duty-admin-v2.service -Sun [PRIVATE_IP] 18:33:59 MSK 16min Sun [PRIVATE_IP] 18:04:09 MSK 12min ago homelab-mkdocs-auto-refresh.timer homelab-mkdocs-auto-refresh.service -Sun [PRIVATE_IP] 18:42:54 MSK 25min Sun [PRIVATE_IP] 18:08:08 MSK 8min ago homelab-reference-refresh.timer homelab-reference-refresh.service -Sun [PRIVATE_IP] 21:06:49 MSK 2h 49min Sun [PRIVATE_IP] 07:42:38 MSK 10h ago apt-daily.timer apt-daily.service -Sun [PRIVATE_IP] 22:24:15 MSK 4h 7min Sun [PRIVATE_IP] 16:24:15 MSK 1h 52min ago homelab-vps-identity-audit.timer homelab-vps-identity-audit.service -Mon [PRIVATE_IP] 00:00:00 MSK 5h 42min Sun [PRIVATE_IP] 00:00:04 MSK 18h ago dpkg-db-backup.timer dpkg-db-backup.service -Mon [PRIVATE_IP] 00:01:56 MSK 5h 44min Sun [PRIVATE_IP] 00:00:15 MSK 18h ago homelab-docker-health.timer homelab-docker-health.service -Mon [PRIVATE_IP] 00:03:49 MSK 5h 46min Sun [PRIVATE_IP] 00:38:29 MSK 17h ago logrotate.timer logrotate.service -Mon [PRIVATE_IP] 00:03:54 MSK 5h 46min Sun [PRIVATE_IP] 00:06:04 MSK 18h ago homelab-storage-capacity.timer homelab-storage-capacity.service -Mon [PRIVATE_IP] 00:11:38 MSK 5h 54min Sun [PRIVATE_IP] 00:09:29 MSK 18h ago homelab-evidence-catalog.timer homelab-evidence-catalog.service -Mon [PRIVATE_IP] 00:13:25 MSK 5h 56min Mon [PRIVATE_IP] 00:00:29 MSK - homelab-secret-sanity.timer homelab-secret-sanity.service -Mon [PRIVATE_IP] 00:15:18 MSK 5h 58min Sun [PRIVATE_IP] 00:11:04 MSK 18h ago homelab-quality-gate.timer homelab-quality-gate.service -Mon [PRIVATE_IP] 01:17:55 MSK 7h Mon [PRIVATE_IP] 00:51:14 MSK - fstrim.timer fstrim.service -Mon [PRIVATE_IP] 02:36:56 MSK 8h Sun [PRIVATE_IP] 05:58:48 MSK 12h ago pve-daily-update.timer pve-daily-update.service -Mon [PRIVATE_IP] 03:50:00 MSK 9h Sun [PRIVATE_IP] 03:50:04 MSK 14h ago filebrowser-offhost-sync.timer filebrowser-offhost-sync.service -Mon [PRIVATE_IP] 04:52:03 MSK 10h Sun [PRIVATE_IP] 04:44:28 MSK 13h ago homelab-edge-vm-offhost-freshness.timer homelab-edge-vm-offhost-freshness.service -Mon [PRIVATE_IP] 05:04:46 MSK 10h Sun [PRIVATE_IP] 05:14:37 MSK 13h ago homelab-sops-edge-secret-coverage.timer homelab-sops-edge-secret-coverage.service -Mon [PRIVATE_IP] 05:04:47 MSK 10h Sun [PRIVATE_IP] 06:05:19 MSK 12h ago man-db.timer man-db.service -Mon [PRIVATE_IP] 06:01:10 MSK 11h Sun [PRIVATE_IP] 06:15:38 MSK 12h ago apt-daily-upgrade.timer apt-daily-upgrade.service -Mon [PRIVATE_IP] 07:10:05 MSK 12h Sun [PRIVATE_IP] 07:04:15 MSK 11h ago homelab-drift-check.timer homelab-drift-check.service -Mon [PRIVATE_IP] 07:39:10 MSK 13h Sun [PRIVATE_IP] 07:33:28 MSK 10h ago homelab-service-registry-check.timer homelab-service-registry-check.service -Mon [PRIVATE_IP] 07:52:53 MSK 13h Sun [PRIVATE_IP] 07:54:04 MSK 10h ago homelab-alerting-health.timer homelab-alerting-health.service -Mon [PRIVATE_IP] 07:57:55 MSK 13h Sun [PRIVATE_IP] 07:49:31 MSK 10h ago homelab-dependency-map-check.timer homelab-dependency-map-check.service -Mon [PRIVATE_IP] 07:59:28 MSK 13h Sun [PRIVATE_IP] 07:42:29 MSK 10h ago homelab-runbook-generate.timer homelab-runbook-generate.service -Mon [PRIVATE_IP] 08:00:53 MSK 13h Sun [PRIVATE_IP] 08:14:04 MSK 10h ago homelab-duty-admin-report.timer homelab-duty-admin-report.service -Mon [PRIVATE_IP] 08:03:36 MSK 13h Sun [PRIVATE_IP] 08:02:37 MSK 10h ago homelab-overall-health.timer homelab-overall-health.service -Mon [PRIVATE_IP] 08:07:41 MSK 13h Sun [PRIVATE_IP] 07:51:04 MSK 10h ago homelab-desired-state-sync.timer homelab-desired-state-sync.service -Mon [PRIVATE_IP] 08:09:45 MSK 13h Sun [PRIVATE_IP] 08:16:38 MSK 10h ago homelab-kuma-monitor-policy.timer homelab-kuma-monitor-policy.service -Mon [PRIVATE_IP] 08:17:59 MSK 14h Sun [PRIVATE_IP] 08:24:58 MSK 9h ago homelab-final-readiness-gate.timer homelab-final-readiness-gate.service -Mon [PRIVATE_IP] 08:26:42 MSK 14h Sun [PRIVATE_IP] 08:29:26 MSK 9h ago homelab-capacity-risk.timer homelab-capacity-risk.service -Mon [PRIVATE_IP] 08:39:35 MSK 14h Sun [PRIVATE_IP] 08:38:04 MSK 9h ago homelab-secret-exposure-guard.timer homelab-secret-exposure-guard.service -Mon [PRIVATE_IP] 08:47:05 MSK 14h Sun [PRIVATE_IP] 08:43:29 MSK 9h ago homelab-cluster-passport.timer homelab-cluster-passport.service -Mon [PRIVATE_IP] 08:52:59 MSK 14h Sun [PRIVATE_IP] 08:48:08 MSK 9h ago immich-media-offhost-sync.timer immich-media-offhost-sync.service -Mon [PRIVATE_IP] 09:01:44 MSK 14h Sun [PRIVATE_IP] 09:01:04 MSK 9h ago homelab-golden-state-index.timer homelab-golden-state-index.service -Mon [PRIVATE_IP] 09:15:59 MSK 14h Sun [PRIVATE_IP] 09:09:04 MSK 9h ago memos-offhost-sync.timer memos-offhost-sync.service -Mon [PRIVATE_IP] 10:15:00 MSK 15h Sun [PRIVATE_IP] 10:15:04 MSK 8h ago skladchik-reports-monitor-monthly-selftest-watch.timer skladchik-reports-monitor-monthly-selftest-watch.service -Mon [PRIVATE_IP] 16:35:54 MSK 22h Sun [PRIVATE_IP] 16:35:54 MSK 1h 41min ago systemd-tmpfiles-clean.timer systemd-tmpfiles-clean.service -Sun [PRIVATE_IP] 03:10:03 MSK 6 days Sun [PRIVATE_IP] 03:10:15 MSK 15h ago e2scrub_all.timer e2scrub_all.service -Sun [PRIVATE_IP] 03:10:09 MSK 6 days Sun [PRIVATE_IP] 03:10:15 MSK 15h ago xfs_scrub_all.timer xfs_scrub_all.service -- - - - prometheus-node-exporter-ipmitool-sensor.timer prometheus-node-exporter-ipmitool-sensor.service -- - - - prometheus-node-exporter-mellanox-hca-temp.timer prometheus-node-exporter-mellanox-hca-temp.service -- - - - prometheus-node-exporter-smartmon.timer prometheus-node-exporter-smartmon.service -- - Sun [PRIVATE_IP] 18:17:02 MSK 10ms ago skladchik-reports-monitor-supervisor.timer skladchik-reports-monitor-supervisor.service - -58 timers listed. -- Обрезанная, повреждённая или частично вставленная команда считается невыполненной; сначала проводится side-effect audit. -- Хвост parse-ошибки закрывается только после доказательства отсутствия изменений и временных артефактов. -- Proof: /var/lib/homelab-change-sets/operator-command-size-guard-v[PRIVATE_IP]T182653Z/report.txt - -## HOMELAB_ADMIN_CLI_CONTRACT_[PRIVATE_IP] -- Поддерживаемые команды: version, status, lint TASK_DIR, validate TASK_DIR, run TASK_DIR PHASE. -- Отдельная команда --help отсутствует. Неизвестный аргумент печатает usage и штатно возвращает RC=64. -- Нельзя трактовать RC=64 от --help как неисправность runner. -- Для проверки доступности runner использовать homelab-admin version; для структуры CLI использовать зафиксированную usage-строку. -- Proof: /var/lib/homelab-change-sets/homelab-admin-cli-contract-fix-v[PRIVATE_IP]T183338Z/report.txt - -AUTOMATIC_CLUSTER_CONTEXT_REFRESH_[PRIVATE_IP] -Collector: /usr/local/sbin/homelab-context-collect -Scheduler: hourly at minute 17 via /etc/cron.d/homelab-context-refresh -Destination: immutable Gitea runtime history with CONTEXT-AUTO UTC command IDs. - -AUTOMATIC_CLUSTER_CONTEXT_REFRESH_DIRECT_[PRIVATE_IP] -Publisher: /usr/local/sbin/homelab-context-refresh-direct -Schedule: hourly at minute 17. -Destination: immutable assistant-runtime history with CONTEXT-AUTO UTC IDs. -ANTI_REGRESSION_TOKEN=HOMELAB_VM_2CLOUD_444_OUTER_LAUNCHER_BOUNDARY_REQUIRED -- Anti-regression: validate the complete outer launcher with a shell syntax check and verify the wrapper argument boundary before issuing it. -- Infrastructure action executed: none. -- Backup payload impact: none. -- Production impact: none. -- Status: CLOSED; UNRESOLVED_TAIL_COUNT=0. -- Registered at: [PRIVATE_IP]T11:20:32Z - -## ERROR_[PRIVATE_IP]_HOMELAB_VM_2CLOUD_DRAIN_CLOSURE_PREFLIGHT_446 -COMMAND_ID=HOMELAB-VM-2CLOUD-DRAIN-CLOSURE-PREFLIGHT-446 -- Symptom: homelab-chat-run-linked printed Usage and skipped result-feed publication as invalid input. -- Root cause: the diagnostic script was piped to wrapper stdin and the mandatory double-dash command separator was omitted. -- Correction: invoke homelab-chat-run-linked as COMMAND_ID MODE COMPONENT -- command arguments; stdin is not its command interface. -ANTI_REGRESSION_TOKEN=HOMELAB_CHAT_RUN_COMMAND_REQUIRES_DOUBLE_DASH -- Anti-regression: treat wrapper Usage as the authoritative CLI contract and syntax-check the complete launcher, not only its inner payload. -- Infrastructure action executed: none. -- Backup payload impact: none. -- Production impact: none. -- Status: CLOSED; UNRESOLVED_TAIL_COUNT=0. -- Registered at: [PRIVATE_IP]T11:20:32Z OUTPUT_END CHAT_OUTPUT_END