#!/usr/bin/env node "use strict"; // Resident deployment hand: it consumes only immutable events created by the // explicit dispatch endpoint. It has no public listener and no command field. const fs = require("node:fs"); const path = require("node:path"); const { provision } = require("./architecture-provision-broker"); const { validateDeploymentSource } = require("./deployment-source-policy"); const QUEUE_DIR = process.env.LAKE_LAMP_DEPLOYMENT_EVENT_DIR || "/var/lib/guanghu/deployment-events"; const RECEIPTS_DIR = process.env.LAKE_LAMP_DEPLOYMENT_RECEIPTS_DIR || "/var/lib/guanghu/deployment-events/receipts"; const REGISTRY_FILE = process.env.LAKE_LAMP_DEPLOYMENT_REPOSITORIES || "/etc/guanghu/lake-lamp/deployment-repositories.json"; function loadRegistry(file = REGISTRY_FILE) { const parsed = JSON.parse(fs.readFileSync(file, "utf8")); if (!parsed || !parsed.repos || typeof parsed.repos !== "object") throw new Error("invalid_deployment_repository_registry"); return parsed.repos; } async function processOne(options = {}) { const queueDir = options.queueDir || QUEUE_DIR, receiptsDir = options.receiptsDir || RECEIPTS_DIR; fs.mkdirSync(queueDir, { recursive: true, mode: 0o750 }); fs.mkdirSync(receiptsDir, { recursive: true, mode: 0o700 }); const file = fs.readdirSync(queueDir).filter(name => name.endsWith(".json")).sort()[0]; if (!file) return { ok: true, state: "idle" }; const original = path.join(queueDir, file), processing = `${original}.processing`; try { fs.renameSync(original, processing); } catch { return { ok: true, state: "contended" }; } let event; try { event = JSON.parse(fs.readFileSync(processing, "utf8")); const checked = validateEvent(event, options.registry || loadRegistry(options.registryFile)); if (checked) throw new Error(checked); const result = await (options.provisionFn || provision)({ target: event.target, action: "provision-approved-architecture", resource: event.resource }, { repoUrl: options.registry ? options.registry[event.repo].repo_url : loadRegistry(options.registryFile)[event.repo].repo_url }); const receipt = { schema: "guanghu.deployment-agent-receipt/v1", event_id: event.event_id, workorder_id: event.workorder_id, repo: event.repo, branch: event.branch, commit_sha: event.commit_sha, resource: event.resource, result: result.ok ? "DEPLOYED_AND_VERIFIED" : "FAILED_OR_ROLLED_BACK", diagnostic_code: result.ok ? "deployment_succeeded" : String(result.error || "deployment_failed"), evidence: result, recorded_at: new Date().toISOString() }; writeAtomic(path.join(receiptsDir, `${event.event_id}.json`), `${JSON.stringify(receipt, null, 2)}\n`); fs.renameSync(processing, `${processing}.${result.ok ? "done" : "failed"}`); return { ok: result.ok, state: receipt.result, receipt }; } catch (error) { const receipt = { schema: "guanghu.deployment-agent-receipt/v1", event_id: event && event.event_id || "", result: "REJECTED", diagnostic_code: String(error && error.message || "deployment_event_invalid").slice(0, 160), recorded_at: new Date().toISOString() }; writeAtomic(path.join(receiptsDir, `${path.basename(file, ".json")}.rejected.json`), `${JSON.stringify(receipt, null, 2)}\n`); fs.renameSync(processing, `${processing}.rejected`); return { ok: false, state: "REJECTED", receipt }; } } 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"; 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); } if (require.main === module) setInterval(() => { processOne().catch(error => process.stderr.write(`deployment event error: ${error.message}\n`)); }, 1500); module.exports = { processOne, validateEvent };