[HLCC-ICE-000003] fix: recover code-channel owner login safely

This commit is contained in:
冰朔 2026-07-26 15:02:01 +08:00
commit 100848e9e4
12 changed files with 235 additions and 16 deletions

View file

@ -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/

View file

@ -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"
}
],

View file

@ -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."
]
}

View file

@ -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", "当前提交", "公开读取"]
}
]
}

View file

@ -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");

View file

@ -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", () => {

View file

@ -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`,并把仓库请求编号与不可变提交绑定进工单:

View file

@ -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 = "";

View file

@ -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,
};

View file

@ -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" });
});

View file

@ -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"],
});

View file

@ -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));