fix: separate Fifth Domain source deployment identities
This commit is contained in:
parent
711cd232ca
commit
83589aa2c2
27 changed files with 620 additions and 73 deletions
|
|
@ -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"],
|
||||
|
|
|
|||
|
|
@ -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); }
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }); }
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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 }); }
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
27
server-tools/lake-lamp-authz/deployment-source-policy.js
Normal file
27
server-tools/lake-lamp-authz/deployment-source-policy.js
Normal 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 };
|
||||
|
|
@ -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 => ({
|
||||
|
|
|
|||
|
|
@ -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 }); }
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue