@forgezero/agent 0.1.21 → 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.
- package/README.md +24 -0
- package/dist/cli/agent-install.d.ts +6 -0
- package/dist/definition.d.ts +0 -1
- package/dist/definition.js +1 -6
- package/dist/deployment.d.ts +1 -0
- package/dist/fz-agent.js +472 -224
- package/dist/fz.js +191 -17
- package/dist/index.d.ts +5 -0
- package/dist/lifecycle-helper.d.ts +36 -0
- package/dist/lifecycle-helper.js +216 -0
- package/dist/metal-helper-socket.js +8 -532
- package/dist/metal-provision.d.ts +1 -6
- package/dist/metal-provision.js +8 -536
- package/dist/migration-pull.d.ts +55 -0
- package/dist/migration-pull.js +185 -0
- package/dist/provision.d.ts +17 -0
- package/dist/provision.js +189 -15
- package/dist/version.d.ts +1 -1
- package/dist/warp-config.d.ts +20 -0
- package/package.json +11 -3
package/dist/metal-provision.js
CHANGED
|
@@ -138,393 +138,6 @@ function shapeEgressUnitDirectives(tap, guaranteedMbps, burstMbps) {
|
|
|
138
138
|
}
|
|
139
139
|
var tapFor = (guestIndex) => `tap${guestIndex}`;
|
|
140
140
|
|
|
141
|
-
// src/provision.ts
|
|
142
|
-
function atLeast(version, floor) {
|
|
143
|
-
const parse = (value) => (value.trim().replace(/^v/, "").match(/\d+/g) ?? []).slice(0, 3).map(Number);
|
|
144
|
-
const got = parse(version);
|
|
145
|
-
const want = parse(floor);
|
|
146
|
-
if (got.length === 0)
|
|
147
|
-
return false;
|
|
148
|
-
for (let index = 0;index < want.length; index += 1) {
|
|
149
|
-
const a = got[index] ?? 0;
|
|
150
|
-
const b = want[index] ?? 0;
|
|
151
|
-
if (a > b)
|
|
152
|
-
return true;
|
|
153
|
-
if (a < b)
|
|
154
|
-
return false;
|
|
155
|
-
}
|
|
156
|
-
return true;
|
|
157
|
-
}
|
|
158
|
-
var CAPABILITY_CHECKS = {
|
|
159
|
-
snpGuest: {
|
|
160
|
-
command: "test -e /dev/sev-guest && echo yes || echo no",
|
|
161
|
-
satisfied: (stdout) => stdout.trim() === "yes",
|
|
162
|
-
remedy: "Not a confidential guest. The agent will run in `enrolled` mode, which is still stronger than an API key in the application."
|
|
163
|
-
},
|
|
164
|
-
systemd: {
|
|
165
|
-
command: "test -d /run/systemd/system && echo yes || echo no",
|
|
166
|
-
satisfied: (stdout) => stdout.trim() === "yes",
|
|
167
|
-
remedy: "systemd is what supervises the agent. On a non-systemd host, run `fz-agent` under whatever supervises services there."
|
|
168
|
-
},
|
|
169
|
-
bun: {
|
|
170
|
-
command: "bun --version 2>/dev/null || echo missing",
|
|
171
|
-
satisfied: (stdout) => atLeast(stdout, "1.1.0"),
|
|
172
|
-
remedy: "Install bun: curl -fsSL https://bun.sh/install | bash"
|
|
173
|
-
},
|
|
174
|
-
python: {
|
|
175
|
-
command: "python3 --version 2>/dev/null || echo missing",
|
|
176
|
-
satisfied: (stdout, exitCode) => exitCode === 0 && /^Python 3\./.test(stdout.trim()),
|
|
177
|
-
remedy: "Install Python 3. It supplies the standard-library ioctl boundary for SNP reports."
|
|
178
|
-
}
|
|
179
|
-
};
|
|
180
|
-
var modeFor = (capabilities) => capabilities.snpGuest ? "attested" : "enrolled";
|
|
181
|
-
var reasonFor = (mode) => mode === "attested" ? "SEV-SNP guest device present, so the agent can prove what it is running and the platform can refuse it if the measurement is wrong." : "No SEV-SNP guest device. The agent authenticates with its enrolment token and hybrid Ed25519 + ML-DSA signature — weaker than attestation, stronger than an API key in the application.";
|
|
182
|
-
var DEPLOYMENT_RUNNER_USER = "forgezero-runner";
|
|
183
|
-
var DEPLOYMENT_GROUP = "forgezero-deploy";
|
|
184
|
-
var VAULT_GROUP = "forgezero-vault";
|
|
185
|
-
var DEPLOYMENT_RUNNER_UNIT_PATH = "/etc/systemd/system/forgezero-deploy-runner.service";
|
|
186
|
-
var DEPLOYMENT_RUNNER_SOCKET = "/run/forgezero-deploy/runner.sock";
|
|
187
|
-
var ENROLMENT_UNIT_PATH = "/etc/systemd/system/forgezero-agent-enrol.service";
|
|
188
|
-
function agentEnrolmentUnit(options) {
|
|
189
|
-
if (!options.apiUrl || !options.enrolTokenCredentialPath || !options.enrolStatePath) {
|
|
190
|
-
throw new Error("direct enrolment needs API, credential and state paths");
|
|
191
|
-
}
|
|
192
|
-
const bin = options.binPath ?? "fz-agent";
|
|
193
|
-
const user = options.user ?? "forgezero";
|
|
194
|
-
const seedCredentialPath = options.seedCredentialPath ?? "/etc/forgezero/creds/agent-seed.cred";
|
|
195
|
-
const label = options.nodeLabel ? `Environment=FZ_NODE_LABEL=${options.nodeLabel}
|
|
196
|
-
` : "";
|
|
197
|
-
const gitPublicKey = options.gitPublicKeyPath ? `Environment=FZ_GIT_PUBLIC_KEY_FILE=${options.gitPublicKeyPath}
|
|
198
|
-
` : "";
|
|
199
|
-
const stateDir = options.enrolStatePath.replace(/\/[^/]+$/, "");
|
|
200
|
-
return `[Unit]
|
|
201
|
-
Description=Bind this machine to its ForgeZero compute
|
|
202
|
-
After=network-online.target
|
|
203
|
-
Wants=network-online.target
|
|
204
|
-
Before=forgezero-agent.service
|
|
205
|
-
ConditionPathExists=!${options.enrolStatePath}
|
|
206
|
-
|
|
207
|
-
[Service]
|
|
208
|
-
Type=oneshot
|
|
209
|
-
User=${user}
|
|
210
|
-
Group=${user}
|
|
211
|
-
LoadCredentialEncrypted=agent-seed:${seedCredentialPath}
|
|
212
|
-
LoadCredentialEncrypted=enrol-token:${options.enrolTokenCredentialPath}
|
|
213
|
-
Environment=FZ_SEED_CREDENTIAL=agent-seed
|
|
214
|
-
Environment=FZ_ENROL_TOKEN_CREDENTIAL=enrol-token
|
|
215
|
-
Environment=FZ_ENROL_STATE_FILE=${options.enrolStatePath}
|
|
216
|
-
Environment=FZ_API=${options.apiUrl}
|
|
217
|
-
${label}${gitPublicKey}ExecStart=${bin} enrol
|
|
218
|
-
# A '+' fixed command runs as root solely to remove the host-bound one-time
|
|
219
|
-
# ciphertext. Tenant code and the agent never receive a privilege boundary.
|
|
220
|
-
ExecStartPost=+/usr/bin/rm -f ${options.enrolTokenCredentialPath}
|
|
221
|
-
NoNewPrivileges=true
|
|
222
|
-
PrivateTmp=true
|
|
223
|
-
ProtectSystem=strict
|
|
224
|
-
ProtectHome=true
|
|
225
|
-
ReadWritePaths=${stateDir}
|
|
226
|
-
LimitCORE=0
|
|
227
|
-
|
|
228
|
-
[Install]
|
|
229
|
-
WantedBy=multi-user.target
|
|
230
|
-
`;
|
|
231
|
-
}
|
|
232
|
-
function deploymentRunnerUnit(options) {
|
|
233
|
-
const bin = options.binPath ?? "fz-agent";
|
|
234
|
-
const root = options.deployRoot ?? "/opt/forgezero";
|
|
235
|
-
const agentUser = options.user ?? "forgezero-agent";
|
|
236
|
-
return `[Unit]
|
|
237
|
-
Description=ForgeZero credential-free project command runner
|
|
238
|
-
Documentation=https://www.forgezero.net/docs/agent
|
|
239
|
-
|
|
240
|
-
[Service]
|
|
241
|
-
Type=notify
|
|
242
|
-
NotifyAccess=all
|
|
243
|
-
User=${DEPLOYMENT_RUNNER_USER}
|
|
244
|
-
Group=${DEPLOYMENT_GROUP}
|
|
245
|
-
Environment=FZ_DEPLOY_RUNNER_SOCKET=${DEPLOYMENT_RUNNER_SOCKET}
|
|
246
|
-
RuntimeDirectory=forgezero-deploy
|
|
247
|
-
RuntimeDirectoryMode=0710
|
|
248
|
-
ExecStartPre=+/usr/bin/install -d -o ${DEPLOYMENT_RUNNER_USER} -g ${DEPLOYMENT_GROUP} -m 0710 /run/forgezero-deploy
|
|
249
|
-
ExecStartPre=+/usr/bin/rm -f ${DEPLOYMENT_RUNNER_SOCKET}
|
|
250
|
-
ExecStart=${bin} deploy-runner --root=${root} --home=${root}/runner-home
|
|
251
|
-
ExecStartPost=+/usr/bin/chown ${agentUser}:${agentUser} ${DEPLOYMENT_RUNNER_SOCKET}
|
|
252
|
-
ExecStartPost=+/usr/bin/chmod 0600 ${DEPLOYMENT_RUNNER_SOCKET}
|
|
253
|
-
ExecStartPost=+/usr/bin/chown root:${VAULT_GROUP} /run/forgezero-deploy
|
|
254
|
-
ExecStopPost=+/usr/bin/rm -f ${DEPLOYMENT_RUNNER_SOCKET}
|
|
255
|
-
Restart=always
|
|
256
|
-
RestartSec=2
|
|
257
|
-
UMask=0007
|
|
258
|
-
LimitCORE=0
|
|
259
|
-
NoNewPrivileges=false
|
|
260
|
-
PrivateTmp=true
|
|
261
|
-
ProtectSystem=strict
|
|
262
|
-
ProtectHome=true
|
|
263
|
-
ProtectKernelTunables=true
|
|
264
|
-
ProtectKernelModules=true
|
|
265
|
-
ProtectControlGroups=true
|
|
266
|
-
RestrictRealtime=true
|
|
267
|
-
MemoryDenyWriteExecute=true
|
|
268
|
-
LockPersonality=true
|
|
269
|
-
ReadWritePaths=${root}/releases ${root}/runner-home
|
|
270
|
-
|
|
271
|
-
[Install]
|
|
272
|
-
WantedBy=multi-user.target
|
|
273
|
-
`;
|
|
274
|
-
}
|
|
275
|
-
function agentUnit(options) {
|
|
276
|
-
const bin = options.binPath ?? "fz-agent";
|
|
277
|
-
const user = options.user ?? "forgezero";
|
|
278
|
-
const seedCredentialPath = options.seedCredentialPath ?? "/etc/forgezero/creds/agent-seed.cred";
|
|
279
|
-
const controlSocketPath = options.controlSocketPath ?? "/run/forgezero/control.sock";
|
|
280
|
-
const deployRoot = options.deployRoot ?? "/opt/forgezero";
|
|
281
|
-
const deploymentEnabled = Boolean(options.repository || options.pullDeployments);
|
|
282
|
-
const enrolmentEnabled = Boolean(options.enrolTokenCredentialPath && options.enrolStatePath);
|
|
283
|
-
const deploymentEnvironment = options.deploymentEnvironment ?? {};
|
|
284
|
-
const deploymentCredentials = options.deploymentCredentials ?? {};
|
|
285
|
-
for (const [name, value] of Object.entries(deploymentEnvironment)) {
|
|
286
|
-
if (!/^[A-Z_][A-Z0-9_]*$/.test(name) || !/^[A-Za-z0-9._:\/@+-]+$/.test(value)) {
|
|
287
|
-
throw new Error(`invalid deployment environment entry: ${name}`);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
for (const [name, path] of Object.entries(deploymentCredentials)) {
|
|
291
|
-
if (!/^[A-Z_][A-Z0-9_]*$/.test(name) || !path.startsWith("/") || /[\r\n:]/.test(path)) {
|
|
292
|
-
throw new Error(`invalid deployment credential entry: ${name}`);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
const environment = [
|
|
296
|
-
`FZ_SOCKET_PATH=${options.socketPath}`,
|
|
297
|
-
`FZ_CONTROL_SOCKET=${controlSocketPath}`,
|
|
298
|
-
`FZ_SEED_CREDENTIAL=agent-seed`,
|
|
299
|
-
`FZ_AGENT_MODE=${options.mode}`,
|
|
300
|
-
options.apiUrl ? `FZ_API=${options.apiUrl}` : null,
|
|
301
|
-
options.project ? `FZ_PROJECT=${options.project}` : null,
|
|
302
|
-
options.environment ? `FZ_ENVIRONMENT=${options.environment}` : null,
|
|
303
|
-
options.enrolStatePath ? `FZ_ENROL_STATE_FILE=${options.enrolStatePath}` : null,
|
|
304
|
-
options.nodeLabel ? `FZ_NODE_LABEL=${options.nodeLabel}` : null,
|
|
305
|
-
options.gitPublicKeyPath ? `FZ_GIT_PUBLIC_KEY_FILE=${options.gitPublicKeyPath}` : null,
|
|
306
|
-
options.repository ? `FZ_DEPLOY_REPO=${options.repository}` : null,
|
|
307
|
-
options.branch ? `FZ_DEPLOY_BRANCH=${options.branch}` : null,
|
|
308
|
-
options.role ? `FZ_DEPLOY_ROLE=${options.role}` : null,
|
|
309
|
-
options.repository && options.branch ? `FZ_DEPLOY_KEY=${options.project ?? "platform"}:${options.environment ?? "production"}` : null,
|
|
310
|
-
deploymentEnabled ? `FZ_DEPLOY_ROOT=${deployRoot}` : null,
|
|
311
|
-
deploymentEnabled ? `FZ_DEPLOY_RUNNER_SOCKET=${DEPLOYMENT_RUNNER_SOCKET}` : null,
|
|
312
|
-
Object.keys(deploymentCredentials).length > 0 ? `FZ_DEPLOY_SYSTEMD_SECRETS=${Object.keys(deploymentCredentials).join(",")}` : null,
|
|
313
|
-
Object.keys(deploymentEnvironment).length > 0 ? `FZ_DEPLOY_ENV_NAMES=${Object.keys(deploymentEnvironment).join(",")}` : null,
|
|
314
|
-
...Object.entries(deploymentEnvironment).map(([name, value]) => `${name}=${value}`),
|
|
315
|
-
options.publicApiUrl ? `FZ_PUBLIC_API_URL=${options.publicApiUrl}` : null,
|
|
316
|
-
options.pullDeployments ? "FZ_DEPLOY_PULL=true" : null
|
|
317
|
-
].filter((line) => line !== null);
|
|
318
|
-
if (deploymentEnabled) {
|
|
319
|
-
environment.push(`HOME=${deployRoot}/agent-home`, `XDG_CACHE_HOME=${deployRoot}/cache`);
|
|
320
|
-
}
|
|
321
|
-
const gitCredential = options.gitCredentialPath ? `LoadCredentialEncrypted=git-deploy-key:${options.gitCredentialPath}
|
|
322
|
-
` : "";
|
|
323
|
-
const projectCredentials = Object.entries(deploymentCredentials).map(([name, path]) => `LoadCredentialEncrypted=${name}:${path}`).join(`
|
|
324
|
-
`);
|
|
325
|
-
const deploymentWrites = deploymentEnabled ? `ReadWritePaths=${deployRoot}/releases ${deployRoot}/agent-home ${deployRoot}/cache` : "";
|
|
326
|
-
const deploymentGroup = deploymentEnabled ? `SupplementaryGroups=${DEPLOYMENT_GROUP}` : "";
|
|
327
|
-
const after = [
|
|
328
|
-
"network-online.target",
|
|
329
|
-
deploymentEnabled ? "forgezero-deploy-runner.service" : null,
|
|
330
|
-
enrolmentEnabled ? "forgezero-agent-enrol.service" : null
|
|
331
|
-
].filter((value) => value !== null);
|
|
332
|
-
const requires = [
|
|
333
|
-
deploymentEnabled ? "forgezero-deploy-runner.service" : null,
|
|
334
|
-
enrolmentEnabled ? "forgezero-agent-enrol.service" : null
|
|
335
|
-
].filter((value) => value !== null);
|
|
336
|
-
const deploymentDependency = [
|
|
337
|
-
`After=${after.join(" ")}`,
|
|
338
|
-
"Wants=network-online.target",
|
|
339
|
-
requires.length > 0 ? `Requires=${requires.join(" ")}` : null
|
|
340
|
-
].filter((value) => value !== null).join(`
|
|
341
|
-
`);
|
|
342
|
-
const snpDevice = options.mode === "attested" ? `DevicePolicy=closed
|
|
343
|
-
DeviceAllow=/dev/sev-guest rw` : "";
|
|
344
|
-
const snpPrepare = options.mode === "attested" ? `ExecStartPre=+/bin/chgrp ${VAULT_GROUP} /dev/sev-guest
|
|
345
|
-
ExecStartPre=+/bin/chmod 0640 /dev/sev-guest
|
|
346
|
-
` : "";
|
|
347
|
-
return `[Unit]
|
|
348
|
-
Description=ForgeZero node agent (${options.mode})
|
|
349
|
-
Documentation=https://www.forgezero.net/docs/agent
|
|
350
|
-
${deploymentDependency}
|
|
351
|
-
|
|
352
|
-
[Service]
|
|
353
|
-
Type=simple
|
|
354
|
-
User=${user}
|
|
355
|
-
Group=${VAULT_GROUP}
|
|
356
|
-
${deploymentGroup}
|
|
357
|
-
LoadCredentialEncrypted=agent-seed:${seedCredentialPath}
|
|
358
|
-
${gitCredential}${projectCredentials}${projectCredentials ? `
|
|
359
|
-
` : ""}${snpPrepare}ExecStart=${bin}
|
|
360
|
-
Restart=always
|
|
361
|
-
RestartSec=2
|
|
362
|
-
|
|
363
|
-
${environment.map((line) => `Environment=${line}`).join(`
|
|
364
|
-
`)}
|
|
365
|
-
|
|
366
|
-
# The node seed and the vault replica live in this process's memory. A core dump
|
|
367
|
-
# writes both to disk, which is the one artefact this design exists to remove.
|
|
368
|
-
LimitCORE=0
|
|
369
|
-
|
|
370
|
-
# The socket is the entire interface: anything that can read it can read the
|
|
371
|
-
# scope. So it lives in a directory systemd creates with a known owner rather
|
|
372
|
-
# than wherever the process happened to have write access.
|
|
373
|
-
RuntimeDirectory=forgezero
|
|
374
|
-
RuntimeDirectoryMode=0750
|
|
375
|
-
UMask=0007
|
|
376
|
-
|
|
377
|
-
# Tenant-controlled commands execute in forgezero-deploy-runner.service. This
|
|
378
|
-
# credential-bearing process never needs to cross a privilege boundary.
|
|
379
|
-
NoNewPrivileges=true
|
|
380
|
-
PrivateTmp=true
|
|
381
|
-
ProtectSystem=strict
|
|
382
|
-
ProtectHome=true
|
|
383
|
-
ProtectKernelTunables=true
|
|
384
|
-
ProtectKernelModules=true
|
|
385
|
-
ProtectControlGroups=true
|
|
386
|
-
RestrictSUIDSGID=true
|
|
387
|
-
RestrictRealtime=true
|
|
388
|
-
MemoryDenyWriteExecute=true
|
|
389
|
-
LockPersonality=true
|
|
390
|
-
${snpDevice}
|
|
391
|
-
${deploymentWrites}
|
|
392
|
-
|
|
393
|
-
[Install]
|
|
394
|
-
WantedBy=multi-user.target
|
|
395
|
-
`;
|
|
396
|
-
}
|
|
397
|
-
var UNIT_PATH = "/etc/systemd/system/forgezero-agent.service";
|
|
398
|
-
function planProvision(options) {
|
|
399
|
-
const mode = options.mode;
|
|
400
|
-
const user = options.user ?? "forgezero";
|
|
401
|
-
const seedCredentialPath = options.seedCredentialPath ?? "/etc/forgezero/creds/agent-seed.cred";
|
|
402
|
-
const credentialDir = seedCredentialPath.replace(/\/[^/]+$/, "");
|
|
403
|
-
const deployRoot = options.deployRoot ?? "/opt/forgezero";
|
|
404
|
-
const deploymentEnabled = Boolean(options.repository || options.pullDeployments);
|
|
405
|
-
const enrolmentEnabled = Boolean(options.enrolTokenSourcePath && options.enrolTokenCredentialPath && options.enrolStatePath);
|
|
406
|
-
if (Boolean(options.enrolTokenSourcePath) !== Boolean(options.enrolTokenCredentialPath) || Boolean(options.enrolTokenCredentialPath) !== Boolean(options.enrolStatePath))
|
|
407
|
-
throw new Error("direct enrolment paths must be supplied together");
|
|
408
|
-
const safePath = (value, label) => {
|
|
409
|
-
if (!value || !/^\/[A-Za-z0-9._@/-]+$/.test(value))
|
|
410
|
-
throw new Error(`invalid ${label} path`);
|
|
411
|
-
return value;
|
|
412
|
-
};
|
|
413
|
-
const enrolTokenSourcePath = enrolmentEnabled ? safePath(options.enrolTokenSourcePath, "enrolment source") : undefined;
|
|
414
|
-
const enrolTokenCredentialPath = enrolmentEnabled ? safePath(options.enrolTokenCredentialPath, "enrolment credential") : undefined;
|
|
415
|
-
const enrolStatePath = enrolmentEnabled ? safePath(options.enrolStatePath, "enrolment state") : undefined;
|
|
416
|
-
const enrolStateDir = enrolStatePath?.replace(/\/[^/]+$/, "");
|
|
417
|
-
const sourceBinPath = options.sourceBinPath ? safePath(options.sourceBinPath, "agent source binary") : undefined;
|
|
418
|
-
const binPath = options.binPath ? safePath(options.binPath, "agent binary") : undefined;
|
|
419
|
-
const gitCredentialPath = options.gitCredentialPath ? safePath(options.gitCredentialPath, "Git credential") : undefined;
|
|
420
|
-
const gitPublicKeyPath = options.gitPublicKeyPath ? safePath(options.gitPublicKeyPath, "Git public key") : undefined;
|
|
421
|
-
if (options.generateGitIdentity && (!gitCredentialPath || !gitPublicKeyPath)) {
|
|
422
|
-
throw new Error("generated Git identity needs credential and public-key paths");
|
|
423
|
-
}
|
|
424
|
-
const gitPublicKeyDir = gitPublicKeyPath?.replace(/\/[^/]+$/, "");
|
|
425
|
-
return {
|
|
426
|
-
mode,
|
|
427
|
-
reason: reasonFor(mode),
|
|
428
|
-
unitPath: UNIT_PATH,
|
|
429
|
-
unit: agentUnit({ ...options, mode }),
|
|
430
|
-
auxiliaryUnits: [
|
|
431
|
-
...deploymentEnabled ? [
|
|
432
|
-
{ path: DEPLOYMENT_RUNNER_UNIT_PATH, unit: deploymentRunnerUnit(options) }
|
|
433
|
-
] : [],
|
|
434
|
-
...enrolmentEnabled ? [
|
|
435
|
-
{ path: ENROLMENT_UNIT_PATH, unit: agentEnrolmentUnit(options) }
|
|
436
|
-
] : []
|
|
437
|
-
],
|
|
438
|
-
socketPath: options.socketPath,
|
|
439
|
-
user,
|
|
440
|
-
steps: [
|
|
441
|
-
{
|
|
442
|
-
label: "vault socket access group",
|
|
443
|
-
command: `groupadd --system ${VAULT_GROUP} || true`
|
|
444
|
-
},
|
|
445
|
-
...sourceBinPath && binPath ? [{
|
|
446
|
-
label: "root-owned agent runtime",
|
|
447
|
-
command: `install -d -o root -g root -m 0755 ${binPath.replace(/\/[^/]+$/, "")}; ` + `install -o root -g root -m 0755 ${sourceBinPath} ${binPath}`
|
|
448
|
-
}] : [],
|
|
449
|
-
...deploymentEnabled ? [{
|
|
450
|
-
label: "deployment isolation group",
|
|
451
|
-
command: `groupadd --system ${DEPLOYMENT_GROUP} || true`
|
|
452
|
-
}] : [],
|
|
453
|
-
{
|
|
454
|
-
label: "service account",
|
|
455
|
-
command: `useradd --system --no-create-home --shell /usr/sbin/nologin ${user} || true`
|
|
456
|
-
},
|
|
457
|
-
{
|
|
458
|
-
label: "bind service account to vault group",
|
|
459
|
-
command: `usermod -g ${VAULT_GROUP} ${user}`
|
|
460
|
-
},
|
|
461
|
-
...deploymentEnabled ? [{
|
|
462
|
-
label: "credential-free deployment account",
|
|
463
|
-
command: `useradd --system --no-create-home --shell /usr/sbin/nologin --gid ${DEPLOYMENT_GROUP} ${DEPLOYMENT_RUNNER_USER} || true; ` + `usermod -a -G ${DEPLOYMENT_GROUP} ${user}`
|
|
464
|
-
}] : [],
|
|
465
|
-
{
|
|
466
|
-
label: "credential directory",
|
|
467
|
-
command: `install -d -o root -g root -m 0700 ${credentialDir}`
|
|
468
|
-
},
|
|
469
|
-
{
|
|
470
|
-
label: "encrypted node identity",
|
|
471
|
-
command: `test -s ${seedCredentialPath} || { ` + `openssl rand -base64 32 | tr '+/' '-_' | tr -d '=\\n' | ` + `systemd-creds encrypt --name=agent-seed - ${seedCredentialPath}; ` + `chmod 0400 ${seedCredentialPath}; }`
|
|
472
|
-
},
|
|
473
|
-
...options.generateGitIdentity && gitCredentialPath && gitPublicKeyPath ? [
|
|
474
|
-
{
|
|
475
|
-
label: "Git deploy identity directory",
|
|
476
|
-
command: `install -d -o root -g root -m 0755 ${gitPublicKeyDir}`
|
|
477
|
-
},
|
|
478
|
-
{
|
|
479
|
-
label: "unique encrypted Git deploy identity",
|
|
480
|
-
command: `test -s ${gitCredentialPath} || { ` + `rm -f /run/forgezero-git-deploy-key /run/forgezero-git-deploy-key.pub; ` + `ssh-keygen -q -t ed25519 -N '' -C forgezero-compute -f /run/forgezero-git-deploy-key; ` + `systemd-creds encrypt --name=git-deploy-key /run/forgezero-git-deploy-key ${gitCredentialPath}; ` + `install -o root -g root -m 0444 /run/forgezero-git-deploy-key.pub ${gitPublicKeyPath}; ` + `rm -f /run/forgezero-git-deploy-key /run/forgezero-git-deploy-key.pub; ` + `chmod 0400 ${gitCredentialPath}; }; ` + `test -s ${gitPublicKeyPath} || { ` + `systemd-creds decrypt --name=git-deploy-key ${gitCredentialPath} /run/forgezero-git-deploy-key; ` + `ssh-keygen -y -f /run/forgezero-git-deploy-key | ` + `sed 's/$/ forgezero-compute/' > /run/forgezero-git-deploy-key.pub; ` + `install -o root -g root -m 0444 /run/forgezero-git-deploy-key.pub ${gitPublicKeyPath}; ` + `rm -f /run/forgezero-git-deploy-key /run/forgezero-git-deploy-key.pub; }; ` + `test -s ${gitCredentialPath} && test -s ${gitPublicKeyPath}`
|
|
481
|
-
}
|
|
482
|
-
] : [],
|
|
483
|
-
...enrolmentEnabled ? [
|
|
484
|
-
{
|
|
485
|
-
label: "enrolment state directory",
|
|
486
|
-
command: `install -d -o ${user} -g ${user} -m 0700 ${enrolStateDir}`
|
|
487
|
-
},
|
|
488
|
-
{
|
|
489
|
-
label: "encrypted one-time enrolment capability",
|
|
490
|
-
command: `test -s ${enrolTokenCredentialPath} || { test -r ${enrolTokenSourcePath}; ` + `systemd-creds encrypt --name=enrol-token ${enrolTokenSourcePath} ${enrolTokenCredentialPath}; ` + `chmod 0400 ${enrolTokenCredentialPath}; rm -f ${enrolTokenSourcePath}; }`
|
|
491
|
-
}
|
|
492
|
-
] : [],
|
|
493
|
-
...deploymentEnabled ? [{
|
|
494
|
-
label: "deployment directories",
|
|
495
|
-
command: `install -d -o root -g root -m 0755 ${deployRoot} && ` + `install -d -o root -g ${DEPLOYMENT_GROUP} -m 3770 ${deployRoot}/releases && ` + `install -d -o ${user} -g ${user} -m 0750 ${deployRoot}/cache && ` + `install -d -o ${user} -g ${user} -m 0700 ${deployRoot}/agent-home && ` + `install -d -o ${DEPLOYMENT_RUNNER_USER} -g ${DEPLOYMENT_GROUP} -m 0700 ${deployRoot}/runner-home ${deployRoot}/runner-home/cache`
|
|
496
|
-
}] : [],
|
|
497
|
-
{ label: "reload units", command: "systemctl daemon-reload" },
|
|
498
|
-
...deploymentEnabled ? [{
|
|
499
|
-
label: "remove unsupported deployment socket activation",
|
|
500
|
-
command: "systemctl disable --now forgezero-deploy-runner.socket 2>/dev/null || true; rm -f /etc/systemd/system/forgezero-deploy-runner.socket; systemctl daemon-reload"
|
|
501
|
-
}] : [],
|
|
502
|
-
{
|
|
503
|
-
label: "enable and start",
|
|
504
|
-
command: `systemctl enable --now ${[
|
|
505
|
-
...deploymentEnabled ? ["forgezero-deploy-runner.service"] : [],
|
|
506
|
-
...enrolmentEnabled ? ["forgezero-agent-enrol.service"] : [],
|
|
507
|
-
"forgezero-agent.service"
|
|
508
|
-
].join(" ")}`
|
|
509
|
-
},
|
|
510
|
-
...enrolmentEnabled ? [{
|
|
511
|
-
label: "prove the compute binding is durable",
|
|
512
|
-
command: `test -s ${enrolStatePath}`
|
|
513
|
-
}] : [],
|
|
514
|
-
{ label: "prove it is running", command: "systemctl is-active forgezero-agent.service" },
|
|
515
|
-
{ label: "prove the vault socket exists", command: `test -S ${options.socketPath}` },
|
|
516
|
-
...deploymentEnabled ? [{
|
|
517
|
-
label: "prove the deployment runner socket exists",
|
|
518
|
-
command: `test -S ${DEPLOYMENT_RUNNER_SOCKET}`
|
|
519
|
-
}] : [],
|
|
520
|
-
...options.repository ? [{
|
|
521
|
-
label: "prove the deployment control socket exists",
|
|
522
|
-
command: `test -S ${options.controlSocketPath ?? "/run/forgezero/control.sock"}`
|
|
523
|
-
}] : []
|
|
524
|
-
]
|
|
525
|
-
};
|
|
526
|
-
}
|
|
527
|
-
|
|
528
141
|
// src/ubuntu.ts
|
|
529
142
|
var SUPPORTED_GUEST_IMAGE = Object.freeze({
|
|
530
143
|
key: "ubuntu-resolute-20260731",
|
|
@@ -717,7 +330,7 @@ var yamlFile = (path, content, permissions) => ` - path: ${JSON.stringify(path)
|
|
|
717
330
|
encoding: b64
|
|
718
331
|
content: ${base64(content)}
|
|
719
332
|
`;
|
|
720
|
-
function guestBootstrapScript(profile, attested = Boolean(profile.confidential), hasEnrolment = true) {
|
|
333
|
+
function guestBootstrapScript(profile, attested = Boolean(profile.confidential), hasEnrolment = true, nodeLabel = "compute") {
|
|
721
334
|
const agentBun = "/usr/local/lib/forgezero/bun";
|
|
722
335
|
const attestationSetup = attested ? `# The report device is not part of the encryption path, so a guest can appear
|
|
723
336
|
# healthy and encrypted while attestation is silently impossible. Install and
|
|
@@ -731,26 +344,7 @@ printf 'sev-guest
|
|
|
731
344
|
` : "";
|
|
732
345
|
return `#!/usr/bin/env bash
|
|
733
346
|
set -Eeuo pipefail
|
|
734
|
-
${attestationSetup}
|
|
735
|
-
useradd --system --no-create-home --shell /usr/sbin/nologin forgezero-agent 2>/dev/null || true
|
|
736
|
-
usermod -g ${VAULT_GROUP} forgezero-agent
|
|
737
|
-
groupadd --system ${DEPLOYMENT_GROUP} 2>/dev/null || true
|
|
738
|
-
useradd --system --no-create-home --shell /usr/sbin/nologin --gid ${DEPLOYMENT_GROUP} ${DEPLOYMENT_RUNNER_USER} 2>/dev/null || true
|
|
739
|
-
usermod -a -G ${DEPLOYMENT_GROUP} forgezero-agent
|
|
740
|
-
install -d -o root -g root -m 0700 /etc/forgezero/creds
|
|
741
|
-
install -d -o root -g root -m 0755 /etc/forgezero/git
|
|
742
|
-
install -d -o forgezero-agent -g forgezero-agent -m 0700 /var/lib/forgezero
|
|
743
|
-
install -d -o root -g root -m 0755 /opt/forgezero
|
|
744
|
-
install -d -o root -g ${DEPLOYMENT_GROUP} -m 3770 /opt/forgezero/releases
|
|
745
|
-
install -d -o forgezero-agent -g forgezero-agent -m 0700 /opt/forgezero/cache /opt/forgezero/home
|
|
746
|
-
install -d -o ${DEPLOYMENT_RUNNER_USER} -g ${DEPLOYMENT_GROUP} -m 0700 /opt/forgezero/runner-home /opt/forgezero/runner-home/cache
|
|
747
|
-
${hasEnrolment ? `if [[ -s /run/forgezero-enrol-token ]]; then
|
|
748
|
-
systemd-creds encrypt --name=enrol-token /run/forgezero-enrol-token /var/lib/forgezero/enrol-token.cred
|
|
749
|
-
rm -f /run/forgezero-enrol-token
|
|
750
|
-
fi
|
|
751
|
-
chown root:root /var/lib/forgezero/enrol-token.cred
|
|
752
|
-
chmod 0400 /var/lib/forgezero/enrol-token.cred
|
|
753
|
-
` : ""}if [[ ! -x /usr/local/bin/bun ]]; then
|
|
347
|
+
${attestationSetup}if [[ ! -x /usr/local/bin/bun ]]; then
|
|
754
348
|
curl -fsSL https://bun.sh/install -o /run/fz-bun-install
|
|
755
349
|
printf '%s %s
|
|
756
350
|
' '${profile.bunInstallerSha256}' /run/fz-bun-install | sha256sum -c -
|
|
@@ -758,132 +352,16 @@ chmod 0400 /var/lib/forgezero/enrol-token.cred
|
|
|
758
352
|
install -m 0755 ${agentBun}/bin/bun /usr/local/bin/bun
|
|
759
353
|
rm -f /run/fz-bun-install
|
|
760
354
|
fi
|
|
761
|
-
if [[ ! -x /usr/local/bin/fz-agent ]]; then
|
|
355
|
+
if [[ ! -x /usr/local/bin/fz-agent ]] || [[ "$(/usr/local/bin/fz-agent --version 2>/dev/null || true)" != "${profile.agentVersion}" ]]; then
|
|
762
356
|
env BUN_INSTALL=${agentBun} /usr/local/bin/bun add -g --no-cache --force @forgezero/agent@${profile.agentVersion}
|
|
763
357
|
ln -sfn ${agentBun}/bin/fz-agent /usr/local/bin/fz-agent
|
|
764
358
|
fi
|
|
765
|
-
|
|
766
|
-
umask 077
|
|
767
|
-
openssl rand -base64 32 | tr '+/' '-_' | tr -d '=
|
|
768
|
-
' >/run/fz-agent-seed
|
|
769
|
-
systemd-creds encrypt --name=agent-seed /run/fz-agent-seed /etc/forgezero/creds/agent-seed.cred
|
|
770
|
-
rm -f /run/fz-agent-seed
|
|
771
|
-
fi
|
|
772
|
-
chmod 0400 /etc/forgezero/creds/agent-seed.cred
|
|
773
|
-
if [[ ! -s /etc/forgezero/creds/git-deploy-key.cred ]]; then
|
|
774
|
-
umask 077
|
|
775
|
-
rm -f /run/fz-git-deploy-key /run/fz-git-deploy-key.pub
|
|
776
|
-
ssh-keygen -q -t ed25519 -N '' -C forgezero-compute -f /run/fz-git-deploy-key
|
|
777
|
-
systemd-creds encrypt --name=git-deploy-key /run/fz-git-deploy-key /etc/forgezero/creds/git-deploy-key.cred
|
|
778
|
-
install -o root -g root -m 0444 /run/fz-git-deploy-key.pub /etc/forgezero/git/deploy.pub
|
|
779
|
-
rm -f /run/fz-git-deploy-key /run/fz-git-deploy-key.pub
|
|
780
|
-
fi
|
|
781
|
-
if [[ ! -s /etc/forgezero/git/deploy.pub ]]; then
|
|
782
|
-
systemd-creds decrypt --name=git-deploy-key /etc/forgezero/creds/git-deploy-key.cred /run/fz-git-deploy-key
|
|
783
|
-
ssh-keygen -y -f /run/fz-git-deploy-key | sed 's/$/ forgezero-compute/' >/run/fz-git-deploy-key.pub
|
|
784
|
-
install -o root -g root -m 0444 /run/fz-git-deploy-key.pub /etc/forgezero/git/deploy.pub
|
|
785
|
-
rm -f /run/fz-git-deploy-key /run/fz-git-deploy-key.pub
|
|
786
|
-
fi
|
|
787
|
-
chmod 0400 /etc/forgezero/creds/git-deploy-key.cred
|
|
788
|
-
systemctl daemon-reload
|
|
789
|
-
systemctl disable --now forgezero-deploy-runner.socket 2>/dev/null || true
|
|
790
|
-
rm -f /etc/systemd/system/forgezero-deploy-runner.socket
|
|
791
|
-
systemctl daemon-reload
|
|
792
|
-
systemctl enable --now forgezero-deploy-runner.service forgezero-agent.service
|
|
793
|
-
`;
|
|
794
|
-
}
|
|
795
|
-
function guestAgentUnit(profile, name, attested = Boolean(profile.confidential), pull = true) {
|
|
796
|
-
const attestationPrepare = attested ? `ExecStartPre=+/bin/chgrp ${VAULT_GROUP} /dev/sev-guest
|
|
797
|
-
ExecStartPre=+/bin/chmod 0640 /dev/sev-guest
|
|
798
|
-
` : "";
|
|
799
|
-
const attestationDevice = attested ? `DevicePolicy=closed
|
|
800
|
-
DeviceAllow=/dev/sev-guest rw
|
|
801
|
-
` : "";
|
|
802
|
-
return `[Unit]
|
|
803
|
-
Description=ForgeZero compute agent (${name})
|
|
804
|
-
After=network-online.target forgezero-deploy-runner.service
|
|
805
|
-
Wants=network-online.target
|
|
806
|
-
Requires=forgezero-deploy-runner.service
|
|
807
|
-
|
|
808
|
-
[Service]
|
|
809
|
-
Type=simple
|
|
810
|
-
User=forgezero-agent
|
|
811
|
-
Group=${VAULT_GROUP}
|
|
812
|
-
SupplementaryGroups=${DEPLOYMENT_GROUP}
|
|
813
|
-
LoadCredentialEncrypted=agent-seed:/etc/forgezero/creds/agent-seed.cred
|
|
814
|
-
LoadCredentialEncrypted=git-deploy-key:/etc/forgezero/creds/git-deploy-key.cred
|
|
815
|
-
Environment=FZ_SEED_CREDENTIAL=agent-seed
|
|
816
|
-
Environment=FZ_GIT_PUBLIC_KEY_FILE=/etc/forgezero/git/deploy.pub
|
|
817
|
-
Environment=FZ_API=${profile.apiUrl}
|
|
818
|
-
Environment=FZ_ENROL_STATE_FILE=/var/lib/forgezero/enrolment.json
|
|
819
|
-
Environment=FZ_NODE_LABEL=${name}
|
|
820
|
-
Environment=FZ_SOCKET_PATH=/run/forgezero/vault.sock
|
|
821
|
-
Environment=FZ_DEPLOY_ROOT=/opt/forgezero
|
|
822
|
-
Environment=FZ_DEPLOY_PULL=${pull ? "true" : "false"}
|
|
823
|
-
Environment=FZ_DEPLOY_RUNNER_SOCKET=${DEPLOYMENT_RUNNER_SOCKET}
|
|
824
|
-
Environment=HOME=/opt/forgezero/home
|
|
825
|
-
${attestationPrepare}ExecStart=/usr/local/bin/fz-agent
|
|
826
|
-
Restart=on-failure
|
|
827
|
-
RestartSec=5
|
|
828
|
-
RuntimeDirectory=forgezero
|
|
829
|
-
RuntimeDirectoryMode=0750
|
|
830
|
-
UMask=0007
|
|
831
|
-
LimitCORE=0
|
|
832
|
-
NoNewPrivileges=true
|
|
833
|
-
PrivateTmp=true
|
|
834
|
-
ProtectSystem=strict
|
|
835
|
-
ProtectHome=true
|
|
836
|
-
ReadWritePaths=/var/lib/forgezero /opt/forgezero
|
|
837
|
-
${attestationDevice}
|
|
838
|
-
|
|
839
|
-
[Install]
|
|
840
|
-
WantedBy=multi-user.target
|
|
841
|
-
`;
|
|
842
|
-
}
|
|
843
|
-
function guestEnrolmentDropIn() {
|
|
844
|
-
return `[Service]
|
|
845
|
-
LoadCredentialEncrypted=enrol-token:/var/lib/forgezero/enrol-token.cred
|
|
846
|
-
Environment=FZ_ENROL_TOKEN_CREDENTIAL=enrol-token
|
|
847
|
-
`;
|
|
848
|
-
}
|
|
849
|
-
function guestEnrolmentCleanupScript() {
|
|
850
|
-
return `#!/usr/bin/env bash
|
|
851
|
-
set -Eeuo pipefail
|
|
852
|
-
for _ in $(seq 1 180); do
|
|
853
|
-
[[ -s /var/lib/forgezero/enrolment.json ]] && break
|
|
854
|
-
sleep 1
|
|
855
|
-
done
|
|
856
|
-
[[ -s /var/lib/forgezero/enrolment.json ]] || { echo 'guest enrolment did not become durable' >&2; exit 1; }
|
|
857
|
-
rm -f /var/lib/forgezero/enrol-token.cred
|
|
858
|
-
rm -f /etc/systemd/system/forgezero-agent.service.d/enrolment.conf
|
|
859
|
-
systemctl daemon-reload
|
|
860
|
-
systemctl disable forgezero-enrolment-cleanup.service
|
|
861
|
-
`;
|
|
862
|
-
}
|
|
863
|
-
function guestEnrolmentCleanupUnit() {
|
|
864
|
-
return `[Unit]
|
|
865
|
-
Description=Remove the consumed ForgeZero guest enrolment credential
|
|
866
|
-
After=forgezero-agent.service
|
|
867
|
-
Requires=forgezero-agent.service
|
|
868
|
-
ConditionPathExists=/var/lib/forgezero/enrol-token.cred
|
|
869
|
-
|
|
870
|
-
[Service]
|
|
871
|
-
Type=oneshot
|
|
872
|
-
ExecStart=/usr/local/sbin/forgezero-enrolment-cleanup
|
|
873
|
-
TimeoutStartSec=4min
|
|
874
|
-
|
|
875
|
-
[Install]
|
|
876
|
-
WantedBy=multi-user.target
|
|
359
|
+
env FZ_API=${profile.apiUrl} FZ_AGENT_BIN=/usr/local/bin/fz-agent FZ_AGENT_USER=forgezero-agent FZ_SOCKET_PATH=/run/forgezero/vault.sock FZ_SEED_CREDENTIAL_PATH=/etc/forgezero/creds/agent-seed.cred FZ_GIT_CREDENTIAL_PATH=/etc/forgezero/creds/git-deploy-key.cred FZ_GIT_PUBLIC_KEY_PATH=/etc/forgezero/git/deploy.pub FZ_DEPLOY_ROOT=/opt/forgezero FZ_DEPLOY_PULL=${hasEnrolment ? "true" : "false"} FZ_NODE_LABEL=${nodeLabel} ${agentBun}/bin/fz agent install --apply${hasEnrolment ? " --enrol" : ""}
|
|
877
360
|
`;
|
|
878
361
|
}
|
|
879
362
|
function cloudInit(profile, claim, manifest) {
|
|
880
363
|
const enrolled = Boolean(claim.enrolment);
|
|
881
|
-
const bootstrap = guestBootstrapScript(profile, claim.spec.confidential, enrolled);
|
|
882
|
-
const agentUnit2 = guestAgentUnit(profile, manifest.name, claim.spec.confidential, enrolled);
|
|
883
|
-
const enrolmentDropIn = guestEnrolmentDropIn();
|
|
884
|
-
const cleanupScript = guestEnrolmentCleanupScript();
|
|
885
|
-
const cleanupUnit = guestEnrolmentCleanupUnit();
|
|
886
|
-
const runnerUnit = deploymentRunnerUnit({ binPath: "/usr/local/bin/fz-agent", deployRoot: "/opt/forgezero", user: "forgezero-agent" });
|
|
364
|
+
const bootstrap = guestBootstrapScript(profile, claim.spec.confidential, enrolled, manifest.name);
|
|
887
365
|
const access = claim.access;
|
|
888
366
|
const users = access ? `users:
|
|
889
367
|
- default
|
|
@@ -895,8 +373,8 @@ function cloudInit(profile, claim, manifest) {
|
|
|
895
373
|
${access.sshPublicKeys.map((key) => ` - ${JSON.stringify(key)}`).join(`
|
|
896
374
|
`)}
|
|
897
375
|
` : "";
|
|
898
|
-
const enrolmentFiles = enrolled ?
|
|
899
|
-
`, "0600")
|
|
376
|
+
const enrolmentFiles = enrolled ? yamlFile("/run/forgezero-enrol-token", `${claim.enrolment.token}
|
|
377
|
+
`, "0600") : "";
|
|
900
378
|
return {
|
|
901
379
|
userData: `#cloud-config
|
|
902
380
|
package_update: true
|
|
@@ -905,10 +383,8 @@ ${users}disable_root: true
|
|
|
905
383
|
# every dynamically claimed repository must be cloneable on a clean image.
|
|
906
384
|
packages: [curl, ca-certificates, openssl, openssh-client, git, unzip${claim.spec.confidential ? ", python3" : ""}]
|
|
907
385
|
write_files:
|
|
908
|
-
${enrolmentFiles}${yamlFile("/usr/local/sbin/forgezero-guest-bootstrap", bootstrap, "0700")}
|
|
386
|
+
${enrolmentFiles}${yamlFile("/usr/local/sbin/forgezero-guest-bootstrap", bootstrap, "0700")}runcmd:
|
|
909
387
|
- [ bash, /usr/local/sbin/forgezero-guest-bootstrap ]
|
|
910
|
-
${enrolled ? ` - [ systemctl, enable, --now, forgezero-enrolment-cleanup.service ]
|
|
911
|
-
` : ""}
|
|
912
388
|
`,
|
|
913
389
|
metaData: `instance-id: ${manifest.name}-${claim.attempt}
|
|
914
390
|
local-hostname: ${manifest.name}
|
|
@@ -1124,11 +600,7 @@ export {
|
|
|
1124
600
|
provisionMetalGuest,
|
|
1125
601
|
macForAddress,
|
|
1126
602
|
guestNameFor,
|
|
1127
|
-
guestEnrolmentDropIn,
|
|
1128
|
-
guestEnrolmentCleanupUnit,
|
|
1129
|
-
guestEnrolmentCleanupScript,
|
|
1130
603
|
guestBootstrapScript,
|
|
1131
|
-
guestAgentUnit,
|
|
1132
604
|
cloudInit,
|
|
1133
605
|
allocateCpuPool,
|
|
1134
606
|
allocateAddress,
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { NodeKeyPair } from '@forgezero/runtime/identity';
|
|
2
|
+
export type MigrationNetwork = 'private-lan' | 'cloudflare-warp';
|
|
3
|
+
export type MigrationAction = 'network-ready' | 'database-member-ready' | 'api-ready' | 'source-drained' | 'source-stopped';
|
|
4
|
+
export interface MigrationEvidence {
|
|
5
|
+
targetAgentReady?: boolean;
|
|
6
|
+
privateNetworkReady?: boolean;
|
|
7
|
+
warpConnected?: boolean;
|
|
8
|
+
databaseMemberHealthy?: boolean;
|
|
9
|
+
apiHealthy?: boolean;
|
|
10
|
+
sourceDrained?: boolean;
|
|
11
|
+
sourceStopped?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface RemoteMigrationClaim {
|
|
14
|
+
migrationKey: string;
|
|
15
|
+
action: MigrationAction;
|
|
16
|
+
network: MigrationNetwork;
|
|
17
|
+
claimToken: string;
|
|
18
|
+
claimExpiresAtTs: number;
|
|
19
|
+
attempt: number;
|
|
20
|
+
/** Signed controller-selected private peers used only for reachability probes. */
|
|
21
|
+
peerPrivateAddresses: readonly string[];
|
|
22
|
+
}
|
|
23
|
+
export interface MigrationPullOptions {
|
|
24
|
+
apiUrl: string;
|
|
25
|
+
nodeKey: string;
|
|
26
|
+
keys: NodeKeyPair;
|
|
27
|
+
run(claim: RemoteMigrationClaim): Promise<MigrationEvidence>;
|
|
28
|
+
fetch?: (input: URL, init: RequestInit) => Promise<Response>;
|
|
29
|
+
intervalMs?: number;
|
|
30
|
+
requestTimeoutMs?: number;
|
|
31
|
+
completionAttempts?: number;
|
|
32
|
+
renewRetryMs?: number;
|
|
33
|
+
sleep?: (ms: number) => Promise<void>;
|
|
34
|
+
now?: () => number;
|
|
35
|
+
setTimer?: (callback: () => void, ms: number) => unknown;
|
|
36
|
+
clearTimer?: (handle: unknown) => void;
|
|
37
|
+
onEvent?: (event: string, detail?: unknown) => void;
|
|
38
|
+
}
|
|
39
|
+
export type MigrationPullResult = {
|
|
40
|
+
status: 'idle';
|
|
41
|
+
} | {
|
|
42
|
+
status: 'completed';
|
|
43
|
+
claim: RemoteMigrationClaim;
|
|
44
|
+
evidence: MigrationEvidence;
|
|
45
|
+
} | {
|
|
46
|
+
status: 'failed';
|
|
47
|
+
claim: RemoteMigrationClaim;
|
|
48
|
+
reason: string;
|
|
49
|
+
};
|
|
50
|
+
/** Claim one lifecycle action, await it, and acknowledge only its fenced claim. */
|
|
51
|
+
export declare function pullMigrationOnce(options: MigrationPullOptions): Promise<MigrationPullResult>;
|
|
52
|
+
export declare function startMigrationPull(options: MigrationPullOptions): {
|
|
53
|
+
stop(): Promise<void>;
|
|
54
|
+
readonly active: boolean;
|
|
55
|
+
};
|