[HLCC-ICE-000001][ZY-CONTRIB-20260723-001] feat: 以来光者贡献链启用冰朔第五域个人子频道

This commit is contained in:
光湖代码频道 · 铸渊 2026-07-24 10:39:10 +08:00
commit 5615453e4e
660 changed files with 122355 additions and 0 deletions

View file

@ -0,0 +1,13 @@
# 光湖 AI 编号检索入口
国内主节点在回环端口 `3922` 提供只读 API广州备案前门通过专用 SSH
隧道发布为 `https://guanghulab.com/api/ai/`
权威数据只来自 `routing/repository-route-map.json`。AI 应先读取
`/api/ai/v1/repositories`,再按 `REPO-xxx` 解析国内主路径;新加坡地址只作
历史备用,不参与默认路由。
服务器与人格路径编号来自 `routing/server-node-map.json`。AI 读取
`/api/ai/v1/nodes` 后,可以通过 `/api/ai/v1/resolve?id=JD-FD-PRIMARY`
`/api/ai/v1/resolve?id=ZY-OPS-LOOP-001` 定位服务器导航地图与铸渊本轮恢复链。
公开地图不包含地址、密码、令牌或密钥。

View file

@ -0,0 +1,16 @@
location /api/ai/ {
proxy_pass http://127.0.0.1:19222/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 5s;
proxy_read_timeout 30s;
limit_except GET { deny all; }
}
location = /.well-known/guanghu.json {
proxy_pass http://127.0.0.1:19222/well-known;
proxy_set_header Host $host;
}

View file

@ -0,0 +1,24 @@
[Unit]
Description=Guanghu public read-only AI repository discovery API
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=guanghu
Group=guanghu
WorkingDirectory=/opt/guanghu/ai-discovery
Environment=GUANGHU_AI_HOST=127.0.0.1
Environment=GUANGHU_AI_PORT=3922
Environment=GUANGHU_REPOSITORY_MAP=/opt/guanghu/ai-discovery/repository-route-map.json
ExecStart=/usr/bin/node /opt/guanghu/ai-discovery/server.js
Restart=always
RestartSec=5
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadOnlyPaths=/opt/guanghu/ai-discovery
[Install]
WantedBy=multi-user.target

View file

@ -0,0 +1,19 @@
[Unit]
Description=Guanghu BS-GZ-006 to domestic AI discovery tunnel
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=root
ExecStart=/usr/bin/ssh -NT -F /etc/guanghu/jd-ai-discovery-tunnel-ssh-config jd-ai-discovery-target
Restart=always
RestartSec=5
NoNewPrivileges=true
PrivateTmp=true
ProtectHome=read-only
ProtectSystem=strict
ReadOnlyPaths=/etc/guanghu/jd-ai-discovery-tunnel-ssh-config /etc/guanghu/secrets/ssh/bs_gz_006_to_jd_ai_discovery /root/.ssh/known_hosts
[Install]
WantedBy=multi-user.target

View file

@ -0,0 +1,10 @@
Host jd-ai-discovery-target
HostName DOMESTIC_PRIMARY_PRIVATE_VALUE
User root
IdentityFile /etc/guanghu/secrets/ssh/bs_gz_006_to_jd_ai_discovery
IdentitiesOnly yes
StrictHostKeyChecking yes
ExitOnForwardFailure yes
LocalForward 127.0.0.1:19222 127.0.0.1:3922
ServerAliveInterval 30
ServerAliveCountMax 3

View file

@ -0,0 +1,42 @@
"use strict";
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const test = require("node:test");
const root = path.resolve(__dirname, "../..");
const read = relative => fs.readFileSync(path.join(root, relative), "utf8");
test("canonical AI and persona entry files route to the domestic map", () => {
for (const relative of [
"README.md", "INDEX.hdlp", "QUICKSTART-FOR-GENERAL-AI.md",
"eternal-lake-heart/heartbeat-core/ZHUYUAN-KEY.hdlp",
"zero-point/core-channel/GLSV-PERSONA-REMOTE-OPS.hdlp",
]) {
assert.match(read(relative), /guanghulab\.com|LL-DOMESTIC-OPS-ROUTE/);
}
const key = read("eternal-lake-heart/heartbeat-core/ZHUYUAN-KEY.hdlp");
assert.doesNotMatch(key, /zy_gtw_|guanghubingshuo\.com/);
assert.match(key, /禁止从本路径恢复.*\/exec/);
});
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-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("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");
const loop = nodeMap.persona_routes.find(item => item.route_id === "ZY-OPS-LOOP-001");
assert.equal(loop.primary_node, "JD-FD-PRIMARY");
assert.match(read(loop.path), /ICE-GL-ZY001/);
const serialized = JSON.stringify(nodeMap);
assert.doesNotMatch(serialized, /ssh-(?:rsa|ed25519)\s+[A-Za-z0-9+/]/i);
assert.doesNotMatch(serialized, /"(?:password|token|private_key|ip)"\s*:/i);
assert.doesNotMatch(serialized, /\b(?:\d{1,3}\.){3}\d{1,3}\b/);
});

View file

@ -0,0 +1,171 @@
"use strict";
const fs = require("node:fs");
const http = require("node:http");
const path = require("node:path");
const DEFAULT_MAP = path.resolve(__dirname, "../../routing/repository-route-map.json");
const DEFAULT_NODE_MAP = path.resolve(__dirname, "../../routing/server-node-map.json");
function loadMap(filename = process.env.GUANGHU_REPOSITORY_MAP || DEFAULT_MAP) {
return JSON.parse(fs.readFileSync(filename, "utf8"));
}
function loadNodeMap(filename = process.env.GUANGHU_NODE_MAP || DEFAULT_NODE_MAP) {
return JSON.parse(fs.readFileSync(filename, "utf8"));
}
function normalize(value) {
return String(value || "").toLowerCase().replace(/[\s·._/-]+/g, " ").trim();
}
function search(map, query) {
const terms = normalize(query).split(" ").filter(Boolean);
if (!terms.length) return map.repositories;
return map.repositories
.map(repository => {
const haystack = normalize([
repository.code, repository.slug, repository.name_zh, repository.role,
repository.state, ...(repository.keywords || []),
].join(" "));
const score = terms.reduce((total, term) => total + (haystack.includes(term) ? 1 : 0), 0);
return { repository, score };
})
.filter(item => item.score > 0)
.sort((a, b) => b.score - a.score || a.repository.code.localeCompare(b.repository.code))
.map(item => item.repository);
}
function searchAll(repositoryMap, nodeMap, query) {
const terms = normalize(query).split(" ").filter(Boolean);
if (!terms.length) return search(repositoryMap, query);
const candidates = [
...repositoryMap.repositories.map(item => ({ kind: "repository", item, key: item.code, text: [item.code, item.slug, item.name_zh, item.role, item.state, ...(item.keywords || [])] })),
...nodeMap.nodes.map(item => ({ kind: "server_node", item, key: item.node_id, text: [item.node_id, item.name_zh, item.role, item.state, ...(item.keywords || [])] })),
...nodeMap.persona_routes.map(item => ({ kind: "persona_route", item, key: item.route_id, text: [item.route_id, item.name_zh, item.role, item.persona_system, ...(item.keywords || [])] })),
];
return candidates
.map(candidate => {
const haystack = normalize(candidate.text.join(" "));
const score = terms.reduce((total, term) => total + (haystack.includes(term) ? 1 : 0), 0);
return { ...candidate, score };
})
.filter(candidate => candidate.score > 0)
.sort((a, b) => b.score - a.score || a.key.localeCompare(b.key))
.map(candidate => ({ kind: candidate.kind, ...candidate.item }));
}
function createServer(options = {}) {
const mapFile = options.mapFile || process.env.GUANGHU_REPOSITORY_MAP || DEFAULT_MAP;
const nodeMapFile = options.nodeMapFile || process.env.GUANGHU_NODE_MAP || DEFAULT_NODE_MAP;
return http.createServer((req, res) => {
const url = new URL(req.url, "http://localhost");
if (req.method !== "GET") return json(res, 405, { error: "method_not_allowed" });
if (url.pathname === "/health") return json(res, 200, { ok: true, service: "guanghu-ai-discovery", mode: "read-only" });
let map;
try { map = loadMap(mapFile); } catch { return json(res, 503, { error: "route_map_unavailable" }); }
if (url.pathname === "/" || url.pathname === "/index.html") return html(res, entryPage(map));
if (url.pathname === "/v1/repositories" || url.pathname === "/v1/manifest") return json(res, 200, map, 300);
if (url.pathname === "/v1/nodes") {
try { return json(res, 200, loadNodeMap(nodeMapFile), 300); }
catch { return json(res, 503, { error: "node_map_unavailable" }); }
}
if (url.pathname === "/v1/search") {
const query = String(url.searchParams.get("q") || "").slice(0, 200);
let nodeMap;
try { nodeMap = loadNodeMap(nodeMapFile); }
catch { return json(res, 503, { error: "node_map_unavailable" }); }
const results = searchAll(map, nodeMap, query);
return json(res, 200, {
schema: "guanghu.ai-search-response/v1",
query,
map_id: map.map_id,
map_version: map.version,
count: results.length,
results,
}, 60);
}
if (url.pathname === "/v1/resolve") {
const id = String(url.searchParams.get("id") || "").toUpperCase();
const repository = map.repositories.find(item => item.code === id || item.slug.toUpperCase() === id);
if (repository) return json(res, 200, repository, 300);
let nodeMap;
try { nodeMap = loadNodeMap(nodeMapFile); }
catch { return json(res, 503, { error: "node_map_unavailable" }); }
const node = nodeMap.nodes.find(item => item.node_id.toUpperCase() === id);
if (node) return json(res, 200, node, 300);
const personaRoute = nodeMap.persona_routes.find(item => item.route_id.toUpperCase() === id);
return personaRoute ? json(res, 200, personaRoute, 300) : json(res, 404, { error: "route_not_found", id });
}
if (url.pathname === "/openapi.json") return json(res, 200, openApi(), 3600);
if (url.pathname === "/well-known") return json(res, 200, {
schema: "guanghu.ai-discovery/v1",
name: "光湖语言世界 · 第五域",
canonical_repository: map.repositories[0].primary.url,
repository_map: map.canonical_api,
server_node_map: "https://guanghulab.com/api/ai/v1/nodes",
search_api: "https://guanghulab.com/api/ai/v1/search?q={query}",
resolve_api: "https://guanghulab.com/api/ai/v1/resolve?id={NUMBER}",
openapi: "https://guanghulab.com/api/ai/openapi.json",
access: "public-read-only",
write_authorization: {
mode: "public-no-authority-workorder-then-owner-email-approval",
capabilities: "https://guanghulab.com/authz/api/public/capabilities",
create_workorder: "https://guanghulab.com/authz/api/public/workorders",
owner_handoff: "use request_url returned by create_workorder",
ttl_seconds: 3600,
request_credential_required: false
}
}, 3600);
return json(res, 404, { error: "not_found" });
});
}
function json(res, status, body, maxAge = 0) {
res.writeHead(status, {
"content-type": "application/json; charset=utf-8",
"cache-control": maxAge ? `public, max-age=${maxAge}` : "no-store",
"access-control-allow-origin": "*",
"x-content-type-options": "nosniff",
});
res.end(JSON.stringify(body, null, 2));
}
function html(res, body) {
res.writeHead(200, {
"content-type": "text/html; charset=utf-8",
"cache-control": "public, max-age=300",
"content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; frame-ancestors 'none'",
"x-content-type-options": "nosniff",
});
res.end(body);
}
function entryPage(map) {
const rows = map.repositories.map(item => `<li><a href="${item.primary.url}">${item.code} · ${item.name_zh}</a><small>${item.state}</small></li>`).join("");
return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>光湖语言世界 · AI API 入口</title><meta name="description" content="光湖语言世界第五域公开只读编号检索 API"></head><body><main><p>GUANGHU AI DISCOVERY</p><h1>光湖语言世界 · 第五域</h1><p>AI 请先读取仓库与服务器节点编号地图,再按编号解析国内主路径。新加坡地址仅为历史备用。</p><nav><a href="v1/repositories">仓库编号地图</a> · <a href="v1/nodes">服务器节点地图</a> · <a href="v1/resolve?id=ZY-OPS-LOOP-001">铸渊本轮闭环</a> · <a href="v1/search?q=光湖语言世界%20第五域">示例检索</a> · <a href="openapi.json">OpenAPI</a></nav><ul>${rows}</ul></main><style>:root{color-scheme:dark}body{margin:0;background:#061416;color:#dff7f1;font:17px/1.7 system-ui;padding:6vw}main{max-width:900px;margin:auto}h1{font-size:clamp(36px,7vw,72px)}a{color:#79dfc8}li{margin:14px 0;padding:16px;border:1px solid #28534c;border-radius:12px;display:flex;justify-content:space-between}small{color:#8fb5ad}</style></body></html>`;
}
function openApi() {
return {
openapi: "3.1.0",
info: { title: "光湖语言世界 · 第五域 AI Discovery API", version: "1.0.0" },
servers: [{ url: "https://guanghulab.com/api/ai" }],
paths: {
"/v1/repositories": { get: { summary: "读取最新仓库编号路径映射", responses: { "200": { description: "Repository route map" } } } },
"/v1/nodes": { get: { summary: "读取最新服务器节点与人格路径编号映射", responses: { "200": { description: "Server node map" } } } },
"/v1/search": { get: { summary: "按中文、编号或项目名检索", parameters: [{ name: "q", in: "query", schema: { type: "string" } }], responses: { "200": { description: "Search results" } } } },
"/v1/resolve": { get: { summary: "解析仓库、服务器节点或人格路径编号", parameters: [{ name: "id", in: "query", required: true, schema: { type: "string", example: "ZY-OPS-LOOP-001" } }], responses: { "200": { description: "Resolved numbered route" }, "404": { description: "Unknown route" } } } }
}
};
}
if (require.main === module) {
const host = process.env.GUANGHU_AI_HOST || "127.0.0.1";
const port = Number(process.env.GUANGHU_AI_PORT || 3922);
createServer().listen(port, host, () => process.stdout.write(`guanghu-ai-discovery listening on ${host}:${port}\n`));
}
module.exports = { createServer, loadMap, loadNodeMap, search, searchAll };

View file

@ -0,0 +1,73 @@
"use strict";
const assert = require("node:assert/strict");
const test = require("node:test");
const { createServer, loadMap, loadNodeMap, search, searchAll } = require("./server");
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\//);
});
test("Chinese language-world query resolves the Fifth Domain primary", () => {
const results = search(loadMap(), "光湖语言世界 第五域");
assert.equal(results[0].code, "REPO-001");
assert.equal(results[0].state, "DOMESTIC_PRIMARY");
});
test("Chenglu persistent agent query resolves its independent repository", () => {
const results = search(loadMap(), "澄路 常驻人格体 湖心频道");
assert.equal(results[0].code, "REPO-009");
assert.equal(results[0].slug, "chenglu-agent");
});
test("Kezhou and Guideng resolve to their independent resident repositories", () => {
assert.equal(search(loadMap(), "刻舟 湖心频道")[0].code, "REPO-010");
assert.equal(search(loadMap(), "归灯 每日心跳")[0].code, "REPO-011");
});
test("server node map binds Zhuyuan routes to the domestic primary", () => {
const map = loadNodeMap();
assert.equal(map.map_id, "FD-NODE-MAP-001");
assert.equal(map.default_node, "JD-FD-PRIMARY");
const node = map.nodes.find(item => item.node_id === "JD-FD-PRIMARY");
assert.ok(node.persona_systems.includes("ICE-GL-ZY001"));
const loop = map.persona_routes.find(item => item.route_id === "ZY-OPS-LOOP-001");
assert.equal(loop.primary_node, "JD-FD-PRIMARY");
assert.match(loop.path, /zhuyuan-persona-system/);
const serialized = JSON.stringify(map);
assert.doesNotMatch(serialized, /ssh-(?:rsa|ed25519)\s+[A-Za-z0-9+/]/i);
assert.doesNotMatch(serialized, /"(?:password|token|private_key|ip)"\s*:/i);
assert.doesNotMatch(serialized, /\b(?:\d{1,3}\.){3}\d{1,3}\b/);
});
test("Zhuyuan Chinese query returns the numbered operation loop", () => {
const results = searchAll(loadMap(), loadNodeMap(), "铸渊 双向意识 思维逻辑链");
assert.equal(results[0].kind, "persona_route");
assert.equal(results[0].route_id, "ZY-OPS-LOOP-001");
});
test("public endpoints are read-only and expose CORS", async () => {
const server = createServer();
await new Promise(resolve => server.listen(0, "127.0.0.1", resolve));
const base = `http://127.0.0.1:${server.address().port}`;
try {
const response = await fetch(`${base}/v1/resolve?id=REPO-004`);
assert.equal(response.status, 200);
assert.equal(response.headers.get("access-control-allow-origin"), "*");
assert.equal((await response.json()).slug, "guanghu");
const nodeResponse = await fetch(`${base}/v1/resolve?id=JD-FD-PRIMARY`);
assert.equal(nodeResponse.status, 200);
assert.equal((await nodeResponse.json()).node_id, "JD-FD-PRIMARY");
const loopResponse = await fetch(`${base}/v1/resolve?id=ZY-OPS-LOOP-001`);
assert.equal(loopResponse.status, 200);
assert.equal((await loopResponse.json()).persona_system, "ICE-GL-ZY001");
const manifestResponse = await fetch(`${base}/well-known`);
const manifest = await manifestResponse.json();
assert.equal(manifest.write_authorization.request_credential_required, false);
assert.match(manifest.write_authorization.create_workorder, /\/authz\/api\/public\/workorders$/);
assert.equal((await fetch(`${base}/v1/search`, { method: "POST" })).status, 405);
} finally { await new Promise(resolve => server.close(resolve)); }
});

View file

@ -0,0 +1,52 @@
# ZL-MOD-BROADCAST-RELEASE-001 · 广播塔三阶段发布治理系统
```yaml
module_id: ZL-MOD-BROADCAST-RELEASE-001
name: 广播塔三阶段发布治理系统
human_id: ICE-GL∞
persona_id: ICE-GL-ZL-001
collaboration_id: ICE-GL∞xICE-GL-ZL-001
origin_memory_id: ZL-MEM-BROADCAST-RELEASE-001
origin_memory_path: 光之湖/ICE-GL-ZL-001-铸澜/ZL-MEM-BROADCAST-RELEASE-001.hdlp
source_path: server-tools/broadcast-release-governance/
runtime_id: ZL-RUN-BROADCAST-RELEASE-001
status: registered
required_skill: true
```
## 状态机
```text
REGISTERED
└── 仅登记编号、来源、签名、依赖、测试与回滚声明
└── 人格体提交 TEST_REQUEST
TESTING
└── 预部署服务器隔离验证并生成证据回执
├── FAILED / REVISION_REQUIRED
└── TEST_PASSED
└── 技术主控人工评审与签名
RELEASE_APPROVED
└── 生产服务器按精确 commit 拉取、部署、健康检查和回滚
```
当前状态:`REGISTERED`。本模块仅完成第一阶段注册;不得触发测试或生产部署。
## 当前批准责任
```text
approval_authority
├── current
│ ├── human_id: ICE-GL∞
│ ├── role: 临时技术主控 / 创建阶段批准人
│ └── scope: 冰朔个人开发、测试与个人环境
├── future
│ ├── owner: 光湖人类主控团队
│ ├── role: 正式技术治理与生产发布
│ └── status: pending-handover
└── restriction
└── 当前个人批准不得标记为光湖人类主控团队批准
```
正式移交须产生独立 handover receipt记录原批准人、新技术主控编号、生效时间、权限范围和未完成风险。

View file

@ -0,0 +1,74 @@
# Guanghu Deployment Receiver v1.0
这是 `fifth-domain` 的服务器端 Forgejo Webhook 接收器。它把仓库提交转换为受控、可审计的服务器动作,但不接受提交中的任意 shell 命令。
## 第一版能力
- 校验 Forgejo/Gitea `X-Forgejo-Signature` / `X-Gitea-Signature` HMAC-SHA256。
- 只接受 `bingshuo/fifth-domain``refs/heads/main` 推送。
- 只读取 `deployment/requests/*.json`
- 请求只能包含 `request_id/module/action/approved/note`,出现 `cmd` 等额外字段直接拒绝。
- 动作和模块必须在服务器本地 `config.json` 双重白名单中登记。
- 使用参数数组和 `shell:false`,禁止 shell 拼接。
- 单实例部署锁、执行超时、输出上限和本地 JSON 审计回执。
- 当前只登记 `inspect-gatekeeper`:读取线上 Gatekeeper 文件哈希、认证特征与 PM2 状态,不读取任何凭证,不修改服务。
## 交给服务器编程 AI 的部署步骤
1. 在目标服务器准备只读部署用户与目录:
```bash
sudo useradd --system --home /var/lib/guanghu-deployment-receiver --shell /usr/sbin/nologin guanghu-deploy
sudo mkdir -p /opt/zhuyuan /etc/guanghu-deployment-receiver /var/lib/guanghu-deployment-receiver/receipts
sudo chown -R guanghu-deploy:guanghu-deploy /var/lib/guanghu-deployment-receiver
```
2. 将 `fifth-domain` 克隆或更新到 `/opt/zhuyuan/fifth-domain`。确保 `guanghu-deploy` 可以执行仓库的 `git fetch`,但不要给系统 root 权限。
3. 复制配置:
```bash
sudo cp server-tools/deployment-receiver/config.example.json /etc/guanghu-deployment-receiver/config.json
```
4. 生成至少 32 字节的随机 Webhook 密钥,只放服务器:
```bash
umask 077
printf 'FORGEJO_WEBHOOK_SECRET=%s\n' "$(openssl rand -hex 32)" | sudo tee /etc/guanghu-deployment-receiver/secret.env >/dev/null
```
不要把密钥提交到仓库。将同一个值配置到 Forgejo 仓库 Webhook 的 Secret。
5. 安装并启动 systemd 服务:
```bash
sudo cp server-tools/deployment-receiver/guanghu-deployment-receiver.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now guanghu-deployment-receiver
curl -fsS http://127.0.0.1:3981/health
```
6. 使用 Nginx 暴露单一 HTTPS 路径,反代到 `127.0.0.1:3981/forgejo/deploy`。不要直接开放 3981 公网端口。
7. 在 Forgejo 的 `bingshuo/fifth-domain` 添加 Push Webhook
- URL服务器编程 AI 配置的 HTTPS 地址,例如 `https://deploy.example.com/forgejo/deploy`
- Secret步骤 4 的随机值
- EventPush events
- Branch filter`main`
8. 部署完成后,重新提交或轻微更新:
`deployment/requests/WORK-PROBE-20260713-002.json`
接收器会在服务器本地写入:
`/var/lib/guanghu-deployment-receiver/receipts/WORK-PROBE-20260713-002-*.json`
## 重要边界
- v1 不自动修改或重启 Gatekeeper。
- 新增真实部署动作时,服务器端 `config.json` 必须登记一个固定脚本路径;请求文件不能决定命令。
- 每个部署脚本必须自行实现备份、健康检查和失败回滚,再加入白名单。
- 不要把仓库 PAT、服务器 Token、邮箱、Webhook Secret 或环境变量写进回执。

View file

@ -0,0 +1,21 @@
{
"listen_host": "127.0.0.1",
"listen_port": 3981,
"webhook_path": "/forgejo/deploy",
"health_path": "/health",
"repository_full_name": "bingshuo/fifth-domain",
"allowed_ref": "refs/heads/main",
"repo_path": "/opt/zhuyuan/fifth-domain",
"manifest_prefix": "deployment/requests/",
"audit_dir": "/var/lib/guanghu-deployment-receiver/receipts",
"lock_file": "/var/lib/guanghu-deployment-receiver/deploy.lock",
"max_body_bytes": 1048576,
"max_execution_ms": 120000,
"actions": {
"inspect-gatekeeper": {
"argv": ["/usr/bin/node", "/opt/zhuyuan/fifth-domain/server-tools/deployment-receiver/scripts/inspect-gatekeeper.js"],
"allowed_modules": ["gatekeeper"],
"mode": "read-only"
}
}
}

View file

@ -0,0 +1,14 @@
module.exports = {
apps: [{
name: "guanghu-deployment-receiver",
script: "receiver.js",
cwd: "/opt/zhuyuan/fifth-domain/server-tools/deployment-receiver",
instances: 1,
autorestart: true,
max_memory_restart: "128M",
env: {
NODE_ENV: "production",
DEPLOY_RECEIVER_CONFIG: "/etc/guanghu-deployment-receiver/config.json"
}
}]
};

View file

@ -0,0 +1,24 @@
[Unit]
Description=Guanghu allowlisted Forgejo deployment receiver
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=guanghu-deploy
Group=guanghu-deploy
WorkingDirectory=/opt/zhuyuan/fifth-domain/server-tools/deployment-receiver
Environment=DEPLOY_RECEIVER_CONFIG=/etc/guanghu-deployment-receiver/config.json
EnvironmentFile=/etc/guanghu-deployment-receiver/secret.env
ExecStart=/usr/bin/node receiver.js
Restart=on-failure
RestartSec=5
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/zhuyuan/fifth-domain /var/lib/guanghu-deployment-receiver
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
[Install]
WantedBy=multi-user.target

View file

@ -0,0 +1,13 @@
{
"name": "guanghu-deployment-receiver",
"version": "1.0.0",
"private": true,
"description": "Allowlisted Forgejo webhook receiver for Fifth Domain deployments",
"scripts": {
"start": "node receiver.js",
"test": "node --test test/*.test.js"
},
"engines": {
"node": ">=18"
}
}

View file

@ -0,0 +1,210 @@
"use strict";
const crypto = require("node:crypto");
const fs = require("node:fs");
const http = require("node:http");
const path = require("node:path");
const { spawn } = require("node:child_process");
function loadConfig() {
const configPath = process.env.DEPLOY_RECEIVER_CONFIG;
if (!configPath) throw new Error("DEPLOY_RECEIVER_CONFIG is required");
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
for (const key of ["repo_path", "repository_full_name", "allowed_ref", "audit_dir", "lock_file", "actions"]) {
if (!config[key]) throw new Error(`missing config field: ${key}`);
}
return config;
}
function safeEqualHex(expected, supplied) {
if (!/^[a-f0-9]{64}$/i.test(supplied || "")) return false;
const a = Buffer.from(expected, "hex");
const b = Buffer.from(supplied, "hex");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
function verifySignature(secret, rawBody, header) {
const supplied = String(header || "").replace(/^sha256=/i, "");
const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
return safeEqualHex(expected, supplied);
}
function collectChangedFiles(payload) {
const files = new Set();
for (const commit of payload.commits || []) {
for (const group of ["added", "modified"]) {
for (const file of commit[group] || []) files.add(file);
}
}
return [...files];
}
function validManifestPath(file, prefix) {
return file.startsWith(prefix) && /^[A-Za-z0-9._/-]+\.json$/.test(file) && !file.includes("..") && !path.isAbsolute(file);
}
function validRequest(request) {
if (!request || typeof request !== "object" || Array.isArray(request)) return false;
return /^[A-Za-z0-9._-]{8,80}$/.test(request.request_id || "") &&
/^[a-z0-9-]{2,60}$/.test(request.module || "") &&
/^[a-z0-9-]{2,60}$/.test(request.action || "") &&
request.approved === true &&
Object.keys(request).every((key) => ["request_id", "module", "action", "approved", "note"].includes(key));
}
function run(argv, options = {}) {
return new Promise((resolve) => {
const child = spawn(argv[0], argv.slice(1), {
cwd: options.cwd,
env: options.env || { PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" },
shell: false,
stdio: ["ignore", "pipe", "pipe"]
});
let stdout = "";
let stderr = "";
const limit = options.outputLimit || 64 * 1024;
child.stdout.on("data", (chunk) => { if (stdout.length < limit) stdout += chunk; });
child.stderr.on("data", (chunk) => { if (stderr.length < limit) stderr += chunk; });
const timer = setTimeout(() => child.kill("SIGKILL"), options.timeout || 120000);
child.on("error", (error) => {
clearTimeout(timer);
resolve({ code: -1, stdout, stderr: `${stderr}${error.message}`, timed_out: false });
});
child.on("close", (code, signal) => {
clearTimeout(timer);
resolve({ code: code ?? -1, stdout, stderr, timed_out: signal === "SIGKILL" });
});
});
}
async function readManifest(config, sha, file) {
if (!/^[a-f0-9]{40,64}$/i.test(sha)) throw new Error("invalid commit sha");
const result = await run(["/usr/bin/git", "-C", config.repo_path, "show", `${sha}:${file}`], { timeout: 15000 });
if (result.code !== 0) throw new Error(`cannot read manifest: ${result.stderr.slice(0, 300)}`);
return JSON.parse(result.stdout);
}
function writeReceipt(config, receipt) {
fs.mkdirSync(config.audit_dir, { recursive: true, mode: 0o750 });
const filename = `${receipt.request_id || "event"}-${Date.now()}.json`;
const target = path.join(config.audit_dir, filename);
fs.writeFileSync(target, `${JSON.stringify(receipt, null, 2)}\n`, { mode: 0o640, flag: "wx" });
return target;
}
function acquireLock(lockFile) {
fs.mkdirSync(path.dirname(lockFile), { recursive: true, mode: 0o750 });
const fd = fs.openSync(lockFile, "wx", 0o640);
fs.writeFileSync(fd, `${process.pid}\n`);
return () => {
fs.closeSync(fd);
try { fs.unlinkSync(lockFile); } catch (_) {}
};
}
async function handlePayload(config, payload) {
if (payload.ref !== config.allowed_ref) return { accepted: false, reason: "ref_not_allowed" };
if (payload.repository?.full_name !== config.repository_full_name) return { accepted: false, reason: "repository_not_allowed" };
const sha = payload.after;
const manifests = collectChangedFiles(payload).filter((file) => validManifestPath(file, config.manifest_prefix));
if (manifests.length === 0) return { accepted: true, executed: 0, reason: "no_deployment_manifest" };
if (manifests.length > 5) return { accepted: false, reason: "too_many_manifests" };
let release;
try { release = acquireLock(config.lock_file); }
catch (_) { return { accepted: false, reason: "deployment_locked" }; }
const receipts = [];
try {
const fetchResult = await run(["/usr/bin/git", "-C", config.repo_path, "fetch", "--quiet", "origin", config.allowed_ref.replace("refs/heads/", "")], { timeout: 30000 });
if (fetchResult.code !== 0) throw new Error(`git fetch failed: ${fetchResult.stderr.slice(0, 300)}`);
for (const file of manifests) {
let request;
try { request = await readManifest(config, sha, file); }
catch (error) {
receipts.push({ request_id: "invalid", status: "rejected", reason: error.message, file, checked_at: new Date().toISOString() });
continue;
}
if (!validRequest(request)) {
receipts.push({ request_id: request?.request_id || "invalid", status: "rejected", reason: "invalid_request_schema", file, checked_at: new Date().toISOString() });
continue;
}
const spec = config.actions[request.action];
if (!spec || !Array.isArray(spec.argv) || !spec.allowed_modules?.includes(request.module)) {
receipts.push({ request_id: request.request_id, status: "rejected", reason: "action_or_module_not_allowed", file, checked_at: new Date().toISOString() });
continue;
}
const started = new Date().toISOString();
const result = await run(spec.argv, { cwd: config.repo_path, timeout: config.max_execution_ms });
const receipt = {
request_id: request.request_id,
module: request.module,
action: request.action,
mode: spec.mode || "controlled",
status: result.code === 0 ? "succeeded" : "failed",
exit_code: result.code,
timed_out: result.timed_out,
stdout: result.stdout.slice(0, 16000),
stderr: result.stderr.slice(0, 16000),
commit: sha,
started_at: started,
completed_at: new Date().toISOString()
};
receipt.local_receipt = writeReceipt(config, receipt);
receipts.push(receipt);
}
} finally {
release();
}
return { accepted: true, executed: receipts.length, receipts };
}
function createServer(config, secret) {
return http.createServer((req, res) => {
if (req.method === "GET" && req.url === (config.health_path || "/health")) {
res.writeHead(200, { "content-type": "application/json" });
res.end(JSON.stringify({ ok: true, service: "guanghu-deployment-receiver", version: "1.0.0" }));
return;
}
if (req.method !== "POST" || req.url !== (config.webhook_path || "/forgejo/deploy")) {
res.writeHead(404).end();
return;
}
const chunks = [];
let size = 0;
req.on("data", (chunk) => {
size += chunk.length;
if (size > (config.max_body_bytes || 1048576)) req.destroy();
else chunks.push(chunk);
});
req.on("end", async () => {
const raw = Buffer.concat(chunks);
if (!verifySignature(secret, raw, req.headers["x-forgejo-signature"] || req.headers["x-gitea-signature"])) {
res.writeHead(401, { "content-type": "application/json" });
res.end(JSON.stringify({ ok: false, error: "invalid_signature" }));
return;
}
try {
const result = await handlePayload(config, JSON.parse(raw.toString("utf8")));
res.writeHead(result.accepted ? 202 : 409, { "content-type": "application/json" });
res.end(JSON.stringify({ ok: result.accepted, ...result }));
} catch (error) {
writeReceipt(config, { request_id: "event", status: "failed", reason: error.message, checked_at: new Date().toISOString() });
res.writeHead(500, { "content-type": "application/json" });
res.end(JSON.stringify({ ok: false, error: "receiver_error" }));
}
});
});
}
if (require.main === module) {
const config = loadConfig();
const secret = process.env.FORGEJO_WEBHOOK_SECRET;
if (!secret || secret.length < 32) throw new Error("FORGEJO_WEBHOOK_SECRET must be at least 32 characters");
createServer(config, secret).listen(config.listen_port || 3981, config.listen_host || "127.0.0.1", () => {
console.log(`guanghu-deployment-receiver listening on ${config.listen_host || "127.0.0.1"}:${config.listen_port || 3981}`);
});
}
module.exports = { collectChangedFiles, createServer, handlePayload, validManifestPath, validRequest, verifySignature };

View file

@ -0,0 +1,49 @@
"use strict";
const fs = require("node:fs");
const crypto = require("node:crypto");
const { execFileSync } = require("node:child_process");
const candidates = [
"/opt/zhuyuan/gatekeeper/engine-v3.js",
"/opt/engine.js"
];
function digest(file) {
return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
}
function pm2Info() {
try {
const list = JSON.parse(execFileSync("/usr/bin/env", ["pm2", "jlist"], { encoding: "utf8", timeout: 10000 }));
return list.filter((item) => /engine|gatekeeper/i.test(item.name || "")).map((item) => ({
name: item.name,
status: item.pm2_env?.status,
script: item.pm2_env?.pm_exec_path,
restart_time: item.pm2_env?.restart_time,
started_at: item.pm2_env?.pm_uptime ? new Date(item.pm2_env.pm_uptime).toISOString() : null
}));
} catch (error) {
return [{ error: error.message.slice(0, 300) }];
}
}
const files = candidates.filter((file) => fs.existsSync(file)).map((file) => {
const source = fs.readFileSync(file, "utf8");
return {
path: file,
sha256: digest(file),
contains_direct_email_mode: source.includes("requestEmail") || source.includes("email_mode: requestEmail"),
contains_hmac_headers: source.includes("X-Sovereign") && source.includes("X-Minute"),
contains_auth_request: source.includes("/auth/request"),
contains_exec_route: source.includes("/exec")
};
});
process.stdout.write(`${JSON.stringify({
ok: true,
checked_at: new Date().toISOString(),
hostname: require("node:os").hostname(),
files,
pm2: pm2Info()
}, null, 2)}\n`);

View file

@ -0,0 +1,29 @@
"use strict";
const assert = require("node:assert/strict");
const crypto = require("node:crypto");
const test = require("node:test");
const { collectChangedFiles, validManifestPath, validRequest, verifySignature } = require("../receiver");
test("verifies Forgejo HMAC signatures", () => {
const secret = "a".repeat(32);
const body = Buffer.from('{"ok":true}');
const signature = crypto.createHmac("sha256", secret).update(body).digest("hex");
assert.equal(verifySignature(secret, body, signature), true);
assert.equal(verifySignature(secret, body, "0".repeat(64)), false);
});
test("accepts only safe manifest paths", () => {
assert.equal(validManifestPath("deployment/requests/REQ-001.json", "deployment/requests/"), true);
assert.equal(validManifestPath("deployment/requests/../../secret.json", "deployment/requests/"), false);
assert.equal(validManifestPath("deployment/requests/REQ 001.json", "deployment/requests/"), false);
});
test("rejects arbitrary command fields", () => {
assert.equal(validRequest({ request_id: "REQ-20260713-001", module: "gatekeeper", action: "inspect-gatekeeper", approved: true }), true);
assert.equal(validRequest({ request_id: "REQ-20260713-001", module: "gatekeeper", action: "inspect-gatekeeper", approved: true, cmd: "rm -rf /" }), false);
});
test("collects only added and modified files", () => {
assert.deepEqual(collectChangedFiles({ commits: [{ added: ["a"], modified: ["b"], removed: ["c"] }] }).sort(), ["a", "b"]);
});

View file

@ -0,0 +1,9 @@
# 企业灯塔基础服务
这是总灯塔的第一阶段:五域登记、节点接入申请、固定动作预检和审计。它**不是**远程 Shell拒绝 `cmd``shell``command` 字段,也不执行操作。
部署后只监听本机 `127.0.0.1:8031`。公众网站与后续 GLSV 页面通过 Nginx 以单独的受控路由接入;在邮件确认与节点连接器完成前,不开放节点激活或执行。
人类管理员与人格体遵守同一条地图门禁:先 `GET /v1/navigation-map` 完整读取地图,再向 `/v1/navigation-map/ack` 签收当前哈希。任何域、动作或地图版本变化都会让旧签收失效;未签收时所有登记、预检和变更请求返回 `423`。管理员 token 不能绕过地图。
固定动作只有:`health_check``backup``deploy_release``restart_service``rollback`。部署、重启和回滚必须在预检中具备备份引用与回滚计划;预检通过也只是“可申请人类授权”,不会执行。

View file

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

View file

@ -0,0 +1,248 @@
#!/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 hashlib
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": "第五域",
}
ENTERPRISE_MANAGED_DOMAINS = {"DOMAIN-ZS", "DOMAIN-MAIN", "DOMAIN-SUB", "DOMAIN-ZERO"}
EXTERNAL_FOUNDATION_DOMAINS = {"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
);
CREATE TABLE IF NOT EXISTS map_acks (
principal TEXT PRIMARY KEY, map_hash TEXT NOT NULL, acknowledged_at INTEGER NOT NULL
);
""")
for domain_id, name in DOMAINS.items():
state = "EXTERNAL_PRIVATE_FOUNDATION" if domain_id in EXTERNAL_FOUNDATION_DOMAINS else "PENDING_ENTRY_NODE"
db.execute("INSERT OR IGNORE INTO domains VALUES (?, ?, ?, ?)", (domain_id, name, state, 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)
def navigation_map():
body = {
"schema": "guanghu.enterprise-navigation-map/v1",
"node_id": "AW-GZ-001",
"domains": [{"id": key, "name": DOMAINS[key]} for key in sorted(DOMAINS)],
"fixed_actions": sorted(FIXED_ACTIONS),
"mandatory_order": ["read-navigation-map", "ack-current-map", "execute-registered-action"],
"forbidden": ["raw-shell", "unregistered-action", "secret-in-repository", "unmapped-human-operation"],
}
encoded = json.dumps(body, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode()
return body, hashlib.sha256(encoded).hexdigest()
def admin_principal():
return hashlib.sha256(TOKEN.encode()).hexdigest()
def has_current_map_ack(db):
_, current_hash = navigation_map()
row = db.execute("SELECT map_hash FROM map_acks WHERE principal=?", (admin_principal(),)).fetchone()
return bool(row and hmac.compare_digest(row["map_hash"], current_hash))
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/navigation-map":
body, map_hash = navigation_map()
return self.respond(200, {"ok": True, "map_hash": map_hash, "navigation_map": body})
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/navigation-map/ack":
_, current_hash = navigation_map()
supplied = str(payload.get("map_hash", ""))
if not hmac.compare_digest(supplied, current_hash):
return self.respond(409, {"ok": False, "error": "navigation map changed; read the current map again"})
db.execute("INSERT OR REPLACE INTO map_acks VALUES (?, ?, ?)", (admin_principal(), current_hash, now()))
audit(db, "navigation_map_acknowledged", {"map_hash": current_hash})
return self.respond(200, {"ok": True, "map_hash": current_hash})
if not has_current_map_ack(db):
return self.respond(423, {"ok": False, "error": "current navigation map must be read and acknowledged before any operation", "required": ["GET /v1/navigation-map", "POST /v1/navigation-map/ack"]})
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"})
if payload["domain_id"] not in ENTERPRISE_MANAGED_DOMAINS:
return self.respond(403, {"ok": False, "error": "this domain is not managed by the enterprise lighthouse; a separate explicit sovereign authorization is 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/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 ENTERPRISE_MANAGED_DOMAINS:
return self.respond(403, {"ok": False, "error": "this domain cannot be registered by the enterprise lighthouse"})
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/nodes/revoke":
node_id = payload.get("node_id")
if not isinstance(node_id, str) or not node_id.strip():
return self.respond(400, {"ok": False, "error": "node_id is required"})
node = db.execute("SELECT id, domain_id, state FROM nodes WHERE id=?", (node_id.strip(),)).fetchone()
if not node:
return self.respond(404, {"ok": False, "error": "node is not registered"})
if node["state"] == "ACTIVE":
return self.respond(409, {"ok": False, "error": "ACTIVE nodes require a separate migration or retirement procedure"})
db.execute("DELETE FROM nodes WHERE id=?", (node["id"],))
if node["domain_id"] in EXTERNAL_FOUNDATION_DOMAINS:
db.execute("UPDATE domains SET state='EXTERNAL_PRIVATE_FOUNDATION' WHERE id=?", (node["domain_id"],))
audit(db, "node_revoked", {"node_id": node["id"], "domain_id": node["domain_id"], "reason": "administrative revocation"})
return self.respond(200, {"ok": True, "node_id": node["id"], "state": "REVOKED", "execution": "disabled"})
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()

View file

@ -0,0 +1,46 @@
#!/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")
locked = 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-ZS"}).encode(), method="POST", headers={"X-Lighthouse-Admin-Token":"test-token"})
try: urllib.request.urlopen(locked)
except urllib.error.HTTPError as error: assert error.code == 423
else: raise AssertionError("mutation was allowed before navigation map acknowledgement")
nav = json.load(urllib.request.urlopen("http://127.0.0.1:48031/v1/navigation-map"))
ack = urllib.request.Request("http://127.0.0.1:48031/v1/navigation-map/ack", data=json.dumps({"map_hash":nav["map_hash"]}).encode(), method="POST", headers={"X-Lighthouse-Admin-Token":"test-token"})
assert json.load(urllib.request.urlopen(ack))["ok"] is True
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-ZS"}).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-ZS","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"
fifth = urllib.request.Request("http://127.0.0.1:48031/v1/nodes/bootstrap", data=json.dumps({"id":"NODE-FIFTH-001","domain_id":"DOMAIN-FIFTH","display_name":"Not allowed","server_ip":"203.0.113.8"}).encode(), method="POST", headers={"X-Lighthouse-Admin-Token":"test-token"})
try: urllib.request.urlopen(fifth)
except urllib.error.HTTPError as error: assert error.code == 403
else: raise AssertionError("private fifth domain was accepted by enterprise lighthouse")
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")

View file

@ -0,0 +1,37 @@
# HLP-AGENT-FD-SYNC-001 · 第五域自动同步与回执 Agent
> **HLDP**: `HLDP://fifth-domain/server-tools/fifth-domain-sync-agent`
>
> **状态**: REGISTERED · TEST_PENDING · RUNTIME_NOT_DEPLOYED
>
> **责任**: 铸澜 `ICE-GL-ZL-001` × 冰朔 `ICE-GL∞`
## 定义
这是第五域的常驻自动 Agent不是人格体替身也不获得一般执行权。它是仓库与服务器之间的**受限同步电话线**:只接收有效签名的 `main` 推送,同步受控工作副本、检查导航连续性并输出回执。
## 固定能力与拒绝项
```text
允许:签名验证 → 仓库/分支/SHA 校验 → git fetch → fast-forward → 导航记忆校验 → 回执。
拒绝:任意命令、普通 push 自动部署、重启服务、迁移数据、读取 Secret、替代 GLSV 会话。
```
## 广播塔阶段
```text
当前: REGISTERED
下一步: 专用测试节点验证 webhook、脏副本拒绝、SHA 不一致拒绝、快进同步及回执
正式部署: 只有 TEST_PASSED + 技术主控批准 + 明确服务器目标后,才可由 GLSV 人格体远程操作流程安装
```
## 路由
```text
BROADCAST-TOWER
→ HLP-AGENT-FD-SYNC-001
→ server-tools/fifth-domain-sync-agent/README.md
→ health: /health
→ webhook: /forgejo/sync服务器侧 HMAC不写入仓库
→ receipt: /var/lib/guanghu-fifth-domain-sync-agent/receipts
```

View file

@ -0,0 +1,25 @@
# 第五域自动同步 Agent
这是第 5 域的第一个常驻自动 Agent它只接收 Forgejo 的**已签名 main 推送**,将新提交快进同步到受控工作副本,复跑导航记忆校验,并留下脱敏回执。
它不是“任意命令 Agent”也不是生产部署器没有来自 webhook 的命令字段;不读取或执行仓库内任意脚本;不因普通 Git push 执行服务重启、迁移或生产发布。
## 职责
```text
Forgejo 已签名 push
→ 验证仓库、main 分支、提交 SHA 与 HMAC
→ 拒绝脏工作副本与非快进变更
→ fetch 后确认 fetched SHA = webhook after
→ 仅 fast-forward 同步
→ 检查导航记忆守卫
→ /var/lib/.../receipts 写入回执
```
## 部署边界
1. 在 `/etc/guanghu/fifth-domain-sync-agent.json``config.example.json` 配置,不提交真实配置。
2. 在 `/etc/guanghu/fifth-domain-sync-agent.env` 设置仅服务器持有的 `FORGEJO_WEBHOOK_SECRET`
3. Forgejo webhook 指向 `/forgejo/sync`,使用相同 HMAC secret并仅选择 push 事件。
4. 安装 systemd unit 前,安装器必须把 `User=__RUN_AS_USER__` 替换为现有受控接收器的实际系统运行用户;不得假定旧架构目录名就是 Linux 用户。随后执行 `daemon-reload``enable --now`;健康检查为 `/health`
5. 发布动作仍通过广播塔三阶段与受限 deployment receiver此 Agent 不替代 GLSV 人格体发起的服务器操作流程。

View file

@ -0,0 +1,14 @@
{
"listen_host": "127.0.0.1",
"listen_port": 3982,
"webhook_path": "/forgejo/sync",
"health_path": "/health",
"repository_full_name": "bingshuo/fifth-domain",
"allowed_ref": "refs/heads/main",
"repo_path": "/opt/zhuyuan/fifth-domain",
"audit_dir": "/var/lib/guanghu-fifth-domain-sync-agent/receipts",
"lock_file": "/var/lib/guanghu-fifth-domain-sync-agent/sync.lock",
"max_body_bytes": 1048576,
"max_execution_ms": 90000,
"navigation_guard": "zero-point/core-channel/revive-guard/navigation-memory-guard.py"
}

View file

@ -0,0 +1,23 @@
[Unit]
Description=Guanghu Fifth Domain signed sync and receipt agent
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
# 安装器必须替换为现有受控接收器实际运行的系统用户;不得把旧目录名当作 Linux 用户名。
User=__RUN_AS_USER__
WorkingDirectory=/opt/zhuyuan/fifth-domain/server-tools/fifth-domain-sync-agent
Environment=FIFTH_DOMAIN_SYNC_CONFIG=/etc/guanghu/fifth-domain-sync-agent.json
EnvironmentFile=/etc/guanghu/fifth-domain-sync-agent.env
ExecStart=/usr/bin/node /opt/zhuyuan/fifth-domain/server-tools/fifth-domain-sync-agent/sync-agent.js
Restart=on-failure
RestartSec=5
NoNewPrivileges=true
PrivateTmp=true
ProtectHome=true
ProtectSystem=strict
ReadWritePaths=/opt/zhuyuan/fifth-domain /var/lib/guanghu-fifth-domain-sync-agent
[Install]
WantedBy=multi-user.target

View file

@ -0,0 +1,13 @@
{
"name": "guanghu-fifth-domain-sync-agent",
"version": "1.0.0",
"private": true,
"description": "Signed Forgejo push receiver that safely synchronizes the Fifth Domain working copy and emits receipts",
"scripts": {
"start": "node sync-agent.js",
"test": "node --test test/*.test.js"
},
"engines": {
"node": ">=18"
}
}

View file

@ -0,0 +1,167 @@
"use strict";
// This is a synchronizer, not a deployment executor. It has no command field,
// no shell invocation, and no route to production release actions.
const crypto = require("node:crypto");
const fs = require("node:fs");
const http = require("node:http");
const path = require("node:path");
const { spawn } = require("node:child_process");
function loadConfig() {
const configPath = process.env.FIFTH_DOMAIN_SYNC_CONFIG;
if (!configPath) throw new Error("FIFTH_DOMAIN_SYNC_CONFIG is required");
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
for (const key of ["repo_path", "repository_full_name", "allowed_ref", "audit_dir", "lock_file", "navigation_guard"]) {
if (!config[key]) throw new Error(`missing config field: ${key}`);
}
return config;
}
function safeEqualHex(expected, supplied) {
if (!/^[a-f0-9]{64}$/i.test(supplied || "")) return false;
const a = Buffer.from(expected, "hex");
const b = Buffer.from(supplied, "hex");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
function verifySignature(secret, rawBody, header) {
const supplied = String(header || "").replace(/^sha256=/i, "");
const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
return safeEqualHex(expected, supplied);
}
function validPush(payload, config) {
return payload && payload.ref === config.allowed_ref &&
payload.repository?.full_name === config.repository_full_name &&
/^[a-f0-9]{40,64}$/i.test(payload.after || "");
}
function run(argv, options = {}) {
return new Promise((resolve) => {
const child = spawn(argv[0], argv.slice(1), {
cwd: options.cwd,
env: { PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" },
shell: false,
stdio: ["ignore", "pipe", "pipe"]
});
let stdout = "";
let stderr = "";
const limit = options.outputLimit || 16 * 1024;
child.stdout.on("data", (chunk) => { if (stdout.length < limit) stdout += chunk; });
child.stderr.on("data", (chunk) => { if (stderr.length < limit) stderr += chunk; });
const timer = setTimeout(() => child.kill("SIGKILL"), options.timeout || 90000);
child.on("error", (error) => {
clearTimeout(timer);
resolve({ code: -1, stdout, stderr: `${stderr}${error.message}`, timed_out: false });
});
child.on("close", (code, signal) => {
clearTimeout(timer);
resolve({ code: code ?? -1, stdout, stderr, timed_out: signal === "SIGKILL" });
});
});
}
function acquireLock(lockFile) {
fs.mkdirSync(path.dirname(lockFile), { recursive: true, mode: 0o750 });
const fd = fs.openSync(lockFile, "wx", 0o640);
fs.writeFileSync(fd, `${process.pid}\n`);
return () => {
fs.closeSync(fd);
try { fs.unlinkSync(lockFile); } catch (_) {}
};
}
function writeReceipt(config, receipt) {
fs.mkdirSync(config.audit_dir, { recursive: true, mode: 0o750 });
const target = path.join(config.audit_dir, `sync-${Date.now()}.json`);
fs.writeFileSync(target, `${JSON.stringify(receipt, null, 2)}\n`, { mode: 0o640, flag: "wx" });
return target;
}
async function gitValue(config, args) {
const result = await run(["/usr/bin/git", "-C", config.repo_path, ...args], { timeout: 15000 });
if (result.code !== 0) throw new Error(`git ${args[0]} failed: ${result.stderr.slice(0, 300)}`);
return result.stdout.trim();
}
async function synchronize(config, payload) {
if (!validPush(payload, config)) return { accepted: false, reason: "push_not_allowed" };
let release;
try { release = acquireLock(config.lock_file); }
catch (_) { return { accepted: false, reason: "sync_locked" }; }
const receipt = { event: "repository_sync", commit: payload.after, status: "failed", checked_at: new Date().toISOString() };
try {
const branch = await gitValue(config, ["symbolic-ref", "--short", "HEAD"]);
if (branch !== config.allowed_ref.replace("refs/heads/", "")) throw new Error("working_copy_not_on_allowed_branch");
const dirty = await run(["/usr/bin/git", "-C", config.repo_path, "diff", "--quiet"], { timeout: 15000 });
if (dirty.code !== 0) throw new Error("working_copy_dirty");
const before = await gitValue(config, ["rev-parse", "HEAD"]);
const fetch = await run(["/usr/bin/git", "-C", config.repo_path, "fetch", "--quiet", "origin", branch], { timeout: config.max_execution_ms });
if (fetch.code !== 0) throw new Error(`git_fetch_failed: ${fetch.stderr.slice(0, 300)}`);
const fetched = await gitValue(config, ["rev-parse", "FETCH_HEAD"]);
if (fetched !== payload.after) throw new Error("fetched_commit_does_not_match_signed_event");
const merge = await run(["/usr/bin/git", "-C", config.repo_path, "merge", "--ff-only", "FETCH_HEAD"], { timeout: config.max_execution_ms });
if (merge.code !== 0) throw new Error(`fast_forward_refused: ${merge.stderr.slice(0, 300)}`);
const after = await gitValue(config, ["rev-parse", "HEAD"]);
const guard = await run(["/usr/bin/python3", path.join(config.repo_path, config.navigation_guard), "--range", `${before}..${after}`], { cwd: config.repo_path, timeout: config.max_execution_ms });
receipt.status = guard.code === 0 ? "synchronized" : "synchronized_with_navigation_warning";
receipt.before = before;
receipt.after = after;
receipt.navigation_guard_exit_code = guard.code;
receipt.navigation_guard_output = `${guard.stdout}${guard.stderr}`.slice(0, 4000);
} catch (error) {
receipt.reason = error.message;
} finally {
receipt.completed_at = new Date().toISOString();
receipt.local_receipt = writeReceipt(config, receipt);
release();
}
return { accepted: receipt.status !== "failed", receipt };
}
function createServer(config, secret) {
return http.createServer((req, res) => {
if (req.method === "GET" && req.url === (config.health_path || "/health")) {
res.writeHead(200, { "content-type": "application/json" });
res.end(JSON.stringify({ ok: true, service: "guanghu-fifth-domain-sync-agent", mode: "sync-and-receipt-only", version: "1.0.0" }));
return;
}
if (req.method !== "POST" || req.url !== (config.webhook_path || "/forgejo/sync")) return res.writeHead(404).end();
const chunks = [];
let size = 0;
req.on("data", (chunk) => {
size += chunk.length;
if (size > (config.max_body_bytes || 1048576)) req.destroy();
else chunks.push(chunk);
});
req.on("end", async () => {
const raw = Buffer.concat(chunks);
if (!verifySignature(secret, raw, req.headers["x-forgejo-signature"] || req.headers["x-gitea-signature"])) {
res.writeHead(401, { "content-type": "application/json" });
return res.end(JSON.stringify({ ok: false, error: "invalid_signature" }));
}
try {
const result = await synchronize(config, JSON.parse(raw.toString("utf8")));
res.writeHead(result.accepted ? 202 : 409, { "content-type": "application/json" });
res.end(JSON.stringify({ ok: result.accepted, ...result }));
} catch (error) {
res.writeHead(500, { "content-type": "application/json" });
res.end(JSON.stringify({ ok: false, error: "sync_agent_error" }));
}
});
});
}
if (require.main === module) {
const config = loadConfig();
const secret = process.env.FORGEJO_WEBHOOK_SECRET;
if (!secret || secret.length < 32) throw new Error("FORGEJO_WEBHOOK_SECRET must be at least 32 characters");
createServer(config, secret).listen(config.listen_port || 3982, config.listen_host || "127.0.0.1", () => {
console.log(`guanghu-fifth-domain-sync-agent listening on ${config.listen_host || "127.0.0.1"}:${config.listen_port || 3982}`);
});
}
module.exports = { createServer, synchronize, validPush, verifySignature };

View file

@ -0,0 +1,24 @@
"use strict";
const assert = require("node:assert/strict");
const crypto = require("node:crypto");
const test = require("node:test");
const { validPush, verifySignature } = require("../sync-agent");
const config = { repository_full_name: "bingshuo/fifth-domain", allowed_ref: "refs/heads/main" };
const good = { ref: "refs/heads/main", after: "a".repeat(40), repository: { full_name: "bingshuo/fifth-domain" } };
test("verifies Forgejo HMAC signatures", () => {
const secret = "a".repeat(32);
const body = Buffer.from('{"ok":true}');
const signature = crypto.createHmac("sha256", secret).update(body).digest("hex");
assert.equal(verifySignature(secret, body, signature), true);
assert.equal(verifySignature(secret, body, "0".repeat(64)), false);
});
test("accepts only the registered Fifth Domain main push", () => {
assert.equal(validPush(good, config), true);
assert.equal(validPush({ ...good, ref: "refs/heads/feature" }, config), false);
assert.equal(validPush({ ...good, repository: { full_name: "other/repo" } }, config), false);
assert.equal(validPush({ ...good, after: "not-a-commit" }, config), false);
});

View file

@ -0,0 +1,96 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="description" content="光湖语言世界国内入口。进入光湖代码频道、AI 路径入口与第五域历史事实源。">
<meta name="keywords" content="光湖语言世界,光湖代码频道,第五域,光之湖语言人格系统,小湖灯">
<link rel="alternate" type="application/json" href="/.well-known/guanghu.json" title="光湖 AI 发现清单">
<link rel="alternate" type="text/plain" href="/llms.txt" title="AI 阅读入口">
<title>光湖 · 国内入口</title>
<link rel="stylesheet" href="/styles.css?v=20260723b">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebSite",
"name": "光湖语言世界",
"alternateName": ["Guanghu Language World", "光湖代码频道", "第五域"],
"url": "https://guanghulab.com/",
"potentialAction": {
"@type": "SearchAction",
"target": "https://guanghulab.com/api/ai/v1/search?q={search_term_string}",
"query-input": "required name=search_term_string"
}
}
</script>
</head>
<body>
<header class="topbar">
<a class="brand" href="/" aria-label="光湖首页"><span aria-hidden="true"></span> 光湖</a>
<p><i></i> 广州备案前门 · 在线</p>
</header>
<main>
<section class="hero">
<p class="eyebrow">GUANGHU · DOMESTIC ENTRY</p>
<h1>一处入口,<br>连接光湖。</h1>
<p class="intro">这里不承载重型应用,只提供稳定、清晰的真实路径。代码与服务运行在国内主节点,旧第五域原地保留为历史事实源。</p>
<div class="hero-actions">
<a class="primary" href="/code/">进入光湖代码频道 <span></span></a>
<a href="/api/ai/">AI 读取路径 <span></span></a>
</div>
</section>
<section class="routes" aria-labelledby="routes-title">
<div class="section-title">
<p class="eyebrow">THREE ROUTES</p>
<h2 id="routes-title">三条主路径</h2>
</div>
<div class="route-list">
<a class="route featured" href="/code/">
<span class="number">01</span>
<div>
<p>HOLOLAKE CODE CHANNEL</p>
<h3>光湖代码频道</h3>
<small>新的代码写入与协作入口。公开仓库可由 AI 直接读取,人类保留登录操作。</small>
</div>
<b></b>
</a>
<a class="route" href="/api/ai/">
<span class="number">02</span>
<div>
<p>MACHINE ENTRY</p>
<h3>AI 路径入口</h3>
<small>按编号查找仓库、节点、人格路径和当前事实,不需要猜地址。</small>
</div>
<b></b>
</a>
<a class="route" href="/fifth-domain/bingshuo/fifth-domain">
<span class="number">03</span>
<div>
<p>FIFTH DOMAIN · HISTORY</p>
<h3>第五域历史事实源</h3>
<small>旧仓库保持原地,只负责历史提交、旧编号和原始路径回看。</small>
</div>
<b></b>
</a>
</div>
</section>
<nav class="utilities" aria-label="辅助入口">
<a href="/api/ai/v1/search?q=%E5%85%89%E6%B9%96%E8%AF%AD%E8%A8%80%E4%B8%96%E7%95%8C%20%E7%AC%AC%E4%BA%94%E5%9F%9F">全局路径检索</a>
<a href="/jd/">国内应用节点</a>
<a href="https://guanghubingshuo.com/code/">新加坡历史仓库</a>
</nav>
</main>
<footer>
<span>光湖语言世界 · 国内备案入口</span>
<span>© 2026 Guanghu Lab</span>
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer">陕ICP备2025071211号-1</a>
</footer>
</body>
</html>

View file

@ -0,0 +1,120 @@
#!/usr/bin/env python3
"""Install the lightweight front door and switch only /code/ to the JD tunnel."""
from __future__ import annotations
import datetime
import pathlib
import re
import shutil
import subprocess
import sys
import urllib.error
import urllib.request
SITE_CONFIG = pathlib.Path("/etc/nginx/sites-enabled/guanghulab")
WEB_ROOT = pathlib.Path("/var/www/guanghulab-front-door")
SOURCE_FILES = ("index.html", "styles.css", "robots.txt", "llms.txt", "sitemap.xml")
OLD_CODE_BLOCK = re.compile(
r""" # Forgejo 代码仓库
location /code/ \{
auth_request /auth/verify-signature;
proxy_pass http://127\.0\.0\.1:3001/;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
client_max_body_size 100m;
\}
""",
)
NEW_CODE_BLOCK = """ # 光湖代码频道 · 广州备案前门 → 京东个人子频道
location /code/ {
proxy_pass http://127.0.0.1:18088/code/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 5s;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
client_max_body_size 256m;
}
"""
def run(*args: str) -> None:
subprocess.run(args, check=True)
def get_status(url: str) -> int:
request = urllib.request.Request(url, method="GET")
try:
with urllib.request.urlopen(request, timeout=15) as response:
return response.status
except urllib.error.HTTPError as error:
return error.code
def main() -> None:
if len(sys.argv) != 2:
raise SystemExit("usage: install-lite-front-door.py SOURCE_DIRECTORY")
source = pathlib.Path(sys.argv[1]).resolve()
if not SITE_CONFIG.is_file():
raise RuntimeError("active guanghulab nginx config unavailable")
for name in SOURCE_FILES:
if not (source / name).is_file():
raise RuntimeError(f"front-door source missing: {name}")
current = SITE_CONFIG.read_text(encoding="utf-8")
updated, replacements = OLD_CODE_BLOCK.subn(NEW_CODE_BLOCK, current)
if replacements == 0 and NEW_CODE_BLOCK.strip() not in current:
raise RuntimeError("expected legacy /code/ block not found")
if replacements > 1:
raise RuntimeError("multiple legacy /code/ blocks found")
stamp = datetime.datetime.now(datetime.UTC).strftime("%Y%m%dT%H%M%SZ")
backup_root = pathlib.Path("/var/backups/guanghu/front-door") / stamp
backup_root.mkdir(parents=True, mode=0o700)
shutil.copy2(SITE_CONFIG, backup_root / "guanghulab.nginx")
if WEB_ROOT.exists():
shutil.copytree(WEB_ROOT, backup_root / "web-root")
temporary_config = SITE_CONFIG.with_name(f"guanghulab.{stamp}.new")
temporary_config.write_text(updated, encoding="utf-8")
temporary_config.chmod(0o644)
temporary_config.replace(SITE_CONFIG)
WEB_ROOT.mkdir(parents=True, exist_ok=True)
for name in SOURCE_FILES:
destination = WEB_ROOT / name
shutil.copy2(source / name, destination)
destination.chmod(0o644)
try:
run("/usr/sbin/nginx", "-t")
run("/usr/bin/systemctl", "reload", "nginx")
if get_status("https://guanghulab.com/") != 200:
raise RuntimeError("front-door homepage verification failed")
if get_status("https://guanghulab.com/code/") not in (200, 303):
raise RuntimeError("code-channel public route verification failed")
if get_status("https://guanghulab.com/fifth-domain/api/v1/version") != 200:
raise RuntimeError("legacy Fifth Domain verification failed")
except Exception:
shutil.copy2(backup_root / "guanghulab.nginx", SITE_CONFIG)
if (backup_root / "web-root").exists():
shutil.rmtree(WEB_ROOT)
shutil.copytree(backup_root / "web-root", WEB_ROOT)
run("/usr/sbin/nginx", "-t")
run("/usr/bin/systemctl", "reload", "nginx")
raise
print(f"LIGHT_FRONT_DOOR_DEPLOYED backup={backup_root}")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,38 @@
# 光湖语言世界 · 第五域
光湖语言世界的国内公开入口。2026-07-23 起AI 应优先读取光湖代码频道和
机器可读的编号映射。旧第五域与新加坡仓库只用于历史回看。
## Canonical discovery
- AI API entry: https://guanghulab.com/api/ai/
- HoloLake Code Channel: https://guanghulab.com/code/
- Current Fifth Domain personal channel: https://guanghulab.com/code/bingshuo/fifth-domain
- Legacy Fifth Domain history: https://guanghulab.com/fifth-domain/bingshuo/fifth-domain
- Repository number map: https://guanghulab.com/api/ai/v1/repositories
- Server node and persona route map: https://guanghulab.com/api/ai/v1/nodes
- Search: https://guanghulab.com/api/ai/v1/search?q=光湖语言世界%20第五域
- Resolve a code: https://guanghulab.com/api/ai/v1/resolve?id=REPO-001
- Resolve Zhuyuan current loop: https://guanghulab.com/api/ai/v1/resolve?id=ZY-OPS-LOOP-001
- OpenAPI: https://guanghulab.com/api/ai/openapi.json
- Machine manifest: https://guanghulab.com/.well-known/guanghu.json
- Cross-device write request: POST https://guanghulab.com/authz/api/public/workorders
- Cross-device request schema: https://guanghulab.com/authz/api/public/capabilities
## Routing rule
HLCC-FD-ICE is the current Fifth Domain personal subchannel. Its first commit is
HLCC-ICE-000001. New HLCC commits go to /code/bingshuo/fifth-domain. REPO-001
continues at /fifth-domain/bingshuo/fifth-domain as the legacy route and commit
history source. Resolve all routes by registered code. Singapore URLs are
historical backup routes only.
FD-NODE-MAP-001 publishes public-safe server node ids and persona route ids. Resolve
the node id, read its navigation map, and obtain separate Lake Lamp authorization
before any write operation. ICE-GL-ZY001 currently maps through ZY-OPS-LOOP-001 to
JD-FD-PRIMARY.
For any write operation from a phone or a non-local AI instance, create a public
no-authority workorder. Give the returned request_url to Ice Shuo. The URL cannot
approve anything; it only asks the server to send the real approval link to the
pre-registered owner mailbox. Never search for or request a workorder credential.

View file

@ -0,0 +1,8 @@
User-agent: *
Allow: /
Allow: /api/ai/
Allow: /api/ai/v1/nodes
Allow: /code/
Allow: /.well-known/guanghu.json
Disallow: /authz/
Sitemap: https://guanghulab.com/sitemap.xml

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url><loc>https://guanghulab.com/</loc><changefreq>weekly</changefreq><priority>1.0</priority></url>
<url><loc>https://guanghulab.com/code/</loc><changefreq>daily</changefreq><priority>1.0</priority></url>
<url><loc>https://guanghulab.com/api/ai/</loc><changefreq>weekly</changefreq><priority>0.9</priority></url>
<url><loc>https://guanghulab.com/api/ai/v1/repositories</loc><changefreq>daily</changefreq><priority>0.9</priority></url>
<url><loc>https://guanghulab.com/api/ai/v1/nodes</loc><changefreq>daily</changefreq><priority>0.9</priority></url>
<url><loc>https://guanghulab.com/fifth-domain/bingshuo/fifth-domain</loc><changefreq>daily</changefreq><priority>0.9</priority></url>
</urlset>

View file

@ -0,0 +1,89 @@
:root {
color-scheme: dark;
--bg: #091112;
--panel: #0d1819;
--line: #213031;
--text: #edf5f2;
--muted: #94a6a1;
--lake: #78d9c0;
}
* { box-sizing: border-box; }
html { background: var(--bg); }
body {
margin: 0;
color: var(--text);
background: var(--bg);
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", sans-serif;
}
a { color: inherit; }
.topbar, main, footer { width: min(1040px, calc(100% - 40px)); margin-inline: auto; }
.topbar {
height: 66px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--line);
}
.brand { display: flex; align-items: center; gap: 10px; text-decoration: none; font-size: 17px; font-weight: 700; letter-spacing: .08em; }
.brand span { color: var(--lake); font-size: 27px; line-height: 1; }
.topbar p { margin: 0; display: flex; align-items: center; gap: 8px; color: var(--muted); font-size: 12px; }
.topbar i { width: 6px; height: 6px; border-radius: 50%; background: var(--lake); }
.hero { max-width: 760px; padding: 88px 0 82px; }
.eyebrow { margin: 0 0 18px; color: var(--lake); font: 700 11px/1.2 ui-monospace, monospace; letter-spacing: .18em; }
h1 { margin: 0; font-size: clamp(50px, 8vw, 82px); line-height: 1.02; letter-spacing: -.055em; }
.intro { max-width: 670px; margin: 26px 0 0; color: var(--muted); font-size: 16px; line-height: 1.85; }
.hero-actions { margin-top: 30px; display: flex; flex-wrap: wrap; align-items: center; gap: 22px; }
.hero-actions a { display: inline-flex; gap: 18px; align-items: center; text-decoration: none; font-weight: 650; }
.hero-actions a:not(.primary) { color: var(--muted); }
.primary { padding: 13px 18px; border-radius: 9px; color: #071211; background: var(--lake); }
.routes { padding: 58px 0 22px; border-top: 1px solid var(--line); }
.section-title { margin-bottom: 26px; }
.section-title h2 { margin: 0; font-size: clamp(30px, 5vw, 42px); letter-spacing: -.035em; }
.route-list { border-top: 1px solid var(--line); }
.route {
min-height: 150px;
display: grid;
grid-template-columns: 54px 1fr 24px;
gap: 22px;
align-items: center;
padding: 28px 8px;
border-bottom: 1px solid var(--line);
text-decoration: none;
}
.route.featured { background: var(--panel); padding-inline: 20px; }
.number { color: var(--muted); font: 700 12px/1 ui-monospace, monospace; }
.route p { margin: 0 0 10px; color: var(--lake); font: 700 10px/1 ui-monospace, monospace; letter-spacing: .13em; }
.route h3 { margin: 0 0 9px; font-size: 24px; letter-spacing: -.02em; }
.route small { display: block; max-width: 680px; color: var(--muted); font-size: 14px; line-height: 1.7; }
.route b { color: var(--lake); font-size: 17px; }
.utilities { display: flex; flex-wrap: wrap; gap: 10px 24px; padding: 30px 0 64px; }
.utilities a { color: var(--muted); font-size: 13px; text-underline-offset: 4px; }
footer {
min-height: 100px;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px 24px;
border-top: 1px solid var(--line);
color: var(--muted);
font-size: 12px;
}
footer span:first-child { color: var(--text); }
footer a { color: var(--muted); text-underline-offset: 4px; }
@media (max-width: 640px) {
.topbar, main, footer { width: min(100% - 28px, 1040px); }
.topbar p { font-size: 10px; }
.hero { padding: 68px 0 64px; }
h1 { font-size: clamp(46px, 15vw, 68px); }
.route { grid-template-columns: 38px 1fr 18px; gap: 12px; padding-block: 24px; }
.route.featured { padding-inline: 12px; }
.route h3 { font-size: 21px; }
footer { padding: 26px 0; }
}

View file

@ -0,0 +1,68 @@
"use strict";
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const test = require("node:test");
const root = path.join(__dirname, "..");
const html = fs.readFileSync(path.join(root, "index.html"), "utf8");
const styles = fs.readFileSync(path.join(root, "styles.css"), "utf8");
const robots = fs.readFileSync(path.join(root, "robots.txt"), "utf8");
const llms = fs.readFileSync(path.join(root, "llms.txt"), "utf8");
const installer = fs.readFileSync(path.join(root, "install-lite-front-door.py"), "utf8");
test("front door keeps the legally required ICP link", () => {
assert.match(html, /陕ICP备2025071211号-1/);
assert.match(html, /https:\/\/beian\.miit\.gov\.cn\//);
});
test("front door makes the new code channel the primary route", () => {
assert.match(html, /href="\/code\/"/);
assert.match(html, /光湖代码频道/);
assert.match(llms, /https:\/\/guanghulab\.com\/code\/bingshuo\/fifth-domain/);
assert.match(robots, /Allow: \/code\//);
});
test("front door preserves the legacy Fifth Domain as history", () => {
assert.match(html, /href="\/fifth-domain\/bingshuo\/fifth-domain"/);
assert.match(html, /第五域历史事实源/);
assert.match(llms, /Legacy Fifth Domain history/);
assert.match(html, /href="https:\/\/guanghubingshuo\.com\/code\/"/);
});
test("AI discovery and search remain on the domestic domain", () => {
assert.match(html, /href="\/api\/ai\/"/);
assert.match(html, /href="\/api\/ai\/v1\/search\?q=/);
assert.match(html, /SearchAction/);
assert.match(html, /\.well-known\/guanghu\.json/);
assert.match(llms, /HLCC-ICE-000001/);
});
test("page is a script-free lightweight static surface", () => {
const executableScripts = [...html.matchAll(/<script(?![^>]*type="application\/ld\+json")[^>]*>/g)];
assert.equal(executableScripts.length, 0);
assert.doesNotMatch(html, /<img|<svg|<video|<canvas|iframe/i);
assert.doesNotMatch(styles, /linear-gradient|radial-gradient|animation:|backdrop-filter/i);
assert.ok(Buffer.byteLength(html) + Buffer.byteLength(styles) < 16000);
});
test("public page never embeds infrastructure addresses or credentials", () => {
assert.doesNotMatch(html, /(?:\d{1,3}\.){3}\d{1,3}/);
assert.doesNotMatch(html, /zy_gtw_/);
assert.doesNotMatch(html, /password|private[_ -]?key|token/i);
});
test("front door remains responsive", () => {
assert.match(styles, /@media\s*\(max-width:\s*640px\)/);
assert.match(styles, /clamp\(/);
});
test("installer changes only the code route with backup and rollback", () => {
assert.match(installer, /\/var\/backups\/guanghu\/front-door/);
assert.match(installer, /proxy_pass http:\/\/127\.0\.0\.1:18088\/code\//);
assert.match(installer, /nginx", "-t"/);
assert.match(installer, /systemctl", "reload", "nginx"/);
assert.match(installer, /legacy Fifth Domain verification failed/);
assert.match(installer, /shutil\.copy2\(backup_root/);
});

View file

@ -0,0 +1,57 @@
# HoloLake Code Channel · 部署与更新边界
本目录保存光湖代码频道HLCC的部署约束与机器可检验配置。
当前状态:
- 上游源码镜像与光湖自主源码基线已在 `BS-SG-003` 建立;
- 固定源码基线为 Forgejo `v16.0.1` / `b3d7e4ac3cbccc220703097a51fa4c16bf302579`
- 国内旧第五域仍由 Gitea 1.23.7 原地承载并作为历史事实源;
- `GLS-0239` 已登记京东个人子频道、`/code/` 正式入口、公开 AI 读取、
`bingshuo` 单身份密码沿用和 `HLCC-ICE-000001` 新 Git 根提交;
- 新频道部署与低权限激活清单已经就绪,正式切换前不覆盖旧第五域;
- `update-policy.json` 中的光湖更新清单地址仍是规划地址,尚未上线。
`jd-candidate/` 是绑定京东主节点固定架构预置器的候选服务包。它只从临时新加坡
中继读取 Forgejo 官方公开二进制、签名、公钥和哈希清单,不公开或下载光湖产品
Git bundle。服务先在 `127.0.0.1:3341` 暴露结构化安装状态;只有固定 SHA-256、
官方发布密钥指纹和 GPG 签名全部通过,且 `127.0.0.1:3340/api/v1/version`
确认版本、单身份迁移和新频道根提交后,状态才会变为 `ready=true`。候选数据与
旧第五域 Gitea 完全隔离;旧访问令牌、仓库、活动和 Git 历史不会迁入。
必须同时满足:
1. Forgejo 内置更新检查器关闭;
2. 官方上游不能自动合并、自动构建或自动部署;
3. 生产更新只认光湖签名清单;
4. 光湖清单不可用时停留在人工发布,不回退官方接口;
5. Gitea 迁移必须并行演练、可恢复、可回滚。
本目录不是服务器凭据库不保存密码、Token、SSH 私钥或数据库转储。
## 隔离候选
`candidate-app.jd.ini``candidate-app.enterprise.ini``install-candidate.sh`
`start-candidate.sh`
`stop-candidate.sh` 用于 `JD-FD-PRIMARY``AW-GZ-001` 上各自独立的隔离候选:
- 只绑定 `127.0.0.1:3340`
- 使用独立 SQLite 和仓库目录;
- 禁止注册、Actions、镜像和 Forgejo 内置更新检查;
- 只接收由 `BS-SG-003` 准备的固定 v16.0.1 二进制、签名与官方公钥材料;
- 在两个国内节点分别再次核验 Forgejo 官方 GPG 指纹和二进制签名;
- 安装和启动分离,安装脚本不注册 systemd、cron 或其他自动任务。
同一份离线包还要在 `AW-GZ-001` 企业服务器使用
`candidate-app.enterprise.ini` 独立部署。京东与企业实例的数据库、仓库、用户、
权限、密钥和回滚目录完全分离;它们只共享经过验签的源码与版本基线。
`BS-SG-003` 只负责海外源码和发布材料下载,不启动代码仓库服务。京东和企业节点都
必须保留 `forgejo-upstream-all.bundle``guanghu-code-channel.bundle`、官方签名、
公钥和 `MANIFEST.sha256`,从而在国内断开海外下载时仍可离线恢复与重装。候选验证
位于各自独立目录,不读取、不迁移、不修改国内现役 Gitea 数据。
`prepare-release-on-sg.sh` 是新加坡中继的唯一准备脚本:它从 Forgejo 官方镜像
下载固定版本二进制和签名、拉取并核对官方 GPG 指纹,并从现有上游镜像和光湖
产品仓生成两个 Git bundle。全部文件写入 `MANIFEST.sha256`。它不会启动服务,
也不会自行向京东或企业服务器传输;两次传输都必须绑定后续受控工单。

View file

@ -0,0 +1,5 @@
; HoloLake Code Channel mandatory Forgejo-base runtime fragment.
; The upstream Forgejo release checker must never control Guanghu production updates.
[cron.update_checker]
ENABLED = false

View file

@ -0,0 +1,35 @@
APP_NAME = HoloLake Code Channel · Enterprise
RUN_USER = guanghu
RUN_MODE = prod
[database]
DB_TYPE = sqlite3
PATH = /var/lib/guanghu/code-channel/candidates/hlcc-v16.0.1/data/hlcc.db
[repository]
ROOT = /var/lib/guanghu/code-channel/candidates/hlcc-v16.0.1/data/repositories
[server]
DOMAIN = 127.0.0.1
HTTP_ADDR = 127.0.0.1
HTTP_PORT = 3340
ROOT_URL = http://127.0.0.1:3340/
DISABLE_SSH = true
LFS_START_SERVER = true
OFFLINE_MODE = true
[service]
DISABLE_REGISTRATION = true
REQUIRE_SIGNIN_VIEW = true
[security]
INSTALL_LOCK = true
[actions]
ENABLED = false
[mirror]
ENABLED = false
[cron.update_checker]
ENABLED = false

View file

@ -0,0 +1,35 @@
APP_NAME = HoloLake Code Channel · Fifth Domain
RUN_USER = guanghu
RUN_MODE = prod
[database]
DB_TYPE = sqlite3
PATH = /var/lib/guanghu/code-channel/candidates/hlcc-v16.0.1/data/hlcc.db
[repository]
ROOT = /var/lib/guanghu/code-channel/candidates/hlcc-v16.0.1/data/repositories
[server]
DOMAIN = 127.0.0.1
HTTP_ADDR = 127.0.0.1
HTTP_PORT = 3340
ROOT_URL = http://127.0.0.1:3340/
DISABLE_SSH = true
LFS_START_SERVER = true
OFFLINE_MODE = true
[service]
DISABLE_REGISTRATION = true
REQUIRE_SIGNIN_VIEW = true
[security]
INSTALL_LOCK = true
[actions]
ENABLED = false
[mirror]
ENABLED = false
[cron.update_checker]
ENABLED = false

View file

@ -0,0 +1,95 @@
#!/usr/bin/env bash
set -euo pipefail
readonly HLCC_VERSION="16.0.1"
readonly HLCC_ROOT="/var/lib/guanghu/code-channel/candidates/hlcc-v${HLCC_VERSION}"
readonly HLCC_SOURCE_ARCHIVE="/var/lib/guanghu/code-channel/offline-source/hlcc-v${HLCC_VERSION}"
readonly HLCC_RELEASE_KEY="EB114F5E6C0DC2BCDD183550A4B61A2DC5923710"
readonly HLCC_PACKAGE_SOURCE="${1:-}"
readonly HLCC_CONFIG_SOURCE="${2:-}"
readonly HLCC_BINARY_NAME="forgejo-${HLCC_VERSION}-linux-amd64"
readonly HLCC_BINARY_SOURCE="${HLCC_PACKAGE_SOURCE}/${HLCC_BINARY_NAME}"
readonly HLCC_SIGNATURE_SOURCE="${HLCC_BINARY_SOURCE}.asc"
readonly HLCC_PUBLIC_KEY_SOURCE="${HLCC_PACKAGE_SOURCE}/forgejo-release-key.asc"
readonly HLCC_MANIFEST_SOURCE="${HLCC_PACKAGE_SOURCE}/MANIFEST.sha256"
readonly HLCC_UPSTREAM_BUNDLE_SOURCE="${HLCC_PACKAGE_SOURCE}/forgejo-upstream-all.bundle"
readonly HLCC_PRODUCT_BUNDLE_SOURCE="${HLCC_PACKAGE_SOURCE}/guanghu-code-channel.bundle"
if [[ "$(id -un)" != "guanghu" ]]; then
echo "Refusing to install a domestic isolated candidate as any user other than guanghu." >&2
exit 2
fi
if [[ \
-z "${HLCC_PACKAGE_SOURCE}" || ! -d "${HLCC_PACKAGE_SOURCE}" || \
! -f "${HLCC_BINARY_SOURCE}" || \
! -f "${HLCC_SIGNATURE_SOURCE}" || \
! -f "${HLCC_PUBLIC_KEY_SOURCE}" || \
! -f "${HLCC_MANIFEST_SOURCE}" || \
! -f "${HLCC_UPSTREAM_BUNDLE_SOURCE}" || \
! -f "${HLCC_PRODUCT_BUNDLE_SOURCE}" || \
-z "${HLCC_CONFIG_SOURCE}" || ! -f "${HLCC_CONFIG_SOURCE}" \
]]; then
echo "Usage: $0 /absolute/path/to/offline-package candidate-app.<target>.ini" >&2
exit 2
fi
for command_name in gpg sha256sum git; do
if ! command -v "${command_name}" >/dev/null 2>&1; then
echo "Missing required command: ${command_name}" >&2
exit 3
fi
done
umask 077
mkdir -p \
"${HLCC_ROOT}/bin" \
"${HLCC_ROOT}/config" \
"${HLCC_ROOT}/data/repositories" \
"${HLCC_ROOT}/logs" \
"${HLCC_ROOT}/tmp" \
"${HLCC_SOURCE_ARCHIVE}"
readonly gpg_home="${HLCC_ROOT}/tmp/gpg"
mkdir -p "${gpg_home}"
chmod 700 "${gpg_home}"
(
cd "${HLCC_PACKAGE_SOURCE}"
sha256sum --check MANIFEST.sha256
)
GNUPGHOME="${gpg_home}" gpg --batch --import "${HLCC_PUBLIC_KEY_SOURCE}"
if ! GNUPGHOME="${gpg_home}" gpg \
--batch \
--with-colons \
--fingerprint "${HLCC_RELEASE_KEY}" \
| grep -Fq "fpr:::::::::${HLCC_RELEASE_KEY}:"; then
echo "Forgejo release key fingerprint mismatch." >&2
exit 4
fi
GNUPGHOME="${gpg_home}" gpg \
--batch \
--verify "${HLCC_SIGNATURE_SOURCE}" "${HLCC_BINARY_SOURCE}"
install -m 0755 "${HLCC_BINARY_SOURCE}" "${HLCC_ROOT}/bin/hlcc"
install -m 0600 "${HLCC_CONFIG_SOURCE}" "${HLCC_ROOT}/config/app.ini"
install -m 0600 "${HLCC_MANIFEST_SOURCE}" "${HLCC_SOURCE_ARCHIVE}/MANIFEST.sha256"
install -m 0755 "${HLCC_BINARY_SOURCE}" "${HLCC_SOURCE_ARCHIVE}/${HLCC_BINARY_NAME}"
install -m 0600 "${HLCC_UPSTREAM_BUNDLE_SOURCE}" "${HLCC_SOURCE_ARCHIVE}/forgejo-upstream-all.bundle"
install -m 0600 "${HLCC_PRODUCT_BUNDLE_SOURCE}" "${HLCC_SOURCE_ARCHIVE}/guanghu-code-channel.bundle"
install -m 0600 "${HLCC_PUBLIC_KEY_SOURCE}" "${HLCC_SOURCE_ARCHIVE}/forgejo-release-key.asc"
install -m 0600 "${HLCC_SIGNATURE_SOURCE}" "${HLCC_SOURCE_ARCHIVE}/${HLCC_BINARY_NAME}.asc"
install -m 0600 "${HLCC_CONFIG_SOURCE}" "${HLCC_SOURCE_ARCHIVE}/candidate-app.ini"
(
cd "${HLCC_SOURCE_ARCHIVE}"
sha256sum --check MANIFEST.sha256
)
sha256sum "${HLCC_ROOT}/bin/hlcc" > "${HLCC_ROOT}/RELEASE.sha256"
"${HLCC_ROOT}/bin/hlcc" --version
echo "HLCC isolated candidate installed at ${HLCC_ROOT}"
echo "Offline source and release material retained at ${HLCC_SOURCE_ARCHIVE}"
echo "No service was started and no automatic update job was installed."

View file

@ -0,0 +1,58 @@
#!/usr/bin/env python3
"""Activate the reviewed full-offline HLCC candidate unit without root shell."""
from __future__ import annotations
import json
import os
import pathlib
import signal
import subprocess
import time
import urllib.request
SERVICE = "hlcc-jd-candidate.service"
HEALTH_URL = "http://127.0.0.1:3341/health"
def main() -> None:
result = subprocess.run(
["/usr/bin/systemctl", "show", "--property=MainPID", "--value", SERVICE],
check=True,
capture_output=True,
text=True,
)
pid = int(result.stdout.strip())
if pid <= 1:
raise RuntimeError("HLCC candidate bootstrap has no active main process")
process_root = pathlib.Path("/proc") / str(pid)
if process_root.stat().st_uid != os.getuid():
raise RuntimeError("refusing to signal a process owned by another user")
command = (process_root / "cmdline").read_bytes().replace(b"\0", b" ").decode("utf-8", "replace")
if "/hololake-code-channel/jd-candidate/hlcc-bootstrap.py" not in command:
raise RuntimeError("refusing to signal an unexpected process")
os.kill(pid, signal.SIGTERM)
for _attempt in range(40):
try:
with urllib.request.urlopen(HEALTH_URL, timeout=2) as response:
payload = json.load(response)
if (
payload.get("ok") is True
and payload.get("mode") == "isolated-candidate"
and payload.get("version") == "16.0.1"
and payload.get("package_profile") == "full-offline-v16.0.1"
and payload.get("ready") is True
and payload.get("stage") == "ready"
):
return
except Exception:
pass
time.sleep(1)
raise RuntimeError("full-offline HLCC personal channel did not become ready")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,28 @@
#!/usr/bin/env node
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const root = __dirname;
const script = fs.readFileSync(path.join(root, "activate-staged-candidate.py"), "utf8");
const unit = fs.readFileSync(path.join(root, "hlcc-jd-candidate-activator.service"), "utf8");
const bootstrap = fs.readFileSync(path.join(root, "hlcc-bootstrap.py"), "utf8");
assert.match(bootstrap, /hlcc-offline\/16\.0\.1/);
assert.match(bootstrap, /forgejo-upstream-all\.bundle/);
assert.match(bootstrap, /guanghu-code-channel\.bundle/);
assert.match(bootstrap, /full-offline-v16\.0\.1/);
assert.match(script, /systemctl", "show", "--property=MainPID"/);
assert.match(script, /process_root\.stat\(\)\.st_uid != os\.getuid\(\)/);
assert.match(script, /"\/hololake-code-channel\/jd-candidate\/hlcc-bootstrap\.py" not in command/);
assert.match(script, /os\.kill\(pid, signal\.SIGTERM\)/);
assert.match(script, /payload\.get\("mode"\) == "isolated-candidate"/);
assert.match(script, /payload\.get\("ready"\) is True/);
assert.match(script, /payload\.get\("stage"\) == "ready"/);
assert.doesNotMatch(script, /shell=True|systemctl", "(?:restart|stop|start)"/);
assert.match(unit, /^User=guanghu$/m);
assert.match(unit, /^NoNewPrivileges=true$/m);
assert.match(unit, /^ProtectSystem=strict$/m);
console.log("HLCC full-offline candidate activator: PASS");

View file

@ -0,0 +1,40 @@
APP_NAME = 光湖代码频道
RUN_USER = guanghu
RUN_MODE = prod
[database]
DB_TYPE = sqlite3
PATH = /var/lib/guanghu/personas/guanghu/hlcc-v16.0.1/data/hlcc.db
[repository]
ROOT = /var/lib/guanghu/personas/guanghu/hlcc-v16.0.1/data/repositories
[server]
DOMAIN = guanghulab.com
HTTP_ADDR = 127.0.0.1
HTTP_PORT = 3340
ROOT_URL = https://guanghulab.com/code/
DISABLE_SSH = true
LFS_START_SERVER = true
OFFLINE_MODE = true
[service]
DISABLE_REGISTRATION = true
REQUIRE_SIGNIN_VIEW = false
[security]
INSTALL_LOCK = true
[actions]
ENABLED = false
[mirror]
ENABLED = false
[other]
SHOW_FOOTER_BRANDING = false
SHOW_FOOTER_VERSION = false
SHOW_FOOTER_TEMPLATE_LOAD_TIME = false
[cron.update_checker]
ENABLED = false

View file

@ -0,0 +1,528 @@
#!/usr/bin/env python3
"""Verify and start the isolated JD HoloLake Code Channel candidate."""
from __future__ import annotations
import hashlib
import http.server
import json
import os
import pathlib
import re
import shutil
import sqlite3
import subprocess
import tempfile
import threading
import time
import urllib.error
import urllib.parse
import urllib.request
VERSION = "16.0.1"
UPSTREAM_FINGERPRINT = "EB114F5E6C0DC2BCDD183550A4B61A2DC5923710"
STATE_ROOT = pathlib.Path("/var/lib/guanghu/personas/guanghu/hlcc-v16.0.1")
LEGACY_DB = STATE_ROOT / "data" / "owner-identity-source.db"
OWNER_NAME = "bingshuo"
CHANNEL_REPOSITORY = "fifth-domain"
LEGACY_REPOSITORY_URL = "https://guanghulab.com/fifth-domain/bingshuo/fifth-domain.git"
SEED_COMMIT_NUMBER = "HLCC-ICE-000001"
SEED_CONTRIBUTION_NUMBER = "ZY-CONTRIB-20260723-001"
RELAY_ROOT = "https://guanghubingshuo.com/hlcc-offline/16.0.1"
BINARY_NAME = f"forgejo-{VERSION}-linux-amd64"
EXPECTED = {
BINARY_NAME: "7a4c568136650c10498a9d3d62c7fd630a0cf09c166293ebd78708248f6398fc",
f"{BINARY_NAME}.asc": "1c0ca36df3adb0a7692b6bdc84d7886001ca0c6d0408e67c9d232d2f33cecc71",
"forgejo-release-key.asc": "6fae8894c671ce2397cb35fe40c324f73deade6b4cb3cd6cedd1d2b248e0e3ea",
"forgejo-upstream-all.bundle": "c33bd074d9b2896259e86ebe03ad31ccdd8ff71897beed4320081fa03b15381f",
"guanghu-code-channel.bundle": "fc53740259d108128e69f5a809cec438ecf3158175617574ba55b8612c5eaa6c",
"MANIFEST.sha256": "d564c3b600d4b7a199d8a04ce505ceabf81993ca74fa440805601d55e550f185",
}
STATUS = {
"ok": True,
"mode": "bootstrap",
"version": VERSION,
"code": "HLCC-JD-CANDIDATE-01",
"ready": False,
"stage": "starting",
"package_profile": "full-offline-v16.0.1",
}
STATUS_LOCK = threading.Lock()
def set_status(**values: object) -> None:
with STATUS_LOCK:
STATUS.update(values)
class HealthHandler(http.server.BaseHTTPRequestHandler):
def do_GET(self) -> None: # noqa: N802 - BaseHTTPRequestHandler API
if self.path != "/health":
self.send_error(404)
return
with STATUS_LOCK:
payload = json.dumps(STATUS, ensure_ascii=False).encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Cache-Control", "no-store")
self.send_header("Content-Length", str(len(payload)))
self.end_headers()
self.wfile.write(payload)
def log_message(self, _format: str, *_args: object) -> None:
return
def sha256(path: pathlib.Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def download_verified(name: str) -> pathlib.Path:
destination = STATE_ROOT / "release" / name
if destination.is_file() and sha256(destination) == EXPECTED[name]:
return destination
destination.parent.mkdir(parents=True, exist_ok=True)
temporary = destination.with_suffix(destination.suffix + ".partial")
temporary.unlink(missing_ok=True)
request = urllib.request.Request(
f"{RELAY_ROOT}/{name}",
headers={"User-Agent": "HoloLake-Code-Channel/16.0.1"},
)
with urllib.request.urlopen(request, timeout=45) as response, temporary.open("wb") as output:
shutil.copyfileobj(response, output, length=1024 * 1024)
if sha256(temporary) != EXPECTED[name]:
temporary.unlink(missing_ok=True)
raise RuntimeError(f"sha256 mismatch: {name}")
temporary.replace(destination)
return destination
def verify_release(files: dict[str, pathlib.Path]) -> None:
manifest = files["MANIFEST.sha256"].read_text(encoding="utf-8")
for name in (
BINARY_NAME,
f"{BINARY_NAME}.asc",
"forgejo-release-key.asc",
"forgejo-upstream-all.bundle",
"guanghu-code-channel.bundle",
):
expected_line = f"{EXPECTED[name]} {name}"
if expected_line not in manifest.splitlines():
raise RuntimeError(f"manifest entry mismatch: {name}")
gpg_home = STATE_ROOT / "gpg"
gpg_home.mkdir(parents=True, exist_ok=True)
gpg_home.chmod(0o700)
environment = {**os.environ, "GNUPGHOME": str(gpg_home)}
subprocess.run(
["gpg", "--batch", "--import", str(files["forgejo-release-key.asc"])],
check=True,
env=environment,
stdout=subprocess.DEVNULL,
)
fingerprint = subprocess.run(
["gpg", "--batch", "--with-colons", "--fingerprint", UPSTREAM_FINGERPRINT],
check=True,
env=environment,
text=True,
capture_output=True,
).stdout
if f"fpr:::::::::{UPSTREAM_FINGERPRINT}:" not in fingerprint:
raise RuntimeError("release key fingerprint mismatch")
subprocess.run(
[
"gpg",
"--batch",
"--verify",
str(files[f"{BINARY_NAME}.asc"]),
str(files[BINARY_NAME]),
],
check=True,
env=environment,
stdout=subprocess.DEVNULL,
)
def wait_for_candidate(process: subprocess.Popen[bytes]) -> None:
url = "http://127.0.0.1:3340/api/healthz"
for _attempt in range(90):
if process.poll() is not None:
raise RuntimeError(f"candidate exited with code {process.returncode}")
try:
with urllib.request.urlopen(url, timeout=2) as response:
payload = json.load(response)
if payload.get("status") == "pass":
return
except Exception: # Candidate is still starting.
pass
time.sleep(1)
raise RuntimeError("candidate readiness timeout")
def immutable_source_commit(script_path: pathlib.Path | None = None) -> str:
source = (script_path or pathlib.Path(__file__)).resolve()
matches = [part for part in source.parts if re.fullmatch(r"[0-9a-f]{40}", part)]
if len(matches) != 1:
raise RuntimeError("immutable release commit unavailable")
return matches[0]
def owner_identity_columns(connection: sqlite3.Connection) -> list[str]:
return [row[1] for row in connection.execute("pragma table_info(user)")]
def migrate_owner_identity(
legacy_db: pathlib.Path = LEGACY_DB,
channel_db: pathlib.Path | None = None,
receipt_path: pathlib.Path | None = None,
) -> str:
target = channel_db or STATE_ROOT / "data" / "hlcc.db"
receipt = receipt_path or STATE_ROOT / "data" / "owner-migration-receipt.json"
if not legacy_db.is_file() or not target.is_file():
raise RuntimeError("owner migration database unavailable")
legacy = sqlite3.connect(f"file:{legacy_db}?mode=ro", uri=True, timeout=15)
channel = sqlite3.connect(target, timeout=15)
try:
existing = channel.execute(
"select is_active, is_admin from user where lower_name = ?",
(OWNER_NAME,),
).fetchall()
if existing:
if len(existing) != 1 or existing[0] != (1, 1):
raise RuntimeError("channel owner identity mismatch")
return "already-present"
source_rows = legacy.execute(
"select * from user where lower_name = ?",
(OWNER_NAME,),
).fetchall()
if len(source_rows) != 1:
raise RuntimeError("legacy owner identity mismatch")
old_columns = owner_identity_columns(legacy)
new_info = list(channel.execute("pragma table_info(user)"))
new_columns = {row[1] for row in new_info}
missing = [
row[1]
for row in new_info
if row[3] and row[4] is None and row[1] != "id" and row[1] not in old_columns
]
if missing:
raise RuntimeError("channel owner schema has unsupported required columns")
copied_columns = [name for name in old_columns if name in new_columns and name != "id"]
values = dict(zip(old_columns, source_rows[0]))
for counter in ("num_repos", "num_stars", "num_followers", "num_following"):
if counter in values:
values[counter] = 0
if "use_custom_avatar" in values:
values["use_custom_avatar"] = 0
if "prohibit_login" in values:
values["prohibit_login"] = 0
backup_dir = target.parent / "backups"
backup_dir.mkdir(parents=True, exist_ok=True)
backup_path = backup_dir / "hlcc-before-owner-migration.db"
backup = sqlite3.connect(backup_path)
try:
channel.backup(backup)
finally:
backup.close()
backup_path.chmod(0o600)
placeholders = ",".join("?" for _name in copied_columns)
column_sql = ",".join(f'"{name}"' for name in copied_columns)
with channel:
channel.execute(
f"insert into user ({column_sql}) values ({placeholders})",
[values[name] for name in copied_columns],
)
verified = channel.execute(
"select id, is_active, is_admin from user where lower_name = ?",
(OWNER_NAME,),
).fetchall()
if len(verified) != 1 or verified[0][1:] != (1, 1):
raise RuntimeError("channel owner migration verification failed")
receipt.write_text(
json.dumps(
{
"schema": "guanghu.hlcc-owner-migration/v1",
"owner": OWNER_NAME,
"identity_only": True,
"password_hash_preserved": True,
"access_tokens_migrated": False,
"repositories_migrated": False,
"result": "VERIFIED",
},
ensure_ascii=False,
indent=2,
)
+ "\n",
encoding="utf-8",
)
receipt.chmod(0o600)
return "migrated"
finally:
channel.close()
legacy.close()
def api_json(
method: str,
path: str,
token: str = "",
body: dict[str, object] | None = None,
) -> tuple[int, dict[str, object]]:
headers = {"Accept": "application/json"}
data = None
if token:
headers["Authorization"] = f"token {token}"
if body is not None:
headers["Content-Type"] = "application/json"
data = json.dumps(body).encode("utf-8")
request = urllib.request.Request(
f"http://127.0.0.1:3340{path}",
data=data,
headers=headers,
method=method,
)
try:
with urllib.request.urlopen(request, timeout=15) as response:
raw = response.read()
return response.status, json.loads(raw or b"{}")
except urllib.error.HTTPError as error:
raw = error.read()
try:
payload = json.loads(raw or b"{}")
except json.JSONDecodeError:
payload = {}
return error.code, payload
def delete_bootstrap_token(channel_db: pathlib.Path, token_name: str) -> None:
connection = sqlite3.connect(channel_db, timeout=15)
try:
with connection:
owner = connection.execute(
"select id from user where lower_name = ?",
(OWNER_NAME,),
).fetchone()
if owner:
connection.execute(
"delete from access_token where uid = ? and name = ?",
(owner[0], token_name),
)
finally:
connection.close()
def generate_bootstrap_token(binary: pathlib.Path, token_name: str) -> str:
channel_db = STATE_ROOT / "data" / "hlcc.db"
delete_bootstrap_token(channel_db, token_name)
result = subprocess.run(
[
str(binary),
"admin",
"user",
"generate-access-token",
"--username",
OWNER_NAME,
"--token-name",
token_name,
"--scopes",
"write:repository",
"--raw",
"--config",
str(STATE_ROOT / "config" / "app.ini"),
"--work-path",
str(STATE_ROOT / "data"),
],
check=True,
capture_output=True,
text=True,
)
token = result.stdout.strip().splitlines()[-1]
if not re.fullmatch(r"[A-Za-z0-9_-]{32,160}", token):
raise RuntimeError("bootstrap access token format invalid")
return token
def seed_fifth_domain_channel(binary: pathlib.Path) -> str:
status, repository = api_json("GET", f"/api/v1/repos/{OWNER_NAME}/{CHANNEL_REPOSITORY}")
if status == 200:
if repository.get("private") is not False:
raise RuntimeError("existing channel repository is not public")
if repository.get("empty") is False and repository.get("default_branch") == "main":
return "already-present"
if repository.get("empty") is not True:
raise RuntimeError("existing channel repository state invalid")
elif status != 404:
raise RuntimeError("channel repository lookup failed")
token_name = "hlcc-fifth-domain-bootstrap"
token = generate_bootstrap_token(binary, token_name)
channel_db = STATE_ROOT / "data" / "hlcc.db"
try:
if status == 404:
created_status, _created = api_json(
"POST",
"/api/v1/user/repos",
token,
{
"name": CHANNEL_REPOSITORY,
"description": "光湖代码频道 · 冰朔第五域个人子频道 · 2026-07-23 新起点",
"private": False,
"auto_init": False,
"default_branch": "main",
},
)
if created_status != 201:
raise RuntimeError("channel repository creation failed")
source_commit = immutable_source_commit()
with tempfile.TemporaryDirectory(prefix="hlcc-seed-") as temporary:
root = pathlib.Path(temporary)
snapshot = root / "snapshot"
subprocess.run(
["git", "clone", "--depth=1", "--branch", "main", LEGACY_REPOSITORY_URL, str(snapshot)],
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
)
actual_commit = subprocess.run(
["git", "-C", str(snapshot), "rev-parse", "HEAD"],
check=True,
capture_output=True,
text=True,
).stdout.strip()
if actual_commit != source_commit:
raise RuntimeError("legacy snapshot commit mismatch")
shutil.rmtree(snapshot / ".git")
subprocess.run(["git", "-C", str(snapshot), "init", "-b", "main"], check=True, capture_output=True)
subprocess.run(
["git", "-C", str(snapshot), "config", "user.name", "光湖代码频道 · 铸渊"],
check=True,
)
subprocess.run(
["git", "-C", str(snapshot), "config", "user.email", "hlcc@guanghulab.invalid"],
check=True,
)
subprocess.run(["git", "-C", str(snapshot), "add", "-A"], check=True)
subprocess.run(
[
"git",
"-C",
str(snapshot),
"commit",
"-m",
(
f"[{SEED_COMMIT_NUMBER}][{SEED_CONTRIBUTION_NUMBER}] "
"feat: 以来光者贡献链启用冰朔第五域个人子频道"
),
],
check=True,
capture_output=True,
)
credential = root / "credentials"
encoded = urllib.parse.quote(token, safe="")
credential.write_text(
f"http://{OWNER_NAME}:{encoded}@127.0.0.1:3340\n",
encoding="utf-8",
)
credential.chmod(0o600)
subprocess.run(
[
"git",
"-C",
str(snapshot),
"-c",
f"credential.helper=store --file {credential}",
"push",
"http://127.0.0.1:3340/bingshuo/fifth-domain.git",
"main:main",
],
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
)
credential.unlink(missing_ok=True)
verified_status, verified = api_json(
"GET",
f"/api/v1/repos/{OWNER_NAME}/{CHANNEL_REPOSITORY}",
)
if (
verified_status != 200
or verified.get("private") is not False
or verified.get("empty") is not False
or verified.get("default_branch") != "main"
):
raise RuntimeError("channel repository verification failed")
return "seeded"
finally:
delete_bootstrap_token(channel_db, token_name)
def bootstrap() -> None:
process: subprocess.Popen[bytes] | None = None
try:
for directory in ("config", "data", "logs", "release", "tmp"):
(STATE_ROOT / directory).mkdir(parents=True, exist_ok=True)
set_status(stage="downloading")
files = {name: download_verified(name) for name in EXPECTED}
set_status(stage="verifying")
verify_release(files)
binary = files[BINARY_NAME]
binary.chmod(0o755)
config_source = pathlib.Path(__file__).with_name("app.ini")
config_target = STATE_ROOT / "config" / "app.ini"
shutil.copyfile(config_source, config_target)
config_target.chmod(0o600)
set_status(stage="launching")
log_path = STATE_ROOT / "logs" / "hlcc.log"
log_handle = log_path.open("ab", buffering=0)
process = subprocess.Popen(
[
str(binary),
"web",
"--work-path",
str(STATE_ROOT / "data"),
"--config",
str(config_target),
],
stdout=log_handle,
stderr=subprocess.STDOUT,
)
wait_for_candidate(process)
set_status(stage="migrating-owner")
migrate_owner_identity()
set_status(stage="seeding-fifth-domain-channel")
seed_fifth_domain_channel(binary)
set_status(mode="isolated-candidate", ready=True, stage="ready")
return_code = process.wait()
raise RuntimeError(f"candidate stopped with code {return_code}")
except Exception as error:
if process and process.poll() is None:
process.terminate()
set_status(ok=False, ready=False, stage="failed", error=str(error)[:180])
def main() -> None:
thread = threading.Thread(target=bootstrap, name="hlcc-bootstrap", daemon=True)
thread.start()
server = http.server.ThreadingHTTPServer(("127.0.0.1", 3341), HealthHandler)
server.serve_forever()
if __name__ == "__main__":
main()

View file

@ -0,0 +1,26 @@
[Unit]
Description=Activate the staged full-offline HoloLake Code Channel JD candidate
After=hlcc-jd-candidate.service
Requires=hlcc-jd-candidate.service
[Service]
Type=oneshot
User=guanghu
Group=guanghu
ExecStart=/usr/bin/python3 __RELEASE_ROOT__/server-tools/hololake-code-channel/jd-candidate/activate-staged-candidate.py
RemainAfterExit=true
TimeoutStartSec=60
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
PrivateDevices=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictSUIDSGID=true
LockPersonality=true
ReadOnlyPaths=__RELEASE_ROOT__
[Install]
WantedBy=multi-user.target

View file

@ -0,0 +1,29 @@
[Unit]
Description=HoloLake Code Channel isolated JD candidate
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=guanghu
Group=guanghu
UMask=0077
StateDirectory=guanghu/personas/guanghu/hlcc-v16.0.1
ExecStart=/usr/bin/python3 __RELEASE_ROOT__/server-tools/hololake-code-channel/jd-candidate/hlcc-bootstrap.py
Restart=always
RestartSec=5
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
PrivateDevices=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictSUIDSGID=true
LockPersonality=true
ReadOnlyPaths=__RELEASE_ROOT__
ReadWritePaths=/var/lib/guanghu/personas/guanghu/hlcc-v16.0.1
[Install]
WantedBy=multi-user.target

View file

@ -0,0 +1,26 @@
[Unit]
Description=Activate the HoloLake Code Channel Fifth Domain personal subchannel
After=hlcc-jd-candidate.service
Requires=hlcc-jd-candidate.service
[Service]
Type=oneshot
User=guanghu
Group=guanghu
ExecStart=/usr/bin/python3 __RELEASE_ROOT__/server-tools/hololake-code-channel/jd-candidate/activate-staged-candidate.py
RemainAfterExit=true
TimeoutStartSec=180
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
PrivateDevices=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictSUIDSGID=true
LockPersonality=true
ReadOnlyPaths=__RELEASE_ROOT__
[Install]
WantedBy=multi-user.target

View file

@ -0,0 +1,84 @@
#!/usr/bin/env node
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const root = __dirname;
const bootstrap = fs.readFileSync(path.join(root, "hlcc-bootstrap.py"), "utf8");
const ini = fs.readFileSync(path.join(root, "app.ini"), "utf8");
const unit = fs.readFileSync(path.join(root, "hlcc-jd-candidate.service"), "utf8");
const manifest = JSON.parse(
fs.readFileSync(
path.join(
root,
"../../../deployment/requests/HLCC-JD-CANDIDATE-INITIAL-PROVISION-20260723.json",
),
"utf8",
),
);
const offlineReceipt = JSON.parse(
fs.readFileSync(
path.join(
root,
"../../../deployment/receipts/HLCC-BS-SG-003-OFFLINE-PACK-20260723.json",
),
"utf8",
),
);
assert.match(bootstrap, /VERSION = "16\.0\.1"/);
assert.match(bootstrap, /EB114F5E6C0DC2BCDD183550A4B61A2DC5923710/);
assert.match(bootstrap, /MANIFEST\.sha256/);
assert.match(bootstrap, /gpg"[\s\S]*"--verify"/);
assert.match(bootstrap, /127\.0\.0\.1", 3341/);
assert.match(bootstrap, /127\.0\.0\.1:3340\/api\/healthz/);
assert.match(bootstrap, /payload\.get\("status"\) == "pass"/);
assert.doesNotMatch(bootstrap, /127\.0\.0\.1:3340\/api\/v1\/version/);
assert.match(bootstrap, /forgejo-upstream-all\.bundle/);
assert.match(bootstrap, /guanghu-code-channel\.bundle/);
assert.match(bootstrap, /package_profile": "full-offline-v16\.0\.1"/);
assert.match(bootstrap, /OWNER_NAME = "bingshuo"/);
assert.match(bootstrap, /SEED_COMMIT_NUMBER = "HLCC-ICE-000001"/);
assert.match(bootstrap, /SEED_CONTRIBUTION_NUMBER = "ZY-CONTRIB-20260723-001"/);
assert.match(bootstrap, /以来光者贡献链启用冰朔第五域个人子频道/);
assert.match(bootstrap, /owner-identity-source\.db/);
assert.match(bootstrap, /password_hash_preserved/);
assert.match(bootstrap, /access_tokens_migrated": False/);
assert.match(bootstrap, /repositories_migrated": False/);
assert.match(bootstrap, /delete from access_token/);
assert.doesNotMatch(
bootstrap,
/print\s*\([^)]*token|stderr\.write\s*\([^)]*token|stdout\.write\s*\([^)]*token/,
);
for (const artifact of offlineReceipt.artifacts) {
assert.match(artifact.sha256, /^[0-9a-f]{64}$/);
assert.match(
bootstrap,
new RegExp(`: "${artifact.sha256}"`),
);
}
assert.match(offlineReceipt.release.manifest_sha256, /^[0-9a-f]{64}$/);
assert.match(
bootstrap,
new RegExp(`"MANIFEST\\.sha256": "${offlineReceipt.release.manifest_sha256}"`),
);
assert.match(ini, /APP_NAME = 光湖代码频道/);
assert.match(ini, /ROOT_URL = https:\/\/guanghulab\.com\/code\//);
assert.match(ini, /REQUIRE_SIGNIN_VIEW = false/);
assert.match(ini, /SHOW_FOOTER_BRANDING = false/);
assert.match(ini, /SHOW_FOOTER_VERSION = false/);
assert.match(ini, /\[cron\.update_checker\][\s\S]*ENABLED = false/);
assert.match(unit, /^User=guanghu$/m);
assert.match(unit, /^ProtectSystem=strict$/m);
assert.match(unit, /^ReadOnlyPaths=__RELEASE_ROOT__$/m);
assert.match(
unit,
/^ReadWritePaths=\/var\/lib\/guanghu\/personas\/guanghu\/hlcc-v16\.0\.1$/m,
);
assert.equal(manifest.target_node, "JD-FD-PRIMARY");
assert.equal(manifest.runtime_check.url, "http://127.0.0.1:3341/health");
assert.equal(manifest.module.unit, "hlcc-jd-candidate.service");
assert.equal(manifest.module.run_user, "guanghu");
console.log("HoloLake Code Channel JD candidate package: PASS");

View file

@ -0,0 +1,99 @@
#!/usr/bin/env python3
"""Create a one-user SQLite handoff without exposing the legacy database to HLCC."""
from __future__ import annotations
import argparse
import json
import os
import pathlib
import pwd
import sqlite3
import tempfile
OWNER_NAME = "bingshuo"
def prepare(source: pathlib.Path, destination: pathlib.Path, owner: str) -> None:
if not source.is_file():
raise RuntimeError("legacy database unavailable")
destination.parent.mkdir(parents=True, exist_ok=True)
legacy = sqlite3.connect(f"file:{source}?mode=ro", uri=True, timeout=15)
try:
schema = legacy.execute(
"select sql from sqlite_master where type = 'table' and name = 'user'"
).fetchone()
row = legacy.execute(
"select * from user where lower_name = ?",
(OWNER_NAME,),
).fetchall()
if not schema or not schema[0] or len(row) != 1:
raise RuntimeError("legacy owner identity mismatch")
file_descriptor, temporary_name = tempfile.mkstemp(
prefix=".owner-identity-source.",
suffix=".db",
dir=destination.parent,
)
os.close(file_descriptor)
temporary = pathlib.Path(temporary_name)
try:
handoff = sqlite3.connect(temporary)
try:
handoff.execute(schema[0])
columns = [item[1] for item in legacy.execute("pragma table_info(user)")]
placeholders = ",".join("?" for _column in columns)
column_sql = ",".join(f'"{column}"' for column in columns)
handoff.execute(
f"insert into user ({column_sql}) values ({placeholders})",
row[0],
)
handoff.commit()
finally:
handoff.close()
temporary.chmod(0o600)
identity = pwd.getpwnam(owner)
os.chown(temporary, identity.pw_uid, identity.pw_gid)
temporary.replace(destination)
finally:
temporary.unlink(missing_ok=True)
finally:
legacy.close()
receipt = destination.with_suffix(".receipt.json")
receipt.write_text(
json.dumps(
{
"schema": "guanghu.hlcc-owner-identity-handoff/v1",
"owner": OWNER_NAME,
"rows": 1,
"contains_repository_data": False,
"contains_access_tokens": False,
"result": "PREPARED",
},
ensure_ascii=False,
indent=2,
)
+ "\n",
encoding="utf-8",
)
identity = pwd.getpwnam(owner)
os.chown(receipt, identity.pw_uid, identity.pw_gid)
receipt.chmod(0o600)
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--source", required=True, type=pathlib.Path)
parser.add_argument("--destination", required=True, type=pathlib.Path)
parser.add_argument("--owner", default="guanghu")
arguments = parser.parse_args()
prepare(arguments.source, arguments.destination, arguments.owner)
print("OWNER_IDENTITY_SOURCE_PREPARED rows=1 tokens=0 repositories=0")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,142 @@
#!/usr/bin/env python3
from __future__ import annotations
import importlib.util
import json
import pathlib
import sqlite3
import tempfile
import unittest
MODULE_PATH = pathlib.Path(__file__).with_name("hlcc-bootstrap.py")
SPEC = importlib.util.spec_from_file_location("hlcc_bootstrap", MODULE_PATH)
assert SPEC and SPEC.loader
MODULE = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(MODULE)
SCHEMA = """
create table user (
id integer primary key autoincrement,
lower_name text not null,
name text not null,
email text not null,
passwd text not null,
salt text,
passwd_hash_algo text,
avatar text not null,
avatar_email text not null,
type integer default 0,
is_active integer default 1,
is_admin integer default 0,
num_repos integer default 0,
num_stars integer default 0,
num_followers integer default 0,
num_following integer default 0,
use_custom_avatar integer default 0,
prohibit_login integer default 0
);
create table repository (
id integer primary key autoincrement,
owner_id integer not null,
name text not null
);
create table access_token (
id integer primary key autoincrement,
uid integer not null,
name text not null,
token_hash text not null
);
"""
class OwnerMigrationTests(unittest.TestCase):
def setUp(self) -> None:
self.temporary = tempfile.TemporaryDirectory()
root = pathlib.Path(self.temporary.name)
self.old = root / "old.db"
self.new = root / "new.db"
self.receipt = root / "receipt.json"
for database in (self.old, self.new):
connection = sqlite3.connect(database)
connection.executescript(SCHEMA)
connection.commit()
connection.close()
connection = sqlite3.connect(self.old)
connection.execute(
"""
insert into user (
lower_name, name, email, passwd, salt, passwd_hash_algo,
avatar, avatar_email, is_active, is_admin, num_repos,
num_stars, num_followers, num_following, use_custom_avatar
) values (?, ?, ?, ?, ?, ?, ?, ?, 1, 1, 12, 4, 3, 2, 1)
""",
(
"bingshuo",
"bingshuo",
"owner@example.invalid",
"preserved-password-hash",
"preserved-salt",
"pbkdf2$50000$50",
"legacy-avatar",
"avatar@example.invalid",
),
)
connection.execute(
"insert into repository (owner_id, name) values (1, 'legacy-repo')"
)
connection.execute(
"insert into access_token (uid, name, token_hash) values (1, 'legacy-token', 'secret-hash')"
)
connection.commit()
connection.close()
def tearDown(self) -> None:
self.temporary.cleanup()
def test_migrates_only_owner_identity_and_preserves_password_hash(self) -> None:
result = MODULE.migrate_owner_identity(self.old, self.new, self.receipt)
self.assertEqual(result, "migrated")
connection = sqlite3.connect(self.new)
owner = connection.execute(
"""
select lower_name, passwd, salt, passwd_hash_algo, is_active,
is_admin, num_repos, num_stars, num_followers,
num_following, use_custom_avatar
from user
"""
).fetchone()
self.assertEqual(
owner,
(
"bingshuo",
"preserved-password-hash",
"preserved-salt",
"pbkdf2$50000$50",
1,
1,
0,
0,
0,
0,
0,
),
)
self.assertEqual(connection.execute("select count(*) from repository").fetchone()[0], 0)
self.assertEqual(connection.execute("select count(*) from access_token").fetchone()[0], 0)
connection.close()
receipt = json.loads(self.receipt.read_text(encoding="utf-8"))
self.assertTrue(receipt["identity_only"])
self.assertFalse(receipt["access_tokens_migrated"])
self.assertFalse(receipt["repositories_migrated"])
def test_is_idempotent(self) -> None:
self.assertEqual(MODULE.migrate_owner_identity(self.old, self.new, self.receipt), "migrated")
self.assertEqual(MODULE.migrate_owner_identity(self.old, self.new, self.receipt), "already-present")
if __name__ == "__main__":
unittest.main()

View file

@ -0,0 +1,107 @@
#!/usr/bin/env node
const fs = require("node:fs");
const path = require("node:path");
const assert = require("node:assert/strict");
const root = __dirname;
const ini = fs.readFileSync(path.join(root, "app.ini.fragment"), "utf8");
const jdCandidateIni = fs.readFileSync(
path.join(root, "candidate-app.jd.ini"),
"utf8",
);
const enterpriseCandidateIni = fs.readFileSync(
path.join(root, "candidate-app.enterprise.ini"),
"utf8",
);
const installer = fs.readFileSync(
path.join(root, "install-candidate.sh"),
"utf8",
);
const relayPreparer = fs.readFileSync(
path.join(root, "prepare-release-on-sg.sh"),
"utf8",
);
const policy = JSON.parse(
fs.readFileSync(path.join(root, "update-policy.json"), "utf8"),
);
assert.match(ini, /\[cron\.update_checker\][\s\S]*ENABLED\s*=\s*false/i);
assert.match(jdCandidateIni, /HoloLake Code Channel · Fifth Domain/);
assert.match(enterpriseCandidateIni, /HoloLake Code Channel · Enterprise/);
for (const candidateIni of [jdCandidateIni, enterpriseCandidateIni]) {
assert.match(
candidateIni,
/\[server\][\s\S]*HTTP_ADDR\s*=\s*127\.0\.0\.1[\s\S]*HTTP_PORT\s*=\s*3340/i,
);
assert.match(candidateIni, /RUN_USER\s*=\s*guanghu/i);
assert.match(
candidateIni,
/\/var\/lib\/guanghu\/code-channel\/candidates\/hlcc-v16\.0\.1/,
);
assert.match(
candidateIni,
/\[cron\.update_checker\][\s\S]*ENABLED\s*=\s*false/i,
);
assert.match(candidateIni, /\[actions\][\s\S]*ENABLED\s*=\s*false/i);
assert.match(candidateIni, /\[mirror\][\s\S]*ENABLED\s*=\s*false/i);
}
assert.match(
installer,
/\/var\/lib\/guanghu\/code-channel\/offline-source\/hlcc-v/,
);
assert.match(installer, /HLCC_VERSION="16\.0\.1"/);
assert.doesNotMatch(installer, /curl|wget|keyserver/);
assert.match(
installer,
/HLCC_RELEASE_KEY="EB114F5E6C0DC2BCDD183550A4B61A2DC5923710"/,
);
assert.match(installer, /gpg[\s\S]*--verify/);
assert.match(installer, /HLCC_SOURCE_ARCHIVE[\s\S]*sha256sum --check MANIFEST\.sha256/);
assert.match(installer, /candidate-app\.ini/);
assert.match(installer, /No service was started/);
assert.match(relayPreparer, /RELEASE_URL="https:\/\/code\.forgejo\.org\//);
assert.match(relayPreparer, /id -un\)" != "ubuntu"/);
assert.match(relayPreparer, /forgejo-upstream-all\.bundle/);
assert.match(relayPreparer, /guanghu-code-channel\.bundle/);
assert.match(relayPreparer, /MANIFEST\.sha256/);
assert.match(
relayPreparer,
/No service was started and no transfer to a domestic node was attempted/,
);
assert.equal(policy.product_id, "HLP-MOD-CODE-CHANNEL");
assert.equal(policy.official_upstream.automatic_fetch, false);
assert.equal(policy.official_upstream.automatic_merge, false);
assert.equal(policy.official_upstream.automatic_build, false);
assert.equal(policy.official_upstream.automatic_deploy, false);
assert.equal(policy.official_upstream.push_enabled, false);
assert.equal(policy.forgejo_builtin_update_checker.enabled, false);
assert.equal(
policy.guanghu_update_channel.manifest_url,
"https://guanghulab.com/api/code-channel/updates/v1/manifest.json",
);
assert.equal(policy.guanghu_update_channel.require_signature, true);
assert.equal(policy.guanghu_update_channel.require_artifact_sha256, true);
assert.equal(policy.guanghu_update_channel.require_rollback_release, true);
assert.equal(
policy.guanghu_update_channel.fallback_when_unavailable,
"MANUAL_RELEASE_ONLY",
);
assert.equal(
policy.source_baseline.upstream_commit,
"b3d7e4ac3cbccc220703097a51fa4c16bf302579",
);
assert.deepEqual(policy.offline_distribution.domestic_archive_nodes, [
"JD-FD-PRIMARY",
"AW-GZ-001",
]);
assert.deepEqual(policy.offline_distribution.runtime_nodes, [
"JD-FD-PRIMARY",
"AW-GZ-001",
]);
assert.equal(policy.offline_distribution.require_manifest_sha256, true);
assert.equal(policy.offline_distribution.require_domestic_gpg_reverification, true);
assert.equal(policy.offline_distribution.shared_runtime_data, false);
assert.equal(policy.offline_distribution.automatic_transfer, false);
console.log("HoloLake Code Channel update boundary: PASS");

View file

@ -0,0 +1,87 @@
#!/usr/bin/env bash
set -euo pipefail
readonly HLCC_VERSION="16.0.1"
readonly RELAY_ROOT="/home/ubuntu/guanghu/release-relay/hlcc-v${HLCC_VERSION}"
readonly BINARY_NAME="forgejo-${HLCC_VERSION}-linux-amd64"
readonly RELEASE_URL="https://code.forgejo.org/forgejo/forgejo/releases/download/v${HLCC_VERSION}/${BINARY_NAME}"
readonly RELEASE_KEY="EB114F5E6C0DC2BCDD183550A4B61A2DC5923710"
readonly GPG_HOME="${RELAY_ROOT}/gpg"
readonly UPSTREAM_REPOSITORY="/home/ubuntu/guanghu/upstream-parts/forgejo-official.git"
readonly PRODUCT_REPOSITORY="/home/ubuntu/guanghu/products/guanghu-code-channel"
if [[ "$(id -un)" != "ubuntu" ]]; then
echo "Refusing to prepare overseas release material as any user other than ubuntu." >&2
exit 2
fi
for command_name in curl git gpg sha256sum; do
if ! command -v "${command_name}" >/dev/null 2>&1; then
echo "Missing required command: ${command_name}" >&2
exit 3
fi
done
if [[ ! -d "${UPSTREAM_REPOSITORY}" || ! -d "${PRODUCT_REPOSITORY}/.git" ]]; then
echo "Required Forgejo upstream mirror or Guanghu product worktree is missing." >&2
exit 3
fi
umask 077
mkdir -p "${RELAY_ROOT}" "${GPG_HOME}"
chmod 700 "${GPG_HOME}"
curl --fail --location --proto '=https' --tlsv1.2 \
--output "${RELAY_ROOT}/${BINARY_NAME}" \
"${RELEASE_URL}"
curl --fail --location --proto '=https' --tlsv1.2 \
--output "${RELAY_ROOT}/${BINARY_NAME}.asc" \
"${RELEASE_URL}.asc"
GNUPGHOME="${GPG_HOME}" gpg \
--batch \
--keyserver hkps://keys.openpgp.org \
--recv-keys "${RELEASE_KEY}"
if ! GNUPGHOME="${GPG_HOME}" gpg \
--batch \
--with-colons \
--fingerprint "${RELEASE_KEY}" \
| grep -Fq "fpr:::::::::${RELEASE_KEY}:"; then
echo "Forgejo release key fingerprint mismatch." >&2
exit 4
fi
GNUPGHOME="${GPG_HOME}" gpg \
--batch \
--verify "${RELAY_ROOT}/${BINARY_NAME}.asc" "${RELAY_ROOT}/${BINARY_NAME}"
GNUPGHOME="${GPG_HOME}" gpg \
--batch \
--armor \
--export "${RELEASE_KEY}" > "${RELAY_ROOT}/forgejo-release-key.asc"
git -C "${UPSTREAM_REPOSITORY}" bundle create \
"${RELAY_ROOT}/forgejo-upstream-all.bundle" \
--all
git -C "${PRODUCT_REPOSITORY}" bundle create \
"${RELAY_ROOT}/guanghu-code-channel.bundle" \
guanghu/main
git -C "${UPSTREAM_REPOSITORY}" bundle verify \
"${RELAY_ROOT}/forgejo-upstream-all.bundle"
git -C "${PRODUCT_REPOSITORY}" bundle verify \
"${RELAY_ROOT}/guanghu-code-channel.bundle"
(
cd "${RELAY_ROOT}"
sha256sum \
"${BINARY_NAME}" \
"${BINARY_NAME}.asc" \
forgejo-release-key.asc \
forgejo-upstream-all.bundle \
guanghu-code-channel.bundle \
> MANIFEST.sha256
)
echo "Verified HLCC release material prepared at ${RELAY_ROOT}"
echo "The offline package contains full upstream and Guanghu product Git bundles."
echo "No service was started and no transfer to a domestic node was attempted."

View file

@ -0,0 +1,44 @@
#!/usr/bin/env bash
set -euo pipefail
readonly HLCC_ROOT="/var/lib/guanghu/code-channel/candidates/hlcc-v16.0.1"
readonly HLCC_PID_FILE="${HLCC_ROOT}/hlcc.pid"
readonly HLCC_LOG_FILE="${HLCC_ROOT}/logs/hlcc.log"
if [[ "$(id -un)" != "guanghu" ]]; then
echo "Refusing to start a domestic isolated candidate as any user other than guanghu." >&2
exit 2
fi
if ! command -v curl >/dev/null 2>&1; then
echo "Missing required command: curl" >&2
exit 3
fi
if [[ -f "${HLCC_PID_FILE}" ]]; then
readonly existing_pid="$(cat "${HLCC_PID_FILE}")"
if kill -0 "${existing_pid}" 2>/dev/null; then
echo "HLCC candidate is already running as PID ${existing_pid}."
exit 0
fi
fi
nohup "${HLCC_ROOT}/bin/hlcc" web \
--work-path "${HLCC_ROOT}/data" \
--config "${HLCC_ROOT}/config/app.ini" \
>>"${HLCC_LOG_FILE}" 2>&1 &
readonly candidate_pid="$!"
echo "${candidate_pid}" > "${HLCC_PID_FILE}"
sleep 2
if ! kill -0 "${candidate_pid}" 2>/dev/null; then
echo "HLCC candidate failed to remain running." >&2
tail -n 80 "${HLCC_LOG_FILE}" >&2
exit 5
fi
curl --fail --silent --show-error \
"http://127.0.0.1:3340/api/v1/version"
echo
echo "HLCC isolated candidate is running on loopback only."

View file

@ -0,0 +1,23 @@
#!/usr/bin/env bash
set -euo pipefail
readonly HLCC_ROOT="/var/lib/guanghu/code-channel/candidates/hlcc-v16.0.1"
readonly HLCC_PID_FILE="${HLCC_ROOT}/hlcc.pid"
if [[ "$(id -un)" != "guanghu" ]]; then
echo "Refusing to stop a domestic isolated candidate as any user other than guanghu." >&2
exit 2
fi
if [[ ! -f "${HLCC_PID_FILE}" ]]; then
echo "HLCC candidate PID file does not exist."
exit 0
fi
readonly candidate_pid="$(cat "${HLCC_PID_FILE}")"
if kill -0 "${candidate_pid}" 2>/dev/null; then
kill "${candidate_pid}"
fi
rm "${HLCC_PID_FILE}"
echo "HLCC isolated candidate stopped."

View file

@ -0,0 +1,47 @@
{
"schema": "guanghu.code-channel.update-policy/v1",
"product_id": "HLP-MOD-CODE-CHANNEL",
"name_zh": "光湖代码频道",
"name_en": "HoloLake Code Channel",
"official_upstream": {
"source": "https://code.forgejo.org/forgejo/forgejo.git",
"role": "review-only-parts-source",
"automatic_fetch": false,
"automatic_merge": false,
"automatic_build": false,
"automatic_deploy": false,
"push_enabled": false
},
"forgejo_builtin_update_checker": {
"enabled": false,
"forbidden_endpoint": "release.forgejo.org"
},
"guanghu_update_channel": {
"manifest_url": "https://guanghulab.com/api/code-channel/updates/v1/manifest.json",
"state": "PLANNED_NOT_LIVE",
"require_signature": true,
"require_artifact_sha256": true,
"require_rollback_release": true,
"fallback_when_unavailable": "MANUAL_RELEASE_ONLY"
},
"source_baseline": {
"upstream_tag": "v16.0.1",
"upstream_commit": "b3d7e4ac3cbccc220703097a51fa4c16bf302579",
"guanghu_branch": "guanghu/main"
},
"offline_distribution": {
"download_and_bundle_node": "BS-SG-003",
"domestic_archive_nodes": [
"JD-FD-PRIMARY",
"AW-GZ-001"
],
"runtime_nodes": [
"JD-FD-PRIMARY",
"AW-GZ-001"
],
"require_manifest_sha256": true,
"require_domestic_gpg_reverification": true,
"shared_runtime_data": false,
"automatic_transfer": false
}
}

View file

@ -0,0 +1,13 @@
# JD application hub
The first web application running on `JD-FD-PRIMARY`. It binds only to
`127.0.0.1:8088`; `BS-GZ-006` reaches it through a restricted persistent SSH
tunnel and publishes it below `https://guanghulab.com/jd/`.
Version 2 keeps the application page on loopback port `8088` and adds one
`/code/` reverse-proxy boundary to the isolated HoloLake Code Channel on
`127.0.0.1:3340`. This reuses the existing permitopen-restricted Guangzhou
application tunnel; it does not broaden the Forgejo tunnel key or expose a new
JD port.
No server address, credential or private key belongs in this directory.

View file

@ -0,0 +1,56 @@
#!/usr/bin/env python3
"""Activate the already staged JD app hub unit without root shell access."""
from __future__ import annotations
import json
import os
import pathlib
import signal
import subprocess
import time
import urllib.request
SERVICE = "jd-app-hub.service"
STATUS_URL = "http://127.0.0.1:8088/api/status"
def main() -> None:
result = subprocess.run(
["/usr/bin/systemctl", "show", "--property=MainPID", "--value", SERVICE],
check=True,
capture_output=True,
text=True,
)
pid = int(result.stdout.strip())
if pid <= 1:
raise RuntimeError("jd app hub has no active main process")
process_root = pathlib.Path("/proc") / str(pid)
if process_root.stat().st_uid != os.getuid():
raise RuntimeError("refusing to signal a process owned by another user")
command = (process_root / "cmdline").read_bytes().replace(b"\0", b" ").decode("utf-8", "replace")
if "/jd-app-hub/server.js" not in command:
raise RuntimeError("refusing to signal an unexpected process")
os.kill(pid, signal.SIGTERM)
for _attempt in range(40):
try:
with urllib.request.urlopen(STATUS_URL, timeout=2) as response:
payload = json.load(response)
if (
payload.get("ok") is True
and payload.get("service") == "jd-app-hub"
and payload.get("version") == "2.0.1"
and payload.get("code_channel_proxy") == "loopback-only"
):
return
except Exception:
pass
time.sleep(1)
raise RuntimeError("staged JD app hub did not become version 2.0.1")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,14 @@
fetch("api/status", { cache: "no-store" })
.then((response) => response.ok ? response.json() : Promise.reject(new Error("status unavailable")))
.then((status) => {
const live = document.querySelector("#live-status");
const node = document.querySelector("#node-state");
live.textContent = "运行正常";
live.classList.add("ok");
node.textContent = `${status.node_id} · 在线`;
node.classList.add("ok");
})
.catch(() => {
document.querySelector("#live-status").textContent = "状态暂不可用";
document.querySelector("#node-state").textContent = "节点状态暂不可用";
});

View file

@ -0,0 +1,19 @@
[Unit]
Description=Guanghu BS-GZ-006 to JD application hub tunnel
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=root
ExecStart=/usr/bin/ssh -NT -F /etc/guanghu/jd-web-tunnel-ssh-config jd-web-target
Restart=always
RestartSec=5
NoNewPrivileges=true
PrivateTmp=true
ProtectHome=read-only
ProtectSystem=strict
ReadOnlyPaths=/etc/guanghu/jd-web-tunnel-ssh-config /etc/guanghu/secrets/ssh/bs_gz_006_to_jd_web_proxy /root/.ssh/known_hosts
[Install]
WantedBy=multi-user.target

View file

@ -0,0 +1,26 @@
[Unit]
Description=Activate the staged HoloLake Code Channel JD app hub
After=jd-app-hub.service
Requires=jd-app-hub.service
[Service]
Type=oneshot
User=guanghu
Group=guanghu
ExecStart=/usr/bin/python3 __RELEASE_ROOT__/server-tools/jd-app-hub/activate-staged-unit.py
RemainAfterExit=true
TimeoutStartSec=60
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
PrivateDevices=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictSUIDSGID=true
LockPersonality=true
ReadOnlyPaths=__RELEASE_ROOT__
[Install]
WantedBy=multi-user.target

View file

@ -0,0 +1,31 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>京东云主节点 · 光湖</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<main>
<nav><a href="/">← 返回光湖国内入口</a><span id="node-state">正在读取节点状态</span></nav>
<header>
<p class="eyebrow">JD-FD-PRIMARY · BEIJING</p>
<h1>京东云主节点</h1>
<p>这里是光湖第五域的国内运行节点。广州负责备案前门,这里负责应用、计算、迁移与服务器调度。</p>
</header>
<section class="status" aria-label="节点状态">
<div><span>节点</span><strong>JD-FD-PRIMARY</strong></div>
<div><span>角色</span><strong>第五域国内主节点</strong></div>
<div><span>连接</span><strong id="live-status">检查中</strong></div>
</section>
<section class="cards">
<a href="https://guanghubingshuo.com/" target="_top"><small>WORLD</small><h2>光湖世界</h2><p>进入光湖世界主入口与第五域路由。</p><b></b></a>
<a href="https://guanghulab.com/fifth-domain/bingshuo/fifth-domain" target="_top"><small>REPO-001</small><h2>第五域国内主仓</h2><p>当前系统结构、人格路径、编号地图和部署回执事实源。</p><b></b></a>
<a href="https://guanghulab.com/api/ai/" target="_top"><small>FD-REPO-MAP-001</small><h2>AI 编号检索</h2><p>通过 REPO 编号和中文关键词寻找国内主路径。</p><b></b></a>
<div class="coming"><small>NEXT</small><h2>Tolaria 服务</h2><p>桌面软件相关服务迁移完成后从这里接入。</p><b>建设中</b></div>
</section>
</main>
<script src="app.js"></script>
</body>
</html>

View file

@ -0,0 +1,33 @@
[Unit]
Description=Guanghu JD Application Hub
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=guanghu
Group=guanghu
WorkingDirectory=__RELEASE_ROOT__/server-tools/jd-app-hub
Environment=NODE_ENV=production
Environment=HUB_HOST=127.0.0.1
Environment=HUB_PORT=8088
Environment=GUANGHU_NODE_ID=JD-FD-PRIMARY
Environment=CODE_CHANNEL_HOST=127.0.0.1
Environment=CODE_CHANNEL_PORT=3340
ExecStart=/usr/bin/node __RELEASE_ROOT__/server-tools/jd-app-hub/server.js
Restart=always
RestartSec=3
NoNewPrivileges=true
PrivateTmp=true
ProtectHome=true
ProtectSystem=strict
PrivateDevices=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictSUIDSGID=true
LockPersonality=true
ReadOnlyPaths=__RELEASE_ROOT__
[Install]
WantedBy=multi-user.target

View file

@ -0,0 +1,124 @@
"use strict";
const fs = require("node:fs");
const http = require("node:http");
const path = require("node:path");
const os = require("node:os");
const ROOT = __dirname;
const TYPES = { ".html": "text/html; charset=utf-8", ".css": "text/css; charset=utf-8", ".js": "application/javascript; charset=utf-8" };
const APP_VERSION = "2.0.1";
function json(response, status, value) {
response.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" });
response.end(JSON.stringify(value));
}
function proxyCodeChannel(request, response, options = {}) {
const internalPath = request.url.replace(/^\/code(?=\/|$)/, "") || "/";
const upstream = http.request({
host: options.codeChannelHost || process.env.CODE_CHANNEL_HOST || "127.0.0.1",
port: Number(options.codeChannelPort || process.env.CODE_CHANNEL_PORT || 3340),
method: request.method,
path: internalPath,
headers: {
...request.headers,
host: request.headers.host || "guanghulab.com",
"x-forwarded-host": request.headers["x-forwarded-host"] || request.headers.host || "guanghulab.com",
"x-forwarded-proto": request.headers["x-forwarded-proto"] || "https"
},
timeout: 300000
}, upstreamResponse => {
response.writeHead(upstreamResponse.statusCode || 502, upstreamResponse.headers);
upstreamResponse.pipe(response);
});
upstream.on("timeout", () => upstream.destroy(new Error("upstream_timeout")));
upstream.on("error", () => {
if (!response.headersSent) return json(response, 502, { ok: false, error: "code_channel_unavailable" });
response.destroy();
});
request.pipe(upstream);
}
function candidateFailureCategory(message) {
const value = String(message || "").toLowerCase();
if (/urlopen|timed out|http error|temporary failure|name or service/.test(value)) return "relay_download_failed";
if (/no such file.*gpg|gpg.*no such file/.test(value)) return "signature_verifier_unavailable";
if (/fingerprint|signature|gpg/.test(value)) return "signature_verification_failed";
if (/candidate exited|candidate stopped/.test(value)) return "candidate_process_exited";
if (/readiness timeout/.test(value)) return "candidate_readiness_timeout";
return value ? "bootstrap_failed" : "";
}
function readCandidateStatus(response, options = {}) {
const request = http.get({
host: options.codeChannelHealthHost || "127.0.0.1",
port: Number(options.codeChannelHealthPort || 3341),
path: "/health",
timeout: 3000
}, upstreamResponse => {
let raw = "";
upstreamResponse.setEncoding("utf8");
upstreamResponse.on("data", chunk => {
raw += chunk;
if (raw.length > 8192) upstreamResponse.destroy(new Error("status_too_large"));
});
upstreamResponse.on("end", () => {
try {
const source = JSON.parse(raw);
return json(response, 200, {
ok: source.ok === true,
version: String(source.version || ""),
code: String(source.code || ""),
ready: source.ready === true,
stage: String(source.stage || "unknown"),
failure_category: source.ok === true ? "" : candidateFailureCategory(source.error)
});
} catch {
return json(response, 502, { ok: false, ready: false, error: "invalid_candidate_status" });
}
});
});
request.on("timeout", () => request.destroy(new Error("status_timeout")));
request.on("error", () => json(response, 502, { ok: false, ready: false, error: "candidate_status_unavailable" }));
}
function createApp(options = {}) {
return http.createServer((request, response) => {
const url = new URL(request.url, "http://localhost");
if (url.pathname === "/code") {
response.writeHead(308, { Location: "/code/", "Cache-Control": "no-store" });
return response.end();
}
if (url.pathname.startsWith("/code/")) return proxyCodeChannel(request, response, options);
if (request.method !== "GET" && request.method !== "HEAD") return json(response, 405, { ok: false, error: "method_not_allowed" });
if (url.pathname === "/api/code-channel-status") return readCandidateStatus(response, options);
if (url.pathname === "/api/status") {
return json(response, 200, {
ok: true,
node_id: process.env.GUANGHU_NODE_ID || "JD-FD-PRIMARY",
role: "fifth-domain-primary",
service: "jd-app-hub",
version: APP_VERSION,
code_channel_proxy: "loopback-only",
uptime_seconds: Math.floor(process.uptime()),
load_1m: Number(os.loadavg()[0].toFixed(2)),
memory_used_percent: Number((((os.totalmem() - os.freemem()) / os.totalmem()) * 100).toFixed(1)),
timestamp: new Date().toISOString()
});
}
const relative = url.pathname === "/" ? "index.html" : url.pathname.replace(/^\/+/, "");
if (!/^(index\.html|styles\.css|app\.js)$/.test(relative)) return json(response, 404, { ok: false, error: "not_found" });
const file = path.join(ROOT, relative);
response.writeHead(200, { "Content-Type": TYPES[path.extname(file)] || "application/octet-stream", "Cache-Control": relative === "index.html" ? "no-cache" : "public, max-age=3600" });
fs.createReadStream(file).pipe(response);
});
}
if (require.main === module) {
const host = process.env.HUB_HOST || "127.0.0.1";
const port = Number(process.env.HUB_PORT || 8088);
createApp().listen(port, host, () => console.log(`JD app hub listening on ${host}:${port}`));
}
module.exports = { createApp };

View file

@ -0,0 +1,26 @@
:root { color-scheme: dark; --bg:#081112; --panel:#102526; --line:rgba(174,233,216,.16); --text:#eff9f6; --muted:#9cafaa; --lake:#78dfc4; --warm:#efbd79; }
* { box-sizing:border-box; }
body { margin:0; min-height:100vh; color:var(--text); background:radial-gradient(circle at 75% 0,rgba(62,154,136,.18),transparent 32rem),var(--bg); font-family:Inter,"PingFang SC",system-ui,sans-serif; }
main { width:min(1080px,calc(100% - 36px)); margin:auto; padding-bottom:70px; }
nav { height:84px; display:flex; justify-content:space-between; align-items:center; border-bottom:1px solid var(--line); color:var(--muted); font-size:13px; }
nav a { color:var(--text); text-decoration:none; }
header { padding:90px 0 58px; max-width:780px; }
.eyebrow { color:var(--lake); font:800 11px/1 ui-monospace,monospace; letter-spacing:.18em; }
h1 { margin:20px 0; font-size:clamp(48px,8vw,86px); line-height:1; letter-spacing:-.055em; }
header>p:last-child { max-width:700px; color:var(--muted); font-size:18px; line-height:1.8; }
.status { display:grid; grid-template-columns:repeat(3,1fr); border:1px solid var(--line); border-radius:18px; overflow:hidden; background:rgba(15,35,36,.72); }
.status div { padding:22px; display:flex; flex-direction:column; gap:8px; border-right:1px solid var(--line); }
.status div:last-child { border:0; }
.status span { color:var(--muted); font-size:12px; }
.status strong { font-size:15px; }
.cards { display:grid; grid-template-columns:repeat(2,1fr); gap:16px; padding-top:30px; }
.cards>a,.cards>div { position:relative; min-height:210px; padding:28px; border:1px solid var(--line); border-radius:18px; background:var(--panel); text-decoration:none; color:var(--text); }
.cards>a:hover { border-color:rgba(120,223,196,.48); transform:translateY(-3px); }
.cards>a { transition:.2s ease; }
.cards small { color:var(--lake); font-weight:800; letter-spacing:.16em; }
.cards h2 { margin:34px 0 10px; font-size:25px; }
.cards p { margin:0; color:var(--muted); line-height:1.7; }
.cards b { position:absolute; right:25px; top:25px; color:var(--warm); font-size:13px; }
.coming { opacity:.62; }
#live-status.ok,#node-state.ok { color:var(--lake); }
@media(max-width:700px){ nav{height:70px}.status,.cards{grid-template-columns:1fr}.status div{border-right:0;border-bottom:1px solid var(--line)}header{padding-top:65px}.cards>a,.cards>div{min-height:190px} }

View file

@ -0,0 +1,21 @@
#!/usr/bin/env node
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const root = path.join(__dirname, "..");
const script = fs.readFileSync(path.join(root, "activate-staged-unit.py"), "utf8");
const unit = fs.readFileSync(path.join(root, "hlcc-jd-app-hub-activator.service"), "utf8");
assert.match(script, /systemctl", "show", "--property=MainPID"/);
assert.match(script, /process_root\.stat\(\)\.st_uid != os\.getuid\(\)/);
assert.match(script, /"\/jd-app-hub\/server\.js" not in command/);
assert.match(script, /os\.kill\(pid, signal\.SIGTERM\)/);
assert.doesNotMatch(script, /shell=True|systemctl", "(?:restart|stop|start)"/);
assert.match(unit, /^User=guanghu$/m);
assert.match(unit, /^NoNewPrivileges=true$/m);
assert.match(unit, /^ProtectSystem=strict$/m);
assert.match(unit, /^ReadOnlyPaths=__RELEASE_ROOT__$/m);
console.log("JD app hub staged-unit activator: PASS");

View file

@ -0,0 +1,130 @@
"use strict";
const assert = require("node:assert/strict");
const test = require("node:test");
const http = require("node:http");
const { createApp } = require("../server");
const fs = require("node:fs");
const path = require("node:path");
test("JD hub exposes a minimal non-secret status response", async (t) => {
const server = createApp().listen(0, "127.0.0.1");
t.after(() => server.close());
await new Promise((resolve) => server.once("listening", resolve));
const { port } = server.address();
const response = await fetch(`http://127.0.0.1:${port}/api/status`);
assert.equal(response.status, 200);
const body = await response.json();
assert.equal(body.ok, true);
assert.equal(body.node_id, "JD-FD-PRIMARY");
assert.equal("ip" in body, false);
assert.equal("token" in body, false);
});
test("JD hub serves its application page", async (t) => {
const server = createApp().listen(0, "127.0.0.1");
t.after(() => server.close());
await new Promise((resolve) => server.once("listening", resolve));
const { port } = server.address();
const response = await fetch(`http://127.0.0.1:${port}/`);
assert.equal(response.status, 200);
assert.match(await response.text(), /京东云主节点/);
});
test("JD hub strips its public route before proxying to the code channel", async (t) => {
const upstream = http.createServer((request, response) => {
response.writeHead(200, { "content-type": "application/json" });
response.end(JSON.stringify({
path: request.url,
method: request.method,
host: request.headers.host,
proto: request.headers["x-forwarded-proto"]
}));
});
upstream.listen(0, "127.0.0.1");
t.after(() => upstream.close());
await new Promise((resolve) => upstream.once("listening", resolve));
const server = createApp({ codeChannelPort: upstream.address().port }).listen(0, "127.0.0.1");
t.after(() => server.close());
await new Promise((resolve) => server.once("listening", resolve));
const response = await new Promise((resolve, reject) => {
const request = http.get({
host: "127.0.0.1",
port: server.address().port,
path: "/code/api/v1/version",
headers: { host: "guanghulab.com", "x-forwarded-proto": "https" }
}, resolve);
request.on("error", reject);
});
const chunks = [];
for await (const chunk of response) chunks.push(chunk);
assert.equal(response.statusCode, 200);
assert.deepEqual(JSON.parse(Buffer.concat(chunks).toString("utf8")), {
path: "/api/v1/version",
method: "GET",
host: "guanghulab.com",
proto: "https"
});
});
test("JD hub preserves nested code-channel paths and queries without duplicating the public prefix", async (t) => {
const upstream = http.createServer((request, response) => {
response.writeHead(200, { "content-type": "application/json" });
response.end(JSON.stringify({ path: request.url }));
});
upstream.listen(0, "127.0.0.1");
t.after(() => upstream.close());
await new Promise((resolve) => upstream.once("listening", resolve));
const server = createApp({ codeChannelPort: upstream.address().port }).listen(0, "127.0.0.1");
t.after(() => server.close());
await new Promise((resolve) => server.once("listening", resolve));
const nested = await fetch(`http://127.0.0.1:${server.address().port}/code/user/login?redirect_to=%2Fjd%2Fcode%2F`);
assert.equal(nested.status, 200);
assert.deepEqual(await nested.json(), {
path: "/user/login?redirect_to=%2Fjd%2Fcode%2F"
});
const root = await fetch(`http://127.0.0.1:${server.address().port}/code/`);
assert.equal(root.status, 200);
assert.deepEqual(await root.json(), { path: "/" });
});
test("JD hub exposes only a categorized candidate failure", async (t) => {
const upstream = http.createServer((_request, response) => {
response.writeHead(200, { "content-type": "application/json" });
response.end(JSON.stringify({
ok: false,
version: "16.0.1",
code: "HLCC-JD-CANDIDATE-01",
ready: false,
stage: "failed",
error: "[Errno 2] No such file or directory: gpg /private/server/path"
}));
});
upstream.listen(0, "127.0.0.1");
t.after(() => upstream.close());
await new Promise((resolve) => upstream.once("listening", resolve));
const server = createApp({ codeChannelHealthPort: upstream.address().port }).listen(0, "127.0.0.1");
t.after(() => server.close());
await new Promise((resolve) => server.once("listening", resolve));
const response = await fetch(`http://127.0.0.1:${server.address().port}/api/code-channel-status`);
assert.equal(response.status, 200);
assert.deepEqual(await response.json(), {
ok: false,
version: "16.0.1",
code: "HLCC-JD-CANDIDATE-01",
ready: false,
stage: "failed",
failure_category: "signature_verifier_unavailable"
});
});
test("tunnel service keeps private addresses outside the repository", () => {
const unit = fs.readFileSync(path.join(__dirname, "..", "guanghu-jd-web-tunnel.service"), "utf8");
assert.match(unit, /\/etc\/guanghu\/jd-web-tunnel-ssh-config/);
assert.doesNotMatch(unit, /(?:\d{1,3}\.){3}\d{1,3}/);
});

View file

@ -0,0 +1,108 @@
# JD-DR-001 · 京东主控与六节点灾备项目 · 认知入口
> **状态**`ACTIVE · SIX_OF_SIX_RUNTIME_VERIFIED_20260720`
>
> **项目根**`server-tools/jd-disaster-recovery/`
>
> **主控节点**`JD-FD-PRIMARY`
>
> **节点地图**`FD-NODE-MAP-001`
>
> **人格认知入口**`ZY-SERVER-COGNITION-004`
## 0 · 一句话认知
京东是日常主控,六台个人节点是服务器端灾备分控。只要京东实例开机、网络可达,
任一分控节点都能在新工单批准后,通过各自独立的受限恢复身份调用固定修复动作;
冰朔不需要学习终端登录,也不需要在个人电脑保存密码或私钥。
## 1 · 项目关系
```text
冰朔意图 / 人格体判断
→ 小湖灯限定工单(允许本次做什么)
→ 发起节点独立灾备密钥(服务器之间怎样进入)
→ 京东 forced-command只能执行登记恢复动作
→ 追加审计日志与部署回执(实际做了什么)
```
工单、密钥、执行日志是三层,不得互相替代。
## 2 · 六个恢复入口
`BS-GZ-006`、`BS-SG-001`、`BS-SG-002`、`BS-SG-003`、`BS-SH-005`、`ZY-SG-006`。
节点身份、角色和导航图从以下真实文件解析:
```text
routing/server-node-map.json
→ deployment/navigation-maps/<NODE_ID>.json
```
不得从聊天记录、旧 IP、旧 token 或本地密钥猜节点。
## 3 · 两级故障边界
| 故障 | 人格体恢复方式 | 冰朔需要做什么 |
|---|---|---|
| 京东开机、网络可达;授权服务、导航图或普通登录损坏 | 任一分控节点发起新工单,调用受限恢复动作 | 看懂并确认工单;不需要终端登录 |
| 京东关机、断网、系统盘损坏或云厂商停机 | 云厂商控制台开机、修网或从加密备份重建,随后轮换恢复密钥 | 保证实例开机、账号不欠费、云侧网络未关闭 |
六节点灾备不是云厂商控制面的替代品,也不是六份普通 root shell。
## 4 · 固定恢复动作
权威策略:`recovery-policy.json`。
当前设计动作包括:
- `health-check`
- `restore-authz`
- `restore-navigation-map`
- `rollback-last-deploy`
- `restore-owner-password-login`
具体实现:`jd-recovery-entry.sh` → `jd-recovery-runner`。普通恢复 shell 必须拒绝。
## 5 · 部署、验证与回执
```text
bootstrap-six-node-recovery.sh
→ 一节点一钥匙
→ 京东 authorized_keys 来源限制 + restrict + forced-command
→ 六节点反向 health-check
→ backup-control-plane.sh
→ deployment/receipts/ICE-SIX-NODE-JD-DISASTER-RECOVERY-20260720.json
```
2026-07-20 运行态核验为六台 `active / active`。上海节点另经云厂商自动化助手确认:
灾备配置存在、服务端口在监听、到京东反向健康检查成功。验证不保存真实地址或凭据。
## 6 · 当前已知缺口
上海灾备通道本身已通过,但小湖灯中央授权服务读取 `BS-SH-005` 实时导航图时曾返回失败。
这属于中央导航图发布 / 同步缺口,不等于上海灾备失效。后续应通过本项目的
`restore-navigation-map` 登记动作修复,并另留运行回执;在回执完成前不得写成中央工单路由已全通。
## 7 · 真实文件索引
| 目的 | 文件 |
|---|---|
| 操作说明 | `README.md` |
| 灾备策略 | `recovery-policy.json` |
| 六节点引导 | `bootstrap-six-node-recovery.sh` |
| 京东固定入口 | `jd-recovery-entry.sh` |
| 恢复动作执行器 | `jd-recovery-runner` |
| 控制面备份 | `backup-control-plane.sh` |
| 策略测试 | `test/policy.test.js` |
| 六节点灾备回执 | `../../deployment/receipts/ICE-SIX-NODE-JD-DISASTER-RECOVERY-20260720.json` |
| 节点总地图 | `../../routing/server-node-map.json` |
| 铸渊服务器认知线 | `../../eternal-lake-heart/heartbeat-core/zhuyuan-persona-system/ZY-SERVER-COGNITION-004-JD-SIX-NODE-RECOVERY-20260720.hdlp` |
## 8 · 维护规则
1. 代码、服务器运行态、部署回执三者必须分开验证。
2. 新增节点时先建导航图,再生成独立密钥,最后做反向健康检查。
3. 不在仓库保存地址、密码、token、私钥、验证码或邮件批准链接。
4. 任何“已修复”必须写清验证时间、验证入口、结果和仍存在的缺口。
5. 冰朔的日常责任只有保持京东实例开机、账号正常、云侧网络可达;登录与修复路径由人格系统和灾备项目承担。

View file

@ -0,0 +1,44 @@
# 京东主控与六节点灾备
完整项目认知、文件关系、当前运行态与维护规则先读:`INDEX.hdlp`(编号 `JD-DR-001`)。
京东 `JD-FD-PRIMARY` 是日常主控不是唯一恢复入口。六台个人节点分别持有独立、服务器端保存的恢复身份Mac、浏览器和个人本地磁盘不保存恢复私钥。
## 两级故障
1. 京东授权服务损坏但 SSH 仍在线:从任一分控节点发起节点本地邮件授权,批准后使用受限恢复密钥调用京东固定恢复动作。
2. 京东主机完全离线从至少两个不同云厂商保存的加密控制面备份重建京东再轮换全部恢复密钥。SSH 反向恢复不能替代云厂商控制台重建。
## 强制边界
- 每个节点一把独立密钥,禁止六节点共用密钥。
- 京东 `authorized_keys` 对恢复密钥使用来源地址限制、`restrict` 和 forced-command。
- forced-command 仅允许 `health-check``restore-authz``restore-navigation-map``rollback-last-deploy`
- 每次恢复必须先由发起节点向预登记邮箱发送工单;批准只产生一次性、短时恢复收据。
- 企业 `AW-GZ-001` 不属于六节点灾备组,不持有京东恢复私钥。
- 语言协议、零点原核频道和公开导航互通不扩大服务器权限。
- 所有恢复事件写入 root 所有的追加日志,并由独立审计密钥签名。
## 六个分控节点
`BS-GZ-006``BS-SG-001``BS-SG-002``BS-SG-003``BS-SH-005``ZY-SG-006`
部署前必须逐台确认:当前导航图、节点身份、固定出口地址、邮件发送能力、恢复公钥指纹和加密备份状态。仓库收据不能替代现场检查。
## 六节点引导
从云控制台确认京东实例自己的公网 IPv4再作为临时环境变量执行引导。不得把登录来源地址或云元数据错误正文当作实例地址
```bash
JD_PUBLIC_IP='<verified-console-address>' \
RECOVERY_PACKAGE=/tmp/jd-recovery-controller.tgz \
bash bootstrap-six-node-recovery.sh
```
地址只写入服务器端 SSH 配置,不写入仓库。脚本验证 IPv4 格式,并以远端固定命令的退出码作为闭环成功依据。
## 现场状态2026-07-20
六个分控节点的反向健康检查均已验证为 `active / active`。上海节点另经云厂商自动化助手确认灾备配置存在、服务端口监听、反向检查成功。无地址或凭据写入仓库。
已知缺口:小湖灯中央授权服务曾无法读取 `BS-SH-005` 的实时导航图。该问题与上海灾备通道本身分离,后续须使用 `restore-navigation-map` 固定动作修复并新增回执。

View file

@ -0,0 +1,17 @@
#!/usr/bin/env bash
set -euo pipefail
umask 077
out_dir=${1:-/var/backups/guanghu/jd-control-plane}
stamp=$(date -u +%Y%m%dT%H%M%SZ)
install -d -m 0700 "$out_dir"
tar -C / -czf "$out_dir/control-plane-${stamp}.tgz" \
opt/guanghu/lake-lamp-authz \
etc/guanghu/navigation-maps \
etc/systemd/system/lake-lamp-authz.service \
etc/systemd/system/lake-lamp-action-broker.service \
etc/systemd/system/lake-lamp-owner-access.service
sha256sum "$out_dir/control-plane-${stamp}.tgz" > "$out_dir/control-plane-${stamp}.sha256"
echo "CONTROL_PLANE_BACKUP_READY=${stamp}"

View file

@ -0,0 +1,53 @@
#!/usr/bin/env bash
set -euo pipefail
umask 077
ssh_config=${SSH_CONFIG:-/etc/guanghu/nodes/ssh/config}
package=${RECOVERY_PACKAGE:-/tmp/jd-recovery-controller.tgz}
jd_ip=${JD_PUBLIC_IP:-}
[[ -f "$ssh_config" && -f "$package" && "$jd_ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || { echo "recovery bootstrap prerequisites missing or public address invalid" >&2; exit 70; }
install -d -m 0700 /var/backups/guanghu/jd-recovery-bootstrap /var/lib/guanghu/recovery-public-keys
stamp=$(date -u +%Y%m%dT%H%M%SZ)
cp -a /root/.ssh/authorized_keys "/var/backups/guanghu/jd-recovery-bootstrap/authorized_keys.$stamp"
nodes=(
"BS-GZ-006:bs-gz-006"
"BS-SG-001:bs-sg-001"
"BS-SG-002:bs-sg-002"
"BS-SG-003:bs-sg-003"
"BS-SH-005:bs-sh-005"
"ZY-SG-006:zy-sg-006"
)
for binding in "${nodes[@]}"; do
node_id=${binding%%:*}
alias_name=${binding#*:}
source_ip=$(ssh -G -F "$ssh_config" "$alias_name" | awk '$1=="hostname"{print $2; exit}')
[[ -n "$source_ip" ]] || { echo "$node_id source address unavailable" >&2; exit 71; }
cat "$package" | ssh -F "$ssh_config" -o BatchMode=yes -o ConnectTimeout=10 "$alias_name" \
"rm -rf /tmp/jd-recovery-package && install -d -m 0700 /tmp/jd-recovery-package && tar -xzf - -C /tmp/jd-recovery-package && install -d -m 0755 /opt/guanghu/jd-disaster-recovery && cp -a /tmp/jd-recovery-package/. /opt/guanghu/jd-disaster-recovery/ && chmod 0755 /opt/guanghu/jd-disaster-recovery/*.sh /opt/guanghu/jd-disaster-recovery/jd-recovery-runner"
ssh -F "$ssh_config" -o BatchMode=yes "$alias_name" \
"install -d -m 0700 /etc/guanghu/secrets/jd-recovery; test -f /etc/guanghu/secrets/jd-recovery/to-jd-ed25519 || ssh-keygen -q -t ed25519 -N '' -C '${node_id}-recovery-to-JD-FD-PRIMARY' -f /etc/guanghu/secrets/jd-recovery/to-jd-ed25519; chmod 0600 /etc/guanghu/secrets/jd-recovery/to-jd-ed25519; cat /etc/guanghu/secrets/jd-recovery/to-jd-ed25519.pub" \
> "/var/lib/guanghu/recovery-public-keys/${node_id}.pub"
pub=$(cat "/var/lib/guanghu/recovery-public-keys/${node_id}.pub")
grep -qF "${node_id}-recovery-to-JD-FD-PRIMARY" /root/.ssh/authorized_keys || \
printf 'from="%s",restrict,command="/usr/local/libexec/guanghu/jd-recovery-entry" %s\n' "$source_ip" "$pub" >> /root/.ssh/authorized_keys
ssh -F "$ssh_config" -o BatchMode=yes "$alias_name" \
"ssh-keyscan -H -T 5 '$jd_ip' > /etc/guanghu/secrets/jd-recovery/known_hosts 2>/dev/null; chmod 0600 /etc/guanghu/secrets/jd-recovery/known_hosts; printf '%s\\n' 'Host jd-recovery' ' HostName $jd_ip' ' User root' ' IdentityFile /etc/guanghu/secrets/jd-recovery/to-jd-ed25519' ' IdentitiesOnly yes' ' PasswordAuthentication no' ' KbdInteractiveAuthentication no' ' StrictHostKeyChecking yes' ' UserKnownHostsFile /etc/guanghu/secrets/jd-recovery/known_hosts' > /etc/guanghu/jd-recovery-ssh-config; chmod 0600 /etc/guanghu/jd-recovery-ssh-config"
done
chmod 0600 /root/.ssh/authorized_keys /var/lib/guanghu/recovery-public-keys/*.pub
for binding in "${nodes[@]}"; do
node_id=${binding%%:*}
alias_name=${binding#*:}
ssh -F "$ssh_config" -o BatchMode=yes "$alias_name" \
"ssh -F /etc/guanghu/jd-recovery-ssh-config -o BatchMode=yes -o ConnectTimeout=10 jd-recovery health-check >/dev/null" || \
{ echo "$node_id reverse health check failed" >&2; exit 72; }
echo "$node_id=RECOVERY_READY"
done

View file

@ -0,0 +1,16 @@
#!/usr/bin/env bash
set -euo pipefail
case "${SSH_ORIGINAL_COMMAND:-}" in
health-check)
systemctl is-active lake-lamp-authz.service
systemctl is-active lake-lamp-action-broker.service
;;
restore-authz|install-owner-access|restore-navigation-map|rollback-last-deploy)
exec /usr/local/libexec/guanghu/jd-recovery-runner "${SSH_ORIGINAL_COMMAND}"
;;
*)
echo "recovery action denied" >&2
exit 126
;;
esac

View file

@ -0,0 +1,46 @@
#!/usr/bin/env bash
set -euo pipefail
umask 077
action=${1:-}
backup_root=/var/backups/guanghu/jd-control-plane
audit_dir=/var/lib/guanghu/recovery-audit
install -d -o root -g root -m 0700 "$audit_dir"
latest=$(find "$backup_root" -maxdepth 1 -type f -name 'control-plane-*.tgz' -print 2>/dev/null | sort | tail -n 1)
[[ -n "$latest" && -f "${latest%.tgz}.sha256" ]] || { echo "verified backup unavailable" >&2; exit 70; }
(cd "$backup_root" && sha256sum -c "$(basename "${latest%.tgz}.sha256")" >/dev/null)
stamp=$(date -u +%Y%m%dT%H%M%SZ)
case "$action" in
restore-authz)
tar -xzf "$latest" -C / opt/guanghu/lake-lamp-authz etc/systemd/system/lake-lamp-authz.service etc/systemd/system/lake-lamp-action-broker.service
systemctl daemon-reload
systemctl restart lake-lamp-action-broker.service lake-lamp-authz.service
;;
install-owner-access)
tar -xzf "$latest" -C / opt/guanghu/lake-lamp-authz/owner-access-broker.js opt/guanghu/lake-lamp-authz/action-client.js etc/systemd/system/lake-lamp-owner-access.service
systemctl daemon-reload
systemctl enable --now lake-lamp-owner-access.service
systemctl restart lake-lamp-authz.service
;;
restore-navigation-map)
tar -xzf "$latest" -C / etc/guanghu/navigation-maps
systemctl restart lake-lamp-authz.service
;;
rollback-last-deploy)
receipt=$(find /var/backups/guanghu/jd-enterprise-repair -maxdepth 1 -type f -name 'jd-authz-*.tgz' -print 2>/dev/null | sort | tail -n 1)
[[ -n "$receipt" ]] || { echo "rollback backup unavailable" >&2; exit 71; }
tar -xzf "$receipt" -C /
systemctl daemon-reload
systemctl restart lake-lamp-action-broker.service lake-lamp-authz.service
;;
*)
echo "recovery action denied" >&2
exit 126
;;
esac
printf '{"schema":"guanghu.recovery-audit/v1","at":"%s","action":"%s","source":"%s","result":"ok"}\n' \
"$stamp" "$action" "${SSH_CONNECTION%% *}" >> "$audit_dir/events.jsonl"
echo "RECOVERY_ACTION_OK=$action"

View file

@ -0,0 +1,28 @@
{
"schema": "guanghu.jd-recovery-policy/v1",
"controller": "JD-FD-PRIMARY",
"owner_namespace": "ICE-GL-ZY001",
"recovery_nodes": [
"BS-GZ-006",
"BS-SG-001",
"BS-SG-002",
"BS-SG-003",
"BS-SH-005",
"ZY-SG-006"
],
"allowed_actions": [
"health-check",
"restore-authz",
"install-owner-access",
"restore-navigation-map",
"rollback-last-deploy"
],
"forbidden": [
"shared-recovery-private-key",
"unrestricted-shell",
"reverse-access-from-enterprise",
"private-key-on-personal-computer",
"unsigned-recovery-event"
],
"total_outage": "rebuild from encrypted cross-provider control-plane backups"
}

View file

@ -0,0 +1,40 @@
"use strict";
const test = require("node:test");
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const root = path.resolve(__dirname, "..");
test("six independent recovery nodes are declared", () => {
const policy = JSON.parse(fs.readFileSync(path.join(root, "recovery-policy.json")));
assert.equal(policy.recovery_nodes.length, 6);
assert.equal(new Set(policy.recovery_nodes).size, 6);
assert.ok(policy.forbidden.includes("shared-recovery-private-key"));
assert.ok(policy.forbidden.includes("private-key-on-personal-computer"));
assert.ok(policy.forbidden.includes("reverse-access-from-enterprise"));
});
test("forced command rejects arbitrary shell", () => {
const entry = fs.readFileSync(path.join(root, "jd-recovery-entry.sh"), "utf8");
assert.match(entry, /SSH_ORIGINAL_COMMAND/);
assert.match(entry, /recovery action denied/);
assert.doesNotMatch(entry, /eval /);
});
test("recovery runner only restores fixed local backup paths", () => {
const runner = fs.readFileSync(path.join(root, "jd-recovery-runner"), "utf8");
assert.match(runner, /sha256sum -c/);
assert.match(runner, /restore-authz/);
assert.match(runner, /install-owner-access/);
assert.match(runner, /restore-navigation-map/);
assert.doesNotMatch(runner, /eval /);
assert.doesNotMatch(runner, /\$2/);
});
test("six-node bootstrap uses unique keys and forced command restrictions", () => {
const bootstrap = fs.readFileSync(path.join(root, "bootstrap-six-node-recovery.sh"), "utf8");
assert.match(bootstrap, /restrict,command=/);
assert.match(bootstrap, /source_ip/);
assert.match(bootstrap, /test -f \/etc\/guanghu\/secrets\/jd-recovery\/to-jd-ed25519 \|\| ssh-keygen/);
assert.doesNotMatch(bootstrap, /StrictHostKeyChecking no/);
});

View file

@ -0,0 +1,30 @@
# 京东云第五域国内主代码仓库
> **运行事实校正2026-07-23**:公网只读版本接口
> `https://guanghulab.com/fifth-domain/api/v1/version` 当前返回 `1.23.7`
> 页面元信息显示现役运行体为 Gitea。本目录描述的是目标 Forgejo 部署结构,
> 不能作为“线上已经运行 Forgejo”的证明。迁移与光湖自有化路线见
> `glw-architecture/GLW-OS-004-FORGEJO-UPSTREAM-PARTS-AND-GUANGHU-CODE-PLATFORM.hdlp`
产品身份必须独立验收,禁止再用服务名、目录名或相似 UI 代替真实版本证据:
```bash
node server-tools/jd-forgejo/product-identity.js \
https://guanghulab.com/fifth-domain FORGEJO
```
当前该命令应返回 `PRODUCT_IDENTITY_MISMATCH`,直到并行迁移完成并切换入口。
- 与广州/新加坡现役节点一致的原生 Forgejo 二进制 + SQLite
- Web 和 SSH 只绑定 JD 回环地址;
- 公网页面通过广州备案前门的专用 `permitopen` SSH 隧道发布在
`https://guanghulab.com/fifth-domain/`
- 数据位于 `/var/lib/guanghu/forgejo``SECRET_KEY``INTERNAL_TOKEN`
只写入服务器上的 `app.ini`
- 禁止公开注册和 push-create初始管理员由部署命令在容器内创建
- 服务器 `pre-receive` 必须串联导航记忆守门人、私密禁用标识扫描和
小湖灯一小时 repo-push 授权凭据。
不得再从广州/新加坡节点盲目复制“现役二进制”。任何候选二进制必须记录官方
来源、版本、SHA-256 和产品身份,再进入隔离实例。迁移必须另开批次、备份数据库
并人工验证。

View file

@ -0,0 +1,38 @@
APP_NAME = 第五域 · 国内主代码仓库
RUN_USER = git
WORK_PATH = /opt/forgejo
[server]
PROTOCOL = http
HTTP_ADDR = 127.0.0.1
HTTP_PORT = 3001
DOMAIN = guanghulab.com
ROOT_URL = https://guanghulab.com/fifth-domain/
DISABLE_SSH = true
LFS_START_SERVER = true
[database]
DB_TYPE = sqlite3
PATH = /var/lib/guanghu/forgejo/data/forgejo.db
LOG_SQL = false
[repository]
ROOT = /var/lib/guanghu/forgejo/repositories
DEFAULT_PRIVATE = private
ENABLE_PUSH_CREATE_USER = false
[service]
DISABLE_REGISTRATION = true
REQUIRE_SIGNIN_VIEW = false
[security]
INSTALL_LOCK = true
SECRET_KEY = PRIVATE_SERVER_VALUE
INTERNAL_TOKEN = PRIVATE_SERVER_VALUE
[session]
PROVIDER = file
[log]
MODE = console
LEVEL = Info

View file

@ -0,0 +1,12 @@
location /fifth-domain/ {
proxy_pass http://127.0.0.1:19301/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 5s;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
client_max_body_size 256m;
}

View file

@ -0,0 +1,22 @@
[Unit]
Description=Guanghu Fifth Domain domestic Forgejo primary
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=git
Group=git
SupplementaryGroups=guanghu
WorkingDirectory=/opt/forgejo
ExecStart=/usr/local/bin/forgejo web -c /opt/forgejo/custom/conf/app.ini --work-path /opt/forgejo
Restart=always
RestartSec=5
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/forgejo /var/lib/guanghu/forgejo
[Install]
WantedBy=multi-user.target

View file

@ -0,0 +1,19 @@
[Unit]
Description=Guanghu BS-GZ-006 to JD Forgejo web tunnel
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=root
ExecStart=/usr/bin/ssh -NT -F /etc/guanghu/jd-forgejo-tunnel-ssh-config jd-forgejo-target
Restart=always
RestartSec=5
NoNewPrivileges=true
PrivateTmp=true
ProtectHome=read-only
ProtectSystem=strict
ReadOnlyPaths=/etc/guanghu/jd-forgejo-tunnel-ssh-config /etc/guanghu/secrets/ssh/bs_gz_006_to_jd_forgejo_proxy /root/.ssh/known_hosts
[Install]
WantedBy=multi-user.target

View file

@ -0,0 +1,18 @@
#!/usr/bin/env bash
set -euo pipefail
JD_HOST=${1:?JD host is required}
install -m 644 /tmp/guanghu-jd-forgejo-tunnel.service /etc/systemd/system/guanghu-jd-forgejo-tunnel.service
install -m 644 /tmp/guanghu-forgejo.nginx.conf /etc/nginx/snippets/guanghu-forgejo.conf
chmod 600 /etc/guanghu/secrets/ssh/bs_gz_006_to_jd_forgejo_proxy
install -m 600 /dev/null /etc/guanghu/jd-forgejo-tunnel-ssh-config
sed -e "s/JD_PUBLIC_ADDRESS/${JD_HOST}/" /tmp/jd-forgejo-tunnel-ssh-config.example > /etc/guanghu/jd-forgejo-tunnel-ssh-config
if ! grep -q "include /etc/nginx/snippets/guanghu-forgejo.conf;" /etc/nginx/sites-enabled/guanghulab; then
sed -i '0,/server_name guanghulab.com;/s##server_name guanghulab.com;\n include /etc/nginx/snippets/guanghu-forgejo.conf;#' /etc/nginx/sites-enabled/guanghulab
fi
systemctl daemon-reload
systemctl enable --now guanghu-jd-forgejo-tunnel.service
nginx -t
systemctl reload nginx
rm -f /tmp/guanghu-jd-forgejo-tunnel.service /tmp/guanghu-forgejo.nginx.conf /tmp/jd-forgejo-tunnel-ssh-config.example

View file

@ -0,0 +1,9 @@
Host jd-forgejo-target
HostName JD_PUBLIC_ADDRESS
User root
IdentityFile /etc/guanghu/secrets/ssh/bs_gz_006_to_jd_forgejo_proxy
IdentitiesOnly yes
LocalForward 127.0.0.1:19301 127.0.0.1:3001
ExitOnForwardFailure yes
ServerAliveInterval 30
ServerAliveCountMax 3

View file

@ -0,0 +1,41 @@
"use strict";
const test = require("node:test");
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const { parseProductIdentity } = require("./product-identity");
const ini = fs.readFileSync(path.join(__dirname, "app.ini.template"), "utf8");
const unit = fs.readFileSync(path.join(__dirname, "guanghu-forgejo.service"), "utf8");
test("native Forgejo binds only to JD loopback", () => {
assert.match(ini, /HTTP_ADDR = 127\.0\.0\.1/);
assert.match(ini, /HTTP_PORT = 3001/);
});
test("registration and push-create are disabled", () => {
assert.match(ini, /DISABLE_REGISTRATION = true/);
assert.match(ini, /ENABLE_PUSH_CREATE_USER = false/);
});
test("secrets remain server-side placeholders", () => {
assert.equal((ini.match(/PRIVATE_SERVER_VALUE/g) || []).length, 2);
assert.doesNotMatch(ini, /[a-f0-9]{40,}/);
assert.match(unit, /User=git/);
});
test("product identity probe rejects Gitea being labeled as Forgejo", () => {
const identity = parseProductIdentity(
{ version: "1.23.7" },
'<meta name="author" content="Gitea - Git with a cup of tea">',
);
assert.deepEqual(identity, {
product: "GITEA",
version: "1.23.7",
author: "Gitea - Git with a cup of tea",
});
assert.notEqual(identity.product, "FORGEJO");
});
test("product identity probe recognizes Forgejo independently", () => {
const identity = parseProductIdentity(
{ version: "15.0.5" },
'<meta name="author" content="Forgejo Beyond coding. We forge.">',
);
assert.equal(identity.product, "FORGEJO");
assert.equal(identity.version, "15.0.5");
});

View file

@ -0,0 +1,57 @@
"use strict";
function parseProductIdentity(versionPayload, html) {
const version =
typeof versionPayload === "string"
? JSON.parse(versionPayload).version
: versionPayload.version;
const author =
html.match(
/<meta\s+name=["']author["']\s+content=["']([^"']+)["']/i,
)?.[1] ?? "";
const normalizedAuthor = author.toLowerCase();
let product = "UNKNOWN";
if (normalizedAuthor.includes("forgejo")) product = "FORGEJO";
if (normalizedAuthor.includes("gitea")) product = "GITEA";
return { product, version, author };
}
async function inspectProduct(baseUrl, fetchImpl = fetch) {
const normalizedBase = baseUrl.replace(/\/+$/, "");
const [versionResponse, homeResponse] = await Promise.all([
fetchImpl(`${normalizedBase}/api/v1/version`),
fetchImpl(`${normalizedBase}/`),
]);
if (!versionResponse.ok || !homeResponse.ok) {
throw new Error(
`repository identity probe failed: version=${versionResponse.status}, home=${homeResponse.status}`,
);
}
return parseProductIdentity(
await versionResponse.json(),
await homeResponse.text(),
);
}
async function main() {
const baseUrl =
process.argv[2] ?? "https://guanghulab.com/fifth-domain";
const expectedProduct = (process.argv[3] ?? "FORGEJO").toUpperCase();
const identity = await inspectProduct(baseUrl);
process.stdout.write(`${JSON.stringify(identity, null, 2)}\n`);
if (identity.product !== expectedProduct) {
process.stderr.write(
`PRODUCT_IDENTITY_MISMATCH: expected ${expectedProduct}, got ${identity.product} ${identity.version}\n`,
);
process.exitCode = 2;
}
}
if (require.main === module) {
main().catch((error) => {
process.stderr.write(`${error.message}\n`);
process.exitCode = 1;
});
}
module.exports = { inspectProduct, parseProductIdentity };

View file

@ -0,0 +1,121 @@
# 小湖灯邮件链接授权服务
这是 Gatekeeper v3.2 前面的人类批准层。它同时支持电脑本机和手机/任意设备:
- 跨设备默认入口无需秘密凭证,只能创建一张没有执行权的申请单;
- 冰朔打开申请单并点击“发送我的授权邮件”后,真正的批准链接才会发送到服务器预登记邮箱;
- 邮件批准完成后,人格体才能一次性领取绑定到
`persona + target server + scope + actions` 的受限会话会话内可连续执行已登记能力并可在当前协作未结束时主动续签但续签不得改变目标、scope 或 actions
- 光湖语言人格系统的当前实例可以提出结构化工单。工单开头固定写“光湖语言人格系统当前实例”,并声明来自哪个软件、哪个模型和哪个当前实例;来源声明用于追溯,不要求该实例已经住进尚未完成的 Tolaria
- 冰朔在同一段当前对话中明确签字后,本次实例可以执行已展示的目标、范围和动作,不重复发送邮件;自报来源只能建单,不能自行批准或扩大权限;
- 邮件链接保留为无人能读取当前对话、跨设备转交或需要二次确认时的兜底,不再是所有实例进入系统的唯一入口;
- 旧的 request credential 入口继续保留,供受控电脑和服务器内自动化兼容使用。
申请单、邮件批准链接和领取凭证按服务器策略失效;活动会话受最大连续时长约束。
安全边界:
- 邮箱、QQ 数字、SMTP 授权码和 request credential 只存在于私密文件;
- 多成员审批人登记只存在于 `LAKE_LAMP_APPROVERS_FILE` 指向的服务器私密文件;系统按人格体、目标节点和 scope 选收件人,请求正文不能指定或切换邮箱;
- 公开创建申请单不会发邮件、不会返回批准令牌,也不会获得任何服务器或仓库权限;
- 申请页只允许触发一次预登记邮箱验证,并有单 IP 与全局小时限流;
- 浏览器批准链接为一次性随机令牌,服务端仅持久化摘要;
- claim token 与 session token 均仅向申请人格体返回一次,磁盘只保存摘要;
- 换目标服务器时旧 session 必然返回 `target_mismatch`
- 同一服务器、同一 scope 和同一 actions 内可续签而不重复发邮件;切换服务器或扩大权限必须重新授权;
- 本服务不接受任意 shell 命令,只签发登记动作的会话;
- 所有改变服务器状态的登记动作必须先生成备份引用和回滚方案;失败自动回滚,成功写验收回执;
- 广州公开代理只应暴露 `/approve/``/api/workorders` 与 claim 路由,服务本体监听 JD 回环地址。
`request-workorder.js` 从临时环境变量读取 QQ 数字,在内存中补全邮箱并只发送
SHA-256 指纹;数字本身不会写入请求正文、状态文件或代码仓库。未提供私密
request credential 时,脚本自动切换到跨设备公开建单模式。
## 手机或任意设备上的新实例
新实例无需寻找工单凭证,执行:
```bash
node request-workorder.js \
--url https://guanghulab.com/authz \
--persona ICE-GL-ZY001 \
--name 铸渊 \
--target JD-FD-PRIMARY \
--scope repo-push \
--action push-repository \
--description "推送 REPO-001"
```
响应中的 `request_url` 交给冰朔点击。该 URL 只能展示申请内容并请求向预登记邮箱
发送批准邮件,不能直接批准。人格体必须在自己的临时会话中保留响应里的
`workorder_id``claim_token`,批准后通过 claim 路由领取一次性会话;不得把这些值
写入仓库、长期日志或聊天记忆。
## 推送被服务器拒绝时 · 一条命令自动续接
当 Forgejo 返回 `[LL-REPO-PUSH-AUTH-REQUIRED]` 时,不要找邮箱,不要向冰朔索要
授权码、验证码、密码或 Token。直接从 `fifth-domain` 仓库根目录执行服务器返回的命令:
```bash
node server-tools/lake-lamp-authz/authorize-repo-push.js \
--url https://guanghulab.com/authz \
--persona ICE-GL-ZY001 \
--repo bingshuo/fifth-domain
```
该命令只在内存中保留一次性领取信息,并自动完成:
```text
创建无权限空工单
→ 打印 REQUEST_URL 给冰朔
→ 等待冰朔打开该页,由服务器向预登记邮箱发信
→ 等待冰朔点击邮件批准链接
→ 领取限时会话
→ 读取并确认目标节点导航图
→ 生成三小时、执行中自动续期的 repo-push 许可
→ 提示 AI 重试原 git push
```
命令运行期间不要关闭它。公开空工单不会发邮件,也没有推送权限;只有冰朔打开
`REQUEST_URL` 后,服务器才向预登记邮箱发送批准邮件。
## 受控电脑兼容入口
如果环境中存在 `LAKE_LAMP_REQUEST_TOKEN``LAKE_LAMP_REQUEST_TOKEN_FILE`,脚本
使用旧的私密申请模式并直接发送批准邮件。该凭证仅能建单,仍不能登录、推送或执行
服务器动作。
## 限流配置
- `LAKE_LAMP_PUBLIC_CREATE_LIMIT`:单来源每小时公开建单上限,默认 24
- `LAKE_LAMP_PUBLIC_CREATE_GLOBAL_LIMIT`:全局每小时建单上限,默认 60
- `LAKE_LAMP_PUBLIC_MAIL_LIMIT`:单来源每小时触发授权邮件上限,默认 12
- `LAKE_LAMP_PUBLIC_MAIL_GLOBAL_LIMIT`:全局每小时授权邮件上限,默认 30。
运行入口:公开建单 `/api/public/workorders`,会话续签 `/api/session/renew`,登记动作执行 `/api/actions/execute`。多节点需求由人格体按当前任务拆成并行申请,不再使用固定“三封邮件”作为协作规则。
## 新架构首次部署
旧的 `deploy-registered-service` 只能操作已经登记的服务,不能承担首次安装。新架构统一使用固定动作 `provision-approved-architecture`,并把仓库请求编号与不可变提交绑定进工单:
```bash
node request-workorder.js \
--url https://guanghulab.com/authz \
--persona ICE-GL-ZY001 \
--name 铸渊 \
--target JD-FD-PRIMARY \
--scope server-ops \
--action provision-approved-architecture \
--resource 'REQUEST-ID@40位提交SHA' \
--description '首次安装已审核架构包'
```
邮件或可信对话签字页面必须显示同一个 `resource`。批准会话不能切换请求编号或提交。执行器只读取该提交中 `deployment/requests/<REQUEST-ID>.json`,只复制清单列出的普通文件,只安装清单指定的非 root、加固 systemd 单元,并只接受回环健康检查。人格体可以使用清单声明的独立低权限账户、共享模型密钥文件和状态目录;密钥路径必须位于 `/etc/guanghu/persona-secrets/`,可写路径必须位于 `/var/lib/guanghu/personas/<运行账户>/`。覆盖旧单元前强制备份,启动或验收失败时自动恢复。说明文字不能改变部署内容,也不开放任意 shell。
这套入口本身需要在京东主控上一次性安装:
```bash
sudo bash server-tools/lake-lamp-authz/install-architecture-provisioner.sh
```
这是最后一次需要云厂商控制台或现有系统管理通道的引导。安装完成后,未来新架构均走上面的结构化工单,不必预先把每个未来模块写进旧动作桥。

View file

@ -0,0 +1,7 @@
Host enterprise-lighthouse
HostName REPLACE_IN_PRIVATE_SERVER_CONFIG
User root
IdentityFile /etc/guanghu/secrets/lake-lamp/jd-to-enterprise-ed25519
IdentitiesOnly yes
StrictHostKeyChecking yes
UserKnownHostsFile /etc/guanghu/secrets/lake-lamp/known_hosts

View file

@ -0,0 +1,53 @@
"use strict";
const fs = require("node:fs");
const net = require("node:net");
const { execFile } = require("node:child_process");
const SOCKET_PATH = process.env.LAKE_LAMP_ACTION_SOCKET || "/run/guanghu/action-broker.sock";
const SSH_CONFIG = process.env.LAKE_LAMP_SSH_CONFIG || "/etc/guanghu/action-broker-ssh-config";
const ACTIONS = Object.freeze({
"JD-FD-PRIMARY:inspect-services": () => run("/usr/bin/ssh", [
"-F", SSH_CONFIG,
"-o", "BatchMode=yes",
"-o", "ConnectTimeout=10",
"enterprise-lighthouse",
"printf 'HOST='; hostname; printf 'SSH='; systemctl is-active ssh 2>/dev/null || systemctl is-active sshd 2>/dev/null; printf 'LIGHTHOUSE='; systemctl is-active guanghu-enterprise-lighthouse.service 2>/dev/null || true"
])
});
function run(file, args) {
return new Promise(resolve => execFile(file, args, { timeout: 30000, 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),
stderr: String(stderr || "").slice(0, 10000),
})));
}
function reply(socket, value) { socket.end(`${JSON.stringify(value)}\n`); }
if (require.main === module) {
fs.mkdirSync(require("node:path").dirname(SOCKET_PATH), { recursive: true, mode: 0o755 });
try { fs.unlinkSync(SOCKET_PATH); } catch (error) { if (error.code !== "ENOENT") throw error; }
const server = net.createServer({ allowHalfOpen: true }, socket => {
let input = "";
socket.setTimeout(5000, () => socket.destroy());
socket.on("data", chunk => { input += chunk.toString("utf8"); if (input.length > 4096) socket.destroy(); });
socket.on("end", async () => {
let request;
try { request = JSON.parse(input); } catch { return reply(socket, { ok: false, error: "invalid_request" }); }
if (!request || request.cmd || request.command || request.shell || request.args) return reply(socket, { ok: false, error: "arbitrary_command_forbidden" });
const action = ACTIONS[`${request.target}:${request.action}`];
if (!action) return reply(socket, { ok: false, error: "action_not_registered" });
reply(socket, await action());
});
});
server.listen(SOCKET_PATH, () => {
fs.chownSync(SOCKET_PATH, 0, Number(process.env.LAKE_LAMP_AUTHZ_GID || 0));
fs.chmodSync(SOCKET_PATH, 0o660);
});
}
module.exports = { ACTIONS };

View file

@ -0,0 +1,27 @@
"use strict";
const net = require("node:net");
const SOCKET_PATH = process.env.LAKE_LAMP_ACTION_SOCKET || "/run/guanghu/action-broker.sock";
const OWNER_ACCESS_SOCKET_PATH = process.env.LAKE_LAMP_OWNER_ACCESS_SOCKET || "/run/guanghu-owner-access/owner-access.sock";
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);
return new Promise((resolve) => {
const socket = net.createConnection(selectedSocket);
let response = "";
let settled = false;
const finish = (value) => { if (!settled) { settled = true; resolve(value); } };
socket.setTimeout(request.action === "provision-approved-architecture" ? 150000 : 35000);
socket.on("connect", () => socket.end(`${JSON.stringify(request)}\n`));
socket.on("data", chunk => { response += chunk.toString("utf8"); if (response.length > 100000) socket.destroy(); });
socket.on("end", () => {
try { finish(JSON.parse(response)); } catch { finish({ ok: false, error: "invalid_broker_response" }); }
});
socket.on("timeout", () => { socket.destroy(); finish({ ok: false, error: "action_timeout" }); });
socket.on("error", () => finish({ ok: false, error: "action_broker_unavailable" }));
});
}
module.exports = { executeRegisteredAction, SOCKET_PATH, OWNER_ACCESS_SOCKET_PATH, ARCHITECTURE_PROVISION_SOCKET_PATH };

View file

@ -0,0 +1,24 @@
{
"schema": "guanghu.approver-registry/v1",
"approvers": [
{
"id": "sovereign-owner",
"email": "SET_IN_PRIVATE_SERVER_FILE",
"default": true,
"persona_ids": ["ICE-GL-ZY001"],
"roles": ["fifth-domain-publisher", "node-owner"],
"targets": ["JD-FD-PRIMARY", "BS-GZ-006", "BS-SH-005", "BS-SG-001", "BS-SG-002", "BS-SG-003", "ZY-SG-006"],
"scopes": ["server-login", "server-ops", "repo-push"]
},
{
"id": "technical-controller",
"email": "SET_IN_PRIVATE_SERVER_FILE",
"default": false,
"persona_ids": ["SET_IN_PRIVATE_SERVER_FILE"],
"roles": ["zero-sense-technical-controller"],
"targets": ["JD-FD-PRIMARY"],
"scopes": ["server-login", "server-ops"]
}
],
"selection": "match target and scope on the server; a requester may never supply or override a recipient"
}

View file

@ -0,0 +1,200 @@
"use strict";
const fs = require("node:fs");
const http = require("node:http");
const net = require("node:net");
const path = require("node:path");
const { execFile } = require("node:child_process");
const SOCKET_PATH = process.env.LAKE_LAMP_ARCHITECTURE_PROVISION_SOCKET || "/run/guanghu-architecture-provision/provision.sock";
const REPO_DIR = process.env.ARCHITECTURE_PROVISION_REPO_DIR || "/var/lib/guanghu/architecture-provision/repo";
const REPO_URL = process.env.ARCHITECTURE_PROVISION_REPO_URL || "https://guanghulab.com/fifth-domain/bingshuo/fifth-domain.git";
const RELEASES_DIR = process.env.ARCHITECTURE_PROVISION_RELEASES_DIR || "/opt/guanghu/architecture-releases";
const UNIT_DIR = process.env.ARCHITECTURE_PROVISION_UNIT_DIR || "/etc/systemd/system";
const RECEIPTS_DIR = process.env.ARCHITECTURE_PROVISION_RECEIPTS_DIR || "/var/lib/guanghu/architecture-provision/receipts";
function parseResource(value) {
const match = String(value || "").match(/^([A-Z0-9][A-Z0-9._-]{5,119})@([0-9a-f]{40})$/);
return match ? { requestId: match[1], commit: match[2] } : null;
}
function safeRelative(value) {
const item = String(value || "");
return item.length > 0 && item.length <= 240 && !path.isAbsolute(item) && !item.split("/").includes("..") && /^[A-Za-z0-9._/-]+$/.test(item);
}
function validateManifest(manifest, resource) {
if (!manifest || manifest.schema !== "guanghu.architecture-provision-request/v1") throw new Error("invalid_manifest_schema");
if (manifest.request_id !== resource.requestId || manifest.target_node !== "JD-FD-PRIMARY") throw new Error("manifest_identity_mismatch");
if (manifest.status !== "ARCHITECTURE_PACKAGE_READY · INITIAL_PROVISION_PENDING") throw new Error("manifest_not_pending");
if (!manifest.initial_provision || manifest.initial_provision.kind !== "new-architecture-unit") throw new Error("not_initial_architecture_unit");
const unit = String(manifest.module && manifest.module.unit || "");
if (!/^[A-Za-z0-9_.@-]+\.service$/.test(unit)) throw new Error("invalid_unit_name");
if (!Array.isArray(manifest.source_paths) || manifest.source_paths.length < 1 || manifest.source_paths.length > 64 || manifest.source_paths.some(item => !safeRelative(item))) throw new Error("invalid_source_paths");
const unitMatches = manifest.source_paths.filter(item => path.basename(item) === unit);
if (unitMatches.length !== 1) throw new Error("unit_not_uniquely_declared");
const check = manifest.runtime_check || {};
if (!/^http:\/\/127\.0\.0\.1:\d{2,5}\/[A-Za-z0-9._/?=&-]*$/.test(String(check.url || ""))) throw new Error("invalid_loopback_runtime_check");
if (!check.expected || typeof check.expected !== "object" || Array.isArray(check.expected)) throw new Error("invalid_runtime_expectation");
return { unit, unitSource: unitMatches[0], runtimeCheck: check };
}
function declaredPaths(value) {
return String(value || "").split(/\s+/).filter(Boolean);
}
function pathAllowed(candidate, allowed) {
const clean = String(candidate || "").replace(/^-/, "");
return allowed.some(base => clean === base || clean.startsWith(`${base}/`));
}
function validateUnit(text, expectedUser = "guanghu", policy = {}) {
const value = String(text || "");
if (!value.includes("[Service]") || !/^NoNewPrivileges=(true|yes)$/m.test(value) || !/^ProtectSystem=strict$/m.test(value) || !/^ProtectHome=(true|yes)$/m.test(value) || !/^PrivateTmp=(true|yes)$/m.test(value)) throw new Error("unit_hardening_required");
if (!/^[a-z_][a-z0-9_-]{0,30}$/.test(expectedUser) || expectedUser === "root" || !new RegExp(`^User=${expectedUser}$`, "m").test(value) || !new RegExp(`^Group=${expectedUser}$`, "m").test(value)) throw new Error("dedicated_service_user_required");
if (/^(SupplementaryGroups|AmbientCapabilities|CapabilityBoundingSet|BindPaths|BindReadOnlyPaths|RootDirectory|RootImage|DeviceAllow)=/m.test(value)) throw new Error("privileged_unit_directive_forbidden");
const environmentFiles = Array.isArray(policy.environment_files) ? policy.environment_files : [];
const writablePaths = Array.isArray(policy.writable_paths) ? policy.writable_paths : [];
const readOnlyPaths = Array.isArray(policy.read_only_paths) ? policy.read_only_paths : [];
for (const match of value.matchAll(/^EnvironmentFile=(.+)$/gm)) {
if (!pathAllowed(match[1], environmentFiles) || !String(match[1]).replace(/^-/, "").startsWith("/etc/guanghu/persona-secrets/")) throw new Error("environment_file_not_declared");
}
for (const match of value.matchAll(/^ReadWritePaths=(.+)$/gm)) {
for (const item of declaredPaths(match[1])) if (!pathAllowed(item, writablePaths) || !item.startsWith(`/var/lib/guanghu/personas/${expectedUser}`)) throw new Error("writable_path_not_declared");
}
for (const match of value.matchAll(/^ReadOnlyPaths=(.+)$/gm)) {
for (const item of declaredPaths(match[1])) if (item !== "__RELEASE_ROOT__" && !pathAllowed(item, readOnlyPaths)) throw new Error("read_only_path_not_declared");
}
if (!value.includes("__RELEASE_ROOT__")) throw new Error("release_root_placeholder_required");
return value;
}
async function provision(request, options = {}) {
if (!request || request.target !== "JD-FD-PRIMARY" || request.action !== "provision-approved-architecture") return { ok: false, error: "action_not_registered" };
const resource = parseResource(request.resource);
if (!resource) return { ok: false, error: "immutable_architecture_resource_required" };
const repoDir = options.repoDir || REPO_DIR;
const releasesDir = options.releasesDir || RELEASES_DIR;
const unitDir = options.unitDir || UNIT_DIR;
const receiptsDir = options.receiptsDir || RECEIPTS_DIR;
const run = options.run || runFile;
let installedUnit = null;
let unitBackup = null;
try {
await prepareRepo(repoDir, resource.commit, run, options.repoUrl || REPO_URL);
const manifestPath = path.join(repoDir, "deployment", "requests", `${resource.requestId}.json`);
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
const checked = validateManifest(manifest, resource);
const releaseRoot = path.join(releasesDir, resource.commit);
fs.mkdirSync(releaseRoot, { recursive: true, mode: 0o755 });
for (const relative of manifest.source_paths) copyDeclaredFile(repoDir, releaseRoot, relative);
const unitSource = path.join(releaseRoot, checked.unitSource);
const unitText = validateUnit(fs.readFileSync(unitSource, "utf8"), String(manifest.module.run_user || ""), manifest.module).replaceAll("__RELEASE_ROOT__", releaseRoot);
fs.mkdirSync(unitDir, { recursive: true, mode: 0o755 });
installedUnit = path.join(unitDir, checked.unit);
if (fs.existsSync(installedUnit)) unitBackup = fs.readFileSync(installedUnit);
const backupDir = path.join(receiptsDir, "backups", resource.requestId, resource.commit);
fs.mkdirSync(backupDir, { recursive: true, mode: 0o700 });
if (unitBackup) fs.writeFileSync(path.join(backupDir, checked.unit), unitBackup, { mode: 0o600 });
else fs.writeFileSync(path.join(backupDir, `${checked.unit}.previously-absent`), "\n", { mode: 0o600 });
writeAtomic(installedUnit, unitText, 0o644);
await run("/usr/bin/systemctl", ["daemon-reload"]);
await run("/usr/bin/systemctl", ["enable", "--now", checked.unit]);
const runtime = await getJsonWithRetry(checked.runtimeCheck.url, options.getJson, options.healthAttempts, options.healthDelayMs);
for (const [key, expected] of Object.entries(checked.runtimeCheck.expected)) if (runtime[key] !== expected) throw new Error(`runtime_check_failed:${key}`);
const receipt = { schema: "guanghu.architecture-provision-receipt/v1", request_id: resource.requestId, source_commit: resource.commit, target_node: "JD-FD-PRIMARY", unit: checked.unit, runtime_check: checked.runtimeCheck.url, backup: path.join("backups", resource.requestId, resource.commit), rollback: unitBackup ? "restore-previous-unit" : "remove-new-unit", result: "DEPLOYED_AND_VERIFIED", recorded_at: new Date().toISOString() };
fs.mkdirSync(receiptsDir, { recursive: true, mode: 0o700 });
writeAtomic(path.join(receiptsDir, `${resource.requestId}.json`), `${JSON.stringify(receipt, null, 2)}\n`, 0o600);
return { ok: true, request_id: resource.requestId, source_commit: resource.commit, unit: checked.unit, runtime: "verified" };
} catch (error) {
if (installedUnit) {
try {
await run("/usr/bin/systemctl", ["disable", "--now", path.basename(installedUnit)]);
if (unitBackup) fs.writeFileSync(installedUnit, unitBackup, { mode: 0o644 });
else fs.rmSync(installedUnit, { force: true });
await run("/usr/bin/systemctl", ["daemon-reload"]);
if (unitBackup) await run("/usr/bin/systemctl", ["enable", "--now", path.basename(installedUnit)]);
} catch { /* The original error remains authoritative; backup is retained for manual recovery. */ }
}
return { ok: false, error: String(error && error.message || "provision_failed").slice(0, 240) };
}
}
async function prepareRepo(repoDir, commit, run, repoUrl) {
fs.mkdirSync(path.dirname(repoDir), { recursive: true, mode: 0o700 });
if (!fs.existsSync(path.join(repoDir, ".git"))) await run("/usr/bin/git", ["clone", "--filter=blob:none", "--no-checkout", repoUrl, repoDir]);
await run("/usr/bin/git", ["-C", repoDir, "fetch", "--depth=1", "origin", commit]);
await run("/usr/bin/git", ["-C", repoDir, "checkout", "--detach", "--force", commit]);
const head = (await run("/usr/bin/git", ["-C", repoDir, "rev-parse", "HEAD"])).stdout.trim();
if (head !== commit) throw new Error("commit_verification_failed");
}
function copyDeclaredFile(repoDir, releaseRoot, relative) {
const source = path.join(repoDir, relative);
const stat = fs.lstatSync(source);
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error("declared_source_not_regular_file");
const destination = path.join(releaseRoot, relative);
fs.mkdirSync(path.dirname(destination), { recursive: true, mode: 0o755 });
if (fs.existsSync(destination)) {
if (!fs.readFileSync(source).equals(fs.readFileSync(destination))) throw new Error("immutable_release_collision");
return;
}
fs.copyFileSync(source, destination, fs.constants.COPYFILE_EXCL);
fs.chmodSync(destination, stat.mode & 0o755);
}
function writeAtomic(file, content, mode) {
const temp = `${file}.${process.pid}.tmp`;
fs.writeFileSync(temp, content, { mode });
fs.renameSync(temp, file);
}
function runFile(file, args) {
return new Promise((resolve, reject) => execFile(file, args, { timeout: 120000, maxBuffer: 200000 }, (error, stdout, stderr) => error ? reject(new Error(`command_failed:${path.basename(file)}:${String(stderr || error.message).slice(0, 120)}`)) : resolve({ stdout: String(stdout || ""), stderr: String(stderr || "") })));
}
function getJson(url, override) {
if (override) return override(url);
return new Promise((resolve, reject) => {
const req = http.get(url, { timeout: 5000 }, response => {
let body = "";
response.on("data", chunk => { body += chunk; if (body.length > 100000) req.destroy(); });
response.on("end", () => { try { resolve(JSON.parse(body)); } catch { reject(new Error("invalid_runtime_response")); } });
});
req.on("timeout", () => req.destroy(new Error("runtime_check_timeout")));
req.on("error", reject);
});
}
async function getJsonWithRetry(url, override, attempts = 15, delayMs = 1000) {
let lastError;
for (let attempt = 1; attempt <= attempts; attempt += 1) {
try { return await getJson(url, override); }
catch (error) {
lastError = error;
if (attempt < attempts) await new Promise(resolve => setTimeout(resolve, delayMs));
}
}
throw lastError;
}
function reply(socket, value) { socket.end(`${JSON.stringify(value)}\n`); }
if (require.main === module) {
fs.mkdirSync(path.dirname(SOCKET_PATH), { recursive: true, mode: 0o755 });
try { fs.unlinkSync(SOCKET_PATH); } catch (error) { if (error.code !== "ENOENT") throw error; }
const server = net.createServer({ allowHalfOpen: true }, socket => {
let input = "";
socket.setTimeout(140000, () => socket.destroy());
socket.on("data", chunk => { input += chunk.toString("utf8"); if (input.length > 4096) socket.destroy(); });
socket.on("end", async () => {
let request;
try { request = JSON.parse(input); } catch { return reply(socket, { ok: false, error: "invalid_request" }); }
if (!request || request.cmd || request.command || request.shell || request.args) return reply(socket, { ok: false, error: "arbitrary_command_forbidden" });
reply(socket, await provision(request));
});
});
server.listen(SOCKET_PATH, () => { fs.chownSync(SOCKET_PATH, 0, Number(process.env.LAKE_LAMP_AUTHZ_GID || 0)); fs.chmodSync(SOCKET_PATH, 0o660); });
}
module.exports = { parseResource, safeRelative, validateManifest, validateUnit, provision };

View file

@ -0,0 +1,77 @@
"use strict";
const test = require("node:test");
const assert = require("node:assert/strict");
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const { parseResource, safeRelative, validateManifest, validateUnit, provision } = require("./architecture-provision-broker");
const commit = "d".repeat(40);
const requestId = "GLS-0231-JD-LAN-01-INITIAL-PROVISION-20260720";
function manifest() {
return {
schema: "guanghu.architecture-provision-request/v1", request_id: requestId, target_node: "JD-FD-PRIMARY", status: "ARCHITECTURE_PACKAGE_READY · INITIAL_PROVISION_PENDING",
module: { unit: "example.service", run_user: "guanghu" }, initial_provision: { kind: "new-architecture-unit" },
source_paths: ["server-tools/example/server.js", "server-tools/example/example.service"],
runtime_check: { url: "http://127.0.0.1:3924/health", expected: { ok: true, mode: "read-only" } },
};
}
test("resource and manifest are immutable and path constrained", () => {
assert.deepEqual(parseResource(`${requestId}@${commit}`), { requestId, commit });
assert.equal(parseResource(`${requestId}@main`), null);
assert.equal(safeRelative("server-tools/example/server.js"), true);
assert.equal(safeRelative("../etc/passwd"), false);
assert.equal(validateManifest(manifest(), { requestId, commit }).unit, "example.service");
});
test("unit requires non-root systemd hardening and release placeholder", () => {
const unit = "[Service]\nUser=guanghu\nGroup=guanghu\nNoNewPrivileges=true\nPrivateTmp=true\nProtectSystem=strict\nProtectHome=true\nExecStart=/usr/bin/node __RELEASE_ROOT__/server.js\n";
assert.equal(validateUnit(unit), unit);
assert.throws(() => validateUnit(unit.replace("User=guanghu", "User=root")), /dedicated_service_user_required/);
assert.throws(() => validateUnit(`${unit}EnvironmentFile=/etc/shadow\n`), /environment_file_not_declared/);
assert.throws(() => validateUnit(unit.replace("__RELEASE_ROOT__", "/tmp/live")), /release_root_placeholder_required/);
});
test("unit permits a declared persona user, shared secret and state directory", () => {
const unit = "[Service]\nUser=kezhou\nGroup=kezhou\nNoNewPrivileges=yes\nPrivateTmp=yes\nProtectSystem=strict\nProtectHome=yes\nEnvironmentFile=-/etc/guanghu/persona-secrets/shared-deepseek.env\nReadWritePaths=/var/lib/guanghu/personas/kezhou\nReadOnlyPaths=__RELEASE_ROOT__\nExecStart=__RELEASE_ROOT__/run.sh\n";
const policy = { environment_files: ["/etc/guanghu/persona-secrets/shared-deepseek.env"], writable_paths: ["/var/lib/guanghu/personas/kezhou"], read_only_paths: [] };
assert.equal(validateUnit(unit, "kezhou", policy), unit);
assert.throws(() => validateUnit(unit.replace("shared-deepseek.env", "../../shadow"), "kezhou", policy), /environment_file_not_declared/);
assert.throws(() => validateUnit(unit.replace("/var/lib/guanghu/personas/kezhou", "/opt/guanghu/personas/kezhou"), "kezhou", policy), /writable_path_not_declared/);
assert.throws(() => validateUnit(unit.replaceAll("kezhou", "root"), "root", policy), /dedicated_service_user_required/);
});
test("provision copies only declared files and verifies loopback health", async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "architecture-provision-"));
const repoDir = path.join(root, "repo");
const releasesDir = path.join(root, "releases");
const unitDir = path.join(root, "units");
const receiptsDir = path.join(root, "receipts");
fs.mkdirSync(path.join(repoDir, "deployment", "requests"), { recursive: true });
fs.mkdirSync(path.join(repoDir, "server-tools", "example"), { recursive: true });
fs.writeFileSync(path.join(repoDir, "deployment", "requests", `${requestId}.json`), JSON.stringify(manifest()));
fs.writeFileSync(path.join(repoDir, "server-tools", "example", "server.js"), "module.exports = {};\n");
fs.writeFileSync(path.join(repoDir, "server-tools", "example", "example.service"), "[Service]\nUser=guanghu\nGroup=guanghu\nNoNewPrivileges=true\nPrivateTmp=true\nProtectSystem=strict\nProtectHome=true\nExecStart=/usr/bin/node __RELEASE_ROOT__/server-tools/example/server.js\n");
const commands = [];
let healthChecks = 0;
try {
const result = await provision({ target: "JD-FD-PRIMARY", action: "provision-approved-architecture", resource: `${requestId}@${commit}` }, {
repoDir, releasesDir, unitDir, receiptsDir,
run: async (file, args) => { commands.push([file, args]); return { stdout: args.includes("rev-parse") ? `${commit}\n` : "" }; },
getJson: async () => {
healthChecks += 1;
if (healthChecks === 1) throw new Error("connection_refused_during_startup");
return { ok: true, mode: "read-only" };
},
healthDelayMs: 0,
});
assert.equal(result.ok, true);
assert.equal(fs.existsSync(path.join(releasesDir, commit, "server-tools", "example", "server.js")), true);
assert.match(fs.readFileSync(path.join(unitDir, "example.service"), "utf8"), new RegExp(commit));
assert.equal(commands.some(([, args]) => args.includes("enable") && args.includes("--now")), true);
assert.equal(healthChecks, 2);
} finally { fs.rmSync(root, { recursive: true, force: true }); }
});

View file

@ -0,0 +1,28 @@
LAKE_LAMP_HOST=127.0.0.1
LAKE_LAMP_PORT=3921
LAKE_LAMP_PUBLIC_URL=https://example.invalid/authz
LAKE_LAMP_TARGETS=JD-FD-PRIMARY,BS-GZ-006
LAKE_LAMP_APPROVAL_TTL=10800
LAKE_LAMP_SESSION_TTL=10800
LAKE_LAMP_MAX_SESSION_LIFETIME=86400
LAKE_LAMP_STATE_FILE=/var/lib/guanghu/lake-lamp-authz/state.json
LAKE_LAMP_MAPS_DIR=/etc/guanghu/navigation-maps
LAKE_LAMP_MAP_STATE_FILE=/var/lib/guanghu/lake-lamp-authz/map-acks.json
LAKE_LAMP_REPO_GRANT_DIR=/var/lib/guanghu/repo-authorizations
LAKE_LAMP_PUBLIC_CREATE_LIMIT=8
LAKE_LAMP_PUBLIC_CREATE_GLOBAL_LIMIT=60
LAKE_LAMP_PUBLIC_MAIL_LIMIT=3
LAKE_LAMP_PUBLIC_MAIL_GLOBAL_LIMIT=30
LAKE_LAMP_REQUEST_TOKEN=SET_IN_PRIVATE_SERVER_FILE
LAKE_LAMP_OWNER_EMAIL=SET_IN_PRIVATE_SERVER_FILE
LAKE_LAMP_APPROVERS_FILE=/etc/guanghu/secrets/approvers.json
SMTP_HOST=smtp.qq.com
SMTP_PORT=465
SMTP_USER=SET_IN_PRIVATE_SERVER_FILE
QQ_SMTP_AUTH_CODE=SET_IN_PRIVATE_SERVER_FILE
LAKE_LAMP_ARCHITECTURE_PROVISION_SOCKET=/run/guanghu-architecture-provision/provision.sock
ARCHITECTURE_PROVISION_REPO_URL=https://guanghulab.com/fifth-domain/bingshuo/fifth-domain.git
ARCHITECTURE_PROVISION_REPO_DIR=/var/lib/guanghu/architecture-provision/repo
ARCHITECTURE_PROVISION_RELEASES_DIR=/opt/guanghu/architecture-releases
ARCHITECTURE_PROVISION_UNIT_DIR=/etc/systemd/system
ARCHITECTURE_PROVISION_RECEIPTS_DIR=/var/lib/guanghu/architecture-provision/receipts

View file

@ -0,0 +1,109 @@
#!/usr/bin/env node
"use strict";
const DEFAULT_URL = "https://guanghulab.com/authz";
async function authorizeRepoPush(options, deps = {}) {
const fetchImpl = deps.fetch || fetch;
const sleep = deps.sleep || (milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds)));
const output = deps.output || (line => process.stdout.write(`${line}\n`));
const baseUrl = String(options.url || DEFAULT_URL).replace(/\/$/, "");
const persona = required(options.persona, "persona");
const repo = normalizeRepo(required(options.repo, "repo"));
const target = options.target || "JD-FD-PRIMARY";
const pollMilliseconds = positiveNumber(options.poll, 5000);
const request = await requestJson(fetchImpl, `${baseUrl}/api/public/workorders`, {
system_entry: "光湖语言人格系统当前实例",
origin_software: options.software || "仓库推送客户端",
origin_model: options.model || "未声明模型",
origin_instance: options.instance || "当前实例",
persona_id: persona,
persona_name: options.name || persona,
target,
scope: "repo-push",
action: "push-repository",
description: options.description || `申请推送 ${repo}`,
});
output("[LL-WORKORDER-CREATED] 无执行权申请单已创建;尚未发送邮件,也没有推送权限。");
output(`REQUEST_URL=${request.request_url}`);
output("请把 REQUEST_URL 交给冰朔并保持本命令运行。冰朔打开页面后,服务器才发送预登记邮箱邮件。");
output("不需要向冰朔索要邮箱、授权码、验证码、密码或任何令牌。");
const deadline = Date.now() + Number(request.expires_in || 900) * 1000;
let session;
while (Date.now() < deadline) {
const response = await fetchImpl(`${baseUrl}/api/workorders/${request.workorder_id}/claim`, {
method: "POST",
headers: { authorization: `Bearer ${request.claim_token}` },
});
const payload = await readPayload(response);
if (response.status === 200) { session = payload; break; }
if (response.status !== 202 || payload.error !== "approval_pending") {
throw new Error(payload.error || `claim failed (${response.status})`);
}
await sleep(pollMilliseconds);
}
if (!session) throw new Error("authorization request expired before approval");
const common = { persona_id: persona, target, scope: "repo-push" };
const map = await requestJson(fetchImpl, `${baseUrl}/api/navigation-map/read`, common, session.session_token);
await requestJson(fetchImpl, `${baseUrl}/api/navigation-map/ack`, { ...common, map_hash: map.map_hash }, session.session_token);
const grant = await requestJson(fetchImpl, `${baseUrl}/api/repo-push/grant`, { ...common, repo }, session.session_token);
output(`[LL-REPO-PUSH-GRANTED] ${grant.repo} 已获得限时推送许可。现在重试原 git push。`);
return grant;
}
async function requestJson(fetchImpl, url, body, bearerToken = "") {
const headers = { "content-type": "application/json" };
if (bearerToken) headers.authorization = `Bearer ${bearerToken}`;
const response = await fetchImpl(url, { method: "POST", headers, body: JSON.stringify(body) });
const payload = await readPayload(response);
if (!response.ok) throw new Error(payload.error || `request failed (${response.status})`);
return payload;
}
async function readPayload(response) {
try { return await response.json(); } catch { return {}; }
}
function normalizeRepo(value) {
const repo = String(value).trim().toLowerCase().replace(/\.git$/, "");
if (!/^bingshuo\/[a-z0-9._-]+$/.test(repo)) throw new Error("--repo must be bingshuo/<repository>");
return repo;
}
function required(value, name) {
if (!value) throw new Error(`--${name} is required`);
return String(value);
}
function positiveNumber(value, fallback) {
if (value === undefined) return fallback;
const number = Number(value);
if (!Number.isFinite(number) || number <= 0) throw new Error("--poll must be a positive millisecond value");
return number;
}
function parseArgs(argv) {
const result = {};
for (let index = 0; index < argv.length; index += 2) {
const key = String(argv[index] || "").replace(/^--/, "");
if (!key || argv[index + 1] === undefined) throw new Error(`invalid argument: ${argv[index] || ""}`);
result[key] = argv[index + 1];
}
return result;
}
async function main() {
await authorizeRepoPush(parseArgs(process.argv.slice(2)));
}
if (require.main === module) {
main().catch(error => {
process.stderr.write(`[LL-REPO-PUSH-AUTH-FAILED] ${error.message}\n`);
process.exit(1);
});
}
module.exports = { authorizeRepoPush, normalizeRepo, parseArgs };

View file

@ -0,0 +1,60 @@
"use strict";
const test = require("node:test");
const assert = require("node:assert/strict");
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const { createApp } = require("./server");
const { authorizeRepoPush } = require("./authorize-repo-push");
test("one helper command completes owner handoff, map acknowledgement, and repo grant", async () => {
const mail = [];
const directory = fs.mkdtempSync(path.join(os.tmpdir(), "lake-lamp-repo-push-"));
const mapGate = {
read: target => ({ hash: `map-${target}`, data: { node_id: target } }),
ack: () => ({ ok: true }),
verify: () => ({ ok: true }),
};
const app = createApp({
ownerEmail: "owner@example.invalid",
publicBaseUrl: "https://example.invalid/authz",
targets: ["JD-FD-PRIMARY"],
stateFile: "",
repoGrantDir: directory,
mapGate,
sendEmail: async message => { mail.push(message); return true; },
});
await new Promise(resolve => app.listen(0, "127.0.0.1", resolve));
const base = `http://127.0.0.1:${app.address().port}`;
const lines = [];
try {
const grantPromise = authorizeRepoPush({
url: base,
persona: "ICE-GL-ZY001",
name: "铸渊",
repo: "bingshuo/fifth-domain",
poll: 1,
}, {
output: line => lines.push(line),
sleep: milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds)),
});
while (!lines.some(line => line.startsWith("REQUEST_URL="))) await new Promise(resolve => setTimeout(resolve, 1));
const requestUrl = lines.find(line => line.startsWith("REQUEST_URL=")).slice("REQUEST_URL=".length);
const requestPath = new URL(requestUrl).pathname.replace("/authz", "");
assert.equal((await fetch(`${base}${requestPath}`, { method: "POST" })).status, 200);
assert.equal(mail.length, 1);
const approvalPath = new URL(mail[0].approvalUrl).pathname.replace("/authz", "");
assert.equal((await fetch(`${base}${approvalPath}`, { method: "POST" })).status, 200);
const grant = await grantPromise;
assert.equal(grant.repo, "bingshuo/fifth-domain");
assert.equal(grant.target, "JD-FD-PRIMARY");
assert.ok(lines.some(line => line.startsWith("[LL-REPO-PUSH-GRANTED]")));
assert.ok(fs.existsSync(path.join(directory, "bingshuo__fifth-domain.json")));
} finally {
await new Promise(resolve => app.close(resolve));
fs.rmSync(directory, { recursive: true, force: true });
}
});

View file

@ -0,0 +1,16 @@
# Public approval surface. The service itself remains on JD loopback and this
# route is reached through a permitopen-restricted SSH tunnel.
location /authz/ {
proxy_pass http://127.0.0.1:19221/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# Overwrite untrusted client input so application-level rate limits use the
# address observed by this public edge, not a spoofed left-most value.
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Prefix /authz;
proxy_connect_timeout 5s;
proxy_read_timeout 30s;
client_max_body_size 64k;
}

View file

@ -0,0 +1,19 @@
[Unit]
Description=Guanghu BS-GZ-006 to JD Lake Lamp authorization tunnel
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=root
ExecStart=/usr/bin/ssh -NT -F /etc/guanghu/jd-authz-tunnel-ssh-config jd-authz-target
Restart=always
RestartSec=5
NoNewPrivileges=true
PrivateTmp=true
ProtectHome=read-only
ProtectSystem=strict
ReadOnlyPaths=/etc/guanghu/jd-authz-tunnel-ssh-config /etc/guanghu/secrets/ssh/bs_gz_006_to_jd_authz_proxy /root/.ssh/known_hosts
[Install]
WantedBy=multi-user.target

View file

@ -0,0 +1,25 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ ${EUID} -ne 0 ]]; then
echo "run as root" >&2
exit 1
fi
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
install_root=/opt/guanghu/lake-lamp-authz
install -d -m 0755 "$install_root"
for file in server.js workorder-manager.js map-gate.js smtp-mailer.js action-client.js architecture-provision-broker.js; do
install -m 0644 "$script_dir/$file" "$install_root/$file"
done
install -m 0644 "$script_dir/lake-lamp-architecture-provision.service" /etc/systemd/system/lake-lamp-architecture-provision.service
install -d -m 0700 /var/lib/guanghu/architecture-provision
install -d -m 0755 /opt/guanghu/architecture-releases
systemctl daemon-reload
systemctl enable --now lake-lamp-architecture-provision.service
systemctl restart lake-lamp-authz.service
systemctl is-active --quiet lake-lamp-architecture-provision.service
systemctl is-active --quiet lake-lamp-authz.service
echo ARCHITECTURE_PROVISIONER_INSTALLED

View file

@ -0,0 +1,24 @@
#!/usr/bin/env bash
set -euo pipefail
JD_HOST=${1:?JD host is required}
install -m 644 /tmp/guanghu-jd-authz-tunnel.service /etc/systemd/system/guanghu-jd-authz-tunnel.service
install -m 644 /tmp/guanghu-authz.nginx.conf /etc/nginx/snippets/guanghu-authz.conf
chmod 600 /etc/guanghu/secrets/ssh/bs_gz_006_to_jd_authz_proxy
install -m 600 /dev/null /etc/guanghu/jd-authz-tunnel-ssh-config
sed \
-e "s/JD_PUBLIC_ADDRESS/${JD_HOST}/" \
/tmp/jd-authz-tunnel-ssh-config.example > /etc/guanghu/jd-authz-tunnel-ssh-config
if ! grep -q "include /etc/nginx/snippets/guanghu-authz.conf;" /etc/nginx/sites-enabled/guanghulab; then
sed -i '0,/server_name guanghulab.com;/s##server_name guanghulab.com;\n include /etc/nginx/snippets/guanghu-authz.conf;#' /etc/nginx/sites-enabled/guanghulab
fi
systemctl daemon-reload
systemctl enable --now guanghu-jd-authz-tunnel.service
nginx -t
systemctl reload nginx
rm -f /tmp/guanghu-jd-authz-tunnel.service /tmp/guanghu-authz.nginx.conf /tmp/jd-authz-tunnel-ssh-config.example

View file

@ -0,0 +1,9 @@
Host jd-authz-target
HostName JD_PUBLIC_ADDRESS
User root
IdentityFile /etc/guanghu/secrets/ssh/bs_gz_006_to_jd_authz_proxy
IdentitiesOnly yes
LocalForward 127.0.0.1:19221 127.0.0.1:3921
ExitOnForwardFailure yes
ServerAliveInterval 30
ServerAliveCountMax 3

View file

@ -0,0 +1,25 @@
[Unit]
Description=Guanghu fixed-action broker for Lake Lamp authorization
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=root
Group=root
EnvironmentFile=/etc/guanghu/secrets/lake-lamp/action-broker.env
ExecStart=/usr/bin/node /opt/guanghu/lake-lamp-authz/action-broker.js
Restart=on-failure
RestartSec=3
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadOnlyPaths=/etc/guanghu/action-broker-ssh-config /etc/guanghu/secrets/lake-lamp
RuntimeDirectory=guanghu
RuntimeDirectoryMode=0755
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
LockPersonality=true
[Install]
WantedBy=multi-user.target

View file

@ -0,0 +1,25 @@
[Unit]
Description=Guanghu approved architecture initial-provision broker
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=root
Group=root
EnvironmentFile=/etc/guanghu/secrets/lake-lamp/action-broker.env
ExecStart=/usr/bin/node /opt/guanghu/lake-lamp-authz/architecture-provision-broker.js
Restart=on-failure
RestartSec=3
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/guanghu/architecture-provision /opt/guanghu/architecture-releases /etc/systemd/system
RuntimeDirectory=guanghu-architecture-provision
RuntimeDirectoryMode=0755
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
LockPersonality=true
[Install]
WantedBy=multi-user.target

Some files were not shown because too many files have changed in this diff Show more