@forgezero/agent 0.1.22 → 0.1.24

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
@@ -101,7 +101,8 @@ async function spawnWith(command, env, report = () => {}) {
101
101
  }
102
102
 
103
103
  // src/cli/index.ts
104
- import { existsSync, readFileSync, statSync, unlinkSync, writeFileSync } from "fs";
104
+ import { existsSync, mkdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from "fs";
105
+ import { dirname } from "path";
105
106
  import { fileURLToPath } from "url";
106
107
  import { DEFAULT_SOCKET } from "@forgezero/vault";
107
108
 
@@ -149,9 +150,104 @@ var reasonFor = (mode) => mode === "attested" ? "SEV-SNP guest device present, s
149
150
  var DEPLOYMENT_RUNNER_USER = "forgezero-runner";
150
151
  var DEPLOYMENT_GROUP = "forgezero-deploy";
151
152
  var VAULT_GROUP = "forgezero-vault";
153
+ var LIFECYCLE_GROUP = "forgezero-lifecycle";
152
154
  var DEPLOYMENT_RUNNER_UNIT_PATH = "/etc/systemd/system/forgezero-deploy-runner.service";
153
155
  var DEPLOYMENT_RUNNER_SOCKET = "/run/forgezero-deploy/runner.sock";
154
156
  var ENROLMENT_UNIT_PATH = "/etc/systemd/system/forgezero-agent-enrol.service";
157
+ var LIFECYCLE_HELPER_UNIT_PATH = "/etc/systemd/system/forgezero-lifecycle-helper.service";
158
+ var LIFECYCLE_HELPER_SOCKET = "/run/forgezero-lifecycle/helper.sock";
159
+ var WARP_CONFIG_UNIT_PATH = "/etc/systemd/system/forgezero-warp-config.service";
160
+ var WARP_SERVICE_DROP_IN_PATH = "/etc/systemd/system/warp-svc.service.d/forgezero.conf";
161
+ var systemdPath = (value, label) => {
162
+ if (!value || !/^\/[A-Za-z0-9._@/-]+$/.test(value))
163
+ throw new Error(`invalid ${label} path`);
164
+ return value;
165
+ };
166
+ function warpConfigUnit(options) {
167
+ if (!options.warpOrganization || !/^[a-z0-9][a-z0-9-]{0,62}$/i.test(options.warpOrganization)) {
168
+ throw new Error("WARP organization is invalid");
169
+ }
170
+ if (!options.warpClientIdCredentialPath || !options.warpClientSecretCredentialPath) {
171
+ throw new Error("WARP service-token credential paths are required");
172
+ }
173
+ const clientIdPath = systemdPath(options.warpClientIdCredentialPath, "WARP client-id credential");
174
+ const clientSecretPath = systemdPath(options.warpClientSecretCredentialPath, "WARP client-secret credential");
175
+ const bin = options.binPath ?? "fz-agent";
176
+ return `[Unit]
177
+ Description=Materialize Cloudflare One enrollment in tmpfs
178
+ Documentation=https://www.forgezero.net/docs/agent
179
+ Before=warp-svc.service
180
+
181
+ [Service]
182
+ Type=oneshot
183
+ RemainAfterExit=yes
184
+ LoadCredentialEncrypted=warp-auth-client-id:${clientIdPath}
185
+ LoadCredentialEncrypted=warp-auth-client-secret:${clientSecretPath}
186
+ Environment=FZ_WARP_CLIENT_ID_CREDENTIAL=warp-auth-client-id
187
+ Environment=FZ_WARP_CLIENT_SECRET_CREDENTIAL=warp-auth-client-secret
188
+ ExecStart=${bin} warp-config --organization=${options.warpOrganization}
189
+ RuntimeDirectory=forgezero-warp
190
+ RuntimeDirectoryMode=0700
191
+ RuntimeDirectoryPreserve=yes
192
+ UMask=0077
193
+ LimitCORE=0
194
+ NoNewPrivileges=true
195
+ PrivateTmp=true
196
+ ProtectSystem=strict
197
+ ProtectHome=true
198
+ ReadWritePaths=/var/lib/cloudflare-warp
199
+
200
+ [Install]
201
+ WantedBy=multi-user.target
202
+ `;
203
+ }
204
+ function warpServiceDropIn() {
205
+ return `[Unit]
206
+ Requires=forgezero-warp-config.service
207
+ After=forgezero-warp-config.service
208
+ `;
209
+ }
210
+ function lifecycleHelperUnit(options) {
211
+ if (!options.lifecycleProfilePath)
212
+ throw new Error("lifecycle helper needs a root-owned profile");
213
+ const bin = options.binPath ?? "fz-agent";
214
+ const profile = systemdPath(options.lifecycleProfilePath, "lifecycle profile");
215
+ const socket = systemdPath(options.lifecycleHelperSocketPath ?? LIFECYCLE_HELPER_SOCKET, "lifecycle helper socket");
216
+ return `[Unit]
217
+ Description=ForgeZero fixed-operation compute lifecycle helper
218
+ Documentation=https://www.forgezero.net/docs/agent
219
+ After=network-online.target
220
+ Wants=network-online.target
221
+
222
+ [Service]
223
+ Type=simple
224
+ User=root
225
+ Group=${LIFECYCLE_GROUP}
226
+ Environment=FZ_LIFECYCLE_HELPER_SOCKET=${socket}
227
+ ExecStart=${bin} lifecycle-helper --profile=${profile}
228
+ Restart=always
229
+ RestartSec=2
230
+ RuntimeDirectory=forgezero-lifecycle
231
+ RuntimeDirectoryMode=0750
232
+ UMask=0007
233
+ LimitCORE=0
234
+ NoNewPrivileges=true
235
+ PrivateTmp=true
236
+ ProtectSystem=strict
237
+ ProtectHome=true
238
+ ProtectKernelTunables=true
239
+ ProtectKernelModules=true
240
+ ProtectControlGroups=true
241
+ RestrictSUIDSGID=true
242
+ RestrictRealtime=true
243
+ MemoryDenyWriteExecute=true
244
+ LockPersonality=true
245
+ RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
246
+
247
+ [Install]
248
+ WantedBy=multi-user.target
249
+ `;
250
+ }
155
251
  function agentEnrolmentUnit(options) {
156
252
  if (!options.apiUrl || !options.enrolTokenCredentialPath || !options.enrolStatePath) {
157
253
  throw new Error("direct enrolment needs API, credential and state paths");
@@ -163,6 +259,16 @@ function agentEnrolmentUnit(options) {
163
259
  ` : "";
164
260
  const gitPublicKey = options.gitPublicKeyPath ? `Environment=FZ_GIT_PUBLIC_KEY_FILE=${options.gitPublicKeyPath}
165
261
  ` : "";
262
+ const networkAttachment = [
263
+ options.cloudflareAccountId ? `Environment=FZ_CF_ACCOUNT_ID=${options.cloudflareAccountId}
264
+ ` : "",
265
+ options.cloudflareTunnelId ? `Environment=FZ_CF_TUNNEL_ID=${options.cloudflareTunnelId}
266
+ ` : "",
267
+ options.cloudflareVirtualNetworkId ? `Environment=FZ_CF_VIRTUAL_NETWORK_ID=${options.cloudflareVirtualNetworkId}
268
+ ` : "",
269
+ options.cloudflareWarpPolicyId ? `Environment=FZ_CF_WARP_POLICY_ID=${options.cloudflareWarpPolicyId}
270
+ ` : ""
271
+ ].join("");
166
272
  const stateDir = options.enrolStatePath.replace(/\/[^/]+$/, "");
167
273
  return `[Unit]
168
274
  Description=Bind this machine to its ForgeZero compute
@@ -181,7 +287,7 @@ Environment=FZ_SEED_CREDENTIAL=agent-seed
181
287
  Environment=FZ_ENROL_TOKEN_CREDENTIAL=enrol-token
182
288
  Environment=FZ_ENROL_STATE_FILE=${options.enrolStatePath}
183
289
  Environment=FZ_API=${options.apiUrl}
184
- ${label}${gitPublicKey}ExecStart=${bin} enrol
290
+ ${label}${gitPublicKey}${networkAttachment}ExecStart=${bin} enrol
185
291
  # A '+' fixed command runs as root solely to remove the host-bound one-time
186
292
  # ciphertext. Tenant code and the agent never receive a privilege boundary.
187
293
  ExecStartPost=+/usr/bin/rm -f ${options.enrolTokenCredentialPath}
@@ -246,6 +352,34 @@ function agentUnit(options) {
246
352
  const controlSocketPath = options.controlSocketPath ?? "/run/forgezero/control.sock";
247
353
  const deployRoot = options.deployRoot ?? "/opt/forgezero";
248
354
  const deploymentEnabled = Boolean(options.repository || options.pullDeployments);
355
+ const lifecycleEnabled = Boolean(options.pullMigrations && options.lifecycleProfilePath);
356
+ if (Boolean(options.pullMigrations) !== Boolean(options.lifecycleProfilePath)) {
357
+ throw new Error("migration pull and lifecycle profile must be supplied together");
358
+ }
359
+ const lifecycleHelperSocketPath = lifecycleEnabled ? systemdPath(options.lifecycleHelperSocketPath ?? LIFECYCLE_HELPER_SOCKET, "lifecycle helper socket") : undefined;
360
+ const warpValues = [
361
+ options.warpOrganization,
362
+ options.warpClientIdCredentialPath,
363
+ options.warpClientSecretCredentialPath
364
+ ];
365
+ const warpEnabled = warpValues.every(Boolean);
366
+ if (warpValues.some(Boolean) && !warpEnabled)
367
+ throw new Error("WARP configuration must be supplied together");
368
+ const networkAttachmentValues = [
369
+ options.cloudflareAccountId,
370
+ options.cloudflareTunnelId,
371
+ options.cloudflareVirtualNetworkId,
372
+ options.cloudflareWarpPolicyId
373
+ ];
374
+ if (networkAttachmentValues.some(Boolean)) {
375
+ if (!options.cloudflareAccountId || !options.cloudflareTunnelId || !options.cloudflareWarpPolicyId) {
376
+ throw new Error("private-network attachment requires account, Tunnel and WARP policy ids");
377
+ }
378
+ const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
379
+ if (!/^[a-f0-9]{32}$/i.test(options.cloudflareAccountId) || !uuid.test(options.cloudflareTunnelId) || options.cloudflareVirtualNetworkId && !uuid.test(options.cloudflareVirtualNetworkId) || !/^[A-Za-z0-9-]{1,64}$/.test(options.cloudflareWarpPolicyId)) {
380
+ throw new Error("private-network attachment coordinates are invalid");
381
+ }
382
+ }
249
383
  const enrolmentEnabled = Boolean(options.enrolTokenCredentialPath && options.enrolStatePath);
250
384
  const deploymentEnvironment = options.deploymentEnvironment ?? {};
251
385
  const deploymentCredentials = options.deploymentCredentials ?? {};
@@ -280,7 +414,9 @@ function agentUnit(options) {
280
414
  Object.keys(deploymentEnvironment).length > 0 ? `FZ_DEPLOY_ENV_NAMES=${Object.keys(deploymentEnvironment).join(",")}` : null,
281
415
  ...Object.entries(deploymentEnvironment).map(([name, value]) => `${name}=${value}`),
282
416
  options.publicApiUrl ? `FZ_PUBLIC_API_URL=${options.publicApiUrl}` : null,
283
- options.pullDeployments ? "FZ_DEPLOY_PULL=true" : null
417
+ options.pullDeployments ? "FZ_DEPLOY_PULL=true" : null,
418
+ options.pullMigrations ? "FZ_MIGRATION_PULL=true" : null,
419
+ options.pullMigrations ? `FZ_LIFECYCLE_HELPER_SOCKET=${lifecycleHelperSocketPath}` : null
284
420
  ].filter((line) => line !== null);
285
421
  if (deploymentEnabled) {
286
422
  environment.push(`HOME=${deployRoot}/agent-home`, `XDG_CACHE_HOME=${deployRoot}/cache`);
@@ -290,14 +426,22 @@ function agentUnit(options) {
290
426
  const projectCredentials = Object.entries(deploymentCredentials).map(([name, path]) => `LoadCredentialEncrypted=${name}:${path}`).join(`
291
427
  `);
292
428
  const deploymentWrites = deploymentEnabled ? `ReadWritePaths=${deployRoot}/releases ${deployRoot}/agent-home ${deployRoot}/cache` : "";
293
- const deploymentGroup = deploymentEnabled ? `SupplementaryGroups=${DEPLOYMENT_GROUP}` : "";
429
+ const supplementaryGroups = [
430
+ deploymentEnabled ? DEPLOYMENT_GROUP : null,
431
+ lifecycleEnabled ? LIFECYCLE_GROUP : null
432
+ ].filter((value) => value !== null);
433
+ const deploymentGroup = supplementaryGroups.length > 0 ? `SupplementaryGroups=${supplementaryGroups.join(" ")}` : "";
294
434
  const after = [
295
435
  "network-online.target",
296
436
  deploymentEnabled ? "forgezero-deploy-runner.service" : null,
437
+ lifecycleEnabled ? "forgezero-lifecycle-helper.service" : null,
438
+ warpEnabled ? "warp-svc.service" : null,
297
439
  enrolmentEnabled ? "forgezero-agent-enrol.service" : null
298
440
  ].filter((value) => value !== null);
299
441
  const requires = [
300
442
  deploymentEnabled ? "forgezero-deploy-runner.service" : null,
443
+ lifecycleEnabled ? "forgezero-lifecycle-helper.service" : null,
444
+ warpEnabled ? "warp-svc.service" : null,
301
445
  enrolmentEnabled ? "forgezero-agent-enrol.service" : null
302
446
  ].filter((value) => value !== null);
303
447
  const deploymentDependency = [
@@ -369,37 +513,63 @@ function planProvision(options) {
369
513
  const credentialDir = seedCredentialPath.replace(/\/[^/]+$/, "");
370
514
  const deployRoot = options.deployRoot ?? "/opt/forgezero";
371
515
  const deploymentEnabled = Boolean(options.repository || options.pullDeployments);
516
+ const lifecycleEnabled = Boolean(options.pullMigrations && options.lifecycleProfilePath);
517
+ if (Boolean(options.pullMigrations) !== Boolean(options.lifecycleProfilePath)) {
518
+ throw new Error("migration pull and lifecycle profile must be supplied together");
519
+ }
520
+ const warpValues = [
521
+ options.warpOrganization,
522
+ options.warpClientIdCredentialPath,
523
+ options.warpClientSecretCredentialPath
524
+ ];
525
+ const warpEnabled = warpValues.every(Boolean);
526
+ if (warpValues.some(Boolean) && !warpEnabled)
527
+ throw new Error("WARP configuration must be supplied together");
372
528
  const enrolmentEnabled = Boolean(options.enrolTokenSourcePath && options.enrolTokenCredentialPath && options.enrolStatePath);
373
529
  if (Boolean(options.enrolTokenSourcePath) !== Boolean(options.enrolTokenCredentialPath) || Boolean(options.enrolTokenCredentialPath) !== Boolean(options.enrolStatePath))
374
530
  throw new Error("direct enrolment paths must be supplied together");
375
- const safePath = (value, label) => {
376
- if (!value || !/^\/[A-Za-z0-9._@/-]+$/.test(value))
377
- throw new Error(`invalid ${label} path`);
378
- return value;
379
- };
380
- const enrolTokenSourcePath = enrolmentEnabled ? safePath(options.enrolTokenSourcePath, "enrolment source") : undefined;
381
- const enrolTokenCredentialPath = enrolmentEnabled ? safePath(options.enrolTokenCredentialPath, "enrolment credential") : undefined;
382
- const enrolStatePath = enrolmentEnabled ? safePath(options.enrolStatePath, "enrolment state") : undefined;
531
+ const enrolTokenSourcePath = enrolmentEnabled ? systemdPath(options.enrolTokenSourcePath, "enrolment source") : undefined;
532
+ const enrolTokenCredentialPath = enrolmentEnabled ? systemdPath(options.enrolTokenCredentialPath, "enrolment credential") : undefined;
533
+ const enrolStatePath = enrolmentEnabled ? systemdPath(options.enrolStatePath, "enrolment state") : undefined;
383
534
  const enrolStateDir = enrolStatePath?.replace(/\/[^/]+$/, "");
384
- const sourceBinPath = options.sourceBinPath ? safePath(options.sourceBinPath, "agent source binary") : undefined;
385
- const binPath = options.binPath ? safePath(options.binPath, "agent binary") : undefined;
386
- const gitCredentialPath = options.gitCredentialPath ? safePath(options.gitCredentialPath, "Git credential") : undefined;
387
- const gitPublicKeyPath = options.gitPublicKeyPath ? safePath(options.gitPublicKeyPath, "Git public key") : undefined;
535
+ const sourceBinPath = options.sourceBinPath ? systemdPath(options.sourceBinPath, "agent source binary") : undefined;
536
+ const binPath = options.binPath ? systemdPath(options.binPath, "agent binary") : undefined;
537
+ const gitCredentialPath = options.gitCredentialPath ? systemdPath(options.gitCredentialPath, "Git credential") : undefined;
538
+ const gitPublicKeyPath = options.gitPublicKeyPath ? systemdPath(options.gitPublicKeyPath, "Git public key") : undefined;
388
539
  if (options.generateGitIdentity && (!gitCredentialPath || !gitPublicKeyPath)) {
389
540
  throw new Error("generated Git identity needs credential and public-key paths");
390
541
  }
391
542
  const gitPublicKeyDir = gitPublicKeyPath?.replace(/\/[^/]+$/, "");
543
+ const lifecycleProfilePath = lifecycleEnabled ? systemdPath(options.lifecycleProfilePath, "lifecycle profile") : undefined;
544
+ const lifecycleHelperSocketPath = lifecycleEnabled ? systemdPath(options.lifecycleHelperSocketPath ?? LIFECYCLE_HELPER_SOCKET, "lifecycle helper socket") : undefined;
545
+ const warpClientIdCredentialPath = warpEnabled ? systemdPath(options.warpClientIdCredentialPath, "WARP client-id credential") : undefined;
546
+ const warpClientSecretCredentialPath = warpEnabled ? systemdPath(options.warpClientSecretCredentialPath, "WARP client-secret credential") : undefined;
392
547
  return {
393
548
  mode,
394
549
  reason: reasonFor(mode),
395
550
  unitPath: UNIT_PATH,
396
- unit: agentUnit({ ...options, mode }),
551
+ unit: agentUnit({ ...options, mode, lifecycleProfilePath, lifecycleHelperSocketPath }),
397
552
  auxiliaryUnits: [
398
553
  ...deploymentEnabled ? [
399
554
  { path: DEPLOYMENT_RUNNER_UNIT_PATH, unit: deploymentRunnerUnit(options) }
400
555
  ] : [],
401
556
  ...enrolmentEnabled ? [
402
557
  { path: ENROLMENT_UNIT_PATH, unit: agentEnrolmentUnit(options) }
558
+ ] : [],
559
+ ...lifecycleEnabled ? [
560
+ { path: LIFECYCLE_HELPER_UNIT_PATH, unit: lifecycleHelperUnit({
561
+ ...options,
562
+ lifecycleProfilePath,
563
+ lifecycleHelperSocketPath
564
+ }) }
565
+ ] : [],
566
+ ...warpEnabled ? [
567
+ { path: WARP_CONFIG_UNIT_PATH, unit: warpConfigUnit({
568
+ ...options,
569
+ warpClientIdCredentialPath,
570
+ warpClientSecretCredentialPath
571
+ }) },
572
+ { path: WARP_SERVICE_DROP_IN_PATH, unit: warpServiceDropIn() }
403
573
  ] : []
404
574
  ],
405
575
  socketPath: options.socketPath,
@@ -413,10 +583,18 @@ function planProvision(options) {
413
583
  label: "root-owned agent runtime",
414
584
  command: `install -d -o root -g root -m 0755 ${binPath.replace(/\/[^/]+$/, "")}; ` + `install -o root -g root -m 0755 ${sourceBinPath} ${binPath}`
415
585
  }] : [],
586
+ ...warpEnabled ? [{
587
+ label: "Cloudflare One client for Ubuntu 26.04",
588
+ command: `. /etc/os-release; test "$ID" = ubuntu && test "$VERSION_ID" = 26.04; ` + `install -d -m 0755 /usr/share/keyrings /etc/apt/sources.list.d /etc/systemd/system/warp-svc.service.d; ` + `curl -fsSL https://pkg.cloudflareclient.com/pubkey.gpg -o /run/cloudflare-warp-key.gpg; ` + `gpg --batch --yes --dearmor -o /usr/share/keyrings/cloudflare-warp-archive-keyring.gpg /run/cloudflare-warp-key.gpg; ` + `rm -f /run/cloudflare-warp-key.gpg; ` + `printf 'deb [signed-by=/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg] https://pkg.cloudflareclient.com/ %s main\\n' "$VERSION_CODENAME" > /etc/apt/sources.list.d/cloudflare-client.list; ` + `apt-get update -qq; DEBIAN_FRONTEND=noninteractive apt-get install -y cloudflare-warp`
589
+ }] : [],
416
590
  ...deploymentEnabled ? [{
417
591
  label: "deployment isolation group",
418
592
  command: `groupadd --system ${DEPLOYMENT_GROUP} || true`
419
593
  }] : [],
594
+ ...lifecycleEnabled ? [{
595
+ label: "lifecycle helper access group",
596
+ command: `groupadd --system ${LIFECYCLE_GROUP} || true`
597
+ }] : [],
420
598
  {
421
599
  label: "service account",
422
600
  command: `useradd --system --no-create-home --shell /usr/sbin/nologin ${user} || true`
@@ -425,6 +603,10 @@ function planProvision(options) {
425
603
  label: "bind service account to vault group",
426
604
  command: `usermod -g ${VAULT_GROUP} ${user}`
427
605
  },
606
+ ...lifecycleEnabled ? [{
607
+ label: "grant lifecycle helper socket access",
608
+ command: `usermod -a -G ${LIFECYCLE_GROUP} ${user}`
609
+ }] : [],
428
610
  ...deploymentEnabled ? [{
429
611
  label: "credential-free deployment account",
430
612
  command: `useradd --system --no-create-home --shell /usr/sbin/nologin --gid ${DEPLOYMENT_GROUP} ${DEPLOYMENT_RUNNER_USER} || true; ` + `usermod -a -G ${DEPLOYMENT_GROUP} ${user}`
@@ -470,6 +652,8 @@ function planProvision(options) {
470
652
  label: "enable and start",
471
653
  command: `systemctl enable --now ${[
472
654
  ...deploymentEnabled ? ["forgezero-deploy-runner.service"] : [],
655
+ ...lifecycleEnabled ? ["forgezero-lifecycle-helper.service"] : [],
656
+ ...warpEnabled ? ["forgezero-warp-config.service", "warp-svc.service"] : [],
473
657
  ...enrolmentEnabled ? ["forgezero-agent-enrol.service"] : [],
474
658
  "forgezero-agent.service"
475
659
  ].join(" ")}`
@@ -484,6 +668,14 @@ function planProvision(options) {
484
668
  label: "prove the deployment runner socket exists",
485
669
  command: `test -S ${DEPLOYMENT_RUNNER_SOCKET}`
486
670
  }] : [],
671
+ ...lifecycleEnabled ? [{
672
+ label: "prove the lifecycle helper socket exists",
673
+ command: `test -S ${lifecycleHelperSocketPath}`
674
+ }] : [],
675
+ ...warpEnabled ? [{
676
+ label: "prove Cloudflare WARP is connected",
677
+ command: `warp-cli --accept-tos status | grep -Eiq '(^|[[:space:]])Connected([[:space:]]|$)'`
678
+ }] : [],
487
679
  ...options.repository ? [{
488
680
  label: "prove the deployment control socket exists",
489
681
  command: `test -S ${options.controlSocketPath ?? "/run/forgezero/control.sock"}`
@@ -774,7 +966,7 @@ async function resolveIdentity(selector, socketPath) {
774
966
  }
775
967
 
776
968
  // src/version.ts
777
- var VERSION = "0.1.22";
969
+ var VERSION = "0.1.24";
778
970
 
779
971
  // src/cli/index.ts
780
972
  var DEFAULT_MODE = THRESHOLD_MODES[0].id;
@@ -997,6 +1189,16 @@ async function cmdAgent(options, args) {
997
1189
  deploymentEnvironment: parseAssignments(process.env.FZ_DEPLOY_ENV),
998
1190
  deploymentCredentials: parseAssignments(process.env.FZ_DEPLOY_CREDENTIALS),
999
1191
  pullDeployments: process.env.FZ_DEPLOY_PULL === "true",
1192
+ pullMigrations: process.env.FZ_MIGRATION_PULL === "true",
1193
+ lifecycleProfilePath: process.env.FZ_LIFECYCLE_PROFILE,
1194
+ lifecycleHelperSocketPath: process.env.FZ_LIFECYCLE_HELPER_SOCKET,
1195
+ warpOrganization: process.env.FZ_WARP_ORGANIZATION,
1196
+ warpClientIdCredentialPath: process.env.FZ_WARP_CLIENT_ID_CREDENTIAL_PATH,
1197
+ warpClientSecretCredentialPath: process.env.FZ_WARP_CLIENT_SECRET_CREDENTIAL_PATH,
1198
+ cloudflareAccountId: process.env.FZ_CF_ACCOUNT_ID,
1199
+ cloudflareTunnelId: process.env.FZ_CF_TUNNEL_ID,
1200
+ cloudflareVirtualNetworkId: process.env.FZ_CF_VIRTUAL_NETWORK_ID,
1201
+ cloudflareWarpPolicyId: process.env.FZ_CF_WARP_POLICY_ID,
1000
1202
  ...options.enrol ? {
1001
1203
  pullDeployments: true,
1002
1204
  enrolTokenSourcePath,
@@ -1033,6 +1235,7 @@ async function cmdAgent(options, args) {
1033
1235
  writeFileSync(plan.unitPath, plan.unit, { mode: 420 });
1034
1236
  out.ok(`Wrote ${plan.unitPath}`);
1035
1237
  for (const auxiliary of plan.auxiliaryUnits) {
1238
+ mkdirSync(dirname(auxiliary.path), { recursive: true, mode: 493 });
1036
1239
  writeFileSync(auxiliary.path, auxiliary.unit, { mode: 420 });
1037
1240
  out.ok(`Wrote ${auxiliary.path}`);
1038
1241
  }
@@ -7,6 +7,14 @@ export interface GuestBinding {
7
7
  realm: 'platform' | 'tenant';
8
8
  tenantSlug?: string;
9
9
  }
10
+ export interface GuestPrivateNetworkAttachment {
11
+ accountId: string;
12
+ tunnelId: string;
13
+ virtualNetworkId?: string;
14
+ warpPolicyId: string;
15
+ }
16
+ /** Fail closed when setup supplied only part of a cross-network attachment. */
17
+ export declare function privateNetworkAttachmentFromEnvironment(env?: Record<string, string | undefined>): GuestPrivateNetworkAttachment | undefined;
10
18
  export interface GuestEnrolmentOptions {
11
19
  apiUrl: string;
12
20
  /** Already-unsealed systemd credential. Production uses this path. */
@@ -21,6 +29,7 @@ export interface GuestEnrolmentOptions {
21
29
  keys: NodeKeyPair;
22
30
  label?: string;
23
31
  gitDeployPublicKey?: string;
32
+ privateNetworkAttachment?: GuestPrivateNetworkAttachment;
24
33
  fetch?: (input: URL, init: RequestInit) => Promise<Response>;
25
34
  requestTimeoutMs?: number;
26
35
  }
@@ -68,6 +68,26 @@ async function postSignedNode(options, path, body) {
68
68
  }
69
69
 
70
70
  // src/guest-enrolment.ts
71
+ function privateNetworkAttachmentFromEnvironment(env = process.env) {
72
+ const values = {
73
+ accountId: env.FZ_CF_ACCOUNT_ID?.trim() ?? "",
74
+ tunnelId: env.FZ_CF_TUNNEL_ID?.trim() ?? "",
75
+ virtualNetworkId: env.FZ_CF_VIRTUAL_NETWORK_ID?.trim() ?? "",
76
+ warpPolicyId: env.FZ_CF_WARP_POLICY_ID?.trim() ?? ""
77
+ };
78
+ const supplied = [values.accountId, values.tunnelId, values.virtualNetworkId, values.warpPolicyId].some(Boolean);
79
+ if (!supplied)
80
+ return;
81
+ if (!values.accountId || !values.tunnelId || !values.warpPolicyId) {
82
+ throw new Error("private-network attachment requires account, Tunnel and WARP policy ids");
83
+ }
84
+ return {
85
+ accountId: values.accountId,
86
+ tunnelId: values.tunnelId,
87
+ ...values.virtualNetworkId ? { virtualNetworkId: values.virtualNetworkId } : {},
88
+ warpPolicyId: values.warpPolicyId
89
+ };
90
+ }
71
91
  var validBinding = (value, expectedNodeKey) => {
72
92
  if (!value || typeof value !== "object")
73
93
  return false;
@@ -114,6 +134,7 @@ async function enrolGuestIdentity(options) {
114
134
  token,
115
135
  label: options.label,
116
136
  gitDeployPublicKey: options.gitDeployPublicKey,
137
+ privateNetworkAttachment: options.privateNetworkAttachment,
117
138
  publicKeys: {
118
139
  ed25519: options.keys.ed25519.publicKey,
119
140
  mlDsa: options.keys.mlDsa.publicKey
@@ -138,6 +159,7 @@ async function enrolGuestIdentity(options) {
138
159
  return binding;
139
160
  }
140
161
  export {
162
+ privateNetworkAttachmentFromEnvironment,
141
163
  loadGuestBinding,
142
164
  enrolGuestIdentity
143
165
  };
package/dist/index.d.ts CHANGED
@@ -14,6 +14,8 @@ export { pullDeploymentOnce, startDeploymentPull } from './deployment-pull';
14
14
  export type { DeploymentPullOptions, RemoteDeploymentClaim, PullResult } from './deployment-pull';
15
15
  export { pullProvisioningOnce, startProvisioningPull } from './provisioning-pull';
16
16
  export type { CreateRemoteProvisionClaim, ProvisioningPullOptions, RemoteProvisionClaim, GuestAccess, ProvisionPullResult, ProvisionRunner, ProvisionResult } from './provisioning-pull';
17
+ export { pullMigrationOnce, startMigrationPull } from './migration-pull';
18
+ export type { MigrationAction, MigrationEvidence, MigrationNetwork, MigrationPullOptions, MigrationPullResult, RemoteMigrationClaim } from './migration-pull';
17
19
  export { enrolGuestIdentity, loadGuestBinding } from './guest-enrolment';
18
20
  export type { GuestBinding, GuestEnrolmentOptions } from './guest-enrolment';
19
21
  export { createNodeVaultCache, projectVaultCacheKey, projectVaultCoordinate, startNodeVaultSync, tenantNodeApiUrl } from './node-vault';
@@ -22,6 +24,9 @@ export { allocateAddress, allocateCpuPool, cloudInit, guestNameFor, provisionMet
22
24
  export { validateMetalProfile } from './metal-provision';
23
25
  export type { GuestManifest, MetalCommandResult, MetalCpuPool, MetalExec, MetalProvisionProfile } from './metal-provision';
24
26
  export { DEFAULT_METAL_HELPER_SOCKET, requestMetalProvision, startMetalHelper } from './metal-helper-socket';
27
+ export { DEFAULT_LIFECYCLE_HELPER_SOCKET, executeLifecycleAction, loadLifecycleProfile, requestLifecycleAction, startLifecycleHelper, validateLifecycleProfile } from './lifecycle-helper';
28
+ export type { LifecycleCommandResult, LifecycleExec, LifecycleProfile } from './lifecycle-helper';
29
+ export { materializeWarpMdm, renderWarpMdm } from './warp-config';
25
30
  export { DEFAULT_DEPLOYMENT_RUNNER_SOCKET, requestDeploymentCommand, startDeploymentRunner } from './deployment-runner';
26
31
  export { createSnpAttestationSource } from './snp-attestation';
27
32
  export type { SnpAttestationOptions } from './snp-attestation';
@@ -0,0 +1,36 @@
1
+ import { type Server } from 'node:net';
2
+ import type { MigrationEvidence, RemoteMigrationClaim } from './migration-pull';
3
+ export declare const DEFAULT_LIFECYCLE_HELPER_SOCKET = "/run/forgezero-lifecycle/helper.sock";
4
+ export interface LifecycleProfile {
5
+ /** Root-owned systemd units whose normal stop path performs application drain. */
6
+ apiUnits: readonly string[];
7
+ /** Root-owned database service stopped only at the final retirement stage. */
8
+ databaseUnit: string;
9
+ /** Loopback health endpoint for the API on this compute. */
10
+ apiHealthUrl: string;
11
+ /** Loopback health endpoint for the database member on this compute. */
12
+ databaseHealthUrl: string;
13
+ /** Ports that must be reachable on every signed controller-provided private peer. */
14
+ databasePorts: readonly number[];
15
+ }
16
+ export interface LifecycleCommandResult {
17
+ exitCode: number;
18
+ stdout: string;
19
+ stderr: string;
20
+ }
21
+ export type LifecycleExec = (argv: readonly string[]) => Promise<LifecycleCommandResult>;
22
+ export declare function validateLifecycleProfile(profile: LifecycleProfile): void;
23
+ export declare function loadLifecycleProfile(path: string): LifecycleProfile;
24
+ export declare const spawnLifecycleCommand: LifecycleExec;
25
+ export declare function executeLifecycleAction(profile: LifecycleProfile, claim: RemoteMigrationClaim, exec?: LifecycleExec, tcpProbe?: (host: string, port: number) => Promise<void>, fetcher?: typeof fetch): Promise<MigrationEvidence>;
26
+ export declare function startLifecycleHelper(options: {
27
+ profile: LifecycleProfile;
28
+ socketPath?: string;
29
+ exec?: LifecycleExec;
30
+ tcpProbe?: (host: string, port: number) => Promise<void>;
31
+ fetch?: typeof fetch;
32
+ }): {
33
+ server: Server;
34
+ stop(): Promise<void>;
35
+ };
36
+ export declare function requestLifecycleAction(claim: RemoteMigrationClaim, socketPath?: string): Promise<MigrationEvidence>;