{
  "schema_version": 1,
  "channel": "homelab-runtime",
  "command_id": "GITEA-READER-ROUTES-AUDIT-289",
  "status": "OK",
  "rc": 0,
  "host": "pve01",
  "mode": "read-only",
  "component": "cluster-audit",
  "started_at_utc": "2026-07-22T21:23:40Z",
  "finished_at_utc": "2026-07-22T21:23:40Z",
  "reference_register_checked": true,
  "reference_sha256": "5763f2f8edc75fcf6f3951f8c95896d1cc112ceccb6cf5b64a383f540a4deb8e",
  "error_register_checked": true,
  "error_register_sha256": "08b3a769b8e38eaa1acec915352fb328180d70caa7ff6cd9d1ec0da426262752",
  "command_sha256": "ca1499ccd79985134e654f663bd829da73f07f8aa2af0a227387a97e6c32c66d",
  "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": "8077c2ba090c3c446ffa120fd830682af6b005d086e4263b67ad67eb043d3a9b",
  "sanitized_output_sha256": "8077c2ba090c3c446ffa120fd830682af6b005d086e4263b67ad67eb043d3a9b",
  "output_truncated_in_json": false,
  "full_sanitized_output_url": "https://git.gram1.ru/.well-known/homelab-runtime/latest.txt",
  "output": "PREVIOUS_COMMAND_ID=GITEA-READER-BIND-AUDIT-288\nDETAIL=     1\t#!/usr/bin/python3\nDETAIL=     2\timport hashlib\nDETAIL=     3\timport http.server\nDETAIL=     4\timport json\nDETAIL=     5\timport os\nDETAIL=     6\timport pathlib\nDETAIL=     7\timport re\nDETAIL=     8\timport sys\nDETAIL=     9\t\nDETAIL=    10\tROOT=pathlib.Path(\"/var/lib/homelab-reader-v3\")\nDETAIL=    11\tMANIFEST=ROOT/\"manifest.json\"\nDETAIL=    12\tRESULTS=ROOT/\"results\"\nDETAIL=    13\tCAPABILITY=os.environ[\"HOMELAB_READER_CAPABILITY\"]\nDETAIL=    14\tPORT=int(os.environ.get(\"HOMELAB_READER_PORT\",\"18788\"))\nDETAIL=    15\tRESULT_RE=re.compile(r\"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}-[0-9a-f]{64}\\.txt$\")\nDETAIL=    16\t\nDETAIL=    17\tclass Handler(http.server.BaseHTTPRequestHandler):\nDETAIL=    18\t    server_version=\"HomelabReader/3\"\nDETAIL=    19\t\nDETAIL=    20\t    def log_message(self,fmt,*args):\nDETAIL=    21\t        sys.stderr.write(\"%s - %s\\n\"%(self.address_string(),fmt%args))\nDETAIL=    22\t\nDETAIL=    23\t    def reply(self,status,content_type,payload,etag=\"\",include_body=True):\nDETAIL=    24\t        self.send_response(status)\nDETAIL=    25\t        self.send_header(\"Content-Type\",content_type)\nDETAIL=    26\t        self.send_header(\"Content-Length\",str(len(payload)))\nDETAIL=    27\t        self.send_header(\"Cache-Control\",\"no-store, no-cache, must-revalidate, max-age=0\")\nDETAIL=    28\t        self.send_header(\"Surrogate-Control\",\"no-store\")\nDETAIL=    29\t        self.send_header(\"Pragma\",\"no-cache\")\nDETAIL=    30\t        self.send_header(\"Expires\",\"0\")\nDETAIL=    31\t        self.send_header(\"X-Robots-Tag\",\"noindex, nofollow, noarchive\")\nDETAIL=    32\t        if etag:\nDETAIL=    33\t            self.send_header(\"ETag\",chr(34)+etag+chr(34))\nDETAIL=    34\t            self.send_header(\"X-Homelab-Generation\",etag)\nDETAIL=    35\t        self.end_headers()\nDETAIL=    36\t        if include_body:\nDETAIL=    37\t            self.wfile.write(payload)\nDETAIL=    38\t\nDETAIL=    39\t    def do_HEAD(self):\nDETAIL=    40\t        self.handle_reader(False)\nDETAIL=    41\t\nDETAIL=    42\t    def do_GET(self):\nDETAIL=    43\t        self.handle_reader(True)\nDETAIL=    44\t\nDETAIL=    45\t    def handle_reader(self,include_body):\nDETAIL=    46\t        request_path=self.path.split(\"?\",1)[0]\nDETAIL=    47\t        prefix=\"/\"+CAPABILITY+\"/\"\nDETAIL=    48\t        if not request_path.startswith(prefix):\nDETAIL=    49\t            self.reply(404,\"text/plain; charset=utf-8\",b\"not found\\n\",include_body=include_body)\nDETAIL=    50\t            return\nDETAIL=    51\t\nDETAIL=    52\t        relative=request_path[len(prefix):]\nDETAIL=    53\t\nDETAIL=    54\t        if relative==\"health\":\nDETAIL=    55\t            self.reply(200,\"application/json; charset=utf-8\",b\"{\\\"status\\\":\\\"ok\\\",\\\"version\\\":3}\\n\",\"health-v3\",include_body)\nDETAIL=    56\t            return\nDETAIL=    57\t\nDETAIL=    58\t        if relative==\"feed\":\nDETAIL=    59\t            try:\nDETAIL=    60\t                payload=MANIFEST.read_bytes()\nDETAIL=    61\t                document=json.loads(payload.decode(\"utf-8\"))\nDETAIL=    62\t                generation=str(document.get(\"generation\",\"0\"))\nDETAIL=    63\t                self.reply(200,\"application/json; charset=utf-8\",payload,generation,include_body)\nDETAIL=    64\t            except Exception:\nDETAIL=    65\t                self.reply(500,\"text/plain; charset=utf-8\",b\"reader error\\n\",include_body=include_body)\nDETAIL=    66\t            return\nDETAIL=    67\t\nDETAIL=    68\t        if relative.startswith(\"results/\"):\nDETAIL=    69\t            name=relative[len(\"results/\"):]\nDETAIL=    70\t            target=RESULTS/name\nDETAIL=    71\t            if not RESULT_RE.fullmatch(name) or not target.is_file():\nDETAIL=    72\t                self.reply(404,\"text/plain; charset=utf-8\",b\"not found\\n\",include_body=include_body)\nDETAIL=    73\t                return\nDETAIL=    74\t            try:\nDETAIL=    75\t                payload=target.read_bytes()\nDETAIL=    76\t                digest=name.rsplit(\"-\",1)[1][:-4]\nDETAIL=    77\t                if hashlib.sha256(payload).hexdigest()!=digest:\nDETAIL=    78\t                    self.reply(500,\"text/plain; charset=utf-8\",b\"integrity error\\n\",include_body=include_body)\nDETAIL=    79\t                    return\nDETAIL=    80\t                self.reply(200,\"text/plain; charset=utf-8\",payload,digest,include_body)\nDETAIL=    81\t            except Exception:\nDETAIL=    82\t                self.reply(500,\"text/plain; charset=utf-8\",b\"reader error\\n\",include_body=include_body)\nDETAIL=    83\t            return\nDETAIL=    84\t\nDETAIL=    85\t        self.reply(404,\"text/plain; charset=utf-8\",b\"not found\\n\",include_body=include_body)\nDETAIL=    86\t\nDETAIL=    87\tclass Server(http.server.ThreadingHTTPServer):\nDETAIL=    88\t    allow_reuse_address=True\nDETAIL=    89\t\nDETAIL=    90\tServer((\"127.0.0.1\",PORT),Handler).serve_forever()\nCHANGES_MADE=NO\n"
}
