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