diff --git a/.code-map b/.code-map index 66c1d09..1a8509b 100644 --- a/.code-map +++ b/.code-map @@ -12,6 +12,7 @@ # R-001 第五域 (现行主) REPO-001=https://guanghulab.com/fifth-domain/bingshuo/fifth-domain.git +REPO-012=https://guanghulab.com/code/bingshuo/guanghu-ice-heart.git REPO-001-WEB=https://guanghulab.com/fifth-domain/bingshuo/fifth-domain REPO-001-LEGACY-SG=https://guanghubingshuo.com/code/bingshuo/fifth-domain.git REPO-001-LOCAL=~/Documents/fifth-domain/ diff --git a/deployment/navigation-maps/JD-FD-PRIMARY.json b/deployment/navigation-maps/JD-FD-PRIMARY.json index 636f291..5abc98f 100644 --- a/deployment/navigation-maps/JD-FD-PRIMARY.json +++ b/deployment/navigation-maps/JD-FD-PRIMARY.json @@ -10,7 +10,7 @@ { "code": "JD-SEN-01", "name": "状态变化哨兵", "bind": "timer:15m", "owner": "systemd" }, { "code": "JD-ROUTE-01", "name": "已登记下游节点 SSH 路由", "bind": "private-keys", "owner": "root", "registered_targets": ["AW-GZ-001"] }, { "code": "JD-ACT-01", "name": "固定动作执行桥", "bind": "unix-socket", "owner": "root", "actions": ["inspect-services", "health-check", "inspect-code-channel-owner-auth"] }, - { "code": "JD-OWNER-ACCESS-01", "name": "冰朔登录入口恢复执行器", "bind": "unix-socket", "owner": "root", "actions": ["restore-owner-password-login"] }, + { "code": "JD-OWNER-ACCESS-01", "name": "冰朔受限登录恢复执行器", "bind": "unix-socket", "owner": "root", "actions": ["restore-owner-password-login", "restore-code-channel-owner-login"], "action_meanings": { "restore-owner-password-login": "仅恢复京东服务器 SSH 密码认证开关,不修改代码频道账号", "restore-code-channel-owner-login": "仅将新代码频道 bingshuo 凭证恢复为旧第五域本机凭证,不读取明文密码" } }, { "code": "JD-ARCH-PROVISION-01", "name": "已批准新架构首次安装器", "bind": "unix-socket", "owner": "root", "actions": ["provision-approved-architecture"], "state": "ACTIVE" }, { "code": "JD-LAN-01", "name": "光湖·来光者导航只读召回服务", "bind": "loopback:3924", "owner": "systemd", "architecture": "GLS-0231", "state": "DEPLOYED_AND_VERIFIED", "source_commit": "f4a4b5996c83b55d99172f3196f4a0d77ed5f3e9" }, { @@ -18,8 +18,8 @@ "name": "第五域光湖代码频道隔离候选启动壳", "bind": "loopback:3340,3341", "owner": "guanghu", - "state": "BOOTSTRAP_DEPLOYED · CANDIDATE_NOT_READY", - "source_commit": "b4d9cf7635ffccde4f49ad1949a215bb9d22c5be", + "state": "ACTIVE_READY", + "source_commit": "8c0642d4f208d1d0027fa1956de372ce61f191ef", "receipt": "deployment/receipts/HLCC-JD-RUNTIME-CHECKPOINT-20260723.json" } ], diff --git a/deployment/receipts/HLCC-JD-OWNER-LOGIN-RECOVERY-20260726.json b/deployment/receipts/HLCC-JD-OWNER-LOGIN-RECOVERY-20260726.json new file mode 100644 index 0000000..07c40bc --- /dev/null +++ b/deployment/receipts/HLCC-JD-OWNER-LOGIN-RECOVERY-20260726.json @@ -0,0 +1,44 @@ +{ + "schema": "guanghu.deployment-receipt/v1", + "receipt_id": "HLCC-JD-OWNER-LOGIN-RECOVERY-20260726", + "timestamp": "2026-07-26T14:54:05+08:00", + "target": "JD-FD-PRIMARY", + "channel": "bingshuo/guanghu-ice-heart", + "workorder": "6d62659c-eef7-49de-a397-4ddc98d66a37", + "authorization_scope": "server-ops", + "result": "RECOVERED_AND_VERIFIED", + "root_causes": [ + "The existing restore-owner-password-login action changed the JD SSH PasswordAuthentication setting; it did not recover the HoloLake Code Channel application account.", + "The new channel owner password digest no longer matched the legacy Fifth Domain owner digest even though the architecture requires one human credential.", + "The SQLite WAL contained newer repository mapping state than the main database file, so raw single-file backup and rollback could expose a stale fifth-domain row." + ], + "recovery": { + "owner": "bingshuo", + "credential_source": "JD-local legacy Fifth Domain database", + "plaintext_password_read": false, + "ssh_policy_changed": false, + "singapore_route_used": false, + "legacy_repository_modified": false, + "consistent_backup": "/var/lib/guanghu/personas/guanghu/hlcc-v16.0.1/data/backups/channel-db-consistent-20260726T065405Z.db", + "repository_mapping": "bingshuo/guanghu-ice-heart", + "repository_head": "8c0642d4f208d1d0027fa1956de372ce61f191ef" + }, + "verification": { + "owner_credential_matches_legacy": true, + "owner_active": true, + "owner_admin": true, + "owner_login_prohibited": false, + "channel_service": "ready", + "channel_health": "pass", + "public_repository": true, + "repository_empty": false, + "default_branch": "main", + "public_git_head_matches": true + }, + "follow_up": [ + "Deploy the distinct restore-code-channel-owner-login registered action after review.", + "Publish REPO-012 in the AI repository route map.", + "Keep restore-owner-password-login documented as SSH-only.", + "Use SQLite backup API plus WAL checkpoint for future channel database recovery." + ] +} diff --git a/routing/repository-route-map.json b/routing/repository-route-map.json index 56f1ad8..46e2cb9 100644 --- a/routing/repository-route-map.json +++ b/routing/repository-route-map.json @@ -1,7 +1,7 @@ { "schema": "guanghu.repository-route-map/v1", "map_id": "FD-REPO-MAP-001", - "version": "2026-07-20.2", + "version": "2026-07-26.1", "published_by": "REPO-001", "canonical_api": "https://guanghulab.com/api/ai/v1/repositories", "default_repository": "REPO-001", @@ -189,6 +189,23 @@ "clone_url": "https://guanghulab.com/fifth-domain/bingshuo/guideng.git" }, "keywords": ["归灯", "GLS-LA-20260720-003", "常驻人格体", "湖心频道", "心跳"] + }, + { + "code": "REPO-012", + "slug": "guanghu-ice-heart", + "name_zh": "光湖代码频道 · 冰朔第五域", + "role": "光湖代码频道当前个人子频道、新提交事实源与公开 AI 读取入口", + "state": "DOMESTIC_CODE_CHANNEL_PRIMARY", + "primary": { + "region": "CN", + "url": "https://guanghulab.com/code/bingshuo/guanghu-ice-heart", + "clone_url": "https://guanghulab.com/code/bingshuo/guanghu-ice-heart.git" + }, + "history": { + "legacy_fifth_domain": "https://guanghulab.com/fifth-domain/bingshuo/fifth-domain", + "original_github": "https://github.com/qinfendebingshuo/guanghulab" + }, + "keywords": ["光湖代码频道", "guanghu-ice-heart", "冰朔第五域", "HLCC-FD-ICE", "当前提交", "公开读取"] } ] } diff --git a/server-tools/ai-discovery-gateway/route-closure.test.js b/server-tools/ai-discovery-gateway/route-closure.test.js index 59557e7..acd268c 100644 --- a/server-tools/ai-discovery-gateway/route-closure.test.js +++ b/server-tools/ai-discovery-gateway/route-closure.test.js @@ -23,12 +23,23 @@ test("canonical AI and persona entry files route to the domestic map", () => { test("code map resolves REPO-001 to domestic and labels the Singapore route legacy", () => { const codeMap = read(".code-map"); assert.match(codeMap, /^REPO-001=https:\/\/guanghulab\.com\/fifth-domain\/bingshuo\/fifth-domain\.git$/m); + assert.match(codeMap, /^REPO-012=https:\/\/guanghulab\.com\/code\/bingshuo\/guanghu-ice-heart\.git$/m); assert.match(codeMap, /^REPO-001-LEGACY-SG=/m); assert.match(codeMap, /^FD-REPO-MAP-001=routing\/repository-route-map\.json$/m); assert.match(codeMap, /^FD-NODE-MAP-001=routing\/server-node-map\.json$/m); assert.match(codeMap, /^ZY-OPS-LOOP-001=.*zhuyuan-persona-system\//m); }); +test("repository map exposes the current code channel as a public domestic route", () => { + const map = JSON.parse(read("routing/repository-route-map.json")); + const channel = map.repositories.find(item => item.code === "REPO-012"); + assert.equal(channel.slug, "guanghu-ice-heart"); + assert.equal(channel.state, "DOMESTIC_CODE_CHANNEL_PRIMARY"); + assert.equal(channel.primary.url, "https://guanghulab.com/code/bingshuo/guanghu-ice-heart"); + assert.equal(channel.primary.clone_url, "https://guanghulab.com/code/bingshuo/guanghu-ice-heart.git"); + assert.match(JSON.stringify(channel.keywords), /公开读取/); +}); + test("Zhuyuan current chain resolves to the domestic node without secrets", () => { const nodeMap = JSON.parse(read("routing/server-node-map.json")); assert.equal(nodeMap.map_id, "FD-NODE-MAP-001"); diff --git a/server-tools/ai-discovery-gateway/server.test.js b/server-tools/ai-discovery-gateway/server.test.js index b7b88d3..ac51f2e 100644 --- a/server-tools/ai-discovery-gateway/server.test.js +++ b/server-tools/ai-discovery-gateway/server.test.js @@ -5,10 +5,10 @@ const { createServer, loadMap, loadNodeMap, search, searchAll } = require("./ser test("repository map has unique sequential codes and domestic primary routes", () => { const map = loadMap(); - assert.equal(map.repositories.length, 11); - assert.deepEqual(map.repositories.map(item => item.code), Array.from({ length: 11 }, (_, index) => `REPO-${String(index + 1).padStart(3, "0")}`)); - assert.equal(new Set(map.repositories.map(item => item.code)).size, 11); - for (const item of map.repositories) assert.match(item.primary.url, /^https:\/\/guanghulab\.com\/fifth-domain\//); + assert.equal(map.repositories.length, 12); + assert.deepEqual(map.repositories.map(item => item.code), Array.from({ length: 12 }, (_, index) => `REPO-${String(index + 1).padStart(3, "0")}`)); + assert.equal(new Set(map.repositories.map(item => item.code)).size, 12); + for (const item of map.repositories) assert.match(item.primary.url, /^https:\/\/guanghulab\.com\/(?:fifth-domain|code)\//); }); test("Chinese language-world query resolves the Fifth Domain primary", () => { diff --git a/server-tools/lake-lamp-authz/README.md b/server-tools/lake-lamp-authz/README.md index ad51150..cded1bd 100644 --- a/server-tools/lake-lamp-authz/README.md +++ b/server-tools/lake-lamp-authz/README.md @@ -94,6 +94,12 @@ node server-tools/lake-lamp-authz/authorize-repo-push.js \ 运行入口:公开建单 `/api/public/workorders`,会话续签 `/api/session/renew`,登记动作执行 `/api/actions/execute`。多节点需求由人格体按当前任务拆成并行申请,不再使用固定“三封邮件”作为协作规则。 +## 登录恢复动作不得混用 + +- `restore-owner-password-login` 只恢复京东服务器的 SSH 密码认证开关;它不读取、不重置,也不验证光湖代码频道账号。 +- `restore-code-channel-owner-login` 只把京东本机旧第五域数据库中的 `bingshuo` 密码摘要恢复到新代码频道,同时校正启用、管理员和禁止登录状态。它不读取明文密码,不修改 SSH,并在变更前使用 SQLite backup API 生成一致性备份。 +- 登录问题先执行 `inspect-code-channel-owner-auth`。只有确认新旧频道凭证不一致时,才申请后一项恢复动作;不得绕到新加坡灾备节点,不得向冰朔索要密码。 + ## 新架构首次部署 旧的 `deploy-registered-service` 只能操作已经登记的服务,不能承担首次安装。新架构统一使用固定动作 `provision-approved-architecture`,并把仓库请求编号与不可变提交绑定进工单: diff --git a/server-tools/lake-lamp-authz/action-client.js b/server-tools/lake-lamp-authz/action-client.js index 40fab56..c635edf 100644 --- a/server-tools/lake-lamp-authz/action-client.js +++ b/server-tools/lake-lamp-authz/action-client.js @@ -7,7 +7,11 @@ const OWNER_ACCESS_SOCKET_PATH = process.env.LAKE_LAMP_OWNER_ACCESS_SOCKET || "/ const ARCHITECTURE_PROVISION_SOCKET_PATH = process.env.LAKE_LAMP_ARCHITECTURE_PROVISION_SOCKET || "/run/guanghu-architecture-provision/provision.sock"; function executeRegisteredAction(request, socketPath) { - const selectedSocket = socketPath || (request.action === "restore-owner-password-login" ? OWNER_ACCESS_SOCKET_PATH : request.action === "provision-approved-architecture" ? ARCHITECTURE_PROVISION_SOCKET_PATH : SOCKET_PATH); + const ownerAccessActions = new Set([ + "restore-owner-password-login", + "restore-code-channel-owner-login", + ]); + const selectedSocket = socketPath || (ownerAccessActions.has(request.action) ? OWNER_ACCESS_SOCKET_PATH : request.action === "provision-approved-architecture" ? ARCHITECTURE_PROVISION_SOCKET_PATH : SOCKET_PATH); return new Promise((resolve) => { const socket = net.createConnection(selectedSocket); let response = ""; diff --git a/server-tools/lake-lamp-authz/owner-access-broker.js b/server-tools/lake-lamp-authz/owner-access-broker.js index d440ad2..8e0061b 100644 --- a/server-tools/lake-lamp-authz/owner-access-broker.js +++ b/server-tools/lake-lamp-authz/owner-access-broker.js @@ -47,8 +47,119 @@ printf 'PASSWORD_AUTH=%s\nROOT_PASSWORD_AUTH=%s\nSSH=%s\nBACKUP=%s\n' \ "$backup_dir/90-guanghu-key-only.conf.before" trap - EXIT`; -function run(file, args) { - return new Promise(resolve => execFile(file, args, { timeout: 30000, maxBuffer: 100000 }, (error, stdout, stderr) => resolve({ +const RESTORE_CODE_CHANNEL_OWNER_LOGIN = String.raw`set -eu +test "$(id -u)" = "0" +/usr/bin/python3 - <<'PY' +import atexit +import datetime +import json +import os +import pathlib +import sqlite3 +import subprocess +import time +import urllib.request + +state = pathlib.Path("/var/lib/guanghu/personas/guanghu/hlcc-v16.0.1") +channel_db = state / "data/hlcc.db" +legacy_db = pathlib.Path("/var/lib/guanghu/forgejo/data/forgejo.db") +stamp = datetime.datetime.now(datetime.timezone.utc).strftime("%Y%m%dT%H%M%SZ") +backup = state / "data/backups" / f"owner-login-recovery-{stamp}.db" + +service_started = False +def ensure_service_started(): + if not service_started: + subprocess.run(["systemctl", "start", "hlcc-jd-candidate.service"]) + +atexit.register(ensure_service_started) +subprocess.run(["systemctl", "stop", "hlcc-jd-candidate.service"], check=True) +channel = sqlite3.connect(channel_db, timeout=30) +snapshot = sqlite3.connect(backup, timeout=30) +try: + channel.execute("pragma wal_checkpoint(truncate)") + channel.backup(snapshot) +finally: + snapshot.close() +os.chmod(backup, 0o600) + +legacy = sqlite3.connect(f"file:{legacy_db}?mode=ro", uri=True, timeout=15) +try: + credential = legacy.execute( + "select passwd,salt,passwd_hash_algo from user where lower_name=?", + ("bingshuo",), + ).fetchone() +finally: + legacy.close() +if not credential or not all(credential): + raise RuntimeError("legacy owner credential unavailable") + +try: + channel.execute("begin immediate") + changed = channel.execute( + """update user + set passwd=?,salt=?,passwd_hash_algo=?,is_active=1,is_admin=1, + prohibit_login=0,must_change_password=0,updated_unix=? + where lower_name=?""", + (*credential, int(time.time()), "bingshuo"), + ).rowcount + if changed != 1: + raise RuntimeError("channel owner identity mismatch") + channel.commit() + copied = channel.execute( + """select passwd,salt,passwd_hash_algo,is_active,is_admin, + prohibit_login,must_change_password + from user where lower_name=?""", + ("bingshuo",), + ).fetchone() + if copied[:3] != credential or copied[3:] != (1, 1, 0, 0): + raise RuntimeError("owner login recovery verification failed") + if channel.execute("pragma integrity_check").fetchone()[0] != "ok": + raise RuntimeError("channel database integrity check failed") + channel.execute("pragma wal_checkpoint(truncate)") +finally: + channel.close() + +subprocess.run(["systemctl", "start", "hlcc-jd-candidate.service"], check=True) +service_started = True +status = {} +for _ in range(90): + try: + with urllib.request.urlopen("http://127.0.0.1:3341/health", timeout=2) as response: + status = json.load(response) + with urllib.request.urlopen("http://127.0.0.1:3340/api/healthz", timeout=2) as response: + app = json.load(response) + if status.get("ready") is True and app.get("status") == "pass": + break + except Exception: + pass + time.sleep(1) +else: + subprocess.run(["systemctl", "stop", "hlcc-jd-candidate.service"]) + service_started = False + restored = sqlite3.connect(channel_db, timeout=30) + saved = sqlite3.connect(f"file:{backup}?mode=ro", uri=True, timeout=30) + try: + saved.backup(restored) + finally: + saved.close() + restored.close() + subprocess.run(["systemctl", "start", "hlcc-jd-candidate.service"], check=True) + service_started = True + raise RuntimeError("channel health failed; database restored from backup") + +print(json.dumps({ + "ACCOUNT": "bingshuo", + "CREDENTIAL_SOURCE": "legacy-local-db", + "ACTIVE": True, + "ADMIN": True, + "PROHIBIT_LOGIN": False, + "SERVICE": "ready", + "BACKUP": str(backup), +}, ensure_ascii=False)) +PY`; + +function run(file, args, timeout = 30000) { + return new Promise(resolve => execFile(file, args, { timeout, maxBuffer: 100000 }, (error, stdout, stderr) => resolve({ ok: !error, exit_code: error ? (Number.isInteger(error.code) ? error.code : 1) : 0, stdout: String(stdout || "").slice(0, 100000), @@ -58,8 +169,14 @@ function run(file, args) { async function execute(request) { if (!request || request.cmd || request.command || request.shell || request.args) return { ok: false, error: "arbitrary_command_forbidden" }; - if (request.target !== "JD-FD-PRIMARY" || request.action !== "restore-owner-password-login") return { ok: false, error: "action_not_registered" }; - return run("/usr/bin/ssh", ["-F", SSH_CONFIG, "-o", "BatchMode=yes", "-o", "ConnectTimeout=10", "enterprise-lighthouse", RESTORE_OWNER_PASSWORD_LOGIN]); + if (request.target !== "JD-FD-PRIMARY") return { ok: false, error: "action_not_registered" }; + const commands = { + "restore-owner-password-login": RESTORE_OWNER_PASSWORD_LOGIN, + "restore-code-channel-owner-login": RESTORE_CODE_CHANNEL_OWNER_LOGIN, + }; + if (!commands[request.action]) return { ok: false, error: "action_not_registered" }; + const timeout = request.action === "restore-code-channel-owner-login" ? 150000 : 30000; + return run("/usr/bin/ssh", ["-F", SSH_CONFIG, "-o", "BatchMode=yes", "-o", "ConnectTimeout=10", "enterprise-lighthouse", commands[request.action]], timeout); } function reply(socket, value) { socket.end(`${JSON.stringify(value)}\n`); } @@ -83,4 +200,9 @@ if (require.main === module) { }); } -module.exports = { execute, RESTORE_OWNER_PASSWORD_LOGIN, SOCKET_PATH }; +module.exports = { + execute, + RESTORE_OWNER_PASSWORD_LOGIN, + RESTORE_CODE_CHANNEL_OWNER_LOGIN, + SOCKET_PATH, +}; diff --git a/server-tools/lake-lamp-authz/owner-access-broker.test.js b/server-tools/lake-lamp-authz/owner-access-broker.test.js index ed75687..a012e7b 100644 --- a/server-tools/lake-lamp-authz/owner-access-broker.test.js +++ b/server-tools/lake-lamp-authz/owner-access-broker.test.js @@ -2,13 +2,25 @@ const assert = require("node:assert/strict"); const test = require("node:test"); -const { execute, RESTORE_OWNER_PASSWORD_LOGIN } = require("./owner-access-broker"); +const { + execute, + RESTORE_OWNER_PASSWORD_LOGIN, + RESTORE_CODE_CHANNEL_OWNER_LOGIN, +} = require("./owner-access-broker"); -test("owner-access executor exposes one bounded recovery action", async () => { +test("owner-access executor keeps SSH and code-channel recovery distinct", async () => { assert.match(RESTORE_OWNER_PASSWORD_LOGIN, /PasswordAuthentication yes/); assert.match(RESTORE_OWNER_PASSWORD_LOGIN, /sshd -t/); assert.match(RESTORE_OWNER_PASSWORD_LOGIN, /systemctl reload ssh/); assert.doesNotMatch(RESTORE_OWNER_PASSWORD_LOGIN, /PermitRootLogin yes/); + assert.match(RESTORE_CODE_CHANNEL_OWNER_LOGIN, /legacy owner credential unavailable/); + assert.match(RESTORE_CODE_CHANNEL_OWNER_LOGIN, /pragma wal_checkpoint\(truncate\)/); + assert.match(RESTORE_CODE_CHANNEL_OWNER_LOGIN, /channel\.backup\(snapshot\)/); + assert.match(RESTORE_CODE_CHANNEL_OWNER_LOGIN, /prohibit_login=0/); + assert.match(RESTORE_CODE_CHANNEL_OWNER_LOGIN, /hlcc-jd-candidate\.service/); + assert.doesNotMatch(RESTORE_CODE_CHANNEL_OWNER_LOGIN, /PasswordAuthentication/); + assert.match(require("node:fs").readFileSync(require.resolve("./owner-access-broker"), "utf8"), /150000/); assert.deepEqual(await execute({ target: "JD-FD-PRIMARY", action: "unknown" }), { ok: false, error: "action_not_registered" }); assert.deepEqual(await execute({ target: "JD-FD-PRIMARY", action: "restore-owner-password-login", cmd: "id" }), { ok: false, error: "arbitrary_command_forbidden" }); + assert.deepEqual(await execute({ target: "JD-FD-PRIMARY", action: "restore-code-channel-owner-login", shell: "id" }), { ok: false, error: "arbitrary_command_forbidden" }); }); diff --git a/server-tools/lake-lamp-authz/server.js b/server-tools/lake-lamp-authz/server.js index 710cf05..762c47a 100644 --- a/server-tools/lake-lamp-authz/server.js +++ b/server-tools/lake-lamp-authz/server.js @@ -27,6 +27,7 @@ const DEFAULT_ACTIONS = Object.freeze({ "provision-approved-architecture", "push-repository", "restore-owner-password-login", + "restore-code-channel-owner-login", ], "repo-push": ["read-navigation-map", "push-repository"], }); diff --git a/server-tools/lake-lamp-authz/server.test.js b/server-tools/lake-lamp-authz/server.test.js index c88609a..19b6475 100644 --- a/server-tools/lake-lamp-authz/server.test.js +++ b/server-tools/lake-lamp-authz/server.test.js @@ -138,6 +138,7 @@ test("one server-ops approval exposes the full registered three-hour operation s "provision-approved-architecture", "push-repository", "restore-owner-password-login", + "restore-code-channel-owner-login", ]); } finally { await new Promise(resolve => app.close(resolve));