@rallycry/conveyor-k3 0.1.10 → 0.1.12

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,9 +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/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
+ }
67
+
10
68
  // src/vm/network.ts
11
69
  import { networkInterfaces } from "os";
12
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
13
131
  var WORKER_SUBNET = "192.168.5.0/24";
14
132
  var WORKER_IP = "192.168.5.15";
15
133
  var MAX_NETWORK_FORWARDS = 100;
@@ -50,6 +168,159 @@ function isGloballyRoutableIPv4(address) {
50
168
  ].some(([base, mask]) => inCidr(value, parseIPv4(base), mask));
51
169
  }
52
170
 
171
+ // src/vm/public-web.ts
172
+ import { resolve4 } from "dns/promises";
173
+ import { networkInterfaces as networkInterfaces2 } from "os";
174
+ var PUBLIC_WEB_IP = "192.168.5.250";
175
+ var PUBLIC_WEB_PORT = 3128;
176
+ var PUBLIC_WEB_URL = `http://${PUBLIC_WEB_IP}:${PUBLIC_WEB_PORT}`;
177
+ async function publicWebAddress(host, lookup = resolve4, interfaces = networkInterfaces2) {
178
+ if (!/^[a-z0-9.-]+$/i.test(host) || host.length > 253)
179
+ throw new Error("Invalid public destination");
180
+ const own = new Set(
181
+ Object.values(interfaces()).flatMap((items) => items?.map((i) => i.address) ?? [])
182
+ );
183
+ const answers = /^\d+\.\d+\.\d+\.\d+$/.test(host) ? [host] : await lookup(host);
184
+ if (!answers.length || answers.some((ip) => !isGloballyRoutableIPv4(ip) || own.has(ip)))
185
+ throw new Error("Host, LAN and reserved destinations are blocked");
186
+ const address = answers[0];
187
+ if (!address) throw new Error("No public DNS answer");
188
+ return address;
189
+ }
190
+ function publicWebRequest(header) {
191
+ const lines = header.split("\r\n");
192
+ const match = /^(CONNECT|GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS) ([^\s]+) HTTP\/1\.[01]$/.exec(
193
+ lines.shift() ?? ""
194
+ );
195
+ if (!match) throw new Error("Unsupported proxy request");
196
+ const connect = match[1] === "CONNECT";
197
+ const target = match[2] ?? "";
198
+ if (connect && !/^[a-z0-9.-]+:(80|443)$/i.test(target))
199
+ throw new Error("Invalid CONNECT destination");
200
+ const { url, port } = proxyUrl(target, connect);
201
+ for (const line of lines.filter(Boolean)) {
202
+ if (!/^[!#$%&'*+.^_`|~0-9A-Za-z-]+:[\t\x20-\x7e]*$/.test(line))
203
+ throw new Error("Invalid proxy header");
204
+ }
205
+ const headers = lines.filter(
206
+ (line) => line && !/^(host|connection|proxy-connection|proxy-authorization):/i.test(line)
207
+ );
208
+ return {
209
+ host: url.hostname,
210
+ port,
211
+ connect,
212
+ forward: connect ? "" : `${match[1]} ${url.pathname}${url.search} HTTP/1.1\r
213
+ Host: ${url.host}\r
214
+ Connection: close\r
215
+ ${headers.length ? headers.join("\r\n") + "\r\n" : ""}\r
216
+ `
217
+ };
218
+ }
219
+ function readPublicWebRequest(input) {
220
+ return new Promise((resolve, reject) => {
221
+ let data = Buffer.alloc(0);
222
+ const cleanup = () => {
223
+ clearTimeout(timer);
224
+ input.pause();
225
+ input.removeListener("data", receive);
226
+ input.removeListener("end", closed);
227
+ input.removeListener("error", failed);
228
+ };
229
+ const failed = (error) => {
230
+ cleanup();
231
+ reject(error);
232
+ };
233
+ const closed = () => failed(new Error("Incomplete proxy request"));
234
+ const timer = setTimeout(() => failed(new Error("Proxy handshake timed out")), 1e4);
235
+ const receive = (chunk) => {
236
+ data = Buffer.concat([data, chunk]);
237
+ const end = data.indexOf("\r\n\r\n");
238
+ if (end < 0 && data.length > 16384 || end > 16384)
239
+ return failed(new Error("Proxy header too large"));
240
+ if (end < 0) return;
241
+ cleanup();
242
+ try {
243
+ const request = publicWebRequest(data.subarray(0, end).toString("latin1"));
244
+ const remaining = Buffer.concat([Buffer.from(request.forward), data.subarray(end + 4)]);
245
+ if (remaining.length) input.unshift(remaining);
246
+ resolve(request);
247
+ } catch (error) {
248
+ reject(error);
249
+ }
250
+ };
251
+ input.on("data", receive);
252
+ input.once("end", closed);
253
+ input.once("error", failed);
254
+ });
255
+ }
256
+ function proxyUrl(target, connect) {
257
+ const url = new URL(connect ? `https://${target}` : target);
258
+ if (url.username || url.password || url.hash || !connect && url.protocol !== "http:")
259
+ throw new Error("Invalid proxy URL");
260
+ const port = Number(url.port || (connect ? 443 : 80));
261
+ if (port !== 80 && port !== 443) throw new Error("Only public web ports 80 and 443 are allowed");
262
+ return { url, port };
263
+ }
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
+
53
324
  // src/vm/qemu-policy.ts
54
325
  var ALLOWED_PROBES = new Set(
55
326
  [
@@ -103,9 +374,7 @@ var REQUIRED_CONTROLS = [
103
374
  "drive:cidata"
104
375
  ];
105
376
  var DISALLOWED_CONTROLS = /* @__PURE__ */ new Set(["-sandbox", "-no-user-config", "-nodefaults"]);
106
- var BOOT_DISK_BPS = "104857600";
107
- var BOOT_DISK_IOPS = "2000";
108
- function assertPort(port, field) {
377
+ function assertPort2(port, field) {
109
378
  if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error(`Invalid ${field}`);
110
379
  }
111
380
  function isProbe(args) {
@@ -226,10 +495,11 @@ function validateDrive(value, context) {
226
495
  }
227
496
  assertFields(fields, { file: /.+/, if: "none", discard: "on", id: "boot-disk" }, "boot drive");
228
497
  assertInstancePath(path, "disk", context);
229
- return {
230
- kind: "boot",
231
- 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
232
501
  };
502
+ return { kind: "boot", value: `${value},bps=${disk.bps},iops=${disk.iops}` };
233
503
  }
234
504
  if (fields.get("id") === "cdrom0") {
235
505
  assertFields(
@@ -274,35 +544,6 @@ function parseOriginalUsernet(value, sshPort) {
274
544
  "Lima QEMU user network"
275
545
  );
276
546
  }
277
- function restrictedUsernet(network, forwards, ncPath) {
278
- const guestForwards = forwards.map(
279
- (forward) => `guestfwd=tcp:${forward.guestIp}:${forward.port}-cmd:${ncPath} -4 -n -w 60 ${forward.address} ${forward.port}`
280
- );
281
- return [
282
- "user",
283
- "id=net0",
284
- `net=${WORKER_SUBNET}`,
285
- `dhcpstart=${WORKER_IP}`,
286
- "restrict=on",
287
- "ipv6=off",
288
- `hostfwd=tcp:127.0.0.1:${network.sshPort}-:22`,
289
- ...guestForwards
290
- ].join(",");
291
- }
292
- function validateForwards(forwards) {
293
- const seen = /* @__PURE__ */ new Set();
294
- for (const forward of forwards) {
295
- assertPort(forward.port, "guest forwarding port");
296
- const key = `${forward.guestIp}:${forward.port}`;
297
- if (seen.has(key)) throw new Error("Duplicate QEMU guest forwarding rule");
298
- seen.add(key);
299
- 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(
300
- forward.address
301
- )) {
302
- throw new Error("Invalid pinned forwarding address");
303
- }
304
- }
305
- }
306
547
  function fixedControl(control, expected) {
307
548
  return (value) => {
308
549
  if (value !== expected) throw new Error(`Unexpected QEMU ${control}`);
@@ -335,7 +576,7 @@ var SIMPLE_VALIDATORS = {
335
576
  "-parallel": fixedControl("-parallel", "none"),
336
577
  "-serial": fixedControl("serial", "chardev:char-serial"),
337
578
  "-qmp": fixedControl("qmp", "chardev:char-qmp"),
338
- "-name": fixedControl("name", "lima-conveyor"),
579
+ "-name": (value, { vmName = VM_NAME }) => fixedControl("name", `lima-${vmName}`)(value, {}),
339
580
  "-pidfile": (value, context) => {
340
581
  assertInstancePath(value, "qemu.pid", context);
341
582
  return { value, control: "pidfile" };
@@ -406,15 +647,16 @@ function assertCompleteProfile(state) {
406
647
  throw new Error("QEMU PCI serial device and chardev must appear together");
407
648
  }
408
649
  }
409
- function rewriteQemuArgs(args, network, ncPath = "/usr/bin/nc", context = {}) {
650
+ function rewriteQemuArgs(args, network, ncPath = "/usr/bin/nc", hostContext = {}) {
410
651
  if (isProbe(args)) return [...args];
411
- assertPort(network.sshPort, "SSH port");
652
+ assertPort2(network.sshPort, "SSH port");
412
653
  if (!TRUSTED_NC_PATH.test(ncPath)) throw new Error("Invalid netcat path");
413
- const forwards = network.hosts.flatMap(
414
- (host) => host.ports.map((port) => ({ guestIp: host.guestIp, address: host.address, port }))
415
- );
416
- if (forwards.length > MAX_NETWORK_FORWARDS) throw new Error("Too many QEMU forwarding rules");
417
- 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
+ };
418
660
  const output = [];
419
661
  const state = {
420
662
  controls: /* @__PURE__ */ new Set(),
@@ -470,12 +712,12 @@ import { execFileSync } from "child_process";
470
712
  import { lstatSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
471
713
  import { join } from "path";
472
714
  import { Socket } from "net";
473
- var MAX_RELAYS = 16;
474
- var MAX_RELAY_BYTES = 1024 * 1024 * 1024;
715
+ var SLOT_CEILING = CAPACITY_BOUNDS.relaySlots.max;
475
716
  var RELAY_CONNECT_MS = 6e4;
476
- var RELAY_IDLE_MS = 3e4;
717
+ var RELAY_IDLE_MS = 15 * 6e4;
477
718
  var GUARD_FRESH_MS = 6e3;
478
719
  var RELAY_PREFIX = "--conveyor-vm-relay";
720
+ var RELAY_FLAGS = ["-4", "-n", "-w", "60"];
479
721
  function validPid(value) {
480
722
  return Number.isInteger(value) && value > 1 && value <= 4194304;
481
723
  }
@@ -526,11 +768,13 @@ function readNetwork(root) {
526
768
  if (typeof parsed !== "object" || parsed === null)
527
769
  throw new Error("VM relay network policy is invalid");
528
770
  const network = parsed;
529
- if (!Array.isArray(network.hosts)) throw new Error("VM relay network policy is invalid");
530
- 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) };
531
774
  }
532
- function assertApprovedDestination(root, invocation) {
533
- 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(
534
778
  (host) => host && host.address === invocation.address && Array.isArray(host.ports) && host.ports.includes(invocation.port)
535
779
  );
536
780
  if (!allowed) throw new Error("VM relay destination is not in the host network policy");
@@ -541,9 +785,10 @@ function recordPath(root, pid) {
541
785
  function slotPath(root, slot) {
542
786
  return join(relayDirectory(root), `slot-${slot}`);
543
787
  }
544
- function claimSlot(root) {
788
+ function claimSlot(root, limits, reserve) {
545
789
  safeDirectory(relayDirectory(root));
546
- 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) {
547
792
  try {
548
793
  mkdirSync(slotPath(root, slot), { mode: 448 });
549
794
  return slot;
@@ -551,10 +796,10 @@ function claimSlot(root) {
551
796
  if (error.code !== "EEXIST") throw error;
552
797
  }
553
798
  }
554
- throw new Error(`VM relay limit reached (${MAX_RELAYS})`);
799
+ throw new Error(`VM relay limit reached (${limits.relaySlots})`);
555
800
  }
556
- function registerRelay(root, runId) {
557
- const slot = claimSlot(root);
801
+ function registerRelay(root, runId, limits, reserve) {
802
+ const slot = claimSlot(root, limits, reserve);
558
803
  const record = {
559
804
  schema: 1,
560
805
  pid: process.pid,
@@ -584,15 +829,20 @@ function unregisterRelay(root, record) {
584
829
  }
585
830
  function parseRelayInvocation(args) {
586
831
  if (args[0] !== RELAY_PREFIX) return null;
587
- 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)) {
588
834
  throw new Error("Invalid VM relay invocation");
589
835
  }
590
836
  const address = args[5] ?? "";
591
837
  const port = Number(args[6]);
592
- if (!isGloballyRoutableIPv4(address) || !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)) {
593
840
  throw new Error("Invalid VM relay destination");
594
841
  }
595
- return { address, port };
842
+ return { address, port, ...control ? { reserve: "control" } : {} };
843
+ }
844
+ function validPort(port) {
845
+ return Number.isInteger(port) && port >= 1 && port <= 65535;
596
846
  }
597
847
  function parsePs(output) {
598
848
  const result = /* @__PURE__ */ new Map();
@@ -613,7 +863,7 @@ function parsePs(output) {
613
863
  }
614
864
  function listedRecords(root, runId) {
615
865
  const records = [];
616
- for (let slot = 0; slot < MAX_RELAYS; slot += 1) {
866
+ for (let slot = 0; slot < SLOT_CEILING; slot += 1) {
617
867
  try {
618
868
  const record = JSON.parse(
619
869
  readFileSync(join(slotPath(root, slot), "owner.json"), "utf8")
@@ -668,21 +918,58 @@ function currentGuardStillValid(root, runId) {
668
918
  function runRelay(root, invocation, io) {
669
919
  return Promise.resolve().then(() => runRelayNow(root, invocation, io));
670
920
  }
671
- function runRelayNow(root, invocation, io) {
921
+ async function runRelayNow(root, invocation, io) {
672
922
  const guard = readLiveGuard(root);
673
- assertApprovedDestination(root, invocation);
674
- const record = registerRelay(root, guard.runId);
923
+ const network = readNetwork(root);
924
+ const publicWeb = invocation.address === "public-web" && invocation.port === PUBLIC_WEB_PORT;
925
+ if (publicWeb) {
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);
929
+ const streams = io ?? { socket: new Socket(), input: process.stdin, output: process.stdout };
930
+ let connectedResponse = false;
931
+ if (publicWeb) {
932
+ const releaseHandshake = handshakeExitCleanup(root, record);
933
+ let timeout;
934
+ try {
935
+ const request = await readPublicWebRequest(streams.input);
936
+ const address = await Promise.race([
937
+ publicWebAddress(request.host),
938
+ new Promise((_, reject) => {
939
+ timeout = setTimeout(() => reject(new Error("DNS lookup timed out")), 5e3);
940
+ })
941
+ ]);
942
+ if (!currentGuardStillValid(root, record.runId))
943
+ throw new Error("Resource guard is no longer healthy");
944
+ invocation = { address, port: request.port };
945
+ connectedResponse = request.connect;
946
+ } catch {
947
+ unregisterRelay(root, record);
948
+ streams.output.write(
949
+ "HTTP/1.1 403 Forbidden\r\nConnection: close\r\nContent-Length: 0\r\n\r\n"
950
+ );
951
+ streams.input.destroy();
952
+ streams.socket.destroy();
953
+ return;
954
+ } finally {
955
+ if (timeout) clearTimeout(timeout);
956
+ releaseHandshake();
957
+ }
958
+ }
959
+ return transportRelay(root, invocation, streams, record, {
960
+ connectedResponse,
961
+ maxBytes: network.limits.relayMaxBytes
962
+ });
963
+ }
964
+ function transportRelay(root, invocation, streams, record, options) {
965
+ const { connectedResponse, maxBytes } = options;
675
966
  return new Promise((resolve) => {
676
967
  let finished = false;
677
968
  let sent = 0;
678
969
  let received = 0;
679
970
  let guardTimer;
680
971
  let connectTimer;
681
- const { socket, input, output } = io ?? {
682
- socket: new Socket(),
683
- input: process.stdin,
684
- output: process.stdout
685
- };
972
+ const { socket, input, output } = streams;
686
973
  const signals = ["SIGINT", "SIGTERM", "SIGHUP"];
687
974
  const cleanup = () => {
688
975
  try {
@@ -709,11 +996,12 @@ function runRelayNow(root, invocation, io) {
709
996
  };
710
997
  process.once("exit", cleanup);
711
998
  for (const signal of signals) process.once(signal, terminated);
712
- const exceedsLimit = (total) => total > MAX_RELAY_BYTES;
999
+ const exceedsLimit = (total) => total > maxBytes;
713
1000
  socket.setTimeout(RELAY_IDLE_MS, () => finish(new Error("VM relay idle timeout")));
714
1001
  socket.once("connect", () => {
715
1002
  if (connectTimer) clearTimeout(connectTimer);
716
1003
  connectTimer = void 0;
1004
+ if (connectedResponse) output.write("HTTP/1.1 200 Connection Established\r\n\r\n");
717
1005
  });
718
1006
  socket.on("error", () => finish(new Error("VM relay connection failed")));
719
1007
  socket.on("close", () => finish());
@@ -741,29 +1029,56 @@ function runRelayNow(root, invocation, io) {
741
1029
  RELAY_CONNECT_MS
742
1030
  );
743
1031
  socket.connect(invocation.port, invocation.address);
1032
+ input.resume();
744
1033
  });
745
1034
  }
1035
+ function handshakeExitCleanup(root, record) {
1036
+ const signals = ["SIGINT", "SIGTERM", "SIGHUP"];
1037
+ const cleanup = () => {
1038
+ try {
1039
+ unregisterRelay(root, record);
1040
+ } catch {
1041
+ }
1042
+ };
1043
+ const release = () => {
1044
+ process.removeListener("exit", cleanup);
1045
+ for (const signal of signals) process.removeListener(signal, terminated);
1046
+ };
1047
+ const terminated = (signal) => {
1048
+ cleanup();
1049
+ release();
1050
+ process.kill(process.pid, signal);
1051
+ };
1052
+ process.once("exit", cleanup);
1053
+ for (const signal of signals) process.once(signal, terminated);
1054
+ return release;
1055
+ }
746
1056
 
747
1057
  // src/vm/memory-budget.ts
748
- function vmHostMemoryGiB(guestGiB) {
749
- 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);
750
1065
  }
751
1066
 
752
1067
  // src/vm/resource-guard.ts
753
- var GIB = 1024 ** 3;
1068
+ var GIB2 = 1024 ** 3;
754
1069
  var GUARD_INTERVAL_MS = 2e3;
755
- function guardLimits(resources) {
1070
+ function guardLimits(resources, capacity = LEGACY_CAPACITY_LIMITS) {
756
1071
  if (!Number.isInteger(resources.memoryGiB) || resources.memoryGiB < 2 || resources.memoryGiB > 1024 || !Number.isInteger(resources.diskGiB) || resources.diskGiB < 20 || resources.diskGiB > 512) {
757
1072
  throw new Error("Invalid resource limits in host state");
758
1073
  }
759
1074
  return {
760
- minFreeBytes: 10 * GIB,
1075
+ minFreeBytes: 10 * GIB2,
761
1076
  // A 24 GiB Mac guest exceeded 27 GiB during image unpacking. Allow 25%
762
1077
  // overhead (at least 1 GiB); admission reserves host RAM using this same cap.
763
- maxRssKiB: vmHostMemoryGiB(resources.memoryGiB) * 1024 ** 2,
764
- maxProcesses: 64,
1078
+ maxRssKiB: vmHostMemoryGiB(resources.memoryGiB, capacity.relaySlots) * 1024 ** 2,
1079
+ maxProcesses: guardProcessLimit(capacity.relaySlots),
765
1080
  maxOutputBytes: 1024 * 1024,
766
- maxDiskBytes: (resources.diskGiB + 1) * GIB,
1081
+ maxDiskBytes: (resources.diskGiB + 1) * GIB2,
767
1082
  maxLogBytes: 8 * 1024 ** 2
768
1083
  };
769
1084
  }
@@ -815,20 +1130,19 @@ function hostPs(args) {
815
1130
  env: { PATH: "/usr/bin:/bin", LANG: "C" }
816
1131
  });
817
1132
  }
818
- function sampleResources(root, qemuPid, runId) {
1133
+ function sampleResources(root, instanceDir, qemuPid, runId, ps = hostPs) {
819
1134
  const fs = statfsSync(root);
820
- const instance = join2(root, "lima", "conveyor");
821
- const processes = parseProcessGroup(hostPs(["-axo", "pgid=,pid=,rss="]), qemuPid);
822
- 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="]));
823
1137
  return {
824
1138
  freeBytes: fs.bavail * fs.bsize,
825
1139
  rssKiB: processes.rssKiB + relays.reduce((total, relay) => total + relay.rssKiB, 0),
826
1140
  processes: processes.processes + relays.length,
827
1141
  relayProcesses: relays.length,
828
- diskBytes: regularFileSize(join2(instance, "disk")),
1142
+ diskBytes: regularFileSize(join2(instanceDir, "disk")),
829
1143
  largestLogBytes: Math.max(
830
1144
  ...["ha.stdout.log", "ha.stderr.log", "serial.log", "serialp.log", "serialv.log"].map(
831
- (file) => regularFileSize(join2(instance, file))
1145
+ (file) => regularFileSize(join2(instanceDir, file))
832
1146
  )
833
1147
  )
834
1148
  };
@@ -843,7 +1157,7 @@ function writeGuardEvidence(root, evidence) {
843
1157
  rmSync2(temporary, { force: true });
844
1158
  }
845
1159
  }
846
- function startResourceGuard(root, identity, limits, stop) {
1160
+ function startResourceGuard(root, identity, limits, stop, instanceDir) {
847
1161
  const evidence = {
848
1162
  schema: 1,
849
1163
  ...identity,
@@ -868,7 +1182,7 @@ function startResourceGuard(root, identity, limits, stop) {
868
1182
  function check() {
869
1183
  if (finished) return;
870
1184
  try {
871
- evidence.observed = sampleResources(root, identity.qemuPid, identity.runId);
1185
+ evidence.observed = sampleResources(root, instanceDir, identity.qemuPid, identity.runId);
872
1186
  const violation = resourceViolation(evidence.observed, limits);
873
1187
  if (violation) throw new Error(violation);
874
1188
  evidence.lastCheckedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -1071,9 +1385,9 @@ function attachBoundedOutput(child, limit, terminate) {
1071
1385
  child.stdout?.on("data", consume);
1072
1386
  child.stderr?.on("data", consume);
1073
1387
  }
1074
- function runGuardedQemu(root, qemu, args, networkHash, resources) {
1388
+ function runGuardedQemu(root, qemu, args, networkHash, resources, instanceDir, capacity) {
1075
1389
  const runId = startIdentity(root);
1076
- const limits = guardLimits(resources);
1390
+ const limits = guardLimits(resources, parseCapacityLimits(capacity));
1077
1391
  const hardenedArgs = [...args, "-no-user-config", "-nodefaults"];
1078
1392
  const child = spawn2(qemu, hardenedArgs, {
1079
1393
  stdio: ["ignore", "pipe", "pipe"],
@@ -1131,7 +1445,7 @@ function runGuardedQemu(root, qemu, args, networkHash, resources) {
1131
1445
  JSON.stringify({ ...identity, args: hardenedArgs, startedAt: (/* @__PURE__ */ new Date()).toISOString() }),
1132
1446
  { mode: 384 }
1133
1447
  );
1134
- guard = startResourceGuard(root, identity, limits, terminate);
1448
+ guard = startResourceGuard(root, identity, limits, terminate, instanceDir);
1135
1449
  } catch {
1136
1450
  terminate("failed to record or start host resource supervision");
1137
1451
  }
@@ -1162,6 +1476,8 @@ function main() {
1162
1476
  if (state.schema !== 1 || !state.qemu.startsWith("/") || state.qemu.includes("wrapper")) {
1163
1477
  throw new Error("Invalid QEMU host state");
1164
1478
  }
1479
+ const vmName = vmNameFor(stateInstance(state.instance));
1480
+ const instanceDir = join5(root, "lima", vmName);
1165
1481
  const networkText = readFileSync4(join5(root, "network.json"), "utf8");
1166
1482
  const network = JSON.parse(networkText);
1167
1483
  const args = process.argv.slice(2);
@@ -1170,7 +1486,8 @@ function main() {
1170
1486
  original?.match(/hostfwd=tcp:127\.0\.0\.1:(\d+)-(?:192\.168\.5\.15)?:22/)?.[1] ?? 0
1171
1487
  );
1172
1488
  const rewritten = rewriteQemuArgs(args, { ...network, sshPort }, join5(root, "qemu-relay"), {
1173
- instanceDir: join5(root, "lima", "conveyor")
1489
+ instanceDir,
1490
+ vmName
1174
1491
  });
1175
1492
  if (original) {
1176
1493
  const expected = process.platform === "darwin" ? "hvf" : "kvm";
@@ -1182,7 +1499,9 @@ function main() {
1182
1499
  state.qemu,
1183
1500
  rewritten,
1184
1501
  createHash("sha256").update(networkText).digest("hex"),
1185
- state.resources
1502
+ state.resources,
1503
+ instanceDir,
1504
+ network.limits
1186
1505
  );
1187
1506
  return;
1188
1507
  }