@forgezero/agent 0.1.32 → 0.1.33
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 +60 -5
- package/dist/agent-heartbeat.d.ts +2 -0
- package/dist/agent-heartbeat.js +27 -13
- package/dist/agent-update-helper.js +1 -0
- package/dist/attestation-client.d.ts +2 -0
- package/dist/cli/agent-install.d.ts +6 -0
- package/dist/cli/genesis.d.ts +7 -48
- package/dist/cli/index.d.ts +5 -0
- package/dist/cli/session-store.d.ts +22 -0
- package/dist/deployment-pull.d.ts +2 -0
- package/dist/deployment.d.ts +3 -0
- package/dist/egress-policy.d.ts +50 -0
- package/dist/fz-agent.js +4699 -1655
- package/dist/fz.js +10336 -7617
- package/dist/git-egress.d.ts +35 -0
- package/dist/index.d.ts +17 -0
- package/dist/metal-helper-socket.js +12 -1
- package/dist/metal-provision.d.ts +2 -0
- package/dist/metal-provision.js +12 -1
- package/dist/migration-pull.d.ts +2 -0
- package/dist/migration-pull.js +18 -6
- package/dist/node-vault.d.ts +2 -0
- package/dist/node-vault.js +2 -1
- package/dist/provision.d.ts +13 -1
- package/dist/provision.js +178 -4
- package/dist/provisioning-pull.d.ts +2 -0
- package/dist/provisioning-pull.js +20 -7
- package/dist/telemetry-runtime.d.ts +20 -0
- package/dist/telemetry.d.ts +56 -0
- package/dist/version.d.ts +1 -1
- package/package.json +10 -10
package/dist/provision.js
CHANGED
|
@@ -229,6 +229,7 @@ var AGENT_UPDATE_JOURNAL = "/var/lib/forgezero/agent-update.json";
|
|
|
229
229
|
var AGENT_UPDATE_RECEIPT = "/var/lib/forgezero/agent-update-receipt.json";
|
|
230
230
|
var MAX_REQUEST_BYTES = 8 * 1024;
|
|
231
231
|
var COMPUTE_HELPER_UNITS = [
|
|
232
|
+
"forgezero-agent-egress.service",
|
|
232
233
|
"forgezero-deploy-runner.service",
|
|
233
234
|
"forgezero-lifecycle-helper.service",
|
|
234
235
|
"forgezero-software-helper.service"
|
|
@@ -883,9 +884,71 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
|
|
|
883
884
|
}
|
|
884
885
|
|
|
885
886
|
// src/version.ts
|
|
886
|
-
var VERSION3 = "0.1.
|
|
887
|
+
var VERSION3 = "0.1.33";
|
|
888
|
+
|
|
889
|
+
// src/egress-policy.ts
|
|
890
|
+
import { realpathSync } from "node:fs";
|
|
891
|
+
var AGENT_EGRESS_TABLE = "forgezero_agent_egress";
|
|
892
|
+
var SYSTEMD_RESOLVED_ADDRESS = "127.0.0.53";
|
|
893
|
+
var BLOCKED_IPV4 = [
|
|
894
|
+
"0.0.0.0/8",
|
|
895
|
+
"10.0.0.0/8",
|
|
896
|
+
"100.64.0.0/10",
|
|
897
|
+
"127.0.0.0/8",
|
|
898
|
+
"168.63.129.16/32",
|
|
899
|
+
"169.254.0.0/16",
|
|
900
|
+
"172.16.0.0/12",
|
|
901
|
+
"192.0.0.0/24",
|
|
902
|
+
"192.0.2.0/24",
|
|
903
|
+
"192.88.99.0/24",
|
|
904
|
+
"192.168.0.0/16",
|
|
905
|
+
"198.18.0.0/15",
|
|
906
|
+
"198.51.100.0/24",
|
|
907
|
+
"203.0.113.0/24",
|
|
908
|
+
"224.0.0.0/4",
|
|
909
|
+
"240.0.0.0/4"
|
|
910
|
+
];
|
|
911
|
+
var BLOCKED_IPV6 = [
|
|
912
|
+
"::/128",
|
|
913
|
+
"::1/128",
|
|
914
|
+
"::ffff:0:0/96",
|
|
915
|
+
"64:ff9b::/96",
|
|
916
|
+
"64:ff9b:1::/48",
|
|
917
|
+
"100::/64",
|
|
918
|
+
"fc00::/7",
|
|
919
|
+
"fec0::/10",
|
|
920
|
+
"fe80::/10",
|
|
921
|
+
"ff00::/8",
|
|
922
|
+
"2001::/32",
|
|
923
|
+
"2001:2::/48",
|
|
924
|
+
"2001:10::/28",
|
|
925
|
+
"2001:20::/28",
|
|
926
|
+
"2001:db8::/32",
|
|
927
|
+
"2002::/16",
|
|
928
|
+
"3fff::/20"
|
|
929
|
+
];
|
|
930
|
+
var normalizeEgressTcpPorts = (ports) => {
|
|
931
|
+
for (const port of ports) {
|
|
932
|
+
if (!Number.isSafeInteger(port) || port < 1 || port > 65535) {
|
|
933
|
+
throw new Error("Agent egress policy refuses an invalid loopback TCP port.");
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
return [...new Set(ports)].sort((left, right) => left - right);
|
|
937
|
+
};
|
|
938
|
+
function systemdAgentEgressDirectives(loopbackTcpPorts = []) {
|
|
939
|
+
const ports = normalizeEgressTcpPorts(loopbackTcpPorts);
|
|
940
|
+
return [
|
|
941
|
+
"RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6",
|
|
942
|
+
`IPAddressAllow=${SYSTEMD_RESOLVED_ADDRESS}/32`,
|
|
943
|
+
...ports.length > 0 ? ["IPAddressAllow=127.0.0.1/32", "IPAddressAllow=::1/128"] : [],
|
|
944
|
+
...BLOCKED_IPV4.map((network) => `IPAddressDeny=${network}`),
|
|
945
|
+
...BLOCKED_IPV6.map((network) => `IPAddressDeny=${network}`)
|
|
946
|
+
].join(`
|
|
947
|
+
`);
|
|
948
|
+
}
|
|
887
949
|
|
|
888
950
|
// src/provision.ts
|
|
951
|
+
import { isIP } from "node:net";
|
|
889
952
|
function atLeast(version, floor) {
|
|
890
953
|
const parse = (value) => (value.trim().replace(/^v/, "").match(/\d+/g) ?? []).slice(0, 3).map(Number);
|
|
891
954
|
const got = parse(version);
|
|
@@ -939,6 +1002,63 @@ var LIFECYCLE_HELPER_UNIT_PATH = "/etc/systemd/system/forgezero-lifecycle-helper
|
|
|
939
1002
|
var LIFECYCLE_HELPER_SOCKET = "/run/forgezero-lifecycle/helper.sock";
|
|
940
1003
|
var WARP_CONFIG_UNIT_PATH = "/etc/systemd/system/forgezero-warp-config.service";
|
|
941
1004
|
var WARP_SERVICE_DROP_IN_PATH = "/etc/systemd/system/warp-svc.service.d/forgezero.conf";
|
|
1005
|
+
var AGENT_EGRESS_UNIT_PATH = "/etc/systemd/system/forgezero-agent-egress.service";
|
|
1006
|
+
var DEFAULT_RUNNER_PUBLIC_TCP_PORTS = [443];
|
|
1007
|
+
function agentEgressUnit(options) {
|
|
1008
|
+
const bin = options.binPath ?? "fz-agent";
|
|
1009
|
+
const user = options.user ?? "forgezero";
|
|
1010
|
+
if (!/^[a-z_][a-z0-9_-]{0,30}$/.test(user))
|
|
1011
|
+
throw new Error("invalid Agent service user");
|
|
1012
|
+
const deploymentEnabled = Boolean(options.repository || options.pullDeployments);
|
|
1013
|
+
const runnerLoopbackPorts = normalizeEgressTcpPorts(options.runnerLoopbackPorts ?? []);
|
|
1014
|
+
const runnerPublicTcpPorts = normalizeEgressTcpPorts(options.runnerPublicTcpPorts ?? DEFAULT_RUNNER_PUBLIC_TCP_PORTS);
|
|
1015
|
+
if (deploymentEnabled && runnerPublicTcpPorts.length < 1) {
|
|
1016
|
+
throw new Error("deployed project runner needs at least one vetted public TCP port");
|
|
1017
|
+
}
|
|
1018
|
+
if (deploymentEnabled)
|
|
1019
|
+
systemdAgentEgressDirectives(runnerLoopbackPorts);
|
|
1020
|
+
const users = [user, ...deploymentEnabled ? [DEPLOYMENT_RUNNER_USER] : []];
|
|
1021
|
+
const runnerGrant = deploymentEnabled ? ` --loopback-user=${DEPLOYMENT_RUNNER_USER}` + runnerLoopbackPorts.map((port) => ` --loopback-tcp-port=${port}`).join("") + runnerPublicTcpPorts.map((port) => ` --public-tcp-port=${port}`).join("") : "";
|
|
1022
|
+
const policyProofs = deploymentEnabled ? [
|
|
1023
|
+
...runnerLoopbackPorts.length > 0 ? [`loopback=.*:${runnerLoopbackPorts.join(",")}`] : [],
|
|
1024
|
+
`public-tcp=${runnerPublicTcpPorts.join(",")}`
|
|
1025
|
+
].map((pattern) => `ExecStartPost=/bin/sh -c '/usr/sbin/nft --numeric list table inet ${AGENT_EGRESS_TABLE} | /usr/bin/grep -q "${pattern}"'`).join(`
|
|
1026
|
+
`) : "";
|
|
1027
|
+
return `[Unit]
|
|
1028
|
+
Description=ForgeZero Agent host egress policy
|
|
1029
|
+
Documentation=https://www.forgezero.net/docs/agent
|
|
1030
|
+
After=systemd-resolved.service nftables.service
|
|
1031
|
+
Requires=systemd-resolved.service
|
|
1032
|
+
Before=forgezero-agent-enrol.service forgezero-agent.service
|
|
1033
|
+
|
|
1034
|
+
[Service]
|
|
1035
|
+
Type=notify
|
|
1036
|
+
NotifyAccess=all
|
|
1037
|
+
User=root
|
|
1038
|
+
Group=root
|
|
1039
|
+
ExecStart=${bin} egress-policy ${users.map((name) => `--user=${name}`).join(" ")}${runnerGrant}
|
|
1040
|
+
${policyProofs}
|
|
1041
|
+
Restart=on-failure
|
|
1042
|
+
RestartSec=2
|
|
1043
|
+
LimitCORE=0
|
|
1044
|
+
NoNewPrivileges=true
|
|
1045
|
+
PrivateTmp=true
|
|
1046
|
+
ProtectSystem=strict
|
|
1047
|
+
ProtectHome=true
|
|
1048
|
+
ProtectKernelTunables=true
|
|
1049
|
+
ProtectKernelModules=true
|
|
1050
|
+
ProtectControlGroups=true
|
|
1051
|
+
RestrictSUIDSGID=true
|
|
1052
|
+
RestrictRealtime=true
|
|
1053
|
+
MemoryDenyWriteExecute=true
|
|
1054
|
+
LockPersonality=true
|
|
1055
|
+
CapabilityBoundingSet=CAP_NET_ADMIN
|
|
1056
|
+
RestrictAddressFamilies=AF_UNIX AF_NETLINK
|
|
1057
|
+
|
|
1058
|
+
[Install]
|
|
1059
|
+
WantedBy=multi-user.target
|
|
1060
|
+
`;
|
|
1061
|
+
}
|
|
942
1062
|
function softwareHelperUnit(options) {
|
|
943
1063
|
const bin = options.binPath ?? "fz-agent";
|
|
944
1064
|
return `[Unit]
|
|
@@ -1183,11 +1303,16 @@ function agentEnrolmentUnit(options) {
|
|
|
1183
1303
|
` : ""
|
|
1184
1304
|
].join("");
|
|
1185
1305
|
const stateDir = options.enrolStatePath.replace(/\/[^/]+$/, "");
|
|
1306
|
+
const egressDependency = options.enforceEgress ? `After=forgezero-agent-egress.service
|
|
1307
|
+
Requires=forgezero-agent-egress.service
|
|
1308
|
+
BindsTo=forgezero-agent-egress.service
|
|
1309
|
+
` : "";
|
|
1310
|
+
const egressDirectives = options.enforceEgress ? systemdAgentEgressDirectives() : "";
|
|
1186
1311
|
return `[Unit]
|
|
1187
1312
|
Description=Bind this machine to its ForgeZero compute
|
|
1188
1313
|
After=network-online.target
|
|
1189
1314
|
Wants=network-online.target
|
|
1190
|
-
Before=forgezero-agent.service
|
|
1315
|
+
${egressDependency}Before=forgezero-agent.service
|
|
1191
1316
|
ConditionPathExists=!${options.enrolStatePath}
|
|
1192
1317
|
|
|
1193
1318
|
[Service]
|
|
@@ -1210,6 +1335,7 @@ ProtectSystem=strict
|
|
|
1210
1335
|
ProtectHome=true
|
|
1211
1336
|
ReadWritePaths=${stateDir}
|
|
1212
1337
|
LimitCORE=0
|
|
1338
|
+
${egressDirectives}
|
|
1213
1339
|
|
|
1214
1340
|
[Install]
|
|
1215
1341
|
WantedBy=multi-user.target
|
|
@@ -1219,9 +1345,15 @@ function deploymentRunnerUnit(options) {
|
|
|
1219
1345
|
const bin = options.binPath ?? "fz-agent";
|
|
1220
1346
|
const root = options.deployRoot ?? "/opt/forgezero";
|
|
1221
1347
|
const agentUser = options.user ?? "forgezero-agent";
|
|
1348
|
+
const egressDependency = options.enforceEgress ? `After=forgezero-agent-egress.service
|
|
1349
|
+
Requires=forgezero-agent-egress.service
|
|
1350
|
+
BindsTo=forgezero-agent-egress.service
|
|
1351
|
+
` : "";
|
|
1352
|
+
const egressDirectives = options.enforceEgress ? systemdAgentEgressDirectives(options.runnerLoopbackPorts ?? []) : "";
|
|
1222
1353
|
return `[Unit]
|
|
1223
1354
|
Description=ForgeZero credential-free project command runner
|
|
1224
1355
|
Documentation=https://www.forgezero.net/docs/agent
|
|
1356
|
+
${egressDependency}
|
|
1225
1357
|
|
|
1226
1358
|
[Service]
|
|
1227
1359
|
Type=notify
|
|
@@ -1252,6 +1384,7 @@ ProtectControlGroups=true
|
|
|
1252
1384
|
RestrictRealtime=true
|
|
1253
1385
|
MemoryDenyWriteExecute=true
|
|
1254
1386
|
LockPersonality=true
|
|
1387
|
+
${egressDirectives}
|
|
1255
1388
|
ReadWritePaths=${root}/releases ${root}/runner-home
|
|
1256
1389
|
|
|
1257
1390
|
[Install]
|
|
@@ -1261,6 +1394,21 @@ WantedBy=multi-user.target
|
|
|
1261
1394
|
function agentUnit(options) {
|
|
1262
1395
|
if (!validNodeHostname(options.nodeHostname))
|
|
1263
1396
|
throw new Error("node hostname is invalid");
|
|
1397
|
+
if (!options.telemetryEndpoint) {
|
|
1398
|
+
throw new Error("compute Agent provisioning requires OTEL_EXPORTER_OTLP_ENDPOINT as an explicit public HTTPS collector coordinate");
|
|
1399
|
+
}
|
|
1400
|
+
let telemetryEndpoint;
|
|
1401
|
+
{
|
|
1402
|
+
let endpoint;
|
|
1403
|
+
try {
|
|
1404
|
+
endpoint = new URL(options.telemetryEndpoint);
|
|
1405
|
+
} catch {
|
|
1406
|
+
throw new Error("compute telemetry endpoint must be an absolute public HTTPS URL");
|
|
1407
|
+
}
|
|
1408
|
+
if (endpoint.protocol !== "https:" || endpoint.username || endpoint.password || endpoint.search || endpoint.hash || isIP(endpoint.hostname) !== 0 || !endpoint.hostname.includes(".") || endpoint.hostname === "localhost" || endpoint.hostname.endsWith(".local"))
|
|
1409
|
+
throw new Error("compute telemetry endpoint must be an absolute public HTTPS URL without credentials, query or fragment");
|
|
1410
|
+
telemetryEndpoint = endpoint.toString().replace(/\/$/, "");
|
|
1411
|
+
}
|
|
1264
1412
|
const bin = options.binPath ?? "fz-agent";
|
|
1265
1413
|
const user = options.user ?? "forgezero";
|
|
1266
1414
|
const seedCredentialPath = options.seedCredentialPath ?? "/etc/forgezero/creds/agent-seed.cred";
|
|
@@ -1309,6 +1457,7 @@ function agentUnit(options) {
|
|
|
1309
1457
|
}
|
|
1310
1458
|
}
|
|
1311
1459
|
const environment = [
|
|
1460
|
+
"NODE_ENV=production",
|
|
1312
1461
|
`FZ_SOCKET_PATH=${agentBackendSocketPath(options.socketPath)}`,
|
|
1313
1462
|
`FZ_CONTROL_SOCKET=${controlSocketPath}`,
|
|
1314
1463
|
`FZ_SEED_CREDENTIAL=agent-seed`,
|
|
@@ -1319,6 +1468,8 @@ function agentUnit(options) {
|
|
|
1319
1468
|
options.enrolStatePath ? `FZ_ENROL_STATE_FILE=${options.enrolStatePath}` : null,
|
|
1320
1469
|
options.nodeLabel ? `FZ_NODE_LABEL=${options.nodeLabel}` : null,
|
|
1321
1470
|
options.nodeHostname ? `FZ_NODE_HOSTNAME=${options.nodeHostname}` : null,
|
|
1471
|
+
`OTEL_EXPORTER_OTLP_ENDPOINT=${telemetryEndpoint}`,
|
|
1472
|
+
"OTEL_SERVICE_NAME=forgezero-agent",
|
|
1322
1473
|
options.gitPublicKeyPath ? `FZ_GIT_PUBLIC_KEY_FILE=${options.gitPublicKeyPath}` : null,
|
|
1323
1474
|
options.repository ? `FZ_DEPLOY_REPO=${options.repository}` : null,
|
|
1324
1475
|
options.branch ? `FZ_DEPLOY_BRANCH=${options.branch}` : null,
|
|
@@ -1354,6 +1505,7 @@ function agentUnit(options) {
|
|
|
1354
1505
|
const after = [
|
|
1355
1506
|
"network-online.target",
|
|
1356
1507
|
"forgezero-agent-update-helper.service",
|
|
1508
|
+
options.enforceEgress ? "forgezero-agent-egress.service" : null,
|
|
1357
1509
|
deploymentEnabled ? "forgezero-deploy-runner.service" : null,
|
|
1358
1510
|
deploymentEnabled ? "forgezero-software-helper.service" : null,
|
|
1359
1511
|
lifecycleEnabled ? "forgezero-lifecycle-helper.service" : null,
|
|
@@ -1362,6 +1514,7 @@ function agentUnit(options) {
|
|
|
1362
1514
|
].filter((value) => value !== null);
|
|
1363
1515
|
const requires = [
|
|
1364
1516
|
"forgezero-agent-update-helper.service",
|
|
1517
|
+
options.enforceEgress ? "forgezero-agent-egress.service" : null,
|
|
1365
1518
|
deploymentEnabled ? "forgezero-deploy-runner.service" : null,
|
|
1366
1519
|
deploymentEnabled ? "forgezero-software-helper.service" : null,
|
|
1367
1520
|
lifecycleEnabled ? "forgezero-lifecycle-helper.service" : null,
|
|
@@ -1371,7 +1524,8 @@ function agentUnit(options) {
|
|
|
1371
1524
|
const deploymentDependency = [
|
|
1372
1525
|
`After=${after.join(" ")}`,
|
|
1373
1526
|
"Wants=network-online.target",
|
|
1374
|
-
requires.length > 0 ? `Requires=${requires.join(" ")}` : null
|
|
1527
|
+
requires.length > 0 ? `Requires=${requires.join(" ")}` : null,
|
|
1528
|
+
options.enforceEgress ? "BindsTo=forgezero-agent-egress.service" : null
|
|
1375
1529
|
].filter((value) => value !== null).join(`
|
|
1376
1530
|
`);
|
|
1377
1531
|
const snpDevice = options.mode === "attested" ? `DevicePolicy=closed
|
|
@@ -1379,6 +1533,7 @@ DeviceAllow=/dev/sev-guest rw` : "";
|
|
|
1379
1533
|
const snpPrepare = options.mode === "attested" ? `ExecStartPre=+/bin/chgrp ${VAULT_GROUP} /dev/sev-guest
|
|
1380
1534
|
ExecStartPre=+/bin/chmod 0640 /dev/sev-guest
|
|
1381
1535
|
` : "";
|
|
1536
|
+
const egressDirectives = options.enforceEgress ? systemdAgentEgressDirectives() : "";
|
|
1382
1537
|
return `[Unit]
|
|
1383
1538
|
Description=ForgeZero node agent (${options.mode})
|
|
1384
1539
|
Documentation=https://www.forgezero.net/docs/agent
|
|
@@ -1423,6 +1578,7 @@ RestrictSUIDSGID=true
|
|
|
1423
1578
|
RestrictRealtime=true
|
|
1424
1579
|
MemoryDenyWriteExecute=true
|
|
1425
1580
|
LockPersonality=true
|
|
1581
|
+
${egressDirectives}
|
|
1426
1582
|
${snpDevice}
|
|
1427
1583
|
${deploymentWrites}
|
|
1428
1584
|
|
|
@@ -1438,6 +1594,8 @@ function planProvision(options) {
|
|
|
1438
1594
|
const credentialDir = seedCredentialPath.replace(/\/[^/]+$/, "");
|
|
1439
1595
|
const deployRoot = options.deployRoot ?? "/opt/forgezero";
|
|
1440
1596
|
const deploymentEnabled = Boolean(options.repository || options.pullDeployments);
|
|
1597
|
+
const runnerLoopbackPorts = normalizeEgressTcpPorts(options.runnerLoopbackPorts ?? []);
|
|
1598
|
+
const runnerPublicTcpPorts = normalizeEgressTcpPorts(options.runnerPublicTcpPorts ?? DEFAULT_RUNNER_PUBLIC_TCP_PORTS);
|
|
1441
1599
|
const lifecycleEnabled = Boolean(options.pullMigrations && options.lifecycleProfilePath);
|
|
1442
1600
|
if (Boolean(options.pullMigrations) !== Boolean(options.lifecycleProfilePath)) {
|
|
1443
1601
|
throw new Error("migration pull and lifecycle profile must be supplied together");
|
|
@@ -1478,6 +1636,9 @@ function planProvision(options) {
|
|
|
1478
1636
|
{ path: AGENT_SOCKET_UNIT_PATH, unit: agentSocketUnit(options) },
|
|
1479
1637
|
{ path: AGENT_SOCKET_PROXY_UNIT_PATH, unit: agentSocketProxyUnit(options) },
|
|
1480
1638
|
{ path: AGENT_UPDATE_HELPER_UNIT_PATH, unit: agentUpdateHelperUnit(options) },
|
|
1639
|
+
...options.enforceEgress ? [
|
|
1640
|
+
{ path: AGENT_EGRESS_UNIT_PATH, unit: agentEgressUnit(options) }
|
|
1641
|
+
] : [],
|
|
1481
1642
|
...deploymentEnabled ? [
|
|
1482
1643
|
{ path: DEPLOYMENT_RUNNER_UNIT_PATH, unit: deploymentRunnerUnit(options) },
|
|
1483
1644
|
{ path: SOFTWARE_HELPER_UNIT_PATH, unit: softwareHelperUnit(options) }
|
|
@@ -1504,6 +1665,10 @@ function planProvision(options) {
|
|
|
1504
1665
|
socketPath: options.socketPath,
|
|
1505
1666
|
user,
|
|
1506
1667
|
steps: [
|
|
1668
|
+
...options.enforceEgress ? [{
|
|
1669
|
+
label: "Ubuntu Agent egress prerequisites",
|
|
1670
|
+
command: `. /etc/os-release; test "$ID" = ubuntu; ` + `DEBIAN_FRONTEND=noninteractive apt-get update -qq; ` + `DEBIAN_FRONTEND=noninteractive apt-get install -y nftables; ` + `systemctl enable --now systemd-resolved.service; ` + `test "$(readlink -f /etc/resolv.conf)" = /run/systemd/resolve/stub-resolv.conf; ` + `test -s /run/systemd/resolve/stub-resolv.conf`
|
|
1671
|
+
}] : [],
|
|
1507
1672
|
{
|
|
1508
1673
|
label: "vault socket access group",
|
|
1509
1674
|
command: `groupadd --system ${VAULT_GROUP} || true`
|
|
@@ -1594,6 +1759,7 @@ function planProvision(options) {
|
|
|
1594
1759
|
command: `systemctl enable ${[
|
|
1595
1760
|
"forgezero-agent.socket",
|
|
1596
1761
|
"forgezero-agent-update-helper.service",
|
|
1762
|
+
...options.enforceEgress ? ["forgezero-agent-egress.service"] : [],
|
|
1597
1763
|
...deploymentEnabled ? ["forgezero-deploy-runner.service", "forgezero-software-helper.service"] : [],
|
|
1598
1764
|
...lifecycleEnabled ? ["forgezero-lifecycle-helper.service"] : [],
|
|
1599
1765
|
...warpEnabled ? ["forgezero-warp-config.service", "warp-svc.service"] : [],
|
|
@@ -1601,6 +1767,7 @@ function planProvision(options) {
|
|
|
1601
1767
|
"forgezero-agent.service"
|
|
1602
1768
|
].join(" ")}; systemctl reset-failed forgezero-agent.service || true; systemctl restart ${[
|
|
1603
1769
|
"forgezero-agent-update-helper.service",
|
|
1770
|
+
...options.enforceEgress ? ["forgezero-agent-egress.service"] : [],
|
|
1604
1771
|
...deploymentEnabled ? ["forgezero-deploy-runner.service", "forgezero-software-helper.service"] : [],
|
|
1605
1772
|
...lifecycleEnabled ? ["forgezero-lifecycle-helper.service"] : [],
|
|
1606
1773
|
...warpEnabled ? ["forgezero-warp-config.service", "warp-svc.service"] : [],
|
|
@@ -1611,6 +1778,10 @@ function planProvision(options) {
|
|
|
1611
1778
|
label: "prove the compute binding is durable",
|
|
1612
1779
|
command: `test -s ${enrolStatePath}`
|
|
1613
1780
|
}] : [],
|
|
1781
|
+
...options.enforceEgress ? [{
|
|
1782
|
+
label: "prove the Agent egress policy is active",
|
|
1783
|
+
command: "systemctl is-active forgezero-agent-egress.service && " + "nft --numeric list table inet forgezero_agent_egress | grep -q forgezero-agent-egress-v1" + (deploymentEnabled ? ` && nft --numeric list table inet forgezero_agent_egress | grep -q 'public-tcp=${runnerPublicTcpPorts.join(",")}'` + (runnerLoopbackPorts.length > 0 ? ` && nft --numeric list table inet forgezero_agent_egress | grep -Eq 'loopback=[0-9]+:${runnerLoopbackPorts.join(",")}( |")'` : "") : "")
|
|
1784
|
+
}] : [],
|
|
1614
1785
|
{ label: "prove it is running", command: "systemctl is-active forgezero-agent.service" },
|
|
1615
1786
|
{ label: "prove the public Vault socket exists", command: awaitSocketCommand(options.socketPath) },
|
|
1616
1787
|
{ label: "prove the Agent Vault backend exists", command: awaitSocketCommand(agentBackendSocketPath(options.socketPath)) },
|
|
@@ -1652,6 +1823,7 @@ export {
|
|
|
1652
1823
|
agentSocketUnit,
|
|
1653
1824
|
agentSocketProxyUnit,
|
|
1654
1825
|
agentEnrolmentUnit,
|
|
1826
|
+
agentEgressUnit,
|
|
1655
1827
|
agentBackendSocketPath,
|
|
1656
1828
|
WARP_SERVICE_DROP_IN_PATH,
|
|
1657
1829
|
WARP_CONFIG_UNIT_PATH,
|
|
@@ -1665,7 +1837,9 @@ export {
|
|
|
1665
1837
|
DEPLOYMENT_RUNNER_UNIT_PATH,
|
|
1666
1838
|
DEPLOYMENT_RUNNER_SOCKET,
|
|
1667
1839
|
DEPLOYMENT_GROUP,
|
|
1840
|
+
DEFAULT_RUNNER_PUBLIC_TCP_PORTS,
|
|
1668
1841
|
CAPABILITY_CHECKS,
|
|
1669
1842
|
AGENT_SOCKET_UNIT_PATH,
|
|
1670
|
-
AGENT_SOCKET_PROXY_UNIT_PATH
|
|
1843
|
+
AGENT_SOCKET_PROXY_UNIT_PATH,
|
|
1844
|
+
AGENT_EGRESS_UNIT_PATH
|
|
1671
1845
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { NodeKeyPair } from '@forgezero/runtime/identity';
|
|
2
2
|
import { type SignedNodeHttpOptions } from './signed-node-http';
|
|
3
|
+
import type { AgentOperationTelemetry } from './telemetry-runtime';
|
|
3
4
|
interface RemoteProvisionClaimBase {
|
|
4
5
|
computeKey: string;
|
|
5
6
|
claimToken: string;
|
|
@@ -73,6 +74,7 @@ export interface ProvisioningPullOptions extends SignedNodeHttpOptions {
|
|
|
73
74
|
kvm: boolean;
|
|
74
75
|
helper: boolean;
|
|
75
76
|
};
|
|
77
|
+
telemetry?: AgentOperationTelemetry;
|
|
76
78
|
}
|
|
77
79
|
export type ProvisionPullResult = {
|
|
78
80
|
status: 'idle';
|
|
@@ -70,6 +70,7 @@ async function postSignedNode(options, path, body) {
|
|
|
70
70
|
// src/provisioning-pull.ts
|
|
71
71
|
class ProvisionClaimLostError extends Error {
|
|
72
72
|
}
|
|
73
|
+
var remoteOutcome = (cause) => cause instanceof SignedNodeHttpError && cause.status < 500 ? "refused" : "retryable";
|
|
73
74
|
var post = (options, operation, body) => postSignedNode(options, `v1/metal/computes/${operation}`, body);
|
|
74
75
|
async function runClaim(options, claim) {
|
|
75
76
|
const setTimer = options.setTimer ?? ((callback, ms) => setTimeout(callback, ms));
|
|
@@ -88,10 +89,11 @@ async function runClaim(options, claim) {
|
|
|
88
89
|
if (stopped || renewal)
|
|
89
90
|
return;
|
|
90
91
|
let retry;
|
|
91
|
-
|
|
92
|
+
const renew = () => post(options, "renew", {
|
|
92
93
|
computeKey: claim.computeKey,
|
|
93
94
|
claimToken: claim.claimToken
|
|
94
|
-
})
|
|
95
|
+
});
|
|
96
|
+
renewal = (options.telemetry ? options.telemetry.observe("provisioning.renew", renew, () => "success", remoteOutcome) : renew()).then((response) => {
|
|
95
97
|
expires = response.claimExpiresAtTs;
|
|
96
98
|
options.onEvent?.("lease-renewed", { computeKey: claim.computeKey, claimExpiresAtTs: expires });
|
|
97
99
|
}).catch((cause) => {
|
|
@@ -112,7 +114,8 @@ async function runClaim(options, claim) {
|
|
|
112
114
|
schedule();
|
|
113
115
|
let result;
|
|
114
116
|
try {
|
|
115
|
-
|
|
117
|
+
const run = () => options.run(claim);
|
|
118
|
+
result = options.telemetry ? await options.telemetry.observe("provisioning.apply", run) : await run();
|
|
116
119
|
} finally {
|
|
117
120
|
stopped = true;
|
|
118
121
|
clearTimer(timer);
|
|
@@ -143,7 +146,8 @@ async function complete(options, body) {
|
|
|
143
146
|
async function pullProvisioningOnce(options) {
|
|
144
147
|
if (options.metalPreflight) {
|
|
145
148
|
const report = options.metalPreflight();
|
|
146
|
-
const
|
|
149
|
+
const preflight = () => postSignedNode(options, "v1/metal/preflight", report);
|
|
150
|
+
const accepted = options.telemetry ? await options.telemetry.observe("provisioning.preflight", preflight, (value) => value.ready ? "success" : "refused", remoteOutcome) : await preflight();
|
|
147
151
|
if (options.metalHostname && accepted.hostname !== options.metalHostname) {
|
|
148
152
|
throw new Error(`metal agent: configured inventory hostname ${options.metalHostname} is bound as ${accepted.hostname}; refusing work.`);
|
|
149
153
|
}
|
|
@@ -156,7 +160,8 @@ async function pullProvisioningOnce(options) {
|
|
|
156
160
|
if (!accepted.ready)
|
|
157
161
|
return { status: "idle" };
|
|
158
162
|
}
|
|
159
|
-
const
|
|
163
|
+
const claimWork = () => post(options, "claim", {});
|
|
164
|
+
const response = options.telemetry ? await options.telemetry.observe("provisioning.claim", claimWork, (value) => value.claim ? "success" : "idle", remoteOutcome) : await claimWork();
|
|
160
165
|
if (!response.claim)
|
|
161
166
|
return { status: "idle" };
|
|
162
167
|
const claim = response.claim;
|
|
@@ -167,20 +172,28 @@ async function pullProvisioningOnce(options) {
|
|
|
167
172
|
if (cause instanceof ProvisionClaimLostError)
|
|
168
173
|
throw cause;
|
|
169
174
|
const reason = (cause instanceof Error ? cause.message : String(cause)).slice(0, 2000);
|
|
170
|
-
|
|
175
|
+
const acknowledge2 = () => complete(options, {
|
|
171
176
|
computeKey: claim.computeKey,
|
|
172
177
|
claimToken: claim.claimToken,
|
|
173
178
|
ok: false,
|
|
174
179
|
detail: reason
|
|
175
180
|
});
|
|
181
|
+
if (options.telemetry) {
|
|
182
|
+
await options.telemetry.observe("provisioning.complete", acknowledge2, () => "success", remoteOutcome);
|
|
183
|
+
} else
|
|
184
|
+
await acknowledge2();
|
|
176
185
|
return { status: "failed", claim, reason };
|
|
177
186
|
}
|
|
178
|
-
|
|
187
|
+
const acknowledge = () => complete(options, {
|
|
179
188
|
computeKey: claim.computeKey,
|
|
180
189
|
claimToken: claim.claimToken,
|
|
181
190
|
ok: true,
|
|
182
191
|
...result.guestAddress ? { guestAddress: result.guestAddress } : {}
|
|
183
192
|
});
|
|
193
|
+
if (options.telemetry) {
|
|
194
|
+
await options.telemetry.observe("provisioning.complete", acknowledge, () => "success", remoteOutcome);
|
|
195
|
+
} else
|
|
196
|
+
await acknowledge();
|
|
184
197
|
return { status: claim.action === "delete" ? "terminated" : "running", claim, result };
|
|
185
198
|
}
|
|
186
199
|
function startProvisioningPull(options) {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { AgentTelemetry, AgentTelemetryEvent, AgentTelemetryOperation, AgentTelemetryOutcome } from './telemetry';
|
|
2
|
+
/**
|
|
3
|
+
* Process-wide operation/state accounting layered over the deliberately small
|
|
4
|
+
* OTLP emitter. Callers still cannot attach arbitrary attributes or payloads.
|
|
5
|
+
*/
|
|
6
|
+
export declare class AgentTelemetryRuntime {
|
|
7
|
+
private readonly telemetry;
|
|
8
|
+
private readonly now;
|
|
9
|
+
private active;
|
|
10
|
+
private completed;
|
|
11
|
+
private failed;
|
|
12
|
+
private draining;
|
|
13
|
+
constructor(telemetry: AgentTelemetry, now?: () => number);
|
|
14
|
+
event(event: AgentTelemetryEvent): void;
|
|
15
|
+
setDraining(draining: boolean): void;
|
|
16
|
+
observe<T>(operation: AgentTelemetryOperation, work: () => Promise<T>, outcome?: (value: T) => AgentTelemetryOutcome, errorOutcome?: (cause: unknown) => AgentTelemetryOutcome): Promise<T>;
|
|
17
|
+
close(): Promise<void>;
|
|
18
|
+
private recordState;
|
|
19
|
+
}
|
|
20
|
+
export type AgentOperationTelemetry = Pick<AgentTelemetryRuntime, 'observe'>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency-free OTLP/HTTP telemetry for the machine Agent.
|
|
3
|
+
*
|
|
4
|
+
* Callers can name only reviewed operations, outcomes and lifecycle events.
|
|
5
|
+
* There is deliberately no field for a URL, request/response body, header,
|
|
6
|
+
* credential, tenant coordinate, command, error detail or arbitrary label.
|
|
7
|
+
*/
|
|
8
|
+
export declare const AGENT_TELEMETRY_OPERATIONS: readonly ["agent.heartbeat", "agent.update", "attestation.refresh", "deployment.claim", "deployment.run", "deployment.renew", "deployment.complete", "migration.claim", "migration.run", "migration.renew", "migration.complete", "provisioning.preflight", "provisioning.claim", "provisioning.apply", "provisioning.renew", "provisioning.complete", "vault.sync", "agent.other"];
|
|
9
|
+
export type AgentTelemetryOperation = (typeof AGENT_TELEMETRY_OPERATIONS)[number];
|
|
10
|
+
export declare const AGENT_TELEMETRY_OUTCOMES: readonly ["success", "idle", "refused", "retryable", "failed"];
|
|
11
|
+
export type AgentTelemetryOutcome = (typeof AGENT_TELEMETRY_OUTCOMES)[number];
|
|
12
|
+
export declare const AGENT_TELEMETRY_EVENTS: readonly ["agent.started", "agent.draining", "agent.stopped", "agent.update_prepared", "agent.update_recovered", "telemetry.queue_overflow"];
|
|
13
|
+
export type AgentTelemetryEvent = (typeof AGENT_TELEMETRY_EVENTS)[number];
|
|
14
|
+
export interface AgentTelemetryConfig {
|
|
15
|
+
endpoint: string;
|
|
16
|
+
serviceName: string;
|
|
17
|
+
instanceId: string;
|
|
18
|
+
environment: 'production' | 'development';
|
|
19
|
+
flushIntervalMs: number;
|
|
20
|
+
traceSampleRatio: number;
|
|
21
|
+
}
|
|
22
|
+
export interface AgentOperationObservation {
|
|
23
|
+
operation: AgentTelemetryOperation;
|
|
24
|
+
outcome: AgentTelemetryOutcome;
|
|
25
|
+
startedAtMs: number;
|
|
26
|
+
durationMs: number;
|
|
27
|
+
}
|
|
28
|
+
export interface AgentTelemetryState {
|
|
29
|
+
active: number;
|
|
30
|
+
completed: number;
|
|
31
|
+
failed: number;
|
|
32
|
+
draining: boolean;
|
|
33
|
+
}
|
|
34
|
+
export interface AgentTelemetry {
|
|
35
|
+
recordOperation(observation: AgentOperationObservation): void;
|
|
36
|
+
recordEvent(event: AgentTelemetryEvent): void;
|
|
37
|
+
recordState(state: AgentTelemetryState): void;
|
|
38
|
+
flush(): Promise<void>;
|
|
39
|
+
close(): Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
42
|
+
export declare const AGENT_OTLP_EXPORT_TIMEOUT_MS = 5000;
|
|
43
|
+
export declare const AGENT_OTLP_MAX_ATTEMPTS = 2;
|
|
44
|
+
export declare const AGENT_OTLP_MAX_RETRY_DELAY_MS = 1000;
|
|
45
|
+
export declare const AGENT_TELEMETRY_MAX_SPANS = 256;
|
|
46
|
+
export declare const AGENT_TELEMETRY_MAX_LOGS = 64;
|
|
47
|
+
/** Resolve once at process startup; production refuses to run without an exporter. */
|
|
48
|
+
export declare function resolveAgentTelemetryConfig(env?: Record<string, string | undefined>): AgentTelemetryConfig | null;
|
|
49
|
+
export declare function createAgentTelemetry(input: AgentTelemetryConfig | null, options?: {
|
|
50
|
+
fetch?: FetchLike;
|
|
51
|
+
warn?: (message: string) => void;
|
|
52
|
+
now?: () => number;
|
|
53
|
+
random?: () => number;
|
|
54
|
+
wait?: (milliseconds: number) => Promise<void>;
|
|
55
|
+
}): AgentTelemetry;
|
|
56
|
+
export {};
|
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.33";
|
package/package.json
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
{
|
|
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
2
|
"name": "@forgezero/agent",
|
|
4
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.33",
|
|
5
4
|
"type": "module",
|
|
6
5
|
"scripts": {
|
|
7
6
|
"check": "tsc --noEmit",
|
|
@@ -18,19 +17,20 @@
|
|
|
18
17
|
"@noble/post-quantum": "^0.6.1"
|
|
19
18
|
},
|
|
20
19
|
"dependencies": {
|
|
21
|
-
"@forgezero/runtime": "
|
|
22
|
-
"@forgezero/vault": "
|
|
23
|
-
"@noble/curves": "
|
|
24
|
-
"@noble/hashes": "
|
|
25
|
-
"@noble/post-quantum": "
|
|
26
|
-
"@scure/bip39": "
|
|
20
|
+
"@forgezero/runtime": "0.1.5",
|
|
21
|
+
"@forgezero/vault": "0.1.8",
|
|
22
|
+
"@noble/curves": "2.2.0",
|
|
23
|
+
"@noble/hashes": "2.2.0",
|
|
24
|
+
"@noble/post-quantum": "0.6.1",
|
|
25
|
+
"@scure/bip39": "2.2.0"
|
|
27
26
|
},
|
|
28
27
|
"bin": {
|
|
29
28
|
"fz-agent": "dist/fz-agent.js",
|
|
30
29
|
"fz": "dist/fz.js"
|
|
31
30
|
},
|
|
32
31
|
"publishConfig": {
|
|
33
|
-
"access": "public"
|
|
32
|
+
"access": "public",
|
|
33
|
+
"provenance": true
|
|
34
34
|
},
|
|
35
35
|
"files": [
|
|
36
36
|
"dist",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"repository": {
|
|
54
54
|
"type": "git",
|
|
55
55
|
"url": "git+https://github.com/forgezero-net/packages.git",
|
|
56
|
-
"directory": "
|
|
56
|
+
"directory": "agent"
|
|
57
57
|
},
|
|
58
58
|
"bugs": "https://github.com/forgezero-net/packages/issues",
|
|
59
59
|
"exports": {
|