31 lines
40 KiB
JSON
31 lines
40 KiB
JSON
{
|
|
"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/<COMMAND_ID>.json\",\"publish runtime/by-command/<COMMAND_ID>.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"
|
|
}
|