@forgezero/agent 0.1.26 → 0.1.28
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 +5 -3
- package/dist/agent-heartbeat.d.ts +45 -0
- package/dist/agent-heartbeat.js +511 -0
- package/dist/agent-update-helper.d.ts +45 -0
- package/dist/agent-update-helper.js +366 -0
- package/dist/agent-update.d.ts +46 -0
- package/dist/agent-update.js +184 -0
- package/dist/definition.d.ts +1 -7
- package/dist/definition.js +92 -23
- package/dist/deployment-pull.d.ts +2 -0
- package/dist/deployment.d.ts +3 -0
- package/dist/fz-agent.js +7865 -2617
- package/dist/fz.js +8287 -38
- package/dist/guest-enrolment.js +14 -1
- package/dist/index.d.ts +13 -0
- package/dist/metal-helper-socket.js +23 -4
- package/dist/metal-provision.js +23 -4
- package/dist/migration-pull.js +14 -1
- package/dist/node-vault.js +15 -2
- package/dist/provision.d.ts +13 -0
- package/dist/provision.js +685 -6
- package/dist/provisioning-pull.js +14 -1
- package/dist/signed-node-http.d.ts +10 -0
- package/dist/socket.d.ts +2 -0
- package/dist/software-helper.d.ts +14 -0
- package/dist/software-helper.js +203 -0
- package/dist/software.d.ts +27 -0
- package/dist/software.js +95 -0
- package/dist/version.d.ts +1 -1
- package/package.json +24 -4
package/dist/guest-enrolment.js
CHANGED
|
@@ -28,8 +28,21 @@ class SignedNodeHttpError extends Error {
|
|
|
28
28
|
this.name = "SignedNodeHttpError";
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
+
function signedNodeApiUrl(value) {
|
|
32
|
+
let url;
|
|
33
|
+
try {
|
|
34
|
+
url = new URL(value);
|
|
35
|
+
} catch {
|
|
36
|
+
throw new Error("Agent API URL must be an absolute HTTPS URL.");
|
|
37
|
+
}
|
|
38
|
+
const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1";
|
|
39
|
+
if (url.protocol !== "https:" && !(loopback && url.protocol === "http:") || url.username || url.password || url.search || url.hash) {
|
|
40
|
+
throw new Error("Agent API URL must be HTTPS without credentials, query or fragment.");
|
|
41
|
+
}
|
|
42
|
+
return url;
|
|
43
|
+
}
|
|
31
44
|
async function postSignedNode(options, path, body) {
|
|
32
|
-
const url =
|
|
45
|
+
const url = signedNodeApiUrl(options.apiUrl);
|
|
33
46
|
url.pathname = `${url.pathname.replace(/\/$/, "")}/${path.replace(/^\//, "")}`.replace(/\/+/g, "/");
|
|
34
47
|
url.search = "";
|
|
35
48
|
url.hash = "";
|
package/dist/index.d.ts
CHANGED
|
@@ -27,6 +27,15 @@ export { DEFAULT_METAL_HELPER_SOCKET, requestMetalProvision, startMetalHelper }
|
|
|
27
27
|
export { DEFAULT_LIFECYCLE_HELPER_SOCKET, executeLifecycleAction, loadLifecycleProfile, requestLifecycleAction, startLifecycleHelper, validateLifecycleProfile } from './lifecycle-helper';
|
|
28
28
|
export type { LifecycleCommandResult, LifecycleExec, LifecycleProfile } from './lifecycle-helper';
|
|
29
29
|
export { materializeWarpMdm, renderWarpMdm } from './warp-config';
|
|
30
|
+
export { compareVersions, restoreAgentRelease, selectAgentRelease, stageAgentRelease, validateAgentRelease, DEFAULT_AGENT_RELEASE_ROOT, DEFAULT_AGENT_UPDATE_SOCKET, MAX_AGENT_TARBALL_BYTES } from './agent-update';
|
|
31
|
+
export type { AgentRelease, StagedAgentRelease, UpdateCommand, UpdateCommandResult } from './agent-update';
|
|
32
|
+
export { activateAgentRelease, probeAgentSocket, requestAgentUpdate, startAgentUpdateHelper, AGENT_UPDATE_GROUP, AGENT_UPDATE_HELPER_UNIT_PATH, AGENT_UPDATE_RECEIPT } from './agent-update-helper';
|
|
33
|
+
export type { AgentUpdateRequest, AgentUpdateResponse } from './agent-update-helper';
|
|
34
|
+
export { DEFAULT_SOFTWARE_HELPER_SOCKET, requestSoftware, startSoftwareHelper, SOFTWARE_HELPER_GROUP, SOFTWARE_HELPER_UNIT_PATH } from './software-helper';
|
|
35
|
+
export { ensureSoftwareRequirements, observeSoftwareHost, validateSoftwareRequirements } from './software';
|
|
36
|
+
export type { SoftwareCommandResult, SoftwareExec, SoftwareObservation, SoftwareRequirement } from './software';
|
|
37
|
+
export { heartbeatAgentOnce, observeAgentHost, startAgentHeartbeat } from './agent-heartbeat';
|
|
38
|
+
export type { AgentHeartbeatOptions, AgentHeartbeatResponse, AgentObservation } from './agent-heartbeat';
|
|
30
39
|
export { DEFAULT_DEPLOYMENT_RUNNER_SOCKET, requestDeploymentCommand, startDeploymentRunner } from './deployment-runner';
|
|
31
40
|
export { createSnpAttestationSource } from './snp-attestation';
|
|
32
41
|
export type { SnpAttestationOptions } from './snp-attestation';
|
|
@@ -45,6 +54,8 @@ export { assertSupportedGuestImage, SUPPORTED_GUEST_IMAGE } from './ubuntu';
|
|
|
45
54
|
export declare function loadOrCreateSeed(path: string): Uint8Array;
|
|
46
55
|
export interface AgentConfig {
|
|
47
56
|
socketPath?: string;
|
|
57
|
+
/** systemd-owned listener fd; preserves the application endpoint across replacement. */
|
|
58
|
+
listenFd?: number;
|
|
48
59
|
seedPath?: string;
|
|
49
60
|
/** Prefer this already-unsealed systemd credential over a persistent file. */
|
|
50
61
|
seedCredential?: string;
|
|
@@ -82,6 +93,8 @@ export declare const DEFAULT_SOCKET_PATH = "/run/forgezero/vault.sock";
|
|
|
82
93
|
export declare const DEFAULT_SEED_PATH = "/var/lib/forgezero/node.seed";
|
|
83
94
|
export declare const DEFAULT_SEED_CREDENTIAL = "agent-seed";
|
|
84
95
|
export declare const DEFAULT_ENROLMENT_STATE_PATH = "/var/lib/forgezero/enrolment.json";
|
|
96
|
+
/** Resolve the one descriptor systemd passes for the application Vault socket. */
|
|
97
|
+
export declare function systemdListenFd(environment?: Record<string, string | undefined>, pid?: number): number | undefined;
|
|
85
98
|
/**
|
|
86
99
|
* Read a systemd credential made available only to this unit.
|
|
87
100
|
*
|
|
@@ -552,16 +552,36 @@ async function removeMetalGuest(profile, claim, exec) {
|
|
|
552
552
|
throw new MetalProvisionError("create claim cannot remove a guest");
|
|
553
553
|
const name = claim.spec.guestName ?? guestNameFor(claim.computeKey);
|
|
554
554
|
const manifestPath = join(profile.stateDir, `${name}.json`);
|
|
555
|
+
const service = `forgezero-guest@${name}.service`;
|
|
556
|
+
const unitPath = join(profile.unitDir, service);
|
|
557
|
+
const lv = `/dev/${profile.volumeGroup}/${name}`;
|
|
555
558
|
const manifest = existsSync(manifestPath) ? JSON.parse(readFileSync(manifestPath, "utf8")) : legacyPlatformManifest(profile, claim, name);
|
|
556
|
-
if (!manifest)
|
|
559
|
+
if (!manifest) {
|
|
560
|
+
const seedBase = join(profile.seedDir, name);
|
|
561
|
+
const localArtifacts = [
|
|
562
|
+
unitPath,
|
|
563
|
+
`${seedBase}-seed.iso`,
|
|
564
|
+
`${seedBase}-user-data`,
|
|
565
|
+
`${seedBase}-meta-data`,
|
|
566
|
+
`${seedBase}-network-config`
|
|
567
|
+
];
|
|
568
|
+
if (localArtifacts.some(existsSync)) {
|
|
569
|
+
throw new MetalProvisionError("guest artifacts exist without owned inventory");
|
|
570
|
+
}
|
|
571
|
+
const [volume, active] = await Promise.all([
|
|
572
|
+
exec(["lvs", "--noheadings", lv]),
|
|
573
|
+
exec(["systemctl", "is-active", service])
|
|
574
|
+
]);
|
|
575
|
+
if (volume.exitCode === 0 || active.exitCode === 0) {
|
|
576
|
+
throw new MetalProvisionError("guest runtime exists without owned inventory");
|
|
577
|
+
}
|
|
557
578
|
return {};
|
|
579
|
+
}
|
|
558
580
|
const currentIdentity = manifest.computeKey === claim.computeKey && manifest.reference === claim.spec.reference && manifest.name === name;
|
|
559
581
|
const legacyPlatformIdentity = claim.bootstrap?.kind === "platform-genesis" && claim.computeKey === `platform:${name}` && claim.spec.reference === `platform:${name}` && manifest.name === name && manifest.disk === `/dev/${profile.volumeGroup}/${name}` && manifest.address === claim.spec.guestAddress && (manifest.unit === `fz-guest@${name}.service` || manifest.unit === `forgezero-guest@${name}.service`);
|
|
560
582
|
if (!currentIdentity && !legacyPlatformIdentity) {
|
|
561
583
|
throw new MetalProvisionError("compute identity conflicts with host inventory");
|
|
562
584
|
}
|
|
563
|
-
const service = `forgezero-guest@${name}.service`;
|
|
564
|
-
const unitPath = join(profile.unitDir, service);
|
|
565
585
|
if (existsSync(unitPath))
|
|
566
586
|
await checked(exec, ["systemctl", "disable", "--now", service]);
|
|
567
587
|
else if (legacyPlatformIdentity)
|
|
@@ -569,7 +589,6 @@ async function removeMetalGuest(profile, claim, exec) {
|
|
|
569
589
|
else if ((await exec(["systemctl", "is-active", service])).exitCode === 0) {
|
|
570
590
|
throw new MetalProvisionError("guest unit is active but its owned unit file is missing");
|
|
571
591
|
}
|
|
572
|
-
const lv = `/dev/${profile.volumeGroup}/${name}`;
|
|
573
592
|
if ((await exec(["lvs", "--noheadings", lv])).exitCode === 0)
|
|
574
593
|
await checked(exec, ["lvremove", "-fy", lv]);
|
|
575
594
|
for (const path of [
|
package/dist/metal-provision.js
CHANGED
|
@@ -552,16 +552,36 @@ async function removeMetalGuest(profile, claim, exec) {
|
|
|
552
552
|
throw new MetalProvisionError("create claim cannot remove a guest");
|
|
553
553
|
const name = claim.spec.guestName ?? guestNameFor(claim.computeKey);
|
|
554
554
|
const manifestPath = join(profile.stateDir, `${name}.json`);
|
|
555
|
+
const service = `forgezero-guest@${name}.service`;
|
|
556
|
+
const unitPath = join(profile.unitDir, service);
|
|
557
|
+
const lv = `/dev/${profile.volumeGroup}/${name}`;
|
|
555
558
|
const manifest = existsSync(manifestPath) ? JSON.parse(readFileSync(manifestPath, "utf8")) : legacyPlatformManifest(profile, claim, name);
|
|
556
|
-
if (!manifest)
|
|
559
|
+
if (!manifest) {
|
|
560
|
+
const seedBase = join(profile.seedDir, name);
|
|
561
|
+
const localArtifacts = [
|
|
562
|
+
unitPath,
|
|
563
|
+
`${seedBase}-seed.iso`,
|
|
564
|
+
`${seedBase}-user-data`,
|
|
565
|
+
`${seedBase}-meta-data`,
|
|
566
|
+
`${seedBase}-network-config`
|
|
567
|
+
];
|
|
568
|
+
if (localArtifacts.some(existsSync)) {
|
|
569
|
+
throw new MetalProvisionError("guest artifacts exist without owned inventory");
|
|
570
|
+
}
|
|
571
|
+
const [volume, active] = await Promise.all([
|
|
572
|
+
exec(["lvs", "--noheadings", lv]),
|
|
573
|
+
exec(["systemctl", "is-active", service])
|
|
574
|
+
]);
|
|
575
|
+
if (volume.exitCode === 0 || active.exitCode === 0) {
|
|
576
|
+
throw new MetalProvisionError("guest runtime exists without owned inventory");
|
|
577
|
+
}
|
|
557
578
|
return {};
|
|
579
|
+
}
|
|
558
580
|
const currentIdentity = manifest.computeKey === claim.computeKey && manifest.reference === claim.spec.reference && manifest.name === name;
|
|
559
581
|
const legacyPlatformIdentity = claim.bootstrap?.kind === "platform-genesis" && claim.computeKey === `platform:${name}` && claim.spec.reference === `platform:${name}` && manifest.name === name && manifest.disk === `/dev/${profile.volumeGroup}/${name}` && manifest.address === claim.spec.guestAddress && (manifest.unit === `fz-guest@${name}.service` || manifest.unit === `forgezero-guest@${name}.service`);
|
|
560
582
|
if (!currentIdentity && !legacyPlatformIdentity) {
|
|
561
583
|
throw new MetalProvisionError("compute identity conflicts with host inventory");
|
|
562
584
|
}
|
|
563
|
-
const service = `forgezero-guest@${name}.service`;
|
|
564
|
-
const unitPath = join(profile.unitDir, service);
|
|
565
585
|
if (existsSync(unitPath))
|
|
566
586
|
await checked(exec, ["systemctl", "disable", "--now", service]);
|
|
567
587
|
else if (legacyPlatformIdentity)
|
|
@@ -569,7 +589,6 @@ async function removeMetalGuest(profile, claim, exec) {
|
|
|
569
589
|
else if ((await exec(["systemctl", "is-active", service])).exitCode === 0) {
|
|
570
590
|
throw new MetalProvisionError("guest unit is active but its owned unit file is missing");
|
|
571
591
|
}
|
|
572
|
-
const lv = `/dev/${profile.volumeGroup}/${name}`;
|
|
573
592
|
if ((await exec(["lvs", "--noheadings", lv])).exitCode === 0)
|
|
574
593
|
await checked(exec, ["lvremove", "-fy", lv]);
|
|
575
594
|
for (const path of [
|
package/dist/migration-pull.js
CHANGED
|
@@ -15,8 +15,21 @@ class SignedNodeHttpError extends Error {
|
|
|
15
15
|
this.name = "SignedNodeHttpError";
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
|
+
function signedNodeApiUrl(value) {
|
|
19
|
+
let url;
|
|
20
|
+
try {
|
|
21
|
+
url = new URL(value);
|
|
22
|
+
} catch {
|
|
23
|
+
throw new Error("Agent API URL must be an absolute HTTPS URL.");
|
|
24
|
+
}
|
|
25
|
+
const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1";
|
|
26
|
+
if (url.protocol !== "https:" && !(loopback && url.protocol === "http:") || url.username || url.password || url.search || url.hash) {
|
|
27
|
+
throw new Error("Agent API URL must be HTTPS without credentials, query or fragment.");
|
|
28
|
+
}
|
|
29
|
+
return url;
|
|
30
|
+
}
|
|
18
31
|
async function postSignedNode(options, path, body) {
|
|
19
|
-
const url =
|
|
32
|
+
const url = signedNodeApiUrl(options.apiUrl);
|
|
20
33
|
url.pathname = `${url.pathname.replace(/\/$/, "")}/${path.replace(/^\//, "")}`.replace(/\/+/g, "/");
|
|
21
34
|
url.search = "";
|
|
22
35
|
url.hash = "";
|
package/dist/node-vault.js
CHANGED
|
@@ -135,8 +135,21 @@ class SignedNodeHttpError extends Error {
|
|
|
135
135
|
this.name = "SignedNodeHttpError";
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
|
+
function signedNodeApiUrl(value) {
|
|
139
|
+
let url;
|
|
140
|
+
try {
|
|
141
|
+
url = new URL(value);
|
|
142
|
+
} catch {
|
|
143
|
+
throw new Error("Agent API URL must be an absolute HTTPS URL.");
|
|
144
|
+
}
|
|
145
|
+
const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1";
|
|
146
|
+
if (url.protocol !== "https:" && !(loopback && url.protocol === "http:") || url.username || url.password || url.search || url.hash) {
|
|
147
|
+
throw new Error("Agent API URL must be HTTPS without credentials, query or fragment.");
|
|
148
|
+
}
|
|
149
|
+
return url;
|
|
150
|
+
}
|
|
138
151
|
async function postSignedNode(options, path, body) {
|
|
139
|
-
const url =
|
|
152
|
+
const url = signedNodeApiUrl(options.apiUrl);
|
|
140
153
|
url.pathname = `${url.pathname.replace(/\/$/, "")}/${path.replace(/^\//, "")}`.replace(/\/+/g, "/");
|
|
141
154
|
url.search = "";
|
|
142
155
|
url.hash = "";
|
|
@@ -196,7 +209,7 @@ function projectVaultCoordinate(key) {
|
|
|
196
209
|
function tenantNodeApiUrl(apiUrl, tenantSlug) {
|
|
197
210
|
if (!/^[a-z0-9][a-z0-9-]{0,62}$/.test(tenantSlug))
|
|
198
211
|
throw new Error("tenant slug is malformed");
|
|
199
|
-
const url =
|
|
212
|
+
const url = signedNodeApiUrl(apiUrl);
|
|
200
213
|
url.pathname = `/api/t/${encodeURIComponent(tenantSlug)}`;
|
|
201
214
|
url.search = "";
|
|
202
215
|
url.hash = "";
|
package/dist/provision.d.ts
CHANGED
|
@@ -127,12 +127,25 @@ export declare const DEPLOYMENT_GROUP = "forgezero-deploy";
|
|
|
127
127
|
export declare const VAULT_GROUP = "forgezero-vault";
|
|
128
128
|
export declare const LIFECYCLE_GROUP = "forgezero-lifecycle";
|
|
129
129
|
export declare const DEPLOYMENT_RUNNER_UNIT_PATH = "/etc/systemd/system/forgezero-deploy-runner.service";
|
|
130
|
+
export declare const AGENT_SOCKET_UNIT_PATH = "/etc/systemd/system/forgezero-agent.socket";
|
|
130
131
|
export declare const DEPLOYMENT_RUNNER_SOCKET = "/run/forgezero-deploy/runner.sock";
|
|
131
132
|
export declare const ENROLMENT_UNIT_PATH = "/etc/systemd/system/forgezero-agent-enrol.service";
|
|
132
133
|
export declare const LIFECYCLE_HELPER_UNIT_PATH = "/etc/systemd/system/forgezero-lifecycle-helper.service";
|
|
133
134
|
export declare const LIFECYCLE_HELPER_SOCKET = "/run/forgezero-lifecycle/helper.sock";
|
|
134
135
|
export declare const WARP_CONFIG_UNIT_PATH = "/etc/systemd/system/forgezero-warp-config.service";
|
|
135
136
|
export declare const WARP_SERVICE_DROP_IN_PATH = "/etc/systemd/system/warp-svc.service.d/forgezero.conf";
|
|
137
|
+
/** Root may execute only package-owned OS strategies selected by id + version. */
|
|
138
|
+
export declare function softwareHelperUnit(options: Pick<UnitOptions, 'binPath'>): string;
|
|
139
|
+
/** Fixed root boundary for verified, health-gated Agent replacement. */
|
|
140
|
+
export declare function agentUpdateHelperUnit(options: Pick<UnitOptions, 'binPath'>): string;
|
|
141
|
+
/**
|
|
142
|
+
* PID 1 owns the stable application endpoint.
|
|
143
|
+
*
|
|
144
|
+
* New connections wait in the kernel while the credential-bearing Agent drains
|
|
145
|
+
* and a health-gated replacement starts. This is supervision, not a background
|
|
146
|
+
* terminal, and it works identically on platform and tenant computes.
|
|
147
|
+
*/
|
|
148
|
+
export declare function agentSocketUnit(options: Pick<UnitOptions, 'socketPath'>): string;
|
|
136
149
|
export declare function warpConfigUnit(options: Pick<UnitOptions, 'binPath' | 'warpOrganization' | 'warpClientIdCredentialPath' | 'warpClientSecretCredentialPath'>): string;
|
|
137
150
|
export declare function warpServiceDropIn(): string;
|
|
138
151
|
export declare function lifecycleHelperUnit(options: Pick<UnitOptions, 'binPath' | 'lifecycleProfilePath' | 'lifecycleHelperSocketPath'>): string;
|