@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.
package/dist/bootstrap.js CHANGED
@@ -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)
@@ -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);