diff --git a/.code-map b/.code-map index 4add4d6..2fb17f1 100644 --- a/.code-map +++ b/.code-map @@ -78,6 +78,8 @@ GLSV-0001=gls/GLSV-0001-SECURITY-VERIFICATION-ARCHITECTURE.hdlp GLS-NODE-0001=gls/GLS-NODE-0001-LIGHTHOUSE-NODE-ARCHITECTURE.hdlp LIGHTHOUSE-NODE-ARCHITECTURE=gls/GLS-NODE-0001-LIGHTHOUSE-NODE-ARCHITECTURE.hdlp NODE-LIGHTHOUSE-WORKORDER=tcs-core/workorders/GLW-WO-20260714-NODE-LIGHTHOUSE-001.hdlp +HLP-LIGHTHOUSE-001=server-tools/enterprise-lighthouse/README.md +ENTERPRISE-LIGHTHOUSE-SERVICE=server-tools/enterprise-lighthouse/lighthouse.py LL-NODE-LIGHTHOUSE=eternal-lake-heart/heartbeat-core/LL-NODE-LIGHTHOUSE-ANCHOR-20260714.hdlp LL-SG-BRAIN-STATUS=eternal-lake-heart/heartbeat-core/LL-SG-BRAIN-STATUS-20260714.hdlp SG-BRAIN-STATUS=eternal-lake-heart/heartbeat-core/LL-SG-BRAIN-STATUS-20260714.hdlp diff --git a/BROADCAST-TOWER.hdlp b/BROADCAST-TOWER.hdlp index f4d0d45..07e3565 100644 --- a/BROADCAST-TOWER.hdlp +++ b/BROADCAST-TOWER.hdlp @@ -101,6 +101,7 @@ Stage 3 · RELEASE · 正式部署 | GLSV-0001 | 光湖系统安全验证架构说明 | ICE-GL∞ × 当前协作实例 | REGISTERED_REFERENCE_ARCHITECTURE | GLS 图书域 · 2026-07-14 | | GLS-NODE-0001 | 光湖总灯塔与分布式服务器节点接入架构 | ICE-GL∞ × ICE-GL-ZY001 | REGISTERED_ARCHITECTURE · IMPLEMENTATION_PENDING | 五域 / 域入口 / 个人服务器 / 人格体 / 当前实例 · 2026-07-14 | | LL-SG-BRAIN-STATUS-20260714 | 新加坡大脑服务器 · 铸渊现实运维状态锚 | ICE-GL∞ × ICE-GL-ZY001 | REGISTERED_OPERATIONAL_STATUS | BS-SG-001 已在线;新总灯塔节点接入待实施 · 2026-07-14 | +| HLP-LIGHTHOUSE-001 | 企业灯塔基础服务 · 节点登记 / 预检 / 审计 | ICE-GL∞ × 当前协作实例 | REGISTERED_TESTED_SOURCE · DEPLOY_PENDING | 仅固定动作预检;拒绝裸命令;2026-07-14 | | HLP-OPS-0001 | HoloLake Era 分布式服务器控制与自然语言运维 · 第五域桥接规范 | ICE-GL∞ × ICE-GL-ZY001 | REGISTERED | Notion GLS 架构 v1.0 · 2026-07-12 | | GLW-RD-002 | HLDP 研发语言与服务器转译链 · HoloLake 产品研发硬准入 | ICE-GL∞ × ICE-GL-ZL-001 | REGISTERED(规范)· RUNTIME_NOT_DEPLOYED | GLW-RD-002 · 2026-07-14 | | SYS-GLW-POS-0001 / GLW-OS-000 / GLW-OS-001 | 冰朔通感语言核操作系统 · Tolaria / 光湖 App | ICE-GL∞ × ICE-GL-ZY001 | REGISTERED + DEVELOPMENT_ACTIVE | HLP-BRD-0002 · GLW-OS-001 · 2026-07-13 | diff --git a/gls/GLS-NODE-0001-LIGHTHOUSE-NODE-ARCHITECTURE.hdlp b/gls/GLS-NODE-0001-LIGHTHOUSE-NODE-ARCHITECTURE.hdlp index 6342c8c..7fedeb4 100644 --- a/gls/GLS-NODE-0001-LIGHTHOUSE-NODE-ARCHITECTURE.hdlp +++ b/gls/GLS-NODE-0001-LIGHTHOUSE-NODE-ARCHITECTURE.hdlp @@ -172,6 +172,7 @@ receipt_path: "" | GLSV 架构说明 | `REGISTERED_REFERENCE_ARCHITECTURE` | 当前已有 Gatekeeper v3.2 兼容实现;企业灯塔迁移未完成 | | 广州企业灯塔公众站 | `ONLINE` | `guanghu.chat` 现有公众主页可访问 | | 广州企业灯塔服务 | `REPAIR_REQUIRED` | 2026-07-14 只读检查发现服务账户缺少运行目录访问权限,处于重启循环;不得在此基础上直接叠加新功能 | +| 企业灯塔基础服务源码 | `REGISTERED_TESTED_SOURCE · DEPLOY_PENDING` | `HLP-LIGHTHOUSE-001` 已实现节点接入申请、固定动作预检与审计;不接受裸命令 | | 节点注册库 | `NOT_DEPLOYED` | 尚未有此架构下的正式 `domain / node / human / persona / instance` 关系库 | | GLSV 授权中心(企业灯塔) | `NOT_DEPLOYED` | 需在修复灯塔生命周期后部署 | | GLSV 节点连接器 | `NOT_DEPLOYED` | 需按域入口、再按个人服务器逐台接入 | diff --git a/server-tools/enterprise-lighthouse/README.md b/server-tools/enterprise-lighthouse/README.md new file mode 100644 index 0000000..0464595 --- /dev/null +++ b/server-tools/enterprise-lighthouse/README.md @@ -0,0 +1,7 @@ +# 企业灯塔基础服务 + +这是总灯塔的第一阶段:五域登记、节点接入申请、固定动作预检和审计。它**不是**远程 Shell,拒绝 `cmd`、`shell`、`command` 字段,也不执行操作。 + +部署后只监听本机 `127.0.0.1:8031`。公众网站与后续 GLSV 页面通过 Nginx 以单独的受控路由接入;在邮件确认与节点连接器完成前,不开放节点激活或执行。 + +固定动作只有:`health_check`、`backup`、`deploy_release`、`restart_service`、`rollback`。部署、重启和回滚必须在预检中具备备份引用与回滚计划;预检通过也只是“可申请人类授权”,不会执行。 diff --git a/server-tools/enterprise-lighthouse/__pycache__/lighthouse.cpython-314.pyc b/server-tools/enterprise-lighthouse/__pycache__/lighthouse.cpython-314.pyc new file mode 100644 index 0000000..315b883 Binary files /dev/null and b/server-tools/enterprise-lighthouse/__pycache__/lighthouse.cpython-314.pyc differ diff --git a/server-tools/enterprise-lighthouse/__pycache__/test_lighthouse.cpython-314.pyc b/server-tools/enterprise-lighthouse/__pycache__/test_lighthouse.cpython-314.pyc new file mode 100644 index 0000000..77ab560 Binary files /dev/null and b/server-tools/enterprise-lighthouse/__pycache__/test_lighthouse.cpython-314.pyc differ diff --git a/server-tools/enterprise-lighthouse/guanghu-enterprise-lighthouse.service b/server-tools/enterprise-lighthouse/guanghu-enterprise-lighthouse.service new file mode 100644 index 0000000..b3bbe1a --- /dev/null +++ b/server-tools/enterprise-lighthouse/guanghu-enterprise-lighthouse.service @@ -0,0 +1,21 @@ +[Unit] +Description=Guanghu Enterprise Lighthouse Registry and Preflight +After=network.target + +[Service] +Type=simple +User=lighthouse +Group=lighthouse +WorkingDirectory=/opt/guanghu-enterprise-lighthouse +EnvironmentFile=/etc/guanghu-enterprise-lighthouse.env +ExecStart=/usr/bin/python3 /opt/guanghu-enterprise-lighthouse/lighthouse.py +Restart=on-failure +RestartSec=3 +NoNewPrivileges=yes +PrivateTmp=yes +ProtectSystem=strict +ProtectHome=yes +ReadWritePaths=/var/lib/guanghu-enterprise-lighthouse + +[Install] +WantedBy=multi-user.target diff --git a/server-tools/enterprise-lighthouse/lighthouse.py b/server-tools/enterprise-lighthouse/lighthouse.py new file mode 100644 index 0000000..2393242 --- /dev/null +++ b/server-tools/enterprise-lighthouse/lighthouse.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 +"""Enterprise Lighthouse foundation: registry, preflight, and audit only. + +This service is deliberately not a remote shell. It accepts node admission +requests and validates fixed operation plans; execution remains with a future +GLSV node connector after human authorization. +""" +import hmac +import json +import os +import sqlite3 +import time +import uuid +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from ipaddress import ip_address + +DB = os.environ.get("LIGHTHOUSE_DB", "/var/lib/guanghu-enterprise-lighthouse/lighthouse.db") +TOKEN = os.environ.get("LIGHTHOUSE_ADMIN_TOKEN", "") +HOST = os.environ.get("LIGHTHOUSE_BIND", "127.0.0.1") +PORT = int(os.environ.get("LIGHTHOUSE_PORT", "8031")) +FIXED_ACTIONS = {"health_check", "backup", "deploy_release", "restart_service", "rollback"} +DOMAINS = { + "DOMAIN-ZS": "零感域", "DOMAIN-MAIN": "光湖主域", "DOMAIN-SUB": "光湖分域", + "DOMAIN-ZERO": "光湖零域", "DOMAIN-FIFTH": "第五域", +} + + +def now(): + return int(time.time()) + + +def connection(): + os.makedirs(os.path.dirname(DB), exist_ok=True) + db = sqlite3.connect(DB) + db.row_factory = sqlite3.Row + db.executescript(""" + CREATE TABLE IF NOT EXISTS domains ( + id TEXT PRIMARY KEY, name TEXT NOT NULL, state TEXT NOT NULL, created_at INTEGER NOT NULL + ); + CREATE TABLE IF NOT EXISTS intakes ( + id TEXT PRIMARY KEY, created_at INTEGER NOT NULL, state TEXT NOT NULL, + human_name TEXT NOT NULL, email TEXT NOT NULL, server_ip TEXT NOT NULL, + domain_id TEXT NOT NULL, persona_ids TEXT NOT NULL, repository_urls TEXT NOT NULL, + hosting_mode TEXT NOT NULL, notes TEXT NOT NULL + ); + CREATE TABLE IF NOT EXISTS nodes ( + id TEXT PRIMARY KEY, domain_id TEXT NOT NULL, intake_id TEXT, state TEXT NOT NULL, + display_name TEXT NOT NULL, server_ip TEXT NOT NULL, allowed_actions TEXT NOT NULL, + public_key_fingerprint TEXT, last_heartbeat INTEGER, created_at INTEGER NOT NULL + ); + CREATE TABLE IF NOT EXISTS audit ( + id TEXT PRIMARY KEY, created_at INTEGER NOT NULL, kind TEXT NOT NULL, payload TEXT NOT NULL + ); + """) + for domain_id, name in DOMAINS.items(): + db.execute("INSERT OR IGNORE INTO domains VALUES (?, ?, 'PENDING_ENTRY_NODE', ?)", (domain_id, name, now())) + db.commit() + return db + + +def audit(db, kind, payload): + db.execute("INSERT INTO audit VALUES (?, ?, ?, ?)", (str(uuid.uuid4()), now(), kind, json.dumps(payload, ensure_ascii=False))) + db.commit() + + +def parse_json(handler): + length = int(handler.headers.get("Content-Length", "0")) + if not 0 < length <= 50_000: + raise ValueError("request body must be between 1 and 50000 bytes") + return json.loads(handler.rfile.read(length).decode("utf-8")) + + +def valid_email(value): + return isinstance(value, str) and len(value) <= 254 and value.count("@") == 1 + + +def require_admin(handler): + received = handler.headers.get("X-Lighthouse-Admin-Token", "") + return bool(TOKEN) and hmac.compare_digest(received, TOKEN) + + +class Handler(BaseHTTPRequestHandler): + server_version = "GuanghuEnterpriseLighthouse/1.0" + + def log_message(self, fmt, *args): + print("[lighthouse] " + fmt % args) + + def respond(self, status, body): + encoded = json.dumps(body, ensure_ascii=False).encode("utf-8") + 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.end_headers() + self.wfile.write(encoded) + + def do_GET(self): + db = connection() + try: + if self.path == "/health": + return self.respond(200, {"ok": True, "service": "guanghu-enterprise-lighthouse", "mode": "registry-and-preflight-only", "execution": "disabled"}) + if self.path == "/v1/status": + counts = {row["state"]: row["count"] for row in db.execute("SELECT state, COUNT(*) AS count FROM nodes GROUP BY state")} + return self.respond(200, {"ok": True, "domains": [dict(row) for row in db.execute("SELECT id,name,state FROM domains ORDER BY id")], "node_counts": counts, "fixed_actions": sorted(FIXED_ACTIONS), "raw_shell": "rejected"}) + if self.path == "/v1/nodes": + return self.respond(200, {"ok": True, "nodes": [dict(row) for row in db.execute("SELECT id,domain_id,state,display_name,allowed_actions,last_heartbeat,created_at FROM nodes ORDER BY created_at DESC")]}) + return self.respond(404, {"ok": False, "error": "not found"}) + finally: + db.close() + + def do_POST(self): + if not require_admin(self): + return self.respond(401, {"ok": False, "error": "admin authorization required"}) + try: + payload = parse_json(self) + except (ValueError, json.JSONDecodeError) as error: + return self.respond(400, {"ok": False, "error": str(error)}) + if "cmd" in payload or "shell" in payload or "command" in payload: + return self.respond(400, {"ok": False, "error": "raw commands are never accepted by the lighthouse"}) + db = connection() + try: + if self.path == "/v1/intakes": + required = ("human_name", "email", "server_ip", "domain_id") + if any(not payload.get(field) for field in required) or payload["domain_id"] not in DOMAINS or not valid_email(payload["email"]): + return self.respond(400, {"ok": False, "error": "human_name, valid email, server_ip, and known domain_id are required"}) + try: + ip_address(payload["server_ip"]) + except ValueError: + return self.respond(400, {"ok": False, "error": "server_ip must be a valid IP address"}) + intake_id = "INTAKE-" + uuid.uuid4().hex[:12].upper() + db.execute("INSERT INTO intakes VALUES (?, ?, 'PENDING_REVIEW', ?, ?, ?, ?, ?, ?, ?, ?)", ( + intake_id, now(), payload["human_name"].strip(), payload["email"].strip(), payload["server_ip"], payload["domain_id"], + json.dumps(payload.get("persona_ids", [])), json.dumps(payload.get("repository_urls", [])), + payload.get("hosting_mode", "own"), payload.get("notes", ""), + )) + audit(db, "intake_created", {"intake_id": intake_id, "domain_id": payload["domain_id"]}) + return self.respond(201, {"ok": True, "intake_id": intake_id, "state": "PENDING_REVIEW", "next": "sovereign approval, human email confirmation, then node connector enrollment"}) + if self.path == "/v1/preflight": + action, node_id = payload.get("action"), payload.get("target_node_id") + if action not in FIXED_ACTIONS or not node_id: + return self.respond(400, {"ok": False, "error": "fixed action and target_node_id are required", "allowed_actions": sorted(FIXED_ACTIONS)}) + node = db.execute("SELECT * FROM nodes WHERE id=?", (node_id,)).fetchone() + if not node: + return self.respond(404, {"ok": False, "decision": "REJECT", "reason": "target node is not registered"}) + allowed = json.loads(node["allowed_actions"]) + reasons = [] + if node["state"] != "ACTIVE": reasons.append("target node is not ACTIVE") + if action not in allowed: reasons.append("action is not in the node allowlist") + if action in {"deploy_release", "restart_service", "rollback"} and not payload.get("rollback_plan"): reasons.append("rollback_plan is required") + if action in {"deploy_release", "restart_service"} and not payload.get("backup_reference"): reasons.append("backup_reference is required") + decision = "ALLOW_FOR_AUTHORIZATION" if not reasons else "REJECT" + result = {"ok": not reasons, "decision": decision, "reasons": reasons, "execution": "not performed; GLSV human authorization still required"} + audit(db, "preflight", {"target_node_id": node_id, "action": action, "decision": decision, "reasons": reasons}) + return self.respond(200, result) + return self.respond(404, {"ok": False, "error": "not found"}) + finally: + db.close() + + +if __name__ == "__main__": + print(f"Enterprise Lighthouse listening on {HOST}:{PORT}") + ThreadingHTTPServer((HOST, PORT), Handler).serve_forever() diff --git a/server-tools/enterprise-lighthouse/test_lighthouse.py b/server-tools/enterprise-lighthouse/test_lighthouse.py new file mode 100644 index 0000000..1bd9b6a --- /dev/null +++ b/server-tools/enterprise-lighthouse/test_lighthouse.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +import json +import os +import subprocess +import sys +import tempfile +import time +import urllib.request +from pathlib import Path + +ROOT = Path(__file__).parent +with tempfile.TemporaryDirectory() as temp: + env = {**os.environ, "LIGHTHOUSE_DB": f"{temp}/lighthouse.db", "LIGHTHOUSE_ADMIN_TOKEN": "test-token", "LIGHTHOUSE_PORT": "48031"} + process = subprocess.Popen([sys.executable, "lighthouse.py"], cwd=ROOT, env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + try: + for _ in range(30): + try: + assert json.load(urllib.request.urlopen("http://127.0.0.1:48031/health", timeout=1))["execution"] == "disabled" + break + except OSError: + time.sleep(.1) + else: raise AssertionError("server did not start") + request = urllib.request.Request("http://127.0.0.1:48031/v1/intakes", data=json.dumps({"human_name":"Test","email":"test@example.invalid","server_ip":"203.0.113.8","domain_id":"DOMAIN-FIFTH"}).encode(), method="POST", headers={"X-Lighthouse-Admin-Token":"test-token"}) + assert json.load(urllib.request.urlopen(request))["state"] == "PENDING_REVIEW" + bad = urllib.request.Request("http://127.0.0.1:48031/v1/intakes", data=b'{"cmd":"rm -rf /"}', method="POST", headers={"X-Lighthouse-Admin-Token":"test-token"}) + try: urllib.request.urlopen(bad) + except urllib.error.HTTPError as error: assert error.code == 400 + else: raise AssertionError("raw command was not rejected") + finally: + process.terminate(); process.wait(timeout=5) +print("enterprise lighthouse tests passed") diff --git a/tcs-core/workorders/GLW-WO-20260714-NODE-LIGHTHOUSE-001.hdlp b/tcs-core/workorders/GLW-WO-20260714-NODE-LIGHTHOUSE-001.hdlp index 58df988..bff61d7 100644 --- a/tcs-core/workorders/GLW-WO-20260714-NODE-LIGHTHOUSE-001.hdlp +++ b/tcs-core/workorders/GLW-WO-20260714-NODE-LIGHTHOUSE-001.hdlp @@ -20,6 +20,7 @@ - [x] 登记桌面接入表为 `PENDING_REVIEW` 的输入窗口 - [x] 登记 GLSV 中心 / 节点连接器双层模型 - [x] 对广州企业灯塔完成只读基础盘点 +- [x] 完成企业灯塔基础服务源码与本地测试:登记申请、固定动作预检、审计;拒绝裸命令 ## 当前阻塞 @@ -31,7 +32,7 @@ ## 后续固定顺序 1. 修复企业灯塔服务生命周期并保存当前配置备份。 -2. 将灯塔源码与部署单元纳入正式仓库;建立测试与回滚点。 +2. 部署已入第五域的 `HLP-LIGHTHOUSE-001`,建立测试与回滚点。 3. 部署 GLSV Authorization Center(先只读 / 申请状态)。 4. 部署一个 GLSV Node Connector 测试节点;验证邮箱确认、心跳和回执。 5. 登记每个域入口节点,再登记个人服务器节点。