@forgezero/agent 0.1.134 → 0.1.136

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/fz.js CHANGED
@@ -6007,7 +6007,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
6007
6007
  var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
6008
6008
 
6009
6009
  // src/version.ts
6010
- var VERSION2 = "0.1.134";
6010
+ var VERSION2 = "0.1.136";
6011
6011
 
6012
6012
  // src/software.ts
6013
6013
  var PINNED_BUN_VERSION = "1.3.14";
@@ -10816,7 +10816,7 @@ function validateBuiltInProvider(requirement2, where) {
10816
10816
 
10817
10817
  // src/deploy-plan.ts
10818
10818
  var DEPLOY_PLAN_FORMAT = "forgezero-deployment-plan";
10819
- var DEPLOY_PLAN_VERSION = 1;
10819
+ var DEPLOY_PLAN_VERSION = 2;
10820
10820
 
10821
10821
  class DeploymentPlanError extends Error {
10822
10822
  constructor(message) {
@@ -10830,6 +10830,13 @@ var ACTION = /^[a-z][a-z0-9.-]{0,127}\/[a-z][a-z0-9.-]{0,127}@[1-9][0-9]{0,5}$/;
10830
10830
  var VERSION3 = /^[A-Za-z0-9][A-Za-z0-9.+_-]{0,63}$/;
10831
10831
  var ABSOLUTE_PATH = /^\/(?:[A-Za-z0-9._-]+\/?)*$/;
10832
10832
  var HEALTH_PATH = /^\/[A-Za-z0-9._~!$&'()*+,;=:@%/-]{0,255}$/;
10833
+ function deploymentBranch(value, where = "deployment.spec.source.branch") {
10834
+ const branch = boundedString(value, where, 255);
10835
+ if (branch === "@" || branch.startsWith("/") || branch.endsWith("/") || branch.startsWith(".") || branch.endsWith(".") || branch.endsWith(".lock") || branch.includes("..") || branch.includes("//") || branch.includes("@{") || /[\x00-\x20~^:?*\[\\]/.test(branch)) {
10836
+ fail2(where, "must be one exact valid Git branch name.");
10837
+ }
10838
+ return branch;
10839
+ }
10833
10840
  var fail2 = (where, message) => {
10834
10841
  throw new DeploymentPlanError(`${where} ${message}`);
10835
10842
  };
@@ -11465,7 +11472,10 @@ function compileDeployment(sourceValue) {
11465
11472
  if (metadata.description !== undefined)
11466
11473
  boundedString(metadata.description, "deployment.metadata.description", 1024);
11467
11474
  const spec = object(source.spec, "deployment.spec");
11468
- exact3(spec, ["security", "inputs", "credentials", "targets", "requirements", "components", "workflows"], "deployment.spec");
11475
+ exact3(spec, ["source", "security", "inputs", "credentials", "targets", "requirements", "components", "workflows"], "deployment.spec");
11476
+ const sourceSelector = object(spec.source, "deployment.spec.source");
11477
+ exact3(sourceSelector, ["branch"], "deployment.spec.source");
11478
+ deploymentBranch(sourceSelector.branch);
11469
11479
  if (spec.security !== undefined) {
11470
11480
  const security = object(spec.security, "deployment.spec.security");
11471
11481
  exact3(security, ["attestation"], "deployment.spec.security");
@@ -11562,11 +11572,13 @@ function compileDeployment(sourceValue) {
11562
11572
  if (provisioning.metalHostname !== undefined && !/^(?=.{1,253}$)[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?$/.test(String(provisioning.metalHostname)))
11563
11573
  fail2(`deployment.spec.targets.${targetName}.provisioning.metalHostname`, "is invalid.");
11564
11574
  const provisionedResources = object(provisioning.resources, `deployment.spec.targets.${targetName}.provisioning.resources`);
11565
- exact3(provisionedResources, ["physicalCores", "vcpu", "memoryGib", "diskGib", "egressGuaranteedMbps", "egressBurstMbps", "confidential"], `deployment.spec.targets.${targetName}.provisioning.resources`);
11575
+ exact3(provisionedResources, ["physicalCores", "vcpu", "memoryGib", "diskGib", "diskEncryption", "egressGuaranteedMbps", "egressBurstMbps", "confidential"], `deployment.spec.targets.${targetName}.provisioning.resources`);
11566
11576
  integer(provisionedResources.physicalCores, `deployment.spec.targets.${targetName}.provisioning.resources.physicalCores`, 1, 1024);
11567
11577
  integer(provisionedResources.vcpu, `deployment.spec.targets.${targetName}.provisioning.resources.vcpu`, 1, 4096);
11568
11578
  integer(provisionedResources.memoryGib, `deployment.spec.targets.${targetName}.provisioning.resources.memoryGib`, 1, 4194304);
11569
11579
  integer(provisionedResources.diskGib, `deployment.spec.targets.${targetName}.provisioning.resources.diskGib`, 1, 1048576);
11580
+ if (provisionedResources.diskEncryption !== undefined && !["none", "luks2"].includes(String(provisionedResources.diskEncryption)))
11581
+ fail2(`deployment.spec.targets.${targetName}.provisioning.resources.diskEncryption`, "must be none or luks2.");
11570
11582
  integer(provisionedResources.egressGuaranteedMbps, `deployment.spec.targets.${targetName}.provisioning.resources.egressGuaranteedMbps`, 0, 1e7);
11571
11583
  integer(provisionedResources.egressBurstMbps, `deployment.spec.targets.${targetName}.provisioning.resources.egressBurstMbps`, Number(provisionedResources.egressGuaranteedMbps), 1e7);
11572
11584
  if (typeof provisionedResources.confidential !== "boolean")
@@ -11796,6 +11808,7 @@ function compileDeployment(sourceValue) {
11796
11808
  ...metadata.description ? { description: metadata.description } : {},
11797
11809
  requiredProviders: [...requiredProviders.values()].sort((left, right) => `${left.provider}:${left.contract}:${left.capability}`.localeCompare(`${right.provider}:${right.contract}:${right.capability}`)),
11798
11810
  spec: {
11811
+ source: sourceSelector,
11799
11812
  ...spec.security ? { security: spec.security } : {},
11800
11813
  inputs,
11801
11814
  credentials,
@@ -11888,6 +11901,7 @@ function initCoordinates(options) {
11888
11901
  const isolation = options.isolation ?? (runtime === "kata-snp" ? "sev-snp" : "standard");
11889
11902
  const operatingSystem = options.operatingSystem ?? (isolation === "sev-snp" ? "ubuntu-26.04" : "ubuntu-24.04");
11890
11903
  const result = {
11904
+ branch: options.branch ?? "main",
11891
11905
  runtime,
11892
11906
  isolation,
11893
11907
  operatingSystem,
@@ -11901,6 +11915,9 @@ function initCoordinates(options) {
11901
11915
  };
11902
11916
  if (!/^[a-z][a-z0-9-]{0,62}$/.test(result.profile))
11903
11917
  throw new Error("deployment profile must be a lowercase typed name");
11918
+ if (result.branch.length < 1 || result.branch.length > 255 || result.branch === "@" || result.branch.startsWith("/") || result.branch.endsWith("/") || result.branch.startsWith(".") || result.branch.endsWith(".") || result.branch.endsWith(".lock") || result.branch.includes("..") || result.branch.includes("//") || result.branch.includes("@{") || /[\x00-\x20~^:?*\[\\]/.test(result.branch)) {
11919
+ throw new Error("deployment branch must be one exact valid Git branch name");
11920
+ }
11904
11921
  if (!Number.isSafeInteger(result.replicas) || result.replicas < 1 || result.replicas > 1024)
11905
11922
  throw new Error("deployment replicas must be an integer from 1 to 1024");
11906
11923
  if (!Number.isSafeInteger(result.cpuCores) || result.cpuCores < 1 || result.cpuCores > 1024)
@@ -11930,7 +11947,7 @@ function initCoordinates(options) {
11930
11947
  if (!/^[a-z][a-z0-9-]{0,62}$/.test(value))
11931
11948
  throw new Error(`deployment provisioning ${label} must be a lowercase typed name`);
11932
11949
  const resources = options.provisioning.resources;
11933
- for (const [label, value] of Object.entries(resources).filter(([key]) => key !== "confidential")) {
11950
+ for (const [label, value] of Object.entries(resources).filter(([key]) => !["confidential", "diskEncryption"].includes(key))) {
11934
11951
  if (!Number.isSafeInteger(value) || Number(value) < 0)
11935
11952
  throw new Error(`deployment provisioning ${label} must be a non-negative integer`);
11936
11953
  }
@@ -11939,6 +11956,8 @@ function initCoordinates(options) {
11939
11956
  }
11940
11957
  if (result.isolation === "sev-snp" && !resources.confidential)
11941
11958
  throw new Error("deployment provisioning must provide confidential capacity for sev-snp");
11959
+ if (resources.diskEncryption !== undefined && !["none", "luks2"].includes(resources.diskEncryption))
11960
+ throw new Error("deployment provisioning disk encryption mode is unsupported");
11942
11961
  if (!/^\d{1,18}$/.test(options.provisioning.monthlyAmountCents))
11943
11962
  throw new Error("deployment provisioning monthly amount must be a bounded USD-cent amount");
11944
11963
  if (!/^\d{1,18}$/.test(options.provisioning.maxMonthlySpendMinor))
@@ -11950,7 +11969,10 @@ function initCoordinates(options) {
11950
11969
  if (options.provisioning.metalHostname && !/^(?=.{1,253}$)[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?$/.test(options.provisioning.metalHostname))
11951
11970
  throw new Error("deployment provisioning metal hostname is invalid");
11952
11971
  }
11953
- return { ...result, ...options.provisioning ? { provisioning: options.provisioning } : {} };
11972
+ return { ...result, ...options.provisioning ? { provisioning: {
11973
+ ...options.provisioning,
11974
+ resources: { ...options.provisioning.resources, diskEncryption: options.provisioning.resources.diskEncryption ?? "luks2" }
11975
+ } } : {} };
11954
11976
  }
11955
11977
  function defaultTypeScriptDeployment(name, options = {}) {
11956
11978
  const selected = initCoordinates(options);
@@ -12019,6 +12041,7 @@ export default defineDeployment({
12019
12041
  kind: 'Deployment',
12020
12042
  metadata: { name: '${safeName2(name)}' },
12021
12043
  spec: {
12044
+ source: { branch: '${selected.branch}' },
12022
12045
  security: { attestation: '${selected.isolation === "sev-snp" ? "required" : "preferred"}' },
12023
12046
  targets: {
12024
12047
  app: target.compute({
@@ -12414,6 +12437,10 @@ function validatePlatformGenesisGuests(guests, expectedCount = 3) {
12414
12437
  throw new Error("platform genesis vCPU count cannot be smaller than physical cores");
12415
12438
  const memoryGib = integer2(input.memoryGib, 1, 8192, "memory");
12416
12439
  const diskGib = integer2(input.diskGib, 8, 65536, "disk");
12440
+ const diskEncryption = input.diskEncryption ?? "none";
12441
+ if (!["none", "luks2"].includes(diskEncryption)) {
12442
+ throw new Error("platform genesis disk encryption mode is unsupported");
12443
+ }
12417
12444
  const egressGuaranteedMbps = integer2(input.egressGuaranteedMbps, 0, 1e6, "guaranteed egress");
12418
12445
  const egressBurstMbps = integer2(input.egressBurstMbps, egressGuaranteedMbps, 1e6, "burst egress");
12419
12446
  return {
@@ -12425,6 +12452,7 @@ function validatePlatformGenesisGuests(guests, expectedCount = 3) {
12425
12452
  vcpu,
12426
12453
  memoryGib,
12427
12454
  diskGib,
12455
+ diskEncryption,
12428
12456
  egressGuaranteedMbps,
12429
12457
  egressBurstMbps,
12430
12458
  confidential: input.confidential
@@ -12455,6 +12483,7 @@ var claimFor = (guest, keys, action) => {
12455
12483
  vcpu: guest.vcpu,
12456
12484
  memoryGib: guest.memoryGib,
12457
12485
  diskGib: guest.diskGib,
12486
+ diskEncryption: guest.diskEncryption,
12458
12487
  egressGuaranteedMbps: guest.egressGuaranteedMbps,
12459
12488
  egressBurstMbps: guest.egressBurstMbps,
12460
12489
  confidential: guest.confidential
@@ -12544,6 +12573,7 @@ function validatePlatformInitialInventory(value) {
12544
12573
  "vcpu",
12545
12574
  "memoryGib",
12546
12575
  "diskGib",
12576
+ "diskEncryption",
12547
12577
  "egressGuaranteedMbps",
12548
12578
  "egressBurstMbps",
12549
12579
  "confidential",
@@ -15850,6 +15880,7 @@ function strictBootstrapDocument(value) {
15850
15880
  "vcpu",
15851
15881
  "memoryGib",
15852
15882
  "diskGib",
15883
+ "diskEncryption",
15853
15884
  "egressGuaranteedMbps",
15854
15885
  "egressBurstMbps",
15855
15886
  "confidential",
@@ -17084,6 +17115,7 @@ async function applyMetalBootstrap(config, options) {
17084
17115
  "qemu-utils",
17085
17116
  "cloud-image-utils",
17086
17117
  "lvm2",
17118
+ "cryptsetup",
17087
17119
  "nftables",
17088
17120
  "curl",
17089
17121
  "ca-certificates"
@@ -17518,11 +17550,12 @@ var validateMetalRequestValue = (value, options = {}) => {
17518
17550
  "vcpu",
17519
17551
  "memoryGib",
17520
17552
  "diskGib",
17553
+ "diskEncryption",
17521
17554
  "egressGuaranteedMbps",
17522
17555
  "egressBurstMbps",
17523
17556
  "confidential"
17524
17557
  ], label);
17525
- if (typeof row2.name !== "string" || typeof row2.address !== "string" || typeof row2.imageKey !== "string" || row2.cpuPoolKey !== undefined && typeof row2.cpuPoolKey !== "string" || !["physicalCores", "vcpu", "memoryGib", "diskGib", "egressGuaranteedMbps", "egressBurstMbps"].every((key) => typeof row2[key] === "number") || typeof row2.confidential !== "boolean") {
17558
+ if (typeof row2.name !== "string" || typeof row2.address !== "string" || typeof row2.imageKey !== "string" || row2.cpuPoolKey !== undefined && typeof row2.cpuPoolKey !== "string" || !["physicalCores", "vcpu", "memoryGib", "diskGib", "egressGuaranteedMbps", "egressBurstMbps"].every((key) => typeof row2[key] === "number") || row2.diskEncryption !== undefined && !["none", "luks2"].includes(row2.diskEncryption) || typeof row2.confidential !== "boolean") {
17526
17559
  throw new Error(`${label} has invalid resource coordinates`);
17527
17560
  }
17528
17561
  return {
@@ -17534,6 +17567,7 @@ var validateMetalRequestValue = (value, options = {}) => {
17534
17567
  vcpu: row2.vcpu,
17535
17568
  memoryGib: row2.memoryGib,
17536
17569
  diskGib: row2.diskGib,
17570
+ diskEncryption: row2.diskEncryption ?? "none",
17537
17571
  egressGuaranteedMbps: row2.egressGuaranteedMbps,
17538
17572
  egressBurstMbps: row2.egressBurstMbps,
17539
17573
  confidential: row2.confidential
@@ -19137,6 +19171,13 @@ function parseOptions(argv2) {
19137
19171
  } else if (token === "--provision-metal") {
19138
19172
  options.deployProvisionMetal = argv2[++index];
19139
19173
  options.deployInitCustomized = true;
19174
+ } else if (token === "--provision-disk-encryption") {
19175
+ const value = argv2[++index];
19176
+ options.deployInitCustomized = true;
19177
+ if (value === "none" || value === "luks2")
19178
+ options.deployProvisionDiskEncryption = value;
19179
+ else
19180
+ options.optionError = "--provision-disk-encryption must be none or luks2.";
19140
19181
  } else if (["--provision-physical-cores", "--provision-vcpu", "--provision-memory-gib", "--provision-disk-gib", "--provision-guaranteed-mbps", "--provision-burst-mbps"].includes(token)) {
19141
19182
  const value = Number(argv2[++index] ?? "");
19142
19183
  options.deployInitCustomized = true;
@@ -19985,6 +20026,7 @@ function interactiveGenesisGuests() {
19985
20026
  vcpu: bootstrapNumber(`Guest ${index} vCPUs`),
19986
20027
  memoryGib: bootstrapNumber(`Guest ${index} memory GiB`),
19987
20028
  diskGib: bootstrapNumber(`Guest ${index} disk GiB`),
20029
+ diskEncryption: bootstrapBoolean(`Guest ${index} encrypts its persistent disk with LUKS2?`, "yes") ? "luks2" : "none",
19988
20030
  egressGuaranteedMbps: bootstrapNumber(`Guest ${index} guaranteed egress Mbps`),
19989
20031
  egressBurstMbps: bootstrapNumber(`Guest ${index} burst egress Mbps`),
19990
20032
  confidential: bootstrapBoolean(`Guest ${index} requires confidential SEV-SNP compute?`)
@@ -20977,6 +21019,7 @@ async function cmdDeploy(options, args) {
20977
21019
  initializeTypeScriptDeployment(options.projectRoot, {
20978
21020
  name: options.projectName ?? basename3(resolve13(options.projectRoot)),
20979
21021
  force: options.force,
21022
+ branch: options.branch,
20980
21023
  runtime: options.deployRuntime,
20981
21024
  operatingSystem: options.deployOperatingSystem,
20982
21025
  isolation: options.deployIsolation,
@@ -20997,6 +21040,7 @@ async function cmdDeploy(options, args) {
20997
21040
  options.deployProvisionMemoryGib,
20998
21041
  options.deployProvisionDiskGib,
20999
21042
  options.deployProvisionGuaranteedMbps,
21043
+ options.deployProvisionDiskEncryption,
21000
21044
  options.deployProvisionBurstMbps,
21001
21045
  options.deployProvisionMonthlyAmountCents,
21002
21046
  options.deployMaxMonthlySpendMinor
@@ -21012,6 +21056,7 @@ async function cmdDeploy(options, args) {
21012
21056
  vcpu: options.deployProvisionVcpu ?? 0,
21013
21057
  memoryGib: options.deployProvisionMemoryGib ?? 0,
21014
21058
  diskGib: options.deployProvisionDiskGib ?? 0,
21059
+ diskEncryption: options.deployProvisionDiskEncryption ?? "luks2",
21015
21060
  egressGuaranteedMbps: options.deployProvisionGuaranteedMbps ?? 0,
21016
21061
  egressBurstMbps: options.deployProvisionBurstMbps ?? 0,
21017
21062
  confidential: options.deployIsolation === "sev-snp"
@@ -21359,7 +21404,7 @@ function usage() {
21359
21404
  --provision-environment <key> --provision-ownership <platform|tenant-metal>
21360
21405
  --provision-metal <host> Required only for tenant-metal ownership
21361
21406
  --provision-physical-cores <n> --provision-vcpu <n>
21362
- --provision-memory-gib <n> --provision-disk-gib <n>
21407
+ --provision-memory-gib <n> --provision-disk-gib <n> --provision-disk-encryption <none|luks2>
21363
21408
  --provision-guaranteed-mbps <n> --provision-burst-mbps <n>
21364
21409
  --provision-monthly-amount-cents <n> Exact approved USD cost per created compute
21365
21410
  --max-monthly-spend-minor <n> Hard approved scale-out spend ceiling
@@ -366,7 +366,7 @@ function systemdAgentEgressDirectives(loopbackTcpPorts = []) {
366
366
  }
367
367
 
368
368
  // src/version.ts
369
- var VERSION = "0.1.134";
369
+ var VERSION = "0.1.136";
370
370
 
371
371
  // src/otel-collector.ts
372
372
  var FORGEZERO_OTEL_COLLECTOR_UNIT = "forgezero-otel-collector.service";
@@ -1058,6 +1058,7 @@ async function applyMetalBootstrap(config, options) {
1058
1058
  "qemu-utils",
1059
1059
  "cloud-image-utils",
1060
1060
  "lvm2",
1061
+ "cryptsetup",
1061
1062
  "nftables",
1062
1063
  "curl",
1063
1064
  "ca-certificates"
@@ -56,6 +56,10 @@ function guestUnit(spec) {
56
56
  `ExecStopPost=-/usr/sbin/ip link del ${spec.tap}`
57
57
  ].join(`
58
58
  `) : "";
59
+ const encryptedDisk = spec.diskEncryption ? `LoadCredentialEncrypted=${spec.diskEncryption.credentialName}:${spec.diskEncryption.credentialPath}
60
+ ExecStartPre=/usr/sbin/cryptsetup open --type luks2 --key-file %d/${spec.diskEncryption.credentialName} ${spec.diskEncryption.source} ${spec.diskEncryption.mapper}
61
+ ExecStopPost=-/usr/sbin/cryptsetup close ${spec.diskEncryption.mapper}
62
+ ` : "";
59
63
  return `[Unit]
60
64
  Description=ForgeZero guest ${spec.name}
61
65
  Documentation=https://www.forgezero.net
@@ -65,7 +69,7 @@ Wants=network-online.target
65
69
  [Service]
66
70
  Type=simple
67
71
  Slice=forgezero-guests.slice
68
- ${tapSetup}
72
+ ${encryptedDisk}${tapSetup}
69
73
  ExecStart=${argv}
70
74
  Restart=always
71
75
  RestartSec=5
@@ -754,8 +758,9 @@ function assertSupportedGuestImage(imageKey, confidential = false) {
754
758
  }
755
759
 
756
760
  // src/metal-provision.ts
757
- import { createHash as createHash2 } from "node:crypto";
761
+ import { createHash as createHash2, randomBytes } from "node:crypto";
758
762
  import {
763
+ chmodSync as chmodSync2,
759
764
  existsSync as existsSync2,
760
765
  mkdirSync as mkdirSync2,
761
766
  readFileSync as readFileSync2,
@@ -989,6 +994,8 @@ function guestBootstrapOperations(profile, attested = Boolean(profile.confidenti
989
994
  validateMetalProfile(profile);
990
995
  const agentBun = "/usr/local/lib/forgezero/bun";
991
996
  return [
997
+ ["/usr/sbin/swapoff", "--all"],
998
+ ["/usr/bin/systemctl", "mask", "--now", "swap.target"],
992
999
  ...attested ? [
993
1000
  ["/usr/sbin/modprobe", "sev-guest"],
994
1001
  ["/usr/bin/test", "-c", "/dev/sev-guest"]
@@ -1065,6 +1072,7 @@ ${access.sshPublicKeys.map((key) => ` - ${JSON.stringify(key)}`).join(`
1065
1072
  return {
1066
1073
  userData: `#cloud-config
1067
1074
  package_update: true
1075
+ swap: { size: 0 }
1068
1076
  ${users}disable_root: true
1069
1077
  # Git is part of the deployment transport, not a tenant-selected prerequisite:
1070
1078
  # every dynamically claimed repository must be cloneable on a clean image.
@@ -1097,7 +1105,8 @@ var checked = async (exec, argv) => {
1097
1105
  async function provisionMetalGuest(profile, claim, exec) {
1098
1106
  validateMetalProfile(profile);
1099
1107
  assertSupportedGuestImage(claim.spec.imageKey, claim.spec.confidential);
1100
- if (!claim.computeKey || !claim.spec.reference || !SAFE_NAME.test(claim.spec.imageKey) || claim.spec.guestName !== undefined && !SAFE_NAME.test(claim.spec.guestName) || claim.spec.cpuPoolKey !== undefined && !SAFE_NAME.test(claim.spec.cpuPoolKey) || !Number.isInteger(claim.spec.physicalCores) || claim.spec.physicalCores < 1 || claim.spec.physicalCores > 256 || !Number.isInteger(claim.spec.vcpu) || claim.spec.vcpu < 1 || claim.spec.vcpu > 512 || !Number.isInteger(claim.spec.memoryGib) || claim.spec.memoryGib < 1 || claim.spec.memoryGib > 8192 || !Number.isInteger(claim.spec.diskGib) || claim.spec.diskGib < 8 || claim.spec.diskGib > 65536 || !Number.isInteger(claim.spec.egressGuaranteedMbps) || claim.spec.egressGuaranteedMbps < 0 || !Number.isInteger(claim.spec.egressBurstMbps) || claim.spec.egressBurstMbps < claim.spec.egressGuaranteedMbps)
1108
+ const diskEncryption = claim.spec.diskEncryption ?? "none";
1109
+ if (!claim.computeKey || !claim.spec.reference || !SAFE_NAME.test(claim.spec.imageKey) || claim.spec.guestName !== undefined && !SAFE_NAME.test(claim.spec.guestName) || claim.spec.cpuPoolKey !== undefined && !SAFE_NAME.test(claim.spec.cpuPoolKey) || !Number.isInteger(claim.spec.physicalCores) || claim.spec.physicalCores < 1 || claim.spec.physicalCores > 256 || !Number.isInteger(claim.spec.vcpu) || claim.spec.vcpu < 1 || claim.spec.vcpu > 512 || !Number.isInteger(claim.spec.memoryGib) || claim.spec.memoryGib < 1 || claim.spec.memoryGib > 8192 || !Number.isInteger(claim.spec.diskGib) || claim.spec.diskGib < 8 || claim.spec.diskGib > 65536 || !["none", "luks2"].includes(diskEncryption) || !Number.isInteger(claim.spec.egressGuaranteedMbps) || claim.spec.egressGuaranteedMbps < 0 || !Number.isInteger(claim.spec.egressBurstMbps) || claim.spec.egressBurstMbps < claim.spec.egressGuaranteedMbps)
1101
1110
  throw new MetalProvisionError("invalid compute claim");
1102
1111
  const image = profile.images[claim.spec.imageKey];
1103
1112
  if (!image || !isAbsolute(image.path) || !SHA256.test(image.sha256)) {
@@ -1126,6 +1135,9 @@ async function provisionMetalGuest(profile, claim, exec) {
1126
1135
  const prior = manifests.find((row) => row.computeKey === claim.computeKey);
1127
1136
  if (prior && prior.reference !== claim.spec.reference)
1128
1137
  throw new MetalProvisionError("compute identity conflicts with host inventory");
1138
+ if (prior && (prior.diskEncryption ?? "none") !== diskEncryption) {
1139
+ throw new MetalProvisionError("an allocated guest disk encryption mode is immutable");
1140
+ }
1129
1141
  const address = requestedAddress(profile, claim, manifests);
1130
1142
  const cpuPool = allocateCpuPool(profile, claim, manifests);
1131
1143
  const manifest = prior ?? {
@@ -1137,6 +1149,7 @@ async function provisionMetalGuest(profile, claim, exec) {
1137
1149
  cpuPoolKey: cpuPool.key,
1138
1150
  allowedCpus: cpuPool.cpus,
1139
1151
  allowedMemoryNodes: cpuPool.memoryNodes,
1152
+ diskEncryption,
1140
1153
  phase: "allocating"
1141
1154
  };
1142
1155
  const save = () => writeFileSync2(manifestPath, `${JSON.stringify(manifest, null, 2)}
@@ -1146,8 +1159,70 @@ async function provisionMetalGuest(profile, claim, exec) {
1146
1159
  const exists = (await exec(["lvs", "--noheadings", lv])).exitCode === 0;
1147
1160
  if (!exists)
1148
1161
  await checked(exec, ["lvcreate", "-y", "-n", name, "-L", `${claim.spec.diskGib}G`, profile.volumeGroup]);
1162
+ const encrypted = diskEncryption === "luks2";
1163
+ const mapper = `fz-${createHash2("sha256").update(claim.computeKey).digest("hex").slice(0, 20)}`;
1164
+ const mappedDisk = `/dev/mapper/${mapper}`;
1165
+ const credentialName = `guest-disk-${createHash2("sha256").update(claim.computeKey).digest("hex").slice(0, 20)}`;
1166
+ const credentialPath = `/etc/forgezero/creds/${credentialName}.cred`;
1167
+ const keyPath = `/run/forgezero/${credentialName}.key`;
1168
+ let imageTarget = lv;
1169
+ if (encrypted) {
1170
+ mkdirSync2(dirname2(credentialPath), { recursive: true, mode: 448 });
1171
+ mkdirSync2(dirname2(keyPath), { recursive: true, mode: 448 });
1172
+ if (existsSync2(keyPath))
1173
+ unlinkSync2(keyPath);
1174
+ if (!existsSync2(credentialPath)) {
1175
+ if (exists && (await exec(["cryptsetup", "isLuks", lv])).exitCode === 0) {
1176
+ throw new MetalProvisionError("encrypted guest disk exists without its sealed unlock credential");
1177
+ }
1178
+ writeFileSync2(keyPath, randomBytes(64), { mode: 384, flag: "wx" });
1179
+ try {
1180
+ await checked(exec, ["systemd-creds", "encrypt", `--name=${credentialName}`, keyPath, credentialPath]);
1181
+ chmodSync2(credentialPath, 384);
1182
+ } catch (cause) {
1183
+ if (existsSync2(credentialPath))
1184
+ unlinkSync2(credentialPath);
1185
+ throw cause;
1186
+ }
1187
+ } else {
1188
+ await checked(exec, ["systemd-creds", "decrypt", `--name=${credentialName}`, credentialPath, keyPath]);
1189
+ }
1190
+ try {
1191
+ if ((await exec(["cryptsetup", "isLuks", lv])).exitCode !== 0) {
1192
+ await checked(exec, [
1193
+ "cryptsetup",
1194
+ "luksFormat",
1195
+ "--batch-mode",
1196
+ "--type",
1197
+ "luks2",
1198
+ "--cipher",
1199
+ "aes-xts-plain64",
1200
+ "--key-size",
1201
+ "512",
1202
+ "--key-file",
1203
+ keyPath,
1204
+ lv
1205
+ ]);
1206
+ }
1207
+ if ((await exec(["cryptsetup", "status", mapper])).exitCode !== 0) {
1208
+ await checked(exec, ["cryptsetup", "open", "--type", "luks2", "--key-file", keyPath, lv, mapper]);
1209
+ }
1210
+ imageTarget = mappedDisk;
1211
+ if (manifest.phase === "allocating") {
1212
+ await checked(exec, ["qemu-img", "convert", "-O", "raw", image.path, imageTarget]);
1213
+ manifest.phase = "image-ready";
1214
+ save();
1215
+ }
1216
+ } finally {
1217
+ if (existsSync2(keyPath))
1218
+ unlinkSync2(keyPath);
1219
+ if ((await exec(["cryptsetup", "status", mapper])).exitCode === 0) {
1220
+ await checked(exec, ["cryptsetup", "close", mapper]);
1221
+ }
1222
+ }
1223
+ }
1149
1224
  if (manifest.phase === "allocating") {
1150
- await checked(exec, ["qemu-img", "convert", "-O", "raw", image.path, lv]);
1225
+ await checked(exec, ["qemu-img", "convert", "-O", "raw", image.path, imageTarget]);
1151
1226
  manifest.phase = "image-ready";
1152
1227
  save();
1153
1228
  }
@@ -1171,7 +1246,8 @@ async function provisionMetalGuest(profile, claim, exec) {
1171
1246
  memoryGib: claim.spec.memoryGib,
1172
1247
  allowedCpus: manifest.allowedCpus,
1173
1248
  allowedMemoryNodes: manifest.allowedMemoryNodes,
1174
- disk: lv,
1249
+ disk: encrypted ? mappedDisk : lv,
1250
+ ...encrypted ? { diskEncryption: { source: lv, mapper, credentialName, credentialPath } } : {},
1175
1251
  seed,
1176
1252
  bridge: profile.bridge,
1177
1253
  mac: manifest.mac,
@@ -1229,6 +1305,7 @@ function legacyPlatformManifest(profile, claim, name) {
1229
1305
  cpuPoolKey: claim.spec.cpuPoolKey ?? "",
1230
1306
  allowedCpus: values.get("CPUSET") ?? "",
1231
1307
  allowedMemoryNodes: values.get("NUMA"),
1308
+ diskEncryption: "none",
1232
1309
  phase: "running",
1233
1310
  disk,
1234
1311
  unit: `forgezero-guest@${name}.service`
@@ -1243,6 +1320,9 @@ async function removeMetalGuest(profile, claim, exec) {
1243
1320
  const service = `forgezero-guest@${name}.service`;
1244
1321
  const unitPath = join2(profile.unitDir, service);
1245
1322
  const lv = `/dev/${profile.volumeGroup}/${name}`;
1323
+ const mapper = `fz-${createHash2("sha256").update(claim.computeKey).digest("hex").slice(0, 20)}`;
1324
+ const credentialName = `guest-disk-${createHash2("sha256").update(claim.computeKey).digest("hex").slice(0, 20)}`;
1325
+ const credentialPath = `/etc/forgezero/creds/${credentialName}.cred`;
1246
1326
  const manifest = existsSync2(manifestPath) ? JSON.parse(readFileSync2(manifestPath, "utf8")) : legacyPlatformManifest(profile, claim, name);
1247
1327
  if (!manifest) {
1248
1328
  const seedBase = join2(profile.seedDir, name);
@@ -1277,9 +1357,14 @@ async function removeMetalGuest(profile, claim, exec) {
1277
1357
  else if ((await exec(["systemctl", "is-active", service])).exitCode === 0) {
1278
1358
  throw new MetalProvisionError("guest unit is active but its owned unit file is missing");
1279
1359
  }
1360
+ const encrypted = (manifest.diskEncryption ?? "none") === "luks2";
1361
+ if (encrypted && (await exec(["cryptsetup", "status", mapper])).exitCode === 0) {
1362
+ throw new MetalProvisionError("guest disk mapping remained active after the guest stopped");
1363
+ }
1280
1364
  if ((await exec(["lvs", "--noheadings", lv])).exitCode === 0)
1281
1365
  await checked(exec, ["lvremove", "-fy", lv]);
1282
1366
  for (const path2 of [
1367
+ ...encrypted ? [credentialPath] : [],
1283
1368
  unitPath,
1284
1369
  join2(profile.seedDir, `${name}-seed.iso`),
1285
1370
  join2(profile.seedDir, `${name}-user-data`),
@@ -1302,7 +1387,7 @@ async function removeMetalGuest(profile, claim, exec) {
1302
1387
  }
1303
1388
 
1304
1389
  // src/metal-helper-socket.ts
1305
- import { chmodSync as chmodSync2, existsSync as existsSync3, unlinkSync as unlinkSync3 } from "node:fs";
1390
+ import { chmodSync as chmodSync3, existsSync as existsSync3, unlinkSync as unlinkSync3 } from "node:fs";
1306
1391
  import { connect, createServer } from "node:net";
1307
1392
  var DEFAULT_METAL_HELPER_SOCKET = "/run/forgezero-metal/helper.sock";
1308
1393
  var MAX_REQUEST_BYTES = 32 * 1024;
@@ -1382,7 +1467,7 @@ function startMetalHelper(options) {
1382
1467
  });
1383
1468
  socket.on("error", () => socket.destroy());
1384
1469
  });
1385
- server.listen(socketPath, () => chmodSync2(socketPath, 432));
1470
+ server.listen(socketPath, () => chmodSync3(socketPath, 432));
1386
1471
  return {
1387
1472
  server,
1388
1473
  async stop() {
@@ -63,6 +63,7 @@ export interface GuestManifest {
63
63
  cpuPoolKey: string;
64
64
  allowedCpus: string;
65
65
  allowedMemoryNodes?: string;
66
+ diskEncryption?: 'none' | 'luks2';
66
67
  phase: 'allocating' | 'image-ready' | 'running';
67
68
  }
68
69
  export declare class MetalProvisionError extends Error {