@rallycry/conveyor-k3 0.1.12 → 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/cli.js CHANGED
@@ -215,6 +215,49 @@ var init_safe_arg = __esm({
215
215
  }
216
216
  });
217
217
 
218
+ // src/lib/env.ts
219
+ function readCliEnv(env = process.env) {
220
+ const apiUrl = env.CONVEYOR_API_URL ?? "";
221
+ const clusterToken = env.CONVEYOR_CLUSTER_TOKEN ?? "";
222
+ const clusterId = env.CONVEYOR_CLUSTER_ID ?? "";
223
+ if (!apiUrl || !clusterToken) {
224
+ throw new Error(
225
+ "This machine is not enrolled. /etc/conveyor/k3.env must set CONVEYOR_API_URL and CONVEYOR_CLUSTER_TOKEN \u2014 re-run the install command from your Conveyor profile."
226
+ );
227
+ }
228
+ return { apiUrl, clusterToken, clusterId };
229
+ }
230
+ async function preflight(exec) {
231
+ const problems = [];
232
+ if (process.platform !== "linux") {
233
+ problems.push(
234
+ `Managed personal compute requires Linux (this machine reports ${process.platform}).`
235
+ );
236
+ }
237
+ if (process.arch !== "x64" && process.arch !== "arm64") {
238
+ problems.push(`Unsupported CPU architecture ${process.arch} \u2014 amd64 or arm64 is required.`);
239
+ }
240
+ if (typeof process.getuid === "function" && process.getuid() !== 0) {
241
+ problems.push(
242
+ "This guest command requires root inside the VM. Use conveyor-k3 vm commands on the host."
243
+ );
244
+ }
245
+ if (!await commandExists(exec, "systemctl")) {
246
+ problems.push("systemd is required and `systemctl` was not found.");
247
+ }
248
+ return { ok: problems.length === 0, problems };
249
+ }
250
+ function checkedHostname(hostname2) {
251
+ return assertHostname(hostname2, "tunnel hostname");
252
+ }
253
+ var init_env = __esm({
254
+ "src/lib/env.ts"() {
255
+ "use strict";
256
+ init_safe_arg();
257
+ init_exec();
258
+ }
259
+ });
260
+
218
261
  // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/util.js
219
262
  var util, objectUtil, ZodParsedType, getParsedType;
220
263
  var init_util = __esm({
@@ -486,104 +529,104 @@ var init_en = __esm({
486
529
  init_ZodError();
487
530
  init_util();
488
531
  errorMap = (issue, _ctx) => {
489
- let message2;
532
+ let message3;
490
533
  switch (issue.code) {
491
534
  case ZodIssueCode.invalid_type:
492
535
  if (issue.received === ZodParsedType.undefined) {
493
- message2 = "Required";
536
+ message3 = "Required";
494
537
  } else {
495
- message2 = `Expected ${issue.expected}, received ${issue.received}`;
538
+ message3 = `Expected ${issue.expected}, received ${issue.received}`;
496
539
  }
497
540
  break;
498
541
  case ZodIssueCode.invalid_literal:
499
- message2 = `Invalid literal value, expected ${JSON.stringify(issue.expected, util.jsonStringifyReplacer)}`;
542
+ message3 = `Invalid literal value, expected ${JSON.stringify(issue.expected, util.jsonStringifyReplacer)}`;
500
543
  break;
501
544
  case ZodIssueCode.unrecognized_keys:
502
- message2 = `Unrecognized key(s) in object: ${util.joinValues(issue.keys, ", ")}`;
545
+ message3 = `Unrecognized key(s) in object: ${util.joinValues(issue.keys, ", ")}`;
503
546
  break;
504
547
  case ZodIssueCode.invalid_union:
505
- message2 = `Invalid input`;
548
+ message3 = `Invalid input`;
506
549
  break;
507
550
  case ZodIssueCode.invalid_union_discriminator:
508
- message2 = `Invalid discriminator value. Expected ${util.joinValues(issue.options)}`;
551
+ message3 = `Invalid discriminator value. Expected ${util.joinValues(issue.options)}`;
509
552
  break;
510
553
  case ZodIssueCode.invalid_enum_value:
511
- message2 = `Invalid enum value. Expected ${util.joinValues(issue.options)}, received '${issue.received}'`;
554
+ message3 = `Invalid enum value. Expected ${util.joinValues(issue.options)}, received '${issue.received}'`;
512
555
  break;
513
556
  case ZodIssueCode.invalid_arguments:
514
- message2 = `Invalid function arguments`;
557
+ message3 = `Invalid function arguments`;
515
558
  break;
516
559
  case ZodIssueCode.invalid_return_type:
517
- message2 = `Invalid function return type`;
560
+ message3 = `Invalid function return type`;
518
561
  break;
519
562
  case ZodIssueCode.invalid_date:
520
- message2 = `Invalid date`;
563
+ message3 = `Invalid date`;
521
564
  break;
522
565
  case ZodIssueCode.invalid_string:
523
566
  if (typeof issue.validation === "object") {
524
567
  if ("includes" in issue.validation) {
525
- message2 = `Invalid input: must include "${issue.validation.includes}"`;
568
+ message3 = `Invalid input: must include "${issue.validation.includes}"`;
526
569
  if (typeof issue.validation.position === "number") {
527
- message2 = `${message2} at one or more positions greater than or equal to ${issue.validation.position}`;
570
+ message3 = `${message3} at one or more positions greater than or equal to ${issue.validation.position}`;
528
571
  }
529
572
  } else if ("startsWith" in issue.validation) {
530
- message2 = `Invalid input: must start with "${issue.validation.startsWith}"`;
573
+ message3 = `Invalid input: must start with "${issue.validation.startsWith}"`;
531
574
  } else if ("endsWith" in issue.validation) {
532
- message2 = `Invalid input: must end with "${issue.validation.endsWith}"`;
575
+ message3 = `Invalid input: must end with "${issue.validation.endsWith}"`;
533
576
  } else {
534
577
  util.assertNever(issue.validation);
535
578
  }
536
579
  } else if (issue.validation !== "regex") {
537
- message2 = `Invalid ${issue.validation}`;
580
+ message3 = `Invalid ${issue.validation}`;
538
581
  } else {
539
- message2 = "Invalid";
582
+ message3 = "Invalid";
540
583
  }
541
584
  break;
542
585
  case ZodIssueCode.too_small:
543
586
  if (issue.type === "array")
544
- message2 = `Array must contain ${issue.exact ? "exactly" : issue.inclusive ? `at least` : `more than`} ${issue.minimum} element(s)`;
587
+ message3 = `Array must contain ${issue.exact ? "exactly" : issue.inclusive ? `at least` : `more than`} ${issue.minimum} element(s)`;
545
588
  else if (issue.type === "string")
546
- message2 = `String must contain ${issue.exact ? "exactly" : issue.inclusive ? `at least` : `over`} ${issue.minimum} character(s)`;
589
+ message3 = `String must contain ${issue.exact ? "exactly" : issue.inclusive ? `at least` : `over`} ${issue.minimum} character(s)`;
547
590
  else if (issue.type === "number")
548
- message2 = `Number must be ${issue.exact ? `exactly equal to ` : issue.inclusive ? `greater than or equal to ` : `greater than `}${issue.minimum}`;
591
+ message3 = `Number must be ${issue.exact ? `exactly equal to ` : issue.inclusive ? `greater than or equal to ` : `greater than `}${issue.minimum}`;
549
592
  else if (issue.type === "bigint")
550
- message2 = `Number must be ${issue.exact ? `exactly equal to ` : issue.inclusive ? `greater than or equal to ` : `greater than `}${issue.minimum}`;
593
+ message3 = `Number must be ${issue.exact ? `exactly equal to ` : issue.inclusive ? `greater than or equal to ` : `greater than `}${issue.minimum}`;
551
594
  else if (issue.type === "date")
552
- message2 = `Date must be ${issue.exact ? `exactly equal to ` : issue.inclusive ? `greater than or equal to ` : `greater than `}${new Date(Number(issue.minimum))}`;
595
+ message3 = `Date must be ${issue.exact ? `exactly equal to ` : issue.inclusive ? `greater than or equal to ` : `greater than `}${new Date(Number(issue.minimum))}`;
553
596
  else
554
- message2 = "Invalid input";
597
+ message3 = "Invalid input";
555
598
  break;
556
599
  case ZodIssueCode.too_big:
557
600
  if (issue.type === "array")
558
- message2 = `Array must contain ${issue.exact ? `exactly` : issue.inclusive ? `at most` : `less than`} ${issue.maximum} element(s)`;
601
+ message3 = `Array must contain ${issue.exact ? `exactly` : issue.inclusive ? `at most` : `less than`} ${issue.maximum} element(s)`;
559
602
  else if (issue.type === "string")
560
- message2 = `String must contain ${issue.exact ? `exactly` : issue.inclusive ? `at most` : `under`} ${issue.maximum} character(s)`;
603
+ message3 = `String must contain ${issue.exact ? `exactly` : issue.inclusive ? `at most` : `under`} ${issue.maximum} character(s)`;
561
604
  else if (issue.type === "number")
562
- message2 = `Number must be ${issue.exact ? `exactly` : issue.inclusive ? `less than or equal to` : `less than`} ${issue.maximum}`;
605
+ message3 = `Number must be ${issue.exact ? `exactly` : issue.inclusive ? `less than or equal to` : `less than`} ${issue.maximum}`;
563
606
  else if (issue.type === "bigint")
564
- message2 = `BigInt must be ${issue.exact ? `exactly` : issue.inclusive ? `less than or equal to` : `less than`} ${issue.maximum}`;
607
+ message3 = `BigInt must be ${issue.exact ? `exactly` : issue.inclusive ? `less than or equal to` : `less than`} ${issue.maximum}`;
565
608
  else if (issue.type === "date")
566
- message2 = `Date must be ${issue.exact ? `exactly` : issue.inclusive ? `smaller than or equal to` : `smaller than`} ${new Date(Number(issue.maximum))}`;
609
+ message3 = `Date must be ${issue.exact ? `exactly` : issue.inclusive ? `smaller than or equal to` : `smaller than`} ${new Date(Number(issue.maximum))}`;
567
610
  else
568
- message2 = "Invalid input";
611
+ message3 = "Invalid input";
569
612
  break;
570
613
  case ZodIssueCode.custom:
571
- message2 = `Invalid input`;
614
+ message3 = `Invalid input`;
572
615
  break;
573
616
  case ZodIssueCode.invalid_intersection_types:
574
- message2 = `Intersection results could not be merged`;
617
+ message3 = `Intersection results could not be merged`;
575
618
  break;
576
619
  case ZodIssueCode.not_multiple_of:
577
- message2 = `Number must be a multiple of ${issue.multipleOf}`;
620
+ message3 = `Number must be a multiple of ${issue.multipleOf}`;
578
621
  break;
579
622
  case ZodIssueCode.not_finite:
580
- message2 = "Number must be finite";
623
+ message3 = "Number must be finite";
581
624
  break;
582
625
  default:
583
- message2 = _ctx.defaultError;
626
+ message3 = _ctx.defaultError;
584
627
  util.assertNever(issue);
585
628
  }
586
- return { message: message2 };
629
+ return { message: message3 };
587
630
  };
588
631
  en_default = errorMap;
589
632
  }
@@ -736,8 +779,8 @@ var init_errorUtil = __esm({
736
779
  "../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/errorUtil.js"() {
737
780
  "use strict";
738
781
  (function(errorUtil2) {
739
- errorUtil2.errToObj = (message2) => typeof message2 === "string" ? { message: message2 } : message2 || {};
740
- errorUtil2.toString = (message2) => typeof message2 === "string" ? message2 : message2?.message;
782
+ errorUtil2.errToObj = (message3) => typeof message3 === "string" ? { message: message3 } : message3 || {};
783
+ errorUtil2.toString = (message3) => typeof message3 === "string" ? message3 : message3?.message;
741
784
  })(errorUtil || (errorUtil = {}));
742
785
  }
743
786
  });
@@ -753,16 +796,16 @@ function processCreateParams(params) {
753
796
  if (errorMap2)
754
797
  return { errorMap: errorMap2, description };
755
798
  const customMap = (iss, ctx) => {
756
- const { message: message2 } = params;
799
+ const { message: message3 } = params;
757
800
  if (iss.code === "invalid_enum_value") {
758
- return { message: message2 ?? ctx.defaultError };
801
+ return { message: message3 ?? ctx.defaultError };
759
802
  }
760
803
  if (typeof ctx.data === "undefined") {
761
- return { message: message2 ?? required_error ?? ctx.defaultError };
804
+ return { message: message3 ?? required_error ?? ctx.defaultError };
762
805
  }
763
806
  if (iss.code !== "invalid_type")
764
807
  return { message: ctx.defaultError };
765
- return { message: message2 ?? invalid_type_error ?? ctx.defaultError };
808
+ return { message: message3 ?? invalid_type_error ?? ctx.defaultError };
766
809
  };
767
810
  return { errorMap: customMap, description };
768
811
  }
@@ -1103,14 +1146,14 @@ var init_types = __esm({
1103
1146
  const result = await (isAsync(maybeAsyncResult) ? maybeAsyncResult : Promise.resolve(maybeAsyncResult));
1104
1147
  return handleResult(ctx, result);
1105
1148
  }
1106
- refine(check, message2) {
1149
+ refine(check, message3) {
1107
1150
  const getIssueProperties = (val) => {
1108
- if (typeof message2 === "string" || typeof message2 === "undefined") {
1109
- return { message: message2 };
1110
- } else if (typeof message2 === "function") {
1111
- return message2(val);
1151
+ if (typeof message3 === "string" || typeof message3 === "undefined") {
1152
+ return { message: message3 };
1153
+ } else if (typeof message3 === "function") {
1154
+ return message3(val);
1112
1155
  } else {
1113
- return message2;
1156
+ return message3;
1114
1157
  }
1115
1158
  };
1116
1159
  return this._refinement((val, ctx) => {
@@ -1583,11 +1626,11 @@ var init_types = __esm({
1583
1626
  }
1584
1627
  return { status: status.value, value: input.data };
1585
1628
  }
1586
- _regex(regex, validation, message2) {
1629
+ _regex(regex, validation, message3) {
1587
1630
  return this.refinement((data) => regex.test(data), {
1588
1631
  validation,
1589
1632
  code: ZodIssueCode.invalid_string,
1590
- ...errorUtil.errToObj(message2)
1633
+ ...errorUtil.errToObj(message3)
1591
1634
  });
1592
1635
  }
1593
1636
  _addCheck(check) {
@@ -1596,37 +1639,37 @@ var init_types = __esm({
1596
1639
  checks: [...this._def.checks, check]
1597
1640
  });
1598
1641
  }
1599
- email(message2) {
1600
- return this._addCheck({ kind: "email", ...errorUtil.errToObj(message2) });
1642
+ email(message3) {
1643
+ return this._addCheck({ kind: "email", ...errorUtil.errToObj(message3) });
1601
1644
  }
1602
- url(message2) {
1603
- return this._addCheck({ kind: "url", ...errorUtil.errToObj(message2) });
1645
+ url(message3) {
1646
+ return this._addCheck({ kind: "url", ...errorUtil.errToObj(message3) });
1604
1647
  }
1605
- emoji(message2) {
1606
- return this._addCheck({ kind: "emoji", ...errorUtil.errToObj(message2) });
1648
+ emoji(message3) {
1649
+ return this._addCheck({ kind: "emoji", ...errorUtil.errToObj(message3) });
1607
1650
  }
1608
- uuid(message2) {
1609
- return this._addCheck({ kind: "uuid", ...errorUtil.errToObj(message2) });
1651
+ uuid(message3) {
1652
+ return this._addCheck({ kind: "uuid", ...errorUtil.errToObj(message3) });
1610
1653
  }
1611
- nanoid(message2) {
1612
- return this._addCheck({ kind: "nanoid", ...errorUtil.errToObj(message2) });
1654
+ nanoid(message3) {
1655
+ return this._addCheck({ kind: "nanoid", ...errorUtil.errToObj(message3) });
1613
1656
  }
1614
- cuid(message2) {
1615
- return this._addCheck({ kind: "cuid", ...errorUtil.errToObj(message2) });
1657
+ cuid(message3) {
1658
+ return this._addCheck({ kind: "cuid", ...errorUtil.errToObj(message3) });
1616
1659
  }
1617
- cuid2(message2) {
1618
- return this._addCheck({ kind: "cuid2", ...errorUtil.errToObj(message2) });
1660
+ cuid2(message3) {
1661
+ return this._addCheck({ kind: "cuid2", ...errorUtil.errToObj(message3) });
1619
1662
  }
1620
- ulid(message2) {
1621
- return this._addCheck({ kind: "ulid", ...errorUtil.errToObj(message2) });
1663
+ ulid(message3) {
1664
+ return this._addCheck({ kind: "ulid", ...errorUtil.errToObj(message3) });
1622
1665
  }
1623
- base64(message2) {
1624
- return this._addCheck({ kind: "base64", ...errorUtil.errToObj(message2) });
1666
+ base64(message3) {
1667
+ return this._addCheck({ kind: "base64", ...errorUtil.errToObj(message3) });
1625
1668
  }
1626
- base64url(message2) {
1669
+ base64url(message3) {
1627
1670
  return this._addCheck({
1628
1671
  kind: "base64url",
1629
- ...errorUtil.errToObj(message2)
1672
+ ...errorUtil.errToObj(message3)
1630
1673
  });
1631
1674
  }
1632
1675
  jwt(options) {
@@ -1656,8 +1699,8 @@ var init_types = __esm({
1656
1699
  ...errorUtil.errToObj(options?.message)
1657
1700
  });
1658
1701
  }
1659
- date(message2) {
1660
- return this._addCheck({ kind: "date", message: message2 });
1702
+ date(message3) {
1703
+ return this._addCheck({ kind: "date", message: message3 });
1661
1704
  }
1662
1705
  time(options) {
1663
1706
  if (typeof options === "string") {
@@ -1673,14 +1716,14 @@ var init_types = __esm({
1673
1716
  ...errorUtil.errToObj(options?.message)
1674
1717
  });
1675
1718
  }
1676
- duration(message2) {
1677
- return this._addCheck({ kind: "duration", ...errorUtil.errToObj(message2) });
1719
+ duration(message3) {
1720
+ return this._addCheck({ kind: "duration", ...errorUtil.errToObj(message3) });
1678
1721
  }
1679
- regex(regex, message2) {
1722
+ regex(regex, message3) {
1680
1723
  return this._addCheck({
1681
1724
  kind: "regex",
1682
1725
  regex,
1683
- ...errorUtil.errToObj(message2)
1726
+ ...errorUtil.errToObj(message3)
1684
1727
  });
1685
1728
  }
1686
1729
  includes(value, options) {
@@ -1691,46 +1734,46 @@ var init_types = __esm({
1691
1734
  ...errorUtil.errToObj(options?.message)
1692
1735
  });
1693
1736
  }
1694
- startsWith(value, message2) {
1737
+ startsWith(value, message3) {
1695
1738
  return this._addCheck({
1696
1739
  kind: "startsWith",
1697
1740
  value,
1698
- ...errorUtil.errToObj(message2)
1741
+ ...errorUtil.errToObj(message3)
1699
1742
  });
1700
1743
  }
1701
- endsWith(value, message2) {
1744
+ endsWith(value, message3) {
1702
1745
  return this._addCheck({
1703
1746
  kind: "endsWith",
1704
1747
  value,
1705
- ...errorUtil.errToObj(message2)
1748
+ ...errorUtil.errToObj(message3)
1706
1749
  });
1707
1750
  }
1708
- min(minLength, message2) {
1751
+ min(minLength, message3) {
1709
1752
  return this._addCheck({
1710
1753
  kind: "min",
1711
1754
  value: minLength,
1712
- ...errorUtil.errToObj(message2)
1755
+ ...errorUtil.errToObj(message3)
1713
1756
  });
1714
1757
  }
1715
- max(maxLength, message2) {
1758
+ max(maxLength, message3) {
1716
1759
  return this._addCheck({
1717
1760
  kind: "max",
1718
1761
  value: maxLength,
1719
- ...errorUtil.errToObj(message2)
1762
+ ...errorUtil.errToObj(message3)
1720
1763
  });
1721
1764
  }
1722
- length(len, message2) {
1765
+ length(len, message3) {
1723
1766
  return this._addCheck({
1724
1767
  kind: "length",
1725
1768
  value: len,
1726
- ...errorUtil.errToObj(message2)
1769
+ ...errorUtil.errToObj(message3)
1727
1770
  });
1728
1771
  }
1729
1772
  /**
1730
1773
  * Equivalent to `.min(1)`
1731
1774
  */
1732
- nonempty(message2) {
1733
- return this.min(1, errorUtil.errToObj(message2));
1775
+ nonempty(message3) {
1776
+ return this.min(1, errorUtil.errToObj(message3));
1734
1777
  }
1735
1778
  trim() {
1736
1779
  return new _ZodString({
@@ -1915,19 +1958,19 @@ var init_types = __esm({
1915
1958
  }
1916
1959
  return { status: status.value, value: input.data };
1917
1960
  }
1918
- gte(value, message2) {
1919
- return this.setLimit("min", value, true, errorUtil.toString(message2));
1961
+ gte(value, message3) {
1962
+ return this.setLimit("min", value, true, errorUtil.toString(message3));
1920
1963
  }
1921
- gt(value, message2) {
1922
- return this.setLimit("min", value, false, errorUtil.toString(message2));
1964
+ gt(value, message3) {
1965
+ return this.setLimit("min", value, false, errorUtil.toString(message3));
1923
1966
  }
1924
- lte(value, message2) {
1925
- return this.setLimit("max", value, true, errorUtil.toString(message2));
1967
+ lte(value, message3) {
1968
+ return this.setLimit("max", value, true, errorUtil.toString(message3));
1926
1969
  }
1927
- lt(value, message2) {
1928
- return this.setLimit("max", value, false, errorUtil.toString(message2));
1970
+ lt(value, message3) {
1971
+ return this.setLimit("max", value, false, errorUtil.toString(message3));
1929
1972
  }
1930
- setLimit(kind, value, inclusive, message2) {
1973
+ setLimit(kind, value, inclusive, message3) {
1931
1974
  return new _ZodNumber({
1932
1975
  ...this._def,
1933
1976
  checks: [
@@ -1936,7 +1979,7 @@ var init_types = __esm({
1936
1979
  kind,
1937
1980
  value,
1938
1981
  inclusive,
1939
- message: errorUtil.toString(message2)
1982
+ message: errorUtil.toString(message3)
1940
1983
  }
1941
1984
  ]
1942
1985
  });
@@ -1947,68 +1990,68 @@ var init_types = __esm({
1947
1990
  checks: [...this._def.checks, check]
1948
1991
  });
1949
1992
  }
1950
- int(message2) {
1993
+ int(message3) {
1951
1994
  return this._addCheck({
1952
1995
  kind: "int",
1953
- message: errorUtil.toString(message2)
1996
+ message: errorUtil.toString(message3)
1954
1997
  });
1955
1998
  }
1956
- positive(message2) {
1999
+ positive(message3) {
1957
2000
  return this._addCheck({
1958
2001
  kind: "min",
1959
2002
  value: 0,
1960
2003
  inclusive: false,
1961
- message: errorUtil.toString(message2)
2004
+ message: errorUtil.toString(message3)
1962
2005
  });
1963
2006
  }
1964
- negative(message2) {
2007
+ negative(message3) {
1965
2008
  return this._addCheck({
1966
2009
  kind: "max",
1967
2010
  value: 0,
1968
2011
  inclusive: false,
1969
- message: errorUtil.toString(message2)
2012
+ message: errorUtil.toString(message3)
1970
2013
  });
1971
2014
  }
1972
- nonpositive(message2) {
2015
+ nonpositive(message3) {
1973
2016
  return this._addCheck({
1974
2017
  kind: "max",
1975
2018
  value: 0,
1976
2019
  inclusive: true,
1977
- message: errorUtil.toString(message2)
2020
+ message: errorUtil.toString(message3)
1978
2021
  });
1979
2022
  }
1980
- nonnegative(message2) {
2023
+ nonnegative(message3) {
1981
2024
  return this._addCheck({
1982
2025
  kind: "min",
1983
2026
  value: 0,
1984
2027
  inclusive: true,
1985
- message: errorUtil.toString(message2)
2028
+ message: errorUtil.toString(message3)
1986
2029
  });
1987
2030
  }
1988
- multipleOf(value, message2) {
2031
+ multipleOf(value, message3) {
1989
2032
  return this._addCheck({
1990
2033
  kind: "multipleOf",
1991
2034
  value,
1992
- message: errorUtil.toString(message2)
2035
+ message: errorUtil.toString(message3)
1993
2036
  });
1994
2037
  }
1995
- finite(message2) {
2038
+ finite(message3) {
1996
2039
  return this._addCheck({
1997
2040
  kind: "finite",
1998
- message: errorUtil.toString(message2)
2041
+ message: errorUtil.toString(message3)
1999
2042
  });
2000
2043
  }
2001
- safe(message2) {
2044
+ safe(message3) {
2002
2045
  return this._addCheck({
2003
2046
  kind: "min",
2004
2047
  inclusive: true,
2005
2048
  value: Number.MIN_SAFE_INTEGER,
2006
- message: errorUtil.toString(message2)
2049
+ message: errorUtil.toString(message3)
2007
2050
  })._addCheck({
2008
2051
  kind: "max",
2009
2052
  inclusive: true,
2010
2053
  value: Number.MAX_SAFE_INTEGER,
2011
- message: errorUtil.toString(message2)
2054
+ message: errorUtil.toString(message3)
2012
2055
  });
2013
2056
  }
2014
2057
  get minValue() {
@@ -2131,19 +2174,19 @@ var init_types = __esm({
2131
2174
  });
2132
2175
  return INVALID;
2133
2176
  }
2134
- gte(value, message2) {
2135
- return this.setLimit("min", value, true, errorUtil.toString(message2));
2177
+ gte(value, message3) {
2178
+ return this.setLimit("min", value, true, errorUtil.toString(message3));
2136
2179
  }
2137
- gt(value, message2) {
2138
- return this.setLimit("min", value, false, errorUtil.toString(message2));
2180
+ gt(value, message3) {
2181
+ return this.setLimit("min", value, false, errorUtil.toString(message3));
2139
2182
  }
2140
- lte(value, message2) {
2141
- return this.setLimit("max", value, true, errorUtil.toString(message2));
2183
+ lte(value, message3) {
2184
+ return this.setLimit("max", value, true, errorUtil.toString(message3));
2142
2185
  }
2143
- lt(value, message2) {
2144
- return this.setLimit("max", value, false, errorUtil.toString(message2));
2186
+ lt(value, message3) {
2187
+ return this.setLimit("max", value, false, errorUtil.toString(message3));
2145
2188
  }
2146
- setLimit(kind, value, inclusive, message2) {
2189
+ setLimit(kind, value, inclusive, message3) {
2147
2190
  return new _ZodBigInt({
2148
2191
  ...this._def,
2149
2192
  checks: [
@@ -2152,7 +2195,7 @@ var init_types = __esm({
2152
2195
  kind,
2153
2196
  value,
2154
2197
  inclusive,
2155
- message: errorUtil.toString(message2)
2198
+ message: errorUtil.toString(message3)
2156
2199
  }
2157
2200
  ]
2158
2201
  });
@@ -2163,43 +2206,43 @@ var init_types = __esm({
2163
2206
  checks: [...this._def.checks, check]
2164
2207
  });
2165
2208
  }
2166
- positive(message2) {
2209
+ positive(message3) {
2167
2210
  return this._addCheck({
2168
2211
  kind: "min",
2169
2212
  value: BigInt(0),
2170
2213
  inclusive: false,
2171
- message: errorUtil.toString(message2)
2214
+ message: errorUtil.toString(message3)
2172
2215
  });
2173
2216
  }
2174
- negative(message2) {
2217
+ negative(message3) {
2175
2218
  return this._addCheck({
2176
2219
  kind: "max",
2177
2220
  value: BigInt(0),
2178
2221
  inclusive: false,
2179
- message: errorUtil.toString(message2)
2222
+ message: errorUtil.toString(message3)
2180
2223
  });
2181
2224
  }
2182
- nonpositive(message2) {
2225
+ nonpositive(message3) {
2183
2226
  return this._addCheck({
2184
2227
  kind: "max",
2185
2228
  value: BigInt(0),
2186
2229
  inclusive: true,
2187
- message: errorUtil.toString(message2)
2230
+ message: errorUtil.toString(message3)
2188
2231
  });
2189
2232
  }
2190
- nonnegative(message2) {
2233
+ nonnegative(message3) {
2191
2234
  return this._addCheck({
2192
2235
  kind: "min",
2193
2236
  value: BigInt(0),
2194
2237
  inclusive: true,
2195
- message: errorUtil.toString(message2)
2238
+ message: errorUtil.toString(message3)
2196
2239
  });
2197
2240
  }
2198
- multipleOf(value, message2) {
2241
+ multipleOf(value, message3) {
2199
2242
  return this._addCheck({
2200
2243
  kind: "multipleOf",
2201
2244
  value,
2202
- message: errorUtil.toString(message2)
2245
+ message: errorUtil.toString(message3)
2203
2246
  });
2204
2247
  }
2205
2248
  get minValue() {
@@ -2322,18 +2365,18 @@ var init_types = __esm({
2322
2365
  checks: [...this._def.checks, check]
2323
2366
  });
2324
2367
  }
2325
- min(minDate, message2) {
2368
+ min(minDate, message3) {
2326
2369
  return this._addCheck({
2327
2370
  kind: "min",
2328
2371
  value: minDate.getTime(),
2329
- message: errorUtil.toString(message2)
2372
+ message: errorUtil.toString(message3)
2330
2373
  });
2331
2374
  }
2332
- max(maxDate, message2) {
2375
+ max(maxDate, message3) {
2333
2376
  return this._addCheck({
2334
2377
  kind: "max",
2335
2378
  value: maxDate.getTime(),
2336
- message: errorUtil.toString(message2)
2379
+ message: errorUtil.toString(message3)
2337
2380
  });
2338
2381
  }
2339
2382
  get minDate() {
@@ -2565,26 +2608,26 @@ var init_types = __esm({
2565
2608
  get element() {
2566
2609
  return this._def.type;
2567
2610
  }
2568
- min(minLength, message2) {
2611
+ min(minLength, message3) {
2569
2612
  return new _ZodArray({
2570
2613
  ...this._def,
2571
- minLength: { value: minLength, message: errorUtil.toString(message2) }
2614
+ minLength: { value: minLength, message: errorUtil.toString(message3) }
2572
2615
  });
2573
2616
  }
2574
- max(maxLength, message2) {
2617
+ max(maxLength, message3) {
2575
2618
  return new _ZodArray({
2576
2619
  ...this._def,
2577
- maxLength: { value: maxLength, message: errorUtil.toString(message2) }
2620
+ maxLength: { value: maxLength, message: errorUtil.toString(message3) }
2578
2621
  });
2579
2622
  }
2580
- length(len, message2) {
2623
+ length(len, message3) {
2581
2624
  return new _ZodArray({
2582
2625
  ...this._def,
2583
- exactLength: { value: len, message: errorUtil.toString(message2) }
2626
+ exactLength: { value: len, message: errorUtil.toString(message3) }
2584
2627
  });
2585
2628
  }
2586
- nonempty(message2) {
2587
- return this.min(1, message2);
2629
+ nonempty(message3) {
2630
+ return this.min(1, message3);
2588
2631
  }
2589
2632
  };
2590
2633
  ZodArray.create = (schema, params) => {
@@ -2701,17 +2744,17 @@ var init_types = __esm({
2701
2744
  get shape() {
2702
2745
  return this._def.shape();
2703
2746
  }
2704
- strict(message2) {
2747
+ strict(message3) {
2705
2748
  errorUtil.errToObj;
2706
2749
  return new _ZodObject({
2707
2750
  ...this._def,
2708
2751
  unknownKeys: "strict",
2709
- ...message2 !== void 0 ? {
2752
+ ...message3 !== void 0 ? {
2710
2753
  errorMap: (issue, ctx) => {
2711
2754
  const defaultError = this._def.errorMap?.(issue, ctx).message ?? ctx.defaultError;
2712
2755
  if (issue.code === "unrecognized_keys")
2713
2756
  return {
2714
- message: errorUtil.errToObj(message2).message ?? defaultError
2757
+ message: errorUtil.errToObj(message3).message ?? defaultError
2715
2758
  };
2716
2759
  return {
2717
2760
  message: defaultError
@@ -3429,23 +3472,23 @@ var init_types = __esm({
3429
3472
  return finalizeSet(elements);
3430
3473
  }
3431
3474
  }
3432
- min(minSize, message2) {
3475
+ min(minSize, message3) {
3433
3476
  return new _ZodSet({
3434
3477
  ...this._def,
3435
- minSize: { value: minSize, message: errorUtil.toString(message2) }
3478
+ minSize: { value: minSize, message: errorUtil.toString(message3) }
3436
3479
  });
3437
3480
  }
3438
- max(maxSize, message2) {
3481
+ max(maxSize, message3) {
3439
3482
  return new _ZodSet({
3440
3483
  ...this._def,
3441
- maxSize: { value: maxSize, message: errorUtil.toString(message2) }
3484
+ maxSize: { value: maxSize, message: errorUtil.toString(message3) }
3442
3485
  });
3443
3486
  }
3444
- size(size, message2) {
3445
- return this.min(size, message2).max(size, message2);
3487
+ size(size, message3) {
3488
+ return this.min(size, message3).max(size, message3);
3446
3489
  }
3447
- nonempty(message2) {
3448
- return this.min(1, message2);
3490
+ nonempty(message3) {
3491
+ return this.min(1, message3);
3449
3492
  }
3450
3493
  };
3451
3494
  ZodSet.create = (valueType, params) => {
@@ -4331,7 +4374,7 @@ var init_zod = __esm({
4331
4374
  }
4332
4375
  });
4333
4376
 
4334
- // ../shared/dist/chunk-BH4SLNGU.js
4377
+ // ../shared/dist/chunk-VSUBG7GR.js
4335
4378
  function formatModelId(provider, model) {
4336
4379
  return `${provider}/${model}`;
4337
4380
  }
@@ -4349,8 +4392,8 @@ function uniqueSortedArray(item, minimum = 0) {
4349
4392
  }).transform((values) => [...values].sort());
4350
4393
  }
4351
4394
  var DEFAULT_SONNET_MODEL, DEFAULT_OPUS_MODEL, DEFAULT_HAIKU_MODEL, FABLE_MODEL, PREVIOUS_SONNET_MODEL, PREVIOUS_OPUS_MODEL, JEV_MODEL, JEV_MODEL_ID, JEV_ZEN_MODEL, JEV_ZEN_MODEL_ID, JEV_INPUT_PRICE_PER_TOKEN, JEV_STATE_MAX_CHARS, PTY_STREAM_PORT_BASE, PTY_STREAM_PORT_ATTEMPTS, PREVIEW_PORT_DENY_LIST, checkpointPathSchema, secretNameSchema, checkpointKeySchema, checkpointDigestRefSchema, ACTIONS_PREBAKE_REGISTRY_PATTERN, actionsPrebakeRegistrySchema, projectCheckpointSettingsSchema, CARD_DESCRIPTION_MAX, CARD_DESCRIPTION_LIMIT_MESSAGE, CARD_DESCRIPTION_FIELD_HINT, MAX_MANUAL_TESTS_PER_TASK, MAX_MANUAL_TEST_TITLE_LENGTH, ManualTestItemsSchema, MAX_CI_WAIT_TIMEOUT_MINUTES;
4352
- var init_chunk_BH4SLNGU = __esm({
4353
- "../shared/dist/chunk-BH4SLNGU.js"() {
4395
+ var init_chunk_VSUBG7GR = __esm({
4396
+ "../shared/dist/chunk-VSUBG7GR.js"() {
4354
4397
  "use strict";
4355
4398
  init_zod();
4356
4399
  init_zod();
@@ -4476,11 +4519,11 @@ function localBakeSudoersCommands(k3sPath) {
4476
4519
  return `${k3sPath} ctr ${command.args.join(" ")}${suffix}`;
4477
4520
  });
4478
4521
  }
4479
- var ACHIEVEMENT_RARITIES, RISK_LEVELS, riskLevelSchema, DEFAULT_RISK_LEVELS, LEVEL_BY_VALUE, ACTIVE_WORK_STATUSES, IDENTIFIED_WORK_STATUSES, DEFAULT_TASK_STATUS_COLOR, DEFAULT_TASK_STATUS_COLOR_INT, MAX_FILE_SIZE_BYTES, MAX_FILE_TAGS, MAX_FILE_TAG_LENGTH, ALLOWED_FILE_MIME_TYPES, ALLOWED_FILE_ACCEPT, EMBED_THRESHOLD_IMAGES, EMBED_THRESHOLD_TEXT, IDLE_HEARTBEAT_MS, TurnEndToolCallSchema, AgentTurnUsageSchema, KnownAgentEventSchema, AgentEventSchema, PtyChatAtSchema, PtyChatTurnUsageSchema, PtyChatTurnFileSchema, PTY_CHAT_TURN_FILES_MAX, PtyChatEventPayloadSchema, PtyChatEventRequestSchema, PtyChatAttachRequestSchema, PTY_EFFORT_LEVELS, SetPtyModelEffortRequestSchema, cardDescription, AgentHeartbeatSchema, CreatePRInputSchema, PostToChatInputSchema, GetTaskContextRequestSchema, GetChatMessagesRequestSchema, GetTaskFilesRequestSchema, GetTaskFileRequestSchema, DeleteTaskFileRequestSchema, SetTaskFileShowcaseRequestSchema, GetTaskRequestSchema, GetCliHistoryRequestSchema, ListSubtasksRequestSchema, GetDependenciesRequestSchema, GetSuggestionsRequestSchema, ListManualTestsRequestSchema, QueryManualTestsRequestSchema, CreatePullRequestRequestSchema, RequestFileUploadRequestSchema, ConfirmFileUploadRequestSchema, UpdateTaskStatusRequestSchema, StoreSessionIdRequestSchema, SetManualTestsRequestSchema, EditManualTestRequestSchema, RemoveManualTestRequestSchema, ApproveManualTestRequestSchema, RejectManualTestRequestSchema, SessionStartRequestSchema, SessionStopRequestSchema, EndReviewSessionRequestSchema, ConnectAgentRequestSchema, ReportAgentStatusRequestSchema, NotifyAgentVersionRequestSchema, DiscoveredPortSchema, ReportDiscoveredPortsRequestSchema, ReportBootMilestoneRequestSchema, CreateSubtaskRequestSchema, UpdateSubtaskRequestSchema, DeleteSubtaskRequestSchema, SetSubtaskParentRequestSchema, GetTaskPropertiesRequestSchema, UpdateTaskFieldsRequestSchema, UpdateTaskPropertiesRequestSchema, ListIconsRequestSchema, GenerateTaskIconRequestSchema, SearchFaIconsRequestSchema, PickFaIconRequestSchema, CreateFollowUpTaskRequestSchema, AddDependencyRequestSchema, RemoveDependencyRequestSchema, CreateSuggestionRequestSchema, VoteSuggestionRequestSchema, TriggerIdentificationRequestSchema, HandoffToImplementerRequestSchema, SubmitCodeReviewResultRequestSchema, CycleCodingAgentKeyRequestSchema, PostChildChatMessageRequestSchema, UpdateChildStatusRequestSchema, GetAgentStatusRequestSchema, GetUiCliHistoryRequestSchema, GetActivePtySessionRequestSchema, ListActivePtySessionsRequestSchema, SendSoftStopRequestSchema, StopTaskSessionRequestSchema, FlushTaskQueueRequestSchema, CancelTaskQueuedMessageRequestSchema, FlushSingleQueuedMessageRequestSchema, AnswerAgentQuestionRequestSchema, ClearAgentTodosRequestSchema, AgentQuestionOptionSchema, AgentQuestionSchema, AskUserQuestionRequestSchema, PostAgentMessageRequestSchema, EmitAgentEventRequestSchema, RefreshGithubTokenRequestSchema, ReportCredentialFailureRequestSchema, ReportReviewSpawnFailureRequestSchema, ReportBuilderSpawnFailureRequestSchema, SpawnTaskSessionRequestSchema, StartCodeReviewRequestSchema, StopCodeReviewRequestSchema, ReportSessionSpawnFailureRequestSchema, RefreshGithubTokenResponseSchema, PTY_FRAME_MAX_CHARS, PTY_MAX_DIMENSION, PtyOutputRequestSchema, PtyEndedRequestSchema, PtyInputRequestSchema, PtyResizeRequestSchema, PtyAttachRequestSchema, ReportPtyStreamRequestSchema, ReportPtyInputRequestSchema, GetPtyStreamEndpointRequestSchema, CreatePRResponseSchema, PostToChatResponseSchema, UpdateTaskStatusResponseSchema, StoreSessionIdResponseSchema, HeartbeatResponseSchema, SessionStartResponseSchema, SessionStopResponseSchema, DeleteSubtaskResponseSchema, ParkOnCheckResultRequestSchema, GIT_BRANCH_NAME_MAX, GIT_BRANCH_NAME_MESSAGE, ALLOWED_REF, cardDescription2, ListAccessibleProjectsRequestSchema, ListProjectTasksRequestSchema, GetProjectTaskRequestSchema, SearchProjectTasksRequestSchema, ListProjectTagsRequestSchema, GetProjectTagRequestSchema, ListProjectTagAttachmentsRequestSchema, SetProjectFileTagsRequestSchema, GetProjectSummaryRequestSchema, GetProjectOnboardingStatusRequestSchema, GetProjectOnboardingStepRequestSchema, GetProjectConnectUrlsRequestSchema, conveyorCapabilitySchema, GetConnectionContextRequestSchema, VerifyConnectionRequestSchema, ListAccessibleSubprojectsRequestSchema, CreateProjectTaskRequestSchema, SetProjectTaskParentRequestSchema, UpdateProjectTaskRequestSchema, TransitionProjectTaskStatusRequestSchema, MoveProjectCardRequestSchema, PostToProjectTaskChatRequestSchema, GetProjectTaskCliRequestSchema, GetProjectTaskSessionsRequestSchema, QueryProjectGcpLogsRequestSchema, QueryProjectGrafanaLogsRequestSchema, driveFileNameSchema, DRIVE_MAX_CONTENT_CHARS, ListProjectDriveFilesRequestSchema, ReadProjectDriveFileRequestSchema, CreateProjectDriveFileRequestSchema, UpdateProjectDriveFileRequestSchema, DeleteProjectDriveFileRequestSchema, CreateProjectDriveFolderRequestSchema, StartProjectBuildRequestSchema, StopProjectBuildRequestSchema, StartProjectWorkspaceRequestSchema, StopProjectWorkspaceRequestSchema, ListMyLiveSessionsRequestSchema, ListProjectSessionGroupsRequestSchema, ListMyLiveSessionsAcrossProjectsRequestSchema, ListSessionGroupsAcrossProjectsRequestSchema, GetProjectAvailableTuisRequestSchema, StartAdhocSessionRequestSchema, StopAdhocSessionRequestSchema, ResumeAdhocSessionRequestSchema, RefreshCodingAgentKeyUsageRequestSchema, ListKeysToProbeRequestSchema, CreateProjectReleaseRequestSchema, AddTasksToProjectReleaseRequestSchema, ApproveProjectMergePRRequestSchema, ListProjectSubtasksRequestSchema, CreateProjectSubtaskRequestSchema, UpdateProjectSubtaskRequestSchema, DeleteProjectSubtaskRequestSchema, GetProjectTaskChatRequestSchema, AddProjectTaskDependencyRequestSchema, RemoveProjectTaskDependencyRequestSchema, VoteProjectSuggestionRequestSchema, GetProjectTaskDependenciesRequestSchema, CreateProjectPullRequestRequestSchema, ListProjectMembersRequestSchema, AddProjectTaskReviewerRequestSchema, RemoveProjectTaskReviewerRequestSchema, ListProjectManualTestsRequestSchema, QueryProjectManualTestsRequestSchema, SetProjectManualTestsRequestSchema, EditProjectManualTestRequestSchema, RemoveProjectManualTestRequestSchema, ApproveProjectManualTestRequestSchema, RejectProjectManualTestRequestSchema, CreateProjectSuggestionRequestSchema, ListProjectChannelsRequestSchema, READ_CHANNEL_MESSAGES_MAX_LIMIT, ReadChannelMessagesRequestSchema, POST_CHANNEL_MESSAGE_MAX_CHARS, PostChannelMessageRequestSchema, GET_ANALYTICS_SUMMARY_MAX_RANGE_DAYS, GetProjectAnalyticsSummaryRequestSchema, ListProjectTaskFilesRequestSchema, GetProjectAttachmentRequestSchema, DeleteProjectTaskFileRequestSchema, SetProjectTaskFileShowcaseRequestSchema, RequestProjectFileUploadRequestSchema, ConfirmProjectFileUploadRequestSchema, RequestWorkspaceRecycleRequestSchema, ReportApiOutageRequestSchema, CONTEXT_LINK_LOCATOR_MAX, TAG_DESCRIPTION_MAX, TAG_OVERVIEW_MAX, TAG_REASON_MAX, ProjectTagContextPathSchema, hexColor, overviewPathSchema, CreateProjectTagRequestSchema, UpdateProjectTagRequestSchema, PostToProjectChatRequestSchema, StartTagAuditRequestSchema, StartTaskAuditRequestSchema, GetActiveAuditSessionsRequestSchema, ReportTaskAuditResultRequestSchema, GetTaskAuditsRequestSchema, GetTaskAuditRequestSchema, GetTaskAuditAggregatesRequestSchema, DeleteTaskAuditRequestSchema, MarkInitialPromptSubmittedRequestSchema, PERSONAL_CLUSTER_EVENTS, PERSONAL_CLUSTER_HEARTBEAT_INTERVAL_MS, DECISION_STATUS_LABELS, DECISION_OPTION_KINDS, DECISION_VOTE_SOURCES, DECISION_OPTIONS_MIN, DECISION_OPTIONS_MAX, DECISION_OPTION_LABEL_MAX, DECISION_QUESTION_MAX, DECISION_RESOLUTION_MAX, DECISION_VOTE_REASON_MAX, DECISION_DEADLINE_MIN_HOURS, DECISION_DEADLINE_MAX_DAYS, DECISION_DEADLINE_MAX_HOURS, DECISION_LIST_MAX_LIMIT, decisionIdSchema, optionInputSchema, optionsArraySchema, CreateDecisionRequestSchema, VoteDecisionRequestSchema, RemoveDecisionVoteRequestSchema, ResolveDecisionRequestSchema, CancelDecisionRequestSchema, BulkDecisionIdsRequestSchema, ReopenDecisionRequestSchema, UpdateDecisionRequestSchema, GetDecisionRequestSchema, ListDecisionsRequestSchema, optionInput, optionsInput, deadlineHours, CreateProjectDecisionRequestSchema, ListProjectDecisionsRequestSchema, GetProjectDecisionRequestSchema, VoteProjectDecisionRequestSchema, ResolveProjectDecisionRequestSchema, UpdateProjectDecisionRequestSchema, EVALUATE_MAX_QUESTIONS, EVALUATE_MAX_ITEMS, EVALUATE_ITEM_MAX_CHARS, EVALUATE_REQUEST_MAX_CHARS, TEXT_MAX, QUESTION_KEY, EvaluateQuestionSchema, EvaluateQuestionsRequestSchema, MEETING_CHECKLIST_TITLE_MAX, MEETING_TRANSCRIPT_MAX_CHARS, MEETING_TITLE_MAX, MEETING_OCCURRED_AT_MIN_YEAR, MEETING_OCCURRED_AT_MAX_FUTURE_MS, OCCURRED_AT_RANGE_MESSAGE, MeetingOccurredAtSchema, CreateMeetingFromTranscriptRequestSchema, GetMeetingRequestSchema, UpdateMeetingRequestSchema, RegenerateMeetingSummaryRequestSchema, DeleteMeetingRequestSchema, checklistTitle, ListMeetingChecklistRequestSchema, AddMeetingChecklistItemsRequestSchema, UpdateMeetingChecklistItemRequestSchema, DeleteMeetingChecklistItemRequestSchema, SetMeetingChecklistItemCheckedRequestSchema, ListMeetingsRequestSchema, ReadMeetingTranscriptRequestSchema, MEETING_SUMMARY_MAX_CHARS, AddProjectMeetingChecklistItemsRequestSchema, CheckProjectMeetingChecklistItemRequestSchema, EditProjectMeetingChecklistItemRequestSchema, RemoveProjectMeetingChecklistItemRequestSchema, CreateProjectMeetingRequestSchema, UpdateProjectMeetingRequestSchema, TASK_CHAT_HISTORY_LIMIT, PM_CHAT_HISTORY_LIMIT, AGENT_CHAT_HISTORY_FETCH_LIMIT, ANTHROPIC_CATALOG, CLAUDESPACE_WORKLOAD_LABEL, CLAUDESPACE_WORKLOAD_VALUE, CONVEYOR_POD_SELECTOR, CONTAINERD_K8S_NAMESPACE, CONTAINERD_PIN_LABEL, CONVEYOR_CONTENT_HASH_LABEL, VM_IMPORT_DISCOVERY_DIR, LOCAL_BAKE_ROOT_COMMANDS, PERSONAL_LOCAL_REGISTRY_NAMESPACE, PERSONAL_LOCAL_REGISTRY_NAME, PERSONAL_LOCAL_REGISTRY_PORT, PERSONAL_LOCAL_REGISTRY_HOST, PERSONAL_LOCAL_REGISTRY_NODE_ENDPOINT, BAKE_COMPONENT_LABEL, BAKE_COMPONENT_PREBAKE, BAKE_COMPONENT_STATE_BAKE, BAKE_COMPONENT_VALUES, MS_PER_DAY, POSTGRES_ENV, FIREBASE_EMULATOR_COMMAND, FIREBASE_EMULATOR_ENV, CATALOG, SERVICE_DEFINITIONS, SUPPORTED_CLAUDESPACE_DEPS, STATEFUL_BAKE_DEPS, MIRRORABLE_SIDECARS, LEVELS_PER_BAND, PRESTIGE_TIERS, TOP_PRESTIGE_BAND, CARD_TYPE_SURFACE, surfaceTypes, BOARD_CARD_TYPES, REPORT_CARD_TYPES, DECISION_CARD_TYPES;
4522
+ var ACHIEVEMENT_RARITIES, RISK_LEVELS, riskLevelSchema, DEFAULT_RISK_LEVELS, LEVEL_BY_VALUE, ACTIVE_WORK_STATUSES, IDENTIFIED_WORK_STATUSES, DEFAULT_TASK_STATUS_COLOR, DEFAULT_TASK_STATUS_COLOR_INT, MAX_FILE_SIZE_BYTES, MAX_FILE_TAGS, MAX_FILE_TAG_LENGTH, ALLOWED_FILE_MIME_TYPES, ALLOWED_FILE_ACCEPT, EMBED_THRESHOLD_IMAGES, EMBED_THRESHOLD_TEXT, IDLE_HEARTBEAT_MS, TurnEndToolCallSchema, AgentTurnUsageSchema, KnownAgentEventSchema, AgentEventSchema, PtyChatAtSchema, PtyChatTurnUsageSchema, PtyChatTurnFileSchema, PTY_CHAT_TURN_FILES_MAX, PtyChatEventPayloadSchema, PtyChatEventRequestSchema, PtyChatAttachRequestSchema, PTY_EFFORT_LEVELS, SetPtyModelEffortRequestSchema, cardDescription, AgentHeartbeatSchema, CreatePRInputSchema, PostToChatInputSchema, GetTaskContextRequestSchema, GetChatMessagesRequestSchema, GetTaskFilesRequestSchema, GetTaskFileRequestSchema, DeleteTaskFileRequestSchema, SetTaskFileShowcaseRequestSchema, GetTaskRequestSchema, GetCliHistoryRequestSchema, ListSubtasksRequestSchema, GetDependenciesRequestSchema, GetSuggestionsRequestSchema, ListManualTestsRequestSchema, QueryManualTestsRequestSchema, CreatePullRequestRequestSchema, RequestFileUploadRequestSchema, ConfirmFileUploadRequestSchema, UpdateTaskStatusRequestSchema, StoreSessionIdRequestSchema, SetManualTestsRequestSchema, EditManualTestRequestSchema, RemoveManualTestRequestSchema, ApproveManualTestRequestSchema, RejectManualTestRequestSchema, SessionStartRequestSchema, SessionStopRequestSchema, EndReviewSessionRequestSchema, ConnectAgentRequestSchema, ReportAgentStatusRequestSchema, NotifyAgentVersionRequestSchema, DiscoveredPortSchema, ReportDiscoveredPortsRequestSchema, ReportBootMilestoneRequestSchema, CreateSubtaskRequestSchema, UpdateSubtaskRequestSchema, DeleteSubtaskRequestSchema, SetSubtaskParentRequestSchema, GetTaskPropertiesRequestSchema, UpdateTaskFieldsRequestSchema, UpdateTaskPropertiesRequestSchema, ListIconsRequestSchema, GenerateTaskIconRequestSchema, SearchFaIconsRequestSchema, PickFaIconRequestSchema, CreateFollowUpTaskRequestSchema, AddDependencyRequestSchema, RemoveDependencyRequestSchema, CreateSuggestionRequestSchema, VoteSuggestionRequestSchema, TriggerIdentificationRequestSchema, HandoffToImplementerRequestSchema, SubmitCodeReviewResultRequestSchema, CycleCodingAgentKeyRequestSchema, PostChildChatMessageRequestSchema, UpdateChildStatusRequestSchema, GetAgentStatusRequestSchema, GetUiCliHistoryRequestSchema, GetActivePtySessionRequestSchema, ListActivePtySessionsRequestSchema, SendSoftStopRequestSchema, StopTaskSessionRequestSchema, FlushTaskQueueRequestSchema, CancelTaskQueuedMessageRequestSchema, FlushSingleQueuedMessageRequestSchema, AnswerAgentQuestionRequestSchema, ClearAgentTodosRequestSchema, AgentQuestionOptionSchema, AgentQuestionSchema, AskUserQuestionRequestSchema, PostAgentMessageRequestSchema, EmitAgentEventRequestSchema, RefreshGithubTokenRequestSchema, ReportCredentialFailureRequestSchema, ReportReviewSpawnFailureRequestSchema, ReportBuilderSpawnFailureRequestSchema, SpawnTaskSessionRequestSchema, StartCodeReviewRequestSchema, StopCodeReviewRequestSchema, ReportSessionSpawnFailureRequestSchema, RefreshGithubTokenResponseSchema, PTY_FRAME_MAX_CHARS, PTY_MAX_DIMENSION, PtyOutputRequestSchema, PtyEndedRequestSchema, PtyInputRequestSchema, PtyResizeRequestSchema, PtyAttachRequestSchema, ReportPtyStreamRequestSchema, ReportPtyInputRequestSchema, GetPtyStreamEndpointRequestSchema, CreatePRResponseSchema, PostToChatResponseSchema, UpdateTaskStatusResponseSchema, StoreSessionIdResponseSchema, HeartbeatResponseSchema, SessionStartResponseSchema, SessionStopResponseSchema, DeleteSubtaskResponseSchema, ParkOnCheckResultRequestSchema, GIT_BRANCH_NAME_MAX, GIT_BRANCH_NAME_MESSAGE, ALLOWED_REF, cardDescription2, ListAccessibleProjectsRequestSchema, ListProjectTasksRequestSchema, GetProjectTaskRequestSchema, SearchProjectTasksRequestSchema, ListProjectTagsRequestSchema, GetProjectTagRequestSchema, ListProjectTagAttachmentsRequestSchema, SetProjectFileTagsRequestSchema, GetProjectSummaryRequestSchema, GetProjectOnboardingStatusRequestSchema, GetProjectOnboardingStepRequestSchema, GetProjectConnectUrlsRequestSchema, conveyorCapabilitySchema, GetConnectionContextRequestSchema, VerifyConnectionRequestSchema, ListAccessibleSubprojectsRequestSchema, CreateProjectTaskRequestSchema, SetProjectTaskParentRequestSchema, UpdateProjectTaskRequestSchema, TransitionProjectTaskStatusRequestSchema, MoveProjectCardRequestSchema, PostToProjectTaskChatRequestSchema, GetProjectTaskCliRequestSchema, GetProjectTaskSessionsRequestSchema, QueryProjectGcpLogsRequestSchema, QueryProjectGrafanaLogsRequestSchema, driveFileNameSchema, DRIVE_MAX_CONTENT_CHARS, ListProjectDriveFilesRequestSchema, ReadProjectDriveFileRequestSchema, CreateProjectDriveFileRequestSchema, UpdateProjectDriveFileRequestSchema, DeleteProjectDriveFileRequestSchema, CreateProjectDriveFolderRequestSchema, StartProjectBuildRequestSchema, StopProjectBuildRequestSchema, StartProjectWorkspaceRequestSchema, StopProjectWorkspaceRequestSchema, ListMyLiveSessionsRequestSchema, ListProjectSessionGroupsRequestSchema, ListMyLiveSessionsAcrossProjectsRequestSchema, ListSessionGroupsAcrossProjectsRequestSchema, GetProjectAvailableTuisRequestSchema, StartAdhocSessionRequestSchema, StopAdhocSessionRequestSchema, ResumeAdhocSessionRequestSchema, RefreshCodingAgentKeyUsageRequestSchema, ListKeysToProbeRequestSchema, CreateProjectReleaseRequestSchema, AddTasksToProjectReleaseRequestSchema, ApproveProjectMergePRRequestSchema, ListProjectSubtasksRequestSchema, CreateProjectSubtaskRequestSchema, UpdateProjectSubtaskRequestSchema, DeleteProjectSubtaskRequestSchema, GetProjectTaskChatRequestSchema, AddProjectTaskDependencyRequestSchema, RemoveProjectTaskDependencyRequestSchema, VoteProjectSuggestionRequestSchema, GetProjectTaskDependenciesRequestSchema, CreateProjectPullRequestRequestSchema, ListProjectMembersRequestSchema, AddProjectTaskReviewerRequestSchema, RemoveProjectTaskReviewerRequestSchema, ListProjectManualTestsRequestSchema, QueryProjectManualTestsRequestSchema, SetProjectManualTestsRequestSchema, EditProjectManualTestRequestSchema, RemoveProjectManualTestRequestSchema, ApproveProjectManualTestRequestSchema, RejectProjectManualTestRequestSchema, CreateProjectSuggestionRequestSchema, ListProjectChannelsRequestSchema, READ_CHANNEL_MESSAGES_MAX_LIMIT, ReadChannelMessagesRequestSchema, POST_CHANNEL_MESSAGE_MAX_CHARS, PostChannelMessageRequestSchema, GET_ANALYTICS_SUMMARY_MAX_RANGE_DAYS, GetProjectAnalyticsSummaryRequestSchema, ListProjectTaskFilesRequestSchema, GetProjectAttachmentRequestSchema, DeleteProjectTaskFileRequestSchema, SetProjectTaskFileShowcaseRequestSchema, RequestProjectFileUploadRequestSchema, ConfirmProjectFileUploadRequestSchema, RequestWorkspaceRecycleRequestSchema, ReportApiOutageRequestSchema, CONTEXT_LINK_LOCATOR_MAX, TAG_DESCRIPTION_MAX, TAG_OVERVIEW_MAX, TAG_REASON_MAX, ProjectTagContextPathSchema, hexColor, overviewPathSchema, CreateProjectTagRequestSchema, UpdateProjectTagRequestSchema, PostToProjectChatRequestSchema, StartTagAuditRequestSchema, StartTaskAuditRequestSchema, GetActiveAuditSessionsRequestSchema, ReportTaskAuditResultRequestSchema, GetTaskAuditsRequestSchema, GetTaskAuditRequestSchema, GetTaskAuditAggregatesRequestSchema, DeleteTaskAuditRequestSchema, MarkInitialPromptSubmittedRequestSchema, PERSONAL_CLUSTER_EVENTS, PERSONAL_CLUSTER_HEARTBEAT_INTERVAL_MS, DECISION_STATUS_LABELS, DECISION_OPTION_KINDS, DECISION_VOTE_SOURCES, DECISION_OPTIONS_MIN, DECISION_OPTIONS_MAX, DECISION_OPTION_LABEL_MAX, DECISION_QUESTION_MAX, DECISION_RESOLUTION_MAX, DECISION_VOTE_REASON_MAX, DECISION_DEADLINE_MIN_HOURS, DECISION_DEADLINE_MAX_DAYS, DECISION_DEADLINE_MAX_HOURS, DECISION_LIST_MAX_LIMIT, decisionIdSchema, optionInputSchema, optionsArraySchema, CreateDecisionRequestSchema, VoteDecisionRequestSchema, RemoveDecisionVoteRequestSchema, ResolveDecisionRequestSchema, CancelDecisionRequestSchema, BulkDecisionIdsRequestSchema, ReopenDecisionRequestSchema, UpdateDecisionRequestSchema, GetDecisionRequestSchema, ListDecisionsRequestSchema, optionInput, optionsInput, deadlineHours, CreateProjectDecisionRequestSchema, ListProjectDecisionsRequestSchema, GetProjectDecisionRequestSchema, VoteProjectDecisionRequestSchema, ResolveProjectDecisionRequestSchema, UpdateProjectDecisionRequestSchema, EVALUATE_MAX_QUESTIONS, EVALUATE_MAX_ITEMS, EVALUATE_ITEM_MAX_CHARS, EVALUATE_REQUEST_MAX_CHARS, TEXT_MAX, QUESTION_KEY, EvaluateQuestionSchema, EvaluateQuestionsRequestSchema, MEETING_CHECKLIST_TITLE_MAX, MEETING_TRANSCRIPT_MAX_CHARS, MEETING_TITLE_MAX, MEETING_OCCURRED_AT_MIN_YEAR, MEETING_OCCURRED_AT_MAX_FUTURE_MS, OCCURRED_AT_RANGE_MESSAGE, MeetingOccurredAtSchema, CreateMeetingFromTranscriptRequestSchema, GetMeetingRequestSchema, UpdateMeetingRequestSchema, RegenerateMeetingSummaryRequestSchema, DeleteMeetingRequestSchema, checklistTitle, ListMeetingChecklistRequestSchema, AddMeetingChecklistItemsRequestSchema, UpdateMeetingChecklistItemRequestSchema, DeleteMeetingChecklistItemRequestSchema, SetMeetingChecklistItemCheckedRequestSchema, ListMeetingsRequestSchema, ReadMeetingTranscriptRequestSchema, MEETING_SUMMARY_MAX_CHARS, AddProjectMeetingChecklistItemsRequestSchema, CheckProjectMeetingChecklistItemRequestSchema, EditProjectMeetingChecklistItemRequestSchema, RemoveProjectMeetingChecklistItemRequestSchema, CreateProjectMeetingRequestSchema, UpdateProjectMeetingRequestSchema, TASK_CHAT_HISTORY_LIMIT, PM_CHAT_HISTORY_LIMIT, AGENT_CHAT_HISTORY_FETCH_LIMIT, ANTHROPIC_CATALOG, CLAUDESPACE_WORKLOAD_LABEL, CLAUDESPACE_WORKLOAD_VALUE, CONVEYOR_POD_SELECTOR, CONTAINERD_K8S_NAMESPACE, CONTAINERD_PIN_LABEL, CONVEYOR_CONTENT_HASH_LABEL, VM_IMPORT_DISCOVERY_DIR, LOCAL_BAKE_ROOT_COMMANDS, PERSONAL_LOCAL_REGISTRY_NAMESPACE, PERSONAL_LOCAL_REGISTRY_NAME, PERSONAL_LOCAL_REGISTRY_PORT, PERSONAL_LOCAL_REGISTRY_HOST, PERSONAL_LOCAL_REGISTRY_NODE_ENDPOINT, BAKE_COMPONENT_LABEL, BAKE_COMPONENT_PREBAKE, BAKE_COMPONENT_STATE_BAKE, BAKE_COMPONENT_VALUES, PERSONAL_BAKE_NAMESPACE, MS_PER_DAY, POSTGRES_ENV, FIREBASE_EMULATOR_COMMAND, FIREBASE_EMULATOR_ENV, CATALOG, SERVICE_DEFINITIONS, SUPPORTED_CLAUDESPACE_DEPS, STATEFUL_BAKE_DEPS, MIRRORABLE_SIDECARS, LEVELS_PER_BAND, PRESTIGE_TIERS, TOP_PRESTIGE_BAND, CARD_TYPE_SURFACE, surfaceTypes, BOARD_CARD_TYPES, REPORT_CARD_TYPES, DECISION_CARD_TYPES;
4480
4523
  var init_dist = __esm({
4481
4524
  "../shared/dist/index.js"() {
4482
4525
  "use strict";
4483
- init_chunk_BH4SLNGU();
4526
+ init_chunk_VSUBG7GR();
4484
4527
  init_zod();
4485
4528
  init_zod();
4486
4529
  init_zod();
@@ -5026,7 +5069,8 @@ var init_dist = __esm({
5026
5069
  port: external_exports.number().int().min(1).max(65535),
5027
5070
  label: external_exports.string().min(1).max(64).optional(),
5028
5071
  protocol: external_exports.enum(["http", "tcp"]).optional(),
5029
- detectedAt: external_exports.string()
5072
+ detectedAt: external_exports.string(),
5073
+ hidden: external_exports.boolean().optional()
5030
5074
  });
5031
5075
  ReportDiscoveredPortsRequestSchema = external_exports.object({
5032
5076
  sessionId: external_exports.string(),
@@ -5173,7 +5217,11 @@ var init_dist = __esm({
5173
5217
  CycleCodingAgentKeyRequestSchema = external_exports.object({
5174
5218
  sessionId: external_exports.string(),
5175
5219
  rateLimitType: external_exports.string(),
5176
- resetsAt: external_exports.string().optional()
5220
+ resetsAt: external_exports.string().optional(),
5221
+ // The key the pod is actually running (CONVEYOR_CODING_AGENT_KEY_ID). Wins
5222
+ // over the session's attribution stamp after owner validation; optional so
5223
+ // older agents keep the session fallback.
5224
+ codingAgentKeyId: external_exports.string().optional()
5177
5225
  });
5178
5226
  PostChildChatMessageRequestSchema = external_exports.object({
5179
5227
  sessionId: external_exports.string(),
@@ -5578,11 +5626,12 @@ var init_dist = __esm({
5578
5626
  sqlInstances: external_exports.array(external_exports.string().min(1).max(200)).max(25).optional(),
5579
5627
  allServices: external_exports.boolean().optional(),
5580
5628
  search: external_exports.string().max(256).optional(),
5581
- filter: external_exports.string().max(1e3).optional(),
5629
+ filter: external_exports.string().max(2e3).optional(),
5582
5630
  startTime: external_exports.string().optional(),
5583
5631
  endTime: external_exports.string().optional(),
5584
5632
  limit: external_exports.number().int().min(1).max(200).optional().default(50),
5585
- pageToken: external_exports.string().max(4096).optional()
5633
+ pageToken: external_exports.string().max(4096).optional(),
5634
+ full: external_exports.boolean().optional()
5586
5635
  });
5587
5636
  QueryProjectGrafanaLogsRequestSchema = external_exports.object({
5588
5637
  projectId: external_exports.string(),
@@ -6511,6 +6560,7 @@ var init_dist = __esm({
6511
6560
  BAKE_COMPONENT_PREBAKE = "prebake";
6512
6561
  BAKE_COMPONENT_STATE_BAKE = "state-bake";
6513
6562
  BAKE_COMPONENT_VALUES = [BAKE_COMPONENT_PREBAKE, BAKE_COMPONENT_STATE_BAKE];
6563
+ PERSONAL_BAKE_NAMESPACE = "conveyor-bakes";
6514
6564
  MS_PER_DAY = 24 * 60 * 60 * 1e3;
6515
6565
  POSTGRES_ENV = {
6516
6566
  POSTGRES_HOST_AUTH_METHOD: "trust",
@@ -6830,48 +6880,6 @@ exec firebase emulators:start --only=auth --project=rally-cry-dev`
6830
6880
  }
6831
6881
  });
6832
6882
 
6833
- // src/lib/journald.ts
6834
- function renderJournaldDropIn() {
6835
- return [
6836
- "# Written by conveyor-k3. Caps the journal so kubelet and container logs",
6837
- "# cannot fill the disk the image store needs. Removed by uninstall.",
6838
- "[Journal]",
6839
- "SystemMaxUse=1G",
6840
- "SystemKeepFree=2G",
6841
- "MaxRetentionSec=14day",
6842
- ""
6843
- ].join("\n");
6844
- }
6845
- async function ensureJournaldCap(ports) {
6846
- const content = renderJournaldDropIn();
6847
- if (await ports.fs.readFile(JOURNALD_DROPIN) === content) return false;
6848
- await ports.fs.writeFile(JOURNALD_DROPIN, content, 420);
6849
- await runOrThrow(ports.exec, "systemctl", ["restart", "systemd-journald"], {
6850
- timeoutMs: 6e4
6851
- });
6852
- return true;
6853
- }
6854
- function journaldCapScript() {
6855
- const encoded = Buffer.from(renderJournaldDropIn(), "utf8").toString("base64");
6856
- return [
6857
- "set -eu",
6858
- `dropin=${JOURNALD_DROPIN}`,
6859
- 'tmp="$(mktemp)"',
6860
- `trap 'rm -f "$tmp"' EXIT`,
6861
- `printf '%s' '${encoded}' | base64 -d > "$tmp"`,
6862
- 'if cmp -s "$tmp" "$dropin"; then exit 0; fi',
6863
- 'install -D -m 0644 "$tmp" "$dropin"',
6864
- "systemctl restart systemd-journald"
6865
- ].join("\n");
6866
- }
6867
- var init_journald = __esm({
6868
- "src/lib/journald.ts"() {
6869
- "use strict";
6870
- init_exec();
6871
- init_paths();
6872
- }
6873
- });
6874
-
6875
6883
  // src/vm/capacity.ts
6876
6884
  function inBounds(key, value) {
6877
6885
  const { min, max } = CAPACITY_BOUNDS[key];
@@ -6975,117 +6983,357 @@ var init_capacity = __esm({
6975
6983
  }
6976
6984
  });
6977
6985
 
6978
- // src/vm/instance-name.ts
6979
- function isNamedInstance(value) {
6980
- return typeof value === "string" && NAMED_INSTANCE.test(value) && value !== DEFAULT_INSTANCE && value !== VM_NAME;
6986
+ // src/lib/journald.ts
6987
+ function renderJournaldDropIn() {
6988
+ return [
6989
+ "# Written by conveyor-k3. Caps the journal so kubelet and container logs",
6990
+ "# cannot fill the disk the image store needs. Removed by uninstall.",
6991
+ "[Journal]",
6992
+ "SystemMaxUse=1G",
6993
+ "SystemKeepFree=2G",
6994
+ "MaxRetentionSec=14day",
6995
+ ""
6996
+ ].join("\n");
6981
6997
  }
6982
- function parseInstanceName(value) {
6983
- if (value === void 0 || value === DEFAULT_INSTANCE) return DEFAULT_INSTANCE;
6984
- if (value === true) throw new Error("Use --instance=<name>.");
6985
- if (value === VM_NAME)
6986
- throw new Error(`"${VM_NAME}" is reserved for the default VM. Omit --instance to use it.`);
6987
- if (!isNamedInstance(value))
6988
- throw new Error(
6989
- "Use an instance name of 1-12 lowercase letters, digits, or single hyphens, starting with a letter."
6990
- );
6991
- return value;
6998
+ async function ensureJournaldCap(ports) {
6999
+ const content = renderJournaldDropIn();
7000
+ if (await ports.fs.readFile(JOURNALD_DROPIN) === content) return false;
7001
+ await ports.fs.writeFile(JOURNALD_DROPIN, content, 420);
7002
+ await runOrThrow(ports.exec, "systemctl", ["restart", "systemd-journald"], {
7003
+ timeoutMs: 6e4
7004
+ });
7005
+ return true;
6992
7006
  }
6993
- function vmNameFor(instance) {
6994
- return instance === DEFAULT_INSTANCE ? VM_NAME : instance;
7007
+ function journaldCapScript() {
7008
+ const encoded = Buffer.from(renderJournaldDropIn(), "utf8").toString("base64");
7009
+ return [
7010
+ "set -eu",
7011
+ `dropin=${JOURNALD_DROPIN}`,
7012
+ 'tmp="$(mktemp)"',
7013
+ `trap 'rm -f "$tmp"' EXIT`,
7014
+ `printf '%s' '${encoded}' | base64 -d > "$tmp"`,
7015
+ 'if cmp -s "$tmp" "$dropin"; then exit 0; fi',
7016
+ 'install -D -m 0644 "$tmp" "$dropin"',
7017
+ "systemctl restart systemd-journald"
7018
+ ].join("\n");
6995
7019
  }
6996
- var VM_NAME, DEFAULT_INSTANCE, NAMED_INSTANCE;
6997
- var init_instance_name = __esm({
6998
- "src/vm/instance-name.ts"() {
7020
+ var init_journald = __esm({
7021
+ "src/lib/journald.ts"() {
6999
7022
  "use strict";
7000
- VM_NAME = "conveyor";
7001
- DEFAULT_INSTANCE = "default";
7002
- NAMED_INSTANCE = /^(?=.{1,12}$)[a-z](?:-?[a-z0-9])*$/;
7023
+ init_exec();
7024
+ init_paths();
7003
7025
  }
7004
7026
  });
7005
7027
 
7006
- // src/vm/host-state.ts
7007
- import { access, chmod as chmod2, lstat, mkdir as mkdir3, readFile as readFile2, rename as rename2, rm as rm3, writeFile as writeFile3 } from "fs/promises";
7008
- import { homedir, uptime } from "os";
7009
- import { join } from "path";
7010
- import { setTimeout as sleep } from "timers/promises";
7011
- function verifiedLink(state) {
7012
- return state.enrolled && (state.linkVerified ?? !!(state.linkedAccountLabel || state.linkedDisplayName));
7013
- }
7014
- function vmProfile() {
7015
- const profile = process.env.CONVEYOR_VM_TEST_PROFILE;
7016
- if (profile !== void 0 && !/^acceptance-[a-z0-9-]{1,32}$/.test(profile)) {
7017
- throw new Error("Test VM profiles must match acceptance-[a-z0-9-]{1,32}.");
7018
- }
7019
- return profile;
7028
+ // src/vm/device-link.ts
7029
+ function isTransientPairingError(error) {
7030
+ if (error instanceof DevicePairingRequestError)
7031
+ return error.status === 429 || error.status !== void 0 && error.status >= 500;
7032
+ return error instanceof Error && /timed out|network|fetch failed/i.test(error.message);
7020
7033
  }
7021
- function instancesBase() {
7022
- const profile = vmProfile();
7023
- return join(
7024
- homedir(),
7025
- ".local",
7026
- "share",
7027
- profile ? `conveyor-k3-${profile}-vms` : "conveyor-k3-vms"
7028
- );
7034
+ function validPublicLabel(value) {
7035
+ return typeof value === "string" && value.length > 0 && value.length <= 160 && [...value].every((character) => {
7036
+ const code = character.charCodeAt(0);
7037
+ return code >= 32 && code !== 127;
7038
+ });
7029
7039
  }
7030
- function hostPaths(instance = DEFAULT_INSTANCE) {
7031
- const name2 = parseInstanceName(instance);
7032
- if (name2 !== DEFAULT_INSTANCE) return pathsForRoot(join(instancesBase(), name2), name2);
7033
- const profile = vmProfile();
7034
- return pathsForRoot(
7035
- join(homedir(), ".local", "share", profile ? `conveyor-k3-${profile}` : "conveyor-k3-vm")
7040
+ function validDevicePublicKey(value) {
7041
+ return typeof value === "string" && Buffer.byteLength(value) <= 1024 && /^-----BEGIN PUBLIC KEY-----\n(?:[A-Za-z0-9+/=]{1,128}\n)+-----END PUBLIC KEY-----\n?$/.test(
7042
+ value
7036
7043
  );
7037
7044
  }
7038
- function pathsForRoot(root, instance = DEFAULT_INSTANCE) {
7039
- const vmName = vmNameFor(parseInstanceName(instance));
7040
- const instanceDir = join(root, "lima", vmName);
7041
- return {
7042
- root,
7043
- limaHome: join(root, "lima"),
7044
- state: join(root, "state.json"),
7045
- network: join(root, "network.json"),
7046
- template: join(root, "conveyor.yaml"),
7047
- wrapper: join(root, "qemu-wrapper"),
7048
- wrapperJs: join(root, "vm-qemu.js"),
7049
- applied: join(root, "applied-network.json"),
7050
- guard: join(root, "guard-state.json"),
7051
- startRequest: join(root, "start-request.json"),
7052
- policyTransaction: join(root, "policy-transaction.json"),
7053
- lock: join(root, "operation.lock"),
7054
- /** Written by an owner stop, so a supervising service does not restart the VM. */
7055
- stopIntent: join(root, "stop-intent.json"),
7056
- instance,
7057
- vmName,
7058
- instanceDir,
7059
- limaConfig: join(instanceDir, "lima.yaml")
7060
- };
7045
+ function devOrigin() {
7046
+ const value = process.env.CONVEYOR_DEVICE_LINK_DEV_ORIGIN;
7047
+ if (!value) return void 0;
7048
+ const url = new URL(value);
7049
+ if (url.pathname !== "/" || url.search || url.hash || url.username || url.password)
7050
+ throw new Error("CONVEYOR_DEVICE_LINK_DEV_ORIGIN must be an origin.");
7051
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && url.hostname === "localhost"))
7052
+ throw new Error("The explicit development pairing origin must use HTTPS or localhost HTTP.");
7053
+ return url.origin;
7061
7054
  }
7062
- function missingVm(paths, defaultCopy = "No Conveyor VM exists. Run conveyor-k3 vm setup first.") {
7063
- return paths.instance === DEFAULT_INSTANCE ? defaultCopy : `No Conveyor VM named ${paths.instance} exists. Run conveyor-k3 vm setup --instance=${paths.instance} first.`;
7055
+ function devWebOrigin() {
7056
+ const value = process.env.CONVEYOR_DEVICE_LINK_DEV_WEB_ORIGIN;
7057
+ if (!value) return void 0;
7058
+ const url = new URL(value);
7059
+ if (url.pathname !== "/" || url.search || url.hash || url.username || url.password)
7060
+ throw new Error("CONVEYOR_DEVICE_LINK_DEV_WEB_ORIGIN must be an origin.");
7061
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && url.hostname === "localhost"))
7062
+ throw new Error("The explicit development browser origin must use HTTPS or localhost HTTP.");
7063
+ return url.origin;
7064
7064
  }
7065
- async function vmOperationInProgress(paths) {
7066
- const editing = await Promise.all(
7067
- [paths.lock, paths.policyTransaction].map(
7068
- (path) => access(path).then(
7069
- () => true,
7070
- () => false
7071
- )
7072
- )
7073
- );
7074
- return editing.some(Boolean);
7065
+ function trustedDeviceApiUrl(value) {
7066
+ const requested = new URL(value ?? PRODUCTION_API);
7067
+ const development = devOrigin();
7068
+ const allowed = /* @__PURE__ */ new Set([PRODUCTION_API, ...development ? [development] : []]);
7069
+ if (requested.pathname !== "/" || requested.search || requested.hash || !allowed.has(requested.origin))
7070
+ throw new Error(
7071
+ "Pairing API must be the production origin or the explicitly configured development origin."
7072
+ );
7073
+ return requested.origin;
7075
7074
  }
7076
- async function privateDirectory(path) {
7077
- await mkdir3(path, { recursive: true, mode: 448 });
7078
- const stat2 = await lstat(path);
7079
- if (!stat2.isDirectory() || stat2.isSymbolicLink() || stat2.uid !== process.getuid?.()) {
7080
- throw new Error(`Refusing unsafe VM directory: ${path}`);
7075
+ function validVerificationUri(value) {
7076
+ if (typeof value !== "string") return false;
7077
+ try {
7078
+ const url = new URL(value);
7079
+ const development = devWebOrigin();
7080
+ const allowed = /* @__PURE__ */ new Set([PRODUCTION_WEB, ...development ? [development] : []]);
7081
+ return allowed.has(url.origin) && url.pathname === "/personal-compute/link" && url.searchParams.size === 1 && /^[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}$/.test(url.searchParams.get("code") ?? "") && !url.username && !url.password && !url.hash;
7082
+ } catch {
7083
+ return false;
7081
7084
  }
7082
- await chmod2(path, 448);
7083
7085
  }
7084
- async function atomicPrivateWrite(path, content, mode = 384) {
7085
- await assertSafeExistingFile(path);
7086
- const temporary = `${path}.${process.pid}.tmp`;
7087
- await writeFile3(temporary, content, { mode, flag: "wx" });
7088
- try {
7086
+ async function responseJson(response) {
7087
+ const length = Number(response.headers.get("content-length"));
7088
+ if (Number.isFinite(length) && (length < 0 || length > MAX_RESPONSE_BYTES))
7089
+ throw new Error("Pairing API response is too large.");
7090
+ if (!response.body) throw new Error("Pairing API returned an empty response.");
7091
+ const reader = response.body.getReader();
7092
+ const chunks = [];
7093
+ let size = 0;
7094
+ try {
7095
+ while (true) {
7096
+ const next = await reader.read();
7097
+ if (next.done) break;
7098
+ size += next.value.byteLength;
7099
+ if (size > MAX_RESPONSE_BYTES) throw new Error("Pairing API response is too large.");
7100
+ chunks.push(next.value);
7101
+ }
7102
+ } finally {
7103
+ reader.releaseLock();
7104
+ }
7105
+ const bytes = new Uint8Array(size);
7106
+ let offset = 0;
7107
+ for (const chunk of chunks) {
7108
+ bytes.set(chunk, offset);
7109
+ offset += chunk.byteLength;
7110
+ }
7111
+ try {
7112
+ return JSON.parse(new TextDecoder().decode(bytes));
7113
+ } catch {
7114
+ throw new Error("Pairing API returned invalid JSON.");
7115
+ }
7116
+ }
7117
+ async function request(api, path, body, signal) {
7118
+ const timeout = AbortSignal.timeout(15e3);
7119
+ const combined = signal ? AbortSignal.any([signal, timeout]) : timeout;
7120
+ let response;
7121
+ try {
7122
+ response = await fetch(`${api}${path}`, {
7123
+ method: "POST",
7124
+ headers: { "content-type": "application/json", accept: "application/json" },
7125
+ body: JSON.stringify(body),
7126
+ redirect: "error",
7127
+ signal: combined
7128
+ });
7129
+ } catch (error) {
7130
+ if (combined.aborted)
7131
+ throw new Error("Pairing request timed out or was cancelled.", { cause: error });
7132
+ throw error;
7133
+ }
7134
+ if (!response.ok) {
7135
+ const failure = await responseJson(response).catch(() => null);
7136
+ const detail2 = typeof failure?.error === "string" && failure.error.length <= 300 && [...failure.error].every(
7137
+ (character) => character.charCodeAt(0) >= 32 && character.charCodeAt(0) !== 127
7138
+ ) ? failure.error : void 0;
7139
+ throw new DevicePairingRequestError(
7140
+ detail2 ?? `Pairing request failed (${response.status}).`,
7141
+ response.status
7142
+ );
7143
+ }
7144
+ return responseJson(response);
7145
+ }
7146
+ async function startPairing(api, name2, devicePublicKey, signal, options) {
7147
+ if (!validDevicePublicKey(devicePublicKey))
7148
+ throw new Error("Guest device public key is invalid.");
7149
+ const value = await request(
7150
+ api,
7151
+ "/api/personal-compute/device/start",
7152
+ {
7153
+ name: name2,
7154
+ platform: process.platform,
7155
+ arch: process.arch,
7156
+ devicePublicKey,
7157
+ ...options?.runtime ? { runtime: options.runtime } : {}
7158
+ },
7159
+ signal
7160
+ );
7161
+ const interval = value.interval;
7162
+ if (typeof value.deviceCode !== "string" || value.deviceCode.length < 24 || typeof value.userCode !== "string" || !/^[A-Z0-9-]{4,32}$/.test(value.userCode) || !validVerificationUri(value.verificationUri) || typeof value.expiresAt !== "string" || typeof interval !== "number" || !Number.isInteger(interval) || interval < 5 || interval > 60)
7163
+ throw new Error("Pairing API returned an invalid authorization response.");
7164
+ return value;
7165
+ }
7166
+ async function pollPairing(api, deviceCode, signal) {
7167
+ const value = await request(
7168
+ api,
7169
+ "/api/personal-compute/device/poll",
7170
+ { deviceCode },
7171
+ signal
7172
+ );
7173
+ if (!["pending", "approved", "completed", "denied", "expired", "cancelled"].includes(
7174
+ value.status ?? ""
7175
+ ))
7176
+ throw new Error("Pairing API returned an invalid status.");
7177
+ if (value.status === "approved" && (typeof value.bootstrapScript !== "string" || value.bootstrapScript.length > 1e6))
7178
+ throw new Error("Approved pairing did not include a safe bootstrap payload.");
7179
+ if (value.accountLabel !== void 0 && !validPublicLabel(value.accountLabel) || value.displayName !== void 0 && !validPublicLabel(value.displayName))
7180
+ throw new Error("Pairing API returned invalid public device metadata.");
7181
+ return value;
7182
+ }
7183
+ async function cancelPairing(api, deviceCode, signal) {
7184
+ await request(api, "/api/personal-compute/device/cancel", { deviceCode }, signal);
7185
+ }
7186
+ function pairingExpired(expiresAt) {
7187
+ const time = Date.parse(expiresAt);
7188
+ return !Number.isFinite(time) || time <= Date.now();
7189
+ }
7190
+ async function pollUntilResolved(poll, expiresAt, intervalSeconds, retry, sleep4 = (milliseconds) => new Promise((resolve) => {
7191
+ setTimeout(resolve, milliseconds);
7192
+ }), accept = (result) => result.status !== "pending") {
7193
+ let delay2 = intervalSeconds * 1e3;
7194
+ while (!pairingExpired(expiresAt)) {
7195
+ try {
7196
+ const result = await poll();
7197
+ delay2 = intervalSeconds * 1e3;
7198
+ if (accept(result)) return result;
7199
+ } catch (error) {
7200
+ if (!isTransientPairingError(error)) throw error;
7201
+ retry();
7202
+ delay2 = Math.min(delay2 * 2, 3e4);
7203
+ }
7204
+ await sleep4(delay2);
7205
+ }
7206
+ throw new Error("Pairing expired. Start a new link request.");
7207
+ }
7208
+ var PRODUCTION_API, PRODUCTION_WEB, MAX_RESPONSE_BYTES, DevicePairingRequestError;
7209
+ var init_device_link = __esm({
7210
+ "src/vm/device-link.ts"() {
7211
+ "use strict";
7212
+ PRODUCTION_API = "https://api.conveyor.rallycryapp.com";
7213
+ PRODUCTION_WEB = "https://conveyor.rallycryapp.com";
7214
+ MAX_RESPONSE_BYTES = 64 * 1024;
7215
+ DevicePairingRequestError = class extends Error {
7216
+ constructor(message3, status, options) {
7217
+ super(message3, options);
7218
+ this.status = status;
7219
+ this.name = "DevicePairingRequestError";
7220
+ }
7221
+ status;
7222
+ };
7223
+ }
7224
+ });
7225
+
7226
+ // src/vm/instance-name.ts
7227
+ function isNamedInstance(value) {
7228
+ return typeof value === "string" && NAMED_INSTANCE.test(value) && value !== DEFAULT_INSTANCE && value !== VM_NAME;
7229
+ }
7230
+ function parseInstanceName(value) {
7231
+ if (value === void 0 || value === DEFAULT_INSTANCE) return DEFAULT_INSTANCE;
7232
+ if (value === true) throw new Error("Use --instance=<name>.");
7233
+ if (value === VM_NAME)
7234
+ throw new Error(`"${VM_NAME}" is reserved for the default VM. Omit --instance to use it.`);
7235
+ if (!isNamedInstance(value))
7236
+ throw new Error(
7237
+ "Use an instance name of 1-12 lowercase letters, digits, or single hyphens, starting with a letter."
7238
+ );
7239
+ return value;
7240
+ }
7241
+ function vmNameFor(instance) {
7242
+ return instance === DEFAULT_INSTANCE ? VM_NAME : instance;
7243
+ }
7244
+ var VM_NAME, DEFAULT_INSTANCE, NAMED_INSTANCE;
7245
+ var init_instance_name = __esm({
7246
+ "src/vm/instance-name.ts"() {
7247
+ "use strict";
7248
+ VM_NAME = "conveyor";
7249
+ DEFAULT_INSTANCE = "default";
7250
+ NAMED_INSTANCE = /^(?=.{1,12}$)[a-z](?:-?[a-z0-9])*$/;
7251
+ }
7252
+ });
7253
+
7254
+ // src/vm/host-state.ts
7255
+ import { access, chmod as chmod3, lstat, mkdir as mkdir4, readFile as readFile2, rename as rename2, rm as rm3, writeFile as writeFile4 } from "fs/promises";
7256
+ import { homedir, uptime } from "os";
7257
+ import { join } from "path";
7258
+ import { setTimeout as sleep } from "timers/promises";
7259
+ function verifiedLink(state) {
7260
+ return state.enrolled && (state.linkVerified ?? !!(state.linkedAccountLabel || state.linkedDisplayName));
7261
+ }
7262
+ function vmProfile() {
7263
+ const profile = process.env.CONVEYOR_VM_TEST_PROFILE;
7264
+ if (profile !== void 0 && !/^acceptance-[a-z0-9-]{1,32}$/.test(profile)) {
7265
+ throw new Error("Test VM profiles must match acceptance-[a-z0-9-]{1,32}.");
7266
+ }
7267
+ return profile;
7268
+ }
7269
+ function instancesBase() {
7270
+ const profile = vmProfile();
7271
+ return join(
7272
+ homedir(),
7273
+ ".local",
7274
+ "share",
7275
+ profile ? `conveyor-k3-${profile}-vms` : "conveyor-k3-vms"
7276
+ );
7277
+ }
7278
+ function hostPaths(instance = DEFAULT_INSTANCE) {
7279
+ const name2 = parseInstanceName(instance);
7280
+ if (name2 !== DEFAULT_INSTANCE) return pathsForRoot(join(instancesBase(), name2), name2);
7281
+ const profile = vmProfile();
7282
+ return pathsForRoot(
7283
+ join(homedir(), ".local", "share", profile ? `conveyor-k3-${profile}` : "conveyor-k3-vm")
7284
+ );
7285
+ }
7286
+ function pathsForRoot(root, instance = DEFAULT_INSTANCE) {
7287
+ const vmName = vmNameFor(parseInstanceName(instance));
7288
+ const instanceDir = join(root, "lima", vmName);
7289
+ return {
7290
+ root,
7291
+ limaHome: join(root, "lima"),
7292
+ state: join(root, "state.json"),
7293
+ network: join(root, "network.json"),
7294
+ template: join(root, "conveyor.yaml"),
7295
+ wrapper: join(root, "qemu-wrapper"),
7296
+ wrapperJs: join(root, "vm-qemu.js"),
7297
+ applied: join(root, "applied-network.json"),
7298
+ guard: join(root, "guard-state.json"),
7299
+ startRequest: join(root, "start-request.json"),
7300
+ policyTransaction: join(root, "policy-transaction.json"),
7301
+ lock: join(root, "operation.lock"),
7302
+ /** Written by an owner stop, so a supervising service does not restart the VM. */
7303
+ stopIntent: join(root, "stop-intent.json"),
7304
+ instance,
7305
+ vmName,
7306
+ instanceDir,
7307
+ limaConfig: join(instanceDir, "lima.yaml")
7308
+ };
7309
+ }
7310
+ function missingVm(paths, defaultCopy = "No Conveyor VM exists. Run conveyor-k3 vm setup first.") {
7311
+ return paths.instance === DEFAULT_INSTANCE ? defaultCopy : `No Conveyor VM named ${paths.instance} exists. Run conveyor-k3 vm setup --instance=${paths.instance} first.`;
7312
+ }
7313
+ async function vmOperationInProgress(paths) {
7314
+ const editing = await Promise.all(
7315
+ [paths.lock, paths.policyTransaction].map(
7316
+ (path) => access(path).then(
7317
+ () => true,
7318
+ () => false
7319
+ )
7320
+ )
7321
+ );
7322
+ return editing.some(Boolean);
7323
+ }
7324
+ async function privateDirectory(path) {
7325
+ await mkdir4(path, { recursive: true, mode: 448 });
7326
+ const stat2 = await lstat(path);
7327
+ if (!stat2.isDirectory() || stat2.isSymbolicLink() || stat2.uid !== process.getuid?.()) {
7328
+ throw new Error(`Refusing unsafe VM directory: ${path}`);
7329
+ }
7330
+ await chmod3(path, 448);
7331
+ }
7332
+ async function atomicPrivateWrite(path, content, mode = 384) {
7333
+ await assertSafeExistingFile(path);
7334
+ const temporary = `${path}.${process.pid}.tmp`;
7335
+ await writeFile4(temporary, content, { mode, flag: "wx" });
7336
+ try {
7089
7337
  await rename2(temporary, path);
7090
7338
  } finally {
7091
7339
  await rm3(temporary, { force: true });
@@ -7212,13 +7460,13 @@ async function reclaimStaleLock(paths, options) {
7212
7460
  }
7213
7461
  async function acquireHostLock(paths, options) {
7214
7462
  try {
7215
- await mkdir3(paths.lock, { mode: 448 });
7463
+ await mkdir4(paths.lock, { mode: 448 });
7216
7464
  return true;
7217
7465
  } catch (error) {
7218
7466
  if (error.code !== "EEXIST") throw error;
7219
7467
  if (!options.reclaimStale) return false;
7220
7468
  await reclaimStaleLock(paths, options);
7221
- return await mkdir3(paths.lock, { mode: 448 }).then(
7469
+ return await mkdir4(paths.lock, { mode: 448 }).then(
7222
7470
  () => true,
7223
7471
  () => false
7224
7472
  );
@@ -7235,7 +7483,7 @@ async function withHostLock(paths, fn, options = {}) {
7235
7483
  await sleep(500, void 0, { signal: options.signal });
7236
7484
  }
7237
7485
  try {
7238
- await writeFile3(join(paths.lock, "pid"), String(process.pid), { mode: 384 });
7486
+ await writeFile4(join(paths.lock, "pid"), String(process.pid), { mode: 384 });
7239
7487
  return await fn();
7240
7488
  } finally {
7241
7489
  await rm3(paths.lock, { recursive: true, force: true });
@@ -7913,7 +8161,7 @@ var init_host_packages = __esm({
7913
8161
  import { constants } from "fs";
7914
8162
  import { access as access2, statfs as statfs2 } from "fs/promises";
7915
8163
  import { userInfo } from "os";
7916
- import { dirname, join as join4 } from "path";
8164
+ import { dirname as dirname2, join as join4 } from "path";
7917
8165
  function realHost() {
7918
8166
  return {
7919
8167
  platform: process.platform,
@@ -7985,11 +8233,11 @@ async function assertLimaVersion(exec, paths, lima, host) {
7985
8233
  }
7986
8234
  async function findFirmware(qemu, arch, host, os) {
7987
8235
  const candidates = arch === "arm64" ? [
7988
- join4(dirname(qemu), "../share/qemu/edk2-aarch64-code.fd"),
8236
+ join4(dirname2(qemu), "../share/qemu/edk2-aarch64-code.fd"),
7989
8237
  "/usr/share/AAVMF/AAVMF_CODE.fd",
7990
8238
  "/usr/share/qemu-efi-aarch64/QEMU_EFI.fd"
7991
8239
  ] : [
7992
- join4(dirname(qemu), "../share/qemu/edk2-x86_64-code.fd"),
8240
+ join4(dirname2(qemu), "../share/qemu/edk2-x86_64-code.fd"),
7993
8241
  "/usr/share/edk2/x64/OVMF_CODE.4m.fd",
7994
8242
  "/usr/share/OVMF/OVMF_CODE_4M.fd",
7995
8243
  "/usr/share/OVMF/OVMF_CODE.fd"
@@ -9352,208 +9600,11 @@ var init_dev_artifact = __esm({
9352
9600
  }
9353
9601
  });
9354
9602
 
9355
- // src/vm/device-link.ts
9356
- function isTransientPairingError(error) {
9357
- if (error instanceof DevicePairingRequestError)
9358
- return error.status === 429 || error.status !== void 0 && error.status >= 500;
9359
- return error instanceof Error && /timed out|network|fetch failed/i.test(error.message);
9360
- }
9361
- function validPublicLabel(value) {
9362
- return typeof value === "string" && value.length > 0 && value.length <= 160 && [...value].every((character) => {
9363
- const code = character.charCodeAt(0);
9364
- return code >= 32 && code !== 127;
9365
- });
9366
- }
9367
- function validDevicePublicKey(value) {
9368
- return typeof value === "string" && Buffer.byteLength(value) <= 1024 && /^-----BEGIN PUBLIC KEY-----\n(?:[A-Za-z0-9+/=]{1,128}\n)+-----END PUBLIC KEY-----\n?$/.test(
9369
- value
9370
- );
9371
- }
9372
- function devOrigin() {
9373
- const value = process.env.CONVEYOR_DEVICE_LINK_DEV_ORIGIN;
9374
- if (!value) return void 0;
9375
- const url = new URL(value);
9376
- if (url.pathname !== "/" || url.search || url.hash || url.username || url.password)
9377
- throw new Error("CONVEYOR_DEVICE_LINK_DEV_ORIGIN must be an origin.");
9378
- if (url.protocol !== "https:" && !(url.protocol === "http:" && url.hostname === "localhost"))
9379
- throw new Error("The explicit development pairing origin must use HTTPS or localhost HTTP.");
9380
- return url.origin;
9381
- }
9382
- function devWebOrigin() {
9383
- const value = process.env.CONVEYOR_DEVICE_LINK_DEV_WEB_ORIGIN;
9384
- if (!value) return void 0;
9385
- const url = new URL(value);
9386
- if (url.pathname !== "/" || url.search || url.hash || url.username || url.password)
9387
- throw new Error("CONVEYOR_DEVICE_LINK_DEV_WEB_ORIGIN must be an origin.");
9388
- if (url.protocol !== "https:" && !(url.protocol === "http:" && url.hostname === "localhost"))
9389
- throw new Error("The explicit development browser origin must use HTTPS or localhost HTTP.");
9390
- return url.origin;
9391
- }
9392
- function trustedDeviceApiUrl(value) {
9393
- const requested = new URL(value ?? PRODUCTION_API);
9394
- const development = devOrigin();
9395
- const allowed = /* @__PURE__ */ new Set([PRODUCTION_API, ...development ? [development] : []]);
9396
- if (requested.pathname !== "/" || requested.search || requested.hash || !allowed.has(requested.origin))
9397
- throw new Error(
9398
- "Pairing API must be the production origin or the explicitly configured development origin."
9399
- );
9400
- return requested.origin;
9401
- }
9402
- function validVerificationUri(value) {
9403
- if (typeof value !== "string") return false;
9404
- try {
9405
- const url = new URL(value);
9406
- const development = devWebOrigin();
9407
- const allowed = /* @__PURE__ */ new Set([PRODUCTION_WEB, ...development ? [development] : []]);
9408
- return allowed.has(url.origin) && url.pathname === "/personal-compute/link" && url.searchParams.size === 1 && /^[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}$/.test(url.searchParams.get("code") ?? "") && !url.username && !url.password && !url.hash;
9409
- } catch {
9410
- return false;
9411
- }
9412
- }
9413
- async function responseJson(response) {
9414
- const length = Number(response.headers.get("content-length"));
9415
- if (Number.isFinite(length) && (length < 0 || length > MAX_RESPONSE_BYTES))
9416
- throw new Error("Pairing API response is too large.");
9417
- if (!response.body) throw new Error("Pairing API returned an empty response.");
9418
- const reader = response.body.getReader();
9419
- const chunks = [];
9420
- let size = 0;
9421
- try {
9422
- while (true) {
9423
- const next = await reader.read();
9424
- if (next.done) break;
9425
- size += next.value.byteLength;
9426
- if (size > MAX_RESPONSE_BYTES) throw new Error("Pairing API response is too large.");
9427
- chunks.push(next.value);
9428
- }
9429
- } finally {
9430
- reader.releaseLock();
9431
- }
9432
- const bytes = new Uint8Array(size);
9433
- let offset = 0;
9434
- for (const chunk of chunks) {
9435
- bytes.set(chunk, offset);
9436
- offset += chunk.byteLength;
9437
- }
9438
- try {
9439
- return JSON.parse(new TextDecoder().decode(bytes));
9440
- } catch {
9441
- throw new Error("Pairing API returned invalid JSON.");
9442
- }
9443
- }
9444
- async function request(api, path, body, signal) {
9445
- const timeout = AbortSignal.timeout(15e3);
9446
- const combined = signal ? AbortSignal.any([signal, timeout]) : timeout;
9447
- let response;
9448
- try {
9449
- response = await fetch(`${api}${path}`, {
9450
- method: "POST",
9451
- headers: { "content-type": "application/json", accept: "application/json" },
9452
- body: JSON.stringify(body),
9453
- redirect: "error",
9454
- signal: combined
9455
- });
9456
- } catch (error) {
9457
- if (combined.aborted)
9458
- throw new Error("Pairing request timed out or was cancelled.", { cause: error });
9459
- throw error;
9460
- }
9461
- if (!response.ok) {
9462
- const failure = await responseJson(response).catch(() => null);
9463
- const detail2 = typeof failure?.error === "string" && failure.error.length <= 300 && [...failure.error].every(
9464
- (character) => character.charCodeAt(0) >= 32 && character.charCodeAt(0) !== 127
9465
- ) ? failure.error : void 0;
9466
- throw new DevicePairingRequestError(
9467
- detail2 ?? `Pairing request failed (${response.status}).`,
9468
- response.status
9469
- );
9470
- }
9471
- return responseJson(response);
9472
- }
9473
- async function startPairing(api, name2, devicePublicKey, signal) {
9474
- if (!validDevicePublicKey(devicePublicKey))
9475
- throw new Error("Guest device public key is invalid.");
9476
- const value = await request(
9477
- api,
9478
- "/api/personal-compute/device/start",
9479
- {
9480
- name: name2,
9481
- platform: process.platform,
9482
- arch: process.arch,
9483
- devicePublicKey
9484
- },
9485
- signal
9486
- );
9487
- const interval = value.interval;
9488
- if (typeof value.deviceCode !== "string" || value.deviceCode.length < 24 || typeof value.userCode !== "string" || !/^[A-Z0-9-]{4,32}$/.test(value.userCode) || !validVerificationUri(value.verificationUri) || typeof value.expiresAt !== "string" || typeof interval !== "number" || !Number.isInteger(interval) || interval < 5 || interval > 60)
9489
- throw new Error("Pairing API returned an invalid authorization response.");
9490
- return value;
9491
- }
9492
- async function pollPairing(api, deviceCode, signal) {
9493
- const value = await request(
9494
- api,
9495
- "/api/personal-compute/device/poll",
9496
- { deviceCode },
9497
- signal
9498
- );
9499
- if (!["pending", "approved", "completed", "denied", "expired", "cancelled"].includes(
9500
- value.status ?? ""
9501
- ))
9502
- throw new Error("Pairing API returned an invalid status.");
9503
- if (value.status === "approved" && (typeof value.bootstrapScript !== "string" || value.bootstrapScript.length > 1e6))
9504
- throw new Error("Approved pairing did not include a safe bootstrap payload.");
9505
- if (value.accountLabel !== void 0 && !validPublicLabel(value.accountLabel) || value.displayName !== void 0 && !validPublicLabel(value.displayName))
9506
- throw new Error("Pairing API returned invalid public device metadata.");
9507
- return value;
9508
- }
9509
- async function cancelPairing(api, deviceCode, signal) {
9510
- await request(api, "/api/personal-compute/device/cancel", { deviceCode }, signal);
9511
- }
9512
- function pairingExpired(expiresAt) {
9513
- const time = Date.parse(expiresAt);
9514
- return !Number.isFinite(time) || time <= Date.now();
9515
- }
9516
- async function pollUntilResolved(poll, expiresAt, intervalSeconds, retry, sleep4 = (milliseconds) => new Promise((resolve) => {
9517
- setTimeout(resolve, milliseconds);
9518
- }), accept = (result) => result.status !== "pending") {
9519
- let delay2 = intervalSeconds * 1e3;
9520
- while (!pairingExpired(expiresAt)) {
9521
- try {
9522
- const result = await poll();
9523
- delay2 = intervalSeconds * 1e3;
9524
- if (accept(result)) return result;
9525
- } catch (error) {
9526
- if (!isTransientPairingError(error)) throw error;
9527
- retry();
9528
- delay2 = Math.min(delay2 * 2, 3e4);
9529
- }
9530
- await sleep4(delay2);
9531
- }
9532
- throw new Error("Pairing expired. Start a new link request.");
9533
- }
9534
- var PRODUCTION_API, PRODUCTION_WEB, MAX_RESPONSE_BYTES, DevicePairingRequestError;
9535
- var init_device_link = __esm({
9536
- "src/vm/device-link.ts"() {
9537
- "use strict";
9538
- PRODUCTION_API = "https://api.conveyor.rallycryapp.com";
9539
- PRODUCTION_WEB = "https://conveyor.rallycryapp.com";
9540
- MAX_RESPONSE_BYTES = 64 * 1024;
9541
- DevicePairingRequestError = class extends Error {
9542
- constructor(message2, status, options) {
9543
- super(message2, options);
9544
- this.status = status;
9545
- this.name = "DevicePairingRequestError";
9546
- }
9547
- status;
9548
- };
9549
- }
9550
- });
9551
-
9552
- // src/vm/instances.ts
9553
- import { lstat as lstat2, readdir } from "fs/promises";
9554
- import { join as join7 } from "path";
9555
- function instanceLayout() {
9556
- return { defaultRoot: hostPaths().root, base: instancesBase() };
9603
+ // src/vm/instances.ts
9604
+ import { lstat as lstat2, readdir } from "fs/promises";
9605
+ import { join as join7 } from "path";
9606
+ function instanceLayout() {
9607
+ return { defaultRoot: hostPaths().root, base: instancesBase() };
9557
9608
  }
9558
9609
  function instancePaths(layout, instance) {
9559
9610
  return instance === DEFAULT_INSTANCE ? pathsForRoot(layout.defaultRoot) : pathsForRoot(join7(layout.base, instance), instance);
@@ -9594,7 +9645,7 @@ var init_instances = __esm({
9594
9645
  });
9595
9646
 
9596
9647
  // src/vm/ledger.ts
9597
- import { mkdir as mkdir4, rm as rm6, writeFile as writeFile4 } from "fs/promises";
9648
+ import { mkdir as mkdir5, rm as rm6, writeFile as writeFile5 } from "fs/promises";
9598
9649
  import { cpus as cpus2, totalmem as totalmem2 } from "os";
9599
9650
  import { join as join8 } from "path";
9600
9651
  import { setTimeout as sleep2 } from "timers/promises";
@@ -9651,7 +9702,7 @@ async function withAdmissionLock(fn, base = instancesBase(), wait = ADMISSION_WA
9651
9702
  await rm6(lock, { recursive: true, force: true });
9652
9703
  };
9653
9704
  try {
9654
- await writeFile4(join8(lock, "pid"), String(process.pid), { mode: 384 });
9705
+ await writeFile5(join8(lock, "pid"), String(process.pid), { mode: 384 });
9655
9706
  return await fn(release);
9656
9707
  } finally {
9657
9708
  await release();
@@ -9659,7 +9710,7 @@ async function withAdmissionLock(fn, base = instancesBase(), wait = ADMISSION_WA
9659
9710
  }
9660
9711
  async function tryLock(path) {
9661
9712
  try {
9662
- await mkdir4(path, { mode: 448 });
9713
+ await mkdir5(path, { mode: 448 });
9663
9714
  return true;
9664
9715
  } catch (error) {
9665
9716
  if (error.code === "EEXIST") return false;
@@ -9683,8 +9734,8 @@ import { Transform } from "stream";
9683
9734
  function isBakeRef(ref, prefix) {
9684
9735
  return ref.startsWith(prefix) && REF_AFTER_PREFIX.test(ref.slice(prefix.length));
9685
9736
  }
9686
- function gateError(message2) {
9687
- const error = new Error(message2);
9737
+ function gateError(message3) {
9738
+ const error = new Error(message3);
9688
9739
  error.name = GATE_ERROR;
9689
9740
  return error;
9690
9741
  }
@@ -9694,8 +9745,8 @@ function isTarGateError(error) {
9694
9745
  function padding(size) {
9695
9746
  return (BLOCK - size % BLOCK) % BLOCK;
9696
9747
  }
9697
- function fail(message2) {
9698
- throw gateError(message2);
9748
+ function fail(message3) {
9749
+ throw gateError(message3);
9699
9750
  }
9700
9751
  function textField(block, start, length) {
9701
9752
  const field = block.subarray(start, start + length);
@@ -10073,7 +10124,7 @@ var init_tar_gate = __esm({
10073
10124
  });
10074
10125
 
10075
10126
  // src/vm/image-import.ts
10076
- import { spawn as spawn2 } from "child_process";
10127
+ import { spawn as spawn3 } from "child_process";
10077
10128
  import { statfs as statfs4 } from "fs/promises";
10078
10129
  import { join as join9 } from "path";
10079
10130
  import { Transform as Transform2 } from "stream";
@@ -10207,7 +10258,7 @@ function stallMeter(limits, stall) {
10207
10258
  return { meter, stop: () => clearInterval(timer) };
10208
10259
  }
10209
10260
  function spawnGuestImport(ports, state) {
10210
- const child = spawn2(state.lima, ["--tty=false", ...guestCtr(ports, "import")], {
10261
+ const child = spawn3(state.lima, ["--tty=false", ...guestCtr(ports, "import")], {
10211
10262
  env: hostEnvironment(ports.paths),
10212
10263
  detached: true,
10213
10264
  stdio: ["pipe", "pipe", "pipe"]
@@ -10324,7 +10375,7 @@ var init_image_import = __esm({
10324
10375
  });
10325
10376
 
10326
10377
  // src/vm/runner-grant.ts
10327
- import { spawn as spawn3 } from "child_process";
10378
+ import { spawn as spawn4 } from "child_process";
10328
10379
  import { existsSync as existsSync2, promises as fs, realpathSync } from "fs";
10329
10380
  import { tmpdir, userInfo as userInfo2 } from "os";
10330
10381
  import { join as join10 } from "path";
@@ -10408,7 +10459,7 @@ async function readGrantRecord(root) {
10408
10459
  }
10409
10460
  function runSudo(args) {
10410
10461
  return new Promise((resolve, reject) => {
10411
- const child = spawn3("sudo", ["--", "/bin/sh", "-c", INSTALL_SCRIPT, "conveyor-k3", ...args], {
10462
+ const child = spawn4("sudo", ["--", "/bin/sh", "-c", INSTALL_SCRIPT, "conveyor-k3", ...args], {
10412
10463
  stdio: "inherit"
10413
10464
  });
10414
10465
  child.once("error", reject);
@@ -10704,10 +10755,12 @@ async function setupVm(ports, parsed, state) {
10704
10755
  if (!state) state = await createInstance(ports, parsed, url);
10705
10756
  await startVm(ports, state);
10706
10757
  if (url) await enrollVm(ports, state, url);
10707
- else
10708
- ports.log(
10709
- state.enrollmentAttempted ? "Enrollment may be incomplete. Regenerate the install link and retry setup --install-url-stdin." : "VM created but not enrolled. Run setup --install-url-stdin with a fresh URL from Personal Compute."
10710
- );
10758
+ else ports.log(setupNextStep(state, ports.paths.instance));
10759
+ }
10760
+ function setupNextStep(state, instance) {
10761
+ const link = instance === DEFAULT_INSTANCE ? "conveyor-k3 vm link" : `conveyor-k3 vm link --instance=${instance}`;
10762
+ if (state.enrolled) return "The VM is running and linked to Conveyor.";
10763
+ return state.enrollmentAttempted ? `Linking may be incomplete. Run ${link} to link the VM again.` : `VM created but not linked yet. Run ${link} to link it to your account.`;
10711
10764
  }
10712
10765
  function defaultMachineName(paths) {
10713
10766
  const base = `${process.platform}-${process.arch}`;
@@ -10743,6 +10796,7 @@ async function linkVm(ports, state, parsed) {
10743
10796
  ports.log(
10744
10797
  `PAIRING ${JSON.stringify({ userCode: pairing.userCode, verificationUri: pairing.verificationUri })}`
10745
10798
  );
10799
+ ports.log(`Open ${pairing.verificationUri} on any device and confirm code ${pairing.userCode}.`);
10746
10800
  ports.log("Waiting for browser approval\u2026");
10747
10801
  const terminate = () => {
10748
10802
  void cancelPairing(api, pairing.deviceCode).finally(() => process.exit(143));
@@ -10879,9 +10933,9 @@ var init_service_unit = __esm({
10879
10933
 
10880
10934
  // src/vm/service-control.ts
10881
10935
  import { constants as constants2, readFileSync as readFileSync4 } from "fs";
10882
- import { access as access5, lstat as lstat3, mkdir as mkdir5, readFile as readFile8, readdir as readdir2, rename as rename3, rm as rm7, writeFile as writeFile5 } from "fs/promises";
10936
+ import { access as access5, lstat as lstat3, mkdir as mkdir6, readFile as readFile8, readdir as readdir2, rename as rename3, rm as rm7, writeFile as writeFile6 } from "fs/promises";
10883
10937
  import { tmpdir as tmpdir2, userInfo as userInfo3 } from "os";
10884
- import { dirname as dirname2, join as join12 } from "path";
10938
+ import { dirname as dirname3, join as join12 } from "path";
10885
10939
  import { setTimeout as sleep3 } from "timers/promises";
10886
10940
  function realServiceHost(exec, log2) {
10887
10941
  const uid = process.getuid?.() ?? -1;
@@ -10992,9 +11046,9 @@ async function writeManagedUnit(path, text) {
10992
11046
  if (existing !== void 0 && !existing.startsWith(MANAGED_UNIT_HEADER))
10993
11047
  throw new Error(`Refusing to replace ${path}: conveyor-k3 did not write it.`);
10994
11048
  if (existing === text) return;
10995
- await mkdir5(dirname2(path), { recursive: true, mode: 493 });
11049
+ await mkdir6(dirname3(path), { recursive: true, mode: 493 });
10996
11050
  const temporary = `${path}.${process.pid}.tmp`;
10997
- await writeFile5(temporary, text, { mode: 420 });
11051
+ await writeFile6(temporary, text, { mode: 420 });
10998
11052
  await rename3(temporary, path);
10999
11053
  }
11000
11054
  async function assertLinger(host) {
@@ -11199,7 +11253,7 @@ var init_service_control = __esm({
11199
11253
 
11200
11254
  // src/vm/supervisor.ts
11201
11255
  import { setTimeout as delay } from "timers/promises";
11202
- function message(error) {
11256
+ function message2(error) {
11203
11257
  return error instanceof Error ? error.message : String(error);
11204
11258
  }
11205
11259
  async function pause(ms, deps) {
@@ -11223,7 +11277,7 @@ async function readyToStart(ports) {
11223
11277
  );
11224
11278
  return state;
11225
11279
  } catch (error) {
11226
- return message(error);
11280
+ return message2(error);
11227
11281
  }
11228
11282
  }
11229
11283
  async function forceStop(ports, state) {
@@ -11234,7 +11288,7 @@ async function forceStop(ports, state) {
11234
11288
  ["stop", "--force", ports.paths.vmName],
11235
11289
  void 0,
11236
11290
  15e3
11237
- ).catch((error) => ports.log(`The forced stop failed: ${message(error)}`));
11291
+ ).catch((error) => ports.log(`The forced stop failed: ${message2(error)}`));
11238
11292
  }
11239
11293
  async function startOrAbort(ports, state, deps) {
11240
11294
  let booted = false;
@@ -11289,7 +11343,7 @@ async function startSupervised(ports, deps) {
11289
11343
  ports.log("Stopped before the VM started.");
11290
11344
  return SERVICE_EXIT.stopped;
11291
11345
  }
11292
- ports.log(`VM start failed: ${message(error)}`);
11346
+ ports.log(`VM start failed: ${message2(error)}`);
11293
11347
  return SERVICE_EXIT.startFailed;
11294
11348
  });
11295
11349
  }
@@ -11304,7 +11358,7 @@ async function watch(ports, state, deps) {
11304
11358
  failures = 0;
11305
11359
  } catch (error) {
11306
11360
  failures += 1;
11307
- ports.log(`Could not read the VM status (${failures} in a row): ${message(error)}`);
11361
+ ports.log(`Could not read the VM status (${failures} in a row): ${message2(error)}`);
11308
11362
  if (failures <= TOLERATED_STATUS_ERRORS) continue;
11309
11363
  await shutdown(ports, state);
11310
11364
  return SERVICE_EXIT.unexpectedStop;
@@ -11348,7 +11402,7 @@ async function shutdownVm(ports, state) {
11348
11402
  );
11349
11403
  ports.log("VM stopped. Its disks and workspaces are preserved.");
11350
11404
  } catch (error) {
11351
- ports.log(`The graceful stop failed (${message(error)}); forcing it.`);
11405
+ ports.log(`The graceful stop failed (${message2(error)}); forcing it.`);
11352
11406
  await limaCommand(
11353
11407
  ports.exec,
11354
11408
  state,
@@ -11356,7 +11410,7 @@ async function shutdownVm(ports, state) {
11356
11410
  ["stop", "--force", ports.paths.vmName],
11357
11411
  void 0,
11358
11412
  2e4
11359
- ).catch((forced) => ports.log(`The forced stop failed: ${message(forced)}`));
11413
+ ).catch((forced) => ports.log(`The forced stop failed: ${message2(forced)}`));
11360
11414
  }
11361
11415
  }
11362
11416
  async function runSupervisor(ports, deps) {
@@ -11703,7 +11757,8 @@ var init_command = __esm({
11703
11757
  init_command_setup();
11704
11758
  init_service_command();
11705
11759
  init_instance_list();
11706
- VM_HELP = `Usage: conveyor-k3 vm <setup|start|stop|status|list|link|internet|logout|pods|pod-logs|disk-usage|configure|update|allow|deny|refresh|uninstall|image import|runners grant>
11760
+ VM_HELP = `Personal computer (VM) runtime.
11761
+ Usage: conveyor-k3 vm <setup|start|stop|status|list|link|internet|logout|pods|pod-logs|disk-usage|configure|update|allow|deny|refresh|uninstall|image import|runners grant>
11707
11762
  Every command but list acts on one VM: --instance=<name> selects a named VM,
11708
11763
  and omitting it selects this user's default VM.
11709
11764
  setup [--install-url-stdin] [--relink] [--cpus=4] [--memory=8] [--disk=64]
@@ -11736,6 +11791,173 @@ Run without sudo. Requires Lima 2.2+ and QEMU with HVF (macOS) or KVM (Linux).
11736
11791
  }
11737
11792
  });
11738
11793
 
11794
+ // src/host/command.ts
11795
+ var command_exports2 = {};
11796
+ __export(command_exports2, {
11797
+ HOST_HELP: () => HOST_HELP,
11798
+ runHostCommand: () => runHostCommand
11799
+ });
11800
+ import { hostname } from "os";
11801
+ function validateFlags2(parsed) {
11802
+ const action = parsed.positional.length === 1 ? parsed.positional[0] : void 0;
11803
+ const allowed = action === void 0 ? void 0 : ALLOWED_FLAGS.get(action);
11804
+ if (action === void 0 || !allowed) throw new Error(HOST_HELP);
11805
+ for (const [flag, value] of Object.entries(parsed.flags)) {
11806
+ if (!allowed.includes(flag)) throw new Error(`Unknown flag for host ${action}: --${flag}`);
11807
+ const boolean = BOOLEAN_FLAGS.has(flag);
11808
+ if (boolean && value !== true) throw new Error(`Use --${flag} without a value.`);
11809
+ if (!boolean && value === true) throw new Error(`Use --${flag}=<value>.`);
11810
+ }
11811
+ return action;
11812
+ }
11813
+ async function requireHostRoot(exec) {
11814
+ if (process.getuid?.() !== 0)
11815
+ throw new Error(
11816
+ "Host commands change this machine's k3s and systemd units. Run them with sudo."
11817
+ );
11818
+ const checks = await preflight(exec);
11819
+ if (!checks.ok) throw new Error(checks.problems.join("\n"));
11820
+ }
11821
+ async function requireApt(exec) {
11822
+ if (await commandExists(exec, "apt-get")) return;
11823
+ throw new Error(
11824
+ "The host runtime installs its prerequisites with apt-get. Use Ubuntu 24.04 or Debian 12, or newer."
11825
+ );
11826
+ }
11827
+ async function markHostRuntime(ports) {
11828
+ const state = await ports.readState();
11829
+ if (state.runtime !== "host") await ports.writeState({ ...state, runtime: "host" });
11830
+ }
11831
+ async function prepareHostLink(parsed, ports, log2) {
11832
+ const api = trustedDeviceApiUrl(
11833
+ typeof parsed.flags["api-url"] === "string" ? parsed.flags["api-url"] : void 0
11834
+ );
11835
+ const name2 = typeof parsed.flags.name === "string" ? parsed.flags.name.trim() : hostname();
11836
+ if (!MACHINE_NAME.test(name2))
11837
+ throw new Error("Use a machine name of 1-80 letters, numbers, spaces, dots, or hyphens.");
11838
+ if (process.env.CONVEYOR_DEV_ARTIFACT || process.env.CONVEYOR_DEV_ARTIFACT_SHA256)
11839
+ throw new Error("Development artifacts are not supported by host link.");
11840
+ const state = await ports.readState();
11841
+ if (state.enrolled) {
11842
+ const current = state.hostname ?? "a Conveyor instance";
11843
+ if (parsed.flags.relink !== true)
11844
+ throw new Error(
11845
+ `This server is already linked as ${current}. Re-run with --relink to move it to the account that approves the new link (its old instance stays until you Disconnect it).`
11846
+ );
11847
+ log2(`Relinking: this server will leave ${current}.`);
11848
+ }
11849
+ return { api, name: name2 };
11850
+ }
11851
+ async function completeHostLink(api, pairing, log2, ports) {
11852
+ const poll = () => pollPairing(api, pairing.deviceCode);
11853
+ const retry = () => log2("Securing connection\u2026 retrying pairing status.");
11854
+ const result = await pollUntilResolved(poll, pairing.expiresAt, pairing.interval, retry);
11855
+ if (result.status === "completed") {
11856
+ await markHostRuntime(ports);
11857
+ log2(
11858
+ `This server is already linked${result.accountLabel ? ` to ${result.accountLabel}` : ""}${result.displayName ? ` as ${result.displayName}` : ""}.`
11859
+ );
11860
+ return;
11861
+ }
11862
+ if (result.status !== "approved")
11863
+ throw new Error(`Pairing ${result.status}. Start a new link request to try again.`);
11864
+ if (typeof result.bootstrapScript !== "string")
11865
+ throw new Error("Approved pairing did not include a bootstrap payload.");
11866
+ log2("Approved. Installing and enrolling this server\u2026");
11867
+ await ports.runBootstrap(result.bootstrapScript);
11868
+ log2("Verifying Conveyor access\u2026");
11869
+ const completed = await pollUntilResolved(
11870
+ poll,
11871
+ pairing.expiresAt,
11872
+ pairing.interval,
11873
+ retry,
11874
+ void 0,
11875
+ (next) => next.status !== "pending" && next.status !== "approved"
11876
+ );
11877
+ if (completed.status !== "completed")
11878
+ throw new Error(`Pairing ${completed.status}. Conveyor did not verify this server.`);
11879
+ await markHostRuntime(ports);
11880
+ log2(
11881
+ `Linked to ${completed.accountLabel ?? "Conveyor"}${completed.displayName ? ` as ${completed.displayName}` : ""}.`
11882
+ );
11883
+ }
11884
+ async function hostLink(parsed, log2, ports) {
11885
+ const { api, name: name2 } = await prepareHostLink(parsed, ports, log2);
11886
+ log2("Preparing this server: k3s, cloudflared, and the cluster CA\u2026");
11887
+ await ports.prepare();
11888
+ const devicePublicKey = await ports.ensureDevicePublicKey();
11889
+ const pairing = await startPairing(api, name2, devicePublicKey, void 0, { runtime: "host" });
11890
+ log2(
11891
+ `PAIRING ${JSON.stringify({ userCode: pairing.userCode, verificationUri: pairing.verificationUri })}`
11892
+ );
11893
+ log2(`Open ${pairing.verificationUri} on any device and confirm code ${pairing.userCode}.`);
11894
+ log2("Waiting for browser approval\u2026");
11895
+ const terminate = () => {
11896
+ void cancelPairing(api, pairing.deviceCode).finally(() => process.exit(143));
11897
+ };
11898
+ process.once("SIGTERM", terminate);
11899
+ try {
11900
+ await completeHostLink(api, pairing, log2, ports);
11901
+ } catch (error) {
11902
+ await cancelPairing(api, pairing.deviceCode).catch(() => void 0);
11903
+ throw error;
11904
+ } finally {
11905
+ process.removeListener("SIGTERM", terminate);
11906
+ }
11907
+ }
11908
+ async function runHostCommand(parsed, exec, log2, ports) {
11909
+ if (parsed.positional.length === 0 || parsed.flags.help === true) {
11910
+ log2(HOST_HELP);
11911
+ return;
11912
+ }
11913
+ const action = validateFlags2(parsed);
11914
+ if (action === "status") {
11915
+ const asJson = parsed.flags.json === true;
11916
+ if (!asJson && process.getuid?.() !== 0)
11917
+ log2("Not root: this server's state file is root-only. Run sudo conveyor-k3 host status.");
11918
+ return ports.status(asJson);
11919
+ }
11920
+ await requireHostRoot(exec);
11921
+ if (action === "setup" || action === "link") await requireApt(exec);
11922
+ if (action === "setup") return ports.setup();
11923
+ if (action === "link") return hostLink(parsed, log2, ports);
11924
+ await ports.uninstall({
11925
+ purgeK3s: parsed.flags["purge-k3s"] === true,
11926
+ confirmed: parsed.flags.yes === true
11927
+ });
11928
+ await ports.removeDeviceKey();
11929
+ }
11930
+ var HOST_HELP, ALLOWED_FLAGS, BOOLEAN_FLAGS, MACHINE_NAME;
11931
+ var init_command2 = __esm({
11932
+ "src/host/command.ts"() {
11933
+ "use strict";
11934
+ init_env();
11935
+ init_exec();
11936
+ init_device_link();
11937
+ HOST_HELP = `Dedicated server (host) runtime.
11938
+ Usage: conveyor-k3 host <setup|link|status|uninstall>
11939
+ setup Install k3s, or adopt the k3s 1.29+ already here, and
11940
+ cloudflared. Safe to re-run.
11941
+ link [--name=machine] [--api-url=trusted-origin] [--relink]
11942
+ Link this server in a browser: prints a URL and a code.
11943
+ status [--json] What is running, and what Conveyor knows.
11944
+ uninstall [--purge-k3s --yes] Remove Conveyor's units, state, and device key. k3s,
11945
+ your config, and your workloads stay unless you purge.
11946
+ Run with sudo on a dedicated Linux server. Builds run on this server's own k3s with
11947
+ no VM boundary; Pod Security and an egress policy stand in for one. See the guide's
11948
+ "Link a dedicated server (host)" section.
11949
+ `;
11950
+ ALLOWED_FLAGS = /* @__PURE__ */ new Map([
11951
+ ["setup", []],
11952
+ ["link", ["name", "api-url", "relink"]],
11953
+ ["status", ["json"]],
11954
+ ["uninstall", ["purge-k3s", "yes"]]
11955
+ ]);
11956
+ BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["relink", "json", "purge-k3s", "yes"]);
11957
+ MACHINE_NAME = /^[\w .-]{1,80}$/;
11958
+ }
11959
+ });
11960
+
11739
11961
  // src/daemon/workload-cache.ts
11740
11962
  init_paths();
11741
11963
  import { mkdir, rename, rm, writeFile } from "fs/promises";
@@ -11830,43 +12052,8 @@ var nodeFileSystem = {
11830
12052
  }
11831
12053
  };
11832
12054
 
11833
- // src/lib/env.ts
11834
- init_safe_arg();
11835
- init_exec();
11836
- function readCliEnv(env = process.env) {
11837
- const apiUrl = env.CONVEYOR_API_URL ?? "";
11838
- const clusterToken = env.CONVEYOR_CLUSTER_TOKEN ?? "";
11839
- const clusterId = env.CONVEYOR_CLUSTER_ID ?? "";
11840
- if (!apiUrl || !clusterToken) {
11841
- throw new Error(
11842
- "This machine is not enrolled. /etc/conveyor/k3.env must set CONVEYOR_API_URL and CONVEYOR_CLUSTER_TOKEN \u2014 re-run the install command from your Conveyor profile."
11843
- );
11844
- }
11845
- return { apiUrl, clusterToken, clusterId };
11846
- }
11847
- async function preflight(exec) {
11848
- const problems = [];
11849
- if (process.platform !== "linux") {
11850
- problems.push(
11851
- `Managed personal compute requires Linux (this machine reports ${process.platform}).`
11852
- );
11853
- }
11854
- if (process.arch !== "x64" && process.arch !== "arm64") {
11855
- problems.push(`Unsupported CPU architecture ${process.arch} \u2014 amd64 or arm64 is required.`);
11856
- }
11857
- if (typeof process.getuid === "function" && process.getuid() !== 0) {
11858
- problems.push(
11859
- "This guest command requires root inside the VM. Use conveyor-k3 vm commands on the host."
11860
- );
11861
- }
11862
- if (!await commandExists(exec, "systemctl")) {
11863
- problems.push("systemd is required and `systemctl` was not found.");
11864
- }
11865
- return { ok: problems.length === 0, problems };
11866
- }
11867
- function checkedHostname(hostname) {
11868
- return assertHostname(hostname, "tunnel hostname");
11869
- }
12055
+ // src/cli.ts
12056
+ init_env();
11870
12057
 
11871
12058
  // src/lib/state.ts
11872
12059
  init_paths();
@@ -11891,6 +12078,9 @@ async function writeState(fs2, state) {
11891
12078
  await fs2.writeFile(STATE_FILE, `${JSON.stringify(state, null, 2)}
11892
12079
  `, 384);
11893
12080
  }
12081
+ function managesK3s(desired, state) {
12082
+ return desired.isolation === void 0 || state.k3sInstalledByConveyor === true;
12083
+ }
11894
12084
 
11895
12085
  // src/cli.ts
11896
12086
  init_paths();
@@ -12775,6 +12965,255 @@ var GRANTED_COMMAND_NAMES = LOCAL_BAKE_ROOT_COMMANDS.map((command) => command.na
12775
12965
 
12776
12966
  // src/commands/uninstall.ts
12777
12967
  init_dist();
12968
+
12969
+ // src/lib/isolation.ts
12970
+ init_dist();
12971
+
12972
+ // src/lib/k3s-config.ts
12973
+ init_safe_arg();
12974
+ var IMAGE_GC_HIGH = "image-gc-high-threshold";
12975
+ var IMAGE_GC_LOW = "image-gc-low-threshold";
12976
+ var SYSTEM_RESERVED = "system-reserved";
12977
+ var K3S_DISABLED_COMPONENTS = ["traefik", "servicelb"];
12978
+ var WRITE_KUBECONFIG_MODE = "write-kubeconfig-mode";
12979
+ var KUBECONFIG_MODE = "600";
12980
+ var K3S_INSTALL_EXEC = [
12981
+ ...K3S_DISABLED_COMPONENTS.flatMap((component) => ["--disable", component]),
12982
+ `--${WRITE_KUBECONFIG_MODE}`,
12983
+ KUBECONFIG_MODE
12984
+ ].join(" ");
12985
+ function bareToken(token) {
12986
+ let bare = token.trim();
12987
+ if (bare.endsWith("\\")) bare = bare.slice(0, -1).trim();
12988
+ const quote = bare[0];
12989
+ if (bare.length >= 2 && (quote === "'" || quote === '"') && bare.endsWith(quote)) {
12990
+ bare = bare.slice(1, -1);
12991
+ }
12992
+ return bare;
12993
+ }
12994
+ function ownerConfiguredKubeletFlags(ownerConfig) {
12995
+ const found = /* @__PURE__ */ new Set();
12996
+ if (!ownerConfig) return found;
12997
+ for (const flag of [IMAGE_GC_HIGH, IMAGE_GC_LOW, SYSTEM_RESERVED]) {
12998
+ if (new RegExp(`(^|[\\s"'-])${flag}\\s*=`, "m").test(ownerConfig)) found.add(flag);
12999
+ }
13000
+ return found;
13001
+ }
13002
+ function ownerConfiguredKeys(ownerConfig) {
13003
+ const found = /* @__PURE__ */ new Set();
13004
+ if (!ownerConfig) return found;
13005
+ for (const key of [WRITE_KUBECONFIG_MODE]) {
13006
+ if (new RegExp(`^${key}\\s*:`, "m").test(ownerConfig)) found.add(key);
13007
+ }
13008
+ return found;
13009
+ }
13010
+ function ownerImageGcHighThreshold(ownerConfig) {
13011
+ if (!ownerConfig) return null;
13012
+ const match = new RegExp(`(^|[\\s"'-])${IMAGE_GC_HIGH}\\s*=\\s*(\\d{1,3})`, "m").exec(
13013
+ ownerConfig
13014
+ );
13015
+ if (!match) return null;
13016
+ const value = Number.parseInt(match[2] ?? "", 10);
13017
+ return Number.isInteger(value) && value >= 0 && value <= 100 ? value : null;
13018
+ }
13019
+ function renderK3sDropIn(kubelet, ownerConfig, options = {}) {
13020
+ const ownerFlags = ownerConfiguredKubeletFlags(ownerConfig);
13021
+ const ownerKeys = ownerConfiguredKeys(ownerConfig);
13022
+ const args = [];
13023
+ const skipped = [];
13024
+ const add = (flag, value) => {
13025
+ if (ownerFlags.has(flag)) {
13026
+ skipped.push(flag);
13027
+ return;
13028
+ }
13029
+ args.push(` - "${flag}=${value}"`);
13030
+ };
13031
+ add(IMAGE_GC_HIGH, String(assertInteger(kubelet.imageGcHighThreshold, 1, 100, IMAGE_GC_HIGH)));
13032
+ add(IMAGE_GC_LOW, String(assertInteger(kubelet.imageGcLowThreshold, 0, 99, IMAGE_GC_LOW)));
13033
+ if (kubelet.systemReserved) {
13034
+ const cpu = assertQuantity(kubelet.systemReserved.cpu, "reserveCpu");
13035
+ const memory = assertQuantity(kubelet.systemReserved.memory, "reserveMemory");
13036
+ add(SYSTEM_RESERVED, `cpu=${cpu},memory=${memory}`);
13037
+ }
13038
+ const lines2 = [
13039
+ "# Managed by Conveyor (@rallycry/conveyor-k3). Do not edit.",
13040
+ "# Your own /etc/rancher/k3s/config.yaml is never modified; k3s merges both.",
13041
+ "# `kubelet-arg+` and `disable+` append rather than replacing a list you set."
13042
+ ];
13043
+ if (options.disableComponents !== false) {
13044
+ lines2.push("disable+:");
13045
+ for (const component of K3S_DISABLED_COMPONENTS) lines2.push(` - ${component}`);
13046
+ }
13047
+ if (ownerKeys.has(WRITE_KUBECONFIG_MODE)) {
13048
+ skipped.push(WRITE_KUBECONFIG_MODE);
13049
+ } else {
13050
+ lines2.push(`${WRITE_KUBECONFIG_MODE}: "${KUBECONFIG_MODE}"`);
13051
+ }
13052
+ if (args.length > 0) {
13053
+ lines2.push("kubelet-arg+:", ...args);
13054
+ }
13055
+ return { content: [...lines2, ""].join("\n"), skipped };
13056
+ }
13057
+
13058
+ // src/lib/isolation.ts
13059
+ init_safe_arg();
13060
+ var EGRESS_POLICY_NAME = "conveyor-k3-egress";
13061
+ var MANAGED_BY2 = { "app.kubernetes.io/managed-by": "conveyor-k3" };
13062
+ var NON_ROUTABLE_IPV4 = [
13063
+ "0.0.0.0/8",
13064
+ "10.0.0.0/8",
13065
+ "100.64.0.0/10",
13066
+ "127.0.0.0/8",
13067
+ "169.254.0.0/16",
13068
+ "172.16.0.0/12",
13069
+ "192.0.0.0/24",
13070
+ "192.0.2.0/24",
13071
+ "192.88.99.0/24",
13072
+ "192.168.0.0/16",
13073
+ "198.18.0.0/15",
13074
+ "198.51.100.0/24",
13075
+ "203.0.113.0/24",
13076
+ "224.0.0.0/4",
13077
+ "240.0.0.0/4"
13078
+ ];
13079
+ var KUBE_DNS = { namespace: "kube-system", labels: { "k8s-app": "kube-dns" } };
13080
+ var PSA_PREFIX = "pod-security.kubernetes.io";
13081
+ function psaLabels(level) {
13082
+ const advisory = level === "baseline" ? "restricted" : level;
13083
+ return {
13084
+ [`${PSA_PREFIX}/enforce`]: level,
13085
+ [`${PSA_PREFIX}/warn`]: advisory,
13086
+ [`${PSA_PREFIX}/audit`]: advisory
13087
+ };
13088
+ }
13089
+ var PSA_LABEL_KEYS = Object.keys(psaLabels("baseline"));
13090
+ function ipv4Value(address) {
13091
+ const parts = address.split(".");
13092
+ if (parts.length !== 4) return null;
13093
+ let value = 0;
13094
+ for (const part of parts) {
13095
+ if (!/^\d{1,3}$/.test(part)) return null;
13096
+ const octet = Number(part);
13097
+ if (octet > 255) return null;
13098
+ value = value * 256 + octet;
13099
+ }
13100
+ return value;
13101
+ }
13102
+ function inNonRoutableRange(address) {
13103
+ const value = ipv4Value(address);
13104
+ if (value === null) return false;
13105
+ return NON_ROUTABLE_IPV4.some((cidr) => {
13106
+ const [base = "", bits = "32"] = cidr.split("/");
13107
+ const start = ipv4Value(base) ?? 0;
13108
+ return value >= start && value < start + 2 ** (32 - Number(bits));
13109
+ });
13110
+ }
13111
+ function hostAddressExceptions(addresses) {
13112
+ const kept = /* @__PURE__ */ new Set();
13113
+ for (const raw of addresses) {
13114
+ const address = raw.trim();
13115
+ if (ipv4Value(address) === null || inNonRoutableRange(address)) continue;
13116
+ kept.add(`${address}/32`);
13117
+ }
13118
+ return [...kept].sort();
13119
+ }
13120
+ function renderEgressNetworkPolicy(namespace, hostAddresses) {
13121
+ const policy = {
13122
+ apiVersion: "networking.k8s.io/v1",
13123
+ kind: "NetworkPolicy",
13124
+ metadata: {
13125
+ name: EGRESS_POLICY_NAME,
13126
+ namespace: assertK8sName(namespace, "namespace"),
13127
+ labels: MANAGED_BY2
13128
+ },
13129
+ spec: {
13130
+ podSelector: {},
13131
+ policyTypes: ["Egress"],
13132
+ egress: [
13133
+ {
13134
+ to: [
13135
+ {
13136
+ namespaceSelector: {
13137
+ matchLabels: { "kubernetes.io/metadata.name": KUBE_DNS.namespace }
13138
+ },
13139
+ podSelector: { matchLabels: KUBE_DNS.labels }
13140
+ }
13141
+ ],
13142
+ ports: [
13143
+ { protocol: "UDP", port: 53 },
13144
+ { protocol: "TCP", port: 53 }
13145
+ ]
13146
+ },
13147
+ {
13148
+ // kube-router enforces after the Service's DNAT, so the registry is
13149
+ // reached by its pods' labels, never by the service range.
13150
+ to: [
13151
+ {
13152
+ namespaceSelector: {
13153
+ matchLabels: { "kubernetes.io/metadata.name": PERSONAL_LOCAL_REGISTRY_NAMESPACE }
13154
+ },
13155
+ podSelector: { matchLabels: LOCAL_REGISTRY_SELECTOR }
13156
+ }
13157
+ ],
13158
+ ports: [{ protocol: "TCP", port: PERSONAL_LOCAL_REGISTRY_PORT }]
13159
+ },
13160
+ {
13161
+ to: [
13162
+ {
13163
+ ipBlock: {
13164
+ cidr: "0.0.0.0/0",
13165
+ except: [...NON_ROUTABLE_IPV4, ...hostAddressExceptions(hostAddresses)]
13166
+ }
13167
+ }
13168
+ ]
13169
+ }
13170
+ ]
13171
+ }
13172
+ };
13173
+ return `${JSON.stringify(policy, null, 2)}
13174
+ `;
13175
+ }
13176
+ var DISABLE_KEY = /(?:^|[\s{,])(["']?)disable-network-policy\1\s*:([^,}]*)/g;
13177
+ var DISABLE_FLAGS = /* @__PURE__ */ new Set(["--disable-network-policy", "-disable-network-policy"]);
13178
+ var FALSE_VALUES = /* @__PURE__ */ new Set(["false", "no", "off", "n", "f", "0"]);
13179
+ function setsTrue(raw) {
13180
+ const value = bareToken(raw).trim().toLowerCase();
13181
+ return !FALSE_VALUES.has(value);
13182
+ }
13183
+ function uncommented(line) {
13184
+ return /^\s*#/.test(line) ? "" : line.replace(/\s#.*$/, "");
13185
+ }
13186
+ function configSetsKey(text) {
13187
+ return text.split("\n").map(uncommented).some((line) => [...line.matchAll(DISABLE_KEY)].some((match) => setsTrue(match[2] ?? "")));
13188
+ }
13189
+ function unitLines(unitText) {
13190
+ const lines2 = [];
13191
+ let pending = "";
13192
+ for (const raw of unitText.split("\n")) {
13193
+ const line = raw.trimEnd();
13194
+ if (/^\s*[#;]/.test(line)) continue;
13195
+ if (line.endsWith("\\")) {
13196
+ pending += `${line.slice(0, -1)} `;
13197
+ continue;
13198
+ }
13199
+ lines2.push(pending + line);
13200
+ pending = "";
13201
+ }
13202
+ return pending ? [...lines2, pending] : lines2;
13203
+ }
13204
+ function flagSetIn(line) {
13205
+ return line.split(/\s+/).some((raw) => {
13206
+ const token = bareToken(raw).replace(/^["']+|["']+$/g, "");
13207
+ const [name2 = "", ...value] = token.split("=");
13208
+ return DISABLE_FLAGS.has(name2) && (value.length === 0 || setsTrue(value.join("=")));
13209
+ });
13210
+ }
13211
+ function networkPolicyControllerDisabled(sources) {
13212
+ if (sources.configs.some((text) => text !== null && configSetsKey(text))) return true;
13213
+ return unitLines(sources.unitText).some(flagSetIn);
13214
+ }
13215
+
13216
+ // src/commands/uninstall.ts
12778
13217
  init_paths();
12779
13218
  async function removeLocalRegistry(ports) {
12780
13219
  const removed = await ports.exec(
@@ -12808,6 +13247,47 @@ async function removeLocalRegistry(ports) {
12808
13247
  `Removed the local image registry's mirror from ${REGISTRIES_FILE} (backup: ${backup}).`
12809
13248
  );
12810
13249
  }
13250
+ async function removeIsolation(ports) {
13251
+ const [key, value] = Object.entries(MANAGED_BY2)[0] ?? [];
13252
+ const removed = await ports.exec(
13253
+ "k3s",
13254
+ ["kubectl", "delete", "networkpolicy", "-A", "-l", `${key}=${value}`, "--ignore-not-found"],
13255
+ { timeoutMs: 12e4 }
13256
+ );
13257
+ if (removed.exitCode !== 0) {
13258
+ ports.log(
13259
+ `Could not remove Conveyor's network policies: ${removed.stderr.trim().slice(0, 200)}`
13260
+ );
13261
+ }
13262
+ const listed = await ports.exec("k3s", [
13263
+ "kubectl",
13264
+ "get",
13265
+ "namespaces",
13266
+ "-o",
13267
+ "jsonpath={.items[*].metadata.name}"
13268
+ ]);
13269
+ if (listed.exitCode !== 0) {
13270
+ ports.log(
13271
+ `Could not list namespaces to remove Conveyor's Pod Security labels: ${listed.stderr.trim().slice(0, 200)}`
13272
+ );
13273
+ return;
13274
+ }
13275
+ const namespaces = listed.stdout.split(/\s+/).filter((name2) => name2.startsWith("claudespace-") || name2 === PERSONAL_BAKE_NAMESPACE);
13276
+ for (const namespace of namespaces) {
13277
+ const unlabelled = await ports.exec("k3s", [
13278
+ "kubectl",
13279
+ "label",
13280
+ "namespace",
13281
+ namespace,
13282
+ ...PSA_LABEL_KEYS.map((labelKey) => `${labelKey}-`)
13283
+ ]);
13284
+ if (unlabelled.exitCode !== 0) {
13285
+ ports.log(
13286
+ `Could not remove Conveyor's Pod Security labels from ${namespace}: ${unlabelled.stderr.trim().slice(0, 200)}`
13287
+ );
13288
+ }
13289
+ }
13290
+ }
12811
13291
  async function uninstall(ports, options = {}) {
12812
13292
  const result = { stoppedUnits: [], removedFiles: [], purgedK3s: false };
12813
13293
  if (options.purgeK3s && !options.confirmed) {
@@ -12820,6 +13300,7 @@ async function uninstall(ports, options = {}) {
12820
13300
  result.stoppedUnits.push(unit);
12821
13301
  }
12822
13302
  await removeLocalRegistry(ports);
13303
+ await removeIsolation(ports);
12823
13304
  for (const path of OWNED_FILES) {
12824
13305
  await ports.fs.removeFile(path);
12825
13306
  result.removedFiles.push(path);
@@ -12872,121 +13353,46 @@ async function uninstall(ports, options = {}) {
12872
13353
  return result;
12873
13354
  }
12874
13355
 
12875
- // src/lib/k3s-config.ts
12876
- init_safe_arg();
12877
- var IMAGE_GC_HIGH = "image-gc-high-threshold";
12878
- var IMAGE_GC_LOW = "image-gc-low-threshold";
12879
- var SYSTEM_RESERVED = "system-reserved";
12880
- var K3S_DISABLED_COMPONENTS = ["traefik", "servicelb"];
12881
- var WRITE_KUBECONFIG_MODE = "write-kubeconfig-mode";
12882
- var KUBECONFIG_MODE = "600";
12883
- var K3S_INSTALL_EXEC = [
12884
- ...K3S_DISABLED_COMPONENTS.flatMap((component) => ["--disable", component]),
12885
- `--${WRITE_KUBECONFIG_MODE}`,
12886
- KUBECONFIG_MODE
12887
- ].join(" ");
12888
- function ownerConfiguredKubeletFlags(ownerConfig) {
12889
- const found = /* @__PURE__ */ new Set();
12890
- if (!ownerConfig) return found;
12891
- for (const flag of [IMAGE_GC_HIGH, IMAGE_GC_LOW, SYSTEM_RESERVED]) {
12892
- if (new RegExp(`(^|[\\s"'-])${flag}\\s*=`, "m").test(ownerConfig)) found.add(flag);
13356
+ // src/commands/update.ts
13357
+ init_paths();
13358
+ var CLOUDFLARED_UPDATED_EXIT = 11;
13359
+ function readExecStart(unit) {
13360
+ const lines2 = unit.split("\n");
13361
+ const start = lines2.findIndex((line) => line.trimStart().startsWith("ExecStart="));
13362
+ if (start < 0) return "";
13363
+ const collected = [];
13364
+ for (let index = start; index < lines2.length; index += 1) {
13365
+ const line = lines2[index] ?? "";
13366
+ collected.push(line);
13367
+ if (!line.trimEnd().endsWith("\\")) break;
12893
13368
  }
12894
- return found;
13369
+ return collected.join(" ");
12895
13370
  }
12896
- function ownerConfiguredKeys(ownerConfig) {
12897
- const found = /* @__PURE__ */ new Set();
12898
- if (!ownerConfig) return found;
12899
- for (const key of [WRITE_KUBECONFIG_MODE]) {
12900
- if (new RegExp(`^${key}\\s*:`, "m").test(ownerConfig)) found.add(key);
13371
+ function parseForeignExecFlags(execStart) {
13372
+ const ours = new Set(K3S_INSTALL_EXEC.split(/\s+/).filter((token) => token.startsWith("--")));
13373
+ const foreign = [];
13374
+ for (const raw of execStart.split(/\s+/)) {
13375
+ const token = bareToken(raw);
13376
+ if (!token.startsWith("--")) continue;
13377
+ const name2 = token.split("=")[0] ?? token;
13378
+ if (!ours.has(name2) && !foreign.includes(name2)) foreign.push(name2);
12901
13379
  }
12902
- return found;
13380
+ return foreign;
12903
13381
  }
12904
- function ownerImageGcHighThreshold(ownerConfig) {
12905
- if (!ownerConfig) return null;
12906
- const match = new RegExp(`(^|[\\s"'-])${IMAGE_GC_HIGH}\\s*=\\s*(\\d{1,3})`, "m").exec(
12907
- ownerConfig
12908
- );
12909
- if (!match) return null;
12910
- const value = Number.parseInt(match[2] ?? "", 10);
12911
- return Number.isInteger(value) && value >= 0 && value <= 100 ? value : null;
12912
- }
12913
- function renderK3sDropIn(kubelet, ownerConfig) {
12914
- const ownerFlags = ownerConfiguredKubeletFlags(ownerConfig);
12915
- const ownerKeys = ownerConfiguredKeys(ownerConfig);
12916
- const args = [];
12917
- const skipped = [];
12918
- const add = (flag, value) => {
12919
- if (ownerFlags.has(flag)) {
12920
- skipped.push(flag);
12921
- return;
12922
- }
12923
- args.push(` - "${flag}=${value}"`);
12924
- };
12925
- add(IMAGE_GC_HIGH, String(assertInteger(kubelet.imageGcHighThreshold, 1, 100, IMAGE_GC_HIGH)));
12926
- add(IMAGE_GC_LOW, String(assertInteger(kubelet.imageGcLowThreshold, 0, 99, IMAGE_GC_LOW)));
12927
- if (kubelet.systemReserved) {
12928
- const cpu = assertQuantity(kubelet.systemReserved.cpu, "reserveCpu");
12929
- const memory = assertQuantity(kubelet.systemReserved.memory, "reserveMemory");
12930
- add(SYSTEM_RESERVED, `cpu=${cpu},memory=${memory}`);
12931
- }
12932
- const lines2 = [
12933
- "# Managed by Conveyor (@rallycry/conveyor-k3). Do not edit.",
12934
- "# Your own /etc/rancher/k3s/config.yaml is never modified; k3s merges both.",
12935
- "# `kubelet-arg+` and `disable+` append rather than replacing a list you set."
12936
- ];
12937
- lines2.push("disable+:");
12938
- for (const component of K3S_DISABLED_COMPONENTS) lines2.push(` - ${component}`);
12939
- if (ownerKeys.has(WRITE_KUBECONFIG_MODE)) {
12940
- skipped.push(WRITE_KUBECONFIG_MODE);
12941
- } else {
12942
- lines2.push(`${WRITE_KUBECONFIG_MODE}: "${KUBECONFIG_MODE}"`);
12943
- }
12944
- if (args.length > 0) {
12945
- lines2.push("kubelet-arg+:", ...args);
12946
- }
12947
- return { content: [...lines2, ""].join("\n"), skipped };
12948
- }
12949
-
12950
- // src/commands/update.ts
12951
- init_paths();
12952
- var CLOUDFLARED_UPDATED_EXIT = 11;
12953
- function readExecStart(unit) {
12954
- const lines2 = unit.split("\n");
12955
- const start = lines2.findIndex((line) => line.trimStart().startsWith("ExecStart="));
12956
- if (start < 0) return "";
12957
- const collected = [];
12958
- for (let index = start; index < lines2.length; index += 1) {
12959
- const line = lines2[index] ?? "";
12960
- collected.push(line);
12961
- if (!line.trimEnd().endsWith("\\")) break;
12962
- }
12963
- return collected.join(" ");
12964
- }
12965
- function bareToken(token) {
12966
- let bare = token.trim();
12967
- if (bare.endsWith("\\")) bare = bare.slice(0, -1).trim();
12968
- if (bare.length >= 2 && bare.startsWith("'") && bare.endsWith("'")) bare = bare.slice(1, -1);
12969
- return bare;
12970
- }
12971
- function parseForeignExecFlags(execStart) {
12972
- const ours = new Set(K3S_INSTALL_EXEC.split(/\s+/).filter((token) => token.startsWith("--")));
12973
- const foreign = [];
12974
- for (const raw of execStart.split(/\s+/)) {
12975
- const token = bareToken(raw);
12976
- if (!token.startsWith("--")) continue;
12977
- const name2 = token.split("=")[0] ?? token;
12978
- if (!ours.has(name2) && !foreign.includes(name2)) foreign.push(name2);
12979
- }
12980
- return foreign;
12981
- }
12982
- async function foreignK3sExecFlags(exec) {
12983
- const unit = await exec("systemctl", ["cat", "k3s.service"], { timeoutMs: 1e4 }).catch(
12984
- () => null
13382
+ async function foreignK3sExecFlags(exec) {
13383
+ const unit = await exec("systemctl", ["cat", "k3s.service"], { timeoutMs: 1e4 }).catch(
13384
+ () => null
12985
13385
  );
12986
13386
  if (!unit || unit.exitCode !== 0) return [];
12987
13387
  return parseForeignExecFlags(readExecStart(unit.stdout));
12988
13388
  }
12989
13389
  function decideK3sUpgrade(input) {
13390
+ if (input.managesK3s === false) {
13391
+ return {
13392
+ upgrade: false,
13393
+ reason: "Conveyor did not install this server's k3s, so its owner upgrades it"
13394
+ };
13395
+ }
12990
13396
  if (!input.autoUpgradeK3s) {
12991
13397
  return { upgrade: false, reason: "automatic k3s upgrades are turned off for this machine" };
12992
13398
  }
@@ -13083,6 +13489,7 @@ async function runUpdate(ports, decision, channel, targetVersion) {
13083
13489
 
13084
13490
  // src/commands/setup.ts
13085
13491
  init_exec();
13492
+ init_env();
13086
13493
  init_paths();
13087
13494
 
13088
13495
  // src/lib/systemd.ts
@@ -13221,6 +13628,8 @@ async function ensureK3s(ports) {
13221
13628
  }
13222
13629
  });
13223
13630
  ports.log("k3s installed.");
13631
+ await ports.fs.ensureDir(STATE_DIR);
13632
+ await writeState(ports.fs, { ...await readState(ports.fs), k3sInstalledByConveyor: true });
13224
13633
  }
13225
13634
  async function ensureCloudflared(ports) {
13226
13635
  if (await commandExists(ports.exec, "cloudflared")) {
@@ -13306,11 +13715,12 @@ async function installUnits(ports, bundle, cloudflaredBin, maintenanceHour) {
13306
13715
  await runOrThrow(ports.exec, "systemctl", ["enable", DAEMON_UNIT]);
13307
13716
  await runOrThrow(ports.exec, "systemctl", ["enable", UPDATE_TIMER]);
13308
13717
  }
13309
- async function recordSetupState(ports, bundle, appliedVersion, bins) {
13718
+ async function recordSetupState(ports, bundle, appliedVersion, bins, recorded = {}) {
13310
13719
  await ports.fs.ensureDir(STATE_DIR);
13311
13720
  const state = await readState(ports.fs);
13312
13721
  await writeState(ports.fs, {
13313
13722
  ...state,
13723
+ ...recorded,
13314
13724
  hostname: checkedHostname(bundle.hostname),
13315
13725
  enrolled: true,
13316
13726
  appliedDesiredStateVersion: appliedVersion,
@@ -13323,86 +13733,6 @@ async function resolveBunBin(ports) {
13323
13733
  return found?.startsWith("/") ? found : "/root/.bun/bin/bun";
13324
13734
  }
13325
13735
 
13326
- // src/lib/bins.ts
13327
- var INSTALLER_PATHS = {
13328
- bun: "/root/.bun/bin/bun",
13329
- cli: "/root/.bun/bin/conveyor-k3",
13330
- cloudflared: "/usr/local/bin/cloudflared"
13331
- };
13332
- async function whichOrNull(exec, bin) {
13333
- const result = await exec("which", [bin]).catch(() => null);
13334
- if (!result || result.exitCode !== 0) return null;
13335
- const path = result.stdout.trim().split("\n")[0]?.trim();
13336
- return path && path.startsWith("/") ? path : null;
13337
- }
13338
- async function resolveBinaries(exec, state, selfPath) {
13339
- const recorded = state.bins;
13340
- const cliSelf = selfPath?.startsWith("/") ? selfPath : null;
13341
- return {
13342
- bun: recorded?.bun ?? await whichOrNull(exec, "bun") ?? INSTALLER_PATHS.bun,
13343
- // `cli` prefers the running process's own path: if this code is executing,
13344
- // that path is by definition correct.
13345
- cli: cliSelf ?? recorded?.cli ?? await whichOrNull(exec, "conveyor-k3") ?? INSTALLER_PATHS.cli,
13346
- cloudflared: recorded?.cloudflared ?? await whichOrNull(exec, "cloudflared") ?? INSTALLER_PATHS.cloudflared
13347
- };
13348
- }
13349
-
13350
- // src/cli.ts
13351
- init_journald();
13352
-
13353
- // src/commands/setup-session.ts
13354
- init_exec();
13355
- init_paths();
13356
- async function withSetupConnection(ports, connection, enroll) {
13357
- for (const unit of [UPDATE_TIMER, UPDATE_UNIT, DAEMON_UNIT]) {
13358
- if (await ports.fs.readFile(`${SYSTEMD_DIR}/${unit}`))
13359
- await runOrThrow(ports.exec, "systemctl", ["stop", unit]);
13360
- }
13361
- let result;
13362
- try {
13363
- await connection.connect();
13364
- result = await enroll();
13365
- } finally {
13366
- connection.disconnect();
13367
- }
13368
- await runOrThrow(ports.exec, "systemctl", ["start", DAEMON_UNIT]);
13369
- await runOrThrow(ports.exec, "systemctl", ["start", UPDATE_TIMER]);
13370
- return result;
13371
- }
13372
-
13373
- // src/lib/guest-vm.ts
13374
- async function requireGuestVm(exec) {
13375
- try {
13376
- const result = await exec("systemd-detect-virt", ["--vm", "--quiet"]);
13377
- if (result.exitCode === 0) return;
13378
- } catch {
13379
- }
13380
- throw new Error("Refusing to run guest setup on a physical host; use `conveyor-k3 vm setup`");
13381
- }
13382
-
13383
- // src/lib/k3s-channel.ts
13384
- var CHANNELS_URL = "https://update.k3s.io/v1-release/channels";
13385
- var REQUEST_TIMEOUT_MS2 = 15e3;
13386
- function parseChannelVersion(version) {
13387
- const match = /v?(\d+)\.(\d+)\./.exec(version);
13388
- if (!match) return null;
13389
- return [Number(match[1]), Number(match[2])];
13390
- }
13391
- async function resolveChannelLatest(channel, fetchImpl = fetch) {
13392
- try {
13393
- const response = await fetchImpl(CHANNELS_URL, {
13394
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS2),
13395
- headers: { Accept: "application/json" }
13396
- });
13397
- if (!response.ok) return null;
13398
- const body = await response.json();
13399
- const match = body.data?.find((entry) => entry.id === channel || entry.name === channel);
13400
- return match?.latest ? parseChannelVersion(match.latest) : null;
13401
- } catch {
13402
- return null;
13403
- }
13404
- }
13405
-
13406
13736
  // src/daemon/connection.ts
13407
13737
  import { io } from "socket.io-client";
13408
13738
 
@@ -13435,27 +13765,27 @@ function readControlFile(path) {
13435
13765
  }
13436
13766
  function controlAlias(apiUrl, text) {
13437
13767
  if (!text) return void 0;
13438
- let hostname;
13768
+ let hostname2;
13439
13769
  let aliases;
13440
13770
  try {
13441
- hostname = new URL(apiUrl).hostname.toLowerCase();
13771
+ hostname2 = new URL(apiUrl).hostname.toLowerCase();
13442
13772
  aliases = JSON.parse(text);
13443
13773
  } catch {
13444
13774
  return void 0;
13445
13775
  }
13446
13776
  if (typeof aliases !== "object" || aliases === null || Array.isArray(aliases)) return void 0;
13447
- const alias = aliases[hostname];
13448
- return typeof alias === "string" && CONTROL_ALIAS_IP.test(alias) ? { hostname, alias } : void 0;
13777
+ const alias = aliases[hostname2];
13778
+ return typeof alias === "string" && CONTROL_ALIAS_IP.test(alias) ? { hostname: hostname2, alias } : void 0;
13449
13779
  }
13450
13780
  function controlPlaneRoute(apiUrl, readFile9 = readControlFile, fileExists = existsSync) {
13451
13781
  if (!fileExists(CONTROL_FILE)) return void 0;
13452
13782
  let skipAlias = false;
13453
13783
  let usedAlias = false;
13454
- const lookup = (hostname, options, callback) => {
13784
+ const lookup = (hostname2, options, callback) => {
13455
13785
  const target2 = skipAlias ? void 0 : controlAlias(apiUrl, readFile9(CONTROL_FILE));
13456
- usedAlias = target2 !== void 0 && hostname.toLowerCase() === target2.hostname;
13786
+ usedAlias = target2 !== void 0 && hostname2.toLowerCase() === target2.hostname;
13457
13787
  if (!target2 || !usedAlias) {
13458
- dnsLookup(hostname, options, callback);
13788
+ dnsLookup(hostname2, options, callback);
13459
13789
  return;
13460
13790
  }
13461
13791
  if (options.all) {
@@ -13500,7 +13830,7 @@ function createClusterProof(apiUrl, clusterToken, key) {
13500
13830
  const origin = new URL(apiUrl).origin;
13501
13831
  const timestamp = Date.now();
13502
13832
  const nonce = randomBytes(32).toString("base64url");
13503
- const message2 = `conveyor-device-v1
13833
+ const message3 = `conveyor-device-v1
13504
13834
  ${origin}
13505
13835
  ${clusterToken}
13506
13836
  ${timestamp}
@@ -13508,7 +13838,7 @@ ${nonce}`;
13508
13838
  return {
13509
13839
  timestamp,
13510
13840
  nonce,
13511
- signature: sign(null, Buffer.from(message2, "utf8"), key).toString("base64url")
13841
+ signature: sign(null, Buffer.from(message3, "utf8"), key).toString("base64url")
13512
13842
  };
13513
13843
  }
13514
13844
  function deviceClusterAuth(apiUrl, clusterToken, keyPath = DEVICE_KEY_PATH, ownerUid = 0) {
@@ -13522,6 +13852,8 @@ function deviceClusterAuth(apiUrl, clusterToken, keyPath = DEVICE_KEY_PATH, owne
13522
13852
 
13523
13853
  // src/daemon/connection.ts
13524
13854
  init_dist();
13855
+ var REDIAL_FIRST_MS = 5e3;
13856
+ var REDIAL_MAX_MS = 5 * 6e4;
13525
13857
  function unwrap(ack, what) {
13526
13858
  if (!ack?.ok || ack.data === void 0) {
13527
13859
  throw new Error(`${what} failed: ${ack?.error ?? "no response"}`);
@@ -13531,6 +13863,8 @@ function unwrap(ack, what) {
13531
13863
  var ClusterConnection = class {
13532
13864
  socket = null;
13533
13865
  config;
13866
+ redialDelayMs = REDIAL_FIRST_MS;
13867
+ redialTimer = null;
13534
13868
  /** Called when Conveyor says the desired state changed. */
13535
13869
  onSyncRequested = null;
13536
13870
  /** Called when Conveyor asks the machine to remove itself. */
@@ -13566,6 +13900,7 @@ var ClusterConnection = class {
13566
13900
  });
13567
13901
  this.socket = socket;
13568
13902
  socket.on("connect", () => {
13903
+ this.redialDelayMs = REDIAL_FIRST_MS;
13569
13904
  if (!settled) {
13570
13905
  settled = true;
13571
13906
  resolve();
@@ -13584,6 +13919,9 @@ var ClusterConnection = class {
13584
13919
  socket.on("disconnect", (reason) => {
13585
13920
  process.stderr.write(`[conveyor-k3] disconnected: ${reason}
13586
13921
  `);
13922
+ if (reason === "io server disconnect" && this.config.reconnectWhenReplaced) {
13923
+ this.redial(socket);
13924
+ }
13587
13925
  });
13588
13926
  socket.on(PERSONAL_CLUSTER_EVENTS.syncRequested, () => {
13589
13927
  this.onSyncRequested?.();
@@ -13645,217 +13983,68 @@ var ClusterConnection = class {
13645
13983
  } catch {
13646
13984
  }
13647
13985
  }
13986
+ /** Dial again after the server closed the socket, waiting longer each time. */
13987
+ redial(socket) {
13988
+ if (this.redialTimer) return;
13989
+ const delay2 = this.redialDelayMs;
13990
+ this.redialDelayMs = Math.min(delay2 * 2, REDIAL_MAX_MS);
13991
+ this.redialTimer = setTimeout(() => {
13992
+ this.redialTimer = null;
13993
+ if (this.socket === socket && !socket.connected) socket.connect();
13994
+ }, delay2);
13995
+ this.redialTimer.unref?.();
13996
+ }
13648
13997
  disconnect() {
13998
+ if (this.redialTimer) clearTimeout(this.redialTimer);
13999
+ this.redialTimer = null;
13649
14000
  this.socket?.disconnect();
13650
14001
  this.socket = null;
13651
14002
  }
13652
14003
  };
13653
14004
 
13654
- // src/daemon/runner.ts
13655
- init_dist();
13656
-
13657
- // src/lib/images.ts
13658
- init_dist();
13659
- init_exec();
13660
- init_safe_arg();
13661
- function parseSize(size) {
13662
- const n = Number.parseInt(size ?? "0", 10);
13663
- return Number.isFinite(n) ? n : 0;
13664
- }
13665
- function refsOf(image) {
13666
- return [...image.repoTags ?? [], ...image.repoDigests ?? []];
13667
- }
13668
- function selectStrandedImages(inventory, wantedImages) {
13669
- const wanted = new Set(wantedImages);
13670
- const candidates = [];
13671
- for (const image of inventory.images) {
13672
- const tags = image.repoTags ?? [];
13673
- if (tags.length > 0) continue;
13674
- if (inventory.referencedIds.has(image.id)) continue;
13675
- if (refsOf(image).some((ref) => wanted.has(ref))) continue;
13676
- candidates.push({ id: image.id, sizeBytes: parseSize(image.size) });
13677
- }
13678
- return candidates;
13679
- }
13680
- async function readImageInventory(exec) {
13681
- const imagesResult = await exec("crictl", ["images", "-o", "json"]);
13682
- const containersResult = await exec("crictl", ["ps", "-a", "-o", "json"]);
13683
- if (imagesResult.exitCode !== 0) {
13684
- throw new Error(
13685
- `could not read the image list: ${imagesResult.stderr.trim().slice(0, 120) || `crictl images exited ${imagesResult.exitCode}`}`
13686
- );
13687
- }
13688
- let images = [];
14005
+ // src/lib/manifest-guard.ts
14006
+ import { isMap, isScalar, parseAllDocuments } from "yaml";
14007
+ var ALLOWED_MANIFEST_KINDS = [
14008
+ "Namespace",
14009
+ "Role",
14010
+ "RoleBinding",
14011
+ "ClusterRole",
14012
+ "ClusterRoleBinding",
14013
+ "PriorityClass"
14014
+ ];
14015
+ var ALLOWED = new Set(ALLOWED_MANIFEST_KINDS);
14016
+ var MISSING_KIND = "(missing kind)";
14017
+ var UNPARSEABLE = "(unparseable)";
14018
+ function findDisallowedKinds(manifest) {
14019
+ let documents;
13689
14020
  try {
13690
- const parsed = JSON.parse(imagesResult.stdout || "{}");
13691
- images = parsed.images ?? [];
14021
+ documents = parseAllDocuments(manifest);
13692
14022
  } catch {
13693
- images = [];
14023
+ return [UNPARSEABLE];
13694
14024
  }
13695
- const referencedIds = /* @__PURE__ */ new Set();
13696
- try {
13697
- const parsed = JSON.parse(containersResult.stdout || "{}");
13698
- for (const container of parsed.containers ?? []) {
13699
- if (container.imageRef) referencedIds.add(container.imageRef);
13700
- if (container.image?.image) referencedIds.add(container.image.image);
14025
+ const disallowed = [];
14026
+ const record2 = (kind) => {
14027
+ if (!disallowed.includes(kind)) disallowed.push(kind);
14028
+ };
14029
+ for (const doc of documents) {
14030
+ if (doc.errors.length > 0) {
14031
+ record2(UNPARSEABLE);
14032
+ continue;
13701
14033
  }
13702
- } catch {
13703
- throw new Error("could not read the container list; skipping prune this cycle");
13704
- }
13705
- return { images, referencedIds };
13706
- }
13707
- async function pruneStrandedImages(exec, inventory, wantedImages) {
13708
- const candidates = selectStrandedImages(inventory, wantedImages);
13709
- const result = { removed: 0, bytesReclaimed: 0, failures: [] };
13710
- for (const candidate of candidates) {
13711
- const removal = await exec("crictl", ["rmi", candidate.id]);
13712
- if (removal.exitCode === 0) {
13713
- result.removed += 1;
13714
- result.bytesReclaimed += candidate.sizeBytes;
13715
- } else {
13716
- result.failures.push(`${candidate.id}: ${removal.stderr.trim().slice(0, 120)}`);
14034
+ if (doc.contents === null || doc.contents === void 0) continue;
14035
+ if (isScalar(doc.contents) && doc.contents.value === null) continue;
14036
+ if (!isMap(doc.contents)) {
14037
+ record2(MISSING_KIND);
14038
+ continue;
14039
+ }
14040
+ const kind = doc.get("kind");
14041
+ if (typeof kind !== "string" || kind.length === 0) {
14042
+ record2(MISSING_KIND);
14043
+ continue;
13717
14044
  }
14045
+ if (!ALLOWED.has(kind)) record2(kind);
13718
14046
  }
13719
- return result;
13720
- }
13721
- async function warmImages(exec, wantedImages, unauthenticatedHosts) {
13722
- const blocked = new Set(unauthenticatedHosts);
13723
- const result = { pulled: [], skipped: [], failures: [], skippedArtifactRegistry: 0 };
13724
- for (const ref of wantedImages) {
13725
- const host = ref.split("/")[0] ?? "";
13726
- if (host === "localhost" || host.startsWith("localhost:")) {
13727
- result.skipped.push(ref);
13728
- continue;
13729
- }
13730
- if (isArtifactRegistryHost(host)) {
13731
- result.skipped.push(ref);
13732
- result.skippedArtifactRegistry += 1;
13733
- continue;
13734
- }
13735
- if (blocked.has(host)) {
13736
- result.skipped.push(ref);
13737
- continue;
13738
- }
13739
- try {
13740
- assertImageRef(ref);
13741
- } catch {
13742
- result.failures.push(`${ref}: not a valid image reference`);
13743
- continue;
13744
- }
13745
- const pull = await exec("crictl", ["pull", ref], { timeoutMs: 15 * 6e4 });
13746
- if (pull.exitCode === 0) {
13747
- result.pulled.push(ref);
13748
- } else {
13749
- result.failures.push(`${ref}: ${pull.stderr.trim().slice(0, 120)}`);
13750
- }
13751
- }
13752
- return result;
13753
- }
13754
- var PIN_LABEL = "io.cri-containerd.pinned";
13755
- async function resolveImageLabelTool(exec) {
13756
- if (await commandExists(exec, "k3s")) {
13757
- return { bin: "k3s", args: ["ctr", "--namespace", "k8s.io", "images", "label"] };
13758
- }
13759
- if (await commandExists(exec, "ctr")) {
13760
- return { bin: "ctr", args: ["--namespace", "k8s.io", "images", "label"] };
13761
- }
13762
- return null;
13763
- }
13764
- function refsPresent(inventory) {
13765
- const present = /* @__PURE__ */ new Set();
13766
- for (const image of inventory.images) {
13767
- for (const ref of refsOf(image)) present.add(ref);
13768
- }
13769
- return present;
13770
- }
13771
- async function pinWantedImages(exec, wantedImages, inventory, previouslyPinned = []) {
13772
- const result = { pinned: [], unpinned: [], absent: [], failures: [] };
13773
- const wanted = new Set(wantedImages);
13774
- const present = refsPresent(inventory);
13775
- const toPin = wantedImages.filter((ref) => present.has(ref));
13776
- const toUnpin = previouslyPinned.filter((ref) => !wanted.has(ref) && present.has(ref));
13777
- for (const ref of wantedImages) if (!present.has(ref)) result.absent.push(ref);
13778
- if (toPin.length === 0 && toUnpin.length === 0) return result;
13779
- const tool = await resolveImageLabelTool(exec);
13780
- if (!tool) {
13781
- result.warning = "No `k3s ctr` or `ctr` on this machine, so Conveyor cannot pin its images against kubelet garbage collection. A freshly baked image can be deleted before the first pod runs it.";
13782
- return result;
13783
- }
13784
- const label2 = async (ref, value) => {
13785
- try {
13786
- assertImageRef(ref);
13787
- } catch {
13788
- result.failures.push(`${ref}: not a valid image reference`);
13789
- return false;
13790
- }
13791
- const run = await exec(tool.bin, [...tool.args, ref, `${PIN_LABEL}=${value}`], {
13792
- timeoutMs: 3e4
13793
- });
13794
- if (run.exitCode === 0) return true;
13795
- result.failures.push(`${ref}: ${run.stderr.trim().slice(0, 120)}`);
13796
- return false;
13797
- };
13798
- for (const ref of toPin) {
13799
- if (await label2(ref, "pinned")) result.pinned.push(ref);
13800
- }
13801
- for (const ref of toUnpin) {
13802
- if (await label2(ref, "")) result.unpinned.push(ref);
13803
- }
13804
- return result;
13805
- }
13806
- function countPinnedWanted(inventory, wantedImages) {
13807
- const wanted = new Set(wantedImages);
13808
- return inventory.images.filter(
13809
- (image) => image.pinned === true && refsOf(image).some((ref) => wanted.has(ref))
13810
- ).length;
13811
- }
13812
-
13813
- // src/daemon/runner.ts
13814
- init_paths();
13815
-
13816
- // src/lib/manifest-guard.ts
13817
- import { isMap, isScalar, parseAllDocuments } from "yaml";
13818
- var ALLOWED_MANIFEST_KINDS = [
13819
- "Namespace",
13820
- "Role",
13821
- "RoleBinding",
13822
- "ClusterRole",
13823
- "ClusterRoleBinding",
13824
- "PriorityClass"
13825
- ];
13826
- var ALLOWED = new Set(ALLOWED_MANIFEST_KINDS);
13827
- var MISSING_KIND = "(missing kind)";
13828
- var UNPARSEABLE = "(unparseable)";
13829
- function findDisallowedKinds(manifest) {
13830
- let documents;
13831
- try {
13832
- documents = parseAllDocuments(manifest);
13833
- } catch {
13834
- return [UNPARSEABLE];
13835
- }
13836
- const disallowed = [];
13837
- const record2 = (kind) => {
13838
- if (!disallowed.includes(kind)) disallowed.push(kind);
13839
- };
13840
- for (const doc of documents) {
13841
- if (doc.errors.length > 0) {
13842
- record2(UNPARSEABLE);
13843
- continue;
13844
- }
13845
- if (doc.contents === null || doc.contents === void 0) continue;
13846
- if (isScalar(doc.contents) && doc.contents.value === null) continue;
13847
- if (!isMap(doc.contents)) {
13848
- record2(MISSING_KIND);
13849
- continue;
13850
- }
13851
- const kind = doc.get("kind");
13852
- if (typeof kind !== "string" || kind.length === 0) {
13853
- record2(MISSING_KIND);
13854
- continue;
13855
- }
13856
- if (!ALLOWED.has(kind)) record2(kind);
13857
- }
13858
- return disallowed;
14047
+ return disallowed;
13859
14048
  }
13860
14049
 
13861
14050
  // src/daemon/reconcile.ts
@@ -13933,6 +14122,80 @@ async function loadRegistryMirror(ctx) {
13933
14122
  return { restarted: true, warning: null };
13934
14123
  }
13935
14124
 
14125
+ // src/daemon/namespace-upkeep.ts
14126
+ init_dist();
14127
+ init_exec();
14128
+ init_safe_arg();
14129
+
14130
+ // src/daemon/isolation-probes.ts
14131
+ init_paths();
14132
+ var CONTROLLER_DISABLED_WARNING = "k3s runs with its network policy controller disabled, so the egress policy Conveyor applies here is not enforced. Remove disable-network-policy from the k3s config or unit and restart k3s. Until then this server is not isolated; Conveyor checks again within 10 minutes.";
14133
+ var CONTROLLER_UNKNOWN_WARNING = "Could not read the k3s unit or its config files to check that its network policy controller is on, so this server does not count as isolated. Check `systemctl cat k3s.service` and /etc/rancher/k3s; Conveyor checks again within 10 minutes.";
14134
+ var NODE_ADDRESSES_JSONPATH = '{range .items[*]}{range .status.addresses[?(@.type=="InternalIP")]}{.address}{"\\n"}{end}{range .status.addresses[?(@.type=="ExternalIP")]}{.address}{"\\n"}{end}{end}';
14135
+ var API_ENDPOINTS_JSONPATH = "{.items[*].endpoints[*].addresses[*]}";
14136
+ var IPV4_ON_INTERFACE = /\binet (\d{1,3}(?:\.\d{1,3}){3})(?:\/| peer )/g;
14137
+ async function stdoutOf(exec, bin, args) {
14138
+ const run = await exec(bin, args, { timeoutMs: 1e4 }).catch(() => null);
14139
+ return run && run.exitCode === 0 ? run.stdout : null;
14140
+ }
14141
+ async function readK3sConfigs(ctx) {
14142
+ const listed = await stdoutOf(ctx.exec, "find", [
14143
+ "-L",
14144
+ K3S_CONFIG_DROPIN_DIR,
14145
+ "-maxdepth",
14146
+ "1",
14147
+ "-type",
14148
+ "f",
14149
+ "(",
14150
+ "-iname",
14151
+ "*.yaml",
14152
+ "-o",
14153
+ "-iname",
14154
+ "*.yml",
14155
+ ")"
14156
+ ]);
14157
+ if (listed === null) return null;
14158
+ const paths = listed.split("\n").map((path) => path.trim()).filter(Boolean).sort();
14159
+ const dropIns = await Promise.all(paths.map((path) => ctx.fs.readFile(path)));
14160
+ if (dropIns.some((text) => text === null)) return null;
14161
+ return [await ctx.fs.readFile(K3S_OWNER_CONFIG), ...dropIns];
14162
+ }
14163
+ async function controllerProblem(ctx) {
14164
+ const unitText = await stdoutOf(ctx.exec, "systemctl", ["cat", "k3s.service"]);
14165
+ const configs = unitText === null ? null : await readK3sConfigs(ctx);
14166
+ if (unitText === null || configs === null) return CONTROLLER_UNKNOWN_WARNING;
14167
+ return networkPolicyControllerDisabled({ configs, unitText }) ? CONTROLLER_DISABLED_WARNING : null;
14168
+ }
14169
+ async function readHostAddresses(exec) {
14170
+ const nodes = await stdoutOf(exec, "k3s", [
14171
+ "kubectl",
14172
+ "get",
14173
+ "nodes",
14174
+ "-o",
14175
+ `jsonpath=${NODE_ADDRESSES_JSONPATH}`
14176
+ ]);
14177
+ const interfaces = await stdoutOf(exec, "ip", ["-o", "-4", "addr", "show"]);
14178
+ const api = await stdoutOf(exec, "k3s", [
14179
+ "kubectl",
14180
+ "get",
14181
+ "endpointslices",
14182
+ "-n",
14183
+ "default",
14184
+ "-l",
14185
+ "kubernetes.io/service-name=kubernetes",
14186
+ "-o",
14187
+ `jsonpath=${API_ENDPOINTS_JSONPATH}`
14188
+ ]);
14189
+ if (nodes === null || interfaces === null || api === null) return null;
14190
+ const onInterfaces = [...interfaces.matchAll(IPV4_ON_INTERFACE)].map((match) => match[1] ?? "");
14191
+ return [...nodes.split(/\s+/), ...onInterfaces, ...api.split(/\s+/)].filter(Boolean);
14192
+ }
14193
+ async function recheckIsolation(ctx, reconciled) {
14194
+ if (reconciled?.applied !== true) return { isolation: reconciled, warnings: [] };
14195
+ const problem = await controllerProblem(ctx);
14196
+ return problem ? { isolation: { applied: false }, warnings: [problem] } : { isolation: reconciled, warnings: [] };
14197
+ }
14198
+
13936
14199
  // src/daemon/prune-namespaces.ts
13937
14200
  init_safe_arg();
13938
14201
  var POOL_NAMESPACE_PREFIX = "claudespace-";
@@ -13996,6 +14259,143 @@ async function pruneStaleNamespaces(ctx, desired, result) {
13996
14259
  }
13997
14260
  }
13998
14261
 
14262
+ // src/daemon/namespace-upkeep.ts
14263
+ function message(error) {
14264
+ return error instanceof Error ? error.message : String(error);
14265
+ }
14266
+ function renderLimitRange(namespace, limits) {
14267
+ const ns = assertK8sName(namespace, "namespace");
14268
+ const values = [
14269
+ assertQuantity(limits.defaultRequest.cpu, "defaultRequest.cpu"),
14270
+ assertQuantity(limits.defaultRequest.memory, "defaultRequest.memory"),
14271
+ assertQuantity(limits.default.cpu, "default.cpu"),
14272
+ assertQuantity(limits.default.memory, "default.memory")
14273
+ ];
14274
+ return [
14275
+ "apiVersion: v1",
14276
+ "kind: LimitRange",
14277
+ "metadata:",
14278
+ " name: conveyor-k3-defaults",
14279
+ ` namespace: ${ns}`,
14280
+ "spec:",
14281
+ " limits:",
14282
+ " - type: Container",
14283
+ " defaultRequest:",
14284
+ ` cpu: "${values[0]}"`,
14285
+ ` memory: "${values[1]}"`,
14286
+ " default:",
14287
+ ` cpu: "${values[2]}"`,
14288
+ ` memory: "${values[3]}"`,
14289
+ ""
14290
+ ].join("\n");
14291
+ }
14292
+ async function reconcileNamespaces(ctx, desired, result) {
14293
+ for (const namespace of desired.namespaces) {
14294
+ try {
14295
+ await kubectlApply(ctx.exec, renderLimitRange(namespace.name, namespace.limitRange));
14296
+ } catch (error) {
14297
+ result.warnings.push(
14298
+ `Could not apply resource defaults to ${namespace.name} (${namespace.projectName}): ${message(error)}`
14299
+ );
14300
+ }
14301
+ }
14302
+ }
14303
+ async function ensureBakeNamespace(ctx, desired) {
14304
+ const found = await ctx.exec(
14305
+ "k3s",
14306
+ ["kubectl", "get", "namespace", PERSONAL_BAKE_NAMESPACE],
14307
+ KUBECTL_TIMEOUT2
14308
+ );
14309
+ if (found.exitCode !== 0) {
14310
+ await runOrThrow(
14311
+ ctx.exec,
14312
+ "k3s",
14313
+ ["kubectl", "create", "namespace", PERSONAL_BAKE_NAMESPACE],
14314
+ KUBECTL_TIMEOUT2
14315
+ );
14316
+ }
14317
+ const limitRange = desired.namespaces[0]?.limitRange;
14318
+ if (limitRange) {
14319
+ await kubectlApply(ctx.exec, renderLimitRange(PERSONAL_BAKE_NAMESPACE, limitRange));
14320
+ }
14321
+ }
14322
+ function knownStance(isolation) {
14323
+ const { podSecurity, egress, ...rest } = { ...isolation };
14324
+ const extra = Object.keys(rest);
14325
+ if (podSecurity === "baseline" && egress === "no-private" && extra.length === 0) return null;
14326
+ const named = `Pod Security "${String(podSecurity).slice(0, 40)}", egress "${String(egress).slice(0, 40)}"`;
14327
+ return extra.length === 0 ? named : `${named}, and ${extra.slice(0, 3).join(", ").slice(0, 80)}`;
14328
+ }
14329
+ var KUBECTL_TIMEOUT2 = { timeoutMs: 3e4 };
14330
+ var RECHECK = "Conveyor checks again within 10 minutes.";
14331
+ async function isolateNamespace(ctx, namespace, isolation, fail2) {
14332
+ const ns = assertK8sName(namespace, "namespace");
14333
+ const labels = Object.entries(psaLabels(isolation.podSecurity)).map(
14334
+ ([key, value]) => `${key}=${value}`
14335
+ );
14336
+ const labelled = await ctx.exec(
14337
+ "k3s",
14338
+ ["kubectl", "label", "namespace", ns, "--overwrite", ...labels],
14339
+ KUBECTL_TIMEOUT2
14340
+ );
14341
+ if (labelled.exitCode !== 0) {
14342
+ fail2(
14343
+ `Could not apply Pod Security labels to ${ns}: ${labelled.stderr.trim().slice(0, 200)} ${RECHECK}`
14344
+ );
14345
+ }
14346
+ if (isolation.hostAddresses === null) return;
14347
+ try {
14348
+ await kubectlApply(ctx.exec, renderEgressNetworkPolicy(ns, isolation.hostAddresses));
14349
+ } catch (error) {
14350
+ fail2(`Could not apply the egress policy to ${ns}: ${message(error).slice(0, 200)} ${RECHECK}`);
14351
+ }
14352
+ }
14353
+ async function reconcileIsolation(ctx, desired, result) {
14354
+ try {
14355
+ await ensureBakeNamespace(ctx, desired);
14356
+ } catch (error) {
14357
+ result.warnings.push(
14358
+ `Could not prepare the bake namespace ${PERSONAL_BAKE_NAMESPACE}: ${message(error)}`
14359
+ );
14360
+ }
14361
+ const isolation = desired.isolation;
14362
+ if (!isolation) {
14363
+ result.isolation = void 0;
14364
+ return;
14365
+ }
14366
+ const unknown = knownStance(isolation);
14367
+ if (unknown) {
14368
+ result.warnings.push(
14369
+ `Conveyor asks for pod isolation this version of conveyor-k3 does not know (${unknown}), so nothing was changed and this server does not count as isolated. Run \`sudo conveyor-k3 update\`.`
14370
+ );
14371
+ result.isolation = { applied: false };
14372
+ return;
14373
+ }
14374
+ let applied = true;
14375
+ const fail2 = (warning) => {
14376
+ applied = false;
14377
+ result.warnings.push(warning);
14378
+ };
14379
+ const problem = await controllerProblem(ctx);
14380
+ if (problem) fail2(problem);
14381
+ const hostAddresses = await readHostAddresses(ctx.exec);
14382
+ if (hostAddresses === null) {
14383
+ fail2(
14384
+ `Could not read every address this server answers on, so the egress policies in place were left as they are and this server does not count as isolated. ${RECHECK}`
14385
+ );
14386
+ }
14387
+ for (const namespace of desired.namespaces) {
14388
+ await isolateNamespace(ctx, namespace.name, { podSecurity: "baseline", hostAddresses }, fail2);
14389
+ }
14390
+ await isolateNamespace(
14391
+ ctx,
14392
+ PERSONAL_BAKE_NAMESPACE,
14393
+ { podSecurity: "privileged", hostAddresses },
14394
+ fail2
14395
+ );
14396
+ result.isolation = { applied };
14397
+ }
14398
+
13999
14399
  // src/daemon/telemetry.ts
14000
14400
  init_dist();
14001
14401
  import { statfs } from "fs/promises";
@@ -14028,30 +14428,186 @@ async function inspectContentHash(exec, imageId) {
14028
14428
  return void 0;
14029
14429
  }
14030
14430
  }
14031
- async function reportLocalImages(exec, inventory, wantedImages, cache) {
14032
- if (!inventory) return void 0;
14431
+ async function reportLocalImages(exec, inventory, wantedImages, cache) {
14432
+ if (!inventory) return void 0;
14433
+ const wanted = new Set(wantedImages);
14434
+ const held = inventory.images.flatMap(
14435
+ (image) => (image.repoTags ?? []).filter((ref) => ref.startsWith("localhost/") && isImageRef(ref)).map((ref) => ({ ref, imageId: image.id }))
14436
+ );
14437
+ held.sort(
14438
+ (a, b) => Number(wanted.has(b.ref)) - Number(wanted.has(a.ref)) || (a.ref < b.ref ? -1 : a.ref > b.ref ? 1 : 0)
14439
+ );
14440
+ const reported = held.slice(0, MAX_LOCAL_IMAGES);
14441
+ const live = new Set(reported.map((entry) => entry.imageId));
14442
+ for (const id2 of [...cache.keys()]) if (!live.has(id2)) cache.delete(id2);
14443
+ const images = [];
14444
+ let inspections = 0;
14445
+ for (const entry of reported) {
14446
+ let contentHash = cache.get(entry.imageId);
14447
+ if (contentHash === void 0 && inspections < MAX_INSPECTIONS_PER_BEAT) {
14448
+ inspections += 1;
14449
+ contentHash = await inspectContentHash(exec, entry.imageId);
14450
+ if (contentHash !== void 0) cache.set(entry.imageId, contentHash);
14451
+ }
14452
+ images.push({ ...entry, contentHash: contentHash ?? null });
14453
+ }
14454
+ return images;
14455
+ }
14456
+
14457
+ // src/lib/images.ts
14458
+ init_dist();
14459
+ init_exec();
14460
+ init_safe_arg();
14461
+ function parseSize(size) {
14462
+ const n = Number.parseInt(size ?? "0", 10);
14463
+ return Number.isFinite(n) ? n : 0;
14464
+ }
14465
+ function refsOf(image) {
14466
+ return [...image.repoTags ?? [], ...image.repoDigests ?? []];
14467
+ }
14468
+ function selectStrandedImages(inventory, wantedImages) {
14469
+ const wanted = new Set(wantedImages);
14470
+ const candidates = [];
14471
+ for (const image of inventory.images) {
14472
+ const tags = image.repoTags ?? [];
14473
+ if (tags.length > 0) continue;
14474
+ if (inventory.referencedIds.has(image.id)) continue;
14475
+ if (refsOf(image).some((ref) => wanted.has(ref))) continue;
14476
+ candidates.push({ id: image.id, sizeBytes: parseSize(image.size) });
14477
+ }
14478
+ return candidates;
14479
+ }
14480
+ async function readImageInventory(exec) {
14481
+ const imagesResult = await exec("crictl", ["images", "-o", "json"]);
14482
+ const containersResult = await exec("crictl", ["ps", "-a", "-o", "json"]);
14483
+ if (imagesResult.exitCode !== 0) {
14484
+ throw new Error(
14485
+ `could not read the image list: ${imagesResult.stderr.trim().slice(0, 120) || `crictl images exited ${imagesResult.exitCode}`}`
14486
+ );
14487
+ }
14488
+ let images = [];
14489
+ try {
14490
+ const parsed = JSON.parse(imagesResult.stdout || "{}");
14491
+ images = parsed.images ?? [];
14492
+ } catch {
14493
+ images = [];
14494
+ }
14495
+ const referencedIds = /* @__PURE__ */ new Set();
14496
+ try {
14497
+ const parsed = JSON.parse(containersResult.stdout || "{}");
14498
+ for (const container of parsed.containers ?? []) {
14499
+ if (container.imageRef) referencedIds.add(container.imageRef);
14500
+ if (container.image?.image) referencedIds.add(container.image.image);
14501
+ }
14502
+ } catch {
14503
+ throw new Error("could not read the container list; skipping prune this cycle");
14504
+ }
14505
+ return { images, referencedIds };
14506
+ }
14507
+ async function pruneStrandedImages(exec, inventory, wantedImages) {
14508
+ const candidates = selectStrandedImages(inventory, wantedImages);
14509
+ const result = { removed: 0, bytesReclaimed: 0, failures: [] };
14510
+ for (const candidate of candidates) {
14511
+ const removal = await exec("crictl", ["rmi", candidate.id]);
14512
+ if (removal.exitCode === 0) {
14513
+ result.removed += 1;
14514
+ result.bytesReclaimed += candidate.sizeBytes;
14515
+ } else {
14516
+ result.failures.push(`${candidate.id}: ${removal.stderr.trim().slice(0, 120)}`);
14517
+ }
14518
+ }
14519
+ return result;
14520
+ }
14521
+ async function warmImages(exec, wantedImages, unauthenticatedHosts) {
14522
+ const blocked = new Set(unauthenticatedHosts);
14523
+ const result = { pulled: [], skipped: [], failures: [], skippedArtifactRegistry: 0 };
14524
+ for (const ref of wantedImages) {
14525
+ const host = ref.split("/")[0] ?? "";
14526
+ if (host === "localhost" || host.startsWith("localhost:")) {
14527
+ result.skipped.push(ref);
14528
+ continue;
14529
+ }
14530
+ if (isArtifactRegistryHost(host)) {
14531
+ result.skipped.push(ref);
14532
+ result.skippedArtifactRegistry += 1;
14533
+ continue;
14534
+ }
14535
+ if (blocked.has(host)) {
14536
+ result.skipped.push(ref);
14537
+ continue;
14538
+ }
14539
+ try {
14540
+ assertImageRef(ref);
14541
+ } catch {
14542
+ result.failures.push(`${ref}: not a valid image reference`);
14543
+ continue;
14544
+ }
14545
+ const pull = await exec("crictl", ["pull", ref], { timeoutMs: 15 * 6e4 });
14546
+ if (pull.exitCode === 0) {
14547
+ result.pulled.push(ref);
14548
+ } else {
14549
+ result.failures.push(`${ref}: ${pull.stderr.trim().slice(0, 120)}`);
14550
+ }
14551
+ }
14552
+ return result;
14553
+ }
14554
+ var PIN_LABEL = "io.cri-containerd.pinned";
14555
+ async function resolveImageLabelTool(exec) {
14556
+ if (await commandExists(exec, "k3s")) {
14557
+ return { bin: "k3s", args: ["ctr", "--namespace", "k8s.io", "images", "label"] };
14558
+ }
14559
+ if (await commandExists(exec, "ctr")) {
14560
+ return { bin: "ctr", args: ["--namespace", "k8s.io", "images", "label"] };
14561
+ }
14562
+ return null;
14563
+ }
14564
+ function refsPresent(inventory) {
14565
+ const present = /* @__PURE__ */ new Set();
14566
+ for (const image of inventory.images) {
14567
+ for (const ref of refsOf(image)) present.add(ref);
14568
+ }
14569
+ return present;
14570
+ }
14571
+ async function pinWantedImages(exec, wantedImages, inventory, previouslyPinned = []) {
14572
+ const result = { pinned: [], unpinned: [], absent: [], failures: [] };
14573
+ const wanted = new Set(wantedImages);
14574
+ const present = refsPresent(inventory);
14575
+ const toPin = wantedImages.filter((ref) => present.has(ref));
14576
+ const toUnpin = previouslyPinned.filter((ref) => !wanted.has(ref) && present.has(ref));
14577
+ for (const ref of wantedImages) if (!present.has(ref)) result.absent.push(ref);
14578
+ if (toPin.length === 0 && toUnpin.length === 0) return result;
14579
+ const tool = await resolveImageLabelTool(exec);
14580
+ if (!tool) {
14581
+ result.warning = "No `k3s ctr` or `ctr` on this machine, so Conveyor cannot pin its images against kubelet garbage collection. A freshly baked image can be deleted before the first pod runs it.";
14582
+ return result;
14583
+ }
14584
+ const label2 = async (ref, value) => {
14585
+ try {
14586
+ assertImageRef(ref);
14587
+ } catch {
14588
+ result.failures.push(`${ref}: not a valid image reference`);
14589
+ return false;
14590
+ }
14591
+ const run = await exec(tool.bin, [...tool.args, ref, `${PIN_LABEL}=${value}`], {
14592
+ timeoutMs: 3e4
14593
+ });
14594
+ if (run.exitCode === 0) return true;
14595
+ result.failures.push(`${ref}: ${run.stderr.trim().slice(0, 120)}`);
14596
+ return false;
14597
+ };
14598
+ for (const ref of toPin) {
14599
+ if (await label2(ref, "pinned")) result.pinned.push(ref);
14600
+ }
14601
+ for (const ref of toUnpin) {
14602
+ if (await label2(ref, "")) result.unpinned.push(ref);
14603
+ }
14604
+ return result;
14605
+ }
14606
+ function countPinnedWanted(inventory, wantedImages) {
14033
14607
  const wanted = new Set(wantedImages);
14034
- const held = inventory.images.flatMap(
14035
- (image) => (image.repoTags ?? []).filter((ref) => ref.startsWith("localhost/") && isImageRef(ref)).map((ref) => ({ ref, imageId: image.id }))
14036
- );
14037
- held.sort(
14038
- (a, b) => Number(wanted.has(b.ref)) - Number(wanted.has(a.ref)) || (a.ref < b.ref ? -1 : a.ref > b.ref ? 1 : 0)
14039
- );
14040
- const reported = held.slice(0, MAX_LOCAL_IMAGES);
14041
- const live = new Set(reported.map((entry) => entry.imageId));
14042
- for (const id2 of [...cache.keys()]) if (!live.has(id2)) cache.delete(id2);
14043
- const images = [];
14044
- let inspections = 0;
14045
- for (const entry of reported) {
14046
- let contentHash = cache.get(entry.imageId);
14047
- if (contentHash === void 0 && inspections < MAX_INSPECTIONS_PER_BEAT) {
14048
- inspections += 1;
14049
- contentHash = await inspectContentHash(exec, entry.imageId);
14050
- if (contentHash !== void 0) cache.set(entry.imageId, contentHash);
14051
- }
14052
- images.push({ ...entry, contentHash: contentHash ?? null });
14053
- }
14054
- return images;
14608
+ return inventory.images.filter(
14609
+ (image) => image.pinned === true && refsOf(image).some((ref) => wanted.has(ref))
14610
+ ).length;
14055
14611
  }
14056
14612
 
14057
14613
  // src/daemon/telemetry.ts
@@ -14189,14 +14745,22 @@ async function collectHeartbeat(ports, state, inventory) {
14189
14745
  ...runnerUsers.length > 0 ? { runnerUsers } : {},
14190
14746
  ...localImages ? { localImages } : {},
14191
14747
  ...registry.fields,
14192
- localWarnings: [
14748
+ ...state.isolation ? { isolation: state.isolation } : {},
14749
+ localWarnings: withinHeartbeatBounds([
14193
14750
  ...state.localWarnings,
14194
14751
  ...gcWarning ? [gcWarning] : [],
14195
14752
  ...ungranted.length > 0 ? [ungrantedRunnersWarning(ungranted)] : [],
14196
14753
  ...registry.warnings
14197
- ]
14754
+ ])
14198
14755
  };
14199
14756
  }
14757
+ var HEARTBEAT_LIST_MAX = 64;
14758
+ var HEARTBEAT_STRING_MAX = 512;
14759
+ function withinHeartbeatBounds(warnings) {
14760
+ return warnings.slice(0, HEARTBEAT_LIST_MAX).map(
14761
+ (warning) => warning.length > HEARTBEAT_STRING_MAX ? `${warning.slice(0, HEARTBEAT_STRING_MAX - 1)}\u2026` : warning
14762
+ );
14763
+ }
14200
14764
  async function readPodIdentities(exec) {
14201
14765
  const result = await exec(
14202
14766
  "k3s",
@@ -14278,9 +14842,12 @@ async function runRegistryUpkeep(ports, lastGcAt, maintenanceHour) {
14278
14842
  }
14279
14843
 
14280
14844
  // src/daemon/reconcile.ts
14281
- async function reconcileKubelet(ctx, desired, result) {
14845
+ function ownerRestartWarning(change) {
14846
+ return `${change} Conveyor did not install this server's k3s, so it never restarts it: run \`sudo systemctl restart k3s\` when no build runs.`;
14847
+ }
14848
+ async function reconcileKubelet(ctx, desired, result, managed) {
14282
14849
  const ownerConfig = await ctx.fs.readFile(K3S_OWNER_CONFIG);
14283
- const rendered = renderK3sDropIn(desired.kubelet, ownerConfig);
14850
+ const rendered = renderK3sDropIn(desired.kubelet, ownerConfig, { disableComponents: managed });
14284
14851
  result.skippedKubeletFlags = rendered.skipped;
14285
14852
  result.effectiveImageGcHighThreshold = rendered.skipped.includes("image-gc-high-threshold") ? ownerImageGcHighThreshold(ownerConfig) : desired.kubelet.imageGcHighThreshold;
14286
14853
  for (const flag of rendered.skipped) {
@@ -14296,6 +14863,10 @@ async function reconcileKubelet(ctx, desired, result) {
14296
14863
  if (current === rendered.content) return;
14297
14864
  await ctx.fs.ensureDir(K3S_CONFIG_DROPIN.slice(0, K3S_CONFIG_DROPIN.lastIndexOf("/")));
14298
14865
  await ctx.fs.writeFile(K3S_CONFIG_DROPIN, rendered.content, 420);
14866
+ if (!managed) {
14867
+ result.warnings.push(ownerRestartWarning("Kubelet settings changed."));
14868
+ return;
14869
+ }
14299
14870
  if (!mayDisrupt(ctx.runningConveyorPods)) {
14300
14871
  result.warnings.push(
14301
14872
  `Kubelet settings changed but ${busyReason(ctx.runningConveyorPods)} \u2014 the restart is deferred until the machine is idle.`
@@ -14346,43 +14917,6 @@ async function reconcilePriorityClass(ctx, desired, result) {
14346
14917
  );
14347
14918
  }
14348
14919
  }
14349
- function renderLimitRange(namespace, limits) {
14350
- const ns = assertK8sName(namespace, "namespace");
14351
- const values = [
14352
- assertQuantity(limits.defaultRequest.cpu, "defaultRequest.cpu"),
14353
- assertQuantity(limits.defaultRequest.memory, "defaultRequest.memory"),
14354
- assertQuantity(limits.default.cpu, "default.cpu"),
14355
- assertQuantity(limits.default.memory, "default.memory")
14356
- ];
14357
- return [
14358
- "apiVersion: v1",
14359
- "kind: LimitRange",
14360
- "metadata:",
14361
- " name: conveyor-k3-defaults",
14362
- ` namespace: ${ns}`,
14363
- "spec:",
14364
- " limits:",
14365
- " - type: Container",
14366
- " defaultRequest:",
14367
- ` cpu: "${values[0]}"`,
14368
- ` memory: "${values[1]}"`,
14369
- " default:",
14370
- ` cpu: "${values[2]}"`,
14371
- ` memory: "${values[3]}"`,
14372
- ""
14373
- ].join("\n");
14374
- }
14375
- async function reconcileNamespaces(ctx, desired, result) {
14376
- for (const namespace of desired.namespaces) {
14377
- try {
14378
- await kubectlApply(ctx.exec, renderLimitRange(namespace.name, namespace.limitRange));
14379
- } catch (error) {
14380
- result.warnings.push(
14381
- `Could not apply resource defaults to ${namespace.name} (${namespace.projectName}): ${error instanceof Error ? error.message : String(error)}`
14382
- );
14383
- }
14384
- }
14385
- }
14386
14920
  async function step(result, label2, run) {
14387
14921
  try {
14388
14922
  await run();
@@ -14419,6 +14953,12 @@ async function reconcile(ctx, desired) {
14419
14953
  "Could not apply namespace defaults",
14420
14954
  () => reconcileNamespaces(ctx, desired, result)
14421
14955
  );
14956
+ await step(
14957
+ result,
14958
+ "Could not apply pod isolation",
14959
+ () => reconcileIsolation(ctx, desired, result)
14960
+ );
14961
+ if (desired.isolation && result.isolation === void 0) result.isolation = { applied: false };
14422
14962
  if (manifestAccepted) {
14423
14963
  await step(
14424
14964
  result,
@@ -14427,6 +14967,7 @@ async function reconcile(ctx, desired) {
14427
14967
  );
14428
14968
  }
14429
14969
  const registry = desired.localRegistry?.enabled === true;
14970
+ const managed = managesK3s(desired, await readState(ctx.fs).catch(() => ({})));
14430
14971
  if (registry) {
14431
14972
  await step(result, "Could not reconcile the local image registry", async () => {
14432
14973
  await deployLocalRegistry(ctx);
@@ -14435,10 +14976,15 @@ async function reconcile(ctx, desired) {
14435
14976
  await step(
14436
14977
  result,
14437
14978
  "Could not apply kubelet settings",
14438
- () => reconcileKubelet(ctx, desired, result)
14979
+ () => reconcileKubelet(ctx, desired, result, managed)
14439
14980
  );
14440
14981
  if (registry && !result.restartedK3s) {
14441
14982
  await step(result, "Could not load the local image registry mirror", async () => {
14983
+ if (!managed) {
14984
+ if (await registryMirrorPending(ctx.fs))
14985
+ result.warnings.push(ownerRestartWarning("The local image registry's mirror is new."));
14986
+ return;
14987
+ }
14442
14988
  const loaded = await loadRegistryMirror(ctx);
14443
14989
  result.restartedK3s = loaded.restarted;
14444
14990
  if (loaded.warning) result.warnings.push(loaded.warning);
@@ -14447,10 +14993,138 @@ async function reconcile(ctx, desired) {
14447
14993
  return result;
14448
14994
  }
14449
14995
 
14996
+ // src/commands/setup-run.ts
14997
+ init_journald();
14998
+ init_paths();
14999
+
15000
+ // src/commands/setup-session.ts
15001
+ init_exec();
15002
+ init_paths();
15003
+ async function withSetupConnection(ports, connection, enroll) {
15004
+ for (const unit of [UPDATE_TIMER, UPDATE_UNIT, DAEMON_UNIT]) {
15005
+ if (await ports.fs.readFile(`${SYSTEMD_DIR}/${unit}`))
15006
+ await runOrThrow(ports.exec, "systemctl", ["stop", unit]);
15007
+ }
15008
+ let result;
15009
+ try {
15010
+ await connection.connect();
15011
+ result = await enroll();
15012
+ } finally {
15013
+ connection.disconnect();
15014
+ }
15015
+ await runOrThrow(ports.exec, "systemctl", ["start", DAEMON_UNIT]);
15016
+ await runOrThrow(ports.exec, "systemctl", ["start", UPDATE_TIMER]);
15017
+ return result;
15018
+ }
15019
+
15020
+ // src/commands/setup-run.ts
15021
+ function requiredAuthHosts(namespaces) {
15022
+ return [...new Set(namespaces.flatMap((ns) => ns.authRequiredHosts))].sort();
15023
+ }
15024
+ async function prepareMachine(ports) {
15025
+ await ensureK3s(ports);
15026
+ await ensureJournaldCap(ports).catch((error) => {
15027
+ ports.log(
15028
+ `Could not cap the system journal; setup continues: ${error instanceof Error ? error.message : String(error)}`
15029
+ );
15030
+ });
15031
+ const cloudflaredBin = await ensureCloudflared(ports);
15032
+ await ensureCaPool(ports);
15033
+ return { cloudflaredBin };
15034
+ }
15035
+ async function enrollMachine(ports, env, cloudflaredBin, recorded = {}) {
15036
+ const { exec, fs: fs2, log: log2 } = ports;
15037
+ const connection = new ClusterConnection({ apiUrl: env.apiUrl, clusterToken: env.clusterToken });
15038
+ const bundle = await withSetupConnection(ports, connection, async () => {
15039
+ const enrollment = await connection.enroll();
15040
+ log2(`Enrolled as ${enrollment.hostname}.`);
15041
+ await installUnits(ports, enrollment, cloudflaredBin, enrollment.desiredState.maintenance.hour);
15042
+ const runningConveyorPods = await countRunningConveyorPods(exec);
15043
+ const result = await reconcile({ exec, fs: fs2, runningConveyorPods }, enrollment.desiredState);
15044
+ for (const warning of result.warnings) log2(warning);
15045
+ const saToken = await mintServiceAccountToken(ports);
15046
+ await connection.completeEnrollment({ saToken });
15047
+ await recordSetupState(
15048
+ ports,
15049
+ enrollment,
15050
+ result.appliedVersion,
15051
+ { bun: await resolveBunBin(ports), cli: ports.cliBin, cloudflared: cloudflaredBin },
15052
+ recorded
15053
+ );
15054
+ return enrollment;
15055
+ });
15056
+ const registries = parseRegistries(await fs2.readFile(REGISTRIES_FILE));
15057
+ const missing = missingAuthHosts(registries, requiredAuthHosts(bundle.desiredState.namespaces));
15058
+ log2("");
15059
+ log2(`This machine is ready. Conveyor reaches it at ${bundle.hostname}.`);
15060
+ for (const line of renderRegistryAuthSummary(missing)) log2(line);
15061
+ }
15062
+
15063
+ // src/lib/bins.ts
15064
+ var INSTALLER_PATHS = {
15065
+ bun: "/root/.bun/bin/bun",
15066
+ cli: "/root/.bun/bin/conveyor-k3",
15067
+ cloudflared: "/usr/local/bin/cloudflared"
15068
+ };
15069
+ async function whichOrNull(exec, bin) {
15070
+ const result = await exec("which", [bin]).catch(() => null);
15071
+ if (!result || result.exitCode !== 0) return null;
15072
+ const path = result.stdout.trim().split("\n")[0]?.trim();
15073
+ return path && path.startsWith("/") ? path : null;
15074
+ }
15075
+ async function resolveBinaries(exec, state, selfPath) {
15076
+ const recorded = state.bins;
15077
+ const cliSelf = selfPath?.startsWith("/") ? selfPath : null;
15078
+ return {
15079
+ bun: recorded?.bun ?? await whichOrNull(exec, "bun") ?? INSTALLER_PATHS.bun,
15080
+ // `cli` prefers the running process's own path: if this code is executing,
15081
+ // that path is by definition correct.
15082
+ cli: cliSelf ?? recorded?.cli ?? await whichOrNull(exec, "conveyor-k3") ?? INSTALLER_PATHS.cli,
15083
+ cloudflared: recorded?.cloudflared ?? await whichOrNull(exec, "cloudflared") ?? INSTALLER_PATHS.cloudflared
15084
+ };
15085
+ }
15086
+
15087
+ // src/lib/guest-vm.ts
15088
+ async function requireGuestVm(exec) {
15089
+ try {
15090
+ const result = await exec("systemd-detect-virt", ["--vm", "--quiet"]);
15091
+ if (result.exitCode === 0) return;
15092
+ } catch {
15093
+ }
15094
+ throw new Error("Refusing to run guest setup on a physical host; use `conveyor-k3 vm setup`");
15095
+ }
15096
+
15097
+ // src/lib/k3s-channel.ts
15098
+ var CHANNELS_URL = "https://update.k3s.io/v1-release/channels";
15099
+ var REQUEST_TIMEOUT_MS2 = 15e3;
15100
+ function parseChannelVersion(version) {
15101
+ const match = /v?(\d+)\.(\d+)\./.exec(version);
15102
+ if (!match) return null;
15103
+ return [Number(match[1]), Number(match[2])];
15104
+ }
15105
+ async function resolveChannelLatest(channel, fetchImpl = fetch) {
15106
+ try {
15107
+ const response = await fetchImpl(CHANNELS_URL, {
15108
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS2),
15109
+ headers: { Accept: "application/json" }
15110
+ });
15111
+ if (!response.ok) return null;
15112
+ const body = await response.json();
15113
+ const match = body.data?.find((entry) => entry.id === channel || entry.name === channel);
15114
+ return match?.latest ? parseChannelVersion(match.latest) : null;
15115
+ } catch {
15116
+ return null;
15117
+ }
15118
+ }
15119
+
14450
15120
  // src/daemon/runner.ts
15121
+ init_dist();
15122
+ init_paths();
14451
15123
  var HOUR_MS2 = 60 * 6e4;
14452
15124
  var DAY_MS = 24 * HOUR_MS2;
14453
15125
  var STORE_MEASURE_INTERVAL_MS = 55 * 6e4;
15126
+ var ISOLATION_RETRY_FIRST_MS = 6e4;
15127
+ var ISOLATION_RETRY_MAX_MS = 10 * 6e4;
14454
15128
  var DISK_PRESSURE_FREE_FRACTION = 0.2;
14455
15129
  var STRANDED_PRUNE_BYTES = 20 * 1024 ** 3;
14456
15130
  function flattenWanted(desired) {
@@ -14491,12 +15165,22 @@ var ClusterDaemon = class {
14491
15165
  localImageHashes = /* @__PURE__ */ new Map();
14492
15166
  /** How long the in-cluster registry has been not ready, across beats. */
14493
15167
  registryWatch = { notReadySince: null };
15168
+ /**
15169
+ * Whether pod isolation is in force, from the last reconcile; undefined
15170
+ * while desired state does not ask for it. Every heartbeat checks it again
15171
+ * against k3s's own settings before reporting it.
15172
+ */
15173
+ isolation = void 0;
14494
15174
  timers = [];
14495
15175
  /** When the last store walk started, so one that failed is not retried on every reconcile. */
14496
15176
  storeWalkStartedAt = 0;
14497
15177
  /** Serializes reconcile against itself — a sync nudge during a reconcile
14498
15178
  * must queue, not interleave two kubectl applies of the same manifests. */
14499
15179
  reconciling = null;
15180
+ /** The run queued behind the running one, until it starts. At most one waits. */
15181
+ queuedRun = null;
15182
+ /** The next reconcile while pod isolation is not applied; `dueAt` 0 = none. */
15183
+ isolationRetry = { delayMs: ISOLATION_RETRY_FIRST_MS, dueAt: 0 };
14500
15184
  constructor(ports) {
14501
15185
  this.ports = ports;
14502
15186
  }
@@ -14532,10 +15216,27 @@ var ClusterDaemon = class {
14532
15216
  }
14533
15217
  /** Fetch the desired state and converge to it. Queued, never concurrent. */
14534
15218
  syncAndReconcile() {
14535
- const run = (this.reconciling ?? Promise.resolve()).then(() => this.runReconcile());
15219
+ if (this.queuedRun) return this.queuedRun;
15220
+ const run = (this.reconciling ?? Promise.resolve()).then(() => {
15221
+ this.queuedRun = null;
15222
+ return this.runReconcile();
15223
+ });
15224
+ this.queuedRun = run;
14536
15225
  this.reconciling = run.catch(() => void 0);
14537
15226
  return run;
14538
15227
  }
15228
+ /** Reconcile again later while pod isolation is not applied; stop once it is. */
15229
+ scheduleIsolationRetry() {
15230
+ if (this.isolation?.applied !== false) {
15231
+ this.isolationRetry = { delayMs: ISOLATION_RETRY_FIRST_MS, dueAt: 0 };
15232
+ return;
15233
+ }
15234
+ const { delayMs } = this.isolationRetry;
15235
+ this.isolationRetry = {
15236
+ delayMs: Math.min(delayMs * 2, ISOLATION_RETRY_MAX_MS),
15237
+ dueAt: Date.now() + delayMs
15238
+ };
15239
+ }
14539
15240
  async runReconcile() {
14540
15241
  try {
14541
15242
  this.desired = await this.ports.connection.requestDesiredState();
@@ -14551,6 +15252,8 @@ var ClusterDaemon = class {
14551
15252
  this.desired
14552
15253
  );
14553
15254
  this.warnings = result.warnings;
15255
+ this.isolation = result.isolation;
15256
+ this.scheduleIsolationRetry();
14554
15257
  for (const warning of result.warnings) this.ports.log(warning);
14555
15258
  if (result.prunedNamespaces.length > 0) {
14556
15259
  this.ports.log(
@@ -14721,6 +15424,10 @@ var ClusterDaemon = class {
14721
15424
  const inventory = await readImageInventory(this.ports.exec).catch(() => null);
14722
15425
  const registryOn = this.desired?.localRegistry?.enabled === true;
14723
15426
  if (!registryOn) this.registryWatch.notReadySince = null;
15427
+ const isolation = await recheckIsolation(
15428
+ { exec: this.ports.exec, fs: this.ports.fs },
15429
+ this.isolation
15430
+ );
14724
15431
  return await collectHeartbeat(
14725
15432
  {
14726
15433
  exec: this.ports.exec,
@@ -14735,10 +15442,11 @@ var ClusterDaemon = class {
14735
15442
  lastPruneAt: this.state?.lastPruneAt ?? null,
14736
15443
  lastWarmAt: this.state?.lastWarmAt ?? null,
14737
15444
  lastUpdateAt: this.state?.lastUpdateAt ?? null,
14738
- localWarnings: [...this.warnings, ...this.warmWarnings],
15445
+ localWarnings: [...this.warnings, ...this.warmWarnings, ...isolation.warnings],
14739
15446
  imageGcHighThreshold: this.effectiveImageGcHigh,
14740
15447
  localImageHashes: this.localImageHashes,
14741
15448
  ...registryOn ? { localRegistry: this.registryWatch } : {},
15449
+ ...isolation.isolation ? { isolation: isolation.isolation } : {},
14742
15450
  k3sStoreBytes: this.measuredStoreBytes
14743
15451
  },
14744
15452
  inventory
@@ -14763,6 +15471,10 @@ var ClusterDaemon = class {
14763
15471
  return this.beating;
14764
15472
  }
14765
15473
  async runBeat() {
15474
+ if (this.isolationRetry.dueAt > 0 && Date.now() >= this.isolationRetry.dueAt) {
15475
+ this.isolationRetry.dueAt = 0;
15476
+ void this.syncAndReconcile();
15477
+ }
14766
15478
  try {
14767
15479
  const workloads = await this.ports.connection.sendHeartbeat({
14768
15480
  ...await this.buildHeartbeat(),
@@ -14776,12 +15488,64 @@ var ClusterDaemon = class {
14776
15488
  }
14777
15489
  };
14778
15490
 
14779
- // src/cli.ts
14780
- init_paths();
15491
+ // src/host/bootstrap.ts
15492
+ import { spawn as spawn2 } from "child_process";
15493
+ var RUN_BOOTSTRAP = `set -euo pipefail; umask 077; installer=$(mktemp /run/conveyor-pairing.XXXXXX); trap 'rm -f "$installer"' EXIT; cat > "$installer"; bash "$installer"`;
15494
+ var DEV_ARTIFACT_ENV = ["CONVEYOR_DEV_ARTIFACT", "CONVEYOR_DEV_ARTIFACT_SHA256"];
15495
+ function runHostBootstrap(script, command = ["bash", "-c", RUN_BOOTSTRAP]) {
15496
+ if (script.length === 0 || script.length > 1e6)
15497
+ return Promise.reject(new Error("Pairing bootstrap payload is invalid."));
15498
+ const env = { ...process.env };
15499
+ for (const name2 of DEV_ARTIFACT_ENV) delete env[name2];
15500
+ const [bin, ...args] = command;
15501
+ return new Promise((resolve, reject) => {
15502
+ const child = spawn2(bin, args, { stdio: ["pipe", "inherit", "inherit"], env });
15503
+ child.on("error", reject);
15504
+ child.on("close", (code, signal) => {
15505
+ if (code === 0) {
15506
+ resolve();
15507
+ return;
15508
+ }
15509
+ reject(
15510
+ new Error(
15511
+ `The installer stopped (${signal ?? `exit ${code}`}). Fix the error above, then run sudo conveyor-k3 host link again.`
15512
+ )
15513
+ );
15514
+ });
15515
+ child.stdin.on("error", () => void 0);
15516
+ child.stdin.end(script);
15517
+ });
15518
+ }
15519
+
15520
+ // src/host/device-key.ts
15521
+ import { createPublicKey, generateKeyPairSync } from "crypto";
15522
+ import { chmod as chmod2, mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
15523
+ import { dirname } from "path";
15524
+ init_device_link();
15525
+ async function createDeviceKey(keyPath) {
15526
+ const directory = dirname(keyPath);
15527
+ await mkdir3(directory, { recursive: true, mode: 448 });
15528
+ await chmod2(directory, 448);
15529
+ const { privateKey } = generateKeyPairSync("ed25519");
15530
+ await writeFile3(keyPath, privateKey.export({ type: "pkcs8", format: "pem" }), {
15531
+ mode: 384,
15532
+ flag: "wx"
15533
+ });
15534
+ await chmod2(keyPath, 384);
15535
+ return privateKey;
15536
+ }
15537
+ async function ensureHostDevicePublicKey(keyPath = DEVICE_KEY_PATH, ownerUid = 0) {
15538
+ const key = privateDeviceKey(keyPath, ownerUid) ?? await createDeviceKey(keyPath);
15539
+ const publicKey = createPublicKey(key).export({ type: "spki", format: "pem" }).toString();
15540
+ if (!validDevicePublicKey(publicKey))
15541
+ throw new Error("This server's device key produced an invalid public key.");
15542
+ return publicKey;
15543
+ }
14781
15544
 
14782
15545
  // src/verbs.ts
14783
15546
  var VERBS = [
14784
15547
  "vm",
15548
+ "host",
14785
15549
  "setup",
14786
15550
  "start",
14787
15551
  "stop",
@@ -14814,14 +15578,22 @@ function parseArgs(argv) {
14814
15578
  }
14815
15579
  return { verb, positional, flags };
14816
15580
  }
14817
- var HELP = `conveyor-k3 \u2014 managed personal compute for Conveyor
15581
+ var HELP = `conveyor-k3 \u2014 Personal Compute for Conveyor
14818
15582
 
14819
15583
  Usage: conveyor-k3 <command> [options]
14820
15584
 
14821
- Commands:
14822
- vm Manage isolated compute on macOS or Linux.
14823
- Run conveyor-k3 vm for host lifecycle commands.
14824
- setup Install and enrol this machine. Safe to re-run.
15585
+ One product, two runtimes. Pick the one that fits the machine:
15586
+
15587
+ On your computer or server:
15588
+ vm Personal computer runtime: an isolated VM on macOS or
15589
+ Linux. Run conveyor-k3 vm for its commands.
15590
+ host Dedicated server runtime: k3s and the daemon on this
15591
+ Linux machine, with no VM. Root only. Run
15592
+ conveyor-k3 host for its commands.
15593
+
15594
+ Inside a linked machine (the VM guest, or the server itself):
15595
+ setup VM guest only: install and enrol this machine. Safe
15596
+ to re-run. On a server, use conveyor-k3 host setup.
14825
15597
  start Start the tunnel and the daemon.
14826
15598
  stop Stop both, and tell Conveyor at once so your next
14827
15599
  build goes straight to project compute.
@@ -14846,10 +15618,12 @@ Commands:
14846
15618
  into this node and pin it. Writes one file under
14847
15619
  /etc/sudoers.d, checked with visudo first. Needed only
14848
15620
  when a project has Bake Locally turned on.
14849
- uninstall Remove Conveyor's units, environment, state, and image
14850
- registry. Your k3s config, your own registries.yaml
14851
- entries, your workloads, and the registry's data are
14852
- left alone.
15621
+ uninstall VM guest only: remove Conveyor's units, environment,
15622
+ state, and image registry. Your k3s config, your own
15623
+ registries.yaml entries, your workloads, and the
15624
+ registry's data are left alone. On a server, use
15625
+ conveyor-k3 host uninstall: it also removes the
15626
+ device key.
14853
15627
  --purge-k3s --yes also remove k3s itself and the
14854
15628
  registry's data
14855
15629
  daemon Run in the foreground (what the systemd unit calls).
@@ -14882,56 +15656,56 @@ async function requireRoot(exec) {
14882
15656
  `);
14883
15657
  process.exit(1);
14884
15658
  }
14885
- function requiredAuthHosts(namespaces) {
14886
- return [...new Set(namespaces.flatMap((ns) => ns.authRequiredHosts))].sort();
15659
+ function setupPorts(exec) {
15660
+ return { exec, fs: nodeFileSystem, cliBin: process.argv[1] ?? "conveyor-k3", log };
14887
15661
  }
14888
15662
  async function runSetup(exec) {
14889
15663
  await requireRoot(exec);
14890
15664
  await requireGuestVm(exec);
14891
15665
  const env = readCliEnv();
14892
- const ports = {
14893
- exec,
14894
- fs: nodeFileSystem,
14895
- cliBin: process.argv[1] ?? "conveyor-k3",
14896
- log
14897
- };
14898
- await ensureK3s(ports);
14899
- await ensureJournaldCap(ports).catch((error) => {
15666
+ const ports = setupPorts(exec);
15667
+ const { cloudflaredBin } = await prepareMachine(ports);
15668
+ await enrollMachine(ports, env, cloudflaredBin);
15669
+ }
15670
+ async function runHostSetup(exec) {
15671
+ const ports = setupPorts(exec);
15672
+ const { cloudflaredBin } = await prepareMachine(ports);
15673
+ let env;
15674
+ try {
15675
+ env = readCliEnv();
15676
+ } catch {
15677
+ const state = await readState(nodeFileSystem);
14900
15678
  log(
14901
- `Could not cap the system journal; setup continues: ${error instanceof Error ? error.message : String(error)}`
14902
- );
14903
- });
14904
- const cloudflaredBin = await ensureCloudflared(ports);
14905
- await ensureCaPool(ports);
14906
- const connection = new ClusterConnection({ apiUrl: env.apiUrl, clusterToken: env.clusterToken });
14907
- const bundle = await withSetupConnection(ports, connection, async () => {
14908
- const enrollment = await connection.enroll();
14909
- log(`Enrolled as ${enrollment.hostname}.`);
14910
- await installUnits(ports, enrollment, cloudflaredBin, enrollment.desiredState.maintenance.hour);
14911
- const runningConveyorPods = await countRunningConveyorPods(exec);
14912
- const result = await reconcile(
14913
- { exec, fs: nodeFileSystem, runningConveyorPods },
14914
- enrollment.desiredState
15679
+ state.enrolled ? `Prepared. This server is already linked as ${state.hostname}; its daemon keeps it current. To move it to another account, run: sudo conveyor-k3 host link --relink` : "Prepared. This server is not linked yet \u2014 run: sudo conveyor-k3 host link --name=<name>"
14915
15680
  );
14916
- for (const warning of result.warnings) log(warning);
14917
- const saToken = await mintServiceAccountToken(ports);
14918
- await connection.completeEnrollment({ saToken });
14919
- await recordSetupState(ports, enrollment, result.appliedVersion, {
14920
- bun: await resolveBunBin(ports),
14921
- cli: ports.cliBin,
14922
- cloudflared: cloudflaredBin
14923
- });
14924
- return enrollment;
14925
- });
14926
- const registries = parseRegistries(await nodeFileSystem.readFile(REGISTRIES_FILE));
14927
- const missing = missingAuthHosts(registries, requiredAuthHosts(bundle.desiredState.namespaces));
14928
- log("");
14929
- log(`This machine is ready. Conveyor reaches it at ${bundle.hostname}.`);
14930
- for (const line of renderRegistryAuthSummary(missing)) log(line);
15681
+ return;
15682
+ }
15683
+ await enrollMachine(ports, env, cloudflaredBin, { runtime: "host" });
15684
+ }
15685
+ function hostPorts(exec) {
15686
+ return {
15687
+ prepare: async () => {
15688
+ await prepareMachine(setupPorts(exec));
15689
+ },
15690
+ setup: () => runHostSetup(exec),
15691
+ status: (asJson) => runStatus(exec, asJson, "sudo conveyor-k3 host link --name=<name>"),
15692
+ uninstall: async (options) => {
15693
+ await uninstall({ exec, fs: nodeFileSystem, log }, options);
15694
+ },
15695
+ runBootstrap: (script) => runHostBootstrap(script),
15696
+ ensureDevicePublicKey: () => ensureHostDevicePublicKey(),
15697
+ removeDeviceKey: () => nodeFileSystem.removeFile(DEVICE_KEY_PATH),
15698
+ readState: () => readState(nodeFileSystem),
15699
+ writeState: (state) => writeState(nodeFileSystem, state)
15700
+ };
14931
15701
  }
14932
15702
  async function runDaemon(exec) {
14933
15703
  const env = readCliEnv();
14934
- const connection = new ClusterConnection({ apiUrl: env.apiUrl, clusterToken: env.clusterToken });
15704
+ const connection = new ClusterConnection({
15705
+ apiUrl: env.apiUrl,
15706
+ clusterToken: env.clusterToken,
15707
+ reconnectWhenReplaced: true
15708
+ });
14935
15709
  const daemon = new ClusterDaemon({
14936
15710
  exec,
14937
15711
  fs: nodeFileSystem,
@@ -14972,7 +15746,7 @@ async function runStop(exec) {
14972
15746
  }
14973
15747
  log("Stopped. Your builds go to project compute until you run `conveyor-k3 start`.");
14974
15748
  }
14975
- async function runStatus(exec, asJson) {
15749
+ async function runStatus(exec, asJson, notEnrolledHint = "conveyor-k3 setup") {
14976
15750
  const state = await readState(nodeFileSystem);
14977
15751
  const units = {};
14978
15752
  for (const unit of ALL_UNITS) {
@@ -14990,14 +15764,18 @@ async function runStatus(exec, asJson) {
14990
15764
  units,
14991
15765
  lastWarmAt: state.lastWarmAt,
14992
15766
  lastPruneAt: state.lastPruneAt,
14993
- runningConveyorPods: await countRunningConveyorPods(exec)
15767
+ runningConveyorPods: await countRunningConveyorPods(exec),
15768
+ ...state.runtime ? { runtime: state.runtime } : {}
14994
15769
  };
14995
15770
  if (asJson) {
14996
15771
  log(JSON.stringify(report, null, 2));
14997
15772
  return;
14998
15773
  }
14999
15774
  log(`conveyor-k3 ${report.cliVersion}`);
15000
- log(report.enrolled ? `Enrolled as ${report.hostname}` : "Not enrolled \u2014 run: conveyor-k3 setup");
15775
+ log(
15776
+ report.enrolled ? `Enrolled as ${report.hostname}` : `Not enrolled \u2014 run: ${notEnrolledHint}`
15777
+ );
15778
+ if (state.runtime) log(`runtime: ${state.runtime}`);
15001
15779
  log(`k3s: ${report.k3sVersion ?? "not detected"}`);
15002
15780
  for (const [unit, active] of Object.entries(units)) log(`${unit}: ${active}`);
15003
15781
  log(`Settings version applied: ${report.appliedSettingsVersion}`);
@@ -15009,9 +15787,9 @@ async function runSync(exec) {
15009
15787
  const connection = new ClusterConnection({ apiUrl: env.apiUrl, clusterToken: env.clusterToken });
15010
15788
  await connection.connect();
15011
15789
  const desired = await connection.requestDesiredState();
15790
+ connection.disconnect();
15012
15791
  const runningConveyorPods = await countRunningConveyorPods(exec);
15013
15792
  const result = await reconcile({ exec, fs: nodeFileSystem, runningConveyorPods }, desired);
15014
- connection.disconnect();
15015
15793
  log(`Applied settings version ${result.appliedVersion}.`);
15016
15794
  for (const warning of result.warnings) log(warning);
15017
15795
  }
@@ -15023,8 +15801,10 @@ async function runUpdateVerb(exec) {
15023
15801
  const desired = await connection.requestDesiredState();
15024
15802
  connection.disconnect();
15025
15803
  const installed = await exec("k3s", ["--version"]).catch(() => null);
15804
+ const state = await readState(nodeFileSystem);
15026
15805
  const decision = decideK3sUpgrade({
15027
15806
  autoUpgradeK3s: desired.maintenance.autoUpgradeK3s,
15807
+ managesK3s: managesK3s(desired, state),
15028
15808
  runningConveyorPods: await countRunningConveyorPods(exec),
15029
15809
  installedK3s: installed ? parseK3sVersion(installed.stdout || installed.stderr) : null,
15030
15810
  // The channel's REAL current version, from Rancher. Comparing against the
@@ -15032,7 +15812,7 @@ async function runUpdateVerb(exec) {
15032
15812
  // which left the profile's auto-upgrade switch controlling nothing.
15033
15813
  channelLatestK3s: await resolveChannelLatest(desired.k3s.channel)
15034
15814
  });
15035
- const bins = await resolveBinaries(exec, await readState(nodeFileSystem), process.argv[1]);
15815
+ const bins = await resolveBinaries(exec, state, process.argv[1]);
15036
15816
  const result = await runUpdate(
15037
15817
  { exec, log, bins },
15038
15818
  decision,
@@ -15118,6 +15898,10 @@ async function dispatch(verb, parsed, exec) {
15118
15898
  const { runVmCommand: runVmCommand2 } = await Promise.resolve().then(() => (init_command(), command_exports));
15119
15899
  return runVmCommand2(parsed, exec, log);
15120
15900
  }
15901
+ case "host": {
15902
+ const { runHostCommand: runHostCommand2 } = await Promise.resolve().then(() => (init_command2(), command_exports2));
15903
+ return runHostCommand2(parsed, exec, log, hostPorts(exec));
15904
+ }
15121
15905
  case "setup":
15122
15906
  return await runSetup(exec);
15123
15907
  case "daemon":