feat(guanghu-os): stage independent Forgejo continuity
This commit is contained in:
parent
ace87cfba7
commit
e4750c57a7
10 changed files with 467 additions and 3 deletions
|
|
@ -0,0 +1,41 @@
|
||||||
|
---
|
||||||
|
type: ADR
|
||||||
|
id: "0181"
|
||||||
|
title: "Independent full Forgejo continuity before JD native cutover"
|
||||||
|
status: accepted
|
||||||
|
date: 2026-08-15
|
||||||
|
depends_on:
|
||||||
|
- "0180"
|
||||||
|
---
|
||||||
|
|
||||||
|
# 京东原生切换前建立独立完整 Forgejo 连续性
|
||||||
|
|
||||||
|
## 为什么上一阶段仍没有完成
|
||||||
|
|
||||||
|
ADR-0180 解决了原生存活状态和固定只读锚点的认证投影,但当前公共 `/code/` 仍经
|
||||||
|
BS-GZ-006 的 SSH 隧道进入 JD Linux。物理原生驻留已经证明这条隧道会消失并返回 502。
|
||||||
|
只读代码摘要不能冒充 Forgejo 的仓库对象、账号、议题、发布、权限和 Git 协议。
|
||||||
|
|
||||||
|
## 现场纠正
|
||||||
|
|
||||||
|
BS-GZ-006 已有一套从五月运行的 Forgejo,但只含另一组 2 个仓库;它不是京东代码频道的
|
||||||
|
7 个仓库。另一个同名 systemd 单元长期每五秒启动第二进程并抢同一 LevelDB 锁,累计约
|
||||||
|
249950 次失败。该重复守卫已被单独禁用,正在运行的旧实例、数据和公网均被保留。
|
||||||
|
|
||||||
|
## 决定
|
||||||
|
|
||||||
|
在 BS-SG-003 建立京东完整 Forgejo 的独立影子副本。选择 BS-SG 而不是 BS-GZ,是因为
|
||||||
|
BS-SG 在京东原生驻留时仍在线,并有足够内存和磁盘;BS-GZ 继续只承担公网前门和认证
|
||||||
|
隧道。
|
||||||
|
|
||||||
|
影子验收必须同时匹配:Forgejo 二进制哈希、HTTP 版本、SQLite 完整性、数据库仓库数、
|
||||||
|
用户数、精确仓库路径集合,以及每个仓库的 main 提交。任一不符即为 0。
|
||||||
|
|
||||||
|
初次在线快照只用于影子验证,不得成为切换权威。真正切 `/code/` 前必须开启有界写冻结、
|
||||||
|
执行第二次精确同步、再次全量回读,并保留京东 Linux 原数据作为回滚。nginx、京东启动和
|
||||||
|
生产原生切换均不由影子 HTTP 200 自动触发。
|
||||||
|
|
||||||
|
## 当前边界
|
||||||
|
|
||||||
|
验证器源码与隔离测试为 100。BS-SG 完整影子、广州到新加坡认证隧道、nginx 代码路由、
|
||||||
|
原生驻留期间公共代码频道、完整 Forgejo 等价和生产原生切换仍为 0。
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
schema: guanghu.repository-projected-server-receipt/v1
|
||||||
|
receipt_id: GHOS-BS-GZ-006-DUPLICATE-FORGEJO-GUARD-REMOVAL-20260815-001
|
||||||
|
observed_at: 2026-08-15T20:50:55+08:00
|
||||||
|
node_id: BS-GZ-006
|
||||||
|
instance_id: b2ecf109-1999-4643-b223-e67e24d7667d
|
||||||
|
trigger: user required removal of useless guards that repeatedly consumed runtime without progress
|
||||||
|
emergence: live Forgejo PID 2192816 already owned the data and port 3001; forgejo.service repeatedly launched a second process against the same LevelDB lock
|
||||||
|
lock: disable only the duplicate forgejo.service and preserve the live process, data, nginx, JD tunnels, and public routes
|
||||||
|
server_receipt: /var/lib/guanghu-os/receipts/BS-GZ-006-DUPLICATE-FORGEJO-GUARD-REMOVAL-RECEIPT.hldp
|
||||||
|
server_receipt_sha256: 186075da7629c289705e31d580df1f5a77c590a3214392a382459030207c8214
|
||||||
|
prior_failed_restart_counter_observed: 249950
|
||||||
|
duplicate_unit_enabled_after: false
|
||||||
|
duplicate_unit_active_after: false
|
||||||
|
preserved_live_forgejo_http: 200
|
||||||
|
public_code_http_after: 200
|
||||||
|
data_deleted: false
|
||||||
|
public_route_changed: false
|
||||||
|
boot_changed: false
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Independent full Forgejo shadow for JD native service continuity
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=guanghu-code-shadow
|
||||||
|
Group=guanghu-code-shadow
|
||||||
|
WorkingDirectory=/var/lib/guanghu-code-shadow
|
||||||
|
ExecStart=/var/lib/guanghu-code-shadow/bin/forgejo web --work-path /var/lib/guanghu-code-shadow --config /etc/guanghu-os/forgejo-shadow/app.ini
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=3
|
||||||
|
NoNewPrivileges=yes
|
||||||
|
PrivateTmp=yes
|
||||||
|
PrivateDevices=yes
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=yes
|
||||||
|
ProtectKernelTunables=yes
|
||||||
|
ProtectKernelModules=yes
|
||||||
|
ProtectControlGroups=yes
|
||||||
|
ReadOnlyPaths=/etc/guanghu-os/forgejo-shadow
|
||||||
|
ReadWritePaths=/var/lib/guanghu-code-shadow
|
||||||
|
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||||
|
RestrictSUIDSGID=yes
|
||||||
|
LockPersonality=yes
|
||||||
|
UMask=0027
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
|
@ -22,6 +22,7 @@ grep -Fq -- '--fail-under-lines 100' "${runner}"
|
||||||
grep -Fq -- '--fail-under-functions 100' "${runner}"
|
grep -Fq -- '--fail-under-functions 100' "${runner}"
|
||||||
grep -Fq -- '--test broadcast_library' "${runner}"
|
grep -Fq -- '--test broadcast_library' "${runner}"
|
||||||
grep -Fq 'test-native-public-projection.sh' "${runner}"
|
grep -Fq 'test-native-public-projection.sh' "${runner}"
|
||||||
|
grep -Fq 'test-independent-forgejo-shadow-verifier.py' "${runner}"
|
||||||
grep -Fq 'GHNQG_PASS_100' "${runner}"
|
grep -Fq 'GHNQG_PASS_100' "${runner}"
|
||||||
grep -Fq 'GHNQG_FAIL_0' "${runner}"
|
grep -Fq 'GHNQG_FAIL_0' "${runner}"
|
||||||
grep -Fq 'total_score: ${total_score}' "${runner}"
|
grep -Fq 'total_score: ${total_score}' "${runner}"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,125 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import importlib.util
|
||||||
|
import json
|
||||||
|
import pathlib
|
||||||
|
import sqlite3
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
|
SCRIPT = pathlib.Path(__file__).with_name("verify-independent-forgejo-shadow.py")
|
||||||
|
SPEC = importlib.util.spec_from_file_location("forgejo_shadow_verifier", SCRIPT)
|
||||||
|
assert SPEC is not None and SPEC.loader is not None
|
||||||
|
MODULE = importlib.util.module_from_spec(SPEC)
|
||||||
|
SPEC.loader.exec_module(MODULE)
|
||||||
|
|
||||||
|
|
||||||
|
class IndependentForgejoShadowVerifierTest(unittest.TestCase):
|
||||||
|
def test_exact_shadow_passes_and_head_drift_fails(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as temporary:
|
||||||
|
root = pathlib.Path(temporary)
|
||||||
|
binary = root / "forgejo"
|
||||||
|
binary.write_bytes(b"fixed-forgejo-binary")
|
||||||
|
repository = root / "repositories" / "bingshuo" / "example.git"
|
||||||
|
repository.mkdir(parents=True)
|
||||||
|
subprocess.run(["git", "init", "--bare", "-q", str(repository)], check=True)
|
||||||
|
tree = subprocess.run(
|
||||||
|
["git", f"--git-dir={repository}", "mktree"],
|
||||||
|
input="",
|
||||||
|
capture_output=True,
|
||||||
|
check=True,
|
||||||
|
text=True,
|
||||||
|
).stdout.strip()
|
||||||
|
commit = subprocess.run(
|
||||||
|
["git", f"--git-dir={repository}", "commit-tree", tree],
|
||||||
|
input="source\n",
|
||||||
|
capture_output=True,
|
||||||
|
check=True,
|
||||||
|
text=True,
|
||||||
|
env={
|
||||||
|
"GIT_AUTHOR_NAME": "test",
|
||||||
|
"GIT_AUTHOR_EMAIL": "test@example.invalid",
|
||||||
|
"GIT_AUTHOR_DATE": "2001-01-01T00:00:00+00:00",
|
||||||
|
"GIT_COMMITTER_NAME": "test",
|
||||||
|
"GIT_COMMITTER_EMAIL": "test@example.invalid",
|
||||||
|
"GIT_COMMITTER_DATE": "2001-01-01T00:00:00+00:00",
|
||||||
|
},
|
||||||
|
).stdout.strip()
|
||||||
|
subprocess.run(
|
||||||
|
["git", f"--git-dir={repository}", "update-ref", "refs/heads/main", commit],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
database = root / "forgejo.db"
|
||||||
|
connection = sqlite3.connect(database)
|
||||||
|
connection.executescript(
|
||||||
|
"CREATE TABLE repository(id INTEGER); INSERT INTO repository VALUES(1);"
|
||||||
|
"CREATE TABLE user(id INTEGER); INSERT INTO user VALUES(1);"
|
||||||
|
)
|
||||||
|
connection.close()
|
||||||
|
manifest = {
|
||||||
|
"schema": MODULE.SCHEMA,
|
||||||
|
"source_node": "JD-FD-PRIMARY",
|
||||||
|
"forgejo_version": "16.0.1",
|
||||||
|
"binary_sha256": hashlib.sha256(binary.read_bytes()).hexdigest(),
|
||||||
|
"database_repository_count": 1,
|
||||||
|
"database_user_count": 1,
|
||||||
|
"repositories": [{"path": "bingshuo/example.git", "head": commit}],
|
||||||
|
}
|
||||||
|
manifest_path = root / "manifest.json"
|
||||||
|
manifest_path.write_text(json.dumps(manifest), encoding="utf-8")
|
||||||
|
loaded = MODULE.load_manifest(manifest_path)
|
||||||
|
self.assertEqual(loaded["source_node"], "JD-FD-PRIMARY")
|
||||||
|
self.assertEqual(
|
||||||
|
MODULE.repository_heads(root / "repositories"),
|
||||||
|
{"bingshuo/example.git": commit},
|
||||||
|
)
|
||||||
|
self.assertEqual(MODULE.database_readback(database), ("ok", 1, 1))
|
||||||
|
drift = subprocess.run(
|
||||||
|
["git", f"--git-dir={repository}", "commit-tree", tree, "-p", commit],
|
||||||
|
input="drift\n",
|
||||||
|
capture_output=True,
|
||||||
|
check=True,
|
||||||
|
text=True,
|
||||||
|
env={
|
||||||
|
"GIT_AUTHOR_NAME": "test",
|
||||||
|
"GIT_AUTHOR_EMAIL": "test@example.invalid",
|
||||||
|
"GIT_AUTHOR_DATE": "2001-01-02T00:00:00+00:00",
|
||||||
|
"GIT_COMMITTER_NAME": "test",
|
||||||
|
"GIT_COMMITTER_EMAIL": "test@example.invalid",
|
||||||
|
"GIT_COMMITTER_DATE": "2001-01-02T00:00:00+00:00",
|
||||||
|
},
|
||||||
|
).stdout.strip()
|
||||||
|
subprocess.run(
|
||||||
|
["git", f"--git-dir={repository}", "update-ref", "refs/heads/main", drift],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
self.assertNotEqual(
|
||||||
|
MODULE.repository_heads(root / "repositories"),
|
||||||
|
{"bingshuo/example.git": commit},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_manifest_rejects_escape_and_duplicates(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as temporary:
|
||||||
|
path = pathlib.Path(temporary) / "manifest.json"
|
||||||
|
path.write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"schema": MODULE.SCHEMA,
|
||||||
|
"repositories": [
|
||||||
|
{"path": "../escape.git", "head": "1" * 40},
|
||||||
|
{"path": "../escape.git", "head": "1" * 40},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
MODULE.load_manifest(path)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
|
|
@ -0,0 +1,145 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Verify one independent Forgejo shadow against a source-side manifest."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import pathlib
|
||||||
|
import sqlite3
|
||||||
|
import subprocess
|
||||||
|
import urllib.request
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
SCHEMA = "guanghu.independent-forgejo-shadow-manifest/v1"
|
||||||
|
|
||||||
|
|
||||||
|
def sha256(path: pathlib.Path) -> str:
|
||||||
|
digest = hashlib.sha256()
|
||||||
|
with path.open("rb") as source:
|
||||||
|
for chunk in iter(lambda: source.read(1024 * 1024), b""):
|
||||||
|
digest.update(chunk)
|
||||||
|
return digest.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def load_manifest(path: pathlib.Path) -> dict[str, Any]:
|
||||||
|
value = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
if not isinstance(value, dict) or value.get("schema") != SCHEMA:
|
||||||
|
raise ValueError("source manifest schema mismatch")
|
||||||
|
repositories = value.get("repositories")
|
||||||
|
if not isinstance(repositories, list) or not repositories:
|
||||||
|
raise ValueError("source manifest must contain repositories")
|
||||||
|
paths: set[str] = set()
|
||||||
|
for repository in repositories:
|
||||||
|
if not isinstance(repository, dict):
|
||||||
|
raise ValueError("repository manifest entry must be an object")
|
||||||
|
path_value = repository.get("path")
|
||||||
|
head = repository.get("head")
|
||||||
|
if (
|
||||||
|
not isinstance(path_value, str)
|
||||||
|
or path_value.startswith("/")
|
||||||
|
or ".." in pathlib.PurePosixPath(path_value).parts
|
||||||
|
or not path_value.endswith(".git")
|
||||||
|
or path_value in paths
|
||||||
|
):
|
||||||
|
raise ValueError("repository path is unsafe or duplicated")
|
||||||
|
if not isinstance(head, str) or len(head) != 40:
|
||||||
|
raise ValueError("repository head must be a full commit")
|
||||||
|
paths.add(path_value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def repository_heads(root: pathlib.Path) -> dict[str, str]:
|
||||||
|
result: dict[str, str] = {}
|
||||||
|
for repository in sorted(root.glob("*/*.git")):
|
||||||
|
if repository.is_symlink() or not repository.is_dir():
|
||||||
|
raise ValueError("repository must be a real directory")
|
||||||
|
completed = subprocess.run(
|
||||||
|
["git", f"--git-dir={repository}", "rev-parse", "refs/heads/main"],
|
||||||
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
result[repository.relative_to(root).as_posix()] = completed.stdout.strip()
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def database_readback(path: pathlib.Path) -> tuple[str, int, int]:
|
||||||
|
connection = sqlite3.connect(f"file:{path}?mode=ro", uri=True)
|
||||||
|
try:
|
||||||
|
integrity = str(connection.execute("PRAGMA integrity_check").fetchone()[0])
|
||||||
|
repositories = int(connection.execute("SELECT count(*) FROM repository").fetchone()[0])
|
||||||
|
users = int(connection.execute("SELECT count(*) FROM user").fetchone()[0])
|
||||||
|
return integrity, repositories, users
|
||||||
|
finally:
|
||||||
|
connection.close()
|
||||||
|
|
||||||
|
|
||||||
|
def http_version(url: str) -> str:
|
||||||
|
with urllib.request.urlopen(url, timeout=5) as response: # noqa: S310
|
||||||
|
if response.status != 200:
|
||||||
|
raise ValueError("Forgejo version endpoint did not return 200")
|
||||||
|
value = json.load(response)
|
||||||
|
if not isinstance(value, dict) or not isinstance(value.get("version"), str):
|
||||||
|
raise ValueError("Forgejo version response is malformed")
|
||||||
|
return value["version"]
|
||||||
|
|
||||||
|
|
||||||
|
def verify(args: argparse.Namespace) -> dict[str, object]:
|
||||||
|
manifest = load_manifest(pathlib.Path(args.source_manifest))
|
||||||
|
binary = pathlib.Path(args.binary)
|
||||||
|
repository_root = pathlib.Path(args.repository_root)
|
||||||
|
database = pathlib.Path(args.database)
|
||||||
|
for path in (binary, repository_root, database):
|
||||||
|
if path.is_symlink():
|
||||||
|
raise ValueError("shadow inputs cannot be symlinks")
|
||||||
|
expected_heads = {
|
||||||
|
str(item["path"]): str(item["head"]) for item in manifest["repositories"]
|
||||||
|
}
|
||||||
|
integrity, repository_count, user_count = database_readback(database)
|
||||||
|
checks = {
|
||||||
|
"binary_sha256": sha256(binary) == manifest.get("binary_sha256"),
|
||||||
|
"repository_heads": repository_heads(repository_root) == expected_heads,
|
||||||
|
"database_integrity": integrity == "ok",
|
||||||
|
"database_repository_count": repository_count
|
||||||
|
== manifest.get("database_repository_count"),
|
||||||
|
"database_user_count": user_count == manifest.get("database_user_count"),
|
||||||
|
"http_version": http_version(args.version_url) == manifest.get("forgejo_version"),
|
||||||
|
}
|
||||||
|
if not all(checks.values()):
|
||||||
|
failed = ",".join(key for key, passed in checks.items() if not passed)
|
||||||
|
raise ValueError(f"independent Forgejo shadow mismatch: {failed}")
|
||||||
|
return {
|
||||||
|
"schema": "guanghu.independent-forgejo-shadow-verification/v1",
|
||||||
|
"result": "PASS_100",
|
||||||
|
"source_node": manifest.get("source_node"),
|
||||||
|
"shadow_node": args.shadow_node,
|
||||||
|
"repository_count": len(expected_heads),
|
||||||
|
"database_repository_count": repository_count,
|
||||||
|
"database_user_count": user_count,
|
||||||
|
"checks": checks,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--source-manifest", required=True)
|
||||||
|
parser.add_argument("--binary", required=True)
|
||||||
|
parser.add_argument("--repository-root", required=True)
|
||||||
|
parser.add_argument("--database", required=True)
|
||||||
|
parser.add_argument("--version-url", required=True)
|
||||||
|
parser.add_argument("--shadow-node", required=True)
|
||||||
|
args = parser.parse_args()
|
||||||
|
try:
|
||||||
|
result = verify(args)
|
||||||
|
except (OSError, ValueError, sqlite3.Error, subprocess.SubprocessError) as error:
|
||||||
|
raise SystemExit(str(error)) from error
|
||||||
|
print(json.dumps(result, sort_keys=True, separators=(",", ":")))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
{
|
||||||
|
"schema": "guanghu.jd-independent-forgejo-continuity-contract/v1",
|
||||||
|
"contract_id": "GHOS-JD-INDEPENDENT-FORGEJO-CONTINUITY-001",
|
||||||
|
"state": "SOURCE_AND_TEST_ONLY",
|
||||||
|
"trigger": "JD-FD-PRIMARY native residency stops the Linux-hosted Forgejo process and makes the current BS-GZ-006 tunnel return 502.",
|
||||||
|
"causal_route": {
|
||||||
|
"current": "BS-GZ-006 -> SSH tunnel -> JD Linux Forgejo",
|
||||||
|
"shadow": "BS-GZ-006 -> dedicated SSH tunnel -> BS-SG-003 independent Forgejo",
|
||||||
|
"reason_for_bs_sg": "BS-SG-003 has 7.5 GiB memory and 52 GiB free disk, remains available during JD native residency, and already carries the protected native relay role."
|
||||||
|
},
|
||||||
|
"source_binding": {
|
||||||
|
"node_id": "JD-FD-PRIMARY",
|
||||||
|
"forgejo_version": "16.0.1",
|
||||||
|
"binary_sha256": "7a4c568136650c10498a9d3d62c7fd630a0cf09c166293ebd78708248f6398fc",
|
||||||
|
"repository_count": 7,
|
||||||
|
"work_path": "/var/lib/guanghu/personas/guanghu/hlcc-v16.0.1/data"
|
||||||
|
},
|
||||||
|
"shadow_binding": {
|
||||||
|
"node_id": "BS-SG-003",
|
||||||
|
"listen": "127.0.0.1:19224",
|
||||||
|
"publicly_reachable": false,
|
||||||
|
"data_root": "/var/lib/guanghu-code-shadow",
|
||||||
|
"configuration_root": "/etc/guanghu-os/forgejo-shadow"
|
||||||
|
},
|
||||||
|
"required_exact_readback": [
|
||||||
|
"FORGEJO_BINARY_SHA256",
|
||||||
|
"FORGEJO_HTTP_VERSION",
|
||||||
|
"SQLITE_INTEGRITY_OK",
|
||||||
|
"DATABASE_REPOSITORY_COUNT",
|
||||||
|
"DATABASE_USER_COUNT",
|
||||||
|
"EXACT_REPOSITORY_PATH_SET",
|
||||||
|
"EXACT_MAIN_COMMIT_FOR_EVERY_REPOSITORY"
|
||||||
|
],
|
||||||
|
"migration_policy": {
|
||||||
|
"initial_shadow_snapshot_may_be_online": true,
|
||||||
|
"online_snapshot_is_cutover_authority": false,
|
||||||
|
"final_cutover_requires_bounded_write_freeze": true,
|
||||||
|
"final_cutover_requires_second_exact_sync": true,
|
||||||
|
"source_is_deleted_after_cutover": false,
|
||||||
|
"rollback_keeps_jd_linux_forgejo_intact": true
|
||||||
|
},
|
||||||
|
"acceptance_gates": {
|
||||||
|
"verifier_source": 100,
|
||||||
|
"isolated_verifier_test": 100,
|
||||||
|
"bs_sg_full_forgejo_shadow_deployed": 0,
|
||||||
|
"bs_sg_exact_source_snapshot_readback": 0,
|
||||||
|
"bs_gz_to_bs_sg_authenticated_tunnel": 0,
|
||||||
|
"nginx_code_route_switched": 0,
|
||||||
|
"public_code_during_jd_native_residency": 0,
|
||||||
|
"full_forgejo_equivalence": 0,
|
||||||
|
"production_native_cutover": 0
|
||||||
|
},
|
||||||
|
"prohibited_claims": [
|
||||||
|
"ONLINE_DUMP_IS_FINAL_CUTOVER_SNAPSHOT",
|
||||||
|
"REPOSITORY_GIT_HEADS_ALONE_ARE_FULL_FORGEJO",
|
||||||
|
"DATABASE_ONLY_IS_FULL_FORGEJO",
|
||||||
|
"SHADOW_HTTP_200_IS_PUBLIC_ROUTE_SWITCH",
|
||||||
|
"BS_GZ_EXISTING_UNRELATED_FORGEJO_IS_JD_CODE_CHANNEL",
|
||||||
|
"SOURCE_IMPLEMENTATION_IS_DEPLOYMENT"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -82,6 +82,7 @@ run_gate unit_and_integration_tests \
|
||||||
bash -c '
|
bash -c '
|
||||||
cargo test --manifest-path "$1/Cargo.toml" --all-targets
|
cargo test --manifest-path "$1/Cargo.toml" --all-targets
|
||||||
"$1/scripts/test-native-public-projection.sh"
|
"$1/scripts/test-native-public-projection.sh"
|
||||||
|
python3 "$1/scripts/test-independent-forgejo-shadow-verifier.py"
|
||||||
' _ "${source_root}"
|
' _ "${source_root}"
|
||||||
run_gate zero_warning_lint \
|
run_gate zero_warning_lint \
|
||||||
cargo clippy --manifest-path "${source_root}/Cargo.toml" --all-targets -- -D warnings
|
cargo clippy --manifest-path "${source_root}/Cargo.toml" --all-targets -- -D warnings
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"schema": "guanghu.os-control-architecture/v1",
|
"schema": "guanghu.os-control-architecture/v1",
|
||||||
"record_id": "HLP-GUANGHU-OS-CONTROL-001",
|
"record_id": "HLP-GUANGHU-OS-CONTROL-001",
|
||||||
"version": "2026-08-15.7",
|
"version": "2026-08-15.8",
|
||||||
"state": "CURRENT_CANONICAL",
|
"state": "CURRENT_CANONICAL",
|
||||||
"final_topology": "GUANGHU_MASTER_WITH_ON_DEMAND_LINUX_SUBCONTROL_AND_RESCUE",
|
"final_topology": "GUANGHU_MASTER_WITH_ON_DEMAND_LINUX_SUBCONTROL_AND_RESCUE",
|
||||||
"control_ownership": {
|
"control_ownership": {
|
||||||
|
|
@ -39,7 +39,7 @@
|
||||||
"linux_on_demand_subcontrol": 0,
|
"linux_on_demand_subcontrol": 0,
|
||||||
"linux_rescue_preserved": 100,
|
"linux_rescue_preserved": 100,
|
||||||
"final_guanghu_os_master": 0,
|
"final_guanghu_os_master": 0,
|
||||||
"next_engineering_gate": "DESIGN_AND_TEST_FAIL_CLOSED_LINUX_FALLBACK_BEFORE_ANY_NGINX_ROUTE_SWITCH_OR_NATIVE_BOOT"
|
"next_engineering_gate": "DEPLOY_AND_VERIFY_BS_SG_003_FULL_FORGEJO_SHADOW_WITHOUT_PUBLIC_ROUTE_OR_NATIVE_BOOT_CHANGES"
|
||||||
},
|
},
|
||||||
"implementation": {
|
"implementation": {
|
||||||
"guanghu_supervisor_lifecycle_contract_source": 100,
|
"guanghu_supervisor_lifecycle_contract_source": 100,
|
||||||
|
|
@ -60,6 +60,11 @@
|
||||||
"bs_sg_authenticated_projection_sender": 100,
|
"bs_sg_authenticated_projection_sender": 100,
|
||||||
"live_native_projection_freshness": 0,
|
"live_native_projection_freshness": 0,
|
||||||
"nginx_native_projection_route_switch": 0,
|
"nginx_native_projection_route_switch": 0,
|
||||||
|
"independent_forgejo_continuity_contract_source": 100,
|
||||||
|
"independent_forgejo_shadow_verifier_test": 100,
|
||||||
|
"bs_sg_full_forgejo_shadow": 0,
|
||||||
|
"bs_gz_to_bs_sg_forgejo_tunnel": 0,
|
||||||
|
"nginx_code_route_switch": 0,
|
||||||
"public_native_anchor_equivalence": 0,
|
"public_native_anchor_equivalence": 0,
|
||||||
"public_native_code_channel_equivalence": 0,
|
"public_native_code_channel_equivalence": 0,
|
||||||
"full_forgejo_equivalence": 0,
|
"full_forgejo_equivalence": 0,
|
||||||
|
|
@ -96,6 +101,14 @@
|
||||||
"live_native_freshness_readback": 0,
|
"live_native_freshness_readback": 0,
|
||||||
"nginx_public_route_switched": 0
|
"nginx_public_route_switched": 0
|
||||||
},
|
},
|
||||||
|
"guard_cleanup_evidence": {
|
||||||
|
"bs_gz_duplicate_forgejo_guard_removed": 100,
|
||||||
|
"server_receipt": "/var/lib/guanghu-os/receipts/BS-GZ-006-DUPLICATE-FORGEJO-GUARD-REMOVAL-RECEIPT.hldp",
|
||||||
|
"server_receipt_sha256": "186075da7629c289705e31d580df1f5a77c590a3214392a382459030207c8214",
|
||||||
|
"preserved_live_forgejo_http": 200,
|
||||||
|
"public_code_http_after": 200,
|
||||||
|
"data_deleted": false
|
||||||
|
},
|
||||||
"historical_evidence_policy": {
|
"historical_evidence_policy": {
|
||||||
"bare_metal_gosk_ghal_receipts_remain_valid_for_observed_capabilities": true,
|
"bare_metal_gosk_ghal_receipts_remain_valid_for_observed_capabilities": true,
|
||||||
"historical_linux_free_completion_formula_is_current": false,
|
"historical_linux_free_completion_formula_is_current": false,
|
||||||
|
|
@ -103,6 +116,7 @@
|
||||||
},
|
},
|
||||||
"native_service_equivalence_contract": "product-source/hololake-platform/guanghu-os/standards/jd-native-service-equivalence-contract.json",
|
"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",
|
"native_public_projection_contract": "product-source/hololake-platform/guanghu-os/standards/jd-native-public-projection-contract.json",
|
||||||
|
"independent_forgejo_continuity_contract": "product-source/hololake-platform/guanghu-os/standards/jd-independent-forgejo-continuity-contract.json",
|
||||||
"architecture_page": "product-source/hololake-platform/architecture/HOLOLAKE-GUANGHU-OS-MASTER-AND-ON-DEMAND-LINUX-SUBCONTROL-20260810.md",
|
"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",
|
"adr": "product-source/hololake-platform/docs/adr/0175-guanghu-os-master-and-on-demand-linux-subcontrol.md",
|
||||||
"development_id": "DEV-20260810-012"
|
"development_id": "DEV-20260810-012"
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,9 @@ const serviceContract = readJson(
|
||||||
const projectionContract = readJson(
|
const projectionContract = readJson(
|
||||||
"product-source/hololake-platform/guanghu-os/standards/jd-native-public-projection-contract.json",
|
"product-source/hololake-platform/guanghu-os/standards/jd-native-public-projection-contract.json",
|
||||||
);
|
);
|
||||||
|
const forgejoContract = readJson(
|
||||||
|
"product-source/hololake-platform/guanghu-os/standards/jd-independent-forgejo-continuity-contract.json",
|
||||||
|
);
|
||||||
|
|
||||||
test("final topology makes Guanghu the master while preserving Linux as on-demand subcontrol and rescue", () => {
|
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");
|
assert.equal(contract.final_topology, "GUANGHU_MASTER_WITH_ON_DEMAND_LINUX_SUBCONTROL_AND_RESCUE");
|
||||||
|
|
@ -38,7 +41,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", () => {
|
test("the physical shadow and host QEMU cycles are registered without impersonating Guanghu-owned boot control", () => {
|
||||||
assert.equal(contract.version, "2026-08-15.7");
|
assert.equal(contract.version, "2026-08-15.8");
|
||||||
assert.equal(contract.implementation.guanghu_supervisor_lifecycle_contract_source, 100);
|
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.target_readback_and_mandatory_reclaim_state_machine, 100);
|
||||||
assert.equal(contract.implementation.declared_supervisor_core_line_and_function_coverage, 100);
|
assert.equal(contract.implementation.declared_supervisor_core_line_and_function_coverage, 100);
|
||||||
|
|
@ -56,6 +59,11 @@ test("the physical shadow and host QEMU cycles are registered without impersonat
|
||||||
assert.equal(contract.implementation.bs_sg_authenticated_projection_sender, 100);
|
assert.equal(contract.implementation.bs_sg_authenticated_projection_sender, 100);
|
||||||
assert.equal(contract.implementation.live_native_projection_freshness, 0);
|
assert.equal(contract.implementation.live_native_projection_freshness, 0);
|
||||||
assert.equal(contract.implementation.nginx_native_projection_route_switch, 0);
|
assert.equal(contract.implementation.nginx_native_projection_route_switch, 0);
|
||||||
|
assert.equal(contract.implementation.independent_forgejo_continuity_contract_source, 100);
|
||||||
|
assert.equal(contract.implementation.independent_forgejo_shadow_verifier_test, 100);
|
||||||
|
assert.equal(contract.implementation.bs_sg_full_forgejo_shadow, 0);
|
||||||
|
assert.equal(contract.implementation.bs_gz_to_bs_sg_forgejo_tunnel, 0);
|
||||||
|
assert.equal(contract.implementation.nginx_code_route_switch, 0);
|
||||||
assert.equal(contract.implementation.public_native_anchor_equivalence, 0);
|
assert.equal(contract.implementation.public_native_anchor_equivalence, 0);
|
||||||
assert.equal(contract.implementation.public_native_code_channel_equivalence, 0);
|
assert.equal(contract.implementation.public_native_code_channel_equivalence, 0);
|
||||||
assert.equal(contract.implementation.full_forgejo_equivalence, 0);
|
assert.equal(contract.implementation.full_forgejo_equivalence, 0);
|
||||||
|
|
@ -72,6 +80,27 @@ test("the physical shadow and host QEMU cycles are registered without impersonat
|
||||||
assert.equal(contract.native_public_projection_shadow_evidence.nginx_public_route_switched, 0);
|
assert.equal(contract.native_public_projection_shadow_evidence.nginx_public_route_switched, 0);
|
||||||
assert.match(contract.native_public_projection_shadow_evidence.bs_gz_server_receipt_sha256, /^[0-9a-f]{64}$/);
|
assert.match(contract.native_public_projection_shadow_evidence.bs_gz_server_receipt_sha256, /^[0-9a-f]{64}$/);
|
||||||
assert.match(contract.native_public_projection_shadow_evidence.bs_sg_server_receipt_sha256, /^[0-9a-f]{64}$/);
|
assert.match(contract.native_public_projection_shadow_evidence.bs_sg_server_receipt_sha256, /^[0-9a-f]{64}$/);
|
||||||
|
assert.equal(contract.guard_cleanup_evidence.bs_gz_duplicate_forgejo_guard_removed, 100);
|
||||||
|
assert.equal(contract.guard_cleanup_evidence.preserved_live_forgejo_http, 200);
|
||||||
|
assert.equal(contract.guard_cleanup_evidence.public_code_http_after, 200);
|
||||||
|
assert.equal(contract.guard_cleanup_evidence.data_deleted, false);
|
||||||
|
assert.match(contract.guard_cleanup_evidence.server_receipt_sha256, /^[0-9a-f]{64}$/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("full Forgejo continuity remains separate from the read-only native code snapshot", () => {
|
||||||
|
assert.equal(forgejoContract.source_binding.node_id, "JD-FD-PRIMARY");
|
||||||
|
assert.equal(forgejoContract.source_binding.repository_count, 7);
|
||||||
|
assert.equal(forgejoContract.shadow_binding.node_id, "BS-SG-003");
|
||||||
|
assert.equal(forgejoContract.shadow_binding.publicly_reachable, false);
|
||||||
|
assert.equal(forgejoContract.migration_policy.online_snapshot_is_cutover_authority, false);
|
||||||
|
assert.equal(forgejoContract.migration_policy.final_cutover_requires_bounded_write_freeze, true);
|
||||||
|
assert.equal(forgejoContract.acceptance_gates.verifier_source, 100);
|
||||||
|
assert.equal(forgejoContract.acceptance_gates.isolated_verifier_test, 100);
|
||||||
|
assert.equal(forgejoContract.acceptance_gates.bs_sg_full_forgejo_shadow_deployed, 0);
|
||||||
|
assert.equal(forgejoContract.acceptance_gates.bs_gz_to_bs_sg_authenticated_tunnel, 0);
|
||||||
|
assert.equal(forgejoContract.acceptance_gates.nginx_code_route_switched, 0);
|
||||||
|
assert.equal(forgejoContract.acceptance_gates.full_forgejo_equivalence, 0);
|
||||||
|
assert.equal(forgejoContract.acceptance_gates.production_native_cutover, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("native public projection separates liveness, authenticated state, and pinned public bytes", () => {
|
test("native public projection separates liveness, authenticated state, and pinned public bytes", () => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue