fix: separate Fifth Domain source deployment identities

This commit is contained in:
冰朔 2026-07-26 17:56:47 +08:00
commit 83589aa2c2
27 changed files with 620 additions and 73 deletions

View file

@ -2,7 +2,7 @@
"schema": "guanghu.approver-registry/v1",
"approvers": [
{
"id": "sovereign-owner",
"id": "ICE-GL∞",
"email": "SET_IN_PRIVATE_SERVER_FILE",
"default": true,
"persona_ids": ["ICE-GL-ZY001"],

View file

@ -6,6 +6,7 @@
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";
@ -30,7 +31,7 @@ async function processOne(options = {}) {
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: "JD-FD-PRIMARY", action: "provision-approved-architecture", resource: event.resource }, { repoUrl: options.registry ? options.registry[event.repo].repo_url : loadRegistry(options.registryFile)[event.repo].repo_url });
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"}`);
@ -47,7 +48,7 @@ function validateEvent(event, registry) {
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";
if (!/^deployment\/requests\/[A-Za-z0-9._/-]{1,180}\.json$/.test(event.manifest || "")) return "deployment_event_manifest_invalid";
return "";
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

@ -14,3 +14,60 @@ test("resident deployment agent consumes only an explicit immutable event and wr
assert.equal(JSON.parse(fs.readFileSync(path.join(receipts, "event-1.json"))).diagnostic_code, "deployment_succeeded");
} finally { fs.rmSync(root, { recursive: true, force: true }); }
});
test("resident agent rechecks source ownership before deployment", async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "lake-lamp-source-policy-"));
const queue = path.join(root, "queue"), receipts = path.join(root, "receipts"), sha = "b".repeat(40);
fs.mkdirSync(queue);
fs.writeFileSync(path.join(queue, "event.json"), JSON.stringify({
schema: "guanghu.deployment-event/v1",
event_id: "event-personal-source",
state: "queued_for_resident_agent",
repo: "bingshuo/hololake-platform",
branch: "main",
commit_sha: sha,
workorder_id: "order-2",
resource: `HLP-PERSONAL-DEPLOY@${sha}`,
manifest: "deployment/requests/HLP-PERSONAL.json",
authorizer_id: "ICE-GL∞",
persona_id: "AGE-TEAM-001",
execution_runtime_id: "SYS-GLW-ZY-EXEC-0001",
target: "JD-FD-PRIMARY",
deployment_source: {
repository_id: "REPO-008",
channel_id: "HLP-CHANNEL-0001",
distribution: "personal",
owner_id: "ICE-GL∞",
},
}));
let provisioned = false;
try {
const result = await processOne({
queueDir: queue,
receiptsDir: receipts,
registry: {
"bingshuo/hololake-platform": {
repo_url: "https://example.invalid/hololake-platform.git",
deployment_policy: {
profiles: [{
repository_id: "REPO-008",
channel_id: "HLP-CHANNEL-0001",
distribution: "personal",
source_owner_id: "ICE-GL∞",
allowed_authorizers: ["ICE-GL∞"],
allowed_personas: ["ICE-GL-ZY001"],
allowed_execution_runtimes: ["SYS-GLW-ZY-EXEC-0001"],
allowed_targets: ["JD-FD-PRIMARY"],
}],
},
},
},
provisionFn: async () => {
provisioned = true;
return { ok: true };
},
});
assert.equal(result.state, "REJECTED");
assert.equal(result.receipt.diagnostic_code, "deployment_persona_not_allowed");
assert.equal(provisioned, false);
} finally { fs.rmSync(root, { recursive: true, force: true }); }
});

View file

@ -2,12 +2,13 @@
const crypto = require("node:crypto");
const fs = require("node:fs");
const path = require("node:path");
const { validateDeploymentSource } = require("./deployment-source-policy");
// A push can request deployment, but it can never execute deployment itself.
// The separate resident deployment agent consumes these immutable event files.
function enqueueDeploymentEvent(intent, push, queueDir) {
function enqueueDeploymentEvent(intent, push, queueDir, context = {}) {
if (!intent) return { state: "not_requested" };
const invalid = validateIntent(intent, push);
const invalid = validateIntent(intent, push, context);
if (invalid) return { state: "rejected", diagnostic_code: invalid };
fs.mkdirSync(queueDir, { recursive: true, mode: 0o750 });
const event = {
@ -19,6 +20,11 @@ function enqueueDeploymentEvent(intent, push, queueDir) {
workorder_id: String(intent.workorder_id || ""),
resource: intent.resource, action: "provision-approved-architecture",
manifest: intent.manifest,
authorizer_id: String(context.authorizer_id || ""),
persona_id: String(context.persona_id || ""),
execution_runtime_id: String(context.execution_runtime_id || ""),
target: String(context.target || ""),
deployment_source: intent.deployment_source || null,
};
const target = path.join(queueDir, `${event.created_at}-${event.event_id}.json`);
const temporary = `${target}.${process.pid}.tmp`;
@ -26,11 +32,21 @@ function enqueueDeploymentEvent(intent, push, queueDir) {
fs.renameSync(temporary, target);
return { state: event.state, event_id: event.event_id };
}
function validateIntent(intent, push) {
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";
if (!/^deployment\/requests\/[A-Za-z0-9._/-]{1,180}\.json$/.test(String(intent.manifest || ""))) return "deployment_intent_manifest_invalid";
if (context.registry) {
return validateDeploymentSource({
repo: push.repo,
authorizer_id: String(context.authorizer_id || ""),
persona_id: String(context.persona_id || ""),
execution_runtime_id: String(context.execution_runtime_id || ""),
target: String(context.target || ""),
deployment_source: intent.deployment_source || null,
}, context.registry);
}
return "";
}
module.exports = { enqueueDeploymentEvent, validateIntent };

View file

@ -5,6 +5,29 @@ const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const { enqueueDeploymentEvent } = require("./deployment-event");
const personalSource = {
repository_id: "REPO-008",
channel_id: "HLP-CHANNEL-0001",
distribution: "personal",
owner_id: "ICE-GL∞",
};
const registry = {
"bingshuo/hololake-platform": {
repo_url: "https://example.invalid/hololake-platform.git",
deployment_policy: {
profiles: [{
repository_id: "REPO-008",
channel_id: "HLP-CHANNEL-0001",
distribution: "personal",
source_owner_id: "ICE-GL∞",
allowed_authorizers: ["ICE-GL∞"],
allowed_personas: ["ICE-GL-ZY001"],
allowed_execution_runtimes: ["SYS-GLW-ZY-EXEC-0001"],
allowed_targets: ["JD-FD-PRIMARY"],
}],
},
},
};
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 };
@ -15,3 +38,47 @@ test("only an immutable deployment intent creates a resident-agent event", () =>
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");
} finally { fs.rmSync(queue, { recursive: true, force: true }); }
});
test("team personas cannot dispatch Ice Shuo personal source", () => {
const queue = fs.mkdtempSync(path.join(os.tmpdir(), "lake-lamp-personal-source-"));
const sha = "a".repeat(40);
const push = { repo: "bingshuo/hololake-platform", branch: "main", commit_sha: sha };
const intent = {
schema: "guanghu.deployment-intent/v1",
repo: push.repo,
branch: push.branch,
commit_sha: sha,
resource: `HLP-PERSONAL-DEPLOY@${sha}`,
manifest: "deployment/requests/HLP-PERSONAL.json",
deployment_source: personalSource,
};
try {
const rejected = enqueueDeploymentEvent(intent, push, queue, {
persona_id: "AGE-TEAM-001",
authorizer_id: "ICE-GL∞",
execution_runtime_id: "SYS-GLW-ZY-EXEC-0001",
target: "JD-FD-PRIMARY",
registry,
});
assert.equal(rejected.state, "rejected");
assert.equal(rejected.diagnostic_code, "deployment_persona_not_allowed");
assert.equal(fs.readdirSync(queue).length, 0);
const allowed = enqueueDeploymentEvent(intent, push, queue, {
authorizer_id: "ICE-GL∞",
persona_id: "ICE-GL-ZY001",
execution_runtime_id: "SYS-GLW-ZY-EXEC-0001",
target: "JD-FD-PRIMARY",
registry,
});
assert.equal(allowed.state, "queued_for_resident_agent");
const humanCannotPoseAsPersona = enqueueDeploymentEvent(intent, push, queue, {
authorizer_id: "ICE-GL∞",
persona_id: "ICE-GL∞",
execution_runtime_id: "SYS-GLW-ZY-EXEC-0001",
target: "JD-FD-PRIMARY",
registry,
});
assert.equal(humanCannotPoseAsPersona.diagnostic_code, "deployment_persona_not_allowed");
} finally { fs.rmSync(queue, { recursive: true, force: true }); }
});

View file

@ -1 +1,25 @@
{"repos":{"bingshuo/guanghu-ice-heart":{"repo_url":"https://guanghulab.com/code/bingshuo/guanghu-ice-heart.git"}}}
{
"repos": {
"bingshuo/guanghu-ice-heart": {
"repo_url": "https://guanghulab.com/code/bingshuo/guanghu-ice-heart.git"
},
"bingshuo/hololake-platform": {
"repo_url": "https://guanghulab.com/fifth-domain/bingshuo/hololake-platform.git",
"deployment_policy": {
"profiles": [
{
"repository_id": "REPO-008",
"channel_id": "HLP-CHANNEL-0001",
"distribution": "personal",
"source_owner_id": "ICE-GL∞",
"allowed_authorizers": ["ICE-GL∞"],
"allowed_personas": ["ICE-GL-ZY001"],
"allowed_execution_runtimes": ["SYS-GLW-ZY-EXEC-0001"],
"allowed_targets": ["JD-FD-PRIMARY"],
"state": "active"
}
]
}
}
}
}

View file

@ -0,0 +1,27 @@
"use strict";
function validateDeploymentSource(event, registry) {
const entry = registry && registry[event.repo];
if (!entry) return "deployment_repository_not_registered";
const policy = entry.deployment_policy;
if (!policy) return "";
if (policy.deployable === false) return "deployment_repository_not_deployable";
const source = event.deployment_source;
if (!source || typeof source !== "object") return "deployment_source_binding_required";
const profile = (policy.profiles || []).find(candidate =>
candidate.repository_id === source.repository_id
&& candidate.channel_id === source.channel_id
&& candidate.distribution === source.distribution
);
if (!profile) return "deployment_source_profile_not_registered";
if (profile.state && profile.state !== "active") return "deployment_source_profile_inactive";
if (profile.source_owner_id !== source.owner_id) return "deployment_source_owner_mismatch";
if (!(profile.allowed_authorizers || []).includes(event.authorizer_id)) return "deployment_authorizer_not_allowed";
if (!(profile.allowed_personas || []).includes(event.persona_id)) return "deployment_persona_not_allowed";
if (!(profile.allowed_execution_runtimes || []).includes(event.execution_runtime_id)) return "deployment_execution_runtime_not_allowed";
if (!(profile.allowed_targets || []).includes(event.target)) return "deployment_target_not_allowed";
return "";
}
module.exports = { validateDeploymentSource };

View file

@ -60,6 +60,7 @@ function createApp(options = {}) {
});
const repoGrantDir = options.repoGrantDir || process.env.LAKE_LAMP_REPO_GRANT_DIR || "/var/lib/guanghu/repo-authorizations";
const deploymentQueueDir = options.deploymentQueueDir || process.env.LAKE_LAMP_DEPLOYMENT_EVENT_DIR || "/var/lib/guanghu/deployment-events";
const deploymentRegistryFile = options.deploymentRegistryFile || process.env.LAKE_LAMP_DEPLOYMENT_REPOSITORIES || "/etc/guanghu/lake-lamp/deployment-repositories.json";
const executeAction = options.executeAction || executeRegisteredAction;
// Creating a powerless request must never become harder than the human mail
// handoff. Keep at least three attempts per network each hour.
@ -81,6 +82,10 @@ function createApp(options = {}) {
manager.failApprovalEmail(handoffToken);
return { ok: false, reason: "no_registered_approver" };
}
if (!manager.bindApprover(handoffToken, approver.id)) {
manager.failApprovalEmail(handoffToken);
return { ok: false, reason: "approver_binding_failed" };
}
const approvalUrl = `${publicBaseUrl}/approve/${issued.approvalToken}`;
const emailSent = await sendEmail({
to: approver.email,
@ -325,7 +330,23 @@ function createApp(options = {}) {
if (!verified.ok) return json(res, 403, failure(verified.reason));
const map = mapGate.read(target);
if (!mapGate.verify(token, target, map.hash).ok) return json(res, 423, failure("map_ack_required", "先读取并确认导航图。", { required_action: "read-navigation-map" }));
const queued = enqueueDeploymentEvent({ schema: "guanghu.deployment-intent/v1", repo, branch, commit_sha: commit, resource, manifest, workorder_id: verified.session.workorderId }, { repo, branch, commit_sha: commit }, deploymentQueueDir);
const deploymentRepositories = options.deploymentRepositories || loadDeploymentRepositories(deploymentRegistryFile);
const queued = enqueueDeploymentEvent({
schema: "guanghu.deployment-intent/v1",
repo,
branch,
commit_sha: commit,
resource,
manifest,
workorder_id: verified.session.workorderId,
deployment_source: body.deployment_source || null,
}, { repo, branch, commit_sha: commit }, deploymentQueueDir, {
authorizer_id: verified.session.authorizerId,
persona_id: verified.session.persona.pid,
execution_runtime_id: String(body.execution_runtime_id || ""),
target,
registry: deploymentRepositories,
});
const operationReceipt = receipt({ state: queued.state === "queued_for_resident_agent" ? "queued" : "blocked", diagnostic_code: queued.diagnostic_code || "deployment_event_queued", workorder_id: verified.session.workorderId, target, action: "dispatch-approved-deployment", evidence: { repo, branch, commit_sha: commit, event_id: queued.event_id || "" }, next_step: queued.state === "queued_for_resident_agent" ? "常驻部署 Agent 将读取该事件并回写部署、健康检查或回滚回执。" : "修正部署绑定信息后重新申请或派发,不要让服务器自行扫描提交。" });
manager.recordReceipt(token, operationReceipt);
return json(res, queued.state === "queued_for_resident_agent" ? 202 : 400, { ok: queued.state === "queued_for_resident_agent", deployment: queued, receipt: operationReceipt });
@ -339,6 +360,12 @@ function createApp(options = {}) {
});
}
function loadDeploymentRepositories(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;
}
function approvalPage(order, token) {
return document("小湖灯授权请求", `
<p class="eyebrow">LAKE LAMP SECURITY PROTOCOL</p>
@ -416,7 +443,7 @@ function safeEqual(left, right) { const a = Buffer.from(String(left)); const b =
function sha256(value) { return crypto.createHash("sha256").update(String(value)).digest("hex"); }
function splitCsv(value) { return value.split(",").map(item => item.trim()).filter(Boolean); }
function loadApprovers(file, ownerEmail) {
if (!file) return ownerEmail ? [{ id: "sovereign-owner", email: ownerEmail, default: true, persona_ids: [], targets: ["*"], scopes: ["*"] }] : [];
if (!file) return ownerEmail ? [{ id: "ICE-GL∞", email: ownerEmail, default: true, persona_ids: [], targets: ["*"], scopes: ["*"] }] : [];
const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
if (!parsed || !Array.isArray(parsed.approvers)) throw new Error("invalid approver registry");
return parsed.approvers.filter(item => item && validEmail(item.email)).map(item => ({

View file

@ -314,7 +314,17 @@ test("deployment is dispatched only by an explicit approved second signal", asyn
assert.equal(dispatch.status, 202);
assert.equal((await dispatch.json()).receipt.state, "queued");
assert.equal(fs.readdirSync(path.join(dir, "queue")).length, 1);
}, { mapsDir, mapStateFile: path.join(dir, "acks.json"), deploymentQueueDir: path.join(dir, "queue"), actions: { "server-ops": ["read-navigation-map", "dispatch-approved-deployment"] } });
}, {
mapsDir,
mapStateFile: path.join(dir, "acks.json"),
deploymentQueueDir: path.join(dir, "queue"),
deploymentRepositories: {
"bingshuo/guanghu-ice-heart": {
repo_url: "https://example.invalid/guanghu-ice-heart.git",
},
},
actions: { "server-ops": ["read-navigation-map", "dispatch-approved-deployment"] },
});
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
});

View file

@ -67,6 +67,14 @@ class WorkOrderManager {
return { ok: true, approvalToken, order: publicOrder(order) };
}
bindApprover(handoffToken, authorizerId) {
const order = this.findByHandoff(handoffToken);
if (!order || order.state !== "pending") return false;
order.authorizerId = String(authorizerId || "");
this.persist();
return true;
}
failApprovalEmail(handoffToken) {
const order = this.findByHandoff(handoffToken);
if (!order || order.state !== "pending") return false;
@ -112,6 +120,7 @@ class WorkOrderManager {
action: order.action,
actions: order.allowedActions || [order.action],
resource: order.resource || "",
authorizerId: order.authorizerId || "",
createdAt: now,
expiresAt: now + this.sessionTtl,
maxExpiresAt: now + this.maxSessionLifetime,