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
