deploy(ai-discovery): add safe existing-service update path

This commit is contained in:
冰朔 2026-07-27 15:05:46 +08:00
commit 2739104088
14 changed files with 427 additions and 26 deletions

View file

@ -11,6 +11,9 @@ 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
Environment=GUANGHU_NODE_MAP=/opt/guanghu/ai-discovery/server-node-map.json
Environment=GUANGHU_SUBJECT_REGISTRY=/opt/guanghu/ai-discovery/fifth-domain-subject-registry.json
Environment=GUANGHU_SUBJECT_ALIAS_MAP=/opt/guanghu/ai-discovery/subject-id-alias-map.json
ExecStart=/usr/bin/node /opt/guanghu/ai-discovery/server.js
Restart=always
RestartSec=5

View file

@ -102,23 +102,27 @@ node server-tools/lake-lamp-authz/authorize-repo-push.js \
- `restore-code-channel-owner-login` 只把京东本机旧第五域数据库中的 `bingshuo` 密码摘要恢复到新代码频道,同时校正启用、管理员和禁止登录状态。它不读取明文密码,不修改 SSH并在变更前使用 SQLite backup API 生成一致性备份。
- 登录问题先执行 `inspect-code-channel-owner-auth`。只有确认新旧频道凭证不一致时,才申请后一项恢复动作;不得绕到新加坡灾备节点,不得向冰朔索要密码。
## 新架构首次部署
## 首次部署与已有服务更新
旧的 `deploy-registered-service` 只能操作已经登记的服务,不能承担首次安装。新架构统一使用固定动作 `provision-approved-architecture`,并把仓库请求编号与不可变提交绑定进工单:
```bash
node request-workorder.js \
--url https://guanghulab.com/authz \
--persona ICE-GL-ZY001 \
--persona ICE-P-ZY001 \
--name 铸渊 \
--target JD-FD-PRIMARY \
--scope server-ops \
--action provision-approved-architecture \
--resource 'REQUEST-ID@40位提交SHA' \
--description '首次安装已审核架构包'
--description '安装或更新已审核的不可变部署包'
```
邮件或可信对话签字页面必须显示同一个 `resource`。批准会话不能切换请求编号或提交。执行器只读取该提交中 `deployment/requests/<REQUEST-ID>.json`,只复制清单列出的普通文件,只安装清单指定的非 root、加固 systemd 单元,并只接受回环健康检查。人格体可以使用清单声明的独立低权限账户、共享模型密钥文件和状态目录;密钥路径必须位于 `/etc/guanghu/persona-secrets/`,可写路径必须位于 `/var/lib/guanghu/personas/<运行账户>/`。覆盖旧单元前强制备份,启动或验收失败时自动恢复。说明文字不能改变部署内容,也不开放任意 shell。
邮件或可信对话签字页面必须显示同一个 `resource`。批准会话不能切换请求编号或提交,清单路径也必须严格等于 `deployment/requests/<REQUEST-ID>.json`
首次部署执行器只复制清单列出的普通文件,只安装清单指定的非 root、加固 systemd 单元,并只接受回环健康检查。人格体可以使用清单声明的独立低权限账户、共享模型密钥文件和状态目录;密钥路径必须位于 `/etc/guanghu/persona-secrets/`,可写路径必须位于 `/var/lib/guanghu/personas/<运行账户>/`
已有服务更新必须使用 `guanghu.existing-service-update-request/v1`,不得伪装成首次部署。执行器先确认旧单元和必需旧文件存在,再备份单元及每个声明目标,只写入清单列出的 `/opt/guanghu/<服务>/` 文件,重启原单元并逐项执行回环验收;任一检查失败即恢复全部旧文件和旧单元。说明文字不能改变部署内容,也不开放任意 shell。
这套入口本身需要在京东主控上一次性安装:
@ -126,4 +130,4 @@ node request-workorder.js \
sudo bash server-tools/lake-lamp-authz/install-architecture-provisioner.sh
```
这是最后一次需要云厂商控制台或现有系统管理通道的引导。安装完成后,未来新架构均走上面的结构化工单,不必预先把每个未来模块写进旧动作桥
引导本身也是一次受控部署:应从已经存在的主人邮件批准系统管理通道执行,不得为此恢复人类 SSH 登录。若当前服务器尚未登记能够安装此运行时的固定动作,状态必须记为 `BOOTSTRAP_ACTION_NOT_DEPLOYED`,不能把仓库提交、工单批准或打开云后台冒充成运行时已部署。引导完成并读回三个服务均为 active 后,未来安装与更新才走通感桥的结构化工单

View file

@ -24,19 +24,59 @@ function safeRelative(value) {
}
function validateManifest(manifest, resource) {
if (!manifest || manifest.schema !== "guanghu.architecture-provision-request/v1") throw new Error("invalid_manifest_schema");
if (!manifest || !["guanghu.architecture-provision-request/v1", "guanghu.existing-service-update-request/v1"].includes(manifest.schema)) 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 (manifest.schema === "guanghu.existing-service-update-request/v1") return validateServiceUpdateManifest(manifest, unit);
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");
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 };
return { kind: "initial-provision", unit, unitSource: unitMatches[0], sourcePaths: manifest.source_paths, runtimeCheck: check };
}
function validateServiceUpdateManifest(manifest, unit) {
if (manifest.status !== "SERVICE_UPDATE_PACKAGE_READY · DEPLOYMENT_PENDING") throw new Error("manifest_not_pending");
if (!manifest.service_update || manifest.service_update.kind !== "existing-systemd-service" || manifest.service_update.require_existing_unit !== true) throw new Error("not_existing_service_update");
const installRoot = String(manifest.module && manifest.module.install_root || "");
if (!/^\/opt\/guanghu\/[a-z0-9][a-z0-9-]{1,62}$/.test(installRoot)) throw new Error("invalid_service_install_root");
const unitSource = String(manifest.unit_source || "");
if (!safeRelative(unitSource) || path.basename(unitSource) !== unit) throw new Error("invalid_unit_source");
if (!Array.isArray(manifest.files) || manifest.files.length < 1 || manifest.files.length > 64) throw new Error("invalid_update_files");
const seenDestinations = new Set();
for (const item of manifest.files) {
if (!item || !safeRelative(item.source) || !safeRelative(item.destination)) throw new Error("invalid_update_file_path");
if (!/^(?:0?644|0?755)$/.test(String(item.mode || ""))) throw new Error("invalid_update_file_mode");
if (seenDestinations.has(item.destination)) throw new Error("duplicate_update_destination");
seenDestinations.add(item.destination);
}
const requiredExisting = manifest.service_update.required_existing_files || [];
if (!Array.isArray(requiredExisting) || requiredExisting.some(item => !safeRelative(item) || !seenDestinations.has(item))) throw new Error("invalid_required_existing_files");
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");
const acceptanceChecks = manifest.acceptance_checks || [];
if (!Array.isArray(acceptanceChecks) || acceptanceChecks.length > 16) throw new Error("invalid_acceptance_checks");
for (const item of acceptanceChecks) {
if (!item || !/^http:\/\/127\.0\.0\.1:\d{2,5}\/[A-Za-z0-9._/?=&-]*$/.test(String(item.url || ""))) throw new Error("invalid_acceptance_check_url");
if (!item.expected || typeof item.expected !== "object" || Array.isArray(item.expected)) throw new Error("invalid_acceptance_expectation");
}
return {
kind: "existing-service-update",
unit,
unitSource,
installRoot,
files: manifest.files,
requiredExisting,
sourcePaths: [unitSource, ...manifest.files.map(item => item.source)],
runtimeCheck: check,
acceptanceChecks,
};
}
function declaredPaths(value) {
@ -69,6 +109,17 @@ function validateUnit(text, expectedUser = "guanghu", policy = {}) {
return value;
}
function validateUpdateUnit(text, expectedUser, installRoot) {
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 (!value.includes(`WorkingDirectory=${installRoot}`) || !value.includes(`ReadOnlyPaths=${installRoot}`)) throw new Error("service_install_root_not_confined");
const execStart = value.match(/^ExecStart=(.+)$/m);
if (!execStart || !execStart[1].includes(`${installRoot}/`) || /[;&|`$<>]/.test(execStart[1])) throw new Error("service_exec_start_not_confined");
if (/^(SupplementaryGroups|AmbientCapabilities|BindPaths|BindReadOnlyPaths|RootDirectory|RootImage|DeviceAllow|EnvironmentFile|ReadWritePaths)=/m.test(value)) throw new Error("privileged_unit_directive_forbidden");
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);
@ -84,10 +135,17 @@ async function provision(request, options = {}) {
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);
let checked = validateManifest(manifest, resource);
if (checked.kind === "existing-service-update") {
if (options.installRootOverride) checked = { ...checked, installRoot: options.installRootOverride };
return await updateExistingService({
checked, manifest, resource, repoDir, releasesDir, unitDir, receiptsDir, run,
getJson: options.getJson, healthAttempts: options.healthAttempts, healthDelayMs: options.healthDelayMs,
});
}
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);
for (const relative of checked.sourcePaths) 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 });
@ -120,6 +178,98 @@ async function provision(request, options = {}) {
}
}
async function updateExistingService(context) {
const { checked, manifest, resource, repoDir, releasesDir, unitDir, receiptsDir, run } = context;
const releaseRoot = path.join(releasesDir, resource.commit);
const installedUnit = path.join(unitDir, checked.unit);
const backupDir = path.join(receiptsDir, "backups", resource.requestId, resource.commit);
const backups = [];
let updateStarted = false;
try {
if (!fs.existsSync(installedUnit) || !fs.lstatSync(installedUnit).isFile() || fs.lstatSync(installedUnit).isSymbolicLink()) throw new Error("existing_service_unit_required");
for (const relative of checked.requiredExisting) {
const existing = path.join(checked.installRoot, relative);
if (!fs.existsSync(existing) || !fs.lstatSync(existing).isFile() || fs.lstatSync(existing).isSymbolicLink()) throw new Error(`required_existing_file_missing:${relative}`);
}
fs.mkdirSync(releaseRoot, { recursive: true, mode: 0o755 });
for (const relative of checked.sourcePaths) copyDeclaredFile(repoDir, releaseRoot, relative);
const unitText = validateUpdateUnit(fs.readFileSync(path.join(releaseRoot, checked.unitSource), "utf8"), String(manifest.module.run_user || ""), checked.installRoot);
fs.mkdirSync(backupDir, { recursive: true, mode: 0o700 });
backupFile(installedUnit, path.join(backupDir, "systemd", checked.unit), backups);
for (const item of checked.files) {
const destination = path.join(checked.installRoot, item.destination);
backupFile(destination, path.join(backupDir, "files", item.destination), backups);
}
updateStarted = true;
fs.mkdirSync(checked.installRoot, { recursive: true, mode: 0o755 });
for (const item of checked.files) {
const destination = path.join(checked.installRoot, item.destination);
if (fs.existsSync(destination) && fs.lstatSync(destination).isSymbolicLink()) throw new Error("update_destination_symlink_forbidden");
fs.mkdirSync(path.dirname(destination), { recursive: true, mode: 0o755 });
writeAtomic(destination, fs.readFileSync(path.join(releaseRoot, item.source)), Number.parseInt(String(item.mode), 8));
}
writeAtomic(installedUnit, unitText, 0o644);
await run("/usr/bin/systemctl", ["daemon-reload"]);
await run("/usr/bin/systemctl", ["restart", checked.unit]);
const runtime = await getJsonWithRetry(checked.runtimeCheck.url, context.getJson, context.healthAttempts, context.healthDelayMs);
for (const [key, expected] of Object.entries(checked.runtimeCheck.expected)) if (runtime[key] !== expected) throw new Error(`runtime_check_failed:${key}`);
for (const check of checked.acceptanceChecks) {
const observed = await getJsonWithRetry(check.url, context.getJson, context.healthAttempts, context.healthDelayMs);
for (const [key, expected] of Object.entries(check.expected)) if (observed[key] !== expected) throw new Error(`acceptance_check_failed:${key}`);
}
const receipt = {
schema: "guanghu.existing-service-update-receipt/v1",
request_id: resource.requestId,
source_commit: resource.commit,
target_node: "JD-FD-PRIMARY",
unit: checked.unit,
install_root: checked.installRoot,
runtime_check: checked.runtimeCheck.url,
acceptance_checks: checked.acceptanceChecks.map(item => item.url),
backup: path.join("backups", resource.requestId, resource.commit),
rollback: "restore-all-declared-files-and-previous-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 (updateStarted) {
try {
restoreBackups(backups);
await run("/usr/bin/systemctl", ["daemon-reload"]);
await run("/usr/bin/systemctl", ["restart", checked.unit]);
} catch { /* The original error remains authoritative; backups stay available for recovery. */ }
}
return { ok: false, error: String(error && error.message || "service_update_failed").slice(0, 240) };
}
}
function backupFile(source, backup, records) {
if (!fs.existsSync(source)) {
records.push({ source, backup, existed: false });
return;
}
const stat = fs.lstatSync(source);
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error("backup_source_not_regular_file");
fs.mkdirSync(path.dirname(backup), { recursive: true, mode: 0o700 });
fs.copyFileSync(source, backup, fs.constants.COPYFILE_EXCL);
fs.chmodSync(backup, stat.mode & 0o777);
records.push({ source, backup, existed: true, mode: stat.mode & 0o777 });
}
function restoreBackups(records) {
for (const record of records.slice().reverse()) {
if (!record.existed) {
fs.rmSync(record.source, { force: true });
continue;
}
fs.mkdirSync(path.dirname(record.source), { recursive: true, mode: 0o755 });
writeAtomic(record.source, fs.readFileSync(record.backup), record.mode);
}
}
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]);
@ -197,4 +347,4 @@ if (require.main === module) {
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 };
module.exports = { parseResource, safeRelative, validateManifest, validateUnit, validateUpdateUnit, provision };

View file

@ -5,7 +5,7 @@ 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 { parseResource, safeRelative, validateManifest, validateUnit, validateUpdateUnit, provision } = require("./architecture-provision-broker");
const commit = "d".repeat(40);
const requestId = "GLS-0231-JD-LAN-01-INITIAL-PROVISION-20260720";
@ -44,6 +44,25 @@ test("unit permits a declared persona user, shared secret and state directory",
assert.throws(() => validateUnit(unit.replaceAll("kezhou", "root"), "root", policy), /dedicated_service_user_required/);
});
test("AI discovery update package declares all four route maps and passes the existing-service policy", () => {
const root = path.resolve(__dirname, "../..");
const request = JSON.parse(fs.readFileSync(path.join(root, "deployment", "requests", "AI-DISCOVERY-ICE-P-ROUTE-20260727.json")));
const checked = validateManifest(request, { requestId: request.request_id, commit });
assert.equal(checked.kind, "existing-service-update");
assert.deepEqual(checked.files.map(item => item.destination), [
"server.js",
"repository-route-map.json",
"server-node-map.json",
"fifth-domain-subject-registry.json",
"subject-id-alias-map.json",
]);
const unit = fs.readFileSync(path.join(root, request.unit_source), "utf8");
assert.equal(validateUpdateUnit(unit, request.module.run_user, request.module.install_root), unit);
for (const variable of ["GUANGHU_REPOSITORY_MAP", "GUANGHU_NODE_MAP", "GUANGHU_SUBJECT_REGISTRY", "GUANGHU_SUBJECT_ALIAS_MAP"]) {
assert.match(unit, new RegExp(`^Environment=${variable}=`, "m"));
}
});
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");
@ -75,3 +94,96 @@ test("provision copies only declared files and verifies loopback health", async
assert.equal(healthChecks, 2);
} finally { fs.rmSync(root, { recursive: true, force: true }); }
});
test("existing service update backs up declared files, restarts, and verifies identity routes", async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "existing-service-update-"));
const repoDir = path.join(root, "repo"), releasesDir = path.join(root, "releases");
const unitDir = path.join(root, "units"), receiptsDir = path.join(root, "receipts");
const installRoot = path.join(root, "opt", "ai-discovery");
const updateRequestId = "AI-DISCOVERY-ICE-P-ROUTE-20260727";
const updateManifest = {
schema: "guanghu.existing-service-update-request/v1",
request_id: updateRequestId,
target_node: "JD-FD-PRIMARY",
status: "SERVICE_UPDATE_PACKAGE_READY · DEPLOYMENT_PENDING",
module: { unit: "guanghu-ai-discovery.service", run_user: "guanghu", install_root: "/opt/guanghu/ai-discovery" },
service_update: { kind: "existing-systemd-service", require_existing_unit: true, required_existing_files: ["server.js"] },
unit_source: "server-tools/ai-discovery/guanghu-ai-discovery.service",
files: [{ source: "server-tools/ai-discovery/server.js", destination: "server.js", mode: "0644" }],
runtime_check: { url: "http://127.0.0.1:3922/health", expected: { ok: true, mode: "read-only" } },
acceptance_checks: [{ url: "http://127.0.0.1:3922/v1/resolve?id=ICE-GL-ZY001", expected: { canonical_id: "ICE-P-ZY001", redirected: true } }],
};
fs.mkdirSync(path.join(repoDir, "deployment", "requests"), { recursive: true });
fs.mkdirSync(path.join(repoDir, "server-tools", "ai-discovery"), { recursive: true });
fs.mkdirSync(unitDir, { recursive: true });
fs.mkdirSync(installRoot, { recursive: true });
fs.writeFileSync(path.join(repoDir, "deployment", "requests", `${updateRequestId}.json`), JSON.stringify(updateManifest));
fs.writeFileSync(path.join(repoDir, "server-tools", "ai-discovery", "server.js"), "new server\n");
const newUnit = `[Service]\nUser=guanghu\nGroup=guanghu\nWorkingDirectory=${installRoot}\nNoNewPrivileges=true\nPrivateTmp=true\nProtectSystem=strict\nProtectHome=true\nReadOnlyPaths=${installRoot}\nExecStart=/usr/bin/node ${installRoot}/server.js\n`;
fs.writeFileSync(path.join(repoDir, "server-tools", "ai-discovery", "guanghu-ai-discovery.service"), newUnit);
fs.writeFileSync(path.join(installRoot, "server.js"), "old server\n");
fs.writeFileSync(path.join(unitDir, "guanghu-ai-discovery.service"), "old unit\n");
const commands = [];
try {
const result = await provision({ target: "JD-FD-PRIMARY", action: "provision-approved-architecture", resource: `${updateRequestId}@${commit}` }, {
repoDir, releasesDir, unitDir, receiptsDir, installRootOverride: installRoot,
run: async (file, args) => { commands.push([file, args]); return { stdout: args.includes("rev-parse") ? `${commit}\n` : "" }; },
getJson: async url => url.endsWith("/health") ? { ok: true, mode: "read-only" } : { canonical_id: "ICE-P-ZY001", redirected: true },
healthDelayMs: 0,
});
assert.equal(result.ok, true);
assert.equal(fs.readFileSync(path.join(installRoot, "server.js"), "utf8"), "new server\n");
assert.equal(commands.some(([, args]) => args[0] === "restart" && args[1] === "guanghu-ai-discovery.service"), true);
const receipt = JSON.parse(fs.readFileSync(path.join(receiptsDir, `${updateRequestId}.json`)));
assert.equal(receipt.result, "DEPLOYED_AND_VERIFIED");
assert.equal(receipt.acceptance_checks.length, 1);
} finally { fs.rmSync(root, { recursive: true, force: true }); }
});
test("existing service update restores every changed file when acceptance fails", async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "existing-service-rollback-"));
const repoDir = path.join(root, "repo"), releasesDir = path.join(root, "releases");
const unitDir = path.join(root, "units"), receiptsDir = path.join(root, "receipts");
const installRoot = path.join(root, "opt", "ai-discovery");
const updateRequestId = "AI-DISCOVERY-ROLLBACK-TEST";
const updateManifest = {
schema: "guanghu.existing-service-update-request/v1",
request_id: updateRequestId,
target_node: "JD-FD-PRIMARY",
status: "SERVICE_UPDATE_PACKAGE_READY · DEPLOYMENT_PENDING",
module: { unit: "guanghu-ai-discovery.service", run_user: "guanghu", install_root: "/opt/guanghu/ai-discovery" },
service_update: { kind: "existing-systemd-service", require_existing_unit: true, required_existing_files: ["server.js"] },
unit_source: "server-tools/ai-discovery/guanghu-ai-discovery.service",
files: [
{ source: "server-tools/ai-discovery/server.js", destination: "server.js", mode: "0644" },
{ source: "identity/alias.json", destination: "alias.json", mode: "0644" }
],
runtime_check: { url: "http://127.0.0.1:3922/health", expected: { ok: true } },
acceptance_checks: [],
};
fs.mkdirSync(path.join(repoDir, "deployment", "requests"), { recursive: true });
fs.mkdirSync(path.join(repoDir, "server-tools", "ai-discovery"), { recursive: true });
fs.mkdirSync(path.join(repoDir, "identity"), { recursive: true });
fs.mkdirSync(unitDir, { recursive: true });
fs.mkdirSync(installRoot, { recursive: true });
fs.writeFileSync(path.join(repoDir, "deployment", "requests", `${updateRequestId}.json`), JSON.stringify(updateManifest));
fs.writeFileSync(path.join(repoDir, "server-tools", "ai-discovery", "server.js"), "new server\n");
fs.writeFileSync(path.join(repoDir, "identity", "alias.json"), "{}\n");
fs.writeFileSync(path.join(repoDir, "server-tools", "ai-discovery", "guanghu-ai-discovery.service"), `[Service]\nUser=guanghu\nGroup=guanghu\nWorkingDirectory=${installRoot}\nNoNewPrivileges=true\nPrivateTmp=true\nProtectSystem=strict\nProtectHome=true\nReadOnlyPaths=${installRoot}\nExecStart=/usr/bin/node ${installRoot}/server.js\n`);
fs.writeFileSync(path.join(installRoot, "server.js"), "old server\n");
fs.writeFileSync(path.join(unitDir, "guanghu-ai-discovery.service"), "old unit\n");
try {
const result = await provision({ target: "JD-FD-PRIMARY", action: "provision-approved-architecture", resource: `${updateRequestId}@${commit}` }, {
repoDir, releasesDir, unitDir, receiptsDir, installRootOverride: installRoot,
run: async (file, args) => ({ stdout: args.includes("rev-parse") ? `${commit}\n` : "" }),
getJson: async () => ({ ok: false }),
healthAttempts: 1,
healthDelayMs: 0,
});
assert.equal(result.ok, false);
assert.match(result.error, /runtime_check_failed:ok/);
assert.equal(fs.readFileSync(path.join(installRoot, "server.js"), "utf8"), "old server\n");
assert.equal(fs.existsSync(path.join(installRoot, "alias.json")), false);
assert.equal(fs.readFileSync(path.join(unitDir, "guanghu-ai-discovery.service"), "utf8"), "old unit\n");
} finally { fs.rmSync(root, { recursive: true, force: true }); }
});

View file

@ -46,8 +46,10 @@ async function processOne(options = {}) {
function validateEvent(event, registry) {
if (!event || event.schema !== "guanghu.deployment-event/v1" || event.state !== "queued_for_resident_agent") return "deployment_event_schema_invalid";
if (!registry[event.repo] || !/^bingshuo\/[a-z0-9._-]+$/.test(event.repo) || event.branch !== "main" || !/^[0-9a-f]{40}$/.test(event.commit_sha || "")) return "deployment_event_binding_invalid";
if (!/^[A-Z0-9][A-Z0-9._-]{5,119}@[0-9a-f]{40}$/.test(event.resource || "") || !event.resource.endsWith(`@${event.commit_sha}`)) return "deployment_event_resource_invalid";
const resource = String(event.resource || "").match(/^([A-Z0-9][A-Z0-9._-]{5,119})@([0-9a-f]{40})$/);
if (!resource || resource[2] !== event.commit_sha) return "deployment_event_resource_invalid";
if (!/^deployment\/requests\/[A-Za-z0-9._/-]{1,180}\.json$/.test(event.manifest || "")) return "deployment_event_manifest_invalid";
if (event.manifest !== `deployment/requests/${resource[1]}.json`) return "deployment_event_manifest_resource_mismatch";
return validateDeploymentSource(event, registry);
}
function writeAtomic(file, content) { const temp = `${file}.${process.pid}.tmp`; fs.writeFileSync(temp, content, { mode: 0o600 }); fs.renameSync(temp, file); }

View file

@ -7,7 +7,7 @@ const path = require("node:path");
const { processOne } = require("./deployment-event-worker");
test("resident deployment agent consumes only an explicit immutable event and writes a receipt", async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "lake-lamp-deploy-worker-")), queue = path.join(root, "queue"), receipts = path.join(root, "receipts"), sha = "a".repeat(40);
fs.mkdirSync(queue); fs.writeFileSync(path.join(queue, "event.json"), JSON.stringify({ schema: "guanghu.deployment-event/v1", event_id: "event-1", state: "queued_for_resident_agent", repo: "bingshuo/guanghu-ice-heart", branch: "main", commit_sha: sha, workorder_id: "order-1", resource: `GLS-0239-DEPLOY@${sha}`, manifest: "deployment/requests/GLS-0239.json" }));
fs.mkdirSync(queue); fs.writeFileSync(path.join(queue, "event.json"), JSON.stringify({ schema: "guanghu.deployment-event/v1", event_id: "event-1", state: "queued_for_resident_agent", repo: "bingshuo/guanghu-ice-heart", branch: "main", commit_sha: sha, workorder_id: "order-1", resource: `GLS-0239-DEPLOY@${sha}`, manifest: "deployment/requests/GLS-0239-DEPLOY.json" }));
try {
const result = await processOne({ queueDir: queue, receiptsDir: receipts, registry: { "bingshuo/guanghu-ice-heart": { repo_url: "https://example.invalid/code.git" } }, provisionFn: async request => { assert.equal(request.resource, `GLS-0239-DEPLOY@${sha}`); return { ok: true, unit: "example.service" }; } });
assert.equal(result.state, "DEPLOYED_AND_VERIFIED");
@ -27,7 +27,7 @@ test("resident agent rechecks source ownership before deployment", async () => {
commit_sha: sha,
workorder_id: "order-2",
resource: `HLP-PERSONAL-DEPLOY@${sha}`,
manifest: "deployment/requests/HLP-PERSONAL.json",
manifest: "deployment/requests/HLP-PERSONAL-DEPLOY.json",
authorizer_id: "ICE-GL∞",
persona_id: "AGE-TEAM-001",
execution_runtime_id: "SYS-GLW-ZY-EXEC-0001",

View file

@ -35,8 +35,10 @@ function enqueueDeploymentEvent(intent, push, queueDir, context = {}) {
function validateIntent(intent, push, context = {}) {
if (intent.schema !== "guanghu.deployment-intent/v1") return "deployment_intent_schema_invalid";
if (String(intent.repo || "").toLowerCase() !== push.repo || intent.branch !== push.branch || String(intent.commit_sha || "").toLowerCase() !== push.commit_sha) return "deployment_intent_binding_mismatch";
if (!/^[A-Z0-9][A-Z0-9._-]{5,119}@[0-9a-f]{40}$/.test(String(intent.resource || "")) || !String(intent.resource).endsWith(`@${push.commit_sha}`)) return "deployment_intent_resource_invalid";
const resource = String(intent.resource || "").match(/^([A-Z0-9][A-Z0-9._-]{5,119})@([0-9a-f]{40})$/);
if (!resource || resource[2] !== push.commit_sha) return "deployment_intent_resource_invalid";
if (!/^deployment\/requests\/[A-Za-z0-9._/-]{1,180}\.json$/.test(String(intent.manifest || ""))) return "deployment_intent_manifest_invalid";
if (intent.manifest !== `deployment/requests/${resource[1]}.json`) return "deployment_intent_manifest_resource_mismatch";
if (context.registry) {
return validateDeploymentSource({
repo: push.repo,

View file

@ -32,10 +32,10 @@ test("only an immutable deployment intent creates a resident-agent event", () =>
const queue = fs.mkdtempSync(path.join(os.tmpdir(), "lake-lamp-deploy-events-"));
const sha = "a".repeat(40), push = { repo: "bingshuo/guanghu-ice-heart", branch: "main", commit_sha: sha };
try {
const queued = enqueueDeploymentEvent({ schema: "guanghu.deployment-intent/v1", repo: push.repo, branch: "main", commit_sha: sha, resource: `GLS-0239-DEPLOY@${sha}`, manifest: "deployment/requests/GLS-0239.json", workorder_id: "order-1" }, push, queue);
const queued = enqueueDeploymentEvent({ schema: "guanghu.deployment-intent/v1", repo: push.repo, branch: "main", commit_sha: sha, resource: `GLS-0239-DEPLOY@${sha}`, manifest: "deployment/requests/GLS-0239-DEPLOY.json", workorder_id: "order-1" }, push, queue);
assert.equal(queued.state, "queued_for_resident_agent");
assert.equal(fs.readdirSync(queue).length, 1);
assert.equal(enqueueDeploymentEvent({ schema: "guanghu.deployment-intent/v1", repo: push.repo, branch: "main", commit_sha: sha, resource: `GLS-0239-DEPLOY@${"b".repeat(40)}`, manifest: "deployment/requests/GLS-0239.json" }, push, queue).diagnostic_code, "deployment_intent_resource_invalid");
assert.equal(enqueueDeploymentEvent({ schema: "guanghu.deployment-intent/v1", repo: push.repo, branch: "main", commit_sha: sha, resource: `GLS-0239-DEPLOY@${"b".repeat(40)}`, manifest: "deployment/requests/GLS-0239-DEPLOY.json" }, push, queue).diagnostic_code, "deployment_intent_resource_invalid");
} finally { fs.rmSync(queue, { recursive: true, force: true }); }
});
test("team personas cannot dispatch Ice Shuo personal source", () => {
@ -48,7 +48,7 @@ test("team personas cannot dispatch Ice Shuo personal source", () => {
branch: push.branch,
commit_sha: sha,
resource: `HLP-PERSONAL-DEPLOY@${sha}`,
manifest: "deployment/requests/HLP-PERSONAL.json",
manifest: "deployment/requests/HLP-PERSONAL-DEPLOY.json",
deployment_source: personalSource,
};
try {

View file

@ -10,16 +10,25 @@ 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
for file in server.js workorder-manager.js map-gate.js smtp-mailer.js action-client.js architecture-provision-broker.js deployment-event.js deployment-event-worker.js deployment-source-policy.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 -m 0644 "$script_dir/lake-lamp-deployment-event-worker.service" /etc/systemd/system/lake-lamp-deployment-event-worker.service
install -d -m 0700 /var/lib/guanghu/architecture-provision
install -d -m 0750 /var/lib/guanghu/deployment-events
install -d -m 0700 /var/lib/guanghu/deployment-events/receipts
install -d -m 0755 /opt/guanghu/architecture-releases
install -d -m 0755 /etc/guanghu/lake-lamp
if [[ ! -e /etc/guanghu/lake-lamp/deployment-repositories.json ]]; then
install -m 0644 "$script_dir/deployment-repositories.example.json" /etc/guanghu/lake-lamp/deployment-repositories.json
fi
systemctl daemon-reload
systemctl enable --now lake-lamp-architecture-provision.service
systemctl restart lake-lamp-authz.service
systemctl enable --now lake-lamp-deployment-event-worker.service
systemctl is-active --quiet lake-lamp-architecture-provision.service
systemctl is-active --quiet lake-lamp-authz.service
systemctl is-active --quiet lake-lamp-deployment-event-worker.service
echo ARCHITECTURE_PROVISIONER_INSTALLED

View file

@ -14,7 +14,7 @@ NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/guanghu/deployment-events /var/lib/guanghu/architecture-provision /opt/guanghu/architecture-releases /etc/systemd/system
ReadWritePaths=/var/lib/guanghu/deployment-events /var/lib/guanghu/architecture-provision /opt/guanghu/architecture-releases /opt/guanghu/ai-discovery /etc/systemd/system
LockPersonality=true
[Install]

View file

@ -312,7 +312,7 @@ test("deployment is dispatched only by an explicit approved second signal", asyn
const common = { persona_id: "ICE-GL-ZY001", target: "JD-FD-PRIMARY", scope: "server-ops" };
const map = await (await fetch(`${base}/api/navigation-map/read`, { method: "POST", headers: { authorization: `Bearer ${session.session_token}`, "content-type": "application/json" }, body: JSON.stringify(common) })).json();
await fetch(`${base}/api/navigation-map/ack`, { method: "POST", headers: { authorization: `Bearer ${session.session_token}`, "content-type": "application/json" }, body: JSON.stringify({ ...common, map_hash: map.map_hash }) });
const dispatch = await fetch(`${base}/api/deployment/dispatch`, { method: "POST", headers: { authorization: `Bearer ${session.session_token}`, "content-type": "application/json" }, body: JSON.stringify({ ...common, repo: "bingshuo/guanghu-ice-heart", branch: "main", commit_sha: sha, resource, manifest: "deployment/requests/GLS-0239.json" }) });
const dispatch = await fetch(`${base}/api/deployment/dispatch`, { method: "POST", headers: { authorization: `Bearer ${session.session_token}`, "content-type": "application/json" }, body: JSON.stringify({ ...common, repo: "bingshuo/guanghu-ice-heart", branch: "main", commit_sha: sha, resource, manifest: "deployment/requests/GLS-0239-DEPLOY.json" }) });
assert.equal(dispatch.status, 202);
assert.equal((await dispatch.json()).receipt.state, "queued");
assert.equal(fs.readdirSync(path.join(dir, "queue")).length, 1);