@forgezero/agent 0.1.78 → 0.1.80

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.
@@ -165,16 +165,38 @@ var runSoftwareCommand = async (argv, env = {}) => {
165
165
  return { exitCode, output: `${stdout}${stderr}` };
166
166
  };
167
167
  var run = runSoftwareCommand;
168
+ var softwareDownloadCommand = (url, destination, maximumBytes) => {
169
+ const source = new URL(url);
170
+ if (source.protocol !== "https:" || source.username || source.password)
171
+ throw new Error("software download must use credential-free HTTPS");
172
+ if (!Number.isSafeInteger(maximumBytes) || maximumBytes < 1)
173
+ throw new Error("software download size bound is invalid");
174
+ return [
175
+ "/usr/bin/curl",
176
+ "--fail",
177
+ "--location",
178
+ "--silent",
179
+ "--show-error",
180
+ "--proto",
181
+ "=https",
182
+ "--tlsv1.2",
183
+ "--max-time",
184
+ "1800",
185
+ "--max-filesize",
186
+ String(maximumBytes),
187
+ "--output",
188
+ destination,
189
+ source.href
190
+ ];
191
+ };
168
192
  var download = async (url, destination, sha256, maximumBytes = 512 * 1024 * 1024) => {
169
- const response = await fetch(url, { redirect: "follow", signal: AbortSignal.timeout(30 * 60000) });
170
- if (!response.ok)
171
- throw new Error(`download failed with HTTP ${response.status}`);
172
- const declared = Number(response.headers.get("content-length") ?? 0);
173
- if (declared > maximumBytes)
174
- throw new Error("download exceeds reviewed size bound");
175
193
  if (existsSync(destination))
176
194
  throw new Error("download destination already exists");
177
- await Bun.write(destination, response);
195
+ const received = await run(softwareDownloadCommand(url, destination, maximumBytes));
196
+ if (received.exitCode !== 0) {
197
+ rmSync(destination, { force: true });
198
+ throw new Error(`software download failed (${received.exitCode}): ${received.output.trim()}`);
199
+ }
178
200
  chmodSync(destination, 384);
179
201
  if (statSync(destination).size > maximumBytes) {
180
202
  rmSync(destination, { force: true });
@@ -949,6 +971,7 @@ function phasePipeline(definition, phase, profile, executeRelease = false) {
949
971
  // src/deployment-connectivity.ts
950
972
  import { createHash as createHash2 } from "node:crypto";
951
973
  import { mkdirSync as mkdirSync2, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "node:fs";
974
+ import { createConnection } from "node:net";
952
975
  import { dirname as dirname2 } from "node:path";
953
976
 
954
977
  // src/process-input.ts
@@ -1001,6 +1024,21 @@ var defaultHost = {
1001
1024
  const [stdout, stderr, exitCode] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
1002
1025
  return { exitCode, output: `${stdout}${stderr}` };
1003
1026
  },
1027
+ probe: (address, port, timeoutMs) => new Promise((resolve) => {
1028
+ const socket = createConnection({ host: address, port });
1029
+ let settled = false;
1030
+ const finish = (value) => {
1031
+ if (settled)
1032
+ return;
1033
+ settled = true;
1034
+ socket.destroy();
1035
+ resolve(value);
1036
+ };
1037
+ socket.setTimeout(timeoutMs);
1038
+ socket.once("connect", () => finish(true));
1039
+ socket.once("timeout", () => finish(false));
1040
+ socket.once("error", () => finish(false));
1041
+ }),
1004
1042
  sleep: (ms) => Bun.sleep(ms)
1005
1043
  };
1006
1044
  function validate(request) {
@@ -1015,7 +1053,7 @@ function validate(request) {
1015
1053
  } else if (request.capabilities.public)
1016
1054
  throw new Error("unexpected public deployment capability");
1017
1055
  if (privateIntent?.mode === "cloudflare-warp") {
1018
- if (!NAME2.test(privateIntent.credential) || privateIntent.network.length < 1 || privateIntent.network.length > 128 || !request.capabilities.private || !UUID.test(request.capabilities.private.connectorId) || !TOKEN.test(request.capabilities.private.connectorToken)) {
1056
+ if (!NAME2.test(privateIntent.credential) || privateIntent.network.length < 1 || privateIntent.network.length > 128 || !request.capabilities.private || !UUID.test(request.capabilities.private.connectorId) || !UUID.test(request.capabilities.private.siteRouteId) || !TOKEN.test(request.capabilities.private.connectorToken)) {
1019
1057
  throw new Error("private deployment connectivity is malformed");
1020
1058
  }
1021
1059
  } else if (request.capabilities.private)
@@ -1025,6 +1063,22 @@ async function applyDeploymentConnectivity(request, host = defaultHost) {
1025
1063
  validate(request);
1026
1064
  const id = idFor(request.key);
1027
1065
  const evidence = { key: request.key };
1066
+ const topology = request.topology;
1067
+ if (topology) {
1068
+ const values = [topology.localRelayAddress, ...topology.localPeerAddresses, ...topology.remoteRelayAddresses];
1069
+ const identities = [
1070
+ topology.nodeIdentity,
1071
+ ...topology.localPeerIdentities,
1072
+ ...topology.remoteRelayIdentities,
1073
+ ...topology.memberIdentities
1074
+ ];
1075
+ if (!/^[A-Za-z0-9.-]{1,253}$/.test(topology.site) || !/^(?:\d{1,3}\.){3}0\/24$/.test(topology.siteCidr) || values.some((value) => !/^(?:\d{1,3}\.){3}\d{1,3}$/.test(value)) || identities.some((value) => !/^[A-Za-z0-9.-]{1,253}$/.test(value)) || topology.localPeerIdentities.length !== topology.localPeerAddresses.length || topology.remoteRelayIdentities.length !== topology.remoteRelayAddresses.length || topology.memberIdentities.length < 1 || topology.memberIdentities.length > 1024 || new Set(topology.memberIdentities).size !== topology.memberIdentities.length || !topology.memberIdentities.includes(topology.nodeIdentity) || !/^sha256:[a-f0-9]{64}$/.test(topology.generation) || (topology.role === "member" ? topology.remoteRelayAddresses.length !== 0 : topology.remoteSiteCidrs.length !== topology.remoteRelayAddresses.length) || topology.remoteSiteCidrs.some((value) => !/^(?:\d{1,3}\.){3}0\/24$/.test(value)) || new Set(topology.remoteSiteCidrs).size !== topology.remoteSiteCidrs.length || !Number.isSafeInteger(topology.healthPort) || topology.healthPort < 1 || topology.healthPort > 65535 || topology.routedTcpPorts.length < 1 || topology.routedTcpPorts.length > 64 || new Set(topology.routedTcpPorts).size !== topology.routedTcpPorts.length || !topology.routedTcpPorts.includes(topology.healthPort) || topology.routedTcpPorts.some((port) => !Number.isSafeInteger(port) || port < 1 || port > 65535) || topology.role === "member" && topology.transport !== "private-lan") {
1076
+ throw new Error("deployment topology is malformed");
1077
+ }
1078
+ if (request.intent.private?.mode === "cloudflare-warp" !== (topology.transport === "cloudflare-warp")) {
1079
+ throw new Error("deployment topology transport differs from private connectivity intent");
1080
+ }
1081
+ }
1028
1082
  if (request.intent.public?.mode === "cloudflare-tunnel") {
1029
1083
  const capability = request.capabilities.public;
1030
1084
  const credentialName = `FZ_TUNNEL_${id.toUpperCase()}`;
@@ -1062,6 +1116,12 @@ WantedBy=multi-user.target
1062
1116
  const capability = request.capabilities.private;
1063
1117
  const credentialPath = "/etc/forgezero/creds/CF_WARP_CONNECTOR_TOKEN.cred";
1064
1118
  const unit = "forgezero-deployment-mesh.service";
1119
+ const forwarding = "/etc/sysctl.d/99-forgezero-mesh.conf";
1120
+ host.write(forwarding, `net.ipv4.ip_forward = 1
1121
+ net.ipv6.conf.all.forwarding = 1
1122
+ net.ipv6.conf.all.accept_ra = 2
1123
+ `, 420);
1124
+ await checked(host, ["/usr/sbin/sysctl", "-p", forwarding], "Mesh subnet forwarding");
1065
1125
  await host.seal("CF_WARP_CONNECTOR_TOKEN", credentialPath, capability.connectorToken);
1066
1126
  host.write(`/etc/systemd/system/${unit}`, `[Unit]
1067
1127
  Description=ForgeZero deployment Mesh/WARP connector
@@ -1096,6 +1156,95 @@ WantedBy=multi-user.target
1096
1156
  if (!evidence.private)
1097
1157
  throw new Error("WARP connector readiness failed");
1098
1158
  }
1159
+ if (topology) {
1160
+ const syncAddresses = topology.role === "relay" ? [...topology.localPeerAddresses, ...topology.remoteRelayAddresses] : [topology.localRelayAddress];
1161
+ const topologyEpoch = `topology-${topology.generation.slice("sha256:".length, "sha256:".length + 48)}`;
1162
+ host.write("/etc/forgezero/deployment-topology.env", [
1163
+ `FZ_TOPOLOGY_NODE_IDENTITY=${topology.nodeIdentity}`,
1164
+ `FZ_TOPOLOGY_PEER_ADDRESSES=${[...new Set(syncAddresses)].join(",")}`,
1165
+ `FZ_TOPOLOGY_MEMBERS=${topology.memberIdentities.join(",")}`,
1166
+ `FZ_TOPOLOGY_EPOCH=${topologyEpoch}`,
1167
+ `FZ_TOPOLOGY_SITE=${topology.site}`,
1168
+ `FZ_TOPOLOGY_ROLE=${topology.role}`,
1169
+ `FZ_TOPOLOGY_TRANSPORT=${topology.transport}`,
1170
+ ""
1171
+ ].join(`
1172
+ `), 384);
1173
+ const routeUnit = "forgezero-deployment-topology.service";
1174
+ const routeUnitPath = `/etc/systemd/system/${routeUnit}`;
1175
+ const routePairs = topology.role === "member" ? topology.remoteSiteCidrs.map((network) => ({ network, via: topology.localRelayAddress })) : topology.transport === "private-lan" ? topology.remoteSiteCidrs.map((network, index) => ({ network, via: topology.remoteRelayAddresses[index] })) : [];
1176
+ for (const { via } of routePairs) {
1177
+ await checked(host, ["/usr/sbin/ip", "route", "get", via], `private route gateway ${via}`);
1178
+ }
1179
+ await host.exec(["/usr/bin/systemctl", "disable", "--now", routeUnit]);
1180
+ const forwarding = topology.role !== "member" && topology.remoteSiteCidrs.length > 0 ? `ExecStart=/usr/sbin/sysctl -w net.ipv4.ip_forward=1
1181
+ ` : "";
1182
+ const starts = routePairs.map(({ network, via }) => `ExecStart=/usr/sbin/ip route replace ${network} via ${via}`).join(`
1183
+ `);
1184
+ const stops = routePairs.map(({ network, via }) => `ExecStop=-/usr/sbin/ip route del ${network} via ${via}`).join(`
1185
+ `);
1186
+ const noOp = !forwarding && !starts ? `ExecStart=/usr/bin/true
1187
+ ` : "";
1188
+ host.write(routeUnitPath, `[Unit]
1189
+ Description=ForgeZero fenced deployment site routing
1190
+ After=network-online.target${topology.transport === "cloudflare-warp" ? " forgezero-deployment-mesh.service" : ""}
1191
+ Wants=network-online.target
1192
+
1193
+ [Service]
1194
+ Type=oneshot
1195
+ RemainAfterExit=yes
1196
+ ${forwarding}${noOp}${starts}${starts ? `
1197
+ ` : ""}${stops}${stops ? `
1198
+ ` : ""}
1199
+ [Install]
1200
+ WantedBy=multi-user.target
1201
+ `, 420);
1202
+ for (const source of [...new Set([topology.siteCidr, ...topology.remoteSiteCidrs])]) {
1203
+ for (const port of topology.routedTcpPorts) {
1204
+ await checked(host, ["/usr/sbin/ufw", "allow", "from", source, "to", "any", "port", String(port), "proto", "tcp"], `private service ${source}:${port}`);
1205
+ }
1206
+ }
1207
+ if (topology.role !== "member")
1208
+ for (const remoteCidr of topology.remoteSiteCidrs) {
1209
+ for (const port of topology.routedTcpPorts) {
1210
+ await checked(host, [
1211
+ "/usr/sbin/ufw",
1212
+ "route",
1213
+ "allow",
1214
+ "proto",
1215
+ "tcp",
1216
+ "from",
1217
+ topology.siteCidr,
1218
+ "to",
1219
+ remoteCidr,
1220
+ "port",
1221
+ String(port)
1222
+ ], `private routed service ${topology.siteCidr}->${remoteCidr}:${port}`);
1223
+ }
1224
+ }
1225
+ await checked(host, ["/usr/bin/systemctl", "daemon-reload"], "topology daemon reload");
1226
+ await checked(host, ["/usr/bin/systemctl", "enable", "--now", routeUnit], "topology routes");
1227
+ const probes = topology.role === "member" ? [topology.localRelayAddress] : topology.remoteRelayAddresses;
1228
+ for (const address of [...new Set(probes)]) {
1229
+ await checked(host, ["/usr/sbin/ip", "route", "get", address], `private route ${address}`);
1230
+ let ready = false;
1231
+ for (let attempt = 0;attempt < 10; attempt += 1) {
1232
+ if (await host.probe(address, topology.healthPort, 2000)) {
1233
+ ready = true;
1234
+ break;
1235
+ }
1236
+ await host.sleep(1000);
1237
+ }
1238
+ if (!ready)
1239
+ throw new Error(`private topology relay ${address}:${topology.healthPort} is unreachable`);
1240
+ }
1241
+ evidence.topology = {
1242
+ site: topology.site,
1243
+ siteCidr: topology.siteCidr,
1244
+ role: topology.role,
1245
+ probed: [...new Set(probes)]
1246
+ };
1247
+ }
1099
1248
  return evidence;
1100
1249
  }
1101
1250
 
@@ -57,6 +57,7 @@ export declare const OS_CATALOG: readonly OsCatalogEntry[];
57
57
  export declare const SOFTWARE_CATALOG: readonly SoftwareCatalogEntry[];
58
58
  export declare const softwareSpawnFailure: (cause: unknown, argv: readonly string[]) => SoftwareCommandResult | undefined;
59
59
  export declare const runSoftwareCommand: (argv: readonly string[], env?: Record<string, string>) => Promise<SoftwareCommandResult>;
60
+ export declare const softwareDownloadCommand: (url: string, destination: string, maximumBytes: number) => readonly string[];
60
61
  /** Closed, fixed-argv privileged strategy executor. No repository value becomes a command. */
61
62
  export declare function executeSoftwareOperation(operation: SoftwareOperation): Promise<SoftwareCommandResult>;
62
63
  export declare function observeSoftwareHost(osRelease?: string, architecture?: NodeJS.Architecture): SoftwareObservation;
package/dist/software.js CHANGED
@@ -165,16 +165,38 @@ var runSoftwareCommand = async (argv, env = {}) => {
165
165
  return { exitCode, output: `${stdout}${stderr}` };
166
166
  };
167
167
  var run = runSoftwareCommand;
168
+ var softwareDownloadCommand = (url, destination, maximumBytes) => {
169
+ const source = new URL(url);
170
+ if (source.protocol !== "https:" || source.username || source.password)
171
+ throw new Error("software download must use credential-free HTTPS");
172
+ if (!Number.isSafeInteger(maximumBytes) || maximumBytes < 1)
173
+ throw new Error("software download size bound is invalid");
174
+ return [
175
+ "/usr/bin/curl",
176
+ "--fail",
177
+ "--location",
178
+ "--silent",
179
+ "--show-error",
180
+ "--proto",
181
+ "=https",
182
+ "--tlsv1.2",
183
+ "--max-time",
184
+ "1800",
185
+ "--max-filesize",
186
+ String(maximumBytes),
187
+ "--output",
188
+ destination,
189
+ source.href
190
+ ];
191
+ };
168
192
  var download = async (url, destination, sha256, maximumBytes = 512 * 1024 * 1024) => {
169
- const response = await fetch(url, { redirect: "follow", signal: AbortSignal.timeout(30 * 60000) });
170
- if (!response.ok)
171
- throw new Error(`download failed with HTTP ${response.status}`);
172
- const declared = Number(response.headers.get("content-length") ?? 0);
173
- if (declared > maximumBytes)
174
- throw new Error("download exceeds reviewed size bound");
175
193
  if (existsSync(destination))
176
194
  throw new Error("download destination already exists");
177
- await Bun.write(destination, response);
195
+ const received = await run(softwareDownloadCommand(url, destination, maximumBytes));
196
+ if (received.exitCode !== 0) {
197
+ rmSync(destination, { force: true });
198
+ throw new Error(`software download failed (${received.exitCode}): ${received.output.trim()}`);
199
+ }
178
200
  chmodSync(destination, 384);
179
201
  if (statSync(destination).size > maximumBytes) {
180
202
  rmSync(destination, { force: true });
@@ -518,6 +540,7 @@ async function ensureSoftwareRequirements(requirementsInput, options) {
518
540
  export {
519
541
  validateSoftwareRequirements,
520
542
  softwareSpawnFailure,
543
+ softwareDownloadCommand,
521
544
  runSoftwareCommand,
522
545
  observeSoftwareHost,
523
546
  executeSoftwareOperation,
package/dist/ubuntu.js CHANGED
@@ -165,16 +165,38 @@ var runSoftwareCommand = async (argv, env = {}) => {
165
165
  return { exitCode, output: `${stdout}${stderr}` };
166
166
  };
167
167
  var run = runSoftwareCommand;
168
+ var softwareDownloadCommand = (url, destination, maximumBytes) => {
169
+ const source = new URL(url);
170
+ if (source.protocol !== "https:" || source.username || source.password)
171
+ throw new Error("software download must use credential-free HTTPS");
172
+ if (!Number.isSafeInteger(maximumBytes) || maximumBytes < 1)
173
+ throw new Error("software download size bound is invalid");
174
+ return [
175
+ "/usr/bin/curl",
176
+ "--fail",
177
+ "--location",
178
+ "--silent",
179
+ "--show-error",
180
+ "--proto",
181
+ "=https",
182
+ "--tlsv1.2",
183
+ "--max-time",
184
+ "1800",
185
+ "--max-filesize",
186
+ String(maximumBytes),
187
+ "--output",
188
+ destination,
189
+ source.href
190
+ ];
191
+ };
168
192
  var download = async (url, destination, sha256, maximumBytes = 512 * 1024 * 1024) => {
169
- const response = await fetch(url, { redirect: "follow", signal: AbortSignal.timeout(30 * 60000) });
170
- if (!response.ok)
171
- throw new Error(`download failed with HTTP ${response.status}`);
172
- const declared = Number(response.headers.get("content-length") ?? 0);
173
- if (declared > maximumBytes)
174
- throw new Error("download exceeds reviewed size bound");
175
193
  if (existsSync(destination))
176
194
  throw new Error("download destination already exists");
177
- await Bun.write(destination, response);
195
+ const received = await run(softwareDownloadCommand(url, destination, maximumBytes));
196
+ if (received.exitCode !== 0) {
197
+ rmSync(destination, { force: true });
198
+ throw new Error(`software download failed (${received.exitCode}): ${received.output.trim()}`);
199
+ }
178
200
  chmodSync(destination, 384);
179
201
  if (statSync(destination).size > maximumBytes) {
180
202
  rmSync(destination, { force: true });
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.78";
2
+ export declare const VERSION = "0.1.80";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgezero/agent",
3
- "version": "0.1.78",
3
+ "version": "0.1.80",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "check": "tsc --noEmit",
@@ -88,6 +88,10 @@
88
88
  "types": "./dist/deployment-targets.d.ts",
89
89
  "default": "./dist/deployment-targets.js"
90
90
  },
91
+ "./deployment-topology": {
92
+ "types": "./dist/deployment-topology.d.ts",
93
+ "default": "./dist/deployment-topology.js"
94
+ },
91
95
  "./deployment-connectivity": {
92
96
  "types": "./dist/deployment-connectivity.d.ts",
93
97
  "default": "./dist/deployment-connectivity.js"