29 lines
36 KiB
Plaintext
29 lines
36 KiB
Plaintext
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/<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":[]}
|
|
|
|
OUTPUT_END
|
|
CHAT_OUTPUT_END
|