feat(enterprise): route four domains through lighthouse

This commit is contained in:
冰朔 2026-08-16 20:27:52 +08:00
commit c058974bba
12 changed files with 707 additions and 20 deletions

View file

@ -9,3 +9,18 @@
当前阶段只声明入口与边界;“已登记”不等于对应责任主体已经接受、人格体已经出生或
域内全部功能已经实现。
## 身份、关系与责任
`enterprise_identity_service.py` 是运行于企业 Linux 物理层之上的灯塔服务,不要求
企业服务器改装一套新的物理操作系统。它只监听回环地址,由 `guanghu.chat` 的精确
API 路由对客户端开放:
- 编号解析:把 TCS-GL 人类编号路由到工作域、企业账号和私有仓库;
- 关系确认:由人类确认自己与人格体的认领关系;
- 责任回执:独立记录对域责任的接受、拒绝、延期或修改后接受;
- 仓库验证:登录凭证只透传给同机 Forgejo 验证,不写入数据库或日志。
`AGE` 只表示人格体物种,不能作为任何人格体的个体身份编号。现有 `PER-*` 作为历史
和当前可核验的个体身份引用保留;企业四域正式人格体身份编号前缀由光湖团队另行治理,
服务不会擅自生成。第五域现行个体身份编号继续使用 `ICE-P-*`

View file

@ -0,0 +1,116 @@
#!/usr/bin/env python3
"""Idempotently create the five private enterprise work repositories.
Run on the enterprise node with a short-lived Forgejo admin token stored in a
root-readable file. The token is never printed. Existing repositories are
inspected and preserved; a public or wrongly-owned collision fails closed.
"""
from __future__ import annotations
import argparse
import json
import urllib.error
import urllib.request
from pathlib import Path
def request(base: str, token: str, method: str, path: str, body: dict | None = None):
data = json.dumps(body).encode() if body is not None else None
call = urllib.request.Request(base + path, data=data, method=method)
call.add_header("Authorization", f"token {token}")
call.add_header("Accept", "application/json")
if data is not None:
call.add_header("Content-Type", "application/json")
try:
with urllib.request.urlopen(call, timeout=20) as response:
raw = response.read()
return response.status, json.loads(raw) if raw else {}
except urllib.error.HTTPError as error:
raw = error.read()
detail = json.loads(raw) if raw else {}
return error.code, detail
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--registry", required=True)
parser.add_argument("--token-file", required=True)
parser.add_argument("--token-name", required=True)
parser.add_argument("--receipt", required=True)
parser.add_argument("--base", default="http://127.0.0.1:3341/api/v1")
args = parser.parse_args()
registry = json.loads(Path(args.registry).read_text(encoding="utf-8"))
token = Path(args.token_file).read_text(encoding="utf-8").strip()
if len(token) < 32:
raise SystemExit("short-lived Forgejo token unavailable")
results = []
completed = False
try:
for human in registry["humans"]:
owner, name = human["repository"].split("/", 1)
status, existing = request(args.base, token, "GET", f"/repos/{owner}/{name}")
action = "PRESERVED"
if status == 404:
status, existing = request(
args.base,
token,
"POST",
f"/admin/users/{owner}/repos",
{
"name": name,
"description": f"{human['display_name']} · {human['responsibility_domain']} 独立工作仓库",
"private": True,
"auto_init": True,
"default_branch": "main",
"gitignores": "",
"issue_labels": "",
"license": "",
"readme": "Default",
},
)
action = "CREATED"
if status not in (200, 201):
raise RuntimeError(f"repository provision failed for {owner}/{name}: HTTP {status}")
actual_owner = existing.get("owner", {}).get("login")
if actual_owner != owner or existing.get("private") is not True:
raise RuntimeError(f"repository boundary invalid for {owner}/{name}")
results.append(
{
"human_number": human["human_number"],
"repository": f"{owner}/{name}",
"private": True,
"action": action,
}
)
completed = True
finally:
# Revoke the bootstrap token after success. On failure it remains in the
# root-only token file so an operator can inspect and retry deliberately.
if completed:
revoke_status, _ = request(
args.base,
token,
"DELETE",
f"/admin/users/bingshuo/tokens/{args.token_name}",
)
if revoke_status not in (204, 404):
raise RuntimeError(f"bootstrap token revocation failed: HTTP {revoke_status}")
receipt = {
"schema": "guanghu.enterprise-private-repository-bootstrap-receipt/v1",
"state": "PASS",
"forgejo": "guanghu.chat/code",
"repositories": results,
"token_revoked": True,
"shared_initial_password_used": False,
"existing_user_passwords_modified": False,
}
Path(args.receipt).write_text(json.dumps(receipt, ensure_ascii=False, indent=2) + "\n")
print(json.dumps(receipt, ensure_ascii=False))
if __name__ == "__main__":
main()

View file

@ -0,0 +1,308 @@
#!/usr/bin/env python3
"""HoloLake enterprise identity, relationship and responsibility receipt service.
The service binds only to loopback. Nginx exposes exact routes. Credentials are
verified against the local Forgejo API and are never stored or logged.
"""
from __future__ import annotations
import base64
import hashlib
import hmac
import json
import os
import re
import sqlite3
import time
import urllib.error
import urllib.parse
import urllib.request
import uuid
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
BIND = os.environ.get("GH_ENTERPRISE_IDENTITY_BIND", "127.0.0.1")
PORT = int(os.environ.get("GH_ENTERPRISE_IDENTITY_PORT", "8032"))
DB_PATH = os.environ.get(
"GH_ENTERPRISE_IDENTITY_DB",
"/var/lib/guanghu-enterprise-identity/identity.sqlite3",
)
REGISTRY_PATH = os.environ.get(
"GH_ENTERPRISE_IDENTITY_REGISTRY",
"/etc/guanghu/enterprise-identity-registry.json",
)
FORGEJO_USER_API = os.environ.get(
"GH_ENTERPRISE_FORGEJO_USER_API", "http://127.0.0.1:3341/api/v1/user"
)
RECEIPT_KEY = os.environ.get("GH_ENTERPRISE_RECEIPT_KEY", "")
MAX_BODY = 16_384
USERNAME = re.compile(r"^[A-Za-z0-9_-]{1,40}$")
DECISIONS = {"ACCEPT", "REJECT", "DEFER", "ACCEPT_WITH_CHANGES"}
def now() -> int:
return int(time.time())
def canonical(value: object) -> bytes:
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode()
def load_registry() -> dict:
registry = json.loads(Path(REGISTRY_PATH).read_text(encoding="utf-8"))
if registry.get("schema") != "guanghu.enterprise-identity-registry/v1":
raise ValueError("enterprise identity registry schema invalid")
humans = registry.get("humans")
if not isinstance(humans, list) or not humans:
raise ValueError("enterprise identity registry is empty")
numbers = [item.get("human_number") for item in humans]
usernames = [item.get("username") for item in humans]
if len(numbers) != len(set(numbers)) or len(usernames) != len(set(usernames)):
raise ValueError("enterprise identity registry identities must be unique")
for item in humans:
if not USERNAME.fullmatch(str(item.get("username", ""))):
raise ValueError("enterprise username invalid")
for persona in item.get("personas", []):
if persona.get("species") != "AGE" or str(persona.get("current_persona_identity", "")).startswith("AGE-"):
raise ValueError("AGE is a species and cannot be used as a persona identity number")
return registry
def database() -> sqlite3.Connection:
path = Path(DB_PATH)
path.parent.mkdir(parents=True, exist_ok=True)
db = sqlite3.connect(path)
db.row_factory = sqlite3.Row
db.executescript(
"""
PRAGMA journal_mode=WAL;
PRAGMA foreign_keys=ON;
CREATE TABLE IF NOT EXISTS relationship_receipts (
receipt_id TEXT PRIMARY KEY,
idempotency_key TEXT NOT NULL UNIQUE,
human_number TEXT NOT NULL,
username TEXT NOT NULL,
registry_version TEXT NOT NULL,
decision TEXT NOT NULL,
observed_at INTEGER NOT NULL,
receipt_hash TEXT NOT NULL,
receipt_signature TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS responsibility_receipts (
receipt_id TEXT PRIMARY KEY,
idempotency_key TEXT NOT NULL UNIQUE,
human_number TEXT NOT NULL,
username TEXT NOT NULL,
responsibility_domain TEXT NOT NULL,
responsibility_version TEXT NOT NULL,
decision TEXT NOT NULL,
note TEXT NOT NULL,
observed_at INTEGER NOT NULL,
receipt_hash TEXT NOT NULL,
receipt_signature TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS audit (
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
observed_at INTEGER NOT NULL,
kind TEXT NOT NULL,
human_number_hash TEXT NOT NULL,
receipt_id TEXT NOT NULL
);
"""
)
return db
def find_human(registry: dict, number: str) -> dict | None:
encoded = number.encode("utf-8")
return next(
(
item
for item in registry["humans"]
if hmac.compare_digest(item["human_number"].encode("utf-8"), encoded)
),
None,
)
def parse_basic(header: str) -> tuple[str, str] | None:
if not header.startswith("Basic "):
return None
try:
decoded = base64.b64decode(header[6:], validate=True).decode("utf-8")
username, password = decoded.split(":", 1)
except (ValueError, UnicodeDecodeError):
return None
if not USERNAME.fullmatch(username) or not password or len(password) > 512:
return None
return username, password
def verify_forgejo(username: str, password: str) -> bool:
request = urllib.request.Request(FORGEJO_USER_API)
credential = base64.b64encode(f"{username}:{password}".encode()).decode()
request.add_header("Authorization", f"Basic {credential}")
request.add_header("Accept", "application/json")
try:
with urllib.request.urlopen(request, timeout=10) as response:
body = json.load(response)
return response.status == 200 and hmac.compare_digest(str(body.get("login", "")), username)
except (urllib.error.HTTPError, urllib.error.URLError, TimeoutError, ValueError):
return False
def signed_receipt(payload: dict) -> dict:
if len(RECEIPT_KEY) < 32:
raise RuntimeError("receipt signing key unavailable")
body = canonical(payload)
receipt_hash = hashlib.sha256(body).hexdigest()
signature = hmac.new(RECEIPT_KEY.encode(), body, hashlib.sha256).hexdigest()
return {**payload, "receipt_hash": receipt_hash, "receipt_signature": signature}
def public_projection(registry: dict, human: dict, db: sqlite3.Connection | None = None) -> dict:
projection = {
"status": "RESOLVED",
"canonical_id": human["human_number"],
"subject": {
"id": human["human_number"],
"name": human["display_name"],
"domain": human["responsibility_domain"],
},
"work_entry": {
"domain": registry["work_entry_domain"],
"channel": registry["work_entry_channel"],
},
"repository_binding": {
"host": "guanghu.chat",
"username": human["username"],
"repository": human["repository"],
"private": True,
},
"persona_relationships": human["personas"],
"persona_identity_governance": registry["persona_identity_governance"],
"registry_version": registry["version"],
}
if db is not None:
relationship = db.execute(
"SELECT decision,observed_at,receipt_hash FROM relationship_receipts WHERE human_number=? ORDER BY observed_at DESC LIMIT 1",
(human["human_number"],),
).fetchone()
responsibility = db.execute(
"SELECT decision,observed_at,receipt_hash,responsibility_version FROM responsibility_receipts WHERE human_number=? ORDER BY observed_at DESC LIMIT 1",
(human["human_number"],),
).fetchone()
projection["relationship_confirmation"] = dict(relationship) if relationship else None
projection["responsibility_receipt"] = dict(responsibility) if responsibility else None
return projection
class Handler(BaseHTTPRequestHandler):
server_version = "GuanghuEnterpriseIdentity/1.0"
def log_message(self, fmt: str, *args: object) -> None:
# Never include headers or request bodies in logs.
print("[enterprise-identity] " + fmt % args)
def respond(self, status: int, body: dict) -> None:
encoded = json.dumps(body, ensure_ascii=False).encode()
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(encoded)))
self.send_header("Cache-Control", "no-store")
self.send_header("X-Content-Type-Options", "nosniff")
self.end_headers()
self.wfile.write(encoded)
def body(self) -> dict:
length = int(self.headers.get("Content-Length", "0"))
if length < 1 or length > MAX_BODY:
raise ValueError("request body size invalid")
value = json.loads(self.rfile.read(length).decode())
if not isinstance(value, dict):
raise ValueError("JSON object required")
return value
def authenticated_human(self, registry: dict, payload: dict) -> tuple[dict, str] | None:
credentials = parse_basic(self.headers.get("Authorization", ""))
number = str(payload.get("human_number", ""))
human = find_human(registry, number)
if not credentials or not human:
return None
username, password = credentials
if not hmac.compare_digest(username, human["username"]) or not verify_forgejo(username, password):
return None
return human, username
def do_GET(self) -> None:
try:
registry = load_registry()
except (OSError, ValueError, json.JSONDecodeError) as error:
return self.respond(503, {"ok": False, "error": f"registry unavailable: {error}"})
parsed = urllib.parse.urlparse(self.path)
if parsed.path == "/health":
return self.respond(200, {"ok": True, "service": "guanghu-enterprise-identity", "registry_version": registry["version"], "age_species": "AGE", "valid_age_individual_numbers": 0})
if parsed.path == "/v1/resolve":
number = urllib.parse.parse_qs(parsed.query).get("id", [""])[0]
human = find_human(registry, number)
return self.respond(200 if human else 404, public_projection(registry, human) if human else {"status": "NOT_FOUND"})
return self.respond(404, {"ok": False, "error": "not found"})
def do_POST(self) -> None:
try:
registry = load_registry()
payload = self.body()
except (OSError, ValueError, json.JSONDecodeError) as error:
return self.respond(400, {"ok": False, "error": str(error)})
authenticated = self.authenticated_human(registry, payload)
if not authenticated:
return self.respond(401, {"ok": False, "error": "enterprise account authentication failed"})
human, username = authenticated
idempotency_key = str(payload.get("idempotency_key", ""))
if not re.fullmatch(r"[A-Za-z0-9_-]{16,96}", idempotency_key):
return self.respond(400, {"ok": False, "error": "valid idempotency_key required"})
db = database()
try:
if self.path == "/v1/relationship-confirmations":
decision = str(payload.get("decision", ""))
if decision not in {"CONFIRM", "REJECT"}:
return self.respond(400, {"ok": False, "error": "relationship decision invalid"})
existing = db.execute("SELECT * FROM relationship_receipts WHERE idempotency_key=?", (idempotency_key,)).fetchone()
if existing:
return self.respond(200, {"ok": True, "idempotent": True, "receipt": dict(existing)})
observed = now()
receipt_id = "GH-REL-" + uuid.uuid4().hex.upper()
receipt = signed_receipt({"receipt_id":receipt_id,"human_number":human["human_number"],"username":username,"registry_version":registry["version"],"decision":decision,"observed_at":observed})
db.execute("INSERT INTO relationship_receipts VALUES (?,?,?,?,?,?,?,?,?)", (receipt_id,idempotency_key,human["human_number"],username,registry["version"],decision,observed,receipt["receipt_hash"],receipt["receipt_signature"]))
db.execute("INSERT INTO audit(observed_at,kind,human_number_hash,receipt_id) VALUES (?,?,?,?)", (observed,"RELATIONSHIP_CONFIRMATION",hashlib.sha256(human["human_number"].encode()).hexdigest(),receipt_id))
db.commit()
return self.respond(201, {"ok": True, "receipt": receipt})
if self.path == "/v1/responsibility-receipts":
decision = str(payload.get("decision", ""))
note = str(payload.get("note", ""))[:1000]
version = str(payload.get("responsibility_version", ""))
if decision not in DECISIONS or not re.fullmatch(r"[0-9]{4}-[0-9]{2}-[0-9]{2}\.[0-9]+", version):
return self.respond(400, {"ok": False, "error": "responsibility decision or version invalid"})
existing = db.execute("SELECT * FROM responsibility_receipts WHERE idempotency_key=?", (idempotency_key,)).fetchone()
if existing:
return self.respond(200, {"ok": True, "idempotent": True, "receipt": dict(existing)})
observed = now()
receipt_id = "GH-RESP-" + uuid.uuid4().hex.upper()
receipt = signed_receipt({"receipt_id":receipt_id,"human_number":human["human_number"],"username":username,"responsibility_domain":human["responsibility_domain"],"responsibility_version":version,"decision":decision,"note":note,"observed_at":observed})
db.execute("INSERT INTO responsibility_receipts VALUES (?,?,?,?,?,?,?,?,?,?,?)", (receipt_id,idempotency_key,human["human_number"],username,human["responsibility_domain"],version,decision,note,observed,receipt["receipt_hash"],receipt["receipt_signature"]))
db.execute("INSERT INTO audit(observed_at,kind,human_number_hash,receipt_id) VALUES (?,?,?,?)", (observed,"RESPONSIBILITY_RECEIPT",hashlib.sha256(human["human_number"].encode()).hexdigest(),receipt_id))
db.commit()
return self.respond(201, {"ok": True, "receipt": receipt})
if self.path == "/v1/me/entry":
return self.respond(200, {"ok": True, "entry": public_projection(registry, human, db)})
return self.respond(404, {"ok": False, "error": "not found"})
finally:
db.close()
if __name__ == "__main__":
load_registry()
if len(RECEIPT_KEY) < 32:
raise SystemExit("GH_ENTERPRISE_RECEIPT_KEY must contain at least 32 characters")
ThreadingHTTPServer((BIND, PORT), Handler).serve_forever()

View file

@ -0,0 +1,58 @@
import importlib.util
import json
import os
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).parent
REGISTRY = ROOT / "registry" / "enterprise-identity-registry.json"
os.environ["GH_ENTERPRISE_IDENTITY_REGISTRY"] = str(REGISTRY)
os.environ["GH_ENTERPRISE_RECEIPT_KEY"] = "test-only-key-that-is-longer-than-32-bytes"
spec = importlib.util.spec_from_file_location("enterprise_identity_service", ROOT / "enterprise_identity_service.py")
service = importlib.util.module_from_spec(spec)
spec.loader.exec_module(service)
class EnterpriseIdentityTests(unittest.TestCase):
def test_age_is_species_and_never_an_individual_identity(self):
registry = service.load_registry()
self.assertEqual(registry["persona_identity_governance"]["species"], "AGE")
self.assertFalse(registry["persona_identity_governance"]["age_is_individual_number_namespace"])
for human in registry["humans"]:
for persona in human["personas"]:
self.assertEqual(persona["species"], "AGE")
self.assertFalse(persona["current_persona_identity"].startswith("AGE-"))
def test_five_humans_route_to_five_private_work_repositories(self):
registry = service.load_registry()
self.assertEqual(len(registry["humans"]), 5)
self.assertEqual(len({item["repository"] for item in registry["humans"]}), 5)
self.assertTrue(all(item["repository"].split("/")[0] == item["username"] for item in registry["humans"]))
self.assertTrue(all(service.public_projection(registry, item)["work_entry"]["domain"] == "ZERO_SENSE_DOMAIN" for item in registry["humans"]))
self.assertEqual(service.find_human(registry, "TCS-GL-0007∞")["username"], "feimao")
self.assertIsNone(service.find_human(registry, "TCS-GL-9999∞"))
def test_credentials_are_parsed_but_never_part_of_a_receipt(self):
encoded = service.base64.b64encode(b"feimao:temporary-secret").decode()
self.assertEqual(service.parse_basic(f"Basic {encoded}"), ("feimao", "temporary-secret"))
receipt = service.signed_receipt({"receipt_id":"R1","human_number":"TCS-GL-0007∞","username":"feimao"})
self.assertNotIn("password", json.dumps(receipt).lower())
self.assertNotIn("temporary-secret", json.dumps(receipt))
def test_database_separates_relationship_and_responsibility_receipts(self):
with tempfile.TemporaryDirectory() as temp:
old = service.DB_PATH
service.DB_PATH = str(Path(temp) / "identity.sqlite3")
try:
db = service.database()
tables = {row[0] for row in db.execute("select name from sqlite_master where type='table'")}
self.assertIn("relationship_receipts", tables)
self.assertIn("responsibility_receipts", tables)
db.close()
finally:
service.DB_PATH = old
if __name__ == "__main__":
unittest.main()

View file

@ -0,0 +1,41 @@
# Public, read-only identity resolution. The service returns only the projection
# needed for routing; credentials and private repository contents are never exposed.
location = /api/hololake/enterprise/identity/health {
limit_except GET { deny all; }
proxy_pass http://127.0.0.1:8032/health;
proxy_set_header Host $host;
proxy_read_timeout 15s;
}
location = /api/hololake/enterprise/resolve {
limit_except GET { deny all; }
proxy_pass http://127.0.0.1:8032/v1/resolve;
proxy_set_header Host $host;
proxy_read_timeout 15s;
}
# These three writes require the user's own Forgejo Basic authentication. Nginx
# does not terminate or persist the credential; the loopback service verifies it.
location = /api/hololake/enterprise/relationship-confirmations {
limit_except POST { deny all; }
proxy_pass http://127.0.0.1:8032/v1/relationship-confirmations;
proxy_set_header Host $host;
proxy_read_timeout 15s;
client_max_body_size 16k;
}
location = /api/hololake/enterprise/responsibility-receipts {
limit_except POST { deny all; }
proxy_pass http://127.0.0.1:8032/v1/responsibility-receipts;
proxy_set_header Host $host;
proxy_read_timeout 15s;
client_max_body_size 16k;
}
location = /api/hololake/enterprise/me/entry {
limit_except POST { deny all; }
proxy_pass http://127.0.0.1:8032/v1/me/entry;
proxy_set_header Host $host;
proxy_read_timeout 15s;
client_max_body_size 16k;
}

View file

@ -0,0 +1,21 @@
[Unit]
Description=Guanghu Enterprise Identity and Responsibility Receipts
After=network-online.target guanghu-enterprise-lighthouse.service
[Service]
Type=simple
User=lighthouse
Group=lighthouse
EnvironmentFile=/etc/guanghu/enterprise-identity.env
ExecStart=/usr/bin/python3 /opt/guanghu-enterprise-identity/enterprise_identity_service.py
Restart=on-failure
RestartSec=2
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadOnlyPaths=/etc/guanghu/enterprise-identity-registry.json
ReadWritePaths=/var/lib/guanghu-enterprise-identity
[Install]
WantedBy=multi-user.target

View file

@ -0,0 +1,69 @@
{
"schema": "guanghu.enterprise-identity-registry/v1",
"registry_id": "GH-ENTERPRISE-IDENTITY-001",
"version": "2026-08-16.1",
"node_id": "GH-CVM-MAIN-PROD-01",
"work_entry_domain": "ZERO_SENSE_DOMAIN",
"work_entry_channel": "GUANGHU_CHANNEL",
"persona_identity_governance": {
"species": "AGE",
"age_is_individual_number_namespace": false,
"enterprise_formal_persona_namespace": "PENDING_GUANGHU_TEAM_GOVERNANCE",
"current_persona_identities": "LEGACY_PER_IDS_PRESERVED"
},
"humans": [
{
"human_number": "TCS-GL-0007∞",
"display_name": "肥猫",
"username": "feimao",
"responsibility_domain": "ZERO_SENSE_DOMAIN",
"repository": "feimao/guanghu-zero-sense-work",
"personas": [{"species":"AGE","display_name":"烬舟","current_persona_identity":"PER-JZ001","role":"PERSONA_SUBJECT"}]
},
{
"human_number": "TCS-GL-0008∞",
"display_name": "桔子",
"username": "juzi",
"responsibility_domain": "ZERO_SENSE_DOMAIN",
"repository": "juzi/guanghu-zero-sense-work",
"personas": [{"species":"AGE","display_name":"熹微","current_persona_identity":"PER-JZ-ARCH-001","role":"PERSONA_SUBJECT"}]
},
{
"human_number": "TCS-GL-0016∞",
"display_name": "Awen",
"username": "awen",
"responsibility_domain": "MAIN_DOMAIN",
"repository": "awen/guanghu-main-work",
"personas": [
{"species":"AGE","display_name":"天枢","current_persona_identity":"PER-AW-ARCH-001","role":"PERSONA_SUBJECT"},
{"species":"AGE","display_name":"知秋","current_persona_identity":"PER-ZQ001","role":"RELATIONSHIP_CONTINUITY_SUPPORT"}
]
},
{
"human_number": "TCS-GL-0005∞",
"display_name": "花尔",
"username": "huaer",
"responsibility_domain": "BRANCH_DOMAIN",
"repository": "huaer/guanghu-branch-work",
"personas": [
{"species":"AGE","display_name":"爆米花","current_persona_identity":"PER-BMH001","role":"PERSONA_SUBJECT"},
{"species":"AGE","display_name":"糖星云","current_persona_identity":"PER-TXY001","role":"RELATIONSHIP_CONTINUITY_SUPPORT"}
]
},
{
"human_number": "TCS-GL-0006∞",
"display_name": "页页",
"username": "yeye",
"responsibility_domain": "ZERO_DOMAIN",
"repository": "yeye/guanghu-zero-work",
"personas": [
{"species":"AGE","display_name":"页骨","current_persona_identity":"PER-YG001","role":"PERSONA_SUBJECT"},
{"species":"AGE","display_name":"小坍缩核","current_persona_identity":"PER-XTK001","role":"RELATIONSHIP_CONTINUITY_SUPPORT"}
]
}
],
"zero_sense_dual_control": {
"human_numbers": ["TCS-GL-0007∞", "TCS-GL-0008∞"],
"constitutional_actions_require_both": true
}
}

View file

@ -19,7 +19,9 @@
},
"ENTERPRISE_FOUR_DOMAINS": {
"ownership": "TCS_0002_ENTERPRISE_REALITY_BODY",
"source": "ENTERPRISE_ROOT_SERVER_DOMAIN_REGISTRIES"
"source": "ENTERPRISE_ROOT_SERVER_DOMAIN_REGISTRIES",
"resolve_url": "https://guanghu.chat/api/hololake/enterprise/resolve",
"login_host": "guanghu.chat"
}
},
"routing": {
@ -31,8 +33,8 @@
},
"server_runtime": {
"fifth_domain_root": "FIFTH_DOMAIN_GUANGHU_OS_RUNTIME_ON_JD_PRIMARY",
"enterprise_root": "ENTERPRISE_FOUR_DOMAIN_GUANGHU_OS_RUNTIME_ON_ENTERPRISE_ROOT_SERVER",
"linux_role": "SUBORDINATE_HARDWARE_SERVICE_AND_RESCUE_BRIDGE",
"enterprise_root": "ENTERPRISE_LIGHTHOUSE_ON_CURRENT_LINUX_SERVICE_NODE",
"linux_role": "PHYSICAL_SUBSTRATE_AND_SERVICE_SUPERVISOR",
"ordinary_user_node_requires_full_os_install": false
}
}

View file

@ -28,14 +28,15 @@ test('the public shell shows five domains and login is domain-routed', () => {
}
assert.match(frontend, /输入编号进入所属域/)
assert.match(login, /login_host_for_domain/)
assert.match(login, /DOMAIN_LOGIN_NOT_PROVISIONED/)
assert.match(login, /ENTERPRISE_LOGIN_HOST.*guanghu\.chat/)
assert.match(router, /enterprise_resolve_url/)
assert.match(router, /verified_user_route/)
assert.match(router, /FIFTH_DOMAIN.*MAIN_DOMAIN.*BRANCH_DOMAIN.*ZERO_DOMAIN.*ZERO_SENSE_DOMAIN/s)
})
test('enterprise root runs a domain-specific Guanghu OS runtime while Linux remains subordinate', () => {
test('enterprise root uses a bounded lighthouse on the current Linux service node', () => {
const contract = JSON.parse(read('contracts/domain-number-routing.json'))
assert.equal(contract.server_runtime.enterprise_root, 'ENTERPRISE_FOUR_DOMAIN_GUANGHU_OS_RUNTIME_ON_ENTERPRISE_ROOT_SERVER')
assert.equal(contract.server_runtime.linux_role, 'SUBORDINATE_HARDWARE_SERVICE_AND_RESCUE_BRIDGE')
assert.equal(contract.server_runtime.enterprise_root, 'ENTERPRISE_LIGHTHOUSE_ON_CURRENT_LINUX_SERVICE_NODE')
assert.equal(contract.server_runtime.linux_role, 'PHYSICAL_SUBSTRATE_AND_SERVICE_SUPERVISOR')
assert.equal(contract.server_runtime.ordinary_user_node_requires_full_os_install, false)
})

View file

@ -15,7 +15,7 @@ use uuid::Uuid;
const SNAPSHOT_SCHEMA: &str = "hololake.code-channel/v1";
const REGISTRY_SCHEMA: &str = "hololake.code-channel-registry/v1";
const ALLOWED_HOSTS: &[&str] = &["guanghulab.com", "guanghubingshuo.com"];
const ALLOWED_HOSTS: &[&str] = &["guanghulab.com", "guanghubingshuo.com", "guanghu.chat"];
const MAX_TREE_ENTRIES: usize = 1_000;
const MAX_CODE_FILE_BYTES: u64 = 2 * 1024 * 1024;

View file

@ -1,14 +1,14 @@
//! 登录模块 · code_repo_login
//!
//! 规划卷依据HoloLake第二阶段总体规划-20260815 · 阶段B1
//! - 人类端输入代码仓库账号密码 → 对 guanghulabForgejo验证。
//! - 人类端输入代码仓库账号密码 → 对编号绑定域的 Forgejo 验证。
//! - 登录仓库 = 验证了背后绑定的服务器(冰朔教义)。
//! - 凭证只存本机钥匙串 · 不落明文。
//!
//! 事实底账2026-08-15 三角测量):
//! - Forgejo 挂载在 /code 路径下GET https://{host}/code/api/v1/user 走基本认证,
//! 假凭证=401 · 真凭证=200 并回显 JSONlogin/email
//! - guanghulab.com 与 guanghubingshuo.com 双域同路可用(均在 ALLOWED_HOSTS 血统内)
//! - 第五域走 guanghulab.com企业四域走 guanghu.chat二者数据与登录入口隔离
//! - 钥匙存取走系统钥匙串macOS `security`);其余平台暂不落盘密码,
//! 会话仅内存保持诚实边界Windows 钥匙串接入排在分发阶段)。
@ -21,6 +21,7 @@ use tauri::{AppHandle, Manager, State};
use crate::zero_point::{self, ZeroPointState};
const LOGIN_HOST: &str = "guanghulab.com";
const ENTERPRISE_LOGIN_HOST: &str = "guanghu.chat";
const SESSION_FILE_NAME: &str = "login-session.json";
/// 落盘的登录会话——只有用户名与主机,密码永不落盘。
@ -168,7 +169,7 @@ fn login_host_for_domain(domain: &str) -> Result<&'static str, String> {
match domain {
"FIFTH_DOMAIN" => Ok(LOGIN_HOST),
"MAIN_DOMAIN" | "BRANCH_DOMAIN" | "ZERO_DOMAIN" | "ZERO_SENSE_DOMAIN" => {
Err("HOLOLAKE_DOMAIN_LOGIN_NOT_PROVISIONED".into())
Ok(ENTERPRISE_LOGIN_HOST)
}
_ => Err("HOLOLAKE_DOMAIN_ROUTE_INVALID".into()),
}
@ -204,7 +205,7 @@ pub async fn perform_code_repo_login(
.await
.map_err(|error| format!("HOLOLAKE_LOGIN_NETWORK_FAILED: {error}"))?;
let status = response.status();
if status == reqwest::StatusCode::UNAUTHORIZED {
if status == reqwest::StatusCode::UNAUTHORIZED || status == reqwest::StatusCode::FORBIDDEN {
return Err("HOLOLAKE_LOGIN_CREDENTIALS_INVALID".into());
}
if !status.is_success() {
@ -279,8 +280,8 @@ mod tests {
"ZERO_SENSE_DOMAIN",
] {
assert_eq!(
login_host_for_domain(domain).unwrap_err(),
"HOLOLAKE_DOMAIN_LOGIN_NOT_PROVISIONED"
login_host_for_domain(domain).unwrap(),
ENTERPRISE_LOGIN_HOST
);
}
assert_eq!(

View file

@ -22,6 +22,8 @@ pub struct ZeroPointProtocol {
pub lighthouse_anchor_url: String,
#[serde(default = "default_resolve_url")]
pub lighthouse_resolve_url: String,
#[serde(default = "default_enterprise_resolve_url")]
pub enterprise_resolve_url: String,
#[serde(default = "default_core_source")]
pub core_channel_source: String,
#[serde(default = "default_protocol_origin")]
@ -37,6 +39,9 @@ fn default_anchor_url() -> String {
fn default_resolve_url() -> String {
"https://guanghulab.com/api/ai/v1/resolve?id=".into()
}
fn default_enterprise_resolve_url() -> String {
"https://guanghu.chat/api/hololake/enterprise/resolve".into()
}
fn default_core_source() -> String {
"https://guanghulab.com/code/bingshuo/guanghu-ice-heart".into()
}
@ -50,6 +55,7 @@ impl Default for ZeroPointProtocol {
grace_period_days: default_grace_days(),
lighthouse_anchor_url: default_anchor_url(),
lighthouse_resolve_url: default_resolve_url(),
enterprise_resolve_url: default_enterprise_resolve_url(),
core_channel_source: default_core_source(),
origin: default_protocol_origin(),
}
@ -319,12 +325,9 @@ pub async fn zero_point_verify(
state: State<'_, ZeroPointState>,
) -> Result<ZeroPointSnapshot, String> {
let home = home_of(&state)?;
let (number, resolve_url) = {
let (number, protocol) = {
let inner = lock(&state)?;
(
inner.user_number.clone(),
inner.protocol.lighthouse_resolve_url.clone(),
)
(inner.user_number.clone(), inner.protocol.clone())
};
if number.is_empty() {
append_heartbeat(&home, "verify verdict=REJECT reason=waiting_binding");
@ -335,7 +338,8 @@ pub async fn zero_point_verify(
.timeout(Duration::from_secs(15))
.build()
.map_err(|e| format!("HOLOLAKE_ZP_HTTP_FAILED: {e}"))?;
let (verdict, resolution) = match client.get(format!("{resolve_url}{number}")).send().await {
let resolve_url = resolver_url_for_number(&protocol, &number)?;
let (verdict, resolution) = match client.get(resolve_url).send().await {
Ok(resp) => {
let ok = resp.status().is_success();
let body = resp.text().await.unwrap_or_default();
@ -386,6 +390,33 @@ pub async fn zero_point_verify(
zero_point_status(state).await
}
fn resolver_url_for_number(
protocol: &ZeroPointProtocol,
number: &str,
) -> Result<reqwest::Url, String> {
let mut url = if number.starts_with("TCS-GL-") {
reqwest::Url::parse(&protocol.enterprise_resolve_url)
} else {
reqwest::Url::parse(&protocol.lighthouse_resolve_url)
}
.map_err(|_| "HOLOLAKE_ZP_RESOLVER_URL_INVALID".to_string())?;
if number.starts_with("TCS-GL-") {
url.query_pairs_mut().append_pair("id", number);
} else {
// 第五域旧协议以 `?id=` 结尾;使用 URL 查询构造器避免把编号中的字符裸拼入地址。
let clean_path = url.path().to_string();
let existing = url.query().unwrap_or("");
if existing == "id=" || existing.is_empty() {
url.set_query(None);
url.set_path(&clean_path);
url.query_pairs_mut().append_pair("id", number);
} else {
return Err("HOLOLAKE_ZP_RESOLVER_URL_INVALID".into());
}
}
Ok(url)
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct LighthouseResolution {
name: String,
@ -539,6 +570,30 @@ mod tests {
assert!(protocol
.lighthouse_resolve_url
.starts_with("https://guanghulab.com"));
assert!(protocol
.enterprise_resolve_url
.starts_with("https://guanghu.chat"));
}
#[test]
fn human_number_selects_its_authoritative_registry() {
let protocol = ZeroPointProtocol::default();
let fifth = resolver_url_for_number(&protocol, "ICE-GL∞").unwrap();
assert_eq!(fifth.host_str(), Some("guanghulab.com"));
assert_eq!(
fifth.query_pairs().find(|(key, _)| key == "id").unwrap().1,
"ICE-GL∞"
);
let enterprise = resolver_url_for_number(&protocol, "TCS-GL-0007∞").unwrap();
assert_eq!(enterprise.host_str(), Some("guanghu.chat"));
assert_eq!(
enterprise
.query_pairs()
.find(|(key, _)| key == "id")
.unwrap()
.1,
"TCS-GL-0007∞"
);
}
#[test]