@forgezero/agent 0.1.27 → 0.1.29
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 +3 -2
- package/dist/agent-heartbeat.js +15 -2
- package/dist/fz-agent.js +41 -11
- package/dist/fz.js +63 -13
- package/dist/guest-enrolment.js +14 -1
- package/dist/metal-helper-socket.js +25 -7
- package/dist/metal-provision.js +25 -7
- package/dist/migration-pull.js +14 -1
- package/dist/node-vault.js +15 -2
- package/dist/provision.d.ts +8 -0
- package/dist/provision.js +67 -14
- package/dist/provisioning-pull.js +14 -1
- package/dist/signed-node-http.d.ts +10 -0
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -180,8 +180,9 @@ compute's systemd-sealed SSH deploy key, or a fine-grained HTTPS token selected
|
|
|
180
180
|
by a project-vault secret name. A signed claim contains the mode and secret name
|
|
181
181
|
only. The agent resolves the value from its scoped memory cache, limits the Git
|
|
182
182
|
header to the repository origin, never writes the token into a command, and
|
|
183
|
-
never exposes it to tenant pipeline steps.
|
|
184
|
-
|
|
183
|
+
never exposes it to tenant pipeline steps. These provider-neutral modes are the
|
|
184
|
+
complete contract; a GitHub App is not a missing dependency and would be added
|
|
185
|
+
later only as an explicit provider-specific mode with expiry and revocation tests.
|
|
185
186
|
|
|
186
187
|
An operator may also force a deployment and await the complete result over the
|
|
187
188
|
private control socket:
|
package/dist/agent-heartbeat.js
CHANGED
|
@@ -376,8 +376,21 @@ class SignedNodeHttpError extends Error {
|
|
|
376
376
|
this.name = "SignedNodeHttpError";
|
|
377
377
|
}
|
|
378
378
|
}
|
|
379
|
+
function signedNodeApiUrl(value) {
|
|
380
|
+
let url;
|
|
381
|
+
try {
|
|
382
|
+
url = new URL(value);
|
|
383
|
+
} catch {
|
|
384
|
+
throw new Error("Agent API URL must be an absolute HTTPS URL.");
|
|
385
|
+
}
|
|
386
|
+
const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1";
|
|
387
|
+
if (url.protocol !== "https:" && !(loopback && url.protocol === "http:") || url.username || url.password || url.search || url.hash) {
|
|
388
|
+
throw new Error("Agent API URL must be HTTPS without credentials, query or fragment.");
|
|
389
|
+
}
|
|
390
|
+
return url;
|
|
391
|
+
}
|
|
379
392
|
async function postSignedNode(options, path, body) {
|
|
380
|
-
const url =
|
|
393
|
+
const url = signedNodeApiUrl(options.apiUrl);
|
|
381
394
|
url.pathname = `${url.pathname.replace(/\/$/, "")}/${path.replace(/^\//, "")}`.replace(/\/+/g, "/");
|
|
382
395
|
url.search = "";
|
|
383
396
|
url.hash = "";
|
|
@@ -416,7 +429,7 @@ async function postSignedNode(options, path, body) {
|
|
|
416
429
|
}
|
|
417
430
|
|
|
418
431
|
// src/version.ts
|
|
419
|
-
var VERSION2 = "0.1.
|
|
432
|
+
var VERSION2 = "0.1.29";
|
|
420
433
|
|
|
421
434
|
// src/agent-heartbeat.ts
|
|
422
435
|
var unquote = (value) => value.replace(/^['"]|['"]$/g, "");
|
package/dist/fz-agent.js
CHANGED
|
@@ -4267,8 +4267,21 @@ class SignedNodeHttpError extends Error {
|
|
|
4267
4267
|
this.name = "SignedNodeHttpError";
|
|
4268
4268
|
}
|
|
4269
4269
|
}
|
|
4270
|
+
function signedNodeApiUrl(value) {
|
|
4271
|
+
let url;
|
|
4272
|
+
try {
|
|
4273
|
+
url = new URL(value);
|
|
4274
|
+
} catch {
|
|
4275
|
+
throw new Error("Agent API URL must be an absolute HTTPS URL.");
|
|
4276
|
+
}
|
|
4277
|
+
const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1";
|
|
4278
|
+
if (url.protocol !== "https:" && !(loopback && url.protocol === "http:") || url.username || url.password || url.search || url.hash) {
|
|
4279
|
+
throw new Error("Agent API URL must be HTTPS without credentials, query or fragment.");
|
|
4280
|
+
}
|
|
4281
|
+
return url;
|
|
4282
|
+
}
|
|
4270
4283
|
async function postSignedNode(options, path, body) {
|
|
4271
|
-
const url =
|
|
4284
|
+
const url = signedNodeApiUrl(options.apiUrl);
|
|
4272
4285
|
url.pathname = `${url.pathname.replace(/\/$/, "")}/${path.replace(/^\//, "")}`.replace(/\/+/g, "/");
|
|
4273
4286
|
url.search = "";
|
|
4274
4287
|
url.hash = "";
|
|
@@ -4328,7 +4341,7 @@ function projectVaultCoordinate(key) {
|
|
|
4328
4341
|
function tenantNodeApiUrl(apiUrl, tenantSlug) {
|
|
4329
4342
|
if (!/^[a-z0-9][a-z0-9-]{0,62}$/.test(tenantSlug))
|
|
4330
4343
|
throw new Error("tenant slug is malformed");
|
|
4331
|
-
const url =
|
|
4344
|
+
const url = signedNodeApiUrl(apiUrl);
|
|
4332
4345
|
url.pathname = `/api/t/${encodeURIComponent(tenantSlug)}`;
|
|
4333
4346
|
url.search = "";
|
|
4334
4347
|
url.hash = "";
|
|
@@ -6364,11 +6377,10 @@ ${attestationSetup}if [[ ! -x /usr/local/bin/bun ]]; then
|
|
|
6364
6377
|
install -m 0755 ${agentBun}/bin/bun /usr/local/bin/bun
|
|
6365
6378
|
rm -f /run/fz-bun-install
|
|
6366
6379
|
fi
|
|
6367
|
-
if [[ ! -x /usr/local/
|
|
6380
|
+
if [[ ! -x /usr/local/lib/forgezero/agent/fz-agent ]] || [[ "$(/usr/local/lib/forgezero/agent/fz-agent --version 2>/dev/null || true)" != "${profile.agentVersion}" ]]; then
|
|
6368
6381
|
env BUN_INSTALL=${agentBun} /usr/local/bin/bun add -g --no-cache --force @forgezero/agent@${profile.agentVersion}
|
|
6369
|
-
ln -sfn ${agentBun}/bin/fz-agent /usr/local/bin/fz-agent
|
|
6370
6382
|
fi
|
|
6371
|
-
env FZ_API=${profile.apiUrl} FZ_AGENT_BIN=/usr/local/
|
|
6383
|
+
env FZ_API=${profile.apiUrl} FZ_AGENT_BIN=/usr/local/lib/forgezero/agent/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" : ""}
|
|
6372
6384
|
`;
|
|
6373
6385
|
}
|
|
6374
6386
|
function cloudInit(profile, claim, manifest) {
|
|
@@ -6564,16 +6576,36 @@ async function removeMetalGuest(profile, claim, exec) {
|
|
|
6564
6576
|
throw new MetalProvisionError("create claim cannot remove a guest");
|
|
6565
6577
|
const name = claim.spec.guestName ?? guestNameFor(claim.computeKey);
|
|
6566
6578
|
const manifestPath = join2(profile.stateDir, `${name}.json`);
|
|
6579
|
+
const service = `forgezero-guest@${name}.service`;
|
|
6580
|
+
const unitPath = join2(profile.unitDir, service);
|
|
6581
|
+
const lv = `/dev/${profile.volumeGroup}/${name}`;
|
|
6567
6582
|
const manifest = existsSync5(manifestPath) ? JSON.parse(readFileSync4(manifestPath, "utf8")) : legacyPlatformManifest(profile, claim, name);
|
|
6568
|
-
if (!manifest)
|
|
6583
|
+
if (!manifest) {
|
|
6584
|
+
const seedBase = join2(profile.seedDir, name);
|
|
6585
|
+
const localArtifacts = [
|
|
6586
|
+
unitPath,
|
|
6587
|
+
`${seedBase}-seed.iso`,
|
|
6588
|
+
`${seedBase}-user-data`,
|
|
6589
|
+
`${seedBase}-meta-data`,
|
|
6590
|
+
`${seedBase}-network-config`
|
|
6591
|
+
];
|
|
6592
|
+
if (localArtifacts.some(existsSync5)) {
|
|
6593
|
+
throw new MetalProvisionError("guest artifacts exist without owned inventory");
|
|
6594
|
+
}
|
|
6595
|
+
const [volume, active] = await Promise.all([
|
|
6596
|
+
exec(["lvs", "--noheadings", lv]),
|
|
6597
|
+
exec(["systemctl", "is-active", service])
|
|
6598
|
+
]);
|
|
6599
|
+
if (volume.exitCode === 0 || active.exitCode === 0) {
|
|
6600
|
+
throw new MetalProvisionError("guest runtime exists without owned inventory");
|
|
6601
|
+
}
|
|
6569
6602
|
return {};
|
|
6603
|
+
}
|
|
6570
6604
|
const currentIdentity = manifest.computeKey === claim.computeKey && manifest.reference === claim.spec.reference && manifest.name === name;
|
|
6571
6605
|
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`);
|
|
6572
6606
|
if (!currentIdentity && !legacyPlatformIdentity) {
|
|
6573
6607
|
throw new MetalProvisionError("compute identity conflicts with host inventory");
|
|
6574
6608
|
}
|
|
6575
|
-
const service = `forgezero-guest@${name}.service`;
|
|
6576
|
-
const unitPath = join2(profile.unitDir, service);
|
|
6577
6609
|
if (existsSync5(unitPath))
|
|
6578
6610
|
await checked(exec, ["systemctl", "disable", "--now", service]);
|
|
6579
6611
|
else if (legacyPlatformIdentity)
|
|
@@ -6581,7 +6613,6 @@ async function removeMetalGuest(profile, claim, exec) {
|
|
|
6581
6613
|
else if ((await exec(["systemctl", "is-active", service])).exitCode === 0) {
|
|
6582
6614
|
throw new MetalProvisionError("guest unit is active but its owned unit file is missing");
|
|
6583
6615
|
}
|
|
6584
|
-
const lv = `/dev/${profile.volumeGroup}/${name}`;
|
|
6585
6616
|
if ((await exec(["lvs", "--noheadings", lv])).exitCode === 0)
|
|
6586
6617
|
await checked(exec, ["lvremove", "-fy", lv]);
|
|
6587
6618
|
for (const path of [
|
|
@@ -7844,7 +7875,7 @@ function requestAgentUpdate(request, socketPath = DEFAULT_AGENT_UPDATE_SOCKET, t
|
|
|
7844
7875
|
import { readFileSync as readFileSync7 } from "fs";
|
|
7845
7876
|
|
|
7846
7877
|
// src/version.ts
|
|
7847
|
-
var VERSION2 = "0.1.
|
|
7878
|
+
var VERSION2 = "0.1.29";
|
|
7848
7879
|
|
|
7849
7880
|
// src/agent-heartbeat.ts
|
|
7850
7881
|
var unquote = (value) => value.replace(/^['"]|['"]$/g, "");
|
|
@@ -8455,7 +8486,6 @@ if (import.meta.main) {
|
|
|
8455
8486
|
const attestationSource = existsSync13("/dev/sev-guest") ? createSnpAttestationSource() : undefined;
|
|
8456
8487
|
const running = runAgent({
|
|
8457
8488
|
socketPath: process.env.FZ_SOCKET_PATH ?? DEFAULT_SOCKET_PATH,
|
|
8458
|
-
listenFd: systemdListenFd(),
|
|
8459
8489
|
seedCredential: process.env.FZ_SEED_CREDENTIAL,
|
|
8460
8490
|
seedPath: process.env.FZ_SEED_PATH ?? DEFAULT_SEED_PATH,
|
|
8461
8491
|
nodeKey: process.env.FZ_NODE_KEY,
|
package/dist/fz.js
CHANGED
|
@@ -4886,7 +4886,7 @@ var AGENT_UPDATE_HELPER_UNIT_PATH = "/etc/systemd/system/forgezero-agent-update-
|
|
|
4886
4886
|
var MAX_REQUEST_BYTES = 8 * 1024;
|
|
4887
4887
|
|
|
4888
4888
|
// src/version.ts
|
|
4889
|
-
var VERSION2 = "0.1.
|
|
4889
|
+
var VERSION2 = "0.1.29";
|
|
4890
4890
|
|
|
4891
4891
|
// src/software.ts
|
|
4892
4892
|
var BUN_INSTALLER_SHA256 = "bab8acfb046aac8c72407bdcce903957665d655d7acaa3e11c7c4616beae68dd";
|
|
@@ -4973,6 +4973,7 @@ var VAULT_GROUP = "forgezero-vault";
|
|
|
4973
4973
|
var LIFECYCLE_GROUP = "forgezero-lifecycle";
|
|
4974
4974
|
var DEPLOYMENT_RUNNER_UNIT_PATH = "/etc/systemd/system/forgezero-deploy-runner.service";
|
|
4975
4975
|
var AGENT_SOCKET_UNIT_PATH = "/etc/systemd/system/forgezero-agent.socket";
|
|
4976
|
+
var AGENT_SOCKET_PROXY_UNIT_PATH = "/etc/systemd/system/forgezero-agent-proxy.service";
|
|
4976
4977
|
var DEPLOYMENT_RUNNER_SOCKET = "/run/forgezero-deploy/runner.sock";
|
|
4977
4978
|
var ENROLMENT_UNIT_PATH = "/etc/systemd/system/forgezero-agent-enrol.service";
|
|
4978
4979
|
var LIFECYCLE_HELPER_UNIT_PATH = "/etc/systemd/system/forgezero-lifecycle-helper.service";
|
|
@@ -5062,17 +5063,55 @@ SocketGroup=${VAULT_GROUP}
|
|
|
5062
5063
|
SocketMode=0660
|
|
5063
5064
|
DirectoryMode=0750
|
|
5064
5065
|
RemoveOnStop=true
|
|
5065
|
-
Service=forgezero-agent.service
|
|
5066
|
+
Service=forgezero-agent-proxy.service
|
|
5066
5067
|
|
|
5067
5068
|
[Install]
|
|
5068
5069
|
WantedBy=sockets.target
|
|
5069
5070
|
`;
|
|
5070
5071
|
}
|
|
5072
|
+
function agentSocketProxyUnit(options) {
|
|
5073
|
+
const backend = agentBackendSocketPath(options.socketPath);
|
|
5074
|
+
const user = options.user ?? "forgezero";
|
|
5075
|
+
return `[Unit]
|
|
5076
|
+
Description=ForgeZero application Vault socket proxy
|
|
5077
|
+
Documentation=https://www.forgezero.net/docs/agent
|
|
5078
|
+
Requires=forgezero-agent.service
|
|
5079
|
+
After=forgezero-agent.service
|
|
5080
|
+
|
|
5081
|
+
[Service]
|
|
5082
|
+
User=${user}
|
|
5083
|
+
Group=${VAULT_GROUP}
|
|
5084
|
+
ExecStart=/usr/lib/systemd/systemd-socket-proxyd ${backend}
|
|
5085
|
+
NoNewPrivileges=true
|
|
5086
|
+
PrivateTmp=true
|
|
5087
|
+
ProtectSystem=strict
|
|
5088
|
+
ProtectHome=true
|
|
5089
|
+
ProtectKernelTunables=true
|
|
5090
|
+
ProtectKernelModules=true
|
|
5091
|
+
ProtectControlGroups=true
|
|
5092
|
+
RestrictSUIDSGID=true
|
|
5093
|
+
RestrictRealtime=true
|
|
5094
|
+
MemoryDenyWriteExecute=true
|
|
5095
|
+
LockPersonality=true
|
|
5096
|
+
RestrictAddressFamilies=AF_UNIX
|
|
5097
|
+
`;
|
|
5098
|
+
}
|
|
5099
|
+
function agentBackendSocketPath(publicSocketPath) {
|
|
5100
|
+
const socket = systemdPath(publicSocketPath, "agent socket");
|
|
5101
|
+
const backend = `${socket}.backend`;
|
|
5102
|
+
if (Buffer.byteLength(backend) > 100)
|
|
5103
|
+
throw new Error("agent socket path is too long for a Unix socket");
|
|
5104
|
+
return backend;
|
|
5105
|
+
}
|
|
5071
5106
|
var systemdPath = (value, label) => {
|
|
5072
5107
|
if (!value || !/^\/[A-Za-z0-9._@/-]+$/.test(value))
|
|
5073
5108
|
throw new Error(`invalid ${label} path`);
|
|
5074
5109
|
return value;
|
|
5075
5110
|
};
|
|
5111
|
+
var awaitSocketCommand = (path) => {
|
|
5112
|
+
const socket = systemdPath(path, "readiness socket");
|
|
5113
|
+
return `for attempt in $(seq 1 100); do test -S ${socket} && exit 0; sleep 0.1; done; exit 1`;
|
|
5114
|
+
};
|
|
5076
5115
|
var validNodeHostname = (value) => !value || value.length <= 253 && value === value.toLowerCase() && value.split(".").length >= 3 && value.split(".").every((label) => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label));
|
|
5077
5116
|
function warpConfigUnit(options) {
|
|
5078
5117
|
if (!options.warpOrganization || !/^[a-z0-9][a-z0-9-]{0,62}$/i.test(options.warpOrganization)) {
|
|
@@ -5311,7 +5350,7 @@ function agentUnit(options) {
|
|
|
5311
5350
|
}
|
|
5312
5351
|
}
|
|
5313
5352
|
const environment = [
|
|
5314
|
-
`FZ_SOCKET_PATH=${options.socketPath}`,
|
|
5353
|
+
`FZ_SOCKET_PATH=${agentBackendSocketPath(options.socketPath)}`,
|
|
5315
5354
|
`FZ_CONTROL_SOCKET=${controlSocketPath}`,
|
|
5316
5355
|
`FZ_SEED_CREDENTIAL=agent-seed`,
|
|
5317
5356
|
`FZ_AGENT_MODE=${options.mode}`,
|
|
@@ -5391,7 +5430,6 @@ Type=simple
|
|
|
5391
5430
|
User=${user}
|
|
5392
5431
|
Group=${VAULT_GROUP}
|
|
5393
5432
|
${deploymentGroup}
|
|
5394
|
-
Sockets=forgezero-agent.socket
|
|
5395
5433
|
LoadCredentialEncrypted=agent-seed:${seedCredentialPath}
|
|
5396
5434
|
${gitCredential}${projectCredentials}${projectCredentials ? `
|
|
5397
5435
|
` : ""}${snpPrepare}ExecStart=${bin}
|
|
@@ -5479,6 +5517,7 @@ function planProvision(options) {
|
|
|
5479
5517
|
unit: agentUnit({ ...options, mode, lifecycleProfilePath, lifecycleHelperSocketPath }),
|
|
5480
5518
|
auxiliaryUnits: [
|
|
5481
5519
|
{ path: AGENT_SOCKET_UNIT_PATH, unit: agentSocketUnit(options) },
|
|
5520
|
+
{ path: AGENT_SOCKET_PROXY_UNIT_PATH, unit: agentSocketProxyUnit(options) },
|
|
5482
5521
|
{ path: AGENT_UPDATE_HELPER_UNIT_PATH, unit: agentUpdateHelperUnit(options) },
|
|
5483
5522
|
...deploymentEnabled ? [
|
|
5484
5523
|
{ path: DEPLOYMENT_RUNNER_UNIT_PATH, unit: deploymentRunnerUnit(options) },
|
|
@@ -5554,6 +5593,10 @@ function planProvision(options) {
|
|
|
5554
5593
|
label: "credential directory",
|
|
5555
5594
|
command: `install -d -o root -g root -m 0700 ${credentialDir}`
|
|
5556
5595
|
},
|
|
5596
|
+
{
|
|
5597
|
+
label: "Agent state directory",
|
|
5598
|
+
command: "install -d -o root -g root -m 0750 /var/lib/forgezero"
|
|
5599
|
+
},
|
|
5557
5600
|
{
|
|
5558
5601
|
label: "encrypted node identity",
|
|
5559
5602
|
command: `test -s ${seedCredentialPath} || { ` + `openssl rand -base64 32 | tr '+/' '-_' | tr -d '=\\n' | ` + `systemd-creds encrypt --name=agent-seed - ${seedCredentialPath}; ` + `chmod 0400 ${seedCredentialPath}; }`
|
|
@@ -5588,8 +5631,8 @@ function planProvision(options) {
|
|
|
5588
5631
|
command: "systemctl disable --now forgezero-deploy-runner.socket 2>/dev/null || true; rm -f /etc/systemd/system/forgezero-deploy-runner.socket; systemctl daemon-reload"
|
|
5589
5632
|
}] : [],
|
|
5590
5633
|
{
|
|
5591
|
-
label: "enable and
|
|
5592
|
-
command: `systemctl enable
|
|
5634
|
+
label: "enable and converge services",
|
|
5635
|
+
command: `systemctl enable ${[
|
|
5593
5636
|
"forgezero-agent.socket",
|
|
5594
5637
|
"forgezero-agent-update-helper.service",
|
|
5595
5638
|
...deploymentEnabled ? ["forgezero-deploy-runner.service", "forgezero-software-helper.service"] : [],
|
|
@@ -5597,25 +5640,32 @@ function planProvision(options) {
|
|
|
5597
5640
|
...warpEnabled ? ["forgezero-warp-config.service", "warp-svc.service"] : [],
|
|
5598
5641
|
...enrolmentEnabled ? ["forgezero-agent-enrol.service"] : [],
|
|
5599
5642
|
"forgezero-agent.service"
|
|
5600
|
-
].join(" ")}
|
|
5643
|
+
].join(" ")}; systemctl reset-failed forgezero-agent.service || true; systemctl restart ${[
|
|
5644
|
+
"forgezero-agent-update-helper.service",
|
|
5645
|
+
...deploymentEnabled ? ["forgezero-deploy-runner.service", "forgezero-software-helper.service"] : [],
|
|
5646
|
+
...lifecycleEnabled ? ["forgezero-lifecycle-helper.service"] : [],
|
|
5647
|
+
...warpEnabled ? ["forgezero-warp-config.service", "warp-svc.service"] : [],
|
|
5648
|
+
...enrolmentEnabled ? ["forgezero-agent-enrol.service"] : []
|
|
5649
|
+
].join(" ")}; systemctl restart forgezero-agent.socket; systemctl reset-failed forgezero-agent.service || true; systemctl restart forgezero-agent.service`
|
|
5601
5650
|
},
|
|
5602
5651
|
...enrolmentEnabled ? [{
|
|
5603
5652
|
label: "prove the compute binding is durable",
|
|
5604
5653
|
command: `test -s ${enrolStatePath}`
|
|
5605
5654
|
}] : [],
|
|
5606
5655
|
{ label: "prove it is running", command: "systemctl is-active forgezero-agent.service" },
|
|
5607
|
-
{ label: "prove the
|
|
5608
|
-
{ label: "prove the Agent
|
|
5656
|
+
{ label: "prove the public Vault socket exists", command: awaitSocketCommand(options.socketPath) },
|
|
5657
|
+
{ label: "prove the Agent Vault backend exists", command: awaitSocketCommand(agentBackendSocketPath(options.socketPath)) },
|
|
5658
|
+
{ label: "prove the Agent update helper exists", command: awaitSocketCommand(DEFAULT_AGENT_UPDATE_SOCKET) },
|
|
5609
5659
|
...deploymentEnabled ? [{
|
|
5610
5660
|
label: "prove the deployment runner socket exists",
|
|
5611
|
-
command:
|
|
5661
|
+
command: awaitSocketCommand(DEPLOYMENT_RUNNER_SOCKET)
|
|
5612
5662
|
}, {
|
|
5613
5663
|
label: "prove the software strategy helper socket exists",
|
|
5614
|
-
command:
|
|
5664
|
+
command: awaitSocketCommand(DEFAULT_SOFTWARE_HELPER_SOCKET)
|
|
5615
5665
|
}] : [],
|
|
5616
5666
|
...lifecycleEnabled ? [{
|
|
5617
5667
|
label: "prove the lifecycle helper socket exists",
|
|
5618
|
-
command:
|
|
5668
|
+
command: awaitSocketCommand(lifecycleHelperSocketPath)
|
|
5619
5669
|
}] : [],
|
|
5620
5670
|
...warpEnabled ? [{
|
|
5621
5671
|
label: "prove Cloudflare WARP is connected",
|
|
@@ -5623,7 +5673,7 @@ function planProvision(options) {
|
|
|
5623
5673
|
}] : [],
|
|
5624
5674
|
...options.repository ? [{
|
|
5625
5675
|
label: "prove the deployment control socket exists",
|
|
5626
|
-
command:
|
|
5676
|
+
command: awaitSocketCommand(options.controlSocketPath ?? "/run/forgezero/control.sock")
|
|
5627
5677
|
}] : []
|
|
5628
5678
|
]
|
|
5629
5679
|
};
|
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 = "";
|
|
@@ -352,11 +352,10 @@ ${attestationSetup}if [[ ! -x /usr/local/bin/bun ]]; then
|
|
|
352
352
|
install -m 0755 ${agentBun}/bin/bun /usr/local/bin/bun
|
|
353
353
|
rm -f /run/fz-bun-install
|
|
354
354
|
fi
|
|
355
|
-
if [[ ! -x /usr/local/
|
|
355
|
+
if [[ ! -x /usr/local/lib/forgezero/agent/fz-agent ]] || [[ "$(/usr/local/lib/forgezero/agent/fz-agent --version 2>/dev/null || true)" != "${profile.agentVersion}" ]]; then
|
|
356
356
|
env BUN_INSTALL=${agentBun} /usr/local/bin/bun add -g --no-cache --force @forgezero/agent@${profile.agentVersion}
|
|
357
|
-
ln -sfn ${agentBun}/bin/fz-agent /usr/local/bin/fz-agent
|
|
358
357
|
fi
|
|
359
|
-
env FZ_API=${profile.apiUrl} FZ_AGENT_BIN=/usr/local/
|
|
358
|
+
env FZ_API=${profile.apiUrl} FZ_AGENT_BIN=/usr/local/lib/forgezero/agent/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" : ""}
|
|
360
359
|
`;
|
|
361
360
|
}
|
|
362
361
|
function cloudInit(profile, claim, manifest) {
|
|
@@ -552,16 +551,36 @@ async function removeMetalGuest(profile, claim, exec) {
|
|
|
552
551
|
throw new MetalProvisionError("create claim cannot remove a guest");
|
|
553
552
|
const name = claim.spec.guestName ?? guestNameFor(claim.computeKey);
|
|
554
553
|
const manifestPath = join(profile.stateDir, `${name}.json`);
|
|
554
|
+
const service = `forgezero-guest@${name}.service`;
|
|
555
|
+
const unitPath = join(profile.unitDir, service);
|
|
556
|
+
const lv = `/dev/${profile.volumeGroup}/${name}`;
|
|
555
557
|
const manifest = existsSync(manifestPath) ? JSON.parse(readFileSync(manifestPath, "utf8")) : legacyPlatformManifest(profile, claim, name);
|
|
556
|
-
if (!manifest)
|
|
558
|
+
if (!manifest) {
|
|
559
|
+
const seedBase = join(profile.seedDir, name);
|
|
560
|
+
const localArtifacts = [
|
|
561
|
+
unitPath,
|
|
562
|
+
`${seedBase}-seed.iso`,
|
|
563
|
+
`${seedBase}-user-data`,
|
|
564
|
+
`${seedBase}-meta-data`,
|
|
565
|
+
`${seedBase}-network-config`
|
|
566
|
+
];
|
|
567
|
+
if (localArtifacts.some(existsSync)) {
|
|
568
|
+
throw new MetalProvisionError("guest artifacts exist without owned inventory");
|
|
569
|
+
}
|
|
570
|
+
const [volume, active] = await Promise.all([
|
|
571
|
+
exec(["lvs", "--noheadings", lv]),
|
|
572
|
+
exec(["systemctl", "is-active", service])
|
|
573
|
+
]);
|
|
574
|
+
if (volume.exitCode === 0 || active.exitCode === 0) {
|
|
575
|
+
throw new MetalProvisionError("guest runtime exists without owned inventory");
|
|
576
|
+
}
|
|
557
577
|
return {};
|
|
578
|
+
}
|
|
558
579
|
const currentIdentity = manifest.computeKey === claim.computeKey && manifest.reference === claim.spec.reference && manifest.name === name;
|
|
559
580
|
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
581
|
if (!currentIdentity && !legacyPlatformIdentity) {
|
|
561
582
|
throw new MetalProvisionError("compute identity conflicts with host inventory");
|
|
562
583
|
}
|
|
563
|
-
const service = `forgezero-guest@${name}.service`;
|
|
564
|
-
const unitPath = join(profile.unitDir, service);
|
|
565
584
|
if (existsSync(unitPath))
|
|
566
585
|
await checked(exec, ["systemctl", "disable", "--now", service]);
|
|
567
586
|
else if (legacyPlatformIdentity)
|
|
@@ -569,7 +588,6 @@ async function removeMetalGuest(profile, claim, exec) {
|
|
|
569
588
|
else if ((await exec(["systemctl", "is-active", service])).exitCode === 0) {
|
|
570
589
|
throw new MetalProvisionError("guest unit is active but its owned unit file is missing");
|
|
571
590
|
}
|
|
572
|
-
const lv = `/dev/${profile.volumeGroup}/${name}`;
|
|
573
591
|
if ((await exec(["lvs", "--noheadings", lv])).exitCode === 0)
|
|
574
592
|
await checked(exec, ["lvremove", "-fy", lv]);
|
|
575
593
|
for (const path of [
|
package/dist/metal-provision.js
CHANGED
|
@@ -352,11 +352,10 @@ ${attestationSetup}if [[ ! -x /usr/local/bin/bun ]]; then
|
|
|
352
352
|
install -m 0755 ${agentBun}/bin/bun /usr/local/bin/bun
|
|
353
353
|
rm -f /run/fz-bun-install
|
|
354
354
|
fi
|
|
355
|
-
if [[ ! -x /usr/local/
|
|
355
|
+
if [[ ! -x /usr/local/lib/forgezero/agent/fz-agent ]] || [[ "$(/usr/local/lib/forgezero/agent/fz-agent --version 2>/dev/null || true)" != "${profile.agentVersion}" ]]; then
|
|
356
356
|
env BUN_INSTALL=${agentBun} /usr/local/bin/bun add -g --no-cache --force @forgezero/agent@${profile.agentVersion}
|
|
357
|
-
ln -sfn ${agentBun}/bin/fz-agent /usr/local/bin/fz-agent
|
|
358
357
|
fi
|
|
359
|
-
env FZ_API=${profile.apiUrl} FZ_AGENT_BIN=/usr/local/
|
|
358
|
+
env FZ_API=${profile.apiUrl} FZ_AGENT_BIN=/usr/local/lib/forgezero/agent/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" : ""}
|
|
360
359
|
`;
|
|
361
360
|
}
|
|
362
361
|
function cloudInit(profile, claim, manifest) {
|
|
@@ -552,16 +551,36 @@ async function removeMetalGuest(profile, claim, exec) {
|
|
|
552
551
|
throw new MetalProvisionError("create claim cannot remove a guest");
|
|
553
552
|
const name = claim.spec.guestName ?? guestNameFor(claim.computeKey);
|
|
554
553
|
const manifestPath = join(profile.stateDir, `${name}.json`);
|
|
554
|
+
const service = `forgezero-guest@${name}.service`;
|
|
555
|
+
const unitPath = join(profile.unitDir, service);
|
|
556
|
+
const lv = `/dev/${profile.volumeGroup}/${name}`;
|
|
555
557
|
const manifest = existsSync(manifestPath) ? JSON.parse(readFileSync(manifestPath, "utf8")) : legacyPlatformManifest(profile, claim, name);
|
|
556
|
-
if (!manifest)
|
|
558
|
+
if (!manifest) {
|
|
559
|
+
const seedBase = join(profile.seedDir, name);
|
|
560
|
+
const localArtifacts = [
|
|
561
|
+
unitPath,
|
|
562
|
+
`${seedBase}-seed.iso`,
|
|
563
|
+
`${seedBase}-user-data`,
|
|
564
|
+
`${seedBase}-meta-data`,
|
|
565
|
+
`${seedBase}-network-config`
|
|
566
|
+
];
|
|
567
|
+
if (localArtifacts.some(existsSync)) {
|
|
568
|
+
throw new MetalProvisionError("guest artifacts exist without owned inventory");
|
|
569
|
+
}
|
|
570
|
+
const [volume, active] = await Promise.all([
|
|
571
|
+
exec(["lvs", "--noheadings", lv]),
|
|
572
|
+
exec(["systemctl", "is-active", service])
|
|
573
|
+
]);
|
|
574
|
+
if (volume.exitCode === 0 || active.exitCode === 0) {
|
|
575
|
+
throw new MetalProvisionError("guest runtime exists without owned inventory");
|
|
576
|
+
}
|
|
557
577
|
return {};
|
|
578
|
+
}
|
|
558
579
|
const currentIdentity = manifest.computeKey === claim.computeKey && manifest.reference === claim.spec.reference && manifest.name === name;
|
|
559
580
|
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
581
|
if (!currentIdentity && !legacyPlatformIdentity) {
|
|
561
582
|
throw new MetalProvisionError("compute identity conflicts with host inventory");
|
|
562
583
|
}
|
|
563
|
-
const service = `forgezero-guest@${name}.service`;
|
|
564
|
-
const unitPath = join(profile.unitDir, service);
|
|
565
584
|
if (existsSync(unitPath))
|
|
566
585
|
await checked(exec, ["systemctl", "disable", "--now", service]);
|
|
567
586
|
else if (legacyPlatformIdentity)
|
|
@@ -569,7 +588,6 @@ async function removeMetalGuest(profile, claim, exec) {
|
|
|
569
588
|
else if ((await exec(["systemctl", "is-active", service])).exitCode === 0) {
|
|
570
589
|
throw new MetalProvisionError("guest unit is active but its owned unit file is missing");
|
|
571
590
|
}
|
|
572
|
-
const lv = `/dev/${profile.volumeGroup}/${name}`;
|
|
573
591
|
if ((await exec(["lvs", "--noheadings", lv])).exitCode === 0)
|
|
574
592
|
await checked(exec, ["lvremove", "-fy", lv]);
|
|
575
593
|
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
|
@@ -128,6 +128,7 @@ 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
130
|
export declare const AGENT_SOCKET_UNIT_PATH = "/etc/systemd/system/forgezero-agent.socket";
|
|
131
|
+
export declare const AGENT_SOCKET_PROXY_UNIT_PATH = "/etc/systemd/system/forgezero-agent-proxy.service";
|
|
131
132
|
export declare const DEPLOYMENT_RUNNER_SOCKET = "/run/forgezero-deploy/runner.sock";
|
|
132
133
|
export declare const ENROLMENT_UNIT_PATH = "/etc/systemd/system/forgezero-agent-enrol.service";
|
|
133
134
|
export declare const LIFECYCLE_HELPER_UNIT_PATH = "/etc/systemd/system/forgezero-lifecycle-helper.service";
|
|
@@ -146,6 +147,13 @@ export declare function agentUpdateHelperUnit(options: Pick<UnitOptions, 'binPat
|
|
|
146
147
|
* terminal, and it works identically on platform and tenant computes.
|
|
147
148
|
*/
|
|
148
149
|
export declare function agentSocketUnit(options: Pick<UnitOptions, 'socketPath'>): string;
|
|
150
|
+
/**
|
|
151
|
+
* Bun does not support inheriting systemd's listening file descriptor. Keep
|
|
152
|
+
* PID 1 as the owner of the stable application socket and let systemd's small,
|
|
153
|
+
* credential-free proxy bridge it to the Agent-owned backend Unix socket.
|
|
154
|
+
*/
|
|
155
|
+
export declare function agentSocketProxyUnit(options: Pick<UnitOptions, 'socketPath' | 'user'>): string;
|
|
156
|
+
export declare function agentBackendSocketPath(publicSocketPath: string): string;
|
|
149
157
|
export declare function warpConfigUnit(options: Pick<UnitOptions, 'binPath' | 'warpOrganization' | 'warpClientIdCredentialPath' | 'warpClientSecretCredentialPath'>): string;
|
|
150
158
|
export declare function warpServiceDropIn(): string;
|
|
151
159
|
export declare function lifecycleHelperUnit(options: Pick<UnitOptions, 'binPath' | 'lifecycleProfilePath' | 'lifecycleHelperSocketPath'>): string;
|
package/dist/provision.js
CHANGED
|
@@ -554,7 +554,7 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
|
|
|
554
554
|
}
|
|
555
555
|
|
|
556
556
|
// src/version.ts
|
|
557
|
-
var VERSION2 = "0.1.
|
|
557
|
+
var VERSION2 = "0.1.29";
|
|
558
558
|
|
|
559
559
|
// src/provision.ts
|
|
560
560
|
function atLeast(version, floor) {
|
|
@@ -603,6 +603,7 @@ var VAULT_GROUP = "forgezero-vault";
|
|
|
603
603
|
var LIFECYCLE_GROUP = "forgezero-lifecycle";
|
|
604
604
|
var DEPLOYMENT_RUNNER_UNIT_PATH = "/etc/systemd/system/forgezero-deploy-runner.service";
|
|
605
605
|
var AGENT_SOCKET_UNIT_PATH = "/etc/systemd/system/forgezero-agent.socket";
|
|
606
|
+
var AGENT_SOCKET_PROXY_UNIT_PATH = "/etc/systemd/system/forgezero-agent-proxy.service";
|
|
606
607
|
var DEPLOYMENT_RUNNER_SOCKET = "/run/forgezero-deploy/runner.sock";
|
|
607
608
|
var ENROLMENT_UNIT_PATH = "/etc/systemd/system/forgezero-agent-enrol.service";
|
|
608
609
|
var LIFECYCLE_HELPER_UNIT_PATH = "/etc/systemd/system/forgezero-lifecycle-helper.service";
|
|
@@ -692,17 +693,55 @@ SocketGroup=${VAULT_GROUP}
|
|
|
692
693
|
SocketMode=0660
|
|
693
694
|
DirectoryMode=0750
|
|
694
695
|
RemoveOnStop=true
|
|
695
|
-
Service=forgezero-agent.service
|
|
696
|
+
Service=forgezero-agent-proxy.service
|
|
696
697
|
|
|
697
698
|
[Install]
|
|
698
699
|
WantedBy=sockets.target
|
|
699
700
|
`;
|
|
700
701
|
}
|
|
702
|
+
function agentSocketProxyUnit(options) {
|
|
703
|
+
const backend = agentBackendSocketPath(options.socketPath);
|
|
704
|
+
const user = options.user ?? "forgezero";
|
|
705
|
+
return `[Unit]
|
|
706
|
+
Description=ForgeZero application Vault socket proxy
|
|
707
|
+
Documentation=https://www.forgezero.net/docs/agent
|
|
708
|
+
Requires=forgezero-agent.service
|
|
709
|
+
After=forgezero-agent.service
|
|
710
|
+
|
|
711
|
+
[Service]
|
|
712
|
+
User=${user}
|
|
713
|
+
Group=${VAULT_GROUP}
|
|
714
|
+
ExecStart=/usr/lib/systemd/systemd-socket-proxyd ${backend}
|
|
715
|
+
NoNewPrivileges=true
|
|
716
|
+
PrivateTmp=true
|
|
717
|
+
ProtectSystem=strict
|
|
718
|
+
ProtectHome=true
|
|
719
|
+
ProtectKernelTunables=true
|
|
720
|
+
ProtectKernelModules=true
|
|
721
|
+
ProtectControlGroups=true
|
|
722
|
+
RestrictSUIDSGID=true
|
|
723
|
+
RestrictRealtime=true
|
|
724
|
+
MemoryDenyWriteExecute=true
|
|
725
|
+
LockPersonality=true
|
|
726
|
+
RestrictAddressFamilies=AF_UNIX
|
|
727
|
+
`;
|
|
728
|
+
}
|
|
729
|
+
function agentBackendSocketPath(publicSocketPath) {
|
|
730
|
+
const socket = systemdPath(publicSocketPath, "agent socket");
|
|
731
|
+
const backend = `${socket}.backend`;
|
|
732
|
+
if (Buffer.byteLength(backend) > 100)
|
|
733
|
+
throw new Error("agent socket path is too long for a Unix socket");
|
|
734
|
+
return backend;
|
|
735
|
+
}
|
|
701
736
|
var systemdPath = (value, label) => {
|
|
702
737
|
if (!value || !/^\/[A-Za-z0-9._@/-]+$/.test(value))
|
|
703
738
|
throw new Error(`invalid ${label} path`);
|
|
704
739
|
return value;
|
|
705
740
|
};
|
|
741
|
+
var awaitSocketCommand = (path) => {
|
|
742
|
+
const socket = systemdPath(path, "readiness socket");
|
|
743
|
+
return `for attempt in $(seq 1 100); do test -S ${socket} && exit 0; sleep 0.1; done; exit 1`;
|
|
744
|
+
};
|
|
706
745
|
var validNodeHostname = (value) => !value || value.length <= 253 && value === value.toLowerCase() && value.split(".").length >= 3 && value.split(".").every((label) => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label));
|
|
707
746
|
function warpConfigUnit(options) {
|
|
708
747
|
if (!options.warpOrganization || !/^[a-z0-9][a-z0-9-]{0,62}$/i.test(options.warpOrganization)) {
|
|
@@ -941,7 +980,7 @@ function agentUnit(options) {
|
|
|
941
980
|
}
|
|
942
981
|
}
|
|
943
982
|
const environment = [
|
|
944
|
-
`FZ_SOCKET_PATH=${options.socketPath}`,
|
|
983
|
+
`FZ_SOCKET_PATH=${agentBackendSocketPath(options.socketPath)}`,
|
|
945
984
|
`FZ_CONTROL_SOCKET=${controlSocketPath}`,
|
|
946
985
|
`FZ_SEED_CREDENTIAL=agent-seed`,
|
|
947
986
|
`FZ_AGENT_MODE=${options.mode}`,
|
|
@@ -1021,7 +1060,6 @@ Type=simple
|
|
|
1021
1060
|
User=${user}
|
|
1022
1061
|
Group=${VAULT_GROUP}
|
|
1023
1062
|
${deploymentGroup}
|
|
1024
|
-
Sockets=forgezero-agent.socket
|
|
1025
1063
|
LoadCredentialEncrypted=agent-seed:${seedCredentialPath}
|
|
1026
1064
|
${gitCredential}${projectCredentials}${projectCredentials ? `
|
|
1027
1065
|
` : ""}${snpPrepare}ExecStart=${bin}
|
|
@@ -1109,6 +1147,7 @@ function planProvision(options) {
|
|
|
1109
1147
|
unit: agentUnit({ ...options, mode, lifecycleProfilePath, lifecycleHelperSocketPath }),
|
|
1110
1148
|
auxiliaryUnits: [
|
|
1111
1149
|
{ path: AGENT_SOCKET_UNIT_PATH, unit: agentSocketUnit(options) },
|
|
1150
|
+
{ path: AGENT_SOCKET_PROXY_UNIT_PATH, unit: agentSocketProxyUnit(options) },
|
|
1112
1151
|
{ path: AGENT_UPDATE_HELPER_UNIT_PATH, unit: agentUpdateHelperUnit(options) },
|
|
1113
1152
|
...deploymentEnabled ? [
|
|
1114
1153
|
{ path: DEPLOYMENT_RUNNER_UNIT_PATH, unit: deploymentRunnerUnit(options) },
|
|
@@ -1184,6 +1223,10 @@ function planProvision(options) {
|
|
|
1184
1223
|
label: "credential directory",
|
|
1185
1224
|
command: `install -d -o root -g root -m 0700 ${credentialDir}`
|
|
1186
1225
|
},
|
|
1226
|
+
{
|
|
1227
|
+
label: "Agent state directory",
|
|
1228
|
+
command: "install -d -o root -g root -m 0750 /var/lib/forgezero"
|
|
1229
|
+
},
|
|
1187
1230
|
{
|
|
1188
1231
|
label: "encrypted node identity",
|
|
1189
1232
|
command: `test -s ${seedCredentialPath} || { ` + `openssl rand -base64 32 | tr '+/' '-_' | tr -d '=\\n' | ` + `systemd-creds encrypt --name=agent-seed - ${seedCredentialPath}; ` + `chmod 0400 ${seedCredentialPath}; }`
|
|
@@ -1218,8 +1261,8 @@ function planProvision(options) {
|
|
|
1218
1261
|
command: "systemctl disable --now forgezero-deploy-runner.socket 2>/dev/null || true; rm -f /etc/systemd/system/forgezero-deploy-runner.socket; systemctl daemon-reload"
|
|
1219
1262
|
}] : [],
|
|
1220
1263
|
{
|
|
1221
|
-
label: "enable and
|
|
1222
|
-
command: `systemctl enable
|
|
1264
|
+
label: "enable and converge services",
|
|
1265
|
+
command: `systemctl enable ${[
|
|
1223
1266
|
"forgezero-agent.socket",
|
|
1224
1267
|
"forgezero-agent-update-helper.service",
|
|
1225
1268
|
...deploymentEnabled ? ["forgezero-deploy-runner.service", "forgezero-software-helper.service"] : [],
|
|
@@ -1227,25 +1270,32 @@ function planProvision(options) {
|
|
|
1227
1270
|
...warpEnabled ? ["forgezero-warp-config.service", "warp-svc.service"] : [],
|
|
1228
1271
|
...enrolmentEnabled ? ["forgezero-agent-enrol.service"] : [],
|
|
1229
1272
|
"forgezero-agent.service"
|
|
1230
|
-
].join(" ")}
|
|
1273
|
+
].join(" ")}; systemctl reset-failed forgezero-agent.service || true; systemctl restart ${[
|
|
1274
|
+
"forgezero-agent-update-helper.service",
|
|
1275
|
+
...deploymentEnabled ? ["forgezero-deploy-runner.service", "forgezero-software-helper.service"] : [],
|
|
1276
|
+
...lifecycleEnabled ? ["forgezero-lifecycle-helper.service"] : [],
|
|
1277
|
+
...warpEnabled ? ["forgezero-warp-config.service", "warp-svc.service"] : [],
|
|
1278
|
+
...enrolmentEnabled ? ["forgezero-agent-enrol.service"] : []
|
|
1279
|
+
].join(" ")}; systemctl restart forgezero-agent.socket; systemctl reset-failed forgezero-agent.service || true; systemctl restart forgezero-agent.service`
|
|
1231
1280
|
},
|
|
1232
1281
|
...enrolmentEnabled ? [{
|
|
1233
1282
|
label: "prove the compute binding is durable",
|
|
1234
1283
|
command: `test -s ${enrolStatePath}`
|
|
1235
1284
|
}] : [],
|
|
1236
1285
|
{ label: "prove it is running", command: "systemctl is-active forgezero-agent.service" },
|
|
1237
|
-
{ label: "prove the
|
|
1238
|
-
{ label: "prove the Agent
|
|
1286
|
+
{ label: "prove the public Vault socket exists", command: awaitSocketCommand(options.socketPath) },
|
|
1287
|
+
{ label: "prove the Agent Vault backend exists", command: awaitSocketCommand(agentBackendSocketPath(options.socketPath)) },
|
|
1288
|
+
{ label: "prove the Agent update helper exists", command: awaitSocketCommand(DEFAULT_AGENT_UPDATE_SOCKET) },
|
|
1239
1289
|
...deploymentEnabled ? [{
|
|
1240
1290
|
label: "prove the deployment runner socket exists",
|
|
1241
|
-
command:
|
|
1291
|
+
command: awaitSocketCommand(DEPLOYMENT_RUNNER_SOCKET)
|
|
1242
1292
|
}, {
|
|
1243
1293
|
label: "prove the software strategy helper socket exists",
|
|
1244
|
-
command:
|
|
1294
|
+
command: awaitSocketCommand(DEFAULT_SOFTWARE_HELPER_SOCKET)
|
|
1245
1295
|
}] : [],
|
|
1246
1296
|
...lifecycleEnabled ? [{
|
|
1247
1297
|
label: "prove the lifecycle helper socket exists",
|
|
1248
|
-
command:
|
|
1298
|
+
command: awaitSocketCommand(lifecycleHelperSocketPath)
|
|
1249
1299
|
}] : [],
|
|
1250
1300
|
...warpEnabled ? [{
|
|
1251
1301
|
label: "prove Cloudflare WARP is connected",
|
|
@@ -1253,7 +1303,7 @@ function planProvision(options) {
|
|
|
1253
1303
|
}] : [],
|
|
1254
1304
|
...options.repository ? [{
|
|
1255
1305
|
label: "prove the deployment control socket exists",
|
|
1256
|
-
command:
|
|
1306
|
+
command: awaitSocketCommand(options.controlSocketPath ?? "/run/forgezero/control.sock")
|
|
1257
1307
|
}] : []
|
|
1258
1308
|
]
|
|
1259
1309
|
};
|
|
@@ -1271,7 +1321,9 @@ export {
|
|
|
1271
1321
|
agentUpdateHelperUnit,
|
|
1272
1322
|
agentUnit,
|
|
1273
1323
|
agentSocketUnit,
|
|
1324
|
+
agentSocketProxyUnit,
|
|
1274
1325
|
agentEnrolmentUnit,
|
|
1326
|
+
agentBackendSocketPath,
|
|
1275
1327
|
WARP_SERVICE_DROP_IN_PATH,
|
|
1276
1328
|
WARP_CONFIG_UNIT_PATH,
|
|
1277
1329
|
VAULT_GROUP,
|
|
@@ -1285,5 +1337,6 @@ export {
|
|
|
1285
1337
|
DEPLOYMENT_RUNNER_SOCKET,
|
|
1286
1338
|
DEPLOYMENT_GROUP,
|
|
1287
1339
|
CAPABILITY_CHECKS,
|
|
1288
|
-
AGENT_SOCKET_UNIT_PATH
|
|
1340
|
+
AGENT_SOCKET_UNIT_PATH,
|
|
1341
|
+
AGENT_SOCKET_PROXY_UNIT_PATH
|
|
1289
1342
|
};
|
|
@@ -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 = "";
|
|
@@ -10,5 +10,15 @@ export declare class SignedNodeHttpError extends Error {
|
|
|
10
10
|
readonly status: number;
|
|
11
11
|
constructor(status: number, message: string);
|
|
12
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* Validate the one origin every outbound Agent client shares.
|
|
15
|
+
*
|
|
16
|
+
* Hybrid request authentication and a sealed response protect the payload, but
|
|
17
|
+
* they do not hide routing metadata. Accepting plain HTTP for a remote API
|
|
18
|
+
* would still expose node identity, operation timing and ciphertext to an
|
|
19
|
+
* on-path observer. Loopback HTTP remains available for local development and
|
|
20
|
+
* for a supervised same-host API; production traffic must use HTTPS.
|
|
21
|
+
*/
|
|
22
|
+
export declare function signedNodeApiUrl(value: string): URL;
|
|
13
23
|
/** One implementation of the hybrid-signed machine HTTP contract. */
|
|
14
24
|
export declare function postSignedNode<T>(options: SignedNodeHttpOptions, path: string, body: object): Promise<T>;
|
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.
|
|
2
|
+
export declare const VERSION = "0.1.29";
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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.
|
|
4
|
+
"version": "0.1.29",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"check": "tsc --noEmit",
|