@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.
- package/README.md +57 -6
- package/dist/agent-heartbeat.js +1 -1
- package/dist/bootstrap.js +21 -6
- package/dist/community-rehearsal-host.js +29 -7
- package/dist/definition.js +29 -7
- package/dist/deploy-compiler.js +50 -2
- package/dist/deploy-file.js +29 -7
- 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 +397 -21
- package/dist/fz.js +75 -12
- package/dist/index.d.ts +1 -0
- package/dist/metal-bootstrap.js +1 -1
- package/dist/metal-helper-socket.js +29 -7
- package/dist/metal-provision.js +29 -7
- package/dist/operator-bootstrap.js +21 -6
- package/dist/platform-bootstrap-runtime.d.ts +4 -1
- package/dist/platform-bootstrap-runtime.js +47 -11
- package/dist/platform-fleet-verification.js +176 -13
- package/dist/platform-genesis.js +29 -7
- package/dist/provision.js +158 -9
- package/dist/software-helper.js +157 -8
- package/dist/software.d.ts +1 -0
- package/dist/software.js +30 -7
- package/dist/ubuntu.js +29 -7
- 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";
|
|
@@ -305,16 +305,38 @@ var runSoftwareCommand = async (argv, env = {}) => {
|
|
|
305
305
|
return { exitCode, output: `${stdout}${stderr}` };
|
|
306
306
|
};
|
|
307
307
|
var run = runSoftwareCommand;
|
|
308
|
+
var softwareDownloadCommand = (url, destination, maximumBytes) => {
|
|
309
|
+
const source = new URL(url);
|
|
310
|
+
if (source.protocol !== "https:" || source.username || source.password)
|
|
311
|
+
throw new Error("software download must use credential-free HTTPS");
|
|
312
|
+
if (!Number.isSafeInteger(maximumBytes) || maximumBytes < 1)
|
|
313
|
+
throw new Error("software download size bound is invalid");
|
|
314
|
+
return [
|
|
315
|
+
"/usr/bin/curl",
|
|
316
|
+
"--fail",
|
|
317
|
+
"--location",
|
|
318
|
+
"--silent",
|
|
319
|
+
"--show-error",
|
|
320
|
+
"--proto",
|
|
321
|
+
"=https",
|
|
322
|
+
"--tlsv1.2",
|
|
323
|
+
"--max-time",
|
|
324
|
+
"1800",
|
|
325
|
+
"--max-filesize",
|
|
326
|
+
String(maximumBytes),
|
|
327
|
+
"--output",
|
|
328
|
+
destination,
|
|
329
|
+
source.href
|
|
330
|
+
];
|
|
331
|
+
};
|
|
308
332
|
var download = async (url, destination, sha256, maximumBytes = 512 * 1024 * 1024) => {
|
|
309
|
-
const response = await fetch(url, { redirect: "follow", signal: AbortSignal.timeout(30 * 60000) });
|
|
310
|
-
if (!response.ok)
|
|
311
|
-
throw new Error(`download failed with HTTP ${response.status}`);
|
|
312
|
-
const declared = Number(response.headers.get("content-length") ?? 0);
|
|
313
|
-
if (declared > maximumBytes)
|
|
314
|
-
throw new Error("download exceeds reviewed size bound");
|
|
315
333
|
if (existsSync(destination))
|
|
316
334
|
throw new Error("download destination already exists");
|
|
317
|
-
await
|
|
335
|
+
const received = await run(softwareDownloadCommand(url, destination, maximumBytes));
|
|
336
|
+
if (received.exitCode !== 0) {
|
|
337
|
+
rmSync(destination, { force: true });
|
|
338
|
+
throw new Error(`software download failed (${received.exitCode}): ${received.output.trim()}`);
|
|
339
|
+
}
|
|
318
340
|
chmodSync(destination, 384);
|
|
319
341
|
if (statSync(destination).size > maximumBytes) {
|
|
320
342
|
rmSync(destination, { force: true });
|
package/dist/metal-provision.js
CHANGED
|
@@ -305,16 +305,38 @@ var runSoftwareCommand = async (argv, env = {}) => {
|
|
|
305
305
|
return { exitCode, output: `${stdout}${stderr}` };
|
|
306
306
|
};
|
|
307
307
|
var run = runSoftwareCommand;
|
|
308
|
+
var softwareDownloadCommand = (url, destination, maximumBytes) => {
|
|
309
|
+
const source = new URL(url);
|
|
310
|
+
if (source.protocol !== "https:" || source.username || source.password)
|
|
311
|
+
throw new Error("software download must use credential-free HTTPS");
|
|
312
|
+
if (!Number.isSafeInteger(maximumBytes) || maximumBytes < 1)
|
|
313
|
+
throw new Error("software download size bound is invalid");
|
|
314
|
+
return [
|
|
315
|
+
"/usr/bin/curl",
|
|
316
|
+
"--fail",
|
|
317
|
+
"--location",
|
|
318
|
+
"--silent",
|
|
319
|
+
"--show-error",
|
|
320
|
+
"--proto",
|
|
321
|
+
"=https",
|
|
322
|
+
"--tlsv1.2",
|
|
323
|
+
"--max-time",
|
|
324
|
+
"1800",
|
|
325
|
+
"--max-filesize",
|
|
326
|
+
String(maximumBytes),
|
|
327
|
+
"--output",
|
|
328
|
+
destination,
|
|
329
|
+
source.href
|
|
330
|
+
];
|
|
331
|
+
};
|
|
308
332
|
var download = async (url, destination, sha256, maximumBytes = 512 * 1024 * 1024) => {
|
|
309
|
-
const response = await fetch(url, { redirect: "follow", signal: AbortSignal.timeout(30 * 60000) });
|
|
310
|
-
if (!response.ok)
|
|
311
|
-
throw new Error(`download failed with HTTP ${response.status}`);
|
|
312
|
-
const declared = Number(response.headers.get("content-length") ?? 0);
|
|
313
|
-
if (declared > maximumBytes)
|
|
314
|
-
throw new Error("download exceeds reviewed size bound");
|
|
315
333
|
if (existsSync(destination))
|
|
316
334
|
throw new Error("download destination already exists");
|
|
317
|
-
await
|
|
335
|
+
const received = await run(softwareDownloadCommand(url, destination, maximumBytes));
|
|
336
|
+
if (received.exitCode !== 0) {
|
|
337
|
+
rmSync(destination, { force: true });
|
|
338
|
+
throw new Error(`software download failed (${received.exitCode}): ${received.output.trim()}`);
|
|
339
|
+
}
|
|
318
340
|
chmodSync(destination, 384);
|
|
319
341
|
if (statSync(destination).size > maximumBytes) {
|
|
320
342
|
rmSync(destination, { force: true });
|
|
@@ -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;
|
|
@@ -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
|
|
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 });
|
|
@@ -789,7 +811,14 @@ function validatePlatformSharedEnvironment(input) {
|
|
|
789
811
|
throw new Error("Bootstrap email provider must be smtp or jetemail.");
|
|
790
812
|
}
|
|
791
813
|
boundedInteger("publicApiPort", input.publicApiPort, 1024, 65533);
|
|
792
|
-
|
|
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
|
+
}
|
|
793
822
|
boundedInteger("concurrencyLimit", input.concurrencyLimit, 1, 1e6);
|
|
794
823
|
boundedInteger("drainDeadlineMs", input.drainDeadlineMs, 1000, 300000);
|
|
795
824
|
boundedInteger("otlpFlushIntervalMs", input.otlpFlushIntervalMs, 1000, 300000);
|
|
@@ -869,7 +898,7 @@ function renderPlatformSharedEnvironment(input) {
|
|
|
869
898
|
FZ_DB_MASTER: value.databaseMaster ?? "",
|
|
870
899
|
FZ_DB_NETWORK_MODE: value.databaseNetworkMode,
|
|
871
900
|
FZ_SEED_SYNC_PEERS: value.seedSyncPeers.join(","),
|
|
872
|
-
FZ_SEED_SYNC_MEMBERS:
|
|
901
|
+
FZ_SEED_SYNC_MEMBERS: value.seedSyncMembers.join(","),
|
|
873
902
|
FZ_SEED_SYNC_EPOCH: value.seedSyncEpoch,
|
|
874
903
|
FZ_SEED_SYNC_CREDENTIAL: "seed-sync-root",
|
|
875
904
|
FZ_SHARED_DIR: value.sharedDirectory,
|
|
@@ -938,7 +967,12 @@ function platformApiCredentialSpecs(options) {
|
|
|
938
967
|
];
|
|
939
968
|
}
|
|
940
969
|
function renderPlatformApiUnits(input) {
|
|
941
|
-
for (const path2 of [
|
|
970
|
+
for (const path2 of [
|
|
971
|
+
input.sharedDirectory,
|
|
972
|
+
input.sharedEnvironmentFile,
|
|
973
|
+
input.slotsDirectory,
|
|
974
|
+
...input.topologyEnvironmentFile ? [input.topologyEnvironmentFile] : []
|
|
975
|
+
]) {
|
|
942
976
|
if (!path2.startsWith("/") || /[\r\n]/.test(path2))
|
|
943
977
|
throw new Error("Runtime paths must be absolute and single-line.");
|
|
944
978
|
}
|
|
@@ -952,6 +986,8 @@ function renderPlatformApiUnits(input) {
|
|
|
952
986
|
const credentials = input.credentials.map((credential) => `LoadCredentialEncrypted=${credential.name}:${credential.encryptedPath}`).join(`
|
|
953
987
|
`);
|
|
954
988
|
const capacityEnvironment = input.capacityEnvironmentFile ? `EnvironmentFile=-${input.capacityEnvironmentFile}
|
|
989
|
+
` : "";
|
|
990
|
+
const topologyEnvironment = input.topologyEnvironmentFile ? `EnvironmentFile=-${input.topologyEnvironmentFile}
|
|
955
991
|
` : "";
|
|
956
992
|
const template = `[Unit]
|
|
957
993
|
Description=ForgeZero (%i slot)
|
|
@@ -965,7 +1001,7 @@ WorkingDirectory=${input.slotsDirectory}/%i
|
|
|
965
1001
|
Environment=NODE_ENV=production
|
|
966
1002
|
Environment=FZ_SLOT=%i
|
|
967
1003
|
EnvironmentFile=${input.sharedEnvironmentFile}
|
|
968
|
-
${capacityEnvironment}${credentials}
|
|
1004
|
+
${capacityEnvironment}${topologyEnvironment}${credentials}
|
|
969
1005
|
ExecStart=/usr/local/bin/bun run ${input.slotsDirectory}/%i/src/index.ts
|
|
970
1006
|
Restart=always
|
|
971
1007
|
RestartSec=2
|