@forgezero/agent 0.1.85 → 0.1.87

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.
@@ -22,9 +22,11 @@ import {
22
22
 
23
23
  class SignedNodeHttpError extends Error {
24
24
  status;
25
- constructor(status, message) {
25
+ code;
26
+ constructor(status, message, code) {
26
27
  super(message);
27
28
  this.status = status;
29
+ this.code = code;
28
30
  this.name = "SignedNodeHttpError";
29
31
  }
30
32
  }
@@ -71,7 +73,7 @@ async function postSignedNode(options, path, body) {
71
73
  if (!response.ok) {
72
74
  const failure = payload;
73
75
  const reason = failure ? failure.error?.message ?? failure.message : undefined;
74
- throw new SignedNodeHttpError(response.status, reason || `signed node request returned HTTP ${response.status}`);
76
+ throw new SignedNodeHttpError(response.status, reason || `signed node request returned HTTP ${response.status}`, failure?.error?.code);
75
77
  }
76
78
  try {
77
79
  return await openResponse(recipient.secretKey, signature, payload);
@@ -366,7 +366,7 @@ function systemdAgentEgressDirectives(loopbackTcpPorts = []) {
366
366
  }
367
367
 
368
368
  // src/version.ts
369
- var VERSION = "0.1.85";
369
+ var VERSION = "0.1.87";
370
370
 
371
371
  // src/otel-collector.ts
372
372
  var FORGEZERO_OTEL_COLLECTOR_UNIT = "forgezero-otel-collector.service";
@@ -705,6 +705,7 @@ function planMetalBootstrap(config) {
705
705
  "forgezero-metal-helper.service",
706
706
  "forgezero-agent-update-helper.service",
707
707
  "forgezero-metal-agent-egress.service",
708
+ "forgezero-metal-agent-candidate.service",
708
709
  "forgezero-metal-agent.service"
709
710
  ],
710
711
  steps: [
@@ -846,6 +847,44 @@ RestrictAddressFamilies=AF_UNIX AF_NETLINK
846
847
 
847
848
  [Install]
848
849
  WantedBy=multi-user.target
850
+ `,
851
+ "forgezero-metal-agent-candidate.service": `[Unit]
852
+ Description=ForgeZero candidate physical-host Agent
853
+ After=network-online.target ${config.hostTelemetryUnit} forgezero-agent-update-helper.service forgezero-metal-agent-egress.service
854
+ Requires=forgezero-agent-update-helper.service forgezero-metal-agent-egress.service
855
+
856
+ [Service]
857
+ Type=simple
858
+ User=forgezero-metal
859
+ Group=forgezero-metal
860
+ SupplementaryGroups=forgezero-update
861
+ LoadCredentialEncrypted=metal-agent-seed:${SEED_CREDENTIAL_PATH}
862
+ Environment=FZ_SEED_CREDENTIAL=metal-agent-seed
863
+ Environment=FZ_AGENT_ROLE=metal
864
+ Environment=FZ_AGENT_HANDOVER_CANDIDATE=true
865
+ Environment=FZ_AGENT_HANDOVER_READY_SOCKET=/run/forgezero/candidate-ready.sock
866
+ Environment=FZ_METAL_HOSTNAME=${config.metalHostname}
867
+ Environment=FZ_API=${config.profile.apiUrl}
868
+ Environment=NODE_ENV=production
869
+ Environment=OTEL_EXPORTER_OTLP_ENDPOINT=${config.hostTelemetryEndpoint}
870
+ Environment=OTEL_SERVICE_NAME=forgezero-metal-agent-candidate
871
+ ExecStart=/opt/forgezero/agent/candidate/dist/fz-agent.js
872
+ Restart=on-failure
873
+ RestartSec=2
874
+ RuntimeDirectory=forgezero
875
+ RuntimeDirectoryMode=0750
876
+ RuntimeDirectoryPreserve=yes
877
+ LimitCORE=0
878
+ NoNewPrivileges=true
879
+ PrivateTmp=true
880
+ ProtectSystem=strict
881
+ ProtectHome=true
882
+ ProtectKernelTunables=true
883
+ ProtectKernelModules=true
884
+ ProtectControlGroups=true
885
+ RestrictSUIDSGID=true
886
+ LockPersonality=true
887
+ ${egress}
849
888
  `,
850
889
  "forgezero-metal-agent.service": `[Unit]
851
890
  Description=ForgeZero identity-only physical-host Agent
@@ -9,9 +9,11 @@ import {
9
9
 
10
10
  class SignedNodeHttpError extends Error {
11
11
  status;
12
- constructor(status, message) {
12
+ code;
13
+ constructor(status, message, code) {
13
14
  super(message);
14
15
  this.status = status;
16
+ this.code = code;
15
17
  this.name = "SignedNodeHttpError";
16
18
  }
17
19
  }
@@ -58,7 +60,7 @@ async function postSignedNode(options, path, body) {
58
60
  if (!response.ok) {
59
61
  const failure = payload;
60
62
  const reason = failure ? failure.error?.message ?? failure.message : undefined;
61
- throw new SignedNodeHttpError(response.status, reason || `signed node request returned HTTP ${response.status}`);
63
+ throw new SignedNodeHttpError(response.status, reason || `signed node request returned HTTP ${response.status}`, failure?.error?.code);
62
64
  }
63
65
  try {
64
66
  return await openResponse(recipient.secretKey, signature, payload);
@@ -129,9 +129,11 @@ import {
129
129
 
130
130
  class SignedNodeHttpError extends Error {
131
131
  status;
132
- constructor(status, message) {
132
+ code;
133
+ constructor(status, message, code) {
133
134
  super(message);
134
135
  this.status = status;
136
+ this.code = code;
135
137
  this.name = "SignedNodeHttpError";
136
138
  }
137
139
  }
@@ -178,7 +180,7 @@ async function postSignedNode(options, path, body) {
178
180
  if (!response.ok) {
179
181
  const failure = payload;
180
182
  const reason = failure ? failure.error?.message ?? failure.message : undefined;
181
- throw new SignedNodeHttpError(response.status, reason || `signed node request returned HTTP ${response.status}`);
183
+ throw new SignedNodeHttpError(response.status, reason || `signed node request returned HTTP ${response.status}`, failure?.error?.code);
182
184
  }
183
185
  try {
184
186
  return await openResponse(recipient.secretKey, signature, payload);
@@ -216,8 +218,17 @@ function tenantNodeApiUrl(apiUrl, tenantSlug) {
216
218
  return url.toString().replace(/\/$/, "");
217
219
  }
218
220
  function createNodeVaultCache(options) {
219
- const post = (operation, body) => postSignedNode(options, `v1/node/vault/${operation}`, body);
220
- return createSecretCache({
221
+ let cache;
222
+ const post = async (operation, body) => {
223
+ try {
224
+ return await postSignedNode(options, `v1/node/vault/${operation}`, body);
225
+ } catch (cause) {
226
+ if (cause instanceof SignedNodeHttpError && cause.code === "VAULT_API_ONLY")
227
+ cache?.clear();
228
+ throw cause;
229
+ }
230
+ };
231
+ cache = createSecretCache({
221
232
  ttlMs: options.ttlMs,
222
233
  maxStaleMs: options.maxStaleMs,
223
234
  list: async () => {
@@ -246,6 +257,7 @@ function createNodeVaultCache(options) {
246
257
  };
247
258
  }
248
259
  });
260
+ return cache;
249
261
  }
250
262
  function startNodeVaultSync(cache, options = {}) {
251
263
  const interval = Math.max(1000, options.intervalMs ?? 30000);
@@ -1405,9 +1405,13 @@ import { DEFAULT_SOCKET } from "@forgezero/vault";
1405
1405
 
1406
1406
  // src/agent-update.ts
1407
1407
  var DEFAULT_AGENT_RELEASE_ROOT = "/opt/forgezero/agent";
1408
+ var DEFAULT_AGENT_CANDIDATE_LINK = `${DEFAULT_AGENT_RELEASE_ROOT}/candidate`;
1408
1409
  var DEFAULT_AGENT_UPDATE_SOCKET = "/run/forgezero-update/helper.sock";
1409
1410
  var MAX_AGENT_TARBALL_BYTES = 32 * 1024 * 1024;
1410
1411
 
1412
+ // src/agent-handover.ts
1413
+ var DEFAULT_AGENT_CANDIDATE_READY_SOCKET = "/run/forgezero/candidate-ready.sock";
1414
+
1411
1415
  // src/agent-update-helper.ts
1412
1416
  var AGENT_UPDATE_GROUP = "forgezero-update";
1413
1417
  var AGENT_UPDATE_HELPER_UNIT_PATH = "/etc/systemd/system/forgezero-agent-update-helper.service";
@@ -1416,7 +1420,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
1416
1420
  var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
1417
1421
 
1418
1422
  // src/version.ts
1419
- var VERSION = "0.1.85";
1423
+ var VERSION = "0.1.87";
1420
1424
 
1421
1425
  // src/software.ts
1422
1426
  var PINNED_BUN_VERSION = "1.3.14";
@@ -1674,6 +1678,7 @@ var LIFECYCLE_GROUP = "forgezero-lifecycle";
1674
1678
  var DEPLOYMENT_RUNNER_UNIT_PATH = "/etc/systemd/system/forgezero-deploy-runner.service";
1675
1679
  var AGENT_SOCKET_UNIT_PATH = "/etc/systemd/system/forgezero-agent.socket";
1676
1680
  var AGENT_SOCKET_PROXY_UNIT_PATH = "/etc/systemd/system/forgezero-agent-proxy.service";
1681
+ var AGENT_CANDIDATE_UNIT_PATH = "/etc/systemd/system/forgezero-agent-candidate.service";
1677
1682
  var DEPLOYMENT_RUNNER_SOCKET = "/run/forgezero-deploy/runner.sock";
1678
1683
  var ENROLMENT_UNIT_PATH = "/etc/systemd/system/forgezero-agent-enrol.service";
1679
1684
  var LIFECYCLE_HELPER_UNIT_PATH = "/etc/systemd/system/forgezero-lifecycle-helper.service";
@@ -1785,6 +1790,7 @@ Type=simple
1785
1790
  User=root
1786
1791
  Group=${AGENT_UPDATE_GROUP}
1787
1792
  Environment=FZ_AGENT_UPDATE_SOCKET=${DEFAULT_AGENT_UPDATE_SOCKET}
1793
+ Environment=FZ_AGENT_PUBLIC_SOCKET=${systemdPath(options.socketPath, "agent socket")}
1788
1794
  ExecStart=${bin} update-helper
1789
1795
  Restart=always
1790
1796
  RestartSec=2
@@ -1829,13 +1835,11 @@ WantedBy=sockets.target
1829
1835
  `;
1830
1836
  }
1831
1837
  function agentSocketProxyUnit(options) {
1832
- const backend = agentBackendSocketPath(options.socketPath);
1838
+ const backend = agentRoutingSocketPath(options.socketPath);
1833
1839
  const user = options.user ?? "forgezero";
1834
1840
  return `[Unit]
1835
1841
  Description=ForgeZero application Vault socket proxy
1836
1842
  Documentation=https://www.forgezero.net/docs/agent
1837
- Requires=forgezero-agent.service
1838
- After=forgezero-agent.service
1839
1843
 
1840
1844
  [Service]
1841
1845
  User=${user}
@@ -1856,12 +1860,26 @@ RestrictAddressFamilies=AF_UNIX
1856
1860
  `;
1857
1861
  }
1858
1862
  function agentBackendSocketPath(publicSocketPath) {
1863
+ const socket = systemdPath(publicSocketPath, "agent socket");
1864
+ const backend = `${socket}.backend.active`;
1865
+ if (Buffer.byteLength(backend) > 100)
1866
+ throw new Error("agent socket path is too long for a Unix socket");
1867
+ return backend;
1868
+ }
1869
+ function agentRoutingSocketPath(publicSocketPath) {
1859
1870
  const socket = systemdPath(publicSocketPath, "agent socket");
1860
1871
  const backend = `${socket}.backend`;
1861
1872
  if (Buffer.byteLength(backend) > 100)
1862
1873
  throw new Error("agent socket path is too long for a Unix socket");
1863
1874
  return backend;
1864
1875
  }
1876
+ function agentCandidateSocketPath(publicSocketPath) {
1877
+ const socket = systemdPath(publicSocketPath, "agent socket");
1878
+ const backend = `${socket}.backend.candidate`;
1879
+ if (Buffer.byteLength(backend) > 100)
1880
+ throw new Error("agent socket path is too long for a Unix socket");
1881
+ return backend;
1882
+ }
1865
1883
  var systemdPath = (value, label) => {
1866
1884
  if (!value || !/^\/[A-Za-z0-9._@/-]+$/.test(value))
1867
1885
  throw new Error(`invalid ${label} path`);
@@ -2145,7 +2163,11 @@ function agentUnit(options) {
2145
2163
  }
2146
2164
  const environment = [
2147
2165
  "NODE_ENV=production",
2148
- `FZ_SOCKET_PATH=${agentBackendSocketPath(options.socketPath)}`,
2166
+ `FZ_SOCKET_PATH=${options.backendSocketPath ?? agentBackendSocketPath(options.socketPath)}`,
2167
+ `FZ_AGENT_PUBLIC_SOCKET=${options.socketPath}`,
2168
+ `FZ_AGENT_ROUTE_SOCKET=${agentRoutingSocketPath(options.socketPath)}`,
2169
+ options.handoverCandidate ? "FZ_AGENT_HANDOVER_CANDIDATE=true" : null,
2170
+ options.handoverCandidate ? `FZ_AGENT_HANDOVER_READY_SOCKET=${DEFAULT_AGENT_CANDIDATE_READY_SOCKET}` : null,
2149
2171
  `FZ_CONTROL_SOCKET=${controlSocketPath}`,
2150
2172
  `FZ_SEED_CREDENTIAL=agent-seed`,
2151
2173
  `FZ_AGENT_MODE=${options.mode}`,
@@ -2282,6 +2304,26 @@ ${deploymentWrites}
2282
2304
  WantedBy=multi-user.target
2283
2305
  `;
2284
2306
  }
2307
+ function agentCandidateUnit(options) {
2308
+ return agentUnit({
2309
+ ...options,
2310
+ binPath: `${DEFAULT_AGENT_RELEASE_ROOT}/candidate/dist/fz-agent.js`,
2311
+ backendSocketPath: agentCandidateSocketPath(options.socketPath),
2312
+ handoverCandidate: true,
2313
+ gitCredentialPath: undefined,
2314
+ deploymentCredentials: {},
2315
+ bootstrapSshCredentialPath: undefined,
2316
+ bootstrapSshPublicKeyPath: undefined,
2317
+ pullBootstrap: false,
2318
+ pullDeployments: false,
2319
+ pullMigrations: false,
2320
+ lifecycleProfilePath: undefined,
2321
+ bootstrapTargetTelemetryEndpoint: undefined,
2322
+ repository: undefined,
2323
+ bootstrapBundlePath: undefined,
2324
+ bootstrapBundleManifestPath: undefined
2325
+ }).replace("Description=ForgeZero node agent", "Description=ForgeZero candidate node agent");
2326
+ }
2285
2327
  var renderOperation = (operation) => {
2286
2328
  if (operation.kind === "commands")
2287
2329
  return operation.commands.map(({ argv }) => argv.join(" ")).join(`
@@ -2417,6 +2459,7 @@ function planProvision(options) {
2417
2459
  auxiliaryUnits: [
2418
2460
  { path: AGENT_SOCKET_UNIT_PATH, unit: agentSocketUnit(options) },
2419
2461
  { path: AGENT_SOCKET_PROXY_UNIT_PATH, unit: agentSocketProxyUnit(options) },
2462
+ { path: AGENT_CANDIDATE_UNIT_PATH, unit: agentCandidateUnit(options) },
2420
2463
  { path: AGENT_UPDATE_HELPER_UNIT_PATH, unit: agentUpdateHelperUnit(options) },
2421
2464
  ...options.enforceEgress ? [
2422
2465
  { path: AGENT_EGRESS_UNIT_PATH, unit: agentEgressUnit(options) }
@@ -3002,12 +3045,14 @@ var httpsOrigin = (name, raw) => {
3002
3045
  return value.origin;
3003
3046
  };
3004
3047
  function validatePlatformInitialInventory(value) {
3005
- if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((key) => !["metalHostname", "region", "computes"].includes(key)) || !/^[A-Za-z0-9][A-Za-z0-9.-]{1,252}$/.test(value.metalHostname) || !value.region || typeof value.region !== "object" || Array.isArray(value.region) || Object.keys(value.region).some((key) => !["key", "label", "country", "city", "confidentialCapable"].includes(key)) || !/^[a-z0-9][a-z0-9-]{0,62}$/.test(value.region.key) || !/^[A-Z]{2}$/.test(value.region.country) || value.region.confidentialCapable !== true || !Array.isArray(value.computes)) {
3048
+ if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((key) => !["metalHostname", "region", "computes", "deployment"].includes(key)) || !/^[A-Za-z0-9][A-Za-z0-9.-]{1,252}$/.test(value.metalHostname) || !value.region || typeof value.region !== "object" || Array.isArray(value.region) || Object.keys(value.region).some((key) => !["key", "label", "country", "city", "confidentialCapable"].includes(key)) || !/^[a-z0-9][a-z0-9-]{0,62}$/.test(value.region.key) || !/^[A-Z]{2}$/.test(value.region.country) || value.region.confidentialCapable !== true || !Array.isArray(value.computes)) {
3006
3049
  throw new Error("initial platform inventory coordinates are invalid");
3007
3050
  }
3008
3051
  safeAtom("initialInventory.region.label", value.region.label);
3009
3052
  if (value.region.city !== undefined)
3010
3053
  safeAtom("initialInventory.region.city", value.region.city);
3054
+ if (value.deployment !== undefined && (!value.deployment || typeof value.deployment !== "object" || Array.isArray(value.deployment) || Object.keys(value.deployment).some((key) => !["source", "branch", "revision", "bundleSha256"].includes(key)) || value.deployment.source !== "bootstrap-bundle" || !["dev", "main"].includes(value.deployment.branch) || !/^[a-f0-9]{40}$/.test(value.deployment.revision) || !/^[a-f0-9]{64}$/.test(value.deployment.bundleSha256)))
3055
+ throw new Error("initial platform deployment evidence is invalid");
3011
3056
  const computes = validatePlatformGenesisGuests(value.computes.map((compute) => {
3012
3057
  if (!compute || typeof compute !== "object" || Array.isArray(compute) || Object.keys(compute).some((key) => ![
3013
3058
  "name",
@@ -3042,7 +3087,8 @@ function validatePlatformInitialInventory(value) {
3042
3087
  databaseRole: compute.databaseRole,
3043
3088
  databaseAgency: compute.databaseAgency,
3044
3089
  confidential: true
3045
- }))
3090
+ })),
3091
+ ...value.deployment ? { deployment: { ...value.deployment } } : {}
3046
3092
  };
3047
3093
  }
3048
3094
  var systemdValue = (name, raw) => {
@@ -4770,8 +4816,10 @@ function strictBootstrapDocument(value) {
4770
4816
  ], "runtime environment");
4771
4817
  const environment = runtime.environment;
4772
4818
  if (environment.initialInventory !== undefined) {
4773
- const inventory = exactKeys(environment.initialInventory, ["metalHostname", "region", "computes"], "initial inventory");
4819
+ const inventory = exactKeys(environment.initialInventory, ["metalHostname", "region", "computes", "deployment"], "initial inventory");
4774
4820
  exactKeys(inventory.region, ["key", "label", "country", "city", "confidentialCapable"], "initial inventory region");
4821
+ if (inventory.deployment !== undefined)
4822
+ exactKeys(inventory.deployment, ["source", "branch", "revision", "bundleSha256"], "initial deployment evidence");
4775
4823
  if (!Array.isArray(inventory.computes))
4776
4824
  throw new Error("initial inventory computes must be an array");
4777
4825
  for (const compute of inventory.computes)
@@ -5392,6 +5440,7 @@ function planMetalBootstrap(config) {
5392
5440
  "forgezero-metal-helper.service",
5393
5441
  "forgezero-agent-update-helper.service",
5394
5442
  "forgezero-metal-agent-egress.service",
5443
+ "forgezero-metal-agent-candidate.service",
5395
5444
  "forgezero-metal-agent.service"
5396
5445
  ],
5397
5446
  steps: [
@@ -5533,6 +5582,44 @@ RestrictAddressFamilies=AF_UNIX AF_NETLINK
5533
5582
 
5534
5583
  [Install]
5535
5584
  WantedBy=multi-user.target
5585
+ `,
5586
+ "forgezero-metal-agent-candidate.service": `[Unit]
5587
+ Description=ForgeZero candidate physical-host Agent
5588
+ After=network-online.target ${config.hostTelemetryUnit} forgezero-agent-update-helper.service forgezero-metal-agent-egress.service
5589
+ Requires=forgezero-agent-update-helper.service forgezero-metal-agent-egress.service
5590
+
5591
+ [Service]
5592
+ Type=simple
5593
+ User=forgezero-metal
5594
+ Group=forgezero-metal
5595
+ SupplementaryGroups=forgezero-update
5596
+ LoadCredentialEncrypted=metal-agent-seed:${SEED_CREDENTIAL_PATH}
5597
+ Environment=FZ_SEED_CREDENTIAL=metal-agent-seed
5598
+ Environment=FZ_AGENT_ROLE=metal
5599
+ Environment=FZ_AGENT_HANDOVER_CANDIDATE=true
5600
+ Environment=FZ_AGENT_HANDOVER_READY_SOCKET=/run/forgezero/candidate-ready.sock
5601
+ Environment=FZ_METAL_HOSTNAME=${config.metalHostname}
5602
+ Environment=FZ_API=${config.profile.apiUrl}
5603
+ Environment=NODE_ENV=production
5604
+ Environment=OTEL_EXPORTER_OTLP_ENDPOINT=${config.hostTelemetryEndpoint}
5605
+ Environment=OTEL_SERVICE_NAME=forgezero-metal-agent-candidate
5606
+ ExecStart=/opt/forgezero/agent/candidate/dist/fz-agent.js
5607
+ Restart=on-failure
5608
+ RestartSec=2
5609
+ RuntimeDirectory=forgezero
5610
+ RuntimeDirectoryMode=0750
5611
+ RuntimeDirectoryPreserve=yes
5612
+ LimitCORE=0
5613
+ NoNewPrivileges=true
5614
+ PrivateTmp=true
5615
+ ProtectSystem=strict
5616
+ ProtectHome=true
5617
+ ProtectKernelTunables=true
5618
+ ProtectKernelModules=true
5619
+ ProtectControlGroups=true
5620
+ RestrictSUIDSGID=true
5621
+ LockPersonality=true
5622
+ ${egress}
5536
5623
  `,
5537
5624
  "forgezero-metal-agent.service": `[Unit]
5538
5625
  Description=ForgeZero identity-only physical-host Agent
@@ -31,6 +31,13 @@ export interface PlatformInitialInventory {
31
31
  PlatformInitialInventoryCompute,
32
32
  PlatformInitialInventoryCompute
33
33
  ];
34
+ /** Generation-one release evidence; external Git automation is connected after custody opens Vault. */
35
+ deployment?: {
36
+ source: 'bootstrap-bundle';
37
+ branch: 'dev' | 'main';
38
+ revision: string;
39
+ bundleSha256: string;
40
+ };
34
41
  }
35
42
  export type PlatformBootstrapEmail = {
36
43
  provider: 'smtp';
@@ -724,12 +724,14 @@ var httpsOrigin = (name, raw) => {
724
724
  return value.origin;
725
725
  };
726
726
  function validatePlatformInitialInventory(value) {
727
- if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((key) => !["metalHostname", "region", "computes"].includes(key)) || !/^[A-Za-z0-9][A-Za-z0-9.-]{1,252}$/.test(value.metalHostname) || !value.region || typeof value.region !== "object" || Array.isArray(value.region) || Object.keys(value.region).some((key) => !["key", "label", "country", "city", "confidentialCapable"].includes(key)) || !/^[a-z0-9][a-z0-9-]{0,62}$/.test(value.region.key) || !/^[A-Z]{2}$/.test(value.region.country) || value.region.confidentialCapable !== true || !Array.isArray(value.computes)) {
727
+ if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((key) => !["metalHostname", "region", "computes", "deployment"].includes(key)) || !/^[A-Za-z0-9][A-Za-z0-9.-]{1,252}$/.test(value.metalHostname) || !value.region || typeof value.region !== "object" || Array.isArray(value.region) || Object.keys(value.region).some((key) => !["key", "label", "country", "city", "confidentialCapable"].includes(key)) || !/^[a-z0-9][a-z0-9-]{0,62}$/.test(value.region.key) || !/^[A-Z]{2}$/.test(value.region.country) || value.region.confidentialCapable !== true || !Array.isArray(value.computes)) {
728
728
  throw new Error("initial platform inventory coordinates are invalid");
729
729
  }
730
730
  safeAtom("initialInventory.region.label", value.region.label);
731
731
  if (value.region.city !== undefined)
732
732
  safeAtom("initialInventory.region.city", value.region.city);
733
+ if (value.deployment !== undefined && (!value.deployment || typeof value.deployment !== "object" || Array.isArray(value.deployment) || Object.keys(value.deployment).some((key) => !["source", "branch", "revision", "bundleSha256"].includes(key)) || value.deployment.source !== "bootstrap-bundle" || !["dev", "main"].includes(value.deployment.branch) || !/^[a-f0-9]{40}$/.test(value.deployment.revision) || !/^[a-f0-9]{64}$/.test(value.deployment.bundleSha256)))
734
+ throw new Error("initial platform deployment evidence is invalid");
733
735
  const computes = validatePlatformGenesisGuests(value.computes.map((compute) => {
734
736
  if (!compute || typeof compute !== "object" || Array.isArray(compute) || Object.keys(compute).some((key) => ![
735
737
  "name",
@@ -764,7 +766,8 @@ function validatePlatformInitialInventory(value) {
764
766
  databaseRole: compute.databaseRole,
765
767
  databaseAgency: compute.databaseAgency,
766
768
  confidential: true
767
- }))
769
+ })),
770
+ ...value.deployment ? { deployment: { ...value.deployment } } : {}
768
771
  };
769
772
  }
770
773
  var systemdValue = (name, raw) => {