@rallycry/conveyor-k3 0.1.11 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/vm-qemu.js CHANGED
@@ -7,13 +7,127 @@ import { dirname, join as join5 } from "path";
7
7
  import { fileURLToPath } from "url";
8
8
  import { createHash } from "crypto";
9
9
 
10
- // src/vm/public-web.ts
11
- import { resolve4 } from "dns/promises";
12
- import { networkInterfaces as networkInterfaces2 } from "os";
10
+ // src/vm/capacity.ts
11
+ var GIB = 1024 ** 3;
12
+ var MIB = 1024 ** 2;
13
+ var CONTROL_RELAY_SLOTS = 6;
14
+ var CAPACITY_BOUNDS = {
15
+ relaySlots: { min: 16, max: 192, flag: "relay-slots" },
16
+ relayMaxGiB: { min: 1, max: 16, flag: "relay-max-gib" },
17
+ diskMiBps: { min: 25, max: 2e3, flag: "disk-mbps" },
18
+ diskIops: { min: 500, max: 5e4, flag: "disk-iops" }
19
+ };
20
+ var LEGACY_CAPACITY_LIMITS = Object.freeze({
21
+ schema: 1,
22
+ profile: "default",
23
+ relaySlots: 16,
24
+ reservedRelaySlots: 0,
25
+ relayMaxBytes: GIB,
26
+ diskBytesPerSecond: 100 * MIB,
27
+ diskIops: 2e3
28
+ });
29
+ var KEYS = Object.keys(CAPACITY_BOUNDS);
30
+ var PROFILES = /* @__PURE__ */ new Set(["default", "server", "custom"]);
31
+ function parseCapacityLimits(value) {
32
+ if (value === void 0) return { ...LEGACY_CAPACITY_LIMITS };
33
+ const limits = typeof value === "object" && value !== null ? value : {};
34
+ const within = (key, min, max) => Number.isSafeInteger(limits[key]) && limits[key] >= min && limits[key] <= max;
35
+ const bounds = CAPACITY_BOUNDS;
36
+ if (limits.schema !== 1 || !PROFILES.has(limits.profile) || !within("relaySlots", bounds.relaySlots.min, bounds.relaySlots.max) || limits.reservedRelaySlots !== 0 && limits.reservedRelaySlots !== CONTROL_RELAY_SLOTS || !within("relayMaxBytes", bounds.relayMaxGiB.min * GIB, bounds.relayMaxGiB.max * GIB) || !within("diskBytesPerSecond", bounds.diskMiBps.min * MIB, bounds.diskMiBps.max * MIB) || !within("diskIops", bounds.diskIops.min, bounds.diskIops.max))
37
+ throw new Error("The VM capacity limits in the host network policy are invalid.");
38
+ return {
39
+ schema: 1,
40
+ profile: limits.profile,
41
+ relaySlots: limits.relaySlots,
42
+ reservedRelaySlots: limits.reservedRelaySlots,
43
+ relayMaxBytes: limits.relayMaxBytes,
44
+ diskBytesPerSecond: limits.diskBytesPerSecond,
45
+ diskIops: limits.diskIops
46
+ };
47
+ }
48
+ function guardProcessLimit(relaySlots) {
49
+ return 48 + relaySlots;
50
+ }
51
+
52
+ // src/vm/instance-name.ts
53
+ var VM_NAME = "conveyor";
54
+ var DEFAULT_INSTANCE = "default";
55
+ var NAMED_INSTANCE = /^(?=.{1,12}$)[a-z](?:-?[a-z0-9])*$/;
56
+ function isNamedInstance(value) {
57
+ return typeof value === "string" && NAMED_INSTANCE.test(value) && value !== DEFAULT_INSTANCE && value !== VM_NAME;
58
+ }
59
+ function vmNameFor(instance) {
60
+ return instance === DEFAULT_INSTANCE ? VM_NAME : instance;
61
+ }
62
+ function stateInstance(value) {
63
+ if (value === void 0) return DEFAULT_INSTANCE;
64
+ if (!isNamedInstance(value)) throw new Error("Invalid VM instance in host state");
65
+ return value;
66
+ }
13
67
 
14
68
  // src/vm/network.ts
15
69
  import { networkInterfaces } from "os";
16
70
  import { resolve4 as systemResolve4, resolveSrv as systemResolveSrv } from "dns/promises";
71
+
72
+ // src/vm/endpoints.ts
73
+ var DEFAULT_ENDPOINTS = [
74
+ "archive.ubuntu.com",
75
+ "security.ubuntu.com",
76
+ "ports.ubuntu.com",
77
+ "registry.npmjs.org",
78
+ // Font Awesome Pro tarballs through Conveyor's Artifact Registry cache.
79
+ "us-central1-npm.pkg.dev",
80
+ // Tiptap Pro packages, which every claudespace bake installs.
81
+ "registry.tiptap.dev",
82
+ // The Python venv a pod builds at runtime.
83
+ "pypi.org",
84
+ "files.pythonhosted.org",
85
+ "bun.sh",
86
+ "get.k3s.io",
87
+ "update.k3s.io",
88
+ "github.com",
89
+ "api.github.com",
90
+ "raw.githubusercontent.com",
91
+ "release-assets.githubusercontent.com",
92
+ "objects.githubusercontent.com",
93
+ "ghcr.io",
94
+ "pkg-containers.githubusercontent.com",
95
+ // The crane image that shadow bakes' state-bake and publish Jobs run. gcr.io
96
+ // serves its blobs from the same host (/artifacts-downloads), on Google's
97
+ // shared frontend addresses like us-central1-npm.pkg.dev.
98
+ "gcr.io",
99
+ // The first shadow bake on a VM builds the toolchain base inside it; base
100
+ // reuse never crosses machines. These are the hosts that build downloads
101
+ // from. Several sit on shared CDN addresses.
102
+ // Plain HTTP too: the first apt-get runs before ca-certificates exists.
103
+ "deb.debian.org",
104
+ "www.postgresql.org",
105
+ "apt.postgresql.org",
106
+ "cli.github.com",
107
+ "johnvansickle.com",
108
+ "astral.sh",
109
+ // uv downloads CPython from here.
110
+ "releases.astral.sh",
111
+ "opencode.ai",
112
+ "cdn.playwright.dev",
113
+ "playwright.download.prss.microsoft.com",
114
+ "registry-1.docker.io",
115
+ "auth.docker.io",
116
+ "production.cloudflare.docker.com",
117
+ "production.cloudfront.docker.com",
118
+ "api.cloudflare.com",
119
+ "conveyor.rallycryapp.com",
120
+ "conveyor-api.rallycryapp.com",
121
+ "api.conveyor.rallycryapp.com",
122
+ "api.anthropic.com",
123
+ "api.openai.com",
124
+ "chatgpt.com"
125
+ ].map((host) => ({
126
+ host,
127
+ ports: host.endsWith(".ubuntu.com") || host === "deb.debian.org" ? [80, 443] : [443]
128
+ }));
129
+
130
+ // src/vm/network.ts
17
131
  var WORKER_SUBNET = "192.168.5.0/24";
18
132
  var WORKER_IP = "192.168.5.15";
19
133
  var MAX_NETWORK_FORWARDS = 100;
@@ -55,6 +169,8 @@ function isGloballyRoutableIPv4(address) {
55
169
  }
56
170
 
57
171
  // src/vm/public-web.ts
172
+ import { resolve4 } from "dns/promises";
173
+ import { networkInterfaces as networkInterfaces2 } from "os";
58
174
  var PUBLIC_WEB_IP = "192.168.5.250";
59
175
  var PUBLIC_WEB_PORT = 3128;
60
176
  var PUBLIC_WEB_URL = `http://${PUBLIC_WEB_IP}:${PUBLIC_WEB_PORT}`;
@@ -146,6 +262,65 @@ function proxyUrl(target, connect) {
146
262
  return { url, port };
147
263
  }
148
264
 
265
+ // src/vm/qemu-forwards.ts
266
+ var ENDPOINT_IP = /^192\.168\.5\.(?:1\d\d)$/;
267
+ var CONTROL_ALIAS_IP = /^192\.168\.5\.24[0-7]$/;
268
+ var PINNED_IPV4 = /^(?:[1-9]?\d|1\d\d|2[0-4]\d|25[0-5])(?:\.(?:[1-9]?\d|1\d\d|2[0-4]\d|25[0-5])){3}$/;
269
+ function assertPort(port) {
270
+ if (!Number.isInteger(port) || port < 1 || port > 65535)
271
+ throw new Error("Invalid guest forwarding port");
272
+ }
273
+ function validateForwards(forwards) {
274
+ const seen = /* @__PURE__ */ new Set();
275
+ for (const forward of forwards) {
276
+ assertPort(forward.port);
277
+ const key = `${forward.guestIp}:${forward.port}`;
278
+ if (seen.has(key)) throw new Error("Duplicate QEMU guest forwarding rule");
279
+ seen.add(key);
280
+ if (!isGloballyRoutableIPv4(forward.address) || !PINNED_IPV4.test(forward.address))
281
+ throw new Error("Invalid pinned forwarding address");
282
+ }
283
+ }
284
+ function forwardsOf(entries, guestIps, control) {
285
+ return entries.flatMap((host) => {
286
+ if (!guestIps.test(host.guestIp) || control && host.reserve !== "control")
287
+ throw new Error("Invalid pinned forwarding address");
288
+ return host.ports.map((port) => ({
289
+ guestIp: host.guestIp,
290
+ address: host.address,
291
+ port,
292
+ ...host.reserve === "control" ? { reserve: "control" } : {}
293
+ }));
294
+ });
295
+ }
296
+ function qemuForwards(network) {
297
+ const forwards = [
298
+ ...forwardsOf(network.hosts, ENDPOINT_IP, false),
299
+ ...forwardsOf(network.control ?? [], CONTROL_ALIAS_IP, true)
300
+ ];
301
+ if (forwards.length > MAX_NETWORK_FORWARDS) throw new Error("Too many QEMU forwarding rules");
302
+ validateForwards(forwards);
303
+ return forwards;
304
+ }
305
+ function restrictedUsernet(network, forwards, ncPath) {
306
+ const guestForwards = forwards.map(
307
+ (forward) => `guestfwd=tcp:${forward.guestIp}:${forward.port}-cmd:${ncPath} -4 -n -w 60 ${forward.address} ${forward.port}${forward.reserve === "control" ? " control" : ""}`
308
+ );
309
+ return [
310
+ "user",
311
+ "id=net0",
312
+ `net=${WORKER_SUBNET}`,
313
+ `dhcpstart=${WORKER_IP}`,
314
+ "restrict=on",
315
+ "ipv6=off",
316
+ `hostfwd=tcp:127.0.0.1:${network.sshPort}-:22`,
317
+ ...guestForwards,
318
+ ...network.publicWeb === true ? [
319
+ `guestfwd=tcp:${PUBLIC_WEB_IP}:${PUBLIC_WEB_PORT}-cmd:${ncPath} -4 -n -w 60 public-web ${PUBLIC_WEB_PORT}`
320
+ ] : []
321
+ ].join(",");
322
+ }
323
+
149
324
  // src/vm/qemu-policy.ts
150
325
  var ALLOWED_PROBES = new Set(
151
326
  [
@@ -199,9 +374,7 @@ var REQUIRED_CONTROLS = [
199
374
  "drive:cidata"
200
375
  ];
201
376
  var DISALLOWED_CONTROLS = /* @__PURE__ */ new Set(["-sandbox", "-no-user-config", "-nodefaults"]);
202
- var BOOT_DISK_BPS = "104857600";
203
- var BOOT_DISK_IOPS = "2000";
204
- function assertPort(port, field) {
377
+ function assertPort2(port, field) {
205
378
  if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error(`Invalid ${field}`);
206
379
  }
207
380
  function isProbe(args) {
@@ -322,10 +495,11 @@ function validateDrive(value, context) {
322
495
  }
323
496
  assertFields(fields, { file: /.+/, if: "none", discard: "on", id: "boot-disk" }, "boot drive");
324
497
  assertInstancePath(path, "disk", context);
325
- return {
326
- kind: "boot",
327
- value: `${value},bps=${BOOT_DISK_BPS},iops=${BOOT_DISK_IOPS}`
498
+ const disk = context.disk ?? {
499
+ bps: LEGACY_CAPACITY_LIMITS.diskBytesPerSecond,
500
+ iops: LEGACY_CAPACITY_LIMITS.diskIops
328
501
  };
502
+ return { kind: "boot", value: `${value},bps=${disk.bps},iops=${disk.iops}` };
329
503
  }
330
504
  if (fields.get("id") === "cdrom0") {
331
505
  assertFields(
@@ -370,38 +544,6 @@ function parseOriginalUsernet(value, sshPort) {
370
544
  "Lima QEMU user network"
371
545
  );
372
546
  }
373
- function restrictedUsernet(network, forwards, ncPath) {
374
- const guestForwards = forwards.map(
375
- (forward) => `guestfwd=tcp:${forward.guestIp}:${forward.port}-cmd:${ncPath} -4 -n -w 60 ${forward.address} ${forward.port}`
376
- );
377
- return [
378
- "user",
379
- "id=net0",
380
- `net=${WORKER_SUBNET}`,
381
- `dhcpstart=${WORKER_IP}`,
382
- "restrict=on",
383
- "ipv6=off",
384
- `hostfwd=tcp:127.0.0.1:${network.sshPort}-:22`,
385
- ...guestForwards,
386
- ...network.publicWeb === true ? [
387
- `guestfwd=tcp:${PUBLIC_WEB_IP}:${PUBLIC_WEB_PORT}-cmd:${ncPath} -4 -n -w 60 public-web ${PUBLIC_WEB_PORT}`
388
- ] : []
389
- ].join(",");
390
- }
391
- function validateForwards(forwards) {
392
- const seen = /* @__PURE__ */ new Set();
393
- for (const forward of forwards) {
394
- assertPort(forward.port, "guest forwarding port");
395
- const key = `${forward.guestIp}:${forward.port}`;
396
- if (seen.has(key)) throw new Error("Duplicate QEMU guest forwarding rule");
397
- seen.add(key);
398
- if (!/^192\.168\.5\.(?:1\d\d)$/.test(forward.guestIp) || !isGloballyRoutableIPv4(forward.address) || !/^(?:[1-9]?\d|1\d\d|2[0-4]\d|25[0-5])(?:\.(?:[1-9]?\d|1\d\d|2[0-4]\d|25[0-5])){3}$/.test(
399
- forward.address
400
- )) {
401
- throw new Error("Invalid pinned forwarding address");
402
- }
403
- }
404
- }
405
547
  function fixedControl(control, expected) {
406
548
  return (value) => {
407
549
  if (value !== expected) throw new Error(`Unexpected QEMU ${control}`);
@@ -434,7 +576,7 @@ var SIMPLE_VALIDATORS = {
434
576
  "-parallel": fixedControl("-parallel", "none"),
435
577
  "-serial": fixedControl("serial", "chardev:char-serial"),
436
578
  "-qmp": fixedControl("qmp", "chardev:char-qmp"),
437
- "-name": fixedControl("name", "lima-conveyor"),
579
+ "-name": (value, { vmName = VM_NAME }) => fixedControl("name", `lima-${vmName}`)(value, {}),
438
580
  "-pidfile": (value, context) => {
439
581
  assertInstancePath(value, "qemu.pid", context);
440
582
  return { value, control: "pidfile" };
@@ -505,15 +647,16 @@ function assertCompleteProfile(state) {
505
647
  throw new Error("QEMU PCI serial device and chardev must appear together");
506
648
  }
507
649
  }
508
- function rewriteQemuArgs(args, network, ncPath = "/usr/bin/nc", context = {}) {
650
+ function rewriteQemuArgs(args, network, ncPath = "/usr/bin/nc", hostContext = {}) {
509
651
  if (isProbe(args)) return [...args];
510
- assertPort(network.sshPort, "SSH port");
652
+ assertPort2(network.sshPort, "SSH port");
511
653
  if (!TRUSTED_NC_PATH.test(ncPath)) throw new Error("Invalid netcat path");
512
- const forwards = network.hosts.flatMap(
513
- (host) => host.ports.map((port) => ({ guestIp: host.guestIp, address: host.address, port }))
514
- );
515
- if (forwards.length > MAX_NETWORK_FORWARDS) throw new Error("Too many QEMU forwarding rules");
516
- validateForwards(forwards);
654
+ const forwards = qemuForwards(network);
655
+ const limits = parseCapacityLimits(network.limits);
656
+ const context = {
657
+ ...hostContext,
658
+ disk: { bps: limits.diskBytesPerSecond, iops: limits.diskIops }
659
+ };
517
660
  const output = [];
518
661
  const state = {
519
662
  controls: /* @__PURE__ */ new Set(),
@@ -569,12 +712,12 @@ import { execFileSync } from "child_process";
569
712
  import { lstatSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
570
713
  import { join } from "path";
571
714
  import { Socket } from "net";
572
- var MAX_RELAYS = 16;
573
- var MAX_RELAY_BYTES = 1024 * 1024 * 1024;
715
+ var SLOT_CEILING = CAPACITY_BOUNDS.relaySlots.max;
574
716
  var RELAY_CONNECT_MS = 6e4;
575
717
  var RELAY_IDLE_MS = 15 * 6e4;
576
718
  var GUARD_FRESH_MS = 6e3;
577
719
  var RELAY_PREFIX = "--conveyor-vm-relay";
720
+ var RELAY_FLAGS = ["-4", "-n", "-w", "60"];
578
721
  function validPid(value) {
579
722
  return Number.isInteger(value) && value > 1 && value <= 4194304;
580
723
  }
@@ -625,11 +768,13 @@ function readNetwork(root) {
625
768
  if (typeof parsed !== "object" || parsed === null)
626
769
  throw new Error("VM relay network policy is invalid");
627
770
  const network = parsed;
628
- if (!Array.isArray(network.hosts)) throw new Error("VM relay network policy is invalid");
629
- return network;
771
+ if (!Array.isArray(network.hosts) || network.control !== void 0 && !Array.isArray(network.control))
772
+ throw new Error("VM relay network policy is invalid");
773
+ return { ...network, limits: parseCapacityLimits(network.limits) };
630
774
  }
631
- function assertApprovedDestination(root, invocation) {
632
- const allowed = readNetwork(root).hosts.some(
775
+ function assertApprovedDestination(network, invocation) {
776
+ const entries = invocation.reserve === "control" ? [...network.hosts, ...network.control ?? []].filter((host) => host?.reserve === "control") : network.hosts;
777
+ const allowed = entries.some(
633
778
  (host) => host && host.address === invocation.address && Array.isArray(host.ports) && host.ports.includes(invocation.port)
634
779
  );
635
780
  if (!allowed) throw new Error("VM relay destination is not in the host network policy");
@@ -640,9 +785,10 @@ function recordPath(root, pid) {
640
785
  function slotPath(root, slot) {
641
786
  return join(relayDirectory(root), `slot-${slot}`);
642
787
  }
643
- function claimSlot(root) {
788
+ function claimSlot(root, limits, reserve) {
644
789
  safeDirectory(relayDirectory(root));
645
- for (let slot = 0; slot < MAX_RELAYS; slot += 1) {
790
+ const first = reserve === "control" ? 0 : limits.reservedRelaySlots;
791
+ for (let slot = first; slot < limits.relaySlots; slot += 1) {
646
792
  try {
647
793
  mkdirSync(slotPath(root, slot), { mode: 448 });
648
794
  return slot;
@@ -650,10 +796,10 @@ function claimSlot(root) {
650
796
  if (error.code !== "EEXIST") throw error;
651
797
  }
652
798
  }
653
- throw new Error(`VM relay limit reached (${MAX_RELAYS})`);
799
+ throw new Error(`VM relay limit reached (${limits.relaySlots})`);
654
800
  }
655
- function registerRelay(root, runId) {
656
- const slot = claimSlot(root);
801
+ function registerRelay(root, runId, limits, reserve) {
802
+ const slot = claimSlot(root, limits, reserve);
657
803
  const record = {
658
804
  schema: 1,
659
805
  pid: process.pid,
@@ -683,15 +829,20 @@ function unregisterRelay(root, record) {
683
829
  }
684
830
  function parseRelayInvocation(args) {
685
831
  if (args[0] !== RELAY_PREFIX) return null;
686
- if (args.length !== 7 || args[1] !== "-4" || args[2] !== "-n" || args[3] !== "-w" || args[4] !== "60") {
832
+ const control = args.length === 8 && args[7] === "control";
833
+ if (args.length !== 7 && !control || RELAY_FLAGS.some((flag, index) => args[index + 1] !== flag)) {
687
834
  throw new Error("Invalid VM relay invocation");
688
835
  }
689
836
  const address = args[5] ?? "";
690
837
  const port = Number(args[6]);
691
- if (!(isGloballyRoutableIPv4(address) || address === "public-web" && port === PUBLIC_WEB_PORT) || !Number.isInteger(port) || port < 1 || port > 65535) {
838
+ const publicWeb = !control && address === "public-web" && port === PUBLIC_WEB_PORT;
839
+ if (!(isGloballyRoutableIPv4(address) || publicWeb) || !validPort(port)) {
692
840
  throw new Error("Invalid VM relay destination");
693
841
  }
694
- return { address, port };
842
+ return { address, port, ...control ? { reserve: "control" } : {} };
843
+ }
844
+ function validPort(port) {
845
+ return Number.isInteger(port) && port >= 1 && port <= 65535;
695
846
  }
696
847
  function parsePs(output) {
697
848
  const result = /* @__PURE__ */ new Map();
@@ -712,7 +863,7 @@ function parsePs(output) {
712
863
  }
713
864
  function listedRecords(root, runId) {
714
865
  const records = [];
715
- for (let slot = 0; slot < MAX_RELAYS; slot += 1) {
866
+ for (let slot = 0; slot < SLOT_CEILING; slot += 1) {
716
867
  try {
717
868
  const record = JSON.parse(
718
869
  readFileSync(join(slotPath(root, slot), "owner.json"), "utf8")
@@ -769,11 +920,12 @@ function runRelay(root, invocation, io) {
769
920
  }
770
921
  async function runRelayNow(root, invocation, io) {
771
922
  const guard = readLiveGuard(root);
923
+ const network = readNetwork(root);
772
924
  const publicWeb = invocation.address === "public-web" && invocation.port === PUBLIC_WEB_PORT;
773
925
  if (publicWeb) {
774
- if (readNetwork(root).publicWeb !== true) throw new Error("Public web access is not enabled");
775
- } else assertApprovedDestination(root, invocation);
776
- const record = registerRelay(root, guard.runId);
926
+ if (network.publicWeb !== true) throw new Error("Public web access is not enabled");
927
+ } else assertApprovedDestination(network, invocation);
928
+ const record = registerRelay(root, guard.runId, network.limits, invocation.reserve);
777
929
  const streams = io ?? { socket: new Socket(), input: process.stdin, output: process.stdout };
778
930
  let connectedResponse = false;
779
931
  if (publicWeb) {
@@ -804,9 +956,13 @@ async function runRelayNow(root, invocation, io) {
804
956
  releaseHandshake();
805
957
  }
806
958
  }
807
- return transportRelay(root, invocation, streams, record, connectedResponse);
959
+ return transportRelay(root, invocation, streams, record, {
960
+ connectedResponse,
961
+ maxBytes: network.limits.relayMaxBytes
962
+ });
808
963
  }
809
- function transportRelay(root, invocation, streams, record, connectedResponse) {
964
+ function transportRelay(root, invocation, streams, record, options) {
965
+ const { connectedResponse, maxBytes } = options;
810
966
  return new Promise((resolve) => {
811
967
  let finished = false;
812
968
  let sent = 0;
@@ -840,7 +996,7 @@ function transportRelay(root, invocation, streams, record, connectedResponse) {
840
996
  };
841
997
  process.once("exit", cleanup);
842
998
  for (const signal of signals) process.once(signal, terminated);
843
- const exceedsLimit = (total) => total > MAX_RELAY_BYTES;
999
+ const exceedsLimit = (total) => total > maxBytes;
844
1000
  socket.setTimeout(RELAY_IDLE_MS, () => finish(new Error("VM relay idle timeout")));
845
1001
  socket.once("connect", () => {
846
1002
  if (connectTimer) clearTimeout(connectTimer);
@@ -899,25 +1055,30 @@ function handshakeExitCleanup(root, record) {
899
1055
  }
900
1056
 
901
1057
  // src/vm/memory-budget.ts
902
- function vmHostMemoryGiB(guestGiB) {
903
- return guestGiB + Math.max(1, guestGiB / 4);
1058
+ var RELAY_RSS_ALLOWANCE_MIB = 128;
1059
+ var BASE_RELAY_SLOTS = 16;
1060
+ function relayAllowanceGiB(relaySlots = BASE_RELAY_SLOTS) {
1061
+ return Math.max(0, relaySlots - BASE_RELAY_SLOTS) * RELAY_RSS_ALLOWANCE_MIB / 1024;
1062
+ }
1063
+ function vmHostMemoryGiB(guestGiB, relaySlots = BASE_RELAY_SLOTS) {
1064
+ return guestGiB + Math.max(1, guestGiB / 4) + relayAllowanceGiB(relaySlots);
904
1065
  }
905
1066
 
906
1067
  // src/vm/resource-guard.ts
907
- var GIB = 1024 ** 3;
1068
+ var GIB2 = 1024 ** 3;
908
1069
  var GUARD_INTERVAL_MS = 2e3;
909
- function guardLimits(resources) {
1070
+ function guardLimits(resources, capacity = LEGACY_CAPACITY_LIMITS) {
910
1071
  if (!Number.isInteger(resources.memoryGiB) || resources.memoryGiB < 2 || resources.memoryGiB > 1024 || !Number.isInteger(resources.diskGiB) || resources.diskGiB < 20 || resources.diskGiB > 512) {
911
1072
  throw new Error("Invalid resource limits in host state");
912
1073
  }
913
1074
  return {
914
- minFreeBytes: 10 * GIB,
1075
+ minFreeBytes: 10 * GIB2,
915
1076
  // A 24 GiB Mac guest exceeded 27 GiB during image unpacking. Allow 25%
916
1077
  // overhead (at least 1 GiB); admission reserves host RAM using this same cap.
917
- maxRssKiB: vmHostMemoryGiB(resources.memoryGiB) * 1024 ** 2,
918
- maxProcesses: 64,
1078
+ maxRssKiB: vmHostMemoryGiB(resources.memoryGiB, capacity.relaySlots) * 1024 ** 2,
1079
+ maxProcesses: guardProcessLimit(capacity.relaySlots),
919
1080
  maxOutputBytes: 1024 * 1024,
920
- maxDiskBytes: (resources.diskGiB + 1) * GIB,
1081
+ maxDiskBytes: (resources.diskGiB + 1) * GIB2,
921
1082
  maxLogBytes: 8 * 1024 ** 2
922
1083
  };
923
1084
  }
@@ -969,20 +1130,19 @@ function hostPs(args) {
969
1130
  env: { PATH: "/usr/bin:/bin", LANG: "C" }
970
1131
  });
971
1132
  }
972
- function sampleResources(root, qemuPid, runId) {
1133
+ function sampleResources(root, instanceDir, qemuPid, runId, ps = hostPs) {
973
1134
  const fs = statfsSync(root);
974
- const instance = join2(root, "lima", "conveyor");
975
- const processes = parseProcessGroup(hostPs(["-axo", "pgid=,pid=,rss="]), qemuPid);
976
- const relays = sampleRelayProcesses(root, runId, hostPs(["-axo", "pid=,pgid=,rss=,command="]));
1135
+ const processes = parseProcessGroup(ps(["-axo", "pgid=,pid=,rss="]), qemuPid);
1136
+ const relays = sampleRelayProcesses(root, runId, ps(["-axo", "pid=,pgid=,rss=,command="]));
977
1137
  return {
978
1138
  freeBytes: fs.bavail * fs.bsize,
979
1139
  rssKiB: processes.rssKiB + relays.reduce((total, relay) => total + relay.rssKiB, 0),
980
1140
  processes: processes.processes + relays.length,
981
1141
  relayProcesses: relays.length,
982
- diskBytes: regularFileSize(join2(instance, "disk")),
1142
+ diskBytes: regularFileSize(join2(instanceDir, "disk")),
983
1143
  largestLogBytes: Math.max(
984
1144
  ...["ha.stdout.log", "ha.stderr.log", "serial.log", "serialp.log", "serialv.log"].map(
985
- (file) => regularFileSize(join2(instance, file))
1145
+ (file) => regularFileSize(join2(instanceDir, file))
986
1146
  )
987
1147
  )
988
1148
  };
@@ -997,7 +1157,7 @@ function writeGuardEvidence(root, evidence) {
997
1157
  rmSync2(temporary, { force: true });
998
1158
  }
999
1159
  }
1000
- function startResourceGuard(root, identity, limits, stop) {
1160
+ function startResourceGuard(root, identity, limits, stop, instanceDir) {
1001
1161
  const evidence = {
1002
1162
  schema: 1,
1003
1163
  ...identity,
@@ -1022,7 +1182,7 @@ function startResourceGuard(root, identity, limits, stop) {
1022
1182
  function check() {
1023
1183
  if (finished) return;
1024
1184
  try {
1025
- evidence.observed = sampleResources(root, identity.qemuPid, identity.runId);
1185
+ evidence.observed = sampleResources(root, instanceDir, identity.qemuPid, identity.runId);
1026
1186
  const violation = resourceViolation(evidence.observed, limits);
1027
1187
  if (violation) throw new Error(violation);
1028
1188
  evidence.lastCheckedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -1225,9 +1385,9 @@ function attachBoundedOutput(child, limit, terminate) {
1225
1385
  child.stdout?.on("data", consume);
1226
1386
  child.stderr?.on("data", consume);
1227
1387
  }
1228
- function runGuardedQemu(root, qemu, args, networkHash, resources) {
1388
+ function runGuardedQemu(root, qemu, args, networkHash, resources, instanceDir, capacity) {
1229
1389
  const runId = startIdentity(root);
1230
- const limits = guardLimits(resources);
1390
+ const limits = guardLimits(resources, parseCapacityLimits(capacity));
1231
1391
  const hardenedArgs = [...args, "-no-user-config", "-nodefaults"];
1232
1392
  const child = spawn2(qemu, hardenedArgs, {
1233
1393
  stdio: ["ignore", "pipe", "pipe"],
@@ -1285,7 +1445,7 @@ function runGuardedQemu(root, qemu, args, networkHash, resources) {
1285
1445
  JSON.stringify({ ...identity, args: hardenedArgs, startedAt: (/* @__PURE__ */ new Date()).toISOString() }),
1286
1446
  { mode: 384 }
1287
1447
  );
1288
- guard = startResourceGuard(root, identity, limits, terminate);
1448
+ guard = startResourceGuard(root, identity, limits, terminate, instanceDir);
1289
1449
  } catch {
1290
1450
  terminate("failed to record or start host resource supervision");
1291
1451
  }
@@ -1316,6 +1476,8 @@ function main() {
1316
1476
  if (state.schema !== 1 || !state.qemu.startsWith("/") || state.qemu.includes("wrapper")) {
1317
1477
  throw new Error("Invalid QEMU host state");
1318
1478
  }
1479
+ const vmName = vmNameFor(stateInstance(state.instance));
1480
+ const instanceDir = join5(root, "lima", vmName);
1319
1481
  const networkText = readFileSync4(join5(root, "network.json"), "utf8");
1320
1482
  const network = JSON.parse(networkText);
1321
1483
  const args = process.argv.slice(2);
@@ -1324,7 +1486,8 @@ function main() {
1324
1486
  original?.match(/hostfwd=tcp:127\.0\.0\.1:(\d+)-(?:192\.168\.5\.15)?:22/)?.[1] ?? 0
1325
1487
  );
1326
1488
  const rewritten = rewriteQemuArgs(args, { ...network, sshPort }, join5(root, "qemu-relay"), {
1327
- instanceDir: join5(root, "lima", "conveyor")
1489
+ instanceDir,
1490
+ vmName
1328
1491
  });
1329
1492
  if (original) {
1330
1493
  const expected = process.platform === "darwin" ? "hvf" : "kvm";
@@ -1336,7 +1499,9 @@ function main() {
1336
1499
  state.qemu,
1337
1500
  rewritten,
1338
1501
  createHash("sha256").update(networkText).digest("hex"),
1339
- state.resources
1502
+ state.resources,
1503
+ instanceDir,
1504
+ network.limits
1340
1505
  );
1341
1506
  return;
1342
1507
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rallycry/conveyor-k3",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "Managed personal compute for Conveyor. Installs and maintains a k3s cluster that runs your own card builds, kept converged on Conveyor's desired state.",
5
5
  "keywords": [
6
6
  "cloudflared",