@forgezero/agent 0.1.79 → 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.
package/dist/fz-agent.js CHANGED
@@ -6384,7 +6384,7 @@ function reference(value, where) {
6384
6384
  const row2 = object(value, where);
6385
6385
  exact3(row2, ["$ref"], where);
6386
6386
  const coordinate = boundedString(row2.$ref, `${where}.$ref`, 256);
6387
- if (!/^(inputs\.[a-z][A-Za-z0-9-]{0,62}|steps\.[a-z][A-Za-z0-9-]{0,62}\.(status|outputs\.[a-z][A-Za-z0-9-]{0,62})|components\.[a-z][A-Za-z0-9-]{0,62}\.[a-z][A-Za-z0-9-]{0,62}|targets\.[a-z][A-Za-z0-9-]{0,62}\.[a-z][A-Za-z0-9-]{0,62}|deployment\.[a-z][A-Za-z0-9-]{0,62}|execution\.(targetName|slot|releaseExecutor|runtime|cpuCores|memoryMiB|storageGiB))$/.test(coordinate)) {
6387
+ if (!/^(inputs\.[a-z][A-Za-z0-9-]{0,62}|steps\.[a-z][A-Za-z0-9-]{0,62}\.(status|outputs\.[a-z][A-Za-z0-9-]{0,62})|components\.[a-z][A-Za-z0-9-]{0,62}\.[a-z][A-Za-z0-9-]{0,62}|targets\.[a-z][A-Za-z0-9-]{0,62}\.[a-z][A-Za-z0-9-]{0,62}|deployment\.[a-z][A-Za-z0-9-]{0,62}|execution\.(targetName|slot|releaseExecutor|runtime|cpuCores|memoryMiB|storageGiB|site|topologyRole|topologyTransport|localRelayAddress))$/.test(coordinate)) {
6388
6388
  fail(where, "contains an unsupported reference.");
6389
6389
  }
6390
6390
  return { $ref: coordinate };
@@ -6816,6 +6816,12 @@ function validateStep(value, where, targets, context) {
6816
6816
  if (!targets.has(String(scope.target)))
6817
6817
  fail(`${where}.scope.target`, "must name an existing target.");
6818
6818
  integer(scope.size, `${where}.scope.size`, 1, 32);
6819
+ } else if (scope.kind === "topology-role") {
6820
+ exact3(scope, ["kind", "target", "role"], `${where}.scope`);
6821
+ if (!targets.has(String(scope.target)))
6822
+ fail(`${where}.scope.target`, "must name an existing target.");
6823
+ if (!["relay", "standby", "member"].includes(String(scope.role)))
6824
+ fail(`${where}.scope.role`, "is unsupported.");
6819
6825
  } else
6820
6826
  fail(`${where}.scope.kind`, "is unsupported.");
6821
6827
  if (row2.timeoutMs !== undefined)
@@ -6970,7 +6976,7 @@ function compileDeployment(sourceValue) {
6970
6976
  const targetDefinitions = new Map;
6971
6977
  for (const [targetName, value] of targetEntries) {
6972
6978
  const target = object(value, `deployment.spec.targets.${targetName}`);
6973
- exact3(target, ["kind", "selector", "cardinality", "allocation", "provisioning", "connectivity", "placement"], `deployment.spec.targets.${targetName}`);
6979
+ exact3(target, ["kind", "selector", "cardinality", "allocation", "provisioning", "connectivity", "topology", "placement"], `deployment.spec.targets.${targetName}`);
6974
6980
  if (target.kind !== "compute")
6975
6981
  fail(`deployment.spec.targets.${targetName}.kind`, "must be compute.");
6976
6982
  const selector = object(target.selector, `deployment.spec.targets.${targetName}.selector`);
@@ -7092,6 +7098,45 @@ function compileDeployment(sourceValue) {
7092
7098
  }
7093
7099
  }
7094
7100
  }
7101
+ if (target.topology !== undefined) {
7102
+ const topology = object(target.topology, `deployment.spec.targets.${targetName}.topology`);
7103
+ exact3(topology, ["groupBy", "relays", "crossMetal"], `deployment.spec.targets.${targetName}.topology`);
7104
+ if (topology.groupBy !== "metal")
7105
+ fail(`deployment.spec.targets.${targetName}.topology.groupBy`, "must be metal.");
7106
+ const relays = object(topology.relays, `deployment.spec.targets.${targetName}.topology.relays`);
7107
+ exact3(relays, ["activePerMetal", "standbyPerMetal", "healthPort", "routedTcpPorts"], `deployment.spec.targets.${targetName}.topology.relays`);
7108
+ if (relays.activePerMetal !== 1 || ![0, 1].includes(Number(relays.standbyPerMetal)))
7109
+ fail(`deployment.spec.targets.${targetName}.topology.relays`, "requires one active relay and zero or one standby per metal.");
7110
+ integer(relays.healthPort, `deployment.spec.targets.${targetName}.topology.relays.healthPort`, 1, 65535);
7111
+ if (!Array.isArray(relays.routedTcpPorts) || relays.routedTcpPorts.length < 1 || relays.routedTcpPorts.length > 64 || new Set(relays.routedTcpPorts).size !== relays.routedTcpPorts.length) {
7112
+ fail(`deployment.spec.targets.${targetName}.topology.relays.routedTcpPorts`, "must contain 1 to 64 unique ports.");
7113
+ }
7114
+ for (const [index, port] of relays.routedTcpPorts.entries()) {
7115
+ integer(port, `deployment.spec.targets.${targetName}.topology.relays.routedTcpPorts.${index}`, 1, 65535);
7116
+ }
7117
+ if (!relays.routedTcpPorts.includes(relays.healthPort)) {
7118
+ fail(`deployment.spec.targets.${targetName}.topology.relays`, "routedTcpPorts must include healthPort.");
7119
+ }
7120
+ const crossMetal = object(topology.crossMetal, `deployment.spec.targets.${targetName}.topology.crossMetal`);
7121
+ if (crossMetal.mode === "private-lan")
7122
+ exact3(crossMetal, ["mode"], `deployment.spec.targets.${targetName}.topology.crossMetal`);
7123
+ else if (crossMetal.mode === "cloudflare-warp" || crossMetal.mode === "auto") {
7124
+ exact3(crossMetal, crossMetal.mode === "auto" ? ["mode", "directPrivateMaximumMetals", "credential", "network"] : ["mode", "credential", "network"], `deployment.spec.targets.${targetName}.topology.crossMetal`);
7125
+ if (crossMetal.mode === "auto" && crossMetal.directPrivateMaximumMetals !== 2)
7126
+ fail(`deployment.spec.targets.${targetName}.topology.crossMetal.directPrivateMaximumMetals`, "must be 2.");
7127
+ if (!credentials[named(crossMetal.credential, `deployment.spec.targets.${targetName}.topology.crossMetal.credential`)])
7128
+ fail(`deployment.spec.targets.${targetName}.topology.crossMetal.credential`, "must name a declared credential.");
7129
+ if (typeof crossMetal.network === "string")
7130
+ named(crossMetal.network, `deployment.spec.targets.${targetName}.topology.crossMetal.network`);
7131
+ else
7132
+ reference(crossMetal.network, `deployment.spec.targets.${targetName}.topology.crossMetal.network`);
7133
+ } else
7134
+ fail(`deployment.spec.targets.${targetName}.topology.crossMetal.mode`, "is unsupported.");
7135
+ if (allocation.sharing !== "exclusive")
7136
+ fail(`deployment.spec.targets.${targetName}.allocation.sharing`, "must be exclusive for a per-metal relay topology.");
7137
+ if (minimum < 1 + Number(relays.standbyPerMetal))
7138
+ fail(`deployment.spec.targets.${targetName}.cardinality.minimum`, "must allow the declared relay and standby.");
7139
+ }
7095
7140
  if (target.placement !== undefined)
7096
7141
  jsonValue(target.placement, `deployment.spec.targets.${targetName}.placement`);
7097
7142
  targetDefinitions.set(targetName, target);
@@ -7115,6 +7160,9 @@ function compileDeployment(sourceValue) {
7115
7160
  if (target.connectivity?.private?.mode === "cloudflare-warp" && ![...requirementDefinitions.values()].some(({ capability }) => capability === "network.private")) {
7116
7161
  fail(`deployment.spec.targets.${targetName}.connectivity.private`, "requires one network.private provider requirement.");
7117
7162
  }
7163
+ if (target.topology && target.topology.crossMetal.mode !== "private-lan" && ![...requirementDefinitions.values()].some(({ capability }) => capability === "network.private")) {
7164
+ fail(`deployment.spec.targets.${targetName}.topology.crossMetal`, "requires one network.private provider requirement.");
7165
+ }
7118
7166
  }
7119
7167
  const components = object(spec.components, "deployment.spec.components");
7120
7168
  const componentEntries = Object.entries(components);
@@ -7372,6 +7420,19 @@ function resolveDeploymentTargets(plan, supplied = {}) {
7372
7420
  } : { mode: "disabled" } } : {}
7373
7421
  };
7374
7422
  }
7423
+ let topology;
7424
+ if (target.topology) {
7425
+ const crossMetal = target.topology.crossMetal;
7426
+ topology = {
7427
+ groupBy: "metal",
7428
+ relays: { ...target.topology.relays },
7429
+ crossMetal: crossMetal.mode === "private-lan" ? { mode: "private-lan" } : {
7430
+ ...crossMetal,
7431
+ credential: credentialSource(crossMetal.credential, `target ${name} topology WARP credential`),
7432
+ network: stringCoordinate(crossMetal.network, `target ${name} topology WARP network`)
7433
+ }
7434
+ };
7435
+ }
7375
7436
  return {
7376
7437
  name,
7377
7438
  profiles: [...target.selector.profiles],
@@ -7390,7 +7451,8 @@ function resolveDeploymentTargets(plan, supplied = {}) {
7390
7451
  }
7391
7452
  },
7392
7453
  ...target.provisioning ? { provisioning: { ...target.provisioning } } : {},
7393
- ...connectivity ? { connectivity } : {}
7454
+ ...connectivity ? { connectivity } : {},
7455
+ ...topology ? { topology } : {}
7394
7456
  };
7395
7457
  });
7396
7458
  }
@@ -8096,12 +8158,19 @@ function createDeploymentManager(options) {
8096
8158
  if (!selected || mismatches.length > 0) {
8097
8159
  throw new DeploymentError("PIPELINE_FAILED", `the control-plane target assignment does not match this exact deployment plan (${mismatches.join(", ")})`);
8098
8160
  }
8099
- const expectedPrivate = selected.connectivity?.private;
8161
+ const assignedTopology = assigned.topology;
8162
+ const selectedTopology = selected.topology;
8163
+ const topologyMatches = selectedTopology ? Boolean(assignedTopology && /^[A-Za-z0-9.-]{1,253}$/.test(assignedTopology.site) && /^[A-Za-z0-9.-]{1,253}$/.test(assignedTopology.nodeIdentity) && /^[A-Za-z0-9._:-]{1,256}$/.test(assignedTopology.localRelayReference) && /^sha256:[a-f0-9]{64}$/.test(assignedTopology.generation) && /^(?:\d{1,3}\.){3}0\/24$/.test(assignedTopology.siteCidr) && assignedTopology.localRelayAddress.length <= 64 && assignedTopology.localPeerAddresses.length <= 1023 && assignedTopology.localPeerAddresses.every((address) => typeof address === "string" && address.length <= 64) && assignedTopology.localPeerIdentities.length === assignedTopology.localPeerAddresses.length && assignedTopology.localPeerIdentities.every((identity) => /^[A-Za-z0-9.-]{1,253}$/.test(identity)) && assignedTopology.remoteRelayAddresses.length <= 1023 && assignedTopology.remoteRelayAddresses.every((address) => typeof address === "string" && address.length <= 64) && assignedTopology.remoteRelayIdentities.length === assignedTopology.remoteRelayAddresses.length && assignedTopology.remoteRelayIdentities.every((identity) => /^[A-Za-z0-9.-]{1,253}$/.test(identity)) && assignedTopology.memberIdentities.length >= 1 && assignedTopology.memberIdentities.length <= 1024 && new Set(assignedTopology.memberIdentities).size === assignedTopology.memberIdentities.length && assignedTopology.memberIdentities.includes(assignedTopology.nodeIdentity) && assignedTopology.memberIdentities.every((identity) => /^[A-Za-z0-9.-]{1,253}$/.test(identity)) && (assignedTopology.role === "member" ? assignedTopology.remoteRelayAddresses.length === 0 : assignedTopology.remoteSiteCidrs.length === assignedTopology.remoteRelayAddresses.length) && assignedTopology.remoteSiteCidrs.every((cidr) => typeof cidr === "string" && /^(?:\d{1,3}\.){3}0\/24$/.test(cidr)) && Number.isSafeInteger(assignedTopology.healthPort) && assignedTopology.healthPort >= 1 && assignedTopology.healthPort <= 65535 && assignedTopology.routedTcpPorts.length >= 1 && assignedTopology.routedTcpPorts.length <= 64 && new Set(assignedTopology.routedTcpPorts).size === assignedTopology.routedTcpPorts.length && assignedTopology.routedTcpPorts.includes(assignedTopology.healthPort) && assignedTopology.routedTcpPorts.every((port) => Number.isSafeInteger(port) && port >= 1 && port <= 65535) && (assignedTopology.role !== "member" || assignedTopology.transport === "private-lan" && assignedTopology.warpRequired === false && assignedTopology.remoteRelayAddresses.length === 0 && assignedTopology.localPeerAddresses.length === 1) && (assignedTopology.warpRequired ? assignedTopology.role !== "member" && assignedTopology.transport === "cloudflare-warp" && selectedTopology.crossMetal.mode !== "private-lan" : assignedTopology.transport === "private-lan")) : assignedTopology === undefined;
8164
+ const expectedPrivate = selectedTopology ? assignedTopology?.warpRequired ? selectedTopology.crossMetal.mode === "private-lan" ? undefined : {
8165
+ mode: "cloudflare-warp",
8166
+ credential: selectedTopology.crossMetal.credential,
8167
+ network: selectedTopology.crossMetal.network
8168
+ } : { mode: "private-lan" } : selected.connectivity?.private;
8100
8169
  const expectedPublic = selected.connectivity?.public;
8101
8170
  const assignedPrivate = assigned.connectivity?.private;
8102
8171
  const assignedPublic = assigned.connectivity?.public;
8103
8172
  const publicMatches = expectedPublic?.mode === "cloudflare-tunnel" ? assignedPublic?.mode === "cloudflare-tunnel" && assignedPublic.credential === expectedPublic.credential && assignedPublic.service.protocol === expectedPublic.service.protocol && assignedPublic.service.port === expectedPublic.service.port && assignedPublic.hostname === (expectedPublic.hostname.mode === "static" ? `${expectedPublic.hostname.label}.${expectedPublic.zone}` : `${expectedPublic.hostname.prefix}${expectedPublic.hostname.startAt + assigned.slot}.${expectedPublic.zone}`) : expectedPublic?.mode === "disabled" ? assignedPublic?.mode === "disabled" : assignedPublic === undefined;
8104
- if (JSON.stringify(assignedPrivate) !== JSON.stringify(expectedPrivate) || !publicMatches) {
8173
+ if (!topologyMatches || JSON.stringify(assignedPrivate) !== JSON.stringify(expectedPrivate) || !publicMatches) {
8105
8174
  throw new DeploymentError("PIPELINE_FAILED", "the control-plane connectivity assignment does not match this target slot");
8106
8175
  }
8107
8176
  targetNames = new Set([assigned.targetName]);
@@ -8118,7 +8187,24 @@ function createDeploymentManager(options) {
8118
8187
  FZ_TARGET_CPU_CORES: String(assigned.resources.cpuCores),
8119
8188
  FZ_TARGET_MEMORY_MIB: String(assigned.resources.memoryMiB),
8120
8189
  FZ_TARGET_STORAGE_GIB: String(assigned.resources.storageGiB),
8121
- FZ_TARGET_EXECUTION: assigned.execution
8190
+ FZ_TARGET_EXECUTION: assigned.execution,
8191
+ ...assigned.topology ? {
8192
+ FZ_TOPOLOGY_SITE: assigned.topology.site,
8193
+ FZ_TOPOLOGY_NODE_IDENTITY: assigned.topology.nodeIdentity,
8194
+ FZ_TOPOLOGY_SITE_CIDR: assigned.topology.siteCidr,
8195
+ FZ_TOPOLOGY_ROLE: assigned.topology.role,
8196
+ FZ_TOPOLOGY_TRANSPORT: assigned.topology.transport,
8197
+ FZ_TOPOLOGY_LOCAL_RELAY: assigned.topology.localRelayAddress,
8198
+ FZ_TOPOLOGY_LOCAL_PEERS: assigned.topology.localPeerAddresses.join(","),
8199
+ FZ_TOPOLOGY_LOCAL_PEER_IDENTITIES: assigned.topology.localPeerIdentities.join(","),
8200
+ FZ_TOPOLOGY_REMOTE_RELAYS: assigned.topology.remoteRelayAddresses.join(","),
8201
+ FZ_TOPOLOGY_REMOTE_RELAY_IDENTITIES: assigned.topology.remoteRelayIdentities.join(","),
8202
+ FZ_TOPOLOGY_REMOTE_CIDRS: assigned.topology.remoteSiteCidrs.join(","),
8203
+ FZ_TOPOLOGY_MEMBERS: assigned.topology.memberIdentities.join(","),
8204
+ FZ_TOPOLOGY_HEALTH_PORT: String(assigned.topology.healthPort),
8205
+ FZ_TOPOLOGY_ROUTED_TCP_PORTS: assigned.topology.routedTcpPorts.join(","),
8206
+ FZ_TOPOLOGY_GENERATION: assigned.topology.generation
8207
+ } : {}
8122
8208
  });
8123
8209
  for (const targetName of targetNames) {
8124
8210
  const capacity2 = resolvedTargets.find(({ name }) => name === targetName)?.allocation.resources;
@@ -8174,11 +8260,15 @@ function createDeploymentManager(options) {
8174
8260
  return { changed: true };
8175
8261
  },
8176
8262
  "forgezero.network/ensure@1": async ({ resolved }) => {
8263
+ if (!assigned && bootstrapBundle && typeof resolved.target === "string" && targetNames.has(resolved.target)) {
8264
+ return { changed: false, evidence: { target: resolved.target, authority: "attended-bootstrap" } };
8265
+ }
8177
8266
  if (!assigned || typeof resolved.target !== "string" || resolved.target !== assigned.targetName) {
8178
8267
  throw new DeploymentActionError("refused", "network.ensure must target this exact control-plane assignment");
8179
8268
  }
8180
- if (!assigned.connectivity)
8181
- return { changed: false, evidence: { target: assigned.targetName } };
8269
+ if (!assigned.connectivity) {
8270
+ return { changed: false, evidence: { target: assigned.targetName, authority: "none" } };
8271
+ }
8182
8272
  if (!options.ensureSoftware || !options.applyConnectivity) {
8183
8273
  throw new DeploymentActionError("provider-unavailable", "the supervised connectivity helper is unavailable");
8184
8274
  }
@@ -8187,12 +8277,31 @@ function createDeploymentManager(options) {
8187
8277
  requirements.push({ id: "cloudflared", version: "latest" });
8188
8278
  if (assigned.connectivity.private?.mode === "cloudflare-warp")
8189
8279
  requirements.push({ id: "cloudflare-warp", version: "latest" });
8280
+ if (assigned.topology)
8281
+ requirements.push({ id: "ufw", version: "latest" });
8190
8282
  if (requirements.length > 0)
8191
8283
  await options.ensureSoftware(requirements);
8192
8284
  const evidence = await options.applyConnectivity({
8193
8285
  key: `${options.key}:${assigned.targetName}:${assigned.slot}`,
8194
8286
  intent: assigned.connectivity,
8195
- capabilities: request.connectivityCapabilities ?? {}
8287
+ capabilities: request.connectivityCapabilities ?? {},
8288
+ ...assigned.topology ? { topology: {
8289
+ site: assigned.topology.site,
8290
+ nodeIdentity: assigned.topology.nodeIdentity,
8291
+ siteCidr: assigned.topology.siteCidr,
8292
+ role: assigned.topology.role,
8293
+ transport: assigned.topology.transport,
8294
+ localRelayAddress: assigned.topology.localRelayAddress,
8295
+ localPeerAddresses: assigned.topology.localPeerAddresses,
8296
+ localPeerIdentities: assigned.topology.localPeerIdentities,
8297
+ remoteRelayAddresses: assigned.topology.remoteRelayAddresses,
8298
+ remoteRelayIdentities: assigned.topology.remoteRelayIdentities,
8299
+ remoteSiteCidrs: assigned.topology.remoteSiteCidrs,
8300
+ memberIdentities: assigned.topology.memberIdentities,
8301
+ healthPort: assigned.topology.healthPort,
8302
+ routedTcpPorts: assigned.topology.routedTcpPorts,
8303
+ generation: assigned.topology.generation
8304
+ } } : {}
8196
8305
  });
8197
8306
  return { changed: true, evidence: structuredClone(evidence) };
8198
8307
  },
@@ -8301,7 +8410,13 @@ function createDeploymentManager(options) {
8301
8410
  runtime: assigned.execution,
8302
8411
  cpuCores: assigned.resources.cpuCores,
8303
8412
  memoryMiB: assigned.resources.memoryMiB,
8304
- storageGiB: assigned.resources.storageGiB
8413
+ storageGiB: assigned.resources.storageGiB,
8414
+ ...assigned.topology ? {
8415
+ site: assigned.topology.site,
8416
+ topologyRole: assigned.topology.role,
8417
+ topologyTransport: assigned.topology.transport,
8418
+ localRelayAddress: assigned.topology.localRelayAddress
8419
+ } : {}
8305
8420
  } : {
8306
8421
  targetName: matchingTargetNames[0],
8307
8422
  slot: 0,
@@ -8311,7 +8426,7 @@ function createDeploymentManager(options) {
8311
8426
  memoryMiB: resolvedTargets[0].allocation.resources.memoryMiB,
8312
8427
  storageGiB: resolvedTargets[0].allocation.resources.storageGiB
8313
8428
  },
8314
- shouldRun: (step2) => step2.scope.kind === "release-executor" || step2.scope.kind === "elected-one" ? request.releaseExecutor === true : targetNames.has(step2.scope.target)
8429
+ shouldRun: (step2) => step2.scope.kind === "release-executor" || step2.scope.kind === "elected-one" ? request.releaseExecutor === true : step2.scope.kind === "topology-role" ? targetNames.has(step2.scope.target) && assigned?.topology?.role === step2.scope.role : targetNames.has(step2.scope.target)
8315
8430
  });
8316
8431
  const phase = {
8317
8432
  pipeline: `${plan.name}:deploy`,
@@ -8751,7 +8866,8 @@ async function pullDeploymentOnce(options) {
8751
8866
  labels: { ...target.labels },
8752
8867
  allocation: { ...target.allocation, resources: { ...target.allocation.resources } },
8753
8868
  ...target.provisioning ? { provisioning: { ...target.provisioning } } : {},
8754
- ...target.connectivity ? { connectivity: structuredClone(target.connectivity) } : {}
8869
+ ...target.connectivity ? { connectivity: structuredClone(target.connectivity) } : {},
8870
+ ...target.topology ? { topology: structuredClone(target.topology) } : {}
8755
8871
  })) } : {},
8756
8872
  ...result.capacity ? { capacity: {
8757
8873
  recommendedConcurrency: result.capacity.recommendedConcurrency,
@@ -9215,7 +9331,7 @@ async function writeAndCloseProcessInput(input, value) {
9215
9331
  }
9216
9332
 
9217
9333
  // src/version.ts
9218
- var VERSION2 = "0.1.79";
9334
+ var VERSION2 = "0.1.80";
9219
9335
 
9220
9336
  // src/ssh-bootstrap.ts
9221
9337
  class SshBootstrapError extends Error {
@@ -12860,6 +12976,7 @@ async function activateContainer(requestValue, host = defaultHost2) {
12860
12976
  // src/deployment-connectivity.ts
12861
12977
  import { createHash as createHash11 } from "crypto";
12862
12978
  import { mkdirSync as mkdirSync12, renameSync as renameSync10, writeFileSync as writeFileSync13 } from "fs";
12979
+ import { createConnection as createConnection2 } from "net";
12863
12980
  import { dirname as dirname9 } from "path";
12864
12981
  var TOKEN2 = /^[A-Za-z0-9._-]{40,16384}$/;
12865
12982
  var UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i;
@@ -12904,6 +13021,21 @@ var defaultHost3 = {
12904
13021
  const [stdout, stderr, exitCode] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
12905
13022
  return { exitCode, output: `${stdout}${stderr}` };
12906
13023
  },
13024
+ probe: (address, port, timeoutMs) => new Promise((resolve6) => {
13025
+ const socket = createConnection2({ host: address, port });
13026
+ let settled = false;
13027
+ const finish = (value) => {
13028
+ if (settled)
13029
+ return;
13030
+ settled = true;
13031
+ socket.destroy();
13032
+ resolve6(value);
13033
+ };
13034
+ socket.setTimeout(timeoutMs);
13035
+ socket.once("connect", () => finish(true));
13036
+ socket.once("timeout", () => finish(false));
13037
+ socket.once("error", () => finish(false));
13038
+ }),
12907
13039
  sleep: (ms) => Bun.sleep(ms)
12908
13040
  };
12909
13041
  function validate2(request) {
@@ -12918,7 +13050,7 @@ function validate2(request) {
12918
13050
  } else if (request.capabilities.public)
12919
13051
  throw new Error("unexpected public deployment capability");
12920
13052
  if (privateIntent?.mode === "cloudflare-warp") {
12921
- if (!NAME3.test(privateIntent.credential) || privateIntent.network.length < 1 || privateIntent.network.length > 128 || !request.capabilities.private || !UUID.test(request.capabilities.private.connectorId) || !TOKEN2.test(request.capabilities.private.connectorToken)) {
13053
+ if (!NAME3.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) || !TOKEN2.test(request.capabilities.private.connectorToken)) {
12922
13054
  throw new Error("private deployment connectivity is malformed");
12923
13055
  }
12924
13056
  } else if (request.capabilities.private)
@@ -12928,6 +13060,22 @@ async function applyDeploymentConnectivity(request, host = defaultHost3) {
12928
13060
  validate2(request);
12929
13061
  const id2 = idFor3(request.key);
12930
13062
  const evidence = { key: request.key };
13063
+ const topology = request.topology;
13064
+ if (topology) {
13065
+ const values = [topology.localRelayAddress, ...topology.localPeerAddresses, ...topology.remoteRelayAddresses];
13066
+ const identities = [
13067
+ topology.nodeIdentity,
13068
+ ...topology.localPeerIdentities,
13069
+ ...topology.remoteRelayIdentities,
13070
+ ...topology.memberIdentities
13071
+ ];
13072
+ 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") {
13073
+ throw new Error("deployment topology is malformed");
13074
+ }
13075
+ if (request.intent.private?.mode === "cloudflare-warp" !== (topology.transport === "cloudflare-warp")) {
13076
+ throw new Error("deployment topology transport differs from private connectivity intent");
13077
+ }
13078
+ }
12931
13079
  if (request.intent.public?.mode === "cloudflare-tunnel") {
12932
13080
  const capability = request.capabilities.public;
12933
13081
  const credentialName = `FZ_TUNNEL_${id2.toUpperCase()}`;
@@ -12965,6 +13113,12 @@ WantedBy=multi-user.target
12965
13113
  const capability = request.capabilities.private;
12966
13114
  const credentialPath = "/etc/forgezero/creds/CF_WARP_CONNECTOR_TOKEN.cred";
12967
13115
  const unit2 = "forgezero-deployment-mesh.service";
13116
+ const forwarding = "/etc/sysctl.d/99-forgezero-mesh.conf";
13117
+ host.write(forwarding, `net.ipv4.ip_forward = 1
13118
+ net.ipv6.conf.all.forwarding = 1
13119
+ net.ipv6.conf.all.accept_ra = 2
13120
+ `, 420);
13121
+ await checked7(host, ["/usr/sbin/sysctl", "-p", forwarding], "Mesh subnet forwarding");
12968
13122
  await host.seal("CF_WARP_CONNECTOR_TOKEN", credentialPath, capability.connectorToken);
12969
13123
  host.write(`/etc/systemd/system/${unit2}`, `[Unit]
12970
13124
  Description=ForgeZero deployment Mesh/WARP connector
@@ -12999,6 +13153,95 @@ WantedBy=multi-user.target
12999
13153
  if (!evidence.private)
13000
13154
  throw new Error("WARP connector readiness failed");
13001
13155
  }
13156
+ if (topology) {
13157
+ const syncAddresses = topology.role === "relay" ? [...topology.localPeerAddresses, ...topology.remoteRelayAddresses] : [topology.localRelayAddress];
13158
+ const topologyEpoch = `topology-${topology.generation.slice("sha256:".length, "sha256:".length + 48)}`;
13159
+ host.write("/etc/forgezero/deployment-topology.env", [
13160
+ `FZ_TOPOLOGY_NODE_IDENTITY=${topology.nodeIdentity}`,
13161
+ `FZ_TOPOLOGY_PEER_ADDRESSES=${[...new Set(syncAddresses)].join(",")}`,
13162
+ `FZ_TOPOLOGY_MEMBERS=${topology.memberIdentities.join(",")}`,
13163
+ `FZ_TOPOLOGY_EPOCH=${topologyEpoch}`,
13164
+ `FZ_TOPOLOGY_SITE=${topology.site}`,
13165
+ `FZ_TOPOLOGY_ROLE=${topology.role}`,
13166
+ `FZ_TOPOLOGY_TRANSPORT=${topology.transport}`,
13167
+ ""
13168
+ ].join(`
13169
+ `), 384);
13170
+ const routeUnit = "forgezero-deployment-topology.service";
13171
+ const routeUnitPath = `/etc/systemd/system/${routeUnit}`;
13172
+ 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] })) : [];
13173
+ for (const { via } of routePairs) {
13174
+ await checked7(host, ["/usr/sbin/ip", "route", "get", via], `private route gateway ${via}`);
13175
+ }
13176
+ await host.exec(["/usr/bin/systemctl", "disable", "--now", routeUnit]);
13177
+ const forwarding = topology.role !== "member" && topology.remoteSiteCidrs.length > 0 ? `ExecStart=/usr/sbin/sysctl -w net.ipv4.ip_forward=1
13178
+ ` : "";
13179
+ const starts = routePairs.map(({ network, via }) => `ExecStart=/usr/sbin/ip route replace ${network} via ${via}`).join(`
13180
+ `);
13181
+ const stops = routePairs.map(({ network, via }) => `ExecStop=-/usr/sbin/ip route del ${network} via ${via}`).join(`
13182
+ `);
13183
+ const noOp = !forwarding && !starts ? `ExecStart=/usr/bin/true
13184
+ ` : "";
13185
+ host.write(routeUnitPath, `[Unit]
13186
+ Description=ForgeZero fenced deployment site routing
13187
+ After=network-online.target${topology.transport === "cloudflare-warp" ? " forgezero-deployment-mesh.service" : ""}
13188
+ Wants=network-online.target
13189
+
13190
+ [Service]
13191
+ Type=oneshot
13192
+ RemainAfterExit=yes
13193
+ ${forwarding}${noOp}${starts}${starts ? `
13194
+ ` : ""}${stops}${stops ? `
13195
+ ` : ""}
13196
+ [Install]
13197
+ WantedBy=multi-user.target
13198
+ `, 420);
13199
+ for (const source of [...new Set([topology.siteCidr, ...topology.remoteSiteCidrs])]) {
13200
+ for (const port of topology.routedTcpPorts) {
13201
+ await checked7(host, ["/usr/sbin/ufw", "allow", "from", source, "to", "any", "port", String(port), "proto", "tcp"], `private service ${source}:${port}`);
13202
+ }
13203
+ }
13204
+ if (topology.role !== "member")
13205
+ for (const remoteCidr of topology.remoteSiteCidrs) {
13206
+ for (const port of topology.routedTcpPorts) {
13207
+ await checked7(host, [
13208
+ "/usr/sbin/ufw",
13209
+ "route",
13210
+ "allow",
13211
+ "proto",
13212
+ "tcp",
13213
+ "from",
13214
+ topology.siteCidr,
13215
+ "to",
13216
+ remoteCidr,
13217
+ "port",
13218
+ String(port)
13219
+ ], `private routed service ${topology.siteCidr}->${remoteCidr}:${port}`);
13220
+ }
13221
+ }
13222
+ await checked7(host, ["/usr/bin/systemctl", "daemon-reload"], "topology daemon reload");
13223
+ await checked7(host, ["/usr/bin/systemctl", "enable", "--now", routeUnit], "topology routes");
13224
+ const probes = topology.role === "member" ? [topology.localRelayAddress] : topology.remoteRelayAddresses;
13225
+ for (const address of [...new Set(probes)]) {
13226
+ await checked7(host, ["/usr/sbin/ip", "route", "get", address], `private route ${address}`);
13227
+ let ready = false;
13228
+ for (let attempt = 0;attempt < 10; attempt += 1) {
13229
+ if (await host.probe(address, topology.healthPort, 2000)) {
13230
+ ready = true;
13231
+ break;
13232
+ }
13233
+ await host.sleep(1000);
13234
+ }
13235
+ if (!ready)
13236
+ throw new Error(`private topology relay ${address}:${topology.healthPort} is unreachable`);
13237
+ }
13238
+ evidence.topology = {
13239
+ site: topology.site,
13240
+ siteCidr: topology.siteCidr,
13241
+ role: topology.role,
13242
+ probed: [...new Set(probes)]
13243
+ };
13244
+ }
13002
13245
  return evidence;
13003
13246
  }
13004
13247
 
@@ -16439,6 +16682,115 @@ async function readBoundedStdin(maxBytes) {
16439
16682
  throw new Error(`stdin request exceeds ${maxBytes} bytes`);
16440
16683
  return Buffer.from(stdout).toString("utf8");
16441
16684
  }
16685
+ // src/deployment-topology.ts
16686
+ import { isIP as isIP6 } from "net";
16687
+
16688
+ class DeploymentTopologyError extends Error {
16689
+ constructor(message) {
16690
+ super(message);
16691
+ this.name = "DeploymentTopologyError";
16692
+ }
16693
+ }
16694
+ var SAFE = /^(?=.{1,253}$)[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?$/;
16695
+ var PRIVATE_V4 = (value) => {
16696
+ const parts = value.split(".").map(Number);
16697
+ return parts.length === 4 && parts.every((part) => Number.isInteger(part) && part >= 0 && part <= 255) && (parts[0] === 10 || parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31 || parts[0] === 192 && parts[1] === 168);
16698
+ };
16699
+ var validSiteCidr = (value) => {
16700
+ const match = value.match(/^((?:\d{1,3}\.){3})0\/24$/);
16701
+ return Boolean(match && PRIVATE_V4(`${match[1]}1`));
16702
+ };
16703
+ function planDeploymentTopology(args) {
16704
+ const topology = args.target.topology;
16705
+ if (!topology)
16706
+ throw new DeploymentTopologyError(`target ${args.target.name} has no per-metal topology`);
16707
+ if (!/^[a-z][a-z0-9-]{0,62}$/.test(args.target.name) || !/^[A-Za-z0-9._:-]{1,128}$/.test(args.generation)) {
16708
+ throw new DeploymentTopologyError("topology identity is invalid");
16709
+ }
16710
+ if (args.placements.length < 1 || args.placements.length > 1024)
16711
+ throw new DeploymentTopologyError("topology requires 1 to 1024 placements");
16712
+ const references = new Set;
16713
+ const slots = new Set;
16714
+ for (const placement of args.placements) {
16715
+ if (!SAFE.test(placement.computeReference) || !SAFE.test(placement.nodeIdentity) || !SAFE.test(placement.metalHostname) || isIP6(placement.privateAddress) !== 4 || !PRIVATE_V4(placement.privateAddress) || !validSiteCidr(placement.siteCidr) || !placement.privateAddress.startsWith(placement.siteCidr.slice(0, -4)) || !Number.isSafeInteger(placement.slot) || placement.slot < 0 || placement.slot > 1e6 || references.has(placement.computeReference) || slots.has(placement.slot)) {
16716
+ throw new DeploymentTopologyError("topology placement is malformed or duplicated");
16717
+ }
16718
+ references.add(placement.computeReference);
16719
+ slots.add(placement.slot);
16720
+ }
16721
+ const sites = new Map;
16722
+ for (const placement of args.placements) {
16723
+ const members2 = sites.get(placement.metalHostname) ?? [];
16724
+ members2.push({ ...placement });
16725
+ sites.set(placement.metalHostname, members2);
16726
+ }
16727
+ for (const members2 of sites.values())
16728
+ members2.sort((left, right) => left.slot - right.slot || left.computeReference.localeCompare(right.computeReference));
16729
+ const siteCidrs = new Map;
16730
+ for (const [site, members2] of sites) {
16731
+ const cidrs = new Set(members2.map(({ siteCidr }) => siteCidr));
16732
+ if (cidrs.size !== 1)
16733
+ throw new DeploymentTopologyError(`metal ${site} has inconsistent private CIDRs`);
16734
+ siteCidrs.set(site, members2[0].siteCidr);
16735
+ }
16736
+ if (new Set(siteCidrs.values()).size !== siteCidrs.size) {
16737
+ throw new DeploymentTopologyError("deployment metals require non-overlapping private CIDRs");
16738
+ }
16739
+ const required = 1 + topology.relays.standbyPerMetal;
16740
+ for (const [site, members2] of sites)
16741
+ if (members2.length < required) {
16742
+ throw new DeploymentTopologyError(`metal ${site} cannot satisfy its relay and standby topology`);
16743
+ }
16744
+ const siteNames = [...sites.keys()].sort();
16745
+ let crossTransport;
16746
+ if (siteNames.length <= 2)
16747
+ crossTransport = "private-lan";
16748
+ else {
16749
+ if (topology.crossMetal.mode === "private-lan") {
16750
+ throw new DeploymentTopologyError("three or more metals require the declared Mesh/WARP authority");
16751
+ }
16752
+ crossTransport = "cloudflare-warp";
16753
+ }
16754
+ const active = new Map(siteNames.map((site) => [site, sites.get(site)[0]]));
16755
+ const memberIdentities = [...args.placements].sort((left, right) => left.slot - right.slot || left.computeReference.localeCompare(right.computeReference)).map(({ nodeIdentity }) => nodeIdentity);
16756
+ if (new Set(memberIdentities).size !== memberIdentities.length) {
16757
+ throw new DeploymentTopologyError("deployment node identities must be unique");
16758
+ }
16759
+ if (!Number.isSafeInteger(topology.relays.healthPort) || topology.relays.healthPort < 1 || topology.relays.healthPort > 65535 || !Array.isArray(topology.relays.routedTcpPorts) || topology.relays.routedTcpPorts.length < 1 || topology.relays.routedTcpPorts.length > 64 || new Set(topology.relays.routedTcpPorts).size !== topology.relays.routedTcpPorts.length || !topology.relays.routedTcpPorts.includes(topology.relays.healthPort) || topology.relays.routedTcpPorts.some((port) => !Number.isSafeInteger(port) || port < 1 || port > 65535)) {
16760
+ throw new DeploymentTopologyError("relay health and routed TCP ports are malformed");
16761
+ }
16762
+ return siteNames.flatMap((site) => {
16763
+ const members2 = sites.get(site);
16764
+ const relay = active.get(site);
16765
+ const remoteRelayAddresses = siteNames.filter((other) => other !== site).map((other) => active.get(other).privateAddress);
16766
+ const remoteRelayIdentities = siteNames.filter((other) => other !== site).map((other) => active.get(other).nodeIdentity);
16767
+ const remoteSiteCidrs = siteNames.filter((other) => other !== site).map((other) => siteCidrs.get(other));
16768
+ return members2.map((placement, index) => {
16769
+ const role = index === 0 ? "relay" : index === 1 && topology.relays.standbyPerMetal === 1 ? "standby" : "member";
16770
+ const crossSite = role !== "member" && siteNames.length > 1;
16771
+ const localPeers = role === "relay" ? members2.filter(({ privateAddress }) => privateAddress !== placement.privateAddress) : [relay];
16772
+ const localPeerAddresses = role === "relay" ? localPeers.map(({ privateAddress }) => privateAddress) : [relay.privateAddress];
16773
+ const localPeerIdentities = role === "relay" ? localPeers.map(({ nodeIdentity }) => nodeIdentity) : [relay.nodeIdentity];
16774
+ return {
16775
+ ...placement,
16776
+ role,
16777
+ transport: crossSite ? crossTransport : "private-lan",
16778
+ localRelayReference: relay.computeReference,
16779
+ localRelayAddress: relay.privateAddress,
16780
+ localPeerAddresses,
16781
+ localPeerIdentities,
16782
+ remoteRelayAddresses: crossSite ? remoteRelayAddresses : [],
16783
+ remoteRelayIdentities: crossSite ? remoteRelayIdentities : [],
16784
+ remoteSiteCidrs: siteNames.length > 1 ? remoteSiteCidrs : [],
16785
+ healthPort: topology.relays.healthPort,
16786
+ routedTcpPorts: [...topology.relays.routedTcpPorts],
16787
+ memberIdentities,
16788
+ warpRequired: crossSite && crossTransport === "cloudflare-warp",
16789
+ generation: args.generation
16790
+ };
16791
+ });
16792
+ });
16793
+ }
16442
16794
 
16443
16795
  // src/index.ts
16444
16796
  function agentCommandArgs(args, command3) {
@@ -17522,6 +17874,7 @@ export {
17522
17874
  projectVaultCoordinate,
17523
17875
  projectVaultCacheKey,
17524
17876
  probeAgentSocket,
17877
+ planDeploymentTopology,
17525
17878
  observeSoftwareHost,
17526
17879
  observeAgentHost,
17527
17880
  normalizeEgressTcpPorts,
@@ -17577,6 +17930,7 @@ export {
17577
17930
  OS_CATALOG,
17578
17931
  METAL_SYSTEMD_CREDENTIALS,
17579
17932
  MAX_AGENT_TARBALL_BYTES,
17933
+ DeploymentTopologyError,
17580
17934
  DeploymentInputError,
17581
17935
  DeploymentError,
17582
17936
  DEFAULT_SOFTWARE_HELPER_SOCKET,