@forgezero/agent 0.1.80 → 0.1.82

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 CHANGED
@@ -1511,7 +1511,7 @@ export const selectedCapability = METAL_BOOTSTRAP_STATE_PATH;
1511
1511
  <a id="forgezero-agent-metal-provision"></a>
1512
1512
  ## @forgezero/agent/metal-provision
1513
1513
 
1514
- The single constrained compute materializer for platform genesis, platform scaling and tenant compute. It executes caller-supplied OS, CPU, memory, disk, bandwidth, network and confidentiality coordinates after capacity and image validation; it owns no environment or product-tier shape. This entry exposes 11 named value exports and 6 named type exports. The generated import block lists one name per line for scanning and copying; keep only the names used by your file.
1514
+ The single constrained compute materializer for platform genesis, platform scaling and tenant compute. It executes caller-supplied OS, CPU, memory, disk, bandwidth, network and confidentiality coordinates after capacity and image validation; it owns no environment or product-tier shape. This entry exposes 11 named value exports and 7 named type exports. The generated import block lists one name per line for scanning and copying; keep only the names used by your file.
1515
1515
 
1516
1516
  ```text
1517
1517
  import {
@@ -1531,6 +1531,7 @@ import {
1531
1531
  import type {
1532
1532
  GuestManifest,
1533
1533
  MetalCommandResult,
1534
+ MetalCpuAllocation,
1534
1535
  MetalCpuPool,
1535
1536
  MetalExec,
1536
1537
  MetalImage,
@@ -750,7 +750,7 @@ async function postSignedNode(options, path, body) {
750
750
  }
751
751
 
752
752
  // src/version.ts
753
- var VERSION3 = "0.1.80";
753
+ var VERSION3 = "0.1.82";
754
754
 
755
755
  // src/agent-heartbeat.ts
756
756
  function readAgentHostMetrics() {
package/dist/bootstrap.js CHANGED
@@ -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.80";
1419
+ var VERSION = "0.1.82";
1420
1420
 
1421
1421
  // src/software.ts
1422
1422
  var PINNED_BUN_VERSION = "1.3.14";
@@ -2879,7 +2879,6 @@ function validatePlatformGenesisGuests(guests, expectedCount = 3) {
2879
2879
  }
2880
2880
  const names = new Set;
2881
2881
  const addresses = new Set;
2882
- const pools = new Set;
2883
2882
  return guests.map((input) => {
2884
2883
  if (!input || typeof input !== "object" || !SAFE_NAME.test(input.name)) {
2885
2884
  throw new Error("platform genesis guest name is malformed");
@@ -2892,13 +2891,11 @@ function validatePlatformGenesisGuests(guests, expectedCount = 3) {
2892
2891
  if (input.cpuPoolKey !== undefined && !SAFE_POOL.test(input.cpuPoolKey)) {
2893
2892
  throw new Error("platform genesis CPU pool key is malformed");
2894
2893
  }
2895
- if (names.has(input.name) || addresses.has(input.address) || input.cpuPoolKey !== undefined && pools.has(input.cpuPoolKey)) {
2896
- throw new Error("platform genesis guest names, addresses and explicit CPU pools must be unique");
2894
+ if (names.has(input.name) || addresses.has(input.address)) {
2895
+ throw new Error("platform genesis guest names and addresses must be unique");
2897
2896
  }
2898
2897
  names.add(input.name);
2899
2898
  addresses.add(input.address);
2900
- if (input.cpuPoolKey !== undefined)
2901
- pools.add(input.cpuPoolKey);
2902
2899
  const physicalCores = integer(input.physicalCores, 1, 256, "physical core count");
2903
2900
  const vcpu = integer(input.vcpu, 1, 512, "vCPU count");
2904
2901
  if (vcpu < physicalCores)
@@ -0,0 +1,10 @@
1
+ export interface AppBuildPlan {
2
+ cwd: string;
3
+ command: readonly string[];
4
+ }
5
+ /** Resolve a checkout without teaching the globally installed CLI any App internals. */
6
+ export declare function planAppBuild(input: {
7
+ root: string;
8
+ profile?: string;
9
+ api?: string;
10
+ }): AppBuildPlan;
package/dist/fz-agent.js CHANGED
@@ -9331,7 +9331,7 @@ async function writeAndCloseProcessInput(input, value) {
9331
9331
  }
9332
9332
 
9333
9333
  // src/version.ts
9334
- var VERSION2 = "0.1.80";
9334
+ var VERSION2 = "0.1.82";
9335
9335
 
9336
9336
  // src/ssh-bootstrap.ts
9337
9337
  class SshBootstrapError extends Error {
@@ -10208,6 +10208,32 @@ function membersOfLinuxList(value, label) {
10208
10208
  throw new MetalProvisionError(`${label} list overlaps itself`);
10209
10209
  return members;
10210
10210
  }
10211
+ function linuxList(members) {
10212
+ const sorted = [...new Set(members)].sort((left, right) => left - right);
10213
+ const ranges = [];
10214
+ for (let index = 0;index < sorted.length; ) {
10215
+ const start = sorted[index];
10216
+ let end = start;
10217
+ while (index + 1 < sorted.length && sorted[index + 1] === end + 1) {
10218
+ index += 1;
10219
+ end = sorted[index];
10220
+ }
10221
+ ranges.push(start === end ? String(start) : `${start}-${end}`);
10222
+ index += 1;
10223
+ }
10224
+ return ranges.join(",");
10225
+ }
10226
+ function physicalCoreGroups(pool) {
10227
+ const cpus = membersOfLinuxList(pool.cpus, "CPU");
10228
+ if (cpus.length % pool.physicalCores !== 0) {
10229
+ throw new MetalProvisionError("CPU pool threads must divide evenly across physical cores");
10230
+ }
10231
+ const threadsPerCore = cpus.length / pool.physicalCores;
10232
+ if (!Number.isInteger(threadsPerCore) || threadsPerCore < 1 || threadsPerCore > 8) {
10233
+ throw new MetalProvisionError("invalid CPU pool threads-per-core topology");
10234
+ }
10235
+ return Array.from({ length: pool.physicalCores }, (_, core) => Array.from({ length: threadsPerCore }, (_2, thread) => cpus[core + thread * pool.physicalCores]));
10236
+ }
10211
10237
  var guestNameFor = (computeKey) => `fzg-${createHash7("sha256").update(computeKey).digest("hex").slice(0, 16)}`;
10212
10238
  var tapNameFor = (computeKey) => `fzt${createHash7("sha256").update(computeKey).digest("hex").slice(0, 12)}`;
10213
10239
  var macForAddress = (address) => {
@@ -10265,6 +10291,7 @@ function validateMetalProfile(profile) {
10265
10291
  if (!Number.isInteger(pool.physicalCores) || pool.physicalCores < 1 || pool.physicalCores > cpus.length) {
10266
10292
  throw new MetalProvisionError("invalid CPU pool physical-core count");
10267
10293
  }
10294
+ physicalCoreGroups(pool);
10268
10295
  for (const cpu of cpus) {
10269
10296
  if (assigned.has(cpu))
10270
10297
  throw new MetalProvisionError("CPU pools overlap");
@@ -10334,26 +10361,38 @@ function allocateCpuPool(profile, claim, rows) {
10334
10361
  const prior = rows.find((row2) => row2.computeKey === claim.computeKey);
10335
10362
  if (prior) {
10336
10363
  const retained = profile.cpuPools.find((pool) => pool.key === prior.cpuPoolKey);
10337
- if (!retained || retained.cpus !== prior.allowedCpus || retained.memoryNodes !== prior.allowedMemoryNodes) {
10364
+ const groups2 = retained ? physicalCoreGroups(retained) : [];
10365
+ const retainedCpus = new Set(membersOfLinuxList(prior.allowedCpus, "persisted guest CPU"));
10366
+ const matchedGroups = groups2.filter((group) => group.every((cpu) => retainedCpus.has(cpu)));
10367
+ if (!retained || retained.memoryNodes !== prior.allowedMemoryNodes || matchedGroups.length !== claim.spec.physicalCores || matchedGroups.flat().length !== retainedCpus.size || retainedCpus.size < claim.spec.vcpu || claim.spec.cpuPoolKey !== undefined && claim.spec.cpuPoolKey !== retained.key) {
10338
10368
  throw new MetalProvisionError("persisted guest CPU pool no longer matches the host profile");
10339
10369
  }
10340
- return retained;
10341
- }
10342
- const used = new Set(rows.map((row2) => row2.cpuPoolKey));
10343
- if (claim.spec.cpuPoolKey) {
10344
- const requested = profile.cpuPools.find((pool) => pool.key === claim.spec.cpuPoolKey);
10345
- if (!requested || used.has(requested.key)) {
10346
- throw new MetalProvisionError("requested CPU pool is unavailable");
10347
- }
10348
- if (requested.physicalCores < claim.spec.physicalCores || membersOfLinuxList(requested.cpus, "CPU").length < claim.spec.vcpu)
10349
- throw new MetalProvisionError("requested CPU pool cannot satisfy this guest");
10350
- return requested;
10370
+ return {
10371
+ key: retained.key,
10372
+ cpus: prior.allowedCpus,
10373
+ physicalCores: matchedGroups.length,
10374
+ memoryNodes: prior.allowedMemoryNodes
10375
+ };
10351
10376
  }
10352
- const candidates = profile.cpuPools.filter((pool) => !used.has(pool.key) && pool.physicalCores >= claim.spec.physicalCores && membersOfLinuxList(pool.cpus, "CPU").length >= claim.spec.vcpu).sort((left, right) => left.physicalCores - right.physicalCores || membersOfLinuxList(left.cpus, "CPU").length - membersOfLinuxList(right.cpus, "CPU").length || left.key.localeCompare(right.key));
10377
+ const usedCpus = new Set(rows.flatMap((row2) => membersOfLinuxList(row2.allowedCpus, "persisted guest CPU")));
10378
+ const candidates = profile.cpuPools.filter((pool) => !claim.spec.cpuPoolKey || pool.key === claim.spec.cpuPoolKey).map((pool) => ({
10379
+ pool,
10380
+ available: physicalCoreGroups(pool).filter((group) => group.every((cpu) => !usedCpus.has(cpu)))
10381
+ })).filter(({ available }) => {
10382
+ const selected2 = available.slice(0, claim.spec.physicalCores);
10383
+ return selected2.length === claim.spec.physicalCores && selected2.flat().length >= claim.spec.vcpu;
10384
+ }).sort((left, right) => left.available.length - right.available.length || left.pool.key.localeCompare(right.pool.key));
10353
10385
  const selected = candidates[0];
10354
- if (!selected)
10355
- throw new MetalProvisionError("no exclusive CPU pool can satisfy this guest");
10356
- return selected;
10386
+ if (!selected) {
10387
+ throw new MetalProvisionError(claim.spec.cpuPoolKey ? "requested CPU pool cannot satisfy this guest" : "no exclusive CPU capacity can satisfy this guest");
10388
+ }
10389
+ const groups = selected.available.slice(0, claim.spec.physicalCores);
10390
+ return {
10391
+ key: selected.pool.key,
10392
+ cpus: linuxList(groups.flat()),
10393
+ physicalCores: groups.length,
10394
+ memoryNodes: selected.pool.memoryNodes
10395
+ };
10357
10396
  }
10358
10397
  var base64 = (value) => Buffer.from(value).toString("base64");
10359
10398
  var yamlFile = (path2, content, permissions) => ` - path: ${JSON.stringify(path2)}
package/dist/fz.js CHANGED
@@ -4810,8 +4810,8 @@ async function spawnWith(command, env, report = () => {}, options = {}) {
4810
4810
  }
4811
4811
 
4812
4812
  // src/cli/index.ts
4813
- import { existsSync as existsSync12, lstatSync as lstatSync10, mkdirSync as mkdirSync13, readFileSync as readFileSync15, writeFileSync as writeFileSync13 } from "fs";
4814
- import { basename as basename3, dirname as dirname14, isAbsolute as isAbsolute7, join as join12, resolve as resolve11 } from "path";
4813
+ import { existsSync as existsSync12, lstatSync as lstatSync10, mkdirSync as mkdirSync13, readFileSync as readFileSync16, writeFileSync as writeFileSync13 } from "fs";
4814
+ import { basename as basename3, dirname as dirname14, isAbsolute as isAbsolute7, join as join12, resolve as resolve12 } from "path";
4815
4815
 
4816
4816
  // src/process-input.ts
4817
4817
  async function writeAndCloseProcessInput(input, value) {
@@ -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.80";
4840
+ var VERSION2 = "0.1.82";
4841
4841
 
4842
4842
  // src/software.ts
4843
4843
  var PINNED_BUN_VERSION = "1.3.14";
@@ -11896,7 +11896,6 @@ function validatePlatformGenesisGuests(guests, expectedCount = 3) {
11896
11896
  }
11897
11897
  const names = new Set;
11898
11898
  const addresses = new Set;
11899
- const pools = new Set;
11900
11899
  return guests.map((input) => {
11901
11900
  if (!input || typeof input !== "object" || !SAFE_NAME.test(input.name)) {
11902
11901
  throw new Error("platform genesis guest name is malformed");
@@ -11909,13 +11908,11 @@ function validatePlatformGenesisGuests(guests, expectedCount = 3) {
11909
11908
  if (input.cpuPoolKey !== undefined && !SAFE_POOL.test(input.cpuPoolKey)) {
11910
11909
  throw new Error("platform genesis CPU pool key is malformed");
11911
11910
  }
11912
- if (names.has(input.name) || addresses.has(input.address) || input.cpuPoolKey !== undefined && pools.has(input.cpuPoolKey)) {
11913
- throw new Error("platform genesis guest names, addresses and explicit CPU pools must be unique");
11911
+ if (names.has(input.name) || addresses.has(input.address)) {
11912
+ throw new Error("platform genesis guest names and addresses must be unique");
11914
11913
  }
11915
11914
  names.add(input.name);
11916
11915
  addresses.add(input.address);
11917
- if (input.cpuPoolKey !== undefined)
11918
- pools.add(input.cpuPoolKey);
11919
11916
  const physicalCores = integer2(input.physicalCores, 1, 256, "physical core count");
11920
11917
  const vcpu = integer2(input.vcpu, 1, 512, "vCPU count");
11921
11918
  if (vcpu < physicalCores)
@@ -15550,6 +15547,17 @@ function membersOfLinuxList(value, label) {
15550
15547
  throw new MetalProvisionError(`${label} list overlaps itself`);
15551
15548
  return members;
15552
15549
  }
15550
+ function physicalCoreGroups(pool) {
15551
+ const cpus = membersOfLinuxList(pool.cpus, "CPU");
15552
+ if (cpus.length % pool.physicalCores !== 0) {
15553
+ throw new MetalProvisionError("CPU pool threads must divide evenly across physical cores");
15554
+ }
15555
+ const threadsPerCore = cpus.length / pool.physicalCores;
15556
+ if (!Number.isInteger(threadsPerCore) || threadsPerCore < 1 || threadsPerCore > 8) {
15557
+ throw new MetalProvisionError("invalid CPU pool threads-per-core topology");
15558
+ }
15559
+ return Array.from({ length: pool.physicalCores }, (_, core) => Array.from({ length: threadsPerCore }, (_2, thread) => cpus[core + thread * pool.physicalCores]));
15560
+ }
15553
15561
  function validateMetalProfile(profile) {
15554
15562
  if (!SAFE_NAME2.test(profile.volumeGroup))
15555
15563
  throw new MetalProvisionError("invalid volume group");
@@ -15598,6 +15606,7 @@ function validateMetalProfile(profile) {
15598
15606
  if (!Number.isInteger(pool.physicalCores) || pool.physicalCores < 1 || pool.physicalCores > cpus.length) {
15599
15607
  throw new MetalProvisionError("invalid CPU pool physical-core count");
15600
15608
  }
15609
+ physicalCoreGroups(pool);
15601
15610
  for (const cpu of cpus) {
15602
15611
  if (assigned.has(cpu))
15603
15612
  throw new MetalProvisionError("CPU pools overlap");
@@ -17678,6 +17687,38 @@ async function runRepositoryOperation(operation, requestedRoot, args = []) {
17678
17687
  `), { cwd: resolved.cwd });
17679
17688
  }
17680
17689
 
17690
+ // src/cli/app-build.ts
17691
+ import { readFileSync as readFileSync15 } from "fs";
17692
+ import { resolve as resolve11 } from "path";
17693
+ function planAppBuild(input) {
17694
+ const cwd = resolve11(input.root);
17695
+ let manifest;
17696
+ try {
17697
+ manifest = JSON.parse(readFileSync15(resolve11(cwd, "package.json"), "utf8"));
17698
+ } catch {
17699
+ throw new Error(`--root must name a ForgeZero App checkout: ${cwd}`);
17700
+ }
17701
+ if (manifest.name !== "@forgezero/app") {
17702
+ throw new Error(`--root must name @forgezero/app, not ${manifest.name ?? "an unnamed package"}.`);
17703
+ }
17704
+ if (input.profile && !["local", "development", "production"].includes(input.profile)) {
17705
+ throw new Error("--profile must be local, development, or production.");
17706
+ }
17707
+ if (input.profile && input.api)
17708
+ throw new Error("Choose --profile or --api, not both.");
17709
+ return {
17710
+ cwd,
17711
+ command: [
17712
+ "bun",
17713
+ "run",
17714
+ "build",
17715
+ "--",
17716
+ ...input.profile ? ["--profile", input.profile] : [],
17717
+ ...input.api ? ["--api", input.api] : []
17718
+ ]
17719
+ };
17720
+ }
17721
+
17681
17722
  // src/cli/index.ts
17682
17723
  var DEFAULT_MODE = (THRESHOLD_MODES.find((mode) => mode.threshold === 1 && mode.total === 1) ?? THRESHOLD_MODES[0]).id;
17683
17724
  var PACKAGED_AGENT_BIN2 = fileURLToPath3(new URL("./fz-agent.js", import.meta.url));
@@ -17907,7 +17948,7 @@ async function api(options, path, init) {
17907
17948
  return { status: response.status, body };
17908
17949
  }
17909
17950
  function sleep(ms) {
17910
- return new Promise((resolve12) => setTimeout(resolve12, ms));
17951
+ return new Promise((resolve13) => setTimeout(resolve13, ms));
17911
17952
  }
17912
17953
  function browserCommand(url) {
17913
17954
  if (process.platform === "darwin")
@@ -18006,7 +18047,7 @@ function readOwnerOnlySecret(path, label) {
18006
18047
  if (uid !== undefined && uid !== 0 && metadata.uid !== uid) {
18007
18048
  throw new Error(`The ${label} file ${path} is not owned by the current user.`);
18008
18049
  }
18009
- const value = readFileSync15(path, "utf8").trim();
18050
+ const value = readFileSync16(path, "utf8").trim();
18010
18051
  if (!value)
18011
18052
  throw new Error(`The ${label} file ${path} is empty.`);
18012
18053
  return value;
@@ -18187,7 +18228,7 @@ async function cmdApi(options, args) {
18187
18228
  let body = undefined;
18188
18229
  if (options.data !== undefined && options.dataFile)
18189
18230
  throw new Error("Use only one of --data or --data-file.");
18190
- const encoded = options.data === "-" ? await Bun.stdin.text() : options.dataFile ? readFileSync15(options.dataFile, "utf8") : options.data;
18231
+ const encoded = options.data === "-" ? await Bun.stdin.text() : options.dataFile ? readFileSync16(options.dataFile, "utf8") : options.data;
18191
18232
  if (encoded !== undefined)
18192
18233
  body = JSON.parse(encoded);
18193
18234
  const result = await api(options, `${url.pathname}${url.search}`, { method, body });
@@ -18366,7 +18407,7 @@ async function cmdAgent(options, args) {
18366
18407
  out.line();
18367
18408
  out.line(" Compatibility SSH deploy public key:");
18368
18409
  out.line();
18369
- out.line(` ${readFileSync15(gitIdentity.gitPublicKeyPath, "utf8").trim()}`);
18410
+ out.line(` ${readFileSync16(gitIdentity.gitPublicKeyPath, "utf8").trim()}`);
18370
18411
  out.line();
18371
18412
  }
18372
18413
  return 0;
@@ -18476,7 +18517,7 @@ function interactiveMetalBootstrap() {
18476
18517
  });
18477
18518
  }
18478
18519
  function writeBootstrapConfig(path, config) {
18479
- if (!isAbsolute7(path) || resolve11(path) !== path)
18520
+ if (!isAbsolute7(path) || resolve12(path) !== path)
18480
18521
  throw new Error("--output must be a canonical absolute path");
18481
18522
  mkdirSync13(dirname14(path), { recursive: true, mode: 448 });
18482
18523
  writeFileSync13(path, `${JSON.stringify(config, null, 2)}
@@ -18540,7 +18581,7 @@ async function interactiveCloudflareBootstrap() {
18540
18581
  });
18541
18582
  }
18542
18583
  function genesisOutputDirectory(path) {
18543
- if (!isAbsolute7(path) || resolve11(path) !== path)
18584
+ if (!isAbsolute7(path) || resolve12(path) !== path)
18544
18585
  throw new Error("--output must be a canonical absolute directory");
18545
18586
  if (!existsSync12(path))
18546
18587
  mkdirSync13(path, { recursive: true, mode: 448 });
@@ -18570,7 +18611,7 @@ function interactiveGenesisGuests() {
18570
18611
  name: bootstrapAnswer(`Guest ${index} name`),
18571
18612
  address: bootstrapAnswer(`Guest ${index} private IPv4`),
18572
18613
  imageKey: bootstrapAnswer(`Guest ${index} OS image key`, SUPPORTED_GUEST_IMAGE.key),
18573
- cpuPoolKey: bootstrapAnswer(`Guest ${index} exclusive CPU pool key`),
18614
+ cpuPoolKey: bootstrapAnswer(`Guest ${index} preferred NUMA CPU pool key (capacity is sliced by request)`),
18574
18615
  physicalCores: bootstrapNumber(`Guest ${index} physical cores`),
18575
18616
  vcpu: bootstrapNumber(`Guest ${index} vCPUs`),
18576
18617
  memoryGib: bootstrapNumber(`Guest ${index} memory GiB`),
@@ -19132,7 +19173,7 @@ async function cmdUnlock(options) {
19132
19173
  if (options.key || options.userExplicit) {
19133
19174
  throw new Error("SSH/user unlock coordinates are not accepted. SSH keys cannot open a WebAuthn-PRF custody envelope; use --phrase-file or --phrase-stdin.");
19134
19175
  }
19135
- const phraseText = options.phraseFile ? readOwnerOnlySecret(options.phraseFile, "recovery phrase") : options.phraseStdin ? readFileSync15(0, "utf8").trim() : "";
19176
+ const phraseText = options.phraseFile ? readOwnerOnlySecret(options.phraseFile, "recovery phrase") : options.phraseStdin ? readFileSync16(0, "utf8").trim() : "";
19136
19177
  const phrase = phraseText ? phraseText.split(/\s+/) : [];
19137
19178
  if (phrase.length !== 24) {
19138
19179
  throw new Error(`Recovery phrase must contain exactly 24 words; received ${phrase.length}. Use --phrase-file or --phrase-stdin.`);
@@ -19229,6 +19270,23 @@ function cmdProject(options, args) {
19229
19270
  return 1;
19230
19271
  }
19231
19272
  }
19273
+ async function cmdApp(options, args) {
19274
+ if ((args[0] ?? "") !== "build" || args.length !== 1) {
19275
+ out.fail("Usage: fz app build [--root <app>] [--profile local|development|production | --api <origin>]");
19276
+ return 2;
19277
+ }
19278
+ try {
19279
+ const plan = planAppBuild({
19280
+ root: options.projectRoot,
19281
+ ...options.deployProfile !== "app" ? { profile: options.deployProfile } : {},
19282
+ ...options.apiExplicit ? { api: options.api } : {}
19283
+ });
19284
+ return await spawnWith(plan.command, Object.fromEntries(Object.entries(process.env).filter((entry) => entry[1] !== undefined)), (line) => out.line(line), { cwd: plan.cwd });
19285
+ } catch (cause) {
19286
+ out.fail(cause instanceof Error ? cause.message : String(cause));
19287
+ return 1;
19288
+ }
19289
+ }
19232
19290
  function repositoryOperationArguments(options, args) {
19233
19291
  const unsupported = unsupportedRepositoryCliOption(process.argv.slice(2));
19234
19292
  if (unsupported === "--root")
@@ -19348,7 +19406,7 @@ function projectFromCheckout(options) {
19348
19406
  try {
19349
19407
  return loadConfig({
19350
19408
  cwd: options.projectRoot,
19351
- readFile: (path) => existsSync12(path) ? readFileSync15(path, "utf8") : undefined
19409
+ readFile: (path) => existsSync12(path) ? readFileSync16(path, "utf8") : undefined
19352
19410
  }).config.project;
19353
19411
  } catch (cause) {
19354
19412
  throw new Error(`No --project was given and the checkout has no usable .fz/config.json: ${cause instanceof Error ? cause.message : String(cause)}`);
@@ -19411,7 +19469,7 @@ async function cmdDeploy(options, args) {
19411
19469
  out.ok(`Initialized legacy .fz/deploy.json (${created.summary.digest}).`);
19412
19470
  return 0;
19413
19471
  }
19414
- initializeTypeScriptDeployment(options.projectRoot, { name: options.projectName ?? basename3(resolve11(options.projectRoot)), force: options.force });
19472
+ initializeTypeScriptDeployment(options.projectRoot, { name: options.projectName ?? basename3(resolve12(options.projectRoot)), force: options.force });
19415
19473
  const compiled = await compileDeploymentProject(options.projectRoot);
19416
19474
  if (options.json)
19417
19475
  out.line(JSON.stringify({ source: compiled.source, output: compiled.output, digest: compiled.digest }, null, 2));
@@ -19436,7 +19494,7 @@ async function cmdDeploy(options, args) {
19436
19494
  return problems.length === 0 ? 0 : 1;
19437
19495
  }
19438
19496
  if (operation === "check" || operation === "sync") {
19439
- if (existsSync12(resolve11(options.projectRoot, DEPLOY_SOURCE_FILE))) {
19497
+ if (existsSync12(resolve12(options.projectRoot, DEPLOY_SOURCE_FILE))) {
19440
19498
  const expected = await compileDeploymentProject(options.projectRoot, { write: false });
19441
19499
  const actual = inspectCompiledDeployment(options.projectRoot);
19442
19500
  const current2 = canonicalJson(expected.plan) === canonicalJson(actual.plan);
@@ -19512,7 +19570,7 @@ async function cmdDeploy(options, args) {
19512
19570
  branch: options.branch,
19513
19571
  cloneUrl: required(options.cloneUrl, "--clone-url"),
19514
19572
  sourceAuth,
19515
- ...options.knownHostsFile ? { knownHosts: readFileSync15(options.knownHostsFile, "utf8") } : {},
19573
+ ...options.knownHostsFile ? { knownHosts: readFileSync16(options.knownHostsFile, "utf8") } : {},
19516
19574
  projectKey
19517
19575
  } });
19518
19576
  const pipelineKey2 = String(created.pipelineKey);
@@ -19660,7 +19718,9 @@ function usage() {
19660
19718
  fz bootstrap repair Reapply an explicitly supplied reviewed config
19661
19719
  fz project init Create vendor-neutral, Git-persisted AI context
19662
19720
  fz project sync Regenerate Claude/Codex/Gemini/Copilot/Cursor adapters
19663
- fz project check Fail when truth sources or generated adapters drift
19721
+ fz project check Fail when truth sources or generated adapters drift
19722
+ fz app build Build the static App for a numbered local/development/
19723
+ production choice, --profile, or an explicit --api origin
19664
19724
  fz deploy init Create forgezero.deploy.ts and its fail-safe canonical plan
19665
19725
  fz deploy compile Compile TypeScript into .fz/deploy.plan.json
19666
19726
  fz deploy check Validate source, plan, actions, providers and topology
@@ -19810,6 +19870,9 @@ async function runCli() {
19810
19870
  case "project":
19811
19871
  code = cmdProject(options, args);
19812
19872
  break;
19873
+ case "app":
19874
+ code = await cmdApp(options, args);
19875
+ break;
19813
19876
  case "deploy":
19814
19877
  code = await cmdDeploy(options, args);
19815
19878
  break;
@@ -118,6 +118,17 @@ function membersOfLinuxList(value, label) {
118
118
  throw new MetalProvisionError(`${label} list overlaps itself`);
119
119
  return members;
120
120
  }
121
+ function physicalCoreGroups(pool) {
122
+ const cpus = membersOfLinuxList(pool.cpus, "CPU");
123
+ if (cpus.length % pool.physicalCores !== 0) {
124
+ throw new MetalProvisionError("CPU pool threads must divide evenly across physical cores");
125
+ }
126
+ const threadsPerCore = cpus.length / pool.physicalCores;
127
+ if (!Number.isInteger(threadsPerCore) || threadsPerCore < 1 || threadsPerCore > 8) {
128
+ throw new MetalProvisionError("invalid CPU pool threads-per-core topology");
129
+ }
130
+ return Array.from({ length: pool.physicalCores }, (_, core) => Array.from({ length: threadsPerCore }, (_2, thread) => cpus[core + thread * pool.physicalCores]));
131
+ }
121
132
  function validateMetalProfile(profile) {
122
133
  if (!SAFE_NAME.test(profile.volumeGroup))
123
134
  throw new MetalProvisionError("invalid volume group");
@@ -166,6 +177,7 @@ function validateMetalProfile(profile) {
166
177
  if (!Number.isInteger(pool.physicalCores) || pool.physicalCores < 1 || pool.physicalCores > cpus.length) {
167
178
  throw new MetalProvisionError("invalid CPU pool physical-core count");
168
179
  }
180
+ physicalCoreGroups(pool);
169
181
  for (const cpu of cpus) {
170
182
  if (assigned.has(cpu))
171
183
  throw new MetalProvisionError("CPU pools overlap");
@@ -354,7 +366,7 @@ function systemdAgentEgressDirectives(loopbackTcpPorts = []) {
354
366
  }
355
367
 
356
368
  // src/version.ts
357
- var VERSION = "0.1.80";
369
+ var VERSION = "0.1.82";
358
370
 
359
371
  // src/otel-collector.ts
360
372
  var FORGEZERO_OTEL_COLLECTOR_UNIT = "forgezero-otel-collector.service";
@@ -748,6 +748,32 @@ function membersOfLinuxList(value, label) {
748
748
  throw new MetalProvisionError(`${label} list overlaps itself`);
749
749
  return members;
750
750
  }
751
+ function linuxList(members) {
752
+ const sorted = [...new Set(members)].sort((left, right) => left - right);
753
+ const ranges = [];
754
+ for (let index = 0;index < sorted.length; ) {
755
+ const start = sorted[index];
756
+ let end = start;
757
+ while (index + 1 < sorted.length && sorted[index + 1] === end + 1) {
758
+ index += 1;
759
+ end = sorted[index];
760
+ }
761
+ ranges.push(start === end ? String(start) : `${start}-${end}`);
762
+ index += 1;
763
+ }
764
+ return ranges.join(",");
765
+ }
766
+ function physicalCoreGroups(pool) {
767
+ const cpus = membersOfLinuxList(pool.cpus, "CPU");
768
+ if (cpus.length % pool.physicalCores !== 0) {
769
+ throw new MetalProvisionError("CPU pool threads must divide evenly across physical cores");
770
+ }
771
+ const threadsPerCore = cpus.length / pool.physicalCores;
772
+ if (!Number.isInteger(threadsPerCore) || threadsPerCore < 1 || threadsPerCore > 8) {
773
+ throw new MetalProvisionError("invalid CPU pool threads-per-core topology");
774
+ }
775
+ return Array.from({ length: pool.physicalCores }, (_, core) => Array.from({ length: threadsPerCore }, (_2, thread) => cpus[core + thread * pool.physicalCores]));
776
+ }
751
777
  var guestNameFor = (computeKey) => `fzg-${createHash2("sha256").update(computeKey).digest("hex").slice(0, 16)}`;
752
778
  var tapNameFor = (computeKey) => `fzt${createHash2("sha256").update(computeKey).digest("hex").slice(0, 12)}`;
753
779
  var macForAddress = (address) => {
@@ -805,6 +831,7 @@ function validateMetalProfile(profile) {
805
831
  if (!Number.isInteger(pool.physicalCores) || pool.physicalCores < 1 || pool.physicalCores > cpus.length) {
806
832
  throw new MetalProvisionError("invalid CPU pool physical-core count");
807
833
  }
834
+ physicalCoreGroups(pool);
808
835
  for (const cpu of cpus) {
809
836
  if (assigned.has(cpu))
810
837
  throw new MetalProvisionError("CPU pools overlap");
@@ -874,26 +901,38 @@ function allocateCpuPool(profile, claim, rows) {
874
901
  const prior = rows.find((row) => row.computeKey === claim.computeKey);
875
902
  if (prior) {
876
903
  const retained = profile.cpuPools.find((pool) => pool.key === prior.cpuPoolKey);
877
- if (!retained || retained.cpus !== prior.allowedCpus || retained.memoryNodes !== prior.allowedMemoryNodes) {
904
+ const groups2 = retained ? physicalCoreGroups(retained) : [];
905
+ const retainedCpus = new Set(membersOfLinuxList(prior.allowedCpus, "persisted guest CPU"));
906
+ const matchedGroups = groups2.filter((group) => group.every((cpu) => retainedCpus.has(cpu)));
907
+ if (!retained || retained.memoryNodes !== prior.allowedMemoryNodes || matchedGroups.length !== claim.spec.physicalCores || matchedGroups.flat().length !== retainedCpus.size || retainedCpus.size < claim.spec.vcpu || claim.spec.cpuPoolKey !== undefined && claim.spec.cpuPoolKey !== retained.key) {
878
908
  throw new MetalProvisionError("persisted guest CPU pool no longer matches the host profile");
879
909
  }
880
- return retained;
910
+ return {
911
+ key: retained.key,
912
+ cpus: prior.allowedCpus,
913
+ physicalCores: matchedGroups.length,
914
+ memoryNodes: prior.allowedMemoryNodes
915
+ };
881
916
  }
882
- const used = new Set(rows.map((row) => row.cpuPoolKey));
883
- if (claim.spec.cpuPoolKey) {
884
- const requested = profile.cpuPools.find((pool) => pool.key === claim.spec.cpuPoolKey);
885
- if (!requested || used.has(requested.key)) {
886
- throw new MetalProvisionError("requested CPU pool is unavailable");
887
- }
888
- if (requested.physicalCores < claim.spec.physicalCores || membersOfLinuxList(requested.cpus, "CPU").length < claim.spec.vcpu)
889
- throw new MetalProvisionError("requested CPU pool cannot satisfy this guest");
890
- return requested;
891
- }
892
- const candidates = profile.cpuPools.filter((pool) => !used.has(pool.key) && pool.physicalCores >= claim.spec.physicalCores && membersOfLinuxList(pool.cpus, "CPU").length >= claim.spec.vcpu).sort((left, right) => left.physicalCores - right.physicalCores || membersOfLinuxList(left.cpus, "CPU").length - membersOfLinuxList(right.cpus, "CPU").length || left.key.localeCompare(right.key));
917
+ const usedCpus = new Set(rows.flatMap((row) => membersOfLinuxList(row.allowedCpus, "persisted guest CPU")));
918
+ const candidates = profile.cpuPools.filter((pool) => !claim.spec.cpuPoolKey || pool.key === claim.spec.cpuPoolKey).map((pool) => ({
919
+ pool,
920
+ available: physicalCoreGroups(pool).filter((group) => group.every((cpu) => !usedCpus.has(cpu)))
921
+ })).filter(({ available }) => {
922
+ const selected2 = available.slice(0, claim.spec.physicalCores);
923
+ return selected2.length === claim.spec.physicalCores && selected2.flat().length >= claim.spec.vcpu;
924
+ }).sort((left, right) => left.available.length - right.available.length || left.pool.key.localeCompare(right.pool.key));
893
925
  const selected = candidates[0];
894
- if (!selected)
895
- throw new MetalProvisionError("no exclusive CPU pool can satisfy this guest");
896
- return selected;
926
+ if (!selected) {
927
+ throw new MetalProvisionError(claim.spec.cpuPoolKey ? "requested CPU pool cannot satisfy this guest" : "no exclusive CPU capacity can satisfy this guest");
928
+ }
929
+ const groups = selected.available.slice(0, claim.spec.physicalCores);
930
+ return {
931
+ key: selected.pool.key,
932
+ cpus: linuxList(groups.flat()),
933
+ physicalCores: groups.length,
934
+ memoryNodes: selected.pool.memoryNodes
935
+ };
897
936
  }
898
937
  var base64 = (value) => Buffer.from(value).toString("base64");
899
938
  var yamlFile = (path2, content, permissions) => ` - path: ${JSON.stringify(path2)}
@@ -3,7 +3,7 @@ export interface MetalImage {
3
3
  path: string;
4
4
  sha256: string;
5
5
  }
6
- /** One non-overlapping physical isolation boundary available to one guest. */
6
+ /** One non-overlapping NUMA-local CPU capacity pool that may serve several guests. */
7
7
  export interface MetalCpuPool {
8
8
  key: string;
9
9
  /** Linux CPU-list syntax, including every SMT sibling in this pool. */
@@ -13,6 +13,9 @@ export interface MetalCpuPool {
13
13
  /** Linux NUMA node-list syntax. Optional on a non-NUMA machine. */
14
14
  memoryNodes?: string;
15
15
  }
16
+ /** Exact non-overlapping physical-core slice reserved for one guest. */
17
+ export interface MetalCpuAllocation extends MetalCpuPool {
18
+ }
16
19
  export interface MetalProvisionProfile {
17
20
  volumeGroup: string;
18
21
  bridge: string;
@@ -70,8 +73,8 @@ export declare const tapNameFor: (computeKey: string) => string;
70
73
  export declare const macForAddress: (address: string) => string;
71
74
  export declare function validateMetalProfile(profile: MetalProvisionProfile): void;
72
75
  export declare function allocateAddress(profile: MetalProvisionProfile, computeKey: string, rows: GuestManifest[]): string;
73
- /** Stable tight-fit allocation of one exclusive CPU/NUMA pool. */
74
- export declare function allocateCpuPool(profile: MetalProvisionProfile, claim: Pick<RemoteProvisionClaim, 'computeKey' | 'spec'>, rows: GuestManifest[]): MetalCpuPool;
76
+ /** Stable tight-fit allocation of exact physical cores inside one NUMA-local pool. */
77
+ export declare function allocateCpuPool(profile: MetalProvisionProfile, claim: Pick<RemoteProvisionClaim, 'computeKey' | 'spec'>, rows: GuestManifest[]): MetalCpuAllocation;
75
78
  export declare function guestBootstrapOperations(profile: MetalProvisionProfile, attested?: boolean, hasEnrolment?: boolean, nodeLabel?: string): readonly (readonly string[])[];
76
79
  export declare function cloudInit(profile: MetalProvisionProfile, claim: CreateRemoteProvisionClaim, manifest: GuestManifest): {
77
80
  userData: string;
@@ -748,6 +748,32 @@ function membersOfLinuxList(value, label) {
748
748
  throw new MetalProvisionError(`${label} list overlaps itself`);
749
749
  return members;
750
750
  }
751
+ function linuxList(members) {
752
+ const sorted = [...new Set(members)].sort((left, right) => left - right);
753
+ const ranges = [];
754
+ for (let index = 0;index < sorted.length; ) {
755
+ const start = sorted[index];
756
+ let end = start;
757
+ while (index + 1 < sorted.length && sorted[index + 1] === end + 1) {
758
+ index += 1;
759
+ end = sorted[index];
760
+ }
761
+ ranges.push(start === end ? String(start) : `${start}-${end}`);
762
+ index += 1;
763
+ }
764
+ return ranges.join(",");
765
+ }
766
+ function physicalCoreGroups(pool) {
767
+ const cpus = membersOfLinuxList(pool.cpus, "CPU");
768
+ if (cpus.length % pool.physicalCores !== 0) {
769
+ throw new MetalProvisionError("CPU pool threads must divide evenly across physical cores");
770
+ }
771
+ const threadsPerCore = cpus.length / pool.physicalCores;
772
+ if (!Number.isInteger(threadsPerCore) || threadsPerCore < 1 || threadsPerCore > 8) {
773
+ throw new MetalProvisionError("invalid CPU pool threads-per-core topology");
774
+ }
775
+ return Array.from({ length: pool.physicalCores }, (_, core) => Array.from({ length: threadsPerCore }, (_2, thread) => cpus[core + thread * pool.physicalCores]));
776
+ }
751
777
  var guestNameFor = (computeKey) => `fzg-${createHash2("sha256").update(computeKey).digest("hex").slice(0, 16)}`;
752
778
  var tapNameFor = (computeKey) => `fzt${createHash2("sha256").update(computeKey).digest("hex").slice(0, 12)}`;
753
779
  var macForAddress = (address) => {
@@ -805,6 +831,7 @@ function validateMetalProfile(profile) {
805
831
  if (!Number.isInteger(pool.physicalCores) || pool.physicalCores < 1 || pool.physicalCores > cpus.length) {
806
832
  throw new MetalProvisionError("invalid CPU pool physical-core count");
807
833
  }
834
+ physicalCoreGroups(pool);
808
835
  for (const cpu of cpus) {
809
836
  if (assigned.has(cpu))
810
837
  throw new MetalProvisionError("CPU pools overlap");
@@ -874,26 +901,38 @@ function allocateCpuPool(profile, claim, rows) {
874
901
  const prior = rows.find((row) => row.computeKey === claim.computeKey);
875
902
  if (prior) {
876
903
  const retained = profile.cpuPools.find((pool) => pool.key === prior.cpuPoolKey);
877
- if (!retained || retained.cpus !== prior.allowedCpus || retained.memoryNodes !== prior.allowedMemoryNodes) {
904
+ const groups2 = retained ? physicalCoreGroups(retained) : [];
905
+ const retainedCpus = new Set(membersOfLinuxList(prior.allowedCpus, "persisted guest CPU"));
906
+ const matchedGroups = groups2.filter((group) => group.every((cpu) => retainedCpus.has(cpu)));
907
+ if (!retained || retained.memoryNodes !== prior.allowedMemoryNodes || matchedGroups.length !== claim.spec.physicalCores || matchedGroups.flat().length !== retainedCpus.size || retainedCpus.size < claim.spec.vcpu || claim.spec.cpuPoolKey !== undefined && claim.spec.cpuPoolKey !== retained.key) {
878
908
  throw new MetalProvisionError("persisted guest CPU pool no longer matches the host profile");
879
909
  }
880
- return retained;
910
+ return {
911
+ key: retained.key,
912
+ cpus: prior.allowedCpus,
913
+ physicalCores: matchedGroups.length,
914
+ memoryNodes: prior.allowedMemoryNodes
915
+ };
881
916
  }
882
- const used = new Set(rows.map((row) => row.cpuPoolKey));
883
- if (claim.spec.cpuPoolKey) {
884
- const requested = profile.cpuPools.find((pool) => pool.key === claim.spec.cpuPoolKey);
885
- if (!requested || used.has(requested.key)) {
886
- throw new MetalProvisionError("requested CPU pool is unavailable");
887
- }
888
- if (requested.physicalCores < claim.spec.physicalCores || membersOfLinuxList(requested.cpus, "CPU").length < claim.spec.vcpu)
889
- throw new MetalProvisionError("requested CPU pool cannot satisfy this guest");
890
- return requested;
891
- }
892
- const candidates = profile.cpuPools.filter((pool) => !used.has(pool.key) && pool.physicalCores >= claim.spec.physicalCores && membersOfLinuxList(pool.cpus, "CPU").length >= claim.spec.vcpu).sort((left, right) => left.physicalCores - right.physicalCores || membersOfLinuxList(left.cpus, "CPU").length - membersOfLinuxList(right.cpus, "CPU").length || left.key.localeCompare(right.key));
917
+ const usedCpus = new Set(rows.flatMap((row) => membersOfLinuxList(row.allowedCpus, "persisted guest CPU")));
918
+ const candidates = profile.cpuPools.filter((pool) => !claim.spec.cpuPoolKey || pool.key === claim.spec.cpuPoolKey).map((pool) => ({
919
+ pool,
920
+ available: physicalCoreGroups(pool).filter((group) => group.every((cpu) => !usedCpus.has(cpu)))
921
+ })).filter(({ available }) => {
922
+ const selected2 = available.slice(0, claim.spec.physicalCores);
923
+ return selected2.length === claim.spec.physicalCores && selected2.flat().length >= claim.spec.vcpu;
924
+ }).sort((left, right) => left.available.length - right.available.length || left.pool.key.localeCompare(right.pool.key));
893
925
  const selected = candidates[0];
894
- if (!selected)
895
- throw new MetalProvisionError("no exclusive CPU pool can satisfy this guest");
896
- return selected;
926
+ if (!selected) {
927
+ throw new MetalProvisionError(claim.spec.cpuPoolKey ? "requested CPU pool cannot satisfy this guest" : "no exclusive CPU capacity can satisfy this guest");
928
+ }
929
+ const groups = selected.available.slice(0, claim.spec.physicalCores);
930
+ return {
931
+ key: selected.pool.key,
932
+ cpus: linuxList(groups.flat()),
933
+ physicalCores: groups.length,
934
+ memoryNodes: selected.pool.memoryNodes
935
+ };
897
936
  }
898
937
  var base64 = (value) => Buffer.from(value).toString("base64");
899
938
  var yamlFile = (path2, content, permissions) => ` - path: ${JSON.stringify(path2)}
@@ -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.80";
1419
+ var VERSION = "0.1.82";
1420
1420
 
1421
1421
  // src/software.ts
1422
1422
  var PINNED_BUN_VERSION = "1.3.14";
@@ -2879,7 +2879,6 @@ function validatePlatformGenesisGuests(guests, expectedCount = 3) {
2879
2879
  }
2880
2880
  const names = new Set;
2881
2881
  const addresses = new Set;
2882
- const pools = new Set;
2883
2882
  return guests.map((input) => {
2884
2883
  if (!input || typeof input !== "object" || !SAFE_NAME.test(input.name)) {
2885
2884
  throw new Error("platform genesis guest name is malformed");
@@ -2892,13 +2891,11 @@ function validatePlatformGenesisGuests(guests, expectedCount = 3) {
2892
2891
  if (input.cpuPoolKey !== undefined && !SAFE_POOL.test(input.cpuPoolKey)) {
2893
2892
  throw new Error("platform genesis CPU pool key is malformed");
2894
2893
  }
2895
- if (names.has(input.name) || addresses.has(input.address) || input.cpuPoolKey !== undefined && pools.has(input.cpuPoolKey)) {
2896
- throw new Error("platform genesis guest names, addresses and explicit CPU pools must be unique");
2894
+ if (names.has(input.name) || addresses.has(input.address)) {
2895
+ throw new Error("platform genesis guest names and addresses must be unique");
2897
2896
  }
2898
2897
  names.add(input.name);
2899
2898
  addresses.add(input.address);
2900
- if (input.cpuPoolKey !== undefined)
2901
- pools.add(input.cpuPoolKey);
2902
2899
  const physicalCores = integer(input.physicalCores, 1, 256, "physical core count");
2903
2900
  const vcpu = integer(input.vcpu, 1, 512, "vCPU count");
2904
2901
  if (vcpu < physicalCores)
@@ -5027,6 +5024,17 @@ function membersOfLinuxList(value, label) {
5027
5024
  throw new MetalProvisionError(`${label} list overlaps itself`);
5028
5025
  return members;
5029
5026
  }
5027
+ function physicalCoreGroups(pool) {
5028
+ const cpus = membersOfLinuxList(pool.cpus, "CPU");
5029
+ if (cpus.length % pool.physicalCores !== 0) {
5030
+ throw new MetalProvisionError("CPU pool threads must divide evenly across physical cores");
5031
+ }
5032
+ const threadsPerCore = cpus.length / pool.physicalCores;
5033
+ if (!Number.isInteger(threadsPerCore) || threadsPerCore < 1 || threadsPerCore > 8) {
5034
+ throw new MetalProvisionError("invalid CPU pool threads-per-core topology");
5035
+ }
5036
+ return Array.from({ length: pool.physicalCores }, (_, core) => Array.from({ length: threadsPerCore }, (_2, thread) => cpus[core + thread * pool.physicalCores]));
5037
+ }
5030
5038
  function validateMetalProfile(profile) {
5031
5039
  if (!SAFE_NAME2.test(profile.volumeGroup))
5032
5040
  throw new MetalProvisionError("invalid volume group");
@@ -5075,6 +5083,7 @@ function validateMetalProfile(profile) {
5075
5083
  if (!Number.isInteger(pool.physicalCores) || pool.physicalCores < 1 || pool.physicalCores > cpus.length) {
5076
5084
  throw new MetalProvisionError("invalid CPU pool physical-core count");
5077
5085
  }
5086
+ physicalCoreGroups(pool);
5078
5087
  for (const cpu of cpus) {
5079
5088
  if (assigned.has(cpu))
5080
5089
  throw new MetalProvisionError("CPU pools overlap");
@@ -584,7 +584,6 @@ function validatePlatformGenesisGuests(guests, expectedCount = 3) {
584
584
  }
585
585
  const names = new Set;
586
586
  const addresses = new Set;
587
- const pools = new Set;
588
587
  return guests.map((input) => {
589
588
  if (!input || typeof input !== "object" || !SAFE_NAME.test(input.name)) {
590
589
  throw new Error("platform genesis guest name is malformed");
@@ -597,13 +596,11 @@ function validatePlatformGenesisGuests(guests, expectedCount = 3) {
597
596
  if (input.cpuPoolKey !== undefined && !SAFE_POOL.test(input.cpuPoolKey)) {
598
597
  throw new Error("platform genesis CPU pool key is malformed");
599
598
  }
600
- if (names.has(input.name) || addresses.has(input.address) || input.cpuPoolKey !== undefined && pools.has(input.cpuPoolKey)) {
601
- throw new Error("platform genesis guest names, addresses and explicit CPU pools must be unique");
599
+ if (names.has(input.name) || addresses.has(input.address)) {
600
+ throw new Error("platform genesis guest names and addresses must be unique");
602
601
  }
603
602
  names.add(input.name);
604
603
  addresses.add(input.address);
605
- if (input.cpuPoolKey !== undefined)
606
- pools.add(input.cpuPoolKey);
607
604
  const physicalCores = integer(input.physicalCores, 1, 256, "physical core count");
608
605
  const vcpu = integer(input.vcpu, 1, 512, "vCPU count");
609
606
  if (vcpu < physicalCores)
@@ -2647,7 +2647,7 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
2647
2647
  }
2648
2648
 
2649
2649
  // src/version.ts
2650
- var VERSION3 = "0.1.80";
2650
+ var VERSION3 = "0.1.82";
2651
2651
 
2652
2652
  // src/egress-policy.ts
2653
2653
  import { realpathSync as realpathSync3 } from "node:fs";
@@ -3680,7 +3680,6 @@ function validatePlatformGenesisGuests(guests, expectedCount = 3) {
3680
3680
  }
3681
3681
  const names = new Set;
3682
3682
  const addresses = new Set;
3683
- const pools = new Set;
3684
3683
  return guests.map((input) => {
3685
3684
  if (!input || typeof input !== "object" || !SAFE_NAME.test(input.name)) {
3686
3685
  throw new Error("platform genesis guest name is malformed");
@@ -3693,13 +3692,11 @@ function validatePlatformGenesisGuests(guests, expectedCount = 3) {
3693
3692
  if (input.cpuPoolKey !== undefined && !SAFE_POOL.test(input.cpuPoolKey)) {
3694
3693
  throw new Error("platform genesis CPU pool key is malformed");
3695
3694
  }
3696
- if (names.has(input.name) || addresses.has(input.address) || input.cpuPoolKey !== undefined && pools.has(input.cpuPoolKey)) {
3697
- throw new Error("platform genesis guest names, addresses and explicit CPU pools must be unique");
3695
+ if (names.has(input.name) || addresses.has(input.address)) {
3696
+ throw new Error("platform genesis guest names and addresses must be unique");
3698
3697
  }
3699
3698
  names.add(input.name);
3700
3699
  addresses.add(input.address);
3701
- if (input.cpuPoolKey !== undefined)
3702
- pools.add(input.cpuPoolKey);
3703
3700
  const physicalCores = integer(input.physicalCores, 1, 256, "physical core count");
3704
3701
  const vcpu = integer(input.vcpu, 1, 512, "vCPU count");
3705
3702
  if (vcpu < physicalCores)
@@ -584,7 +584,6 @@ function validatePlatformGenesisGuests(guests, expectedCount = 3) {
584
584
  }
585
585
  const names = new Set;
586
586
  const addresses = new Set;
587
- const pools = new Set;
588
587
  return guests.map((input) => {
589
588
  if (!input || typeof input !== "object" || !SAFE_NAME.test(input.name)) {
590
589
  throw new Error("platform genesis guest name is malformed");
@@ -597,13 +596,11 @@ function validatePlatformGenesisGuests(guests, expectedCount = 3) {
597
596
  if (input.cpuPoolKey !== undefined && !SAFE_POOL.test(input.cpuPoolKey)) {
598
597
  throw new Error("platform genesis CPU pool key is malformed");
599
598
  }
600
- if (names.has(input.name) || addresses.has(input.address) || input.cpuPoolKey !== undefined && pools.has(input.cpuPoolKey)) {
601
- throw new Error("platform genesis guest names, addresses and explicit CPU pools must be unique");
599
+ if (names.has(input.name) || addresses.has(input.address)) {
600
+ throw new Error("platform genesis guest names and addresses must be unique");
602
601
  }
603
602
  names.add(input.name);
604
603
  addresses.add(input.address);
605
- if (input.cpuPoolKey !== undefined)
606
- pools.add(input.cpuPoolKey);
607
604
  const physicalCores = integer(input.physicalCores, 1, 256, "physical core count");
608
605
  const vcpu = integer(input.vcpu, 1, 512, "vCPU count");
609
606
  if (vcpu < physicalCores)
package/dist/provision.js CHANGED
@@ -2647,7 +2647,7 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
2647
2647
  }
2648
2648
 
2649
2649
  // src/version.ts
2650
- var VERSION3 = "0.1.80";
2650
+ var VERSION3 = "0.1.82";
2651
2651
 
2652
2652
  // src/egress-policy.ts
2653
2653
  import { realpathSync as realpathSync3 } from "node:fs";
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  /** One package version shared by both public binaries. Pinned to package.json by tests. */
2
- export declare const VERSION = "0.1.80";
2
+ export declare const VERSION = "0.1.82";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgezero/agent",
3
- "version": "0.1.80",
3
+ "version": "0.1.82",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "check": "tsc --noEmit",