diff --git a/runtime/history/RESULT-API-PATCH-AUDIT-20260727T075149Z.json b/runtime/history/RESULT-API-PATCH-AUDIT-20260727T075149Z.json new file mode 100644 index 0000000..ad4575a --- /dev/null +++ b/runtime/history/RESULT-API-PATCH-AUDIT-20260727T075149Z.json @@ -0,0 +1,38 @@ +{ + "schema_version": 1, + "channel": "homelab-runtime", + "command_id": "RESULT-API-PATCH-AUDIT-20260727T075149Z", + "status": "OK", + "rc": 0, + "host": "pve01", + "mode": "verify", + "component": "result-integration", + "started_at_utc": "2026-07-27T07:52:45Z", + "finished_at_utc": "2026-07-27T07:52:45Z", + "reference_register_checked": true, + "reference_sha256": "5763f2f8edc75fcf6f3951f8c95896d1cc112ceccb6cf5b64a383f540a4deb8e", + "error_register_checked": true, + "error_register_sha256": "24934a2c7fa5c26f6e828583c4aefac7f143b40c0171736ed7571e617d55ab83", + "command_sha256": "b564f92f4324ba19d5c6752e0c53111024fd37092acacc2281d2ab9ac7f39359", + "duplicate_failed_command_blocked": false, + "block_reason": null, + "execution_started": true, + "change_declared": false, + "result_contract_valid": true, + "result_contract_status": null, + "result_contract_error": null, + "command_rc": 0, + "changes_made": false, + "rollback_started": false, + "rollback_restored": null, + "mutation_outcome": "NO_MUTATION", + "sanitized": true, + "secrets_included": false, + "private_addresses_included": false, + "raw_evidence_retained_locally": true, + "raw_evidence_sha256": "1dabd515e3ddd0d132d8f6cd354770e78207200337dfda97f41dcacb8b3b576d", + "sanitized_output_sha256": "1dabd515e3ddd0d132d8f6cd354770e78207200337dfda97f41dcacb8b3b576d", + "output_truncated_in_json": false, + "full_sanitized_output_url": "https://git.gram1.ru/.well-known/homelab-runtime/latest.txt", + "output": "{\"adapter_functions\":{\"atomic_json\":{\"arguments\":[{\"has_default\":false,\"kind\":\"positional_or_keyword\",\"name\":\"path\"},{\"has_default\":false,\"kind\":\"positional_or_keyword\",\"name\":\"value\"}],\"is_async\":false,\"line_end\":238,\"line_start\":202,\"name\":\"atomic_json\"},\"dispatch\":{\"arguments\":[{\"has_default\":false,\"kind\":\"positional_or_keyword\",\"name\":\"request\"}],\"is_async\":false,\"line_end\":2502,\"line_start\":2419,\"name\":\"dispatch\"},\"load_json\":{\"arguments\":[{\"has_default\":false,\"kind\":\"positional_or_keyword\",\"name\":\"path\"}],\"is_async\":false,\"line_end\":248,\"line_start\":241,\"name\":\"load_json\"},\"public_state\":{\"arguments\":[{\"has_default\":false,\"kind\":\"keyword_only\",\"name\":\"short_id\"},{\"has_default\":false,\"kind\":\"keyword_only\",\"name\":\"session_token\"},{\"has_default\":false,\"kind\":\"keyword_only\",\"name\":\"client_ip\"}],\"is_async\":false,\"line_end\":1324,\"line_start\":1311,\"name\":\"public_state\"},\"run_command\":{\"arguments\":[{\"has_default\":false,\"kind\":\"positional_or_keyword\",\"name\":\"args\"},{\"has_default\":true,\"kind\":\"keyword_only\",\"name\":\"timeout\"},{\"has_default\":true,\"kind\":\"keyword_only\",\"name\":\"env\"}],\"is_async\":false,\"line_end\":673,\"line_start\":658,\"name\":\"run_command\"},\"sanitize_state\":{\"arguments\":[{\"has_default\":false,\"kind\":\"positional_or_keyword\",\"name\":\"state\"}],\"is_async\":false,\"line_end\":1372,\"line_start\":1327,\"name\":\"sanitize_state\"},\"sha256_file\":{\"arguments\":[{\"has_default\":false,\"kind\":\"positional_or_keyword\",\"name\":\"path\"}],\"is_async\":false,\"line_end\":199,\"line_start\":194,\"name\":\"sha256_file\"},\"state_path\":{\"arguments\":[{\"has_default\":false,\"kind\":\"positional_or_keyword\",\"name\":\"short_id\"}],\"is_async\":false,\"line_end\":638,\"line_start\":635,\"name\":\"state_path\"},\"worker\":{\"arguments\":[{\"has_default\":false,\"kind\":\"positional_or_keyword\",\"name\":\"short_id\"}],\"is_async\":false,\"line_end\":2183,\"line_start\":1994,\"name\":\"worker\"}},\"backup_changed\":false,\"changes_made\":false,\"checks\":{\"adapter_functions_complete\":true,\"adapter_healthy\":true,\"adapter_sha\":true,\"current_running_once\":true,\"no_invalid_states\":true,\"no_other_running\":true,\"portal_functions_complete\":true,\"portal_handler_do_get_present\":true,\"portal_handler_present\":true,\"portal_handler_reply_present\":true,\"portal_healthy\":true,\"portal_sha\":true,\"runner_sha\":true,\"source_excerpts_sanitized\":true},\"cluster_changed\":false,\"command_id\":\"RESULT-API-PATCH-AUDIT-20260727T075149Z\",\"gitea_changed\":false,\"plan_contract\":{\"adapter_operations\":[\"result_public\",\"result_public_key\"],\"create\":[\"/var/lib/homelab-command-adapter/result-signing/private.pem\",\"/var/lib/homelab-command-adapter/result-signing/public.pem\"],\"modify\":[\"/usr/local/sbin/homelab-command-adapter\",\"/usr/local/sbin/homelab-command-portal\"],\"preserve\":[\"/usr/local/sbin/homelab-chat-run\",\"existing state files\",\"existing evidence objects\",\"backup\",\"cluster\",\"Gitea history\"],\"public_routes\":[\"/.well-known/homelab-result//.json\",\"/.well-known/homelab-result-key.pem\"]},\"portal_classes\":{\"Handler\":{\"line_end\":892,\"line_start\":451,\"methods\":[{\"arguments\":[{\"has_default\":false,\"kind\":\"positional_or_keyword\",\"name\":\"self\"},{\"has_default\":false,\"kind\":\"positional_or_keyword\",\"name\":\"fmt\"},{\"has_default\":false,\"kind\":\"var_positional\",\"name\":\"args\"}],\"is_async\":false,\"line_end\":469,\"line_start\":455,\"name\":\"log_message\"},{\"arguments\":[{\"has_default\":false,\"kind\":\"positional_or_keyword\",\"name\":\"self\"},{\"has_default\":false,\"kind\":\"positional_or_keyword\",\"name\":\"status\"},{\"has_default\":false,\"kind\":\"positional_or_keyword\",\"name\":\"content_type\"},{\"has_default\":false,\"kind\":\"positional_or_keyword\",\"name\":\"body\"},{\"has_default\":true,\"kind\":\"keyword_only\",\"name\":\"headers\"}],\"is_async\":false,\"line_end\":504,\"line_start\":471,\"name\":\"reply\"},{\"arguments\":[{\"has_default\":false,\"kind\":\"positional_or_keyword\",\"name\":\"self\"},{\"has_default\":false,\"kind\":\"positional_or_keyword\",\"name\":\"location\"},{\"has_default\":true,\"kind\":\"keyword_only\",\"name\":\"headers\"}],\"is_async\":false,\"line_end\":519,\"line_start\":506,\"name\":\"redirect\"},{\"arguments\":[{\"has_default\":false,\"kind\":\"positional_or_keyword\",\"name\":\"self\"},{\"has_default\":false,\"kind\":\"positional_or_keyword\",\"name\":\"status\"},{\"has_default\":false,\"kind\":\"positional_or_keyword\",\"name\":\"message\"}],\"is_async\":false,\"line_end\":529,\"line_start\":521,\"name\":\"error_reply\"},{\"arguments\":[{\"has_default\":false,\"kind\":\"positional_or_keyword\",\"name\":\"self\"}],\"is_async\":false,\"line_end\":535,\"line_start\":531,\"name\":\"content_length\"},{\"arguments\":[{\"has_default\":false,\"kind\":\"positional_or_keyword\",\"name\":\"self\"},{\"has_default\":false,\"kind\":\"positional_or_keyword\",\"name\":\"limit\"}],\"is_async\":false,\"line_end\":544,\"line_start\":537,\"name\":\"read_body\"},{\"arguments\":[{\"has_default\":false,\"kind\":\"positional_or_keyword\",\"name\":\"self\"}],\"is_async\":false,\"line_end\":551,\"line_start\":546,\"name\":\"path_parts\"},{\"arguments\":[{\"has_default\":false,\"kind\":\"positional_or_keyword\",\"name\":\"self\"}],\"is_async\":false,\"line_end\":749,\"line_start\":553,\"name\":\"do_GET\"},{\"arguments\":[{\"has_default\":false,\"kind\":\"positional_or_keyword\",\"name\":\"self\"}],\"is_async\":false,\"line_end\":892,\"line_start\":751,\"name\":\"do_POST\"}],\"name\":\"Handler\"}},\"portal_functions\":{\"adapter_call\":{\"arguments\":[{\"has_default\":false,\"kind\":\"positional_or_keyword\",\"name\":\"request\"}],\"is_async\":false,\"line_end\":91,\"line_start\":44,\"name\":\"adapter_call\"}},\"private_addresses_included\":false,\"rollback_started\":false,\"sanitized\":true,\"schema\":1,\"secrets_included\":false,\"service_files\":{\"adapter\":\"[Unit]\\nDescription=Homelab command adapter\\nAfter=network-online.target\\nWants=network-online.target\\n\\n[Service]\\nType=simple\\nExecStart=/usr/bin/python3 -B /usr/local/sbin/homelab-command-adapter --serve\\nUser=root\\nGroup=homelab-command-portal\\nUMask=0027\\nRuntimeDirectory=homelab-command-adapter\\nRuntimeDirectoryMode=0750\\nRestart=on-failure\\nRestartSec=2\\nNoNewPrivileges=false\\nPrivateTmp=true\\nProtectClock=true\\nProtectControlGroups=true\\nProtectKernelLogs=true\\nProtectKernelModules=true\\nProtectKernelTunables=true\\nRestrictRealtime=true\\nLockPersonality=true\\nMemoryDenyWriteExecute=true\\nSystemCallArchitectures=native\\nTimeoutStopSec=15\\n\\n[Install]\\nWantedBy=multi-user.target\\n\",\"portal\":\"[Unit]\\nDescription=Homelab command portal\\nAfter=network-online.target homelab-command-adapter.service\\nRequires=homelab-command-adapter.service\\nWants=network-online.target\\nStartLimitIntervalSec=30\\nStartLimitBurst=3\\n\\n[Service]\\nType=simple\\nExecStartPre=/usr/bin/python3 -B /usr/local/sbin/homelab-command-portal --self-test\\nExecStart=/usr/bin/python3 -B /usr/local/sbin/homelab-command-portal\\nUser=homelab-command-portal\\nGroup=homelab-command-portal\\nUMask=0077\\nRestart=on-failure\\nRestartSec=2\\nNoNewPrivileges=true\\nPrivateTmp=true\\nPrivateDevices=true\\nProtectSystem=strict\\nProtectHome=true\\nReadWritePaths=/var/lib/homelab-command-portal\\nProtectClock=true\\nProtectControlGroups=true\\nProtectKernelLogs=true\\nProtectKernelModules=true\\nProtectKernelTunables=true\\nRestrictSUIDSGID=true\\nRestrictRealtime=true\\nLockPersonality=true\\nMemoryDenyWriteExecute=true\\nRestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX\\nSystemCallArchitectures=native\\nTimeoutStopSec=15\\n\\n[Install]\\nWantedBy=multi-user.target\\n\"},\"services\":{\"adapter\":{\"active\":\"active\",\"pid\":\"976008\",\"restarts\":\"0\",\"sub\":\"running\"},\"portal\":{\"active\":\"active\",\"pid\":\"976035\",\"restarts\":\"0\",\"sub\":\"running\"}},\"source_excerpts\":{\"adapter_dispatch\":\"2414: \\\"ok\\\": True,\\n2415: \\\"result\\\": value,\\n2416: }\\n2417: \\n2418: \\n2419: def dispatch(request: dict[str, Any]) -> dict[str, Any]:\\n2420: operation = request.get(\\\"op\\\")\\n2421: if operation == \\\"health\\\":\\n2422: return response_ok(\\n2423: {\\n2424: \\\"status\\\": \\\"ok\\\",\\n2425: \\\"version\\\": 1,\\n2426: }\\n2427: )\\n2428: if operation == \\\"bind_enrollment\\\":\\n2429: return response_ok(\\n2430: bind_enrollment(\\n2431: str(request.get(\\\"token\\\", \\\"\\\")),\\n2432: str(request.get(\\\"client_ip\\\", \\\"\\\")),\\n2433: )\\n2434: )\\n2435: if operation == \\\"enroll\\\":\\n2436: return response_ok(\\n2437: enroll_device(\\n2438: str(request.get(\\\"token\\\", \\\"\\\")),\\n2439: str(request.get(\\\"client_ip\\\", \\\"\\\")),\\n2440: )\\n2441: )\\n2442: if operation == \\\"import\\\":\\n2443: return response_ok(\\n2444: import_capsule(\\n2445: device_token=[REDACTED]\\n2446: request.get(\\\"device_token\\\", \\\"\\\")\\n2447: ),\\n2448: client_ip=str(\\n2449: request.get(\\\"client_ip\\\", \\\"\\\")\\n2450: ),\\n2451: upload_path=str(\\n2452: request.get(\\\"upload_path\\\", \\\"\\\")\\n2453: ),\\n2454: )\\n2455: )\\n2456: if operation == \\\"consume_bootstrap\\\":\\n2457: return response_ok(\\n2458: consume_bootstrap(\\n2459: short_id=str(request.get(\\\"short_id\\\", \\\"\\\")),\\n2460: token=[REDACTED]\\\"token\\\", \\\"\\\")),\\n2461: client_ip=str(\\n2462: request.get(\\\"client_ip\\\", \\\"\\\")\\n2463: ),\\n2464: )\\n2465: )\\n2466: if operation == \\\"state\\\":\\n2467: return response_ok(\\n2468: public_state(\\n2469: short_id=str(request.get(\\\"short_id\\\", \\\"\\\")),\\n2470: session_token=[REDACTED]\\n2471: request.get(\\\"session_token\\\", \\\"\\\")\\n2472: ),\\n2473: client_ip=str(\\n2474: request.get(\\\"client_ip\\\", \\\"\\\")\\n2475: ),\\n2476: )\\n2477: )\\n2478: if operation == \\\"execute\\\":\\n2479: return response_ok(\\n2480: start_execution(\\n2481: short_id=str(request.get(\\\"short_id\\\", \\\"\\\")),\\n2482: session_token=[REDACTED]\\n2483: request.get(\\\"session_token\\\", \\\"\\\")\\n2484: ),\\n2485: client_ip=str(\\n2486: request.get(\\\"client_ip\\\", \\\"\\\")\\n2487: ),\\n2488: )\\n2489: )\\n2490: if operation == \\\"evidence_url\\\":\\n2491: return response_ok(\\n2492: evidence_url(\\n2493: short_id=str(request.get(\\\"short_id\\\", \\\"\\\")),\\n2494: session_token=[REDACTED]\\n2495: request.get(\\\"session_token\\\", \\\"\\\")\\n2496: ),\\n2497: client_ip=str(\\n2498: request.get(\\\"client_ip\\\", \\\"\\\")\\n2499: ),\\n2500: )\\n2501: )\\n2502: raise AdapterError(\\\"unsupported operation\\\")\\n2503: \\n2504: \\n2505: class Handler(socketserver.StreamRequestHandler):\\n2506: def handle(self) -> None:\\n2507: credentials = self.request.getsockopt(\",\"adapter_public_state\":\"1306: ):\\n1307: return session\\n1308: raise AdapterError(\\\"session authorization failed\\\")\\n1309: \\n1310: \\n1311: def public_state(\\n1312: *,\\n1313: short_id: str,\\n1314: session_token: [REDACTED]\\n1315: client_ip: str,\\n1316: ) -> dict[str, Any]:\\n1317: with GlobalLock():\\n1318: state = load_json(state_path(short_id))\\n1319: authorize_session(\\n1320: state,\\n1321: session_token=[REDACTED]\\n1322: client_ip=client_ip,\\n1323: )\\n1324: return sanitize_state(state)\\n1325: \\n1326: \\n1327: def sanitize_state(state: dict[str, Any]) -> dict[str, Any]:\\n1328: execution = state.get(\\\"execution\\\")\\n1329: public_execution = None\",\"adapter_sanitize_state\":\"1322: client_ip=client_ip,\\n1323: )\\n1324: return sanitize_state(state)\\n1325: \\n1326: \\n1327: def sanitize_state(state: dict[str, Any]) -> dict[str, Any]:\\n1328: execution = state.get(\\\"execution\\\")\\n1329: public_execution = None\\n1330: if isinstance(execution, dict):\\n1331: public_execution = {\\n1332: key: execution.get(key)\\n1333: for key in (\\n1334: \\\"started_at_utc\\\",\\n1335: \\\"finished_at_utc\\\",\\n1336: \\\"runner_rc\\\",\\n1337: \\\"runner_document_rc\\\",\\n1338: \\\"runner_status\\\",\\n1339: \\\"runner_wrapper_matches_document\\\",\\n1340: \\\"runner_json_sha256\\\",\\n1341: \\\"runner_text_sha256\\\",\\n1342: )\\n1343: if key in execution\\n1344: }\\n1345: \\n1346: evidence = state.get(\\\"evidence\\\")\\n1347: public_evidence = None\\n1348: if isinstance(evidence, dict):\\n1349: public_evidence = {\\n1350: key: evidence.get(key)\\n1351: for key in (\\n1352: \\\"object_key\\\",\\n1353: \\\"sha256\\\",\\n1354: \\\"bytes\\\",\\n1355: \\\"gateway_verified\\\",\\n1356: )\\n1357: if key in evidence\\n1358: }\\n1359: \\n1360: return {\\n1361: \\\"schema_version\\\": 1,\\n1362: \\\"short_id\\\": state[\\\"short_id\\\"],\\n1363: \\\"capsule_sha256\\\": state[\\\"capsule_sha256\\\"],\\n1364: \\\"capsule_bytes\\\": state[\\\"capsule_bytes\\\"],\\n1365: \\\"manifest\\\": state[\\\"manifest\\\"],\\n1366: \\\"imported_at_utc\\\": state[\\\"imported_at_utc\\\"],\\n1367: \\\"command_object\\\": state[\\\"command_object\\\"],\\n1368: \\\"status\\\": state[\\\"status\\\"],\\n1369: \\\"execution\\\": public_execution,\\n1370: \\\"evidence\\\": public_evidence,\\n1371: \\\"error\\\": state.get(\\\"error\\\"),\\n1372: }\\n1373: \\n1374: \\n1375: def start_execution(\\n1376: *,\\n1377: short_id: str,\",\"adapter_worker_finalization\":\"2058: state[\\\"manifest\\\"][\\\"command_id\\\"],\\n2059: started_epoch,\\n2060: )\\n2061: runner_document = load_json(runner_json)\\n2062: \\n2063: bundle, evidence_sha, evidence_bytes = (\\n2064: create_evidence_bundle(\\n2065: state=state,\\n2066: runner_json=runner_json,\\n2067: runner_txt=runner_txt,\\n2068: )\\n2069: )\\n2070: prefix = (\\n2071: \\\"portal-tests\\\"\\n2072: if state[\\\"manifest\\\"][\\\"component\\\"]\\n2073: == \\\"command-portal-smoke\\\"\\n2074: else \\\"portal\\\"\\n2075: )\\n2076: object_key = (\\n2077: f\\\"{prefix}/\\\"\\n2078: f\\\"{state['manifest']['command_id']}/\\\"\\n2079: f\\\"{state['capsule_sha256']}/\\\"\\n2080: \\\"evidence.tgz\\\"\\n2081: )\\n2082: upload_evidence(\\n2083: bundle,\\n2084: object_key,\\n2085: evidence_sha,\\n2086: )\\n2087: \\n2088: with GlobalLock():\\n2089: current = load_json(path)\\n2090: if not worker_state_owned(\\n2091: current,\\n2092: os.getpid(),\\n2093: ):\\n2094: raise AdapterError(\\n2095: \\\"worker finalization ownership lost\\\"\\n2096: )\\n2097: document_status = str(\\n2098: runner_document.get(\\\"status\\\", \\\"FAIL\\\")\\n2099: )\\n2100: try:\\n2101: document_rc = int(runner_document[\\\"rc\\\"])\\n2102: except (KeyError, TypeError, ValueError) as exc:\\n2103: raise AdapterError(\\\"runner artifact rc invalid\\\") from exc\\n2104: wrapper_matches = result.returncode == document_rc\\n2105: current[\\\"status\\\"] = (\\n2106: document_status if wrapper_matches else \\\"FAIL\\\"\\n2107: )\\n2108: current[\\\"execution\\\"].update(\\n2109: {\\n2110: \\\"finished_at_utc\\\": iso(),\\n2111: \\\"runner_rc\\\": result.returncode,\\n2112: \\\"runner_document_rc\\\": document_rc,\\n2113: \\\"runner_status\\\": document_status,\\n2114: \\\"runner_wrapper_matches_document\\\": wrapper_matches,\\n2115: \\\"runner_json\\\": str(runner_json),\\n2116: \\\"runner_text\\\": str(runner_txt),\\n2117: \\\"runner_json_sha256\\\": sha256_file(\\n2118: runner_json\\n2119: ),\\n2120: \\\"runner_text_sha256\\\": sha256_file(\\n2121: runner_txt\\n2122: ),\\n2123: }\\n2124: )\\n2125: current[\\\"evidence\\\"] = {\\n2126: \\\"object_key\\\": object_key,\\n2127: \\\"sha256\\\": evidence_sha,\\n2128: \\\"bytes\\\": evidence_bytes,\\n2129: \\\"local_bundle\\\": str(bundle),\\n2130: \\\"gateway_verified\\\": True,\\n2131: }\\n2132: current[\\\"error\\\"] = (\\n2133: None\\n2134: if wrapper_matches\\n2135: else {\\n2136: \\\"type\\\": \\\"RunnerWrapperMismatch\\\",\\n2137: \\\"message\\\": (\\n2138: \\\"existing runner returned a different rc \\\"\\n2139: \\\"after writing its public artifact; \\\"\\n2140: \\\"runtime publication or wrapper completion failed\\\"\\n2141: ),\\n2142: }\\n2143: )\\n2144: atomic_json(path, current)\\n2145: return 0\\n2146: \\n2147: except Exception as exc:\\n2148: try:\\n2149: with wrapper_log.open(\\n2150: \\\"a\\\",\\n2151: encoding=\\\"utf-8\\\",\\n2152: errors=\\\"replace\\\",\\n2153: ) as stream:\\n2154: stream.write(\\n2155: f\\\"WORKER_ERROR={type(exc).__name__}:{exc}\\\\\\\\n\\\"\\n2156: )\\n2157: os.chmod(wrapper_log, 0o640)\\n2158: except Exception:\\n2159: pass\\n2160: \\n2161: with GlobalLock():\\n2162: try:\\n2163: current = load_json(path)\\n2164: if worker_state_owned(\\n2165: current,\\n2166: os.getpid(),\\n2167: ):\\n2168: current[\\\"status\\\"] = \\\"FAIL\\\"\\n2169: if isinstance(\\n2170: current.get(\\\"execution\\\"),\\n2171: dict,\\n2172: ):\\n2173: current[\\\"execution\\\"][\\n2174: \\\"finished_at_utc\\\"\\n2175: ] = iso()\\n2176: current[\\\"error\\\"] = {\\n2177: \\\"type\\\": type(exc).__name__,\\n2178: \\\"message\\\": str(exc)[:4000],\\n2179: }\\n2180: atomic_json(path, current)\\n2181: except Exception:\\n2182: pass\\n2183: return 1\",\"portal_Handler_do_GET\":\"550: keep_blank_values=True,\\n551: )\\n552: \\n553: def do_GET(self) -> None:\\n554: try:\\n555: path, query = self.path_parts()\\n556: \\n557: if path == \\\"/healthz\\\":\\n558: parse_client(self, allow_local_health=True)\\n559: result = adapter_call({\\\"op\\\": \\\"health\\\"})\\n560: body = (\\n561: json.dumps(\\n562: {\\n563: \\\"status\\\": \\\"ok\\\",\\n564: \\\"version\\\": 1,\\n565: \\\"adapter\\\": result[\\\"status\\\"],\\n566: },\\n567: separators=(\\\",\\\", \\\":\\\"),\\n568: sort_keys=True,\\n569: )\\n570: + \\\"\\\\n\\\"\\n571: ).encode()\\n572: self.reply(\\n573: 200,\\n574: \\\"application/json; charset=utf-8\\\",\\n575: body,\\n576: )\\n577: return\\n578: \\n579: client_ip = parse_client(self)\\n580: \\n581: if path == \\\"/bridge/bridge.cmd\\\":\\n582: self.reply(\\n583: 200,\\n584: \\\"application/octet-stream\\\",\\n585: BRIDGE_CMD.encode(\\\"utf-8\\\"),\\n586: headers={\\n587: \\\"Content-Disposition\\\": (\\n588: 'attachment; filename=\\\"bridge.cmd\\\"'\\n589: )\\n590: },\\n591: )\\n592: return\\n593: \\n594: if path.startswith(\\\"/enroll/\\\"):\\n595: relative = path[len(\\\"/enroll/\\\") :]\\n596: if relative.endswith(\\\"/bridge-setup.cmd\\\"):\\n597: token = [REDACTED]\\n598: : -len(\\\"/bridge-setup.cmd\\\")\\n599: ]\\n600: adapter_call(\\n601: {\\n602: \\\"op\\\": \\\"bind_enrollment\\\",\\n603: \\\"token\\\": token,\\n604: \\\"client_ip\\\": client_ip,\\n605: }\\n606: )\\n607: self.reply(\\n608: 200,\\n609: \\\"application/octet-stream\\\",\\n610: setup_script(token),\\n611: headers={\\n612: \\\"Content-Disposition\\\": (\\n613: 'attachment; '\\n614: 'filename=\\\"HomelabBridgeSetup.cmd\\\"'\\n615: )\\n616: },\\n617: )\\n618: return\\n619: \\n620: token = [REDACTED]\\n621: info = adapter_call(\\n622: {\\n623: \\\"op\\\": \\\"bind_enrollment\\\",\\n624: \\\"token\\\": token,\\n625: \\\"client_ip\\\": client_ip,\\n626: }\\n627: )\\n628: body = f\\\"\\\"\\\"\\n629: \\n630: \\n631: Homelab Bridge\\n632: \\n633: \\n634:

Установка Homelab Bridge

\\n635:

Профиль: {html.escape(str(info['label']))}

\\n636:

Ссылка одноразовая и уже привязана к этому NetBird-адресу.

\\n637:

Скачать установщик

\\n638:

После запуска установщика файлы .hcmd будут открываться двойным кликом.

\\n639: \\\"\\\"\\\".encode()\\n640: self.reply(\\n641: 200,\\n642: \\\"text/html; charset=utf-8\\\",\\n643: body,\\n644: )\\n645: return\\n646: \\n647: match = re.fullmatch(\\n648: r\\\"/r/([A-Z2-7]{10,20})\\\",\\n649: path,\\n650: )\\n651: if match:\\n652: short_id = safe_short(match.group(1))\\n653: token_values = query.get(\\\"t\\\", [])\\n654: if token_values:\\n655: token = [REDACTED]\\n656: session = adapter_call(\\n657: {\\n658: \\\"op\\\": \\\"consume_bootstrap\\\",\\n659: \\\"short_id\\\": short_id,\\n660: \\\"token\\\": token,\\n661: \\\"client_ip\\\": client_ip,\\n662: }\\n663: )\\n664: cookie = (\\n665: f\\\"__Host-HLSESSION={session['session_token']}; \\\"\\n666: f\\\"Max-Age={8 * 3600}; Path=/; \\\"\\n667: \\\"Secure; HttpOnly; SameSite=Strict\\\"\\n668: )\\n669: self.redirect(\\n670: f\\\"/r/{short_id}\\\",\\n671: headers={\\\"Set-Cookie\\\": cookie},\\n672: )\\n673: return\\n674: \\n675: session_token = [REDACTED]\\n676: state = adapter_call(\\n677: {\\n678: \\\"op\\\": \\\"state\\\",\\n679: \\\"short_id\\\": short_id,\\n680: \\\"session_token\\\": session_token,\\n681: \\\"client_ip\\\": client_ip,\\n682: }\\n683: )\\n684: self.reply(\\n685: 200,\\n686: \\\"text/html; charset=utf-8\\\",\\n687: render_card(state, session_token),\\n688: )\\n689: return\\n690: \\n691: evidence_match = re.fullmatch(\\n692: r\\\"/r/([A-Z2-7]{10,20})/evidence\\\",\\n693: path,\\n694: )\\n695: if evidence_match:\\n696: short_id = safe_short(\\n697: evidence_match.group(1)\\n698: )\\n699: signed = adapter_call(\\n700: {\\n701: \\\"op\\\": \\\"evidence_url\\\",\\n702: \\\"short_id\\\": short_id,\\n703: \\\"session_token\\\": cookie_session(self),\\n704: \\\"client_ip\\\": client_ip,\\n705: }\\n706: )\\n707: self.redirect(str(signed[\\\"url\\\"]))\\n708: return\\n709: \\n710: state_match = re.fullmatch(\\n711: r\\\"/api/state/([A-Z2-7]{10,20})\\\",\\n712: path,\\n713: )\\n714: if state_match:\\n715: short_id = safe_short(\\n716: state_match.group(1)\\n717: )\\n718: state = adapter_call(\\n719: {\\n720: \\\"op\\\": \\\"state\\\",\\n721: \\\"short_id\\\": short_id,\\n722: \\\"session_token\\\": cookie_session(self),\\n723: \\\"client_ip\\\": client_ip,\\n724: }\\n725: )\\n726: self.reply(\\n727: 200,\\n728: \\\"application/json; charset=utf-8\\\",\\n729: (\\n730: json.dumps(\\n731: state,\\n732: ensure_ascii=False,\\n733: separators=(\\\",\\\", \\\":\\\"),\\n734: sort_keys=True,\\n735: )\\n736: + \\\"\\\\n\\\"\\n737: ).encode(),\\n738: )\\n739: return\\n740: \\n741: self.error_reply(404, \\\"not found\\\")\\n742: \\n743: except PortalError as exc:\\n744: self.error_reply(403, str(exc))\\n745: except Exception as exc:\\n746: self.error_reply(\\n747: 500,\\n748: f\\\"{type(exc).__name__}:{str(exc)[:1000]}\\\",\\n749: )\\n750: \\n751: def do_POST(self) -> None:\\n752: try:\",\"portal_Handler_error_reply\":\"518: headers=values,\\n519: )\\n520: \\n521: def error_reply(self, status: int, message: str) -> None:\\n522: body = (\\n523: f\\\"PORTAL_ERROR={html.escape(message)}\\\\n\\\"\\n524: ).encode()\\n525: self.reply(\\n526: status,\\n527: \\\"text/plain; charset=utf-8\\\",\\n528: body,\\n529: )\\n530: \\n531: def content_length(self) -> int:\\n532: raw = self.headers.get(\\\"Content-Length\\\")\",\"portal_Handler_path_parts\":\"543: raise PortalError(\\\"short request body\\\")\\n544: return body\\n545: \\n546: def path_parts(self) -> tuple[str, dict[str, list[str]]]:\\n547: parsed = urllib.parse.urlsplit(self.path)\\n548: return parsed.path, urllib.parse.parse_qs(\\n549: parsed.query,\\n550: keep_blank_values=True,\\n551: )\\n552: \\n553: def do_GET(self) -> None:\\n554: try:\",\"portal_Handler_reply\":\"468: f\\\"{self.client_address[0]} - {message}\\\\n\\\"\\n469: )\\n470: \\n471: def reply(\\n472: self,\\n473: status: int,\\n474: content_type: str,\\n475: body: bytes,\\n476: *,\\n477: headers: dict[str, str] | None = None,\\n478: ) -> None:\\n479: self.send_response(status)\\n480: self.send_header(\\\"Content-Type\\\", content_type)\\n481: self.send_header(\\\"Content-Length\\\", str(len(body)))\\n482: self.send_header(\\n483: \\\"Cache-Control\\\",\\n484: \\\"no-store, no-cache, must-revalidate, max-age=0\\\",\\n485: )\\n486: self.send_header(\\\"Pragma\\\", \\\"no-cache\\\")\\n487: self.send_header(\\\"Expires\\\", \\\"0\\\")\\n488: self.send_header(\\n489: \\\"X-Robots-Tag\\\",\\n490: \\\"noindex, nofollow,noarchive\\\",\\n491: )\\n492: self.send_header(\\\"X-Content-Type-Options\\\", \\\"nosniff\\\")\\n493: self.send_header(\\\"Referrer-Policy\\\", \\\"no-referrer\\\")\\n494: self.send_header(\\n495: \\\"Content-Security-Policy\\\",\\n496: \\\"default-src 'none'; style-src 'unsafe-inline'; \\\"\\n497: \\\"form-action 'self'; frame-ancestors 'none'; \\\"\\n498: \\\"base-uri 'none'\\\",\\n499: )\\n500: for key, value in (headers or {}).items():\\n501: self.send_header(key, value)\\n502: self.end_headers()\\n503: if body:\\n504: self.wfile.write(body)\\n505: \\n506: def redirect(\\n507: self,\"},\"source_sha256\":{\"adapter\":\"8d0af84dcfcf6ddbfd1c4e6cbb059cef66b4cfc6c75bfd66da649acc229c7f5e\",\"portal\":\"8056faf4ef6348f0b06319ad3c49fbfeeadf25b6370f857ea19569bd27652ee2\",\"runner\":\"b248a4c32c9cc64e5747e7dce6c7fc0a23f5124a77c71ce72e27a81aceae9d2d\"},\"states\":{\"current_running\":[{\"command_id\":\"RESULT-API-PATCH-AUDIT-20260727T075149Z\",\"state_file\":\"275ZUEB6ZH.json\"}],\"invalid\":[],\"other_running\":[]},\"status\":\"OK\"}\n" +} diff --git a/runtime/history/RESULT-API-PATCH-AUDIT-20260727T075149Z.txt b/runtime/history/RESULT-API-PATCH-AUDIT-20260727T075149Z.txt new file mode 100644 index 0000000..8d2bbef --- /dev/null +++ b/runtime/history/RESULT-API-PATCH-AUDIT-20260727T075149Z.txt @@ -0,0 +1,33 @@ +CHAT_OUTPUT_BEGIN +COMMAND_ID=RESULT-API-PATCH-AUDIT-20260727T075149Z +STATUS=OK +RC=0 +HOST=pve01 +MODE=verify +COMPONENT=result-integration +REFERENCE_REGISTER_CHECK=OK +REFERENCE_SHA256=5763f2f8edc75fcf6f3951f8c95896d1cc112ceccb6cf5b64a383f540a4deb8e +ERROR_REGISTER_CHECK=OK +ERROR_REGISTER_SHA256=24934a2c7fa5c26f6e828583c4aefac7f143b40c0171736ed7571e617d55ab83 +COMMAND_SHA256=b564f92f4324ba19d5c6752e0c53111024fd37092acacc2281d2ab9ac7f39359 +DUPLICATE_FAILED_COMMAND_BLOCKED=false +EXECUTION_STARTED=true +CHANGE_DECLARED=false +RESULT_CONTRACT_VALID=true +RESULT_CONTRACT_STATUS=NOT_APPLICABLE +RESULT_CONTRACT_ERROR=NONE +COMMAND_RC=0 +CHANGES_MADE=false +ROLLBACK_STARTED=false +ROLLBACK_RESTORED=null +MUTATION_OUTCOME=NO_MUTATION +SANITIZED=yes +SECRETS_INCLUDED=no +PRIVATE_ADDRESSES_INCLUDED=no +RAW_EVIDENCE_SHA256=1dabd515e3ddd0d132d8f6cd354770e78207200337dfda97f41dcacb8b3b576d +SANITIZED_OUTPUT_SHA256=1dabd515e3ddd0d132d8f6cd354770e78207200337dfda97f41dcacb8b3b576d +OUTPUT_BEGIN +{"adapter_functions":{"atomic_json":{"arguments":[{"has_default":false,"kind":"positional_or_keyword","name":"path"},{"has_default":false,"kind":"positional_or_keyword","name":"value"}],"is_async":false,"line_end":238,"line_start":202,"name":"atomic_json"},"dispatch":{"arguments":[{"has_default":false,"kind":"positional_or_keyword","name":"request"}],"is_async":false,"line_end":2502,"line_start":2419,"name":"dispatch"},"load_json":{"arguments":[{"has_default":false,"kind":"positional_or_keyword","name":"path"}],"is_async":false,"line_end":248,"line_start":241,"name":"load_json"},"public_state":{"arguments":[{"has_default":false,"kind":"keyword_only","name":"short_id"},{"has_default":false,"kind":"keyword_only","name":"session_token"},{"has_default":false,"kind":"keyword_only","name":"client_ip"}],"is_async":false,"line_end":1324,"line_start":1311,"name":"public_state"},"run_command":{"arguments":[{"has_default":false,"kind":"positional_or_keyword","name":"args"},{"has_default":true,"kind":"keyword_only","name":"timeout"},{"has_default":true,"kind":"keyword_only","name":"env"}],"is_async":false,"line_end":673,"line_start":658,"name":"run_command"},"sanitize_state":{"arguments":[{"has_default":false,"kind":"positional_or_keyword","name":"state"}],"is_async":false,"line_end":1372,"line_start":1327,"name":"sanitize_state"},"sha256_file":{"arguments":[{"has_default":false,"kind":"positional_or_keyword","name":"path"}],"is_async":false,"line_end":199,"line_start":194,"name":"sha256_file"},"state_path":{"arguments":[{"has_default":false,"kind":"positional_or_keyword","name":"short_id"}],"is_async":false,"line_end":638,"line_start":635,"name":"state_path"},"worker":{"arguments":[{"has_default":false,"kind":"positional_or_keyword","name":"short_id"}],"is_async":false,"line_end":2183,"line_start":1994,"name":"worker"}},"backup_changed":false,"changes_made":false,"checks":{"adapter_functions_complete":true,"adapter_healthy":true,"adapter_sha":true,"current_running_once":true,"no_invalid_states":true,"no_other_running":true,"portal_functions_complete":true,"portal_handler_do_get_present":true,"portal_handler_present":true,"portal_handler_reply_present":true,"portal_healthy":true,"portal_sha":true,"runner_sha":true,"source_excerpts_sanitized":true},"cluster_changed":false,"command_id":"RESULT-API-PATCH-AUDIT-20260727T075149Z","gitea_changed":false,"plan_contract":{"adapter_operations":["result_public","result_public_key"],"create":["/var/lib/homelab-command-adapter/result-signing/private.pem","/var/lib/homelab-command-adapter/result-signing/public.pem"],"modify":["/usr/local/sbin/homelab-command-adapter","/usr/local/sbin/homelab-command-portal"],"preserve":["/usr/local/sbin/homelab-chat-run","existing state files","existing evidence objects","backup","cluster","Gitea history"],"public_routes":["/.well-known/homelab-result//.json","/.well-known/homelab-result-key.pem"]},"portal_classes":{"Handler":{"line_end":892,"line_start":451,"methods":[{"arguments":[{"has_default":false,"kind":"positional_or_keyword","name":"self"},{"has_default":false,"kind":"positional_or_keyword","name":"fmt"},{"has_default":false,"kind":"var_positional","name":"args"}],"is_async":false,"line_end":469,"line_start":455,"name":"log_message"},{"arguments":[{"has_default":false,"kind":"positional_or_keyword","name":"self"},{"has_default":false,"kind":"positional_or_keyword","name":"status"},{"has_default":false,"kind":"positional_or_keyword","name":"content_type"},{"has_default":false,"kind":"positional_or_keyword","name":"body"},{"has_default":true,"kind":"keyword_only","name":"headers"}],"is_async":false,"line_end":504,"line_start":471,"name":"reply"},{"arguments":[{"has_default":false,"kind":"positional_or_keyword","name":"self"},{"has_default":false,"kind":"positional_or_keyword","name":"location"},{"has_default":true,"kind":"keyword_only","name":"headers"}],"is_async":false,"line_end":519,"line_start":506,"name":"redirect"},{"arguments":[{"has_default":false,"kind":"positional_or_keyword","name":"self"},{"has_default":false,"kind":"positional_or_keyword","name":"status"},{"has_default":false,"kind":"positional_or_keyword","name":"message"}],"is_async":false,"line_end":529,"line_start":521,"name":"error_reply"},{"arguments":[{"has_default":false,"kind":"positional_or_keyword","name":"self"}],"is_async":false,"line_end":535,"line_start":531,"name":"content_length"},{"arguments":[{"has_default":false,"kind":"positional_or_keyword","name":"self"},{"has_default":false,"kind":"positional_or_keyword","name":"limit"}],"is_async":false,"line_end":544,"line_start":537,"name":"read_body"},{"arguments":[{"has_default":false,"kind":"positional_or_keyword","name":"self"}],"is_async":false,"line_end":551,"line_start":546,"name":"path_parts"},{"arguments":[{"has_default":false,"kind":"positional_or_keyword","name":"self"}],"is_async":false,"line_end":749,"line_start":553,"name":"do_GET"},{"arguments":[{"has_default":false,"kind":"positional_or_keyword","name":"self"}],"is_async":false,"line_end":892,"line_start":751,"name":"do_POST"}],"name":"Handler"}},"portal_functions":{"adapter_call":{"arguments":[{"has_default":false,"kind":"positional_or_keyword","name":"request"}],"is_async":false,"line_end":91,"line_start":44,"name":"adapter_call"}},"private_addresses_included":false,"rollback_started":false,"sanitized":true,"schema":1,"secrets_included":false,"service_files":{"adapter":"[Unit]\nDescription=Homelab command adapter\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=simple\nExecStart=/usr/bin/python3 -B /usr/local/sbin/homelab-command-adapter --serve\nUser=root\nGroup=homelab-command-portal\nUMask=0027\nRuntimeDirectory=homelab-command-adapter\nRuntimeDirectoryMode=0750\nRestart=on-failure\nRestartSec=2\nNoNewPrivileges=false\nPrivateTmp=true\nProtectClock=true\nProtectControlGroups=true\nProtectKernelLogs=true\nProtectKernelModules=true\nProtectKernelTunables=true\nRestrictRealtime=true\nLockPersonality=true\nMemoryDenyWriteExecute=true\nSystemCallArchitectures=native\nTimeoutStopSec=15\n\n[Install]\nWantedBy=multi-user.target\n","portal":"[Unit]\nDescription=Homelab command portal\nAfter=network-online.target homelab-command-adapter.service\nRequires=homelab-command-adapter.service\nWants=network-online.target\nStartLimitIntervalSec=30\nStartLimitBurst=3\n\n[Service]\nType=simple\nExecStartPre=/usr/bin/python3 -B /usr/local/sbin/homelab-command-portal --self-test\nExecStart=/usr/bin/python3 -B /usr/local/sbin/homelab-command-portal\nUser=homelab-command-portal\nGroup=homelab-command-portal\nUMask=0077\nRestart=on-failure\nRestartSec=2\nNoNewPrivileges=true\nPrivateTmp=true\nPrivateDevices=true\nProtectSystem=strict\nProtectHome=true\nReadWritePaths=/var/lib/homelab-command-portal\nProtectClock=true\nProtectControlGroups=true\nProtectKernelLogs=true\nProtectKernelModules=true\nProtectKernelTunables=true\nRestrictSUIDSGID=true\nRestrictRealtime=true\nLockPersonality=true\nMemoryDenyWriteExecute=true\nRestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX\nSystemCallArchitectures=native\nTimeoutStopSec=15\n\n[Install]\nWantedBy=multi-user.target\n"},"services":{"adapter":{"active":"active","pid":"976008","restarts":"0","sub":"running"},"portal":{"active":"active","pid":"976035","restarts":"0","sub":"running"}},"source_excerpts":{"adapter_dispatch":"2414: \"ok\": True,\n2415: \"result\": value,\n2416: }\n2417: \n2418: \n2419: def dispatch(request: dict[str, Any]) -> dict[str, Any]:\n2420: operation = request.get(\"op\")\n2421: if operation == \"health\":\n2422: return response_ok(\n2423: {\n2424: \"status\": \"ok\",\n2425: \"version\": 1,\n2426: }\n2427: )\n2428: if operation == \"bind_enrollment\":\n2429: return response_ok(\n2430: bind_enrollment(\n2431: str(request.get(\"token\", \"\")),\n2432: str(request.get(\"client_ip\", \"\")),\n2433: )\n2434: )\n2435: if operation == \"enroll\":\n2436: return response_ok(\n2437: enroll_device(\n2438: str(request.get(\"token\", \"\")),\n2439: str(request.get(\"client_ip\", \"\")),\n2440: )\n2441: )\n2442: if operation == \"import\":\n2443: return response_ok(\n2444: import_capsule(\n2445: device_token=[REDACTED]\n2446: request.get(\"device_token\", \"\")\n2447: ),\n2448: client_ip=str(\n2449: request.get(\"client_ip\", \"\")\n2450: ),\n2451: upload_path=str(\n2452: request.get(\"upload_path\", \"\")\n2453: ),\n2454: )\n2455: )\n2456: if operation == \"consume_bootstrap\":\n2457: return response_ok(\n2458: consume_bootstrap(\n2459: short_id=str(request.get(\"short_id\", \"\")),\n2460: token=[REDACTED]\"token\", \"\")),\n2461: client_ip=str(\n2462: request.get(\"client_ip\", \"\")\n2463: ),\n2464: )\n2465: )\n2466: if operation == \"state\":\n2467: return response_ok(\n2468: public_state(\n2469: short_id=str(request.get(\"short_id\", \"\")),\n2470: session_token=[REDACTED]\n2471: request.get(\"session_token\", \"\")\n2472: ),\n2473: client_ip=str(\n2474: request.get(\"client_ip\", \"\")\n2475: ),\n2476: )\n2477: )\n2478: if operation == \"execute\":\n2479: return response_ok(\n2480: start_execution(\n2481: short_id=str(request.get(\"short_id\", \"\")),\n2482: session_token=[REDACTED]\n2483: request.get(\"session_token\", \"\")\n2484: ),\n2485: client_ip=str(\n2486: request.get(\"client_ip\", \"\")\n2487: ),\n2488: )\n2489: )\n2490: if operation == \"evidence_url\":\n2491: return response_ok(\n2492: evidence_url(\n2493: short_id=str(request.get(\"short_id\", \"\")),\n2494: session_token=[REDACTED]\n2495: request.get(\"session_token\", \"\")\n2496: ),\n2497: client_ip=str(\n2498: request.get(\"client_ip\", \"\")\n2499: ),\n2500: )\n2501: )\n2502: raise AdapterError(\"unsupported operation\")\n2503: \n2504: \n2505: class Handler(socketserver.StreamRequestHandler):\n2506: def handle(self) -> None:\n2507: credentials = self.request.getsockopt(","adapter_public_state":"1306: ):\n1307: return session\n1308: raise AdapterError(\"session authorization failed\")\n1309: \n1310: \n1311: def public_state(\n1312: *,\n1313: short_id: str,\n1314: session_token: [REDACTED]\n1315: client_ip: str,\n1316: ) -> dict[str, Any]:\n1317: with GlobalLock():\n1318: state = load_json(state_path(short_id))\n1319: authorize_session(\n1320: state,\n1321: session_token=[REDACTED]\n1322: client_ip=client_ip,\n1323: )\n1324: return sanitize_state(state)\n1325: \n1326: \n1327: def sanitize_state(state: dict[str, Any]) -> dict[str, Any]:\n1328: execution = state.get(\"execution\")\n1329: public_execution = None","adapter_sanitize_state":"1322: client_ip=client_ip,\n1323: )\n1324: return sanitize_state(state)\n1325: \n1326: \n1327: def sanitize_state(state: dict[str, Any]) -> dict[str, Any]:\n1328: execution = state.get(\"execution\")\n1329: public_execution = None\n1330: if isinstance(execution, dict):\n1331: public_execution = {\n1332: key: execution.get(key)\n1333: for key in (\n1334: \"started_at_utc\",\n1335: \"finished_at_utc\",\n1336: \"runner_rc\",\n1337: \"runner_document_rc\",\n1338: \"runner_status\",\n1339: \"runner_wrapper_matches_document\",\n1340: \"runner_json_sha256\",\n1341: \"runner_text_sha256\",\n1342: )\n1343: if key in execution\n1344: }\n1345: \n1346: evidence = state.get(\"evidence\")\n1347: public_evidence = None\n1348: if isinstance(evidence, dict):\n1349: public_evidence = {\n1350: key: evidence.get(key)\n1351: for key in (\n1352: \"object_key\",\n1353: \"sha256\",\n1354: \"bytes\",\n1355: \"gateway_verified\",\n1356: )\n1357: if key in evidence\n1358: }\n1359: \n1360: return {\n1361: \"schema_version\": 1,\n1362: \"short_id\": state[\"short_id\"],\n1363: \"capsule_sha256\": state[\"capsule_sha256\"],\n1364: \"capsule_bytes\": state[\"capsule_bytes\"],\n1365: \"manifest\": state[\"manifest\"],\n1366: \"imported_at_utc\": state[\"imported_at_utc\"],\n1367: \"command_object\": state[\"command_object\"],\n1368: \"status\": state[\"status\"],\n1369: \"execution\": public_execution,\n1370: \"evidence\": public_evidence,\n1371: \"error\": state.get(\"error\"),\n1372: }\n1373: \n1374: \n1375: def start_execution(\n1376: *,\n1377: short_id: str,","adapter_worker_finalization":"2058: state[\"manifest\"][\"command_id\"],\n2059: started_epoch,\n2060: )\n2061: runner_document = load_json(runner_json)\n2062: \n2063: bundle, evidence_sha, evidence_bytes = (\n2064: create_evidence_bundle(\n2065: state=state,\n2066: runner_json=runner_json,\n2067: runner_txt=runner_txt,\n2068: )\n2069: )\n2070: prefix = (\n2071: \"portal-tests\"\n2072: if state[\"manifest\"][\"component\"]\n2073: == \"command-portal-smoke\"\n2074: else \"portal\"\n2075: )\n2076: object_key = (\n2077: f\"{prefix}/\"\n2078: f\"{state['manifest']['command_id']}/\"\n2079: f\"{state['capsule_sha256']}/\"\n2080: \"evidence.tgz\"\n2081: )\n2082: upload_evidence(\n2083: bundle,\n2084: object_key,\n2085: evidence_sha,\n2086: )\n2087: \n2088: with GlobalLock():\n2089: current = load_json(path)\n2090: if not worker_state_owned(\n2091: current,\n2092: os.getpid(),\n2093: ):\n2094: raise AdapterError(\n2095: \"worker finalization ownership lost\"\n2096: )\n2097: document_status = str(\n2098: runner_document.get(\"status\", \"FAIL\")\n2099: )\n2100: try:\n2101: document_rc = int(runner_document[\"rc\"])\n2102: except (KeyError, TypeError, ValueError) as exc:\n2103: raise AdapterError(\"runner artifact rc invalid\") from exc\n2104: wrapper_matches = result.returncode == document_rc\n2105: current[\"status\"] = (\n2106: document_status if wrapper_matches else \"FAIL\"\n2107: )\n2108: current[\"execution\"].update(\n2109: {\n2110: \"finished_at_utc\": iso(),\n2111: \"runner_rc\": result.returncode,\n2112: \"runner_document_rc\": document_rc,\n2113: \"runner_status\": document_status,\n2114: \"runner_wrapper_matches_document\": wrapper_matches,\n2115: \"runner_json\": str(runner_json),\n2116: \"runner_text\": str(runner_txt),\n2117: \"runner_json_sha256\": sha256_file(\n2118: runner_json\n2119: ),\n2120: \"runner_text_sha256\": sha256_file(\n2121: runner_txt\n2122: ),\n2123: }\n2124: )\n2125: current[\"evidence\"] = {\n2126: \"object_key\": object_key,\n2127: \"sha256\": evidence_sha,\n2128: \"bytes\": evidence_bytes,\n2129: \"local_bundle\": str(bundle),\n2130: \"gateway_verified\": True,\n2131: }\n2132: current[\"error\"] = (\n2133: None\n2134: if wrapper_matches\n2135: else {\n2136: \"type\": \"RunnerWrapperMismatch\",\n2137: \"message\": (\n2138: \"existing runner returned a different rc \"\n2139: \"after writing its public artifact; \"\n2140: \"runtime publication or wrapper completion failed\"\n2141: ),\n2142: }\n2143: )\n2144: atomic_json(path, current)\n2145: return 0\n2146: \n2147: except Exception as exc:\n2148: try:\n2149: with wrapper_log.open(\n2150: \"a\",\n2151: encoding=\"utf-8\",\n2152: errors=\"replace\",\n2153: ) as stream:\n2154: stream.write(\n2155: f\"WORKER_ERROR={type(exc).__name__}:{exc}\\\\n\"\n2156: )\n2157: os.chmod(wrapper_log, 0o640)\n2158: except Exception:\n2159: pass\n2160: \n2161: with GlobalLock():\n2162: try:\n2163: current = load_json(path)\n2164: if worker_state_owned(\n2165: current,\n2166: os.getpid(),\n2167: ):\n2168: current[\"status\"] = \"FAIL\"\n2169: if isinstance(\n2170: current.get(\"execution\"),\n2171: dict,\n2172: ):\n2173: current[\"execution\"][\n2174: \"finished_at_utc\"\n2175: ] = iso()\n2176: current[\"error\"] = {\n2177: \"type\": type(exc).__name__,\n2178: \"message\": str(exc)[:4000],\n2179: }\n2180: atomic_json(path, current)\n2181: except Exception:\n2182: pass\n2183: return 1","portal_Handler_do_GET":"550: keep_blank_values=True,\n551: )\n552: \n553: def do_GET(self) -> None:\n554: try:\n555: path, query = self.path_parts()\n556: \n557: if path == \"/healthz\":\n558: parse_client(self, allow_local_health=True)\n559: result = adapter_call({\"op\": \"health\"})\n560: body = (\n561: json.dumps(\n562: {\n563: \"status\": \"ok\",\n564: \"version\": 1,\n565: \"adapter\": result[\"status\"],\n566: },\n567: separators=(\",\", \":\"),\n568: sort_keys=True,\n569: )\n570: + \"\\n\"\n571: ).encode()\n572: self.reply(\n573: 200,\n574: \"application/json; charset=utf-8\",\n575: body,\n576: )\n577: return\n578: \n579: client_ip = parse_client(self)\n580: \n581: if path == \"/bridge/bridge.cmd\":\n582: self.reply(\n583: 200,\n584: \"application/octet-stream\",\n585: BRIDGE_CMD.encode(\"utf-8\"),\n586: headers={\n587: \"Content-Disposition\": (\n588: 'attachment; filename=\"bridge.cmd\"'\n589: )\n590: },\n591: )\n592: return\n593: \n594: if path.startswith(\"/enroll/\"):\n595: relative = path[len(\"/enroll/\") :]\n596: if relative.endswith(\"/bridge-setup.cmd\"):\n597: token = [REDACTED]\n598: : -len(\"/bridge-setup.cmd\")\n599: ]\n600: adapter_call(\n601: {\n602: \"op\": \"bind_enrollment\",\n603: \"token\": token,\n604: \"client_ip\": client_ip,\n605: }\n606: )\n607: self.reply(\n608: 200,\n609: \"application/octet-stream\",\n610: setup_script(token),\n611: headers={\n612: \"Content-Disposition\": (\n613: 'attachment; '\n614: 'filename=\"HomelabBridgeSetup.cmd\"'\n615: )\n616: },\n617: )\n618: return\n619: \n620: token = [REDACTED]\n621: info = adapter_call(\n622: {\n623: \"op\": \"bind_enrollment\",\n624: \"token\": token,\n625: \"client_ip\": client_ip,\n626: }\n627: )\n628: body = f\"\"\"\n629: \n630: \n631: Homelab Bridge\n632: \n633: \n634:

Установка Homelab Bridge

\n635:

Профиль: {html.escape(str(info['label']))}

\n636:

Ссылка одноразовая и уже привязана к этому NetBird-адресу.

\n637:

Скачать установщик

\n638:

После запуска установщика файлы .hcmd будут открываться двойным кликом.

\n639: \"\"\".encode()\n640: self.reply(\n641: 200,\n642: \"text/html; charset=utf-8\",\n643: body,\n644: )\n645: return\n646: \n647: match = re.fullmatch(\n648: r\"/r/([A-Z2-7]{10,20})\",\n649: path,\n650: )\n651: if match:\n652: short_id = safe_short(match.group(1))\n653: token_values = query.get(\"t\", [])\n654: if token_values:\n655: token = [REDACTED]\n656: session = adapter_call(\n657: {\n658: \"op\": \"consume_bootstrap\",\n659: \"short_id\": short_id,\n660: \"token\": token,\n661: \"client_ip\": client_ip,\n662: }\n663: )\n664: cookie = (\n665: f\"__Host-HLSESSION={session['session_token']}; \"\n666: f\"Max-Age={8 * 3600}; Path=/; \"\n667: \"Secure; HttpOnly; SameSite=Strict\"\n668: )\n669: self.redirect(\n670: f\"/r/{short_id}\",\n671: headers={\"Set-Cookie\": cookie},\n672: )\n673: return\n674: \n675: session_token = [REDACTED]\n676: state = adapter_call(\n677: {\n678: \"op\": \"state\",\n679: \"short_id\": short_id,\n680: \"session_token\": session_token,\n681: \"client_ip\": client_ip,\n682: }\n683: )\n684: self.reply(\n685: 200,\n686: \"text/html; charset=utf-8\",\n687: render_card(state, session_token),\n688: )\n689: return\n690: \n691: evidence_match = re.fullmatch(\n692: r\"/r/([A-Z2-7]{10,20})/evidence\",\n693: path,\n694: )\n695: if evidence_match:\n696: short_id = safe_short(\n697: evidence_match.group(1)\n698: )\n699: signed = adapter_call(\n700: {\n701: \"op\": \"evidence_url\",\n702: \"short_id\": short_id,\n703: \"session_token\": cookie_session(self),\n704: \"client_ip\": client_ip,\n705: }\n706: )\n707: self.redirect(str(signed[\"url\"]))\n708: return\n709: \n710: state_match = re.fullmatch(\n711: r\"/api/state/([A-Z2-7]{10,20})\",\n712: path,\n713: )\n714: if state_match:\n715: short_id = safe_short(\n716: state_match.group(1)\n717: )\n718: state = adapter_call(\n719: {\n720: \"op\": \"state\",\n721: \"short_id\": short_id,\n722: \"session_token\": cookie_session(self),\n723: \"client_ip\": client_ip,\n724: }\n725: )\n726: self.reply(\n727: 200,\n728: \"application/json; charset=utf-8\",\n729: (\n730: json.dumps(\n731: state,\n732: ensure_ascii=False,\n733: separators=(\",\", \":\"),\n734: sort_keys=True,\n735: )\n736: + \"\\n\"\n737: ).encode(),\n738: )\n739: return\n740: \n741: self.error_reply(404, \"not found\")\n742: \n743: except PortalError as exc:\n744: self.error_reply(403, str(exc))\n745: except Exception as exc:\n746: self.error_reply(\n747: 500,\n748: f\"{type(exc).__name__}:{str(exc)[:1000]}\",\n749: )\n750: \n751: def do_POST(self) -> None:\n752: try:","portal_Handler_error_reply":"518: headers=values,\n519: )\n520: \n521: def error_reply(self, status: int, message: str) -> None:\n522: body = (\n523: f\"PORTAL_ERROR={html.escape(message)}\\n\"\n524: ).encode()\n525: self.reply(\n526: status,\n527: \"text/plain; charset=utf-8\",\n528: body,\n529: )\n530: \n531: def content_length(self) -> int:\n532: raw = self.headers.get(\"Content-Length\")","portal_Handler_path_parts":"543: raise PortalError(\"short request body\")\n544: return body\n545: \n546: def path_parts(self) -> tuple[str, dict[str, list[str]]]:\n547: parsed = urllib.parse.urlsplit(self.path)\n548: return parsed.path, urllib.parse.parse_qs(\n549: parsed.query,\n550: keep_blank_values=True,\n551: )\n552: \n553: def do_GET(self) -> None:\n554: try:","portal_Handler_reply":"468: f\"{self.client_address[0]} - {message}\\n\"\n469: )\n470: \n471: def reply(\n472: self,\n473: status: int,\n474: content_type: str,\n475: body: bytes,\n476: *,\n477: headers: dict[str, str] | None = None,\n478: ) -> None:\n479: self.send_response(status)\n480: self.send_header(\"Content-Type\", content_type)\n481: self.send_header(\"Content-Length\", str(len(body)))\n482: self.send_header(\n483: \"Cache-Control\",\n484: \"no-store, no-cache, must-revalidate, max-age=0\",\n485: )\n486: self.send_header(\"Pragma\", \"no-cache\")\n487: self.send_header(\"Expires\", \"0\")\n488: self.send_header(\n489: \"X-Robots-Tag\",\n490: \"noindex, nofollow,noarchive\",\n491: )\n492: self.send_header(\"X-Content-Type-Options\", \"nosniff\")\n493: self.send_header(\"Referrer-Policy\", \"no-referrer\")\n494: self.send_header(\n495: \"Content-Security-Policy\",\n496: \"default-src 'none'; style-src 'unsafe-inline'; \"\n497: \"form-action 'self'; frame-ancestors 'none'; \"\n498: \"base-uri 'none'\",\n499: )\n500: for key, value in (headers or {}).items():\n501: self.send_header(key, value)\n502: self.end_headers()\n503: if body:\n504: self.wfile.write(body)\n505: \n506: def redirect(\n507: self,"},"source_sha256":{"adapter":"8d0af84dcfcf6ddbfd1c4e6cbb059cef66b4cfc6c75bfd66da649acc229c7f5e","portal":"8056faf4ef6348f0b06319ad3c49fbfeeadf25b6370f857ea19569bd27652ee2","runner":"b248a4c32c9cc64e5747e7dce6c7fc0a23f5124a77c71ce72e27a81aceae9d2d"},"states":{"current_running":[{"command_id":"RESULT-API-PATCH-AUDIT-20260727T075149Z","state_file":"275ZUEB6ZH.json"}],"invalid":[],"other_running":[]},"status":"OK"} + +OUTPUT_END +CHAT_OUTPUT_END diff --git a/runtime/latest.json b/runtime/latest.json index 9e7613c..ad4575a 100644 --- a/runtime/latest.json +++ b/runtime/latest.json @@ -1,19 +1,19 @@ { "schema_version": 1, "channel": "homelab-runtime", - "command_id": "RESULT-API-LOCAL-PREFLIGHT-FIX1-20260727T074036Z", + "command_id": "RESULT-API-PATCH-AUDIT-20260727T075149Z", "status": "OK", "rc": 0, "host": "pve01", "mode": "verify", "component": "result-integration", - "started_at_utc": "2026-07-27T07:42:58Z", - "finished_at_utc": "2026-07-27T07:42:58Z", + "started_at_utc": "2026-07-27T07:52:45Z", + "finished_at_utc": "2026-07-27T07:52:45Z", "reference_register_checked": true, "reference_sha256": "5763f2f8edc75fcf6f3951f8c95896d1cc112ceccb6cf5b64a383f540a4deb8e", "error_register_checked": true, "error_register_sha256": "24934a2c7fa5c26f6e828583c4aefac7f143b40c0171736ed7571e617d55ab83", - "command_sha256": "3ff4d1ea2cda459219b872cac9a804822a5a17f093dc9ac0fdc8925c372b09c4", + "command_sha256": "b564f92f4324ba19d5c6752e0c53111024fd37092acacc2281d2ab9ac7f39359", "duplicate_failed_command_blocked": false, "block_reason": null, "execution_started": true, @@ -30,9 +30,9 @@ "secrets_included": false, "private_addresses_included": false, "raw_evidence_retained_locally": true, - "raw_evidence_sha256": "d46658451693a1872d7a4401e2d1fbec106612ffc95b7a9f8ce712fb494cce79", - "sanitized_output_sha256": "d46658451693a1872d7a4401e2d1fbec106612ffc95b7a9f8ce712fb494cce79", + "raw_evidence_sha256": "1dabd515e3ddd0d132d8f6cd354770e78207200337dfda97f41dcacb8b3b576d", + "sanitized_output_sha256": "1dabd515e3ddd0d132d8f6cd354770e78207200337dfda97f41dcacb8b3b576d", "output_truncated_in_json": false, "full_sanitized_output_url": "https://git.gram1.ru/.well-known/homelab-runtime/latest.txt", - "output": "{\"adapter_capabilities\":{\"has_dispatch\":true,\"has_evidence_url\":true,\"has_public_state\":true,\"has_result_public_key_operation\":false,\"has_result_public_operation\":false,\"has_sanitize_state\":true,\"has_terminal_evidence_fields\":true},\"backup_changed\":false,\"candidate_parent\":{\"exists\":true,\"gid\":990,\"is_symlink\":false,\"mode\":\"0o750\",\"path\":\"/var/lib/homelab-command-adapter\",\"result_signing_exists\":false,\"uid\":0},\"changes_made\":false,\"checks\":{\"adapter_dispatch_available\":true,\"adapter_expected_sha\":true,\"adapter_healthy\":true,\"adapter_root_safe\":true,\"adapter_sanitizer_available\":true,\"adapter_terminal_fields_available\":true,\"current_command_running_once\":true,\"new_routes_not_already_present\":true,\"no_invalid_states\":true,\"no_other_running_states\":true,\"openssl_ed25519_pass\":true,\"portal_adapter_boundary_available\":true,\"portal_expected_sha\":true,\"portal_healthy\":true,\"runner_expected_sha\":true,\"terminal_evidence_exists\":true},\"cluster_changed\":false,\"command_id\":\"RESULT-API-LOCAL-PREFLIGHT-FIX1-20260727T074036Z\",\"crypto_test\":{\"available\":true,\"pass\":true,\"private_mode\":\"0o600\",\"public_key_sha256\":\"2d591f1b49f9e2771be5c02739e6f519c91d9ceebb00cc2a96b03d6f66f779c5\",\"signature_bytes\":64,\"verify_rc\":0},\"gitea_changed\":false,\"inventory\":{\"adapter\":{\"exists\":true,\"gid\":0,\"is_dir\":false,\"is_file\":true,\"is_symlink\":false,\"mode\":\"0o750\",\"path\":\"/usr/local/sbin/homelab-command-adapter\",\"sha256\":\"8d0af84dcfcf6ddbfd1c4e6cbb059cef66b4cfc6c75bfd66da649acc229c7f5e\",\"uid\":0},\"portal\":{\"exists\":true,\"gid\":990,\"is_dir\":false,\"is_file\":true,\"is_symlink\":false,\"mode\":\"0o750\",\"path\":\"/usr/local/sbin/homelab-command-portal\",\"sha256\":\"8056faf4ef6348f0b06319ad3c49fbfeeadf25b6370f857ea19569bd27652ee2\",\"uid\":0},\"runner\":{\"exists\":true,\"gid\":0,\"is_dir\":false,\"is_file\":true,\"is_symlink\":false,\"mode\":\"0o750\",\"path\":\"/usr/local/sbin/homelab-chat-run\",\"sha256\":\"b248a4c32c9cc64e5747e7dce6c7fc0a23f5124a77c71ce72e27a81aceae9d2d\",\"uid\":0}},\"next_plan_scope\":{\"create\":[\"/var/lib/homelab-command-adapter/result-signing/private.pem\",\"/var/lib/homelab-command-adapter/result-signing/public.pem\"],\"modify\":[\"/usr/local/sbin/homelab-command-adapter\",\"/usr/local/sbin/homelab-command-portal\"],\"preserve\":[\"/usr/local/sbin/homelab-chat-run\",\"existing state files\",\"existing evidence objects\",\"Gitea runtime history\",\"backup\",\"cluster\"]},\"portal_capabilities\":{\"classes\":{\"Handler\":{\"line\":451,\"methods\":[\"content_length\",\"do_GET\",\"do_POST\",\"error_reply\",\"log_message\",\"path_parts\",\"read_body\",\"redirect\",\"reply\"],\"strings\":[\"\",\"\\n\",\" - \",\"(/enroll/)[A-Za-z0-9_-]{32,160}\",\"([?&]t=)[^&\\\\s]+\",\",\",\".hcmd\",\".upload-\",\"/api/enroll\",\"/api/import\",\"/api/state/([A-Z2-7]{10,20})\",\"/bridge-setup.cmd\",\"/bridge-setup.cmd\\\">Скачать установщик

\\n

После запуска установщика файлы .hcmd будут открываться двойным кликом.

\\n\",\"/bridge/bridge.cmd\",\"/enroll/\",\"/healthz\",\"/r/\",\"/r/([A-Z2-7]{10,20})\",\"/r/([A-Z2-7]{10,20})/evidence\",\"/r/([A-Z2-7]{10,20})/execute\",\"0\",\":\",\"; Max-Age=\",\"; Path=/; Secure; HttpOnly; SameSite=Strict\",\"\\n\\n\\nHomelab Bridge\\n\\n\\n

Установка Homelab Bridge

\\n

Профиль: \",\"

\\n

Ссылка одноразовая и уже привязана к этому NetBird-адресу.

\\n

Скачать установщик

\\n

После запуска установщика файлы .hcmd будут открываться двойным кликом.

\\n\",\"/bridge/bridge.cmd\",\"/enroll/\",\"/healthz\",\"/r/\",\"/r/([A-Z2-7]{10,20})\",\"/r/([A-Z2-7]{10,20})/evidence\",\"/r/([A-Z2-7]{10,20})/execute\",\"evidence_url\",\"health\"]},\"private_addresses_included\":false,\"proposed_design\":{\"adapter_operations\":[\"result_public\",\"result_public_key\"],\"document_fields\":[\"schema_version\",\"command_id\",\"capsule_sha256\",\"status\",\"started_at_utc\",\"finished_at_utc\",\"runner_rc\",\"runner_document_rc\",\"runner_wrapper_matches_document\",\"evidence_sha256\",\"gateway_verified\",\"summary\",\"document_sha256\",\"signature_algorithm\",\"signature\",\"key_id\"],\"excluded_fields\":[\"tokens\",\"presigned_urls\",\"credentials\",\"cookies\",\"private_keys\",\"raw_logs\",\"private_addresses\",\"local_paths\"],\"lookup_inputs\":[\"command_id\",\"capsule_sha256\"],\"no_listing\":true,\"public_key_url\":\"https://run.vpn.gram1.ru/.well-known/homelab-result-key.pem\",\"result_url_template\":\"https://run.vpn.gram1.ru/.well-known/homelab-result//.json\",\"signature\":{\"algorithm\":\"Ed25519\",\"canonical_json\":\"UTF-8, sort_keys=true, separators=(',',':')\",\"encoding\":\"base64\"},\"terminal_only\":true},\"rollback_started\":false,\"sanitized\":true,\"schema\":1,\"secrets_included\":false,\"services\":{\"adapter\":{\"active\":\"active\",\"pid\":\"976008\",\"restarts\":\"0\",\"sub\":\"running\"},\"portal\":{\"active\":\"active\",\"pid\":\"976035\",\"restarts\":\"0\",\"sub\":\"running\"}},\"state_summary\":{\"current_running\":[{\"command_id\":\"RESULT-API-LOCAL-PREFLIGHT-FIX1-20260727T074036Z\",\"state_file\":\"AZBBQXVDMF.json\"}],\"evidence_ready_count\":28,\"invalid\":[],\"other_running\":[],\"terminal_count\":30},\"status\":\"OK\"}\n" + "output": "{\"adapter_functions\":{\"atomic_json\":{\"arguments\":[{\"has_default\":false,\"kind\":\"positional_or_keyword\",\"name\":\"path\"},{\"has_default\":false,\"kind\":\"positional_or_keyword\",\"name\":\"value\"}],\"is_async\":false,\"line_end\":238,\"line_start\":202,\"name\":\"atomic_json\"},\"dispatch\":{\"arguments\":[{\"has_default\":false,\"kind\":\"positional_or_keyword\",\"name\":\"request\"}],\"is_async\":false,\"line_end\":2502,\"line_start\":2419,\"name\":\"dispatch\"},\"load_json\":{\"arguments\":[{\"has_default\":false,\"kind\":\"positional_or_keyword\",\"name\":\"path\"}],\"is_async\":false,\"line_end\":248,\"line_start\":241,\"name\":\"load_json\"},\"public_state\":{\"arguments\":[{\"has_default\":false,\"kind\":\"keyword_only\",\"name\":\"short_id\"},{\"has_default\":false,\"kind\":\"keyword_only\",\"name\":\"session_token\"},{\"has_default\":false,\"kind\":\"keyword_only\",\"name\":\"client_ip\"}],\"is_async\":false,\"line_end\":1324,\"line_start\":1311,\"name\":\"public_state\"},\"run_command\":{\"arguments\":[{\"has_default\":false,\"kind\":\"positional_or_keyword\",\"name\":\"args\"},{\"has_default\":true,\"kind\":\"keyword_only\",\"name\":\"timeout\"},{\"has_default\":true,\"kind\":\"keyword_only\",\"name\":\"env\"}],\"is_async\":false,\"line_end\":673,\"line_start\":658,\"name\":\"run_command\"},\"sanitize_state\":{\"arguments\":[{\"has_default\":false,\"kind\":\"positional_or_keyword\",\"name\":\"state\"}],\"is_async\":false,\"line_end\":1372,\"line_start\":1327,\"name\":\"sanitize_state\"},\"sha256_file\":{\"arguments\":[{\"has_default\":false,\"kind\":\"positional_or_keyword\",\"name\":\"path\"}],\"is_async\":false,\"line_end\":199,\"line_start\":194,\"name\":\"sha256_file\"},\"state_path\":{\"arguments\":[{\"has_default\":false,\"kind\":\"positional_or_keyword\",\"name\":\"short_id\"}],\"is_async\":false,\"line_end\":638,\"line_start\":635,\"name\":\"state_path\"},\"worker\":{\"arguments\":[{\"has_default\":false,\"kind\":\"positional_or_keyword\",\"name\":\"short_id\"}],\"is_async\":false,\"line_end\":2183,\"line_start\":1994,\"name\":\"worker\"}},\"backup_changed\":false,\"changes_made\":false,\"checks\":{\"adapter_functions_complete\":true,\"adapter_healthy\":true,\"adapter_sha\":true,\"current_running_once\":true,\"no_invalid_states\":true,\"no_other_running\":true,\"portal_functions_complete\":true,\"portal_handler_do_get_present\":true,\"portal_handler_present\":true,\"portal_handler_reply_present\":true,\"portal_healthy\":true,\"portal_sha\":true,\"runner_sha\":true,\"source_excerpts_sanitized\":true},\"cluster_changed\":false,\"command_id\":\"RESULT-API-PATCH-AUDIT-20260727T075149Z\",\"gitea_changed\":false,\"plan_contract\":{\"adapter_operations\":[\"result_public\",\"result_public_key\"],\"create\":[\"/var/lib/homelab-command-adapter/result-signing/private.pem\",\"/var/lib/homelab-command-adapter/result-signing/public.pem\"],\"modify\":[\"/usr/local/sbin/homelab-command-adapter\",\"/usr/local/sbin/homelab-command-portal\"],\"preserve\":[\"/usr/local/sbin/homelab-chat-run\",\"existing state files\",\"existing evidence objects\",\"backup\",\"cluster\",\"Gitea history\"],\"public_routes\":[\"/.well-known/homelab-result//.json\",\"/.well-known/homelab-result-key.pem\"]},\"portal_classes\":{\"Handler\":{\"line_end\":892,\"line_start\":451,\"methods\":[{\"arguments\":[{\"has_default\":false,\"kind\":\"positional_or_keyword\",\"name\":\"self\"},{\"has_default\":false,\"kind\":\"positional_or_keyword\",\"name\":\"fmt\"},{\"has_default\":false,\"kind\":\"var_positional\",\"name\":\"args\"}],\"is_async\":false,\"line_end\":469,\"line_start\":455,\"name\":\"log_message\"},{\"arguments\":[{\"has_default\":false,\"kind\":\"positional_or_keyword\",\"name\":\"self\"},{\"has_default\":false,\"kind\":\"positional_or_keyword\",\"name\":\"status\"},{\"has_default\":false,\"kind\":\"positional_or_keyword\",\"name\":\"content_type\"},{\"has_default\":false,\"kind\":\"positional_or_keyword\",\"name\":\"body\"},{\"has_default\":true,\"kind\":\"keyword_only\",\"name\":\"headers\"}],\"is_async\":false,\"line_end\":504,\"line_start\":471,\"name\":\"reply\"},{\"arguments\":[{\"has_default\":false,\"kind\":\"positional_or_keyword\",\"name\":\"self\"},{\"has_default\":false,\"kind\":\"positional_or_keyword\",\"name\":\"location\"},{\"has_default\":true,\"kind\":\"keyword_only\",\"name\":\"headers\"}],\"is_async\":false,\"line_end\":519,\"line_start\":506,\"name\":\"redirect\"},{\"arguments\":[{\"has_default\":false,\"kind\":\"positional_or_keyword\",\"name\":\"self\"},{\"has_default\":false,\"kind\":\"positional_or_keyword\",\"name\":\"status\"},{\"has_default\":false,\"kind\":\"positional_or_keyword\",\"name\":\"message\"}],\"is_async\":false,\"line_end\":529,\"line_start\":521,\"name\":\"error_reply\"},{\"arguments\":[{\"has_default\":false,\"kind\":\"positional_or_keyword\",\"name\":\"self\"}],\"is_async\":false,\"line_end\":535,\"line_start\":531,\"name\":\"content_length\"},{\"arguments\":[{\"has_default\":false,\"kind\":\"positional_or_keyword\",\"name\":\"self\"},{\"has_default\":false,\"kind\":\"positional_or_keyword\",\"name\":\"limit\"}],\"is_async\":false,\"line_end\":544,\"line_start\":537,\"name\":\"read_body\"},{\"arguments\":[{\"has_default\":false,\"kind\":\"positional_or_keyword\",\"name\":\"self\"}],\"is_async\":false,\"line_end\":551,\"line_start\":546,\"name\":\"path_parts\"},{\"arguments\":[{\"has_default\":false,\"kind\":\"positional_or_keyword\",\"name\":\"self\"}],\"is_async\":false,\"line_end\":749,\"line_start\":553,\"name\":\"do_GET\"},{\"arguments\":[{\"has_default\":false,\"kind\":\"positional_or_keyword\",\"name\":\"self\"}],\"is_async\":false,\"line_end\":892,\"line_start\":751,\"name\":\"do_POST\"}],\"name\":\"Handler\"}},\"portal_functions\":{\"adapter_call\":{\"arguments\":[{\"has_default\":false,\"kind\":\"positional_or_keyword\",\"name\":\"request\"}],\"is_async\":false,\"line_end\":91,\"line_start\":44,\"name\":\"adapter_call\"}},\"private_addresses_included\":false,\"rollback_started\":false,\"sanitized\":true,\"schema\":1,\"secrets_included\":false,\"service_files\":{\"adapter\":\"[Unit]\\nDescription=Homelab command adapter\\nAfter=network-online.target\\nWants=network-online.target\\n\\n[Service]\\nType=simple\\nExecStart=/usr/bin/python3 -B /usr/local/sbin/homelab-command-adapter --serve\\nUser=root\\nGroup=homelab-command-portal\\nUMask=0027\\nRuntimeDirectory=homelab-command-adapter\\nRuntimeDirectoryMode=0750\\nRestart=on-failure\\nRestartSec=2\\nNoNewPrivileges=false\\nPrivateTmp=true\\nProtectClock=true\\nProtectControlGroups=true\\nProtectKernelLogs=true\\nProtectKernelModules=true\\nProtectKernelTunables=true\\nRestrictRealtime=true\\nLockPersonality=true\\nMemoryDenyWriteExecute=true\\nSystemCallArchitectures=native\\nTimeoutStopSec=15\\n\\n[Install]\\nWantedBy=multi-user.target\\n\",\"portal\":\"[Unit]\\nDescription=Homelab command portal\\nAfter=network-online.target homelab-command-adapter.service\\nRequires=homelab-command-adapter.service\\nWants=network-online.target\\nStartLimitIntervalSec=30\\nStartLimitBurst=3\\n\\n[Service]\\nType=simple\\nExecStartPre=/usr/bin/python3 -B /usr/local/sbin/homelab-command-portal --self-test\\nExecStart=/usr/bin/python3 -B /usr/local/sbin/homelab-command-portal\\nUser=homelab-command-portal\\nGroup=homelab-command-portal\\nUMask=0077\\nRestart=on-failure\\nRestartSec=2\\nNoNewPrivileges=true\\nPrivateTmp=true\\nPrivateDevices=true\\nProtectSystem=strict\\nProtectHome=true\\nReadWritePaths=/var/lib/homelab-command-portal\\nProtectClock=true\\nProtectControlGroups=true\\nProtectKernelLogs=true\\nProtectKernelModules=true\\nProtectKernelTunables=true\\nRestrictSUIDSGID=true\\nRestrictRealtime=true\\nLockPersonality=true\\nMemoryDenyWriteExecute=true\\nRestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX\\nSystemCallArchitectures=native\\nTimeoutStopSec=15\\n\\n[Install]\\nWantedBy=multi-user.target\\n\"},\"services\":{\"adapter\":{\"active\":\"active\",\"pid\":\"976008\",\"restarts\":\"0\",\"sub\":\"running\"},\"portal\":{\"active\":\"active\",\"pid\":\"976035\",\"restarts\":\"0\",\"sub\":\"running\"}},\"source_excerpts\":{\"adapter_dispatch\":\"2414: \\\"ok\\\": True,\\n2415: \\\"result\\\": value,\\n2416: }\\n2417: \\n2418: \\n2419: def dispatch(request: dict[str, Any]) -> dict[str, Any]:\\n2420: operation = request.get(\\\"op\\\")\\n2421: if operation == \\\"health\\\":\\n2422: return response_ok(\\n2423: {\\n2424: \\\"status\\\": \\\"ok\\\",\\n2425: \\\"version\\\": 1,\\n2426: }\\n2427: )\\n2428: if operation == \\\"bind_enrollment\\\":\\n2429: return response_ok(\\n2430: bind_enrollment(\\n2431: str(request.get(\\\"token\\\", \\\"\\\")),\\n2432: str(request.get(\\\"client_ip\\\", \\\"\\\")),\\n2433: )\\n2434: )\\n2435: if operation == \\\"enroll\\\":\\n2436: return response_ok(\\n2437: enroll_device(\\n2438: str(request.get(\\\"token\\\", \\\"\\\")),\\n2439: str(request.get(\\\"client_ip\\\", \\\"\\\")),\\n2440: )\\n2441: )\\n2442: if operation == \\\"import\\\":\\n2443: return response_ok(\\n2444: import_capsule(\\n2445: device_token=[REDACTED]\\n2446: request.get(\\\"device_token\\\", \\\"\\\")\\n2447: ),\\n2448: client_ip=str(\\n2449: request.get(\\\"client_ip\\\", \\\"\\\")\\n2450: ),\\n2451: upload_path=str(\\n2452: request.get(\\\"upload_path\\\", \\\"\\\")\\n2453: ),\\n2454: )\\n2455: )\\n2456: if operation == \\\"consume_bootstrap\\\":\\n2457: return response_ok(\\n2458: consume_bootstrap(\\n2459: short_id=str(request.get(\\\"short_id\\\", \\\"\\\")),\\n2460: token=[REDACTED]\\\"token\\\", \\\"\\\")),\\n2461: client_ip=str(\\n2462: request.get(\\\"client_ip\\\", \\\"\\\")\\n2463: ),\\n2464: )\\n2465: )\\n2466: if operation == \\\"state\\\":\\n2467: return response_ok(\\n2468: public_state(\\n2469: short_id=str(request.get(\\\"short_id\\\", \\\"\\\")),\\n2470: session_token=[REDACTED]\\n2471: request.get(\\\"session_token\\\", \\\"\\\")\\n2472: ),\\n2473: client_ip=str(\\n2474: request.get(\\\"client_ip\\\", \\\"\\\")\\n2475: ),\\n2476: )\\n2477: )\\n2478: if operation == \\\"execute\\\":\\n2479: return response_ok(\\n2480: start_execution(\\n2481: short_id=str(request.get(\\\"short_id\\\", \\\"\\\")),\\n2482: session_token=[REDACTED]\\n2483: request.get(\\\"session_token\\\", \\\"\\\")\\n2484: ),\\n2485: client_ip=str(\\n2486: request.get(\\\"client_ip\\\", \\\"\\\")\\n2487: ),\\n2488: )\\n2489: )\\n2490: if operation == \\\"evidence_url\\\":\\n2491: return response_ok(\\n2492: evidence_url(\\n2493: short_id=str(request.get(\\\"short_id\\\", \\\"\\\")),\\n2494: session_token=[REDACTED]\\n2495: request.get(\\\"session_token\\\", \\\"\\\")\\n2496: ),\\n2497: client_ip=str(\\n2498: request.get(\\\"client_ip\\\", \\\"\\\")\\n2499: ),\\n2500: )\\n2501: )\\n2502: raise AdapterError(\\\"unsupported operation\\\")\\n2503: \\n2504: \\n2505: class Handler(socketserver.StreamRequestHandler):\\n2506: def handle(self) -> None:\\n2507: credentials = self.request.getsockopt(\",\"adapter_public_state\":\"1306: ):\\n1307: return session\\n1308: raise AdapterError(\\\"session authorization failed\\\")\\n1309: \\n1310: \\n1311: def public_state(\\n1312: *,\\n1313: short_id: str,\\n1314: session_token: [REDACTED]\\n1315: client_ip: str,\\n1316: ) -> dict[str, Any]:\\n1317: with GlobalLock():\\n1318: state = load_json(state_path(short_id))\\n1319: authorize_session(\\n1320: state,\\n1321: session_token=[REDACTED]\\n1322: client_ip=client_ip,\\n1323: )\\n1324: return sanitize_state(state)\\n1325: \\n1326: \\n1327: def sanitize_state(state: dict[str, Any]) -> dict[str, Any]:\\n1328: execution = state.get(\\\"execution\\\")\\n1329: public_execution = None\",\"adapter_sanitize_state\":\"1322: client_ip=client_ip,\\n1323: )\\n1324: return sanitize_state(state)\\n1325: \\n1326: \\n1327: def sanitize_state(state: dict[str, Any]) -> dict[str, Any]:\\n1328: execution = state.get(\\\"execution\\\")\\n1329: public_execution = None\\n1330: if isinstance(execution, dict):\\n1331: public_execution = {\\n1332: key: execution.get(key)\\n1333: for key in (\\n1334: \\\"started_at_utc\\\",\\n1335: \\\"finished_at_utc\\\",\\n1336: \\\"runner_rc\\\",\\n1337: \\\"runner_document_rc\\\",\\n1338: \\\"runner_status\\\",\\n1339: \\\"runner_wrapper_matches_document\\\",\\n1340: \\\"runner_json_sha256\\\",\\n1341: \\\"runner_text_sha256\\\",\\n1342: )\\n1343: if key in execution\\n1344: }\\n1345: \\n1346: evidence = state.get(\\\"evidence\\\")\\n1347: public_evidence = None\\n1348: if isinstance(evidence, dict):\\n1349: public_evidence = {\\n1350: key: evidence.get(key)\\n1351: for key in (\\n1352: \\\"object_key\\\",\\n1353: \\\"sha256\\\",\\n1354: \\\"bytes\\\",\\n1355: \\\"gateway_verified\\\",\\n1356: )\\n1357: if key in evidence\\n1358: }\\n1359: \\n1360: return {\\n1361: \\\"schema_version\\\": 1,\\n1362: \\\"short_id\\\": state[\\\"short_id\\\"],\\n1363: \\\"capsule_sha256\\\": state[\\\"capsule_sha256\\\"],\\n1364: \\\"capsule_bytes\\\": state[\\\"capsule_bytes\\\"],\\n1365: \\\"manifest\\\": state[\\\"manifest\\\"],\\n1366: \\\"imported_at_utc\\\": state[\\\"imported_at_utc\\\"],\\n1367: \\\"command_object\\\": state[\\\"command_object\\\"],\\n1368: \\\"status\\\": state[\\\"status\\\"],\\n1369: \\\"execution\\\": public_execution,\\n1370: \\\"evidence\\\": public_evidence,\\n1371: \\\"error\\\": state.get(\\\"error\\\"),\\n1372: }\\n1373: \\n1374: \\n1375: def start_execution(\\n1376: *,\\n1377: short_id: str,\",\"adapter_worker_finalization\":\"2058: state[\\\"manifest\\\"][\\\"command_id\\\"],\\n2059: started_epoch,\\n2060: )\\n2061: runner_document = load_json(runner_json)\\n2062: \\n2063: bundle, evidence_sha, evidence_bytes = (\\n2064: create_evidence_bundle(\\n2065: state=state,\\n2066: runner_json=runner_json,\\n2067: runner_txt=runner_txt,\\n2068: )\\n2069: )\\n2070: prefix = (\\n2071: \\\"portal-tests\\\"\\n2072: if state[\\\"manifest\\\"][\\\"component\\\"]\\n2073: == \\\"command-portal-smoke\\\"\\n2074: else \\\"portal\\\"\\n2075: )\\n2076: object_key = (\\n2077: f\\\"{prefix}/\\\"\\n2078: f\\\"{state['manifest']['command_id']}/\\\"\\n2079: f\\\"{state['capsule_sha256']}/\\\"\\n2080: \\\"evidence.tgz\\\"\\n2081: )\\n2082: upload_evidence(\\n2083: bundle,\\n2084: object_key,\\n2085: evidence_sha,\\n2086: )\\n2087: \\n2088: with GlobalLock():\\n2089: current = load_json(path)\\n2090: if not worker_state_owned(\\n2091: current,\\n2092: os.getpid(),\\n2093: ):\\n2094: raise AdapterError(\\n2095: \\\"worker finalization ownership lost\\\"\\n2096: )\\n2097: document_status = str(\\n2098: runner_document.get(\\\"status\\\", \\\"FAIL\\\")\\n2099: )\\n2100: try:\\n2101: document_rc = int(runner_document[\\\"rc\\\"])\\n2102: except (KeyError, TypeError, ValueError) as exc:\\n2103: raise AdapterError(\\\"runner artifact rc invalid\\\") from exc\\n2104: wrapper_matches = result.returncode == document_rc\\n2105: current[\\\"status\\\"] = (\\n2106: document_status if wrapper_matches else \\\"FAIL\\\"\\n2107: )\\n2108: current[\\\"execution\\\"].update(\\n2109: {\\n2110: \\\"finished_at_utc\\\": iso(),\\n2111: \\\"runner_rc\\\": result.returncode,\\n2112: \\\"runner_document_rc\\\": document_rc,\\n2113: \\\"runner_status\\\": document_status,\\n2114: \\\"runner_wrapper_matches_document\\\": wrapper_matches,\\n2115: \\\"runner_json\\\": str(runner_json),\\n2116: \\\"runner_text\\\": str(runner_txt),\\n2117: \\\"runner_json_sha256\\\": sha256_file(\\n2118: runner_json\\n2119: ),\\n2120: \\\"runner_text_sha256\\\": sha256_file(\\n2121: runner_txt\\n2122: ),\\n2123: }\\n2124: )\\n2125: current[\\\"evidence\\\"] = {\\n2126: \\\"object_key\\\": object_key,\\n2127: \\\"sha256\\\": evidence_sha,\\n2128: \\\"bytes\\\": evidence_bytes,\\n2129: \\\"local_bundle\\\": str(bundle),\\n2130: \\\"gateway_verified\\\": True,\\n2131: }\\n2132: current[\\\"error\\\"] = (\\n2133: None\\n2134: if wrapper_matches\\n2135: else {\\n2136: \\\"type\\\": \\\"RunnerWrapperMismatch\\\",\\n2137: \\\"message\\\": (\\n2138: \\\"existing runner returned a different rc \\\"\\n2139: \\\"after writing its public artifact; \\\"\\n2140: \\\"runtime publication or wrapper completion failed\\\"\\n2141: ),\\n2142: }\\n2143: )\\n2144: atomic_json(path, current)\\n2145: return 0\\n2146: \\n2147: except Exception as exc:\\n2148: try:\\n2149: with wrapper_log.open(\\n2150: \\\"a\\\",\\n2151: encoding=\\\"utf-8\\\",\\n2152: errors=\\\"replace\\\",\\n2153: ) as stream:\\n2154: stream.write(\\n2155: f\\\"WORKER_ERROR={type(exc).__name__}:{exc}\\\\\\\\n\\\"\\n2156: )\\n2157: os.chmod(wrapper_log, 0o640)\\n2158: except Exception:\\n2159: pass\\n2160: \\n2161: with GlobalLock():\\n2162: try:\\n2163: current = load_json(path)\\n2164: if worker_state_owned(\\n2165: current,\\n2166: os.getpid(),\\n2167: ):\\n2168: current[\\\"status\\\"] = \\\"FAIL\\\"\\n2169: if isinstance(\\n2170: current.get(\\\"execution\\\"),\\n2171: dict,\\n2172: ):\\n2173: current[\\\"execution\\\"][\\n2174: \\\"finished_at_utc\\\"\\n2175: ] = iso()\\n2176: current[\\\"error\\\"] = {\\n2177: \\\"type\\\": type(exc).__name__,\\n2178: \\\"message\\\": str(exc)[:4000],\\n2179: }\\n2180: atomic_json(path, current)\\n2181: except Exception:\\n2182: pass\\n2183: return 1\",\"portal_Handler_do_GET\":\"550: keep_blank_values=True,\\n551: )\\n552: \\n553: def do_GET(self) -> None:\\n554: try:\\n555: path, query = self.path_parts()\\n556: \\n557: if path == \\\"/healthz\\\":\\n558: parse_client(self, allow_local_health=True)\\n559: result = adapter_call({\\\"op\\\": \\\"health\\\"})\\n560: body = (\\n561: json.dumps(\\n562: {\\n563: \\\"status\\\": \\\"ok\\\",\\n564: \\\"version\\\": 1,\\n565: \\\"adapter\\\": result[\\\"status\\\"],\\n566: },\\n567: separators=(\\\",\\\", \\\":\\\"),\\n568: sort_keys=True,\\n569: )\\n570: + \\\"\\\\n\\\"\\n571: ).encode()\\n572: self.reply(\\n573: 200,\\n574: \\\"application/json; charset=utf-8\\\",\\n575: body,\\n576: )\\n577: return\\n578: \\n579: client_ip = parse_client(self)\\n580: \\n581: if path == \\\"/bridge/bridge.cmd\\\":\\n582: self.reply(\\n583: 200,\\n584: \\\"application/octet-stream\\\",\\n585: BRIDGE_CMD.encode(\\\"utf-8\\\"),\\n586: headers={\\n587: \\\"Content-Disposition\\\": (\\n588: 'attachment; filename=\\\"bridge.cmd\\\"'\\n589: )\\n590: },\\n591: )\\n592: return\\n593: \\n594: if path.startswith(\\\"/enroll/\\\"):\\n595: relative = path[len(\\\"/enroll/\\\") :]\\n596: if relative.endswith(\\\"/bridge-setup.cmd\\\"):\\n597: token = [REDACTED]\\n598: : -len(\\\"/bridge-setup.cmd\\\")\\n599: ]\\n600: adapter_call(\\n601: {\\n602: \\\"op\\\": \\\"bind_enrollment\\\",\\n603: \\\"token\\\": token,\\n604: \\\"client_ip\\\": client_ip,\\n605: }\\n606: )\\n607: self.reply(\\n608: 200,\\n609: \\\"application/octet-stream\\\",\\n610: setup_script(token),\\n611: headers={\\n612: \\\"Content-Disposition\\\": (\\n613: 'attachment; '\\n614: 'filename=\\\"HomelabBridgeSetup.cmd\\\"'\\n615: )\\n616: },\\n617: )\\n618: return\\n619: \\n620: token = [REDACTED]\\n621: info = adapter_call(\\n622: {\\n623: \\\"op\\\": \\\"bind_enrollment\\\",\\n624: \\\"token\\\": token,\\n625: \\\"client_ip\\\": client_ip,\\n626: }\\n627: )\\n628: body = f\\\"\\\"\\\"\\n629: \\n630: \\n631: Homelab Bridge\\n632: \\n633: \\n634:

Установка Homelab Bridge

\\n635:

Профиль: {html.escape(str(info['label']))}

\\n636:

Ссылка одноразовая и уже привязана к этому NetBird-адресу.

\\n637:

Скачать установщик

\\n638:

После запуска установщика файлы .hcmd будут открываться двойным кликом.

\\n639: \\\"\\\"\\\".encode()\\n640: self.reply(\\n641: 200,\\n642: \\\"text/html; charset=utf-8\\\",\\n643: body,\\n644: )\\n645: return\\n646: \\n647: match = re.fullmatch(\\n648: r\\\"/r/([A-Z2-7]{10,20})\\\",\\n649: path,\\n650: )\\n651: if match:\\n652: short_id = safe_short(match.group(1))\\n653: token_values = query.get(\\\"t\\\", [])\\n654: if token_values:\\n655: token = [REDACTED]\\n656: session = adapter_call(\\n657: {\\n658: \\\"op\\\": \\\"consume_bootstrap\\\",\\n659: \\\"short_id\\\": short_id,\\n660: \\\"token\\\": token,\\n661: \\\"client_ip\\\": client_ip,\\n662: }\\n663: )\\n664: cookie = (\\n665: f\\\"__Host-HLSESSION={session['session_token']}; \\\"\\n666: f\\\"Max-Age={8 * 3600}; Path=/; \\\"\\n667: \\\"Secure; HttpOnly; SameSite=Strict\\\"\\n668: )\\n669: self.redirect(\\n670: f\\\"/r/{short_id}\\\",\\n671: headers={\\\"Set-Cookie\\\": cookie},\\n672: )\\n673: return\\n674: \\n675: session_token = [REDACTED]\\n676: state = adapter_call(\\n677: {\\n678: \\\"op\\\": \\\"state\\\",\\n679: \\\"short_id\\\": short_id,\\n680: \\\"session_token\\\": session_token,\\n681: \\\"client_ip\\\": client_ip,\\n682: }\\n683: )\\n684: self.reply(\\n685: 200,\\n686: \\\"text/html; charset=utf-8\\\",\\n687: render_card(state, session_token),\\n688: )\\n689: return\\n690: \\n691: evidence_match = re.fullmatch(\\n692: r\\\"/r/([A-Z2-7]{10,20})/evidence\\\",\\n693: path,\\n694: )\\n695: if evidence_match:\\n696: short_id = safe_short(\\n697: evidence_match.group(1)\\n698: )\\n699: signed = adapter_call(\\n700: {\\n701: \\\"op\\\": \\\"evidence_url\\\",\\n702: \\\"short_id\\\": short_id,\\n703: \\\"session_token\\\": cookie_session(self),\\n704: \\\"client_ip\\\": client_ip,\\n705: }\\n706: )\\n707: self.redirect(str(signed[\\\"url\\\"]))\\n708: return\\n709: \\n710: state_match = re.fullmatch(\\n711: r\\\"/api/state/([A-Z2-7]{10,20})\\\",\\n712: path,\\n713: )\\n714: if state_match:\\n715: short_id = safe_short(\\n716: state_match.group(1)\\n717: )\\n718: state = adapter_call(\\n719: {\\n720: \\\"op\\\": \\\"state\\\",\\n721: \\\"short_id\\\": short_id,\\n722: \\\"session_token\\\": cookie_session(self),\\n723: \\\"client_ip\\\": client_ip,\\n724: }\\n725: )\\n726: self.reply(\\n727: 200,\\n728: \\\"application/json; charset=utf-8\\\",\\n729: (\\n730: json.dumps(\\n731: state,\\n732: ensure_ascii=False,\\n733: separators=(\\\",\\\", \\\":\\\"),\\n734: sort_keys=True,\\n735: )\\n736: + \\\"\\\\n\\\"\\n737: ).encode(),\\n738: )\\n739: return\\n740: \\n741: self.error_reply(404, \\\"not found\\\")\\n742: \\n743: except PortalError as exc:\\n744: self.error_reply(403, str(exc))\\n745: except Exception as exc:\\n746: self.error_reply(\\n747: 500,\\n748: f\\\"{type(exc).__name__}:{str(exc)[:1000]}\\\",\\n749: )\\n750: \\n751: def do_POST(self) -> None:\\n752: try:\",\"portal_Handler_error_reply\":\"518: headers=values,\\n519: )\\n520: \\n521: def error_reply(self, status: int, message: str) -> None:\\n522: body = (\\n523: f\\\"PORTAL_ERROR={html.escape(message)}\\\\n\\\"\\n524: ).encode()\\n525: self.reply(\\n526: status,\\n527: \\\"text/plain; charset=utf-8\\\",\\n528: body,\\n529: )\\n530: \\n531: def content_length(self) -> int:\\n532: raw = self.headers.get(\\\"Content-Length\\\")\",\"portal_Handler_path_parts\":\"543: raise PortalError(\\\"short request body\\\")\\n544: return body\\n545: \\n546: def path_parts(self) -> tuple[str, dict[str, list[str]]]:\\n547: parsed = urllib.parse.urlsplit(self.path)\\n548: return parsed.path, urllib.parse.parse_qs(\\n549: parsed.query,\\n550: keep_blank_values=True,\\n551: )\\n552: \\n553: def do_GET(self) -> None:\\n554: try:\",\"portal_Handler_reply\":\"468: f\\\"{self.client_address[0]} - {message}\\\\n\\\"\\n469: )\\n470: \\n471: def reply(\\n472: self,\\n473: status: int,\\n474: content_type: str,\\n475: body: bytes,\\n476: *,\\n477: headers: dict[str, str] | None = None,\\n478: ) -> None:\\n479: self.send_response(status)\\n480: self.send_header(\\\"Content-Type\\\", content_type)\\n481: self.send_header(\\\"Content-Length\\\", str(len(body)))\\n482: self.send_header(\\n483: \\\"Cache-Control\\\",\\n484: \\\"no-store, no-cache, must-revalidate, max-age=0\\\",\\n485: )\\n486: self.send_header(\\\"Pragma\\\", \\\"no-cache\\\")\\n487: self.send_header(\\\"Expires\\\", \\\"0\\\")\\n488: self.send_header(\\n489: \\\"X-Robots-Tag\\\",\\n490: \\\"noindex, nofollow,noarchive\\\",\\n491: )\\n492: self.send_header(\\\"X-Content-Type-Options\\\", \\\"nosniff\\\")\\n493: self.send_header(\\\"Referrer-Policy\\\", \\\"no-referrer\\\")\\n494: self.send_header(\\n495: \\\"Content-Security-Policy\\\",\\n496: \\\"default-src 'none'; style-src 'unsafe-inline'; \\\"\\n497: \\\"form-action 'self'; frame-ancestors 'none'; \\\"\\n498: \\\"base-uri 'none'\\\",\\n499: )\\n500: for key, value in (headers or {}).items():\\n501: self.send_header(key, value)\\n502: self.end_headers()\\n503: if body:\\n504: self.wfile.write(body)\\n505: \\n506: def redirect(\\n507: self,\"},\"source_sha256\":{\"adapter\":\"8d0af84dcfcf6ddbfd1c4e6cbb059cef66b4cfc6c75bfd66da649acc229c7f5e\",\"portal\":\"8056faf4ef6348f0b06319ad3c49fbfeeadf25b6370f857ea19569bd27652ee2\",\"runner\":\"b248a4c32c9cc64e5747e7dce6c7fc0a23f5124a77c71ce72e27a81aceae9d2d\"},\"states\":{\"current_running\":[{\"command_id\":\"RESULT-API-PATCH-AUDIT-20260727T075149Z\",\"state_file\":\"275ZUEB6ZH.json\"}],\"invalid\":[],\"other_running\":[]},\"status\":\"OK\"}\n" } diff --git a/runtime/latest.txt b/runtime/latest.txt index 6d49456..8d2bbef 100644 --- a/runtime/latest.txt +++ b/runtime/latest.txt @@ -1,5 +1,5 @@ CHAT_OUTPUT_BEGIN -COMMAND_ID=RESULT-API-LOCAL-PREFLIGHT-FIX1-20260727T074036Z +COMMAND_ID=RESULT-API-PATCH-AUDIT-20260727T075149Z STATUS=OK RC=0 HOST=pve01 @@ -9,7 +9,7 @@ REFERENCE_REGISTER_CHECK=OK REFERENCE_SHA256=5763f2f8edc75fcf6f3951f8c95896d1cc112ceccb6cf5b64a383f540a4deb8e ERROR_REGISTER_CHECK=OK ERROR_REGISTER_SHA256=24934a2c7fa5c26f6e828583c4aefac7f143b40c0171736ed7571e617d55ab83 -COMMAND_SHA256=3ff4d1ea2cda459219b872cac9a804822a5a17f093dc9ac0fdc8925c372b09c4 +COMMAND_SHA256=b564f92f4324ba19d5c6752e0c53111024fd37092acacc2281d2ab9ac7f39359 DUPLICATE_FAILED_COMMAND_BLOCKED=false EXECUTION_STARTED=true CHANGE_DECLARED=false @@ -24,10 +24,10 @@ MUTATION_OUTCOME=NO_MUTATION SANITIZED=yes SECRETS_INCLUDED=no PRIVATE_ADDRESSES_INCLUDED=no -RAW_EVIDENCE_SHA256=d46658451693a1872d7a4401e2d1fbec106612ffc95b7a9f8ce712fb494cce79 -SANITIZED_OUTPUT_SHA256=d46658451693a1872d7a4401e2d1fbec106612ffc95b7a9f8ce712fb494cce79 +RAW_EVIDENCE_SHA256=1dabd515e3ddd0d132d8f6cd354770e78207200337dfda97f41dcacb8b3b576d +SANITIZED_OUTPUT_SHA256=1dabd515e3ddd0d132d8f6cd354770e78207200337dfda97f41dcacb8b3b576d OUTPUT_BEGIN -{"adapter_capabilities":{"has_dispatch":true,"has_evidence_url":true,"has_public_state":true,"has_result_public_key_operation":false,"has_result_public_operation":false,"has_sanitize_state":true,"has_terminal_evidence_fields":true},"backup_changed":false,"candidate_parent":{"exists":true,"gid":990,"is_symlink":false,"mode":"0o750","path":"/var/lib/homelab-command-adapter","result_signing_exists":false,"uid":0},"changes_made":false,"checks":{"adapter_dispatch_available":true,"adapter_expected_sha":true,"adapter_healthy":true,"adapter_root_safe":true,"adapter_sanitizer_available":true,"adapter_terminal_fields_available":true,"current_command_running_once":true,"new_routes_not_already_present":true,"no_invalid_states":true,"no_other_running_states":true,"openssl_ed25519_pass":true,"portal_adapter_boundary_available":true,"portal_expected_sha":true,"portal_healthy":true,"runner_expected_sha":true,"terminal_evidence_exists":true},"cluster_changed":false,"command_id":"RESULT-API-LOCAL-PREFLIGHT-FIX1-20260727T074036Z","crypto_test":{"available":true,"pass":true,"private_mode":"0o600","public_key_sha256":"2d591f1b49f9e2771be5c02739e6f519c91d9ceebb00cc2a96b03d6f66f779c5","signature_bytes":64,"verify_rc":0},"gitea_changed":false,"inventory":{"adapter":{"exists":true,"gid":0,"is_dir":false,"is_file":true,"is_symlink":false,"mode":"0o750","path":"/usr/local/sbin/homelab-command-adapter","sha256":"8d0af84dcfcf6ddbfd1c4e6cbb059cef66b4cfc6c75bfd66da649acc229c7f5e","uid":0},"portal":{"exists":true,"gid":990,"is_dir":false,"is_file":true,"is_symlink":false,"mode":"0o750","path":"/usr/local/sbin/homelab-command-portal","sha256":"8056faf4ef6348f0b06319ad3c49fbfeeadf25b6370f857ea19569bd27652ee2","uid":0},"runner":{"exists":true,"gid":0,"is_dir":false,"is_file":true,"is_symlink":false,"mode":"0o750","path":"/usr/local/sbin/homelab-chat-run","sha256":"b248a4c32c9cc64e5747e7dce6c7fc0a23f5124a77c71ce72e27a81aceae9d2d","uid":0}},"next_plan_scope":{"create":["/var/lib/homelab-command-adapter/result-signing/private.pem","/var/lib/homelab-command-adapter/result-signing/public.pem"],"modify":["/usr/local/sbin/homelab-command-adapter","/usr/local/sbin/homelab-command-portal"],"preserve":["/usr/local/sbin/homelab-chat-run","existing state files","existing evidence objects","Gitea runtime history","backup","cluster"]},"portal_capabilities":{"classes":{"Handler":{"line":451,"methods":["content_length","do_GET","do_POST","error_reply","log_message","path_parts","read_body","redirect","reply"],"strings":["","\n"," - ","(/enroll/)[A-Za-z0-9_-]{32,160}","([?&]t=)[^&\\s]+",",",".hcmd",".upload-","/api/enroll","/api/import","/api/state/([A-Z2-7]{10,20})","/bridge-setup.cmd","/bridge-setup.cmd\">Скачать установщик

\n

После запуска установщика файлы .hcmd будут открываться двойным кликом.

\n","/bridge/bridge.cmd","/enroll/","/healthz","/r/","/r/([A-Z2-7]{10,20})","/r/([A-Z2-7]{10,20})/evidence","/r/([A-Z2-7]{10,20})/execute","0",":","; Max-Age=","; Path=/; Secure; HttpOnly; SameSite=Strict","\n\n\nHomelab Bridge\n\n\n

Установка Homelab Bridge

\n

Профиль: ","

\n

Ссылка одноразовая и уже привязана к этому NetBird-адресу.

\n

Скачать установщик

\n

После запуска установщика файлы .hcmd будут открываться двойным кликом.

\n","/bridge/bridge.cmd","/enroll/","/healthz","/r/","/r/([A-Z2-7]{10,20})","/r/([A-Z2-7]{10,20})/evidence","/r/([A-Z2-7]{10,20})/execute","evidence_url","health"]},"private_addresses_included":false,"proposed_design":{"adapter_operations":["result_public","result_public_key"],"document_fields":["schema_version","command_id","capsule_sha256","status","started_at_utc","finished_at_utc","runner_rc","runner_document_rc","runner_wrapper_matches_document","evidence_sha256","gateway_verified","summary","document_sha256","signature_algorithm","signature","key_id"],"excluded_fields":["tokens","presigned_urls","credentials","cookies","private_keys","raw_logs","private_addresses","local_paths"],"lookup_inputs":["command_id","capsule_sha256"],"no_listing":true,"public_key_url":"https://run.vpn.gram1.ru/.well-known/homelab-result-key.pem","result_url_template":"https://run.vpn.gram1.ru/.well-known/homelab-result//.json","signature":{"algorithm":"Ed25519","canonical_json":"UTF-8, sort_keys=true, separators=(',',':')","encoding":"base64"},"terminal_only":true},"rollback_started":false,"sanitized":true,"schema":1,"secrets_included":false,"services":{"adapter":{"active":"active","pid":"976008","restarts":"0","sub":"running"},"portal":{"active":"active","pid":"976035","restarts":"0","sub":"running"}},"state_summary":{"current_running":[{"command_id":"RESULT-API-LOCAL-PREFLIGHT-FIX1-20260727T074036Z","state_file":"AZBBQXVDMF.json"}],"evidence_ready_count":28,"invalid":[],"other_running":[],"terminal_count":30},"status":"OK"} +{"adapter_functions":{"atomic_json":{"arguments":[{"has_default":false,"kind":"positional_or_keyword","name":"path"},{"has_default":false,"kind":"positional_or_keyword","name":"value"}],"is_async":false,"line_end":238,"line_start":202,"name":"atomic_json"},"dispatch":{"arguments":[{"has_default":false,"kind":"positional_or_keyword","name":"request"}],"is_async":false,"line_end":2502,"line_start":2419,"name":"dispatch"},"load_json":{"arguments":[{"has_default":false,"kind":"positional_or_keyword","name":"path"}],"is_async":false,"line_end":248,"line_start":241,"name":"load_json"},"public_state":{"arguments":[{"has_default":false,"kind":"keyword_only","name":"short_id"},{"has_default":false,"kind":"keyword_only","name":"session_token"},{"has_default":false,"kind":"keyword_only","name":"client_ip"}],"is_async":false,"line_end":1324,"line_start":1311,"name":"public_state"},"run_command":{"arguments":[{"has_default":false,"kind":"positional_or_keyword","name":"args"},{"has_default":true,"kind":"keyword_only","name":"timeout"},{"has_default":true,"kind":"keyword_only","name":"env"}],"is_async":false,"line_end":673,"line_start":658,"name":"run_command"},"sanitize_state":{"arguments":[{"has_default":false,"kind":"positional_or_keyword","name":"state"}],"is_async":false,"line_end":1372,"line_start":1327,"name":"sanitize_state"},"sha256_file":{"arguments":[{"has_default":false,"kind":"positional_or_keyword","name":"path"}],"is_async":false,"line_end":199,"line_start":194,"name":"sha256_file"},"state_path":{"arguments":[{"has_default":false,"kind":"positional_or_keyword","name":"short_id"}],"is_async":false,"line_end":638,"line_start":635,"name":"state_path"},"worker":{"arguments":[{"has_default":false,"kind":"positional_or_keyword","name":"short_id"}],"is_async":false,"line_end":2183,"line_start":1994,"name":"worker"}},"backup_changed":false,"changes_made":false,"checks":{"adapter_functions_complete":true,"adapter_healthy":true,"adapter_sha":true,"current_running_once":true,"no_invalid_states":true,"no_other_running":true,"portal_functions_complete":true,"portal_handler_do_get_present":true,"portal_handler_present":true,"portal_handler_reply_present":true,"portal_healthy":true,"portal_sha":true,"runner_sha":true,"source_excerpts_sanitized":true},"cluster_changed":false,"command_id":"RESULT-API-PATCH-AUDIT-20260727T075149Z","gitea_changed":false,"plan_contract":{"adapter_operations":["result_public","result_public_key"],"create":["/var/lib/homelab-command-adapter/result-signing/private.pem","/var/lib/homelab-command-adapter/result-signing/public.pem"],"modify":["/usr/local/sbin/homelab-command-adapter","/usr/local/sbin/homelab-command-portal"],"preserve":["/usr/local/sbin/homelab-chat-run","existing state files","existing evidence objects","backup","cluster","Gitea history"],"public_routes":["/.well-known/homelab-result//.json","/.well-known/homelab-result-key.pem"]},"portal_classes":{"Handler":{"line_end":892,"line_start":451,"methods":[{"arguments":[{"has_default":false,"kind":"positional_or_keyword","name":"self"},{"has_default":false,"kind":"positional_or_keyword","name":"fmt"},{"has_default":false,"kind":"var_positional","name":"args"}],"is_async":false,"line_end":469,"line_start":455,"name":"log_message"},{"arguments":[{"has_default":false,"kind":"positional_or_keyword","name":"self"},{"has_default":false,"kind":"positional_or_keyword","name":"status"},{"has_default":false,"kind":"positional_or_keyword","name":"content_type"},{"has_default":false,"kind":"positional_or_keyword","name":"body"},{"has_default":true,"kind":"keyword_only","name":"headers"}],"is_async":false,"line_end":504,"line_start":471,"name":"reply"},{"arguments":[{"has_default":false,"kind":"positional_or_keyword","name":"self"},{"has_default":false,"kind":"positional_or_keyword","name":"location"},{"has_default":true,"kind":"keyword_only","name":"headers"}],"is_async":false,"line_end":519,"line_start":506,"name":"redirect"},{"arguments":[{"has_default":false,"kind":"positional_or_keyword","name":"self"},{"has_default":false,"kind":"positional_or_keyword","name":"status"},{"has_default":false,"kind":"positional_or_keyword","name":"message"}],"is_async":false,"line_end":529,"line_start":521,"name":"error_reply"},{"arguments":[{"has_default":false,"kind":"positional_or_keyword","name":"self"}],"is_async":false,"line_end":535,"line_start":531,"name":"content_length"},{"arguments":[{"has_default":false,"kind":"positional_or_keyword","name":"self"},{"has_default":false,"kind":"positional_or_keyword","name":"limit"}],"is_async":false,"line_end":544,"line_start":537,"name":"read_body"},{"arguments":[{"has_default":false,"kind":"positional_or_keyword","name":"self"}],"is_async":false,"line_end":551,"line_start":546,"name":"path_parts"},{"arguments":[{"has_default":false,"kind":"positional_or_keyword","name":"self"}],"is_async":false,"line_end":749,"line_start":553,"name":"do_GET"},{"arguments":[{"has_default":false,"kind":"positional_or_keyword","name":"self"}],"is_async":false,"line_end":892,"line_start":751,"name":"do_POST"}],"name":"Handler"}},"portal_functions":{"adapter_call":{"arguments":[{"has_default":false,"kind":"positional_or_keyword","name":"request"}],"is_async":false,"line_end":91,"line_start":44,"name":"adapter_call"}},"private_addresses_included":false,"rollback_started":false,"sanitized":true,"schema":1,"secrets_included":false,"service_files":{"adapter":"[Unit]\nDescription=Homelab command adapter\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=simple\nExecStart=/usr/bin/python3 -B /usr/local/sbin/homelab-command-adapter --serve\nUser=root\nGroup=homelab-command-portal\nUMask=0027\nRuntimeDirectory=homelab-command-adapter\nRuntimeDirectoryMode=0750\nRestart=on-failure\nRestartSec=2\nNoNewPrivileges=false\nPrivateTmp=true\nProtectClock=true\nProtectControlGroups=true\nProtectKernelLogs=true\nProtectKernelModules=true\nProtectKernelTunables=true\nRestrictRealtime=true\nLockPersonality=true\nMemoryDenyWriteExecute=true\nSystemCallArchitectures=native\nTimeoutStopSec=15\n\n[Install]\nWantedBy=multi-user.target\n","portal":"[Unit]\nDescription=Homelab command portal\nAfter=network-online.target homelab-command-adapter.service\nRequires=homelab-command-adapter.service\nWants=network-online.target\nStartLimitIntervalSec=30\nStartLimitBurst=3\n\n[Service]\nType=simple\nExecStartPre=/usr/bin/python3 -B /usr/local/sbin/homelab-command-portal --self-test\nExecStart=/usr/bin/python3 -B /usr/local/sbin/homelab-command-portal\nUser=homelab-command-portal\nGroup=homelab-command-portal\nUMask=0077\nRestart=on-failure\nRestartSec=2\nNoNewPrivileges=true\nPrivateTmp=true\nPrivateDevices=true\nProtectSystem=strict\nProtectHome=true\nReadWritePaths=/var/lib/homelab-command-portal\nProtectClock=true\nProtectControlGroups=true\nProtectKernelLogs=true\nProtectKernelModules=true\nProtectKernelTunables=true\nRestrictSUIDSGID=true\nRestrictRealtime=true\nLockPersonality=true\nMemoryDenyWriteExecute=true\nRestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX\nSystemCallArchitectures=native\nTimeoutStopSec=15\n\n[Install]\nWantedBy=multi-user.target\n"},"services":{"adapter":{"active":"active","pid":"976008","restarts":"0","sub":"running"},"portal":{"active":"active","pid":"976035","restarts":"0","sub":"running"}},"source_excerpts":{"adapter_dispatch":"2414: \"ok\": True,\n2415: \"result\": value,\n2416: }\n2417: \n2418: \n2419: def dispatch(request: dict[str, Any]) -> dict[str, Any]:\n2420: operation = request.get(\"op\")\n2421: if operation == \"health\":\n2422: return response_ok(\n2423: {\n2424: \"status\": \"ok\",\n2425: \"version\": 1,\n2426: }\n2427: )\n2428: if operation == \"bind_enrollment\":\n2429: return response_ok(\n2430: bind_enrollment(\n2431: str(request.get(\"token\", \"\")),\n2432: str(request.get(\"client_ip\", \"\")),\n2433: )\n2434: )\n2435: if operation == \"enroll\":\n2436: return response_ok(\n2437: enroll_device(\n2438: str(request.get(\"token\", \"\")),\n2439: str(request.get(\"client_ip\", \"\")),\n2440: )\n2441: )\n2442: if operation == \"import\":\n2443: return response_ok(\n2444: import_capsule(\n2445: device_token=[REDACTED]\n2446: request.get(\"device_token\", \"\")\n2447: ),\n2448: client_ip=str(\n2449: request.get(\"client_ip\", \"\")\n2450: ),\n2451: upload_path=str(\n2452: request.get(\"upload_path\", \"\")\n2453: ),\n2454: )\n2455: )\n2456: if operation == \"consume_bootstrap\":\n2457: return response_ok(\n2458: consume_bootstrap(\n2459: short_id=str(request.get(\"short_id\", \"\")),\n2460: token=[REDACTED]\"token\", \"\")),\n2461: client_ip=str(\n2462: request.get(\"client_ip\", \"\")\n2463: ),\n2464: )\n2465: )\n2466: if operation == \"state\":\n2467: return response_ok(\n2468: public_state(\n2469: short_id=str(request.get(\"short_id\", \"\")),\n2470: session_token=[REDACTED]\n2471: request.get(\"session_token\", \"\")\n2472: ),\n2473: client_ip=str(\n2474: request.get(\"client_ip\", \"\")\n2475: ),\n2476: )\n2477: )\n2478: if operation == \"execute\":\n2479: return response_ok(\n2480: start_execution(\n2481: short_id=str(request.get(\"short_id\", \"\")),\n2482: session_token=[REDACTED]\n2483: request.get(\"session_token\", \"\")\n2484: ),\n2485: client_ip=str(\n2486: request.get(\"client_ip\", \"\")\n2487: ),\n2488: )\n2489: )\n2490: if operation == \"evidence_url\":\n2491: return response_ok(\n2492: evidence_url(\n2493: short_id=str(request.get(\"short_id\", \"\")),\n2494: session_token=[REDACTED]\n2495: request.get(\"session_token\", \"\")\n2496: ),\n2497: client_ip=str(\n2498: request.get(\"client_ip\", \"\")\n2499: ),\n2500: )\n2501: )\n2502: raise AdapterError(\"unsupported operation\")\n2503: \n2504: \n2505: class Handler(socketserver.StreamRequestHandler):\n2506: def handle(self) -> None:\n2507: credentials = self.request.getsockopt(","adapter_public_state":"1306: ):\n1307: return session\n1308: raise AdapterError(\"session authorization failed\")\n1309: \n1310: \n1311: def public_state(\n1312: *,\n1313: short_id: str,\n1314: session_token: [REDACTED]\n1315: client_ip: str,\n1316: ) -> dict[str, Any]:\n1317: with GlobalLock():\n1318: state = load_json(state_path(short_id))\n1319: authorize_session(\n1320: state,\n1321: session_token=[REDACTED]\n1322: client_ip=client_ip,\n1323: )\n1324: return sanitize_state(state)\n1325: \n1326: \n1327: def sanitize_state(state: dict[str, Any]) -> dict[str, Any]:\n1328: execution = state.get(\"execution\")\n1329: public_execution = None","adapter_sanitize_state":"1322: client_ip=client_ip,\n1323: )\n1324: return sanitize_state(state)\n1325: \n1326: \n1327: def sanitize_state(state: dict[str, Any]) -> dict[str, Any]:\n1328: execution = state.get(\"execution\")\n1329: public_execution = None\n1330: if isinstance(execution, dict):\n1331: public_execution = {\n1332: key: execution.get(key)\n1333: for key in (\n1334: \"started_at_utc\",\n1335: \"finished_at_utc\",\n1336: \"runner_rc\",\n1337: \"runner_document_rc\",\n1338: \"runner_status\",\n1339: \"runner_wrapper_matches_document\",\n1340: \"runner_json_sha256\",\n1341: \"runner_text_sha256\",\n1342: )\n1343: if key in execution\n1344: }\n1345: \n1346: evidence = state.get(\"evidence\")\n1347: public_evidence = None\n1348: if isinstance(evidence, dict):\n1349: public_evidence = {\n1350: key: evidence.get(key)\n1351: for key in (\n1352: \"object_key\",\n1353: \"sha256\",\n1354: \"bytes\",\n1355: \"gateway_verified\",\n1356: )\n1357: if key in evidence\n1358: }\n1359: \n1360: return {\n1361: \"schema_version\": 1,\n1362: \"short_id\": state[\"short_id\"],\n1363: \"capsule_sha256\": state[\"capsule_sha256\"],\n1364: \"capsule_bytes\": state[\"capsule_bytes\"],\n1365: \"manifest\": state[\"manifest\"],\n1366: \"imported_at_utc\": state[\"imported_at_utc\"],\n1367: \"command_object\": state[\"command_object\"],\n1368: \"status\": state[\"status\"],\n1369: \"execution\": public_execution,\n1370: \"evidence\": public_evidence,\n1371: \"error\": state.get(\"error\"),\n1372: }\n1373: \n1374: \n1375: def start_execution(\n1376: *,\n1377: short_id: str,","adapter_worker_finalization":"2058: state[\"manifest\"][\"command_id\"],\n2059: started_epoch,\n2060: )\n2061: runner_document = load_json(runner_json)\n2062: \n2063: bundle, evidence_sha, evidence_bytes = (\n2064: create_evidence_bundle(\n2065: state=state,\n2066: runner_json=runner_json,\n2067: runner_txt=runner_txt,\n2068: )\n2069: )\n2070: prefix = (\n2071: \"portal-tests\"\n2072: if state[\"manifest\"][\"component\"]\n2073: == \"command-portal-smoke\"\n2074: else \"portal\"\n2075: )\n2076: object_key = (\n2077: f\"{prefix}/\"\n2078: f\"{state['manifest']['command_id']}/\"\n2079: f\"{state['capsule_sha256']}/\"\n2080: \"evidence.tgz\"\n2081: )\n2082: upload_evidence(\n2083: bundle,\n2084: object_key,\n2085: evidence_sha,\n2086: )\n2087: \n2088: with GlobalLock():\n2089: current = load_json(path)\n2090: if not worker_state_owned(\n2091: current,\n2092: os.getpid(),\n2093: ):\n2094: raise AdapterError(\n2095: \"worker finalization ownership lost\"\n2096: )\n2097: document_status = str(\n2098: runner_document.get(\"status\", \"FAIL\")\n2099: )\n2100: try:\n2101: document_rc = int(runner_document[\"rc\"])\n2102: except (KeyError, TypeError, ValueError) as exc:\n2103: raise AdapterError(\"runner artifact rc invalid\") from exc\n2104: wrapper_matches = result.returncode == document_rc\n2105: current[\"status\"] = (\n2106: document_status if wrapper_matches else \"FAIL\"\n2107: )\n2108: current[\"execution\"].update(\n2109: {\n2110: \"finished_at_utc\": iso(),\n2111: \"runner_rc\": result.returncode,\n2112: \"runner_document_rc\": document_rc,\n2113: \"runner_status\": document_status,\n2114: \"runner_wrapper_matches_document\": wrapper_matches,\n2115: \"runner_json\": str(runner_json),\n2116: \"runner_text\": str(runner_txt),\n2117: \"runner_json_sha256\": sha256_file(\n2118: runner_json\n2119: ),\n2120: \"runner_text_sha256\": sha256_file(\n2121: runner_txt\n2122: ),\n2123: }\n2124: )\n2125: current[\"evidence\"] = {\n2126: \"object_key\": object_key,\n2127: \"sha256\": evidence_sha,\n2128: \"bytes\": evidence_bytes,\n2129: \"local_bundle\": str(bundle),\n2130: \"gateway_verified\": True,\n2131: }\n2132: current[\"error\"] = (\n2133: None\n2134: if wrapper_matches\n2135: else {\n2136: \"type\": \"RunnerWrapperMismatch\",\n2137: \"message\": (\n2138: \"existing runner returned a different rc \"\n2139: \"after writing its public artifact; \"\n2140: \"runtime publication or wrapper completion failed\"\n2141: ),\n2142: }\n2143: )\n2144: atomic_json(path, current)\n2145: return 0\n2146: \n2147: except Exception as exc:\n2148: try:\n2149: with wrapper_log.open(\n2150: \"a\",\n2151: encoding=\"utf-8\",\n2152: errors=\"replace\",\n2153: ) as stream:\n2154: stream.write(\n2155: f\"WORKER_ERROR={type(exc).__name__}:{exc}\\\\n\"\n2156: )\n2157: os.chmod(wrapper_log, 0o640)\n2158: except Exception:\n2159: pass\n2160: \n2161: with GlobalLock():\n2162: try:\n2163: current = load_json(path)\n2164: if worker_state_owned(\n2165: current,\n2166: os.getpid(),\n2167: ):\n2168: current[\"status\"] = \"FAIL\"\n2169: if isinstance(\n2170: current.get(\"execution\"),\n2171: dict,\n2172: ):\n2173: current[\"execution\"][\n2174: \"finished_at_utc\"\n2175: ] = iso()\n2176: current[\"error\"] = {\n2177: \"type\": type(exc).__name__,\n2178: \"message\": str(exc)[:4000],\n2179: }\n2180: atomic_json(path, current)\n2181: except Exception:\n2182: pass\n2183: return 1","portal_Handler_do_GET":"550: keep_blank_values=True,\n551: )\n552: \n553: def do_GET(self) -> None:\n554: try:\n555: path, query = self.path_parts()\n556: \n557: if path == \"/healthz\":\n558: parse_client(self, allow_local_health=True)\n559: result = adapter_call({\"op\": \"health\"})\n560: body = (\n561: json.dumps(\n562: {\n563: \"status\": \"ok\",\n564: \"version\": 1,\n565: \"adapter\": result[\"status\"],\n566: },\n567: separators=(\",\", \":\"),\n568: sort_keys=True,\n569: )\n570: + \"\\n\"\n571: ).encode()\n572: self.reply(\n573: 200,\n574: \"application/json; charset=utf-8\",\n575: body,\n576: )\n577: return\n578: \n579: client_ip = parse_client(self)\n580: \n581: if path == \"/bridge/bridge.cmd\":\n582: self.reply(\n583: 200,\n584: \"application/octet-stream\",\n585: BRIDGE_CMD.encode(\"utf-8\"),\n586: headers={\n587: \"Content-Disposition\": (\n588: 'attachment; filename=\"bridge.cmd\"'\n589: )\n590: },\n591: )\n592: return\n593: \n594: if path.startswith(\"/enroll/\"):\n595: relative = path[len(\"/enroll/\") :]\n596: if relative.endswith(\"/bridge-setup.cmd\"):\n597: token = [REDACTED]\n598: : -len(\"/bridge-setup.cmd\")\n599: ]\n600: adapter_call(\n601: {\n602: \"op\": \"bind_enrollment\",\n603: \"token\": token,\n604: \"client_ip\": client_ip,\n605: }\n606: )\n607: self.reply(\n608: 200,\n609: \"application/octet-stream\",\n610: setup_script(token),\n611: headers={\n612: \"Content-Disposition\": (\n613: 'attachment; '\n614: 'filename=\"HomelabBridgeSetup.cmd\"'\n615: )\n616: },\n617: )\n618: return\n619: \n620: token = [REDACTED]\n621: info = adapter_call(\n622: {\n623: \"op\": \"bind_enrollment\",\n624: \"token\": token,\n625: \"client_ip\": client_ip,\n626: }\n627: )\n628: body = f\"\"\"\n629: \n630: \n631: Homelab Bridge\n632: \n633: \n634:

Установка Homelab Bridge

\n635:

Профиль: {html.escape(str(info['label']))}

\n636:

Ссылка одноразовая и уже привязана к этому NetBird-адресу.

\n637:

Скачать установщик

\n638:

После запуска установщика файлы .hcmd будут открываться двойным кликом.

\n639: \"\"\".encode()\n640: self.reply(\n641: 200,\n642: \"text/html; charset=utf-8\",\n643: body,\n644: )\n645: return\n646: \n647: match = re.fullmatch(\n648: r\"/r/([A-Z2-7]{10,20})\",\n649: path,\n650: )\n651: if match:\n652: short_id = safe_short(match.group(1))\n653: token_values = query.get(\"t\", [])\n654: if token_values:\n655: token = [REDACTED]\n656: session = adapter_call(\n657: {\n658: \"op\": \"consume_bootstrap\",\n659: \"short_id\": short_id,\n660: \"token\": token,\n661: \"client_ip\": client_ip,\n662: }\n663: )\n664: cookie = (\n665: f\"__Host-HLSESSION={session['session_token']}; \"\n666: f\"Max-Age={8 * 3600}; Path=/; \"\n667: \"Secure; HttpOnly; SameSite=Strict\"\n668: )\n669: self.redirect(\n670: f\"/r/{short_id}\",\n671: headers={\"Set-Cookie\": cookie},\n672: )\n673: return\n674: \n675: session_token = [REDACTED]\n676: state = adapter_call(\n677: {\n678: \"op\": \"state\",\n679: \"short_id\": short_id,\n680: \"session_token\": session_token,\n681: \"client_ip\": client_ip,\n682: }\n683: )\n684: self.reply(\n685: 200,\n686: \"text/html; charset=utf-8\",\n687: render_card(state, session_token),\n688: )\n689: return\n690: \n691: evidence_match = re.fullmatch(\n692: r\"/r/([A-Z2-7]{10,20})/evidence\",\n693: path,\n694: )\n695: if evidence_match:\n696: short_id = safe_short(\n697: evidence_match.group(1)\n698: )\n699: signed = adapter_call(\n700: {\n701: \"op\": \"evidence_url\",\n702: \"short_id\": short_id,\n703: \"session_token\": cookie_session(self),\n704: \"client_ip\": client_ip,\n705: }\n706: )\n707: self.redirect(str(signed[\"url\"]))\n708: return\n709: \n710: state_match = re.fullmatch(\n711: r\"/api/state/([A-Z2-7]{10,20})\",\n712: path,\n713: )\n714: if state_match:\n715: short_id = safe_short(\n716: state_match.group(1)\n717: )\n718: state = adapter_call(\n719: {\n720: \"op\": \"state\",\n721: \"short_id\": short_id,\n722: \"session_token\": cookie_session(self),\n723: \"client_ip\": client_ip,\n724: }\n725: )\n726: self.reply(\n727: 200,\n728: \"application/json; charset=utf-8\",\n729: (\n730: json.dumps(\n731: state,\n732: ensure_ascii=False,\n733: separators=(\",\", \":\"),\n734: sort_keys=True,\n735: )\n736: + \"\\n\"\n737: ).encode(),\n738: )\n739: return\n740: \n741: self.error_reply(404, \"not found\")\n742: \n743: except PortalError as exc:\n744: self.error_reply(403, str(exc))\n745: except Exception as exc:\n746: self.error_reply(\n747: 500,\n748: f\"{type(exc).__name__}:{str(exc)[:1000]}\",\n749: )\n750: \n751: def do_POST(self) -> None:\n752: try:","portal_Handler_error_reply":"518: headers=values,\n519: )\n520: \n521: def error_reply(self, status: int, message: str) -> None:\n522: body = (\n523: f\"PORTAL_ERROR={html.escape(message)}\\n\"\n524: ).encode()\n525: self.reply(\n526: status,\n527: \"text/plain; charset=utf-8\",\n528: body,\n529: )\n530: \n531: def content_length(self) -> int:\n532: raw = self.headers.get(\"Content-Length\")","portal_Handler_path_parts":"543: raise PortalError(\"short request body\")\n544: return body\n545: \n546: def path_parts(self) -> tuple[str, dict[str, list[str]]]:\n547: parsed = urllib.parse.urlsplit(self.path)\n548: return parsed.path, urllib.parse.parse_qs(\n549: parsed.query,\n550: keep_blank_values=True,\n551: )\n552: \n553: def do_GET(self) -> None:\n554: try:","portal_Handler_reply":"468: f\"{self.client_address[0]} - {message}\\n\"\n469: )\n470: \n471: def reply(\n472: self,\n473: status: int,\n474: content_type: str,\n475: body: bytes,\n476: *,\n477: headers: dict[str, str] | None = None,\n478: ) -> None:\n479: self.send_response(status)\n480: self.send_header(\"Content-Type\", content_type)\n481: self.send_header(\"Content-Length\", str(len(body)))\n482: self.send_header(\n483: \"Cache-Control\",\n484: \"no-store, no-cache, must-revalidate, max-age=0\",\n485: )\n486: self.send_header(\"Pragma\", \"no-cache\")\n487: self.send_header(\"Expires\", \"0\")\n488: self.send_header(\n489: \"X-Robots-Tag\",\n490: \"noindex, nofollow,noarchive\",\n491: )\n492: self.send_header(\"X-Content-Type-Options\", \"nosniff\")\n493: self.send_header(\"Referrer-Policy\", \"no-referrer\")\n494: self.send_header(\n495: \"Content-Security-Policy\",\n496: \"default-src 'none'; style-src 'unsafe-inline'; \"\n497: \"form-action 'self'; frame-ancestors 'none'; \"\n498: \"base-uri 'none'\",\n499: )\n500: for key, value in (headers or {}).items():\n501: self.send_header(key, value)\n502: self.end_headers()\n503: if body:\n504: self.wfile.write(body)\n505: \n506: def redirect(\n507: self,"},"source_sha256":{"adapter":"8d0af84dcfcf6ddbfd1c4e6cbb059cef66b4cfc6c75bfd66da649acc229c7f5e","portal":"8056faf4ef6348f0b06319ad3c49fbfeeadf25b6370f857ea19569bd27652ee2","runner":"b248a4c32c9cc64e5747e7dce6c7fc0a23f5124a77c71ce72e27a81aceae9d2d"},"states":{"current_running":[{"command_id":"RESULT-API-PATCH-AUDIT-20260727T075149Z","state_file":"275ZUEB6ZH.json"}],"invalid":[],"other_running":[]},"status":"OK"} OUTPUT_END CHAT_OUTPUT_END