From 8be10cc3dabf59c872e9fb7793d5fc0bd14fb29c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=B0=E6=9C=94?= <565183519@qq.com> Date: Tue, 14 Jul 2026 19:06:11 +0800 Subject: [PATCH] feat: register connected routing nodes without execution --- .../enterprise-lighthouse/lighthouse.py | 27 +++++++++++++++++++ .../enterprise-lighthouse/test_lighthouse.py | 4 +++ 2 files changed, 31 insertions(+) diff --git a/server-tools/enterprise-lighthouse/lighthouse.py b/server-tools/enterprise-lighthouse/lighthouse.py index 2393242..0095d8c 100644 --- a/server-tools/enterprise-lighthouse/lighthouse.py +++ b/server-tools/enterprise-lighthouse/lighthouse.py @@ -135,6 +135,33 @@ class Handler(BaseHTTPRequestHandler): )) 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/nodes/bootstrap": + """Register a manually verified routing node without enabling execution. + + This exists for the controlled migration of an already verified + domain entry. It deliberately cannot make a node ACTIVE: the + GLSV connector, server-local key enrollment, and human email + confirmation remain required before any operation can proceed. + """ + required = ("id", "domain_id", "display_name", "server_ip") + if any(not isinstance(payload.get(field), str) or not payload[field].strip() for field in required): + return self.respond(400, {"ok": False, "error": "id, domain_id, display_name, and server_ip are required"}) + if payload["domain_id"] not in DOMAINS: + return self.respond(400, {"ok": False, "error": "domain_id is not registered"}) + try: + ip_address(payload["server_ip"]) + except ValueError: + return self.respond(400, {"ok": False, "error": "server_ip must be a valid IP address"}) + node_id = payload["id"].strip() + if db.execute("SELECT 1 FROM nodes WHERE id=?", (node_id,)).fetchone(): + return self.respond(409, {"ok": False, "error": "node id is already registered"}) + db.execute("INSERT INTO nodes VALUES (?, ?, NULL, 'CONNECTED_PENDING_CONNECTOR', ?, ?, ?, NULL, ?, ?)", ( + node_id, payload["domain_id"], payload["display_name"].strip(), payload["server_ip"], + json.dumps(["health_check"]), now(), now(), + )) + db.execute("UPDATE domains SET state='ENTRY_NODE_CONNECTED' WHERE id=?", (payload["domain_id"],)) + audit(db, "node_bootstrap_connected", {"node_id": node_id, "domain_id": payload["domain_id"], "mode": "manual_verified_routing_only"}) + return self.respond(201, {"ok": True, "node_id": node_id, "state": "CONNECTED_PENDING_CONNECTOR", "execution": "disabled until GLSV connector enrollment and human authorization"}) 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: diff --git a/server-tools/enterprise-lighthouse/test_lighthouse.py b/server-tools/enterprise-lighthouse/test_lighthouse.py index 1bd9b6a..2d855df 100644 --- a/server-tools/enterprise-lighthouse/test_lighthouse.py +++ b/server-tools/enterprise-lighthouse/test_lighthouse.py @@ -22,6 +22,10 @@ with tempfile.TemporaryDirectory() as temp: 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" + bootstrap = urllib.request.Request("http://127.0.0.1:48031/v1/nodes/bootstrap", data=json.dumps({"id":"NODE-TEST-001","domain_id":"DOMAIN-FIFTH","display_name":"Test entry","server_ip":"203.0.113.8"}).encode(), method="POST", headers={"X-Lighthouse-Admin-Token":"test-token"}) + assert json.load(urllib.request.urlopen(bootstrap))["state"] == "CONNECTED_PENDING_CONNECTOR" + preflight = urllib.request.Request("http://127.0.0.1:48031/v1/preflight", data=json.dumps({"action":"health_check","target_node_id":"NODE-TEST-001"}).encode(), method="POST", headers={"X-Lighthouse-Admin-Token":"test-token"}) + assert json.load(urllib.request.urlopen(preflight))["decision"] == "REJECT" 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