@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/README.md +57 -6
- package/dist/agent-heartbeat.js +1 -1
- package/dist/bootstrap.js +21 -6
- package/dist/deploy-compiler.js +50 -2
- package/dist/deploy-plan-runner.js +50 -2
- package/dist/deploy-plan.js +50 -2
- package/dist/deploy.d.ts +37 -1
- package/dist/deploy.js +1 -0
- package/dist/deployment-connectivity.d.ts +26 -0
- package/dist/deployment-connectivity.js +128 -1
- package/dist/deployment-targets.js +15 -1
- package/dist/deployment-topology.d.ts +46 -0
- package/dist/deployment-topology.js +113 -0
- package/dist/deployment.d.ts +40 -0
- package/dist/fz-agent.js +368 -14
- package/dist/fz.js +75 -12
- package/dist/index.d.ts +1 -0
- package/dist/metal-bootstrap.js +1 -1
- package/dist/operator-bootstrap.js +21 -6
- package/dist/platform-bootstrap-runtime.d.ts +4 -1
- package/dist/platform-bootstrap-runtime.js +18 -4
- package/dist/platform-fleet-verification.js +147 -6
- package/dist/provision.js +129 -2
- package/dist/software-helper.js +128 -1
- package/dist/version.d.ts +1 -1
- package/package.json +5 -1
package/dist/fz.js
CHANGED
|
@@ -4837,7 +4837,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
|
|
|
4837
4837
|
var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
|
|
4838
4838
|
|
|
4839
4839
|
// src/version.ts
|
|
4840
|
-
var VERSION2 = "0.1.
|
|
4840
|
+
var VERSION2 = "0.1.80";
|
|
4841
4841
|
|
|
4842
4842
|
// src/software.ts
|
|
4843
4843
|
var PINNED_BUN_VERSION = "1.3.14";
|
|
@@ -10573,7 +10573,7 @@ function reference(value, where) {
|
|
|
10573
10573
|
const row2 = object(value, where);
|
|
10574
10574
|
exact3(row2, ["$ref"], where);
|
|
10575
10575
|
const coordinate = boundedString(row2.$ref, `${where}.$ref`, 256);
|
|
10576
|
-
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)) {
|
|
10576
|
+
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)) {
|
|
10577
10577
|
fail2(where, "contains an unsupported reference.");
|
|
10578
10578
|
}
|
|
10579
10579
|
return { $ref: coordinate };
|
|
@@ -11005,6 +11005,12 @@ function validateStep(value, where, targets, context) {
|
|
|
11005
11005
|
if (!targets.has(String(scope.target)))
|
|
11006
11006
|
fail2(`${where}.scope.target`, "must name an existing target.");
|
|
11007
11007
|
integer(scope.size, `${where}.scope.size`, 1, 32);
|
|
11008
|
+
} else if (scope.kind === "topology-role") {
|
|
11009
|
+
exact3(scope, ["kind", "target", "role"], `${where}.scope`);
|
|
11010
|
+
if (!targets.has(String(scope.target)))
|
|
11011
|
+
fail2(`${where}.scope.target`, "must name an existing target.");
|
|
11012
|
+
if (!["relay", "standby", "member"].includes(String(scope.role)))
|
|
11013
|
+
fail2(`${where}.scope.role`, "is unsupported.");
|
|
11008
11014
|
} else
|
|
11009
11015
|
fail2(`${where}.scope.kind`, "is unsupported.");
|
|
11010
11016
|
if (row2.timeoutMs !== undefined)
|
|
@@ -11159,7 +11165,7 @@ function compileDeployment(sourceValue) {
|
|
|
11159
11165
|
const targetDefinitions = new Map;
|
|
11160
11166
|
for (const [targetName, value] of targetEntries) {
|
|
11161
11167
|
const target = object(value, `deployment.spec.targets.${targetName}`);
|
|
11162
|
-
exact3(target, ["kind", "selector", "cardinality", "allocation", "provisioning", "connectivity", "placement"], `deployment.spec.targets.${targetName}`);
|
|
11168
|
+
exact3(target, ["kind", "selector", "cardinality", "allocation", "provisioning", "connectivity", "topology", "placement"], `deployment.spec.targets.${targetName}`);
|
|
11163
11169
|
if (target.kind !== "compute")
|
|
11164
11170
|
fail2(`deployment.spec.targets.${targetName}.kind`, "must be compute.");
|
|
11165
11171
|
const selector = object(target.selector, `deployment.spec.targets.${targetName}.selector`);
|
|
@@ -11281,6 +11287,45 @@ function compileDeployment(sourceValue) {
|
|
|
11281
11287
|
}
|
|
11282
11288
|
}
|
|
11283
11289
|
}
|
|
11290
|
+
if (target.topology !== undefined) {
|
|
11291
|
+
const topology = object(target.topology, `deployment.spec.targets.${targetName}.topology`);
|
|
11292
|
+
exact3(topology, ["groupBy", "relays", "crossMetal"], `deployment.spec.targets.${targetName}.topology`);
|
|
11293
|
+
if (topology.groupBy !== "metal")
|
|
11294
|
+
fail2(`deployment.spec.targets.${targetName}.topology.groupBy`, "must be metal.");
|
|
11295
|
+
const relays = object(topology.relays, `deployment.spec.targets.${targetName}.topology.relays`);
|
|
11296
|
+
exact3(relays, ["activePerMetal", "standbyPerMetal", "healthPort", "routedTcpPorts"], `deployment.spec.targets.${targetName}.topology.relays`);
|
|
11297
|
+
if (relays.activePerMetal !== 1 || ![0, 1].includes(Number(relays.standbyPerMetal)))
|
|
11298
|
+
fail2(`deployment.spec.targets.${targetName}.topology.relays`, "requires one active relay and zero or one standby per metal.");
|
|
11299
|
+
integer(relays.healthPort, `deployment.spec.targets.${targetName}.topology.relays.healthPort`, 1, 65535);
|
|
11300
|
+
if (!Array.isArray(relays.routedTcpPorts) || relays.routedTcpPorts.length < 1 || relays.routedTcpPorts.length > 64 || new Set(relays.routedTcpPorts).size !== relays.routedTcpPorts.length) {
|
|
11301
|
+
fail2(`deployment.spec.targets.${targetName}.topology.relays.routedTcpPorts`, "must contain 1 to 64 unique ports.");
|
|
11302
|
+
}
|
|
11303
|
+
for (const [index, port] of relays.routedTcpPorts.entries()) {
|
|
11304
|
+
integer(port, `deployment.spec.targets.${targetName}.topology.relays.routedTcpPorts.${index}`, 1, 65535);
|
|
11305
|
+
}
|
|
11306
|
+
if (!relays.routedTcpPorts.includes(relays.healthPort)) {
|
|
11307
|
+
fail2(`deployment.spec.targets.${targetName}.topology.relays`, "routedTcpPorts must include healthPort.");
|
|
11308
|
+
}
|
|
11309
|
+
const crossMetal = object(topology.crossMetal, `deployment.spec.targets.${targetName}.topology.crossMetal`);
|
|
11310
|
+
if (crossMetal.mode === "private-lan")
|
|
11311
|
+
exact3(crossMetal, ["mode"], `deployment.spec.targets.${targetName}.topology.crossMetal`);
|
|
11312
|
+
else if (crossMetal.mode === "cloudflare-warp" || crossMetal.mode === "auto") {
|
|
11313
|
+
exact3(crossMetal, crossMetal.mode === "auto" ? ["mode", "directPrivateMaximumMetals", "credential", "network"] : ["mode", "credential", "network"], `deployment.spec.targets.${targetName}.topology.crossMetal`);
|
|
11314
|
+
if (crossMetal.mode === "auto" && crossMetal.directPrivateMaximumMetals !== 2)
|
|
11315
|
+
fail2(`deployment.spec.targets.${targetName}.topology.crossMetal.directPrivateMaximumMetals`, "must be 2.");
|
|
11316
|
+
if (!credentials[named(crossMetal.credential, `deployment.spec.targets.${targetName}.topology.crossMetal.credential`)])
|
|
11317
|
+
fail2(`deployment.spec.targets.${targetName}.topology.crossMetal.credential`, "must name a declared credential.");
|
|
11318
|
+
if (typeof crossMetal.network === "string")
|
|
11319
|
+
named(crossMetal.network, `deployment.spec.targets.${targetName}.topology.crossMetal.network`);
|
|
11320
|
+
else
|
|
11321
|
+
reference(crossMetal.network, `deployment.spec.targets.${targetName}.topology.crossMetal.network`);
|
|
11322
|
+
} else
|
|
11323
|
+
fail2(`deployment.spec.targets.${targetName}.topology.crossMetal.mode`, "is unsupported.");
|
|
11324
|
+
if (allocation.sharing !== "exclusive")
|
|
11325
|
+
fail2(`deployment.spec.targets.${targetName}.allocation.sharing`, "must be exclusive for a per-metal relay topology.");
|
|
11326
|
+
if (minimum < 1 + Number(relays.standbyPerMetal))
|
|
11327
|
+
fail2(`deployment.spec.targets.${targetName}.cardinality.minimum`, "must allow the declared relay and standby.");
|
|
11328
|
+
}
|
|
11284
11329
|
if (target.placement !== undefined)
|
|
11285
11330
|
jsonValue(target.placement, `deployment.spec.targets.${targetName}.placement`);
|
|
11286
11331
|
targetDefinitions.set(targetName, target);
|
|
@@ -11304,6 +11349,9 @@ function compileDeployment(sourceValue) {
|
|
|
11304
11349
|
if (target.connectivity?.private?.mode === "cloudflare-warp" && ![...requirementDefinitions.values()].some(({ capability }) => capability === "network.private")) {
|
|
11305
11350
|
fail2(`deployment.spec.targets.${targetName}.connectivity.private`, "requires one network.private provider requirement.");
|
|
11306
11351
|
}
|
|
11352
|
+
if (target.topology && target.topology.crossMetal.mode !== "private-lan" && ![...requirementDefinitions.values()].some(({ capability }) => capability === "network.private")) {
|
|
11353
|
+
fail2(`deployment.spec.targets.${targetName}.topology.crossMetal`, "requires one network.private provider requirement.");
|
|
11354
|
+
}
|
|
11307
11355
|
}
|
|
11308
11356
|
const components = object(spec.components, "deployment.spec.components");
|
|
11309
11357
|
const componentEntries = Object.entries(components);
|
|
@@ -12058,7 +12106,14 @@ function validatePlatformSharedEnvironment(input) {
|
|
|
12058
12106
|
throw new Error("Bootstrap email provider must be smtp or jetemail.");
|
|
12059
12107
|
}
|
|
12060
12108
|
boundedInteger("publicApiPort", input.publicApiPort, 1024, 65533);
|
|
12061
|
-
|
|
12109
|
+
if (!Array.isArray(input.seedSyncMembers) || input.seedSyncMembers.length < 3 || input.seedSyncMembers.length > 64 || new Set(input.seedSyncMembers).size !== input.seedSyncMembers.length) {
|
|
12110
|
+
throw new Error("seedSyncMembers must contain 3 to 64 unique physical host identities.");
|
|
12111
|
+
}
|
|
12112
|
+
for (const member of input.seedSyncMembers)
|
|
12113
|
+
safeAtom("seedSyncMembers item", member);
|
|
12114
|
+
if (!input.seedSyncMembers.includes(input.nodeHostname)) {
|
|
12115
|
+
throw new Error("seedSyncMembers must include this nodeHostname.");
|
|
12116
|
+
}
|
|
12062
12117
|
boundedInteger("concurrencyLimit", input.concurrencyLimit, 1, 1e6);
|
|
12063
12118
|
boundedInteger("drainDeadlineMs", input.drainDeadlineMs, 1000, 300000);
|
|
12064
12119
|
boundedInteger("otlpFlushIntervalMs", input.otlpFlushIntervalMs, 1000, 300000);
|
|
@@ -12138,7 +12193,7 @@ function renderPlatformSharedEnvironment(input) {
|
|
|
12138
12193
|
FZ_DB_MASTER: value.databaseMaster ?? "",
|
|
12139
12194
|
FZ_DB_NETWORK_MODE: value.databaseNetworkMode,
|
|
12140
12195
|
FZ_SEED_SYNC_PEERS: value.seedSyncPeers.join(","),
|
|
12141
|
-
FZ_SEED_SYNC_MEMBERS:
|
|
12196
|
+
FZ_SEED_SYNC_MEMBERS: value.seedSyncMembers.join(","),
|
|
12142
12197
|
FZ_SEED_SYNC_EPOCH: value.seedSyncEpoch,
|
|
12143
12198
|
FZ_SEED_SYNC_CREDENTIAL: "seed-sync-root",
|
|
12144
12199
|
FZ_SHARED_DIR: value.sharedDirectory,
|
|
@@ -12207,7 +12262,12 @@ function platformApiCredentialSpecs(options) {
|
|
|
12207
12262
|
];
|
|
12208
12263
|
}
|
|
12209
12264
|
function renderPlatformApiUnits(input) {
|
|
12210
|
-
for (const path of [
|
|
12265
|
+
for (const path of [
|
|
12266
|
+
input.sharedDirectory,
|
|
12267
|
+
input.sharedEnvironmentFile,
|
|
12268
|
+
input.slotsDirectory,
|
|
12269
|
+
...input.topologyEnvironmentFile ? [input.topologyEnvironmentFile] : []
|
|
12270
|
+
]) {
|
|
12211
12271
|
if (!path.startsWith("/") || /[\r\n]/.test(path))
|
|
12212
12272
|
throw new Error("Runtime paths must be absolute and single-line.");
|
|
12213
12273
|
}
|
|
@@ -12221,6 +12281,8 @@ function renderPlatformApiUnits(input) {
|
|
|
12221
12281
|
const credentials = input.credentials.map((credential) => `LoadCredentialEncrypted=${credential.name}:${credential.encryptedPath}`).join(`
|
|
12222
12282
|
`);
|
|
12223
12283
|
const capacityEnvironment = input.capacityEnvironmentFile ? `EnvironmentFile=-${input.capacityEnvironmentFile}
|
|
12284
|
+
` : "";
|
|
12285
|
+
const topologyEnvironment = input.topologyEnvironmentFile ? `EnvironmentFile=-${input.topologyEnvironmentFile}
|
|
12224
12286
|
` : "";
|
|
12225
12287
|
const template = `[Unit]
|
|
12226
12288
|
Description=ForgeZero (%i slot)
|
|
@@ -12234,7 +12296,7 @@ WorkingDirectory=${input.slotsDirectory}/%i
|
|
|
12234
12296
|
Environment=NODE_ENV=production
|
|
12235
12297
|
Environment=FZ_SLOT=%i
|
|
12236
12298
|
EnvironmentFile=${input.sharedEnvironmentFile}
|
|
12237
|
-
${capacityEnvironment}${credentials}
|
|
12299
|
+
${capacityEnvironment}${topologyEnvironment}${credentials}
|
|
12238
12300
|
ExecStart=/usr/local/bin/bun run ${input.slotsDirectory}/%i/src/index.ts
|
|
12239
12301
|
Restart=always
|
|
12240
12302
|
RestartSec=2
|
|
@@ -14722,7 +14784,8 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
14722
14784
|
greenPort: runtime.greenPort,
|
|
14723
14785
|
collectorUnit: runtime.environment.otlpCollectorUnit,
|
|
14724
14786
|
credentials,
|
|
14725
|
-
capacityEnvironmentFile: "/etc/forgezero/capacity.env"
|
|
14787
|
+
capacityEnvironmentFile: "/etc/forgezero/capacity.env",
|
|
14788
|
+
topologyEnvironmentFile: "/etc/forgezero/deployment-topology.env"
|
|
14726
14789
|
});
|
|
14727
14790
|
const edge = renderPlatformNginx({
|
|
14728
14791
|
publicPort: runtime.environment.publicApiPort,
|
|
@@ -17298,8 +17361,8 @@ function platformGenesisBootstrapConfigs(template, guests, nodes, metalHostname,
|
|
|
17298
17361
|
databaseAddress: guest.address,
|
|
17299
17362
|
databaseMaster: role === "joiner" ? master : undefined,
|
|
17300
17363
|
nodeHostname: node.nodeHostname,
|
|
17301
|
-
seedSyncPeers: fleet.filter((_, peer) => peer !== index).
|
|
17302
|
-
seedSyncMembers:
|
|
17364
|
+
seedSyncPeers: fleet.filter((_, peer) => peer !== index).flatMap((peer) => [config.runtime.bluePort, config.runtime.greenPort].map((port) => `ws://${peer.address}:${port}/_internal/forgezero/seed-mesh/v2`)),
|
|
17365
|
+
seedSyncMembers: nodes.map(({ nodeHostname }) => nodeHostname),
|
|
17303
17366
|
initialInventory,
|
|
17304
17367
|
custodianEmail: index === 0 ? template.runtime.environment.custodianEmail : undefined
|
|
17305
17368
|
};
|
|
@@ -18642,7 +18705,7 @@ function interactiveBootstrap(kind, genesisGuests, genesisCloudflareCheckpoint)
|
|
|
18642
18705
|
const collectorUnit = FORGEZERO_OTEL_COLLECTOR_UNIT;
|
|
18643
18706
|
const replicationFactor = Number(bootstrapAnswer("Database replication factor", "3"));
|
|
18644
18707
|
const writeConcern = Number(bootstrapAnswer("Database write concern", "2"));
|
|
18645
|
-
const seedPeers = genesis ? fleet.slice(1).
|
|
18708
|
+
const seedPeers = genesis ? fleet.slice(1).flatMap((peer) => [3001, 3002].map((port) => `ws://${peer.address}:${port}/_internal/forgezero/seed-mesh/v2`)) : bootstrapAnswer("Seed WebSocket peers (comma-separated)").split(",").map((item) => item.trim()).filter(Boolean);
|
|
18646
18709
|
const emailProvider = bootstrapAnswer("Bootstrap email provider (smtp/jetemail)", "smtp");
|
|
18647
18710
|
if (!["smtp", "jetemail"].includes(emailProvider))
|
|
18648
18711
|
throw new Error("Bootstrap email provider must be smtp or jetemail");
|
|
@@ -18698,7 +18761,7 @@ function interactiveBootstrap(kind, genesisGuests, genesisCloudflareCheckpoint)
|
|
|
18698
18761
|
publicApiPort: Number(bootstrapAnswer("Loopback public API port", "3000")),
|
|
18699
18762
|
sharedDirectory: "/opt/forgezero/shared",
|
|
18700
18763
|
seedSyncPeers: seedPeers,
|
|
18701
|
-
seedSyncMembers:
|
|
18764
|
+
seedSyncMembers: bootstrapList("Seed member hostnames (comma-separated)"),
|
|
18702
18765
|
seedSyncEpoch: bootstrapAnswer("Seed epoch"),
|
|
18703
18766
|
concurrencyLimit: Number(bootstrapAnswer("Initial concurrency limit", "128")),
|
|
18704
18767
|
drainDeadlineMs: Number(bootstrapAnswer("Drain deadline milliseconds", "30000")),
|
package/dist/index.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export type { AgentCredentialLocation, DeploymentCredentialBinding, DeploymentCr
|
|
|
14
14
|
export { createDeploymentManager, DeploymentError } from './deployment';
|
|
15
15
|
export type { DeploymentManager, DeploymentOptions, DeploymentRequest, DeploymentResult } from './deployment';
|
|
16
16
|
export { DeploymentInputError, resolveDeploymentInputs, resolveDeploymentTargets } from './deployment-targets';
|
|
17
|
+
export { DeploymentTopologyError, planDeploymentTopology, type DeploymentTopologyAssignment, type DeploymentTopologyPlacement, type DeploymentTopologyRole, type DeploymentTopologyTransport } from './deployment-topology';
|
|
17
18
|
export { DEFAULT_CONTROL_SOCKET, requestControl, startControlServer } from './control';
|
|
18
19
|
export { pullDeploymentOnce, startDeploymentPull } from './deployment-pull';
|
|
19
20
|
export type { DeploymentPullOptions, RemoteDeploymentClaim, PullResult } from './deployment-pull';
|
package/dist/metal-bootstrap.js
CHANGED
|
@@ -354,7 +354,7 @@ function systemdAgentEgressDirectives(loopbackTcpPorts = []) {
|
|
|
354
354
|
}
|
|
355
355
|
|
|
356
356
|
// src/version.ts
|
|
357
|
-
var VERSION = "0.1.
|
|
357
|
+
var VERSION = "0.1.80";
|
|
358
358
|
|
|
359
359
|
// src/otel-collector.ts
|
|
360
360
|
var FORGEZERO_OTEL_COLLECTOR_UNIT = "forgezero-otel-collector.service";
|
|
@@ -1416,7 +1416,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
|
|
|
1416
1416
|
var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
|
|
1417
1417
|
|
|
1418
1418
|
// src/version.ts
|
|
1419
|
-
var VERSION = "0.1.
|
|
1419
|
+
var VERSION = "0.1.80";
|
|
1420
1420
|
|
|
1421
1421
|
// src/software.ts
|
|
1422
1422
|
var PINNED_BUN_VERSION = "1.3.14";
|
|
@@ -3089,7 +3089,14 @@ function validatePlatformSharedEnvironment(input) {
|
|
|
3089
3089
|
throw new Error("Bootstrap email provider must be smtp or jetemail.");
|
|
3090
3090
|
}
|
|
3091
3091
|
boundedInteger("publicApiPort", input.publicApiPort, 1024, 65533);
|
|
3092
|
-
|
|
3092
|
+
if (!Array.isArray(input.seedSyncMembers) || input.seedSyncMembers.length < 3 || input.seedSyncMembers.length > 64 || new Set(input.seedSyncMembers).size !== input.seedSyncMembers.length) {
|
|
3093
|
+
throw new Error("seedSyncMembers must contain 3 to 64 unique physical host identities.");
|
|
3094
|
+
}
|
|
3095
|
+
for (const member of input.seedSyncMembers)
|
|
3096
|
+
safeAtom("seedSyncMembers item", member);
|
|
3097
|
+
if (!input.seedSyncMembers.includes(input.nodeHostname)) {
|
|
3098
|
+
throw new Error("seedSyncMembers must include this nodeHostname.");
|
|
3099
|
+
}
|
|
3093
3100
|
boundedInteger("concurrencyLimit", input.concurrencyLimit, 1, 1e6);
|
|
3094
3101
|
boundedInteger("drainDeadlineMs", input.drainDeadlineMs, 1000, 300000);
|
|
3095
3102
|
boundedInteger("otlpFlushIntervalMs", input.otlpFlushIntervalMs, 1000, 300000);
|
|
@@ -3169,7 +3176,7 @@ function renderPlatformSharedEnvironment(input) {
|
|
|
3169
3176
|
FZ_DB_MASTER: value.databaseMaster ?? "",
|
|
3170
3177
|
FZ_DB_NETWORK_MODE: value.databaseNetworkMode,
|
|
3171
3178
|
FZ_SEED_SYNC_PEERS: value.seedSyncPeers.join(","),
|
|
3172
|
-
FZ_SEED_SYNC_MEMBERS:
|
|
3179
|
+
FZ_SEED_SYNC_MEMBERS: value.seedSyncMembers.join(","),
|
|
3173
3180
|
FZ_SEED_SYNC_EPOCH: value.seedSyncEpoch,
|
|
3174
3181
|
FZ_SEED_SYNC_CREDENTIAL: "seed-sync-root",
|
|
3175
3182
|
FZ_SHARED_DIR: value.sharedDirectory,
|
|
@@ -3238,7 +3245,12 @@ function platformApiCredentialSpecs(options) {
|
|
|
3238
3245
|
];
|
|
3239
3246
|
}
|
|
3240
3247
|
function renderPlatformApiUnits(input) {
|
|
3241
|
-
for (const path of [
|
|
3248
|
+
for (const path of [
|
|
3249
|
+
input.sharedDirectory,
|
|
3250
|
+
input.sharedEnvironmentFile,
|
|
3251
|
+
input.slotsDirectory,
|
|
3252
|
+
...input.topologyEnvironmentFile ? [input.topologyEnvironmentFile] : []
|
|
3253
|
+
]) {
|
|
3242
3254
|
if (!path.startsWith("/") || /[\r\n]/.test(path))
|
|
3243
3255
|
throw new Error("Runtime paths must be absolute and single-line.");
|
|
3244
3256
|
}
|
|
@@ -3252,6 +3264,8 @@ function renderPlatformApiUnits(input) {
|
|
|
3252
3264
|
const credentials = input.credentials.map((credential) => `LoadCredentialEncrypted=${credential.name}:${credential.encryptedPath}`).join(`
|
|
3253
3265
|
`);
|
|
3254
3266
|
const capacityEnvironment = input.capacityEnvironmentFile ? `EnvironmentFile=-${input.capacityEnvironmentFile}
|
|
3267
|
+
` : "";
|
|
3268
|
+
const topologyEnvironment = input.topologyEnvironmentFile ? `EnvironmentFile=-${input.topologyEnvironmentFile}
|
|
3255
3269
|
` : "";
|
|
3256
3270
|
const template = `[Unit]
|
|
3257
3271
|
Description=ForgeZero (%i slot)
|
|
@@ -3265,7 +3279,7 @@ WorkingDirectory=${input.slotsDirectory}/%i
|
|
|
3265
3279
|
Environment=NODE_ENV=production
|
|
3266
3280
|
Environment=FZ_SLOT=%i
|
|
3267
3281
|
EnvironmentFile=${input.sharedEnvironmentFile}
|
|
3268
|
-
${capacityEnvironment}${credentials}
|
|
3282
|
+
${capacityEnvironment}${topologyEnvironment}${credentials}
|
|
3269
3283
|
ExecStart=/usr/local/bin/bun run ${input.slotsDirectory}/%i/src/index.ts
|
|
3270
3284
|
Restart=always
|
|
3271
3285
|
RestartSec=2
|
|
@@ -4517,7 +4531,8 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4517
4531
|
greenPort: runtime.greenPort,
|
|
4518
4532
|
collectorUnit: runtime.environment.otlpCollectorUnit,
|
|
4519
4533
|
credentials,
|
|
4520
|
-
capacityEnvironmentFile: "/etc/forgezero/capacity.env"
|
|
4534
|
+
capacityEnvironmentFile: "/etc/forgezero/capacity.env",
|
|
4535
|
+
topologyEnvironmentFile: "/etc/forgezero/deployment-topology.env"
|
|
4521
4536
|
});
|
|
4522
4537
|
const edge = renderPlatformNginx({
|
|
4523
4538
|
publicPort: runtime.environment.publicApiPort,
|
|
@@ -62,7 +62,8 @@ export interface PlatformSharedEnvironment {
|
|
|
62
62
|
publicApiPort: number;
|
|
63
63
|
sharedDirectory: string;
|
|
64
64
|
seedSyncPeers: string[];
|
|
65
|
-
|
|
65
|
+
/** Exact physical API host identities participating in this seed epoch. */
|
|
66
|
+
seedSyncMembers: string[];
|
|
66
67
|
seedSyncEpoch: string;
|
|
67
68
|
concurrencyLimit: number;
|
|
68
69
|
drainDeadlineMs: number;
|
|
@@ -120,6 +121,8 @@ export interface ApiRuntimeRenderOptions {
|
|
|
120
121
|
credentials: SystemdCredentialSpec[];
|
|
121
122
|
/** Root-owned calibration override, loaded after the ordinary shared environment. */
|
|
122
123
|
capacityEnvironmentFile?: string;
|
|
124
|
+
/** Root-owned signed deployment topology, absent until post-genesis reconciliation. */
|
|
125
|
+
topologyEnvironmentFile?: string;
|
|
123
126
|
}
|
|
124
127
|
export declare function renderPlatformApiUnits(input: ApiRuntimeRenderOptions): {
|
|
125
128
|
template: string;
|
|
@@ -811,7 +811,14 @@ function validatePlatformSharedEnvironment(input) {
|
|
|
811
811
|
throw new Error("Bootstrap email provider must be smtp or jetemail.");
|
|
812
812
|
}
|
|
813
813
|
boundedInteger("publicApiPort", input.publicApiPort, 1024, 65533);
|
|
814
|
-
|
|
814
|
+
if (!Array.isArray(input.seedSyncMembers) || input.seedSyncMembers.length < 3 || input.seedSyncMembers.length > 64 || new Set(input.seedSyncMembers).size !== input.seedSyncMembers.length) {
|
|
815
|
+
throw new Error("seedSyncMembers must contain 3 to 64 unique physical host identities.");
|
|
816
|
+
}
|
|
817
|
+
for (const member of input.seedSyncMembers)
|
|
818
|
+
safeAtom("seedSyncMembers item", member);
|
|
819
|
+
if (!input.seedSyncMembers.includes(input.nodeHostname)) {
|
|
820
|
+
throw new Error("seedSyncMembers must include this nodeHostname.");
|
|
821
|
+
}
|
|
815
822
|
boundedInteger("concurrencyLimit", input.concurrencyLimit, 1, 1e6);
|
|
816
823
|
boundedInteger("drainDeadlineMs", input.drainDeadlineMs, 1000, 300000);
|
|
817
824
|
boundedInteger("otlpFlushIntervalMs", input.otlpFlushIntervalMs, 1000, 300000);
|
|
@@ -891,7 +898,7 @@ function renderPlatformSharedEnvironment(input) {
|
|
|
891
898
|
FZ_DB_MASTER: value.databaseMaster ?? "",
|
|
892
899
|
FZ_DB_NETWORK_MODE: value.databaseNetworkMode,
|
|
893
900
|
FZ_SEED_SYNC_PEERS: value.seedSyncPeers.join(","),
|
|
894
|
-
FZ_SEED_SYNC_MEMBERS:
|
|
901
|
+
FZ_SEED_SYNC_MEMBERS: value.seedSyncMembers.join(","),
|
|
895
902
|
FZ_SEED_SYNC_EPOCH: value.seedSyncEpoch,
|
|
896
903
|
FZ_SEED_SYNC_CREDENTIAL: "seed-sync-root",
|
|
897
904
|
FZ_SHARED_DIR: value.sharedDirectory,
|
|
@@ -960,7 +967,12 @@ function platformApiCredentialSpecs(options) {
|
|
|
960
967
|
];
|
|
961
968
|
}
|
|
962
969
|
function renderPlatformApiUnits(input) {
|
|
963
|
-
for (const path2 of [
|
|
970
|
+
for (const path2 of [
|
|
971
|
+
input.sharedDirectory,
|
|
972
|
+
input.sharedEnvironmentFile,
|
|
973
|
+
input.slotsDirectory,
|
|
974
|
+
...input.topologyEnvironmentFile ? [input.topologyEnvironmentFile] : []
|
|
975
|
+
]) {
|
|
964
976
|
if (!path2.startsWith("/") || /[\r\n]/.test(path2))
|
|
965
977
|
throw new Error("Runtime paths must be absolute and single-line.");
|
|
966
978
|
}
|
|
@@ -974,6 +986,8 @@ function renderPlatformApiUnits(input) {
|
|
|
974
986
|
const credentials = input.credentials.map((credential) => `LoadCredentialEncrypted=${credential.name}:${credential.encryptedPath}`).join(`
|
|
975
987
|
`);
|
|
976
988
|
const capacityEnvironment = input.capacityEnvironmentFile ? `EnvironmentFile=-${input.capacityEnvironmentFile}
|
|
989
|
+
` : "";
|
|
990
|
+
const topologyEnvironment = input.topologyEnvironmentFile ? `EnvironmentFile=-${input.topologyEnvironmentFile}
|
|
977
991
|
` : "";
|
|
978
992
|
const template = `[Unit]
|
|
979
993
|
Description=ForgeZero (%i slot)
|
|
@@ -987,7 +1001,7 @@ WorkingDirectory=${input.slotsDirectory}/%i
|
|
|
987
1001
|
Environment=NODE_ENV=production
|
|
988
1002
|
Environment=FZ_SLOT=%i
|
|
989
1003
|
EnvironmentFile=${input.sharedEnvironmentFile}
|
|
990
|
-
${capacityEnvironment}${credentials}
|
|
1004
|
+
${capacityEnvironment}${topologyEnvironment}${credentials}
|
|
991
1005
|
ExecStart=/usr/local/bin/bun run ${input.slotsDirectory}/%i/src/index.ts
|
|
992
1006
|
Restart=always
|
|
993
1007
|
RestartSec=2
|
|
@@ -1649,6 +1649,7 @@ function phasePipeline(definition, phase, profile, executeRelease = false) {
|
|
|
1649
1649
|
// src/deployment-connectivity.ts
|
|
1650
1650
|
import { createHash as createHash3 } from "node:crypto";
|
|
1651
1651
|
import { mkdirSync as mkdirSync4, renameSync as renameSync4, writeFileSync as writeFileSync4 } from "node:fs";
|
|
1652
|
+
import { createConnection } from "node:net";
|
|
1652
1653
|
import { dirname as dirname4 } from "node:path";
|
|
1653
1654
|
|
|
1654
1655
|
// src/process-input.ts
|
|
@@ -1701,6 +1702,21 @@ var defaultHost = {
|
|
|
1701
1702
|
const [stdout, stderr, exitCode] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
|
|
1702
1703
|
return { exitCode, output: `${stdout}${stderr}` };
|
|
1703
1704
|
},
|
|
1705
|
+
probe: (address, port, timeoutMs) => new Promise((resolve3) => {
|
|
1706
|
+
const socket = createConnection({ host: address, port });
|
|
1707
|
+
let settled = false;
|
|
1708
|
+
const finish = (value) => {
|
|
1709
|
+
if (settled)
|
|
1710
|
+
return;
|
|
1711
|
+
settled = true;
|
|
1712
|
+
socket.destroy();
|
|
1713
|
+
resolve3(value);
|
|
1714
|
+
};
|
|
1715
|
+
socket.setTimeout(timeoutMs);
|
|
1716
|
+
socket.once("connect", () => finish(true));
|
|
1717
|
+
socket.once("timeout", () => finish(false));
|
|
1718
|
+
socket.once("error", () => finish(false));
|
|
1719
|
+
}),
|
|
1704
1720
|
sleep: (ms) => Bun.sleep(ms)
|
|
1705
1721
|
};
|
|
1706
1722
|
function validate(request) {
|
|
@@ -1715,7 +1731,7 @@ function validate(request) {
|
|
|
1715
1731
|
} else if (request.capabilities.public)
|
|
1716
1732
|
throw new Error("unexpected public deployment capability");
|
|
1717
1733
|
if (privateIntent?.mode === "cloudflare-warp") {
|
|
1718
|
-
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)) {
|
|
1734
|
+
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)) {
|
|
1719
1735
|
throw new Error("private deployment connectivity is malformed");
|
|
1720
1736
|
}
|
|
1721
1737
|
} else if (request.capabilities.private)
|
|
@@ -1725,6 +1741,22 @@ async function applyDeploymentConnectivity(request, host = defaultHost) {
|
|
|
1725
1741
|
validate(request);
|
|
1726
1742
|
const id = idFor(request.key);
|
|
1727
1743
|
const evidence = { key: request.key };
|
|
1744
|
+
const topology = request.topology;
|
|
1745
|
+
if (topology) {
|
|
1746
|
+
const values = [topology.localRelayAddress, ...topology.localPeerAddresses, ...topology.remoteRelayAddresses];
|
|
1747
|
+
const identities = [
|
|
1748
|
+
topology.nodeIdentity,
|
|
1749
|
+
...topology.localPeerIdentities,
|
|
1750
|
+
...topology.remoteRelayIdentities,
|
|
1751
|
+
...topology.memberIdentities
|
|
1752
|
+
];
|
|
1753
|
+
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") {
|
|
1754
|
+
throw new Error("deployment topology is malformed");
|
|
1755
|
+
}
|
|
1756
|
+
if (request.intent.private?.mode === "cloudflare-warp" !== (topology.transport === "cloudflare-warp")) {
|
|
1757
|
+
throw new Error("deployment topology transport differs from private connectivity intent");
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1728
1760
|
if (request.intent.public?.mode === "cloudflare-tunnel") {
|
|
1729
1761
|
const capability = request.capabilities.public;
|
|
1730
1762
|
const credentialName = `FZ_TUNNEL_${id.toUpperCase()}`;
|
|
@@ -1762,6 +1794,12 @@ WantedBy=multi-user.target
|
|
|
1762
1794
|
const capability = request.capabilities.private;
|
|
1763
1795
|
const credentialPath = "/etc/forgezero/creds/CF_WARP_CONNECTOR_TOKEN.cred";
|
|
1764
1796
|
const unit = "forgezero-deployment-mesh.service";
|
|
1797
|
+
const forwarding = "/etc/sysctl.d/99-forgezero-mesh.conf";
|
|
1798
|
+
host.write(forwarding, `net.ipv4.ip_forward = 1
|
|
1799
|
+
net.ipv6.conf.all.forwarding = 1
|
|
1800
|
+
net.ipv6.conf.all.accept_ra = 2
|
|
1801
|
+
`, 420);
|
|
1802
|
+
await checked2(host, ["/usr/sbin/sysctl", "-p", forwarding], "Mesh subnet forwarding");
|
|
1765
1803
|
await host.seal("CF_WARP_CONNECTOR_TOKEN", credentialPath, capability.connectorToken);
|
|
1766
1804
|
host.write(`/etc/systemd/system/${unit}`, `[Unit]
|
|
1767
1805
|
Description=ForgeZero deployment Mesh/WARP connector
|
|
@@ -1796,6 +1834,95 @@ WantedBy=multi-user.target
|
|
|
1796
1834
|
if (!evidence.private)
|
|
1797
1835
|
throw new Error("WARP connector readiness failed");
|
|
1798
1836
|
}
|
|
1837
|
+
if (topology) {
|
|
1838
|
+
const syncAddresses = topology.role === "relay" ? [...topology.localPeerAddresses, ...topology.remoteRelayAddresses] : [topology.localRelayAddress];
|
|
1839
|
+
const topologyEpoch = `topology-${topology.generation.slice("sha256:".length, "sha256:".length + 48)}`;
|
|
1840
|
+
host.write("/etc/forgezero/deployment-topology.env", [
|
|
1841
|
+
`FZ_TOPOLOGY_NODE_IDENTITY=${topology.nodeIdentity}`,
|
|
1842
|
+
`FZ_TOPOLOGY_PEER_ADDRESSES=${[...new Set(syncAddresses)].join(",")}`,
|
|
1843
|
+
`FZ_TOPOLOGY_MEMBERS=${topology.memberIdentities.join(",")}`,
|
|
1844
|
+
`FZ_TOPOLOGY_EPOCH=${topologyEpoch}`,
|
|
1845
|
+
`FZ_TOPOLOGY_SITE=${topology.site}`,
|
|
1846
|
+
`FZ_TOPOLOGY_ROLE=${topology.role}`,
|
|
1847
|
+
`FZ_TOPOLOGY_TRANSPORT=${topology.transport}`,
|
|
1848
|
+
""
|
|
1849
|
+
].join(`
|
|
1850
|
+
`), 384);
|
|
1851
|
+
const routeUnit = "forgezero-deployment-topology.service";
|
|
1852
|
+
const routeUnitPath = `/etc/systemd/system/${routeUnit}`;
|
|
1853
|
+
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] })) : [];
|
|
1854
|
+
for (const { via } of routePairs) {
|
|
1855
|
+
await checked2(host, ["/usr/sbin/ip", "route", "get", via], `private route gateway ${via}`);
|
|
1856
|
+
}
|
|
1857
|
+
await host.exec(["/usr/bin/systemctl", "disable", "--now", routeUnit]);
|
|
1858
|
+
const forwarding = topology.role !== "member" && topology.remoteSiteCidrs.length > 0 ? `ExecStart=/usr/sbin/sysctl -w net.ipv4.ip_forward=1
|
|
1859
|
+
` : "";
|
|
1860
|
+
const starts = routePairs.map(({ network, via }) => `ExecStart=/usr/sbin/ip route replace ${network} via ${via}`).join(`
|
|
1861
|
+
`);
|
|
1862
|
+
const stops = routePairs.map(({ network, via }) => `ExecStop=-/usr/sbin/ip route del ${network} via ${via}`).join(`
|
|
1863
|
+
`);
|
|
1864
|
+
const noOp = !forwarding && !starts ? `ExecStart=/usr/bin/true
|
|
1865
|
+
` : "";
|
|
1866
|
+
host.write(routeUnitPath, `[Unit]
|
|
1867
|
+
Description=ForgeZero fenced deployment site routing
|
|
1868
|
+
After=network-online.target${topology.transport === "cloudflare-warp" ? " forgezero-deployment-mesh.service" : ""}
|
|
1869
|
+
Wants=network-online.target
|
|
1870
|
+
|
|
1871
|
+
[Service]
|
|
1872
|
+
Type=oneshot
|
|
1873
|
+
RemainAfterExit=yes
|
|
1874
|
+
${forwarding}${noOp}${starts}${starts ? `
|
|
1875
|
+
` : ""}${stops}${stops ? `
|
|
1876
|
+
` : ""}
|
|
1877
|
+
[Install]
|
|
1878
|
+
WantedBy=multi-user.target
|
|
1879
|
+
`, 420);
|
|
1880
|
+
for (const source of [...new Set([topology.siteCidr, ...topology.remoteSiteCidrs])]) {
|
|
1881
|
+
for (const port of topology.routedTcpPorts) {
|
|
1882
|
+
await checked2(host, ["/usr/sbin/ufw", "allow", "from", source, "to", "any", "port", String(port), "proto", "tcp"], `private service ${source}:${port}`);
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
if (topology.role !== "member")
|
|
1886
|
+
for (const remoteCidr of topology.remoteSiteCidrs) {
|
|
1887
|
+
for (const port of topology.routedTcpPorts) {
|
|
1888
|
+
await checked2(host, [
|
|
1889
|
+
"/usr/sbin/ufw",
|
|
1890
|
+
"route",
|
|
1891
|
+
"allow",
|
|
1892
|
+
"proto",
|
|
1893
|
+
"tcp",
|
|
1894
|
+
"from",
|
|
1895
|
+
topology.siteCidr,
|
|
1896
|
+
"to",
|
|
1897
|
+
remoteCidr,
|
|
1898
|
+
"port",
|
|
1899
|
+
String(port)
|
|
1900
|
+
], `private routed service ${topology.siteCidr}->${remoteCidr}:${port}`);
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
await checked2(host, ["/usr/bin/systemctl", "daemon-reload"], "topology daemon reload");
|
|
1904
|
+
await checked2(host, ["/usr/bin/systemctl", "enable", "--now", routeUnit], "topology routes");
|
|
1905
|
+
const probes = topology.role === "member" ? [topology.localRelayAddress] : topology.remoteRelayAddresses;
|
|
1906
|
+
for (const address of [...new Set(probes)]) {
|
|
1907
|
+
await checked2(host, ["/usr/sbin/ip", "route", "get", address], `private route ${address}`);
|
|
1908
|
+
let ready = false;
|
|
1909
|
+
for (let attempt = 0;attempt < 10; attempt += 1) {
|
|
1910
|
+
if (await host.probe(address, topology.healthPort, 2000)) {
|
|
1911
|
+
ready = true;
|
|
1912
|
+
break;
|
|
1913
|
+
}
|
|
1914
|
+
await host.sleep(1000);
|
|
1915
|
+
}
|
|
1916
|
+
if (!ready)
|
|
1917
|
+
throw new Error(`private topology relay ${address}:${topology.healthPort} is unreachable`);
|
|
1918
|
+
}
|
|
1919
|
+
evidence.topology = {
|
|
1920
|
+
site: topology.site,
|
|
1921
|
+
siteCidr: topology.siteCidr,
|
|
1922
|
+
role: topology.role,
|
|
1923
|
+
probed: [...new Set(probes)]
|
|
1924
|
+
};
|
|
1925
|
+
}
|
|
1799
1926
|
return evidence;
|
|
1800
1927
|
}
|
|
1801
1928
|
|
|
@@ -2520,7 +2647,7 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
|
|
|
2520
2647
|
}
|
|
2521
2648
|
|
|
2522
2649
|
// src/version.ts
|
|
2523
|
-
var VERSION3 = "0.1.
|
|
2650
|
+
var VERSION3 = "0.1.80";
|
|
2524
2651
|
|
|
2525
2652
|
// src/egress-policy.ts
|
|
2526
2653
|
import { realpathSync as realpathSync3 } from "node:fs";
|
|
@@ -3780,7 +3907,14 @@ function validatePlatformSharedEnvironment(input) {
|
|
|
3780
3907
|
throw new Error("Bootstrap email provider must be smtp or jetemail.");
|
|
3781
3908
|
}
|
|
3782
3909
|
boundedInteger("publicApiPort", input.publicApiPort, 1024, 65533);
|
|
3783
|
-
|
|
3910
|
+
if (!Array.isArray(input.seedSyncMembers) || input.seedSyncMembers.length < 3 || input.seedSyncMembers.length > 64 || new Set(input.seedSyncMembers).size !== input.seedSyncMembers.length) {
|
|
3911
|
+
throw new Error("seedSyncMembers must contain 3 to 64 unique physical host identities.");
|
|
3912
|
+
}
|
|
3913
|
+
for (const member of input.seedSyncMembers)
|
|
3914
|
+
safeAtom("seedSyncMembers item", member);
|
|
3915
|
+
if (!input.seedSyncMembers.includes(input.nodeHostname)) {
|
|
3916
|
+
throw new Error("seedSyncMembers must include this nodeHostname.");
|
|
3917
|
+
}
|
|
3784
3918
|
boundedInteger("concurrencyLimit", input.concurrencyLimit, 1, 1e6);
|
|
3785
3919
|
boundedInteger("drainDeadlineMs", input.drainDeadlineMs, 1000, 300000);
|
|
3786
3920
|
boundedInteger("otlpFlushIntervalMs", input.otlpFlushIntervalMs, 1000, 300000);
|
|
@@ -3860,7 +3994,7 @@ function renderPlatformSharedEnvironment(input) {
|
|
|
3860
3994
|
FZ_DB_MASTER: value.databaseMaster ?? "",
|
|
3861
3995
|
FZ_DB_NETWORK_MODE: value.databaseNetworkMode,
|
|
3862
3996
|
FZ_SEED_SYNC_PEERS: value.seedSyncPeers.join(","),
|
|
3863
|
-
FZ_SEED_SYNC_MEMBERS:
|
|
3997
|
+
FZ_SEED_SYNC_MEMBERS: value.seedSyncMembers.join(","),
|
|
3864
3998
|
FZ_SEED_SYNC_EPOCH: value.seedSyncEpoch,
|
|
3865
3999
|
FZ_SEED_SYNC_CREDENTIAL: "seed-sync-root",
|
|
3866
4000
|
FZ_SHARED_DIR: value.sharedDirectory,
|
|
@@ -3929,7 +4063,12 @@ function platformApiCredentialSpecs(options) {
|
|
|
3929
4063
|
];
|
|
3930
4064
|
}
|
|
3931
4065
|
function renderPlatformApiUnits(input) {
|
|
3932
|
-
for (const path2 of [
|
|
4066
|
+
for (const path2 of [
|
|
4067
|
+
input.sharedDirectory,
|
|
4068
|
+
input.sharedEnvironmentFile,
|
|
4069
|
+
input.slotsDirectory,
|
|
4070
|
+
...input.topologyEnvironmentFile ? [input.topologyEnvironmentFile] : []
|
|
4071
|
+
]) {
|
|
3933
4072
|
if (!path2.startsWith("/") || /[\r\n]/.test(path2))
|
|
3934
4073
|
throw new Error("Runtime paths must be absolute and single-line.");
|
|
3935
4074
|
}
|
|
@@ -3943,6 +4082,8 @@ function renderPlatformApiUnits(input) {
|
|
|
3943
4082
|
const credentials = input.credentials.map((credential) => `LoadCredentialEncrypted=${credential.name}:${credential.encryptedPath}`).join(`
|
|
3944
4083
|
`);
|
|
3945
4084
|
const capacityEnvironment = input.capacityEnvironmentFile ? `EnvironmentFile=-${input.capacityEnvironmentFile}
|
|
4085
|
+
` : "";
|
|
4086
|
+
const topologyEnvironment = input.topologyEnvironmentFile ? `EnvironmentFile=-${input.topologyEnvironmentFile}
|
|
3946
4087
|
` : "";
|
|
3947
4088
|
const template = `[Unit]
|
|
3948
4089
|
Description=ForgeZero (%i slot)
|
|
@@ -3956,7 +4097,7 @@ WorkingDirectory=${input.slotsDirectory}/%i
|
|
|
3956
4097
|
Environment=NODE_ENV=production
|
|
3957
4098
|
Environment=FZ_SLOT=%i
|
|
3958
4099
|
EnvironmentFile=${input.sharedEnvironmentFile}
|
|
3959
|
-
${capacityEnvironment}${credentials}
|
|
4100
|
+
${capacityEnvironment}${topologyEnvironment}${credentials}
|
|
3960
4101
|
ExecStart=/usr/local/bin/bun run ${input.slotsDirectory}/%i/src/index.ts
|
|
3961
4102
|
Restart=always
|
|
3962
4103
|
RestartSec=2
|