From 2e076c4d5ea75da55abecfd9d021fa4d1709705e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=B0=E6=9C=94?= <565183519@qq.com> Date: Sat, 15 Aug 2026 20:02:58 +0800 Subject: [PATCH] feat(guanghu-os): add authenticated native public projection --- ...e-public-projection-before-boot-cutover.md | 50 +++++ ...-PROJECTION-SOURCE-READINESS-20260815.hldp | 28 +++ ...u-native-public-projection-gateway.service | 29 +++ ...ghu-native-public-projection-relay.service | 23 +++ ...anghu-native-public-projection-relay.timer | 11 ++ .../accept-native-public-projection.py | 89 +++++++++ .../native-public-projection-gateway.py | 177 ++++++++++++++++++ .../scripts/native-public-projection-relay.py | 98 ++++++++++ ...st-guanghu-native-quality-gate-contract.sh | 1 + .../scripts/test-native-public-projection.sh | 143 ++++++++++++++ .../jd-native-public-projection-contract.json | 88 +++++++++ .../run-guanghu-native-quality-gate.sh | 5 +- routing/guanghu-os-control-architecture.json | 11 +- .../guanghu-os-control-architecture.test.mjs | 31 ++- 14 files changed, 780 insertions(+), 4 deletions(-) create mode 100644 product-source/hololake-platform/docs/adr/0180-authenticated-native-public-projection-before-boot-cutover.md create mode 100644 product-source/hololake-platform/guanghu-os/deployments/JD-FD-PRIMARY/NATIVE-PUBLIC-PROJECTION-SOURCE-READINESS-20260815.hldp create mode 100644 product-source/hololake-platform/guanghu-os/packaging/systemd/guanghu-native-public-projection-gateway.service create mode 100644 product-source/hololake-platform/guanghu-os/packaging/systemd/guanghu-native-public-projection-relay.service create mode 100644 product-source/hololake-platform/guanghu-os/packaging/systemd/guanghu-native-public-projection-relay.timer create mode 100755 product-source/hololake-platform/guanghu-os/scripts/accept-native-public-projection.py create mode 100755 product-source/hololake-platform/guanghu-os/scripts/native-public-projection-gateway.py create mode 100755 product-source/hololake-platform/guanghu-os/scripts/native-public-projection-relay.py create mode 100755 product-source/hololake-platform/guanghu-os/scripts/test-native-public-projection.sh create mode 100644 product-source/hololake-platform/guanghu-os/standards/jd-native-public-projection-contract.json diff --git a/product-source/hololake-platform/docs/adr/0180-authenticated-native-public-projection-before-boot-cutover.md b/product-source/hololake-platform/docs/adr/0180-authenticated-native-public-projection-before-boot-cutover.md new file mode 100644 index 0000000..2b440fe --- /dev/null +++ b/product-source/hololake-platform/docs/adr/0180-authenticated-native-public-projection-before-boot-cutover.md @@ -0,0 +1,50 @@ +--- +type: ADR +id: "0180" +title: "Authenticated native public projection before boot cutover" +status: accepted +date: 2026-08-15 +depends_on: + - "0179" +--- + +# 原生公网投影先于启动切换 + +## 为什么来到这里 + +ADR-0179 已经在 JD-FD-PRIMARY 的隔离 QEMU 中证明原生只读锚点和代码发现面能够运行, +但这不等于公网能够在原生驻留时继续工作。当前 BS-GZ-006 公网入口经 SSH 隧道回到 +JD Linux;一旦切换成光湖原生驻留,Linux 和隧道端点同时退出,公网会变成 502。 + +因此,下一步不能直接改启动项。必须先建立一个不依赖 JD Linux 的认证状态通道,并在 +BS-GZ-006 上以影子方式完成回读。 + +## 为什么不是把原文塞进心跳 + +原生心跳只回答“这个候选现在是否活着并处于规定状态”。它不承担知识、源码、命令或 +公网内容传输。这样既保持现有中继的最小权限,也避免把一次存活信号误当成内容来源或 +授权。 + +公网锚点和只读代码发现面的字节来自固定的 REPO-012 main 快照;心跳只决定这些固定字节 +当前能否被读出。这保留了两条互不冒充的因果链:内容由源码提交证明,在线状态由短时效 +认证状态证明。 + +## 决定 + +采用单向三节点投影:JD-FD-PRIMARY 的既有六序列原生心跳到 BS-SG-003;BS-SG-003 只把 +`ready` 或 `dormant` 通过专用 SSH 身份和强制命令投影到 BS-GZ-006;BS-GZ-006 只在状态 +绑定完全匹配且未过期时,从回环地址提供固定只读快照。 + +任何缺失、格式错误、绑定不符、休眠、未来时间或过期状态都返回 503。中继不能提供或 +修改公网内容。代码发现面明确不是 Forgejo 等价物。 + +## 为什么先影子部署 + +接收端先监听 BS-GZ-006 回环影子端口,不修改 nginx,不改变当前公网路径,也不修改 +JD 启动、磁盘或默认启动项。只有接收端、认证投影器、真实新鲜度和公网回读分别取得独立 +回执后,才能另行决定是否切公网;公网通过后仍不能自动推出物理启动切换。 + +## 当前结论 + +协议源码和隔离整链测试为 100。BS-GZ 接收端部署、BS-SG 认证投影器部署、真实原生新鲜度、 +nginx 切换、原生公网等价、完整 Forgejo 等价和生产启动切换均保持 0。 diff --git a/product-source/hololake-platform/guanghu-os/deployments/JD-FD-PRIMARY/NATIVE-PUBLIC-PROJECTION-SOURCE-READINESS-20260815.hldp b/product-source/hololake-platform/guanghu-os/deployments/JD-FD-PRIMARY/NATIVE-PUBLIC-PROJECTION-SOURCE-READINESS-20260815.hldp new file mode 100644 index 0000000..0424773 --- /dev/null +++ b/product-source/hololake-platform/guanghu-os/deployments/JD-FD-PRIMARY/NATIVE-PUBLIC-PROJECTION-SOURCE-READINESS-20260815.hldp @@ -0,0 +1,28 @@ +schema: guanghu.native-public-projection-source-readiness/v1 +receipt_id: GHOS-JD-NATIVE-PUBLIC-PROJECTION-SOURCE-20260815-001 +observed_at: 2026-08-15T20:01:04+08:00 +route: JD-FD-PRIMARY -> BS-SG-003 -> BS-GZ-006 +why: existing BS-GZ public tunnels terminate in JD Linux and cannot survive native residency +causal_separation: native heartbeat proves liveness; pinned REPO-012 snapshot proves public bytes +candidate_sha256: 9807fa751616d4b421c103d60eecface1c5eb8013cbd89417ef9e027da281fb3 +snapshot_source_commit: fb1096c5ecdcfb36015d51273f487c2f36d9c9cd +acceptor_sha256: 5a26f7fcf48a523ef598123d3c0eef524126ad87cf67c5b78bb6eda55cf05d14 +gateway_sha256: cedea65bd1c3e21d5b5944fe65a0f51364ef806abbf0218812216bd34af35c10 +relay_projector_sha256: 194af5f20843d5820090f2fa791b685d5d62289a3d649d0bd48d9f6be9a9556e +isolated_test_sha256: 22901d8ab180a3fdf9d0171058819c8a9a1632dd41113e5ea916aa2477ff7188 +contract_sha256: 1566dfde3d5487f7de0c039024f38088168b90bc5c989fb5c1f2687d442082ef +isolated_end_to_end_result: NATIVE_PUBLIC_PROJECTION_TEST_PASS_100 +contract_source: 100 +isolated_end_to_end_test: 100 +bs_gz_shadow_receiver_deployed: 0 +bs_sg_authenticated_projector_deployed: 0 +live_native_freshness_readback: 0 +nginx_public_route_switched: 0 +public_native_anchor_equivalence: 0 +public_native_code_channel_equivalence: 0 +full_forgejo_equivalence: 0 +production_native_cutover: 0 +bootloader_changed: false +default_boot_changed: false +reboot_performed: false +next_gate: publish exact source, then deploy loopback shadow receiver without nginx or boot changes diff --git a/product-source/hololake-platform/guanghu-os/packaging/systemd/guanghu-native-public-projection-gateway.service b/product-source/hololake-platform/guanghu-os/packaging/systemd/guanghu-native-public-projection-gateway.service new file mode 100644 index 0000000..75de237 --- /dev/null +++ b/product-source/hololake-platform/guanghu-os/packaging/systemd/guanghu-native-public-projection-gateway.service @@ -0,0 +1,29 @@ +[Unit] +Description=Guanghu native public projection loopback shadow gateway +After=network.target + +[Service] +Type=simple +User=guanghu-native-projection +Group=guanghu-native-projection +ExecStart=/usr/bin/python3 /usr/local/libexec/guanghu-os/native-public-projection-gateway.py --listen-host 127.0.0.1 --listen-port 19223 --state-file /var/lib/guanghu-native-projection/state.json --anchor-file /etc/guanghu-os/native-public-projection/anchor.json --code-file /etc/guanghu-os/native-public-projection/code-channel.json --expected-node JD-FD-PRIMARY --expected-relay BS-SG-003 --expected-candidate-sha256 9807fa751616d4b421c103d60eecface1c5eb8013cbd89417ef9e027da281fb3 --expected-source-commit fb1096c5ecdcfb36015d51273f487c2f36d9c9cd --max-age-seconds 15 +Restart=on-failure +RestartSec=2 +NoNewPrivileges=yes +PrivateTmp=yes +PrivateDevices=yes +ProtectSystem=strict +ProtectHome=yes +ProtectKernelTunables=yes +ProtectKernelModules=yes +ProtectControlGroups=yes +ReadOnlyPaths=/etc/guanghu-os/native-public-projection +ReadWritePaths=/var/lib/guanghu-native-projection +RestrictAddressFamilies=AF_INET AF_INET6 +RestrictSUIDSGID=yes +LockPersonality=yes +MemoryDenyWriteExecute=yes +UMask=0027 + +[Install] +WantedBy=multi-user.target diff --git a/product-source/hololake-platform/guanghu-os/packaging/systemd/guanghu-native-public-projection-relay.service b/product-source/hololake-platform/guanghu-os/packaging/systemd/guanghu-native-public-projection-relay.service new file mode 100644 index 0000000..9b093fb --- /dev/null +++ b/product-source/hololake-platform/guanghu-os/packaging/systemd/guanghu-native-public-projection-relay.service @@ -0,0 +1,23 @@ +[Unit] +Description=Project protected JD native residency state to the BS-GZ shadow receiver +After=network-online.target guanghu-jd-native-final-resident-relay.service +Wants=network-online.target + +[Service] +Type=oneshot +ExecStart=/usr/bin/python3 /usr/local/libexec/guanghu-os/native-public-projection-relay.py --relay-receipt /var/lib/guanghu-os/jd-final-resident-relay.hldp --max-receipt-age-seconds 15 --expected-nat-source 111.228.0.139 --node JD-FD-PRIMARY --relay BS-SG-003 --candidate-sha256 9807fa751616d4b421c103d60eecface1c5eb8013cbd89417ef9e027da281fb3 --source-commit fb1096c5ecdcfb36015d51273f487c2f36d9c9cd --ssh-config /etc/guanghu-os/native-public-projection-ssh.conf --ssh-target native-projection-gz +User=root +Group=root +NoNewPrivileges=yes +PrivateTmp=yes +PrivateDevices=yes +ProtectSystem=strict +ProtectHome=yes +ProtectKernelTunables=yes +ProtectKernelModules=yes +ProtectControlGroups=yes +ReadOnlyPaths=/etc/guanghu-os /var/lib/guanghu-os/jd-final-resident-relay.hldp +RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 +RestrictSUIDSGID=yes +LockPersonality=yes +UMask=0077 diff --git a/product-source/hololake-platform/guanghu-os/packaging/systemd/guanghu-native-public-projection-relay.timer b/product-source/hololake-platform/guanghu-os/packaging/systemd/guanghu-native-public-projection-relay.timer new file mode 100644 index 0000000..6fd650b --- /dev/null +++ b/product-source/hololake-platform/guanghu-os/packaging/systemd/guanghu-native-public-projection-relay.timer @@ -0,0 +1,11 @@ +[Unit] +Description=Refresh the Guanghu native public projection shadow state + +[Timer] +OnBootSec=20s +OnUnitActiveSec=5s +AccuracySec=1s +Unit=guanghu-native-public-projection-relay.service + +[Install] +WantedBy=timers.target diff --git a/product-source/hololake-platform/guanghu-os/scripts/accept-native-public-projection.py b/product-source/hololake-platform/guanghu-os/scripts/accept-native-public-projection.py new file mode 100755 index 0000000..7c1c80b --- /dev/null +++ b/product-source/hololake-platform/guanghu-os/scripts/accept-native-public-projection.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +"""Accept one SSH-authenticated native projection state transition.""" + +from __future__ import annotations + +import argparse +import json +import os +import pathlib +import secrets +import shlex +import tempfile +import time + + +SCHEMA = "guanghu.native-public-projection-state/v1" + + +def parse_command(command: str) -> tuple[str, str, str, str, str]: + fields = shlex.split(command) + if len(fields) != 5 or fields[0] not in {"ready", "dormant"}: + raise ValueError("exact ready or dormant projection command required") + return fields[0], fields[1], fields[2], fields[3], fields[4] + + +def atomic_write(path: pathlib.Path, payload: dict[str, object]) -> None: + if not path.is_absolute() or not path.parent.is_dir(): + raise ValueError("state file must have an existing absolute parent") + encoded = (json.dumps(payload, sort_keys=True, separators=(",", ":")) + "\n").encode() + descriptor, temporary_name = tempfile.mkstemp( + dir=path.parent, + prefix=f".{path.name}.", + ) + temporary = pathlib.Path(temporary_name) + try: + os.fchmod(descriptor, 0o640) + with os.fdopen(descriptor, "wb") as output: + output.write(encoded) + output.flush() + os.fsync(output.fileno()) + os.replace(temporary, path) + finally: + temporary.unlink(missing_ok=True) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--state-file", required=True) + parser.add_argument("--expected-node", required=True) + parser.add_argument("--expected-relay", required=True) + parser.add_argument("--expected-candidate-sha256", required=True) + parser.add_argument("--expected-source-commit", required=True) + args = parser.parse_args() + + try: + action, node, candidate_sha, source_commit, relay = parse_command( + os.environ.get("SSH_ORIGINAL_COMMAND", "") + ) + except ValueError as error: + raise SystemExit(str(error)) from error + expected = ( + args.expected_node, + args.expected_candidate_sha256, + args.expected_source_commit, + args.expected_relay, + ) + if (node, candidate_sha, source_commit, relay) != expected: + raise SystemExit("projection command does not match the pinned deployment") + if len(candidate_sha) != 64 or len(source_commit) != 40: + raise SystemExit("projection hashes are malformed") + + payload: dict[str, object] = { + "schema": SCHEMA, + "status": "READY_NATIVE_RESIDENT" if action == "ready" else "DORMANT", + "node_id": node, + "relay_node": relay, + "candidate_sha256": candidate_sha, + "source_commit": source_commit, + "accepted_at_epoch": time.time(), + "receipt_nonce": secrets.token_hex(16), + "transport_authentication": "SSH_FORCED_COMMAND", + } + atomic_write(pathlib.Path(args.state_file), payload) + print(json.dumps(payload, sort_keys=True, separators=(",", ":"))) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/product-source/hololake-platform/guanghu-os/scripts/native-public-projection-gateway.py b/product-source/hololake-platform/guanghu-os/scripts/native-public-projection-gateway.py new file mode 100755 index 0000000..e6bde15 --- /dev/null +++ b/product-source/hololake-platform/guanghu-os/scripts/native-public-projection-gateway.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +"""Serve pinned public snapshots only while an authenticated native state is fresh.""" + +from __future__ import annotations + +import argparse +import json +import os +import pathlib +import time +from http import HTTPStatus +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from typing import Any + + +STATE_SCHEMA = "guanghu.native-public-projection-state/v1" + + +def load_snapshot(path: pathlib.Path, schema: str, source_commit: str) -> bytes: + payload = path.read_bytes() + value = json.loads(payload) + if not isinstance(value, dict) or value.get("schema") != schema: + raise ValueError(f"snapshot schema mismatch: {path}") + if schema == "guanghu.public-navigation-anchor/v1": + observed_commit = value.get("navigation_source", {}).get("source_commit") + else: + observed_commit = value.get("commit") + if value.get("full_forgejo_equivalence") is not False: + raise ValueError("code snapshot cannot claim full Forgejo equivalence") + if observed_commit != source_commit: + raise ValueError(f"snapshot source commit mismatch: {path}") + return payload + + +class Projection: + def __init__(self, args: argparse.Namespace) -> None: + self.state_file = pathlib.Path(args.state_file) + self.expected = { + "schema": STATE_SCHEMA, + "node_id": args.expected_node, + "relay_node": args.expected_relay, + "candidate_sha256": args.expected_candidate_sha256, + "source_commit": args.expected_source_commit, + "transport_authentication": "SSH_FORCED_COMMAND", + } + self.max_age_seconds = args.max_age_seconds + self.anchor = load_snapshot( + pathlib.Path(args.anchor_file), + "guanghu.public-navigation-anchor/v1", + args.expected_source_commit, + ) + self.code = load_snapshot( + pathlib.Path(args.code_file), + "guanghu.native-code-channel-read-only/v1", + args.expected_source_commit, + ) + + def state(self) -> tuple[bool, str, dict[str, Any] | None]: + try: + if self.state_file.is_symlink(): + return False, "state_file_symlink", None + value = json.loads(self.state_file.read_text(encoding="utf-8")) + except FileNotFoundError: + return False, "state_missing", None + except (OSError, json.JSONDecodeError): + return False, "state_invalid", None + if not isinstance(value, dict): + return False, "state_invalid", None + if any(value.get(key) != expected for key, expected in self.expected.items()): + return False, "state_binding_mismatch", value + if value.get("status") != "READY_NATIVE_RESIDENT": + return False, "native_not_ready", value + accepted_at = value.get("accepted_at_epoch") + if not isinstance(accepted_at, (int, float)): + return False, "state_time_invalid", value + age = time.time() - float(accepted_at) + if age < -5: + return False, "state_time_in_future", value + if age > self.max_age_seconds: + return False, "projection_expired", value + return True, "ready", value + + +def handler_for(projection: Projection) -> type[BaseHTTPRequestHandler]: + class Handler(BaseHTTPRequestHandler): + server_version = "GuanghuNativeProjection/1" + + def log_message(self, _format: str, *_args: object) -> None: + return + + def send_bytes(self, status: int, payload: bytes, content_type: str) -> None: + self.send_response(status) + self.send_header("Content-Type", content_type) + self.send_header("Content-Length", str(len(payload))) + self.send_header("Cache-Control", "no-store") + self.send_header("X-Content-Type-Options", "nosniff") + self.end_headers() + if self.command != "HEAD": + self.wfile.write(payload) + + def send_json(self, status: int, value: dict[str, object]) -> None: + payload = (json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n").encode() + self.send_bytes(status, payload, "application/json; charset=utf-8") + + def do_HEAD(self) -> None: # noqa: N802 + self.do_GET() + + def do_GET(self) -> None: # noqa: N802 + fresh, reason, state = projection.state() + if self.path == "/healthz": + self.send_json( + HTTPStatus.OK, + { + "schema": "guanghu.native-public-projection-health/v1", + "native_projection_fresh": fresh, + "reason": reason, + "node_id": projection.expected["node_id"], + }, + ) + return + if self.path not in { + "/api/ai/v1/anchor", + "/api/ai/v1/code-channel", + }: + self.send_json(HTTPStatus.NOT_FOUND, {"error": "not_found"}) + return + if not fresh: + self.send_json( + HTTPStatus.SERVICE_UNAVAILABLE, + { + "schema": "guanghu.native-public-projection-unavailable/v1", + "native_projection_fresh": False, + "reason": reason, + }, + ) + return + assert state is not None + payload = ( + projection.anchor + if self.path == "/api/ai/v1/anchor" + else projection.code + ) + self.send_bytes(HTTPStatus.OK, payload, "application/json; charset=utf-8") + + def do_POST(self) -> None: # noqa: N802 + self.send_json(HTTPStatus.METHOD_NOT_ALLOWED, {"error": "method_not_allowed"}) + + return Handler + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--listen-host", default="127.0.0.1") + parser.add_argument("--listen-port", required=True, type=int) + parser.add_argument("--state-file", required=True) + parser.add_argument("--anchor-file", required=True) + parser.add_argument("--code-file", required=True) + parser.add_argument("--expected-node", required=True) + parser.add_argument("--expected-relay", required=True) + parser.add_argument("--expected-candidate-sha256", required=True) + parser.add_argument("--expected-source-commit", required=True) + parser.add_argument("--max-age-seconds", type=float, default=15.0) + args = parser.parse_args() + if args.listen_host not in {"127.0.0.1", "::1"}: + raise SystemExit("projection gateway must remain loopback-only") + if not 1 <= args.listen_port <= 65535: + raise SystemExit("invalid listen port") + if args.max_age_seconds <= 0: + raise SystemExit("max age must be greater than zero") + projection = Projection(args) + server = ThreadingHTTPServer((args.listen_host, args.listen_port), handler_for(projection)) + server.serve_forever() + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/product-source/hololake-platform/guanghu-os/scripts/native-public-projection-relay.py b/product-source/hololake-platform/guanghu-os/scripts/native-public-projection-relay.py new file mode 100755 index 0000000..64030bb --- /dev/null +++ b/product-source/hololake-platform/guanghu-os/scripts/native-public-projection-relay.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 +"""Project a fresh protected native-relay receipt through one pinned SSH route.""" + +from __future__ import annotations + +import argparse +import os +import pathlib +import subprocess +import time + + +def parse_receipt(path: pathlib.Path) -> tuple[dict[str, str], float]: + if path.is_symlink(): + raise ValueError("relay receipt cannot be a symlink") + stat = path.stat() + values: dict[str, str] = {} + for line in path.read_text(encoding="utf-8").splitlines(): + if not line or line[:1].isspace() or ": " not in line: + continue + key, value = line.split(": ", 1) + if key in values: + raise ValueError(f"duplicate relay receipt key: {key}") + values[key] = value + return values, time.time() - stat.st_mtime + + +def projection_action( + values: dict[str, str], age: float, max_age: float, expected_source: str +) -> str: + ready = ( + age >= -5 + and age <= max_age + and values.get("schema") + == "guanghu.physical-native-final-resident-relay/v1" + and values.get("status") == "READY_NATIVE_RESIDENT" + and values.get("recovery_capability_sent") == "false" + and values.get("acknowledged_sequences") == "1,2,3,4,5,6" + and values.get("observed_nat_sources") == expected_source + ) + return "ready" if ready else "dormant" + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--relay-receipt", required=True) + parser.add_argument("--max-receipt-age-seconds", type=float, default=15.0) + parser.add_argument("--expected-nat-source", required=True) + parser.add_argument("--node", required=True) + parser.add_argument("--relay", required=True) + parser.add_argument("--candidate-sha256", required=True) + parser.add_argument("--source-commit", required=True) + parser.add_argument("--ssh-bin", default="/usr/bin/ssh") + parser.add_argument("--ssh-config", required=True) + parser.add_argument("--ssh-target", required=True) + args = parser.parse_args() + if args.max_receipt_age_seconds <= 0: + raise SystemExit("max receipt age must be greater than zero") + ssh_bin = pathlib.Path(args.ssh_bin) + ssh_config = pathlib.Path(args.ssh_config) + if not ssh_bin.is_absolute() or not os.access(ssh_bin, os.X_OK): + raise SystemExit("SSH binary must be an executable absolute path") + if not ssh_config.is_absolute() or not ssh_config.is_file(): + raise SystemExit("SSH config must be an existing absolute file") + try: + values, age = parse_receipt(pathlib.Path(args.relay_receipt)) + action = projection_action( + values, + age, + args.max_receipt_age_seconds, + args.expected_nat_source, + ) + except (OSError, UnicodeError, ValueError): + action = "dormant" + command = [ + str(ssh_bin), + "-F", + str(ssh_config), + "-o", + "BatchMode=yes", + "-o", + "ConnectTimeout=5", + args.ssh_target, + action, + args.node, + args.candidate_sha256, + args.source_commit, + args.relay, + ] + completed = subprocess.run(command, check=False, timeout=10) + if completed.returncode != 0: + raise SystemExit("native projection transport failed closed") + print(f"NATIVE_PUBLIC_PROJECTION_RELAY_OK action={action}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/product-source/hololake-platform/guanghu-os/scripts/test-guanghu-native-quality-gate-contract.sh b/product-source/hololake-platform/guanghu-os/scripts/test-guanghu-native-quality-gate-contract.sh index 9f8e3a1..7de6fd1 100755 --- a/product-source/hololake-platform/guanghu-os/scripts/test-guanghu-native-quality-gate-contract.sh +++ b/product-source/hololake-platform/guanghu-os/scripts/test-guanghu-native-quality-gate-contract.sh @@ -21,6 +21,7 @@ grep -Fxq ' native_target: GOSK_CODE_CHANNEL_QUALITY_EXECUTOR' "${protocol}" grep -Fq -- '--fail-under-lines 100' "${runner}" grep -Fq -- '--fail-under-functions 100' "${runner}" grep -Fq -- '--test broadcast_library' "${runner}" +grep -Fq 'test-native-public-projection.sh' "${runner}" grep -Fq 'GHNQG_PASS_100' "${runner}" grep -Fq 'GHNQG_FAIL_0' "${runner}" grep -Fq 'total_score: ${total_score}' "${runner}" diff --git a/product-source/hololake-platform/guanghu-os/scripts/test-native-public-projection.sh b/product-source/hololake-platform/guanghu-os/scripts/test-native-public-projection.sh new file mode 100755 index 0000000..272f5be --- /dev/null +++ b/product-source/hololake-platform/guanghu-os/scripts/test-native-public-projection.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env bash +set -euo pipefail + +source_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +test_root=$(mktemp -d) +gateway_pid= +cleanup() { + if [[ -n ${gateway_pid} ]]; then + kill "${gateway_pid}" 2>/dev/null || true + wait "${gateway_pid}" 2>/dev/null || true + fi + rm -rf "${test_root}" +} +trap cleanup EXIT + +port=$((35000 + ${BASHPID:-$$} % 1000)) +state_file=${test_root}/state.json +anchor_file=${test_root}/anchor.json +code_file=${test_root}/code.json +relay_receipt=${test_root}/relay.hldp +fake_ssh=${test_root}/ssh +ssh_args=${test_root}/ssh-args.txt +ssh_config=${test_root}/ssh-config +candidate_sha=9807fa751616d4b421c103d60eecface1c5eb8013cbd89417ef9e027da281fb3 +source_commit=fb1096c5ecdcfb36015d51273f487c2f36d9c9cd + +cat >"${anchor_file}" <"${code_file}" <"${test_root}/gateway.log" 2>&1 & +gateway_pid=$! + +for _ in $(seq 1 50); do + curl -fsS "http://127.0.0.1:${port}/healthz" >/dev/null 2>&1 && break + sleep 0.05 +done + +[[ $(curl -sS -o "${test_root}/stale.json" -w '%{http_code}' \ + "http://127.0.0.1:${port}/api/ai/v1/anchor") == 503 ]] +grep -q '"native_projection_fresh":false' "${test_root}/stale.json" + +if SSH_ORIGINAL_COMMAND="ready JD-FD-PRIMARY ${candidate_sha} ${source_commit} BS-SG-003 extra" \ + python3 "${source_root}/scripts/accept-native-public-projection.py" \ + --state-file "${state_file}" \ + --expected-node JD-FD-PRIMARY \ + --expected-relay BS-SG-003 \ + --expected-candidate-sha256 "${candidate_sha}" \ + --expected-source-commit "${source_commit}"; then + echo "projection acceptor allowed an overlong command" >&2 + exit 1 +fi + +SSH_ORIGINAL_COMMAND="ready JD-FD-PRIMARY ${candidate_sha} ${source_commit} BS-SG-003" \ +python3 "${source_root}/scripts/accept-native-public-projection.py" \ + --state-file "${state_file}" \ + --expected-node JD-FD-PRIMARY \ + --expected-relay BS-SG-003 \ + --expected-candidate-sha256 "${candidate_sha}" \ + --expected-source-commit "${source_commit}" + +curl -fsS "http://127.0.0.1:${port}/api/ai/v1/anchor" >"${test_root}/anchor-readback.json" +curl -fsS "http://127.0.0.1:${port}/api/ai/v1/code-channel" >"${test_root}/code-readback.json" +cmp "${anchor_file}" "${test_root}/anchor-readback.json" +cmp "${code_file}" "${test_root}/code-readback.json" +[[ $(curl -sS -o /dev/null -w '%{http_code}' -X POST \ + "http://127.0.0.1:${port}/api/ai/v1/anchor") == 405 ]] + +sleep 2 +[[ $(curl -sS -o "${test_root}/expired.json" -w '%{http_code}' \ + "http://127.0.0.1:${port}/api/ai/v1/anchor") == 503 ]] +grep -q '"reason":"projection_expired"' "${test_root}/expired.json" + +SSH_ORIGINAL_COMMAND="dormant JD-FD-PRIMARY ${candidate_sha} ${source_commit} BS-SG-003" \ +python3 "${source_root}/scripts/accept-native-public-projection.py" \ + --state-file "${state_file}" \ + --expected-node JD-FD-PRIMARY \ + --expected-relay BS-SG-003 \ + --expected-candidate-sha256 "${candidate_sha}" \ + --expected-source-commit "${source_commit}" +[[ $(curl -sS -o "${test_root}/dormant.json" -w '%{http_code}' \ + "http://127.0.0.1:${port}/api/ai/v1/code-channel") == 503 ]] +grep -q '"reason":"native_not_ready"' "${test_root}/dormant.json" + +cat >"${fake_ssh}" <<'EOF' +#!/usr/bin/env bash +printf '%s\n' "$*" >"${GHOS_TEST_SSH_ARGS}" +EOF +chmod 0700 "${fake_ssh}" +: >"${ssh_config}" +cat >"${relay_receipt}" <<'EOF' +schema: guanghu.physical-native-final-resident-relay/v1 +status: READY_NATIVE_RESIDENT +recovery_capability_sent: false +acknowledged_sequences: 1,2,3,4,5,6 +observed_nat_sources: 111.228.0.139 +EOF +GHOS_TEST_SSH_ARGS=${ssh_args} python3 \ + "${source_root}/scripts/native-public-projection-relay.py" \ + --relay-receipt "${relay_receipt}" \ + --max-receipt-age-seconds 5 \ + --expected-nat-source 111.228.0.139 \ + --node JD-FD-PRIMARY \ + --relay BS-SG-003 \ + --candidate-sha256 "${candidate_sha}" \ + --source-commit "${source_commit}" \ + --ssh-bin "${fake_ssh}" \ + --ssh-config "${ssh_config}" \ + --ssh-target native-projection-gz +grep -q "native-projection-gz ready JD-FD-PRIMARY ${candidate_sha} ${source_commit} BS-SG-003$" \ + "${ssh_args}" + +sed -i.bak 's/recovery_capability_sent: false/recovery_capability_sent: true/' \ + "${relay_receipt}" +GHOS_TEST_SSH_ARGS=${ssh_args} python3 \ + "${source_root}/scripts/native-public-projection-relay.py" \ + --relay-receipt "${relay_receipt}" \ + --max-receipt-age-seconds 5 \ + --expected-nat-source 111.228.0.139 \ + --node JD-FD-PRIMARY \ + --relay BS-SG-003 \ + --candidate-sha256 "${candidate_sha}" \ + --source-commit "${source_commit}" \ + --ssh-bin "${fake_ssh}" \ + --ssh-config "${ssh_config}" \ + --ssh-target native-projection-gz +grep -q "native-projection-gz dormant JD-FD-PRIMARY ${candidate_sha} ${source_commit} BS-SG-003$" \ + "${ssh_args}" + +echo "NATIVE_PUBLIC_PROJECTION_TEST_PASS_100" diff --git a/product-source/hololake-platform/guanghu-os/standards/jd-native-public-projection-contract.json b/product-source/hololake-platform/guanghu-os/standards/jd-native-public-projection-contract.json new file mode 100644 index 0000000..435e4e4 --- /dev/null +++ b/product-source/hololake-platform/guanghu-os/standards/jd-native-public-projection-contract.json @@ -0,0 +1,88 @@ +{ + "schema": "guanghu.jd-native-public-projection-contract/v1", + "contract_id": "GHOS-JD-NATIVE-PUBLIC-PROJECTION-001", + "state": "SOURCE_AND_ISOLATED_TEST_ONLY", + "causal_route": [ + "JD-FD-PRIMARY", + "BS-SG-003", + "BS-GZ-006" + ], + "why": { + "problem": "The current BS-GZ-006 public route depends on SSH tunnels terminated by JD Linux and therefore cannot survive Guanghu native residency.", + "separation": "The native heartbeat proves current liveness only; public bytes come from a pinned repository snapshot and never from relay input.", + "transport": "A dedicated SSH identity and forced command carry only a bounded ready or dormant state transition from BS-SG-003 to BS-GZ-006.", + "failure_mode": "Missing, malformed, mismatched, dormant, future-dated, or stale state fails closed with HTTP 503." + }, + "bindings": { + "native_node": "JD-FD-PRIMARY", + "relay_node": "BS-SG-003", + "front_door_node": "BS-GZ-006", + "native_candidate_sha256": "9807fa751616d4b421c103d60eecface1c5eb8013cbd89417ef9e027da281fb3", + "snapshot_repository_id": "REPO-012", + "snapshot_branch": "main", + "snapshot_source_commit": "fb1096c5ecdcfb36015d51273f487c2f36d9c9cd", + "expected_native_nat_source": "111.228.0.139" + }, + "authentication": { + "native_to_relay": { + "evidence": "EXISTING_SOURCE_BOUND_SIX_SEQUENCE_NATIVE_HEARTBEAT", + "meaning": "NATIVE_LIVENESS_AND_RESIDENCY_ONLY", + "may_supply_public_content": false, + "max_receipt_age_seconds": 15 + }, + "relay_to_front_door": { + "transport": "DEDICATED_SSH_KEY_WITH_FORCED_COMMAND", + "allowed_actions": [ + "ready", + "dormant" + ], + "pty": false, + "port_forwarding": false, + "agent_forwarding": false, + "x11_forwarding": false, + "command_fields_are_exact_and_pinned": true + } + }, + "public_bytes": { + "provenance": "PINNED_REPO_012_SNAPSHOT", + "relay_may_supply_or_mutate_bytes": false, + "anchor_schema": "guanghu.public-navigation-anchor/v1", + "code_schema": "guanghu.native-code-channel-read-only/v1", + "full_forgejo_equivalence": false + }, + "front_door_projection": { + "listen_scope": "LOOPBACK_ONLY", + "shadow_port": 19223, + "state_max_age_seconds": 15, + "paths": [ + "/api/ai/v1/anchor", + "/api/ai/v1/code-channel" + ], + "methods": [ + "GET", + "HEAD" + ], + "stale_or_invalid_http_status": 503, + "nginx_route_switch_authorized": false + }, + "acceptance_gates": { + "contract_source": 100, + "isolated_end_to_end_test": 100, + "bs_gz_shadow_receiver_deployed": 0, + "bs_sg_authenticated_projector_deployed": 0, + "live_native_freshness_readback": 0, + "nginx_public_route_switched": 0, + "public_native_anchor_equivalence": 0, + "public_native_code_channel_equivalence": 0, + "full_forgejo_equivalence": 0, + "production_native_cutover": 0 + }, + "prohibited_claims": [ + "HEARTBEAT_IS_CONTENT_PROVENANCE", + "RELAY_ACK_IS_NATIVE_AUTHORIZATION", + "SHADOW_LOOPBACK_DEPLOYMENT_IS_PUBLIC_CUTOVER", + "READ_ONLY_CODE_SNAPSHOT_IS_FULL_FORGEJO", + "SOURCE_AND_TEST_PASS_IS_LIVE_SERVER_DEPLOYMENT", + "PUBLIC_LINUX_TUNNEL_PROVES_NATIVE_RESIDENCY" + ] +} diff --git a/product-source/hololake-platform/guanghu-os/world-seed/scripts/run-guanghu-native-quality-gate.sh b/product-source/hololake-platform/guanghu-os/world-seed/scripts/run-guanghu-native-quality-gate.sh index 7ac8a1b..bebf691 100755 --- a/product-source/hololake-platform/guanghu-os/world-seed/scripts/run-guanghu-native-quality-gate.sh +++ b/product-source/hololake-platform/guanghu-os/world-seed/scripts/run-guanghu-native-quality-gate.sh @@ -79,7 +79,10 @@ run_gate() { run_gate diff_whitespace git -C "${repository_root}" diff --check HEAD run_gate format cargo fmt --all --manifest-path "${source_root}/Cargo.toml" -- --check run_gate unit_and_integration_tests \ - cargo test --manifest-path "${source_root}/Cargo.toml" --all-targets + bash -c ' + cargo test --manifest-path "$1/Cargo.toml" --all-targets + "$1/scripts/test-native-public-projection.sh" + ' _ "${source_root}" run_gate zero_warning_lint \ cargo clippy --manifest-path "${source_root}/Cargo.toml" --all-targets -- -D warnings run_gate world_and_protocol_validation \ diff --git a/routing/guanghu-os-control-architecture.json b/routing/guanghu-os-control-architecture.json index 7edb2c1..67b1d75 100644 --- a/routing/guanghu-os-control-architecture.json +++ b/routing/guanghu-os-control-architecture.json @@ -1,7 +1,7 @@ { "schema": "guanghu.os-control-architecture/v1", "record_id": "HLP-GUANGHU-OS-CONTROL-001", - "version": "2026-08-15.5", + "version": "2026-08-15.6", "state": "CURRENT_CANONICAL", "final_topology": "GUANGHU_MASTER_WITH_ON_DEMAND_LINUX_SUBCONTROL_AND_RESCUE", "control_ownership": { @@ -39,7 +39,7 @@ "linux_on_demand_subcontrol": 0, "linux_rescue_preserved": 100, "final_guanghu_os_master": 0, - "next_engineering_gate": "IMPLEMENT_AUTHENTICATED_NATIVE_TO_BS_GZ_006_TRANSPORT_AND_PUBLIC_READBACK_BEFORE_ANY_BOOT_CUTOVER" + "next_engineering_gate": "DEPLOY_AND_VERIFY_BS_GZ_006_LOOPBACK_SHADOW_RECEIVER_WITHOUT_NGINX_OR_BOOT_CHANGES" }, "implementation": { "guanghu_supervisor_lifecycle_contract_source": 100, @@ -54,6 +54,12 @@ "native_anchor_http_qemu_capability": 100, "native_code_channel_read_only_qemu_capability": 100, "jd_physical_host_isolated_qemu_capability": 100, + "native_public_projection_contract_source": 100, + "native_public_projection_isolated_end_to_end_test": 100, + "bs_gz_shadow_projection_receiver": 0, + "bs_sg_authenticated_projection_sender": 0, + "live_native_projection_freshness": 0, + "nginx_native_projection_route_switch": 0, "public_native_anchor_equivalence": 0, "public_native_code_channel_equivalence": 0, "full_forgejo_equivalence": 0, @@ -84,6 +90,7 @@ "adr_0172_linux_hosted_control_is_transition_not_final_topology": true }, "native_service_equivalence_contract": "product-source/hololake-platform/guanghu-os/standards/jd-native-service-equivalence-contract.json", + "native_public_projection_contract": "product-source/hololake-platform/guanghu-os/standards/jd-native-public-projection-contract.json", "architecture_page": "product-source/hololake-platform/architecture/HOLOLAKE-GUANGHU-OS-MASTER-AND-ON-DEMAND-LINUX-SUBCONTROL-20260810.md", "adr": "product-source/hololake-platform/docs/adr/0175-guanghu-os-master-and-on-demand-linux-subcontrol.md", "development_id": "DEV-20260810-012" diff --git a/routing/guanghu-os-control-architecture.test.mjs b/routing/guanghu-os-control-architecture.test.mjs index 10ebbd8..a025d27 100644 --- a/routing/guanghu-os-control-architecture.test.mjs +++ b/routing/guanghu-os-control-architecture.test.mjs @@ -12,6 +12,9 @@ const rules = readJson("routing/hololake-engineering-rules.json"); const serviceContract = readJson( "product-source/hololake-platform/guanghu-os/standards/jd-native-service-equivalence-contract.json", ); +const projectionContract = readJson( + "product-source/hololake-platform/guanghu-os/standards/jd-native-public-projection-contract.json", +); test("final topology makes Guanghu the master while preserving Linux as on-demand subcontrol and rescue", () => { assert.equal(contract.final_topology, "GUANGHU_MASTER_WITH_ON_DEMAND_LINUX_SUBCONTROL_AND_RESCUE"); @@ -35,7 +38,7 @@ test("current JD state stays transitional and cannot impersonate final master co }); test("the physical shadow and host QEMU cycles are registered without impersonating Guanghu-owned boot control", () => { - assert.equal(contract.version, "2026-08-15.5"); + assert.equal(contract.version, "2026-08-15.6"); assert.equal(contract.implementation.guanghu_supervisor_lifecycle_contract_source, 100); assert.equal(contract.implementation.target_readback_and_mandatory_reclaim_state_machine, 100); assert.equal(contract.implementation.declared_supervisor_core_line_and_function_coverage, 100); @@ -47,6 +50,12 @@ test("the physical shadow and host QEMU cycles are registered without impersonat assert.equal(contract.implementation.native_anchor_http_qemu_capability, 100); assert.equal(contract.implementation.native_code_channel_read_only_qemu_capability, 100); assert.equal(contract.implementation.jd_physical_host_isolated_qemu_capability, 100); + assert.equal(contract.implementation.native_public_projection_contract_source, 100); + assert.equal(contract.implementation.native_public_projection_isolated_end_to_end_test, 100); + assert.equal(contract.implementation.bs_gz_shadow_projection_receiver, 0); + assert.equal(contract.implementation.bs_sg_authenticated_projection_sender, 0); + assert.equal(contract.implementation.live_native_projection_freshness, 0); + assert.equal(contract.implementation.nginx_native_projection_route_switch, 0); assert.equal(contract.implementation.public_native_anchor_equivalence, 0); assert.equal(contract.implementation.public_native_code_channel_equivalence, 0); assert.equal(contract.implementation.full_forgejo_equivalence, 0); @@ -59,6 +68,26 @@ test("the physical shadow and host QEMU cycles are registered without impersonat assert.match(contract.native_service_qemu_evidence.server_receipt_sha256, /^[0-9a-f]{64}$/); }); +test("native public projection separates liveness, authenticated state, and pinned public bytes", () => { + assert.deepEqual(projectionContract.causal_route, ["JD-FD-PRIMARY", "BS-SG-003", "BS-GZ-006"]); + assert.equal(projectionContract.authentication.native_to_relay.may_supply_public_content, false); + assert.equal(projectionContract.public_bytes.provenance, "PINNED_REPO_012_SNAPSHOT"); + assert.equal(projectionContract.public_bytes.relay_may_supply_or_mutate_bytes, false); + assert.equal(projectionContract.public_bytes.full_forgejo_equivalence, false); + assert.equal(projectionContract.front_door_projection.listen_scope, "LOOPBACK_ONLY"); + assert.equal(projectionContract.front_door_projection.nginx_route_switch_authorized, false); + assert.equal(projectionContract.acceptance_gates.contract_source, 100); + assert.equal(projectionContract.acceptance_gates.isolated_end_to_end_test, 100); + assert.equal(projectionContract.acceptance_gates.bs_gz_shadow_receiver_deployed, 0); + assert.equal(projectionContract.acceptance_gates.bs_sg_authenticated_projector_deployed, 0); + assert.equal(projectionContract.acceptance_gates.live_native_freshness_readback, 0); + assert.equal(projectionContract.acceptance_gates.nginx_public_route_switched, 0); + assert.equal(projectionContract.acceptance_gates.public_native_anchor_equivalence, 0); + assert.equal(projectionContract.acceptance_gates.public_native_code_channel_equivalence, 0); + assert.equal(projectionContract.acceptance_gates.full_forgejo_equivalence, 0); + assert.equal(projectionContract.acceptance_gates.production_native_cutover, 0); +}); + test("current architecture and global engineering rules project the same control contract", () => { assert.equal(architecture.version, "2026-08-14.1"); assert.equal(architecture.server_os_control.machine_projection, "routing/guanghu-os-control-architecture.json");