@rallycry/conveyor-k3 0.1.12 → 0.1.14

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.
Files changed (4) hide show
  1. package/dist/cli.js +2720 -1291
  2. package/dist/index.d.ts +85 -28
  3. package/dist/index.js +1533 -423
  4. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -101,7 +101,7 @@ function createClusterProof(apiUrl, clusterToken, key) {
101
101
  const origin = new URL(apiUrl).origin;
102
102
  const timestamp = Date.now();
103
103
  const nonce = randomBytes(32).toString("base64url");
104
- const message = `conveyor-device-v1
104
+ const message2 = `conveyor-device-v1
105
105
  ${origin}
106
106
  ${clusterToken}
107
107
  ${timestamp}
@@ -109,7 +109,7 @@ ${nonce}`;
109
109
  return {
110
110
  timestamp,
111
111
  nonce,
112
- signature: sign(null, Buffer.from(message, "utf8"), key).toString("base64url")
112
+ signature: sign(null, Buffer.from(message2, "utf8"), key).toString("base64url")
113
113
  };
114
114
  }
115
115
  function deviceClusterAuth(apiUrl, clusterToken, keyPath = DEVICE_KEY_PATH, ownerUid = 0) {
@@ -487,104 +487,104 @@ ZodError.create = (issues) => {
487
487
 
488
488
  // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/locales/en.js
489
489
  var errorMap = (issue, _ctx) => {
490
- let message;
490
+ let message2;
491
491
  switch (issue.code) {
492
492
  case ZodIssueCode.invalid_type:
493
493
  if (issue.received === ZodParsedType.undefined) {
494
- message = "Required";
494
+ message2 = "Required";
495
495
  } else {
496
- message = `Expected ${issue.expected}, received ${issue.received}`;
496
+ message2 = `Expected ${issue.expected}, received ${issue.received}`;
497
497
  }
498
498
  break;
499
499
  case ZodIssueCode.invalid_literal:
500
- message = `Invalid literal value, expected ${JSON.stringify(issue.expected, util.jsonStringifyReplacer)}`;
500
+ message2 = `Invalid literal value, expected ${JSON.stringify(issue.expected, util.jsonStringifyReplacer)}`;
501
501
  break;
502
502
  case ZodIssueCode.unrecognized_keys:
503
- message = `Unrecognized key(s) in object: ${util.joinValues(issue.keys, ", ")}`;
503
+ message2 = `Unrecognized key(s) in object: ${util.joinValues(issue.keys, ", ")}`;
504
504
  break;
505
505
  case ZodIssueCode.invalid_union:
506
- message = `Invalid input`;
506
+ message2 = `Invalid input`;
507
507
  break;
508
508
  case ZodIssueCode.invalid_union_discriminator:
509
- message = `Invalid discriminator value. Expected ${util.joinValues(issue.options)}`;
509
+ message2 = `Invalid discriminator value. Expected ${util.joinValues(issue.options)}`;
510
510
  break;
511
511
  case ZodIssueCode.invalid_enum_value:
512
- message = `Invalid enum value. Expected ${util.joinValues(issue.options)}, received '${issue.received}'`;
512
+ message2 = `Invalid enum value. Expected ${util.joinValues(issue.options)}, received '${issue.received}'`;
513
513
  break;
514
514
  case ZodIssueCode.invalid_arguments:
515
- message = `Invalid function arguments`;
515
+ message2 = `Invalid function arguments`;
516
516
  break;
517
517
  case ZodIssueCode.invalid_return_type:
518
- message = `Invalid function return type`;
518
+ message2 = `Invalid function return type`;
519
519
  break;
520
520
  case ZodIssueCode.invalid_date:
521
- message = `Invalid date`;
521
+ message2 = `Invalid date`;
522
522
  break;
523
523
  case ZodIssueCode.invalid_string:
524
524
  if (typeof issue.validation === "object") {
525
525
  if ("includes" in issue.validation) {
526
- message = `Invalid input: must include "${issue.validation.includes}"`;
526
+ message2 = `Invalid input: must include "${issue.validation.includes}"`;
527
527
  if (typeof issue.validation.position === "number") {
528
- message = `${message} at one or more positions greater than or equal to ${issue.validation.position}`;
528
+ message2 = `${message2} at one or more positions greater than or equal to ${issue.validation.position}`;
529
529
  }
530
530
  } else if ("startsWith" in issue.validation) {
531
- message = `Invalid input: must start with "${issue.validation.startsWith}"`;
531
+ message2 = `Invalid input: must start with "${issue.validation.startsWith}"`;
532
532
  } else if ("endsWith" in issue.validation) {
533
- message = `Invalid input: must end with "${issue.validation.endsWith}"`;
533
+ message2 = `Invalid input: must end with "${issue.validation.endsWith}"`;
534
534
  } else {
535
535
  util.assertNever(issue.validation);
536
536
  }
537
537
  } else if (issue.validation !== "regex") {
538
- message = `Invalid ${issue.validation}`;
538
+ message2 = `Invalid ${issue.validation}`;
539
539
  } else {
540
- message = "Invalid";
540
+ message2 = "Invalid";
541
541
  }
542
542
  break;
543
543
  case ZodIssueCode.too_small:
544
544
  if (issue.type === "array")
545
- message = `Array must contain ${issue.exact ? "exactly" : issue.inclusive ? `at least` : `more than`} ${issue.minimum} element(s)`;
545
+ message2 = `Array must contain ${issue.exact ? "exactly" : issue.inclusive ? `at least` : `more than`} ${issue.minimum} element(s)`;
546
546
  else if (issue.type === "string")
547
- message = `String must contain ${issue.exact ? "exactly" : issue.inclusive ? `at least` : `over`} ${issue.minimum} character(s)`;
547
+ message2 = `String must contain ${issue.exact ? "exactly" : issue.inclusive ? `at least` : `over`} ${issue.minimum} character(s)`;
548
548
  else if (issue.type === "number")
549
- message = `Number must be ${issue.exact ? `exactly equal to ` : issue.inclusive ? `greater than or equal to ` : `greater than `}${issue.minimum}`;
549
+ message2 = `Number must be ${issue.exact ? `exactly equal to ` : issue.inclusive ? `greater than or equal to ` : `greater than `}${issue.minimum}`;
550
550
  else if (issue.type === "bigint")
551
- message = `Number must be ${issue.exact ? `exactly equal to ` : issue.inclusive ? `greater than or equal to ` : `greater than `}${issue.minimum}`;
551
+ message2 = `Number must be ${issue.exact ? `exactly equal to ` : issue.inclusive ? `greater than or equal to ` : `greater than `}${issue.minimum}`;
552
552
  else if (issue.type === "date")
553
- message = `Date must be ${issue.exact ? `exactly equal to ` : issue.inclusive ? `greater than or equal to ` : `greater than `}${new Date(Number(issue.minimum))}`;
553
+ message2 = `Date must be ${issue.exact ? `exactly equal to ` : issue.inclusive ? `greater than or equal to ` : `greater than `}${new Date(Number(issue.minimum))}`;
554
554
  else
555
- message = "Invalid input";
555
+ message2 = "Invalid input";
556
556
  break;
557
557
  case ZodIssueCode.too_big:
558
558
  if (issue.type === "array")
559
- message = `Array must contain ${issue.exact ? `exactly` : issue.inclusive ? `at most` : `less than`} ${issue.maximum} element(s)`;
559
+ message2 = `Array must contain ${issue.exact ? `exactly` : issue.inclusive ? `at most` : `less than`} ${issue.maximum} element(s)`;
560
560
  else if (issue.type === "string")
561
- message = `String must contain ${issue.exact ? `exactly` : issue.inclusive ? `at most` : `under`} ${issue.maximum} character(s)`;
561
+ message2 = `String must contain ${issue.exact ? `exactly` : issue.inclusive ? `at most` : `under`} ${issue.maximum} character(s)`;
562
562
  else if (issue.type === "number")
563
- message = `Number must be ${issue.exact ? `exactly` : issue.inclusive ? `less than or equal to` : `less than`} ${issue.maximum}`;
563
+ message2 = `Number must be ${issue.exact ? `exactly` : issue.inclusive ? `less than or equal to` : `less than`} ${issue.maximum}`;
564
564
  else if (issue.type === "bigint")
565
- message = `BigInt must be ${issue.exact ? `exactly` : issue.inclusive ? `less than or equal to` : `less than`} ${issue.maximum}`;
565
+ message2 = `BigInt must be ${issue.exact ? `exactly` : issue.inclusive ? `less than or equal to` : `less than`} ${issue.maximum}`;
566
566
  else if (issue.type === "date")
567
- message = `Date must be ${issue.exact ? `exactly` : issue.inclusive ? `smaller than or equal to` : `smaller than`} ${new Date(Number(issue.maximum))}`;
567
+ message2 = `Date must be ${issue.exact ? `exactly` : issue.inclusive ? `smaller than or equal to` : `smaller than`} ${new Date(Number(issue.maximum))}`;
568
568
  else
569
- message = "Invalid input";
569
+ message2 = "Invalid input";
570
570
  break;
571
571
  case ZodIssueCode.custom:
572
- message = `Invalid input`;
572
+ message2 = `Invalid input`;
573
573
  break;
574
574
  case ZodIssueCode.invalid_intersection_types:
575
- message = `Intersection results could not be merged`;
575
+ message2 = `Intersection results could not be merged`;
576
576
  break;
577
577
  case ZodIssueCode.not_multiple_of:
578
- message = `Number must be a multiple of ${issue.multipleOf}`;
578
+ message2 = `Number must be a multiple of ${issue.multipleOf}`;
579
579
  break;
580
580
  case ZodIssueCode.not_finite:
581
- message = "Number must be finite";
581
+ message2 = "Number must be finite";
582
582
  break;
583
583
  default:
584
- message = _ctx.defaultError;
584
+ message2 = _ctx.defaultError;
585
585
  util.assertNever(issue);
586
586
  }
587
- return { message };
587
+ return { message: message2 };
588
588
  };
589
589
  var en_default = errorMap;
590
590
 
@@ -710,8 +710,8 @@ var isAsync = (x) => typeof Promise !== "undefined" && x instanceof Promise;
710
710
  // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/errorUtil.js
711
711
  var errorUtil;
712
712
  (function(errorUtil2) {
713
- errorUtil2.errToObj = (message) => typeof message === "string" ? { message } : message || {};
714
- errorUtil2.toString = (message) => typeof message === "string" ? message : message?.message;
713
+ errorUtil2.errToObj = (message2) => typeof message2 === "string" ? { message: message2 } : message2 || {};
714
+ errorUtil2.toString = (message2) => typeof message2 === "string" ? message2 : message2?.message;
715
715
  })(errorUtil || (errorUtil = {}));
716
716
 
717
717
  // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/types.js
@@ -763,16 +763,16 @@ function processCreateParams(params) {
763
763
  if (errorMap2)
764
764
  return { errorMap: errorMap2, description };
765
765
  const customMap = (iss, ctx) => {
766
- const { message } = params;
766
+ const { message: message2 } = params;
767
767
  if (iss.code === "invalid_enum_value") {
768
- return { message: message ?? ctx.defaultError };
768
+ return { message: message2 ?? ctx.defaultError };
769
769
  }
770
770
  if (typeof ctx.data === "undefined") {
771
- return { message: message ?? required_error ?? ctx.defaultError };
771
+ return { message: message2 ?? required_error ?? ctx.defaultError };
772
772
  }
773
773
  if (iss.code !== "invalid_type")
774
774
  return { message: ctx.defaultError };
775
- return { message: message ?? invalid_type_error ?? ctx.defaultError };
775
+ return { message: message2 ?? invalid_type_error ?? ctx.defaultError };
776
776
  };
777
777
  return { errorMap: customMap, description };
778
778
  }
@@ -898,14 +898,14 @@ var ZodType = class {
898
898
  const result = await (isAsync(maybeAsyncResult) ? maybeAsyncResult : Promise.resolve(maybeAsyncResult));
899
899
  return handleResult(ctx, result);
900
900
  }
901
- refine(check, message) {
901
+ refine(check, message2) {
902
902
  const getIssueProperties = (val) => {
903
- if (typeof message === "string" || typeof message === "undefined") {
904
- return { message };
905
- } else if (typeof message === "function") {
906
- return message(val);
903
+ if (typeof message2 === "string" || typeof message2 === "undefined") {
904
+ return { message: message2 };
905
+ } else if (typeof message2 === "function") {
906
+ return message2(val);
907
907
  } else {
908
- return message;
908
+ return message2;
909
909
  }
910
910
  };
911
911
  return this._refinement((val, ctx) => {
@@ -1441,11 +1441,11 @@ var ZodString = class _ZodString extends ZodType {
1441
1441
  }
1442
1442
  return { status: status.value, value: input.data };
1443
1443
  }
1444
- _regex(regex, validation, message) {
1444
+ _regex(regex, validation, message2) {
1445
1445
  return this.refinement((data) => regex.test(data), {
1446
1446
  validation,
1447
1447
  code: ZodIssueCode.invalid_string,
1448
- ...errorUtil.errToObj(message)
1448
+ ...errorUtil.errToObj(message2)
1449
1449
  });
1450
1450
  }
1451
1451
  _addCheck(check) {
@@ -1454,37 +1454,37 @@ var ZodString = class _ZodString extends ZodType {
1454
1454
  checks: [...this._def.checks, check]
1455
1455
  });
1456
1456
  }
1457
- email(message) {
1458
- return this._addCheck({ kind: "email", ...errorUtil.errToObj(message) });
1457
+ email(message2) {
1458
+ return this._addCheck({ kind: "email", ...errorUtil.errToObj(message2) });
1459
1459
  }
1460
- url(message) {
1461
- return this._addCheck({ kind: "url", ...errorUtil.errToObj(message) });
1460
+ url(message2) {
1461
+ return this._addCheck({ kind: "url", ...errorUtil.errToObj(message2) });
1462
1462
  }
1463
- emoji(message) {
1464
- return this._addCheck({ kind: "emoji", ...errorUtil.errToObj(message) });
1463
+ emoji(message2) {
1464
+ return this._addCheck({ kind: "emoji", ...errorUtil.errToObj(message2) });
1465
1465
  }
1466
- uuid(message) {
1467
- return this._addCheck({ kind: "uuid", ...errorUtil.errToObj(message) });
1466
+ uuid(message2) {
1467
+ return this._addCheck({ kind: "uuid", ...errorUtil.errToObj(message2) });
1468
1468
  }
1469
- nanoid(message) {
1470
- return this._addCheck({ kind: "nanoid", ...errorUtil.errToObj(message) });
1469
+ nanoid(message2) {
1470
+ return this._addCheck({ kind: "nanoid", ...errorUtil.errToObj(message2) });
1471
1471
  }
1472
- cuid(message) {
1473
- return this._addCheck({ kind: "cuid", ...errorUtil.errToObj(message) });
1472
+ cuid(message2) {
1473
+ return this._addCheck({ kind: "cuid", ...errorUtil.errToObj(message2) });
1474
1474
  }
1475
- cuid2(message) {
1476
- return this._addCheck({ kind: "cuid2", ...errorUtil.errToObj(message) });
1475
+ cuid2(message2) {
1476
+ return this._addCheck({ kind: "cuid2", ...errorUtil.errToObj(message2) });
1477
1477
  }
1478
- ulid(message) {
1479
- return this._addCheck({ kind: "ulid", ...errorUtil.errToObj(message) });
1478
+ ulid(message2) {
1479
+ return this._addCheck({ kind: "ulid", ...errorUtil.errToObj(message2) });
1480
1480
  }
1481
- base64(message) {
1482
- return this._addCheck({ kind: "base64", ...errorUtil.errToObj(message) });
1481
+ base64(message2) {
1482
+ return this._addCheck({ kind: "base64", ...errorUtil.errToObj(message2) });
1483
1483
  }
1484
- base64url(message) {
1484
+ base64url(message2) {
1485
1485
  return this._addCheck({
1486
1486
  kind: "base64url",
1487
- ...errorUtil.errToObj(message)
1487
+ ...errorUtil.errToObj(message2)
1488
1488
  });
1489
1489
  }
1490
1490
  jwt(options) {
@@ -1514,8 +1514,8 @@ var ZodString = class _ZodString extends ZodType {
1514
1514
  ...errorUtil.errToObj(options?.message)
1515
1515
  });
1516
1516
  }
1517
- date(message) {
1518
- return this._addCheck({ kind: "date", message });
1517
+ date(message2) {
1518
+ return this._addCheck({ kind: "date", message: message2 });
1519
1519
  }
1520
1520
  time(options) {
1521
1521
  if (typeof options === "string") {
@@ -1531,14 +1531,14 @@ var ZodString = class _ZodString extends ZodType {
1531
1531
  ...errorUtil.errToObj(options?.message)
1532
1532
  });
1533
1533
  }
1534
- duration(message) {
1535
- return this._addCheck({ kind: "duration", ...errorUtil.errToObj(message) });
1534
+ duration(message2) {
1535
+ return this._addCheck({ kind: "duration", ...errorUtil.errToObj(message2) });
1536
1536
  }
1537
- regex(regex, message) {
1537
+ regex(regex, message2) {
1538
1538
  return this._addCheck({
1539
1539
  kind: "regex",
1540
1540
  regex,
1541
- ...errorUtil.errToObj(message)
1541
+ ...errorUtil.errToObj(message2)
1542
1542
  });
1543
1543
  }
1544
1544
  includes(value, options) {
@@ -1549,46 +1549,46 @@ var ZodString = class _ZodString extends ZodType {
1549
1549
  ...errorUtil.errToObj(options?.message)
1550
1550
  });
1551
1551
  }
1552
- startsWith(value, message) {
1552
+ startsWith(value, message2) {
1553
1553
  return this._addCheck({
1554
1554
  kind: "startsWith",
1555
1555
  value,
1556
- ...errorUtil.errToObj(message)
1556
+ ...errorUtil.errToObj(message2)
1557
1557
  });
1558
1558
  }
1559
- endsWith(value, message) {
1559
+ endsWith(value, message2) {
1560
1560
  return this._addCheck({
1561
1561
  kind: "endsWith",
1562
1562
  value,
1563
- ...errorUtil.errToObj(message)
1563
+ ...errorUtil.errToObj(message2)
1564
1564
  });
1565
1565
  }
1566
- min(minLength, message) {
1566
+ min(minLength, message2) {
1567
1567
  return this._addCheck({
1568
1568
  kind: "min",
1569
1569
  value: minLength,
1570
- ...errorUtil.errToObj(message)
1570
+ ...errorUtil.errToObj(message2)
1571
1571
  });
1572
1572
  }
1573
- max(maxLength, message) {
1573
+ max(maxLength, message2) {
1574
1574
  return this._addCheck({
1575
1575
  kind: "max",
1576
1576
  value: maxLength,
1577
- ...errorUtil.errToObj(message)
1577
+ ...errorUtil.errToObj(message2)
1578
1578
  });
1579
1579
  }
1580
- length(len, message) {
1580
+ length(len, message2) {
1581
1581
  return this._addCheck({
1582
1582
  kind: "length",
1583
1583
  value: len,
1584
- ...errorUtil.errToObj(message)
1584
+ ...errorUtil.errToObj(message2)
1585
1585
  });
1586
1586
  }
1587
1587
  /**
1588
1588
  * Equivalent to `.min(1)`
1589
1589
  */
1590
- nonempty(message) {
1591
- return this.min(1, errorUtil.errToObj(message));
1590
+ nonempty(message2) {
1591
+ return this.min(1, errorUtil.errToObj(message2));
1592
1592
  }
1593
1593
  trim() {
1594
1594
  return new _ZodString({
@@ -1781,19 +1781,19 @@ var ZodNumber = class _ZodNumber extends ZodType {
1781
1781
  }
1782
1782
  return { status: status.value, value: input.data };
1783
1783
  }
1784
- gte(value, message) {
1785
- return this.setLimit("min", value, true, errorUtil.toString(message));
1784
+ gte(value, message2) {
1785
+ return this.setLimit("min", value, true, errorUtil.toString(message2));
1786
1786
  }
1787
- gt(value, message) {
1788
- return this.setLimit("min", value, false, errorUtil.toString(message));
1787
+ gt(value, message2) {
1788
+ return this.setLimit("min", value, false, errorUtil.toString(message2));
1789
1789
  }
1790
- lte(value, message) {
1791
- return this.setLimit("max", value, true, errorUtil.toString(message));
1790
+ lte(value, message2) {
1791
+ return this.setLimit("max", value, true, errorUtil.toString(message2));
1792
1792
  }
1793
- lt(value, message) {
1794
- return this.setLimit("max", value, false, errorUtil.toString(message));
1793
+ lt(value, message2) {
1794
+ return this.setLimit("max", value, false, errorUtil.toString(message2));
1795
1795
  }
1796
- setLimit(kind, value, inclusive, message) {
1796
+ setLimit(kind, value, inclusive, message2) {
1797
1797
  return new _ZodNumber({
1798
1798
  ...this._def,
1799
1799
  checks: [
@@ -1802,7 +1802,7 @@ var ZodNumber = class _ZodNumber extends ZodType {
1802
1802
  kind,
1803
1803
  value,
1804
1804
  inclusive,
1805
- message: errorUtil.toString(message)
1805
+ message: errorUtil.toString(message2)
1806
1806
  }
1807
1807
  ]
1808
1808
  });
@@ -1813,68 +1813,68 @@ var ZodNumber = class _ZodNumber extends ZodType {
1813
1813
  checks: [...this._def.checks, check]
1814
1814
  });
1815
1815
  }
1816
- int(message) {
1816
+ int(message2) {
1817
1817
  return this._addCheck({
1818
1818
  kind: "int",
1819
- message: errorUtil.toString(message)
1819
+ message: errorUtil.toString(message2)
1820
1820
  });
1821
1821
  }
1822
- positive(message) {
1822
+ positive(message2) {
1823
1823
  return this._addCheck({
1824
1824
  kind: "min",
1825
1825
  value: 0,
1826
1826
  inclusive: false,
1827
- message: errorUtil.toString(message)
1827
+ message: errorUtil.toString(message2)
1828
1828
  });
1829
1829
  }
1830
- negative(message) {
1830
+ negative(message2) {
1831
1831
  return this._addCheck({
1832
1832
  kind: "max",
1833
1833
  value: 0,
1834
1834
  inclusive: false,
1835
- message: errorUtil.toString(message)
1835
+ message: errorUtil.toString(message2)
1836
1836
  });
1837
1837
  }
1838
- nonpositive(message) {
1838
+ nonpositive(message2) {
1839
1839
  return this._addCheck({
1840
1840
  kind: "max",
1841
1841
  value: 0,
1842
1842
  inclusive: true,
1843
- message: errorUtil.toString(message)
1843
+ message: errorUtil.toString(message2)
1844
1844
  });
1845
1845
  }
1846
- nonnegative(message) {
1846
+ nonnegative(message2) {
1847
1847
  return this._addCheck({
1848
1848
  kind: "min",
1849
1849
  value: 0,
1850
1850
  inclusive: true,
1851
- message: errorUtil.toString(message)
1851
+ message: errorUtil.toString(message2)
1852
1852
  });
1853
1853
  }
1854
- multipleOf(value, message) {
1854
+ multipleOf(value, message2) {
1855
1855
  return this._addCheck({
1856
1856
  kind: "multipleOf",
1857
1857
  value,
1858
- message: errorUtil.toString(message)
1858
+ message: errorUtil.toString(message2)
1859
1859
  });
1860
1860
  }
1861
- finite(message) {
1861
+ finite(message2) {
1862
1862
  return this._addCheck({
1863
1863
  kind: "finite",
1864
- message: errorUtil.toString(message)
1864
+ message: errorUtil.toString(message2)
1865
1865
  });
1866
1866
  }
1867
- safe(message) {
1867
+ safe(message2) {
1868
1868
  return this._addCheck({
1869
1869
  kind: "min",
1870
1870
  inclusive: true,
1871
1871
  value: Number.MIN_SAFE_INTEGER,
1872
- message: errorUtil.toString(message)
1872
+ message: errorUtil.toString(message2)
1873
1873
  })._addCheck({
1874
1874
  kind: "max",
1875
1875
  inclusive: true,
1876
1876
  value: Number.MAX_SAFE_INTEGER,
1877
- message: errorUtil.toString(message)
1877
+ message: errorUtil.toString(message2)
1878
1878
  });
1879
1879
  }
1880
1880
  get minValue() {
@@ -1997,19 +1997,19 @@ var ZodBigInt = class _ZodBigInt extends ZodType {
1997
1997
  });
1998
1998
  return INVALID;
1999
1999
  }
2000
- gte(value, message) {
2001
- return this.setLimit("min", value, true, errorUtil.toString(message));
2000
+ gte(value, message2) {
2001
+ return this.setLimit("min", value, true, errorUtil.toString(message2));
2002
2002
  }
2003
- gt(value, message) {
2004
- return this.setLimit("min", value, false, errorUtil.toString(message));
2003
+ gt(value, message2) {
2004
+ return this.setLimit("min", value, false, errorUtil.toString(message2));
2005
2005
  }
2006
- lte(value, message) {
2007
- return this.setLimit("max", value, true, errorUtil.toString(message));
2006
+ lte(value, message2) {
2007
+ return this.setLimit("max", value, true, errorUtil.toString(message2));
2008
2008
  }
2009
- lt(value, message) {
2010
- return this.setLimit("max", value, false, errorUtil.toString(message));
2009
+ lt(value, message2) {
2010
+ return this.setLimit("max", value, false, errorUtil.toString(message2));
2011
2011
  }
2012
- setLimit(kind, value, inclusive, message) {
2012
+ setLimit(kind, value, inclusive, message2) {
2013
2013
  return new _ZodBigInt({
2014
2014
  ...this._def,
2015
2015
  checks: [
@@ -2018,7 +2018,7 @@ var ZodBigInt = class _ZodBigInt extends ZodType {
2018
2018
  kind,
2019
2019
  value,
2020
2020
  inclusive,
2021
- message: errorUtil.toString(message)
2021
+ message: errorUtil.toString(message2)
2022
2022
  }
2023
2023
  ]
2024
2024
  });
@@ -2029,43 +2029,43 @@ var ZodBigInt = class _ZodBigInt extends ZodType {
2029
2029
  checks: [...this._def.checks, check]
2030
2030
  });
2031
2031
  }
2032
- positive(message) {
2032
+ positive(message2) {
2033
2033
  return this._addCheck({
2034
2034
  kind: "min",
2035
2035
  value: BigInt(0),
2036
2036
  inclusive: false,
2037
- message: errorUtil.toString(message)
2037
+ message: errorUtil.toString(message2)
2038
2038
  });
2039
2039
  }
2040
- negative(message) {
2040
+ negative(message2) {
2041
2041
  return this._addCheck({
2042
2042
  kind: "max",
2043
2043
  value: BigInt(0),
2044
2044
  inclusive: false,
2045
- message: errorUtil.toString(message)
2045
+ message: errorUtil.toString(message2)
2046
2046
  });
2047
2047
  }
2048
- nonpositive(message) {
2048
+ nonpositive(message2) {
2049
2049
  return this._addCheck({
2050
2050
  kind: "max",
2051
2051
  value: BigInt(0),
2052
2052
  inclusive: true,
2053
- message: errorUtil.toString(message)
2053
+ message: errorUtil.toString(message2)
2054
2054
  });
2055
2055
  }
2056
- nonnegative(message) {
2056
+ nonnegative(message2) {
2057
2057
  return this._addCheck({
2058
2058
  kind: "min",
2059
2059
  value: BigInt(0),
2060
2060
  inclusive: true,
2061
- message: errorUtil.toString(message)
2061
+ message: errorUtil.toString(message2)
2062
2062
  });
2063
2063
  }
2064
- multipleOf(value, message) {
2064
+ multipleOf(value, message2) {
2065
2065
  return this._addCheck({
2066
2066
  kind: "multipleOf",
2067
2067
  value,
2068
- message: errorUtil.toString(message)
2068
+ message: errorUtil.toString(message2)
2069
2069
  });
2070
2070
  }
2071
2071
  get minValue() {
@@ -2188,18 +2188,18 @@ var ZodDate = class _ZodDate extends ZodType {
2188
2188
  checks: [...this._def.checks, check]
2189
2189
  });
2190
2190
  }
2191
- min(minDate, message) {
2191
+ min(minDate, message2) {
2192
2192
  return this._addCheck({
2193
2193
  kind: "min",
2194
2194
  value: minDate.getTime(),
2195
- message: errorUtil.toString(message)
2195
+ message: errorUtil.toString(message2)
2196
2196
  });
2197
2197
  }
2198
- max(maxDate, message) {
2198
+ max(maxDate, message2) {
2199
2199
  return this._addCheck({
2200
2200
  kind: "max",
2201
2201
  value: maxDate.getTime(),
2202
- message: errorUtil.toString(message)
2202
+ message: errorUtil.toString(message2)
2203
2203
  });
2204
2204
  }
2205
2205
  get minDate() {
@@ -2431,26 +2431,26 @@ var ZodArray = class _ZodArray extends ZodType {
2431
2431
  get element() {
2432
2432
  return this._def.type;
2433
2433
  }
2434
- min(minLength, message) {
2434
+ min(minLength, message2) {
2435
2435
  return new _ZodArray({
2436
2436
  ...this._def,
2437
- minLength: { value: minLength, message: errorUtil.toString(message) }
2437
+ minLength: { value: minLength, message: errorUtil.toString(message2) }
2438
2438
  });
2439
2439
  }
2440
- max(maxLength, message) {
2440
+ max(maxLength, message2) {
2441
2441
  return new _ZodArray({
2442
2442
  ...this._def,
2443
- maxLength: { value: maxLength, message: errorUtil.toString(message) }
2443
+ maxLength: { value: maxLength, message: errorUtil.toString(message2) }
2444
2444
  });
2445
2445
  }
2446
- length(len, message) {
2446
+ length(len, message2) {
2447
2447
  return new _ZodArray({
2448
2448
  ...this._def,
2449
- exactLength: { value: len, message: errorUtil.toString(message) }
2449
+ exactLength: { value: len, message: errorUtil.toString(message2) }
2450
2450
  });
2451
2451
  }
2452
- nonempty(message) {
2453
- return this.min(1, message);
2452
+ nonempty(message2) {
2453
+ return this.min(1, message2);
2454
2454
  }
2455
2455
  };
2456
2456
  ZodArray.create = (schema, params) => {
@@ -2593,17 +2593,17 @@ var ZodObject = class _ZodObject extends ZodType {
2593
2593
  get shape() {
2594
2594
  return this._def.shape();
2595
2595
  }
2596
- strict(message) {
2596
+ strict(message2) {
2597
2597
  errorUtil.errToObj;
2598
2598
  return new _ZodObject({
2599
2599
  ...this._def,
2600
2600
  unknownKeys: "strict",
2601
- ...message !== void 0 ? {
2601
+ ...message2 !== void 0 ? {
2602
2602
  errorMap: (issue, ctx) => {
2603
2603
  const defaultError = this._def.errorMap?.(issue, ctx).message ?? ctx.defaultError;
2604
2604
  if (issue.code === "unrecognized_keys")
2605
2605
  return {
2606
- message: errorUtil.errToObj(message).message ?? defaultError
2606
+ message: errorUtil.errToObj(message2).message ?? defaultError
2607
2607
  };
2608
2608
  return {
2609
2609
  message: defaultError
@@ -3359,23 +3359,23 @@ var ZodSet = class _ZodSet extends ZodType {
3359
3359
  return finalizeSet(elements);
3360
3360
  }
3361
3361
  }
3362
- min(minSize, message) {
3362
+ min(minSize, message2) {
3363
3363
  return new _ZodSet({
3364
3364
  ...this._def,
3365
- minSize: { value: minSize, message: errorUtil.toString(message) }
3365
+ minSize: { value: minSize, message: errorUtil.toString(message2) }
3366
3366
  });
3367
3367
  }
3368
- max(maxSize, message) {
3368
+ max(maxSize, message2) {
3369
3369
  return new _ZodSet({
3370
3370
  ...this._def,
3371
- maxSize: { value: maxSize, message: errorUtil.toString(message) }
3371
+ maxSize: { value: maxSize, message: errorUtil.toString(message2) }
3372
3372
  });
3373
3373
  }
3374
- size(size, message) {
3375
- return this.min(size, message).max(size, message);
3374
+ size(size, message2) {
3375
+ return this.min(size, message2).max(size, message2);
3376
3376
  }
3377
- nonempty(message) {
3378
- return this.min(1, message);
3377
+ nonempty(message2) {
3378
+ return this.min(1, message2);
3379
3379
  }
3380
3380
  };
3381
3381
  ZodSet.create = (valueType, params) => {
@@ -4162,7 +4162,7 @@ var coerce = {
4162
4162
  };
4163
4163
  var NEVER = INVALID;
4164
4164
 
4165
- // ../shared/dist/chunk-BH4SLNGU.js
4165
+ // ../shared/dist/chunk-VSUBG7GR.js
4166
4166
  var DEFAULT_SONNET_MODEL = "claude-sonnet-5-5";
4167
4167
  var DEFAULT_OPUS_MODEL = "claude-opus-5-5";
4168
4168
  var DEFAULT_HAIKU_MODEL = "claude-haiku-4-5-20251001";
@@ -4356,6 +4356,15 @@ var ALLOWED_FILE_ACCEPT = [
4356
4356
  var EMBED_THRESHOLD_IMAGES = 5 * 1024 * 1024;
4357
4357
  var EMBED_THRESHOLD_TEXT = 2 * 1024 * 1024;
4358
4358
  var IDLE_HEARTBEAT_MS = 90 * 1e3;
4359
+ var siteAssetExtension = external_exports.string().regex(/^[a-z0-9]{1,10}$/);
4360
+ var RequestSiteAssetUploadRequestSchema = external_exports.object({
4361
+ projectId: external_exports.string(),
4362
+ siteId: external_exports.string().min(1).max(100),
4363
+ sha256: external_exports.string().regex(/^[a-f0-9]{64}$/),
4364
+ ext: siteAssetExtension,
4365
+ sizeBytes: external_exports.number().int().positive()
4366
+ });
4367
+ var ListSiteAssetTargetsRequestSchema = external_exports.object({ projectId: external_exports.string() });
4359
4368
  var TurnEndToolCallSchema = external_exports.object({
4360
4369
  tool: external_exports.string(),
4361
4370
  input: external_exports.string().optional(),
@@ -4780,7 +4789,8 @@ var DiscoveredPortSchema = external_exports.object({
4780
4789
  port: external_exports.number().int().min(1).max(65535),
4781
4790
  label: external_exports.string().min(1).max(64).optional(),
4782
4791
  protocol: external_exports.enum(["http", "tcp"]).optional(),
4783
- detectedAt: external_exports.string()
4792
+ detectedAt: external_exports.string(),
4793
+ hidden: external_exports.boolean().optional()
4784
4794
  });
4785
4795
  var ReportDiscoveredPortsRequestSchema = external_exports.object({
4786
4796
  sessionId: external_exports.string(),
@@ -4927,7 +4937,11 @@ var SubmitCodeReviewResultRequestSchema = external_exports.object({
4927
4937
  var CycleCodingAgentKeyRequestSchema = external_exports.object({
4928
4938
  sessionId: external_exports.string(),
4929
4939
  rateLimitType: external_exports.string(),
4930
- resetsAt: external_exports.string().optional()
4940
+ resetsAt: external_exports.string().optional(),
4941
+ // The key the pod is actually running (CONVEYOR_CODING_AGENT_KEY_ID). Wins
4942
+ // over the session's attribution stamp after owner validation; optional so
4943
+ // older agents keep the session fallback.
4944
+ codingAgentKeyId: external_exports.string().optional()
4931
4945
  });
4932
4946
  var PostChildChatMessageRequestSchema = external_exports.object({
4933
4947
  sessionId: external_exports.string(),
@@ -5341,11 +5355,12 @@ var QueryProjectGcpLogsRequestSchema = external_exports.object({
5341
5355
  sqlInstances: external_exports.array(external_exports.string().min(1).max(200)).max(25).optional(),
5342
5356
  allServices: external_exports.boolean().optional(),
5343
5357
  search: external_exports.string().max(256).optional(),
5344
- filter: external_exports.string().max(1e3).optional(),
5358
+ filter: external_exports.string().max(2e3).optional(),
5345
5359
  startTime: external_exports.string().optional(),
5346
5360
  endTime: external_exports.string().optional(),
5347
5361
  limit: external_exports.number().int().min(1).max(200).optional().default(50),
5348
- pageToken: external_exports.string().max(4096).optional()
5362
+ pageToken: external_exports.string().max(4096).optional(),
5363
+ full: external_exports.boolean().optional()
5349
5364
  });
5350
5365
  var QueryProjectGrafanaLogsRequestSchema = external_exports.object({
5351
5366
  projectId: external_exports.string(),
@@ -6260,6 +6275,8 @@ var ANTHROPIC_CATALOG = [
6260
6275
  anthropicEntry(DEFAULT_HAIKU_MODEL, "Haiku 4.5", 1, 5, { supportsEffort: false }),
6261
6276
  anthropicEntry(FABLE_MODEL, "Fable 5.1", 10, 50)
6262
6277
  ];
6278
+ var PROJECT_ARCHIVE_RETENTION_DAYS = 30;
6279
+ var RETENTION_MS = PROJECT_ARCHIVE_RETENTION_DAYS * 24 * 60 * 60 * 1e3;
6263
6280
  var CLAUDESPACE_WORKLOAD_LABEL = "rc-workload";
6264
6281
  var CLAUDESPACE_WORKLOAD_VALUE = "claudespace";
6265
6282
  var CONVEYOR_POD_SELECTOR = `${CLAUDESPACE_WORKLOAD_LABEL}=${CLAUDESPACE_WORKLOAD_VALUE}`;
@@ -6286,10 +6303,13 @@ var PERSONAL_LOCAL_REGISTRY_NAME = "conveyor-registry";
6286
6303
  var PERSONAL_LOCAL_REGISTRY_PORT = 5e3;
6287
6304
  var PERSONAL_LOCAL_REGISTRY_HOST = `${PERSONAL_LOCAL_REGISTRY_NAME}.${PERSONAL_LOCAL_REGISTRY_NAMESPACE}.svc.cluster.local:${PERSONAL_LOCAL_REGISTRY_PORT}`;
6288
6305
  var PERSONAL_LOCAL_REGISTRY_NODE_ENDPOINT = `http://127.0.0.1:${PERSONAL_LOCAL_REGISTRY_PORT}`;
6306
+ var PERSONAL_LOCAL_REGISTRY_PUSH_SECRET = "conveyor-registry-push";
6307
+ var PERSONAL_LOCAL_REGISTRY_PUSH_SECRET_KEY = "config.json";
6289
6308
  var BAKE_COMPONENT_LABEL = "claudespace.rallycry.com/component";
6290
6309
  var BAKE_COMPONENT_PREBAKE = "prebake";
6291
6310
  var BAKE_COMPONENT_STATE_BAKE = "state-bake";
6292
6311
  var BAKE_COMPONENT_VALUES = [BAKE_COMPONENT_PREBAKE, BAKE_COMPONENT_STATE_BAKE];
6312
+ var PERSONAL_BAKE_NAMESPACE = "conveyor-bakes";
6293
6313
  var MS_PER_DAY = 24 * 60 * 60 * 1e3;
6294
6314
  var POSTGRES_ENV = {
6295
6315
  POSTGRES_HOST_AUTH_METHOD: "trust",
@@ -6608,6 +6628,8 @@ var REPORT_CARD_TYPES = surfaceTypes("report");
6608
6628
  var DECISION_CARD_TYPES = surfaceTypes("decision");
6609
6629
 
6610
6630
  // src/daemon/connection.ts
6631
+ var REDIAL_FIRST_MS = 5e3;
6632
+ var REDIAL_MAX_MS = 5 * 6e4;
6611
6633
  function unwrap(ack, what) {
6612
6634
  if (!ack?.ok || ack.data === void 0) {
6613
6635
  throw new Error(`${what} failed: ${ack?.error ?? "no response"}`);
@@ -6617,6 +6639,8 @@ function unwrap(ack, what) {
6617
6639
  var ClusterConnection = class {
6618
6640
  socket = null;
6619
6641
  config;
6642
+ redialDelayMs = REDIAL_FIRST_MS;
6643
+ redialTimer = null;
6620
6644
  /** Called when Conveyor says the desired state changed. */
6621
6645
  onSyncRequested = null;
6622
6646
  /** Called when Conveyor asks the machine to remove itself. */
@@ -6652,6 +6676,7 @@ var ClusterConnection = class {
6652
6676
  });
6653
6677
  this.socket = socket;
6654
6678
  socket.on("connect", () => {
6679
+ this.redialDelayMs = REDIAL_FIRST_MS;
6655
6680
  if (!settled) {
6656
6681
  settled = true;
6657
6682
  resolve();
@@ -6670,6 +6695,9 @@ var ClusterConnection = class {
6670
6695
  socket.on("disconnect", (reason) => {
6671
6696
  process.stderr.write(`[conveyor-k3] disconnected: ${reason}
6672
6697
  `);
6698
+ if (reason === "io server disconnect" && this.config.reconnectWhenReplaced) {
6699
+ this.redial(socket);
6700
+ }
6673
6701
  });
6674
6702
  socket.on(PERSONAL_CLUSTER_EVENTS.syncRequested, () => {
6675
6703
  this.onSyncRequested?.();
@@ -6731,7 +6759,20 @@ var ClusterConnection = class {
6731
6759
  } catch {
6732
6760
  }
6733
6761
  }
6762
+ /** Dial again after the server closed the socket, waiting longer each time. */
6763
+ redial(socket) {
6764
+ if (this.redialTimer) return;
6765
+ const delay = this.redialDelayMs;
6766
+ this.redialDelayMs = Math.min(delay * 2, REDIAL_MAX_MS);
6767
+ this.redialTimer = setTimeout(() => {
6768
+ this.redialTimer = null;
6769
+ if (this.socket === socket && !socket.connected) socket.connect();
6770
+ }, delay);
6771
+ this.redialTimer.unref?.();
6772
+ }
6734
6773
  disconnect() {
6774
+ if (this.redialTimer) clearTimeout(this.redialTimer);
6775
+ this.redialTimer = null;
6735
6776
  this.socket?.disconnect();
6736
6777
  this.socket = null;
6737
6778
  }
@@ -6929,14 +6970,22 @@ async function readImageInventory(exec) {
6929
6970
  }
6930
6971
  return { images, referencedIds };
6931
6972
  }
6973
+ var CRICTL_RMI_TIMEOUT = "120s";
6974
+ var RMI_EXEC_TIMEOUT_MS = 15e4;
6975
+ var DEADLINE_EXCEEDED = "context deadline exceeded";
6932
6976
  async function pruneStrandedImages(exec, inventory, wantedImages) {
6933
6977
  const candidates = selectStrandedImages(inventory, wantedImages);
6934
- const result = { removed: 0, bytesReclaimed: 0, failures: [] };
6978
+ const result = { removed: 0, bytesReclaimed: 0, failures: [], pending: [] };
6935
6979
  for (const candidate of candidates) {
6936
- const removal = await exec("crictl", ["rmi", candidate.id]);
6980
+ const removal = await exec("crictl", ["--timeout", CRICTL_RMI_TIMEOUT, "rmi", candidate.id], {
6981
+ timeoutMs: RMI_EXEC_TIMEOUT_MS
6982
+ });
6937
6983
  if (removal.exitCode === 0) {
6938
6984
  result.removed += 1;
6939
6985
  result.bytesReclaimed += candidate.sizeBytes;
6986
+ } else if (`${removal.stderr}
6987
+ ${removal.stdout}`.includes(DEADLINE_EXCEEDED)) {
6988
+ result.pending.push(candidate.id);
6940
6989
  } else {
6941
6990
  result.failures.push(`${candidate.id}: ${removal.stderr.trim().slice(0, 120)}`);
6942
6991
  }
@@ -7046,6 +7095,8 @@ var STATE_DIR = "/var/lib/conveyor-k3";
7046
7095
  var STATE_FILE = `${STATE_DIR}/state.json`;
7047
7096
  var LOCAL_REGISTRY_DATA_DIR = `${STATE_DIR}/registry`;
7048
7097
  var LOCAL_REGISTRY_MIRROR_RESTART_FILE = `${STATE_DIR}/registry-mirror-restart`;
7098
+ var LOCAL_REGISTRY_CREDENTIALS_FILE = `${STATE_DIR}/registry-credentials.json`;
7099
+ var LOCAL_REGISTRY_AUTH_STATE_FILE = `${STATE_DIR}/registry-auth.json`;
7049
7100
  var RANCHER_DIR = "/etc/rancher/k3s";
7050
7101
  var K3S_OWNER_CONFIG = `${RANCHER_DIR}/config.yaml`;
7051
7102
  var K3S_CONFIG_DROPIN_DIR = `${RANCHER_DIR}/config.yaml.d`;
@@ -7095,6 +7146,16 @@ function upsertRegistryAuth(file, host, auth) {
7095
7146
  next.configs = configs;
7096
7147
  return next;
7097
7148
  }
7149
+ function removeRegistryAuth(file, host) {
7150
+ const existing = file.configs?.[host];
7151
+ if (!existing?.auth) return file;
7152
+ const { auth: _auth, ...rest } = existing;
7153
+ const { [host]: _removed, ...others } = file.configs ?? {};
7154
+ const configs = Object.keys(rest).length > 0 ? { ...others, [host]: rest } : others;
7155
+ const next = { ...file, configs };
7156
+ if (Object.keys(configs).length === 0) delete next.configs;
7157
+ return next;
7158
+ }
7098
7159
  function mirrorEndpoints(file, host) {
7099
7160
  const entry = file.mirrors?.[host];
7100
7161
  if (!entry || typeof entry !== "object" || Array.isArray(entry)) return [];
@@ -7147,6 +7208,88 @@ async function writeState(fs, state) {
7147
7208
  await fs.writeFile(STATE_FILE, `${JSON.stringify(state, null, 2)}
7148
7209
  `, 384);
7149
7210
  }
7211
+ function managesK3s(desired, state) {
7212
+ return desired.isolation === void 0 || state.k3sInstalledByConveyor === true;
7213
+ }
7214
+
7215
+ // src/lib/manifest-guard.ts
7216
+ import { isAlias, isMap, isScalar, parseAllDocuments } from "yaml";
7217
+ var ALLOWED_MANIFEST_KINDS = [
7218
+ "Namespace",
7219
+ "Role",
7220
+ "RoleBinding",
7221
+ "ClusterRole",
7222
+ "ClusterRoleBinding",
7223
+ "PriorityClass"
7224
+ ];
7225
+ var ALLOWED = new Set(ALLOWED_MANIFEST_KINDS);
7226
+ var MISSING_KIND = "(missing kind)";
7227
+ var UNPARSEABLE = "(unparseable)";
7228
+ var LIST_SHAPED = "(list-shaped document)";
7229
+ function isListShaped(map) {
7230
+ return map.items.some(({ key }) => {
7231
+ if (isAlias(key)) return true;
7232
+ const name = isScalar(key) ? key.value : key;
7233
+ return typeof name === "string" && (name === "<<" || name.toLowerCase() === "items");
7234
+ });
7235
+ }
7236
+ function findDisallowedKinds(manifest) {
7237
+ let documents;
7238
+ try {
7239
+ documents = parseAllDocuments(manifest);
7240
+ } catch {
7241
+ return [UNPARSEABLE];
7242
+ }
7243
+ const disallowed = [];
7244
+ const record = (kind) => {
7245
+ if (!disallowed.includes(kind)) disallowed.push(kind);
7246
+ };
7247
+ for (const doc of documents) {
7248
+ if (doc.errors.length > 0) {
7249
+ record(UNPARSEABLE);
7250
+ continue;
7251
+ }
7252
+ if (doc.contents === null || doc.contents === void 0) continue;
7253
+ if (isScalar(doc.contents) && doc.contents.value === null) continue;
7254
+ if (!isMap(doc.contents)) {
7255
+ record(MISSING_KIND);
7256
+ continue;
7257
+ }
7258
+ if (isListShaped(doc.contents)) {
7259
+ record(LIST_SHAPED);
7260
+ continue;
7261
+ }
7262
+ const kind = doc.get("kind");
7263
+ if (typeof kind !== "string" || kind.length === 0) {
7264
+ record(MISSING_KIND);
7265
+ continue;
7266
+ }
7267
+ if (!ALLOWED.has(kind)) record(kind);
7268
+ }
7269
+ return disallowed;
7270
+ }
7271
+ function withoutKind(manifest, kind) {
7272
+ return parseAllDocuments(manifest).filter((doc) => !(isMap(doc.contents) && doc.get("kind") === kind)).map((doc) => doc.toString()).join("---\n");
7273
+ }
7274
+ function declaresPriorityClass(manifest, name, value) {
7275
+ return parseAllDocuments(manifest).some(
7276
+ (doc) => isMap(doc.contents) && doc.get("kind") === "PriorityClass" && doc.getIn(["metadata", "name"]) === name && doc.get("value") === value
7277
+ );
7278
+ }
7279
+
7280
+ // src/daemon/cluster-ops.ts
7281
+ async function kubectlApply(exec, manifest) {
7282
+ await runOrThrow(exec, "k3s", ["kubectl", "apply", "-f", "-"], {
7283
+ stdin: manifest,
7284
+ timeoutMs: 6e4
7285
+ });
7286
+ }
7287
+ function mayDisrupt(runningConveyorPods) {
7288
+ return runningConveyorPods === 0;
7289
+ }
7290
+ function busyReason(runningConveyorPods) {
7291
+ return runningConveyorPods === null ? "the number of running Conveyor pods could not be read" : `${runningConveyorPods} Conveyor pod(s) are running`;
7292
+ }
7150
7293
 
7151
7294
  // src/lib/k3s-config.ts
7152
7295
  var IMAGE_GC_HIGH = "image-gc-high-threshold";
@@ -7160,6 +7303,15 @@ var K3S_INSTALL_EXEC = [
7160
7303
  `--${WRITE_KUBECONFIG_MODE}`,
7161
7304
  KUBECONFIG_MODE
7162
7305
  ].join(" ");
7306
+ function bareToken(token) {
7307
+ let bare = token.trim();
7308
+ if (bare.endsWith("\\")) bare = bare.slice(0, -1).trim();
7309
+ const quote = bare[0];
7310
+ if (bare.length >= 2 && (quote === "'" || quote === '"') && bare.endsWith(quote)) {
7311
+ bare = bare.slice(1, -1);
7312
+ }
7313
+ return bare;
7314
+ }
7163
7315
  function ownerConfiguredKubeletFlags(ownerConfig) {
7164
7316
  const found = /* @__PURE__ */ new Set();
7165
7317
  if (!ownerConfig) return found;
@@ -7185,7 +7337,7 @@ function ownerImageGcHighThreshold(ownerConfig) {
7185
7337
  const value = Number.parseInt(match[2] ?? "", 10);
7186
7338
  return Number.isInteger(value) && value >= 0 && value <= 100 ? value : null;
7187
7339
  }
7188
- function renderK3sDropIn(kubelet, ownerConfig) {
7340
+ function renderK3sDropIn(kubelet, ownerConfig, options = {}) {
7189
7341
  const ownerFlags = ownerConfiguredKubeletFlags(ownerConfig);
7190
7342
  const ownerKeys = ownerConfiguredKeys(ownerConfig);
7191
7343
  const args = [];
@@ -7209,8 +7361,10 @@ function renderK3sDropIn(kubelet, ownerConfig) {
7209
7361
  "# Your own /etc/rancher/k3s/config.yaml is never modified; k3s merges both.",
7210
7362
  "# `kubelet-arg+` and `disable+` append rather than replacing a list you set."
7211
7363
  ];
7212
- lines2.push("disable+:");
7213
- for (const component of K3S_DISABLED_COMPONENTS) lines2.push(` - ${component}`);
7364
+ if (options.disableComponents !== false) {
7365
+ lines2.push("disable+:");
7366
+ for (const component of K3S_DISABLED_COMPONENTS) lines2.push(` - ${component}`);
7367
+ }
7214
7368
  if (ownerKeys.has(WRITE_KUBECONFIG_MODE)) {
7215
7369
  skipped.push(WRITE_KUBECONFIG_MODE);
7216
7370
  } else {
@@ -7221,76 +7375,345 @@ function renderK3sDropIn(kubelet, ownerConfig) {
7221
7375
  }
7222
7376
  return { content: [...lines2, ""].join("\n"), skipped };
7223
7377
  }
7224
- var renderKubeletDropIn = renderK3sDropIn;
7225
7378
 
7226
- // src/lib/manifest-guard.ts
7227
- import { isMap, isScalar, parseAllDocuments } from "yaml";
7228
- var ALLOWED_MANIFEST_KINDS = [
7229
- "Namespace",
7230
- "Role",
7231
- "RoleBinding",
7232
- "ClusterRole",
7233
- "ClusterRoleBinding",
7234
- "PriorityClass"
7235
- ];
7236
- var ALLOWED = new Set(ALLOWED_MANIFEST_KINDS);
7237
- var MISSING_KIND = "(missing kind)";
7238
- var UNPARSEABLE = "(unparseable)";
7239
- function findDisallowedKinds(manifest) {
7240
- let documents;
7241
- try {
7242
- documents = parseAllDocuments(manifest);
7243
- } catch {
7244
- return [UNPARSEABLE];
7379
+ // src/daemon/pod-cpu-quota.ts
7380
+ var POD_SLICE = "kubepods.slice";
7381
+ function cpuMillicores(quantity) {
7382
+ const checked = assertQuantity(quantity, "reserveCpu");
7383
+ const match = /^(\d+(?:\.\d+)?)(m?)$/.exec(checked);
7384
+ if (!match?.[1])
7385
+ throw new Error(`Invalid reserveCpu: ${JSON.stringify(checked)} is not a CPU amount`);
7386
+ const amount = Number.parseFloat(match[1]);
7387
+ return Math.round(match[2] === "m" ? amount : amount * 1e3);
7388
+ }
7389
+ function podCpuQuotaPercent(cores, reservedMillicores) {
7390
+ const left = cores * 1e3 - reservedMillicores;
7391
+ if (left < 10) return null;
7392
+ return Math.floor(left / 10);
7393
+ }
7394
+ function setQuota(exec, value) {
7395
+ return exec("systemctl", ["set-property", POD_SLICE, `CPUQuota=${value}`]);
7396
+ }
7397
+ async function liftPodCpuQuota(exec) {
7398
+ const shown = await exec("systemctl", ["show", POD_SLICE, "-p", "CPUQuotaPerSecUSec", "--value"]);
7399
+ if (shown.exitCode !== 0 || ["", "infinity"].includes(shown.stdout.trim())) return null;
7400
+ const cleared = await setQuota(exec, "");
7401
+ return cleared.exitCode === 0 ? null : cleared.stderr.trim().slice(0, 200);
7402
+ }
7403
+ async function reconcilePodCpuQuota(exec, desired, warnings) {
7404
+ const reserve = desired.kubelet.systemReserved?.cpu;
7405
+ if (!reserve) {
7406
+ const failure = await liftPodCpuQuota(exec);
7407
+ if (failure) warnings.push(`Could not lift the CPU cap on ${POD_SLICE}: ${failure}`);
7408
+ return void 0;
7245
7409
  }
7246
- const disallowed = [];
7247
- const record = (kind) => {
7248
- if (!disallowed.includes(kind)) disallowed.push(kind);
7410
+ const reserved = cpuMillicores(reserve);
7411
+ const state = await exec("systemctl", ["show", POD_SLICE, "-p", "ActiveState", "--value"]);
7412
+ if (state.exitCode !== 0 || state.stdout.trim() !== "active") {
7413
+ warnings.push(
7414
+ `Pods do not run under ${POD_SLICE} (k3s is not using the systemd cgroup driver), so the CPU you keep for this machine is weighted, not capped.`
7415
+ );
7416
+ return false;
7417
+ }
7418
+ const nproc = await exec("nproc", []);
7419
+ const cores = Number.parseInt(nproc.stdout.trim(), 10);
7420
+ if (nproc.exitCode !== 0 || !Number.isInteger(cores) || cores <= 0) {
7421
+ warnings.push(
7422
+ "Could not count this machine's CPUs, so the CPU you keep is weighted, not capped."
7423
+ );
7424
+ return false;
7425
+ }
7426
+ const percent = podCpuQuotaPercent(cores, reserved);
7427
+ if (percent === null) {
7428
+ warnings.push(
7429
+ `The CPU you keep (${reserve}) leaves no CPU for builds on this ${cores}-CPU machine. Lower it in your Conveyor profile.`
7430
+ );
7431
+ return false;
7432
+ }
7433
+ const set = await setQuota(exec, `${percent}%`);
7434
+ if (set.exitCode !== 0) {
7435
+ warnings.push(
7436
+ `Could not cap pod CPU at ${percent / 100} CPUs: ${set.stderr.trim().slice(0, 200)}`
7437
+ );
7438
+ return false;
7439
+ }
7440
+ return true;
7441
+ }
7442
+
7443
+ // src/daemon/kubelet-upkeep.ts
7444
+ function ownerRestartWarning(change) {
7445
+ return `${change} Conveyor did not install this server's k3s, so it never restarts it: run \`sudo systemctl restart k3s\` when no build runs.`;
7446
+ }
7447
+ async function reconcileKubelet(ctx, desired, result, managed) {
7448
+ const ownerConfig = await ctx.fs.readFile(K3S_OWNER_CONFIG);
7449
+ const rendered = renderK3sDropIn(desired.kubelet, ownerConfig, { disableComponents: managed });
7450
+ result.skippedKubeletFlags = rendered.skipped;
7451
+ result.effectiveImageGcHighThreshold = rendered.skipped.includes("image-gc-high-threshold") ? ownerImageGcHighThreshold(ownerConfig) : desired.kubelet.imageGcHighThreshold;
7452
+ for (const flag of rendered.skipped) {
7453
+ result.warnings.push(
7454
+ `Left \`${flag}\` to your own /etc/rancher/k3s/config.yaml \u2014 Conveyor does not override a setting you chose.`
7455
+ );
7456
+ }
7457
+ const current = await ctx.fs.readFile(K3S_CONFIG_DROPIN);
7458
+ if (rendered.content === null) {
7459
+ if (current !== null) await ctx.fs.removeFile(K3S_CONFIG_DROPIN);
7460
+ return;
7461
+ }
7462
+ if (current === rendered.content) return;
7463
+ await ctx.fs.ensureDir(K3S_CONFIG_DROPIN.slice(0, K3S_CONFIG_DROPIN.lastIndexOf("/")));
7464
+ await ctx.fs.writeFile(K3S_CONFIG_DROPIN, rendered.content, 420);
7465
+ if (!managed) {
7466
+ result.warnings.push(ownerRestartWarning("Kubelet settings changed."));
7467
+ return;
7468
+ }
7469
+ if (!mayDisrupt(ctx.runningConveyorPods)) {
7470
+ result.warnings.push(
7471
+ `Kubelet settings changed but ${busyReason(ctx.runningConveyorPods)} \u2014 the restart is deferred until the machine is idle.`
7472
+ );
7473
+ return;
7474
+ }
7475
+ const restart = await ctx.exec("systemctl", ["restart", "k3s"], { timeoutMs: 12e4 });
7476
+ if (restart.exitCode === 0) {
7477
+ result.restartedK3s = true;
7478
+ } else {
7479
+ result.warnings.push(
7480
+ `Could not restart k3s to apply new kubelet settings: ${restart.stderr.trim().slice(0, 200)}`
7481
+ );
7482
+ }
7483
+ }
7484
+
7485
+ // src/daemon/local-registry-upkeep.ts
7486
+ import { createHash as createHash3 } from "crypto";
7487
+
7488
+ // src/lib/local-registry.ts
7489
+ import { createHash as createHash2 } from "crypto";
7490
+
7491
+ // src/lib/registry-credentials.ts
7492
+ import { createHash, randomBytes as randomBytes2 } from "crypto";
7493
+
7494
+ // src/lib/bcrypt.ts
7495
+ var P_WORDS = 18;
7496
+ var S_WORDS = 1024;
7497
+ var MAGIC = "OrpheanBeholderScryDoubt";
7498
+ var ALPHABET = "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
7499
+ var MAX_KEY_BYTES = 72;
7500
+ var BCRYPT_SALT_BYTES = 16;
7501
+ function scaledAtanInverse(x, one) {
7502
+ const xSquared = x * x;
7503
+ let power = one / x;
7504
+ let sum = 0n;
7505
+ for (let n = 0n; power !== 0n; n++) {
7506
+ const term = power / (2n * n + 1n);
7507
+ sum = n % 2n === 0n ? sum + term : sum - term;
7508
+ power /= xSquared;
7509
+ }
7510
+ return sum;
7511
+ }
7512
+ function piFractionWords(count) {
7513
+ const guard = 64n;
7514
+ const bits = BigInt(count * 32) + guard;
7515
+ const one = 1n << bits;
7516
+ const pi = 16n * scaledAtanInverse(5n, one) - 4n * scaledAtanInverse(239n, one);
7517
+ const fraction = pi - 3n * one >> guard;
7518
+ const words = new Uint32Array(count);
7519
+ for (let index = 0; index < count; index++) {
7520
+ words[index] = Number(fraction >> BigInt(32 * (count - 1 - index)) & 0xffffffffn);
7521
+ }
7522
+ return words;
7523
+ }
7524
+ var initialState = null;
7525
+ function blowfishInitialState() {
7526
+ if (!initialState) {
7527
+ const words = piFractionWords(P_WORDS + S_WORDS);
7528
+ initialState = { p: words.slice(0, P_WORDS), s: words.slice(P_WORDS) };
7529
+ }
7530
+ return initialState;
7531
+ }
7532
+ function word(array, index) {
7533
+ return array[index] ?? 0;
7534
+ }
7535
+ function round(s, x) {
7536
+ const mixed = word(s, x >>> 24) + word(s, 256 + (x >>> 16 & 255)) ^ word(s, 512 + (x >>> 8 & 255));
7537
+ return mixed + word(s, 768 + (x & 255)) | 0;
7538
+ }
7539
+ function encipher(state, data, offset) {
7540
+ const { p, s } = state;
7541
+ let left = word(data, offset) ^ word(p, 0);
7542
+ let right = word(data, offset + 1);
7543
+ for (let index = 1; index <= 16; index += 2) {
7544
+ right ^= round(s, left) ^ word(p, index);
7545
+ left ^= round(s, right) ^ word(p, index + 1);
7546
+ }
7547
+ data[offset] = right ^ word(p, 17);
7548
+ data[offset + 1] = left;
7549
+ }
7550
+ function wordStream(bytes) {
7551
+ let position = 0;
7552
+ return () => {
7553
+ let value = 0;
7554
+ for (let index = 0; index < 4; index++) {
7555
+ value = value << 8 | (bytes[position] ?? 0);
7556
+ position = (position + 1) % bytes.length;
7557
+ }
7558
+ return value >>> 0;
7249
7559
  };
7250
- for (const doc of documents) {
7251
- if (doc.errors.length > 0) {
7252
- record(UNPARSEABLE);
7253
- continue;
7560
+ }
7561
+ function expandKey(state, key, salt) {
7562
+ const nextKeyWord = wordStream(key);
7563
+ for (let index = 0; index < P_WORDS; index++) {
7564
+ state.p[index] = word(state.p, index) ^ nextKeyWord();
7565
+ }
7566
+ const nextSaltWord = salt ? wordStream(salt) : () => 0;
7567
+ const block = new Uint32Array(2);
7568
+ const fill = (target, length) => {
7569
+ for (let index = 0; index < length; index += 2) {
7570
+ block[0] = word(block, 0) ^ nextSaltWord();
7571
+ block[1] = word(block, 1) ^ nextSaltWord();
7572
+ encipher(state, block, 0);
7573
+ target[index] = word(block, 0);
7574
+ target[index + 1] = word(block, 1);
7254
7575
  }
7255
- if (doc.contents === null || doc.contents === void 0) continue;
7256
- if (isScalar(doc.contents) && doc.contents.value === null) continue;
7257
- if (!isMap(doc.contents)) {
7258
- record(MISSING_KIND);
7259
- continue;
7576
+ };
7577
+ fill(state.p, P_WORDS);
7578
+ fill(state.s, S_WORDS);
7579
+ }
7580
+ function encodeBase64(bytes, length) {
7581
+ let out = "";
7582
+ let offset = 0;
7583
+ while (offset < length) {
7584
+ let first = bytes[offset++] ?? 0;
7585
+ out += ALPHABET[first >> 2];
7586
+ first = (first & 3) << 4;
7587
+ if (offset >= length) {
7588
+ out += ALPHABET[first];
7589
+ break;
7260
7590
  }
7261
- const kind = doc.get("kind");
7262
- if (typeof kind !== "string" || kind.length === 0) {
7263
- record(MISSING_KIND);
7264
- continue;
7591
+ let second = bytes[offset++] ?? 0;
7592
+ out += ALPHABET[first | second >> 4];
7593
+ first = (second & 15) << 2;
7594
+ if (offset >= length) {
7595
+ out += ALPHABET[first];
7596
+ break;
7265
7597
  }
7266
- if (!ALLOWED.has(kind)) record(kind);
7267
- }
7268
- return disallowed;
7598
+ second = bytes[offset++] ?? 0;
7599
+ out += ALPHABET[first | second >> 6];
7600
+ out += ALPHABET[second & 63];
7601
+ }
7602
+ return out;
7603
+ }
7604
+ function bcryptHash(password, cost, salt) {
7605
+ if (!Number.isInteger(cost) || cost < 4 || cost > 31) {
7606
+ throw new Error(`bcrypt cost must be an integer from 4 to 31, not ${cost}`);
7607
+ }
7608
+ if (salt.length !== BCRYPT_SALT_BYTES) {
7609
+ throw new Error(`bcrypt needs a ${BCRYPT_SALT_BYTES}-byte salt`);
7610
+ }
7611
+ const passwordBytes = new TextEncoder().encode(password).slice(0, MAX_KEY_BYTES);
7612
+ const key = new Uint8Array(passwordBytes.length + 1);
7613
+ key.set(passwordBytes);
7614
+ const base = blowfishInitialState();
7615
+ const state = { p: base.p.slice(), s: base.s.slice() };
7616
+ expandKey(state, key, salt);
7617
+ for (let rounds = 2 ** cost; rounds > 0; rounds--) {
7618
+ expandKey(state, key, null);
7619
+ expandKey(state, salt, null);
7620
+ }
7621
+ const magic = new TextEncoder().encode(MAGIC);
7622
+ const nextMagicWord = wordStream(magic);
7623
+ const block = Uint32Array.from({ length: 6 }, () => nextMagicWord());
7624
+ for (let pass = 0; pass < 64; pass++) {
7625
+ for (let offset = 0; offset < 6; offset += 2) encipher(state, block, offset);
7626
+ }
7627
+ const digest = new Uint8Array(24);
7628
+ block.forEach((value, index) => {
7629
+ digest[index * 4] = value >>> 24;
7630
+ digest[index * 4 + 1] = value >>> 16 & 255;
7631
+ digest[index * 4 + 2] = value >>> 8 & 255;
7632
+ digest[index * 4 + 3] = value & 255;
7633
+ });
7634
+ const costText = String(cost).padStart(2, "0");
7635
+ return `$2y$${costText}$${encodeBase64(salt, BCRYPT_SALT_BYTES)}${encodeBase64(digest, 23)}`;
7269
7636
  }
7270
7637
 
7271
- // src/daemon/cluster-ops.ts
7272
- async function kubectlApply(exec, manifest) {
7273
- await runOrThrow(exec, "k3s", ["kubectl", "apply", "-f", "-"], {
7274
- stdin: manifest,
7275
- timeoutMs: 6e4
7276
- });
7638
+ // src/lib/registry-credentials.ts
7639
+ var LOCAL_REGISTRY_USERNAME = "conveyor";
7640
+ var BCRYPT_COST = 5;
7641
+ var USERNAME = /^[a-z][a-z0-9-]{0,31}$/;
7642
+ var PASSWORD = /^[A-Za-z0-9_-]{32,128}$/;
7643
+ function generateRegistryCredentials(random = randomBytes2) {
7644
+ const username = LOCAL_REGISTRY_USERNAME;
7645
+ const password = Buffer.from(random(32)).toString("base64url");
7646
+ const hash = bcryptHash(password, BCRYPT_COST, random(BCRYPT_SALT_BYTES));
7647
+ return { username, password, htpasswd: `${username}:${hash}` };
7648
+ }
7649
+ function parseRegistryCredentials(raw) {
7650
+ if (!raw) return null;
7651
+ try {
7652
+ const value = JSON.parse(raw);
7653
+ const { username, password, htpasswd } = value ?? {};
7654
+ if (typeof username !== "string" || !USERNAME.test(username)) return null;
7655
+ if (typeof password !== "string" || !PASSWORD.test(password)) return null;
7656
+ if (typeof htpasswd !== "string" || !htpasswd.startsWith(`${username}:$2`)) return null;
7657
+ if (htpasswd.includes("\n")) return null;
7658
+ return { username, password, htpasswd };
7659
+ } catch {
7660
+ return null;
7661
+ }
7277
7662
  }
7278
- function mayDisrupt(runningConveyorPods) {
7279
- return runningConveyorPods === 0;
7663
+ async function loadOrCreateRegistryCredentials(fs, random) {
7664
+ const stored = parseRegistryCredentials(await fs.readFile(LOCAL_REGISTRY_CREDENTIALS_FILE));
7665
+ if (stored) return stored;
7666
+ const created = generateRegistryCredentials(random);
7667
+ await fs.writeFile(LOCAL_REGISTRY_CREDENTIALS_FILE, `${JSON.stringify(created)}
7668
+ `, 384);
7669
+ return created;
7280
7670
  }
7281
- function busyReason(runningConveyorPods) {
7282
- return runningConveyorPods === null ? "the number of running Conveyor pods could not be read" : `${runningConveyorPods} Conveyor pod(s) are running`;
7671
+ function credentialFingerprint(credentials) {
7672
+ return createHash("sha256").update(`${credentials.username}:${credentials.password}`).digest("hex").slice(0, 16);
7673
+ }
7674
+ function basicAuthorization(credentials) {
7675
+ return `Basic ${Buffer.from(`${credentials.username}:${credentials.password}`).toString("base64")}`;
7676
+ }
7677
+ function registryDockerConfig(credentials, host) {
7678
+ const auth = Buffer.from(`${credentials.username}:${credentials.password}`).toString("base64");
7679
+ return JSON.stringify({ auths: { [host]: { auth } } });
7680
+ }
7681
+ function parseRegistryAuthState(raw) {
7682
+ if (!raw) return null;
7683
+ try {
7684
+ const value = JSON.parse(raw);
7685
+ if (typeof value?.credential !== "string") return null;
7686
+ const writtenAt = typeof value.writtenAt === "string" && !Number.isNaN(Date.parse(value.writtenAt)) ? value.writtenAt : null;
7687
+ return { credential: value.credential, writtenAt, enforced: value.enforced === true };
7688
+ } catch {
7689
+ return null;
7690
+ }
7691
+ }
7692
+ async function registryAuthEnforced(readFile3) {
7693
+ return parseRegistryAuthState(await readFile3(LOCAL_REGISTRY_AUTH_STATE_FILE))?.enforced === true;
7694
+ }
7695
+ function withRegistryCredentials(fetchImpl, readFile3) {
7696
+ let credentials = null;
7697
+ return async (url, init) => {
7698
+ credentials ??= readFile3(LOCAL_REGISTRY_CREDENTIALS_FILE).then(parseRegistryCredentials);
7699
+ const stored = await credentials;
7700
+ if (!stored) return fetchImpl(url, init);
7701
+ const headers = new Headers(init?.headers);
7702
+ headers.set("Authorization", basicAuthorization(stored));
7703
+ return fetchImpl(url, { ...init, headers });
7704
+ };
7283
7705
  }
7284
-
7285
- // src/daemon/local-registry-upkeep.ts
7286
- import { createHash } from "crypto";
7287
7706
 
7288
7707
  // src/lib/local-registry.ts
7289
7708
  var LOCAL_REGISTRY_IMAGE = "docker.io/library/registry:3.1.2@sha256:c87f33837722a100572e95d7dc4bf539fc42cf68202b13c3bc03c0ff54c3a649";
7290
7709
  var LOCAL_REGISTRY_CONFIG = "/etc/distribution/config.yml";
7291
7710
  var LOCAL_REGISTRY_GC_JOB = "conveyor-registry-gc";
7711
+ var LOCAL_REGISTRY_HTPASSWD_SECRET = "conveyor-registry-htpasswd";
7712
+ var HTPASSWD_KEY = "htpasswd";
7713
+ var AUTH_MOUNT = "/etc/distribution/auth";
7292
7714
  var LOCAL_REGISTRY_SELECTOR = { "app.kubernetes.io/name": PERSONAL_LOCAL_REGISTRY_NAME };
7293
7715
  var MANAGED_BY = { "app.kubernetes.io/managed-by": "conveyor-k3" };
7716
+ var AUTH_CHECKSUM_ANNOTATION = "conveyor.rallycry.com/registry-auth";
7294
7717
  var DATA_MOUNT = "/var/lib/registry";
7295
7718
  var REGISTRY_ENV = [
7296
7719
  { name: "REGISTRY_STORAGE_DELETE_ENABLED", value: "true" },
@@ -7305,6 +7728,23 @@ var VOLUME_MOUNTS = [
7305
7728
  { name: "data", mountPath: DATA_MOUNT },
7306
7729
  { name: "tmp", mountPath: "/tmp" }
7307
7730
  ];
7731
+ var AUTH_ENV = [
7732
+ { name: "REGISTRY_AUTH", value: "htpasswd" },
7733
+ { name: "REGISTRY_AUTH_HTPASSWD_REALM", value: "conveyor" },
7734
+ { name: "REGISTRY_AUTH_HTPASSWD_PATH", value: `${AUTH_MOUNT}/${HTPASSWD_KEY}` }
7735
+ ];
7736
+ var AUTH_VOLUME = {
7737
+ name: "auth",
7738
+ secret: {
7739
+ secretName: LOCAL_REGISTRY_HTPASSWD_SECRET,
7740
+ items: [{ key: HTPASSWD_KEY, path: HTPASSWD_KEY }],
7741
+ defaultMode: 256
7742
+ }
7743
+ };
7744
+ var AUTH_VOLUME_MOUNT = { name: "auth", mountPath: AUTH_MOUNT, readOnly: true };
7745
+ function htpasswdChecksum(htpasswd) {
7746
+ return createHash2("sha256").update(htpasswd).digest("hex").slice(0, 16);
7747
+ }
7308
7748
  var VOLUMES = [
7309
7749
  { name: "data", hostPath: { path: LOCAL_REGISTRY_DATA_DIR, type: "Directory" } },
7310
7750
  { name: "tmp", emptyDir: { sizeLimit: "64Mi" } }
@@ -7341,7 +7781,34 @@ function registryNetworkPolicy(metadata) {
7341
7781
  }
7342
7782
  };
7343
7783
  }
7344
- function renderLocalRegistryManifests() {
7784
+ function renderLocalRegistrySecrets(credentials) {
7785
+ const encode = (value) => Buffer.from(value).toString("base64");
7786
+ const secret = (namespace, name, key, value) => ({
7787
+ apiVersion: "v1",
7788
+ kind: "Secret",
7789
+ metadata: { name, namespace, labels: { ...MANAGED_BY } },
7790
+ type: "Opaque",
7791
+ data: { [key]: encode(value) }
7792
+ });
7793
+ const items = [
7794
+ secret(
7795
+ PERSONAL_LOCAL_REGISTRY_NAMESPACE,
7796
+ LOCAL_REGISTRY_HTPASSWD_SECRET,
7797
+ HTPASSWD_KEY,
7798
+ `${credentials.htpasswd}
7799
+ `
7800
+ ),
7801
+ secret(
7802
+ PERSONAL_BAKE_NAMESPACE,
7803
+ PERSONAL_LOCAL_REGISTRY_PUSH_SECRET,
7804
+ PERSONAL_LOCAL_REGISTRY_PUSH_SECRET_KEY,
7805
+ registryDockerConfig(credentials, PERSONAL_LOCAL_REGISTRY_HOST)
7806
+ )
7807
+ ];
7808
+ return `${JSON.stringify({ apiVersion: "v1", kind: "List", items }, null, 2)}
7809
+ `;
7810
+ }
7811
+ function renderLocalRegistryManifests(auth = null) {
7345
7812
  const metadata = {
7346
7813
  name: PERSONAL_LOCAL_REGISTRY_NAME,
7347
7814
  namespace: PERSONAL_LOCAL_REGISTRY_NAMESPACE,
@@ -7357,15 +7824,19 @@ function renderLocalRegistryManifests() {
7357
7824
  strategy: { type: "Recreate" },
7358
7825
  selector: { matchLabels: LOCAL_REGISTRY_SELECTOR },
7359
7826
  template: {
7360
- metadata: { labels: metadata.labels },
7827
+ metadata: {
7828
+ labels: metadata.labels,
7829
+ ...auth ? { annotations: { [AUTH_CHECKSUM_ANNOTATION]: htpasswdChecksum(auth.htpasswd) } } : {}
7830
+ },
7361
7831
  spec: {
7362
7832
  ...POD_BASE,
7833
+ ...auth ? { volumes: [...VOLUMES, AUTH_VOLUME] } : {},
7363
7834
  containers: [
7364
7835
  {
7365
7836
  name: "registry",
7366
7837
  image: LOCAL_REGISTRY_IMAGE,
7367
7838
  imagePullPolicy: "IfNotPresent",
7368
- env: REGISTRY_ENV,
7839
+ env: auth ? [...REGISTRY_ENV, ...AUTH_ENV] : REGISTRY_ENV,
7369
7840
  ports: [
7370
7841
  {
7371
7842
  name: "registry",
@@ -7377,8 +7848,10 @@ function renderLocalRegistryManifests() {
7377
7848
  protocol: "TCP"
7378
7849
  }
7379
7850
  ],
7851
+ // A TCP check: once the credential is enforced, `/v2/` answers
7852
+ // 401 to a probe that does not carry it.
7380
7853
  readinessProbe: {
7381
- httpGet: { path: "/v2/", port: PERSONAL_LOCAL_REGISTRY_PORT },
7854
+ tcpSocket: { port: PERSONAL_LOCAL_REGISTRY_PORT },
7382
7855
  periodSeconds: 10
7383
7856
  },
7384
7857
  resources: {
@@ -7386,7 +7859,7 @@ function renderLocalRegistryManifests() {
7386
7859
  limits: { memory: "512Mi" }
7387
7860
  },
7388
7861
  securityContext: CONTAINER_SECURITY,
7389
- volumeMounts: VOLUME_MOUNTS
7862
+ volumeMounts: auth ? [...VOLUME_MOUNTS, AUTH_VOLUME_MOUNT] : VOLUME_MOUNTS
7390
7863
  }
7391
7864
  ]
7392
7865
  }
@@ -7477,11 +7950,18 @@ async function registryAnswers(fetchImpl = fetch) {
7477
7950
  }
7478
7951
  async function localRegistryReady(ports) {
7479
7952
  const [answers, mirrored] = await Promise.all([
7480
- registryAnswers(ports.fetch),
7953
+ registryAnswers(withRegistryCredentials(ports.fetch ?? fetch, ports.readFile)),
7481
7954
  mirrorLoaded(ports.readFile)
7482
7955
  ]);
7483
7956
  return answers && mirrored;
7484
7957
  }
7958
+ async function localRegistryStatus(ports) {
7959
+ const [ready, authEnforced] = await Promise.all([
7960
+ localRegistryReady(ports),
7961
+ registryAuthEnforced(ports.readFile)
7962
+ ]);
7963
+ return { ready, authEnforced };
7964
+ }
7485
7965
 
7486
7966
  // src/daemon/registry-gc-lock.ts
7487
7967
  var REGISTRY_GC_MARK = "conveyor.rallycry.com/registry-gc";
@@ -7572,6 +8052,59 @@ async function deleteSweepJob(exec) {
7572
8052
  return result.exitCode === 0;
7573
8053
  }
7574
8054
 
8055
+ // src/daemon/registry-auth.ts
8056
+ var LOCAL_REGISTRY_AUTH_HOSTS = [
8057
+ PERSONAL_LOCAL_REGISTRY_HOST,
8058
+ new URL(PERSONAL_LOCAL_REGISTRY_NODE_ENDPOINT).host
8059
+ ];
8060
+ function registryAuthConfigured(file, credentials) {
8061
+ return LOCAL_REGISTRY_AUTH_HOSTS.every((host) => {
8062
+ const auth = file.configs?.[host]?.auth;
8063
+ return auth?.username === credentials.username && auth.password === credentials.password && auth.auth === void 0 && auth.identitytoken === void 0;
8064
+ });
8065
+ }
8066
+ function withLocalRegistryAuth(file, credentials) {
8067
+ return LOCAL_REGISTRY_AUTH_HOSTS.reduce(
8068
+ (next, host) => upsertRegistryAuth(next, host, {
8069
+ username: credentials.username,
8070
+ password: credentials.password
8071
+ }),
8072
+ file
8073
+ );
8074
+ }
8075
+ function monotonicNowUs() {
8076
+ return Number(process.hrtime.bigint() / 1000n);
8077
+ }
8078
+ async function k3sStartedAtMs(exec, nowMs, monotonicNow = monotonicNowUs) {
8079
+ const result = await exec(
8080
+ "systemctl",
8081
+ ["show", "k3s", "--property=ActiveEnterTimestampMonotonic", "--value"],
8082
+ { timeoutMs: 1e4 }
8083
+ ).catch(() => null);
8084
+ if (!result || result.exitCode !== 0) return null;
8085
+ const startedUs = Number.parseInt(result.stdout.trim(), 10);
8086
+ if (!Number.isFinite(startedUs) || startedUs <= 0) return null;
8087
+ return nowMs - (monotonicNow() - startedUs) / 1e3;
8088
+ }
8089
+ async function settleRegistryAuth(ctx, credentials, written) {
8090
+ const now = (ctx.now ?? (() => /* @__PURE__ */ new Date()))();
8091
+ const raw = await ctx.fs.readFile(LOCAL_REGISTRY_AUTH_STATE_FILE);
8092
+ const credential = credentialFingerprint(credentials);
8093
+ const stored = parseRegistryAuthState(raw);
8094
+ const state = stored?.credential === credential ? stored : { credential, writtenAt: null, enforced: false };
8095
+ if (!state.enforced && (written || state.writtenAt === null)) {
8096
+ state.writtenAt = now.toISOString();
8097
+ }
8098
+ if (!state.enforced && state.writtenAt !== null) {
8099
+ const startedAt = await k3sStartedAtMs(ctx.exec, now.getTime(), ctx.monotonicNowUs);
8100
+ if (startedAt !== null && startedAt > Date.parse(state.writtenAt)) state.enforced = true;
8101
+ }
8102
+ const next = `${JSON.stringify(state)}
8103
+ `;
8104
+ if (next !== raw) await ctx.fs.writeFile(LOCAL_REGISTRY_AUTH_STATE_FILE, next, 384);
8105
+ return state.enforced;
8106
+ }
8107
+
7575
8108
  // src/daemon/local-registry-upkeep.ts
7576
8109
  function nowDate() {
7577
8110
  return /* @__PURE__ */ new Date();
@@ -7580,33 +8113,47 @@ function mirrorConfigured(raw) {
7580
8113
  const endpoints = mirrorEndpoints(parseRegistries(raw), PERSONAL_LOCAL_REGISTRY_HOST);
7581
8114
  return endpoints.length === 1 && endpoints[0] === PERSONAL_LOCAL_REGISTRY_NODE_ENDPOINT;
7582
8115
  }
7583
- async function applyUnlessCollecting(ctx) {
8116
+ async function applyUnlessCollecting(ctx, auth) {
7584
8117
  const deployment = await readRegistryDeployment(ctx.exec);
7585
8118
  if (!deployment) throw new Error("the registry's Deployment could not be read");
7586
8119
  if (gcMarkIsLive(deployment.mark, (ctx.now ?? nowDate)().getTime(), ctx.isAlive)) return false;
7587
8120
  if (deployment.mark !== null && !await deleteSweepJob(ctx.exec)) {
7588
8121
  throw new Error("a stopped cleanup's Job could not be deleted, so the registry stays stopped");
7589
8122
  }
7590
- await kubectlApply(ctx.exec, renderLocalRegistryManifests());
8123
+ await kubectlApply(ctx.exec, renderLocalRegistryManifests(auth));
7591
8124
  if (deployment.mark !== null) await clearGcMark(ctx.exec);
7592
8125
  return true;
7593
8126
  }
7594
8127
  async function deployLocalRegistry(ctx) {
7595
8128
  await ctx.fs.ensureDir(LOCAL_REGISTRY_DATA_DIR, 448);
7596
- const applied = await applyUnlessCollecting(ctx);
8129
+ const credentials = await loadOrCreateRegistryCredentials(ctx.fs, ctx.random);
7597
8130
  const raw = await ctx.fs.readFile(REGISTRIES_FILE);
7598
- if (mirrorConfigured(raw)) return { applied, mirrorWritten: false, backupPath: null };
7599
- const next = upsertRegistryMirror(parseRegistries(raw), PERSONAL_LOCAL_REGISTRY_HOST, [
7600
- PERSONAL_LOCAL_REGISTRY_NODE_ENDPOINT
7601
- ]);
7602
- const backupPath = await writeRegistriesWithBackup(ctx.fs, raw, next, (ctx.now ?? nowDate)());
7603
- return { applied, mirrorWritten: true, backupPath };
8131
+ let file = parseRegistries(raw);
8132
+ const mirrorWritten = !mirrorConfigured(raw);
8133
+ const authWritten = !registryAuthConfigured(file, credentials);
8134
+ let backupPath = null;
8135
+ if (mirrorWritten) {
8136
+ file = upsertRegistryMirror(file, PERSONAL_LOCAL_REGISTRY_HOST, [
8137
+ PERSONAL_LOCAL_REGISTRY_NODE_ENDPOINT
8138
+ ]);
8139
+ }
8140
+ if (authWritten) file = withLocalRegistryAuth(file, credentials);
8141
+ if (mirrorWritten || authWritten) {
8142
+ backupPath = await writeRegistriesWithBackup(ctx.fs, raw, file, (ctx.now ?? nowDate)());
8143
+ }
8144
+ const authEnforced = await settleRegistryAuth(ctx, credentials, authWritten);
8145
+ await kubectlApply(ctx.exec, renderLocalRegistrySecrets(credentials));
8146
+ const applied = await applyUnlessCollecting(ctx, authEnforced ? credentials : null);
8147
+ return { applied, mirrorWritten, authWritten, backupPath, authEnforced };
7604
8148
  }
7605
8149
  async function readMirrorState(fs) {
7606
8150
  const raw = await fs.readFile(REGISTRIES_FILE);
7607
- const fingerprint = createHash("sha256").update(raw ?? "").digest("hex");
8151
+ const authPending = !await registryAuthEnforced((path) => fs.readFile(path));
8152
+ const fingerprint = createHash3("sha256").update(raw ?? "").digest("hex");
7608
8153
  if (!mirrorConfigured(raw)) return { state: "absent", fingerprint };
7609
- if (await mirrorLoaded((path) => fs.readFile(path))) return { state: "loaded", fingerprint };
8154
+ if (!authPending && await mirrorLoaded((path) => fs.readFile(path))) {
8155
+ return { state: "loaded", fingerprint };
8156
+ }
7610
8157
  const tried = (await fs.readFile(LOCAL_REGISTRY_MIRROR_RESTART_FILE))?.trim() === fingerprint;
7611
8158
  return { state: tried ? "tried" : "owed", fingerprint };
7612
8159
  }
@@ -7618,14 +8165,14 @@ async function loadRegistryMirror(ctx) {
7618
8165
  if (state === "tried") {
7619
8166
  return {
7620
8167
  restarted: false,
7621
- warning: `k3s restarted but did not load the local image registry's mirror, so pods cannot pull from that registry. Conveyor restarts k3s for it again only after /etc/rancher/k3s/registries.yaml changes. Check that k3s reads that file and keeps its data in the default directory: the mirror should appear under ${K3S_CERTS_D}.`
8168
+ warning: `k3s restarted but did not load the local image registry's mirror or credential, so pods cannot pull from that registry or the registry stays open to every account on this machine. Conveyor restarts k3s for it again only after /etc/rancher/k3s/registries.yaml changes. Check that k3s reads that file and keeps its data in the default directory: the mirror should appear under ${K3S_CERTS_D}.`
7622
8169
  };
7623
8170
  }
7624
8171
  if (state !== "owed") return { restarted: false, warning: null };
7625
8172
  if (!mayDisrupt(ctx.runningConveyorPods)) {
7626
8173
  return {
7627
8174
  restarted: false,
7628
- warning: `The local image registry needs a k3s restart to load its mirror, but ${busyReason(ctx.runningConveyorPods)} \u2014 the restart is deferred until the machine is idle.`
8175
+ warning: `The local image registry needs a k3s restart to load its mirror or credential, but ${busyReason(ctx.runningConveyorPods)} \u2014 the restart is deferred until the machine is idle.`
7629
8176
  };
7630
8177
  }
7631
8178
  const restart = await ctx.exec("systemctl", ["restart", "k3s"], { timeoutMs: 12e4 });
@@ -7640,21 +8187,257 @@ async function loadRegistryMirror(ctx) {
7640
8187
  return { restarted: true, warning: null };
7641
8188
  }
7642
8189
 
7643
- // src/daemon/prune-namespaces.ts
7644
- var POOL_NAMESPACE_PREFIX = "claudespace-";
7645
- var WORKSPACE_MANAGER_ROLE = "conveyor-workspace-manager";
7646
- var MAX_NAMESPACE_DELETES_PER_RUN = 5;
7647
- var KUBECTL_TIMEOUT = "--request-timeout=10s";
7648
- var EXEC_OPTIONS = { timeoutMs: 15e3 };
7649
- function kubectl(exec, args) {
7650
- return exec("k3s", ["kubectl", ...args, KUBECTL_TIMEOUT], EXEC_OPTIONS).catch(() => null);
8190
+ // src/lib/isolation.ts
8191
+ var EGRESS_POLICY_NAME = "conveyor-k3-egress";
8192
+ var MANAGED_BY2 = { "app.kubernetes.io/managed-by": "conveyor-k3" };
8193
+ var NON_ROUTABLE_IPV4 = [
8194
+ "0.0.0.0/8",
8195
+ "10.0.0.0/8",
8196
+ "100.64.0.0/10",
8197
+ "127.0.0.0/8",
8198
+ "169.254.0.0/16",
8199
+ "172.16.0.0/12",
8200
+ "192.0.0.0/24",
8201
+ "192.0.2.0/24",
8202
+ "192.88.99.0/24",
8203
+ "192.168.0.0/16",
8204
+ "198.18.0.0/15",
8205
+ "198.51.100.0/24",
8206
+ "203.0.113.0/24",
8207
+ "224.0.0.0/4",
8208
+ "240.0.0.0/4"
8209
+ ];
8210
+ var KUBE_DNS = { namespace: "kube-system", labels: { "k8s-app": "kube-dns" } };
8211
+ var PSA_PREFIX = "pod-security.kubernetes.io";
8212
+ function psaLabels(level) {
8213
+ const advisory = level === "baseline" ? "restricted" : level;
8214
+ return {
8215
+ [`${PSA_PREFIX}/enforce`]: level,
8216
+ [`${PSA_PREFIX}/warn`]: advisory,
8217
+ [`${PSA_PREFIX}/audit`]: advisory
8218
+ };
7651
8219
  }
7652
- function candidateNamespaces(listing, wanted) {
7653
- const names = [];
7654
- for (const line of listing.split("\n")) {
7655
- const [name, phase] = line.trim().split(/\s+/);
7656
- if (!name?.startsWith(POOL_NAMESPACE_PREFIX) || wanted.has(name)) continue;
7657
- if (phase === "Terminating") continue;
8220
+ var PSA_LABEL_KEYS = Object.keys(psaLabels("baseline"));
8221
+ function ipv4Value(address) {
8222
+ const parts = address.split(".");
8223
+ if (parts.length !== 4) return null;
8224
+ let value = 0;
8225
+ for (const part of parts) {
8226
+ if (!/^\d{1,3}$/.test(part)) return null;
8227
+ const octet = Number(part);
8228
+ if (octet > 255) return null;
8229
+ value = value * 256 + octet;
8230
+ }
8231
+ return value;
8232
+ }
8233
+ function inNonRoutableRange(address) {
8234
+ const value = ipv4Value(address);
8235
+ if (value === null) return false;
8236
+ return NON_ROUTABLE_IPV4.some((cidr) => {
8237
+ const [base = "", bits = "32"] = cidr.split("/");
8238
+ const start = ipv4Value(base) ?? 0;
8239
+ return value >= start && value < start + 2 ** (32 - Number(bits));
8240
+ });
8241
+ }
8242
+ function hostAddressExceptions(addresses) {
8243
+ const kept = /* @__PURE__ */ new Set();
8244
+ for (const raw of addresses) {
8245
+ const address = raw.trim();
8246
+ if (ipv4Value(address) === null || inNonRoutableRange(address)) continue;
8247
+ kept.add(`${address}/32`);
8248
+ }
8249
+ return [...kept].sort();
8250
+ }
8251
+ function renderEgressNetworkPolicy(namespace, hostAddresses) {
8252
+ const policy = {
8253
+ apiVersion: "networking.k8s.io/v1",
8254
+ kind: "NetworkPolicy",
8255
+ metadata: {
8256
+ name: EGRESS_POLICY_NAME,
8257
+ namespace: assertK8sName(namespace, "namespace"),
8258
+ labels: MANAGED_BY2
8259
+ },
8260
+ spec: {
8261
+ podSelector: {},
8262
+ policyTypes: ["Egress"],
8263
+ egress: [
8264
+ {
8265
+ to: [
8266
+ {
8267
+ namespaceSelector: {
8268
+ matchLabels: { "kubernetes.io/metadata.name": KUBE_DNS.namespace }
8269
+ },
8270
+ podSelector: { matchLabels: KUBE_DNS.labels }
8271
+ }
8272
+ ],
8273
+ ports: [
8274
+ { protocol: "UDP", port: 53 },
8275
+ { protocol: "TCP", port: 53 }
8276
+ ]
8277
+ },
8278
+ {
8279
+ // kube-router enforces after the Service's DNAT, so the registry is
8280
+ // reached by its pods' labels, never by the service range.
8281
+ to: [
8282
+ {
8283
+ namespaceSelector: {
8284
+ matchLabels: { "kubernetes.io/metadata.name": PERSONAL_LOCAL_REGISTRY_NAMESPACE }
8285
+ },
8286
+ podSelector: { matchLabels: LOCAL_REGISTRY_SELECTOR }
8287
+ }
8288
+ ],
8289
+ ports: [{ protocol: "TCP", port: PERSONAL_LOCAL_REGISTRY_PORT }]
8290
+ },
8291
+ {
8292
+ to: [
8293
+ {
8294
+ ipBlock: {
8295
+ cidr: "0.0.0.0/0",
8296
+ except: [...NON_ROUTABLE_IPV4, ...hostAddressExceptions(hostAddresses)]
8297
+ }
8298
+ }
8299
+ ]
8300
+ }
8301
+ ]
8302
+ }
8303
+ };
8304
+ return `${JSON.stringify(policy, null, 2)}
8305
+ `;
8306
+ }
8307
+ var DISABLE_KEY = /(?:^|[\s{,])(["']?)disable-network-policy\1\s*:([^,}]*)/g;
8308
+ var DISABLE_FLAGS = /* @__PURE__ */ new Set(["--disable-network-policy", "-disable-network-policy"]);
8309
+ var FALSE_VALUES = /* @__PURE__ */ new Set(["false", "no", "off", "n", "f", "0"]);
8310
+ function setsTrue(raw) {
8311
+ const value = bareToken(raw).trim().toLowerCase();
8312
+ return !FALSE_VALUES.has(value);
8313
+ }
8314
+ function uncommented(line) {
8315
+ return /^\s*#/.test(line) ? "" : line.replace(/\s#.*$/, "");
8316
+ }
8317
+ function configSetsKey(text) {
8318
+ return text.split("\n").map(uncommented).some((line) => [...line.matchAll(DISABLE_KEY)].some((match) => setsTrue(match[2] ?? "")));
8319
+ }
8320
+ function unitLines(unitText) {
8321
+ const lines2 = [];
8322
+ let pending = "";
8323
+ for (const raw of unitText.split("\n")) {
8324
+ const line = raw.trimEnd();
8325
+ if (/^\s*[#;]/.test(line)) continue;
8326
+ if (line.endsWith("\\")) {
8327
+ pending += `${line.slice(0, -1)} `;
8328
+ continue;
8329
+ }
8330
+ lines2.push(pending + line);
8331
+ pending = "";
8332
+ }
8333
+ return pending ? [...lines2, pending] : lines2;
8334
+ }
8335
+ function flagSetIn(line) {
8336
+ return line.split(/\s+/).some((raw) => {
8337
+ const token = bareToken(raw).replace(/^["']+|["']+$/g, "");
8338
+ const [name = "", ...value] = token.split("=");
8339
+ return DISABLE_FLAGS.has(name) && (value.length === 0 || setsTrue(value.join("=")));
8340
+ });
8341
+ }
8342
+ function networkPolicyControllerDisabled(sources) {
8343
+ if (sources.configs.some((text) => text !== null && configSetsKey(text))) return true;
8344
+ return unitLines(sources.unitText).some(flagSetIn);
8345
+ }
8346
+
8347
+ // src/daemon/isolation-probes.ts
8348
+ 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.";
8349
+ 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.";
8350
+ var NODE_ADDRESSES_JSONPATH = '{range .items[*]}{range .status.addresses[?(@.type=="InternalIP")]}{.address}{"\\n"}{end}{range .status.addresses[?(@.type=="ExternalIP")]}{.address}{"\\n"}{end}{end}';
8351
+ var API_ENDPOINTS_JSONPATH = "{.items[*].endpoints[*].addresses[*]}";
8352
+ var IPV4_ON_INTERFACE = /\binet (\d{1,3}(?:\.\d{1,3}){3})(?:\/| peer )/g;
8353
+ async function stdoutOf(exec, bin, args) {
8354
+ const run = await exec(bin, args, { timeoutMs: 1e4 }).catch(() => null);
8355
+ return run && run.exitCode === 0 ? run.stdout : null;
8356
+ }
8357
+ async function dropInDirExists(exec) {
8358
+ const run = await exec("test", ["-d", K3S_CONFIG_DROPIN_DIR], { timeoutMs: 1e4 }).catch(
8359
+ () => null
8360
+ );
8361
+ if (run === null) return null;
8362
+ if (run.exitCode === 0) return true;
8363
+ return run.exitCode === 1 && run.stderr.trim() === "" ? false : null;
8364
+ }
8365
+ async function readK3sConfigs(ctx) {
8366
+ const dirExists = await dropInDirExists(ctx.exec);
8367
+ if (dirExists === null) return null;
8368
+ if (!dirExists) return [await ctx.fs.readFile(K3S_OWNER_CONFIG)];
8369
+ const listed = await stdoutOf(ctx.exec, "find", [
8370
+ "-L",
8371
+ K3S_CONFIG_DROPIN_DIR,
8372
+ "-maxdepth",
8373
+ "1",
8374
+ "-type",
8375
+ "f",
8376
+ "(",
8377
+ "-iname",
8378
+ "*.yaml",
8379
+ "-o",
8380
+ "-iname",
8381
+ "*.yml",
8382
+ ")"
8383
+ ]);
8384
+ if (listed === null) return null;
8385
+ const paths = listed.split("\n").map((path) => path.trim()).filter(Boolean).sort();
8386
+ const dropIns = await Promise.all(paths.map((path) => ctx.fs.readFile(path)));
8387
+ if (dropIns.some((text) => text === null)) return null;
8388
+ return [await ctx.fs.readFile(K3S_OWNER_CONFIG), ...dropIns];
8389
+ }
8390
+ async function controllerProblem(ctx) {
8391
+ const unitText = await stdoutOf(ctx.exec, "systemctl", ["cat", "k3s.service"]);
8392
+ const configs = unitText === null ? null : await readK3sConfigs(ctx);
8393
+ if (unitText === null || configs === null) return CONTROLLER_UNKNOWN_WARNING;
8394
+ return networkPolicyControllerDisabled({ configs, unitText }) ? CONTROLLER_DISABLED_WARNING : null;
8395
+ }
8396
+ async function readHostAddresses(exec) {
8397
+ const nodes = await stdoutOf(exec, "k3s", [
8398
+ "kubectl",
8399
+ "get",
8400
+ "nodes",
8401
+ "-o",
8402
+ `jsonpath=${NODE_ADDRESSES_JSONPATH}`
8403
+ ]);
8404
+ const interfaces = await stdoutOf(exec, "ip", ["-o", "-4", "addr", "show"]);
8405
+ const api = await stdoutOf(exec, "k3s", [
8406
+ "kubectl",
8407
+ "get",
8408
+ "endpointslices",
8409
+ "-n",
8410
+ "default",
8411
+ "-l",
8412
+ "kubernetes.io/service-name=kubernetes",
8413
+ "-o",
8414
+ `jsonpath=${API_ENDPOINTS_JSONPATH}`
8415
+ ]);
8416
+ if (nodes === null || interfaces === null || api === null) return null;
8417
+ const onInterfaces = [...interfaces.matchAll(IPV4_ON_INTERFACE)].map((match) => match[1] ?? "");
8418
+ return [...nodes.split(/\s+/), ...onInterfaces, ...api.split(/\s+/)].filter(Boolean);
8419
+ }
8420
+ async function recheckIsolation(ctx, reconciled) {
8421
+ if (reconciled?.applied !== true) return { isolation: reconciled, warnings: [] };
8422
+ const problem = await controllerProblem(ctx);
8423
+ return problem ? { isolation: { applied: false }, warnings: [problem] } : { isolation: reconciled, warnings: [] };
8424
+ }
8425
+
8426
+ // src/daemon/prune-namespaces.ts
8427
+ var POOL_NAMESPACE_PREFIX = "claudespace-";
8428
+ var WORKSPACE_MANAGER_ROLE = "conveyor-workspace-manager";
8429
+ var MAX_NAMESPACE_DELETES_PER_RUN = 5;
8430
+ var KUBECTL_TIMEOUT = "--request-timeout=10s";
8431
+ var EXEC_OPTIONS = { timeoutMs: 15e3 };
8432
+ function kubectl(exec, args) {
8433
+ return exec("k3s", ["kubectl", ...args, KUBECTL_TIMEOUT], EXEC_OPTIONS).catch(() => null);
8434
+ }
8435
+ function candidateNamespaces(listing, wanted) {
8436
+ const names = [];
8437
+ for (const line of listing.split("\n")) {
8438
+ const [name, phase] = line.trim().split(/\s+/);
8439
+ if (!name?.startsWith(POOL_NAMESPACE_PREFIX) || wanted.has(name)) continue;
8440
+ if (phase === "Terminating") continue;
7658
8441
  try {
7659
8442
  names.push(assertK8sName(name, "namespace"));
7660
8443
  } catch {
@@ -7702,6 +8485,192 @@ async function pruneStaleNamespaces(ctx, desired, result) {
7702
8485
  }
7703
8486
  }
7704
8487
 
8488
+ // src/daemon/namespace-upkeep.ts
8489
+ function message(error) {
8490
+ return error instanceof Error ? error.message : String(error);
8491
+ }
8492
+ function renderLimitRange(namespace, limits) {
8493
+ const ns = assertK8sName(namespace, "namespace");
8494
+ const values = [
8495
+ assertQuantity(limits.defaultRequest.cpu, "defaultRequest.cpu"),
8496
+ assertQuantity(limits.defaultRequest.memory, "defaultRequest.memory"),
8497
+ assertQuantity(limits.default.cpu, "default.cpu"),
8498
+ assertQuantity(limits.default.memory, "default.memory")
8499
+ ];
8500
+ return [
8501
+ "apiVersion: v1",
8502
+ "kind: LimitRange",
8503
+ "metadata:",
8504
+ " name: conveyor-k3-defaults",
8505
+ ` namespace: ${ns}`,
8506
+ "spec:",
8507
+ " limits:",
8508
+ " - type: Container",
8509
+ " defaultRequest:",
8510
+ ` cpu: "${values[0]}"`,
8511
+ ` memory: "${values[1]}"`,
8512
+ " default:",
8513
+ ` cpu: "${values[2]}"`,
8514
+ ` memory: "${values[3]}"`,
8515
+ ""
8516
+ ].join("\n");
8517
+ }
8518
+ async function reconcileNamespaces(ctx, desired, result) {
8519
+ for (const namespace of desired.namespaces) {
8520
+ try {
8521
+ await kubectlApply(ctx.exec, renderLimitRange(namespace.name, namespace.limitRange));
8522
+ } catch (error) {
8523
+ result.warnings.push(
8524
+ `Could not apply resource defaults to ${namespace.name} (${namespace.projectName}): ${message(error)}`
8525
+ );
8526
+ }
8527
+ }
8528
+ }
8529
+ async function ensureBakeNamespace(ctx, desired) {
8530
+ const found = await ctx.exec(
8531
+ "k3s",
8532
+ ["kubectl", "get", "namespace", PERSONAL_BAKE_NAMESPACE],
8533
+ KUBECTL_TIMEOUT2
8534
+ );
8535
+ if (found.exitCode !== 0) {
8536
+ await runOrThrow(
8537
+ ctx.exec,
8538
+ "k3s",
8539
+ ["kubectl", "create", "namespace", PERSONAL_BAKE_NAMESPACE],
8540
+ KUBECTL_TIMEOUT2
8541
+ );
8542
+ }
8543
+ const limitRange = desired.namespaces[0]?.limitRange;
8544
+ if (limitRange) {
8545
+ await kubectlApply(ctx.exec, renderLimitRange(PERSONAL_BAKE_NAMESPACE, limitRange));
8546
+ }
8547
+ }
8548
+ function knownStance(isolation) {
8549
+ const { podSecurity, egress, ...rest } = { ...isolation };
8550
+ const extra = Object.keys(rest);
8551
+ if (podSecurity === "baseline" && egress === "no-private" && extra.length === 0) return null;
8552
+ const named = `Pod Security "${String(podSecurity).slice(0, 40)}", egress "${String(egress).slice(0, 40)}"`;
8553
+ return extra.length === 0 ? named : `${named}, and ${extra.slice(0, 3).join(", ").slice(0, 80)}`;
8554
+ }
8555
+ var KUBECTL_TIMEOUT2 = { timeoutMs: 3e4 };
8556
+ var RECHECK = "Conveyor checks again within 10 minutes.";
8557
+ async function isolateNamespace(ctx, namespace, isolation, fail) {
8558
+ const ns = assertK8sName(namespace, "namespace");
8559
+ const labels = Object.entries(psaLabels(isolation.podSecurity)).map(
8560
+ ([key, value]) => `${key}=${value}`
8561
+ );
8562
+ const labelled = await ctx.exec(
8563
+ "k3s",
8564
+ ["kubectl", "label", "namespace", ns, "--overwrite", ...labels],
8565
+ KUBECTL_TIMEOUT2
8566
+ );
8567
+ if (labelled.exitCode !== 0) {
8568
+ fail(
8569
+ `Could not apply Pod Security labels to ${ns}: ${labelled.stderr.trim().slice(0, 200)} ${RECHECK}`
8570
+ );
8571
+ }
8572
+ if (isolation.hostAddresses === null) return;
8573
+ try {
8574
+ await kubectlApply(ctx.exec, renderEgressNetworkPolicy(ns, isolation.hostAddresses));
8575
+ } catch (error) {
8576
+ fail(`Could not apply the egress policy to ${ns}: ${message(error).slice(0, 200)} ${RECHECK}`);
8577
+ }
8578
+ }
8579
+ async function reconcileIsolation(ctx, desired, result) {
8580
+ try {
8581
+ await ensureBakeNamespace(ctx, desired);
8582
+ } catch (error) {
8583
+ result.warnings.push(
8584
+ `Could not prepare the bake namespace ${PERSONAL_BAKE_NAMESPACE}: ${message(error)}`
8585
+ );
8586
+ }
8587
+ const isolation = desired.isolation;
8588
+ if (!isolation) {
8589
+ result.isolation = void 0;
8590
+ return;
8591
+ }
8592
+ const unknown = knownStance(isolation);
8593
+ if (unknown) {
8594
+ result.warnings.push(
8595
+ `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\`.`
8596
+ );
8597
+ result.isolation = { applied: false };
8598
+ return;
8599
+ }
8600
+ let applied = true;
8601
+ const fail = (warning) => {
8602
+ applied = false;
8603
+ result.warnings.push(warning);
8604
+ };
8605
+ const problem = await controllerProblem(ctx);
8606
+ if (problem) fail(problem);
8607
+ const hostAddresses = await readHostAddresses(ctx.exec);
8608
+ if (hostAddresses === null) {
8609
+ fail(
8610
+ `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}`
8611
+ );
8612
+ }
8613
+ for (const namespace of desired.namespaces) {
8614
+ await isolateNamespace(ctx, namespace.name, { podSecurity: "baseline", hostAddresses }, fail);
8615
+ }
8616
+ await isolateNamespace(
8617
+ ctx,
8618
+ PERSONAL_BAKE_NAMESPACE,
8619
+ { podSecurity: "privileged", hostAddresses },
8620
+ fail
8621
+ );
8622
+ result.isolation = { applied };
8623
+ }
8624
+
8625
+ // src/lib/fs-port.ts
8626
+ import { chmod, mkdir, readFile, rm, writeFile } from "fs/promises";
8627
+ var nodeFileSystem = {
8628
+ async readFile(path) {
8629
+ try {
8630
+ return await readFile(path, "utf8");
8631
+ } catch {
8632
+ return null;
8633
+ }
8634
+ },
8635
+ async writeFile(path, content, mode) {
8636
+ await mkdir(path.slice(0, path.lastIndexOf("/")), { recursive: true });
8637
+ await writeFile(path, content, { encoding: "utf8", ...mode === void 0 ? {} : { mode } });
8638
+ if (mode !== void 0) await chmod(path, mode);
8639
+ },
8640
+ async removeFile(path) {
8641
+ await rm(path, { force: true });
8642
+ },
8643
+ async ensureDir(path, mode) {
8644
+ await mkdir(path, { recursive: true });
8645
+ if (mode !== void 0) await chmod(path, mode);
8646
+ }
8647
+ };
8648
+ function createMemoryFileSystem(seed = {}) {
8649
+ const files = new Map(Object.entries(seed));
8650
+ const modes = /* @__PURE__ */ new Map();
8651
+ const removed = [];
8652
+ return {
8653
+ files,
8654
+ modes,
8655
+ removed,
8656
+ readFile: (path) => Promise.resolve(files.get(path) ?? null),
8657
+ writeFile: (path, content, mode) => {
8658
+ files.set(path, content);
8659
+ if (mode !== void 0) modes.set(path, mode);
8660
+ return Promise.resolve();
8661
+ },
8662
+ removeFile: (path) => {
8663
+ files.delete(path);
8664
+ removed.push(path);
8665
+ return Promise.resolve();
8666
+ },
8667
+ ensureDir: (path, mode) => {
8668
+ if (mode !== void 0) modes.set(path, mode);
8669
+ return Promise.resolve();
8670
+ }
8671
+ };
8672
+ }
8673
+
7705
8674
  // src/daemon/registry-tags.ts
7706
8675
  var REPO_NAME = /^[a-z0-9]+(?:[._-][a-z0-9]+)*(?:\/[a-z0-9]+(?:[._-][a-z0-9]+)*)*$/;
7707
8676
  var TAG_NAME = /^[A-Za-z0-9_][A-Za-z0-9._-]{0,127}$/;
@@ -7987,7 +8956,10 @@ async function collectRegistryGarbage(ports, options) {
7987
8956
  blobsCollected: false
7988
8957
  });
7989
8958
  const now = ports.now ?? Date.now;
7990
- const fetchImpl = ports.fetch ?? fetch;
8959
+ const fetchImpl = withRegistryCredentials(
8960
+ ports.fetch ?? fetch,
8961
+ ports.readFile ?? ((path) => nodeFileSystem.readFile(path))
8962
+ );
7991
8963
  const retain = await readRetainList(ports);
7992
8964
  if (typeof retain === "string") return skip(retain);
7993
8965
  const blocked = await registryBlocker(ports, now());
@@ -8153,17 +9125,26 @@ var GRANTED_COMMAND_NAMES = LOCAL_BAKE_ROOT_COMMANDS.map((command) => command.na
8153
9125
 
8154
9126
  // src/daemon/telemetry.ts
8155
9127
  var LOCAL_REGISTRY_WARN_AFTER_MS = 10 * 6e4;
9128
+ var LOCAL_REGISTRY_OPEN_WARNING = "The local image registry is open to every account on this machine: any of them can read and replace the images your workspaces start from. It requires its credential once k3s restarts and loads it.";
8156
9129
  async function reportLocalRegistry(ports, watch, now = Date.now()) {
8157
9130
  if (!watch) return { fields: {}, warnings: [] };
8158
- const ready = await localRegistryReady({ readFile: ports.readFile, fetch: ports.fetch });
9131
+ const { ready, authEnforced } = await localRegistryStatus({
9132
+ readFile: ports.readFile,
9133
+ fetch: ports.fetch
9134
+ });
8159
9135
  const paused = (watch.pausedUntil ?? 0) > now;
8160
9136
  watch.notReadySince = ready || paused ? null : watch.notReadySince ?? now;
8161
9137
  const stuckMs = watch.notReadySince === null ? 0 : now - watch.notReadySince;
8162
- return {
8163
- fields: { localRegistry: { host: PERSONAL_LOCAL_REGISTRY_HOST, ready } },
8164
- warnings: stuckMs >= LOCAL_REGISTRY_WARN_AFTER_MS ? [
9138
+ const warnings = [];
9139
+ if (stuckMs >= LOCAL_REGISTRY_WARN_AFTER_MS) {
9140
+ warnings.push(
8165
9141
  `The local image registry has not been ready for ${Math.floor(stuckMs / 6e4)} minutes, so shadow bakes cannot use it yet. Check its pod with \`k3s kubectl -n conveyor-system get pods -l app.kubernetes.io/name=conveyor-registry\`. If the pod is running, k3s has not loaded the registry's mirror yet: it restarts k3s for that only while no Conveyor pod runs.`
8166
- ] : []
9142
+ );
9143
+ }
9144
+ if (!authEnforced) warnings.push(LOCAL_REGISTRY_OPEN_WARNING);
9145
+ return {
9146
+ fields: { localRegistry: { host: PERSONAL_LOCAL_REGISTRY_HOST, ready, authEnforced } },
9147
+ warnings
8167
9148
  };
8168
9149
  }
8169
9150
  var VM_CAPACITY_FILE = "/etc/conveyor-vm/capacity.json";
@@ -8285,14 +9266,23 @@ async function collectHeartbeat(ports, state, inventory) {
8285
9266
  ...runnerUsers.length > 0 ? { runnerUsers } : {},
8286
9267
  ...localImages ? { localImages } : {},
8287
9268
  ...registry.fields,
8288
- localWarnings: [
9269
+ ...state.isolation ? { isolation: state.isolation } : {},
9270
+ ...state.cpuQuotaApplied === void 0 ? {} : { cpuQuotaApplied: state.cpuQuotaApplied },
9271
+ localWarnings: withinHeartbeatBounds([
8289
9272
  ...state.localWarnings,
8290
9273
  ...gcWarning ? [gcWarning] : [],
8291
9274
  ...ungranted.length > 0 ? [ungrantedRunnersWarning(ungranted)] : [],
8292
9275
  ...registry.warnings
8293
- ]
9276
+ ])
8294
9277
  };
8295
9278
  }
9279
+ var HEARTBEAT_LIST_MAX = 64;
9280
+ var HEARTBEAT_STRING_MAX = 512;
9281
+ function withinHeartbeatBounds(warnings) {
9282
+ return warnings.slice(0, HEARTBEAT_LIST_MAX).map(
9283
+ (warning) => warning.length > HEARTBEAT_STRING_MAX ? `${warning.slice(0, HEARTBEAT_STRING_MAX - 1)}\u2026` : warning
9284
+ );
9285
+ }
8296
9286
  async function readPodIdentities(exec) {
8297
9287
  const result = await exec(
8298
9288
  "k3s",
@@ -8328,7 +9318,15 @@ async function collect(ports, now) {
8328
9318
  if (watch) watch.pausedUntil = now.getTime() + REGISTRY_GC_MARK_TTL_MS;
8329
9319
  try {
8330
9320
  const outcome = await collectRegistryGarbage(
8331
- { exec, log, requestDesiredState, fetch: fetch2, sleep: sleep2, isAlive },
9321
+ {
9322
+ exec,
9323
+ log,
9324
+ requestDesiredState,
9325
+ fetch: fetch2,
9326
+ readFile: (path) => ports.fs.readFile(path),
9327
+ sleep: sleep2,
9328
+ isAlive
9329
+ },
8332
9330
  { dryRun: false }
8333
9331
  );
8334
9332
  if (outcome.skipped) return outcome.skipped;
@@ -8374,41 +9372,7 @@ async function runRegistryUpkeep(ports, lastGcAt, maintenanceHour) {
8374
9372
  }
8375
9373
 
8376
9374
  // src/daemon/reconcile.ts
8377
- async function reconcileKubelet(ctx, desired, result) {
8378
- const ownerConfig = await ctx.fs.readFile(K3S_OWNER_CONFIG);
8379
- const rendered = renderK3sDropIn(desired.kubelet, ownerConfig);
8380
- result.skippedKubeletFlags = rendered.skipped;
8381
- result.effectiveImageGcHighThreshold = rendered.skipped.includes("image-gc-high-threshold") ? ownerImageGcHighThreshold(ownerConfig) : desired.kubelet.imageGcHighThreshold;
8382
- for (const flag of rendered.skipped) {
8383
- result.warnings.push(
8384
- `Left \`${flag}\` to your own /etc/rancher/k3s/config.yaml \u2014 Conveyor does not override a setting you chose.`
8385
- );
8386
- }
8387
- const current = await ctx.fs.readFile(K3S_CONFIG_DROPIN);
8388
- if (rendered.content === null) {
8389
- if (current !== null) await ctx.fs.removeFile(K3S_CONFIG_DROPIN);
8390
- return;
8391
- }
8392
- if (current === rendered.content) return;
8393
- await ctx.fs.ensureDir(K3S_CONFIG_DROPIN.slice(0, K3S_CONFIG_DROPIN.lastIndexOf("/")));
8394
- await ctx.fs.writeFile(K3S_CONFIG_DROPIN, rendered.content, 420);
8395
- if (!mayDisrupt(ctx.runningConveyorPods)) {
8396
- result.warnings.push(
8397
- `Kubelet settings changed but ${busyReason(ctx.runningConveyorPods)} \u2014 the restart is deferred until the machine is idle.`
8398
- );
8399
- return;
8400
- }
8401
- const restart = await ctx.exec("systemctl", ["restart", "k3s"], { timeoutMs: 12e4 });
8402
- if (restart.exitCode === 0) {
8403
- result.restartedK3s = true;
8404
- } else {
8405
- result.warnings.push(
8406
- `Could not restart k3s to apply new kubelet settings: ${restart.stderr.trim().slice(0, 200)}`
8407
- );
8408
- }
8409
- }
8410
- async function reconcilePriorityClass(ctx, desired, result) {
8411
- const name = assertK8sName(desired.priorityClass.name, "priorityClass.name");
9375
+ async function livePriorityClassValue(ctx, name) {
8412
9376
  const read = await ctx.exec("k3s", [
8413
9377
  "kubectl",
8414
9378
  "get",
@@ -8417,67 +9381,59 @@ async function reconcilePriorityClass(ctx, desired, result) {
8417
9381
  "-o",
8418
9382
  "jsonpath={.value}"
8419
9383
  ]);
8420
- if (read.exitCode !== 0) return;
8421
- const existingValue = Number.parseInt(read.stdout.trim(), 10);
8422
- if (existingValue === desired.priorityClass.value) return;
9384
+ const value = Number.parseInt(read.stdout.trim(), 10);
9385
+ return read.exitCode === 0 && Number.isFinite(value) ? value : null;
9386
+ }
9387
+ async function reconcilePriorityClass(ctx, desired, result) {
9388
+ const name = assertK8sName(desired.priorityClass.name, "priorityClass.name");
9389
+ const wanted = desired.priorityClass.value;
9390
+ const existingValue = await livePriorityClassValue(ctx, name);
9391
+ if (existingValue === null || existingValue === wanted) return "ready";
9392
+ if (!declaresPriorityClass(desired.rbacManifest, name, wanted)) {
9393
+ result.warnings.push(
9394
+ `The \`${name}\` PriorityClass has value ${existingValue}, but Conveyor's manifest does not declare it at ${wanted}, so it is left as it is. This should never happen; report it.`
9395
+ );
9396
+ return "kept";
9397
+ }
8423
9398
  if (!mayDisrupt(ctx.runningConveyorPods)) {
8424
9399
  result.warnings.push(
8425
- `The \`${name}\` PriorityClass has value ${existingValue}, but Conveyor needs ${desired.priorityClass.value}. The value is immutable, so it must be recreated \u2014 deferred because ${busyReason(ctx.runningConveyorPods)}.`
9400
+ `The \`${name}\` PriorityClass has value ${existingValue}, but Conveyor needs ${wanted}. The value is immutable, so it must be recreated \u2014 deferred because ${busyReason(ctx.runningConveyorPods)}.`
8426
9401
  );
8427
- return;
9402
+ return "kept";
8428
9403
  }
8429
- const removed = await ctx.exec("k3s", ["kubectl", "delete", "priorityclass", name]);
9404
+ const removed = await ctx.exec("k3s", [
9405
+ "kubectl",
9406
+ "delete",
9407
+ "priorityclass",
9408
+ name,
9409
+ "--ignore-not-found"
9410
+ ]);
8430
9411
  if (removed.exitCode !== 0) {
8431
9412
  result.warnings.push(
8432
- `Could not replace the \`${name}\` PriorityClass (value ${existingValue}, needs ${desired.priorityClass.value}): ${removed.stderr.trim().slice(0, 160)}`
9413
+ `Could not replace the \`${name}\` PriorityClass (value ${existingValue}, needs ${wanted}): ${removed.stderr.trim().slice(0, 160)}`
8433
9414
  );
8434
- return;
9415
+ return "kept";
8435
9416
  }
9417
+ return "removed";
9418
+ }
9419
+ async function applyRbac(ctx, desired, result) {
9420
+ const outcome = await reconcilePriorityClass(ctx, desired, result);
9421
+ const manifest = outcome === "kept" ? withoutKind(desired.rbacManifest, "PriorityClass") : desired.rbacManifest;
8436
9422
  try {
8437
- await kubectlApply(ctx.exec, desired.rbacManifest);
8438
- result.recreatedPriorityClass = true;
9423
+ await kubectlApply(ctx.exec, manifest);
8439
9424
  } catch (error) {
9425
+ if (outcome !== "removed") throw error;
9426
+ const value = await livePriorityClassValue(ctx, desired.priorityClass.name).catch(() => null);
9427
+ if (value === desired.priorityClass.value) {
9428
+ result.recreatedPriorityClass = true;
9429
+ throw error;
9430
+ }
8440
9431
  result.warnings.push(
8441
- `The \`${name}\` PriorityClass was removed to change its value but could not be recreated: ${error instanceof Error ? error.message : String(error)}. No build can start on this machine until it exists \u2014 run \`conveyor-k3 sync\`.`
9432
+ `The \`${desired.priorityClass.name}\` PriorityClass was removed to change its value but could not be recreated: ${error instanceof Error ? error.message : String(error)}. No build can start on this machine until it exists \u2014 run \`conveyor-k3 sync\`.`
8442
9433
  );
9434
+ return;
8443
9435
  }
8444
- }
8445
- function renderLimitRange(namespace, limits) {
8446
- const ns = assertK8sName(namespace, "namespace");
8447
- const values = [
8448
- assertQuantity(limits.defaultRequest.cpu, "defaultRequest.cpu"),
8449
- assertQuantity(limits.defaultRequest.memory, "defaultRequest.memory"),
8450
- assertQuantity(limits.default.cpu, "default.cpu"),
8451
- assertQuantity(limits.default.memory, "default.memory")
8452
- ];
8453
- return [
8454
- "apiVersion: v1",
8455
- "kind: LimitRange",
8456
- "metadata:",
8457
- " name: conveyor-k3-defaults",
8458
- ` namespace: ${ns}`,
8459
- "spec:",
8460
- " limits:",
8461
- " - type: Container",
8462
- " defaultRequest:",
8463
- ` cpu: "${values[0]}"`,
8464
- ` memory: "${values[1]}"`,
8465
- " default:",
8466
- ` cpu: "${values[2]}"`,
8467
- ` memory: "${values[3]}"`,
8468
- ""
8469
- ].join("\n");
8470
- }
8471
- async function reconcileNamespaces(ctx, desired, result) {
8472
- for (const namespace of desired.namespaces) {
8473
- try {
8474
- await kubectlApply(ctx.exec, renderLimitRange(namespace.name, namespace.limitRange));
8475
- } catch (error) {
8476
- result.warnings.push(
8477
- `Could not apply resource defaults to ${namespace.name} (${namespace.projectName}): ${error instanceof Error ? error.message : String(error)}`
8478
- );
8479
- }
8480
- }
9436
+ if (outcome === "removed") result.recreatedPriorityClass = true;
8481
9437
  }
8482
9438
  async function step(result, label, run) {
8483
9439
  try {
@@ -8503,18 +9459,19 @@ async function reconcile(ctx, desired) {
8503
9459
  `Conveyor sent a cluster manifest containing disallowed kind(s) ${disallowedKinds.join(", ")} \u2014 refused. RBAC and PriorityClass steps skipped; this should never happen, report it.`
8504
9460
  );
8505
9461
  } else {
8506
- await step(result, "Could not apply RBAC", () => kubectlApply(ctx.exec, desired.rbacManifest));
8507
- await step(
8508
- result,
8509
- "Could not reconcile the PriorityClass",
8510
- () => reconcilePriorityClass(ctx, desired, result)
8511
- );
9462
+ await step(result, "Could not apply RBAC", () => applyRbac(ctx, desired, result));
8512
9463
  }
8513
9464
  await step(
8514
9465
  result,
8515
9466
  "Could not apply namespace defaults",
8516
9467
  () => reconcileNamespaces(ctx, desired, result)
8517
9468
  );
9469
+ await step(
9470
+ result,
9471
+ "Could not apply pod isolation",
9472
+ () => reconcileIsolation(ctx, desired, result)
9473
+ );
9474
+ if (desired.isolation && result.isolation === void 0) result.isolation = { applied: false };
8518
9475
  if (manifestAccepted) {
8519
9476
  await step(
8520
9477
  result,
@@ -8523,6 +9480,7 @@ async function reconcile(ctx, desired) {
8523
9480
  );
8524
9481
  }
8525
9482
  const registry = desired.localRegistry?.enabled === true;
9483
+ const managed = managesK3s(desired, await readState(ctx.fs).catch(() => ({})));
8526
9484
  if (registry) {
8527
9485
  await step(result, "Could not reconcile the local image registry", async () => {
8528
9486
  await deployLocalRegistry(ctx);
@@ -8531,10 +9489,27 @@ async function reconcile(ctx, desired) {
8531
9489
  await step(
8532
9490
  result,
8533
9491
  "Could not apply kubelet settings",
8534
- () => reconcileKubelet(ctx, desired, result)
9492
+ () => reconcileKubelet(ctx, desired, result, managed)
8535
9493
  );
9494
+ if (desired.isolation !== void 0) {
9495
+ await step(result, "Could not cap pod CPU", async () => {
9496
+ result.cpuQuotaApplied = await reconcilePodCpuQuota(ctx.exec, desired, result.warnings);
9497
+ });
9498
+ if (desired.kubelet.systemReserved?.cpu && result.cpuQuotaApplied === void 0) {
9499
+ result.cpuQuotaApplied = false;
9500
+ }
9501
+ }
8536
9502
  if (registry && !result.restartedK3s) {
8537
9503
  await step(result, "Could not load the local image registry mirror", async () => {
9504
+ if (!managed) {
9505
+ if (await registryMirrorPending(ctx.fs))
9506
+ result.warnings.push(
9507
+ ownerRestartWarning(
9508
+ "The local image registry's mirror or credential is new, and the registry stays open to every account on this machine until k3s loads it."
9509
+ )
9510
+ );
9511
+ return;
9512
+ }
8538
9513
  const loaded = await loadRegistryMirror(ctx);
8539
9514
  result.restartedK3s = loaded.restarted;
8540
9515
  if (loaded.warning) result.warnings.push(loaded.warning);
@@ -8547,6 +9522,8 @@ async function reconcile(ctx, desired) {
8547
9522
  var HOUR_MS2 = 60 * 6e4;
8548
9523
  var DAY_MS = 24 * HOUR_MS2;
8549
9524
  var STORE_MEASURE_INTERVAL_MS = 55 * 6e4;
9525
+ var ISOLATION_RETRY_FIRST_MS = 6e4;
9526
+ var ISOLATION_RETRY_MAX_MS = 10 * 6e4;
8550
9527
  var DISK_PRESSURE_FREE_FRACTION = 0.2;
8551
9528
  var STRANDED_PRUNE_BYTES = 20 * 1024 ** 3;
8552
9529
  function flattenWanted(desired) {
@@ -8587,12 +9564,24 @@ var ClusterDaemon = class {
8587
9564
  localImageHashes = /* @__PURE__ */ new Map();
8588
9565
  /** How long the in-cluster registry has been not ready, across beats. */
8589
9566
  registryWatch = { notReadySince: null };
9567
+ /**
9568
+ * Whether pod isolation is in force, from the last reconcile; undefined
9569
+ * while desired state does not ask for it. Every heartbeat checks it again
9570
+ * against k3s's own settings before reporting it.
9571
+ */
9572
+ isolation = void 0;
9573
+ /** Whether the pod slice's CPU cap is in force, from the last reconcile. */
9574
+ cpuQuotaApplied = void 0;
8590
9575
  timers = [];
8591
9576
  /** When the last store walk started, so one that failed is not retried on every reconcile. */
8592
9577
  storeWalkStartedAt = 0;
8593
9578
  /** Serializes reconcile against itself — a sync nudge during a reconcile
8594
9579
  * must queue, not interleave two kubectl applies of the same manifests. */
8595
9580
  reconciling = null;
9581
+ /** The run queued behind the running one, until it starts. At most one waits. */
9582
+ queuedRun = null;
9583
+ /** The next reconcile while pod isolation is not applied; `dueAt` 0 = none. */
9584
+ isolationRetry = { delayMs: ISOLATION_RETRY_FIRST_MS, dueAt: 0 };
8596
9585
  constructor(ports) {
8597
9586
  this.ports = ports;
8598
9587
  }
@@ -8628,10 +9617,27 @@ var ClusterDaemon = class {
8628
9617
  }
8629
9618
  /** Fetch the desired state and converge to it. Queued, never concurrent. */
8630
9619
  syncAndReconcile() {
8631
- const run = (this.reconciling ?? Promise.resolve()).then(() => this.runReconcile());
9620
+ if (this.queuedRun) return this.queuedRun;
9621
+ const run = (this.reconciling ?? Promise.resolve()).then(() => {
9622
+ this.queuedRun = null;
9623
+ return this.runReconcile();
9624
+ });
9625
+ this.queuedRun = run;
8632
9626
  this.reconciling = run.catch(() => void 0);
8633
9627
  return run;
8634
9628
  }
9629
+ /** Reconcile again later while pod isolation is not applied; stop once it is. */
9630
+ scheduleIsolationRetry() {
9631
+ if (this.isolation?.applied !== false) {
9632
+ this.isolationRetry = { delayMs: ISOLATION_RETRY_FIRST_MS, dueAt: 0 };
9633
+ return;
9634
+ }
9635
+ const { delayMs } = this.isolationRetry;
9636
+ this.isolationRetry = {
9637
+ delayMs: Math.min(delayMs * 2, ISOLATION_RETRY_MAX_MS),
9638
+ dueAt: Date.now() + delayMs
9639
+ };
9640
+ }
8635
9641
  async runReconcile() {
8636
9642
  try {
8637
9643
  this.desired = await this.ports.connection.requestDesiredState();
@@ -8647,6 +9653,9 @@ var ClusterDaemon = class {
8647
9653
  this.desired
8648
9654
  );
8649
9655
  this.warnings = result.warnings;
9656
+ this.isolation = result.isolation;
9657
+ this.cpuQuotaApplied = result.cpuQuotaApplied;
9658
+ this.scheduleIsolationRetry();
8650
9659
  for (const warning of result.warnings) this.ports.log(warning);
8651
9660
  if (result.prunedNamespaces.length > 0) {
8652
9661
  this.ports.log(
@@ -8802,6 +9811,11 @@ var ClusterDaemon = class {
8802
9811
  );
8803
9812
  }
8804
9813
  for (const failure of result.failures) this.ports.log(`Image prune failed \u2014 ${failure}`);
9814
+ if (result.pending.length > 0) {
9815
+ this.ports.log(
9816
+ `Image removal still in progress for ${result.pending.length} image(s); the next prune re-checks them.`
9817
+ );
9818
+ }
8805
9819
  } catch (error) {
8806
9820
  this.ports.log(
8807
9821
  `Skipping image prune: ${error instanceof Error ? error.message : String(error)}`
@@ -8817,6 +9831,10 @@ var ClusterDaemon = class {
8817
9831
  const inventory = await readImageInventory(this.ports.exec).catch(() => null);
8818
9832
  const registryOn = this.desired?.localRegistry?.enabled === true;
8819
9833
  if (!registryOn) this.registryWatch.notReadySince = null;
9834
+ const isolation = await recheckIsolation(
9835
+ { exec: this.ports.exec, fs: this.ports.fs },
9836
+ this.isolation
9837
+ );
8820
9838
  return await collectHeartbeat(
8821
9839
  {
8822
9840
  exec: this.ports.exec,
@@ -8831,10 +9849,12 @@ var ClusterDaemon = class {
8831
9849
  lastPruneAt: this.state?.lastPruneAt ?? null,
8832
9850
  lastWarmAt: this.state?.lastWarmAt ?? null,
8833
9851
  lastUpdateAt: this.state?.lastUpdateAt ?? null,
8834
- localWarnings: [...this.warnings, ...this.warmWarnings],
9852
+ localWarnings: [...this.warnings, ...this.warmWarnings, ...isolation.warnings],
8835
9853
  imageGcHighThreshold: this.effectiveImageGcHigh,
8836
9854
  localImageHashes: this.localImageHashes,
8837
9855
  ...registryOn ? { localRegistry: this.registryWatch } : {},
9856
+ ...isolation.isolation ? { isolation: isolation.isolation } : {},
9857
+ cpuQuotaApplied: this.cpuQuotaApplied,
8838
9858
  k3sStoreBytes: this.measuredStoreBytes
8839
9859
  },
8840
9860
  inventory
@@ -8859,6 +9879,10 @@ var ClusterDaemon = class {
8859
9879
  return this.beating;
8860
9880
  }
8861
9881
  async runBeat() {
9882
+ if (this.isolationRetry.dueAt > 0 && Date.now() >= this.isolationRetry.dueAt) {
9883
+ this.isolationRetry.dueAt = 0;
9884
+ void this.syncAndReconcile();
9885
+ }
8862
9886
  try {
8863
9887
  const workloads = await this.ports.connection.sendHeartbeat({
8864
9888
  ...await this.buildHeartbeat(),
@@ -8965,58 +9989,10 @@ function renderAllUnits(options) {
8965
9989
  };
8966
9990
  }
8967
9991
 
8968
- // src/lib/fs-port.ts
8969
- import { chmod, mkdir, readFile, rm, writeFile } from "fs/promises";
8970
- var nodeFileSystem = {
8971
- async readFile(path) {
8972
- try {
8973
- return await readFile(path, "utf8");
8974
- } catch {
8975
- return null;
8976
- }
8977
- },
8978
- async writeFile(path, content, mode) {
8979
- await mkdir(path.slice(0, path.lastIndexOf("/")), { recursive: true });
8980
- await writeFile(path, content, "utf8");
8981
- if (mode !== void 0) await chmod(path, mode);
8982
- },
8983
- async removeFile(path) {
8984
- await rm(path, { force: true });
8985
- },
8986
- async ensureDir(path, mode) {
8987
- await mkdir(path, { recursive: true });
8988
- if (mode !== void 0) await chmod(path, mode);
8989
- }
8990
- };
8991
- function createMemoryFileSystem(seed = {}) {
8992
- const files = new Map(Object.entries(seed));
8993
- const modes = /* @__PURE__ */ new Map();
8994
- const removed = [];
8995
- return {
8996
- files,
8997
- modes,
8998
- removed,
8999
- readFile: (path) => Promise.resolve(files.get(path) ?? null),
9000
- writeFile: (path, content, mode) => {
9001
- files.set(path, content);
9002
- if (mode !== void 0) modes.set(path, mode);
9003
- return Promise.resolve();
9004
- },
9005
- removeFile: (path) => {
9006
- files.delete(path);
9007
- removed.push(path);
9008
- return Promise.resolve();
9009
- },
9010
- ensureDir: (path, mode) => {
9011
- if (mode !== void 0) modes.set(path, mode);
9012
- return Promise.resolve();
9013
- }
9014
- };
9015
- }
9016
-
9017
9992
  // src/verbs.ts
9018
9993
  var VERBS = [
9019
9994
  "vm",
9995
+ "host",
9020
9996
  "setup",
9021
9997
  "start",
9022
9998
  "stop",
@@ -9049,14 +10025,22 @@ function parseArgs(argv) {
9049
10025
  }
9050
10026
  return { verb, positional, flags };
9051
10027
  }
9052
- var HELP = `conveyor-k3 \u2014 managed personal compute for Conveyor
10028
+ var HELP = `conveyor-k3 \u2014 Personal Compute for Conveyor
9053
10029
 
9054
10030
  Usage: conveyor-k3 <command> [options]
9055
10031
 
9056
- Commands:
9057
- vm Manage isolated compute on macOS or Linux.
9058
- Run conveyor-k3 vm for host lifecycle commands.
9059
- setup Install and enrol this machine. Safe to re-run.
10032
+ One product, two runtimes. Pick the one that fits the machine:
10033
+
10034
+ On your computer or server:
10035
+ vm Personal computer runtime: an isolated VM on macOS or
10036
+ Linux. Run conveyor-k3 vm for its commands.
10037
+ host Dedicated server runtime: k3s and the daemon on this
10038
+ Linux machine, with no VM. Root only. Run
10039
+ conveyor-k3 host for its commands.
10040
+
10041
+ Inside a linked machine (the VM guest, or the server itself):
10042
+ setup VM guest only: install and enrol this machine. Safe
10043
+ to re-run. On a server, use conveyor-k3 host setup.
9060
10044
  start Start the tunnel and the daemon.
9061
10045
  stop Stop both, and tell Conveyor at once so your next
9062
10046
  build goes straight to project compute.
@@ -9081,10 +10065,12 @@ Commands:
9081
10065
  into this node and pin it. Writes one file under
9082
10066
  /etc/sudoers.d, checked with visudo first. Needed only
9083
10067
  when a project has Bake Locally turned on.
9084
- uninstall Remove Conveyor's units, environment, state, and image
9085
- registry. Your k3s config, your own registries.yaml
9086
- entries, your workloads, and the registry's data are
9087
- left alone.
10068
+ uninstall VM guest only: remove Conveyor's units, environment,
10069
+ state, and image registry. Your k3s config, your own
10070
+ registries.yaml entries, your workloads, and the
10071
+ registry's data are left alone. On a server, use
10072
+ conveyor-k3 host uninstall: it also removes the
10073
+ device key.
9088
10074
  --purge-k3s --yes also remove k3s itself and the
9089
10075
  registry's data
9090
10076
  daemon Run in the foreground (what the systemd unit calls).
@@ -9168,19 +10154,79 @@ async function removeLocalRegistry(ports) {
9168
10154
  `Could not remove the local image registry from the cluster: ${removed.stderr.trim().slice(0, 200)}`
9169
10155
  );
9170
10156
  }
10157
+ for (const [namespace, name] of [
10158
+ [PERSONAL_LOCAL_REGISTRY_NAMESPACE, LOCAL_REGISTRY_HTPASSWD_SECRET],
10159
+ [PERSONAL_BAKE_NAMESPACE, PERSONAL_LOCAL_REGISTRY_PUSH_SECRET]
10160
+ ]) {
10161
+ const secret = await ports.exec(
10162
+ "k3s",
10163
+ ["kubectl", "-n", namespace, "delete", "secret", name, "--ignore-not-found"],
10164
+ { timeoutMs: 6e4 }
10165
+ );
10166
+ if (secret.exitCode !== 0) {
10167
+ ports.log(
10168
+ `Could not remove the local image registry's Secret ${namespace}/${name}: ${secret.stderr.trim().slice(0, 200)}`
10169
+ );
10170
+ }
10171
+ }
9171
10172
  const raw = await ports.fs.readFile(REGISTRIES_FILE);
9172
10173
  const parsed = parseRegistries(raw);
9173
- if (mirrorEndpoints(parsed, PERSONAL_LOCAL_REGISTRY_HOST).length === 0) return;
10174
+ const next = LOCAL_REGISTRY_AUTH_HOSTS.reduce(
10175
+ removeRegistryAuth,
10176
+ removeRegistryMirror(parsed, PERSONAL_LOCAL_REGISTRY_HOST)
10177
+ );
10178
+ if (serializeRegistries(next) === serializeRegistries(parsed)) return;
9174
10179
  const backup = await writeRegistriesWithBackup(
9175
10180
  ports.fs,
9176
10181
  raw,
9177
- removeRegistryMirror(parsed, PERSONAL_LOCAL_REGISTRY_HOST),
10182
+ next,
9178
10183
  (ports.now ?? (() => /* @__PURE__ */ new Date()))()
9179
10184
  );
9180
10185
  ports.log(
9181
- `Removed the local image registry's mirror from ${REGISTRIES_FILE} (backup: ${backup}).`
10186
+ `Removed the local image registry's mirror and credential from ${REGISTRIES_FILE} (backup: ${backup}).`
9182
10187
  );
9183
10188
  }
10189
+ async function removeIsolation(ports) {
10190
+ const [key, value] = Object.entries(MANAGED_BY2)[0] ?? [];
10191
+ const removed = await ports.exec(
10192
+ "k3s",
10193
+ ["kubectl", "delete", "networkpolicy", "-A", "-l", `${key}=${value}`, "--ignore-not-found"],
10194
+ { timeoutMs: 12e4 }
10195
+ );
10196
+ if (removed.exitCode !== 0) {
10197
+ ports.log(
10198
+ `Could not remove Conveyor's network policies: ${removed.stderr.trim().slice(0, 200)}`
10199
+ );
10200
+ }
10201
+ const listed = await ports.exec("k3s", [
10202
+ "kubectl",
10203
+ "get",
10204
+ "namespaces",
10205
+ "-o",
10206
+ "jsonpath={.items[*].metadata.name}"
10207
+ ]);
10208
+ if (listed.exitCode !== 0) {
10209
+ ports.log(
10210
+ `Could not list namespaces to remove Conveyor's Pod Security labels: ${listed.stderr.trim().slice(0, 200)}`
10211
+ );
10212
+ return;
10213
+ }
10214
+ const namespaces = listed.stdout.split(/\s+/).filter((name) => name.startsWith("claudespace-") || name === PERSONAL_BAKE_NAMESPACE);
10215
+ for (const namespace of namespaces) {
10216
+ const unlabelled = await ports.exec("k3s", [
10217
+ "kubectl",
10218
+ "label",
10219
+ "namespace",
10220
+ namespace,
10221
+ ...PSA_LABEL_KEYS.map((labelKey) => `${labelKey}-`)
10222
+ ]);
10223
+ if (unlabelled.exitCode !== 0) {
10224
+ ports.log(
10225
+ `Could not remove Conveyor's Pod Security labels from ${namespace}: ${unlabelled.stderr.trim().slice(0, 200)}`
10226
+ );
10227
+ }
10228
+ }
10229
+ }
9184
10230
  async function uninstall(ports, options = {}) {
9185
10231
  const result = { stoppedUnits: [], removedFiles: [], purgedK3s: false };
9186
10232
  if (options.purgeK3s && !options.confirmed) {
@@ -9193,6 +10239,11 @@ async function uninstall(ports, options = {}) {
9193
10239
  result.stoppedUnits.push(unit);
9194
10240
  }
9195
10241
  await removeLocalRegistry(ports);
10242
+ await removeIsolation(ports);
10243
+ const lifted = await liftPodCpuQuota(ports.exec).catch(
10244
+ (error) => error instanceof Error ? error.message : String(error)
10245
+ );
10246
+ if (lifted) ports.log(`Could not lift Conveyor's CPU cap on kubepods.slice: ${lifted}`);
9196
10247
  for (const path of OWNED_FILES) {
9197
10248
  await ports.fs.removeFile(path);
9198
10249
  result.removedFiles.push(path);
@@ -9245,6 +10296,45 @@ async function uninstall(ports, options = {}) {
9245
10296
  return result;
9246
10297
  }
9247
10298
 
10299
+ // src/lib/bins.ts
10300
+ var INSTALLER_PATHS = {
10301
+ bun: "/root/.bun/bin/bun",
10302
+ cli: "/root/.bun/bin/conveyor-k3",
10303
+ cloudflared: "/usr/local/bin/cloudflared"
10304
+ };
10305
+ var CLI_PATH_LINK = "/usr/local/bin/conveyor-k3";
10306
+ async function linkCliOnPath(exec, target = INSTALLER_PATHS.cli) {
10307
+ if (target === CLI_PATH_LINK) return { linked: false };
10308
+ const present = await exec("test", ["-x", target]).catch(() => null);
10309
+ if (present?.exitCode !== 0) return { linked: false };
10310
+ const link = await exec("ln", ["-sfn", target, CLI_PATH_LINK]).catch(() => null);
10311
+ if (link?.exitCode === 0) return { linked: true };
10312
+ return {
10313
+ linked: false,
10314
+ problem: `Could not link ${CLI_PATH_LINK} to ${target}: ${link?.stderr.trim().slice(0, 200) || "ln failed"}`
10315
+ };
10316
+ }
10317
+
10318
+ // src/lib/cloudflared.ts
10319
+ var PACKAGE_MANAGER_REFUSAL = "installed by a package manager";
10320
+ async function resolveRealPath(exec, bin) {
10321
+ const result = await exec("readlink", ["-f", bin]).catch(() => null);
10322
+ const resolved = result?.exitCode === 0 ? result.stdout.trim() : "";
10323
+ return resolved.startsWith("/") ? resolved : bin;
10324
+ }
10325
+ async function cloudflaredUpdater(exec, bin) {
10326
+ const real = await resolveRealPath(exec, bin);
10327
+ const owner = await exec("dpkg", ["-S", real]).catch(() => null);
10328
+ return owner?.exitCode === 0 ? "package-manager" : "conveyor";
10329
+ }
10330
+ function isPackageManagerRefusal(output) {
10331
+ return `${output.stdout}
10332
+ ${output.stderr}`.toLowerCase().includes(PACKAGE_MANAGER_REFUSAL);
10333
+ }
10334
+ function describeCloudflaredUpdater(updater) {
10335
+ return updater === "package-manager" ? "cloudflared is updated by your package manager, not by Conveyor" : "cloudflared is updated by Conveyor's daily update";
10336
+ }
10337
+
9248
10338
  // src/commands/update.ts
9249
10339
  var CLOUDFLARED_UPDATED_EXIT = 11;
9250
10340
  function readExecStart(unit) {
@@ -9259,12 +10349,6 @@ function readExecStart(unit) {
9259
10349
  }
9260
10350
  return collected.join(" ");
9261
10351
  }
9262
- function bareToken(token) {
9263
- let bare = token.trim();
9264
- if (bare.endsWith("\\")) bare = bare.slice(0, -1).trim();
9265
- if (bare.length >= 2 && bare.startsWith("'") && bare.endsWith("'")) bare = bare.slice(1, -1);
9266
- return bare;
9267
- }
9268
10352
  function parseForeignExecFlags(execStart) {
9269
10353
  const ours = new Set(K3S_INSTALL_EXEC.split(/\s+/).filter((token) => token.startsWith("--")));
9270
10354
  const foreign = [];
@@ -9284,6 +10368,12 @@ async function foreignK3sExecFlags(exec) {
9284
10368
  return parseForeignExecFlags(readExecStart(unit.stdout));
9285
10369
  }
9286
10370
  function decideK3sUpgrade(input) {
10371
+ if (input.managesK3s === false) {
10372
+ return {
10373
+ upgrade: false,
10374
+ reason: "Conveyor did not install this server's k3s, so its owner upgrades it"
10375
+ };
10376
+ }
9287
10377
  if (!input.autoUpgradeK3s) {
9288
10378
  return { upgrade: false, reason: "automatic k3s upgrades are turned off for this machine" };
9289
10379
  }
@@ -9332,7 +10422,22 @@ async function updatePackage(ports, result, targetVersion) {
9332
10422
  await ports.exec("systemctl", ["restart", DAEMON_UNIT]);
9333
10423
  }
9334
10424
  async function updateCloudflared(ports, result) {
10425
+ const packaged = () => {
10426
+ result.cloudflaredUpdater = "package-manager";
10427
+ result.notes.push(
10428
+ `cloudflared update skipped: ${describeCloudflaredUpdater("package-manager")}`
10429
+ );
10430
+ };
10431
+ if (await cloudflaredUpdater(ports.exec, ports.bins.cloudflared) === "package-manager") {
10432
+ packaged();
10433
+ return;
10434
+ }
9335
10435
  const update = await ports.exec(ports.bins.cloudflared, ["update"], { timeoutMs: 5 * 6e4 });
10436
+ if (isPackageManagerRefusal(update)) {
10437
+ packaged();
10438
+ return;
10439
+ }
10440
+ result.cloudflaredUpdater = "conveyor";
9336
10441
  if (update.exitCode !== CLOUDFLARED_UPDATED_EXIT) return;
9337
10442
  result.cloudflaredUpdated = true;
9338
10443
  ports.log("cloudflared was updated; restarting the tunnel.");
@@ -9342,10 +10447,13 @@ async function runUpdate(ports, decision, channel, targetVersion) {
9342
10447
  const result = {
9343
10448
  packageUpdated: false,
9344
10449
  cloudflaredUpdated: false,
10450
+ cloudflaredUpdater: null,
9345
10451
  k3sUpgraded: false,
9346
10452
  notes: []
9347
10453
  };
9348
10454
  await updatePackage(ports, result, targetVersion);
10455
+ const linked = await linkCliOnPath(ports.exec);
10456
+ if (linked.problem) result.notes.push(linked.problem);
9349
10457
  await updateCloudflared(ports, result);
9350
10458
  if (!decision.upgrade) {
9351
10459
  result.notes.push(`k3s upgrade skipped: ${decision.reason}`);
@@ -9378,6 +10486,9 @@ async function runUpdate(ports, decision, channel, targetVersion) {
9378
10486
  return result;
9379
10487
  }
9380
10488
 
10489
+ // src/lib/env.ts
10490
+ import { readFileSync as readFileSync3 } from "fs";
10491
+
9381
10492
  // src/commands/setup.ts
9382
10493
  var K3S_MIN_MAJOR_MINOR = [1, 29];
9383
10494
  function parseK3sVersion(output) {
@@ -10381,7 +11492,6 @@ export {
10381
11492
  renderAllUnits,
10382
11493
  renderGuestDnsProvision,
10383
11494
  renderK3sDropIn,
10384
- renderKubeletDropIn,
10385
11495
  renderRegistryAuthSummary,
10386
11496
  resolveCloudflaredSrvRecords,
10387
11497
  resolveNetwork,