@forgezero/agent 0.1.22 → 0.1.23

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.
@@ -0,0 +1,185 @@
1
+ // src/signed-node-http.ts
2
+ import {
3
+ encodeSignatureHeader,
4
+ generateResponseRecipient,
5
+ openResponse,
6
+ RESPONSE_KEY_HEADER,
7
+ signRequest
8
+ } from "@forgezero/runtime/identity";
9
+
10
+ class SignedNodeHttpError extends Error {
11
+ status;
12
+ constructor(status, message) {
13
+ super(message);
14
+ this.status = status;
15
+ this.name = "SignedNodeHttpError";
16
+ }
17
+ }
18
+ async function postSignedNode(options, path, body) {
19
+ const url = new URL(options.apiUrl);
20
+ url.pathname = `${url.pathname.replace(/\/$/, "")}/${path.replace(/^\//, "")}`.replace(/\/+/g, "/");
21
+ url.search = "";
22
+ url.hash = "";
23
+ const raw = JSON.stringify(body);
24
+ const recipient = generateResponseRecipient();
25
+ const envelope = signRequest(options.keys, options.nodeKey, {
26
+ method: "POST",
27
+ path: url.pathname,
28
+ query: "",
29
+ body: raw,
30
+ responseKey: recipient.publicKey
31
+ });
32
+ const signature = encodeSignatureHeader(envelope);
33
+ const response = await (options.fetch ?? globalThis.fetch)(url, {
34
+ method: "POST",
35
+ headers: {
36
+ "content-type": "application/json",
37
+ "x-fz-node": options.nodeKey,
38
+ "x-fz-signature": signature,
39
+ [RESPONSE_KEY_HEADER]: recipient.publicKey
40
+ },
41
+ body: raw,
42
+ signal: AbortSignal.timeout(options.requestTimeoutMs ?? 15000)
43
+ });
44
+ const payload = await response.json().catch(() => null);
45
+ if (!response.ok) {
46
+ const failure = payload;
47
+ const reason = failure ? failure.error?.message ?? failure.message : undefined;
48
+ throw new SignedNodeHttpError(response.status, reason || `signed node request returned HTTP ${response.status}`);
49
+ }
50
+ try {
51
+ return await openResponse(recipient.secretKey, signature, payload);
52
+ } catch {
53
+ throw new SignedNodeHttpError(502, "The node response was not sealed to this request.");
54
+ }
55
+ }
56
+
57
+ // src/migration-pull.ts
58
+ class MigrationClaimLostError extends Error {
59
+ constructor(message) {
60
+ super(message);
61
+ this.name = "MigrationClaimLostError";
62
+ }
63
+ }
64
+ var post = (options, operation, body) => postSignedNode(options, `v1/node/migrations/${operation}`, body);
65
+ async function complete(options, body) {
66
+ const attempts = Math.max(1, Math.min(options.completionAttempts ?? 5, 10));
67
+ const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
68
+ let last;
69
+ for (let attempt = 1;attempt <= attempts; attempt += 1) {
70
+ try {
71
+ await post(options, "complete", body);
72
+ return;
73
+ } catch (cause) {
74
+ last = cause;
75
+ if (cause instanceof SignedNodeHttpError && cause.status < 500)
76
+ throw cause;
77
+ if (attempt < attempts)
78
+ await sleep(Math.min(2000, 250 * 2 ** (attempt - 1)));
79
+ }
80
+ }
81
+ throw last;
82
+ }
83
+ async function pullMigrationOnce(options) {
84
+ const response = await post(options, "claim", {});
85
+ if (!response.claim)
86
+ return { status: "idle" };
87
+ const claim = response.claim;
88
+ const setTimer = options.setTimer ?? ((callback, ms) => setTimeout(callback, ms));
89
+ const clearTimer = options.clearTimer ?? ((handle) => clearTimeout(handle));
90
+ const now = options.now ?? Date.now;
91
+ let expires = claim.claimExpiresAtTs;
92
+ let stopped = false;
93
+ let timer;
94
+ let renewal = null;
95
+ let lost = null;
96
+ const schedule = (override) => {
97
+ if (stopped)
98
+ return;
99
+ const delay = override ?? Math.max(1000, Math.min(5 * 60000, Math.floor(Math.max(0, expires - now()) / 3)));
100
+ timer = setTimer(() => {
101
+ if (stopped || renewal)
102
+ return;
103
+ let retry;
104
+ renewal = post(options, "renew", {
105
+ migrationKey: claim.migrationKey,
106
+ claimToken: claim.claimToken
107
+ }).then((value) => {
108
+ expires = value.claimExpiresAtTs;
109
+ }).catch((cause) => {
110
+ if (cause instanceof SignedNodeHttpError && cause.status < 500) {
111
+ lost = new MigrationClaimLostError(cause.message);
112
+ stopped = true;
113
+ } else
114
+ retry = Math.max(1000, options.renewRetryMs ?? 5000);
115
+ }).finally(() => {
116
+ renewal = null;
117
+ if (!stopped)
118
+ schedule(retry);
119
+ });
120
+ }, delay);
121
+ };
122
+ schedule();
123
+ let evidence;
124
+ try {
125
+ evidence = await options.run(claim);
126
+ } catch (cause) {
127
+ stopped = true;
128
+ clearTimer(timer);
129
+ await renewal;
130
+ if (lost)
131
+ throw lost;
132
+ const reason = (cause instanceof Error ? cause.message : String(cause)).slice(0, 2000);
133
+ await complete(options, {
134
+ migrationKey: claim.migrationKey,
135
+ claimToken: claim.claimToken,
136
+ ok: false,
137
+ detail: reason
138
+ });
139
+ return { status: "failed", claim, reason };
140
+ }
141
+ stopped = true;
142
+ clearTimer(timer);
143
+ await renewal;
144
+ if (lost)
145
+ throw lost;
146
+ await complete(options, {
147
+ migrationKey: claim.migrationKey,
148
+ claimToken: claim.claimToken,
149
+ ok: true,
150
+ evidence
151
+ });
152
+ return { status: "completed", claim, evidence };
153
+ }
154
+ function startMigrationPull(options) {
155
+ const interval = Math.max(1000, options.intervalMs ?? 5000);
156
+ const setTimer = options.setTimer ?? ((callback, ms) => setTimeout(callback, ms));
157
+ const clearTimer = options.clearTimer ?? ((handle) => clearTimeout(handle));
158
+ let stopped = false;
159
+ let timer;
160
+ let active = null;
161
+ const tick = () => {
162
+ if (stopped || active)
163
+ return;
164
+ active = pullMigrationOnce(options).then((result) => options.onEvent?.(result.status, result)).catch((cause) => options.onEvent?.("poll-failed", cause)).finally(() => {
165
+ active = null;
166
+ if (!stopped)
167
+ timer = setTimer(tick, interval);
168
+ });
169
+ };
170
+ tick();
171
+ return {
172
+ async stop() {
173
+ stopped = true;
174
+ clearTimer(timer);
175
+ await active;
176
+ },
177
+ get active() {
178
+ return !stopped;
179
+ }
180
+ };
181
+ }
182
+ export {
183
+ startMigrationPull,
184
+ pullMigrationOnce
185
+ };
@@ -92,6 +92,15 @@ export interface UnitOptions {
92
92
  deploymentCredentials?: Record<string, string>;
93
93
  /** Enable authenticated outbound deployment claims after enrolment. */
94
94
  pullDeployments?: boolean;
95
+ /** Enable PQ-authenticated lifecycle claims through a constrained root helper. */
96
+ pullMigrations?: boolean;
97
+ /** Root-owned declarative lifecycle profile; never supplied by a migration claim. */
98
+ lifecycleProfilePath?: string;
99
+ lifecycleHelperSocketPath?: string;
100
+ /** Optional cross-network overlay. All three values are required together. */
101
+ warpOrganization?: string;
102
+ warpClientIdCredentialPath?: string;
103
+ warpClientSecretCredentialPath?: string;
95
104
  /** Where `fz-agent` ended up. `bun add -g` puts it on PATH. */
96
105
  binPath?: string;
97
106
  /** Package-owned binary copied to binPath before hardened units start. */
@@ -109,9 +118,17 @@ export interface UnitOptions {
109
118
  export declare const DEPLOYMENT_RUNNER_USER = "forgezero-runner";
110
119
  export declare const DEPLOYMENT_GROUP = "forgezero-deploy";
111
120
  export declare const VAULT_GROUP = "forgezero-vault";
121
+ export declare const LIFECYCLE_GROUP = "forgezero-lifecycle";
112
122
  export declare const DEPLOYMENT_RUNNER_UNIT_PATH = "/etc/systemd/system/forgezero-deploy-runner.service";
113
123
  export declare const DEPLOYMENT_RUNNER_SOCKET = "/run/forgezero-deploy/runner.sock";
114
124
  export declare const ENROLMENT_UNIT_PATH = "/etc/systemd/system/forgezero-agent-enrol.service";
125
+ export declare const LIFECYCLE_HELPER_UNIT_PATH = "/etc/systemd/system/forgezero-lifecycle-helper.service";
126
+ export declare const LIFECYCLE_HELPER_SOCKET = "/run/forgezero-lifecycle/helper.sock";
127
+ export declare const WARP_CONFIG_UNIT_PATH = "/etc/systemd/system/forgezero-warp-config.service";
128
+ export declare const WARP_SERVICE_DROP_IN_PATH = "/etc/systemd/system/warp-svc.service.d/forgezero.conf";
129
+ export declare function warpConfigUnit(options: Pick<UnitOptions, 'binPath' | 'warpOrganization' | 'warpClientIdCredentialPath' | 'warpClientSecretCredentialPath'>): string;
130
+ export declare function warpServiceDropIn(): string;
131
+ export declare function lifecycleHelperUnit(options: Pick<UnitOptions, 'binPath' | 'lifecycleProfilePath' | 'lifecycleHelperSocketPath'>): string;
115
132
  /**
116
133
  * Exchange a tenant-issued one-time capability before the durable agent starts.
117
134
  *
package/dist/provision.js CHANGED
@@ -42,9 +42,104 @@ var reasonFor = (mode) => mode === "attested" ? "SEV-SNP guest device present, s
42
42
  var DEPLOYMENT_RUNNER_USER = "forgezero-runner";
43
43
  var DEPLOYMENT_GROUP = "forgezero-deploy";
44
44
  var VAULT_GROUP = "forgezero-vault";
45
+ var LIFECYCLE_GROUP = "forgezero-lifecycle";
45
46
  var DEPLOYMENT_RUNNER_UNIT_PATH = "/etc/systemd/system/forgezero-deploy-runner.service";
46
47
  var DEPLOYMENT_RUNNER_SOCKET = "/run/forgezero-deploy/runner.sock";
47
48
  var ENROLMENT_UNIT_PATH = "/etc/systemd/system/forgezero-agent-enrol.service";
49
+ var LIFECYCLE_HELPER_UNIT_PATH = "/etc/systemd/system/forgezero-lifecycle-helper.service";
50
+ var LIFECYCLE_HELPER_SOCKET = "/run/forgezero-lifecycle/helper.sock";
51
+ var WARP_CONFIG_UNIT_PATH = "/etc/systemd/system/forgezero-warp-config.service";
52
+ var WARP_SERVICE_DROP_IN_PATH = "/etc/systemd/system/warp-svc.service.d/forgezero.conf";
53
+ var systemdPath = (value, label) => {
54
+ if (!value || !/^\/[A-Za-z0-9._@/-]+$/.test(value))
55
+ throw new Error(`invalid ${label} path`);
56
+ return value;
57
+ };
58
+ function warpConfigUnit(options) {
59
+ if (!options.warpOrganization || !/^[a-z0-9][a-z0-9-]{0,62}$/i.test(options.warpOrganization)) {
60
+ throw new Error("WARP organization is invalid");
61
+ }
62
+ if (!options.warpClientIdCredentialPath || !options.warpClientSecretCredentialPath) {
63
+ throw new Error("WARP service-token credential paths are required");
64
+ }
65
+ const clientIdPath = systemdPath(options.warpClientIdCredentialPath, "WARP client-id credential");
66
+ const clientSecretPath = systemdPath(options.warpClientSecretCredentialPath, "WARP client-secret credential");
67
+ const bin = options.binPath ?? "fz-agent";
68
+ return `[Unit]
69
+ Description=Materialize Cloudflare One enrollment in tmpfs
70
+ Documentation=https://www.forgezero.net/docs/agent
71
+ Before=warp-svc.service
72
+
73
+ [Service]
74
+ Type=oneshot
75
+ RemainAfterExit=yes
76
+ LoadCredentialEncrypted=warp-auth-client-id:${clientIdPath}
77
+ LoadCredentialEncrypted=warp-auth-client-secret:${clientSecretPath}
78
+ Environment=FZ_WARP_CLIENT_ID_CREDENTIAL=warp-auth-client-id
79
+ Environment=FZ_WARP_CLIENT_SECRET_CREDENTIAL=warp-auth-client-secret
80
+ ExecStart=${bin} warp-config --organization=${options.warpOrganization}
81
+ RuntimeDirectory=forgezero-warp
82
+ RuntimeDirectoryMode=0700
83
+ RuntimeDirectoryPreserve=yes
84
+ UMask=0077
85
+ LimitCORE=0
86
+ NoNewPrivileges=true
87
+ PrivateTmp=true
88
+ ProtectSystem=strict
89
+ ProtectHome=true
90
+ ReadWritePaths=/var/lib/cloudflare-warp
91
+
92
+ [Install]
93
+ WantedBy=multi-user.target
94
+ `;
95
+ }
96
+ function warpServiceDropIn() {
97
+ return `[Unit]
98
+ Requires=forgezero-warp-config.service
99
+ After=forgezero-warp-config.service
100
+ `;
101
+ }
102
+ function lifecycleHelperUnit(options) {
103
+ if (!options.lifecycleProfilePath)
104
+ throw new Error("lifecycle helper needs a root-owned profile");
105
+ const bin = options.binPath ?? "fz-agent";
106
+ const profile = systemdPath(options.lifecycleProfilePath, "lifecycle profile");
107
+ const socket = systemdPath(options.lifecycleHelperSocketPath ?? LIFECYCLE_HELPER_SOCKET, "lifecycle helper socket");
108
+ return `[Unit]
109
+ Description=ForgeZero fixed-operation compute lifecycle helper
110
+ Documentation=https://www.forgezero.net/docs/agent
111
+ After=network-online.target
112
+ Wants=network-online.target
113
+
114
+ [Service]
115
+ Type=simple
116
+ User=root
117
+ Group=${LIFECYCLE_GROUP}
118
+ Environment=FZ_LIFECYCLE_HELPER_SOCKET=${socket}
119
+ ExecStart=${bin} lifecycle-helper --profile=${profile}
120
+ Restart=always
121
+ RestartSec=2
122
+ RuntimeDirectory=forgezero-lifecycle
123
+ RuntimeDirectoryMode=0750
124
+ UMask=0007
125
+ LimitCORE=0
126
+ NoNewPrivileges=true
127
+ PrivateTmp=true
128
+ ProtectSystem=strict
129
+ ProtectHome=true
130
+ ProtectKernelTunables=true
131
+ ProtectKernelModules=true
132
+ ProtectControlGroups=true
133
+ RestrictSUIDSGID=true
134
+ RestrictRealtime=true
135
+ MemoryDenyWriteExecute=true
136
+ LockPersonality=true
137
+ RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
138
+
139
+ [Install]
140
+ WantedBy=multi-user.target
141
+ `;
142
+ }
48
143
  function agentEnrolmentUnit(options) {
49
144
  if (!options.apiUrl || !options.enrolTokenCredentialPath || !options.enrolStatePath) {
50
145
  throw new Error("direct enrolment needs API, credential and state paths");
@@ -139,6 +234,19 @@ function agentUnit(options) {
139
234
  const controlSocketPath = options.controlSocketPath ?? "/run/forgezero/control.sock";
140
235
  const deployRoot = options.deployRoot ?? "/opt/forgezero";
141
236
  const deploymentEnabled = Boolean(options.repository || options.pullDeployments);
237
+ const lifecycleEnabled = Boolean(options.pullMigrations && options.lifecycleProfilePath);
238
+ if (Boolean(options.pullMigrations) !== Boolean(options.lifecycleProfilePath)) {
239
+ throw new Error("migration pull and lifecycle profile must be supplied together");
240
+ }
241
+ const lifecycleHelperSocketPath = lifecycleEnabled ? systemdPath(options.lifecycleHelperSocketPath ?? LIFECYCLE_HELPER_SOCKET, "lifecycle helper socket") : undefined;
242
+ const warpValues = [
243
+ options.warpOrganization,
244
+ options.warpClientIdCredentialPath,
245
+ options.warpClientSecretCredentialPath
246
+ ];
247
+ const warpEnabled = warpValues.every(Boolean);
248
+ if (warpValues.some(Boolean) && !warpEnabled)
249
+ throw new Error("WARP configuration must be supplied together");
142
250
  const enrolmentEnabled = Boolean(options.enrolTokenCredentialPath && options.enrolStatePath);
143
251
  const deploymentEnvironment = options.deploymentEnvironment ?? {};
144
252
  const deploymentCredentials = options.deploymentCredentials ?? {};
@@ -173,7 +281,9 @@ function agentUnit(options) {
173
281
  Object.keys(deploymentEnvironment).length > 0 ? `FZ_DEPLOY_ENV_NAMES=${Object.keys(deploymentEnvironment).join(",")}` : null,
174
282
  ...Object.entries(deploymentEnvironment).map(([name, value]) => `${name}=${value}`),
175
283
  options.publicApiUrl ? `FZ_PUBLIC_API_URL=${options.publicApiUrl}` : null,
176
- options.pullDeployments ? "FZ_DEPLOY_PULL=true" : null
284
+ options.pullDeployments ? "FZ_DEPLOY_PULL=true" : null,
285
+ options.pullMigrations ? "FZ_MIGRATION_PULL=true" : null,
286
+ options.pullMigrations ? `FZ_LIFECYCLE_HELPER_SOCKET=${lifecycleHelperSocketPath}` : null
177
287
  ].filter((line) => line !== null);
178
288
  if (deploymentEnabled) {
179
289
  environment.push(`HOME=${deployRoot}/agent-home`, `XDG_CACHE_HOME=${deployRoot}/cache`);
@@ -183,14 +293,22 @@ function agentUnit(options) {
183
293
  const projectCredentials = Object.entries(deploymentCredentials).map(([name, path]) => `LoadCredentialEncrypted=${name}:${path}`).join(`
184
294
  `);
185
295
  const deploymentWrites = deploymentEnabled ? `ReadWritePaths=${deployRoot}/releases ${deployRoot}/agent-home ${deployRoot}/cache` : "";
186
- const deploymentGroup = deploymentEnabled ? `SupplementaryGroups=${DEPLOYMENT_GROUP}` : "";
296
+ const supplementaryGroups = [
297
+ deploymentEnabled ? DEPLOYMENT_GROUP : null,
298
+ lifecycleEnabled ? LIFECYCLE_GROUP : null
299
+ ].filter((value) => value !== null);
300
+ const deploymentGroup = supplementaryGroups.length > 0 ? `SupplementaryGroups=${supplementaryGroups.join(" ")}` : "";
187
301
  const after = [
188
302
  "network-online.target",
189
303
  deploymentEnabled ? "forgezero-deploy-runner.service" : null,
304
+ lifecycleEnabled ? "forgezero-lifecycle-helper.service" : null,
305
+ warpEnabled ? "warp-svc.service" : null,
190
306
  enrolmentEnabled ? "forgezero-agent-enrol.service" : null
191
307
  ].filter((value) => value !== null);
192
308
  const requires = [
193
309
  deploymentEnabled ? "forgezero-deploy-runner.service" : null,
310
+ lifecycleEnabled ? "forgezero-lifecycle-helper.service" : null,
311
+ warpEnabled ? "warp-svc.service" : null,
194
312
  enrolmentEnabled ? "forgezero-agent-enrol.service" : null
195
313
  ].filter((value) => value !== null);
196
314
  const deploymentDependency = [
@@ -262,37 +380,63 @@ function planProvision(options) {
262
380
  const credentialDir = seedCredentialPath.replace(/\/[^/]+$/, "");
263
381
  const deployRoot = options.deployRoot ?? "/opt/forgezero";
264
382
  const deploymentEnabled = Boolean(options.repository || options.pullDeployments);
383
+ const lifecycleEnabled = Boolean(options.pullMigrations && options.lifecycleProfilePath);
384
+ if (Boolean(options.pullMigrations) !== Boolean(options.lifecycleProfilePath)) {
385
+ throw new Error("migration pull and lifecycle profile must be supplied together");
386
+ }
387
+ const warpValues = [
388
+ options.warpOrganization,
389
+ options.warpClientIdCredentialPath,
390
+ options.warpClientSecretCredentialPath
391
+ ];
392
+ const warpEnabled = warpValues.every(Boolean);
393
+ if (warpValues.some(Boolean) && !warpEnabled)
394
+ throw new Error("WARP configuration must be supplied together");
265
395
  const enrolmentEnabled = Boolean(options.enrolTokenSourcePath && options.enrolTokenCredentialPath && options.enrolStatePath);
266
396
  if (Boolean(options.enrolTokenSourcePath) !== Boolean(options.enrolTokenCredentialPath) || Boolean(options.enrolTokenCredentialPath) !== Boolean(options.enrolStatePath))
267
397
  throw new Error("direct enrolment paths must be supplied together");
268
- const safePath = (value, label) => {
269
- if (!value || !/^\/[A-Za-z0-9._@/-]+$/.test(value))
270
- throw new Error(`invalid ${label} path`);
271
- return value;
272
- };
273
- const enrolTokenSourcePath = enrolmentEnabled ? safePath(options.enrolTokenSourcePath, "enrolment source") : undefined;
274
- const enrolTokenCredentialPath = enrolmentEnabled ? safePath(options.enrolTokenCredentialPath, "enrolment credential") : undefined;
275
- const enrolStatePath = enrolmentEnabled ? safePath(options.enrolStatePath, "enrolment state") : undefined;
398
+ const enrolTokenSourcePath = enrolmentEnabled ? systemdPath(options.enrolTokenSourcePath, "enrolment source") : undefined;
399
+ const enrolTokenCredentialPath = enrolmentEnabled ? systemdPath(options.enrolTokenCredentialPath, "enrolment credential") : undefined;
400
+ const enrolStatePath = enrolmentEnabled ? systemdPath(options.enrolStatePath, "enrolment state") : undefined;
276
401
  const enrolStateDir = enrolStatePath?.replace(/\/[^/]+$/, "");
277
- const sourceBinPath = options.sourceBinPath ? safePath(options.sourceBinPath, "agent source binary") : undefined;
278
- const binPath = options.binPath ? safePath(options.binPath, "agent binary") : undefined;
279
- const gitCredentialPath = options.gitCredentialPath ? safePath(options.gitCredentialPath, "Git credential") : undefined;
280
- const gitPublicKeyPath = options.gitPublicKeyPath ? safePath(options.gitPublicKeyPath, "Git public key") : undefined;
402
+ const sourceBinPath = options.sourceBinPath ? systemdPath(options.sourceBinPath, "agent source binary") : undefined;
403
+ const binPath = options.binPath ? systemdPath(options.binPath, "agent binary") : undefined;
404
+ const gitCredentialPath = options.gitCredentialPath ? systemdPath(options.gitCredentialPath, "Git credential") : undefined;
405
+ const gitPublicKeyPath = options.gitPublicKeyPath ? systemdPath(options.gitPublicKeyPath, "Git public key") : undefined;
281
406
  if (options.generateGitIdentity && (!gitCredentialPath || !gitPublicKeyPath)) {
282
407
  throw new Error("generated Git identity needs credential and public-key paths");
283
408
  }
284
409
  const gitPublicKeyDir = gitPublicKeyPath?.replace(/\/[^/]+$/, "");
410
+ const lifecycleProfilePath = lifecycleEnabled ? systemdPath(options.lifecycleProfilePath, "lifecycle profile") : undefined;
411
+ const lifecycleHelperSocketPath = lifecycleEnabled ? systemdPath(options.lifecycleHelperSocketPath ?? LIFECYCLE_HELPER_SOCKET, "lifecycle helper socket") : undefined;
412
+ const warpClientIdCredentialPath = warpEnabled ? systemdPath(options.warpClientIdCredentialPath, "WARP client-id credential") : undefined;
413
+ const warpClientSecretCredentialPath = warpEnabled ? systemdPath(options.warpClientSecretCredentialPath, "WARP client-secret credential") : undefined;
285
414
  return {
286
415
  mode,
287
416
  reason: reasonFor(mode),
288
417
  unitPath: UNIT_PATH,
289
- unit: agentUnit({ ...options, mode }),
418
+ unit: agentUnit({ ...options, mode, lifecycleProfilePath, lifecycleHelperSocketPath }),
290
419
  auxiliaryUnits: [
291
420
  ...deploymentEnabled ? [
292
421
  { path: DEPLOYMENT_RUNNER_UNIT_PATH, unit: deploymentRunnerUnit(options) }
293
422
  ] : [],
294
423
  ...enrolmentEnabled ? [
295
424
  { path: ENROLMENT_UNIT_PATH, unit: agentEnrolmentUnit(options) }
425
+ ] : [],
426
+ ...lifecycleEnabled ? [
427
+ { path: LIFECYCLE_HELPER_UNIT_PATH, unit: lifecycleHelperUnit({
428
+ ...options,
429
+ lifecycleProfilePath,
430
+ lifecycleHelperSocketPath
431
+ }) }
432
+ ] : [],
433
+ ...warpEnabled ? [
434
+ { path: WARP_CONFIG_UNIT_PATH, unit: warpConfigUnit({
435
+ ...options,
436
+ warpClientIdCredentialPath,
437
+ warpClientSecretCredentialPath
438
+ }) },
439
+ { path: WARP_SERVICE_DROP_IN_PATH, unit: warpServiceDropIn() }
296
440
  ] : []
297
441
  ],
298
442
  socketPath: options.socketPath,
@@ -306,10 +450,18 @@ function planProvision(options) {
306
450
  label: "root-owned agent runtime",
307
451
  command: `install -d -o root -g root -m 0755 ${binPath.replace(/\/[^/]+$/, "")}; ` + `install -o root -g root -m 0755 ${sourceBinPath} ${binPath}`
308
452
  }] : [],
453
+ ...warpEnabled ? [{
454
+ label: "Cloudflare One client for Ubuntu 26.04",
455
+ 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`
456
+ }] : [],
309
457
  ...deploymentEnabled ? [{
310
458
  label: "deployment isolation group",
311
459
  command: `groupadd --system ${DEPLOYMENT_GROUP} || true`
312
460
  }] : [],
461
+ ...lifecycleEnabled ? [{
462
+ label: "lifecycle helper access group",
463
+ command: `groupadd --system ${LIFECYCLE_GROUP} || true`
464
+ }] : [],
313
465
  {
314
466
  label: "service account",
315
467
  command: `useradd --system --no-create-home --shell /usr/sbin/nologin ${user} || true`
@@ -318,6 +470,10 @@ function planProvision(options) {
318
470
  label: "bind service account to vault group",
319
471
  command: `usermod -g ${VAULT_GROUP} ${user}`
320
472
  },
473
+ ...lifecycleEnabled ? [{
474
+ label: "grant lifecycle helper socket access",
475
+ command: `usermod -a -G ${LIFECYCLE_GROUP} ${user}`
476
+ }] : [],
321
477
  ...deploymentEnabled ? [{
322
478
  label: "credential-free deployment account",
323
479
  command: `useradd --system --no-create-home --shell /usr/sbin/nologin --gid ${DEPLOYMENT_GROUP} ${DEPLOYMENT_RUNNER_USER} || true; ` + `usermod -a -G ${DEPLOYMENT_GROUP} ${user}`
@@ -363,6 +519,8 @@ function planProvision(options) {
363
519
  label: "enable and start",
364
520
  command: `systemctl enable --now ${[
365
521
  ...deploymentEnabled ? ["forgezero-deploy-runner.service"] : [],
522
+ ...lifecycleEnabled ? ["forgezero-lifecycle-helper.service"] : [],
523
+ ...warpEnabled ? ["forgezero-warp-config.service", "warp-svc.service"] : [],
366
524
  ...enrolmentEnabled ? ["forgezero-agent-enrol.service"] : [],
367
525
  "forgezero-agent.service"
368
526
  ].join(" ")}`
@@ -377,6 +535,14 @@ function planProvision(options) {
377
535
  label: "prove the deployment runner socket exists",
378
536
  command: `test -S ${DEPLOYMENT_RUNNER_SOCKET}`
379
537
  }] : [],
538
+ ...lifecycleEnabled ? [{
539
+ label: "prove the lifecycle helper socket exists",
540
+ command: `test -S ${lifecycleHelperSocketPath}`
541
+ }] : [],
542
+ ...warpEnabled ? [{
543
+ label: "prove Cloudflare WARP is connected",
544
+ command: `warp-cli --accept-tos status | grep -Eiq '(^|[[:space:]])Connected([[:space:]]|$)'`
545
+ }] : [],
380
546
  ...options.repository ? [{
381
547
  label: "prove the deployment control socket exists",
382
548
  command: `test -S ${options.controlSocketPath ?? "/run/forgezero/control.sock"}`
@@ -385,15 +551,23 @@ function planProvision(options) {
385
551
  };
386
552
  }
387
553
  export {
554
+ warpServiceDropIn,
555
+ warpConfigUnit,
388
556
  reasonFor,
389
557
  planProvision,
390
558
  modeFor,
559
+ lifecycleHelperUnit,
391
560
  deploymentRunnerUnit,
392
561
  atLeast,
393
562
  agentUnit,
394
563
  agentEnrolmentUnit,
564
+ WARP_SERVICE_DROP_IN_PATH,
565
+ WARP_CONFIG_UNIT_PATH,
395
566
  VAULT_GROUP,
396
567
  UNIT_PATH,
568
+ LIFECYCLE_HELPER_UNIT_PATH,
569
+ LIFECYCLE_HELPER_SOCKET,
570
+ LIFECYCLE_GROUP,
397
571
  ENROLMENT_UNIT_PATH,
398
572
  DEPLOYMENT_RUNNER_USER,
399
573
  DEPLOYMENT_RUNNER_UNIT_PATH,
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  /** One package version shared by both public binaries. Pinned to package.json by tests. */
2
- export declare const VERSION = "0.1.22";
2
+ export declare const VERSION = "0.1.23";
@@ -0,0 +1,20 @@
1
+ /** Cloudflare One MDM document; callers keep both token values out of argv/env. */
2
+ export declare function renderWarpMdm(options: {
3
+ organization: string;
4
+ clientId: string;
5
+ clientSecret: string;
6
+ }): string;
7
+ /**
8
+ * Materialize enrollment only in /run. The durable /var/lib path is a symlink,
9
+ * so neither service-token half is left as plaintext on the filesystem.
10
+ */
11
+ export declare function materializeWarpMdm(options: {
12
+ organization: string;
13
+ clientId: string;
14
+ clientSecret: string;
15
+ runtimePath?: string;
16
+ servicePath?: string;
17
+ }): {
18
+ runtimePath: string;
19
+ servicePath: string;
20
+ };
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "//": "Publishing happens from an operator's machine, not CI \u2014 CLAUDE.md records that the absence of CI is deliberate. npm's `provenance` attests a tarball was built by a recognised CI provider from a named commit, so it cannot be produced here: it was set, and the first publish failed with `Automatic provenance generation not supported for provider: null`. A setting that can never be satisfied is worse than none, because it reads as a guarantee nobody is getting. Restore it the day this publishes from CI, and not before.",
3
3
  "name": "@forgezero/agent",
4
- "version": "0.1.22",
4
+ "version": "0.1.23",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "check": "tsc --noEmit",
8
8
  "prebuild": "rm -rf dist",
9
- "build": "bun build src/index.ts --outfile dist/fz-agent.js --target bun --format esm --packages external && bun build src/cli/index.ts --outfile dist/fz.js --target bun --format esm --packages external && bun build src/compute.ts src/provision.ts src/subscribe.ts src/pipeline.ts src/definition.ts src/ssh-server.ts src/ssh-listen.ts src/provisioning-pull.ts src/guest-enrolment.ts src/node-vault.ts src/metal-provision.ts src/metal-helper-socket.ts src/deployment-runner.ts src/ubuntu.ts --root src --outdir dist --target browser --format esm --packages external && tsc --emitDeclarationOnly --declaration --noEmit false --outDir dist",
9
+ "build": "bun build src/index.ts --outfile dist/fz-agent.js --target bun --format esm --packages external && bun build src/cli/index.ts --outfile dist/fz.js --target bun --format esm --packages external && bun build src/compute.ts src/provision.ts src/subscribe.ts src/pipeline.ts src/definition.ts src/ssh-server.ts src/ssh-listen.ts src/provisioning-pull.ts src/migration-pull.ts src/guest-enrolment.ts src/node-vault.ts src/metal-provision.ts src/metal-helper-socket.ts src/lifecycle-helper.ts src/deployment-runner.ts src/ubuntu.ts --root src --outdir dist --target browser --format esm --packages external && tsc --emitDeclarationOnly --declaration --noEmit false --outDir dist",
10
10
  "prepublishOnly": "bun run check && bun run build"
11
11
  },
12
12
  "devDependencies": {
@@ -88,6 +88,14 @@
88
88
  "types": "./dist/provisioning-pull.d.ts",
89
89
  "default": "./dist/provisioning-pull.js"
90
90
  },
91
+ "./migration-pull": {
92
+ "types": "./dist/migration-pull.d.ts",
93
+ "default": "./dist/migration-pull.js"
94
+ },
95
+ "./lifecycle-helper": {
96
+ "types": "./dist/lifecycle-helper.d.ts",
97
+ "default": "./dist/lifecycle-helper.js"
98
+ },
91
99
  "./metal-provision": {
92
100
  "types": "./dist/metal-provision.d.ts",
93
101
  "default": "./dist/metal-provision.js"