@rallycry/conveyor-k3 0.1.11 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -5,10 +5,17 @@ interface ClusterConnectionConfig {
5
5
  clusterToken: string;
6
6
  /** Test-only override; production always reads the root-owned guest key. */
7
7
  deviceKeyPath?: string;
8
+ /** Test-only overrides; production reads the host-written control aliases. */
9
+ readControlFile?: (path: string) => string | null;
10
+ controlFileExists?: (path: string) => boolean;
11
+ /** Dial again after the server closes the socket. The daemon's connection only. */
12
+ reconnectWhenReplaced?: boolean;
8
13
  }
9
14
  declare class ClusterConnection {
10
15
  private socket;
11
16
  private readonly config;
17
+ private redialDelayMs;
18
+ private redialTimer;
12
19
  /** Called when Conveyor says the desired state changed. */
13
20
  onSyncRequested: (() => void) | null;
14
21
  /** Called when Conveyor asks the machine to remove itself. */
@@ -26,6 +33,8 @@ declare class ClusterConnection {
26
33
  * instead of waiting out the stale window.
27
34
  */
28
35
  sayGoodbye(timeoutMs?: number): Promise<void>;
36
+ /** Dial again after the server closed the socket, waiting longer each time. */
37
+ private redial;
29
38
  disconnect(): void;
30
39
  }
31
40
 
@@ -68,7 +77,8 @@ interface FileSystemPort$1 {
68
77
  readFile(path: string): Promise<string | null>;
69
78
  writeFile(path: string, content: string, mode?: number): Promise<void>;
70
79
  removeFile(path: string): Promise<void>;
71
- ensureDir(path: string): Promise<void>;
80
+ /** With a mode, an existing directory is set to it as well. */
81
+ ensureDir(path: string, mode?: number): Promise<void>;
72
82
  }
73
83
  declare const nodeFileSystem: FileSystemPort$1;
74
84
  /** In-memory filesystem for tests. Exported so suites share one shape. */
@@ -78,6 +88,23 @@ declare function createMemoryFileSystem(seed?: Record<string, string>): FileSyst
78
88
  removed: string[];
79
89
  };
80
90
 
91
+ /**
92
+ * The long-lived daemon: connect, converge, report, repeat.
93
+ *
94
+ * Three clocks, deliberately independent:
95
+ *
96
+ * - **Heartbeat, 30s.** Placement reads what this writes, so it is the one
97
+ * thing that must never stall behind slower work.
98
+ * - **Warm, hourly.** Re-pull the wanted refs so a bake never costs the next
99
+ * card a cold multi-gigabyte pull.
100
+ * - **Reconcile + prune, daily** — plus on connect and whenever Conveyor says
101
+ * the desired state changed.
102
+ *
103
+ * The daily reconcile is what makes this a reconciler rather than a command
104
+ * listener: a machine that was powered off, or whose sync nudge landed on
105
+ * another API replica, still converges on its own.
106
+ */
107
+
81
108
  interface DaemonPorts {
82
109
  exec: Executor;
83
110
  fs: FileSystemPort$1;
@@ -85,6 +112,8 @@ interface DaemonPorts {
85
112
  cliVersion: string;
86
113
  writeWorkloads?: (data: PersonalClusterWorkloads | undefined) => Promise<void>;
87
114
  log: (line: string) => void;
115
+ /** The in-cluster registry's HTTP API; the global `fetch` when absent. */
116
+ fetch?: (url: string, init?: RequestInit) => Promise<Response>;
88
117
  }
89
118
  declare class ClusterDaemon {
90
119
  private readonly ports;
@@ -105,15 +134,33 @@ declare class ClusterDaemon {
105
134
  * the next reconcile silently clear them.
106
135
  */
107
136
  private warmWarnings;
137
+ /** Content hash labels by image id, so each image is inspected once. */
138
+ private readonly localImageHashes;
139
+ /** How long the in-cluster registry has been not ready, across beats. */
140
+ private readonly registryWatch;
141
+ /**
142
+ * Whether pod isolation is in force, from the last reconcile; undefined
143
+ * while desired state does not ask for it. Every heartbeat checks it again
144
+ * against k3s's own settings before reporting it.
145
+ */
146
+ private isolation;
108
147
  private timers;
148
+ /** When the last store walk started, so one that failed is not retried on every reconcile. */
149
+ private storeWalkStartedAt;
109
150
  /** Serializes reconcile against itself — a sync nudge during a reconcile
110
151
  * must queue, not interleave two kubectl applies of the same manifests. */
111
152
  private reconciling;
153
+ /** The run queued behind the running one, until it starts. At most one waits. */
154
+ private queuedRun;
155
+ /** The next reconcile while pod isolation is not applied; `dueAt` 0 = none. */
156
+ private isolationRetry;
112
157
  constructor(ports: DaemonPorts);
113
158
  start(): Promise<void>;
114
159
  stop(): Promise<void>;
115
160
  /** Fetch the desired state and converge to it. Queued, never concurrent. */
116
161
  syncAndReconcile(): Promise<void>;
162
+ /** Reconcile again later while pod isolation is not applied; stop once it is. */
163
+ private scheduleIsolationRetry;
117
164
  private runReconcile;
118
165
  /**
119
166
  * The hourly clock: warm, re-pin, and reclaim if the pile justifies it.
@@ -124,6 +171,14 @@ declare class ClusterDaemon {
124
171
  * generations in between.
125
172
  */
126
173
  private warmAndPruneIfDue;
174
+ /**
175
+ * Measure k3s's containerd store when the last measurement is old enough,
176
+ * and keep the result for the heartbeat. After a prune, so the number it
177
+ * reports is what the prune left. A failed measurement keeps the last one.
178
+ */
179
+ private measureStoreIfDue;
180
+ /** The store size the heartbeat reports: the last measurement, null before one. */
181
+ private get measuredStoreBytes();
127
182
  /** Pre-pull the wanted refs. Silent on success; that is the normal case. */
128
183
  warm(): Promise<void>;
129
184
  /**
@@ -158,63 +213,8 @@ declare class ClusterDaemon {
158
213
  private runBeat;
159
214
  }
160
215
 
161
- /**
162
- * Converge the machine onto Conveyor's desired state.
163
- *
164
- * This is what "manages itself" means in code. Conveyor publishes what the
165
- * cluster SHOULD look like; this function makes it so, and it runs on connect,
166
- * on request, and daily. Nothing here is a command from the server — a machine
167
- * that was powered off for a week catches up on its next run, and a practice
168
- * Conveyor changes reaches every enrolled box without anyone touching one.
169
- *
170
- * Three properties every step keeps:
171
- *
172
- * 1. **Idempotent.** Running twice changes nothing the first run already did.
173
- * 2. **Independently fallible.** A step that fails records a warning and the
174
- * rest still run. A cluster with one bad namespace is not a reason to skip
175
- * image hygiene.
176
- * 3. **Additive.** Nothing here deletes a namespace, a workload, or an image
177
- * that is in use. The machine belongs to its owner.
178
- * 4. **Bounded.** The server does not get to say what runs here. The one field
179
- * it sends as raw YAML is checked against a local kind allowlist before it
180
- * reaches `kubectl` — see `../lib/manifest-guard.js`.
181
- */
182
-
183
- /** The filesystem operations reconcile needs, injectable for tests. */
184
- interface FileSystemPort {
185
- readFile(path: string): Promise<string | null>;
186
- writeFile(path: string, content: string, mode?: number): Promise<void>;
187
- removeFile(path: string): Promise<void>;
188
- ensureDir(path: string): Promise<void>;
189
- }
190
- interface ReconcileContext {
191
- exec: Executor;
192
- fs: FileSystemPort;
193
- /**
194
- * Conveyor pods currently running here, or null when that could not be
195
- * determined. A disruptive step is unsafe above zero AND unsafe on null —
196
- * "I could not ask" is not "nothing is running".
197
- */
198
- runningConveyorPods: number | null;
199
- }
200
- interface ReconcileResult {
201
- appliedVersion: number;
202
- warnings: string[];
203
- /** True when the kubelet drop-in changed and k3s was restarted. */
204
- restartedK3s: boolean;
205
- /** Kubelet flags left to the owner because their own config sets them. */
206
- skippedKubeletFlags: string[];
207
- /**
208
- * The image-GC high threshold the kubelet is actually enforcing — ours, or
209
- * the owner's when their own config.yaml set it and the drop-in stood down.
210
- * Null when neither side named one. Reported so the heartbeat can warn about
211
- * GC pressure against the number in force rather than the one we sent.
212
- */
213
- effectiveImageGcHighThreshold: number | null;
214
- /** True when a wrong-valued PriorityClass was deleted AND put back. */
215
- recreatedPriorityClass: boolean;
216
- }
217
- declare function reconcile(ctx: ReconcileContext, desired: PersonalClusterDesiredState): Promise<ReconcileResult>;
216
+ /** The `fetch` the registry API calls go through, injectable for tests. */
217
+ type RegistryFetch = (url: string, init?: RequestInit) => Promise<Response>;
218
218
 
219
219
  /** One entry of `crictl images -o json`. */
220
220
  interface CrictlImage {
@@ -277,6 +277,22 @@ interface WarmResult {
277
277
  */
278
278
  declare function warmImages(exec: Executor, wantedImages: string[], unauthenticatedHosts: string[]): Promise<WarmResult>;
279
279
 
280
+ /**
281
+ * Which locally baked images this machine holds, for placement.
282
+ *
283
+ * A Bake Locally project's image exists only where a bake imported it: the
284
+ * runner's own machine, or the managed VM on it. When one account has several
285
+ * instances, placement has to send a card to an instance that holds the image,
286
+ * and should prefer one holding the latest bake. So the heartbeat lists every
287
+ * `localhost/` ref, with the content hash the bake stamped on the image.
288
+ *
289
+ * The hash takes one `crictl inspecti` per image, cached by image id: an id
290
+ * names exactly one image config, so its label never changes.
291
+ */
292
+
293
+ /** Image id to the content hash label on that image, null when it has none. */
294
+ type LocalImageHashCache = Map<string, string | null>;
295
+
280
296
  interface TelemetryState {
281
297
  cliVersion: string;
282
298
  appliedDesiredStateVersion: number;
@@ -291,10 +307,36 @@ interface TelemetryState {
291
307
  * no reconcile has read one yet. Drives the GC-pressure warning below.
292
308
  */
293
309
  imageGcHighThreshold?: number | null;
310
+ /** The daemon's own cache of content hash labels, kept across beats. */
311
+ localImageHashes?: LocalImageHashCache;
312
+ /** Present while desired state turns the in-cluster registry on. */
313
+ localRegistry?: LocalRegistryWatch;
314
+ /** Present while desired state asks for pod isolation (a host-runtime machine). */
315
+ isolation?: {
316
+ applied: boolean;
317
+ };
318
+ /**
319
+ * The last measured size of k3s's containerd store. Null (or absent) until
320
+ * the first measurement, and the heartbeat says null then: a 0 would read
321
+ * as an empty store.
322
+ */
323
+ k3sStoreBytes?: number | null;
294
324
  }
295
325
  interface TelemetryPorts {
296
326
  exec: Executor;
297
327
  readFile(path: string): Promise<string | null>;
328
+ /** Injectable for tests; the registry probe uses the global otherwise. */
329
+ fetch?: RegistryFetch;
330
+ }
331
+ /** The daemon's memory of the registry's readiness, kept across beats. */
332
+ interface LocalRegistryWatch {
333
+ /** When a beat first found the registry not ready; null while it is. */
334
+ notReadySince: number | null;
335
+ /**
336
+ * Until when a cleanup stops the registry on purpose. The clock does not
337
+ * run until then, so the panel never calls that stop an outage.
338
+ */
339
+ pausedUntil?: number | null;
298
340
  }
299
341
  /**
300
342
  * Conveyor pods running here, across every namespace it manages — or null when
@@ -313,6 +355,78 @@ interface TelemetryPorts {
313
355
  declare function countRunningConveyorPods(exec: Executor): Promise<number | null>;
314
356
  declare function collectHeartbeat(ports: TelemetryPorts, state: TelemetryState, inventory: ImageInventory | null): Promise<PersonalClusterHeartbeat>;
315
357
 
358
+ /**
359
+ * Converge the machine onto Conveyor's desired state.
360
+ *
361
+ * This is what "manages itself" means in code. Conveyor publishes what the
362
+ * cluster SHOULD look like; this function makes it so, and it runs on connect,
363
+ * on request, and daily. Nothing here is a command from the server — a machine
364
+ * that was powered off for a week catches up on its next run, and a practice
365
+ * Conveyor changes reaches every enrolled box without anyone touching one.
366
+ *
367
+ * Three properties every step keeps:
368
+ *
369
+ * 1. **Idempotent.** Running twice changes nothing the first run already did.
370
+ * 2. **Independently fallible.** A step that fails records a warning and the
371
+ * rest still run. A cluster with one bad namespace is not a reason to skip
372
+ * image hygiene.
373
+ * 3. **Additive.** Nothing here deletes a namespace, a workload, or an image
374
+ * that is in use. The machine belongs to its owner. The one delete is an
375
+ * EMPTY pool namespace of a project the machine no longer serves, under
376
+ * local bounds the server cannot widen — see `./prune-namespaces.js`.
377
+ * 4. **Bounded.** The server does not get to say what runs here. The one field
378
+ * it sends as raw YAML is checked against a local kind allowlist before it
379
+ * reaches `kubectl` — see `../lib/manifest-guard.js`.
380
+ */
381
+
382
+ /** The filesystem operations reconcile needs, injectable for tests. */
383
+ interface FileSystemPort {
384
+ readFile(path: string): Promise<string | null>;
385
+ writeFile(path: string, content: string, mode?: number): Promise<void>;
386
+ removeFile(path: string): Promise<void>;
387
+ ensureDir(path: string, mode?: number): Promise<void>;
388
+ }
389
+ interface ReconcileContext {
390
+ exec: Executor;
391
+ fs: FileSystemPort;
392
+ /**
393
+ * Conveyor pods currently running here, or null when that could not be
394
+ * determined. A disruptive step is unsafe above zero AND unsafe on null —
395
+ * "I could not ask" is not "nothing is running".
396
+ */
397
+ runningConveyorPods: number | null;
398
+ }
399
+ interface ReconcileResult {
400
+ appliedVersion: number;
401
+ warnings: string[];
402
+ /**
403
+ * True when this pass restarted k3s: for new kubelet settings, or to load
404
+ * the local image registry's mirror. At most one restart per pass.
405
+ */
406
+ restartedK3s: boolean;
407
+ /** Kubelet flags left to the owner because their own config sets them. */
408
+ skippedKubeletFlags: string[];
409
+ /**
410
+ * The image-GC high threshold the kubelet is actually enforcing — ours, or
411
+ * the owner's when their own config.yaml set it and the drop-in stood down.
412
+ * Null when neither side named one. Reported so the heartbeat can warn about
413
+ * GC pressure against the number in force rather than the one we sent.
414
+ */
415
+ effectiveImageGcHighThreshold: number | null;
416
+ /** True when a wrong-valued PriorityClass was deleted AND put back. */
417
+ recreatedPriorityClass: boolean;
418
+ /** Empty pool namespaces of projects this machine no longer serves, removed. */
419
+ prunedNamespaces: string[];
420
+ /**
421
+ * Whether pod isolation is in force, while desired state asks for it (a
422
+ * host-runtime machine). Undefined otherwise. Reported on every heartbeat.
423
+ */
424
+ isolation?: {
425
+ applied: boolean;
426
+ };
427
+ }
428
+ declare function reconcile(ctx: ReconcileContext, desired: PersonalClusterDesiredState): Promise<ReconcileResult>;
429
+
316
430
  interface RegistryAuth {
317
431
  username?: string;
318
432
  password?: string;
@@ -434,7 +548,11 @@ declare function ownerConfiguredKeys(ownerConfig: string | null): Set<string>;
434
548
  * they chose, and last-writer-wins on their own file is exactly what this
435
549
  * module exists not to do.
436
550
  */
437
- declare function renderK3sDropIn(kubelet: PersonalClusterKubeletState, ownerConfig: string | null): DropInRenderResult;
551
+ declare function renderK3sDropIn(kubelet: PersonalClusterKubeletState, ownerConfig: string | null,
552
+ /** False on an adopted k3s: its components stay as its owner set them. */
553
+ options?: {
554
+ disableComponents?: boolean;
555
+ }): DropInRenderResult;
438
556
  /**
439
557
  * @deprecated Use {@link renderK3sDropIn}. Kept as a named alias because the
440
558
  * package exports it and a published CLI may still import it by this name.
@@ -491,6 +609,27 @@ interface MachineState {
491
609
  * wanted" is not a safe rule to unpin on. Only what we pinned, we unpin.
492
610
  */
493
611
  pinnedImages?: string[];
612
+ /** When the in-cluster registry's garbage was last collected. */
613
+ lastRegistryGcAt?: string | null;
614
+ /**
615
+ * The size of k3s's containerd store at the last measurement, and when that
616
+ * was. Kept so the heartbeat reports it between the hourly measurements,
617
+ * and a daemon restart does not walk the store again straight away.
618
+ */
619
+ k3sStoreBytes?: number;
620
+ k3sStoreMeasuredAt?: string;
621
+ /**
622
+ * "host" once `conveyor-k3 host setup` or `host link` enrolled this machine
623
+ * as a host-runtime instance. Absent on a VM guest, including every state
624
+ * file written before the host runtime existed.
625
+ */
626
+ runtime?: "host";
627
+ /**
628
+ * True once setup installed this machine's k3s. Absent on a k3s setup found
629
+ * already installed, and in every state file written before the field
630
+ * existed. See {@link managesK3s}.
631
+ */
632
+ k3sInstalledByConveyor?: boolean;
494
633
  }
495
634
  declare function readState(fs: FileSystemPort$1): Promise<MachineState>;
496
635
  declare function writeState(fs: FileSystemPort$1, state: MachineState): Promise<void>;
@@ -503,7 +642,7 @@ declare function writeState(fs: FileSystemPort$1, state: MachineState): Promise<
503
642
  * help output below is part of the product: every verb states what it does to
504
643
  * their machine, and `uninstall` states what it does not.
505
644
  */
506
- declare const VERBS: readonly ["vm", "setup", "start", "stop", "status", "sync", "update", "registry", "runners", "uninstall", "daemon"];
645
+ declare const VERBS: readonly ["vm", "host", "setup", "start", "stop", "status", "sync", "update", "registry", "runners", "uninstall", "daemon"];
507
646
  type Verb = (typeof VERBS)[number];
508
647
  declare function isVerb(value: string): value is Verb;
509
648
  interface ParsedArgs {
@@ -519,15 +658,17 @@ interface ParsedArgs {
519
658
  * this surface is nine verbs and six flags, not a general CLI framework.
520
659
  */
521
660
  declare function parseArgs(argv: readonly string[]): ParsedArgs;
522
- declare const HELP = "conveyor-k3 \u2014 managed personal compute for Conveyor\n\nUsage: conveyor-k3 <command> [options]\n\nCommands:\n vm Manage isolated compute on macOS or Linux.\n Run conveyor-k3 vm for host lifecycle commands.\n setup Install and enrol this machine. Safe to re-run.\n start Start the tunnel and the daemon.\n stop Stop both, and tell Conveyor at once so your next\n build goes straight to project compute.\n status [--json] What is running, what Conveyor knows, and what is\n missing.\n sync Re-read your settings from Conveyor and apply them.\n update Update conveyor-k3, cloudflared, and (when you have\n allowed it and nothing is running) k3s.\n registry list Which registry credentials are configured, and which\n ones your projects still need.\n registry add <host> Add one registry credential.\n --username <user>\n --password-stdin read the password from stdin\n --no-restart skip the required k3s restart\n runners list Which GitHub Actions runners this machine hosts, and\n whether each may import a locally baked image.\n runners grant <user>\u2026 Let those runner users import a locally baked image\n into this node and pin it. Writes one file under\n /etc/sudoers.d, checked with visudo first. Needed only\n when a project has Bake Locally turned on.\n uninstall Remove Conveyor's units, environment, and state. Your\n k3s config, registries.yaml, and workloads are left\n alone.\n --purge-k3s --yes also remove k3s itself\n daemon Run in the foreground (what the systemd unit calls).\n\nOptions:\n --version Print the version.\n --help Print this message.\n\nDocs: https://conveyor.rallycryapp.com/docs/guides/managed-personal-compute\n";
661
+ declare const HELP = "conveyor-k3 \u2014 Personal Compute for Conveyor\n\nUsage: conveyor-k3 <command> [options]\n\nOne product, two runtimes. Pick the one that fits the machine:\n\nOn your computer or server:\n vm Personal computer runtime: an isolated VM on macOS or\n Linux. Run conveyor-k3 vm for its commands.\n host Dedicated server runtime: k3s and the daemon on this\n Linux machine, with no VM. Root only. Run\n conveyor-k3 host for its commands.\n\nInside a linked machine (the VM guest, or the server itself):\n setup VM guest only: install and enrol this machine. Safe\n to re-run. On a server, use conveyor-k3 host setup.\n start Start the tunnel and the daemon.\n stop Stop both, and tell Conveyor at once so your next\n build goes straight to project compute.\n status [--json] What is running, what Conveyor knows, and what is\n missing.\n sync Re-read your settings from Conveyor and apply them.\n update Update conveyor-k3, cloudflared, and (when you have\n allowed it and nothing is running) k3s.\n registry list Which registry credentials are configured, and which\n ones your projects still need.\n registry add <host> Add one registry credential.\n --username <user>\n --password-stdin read the password from stdin\n --no-restart skip the required k3s restart\n registry gc Delete superseded shadow-bake images from the image\n registry inside this cluster. Runs daily on its own,\n and only while no Conveyor pod or bake is running.\n --dry-run list what it would delete\n runners list Which GitHub Actions runners this machine hosts, and\n whether each may import a locally baked image.\n runners grant <user>\u2026 Let those runner users import a locally baked image\n into this node and pin it. Writes one file under\n /etc/sudoers.d, checked with visudo first. Needed only\n when a project has Bake Locally turned on.\n uninstall VM guest only: remove Conveyor's units, environment,\n state, and image registry. Your k3s config, your own\n registries.yaml entries, your workloads, and the\n registry's data are left alone. On a server, use\n conveyor-k3 host uninstall: it also removes the\n device key.\n --purge-k3s --yes also remove k3s itself and the\n registry's data\n daemon Run in the foreground (what the systemd unit calls).\n\nOptions:\n --version Print the version.\n --help Print this message.\n\nDocs: https://conveyor.rallycryapp.com/docs/guides/managed-personal-compute\n";
523
662
 
524
663
  /**
525
- * `conveyor-k3 registry add|list` — the one place this CLI writes
526
- * `registries.yaml`.
527
- *
528
- * The reconcile loop never touches that file. Only an explicit command does,
529
- * it takes a timestamped backup first, and it merges rather than templates —
530
- * so a machine pulling through its own LAN mirror keeps that mirror, its TLS
664
+ * `conveyor-k3 registry add|list|gc` — where an owner adds registry
665
+ * credentials, and cleans up the registry inside the cluster.
666
+ *
667
+ * `add` is the one command that writes a credential into `registries.yaml`.
668
+ * The reconcile loop writes that file for one reason only: the mirror for the
669
+ * in-cluster registry's host (`lib/registries.ts` lists both writers). Both
670
+ * take a timestamped backup first, and both merge rather than template — so a
671
+ * machine pulling through its own LAN mirror keeps that mirror, its TLS
531
672
  * settings, and every other host it had configured.
532
673
  *
533
674
  * A restart is required and is not optional: k3s reads `registries.yaml` at
@@ -568,23 +709,12 @@ declare function registryList(ports: RegistryPorts, requiredHosts: string[]): Pr
568
709
  */
569
710
  declare function renderRegistryAuthSummary(missing: string[]): string[];
570
711
 
571
- /**
572
- * `conveyor-k3 uninstall` — remove exactly what Conveyor put on this machine.
573
- *
574
- * Everything this CLI created carries a `conveyor-k3` name and lives under
575
- * `/etc/conveyor` or `/var/lib/conveyor-k3`, which is what makes a precise
576
- * removal possible. Nothing else is touched: not the owner's `config.yaml`,
577
- * not `registries.yaml`, not their workloads.
578
- *
579
- * k3s itself is NOT removed by default. On most machines it predates us, or
580
- * the owner wants to keep it — so removing it is `--purge-k3s`, and even then
581
- * only after an explicit confirmation.
582
- */
583
-
584
712
  interface UninstallPorts {
585
713
  exec: Executor;
586
714
  fs: FileSystemPort$1;
587
715
  log: (line: string) => void;
716
+ /** Injected so the `registries.yaml` backup's name is deterministic in tests. */
717
+ now?: () => Date;
588
718
  }
589
719
  interface UninstallOptions {
590
720
  /** Also run k3s-uninstall.sh. Requires `confirmed`. */
@@ -644,6 +774,12 @@ interface UpdateBinaries {
644
774
  }
645
775
  interface UpdateDecisionInput {
646
776
  autoUpgradeK3s: boolean;
777
+ /**
778
+ * False on a server whose k3s setup did not install (`managesK3s`): the
779
+ * installer would turn off the owner's traefik and servicelb. Absent reads
780
+ * as true, which is every VM guest.
781
+ */
782
+ managesK3s?: boolean;
647
783
  /** Null when the count could not be read — treated exactly like "busy". */
648
784
  runningConveyorPods: number | null;
649
785
  installedK3s: [number, number] | null;
@@ -694,6 +830,27 @@ declare function runUpdate(ports: UpdatePorts, decision: K3sUpgradeDecision, cha
694
830
  declare function parseK3sVersion(output: string): [number, number] | null;
695
831
  declare function meetsK3sMinimum(version: [number, number] | null): boolean;
696
832
 
833
+ /**
834
+ * Capacity profiles: how many guest connections a VM's host relays at once, how
835
+ * much one connection may carry, and how fast the boot disk may go.
836
+ *
837
+ * Pure and import-free on purpose: the QEMU wrapper bundle (dist/vm-qemu.js)
838
+ * includes it, and that bundle starts once for every guest connection.
839
+ */
840
+ /** `custom` is any set of values that no longer matches a preset. */
841
+ type CapacityProfile = "default" | "server" | "custom";
842
+ /** What the host enforces, in bytes, rendered into network.json as `limits`. */
843
+ interface CapacityLimits {
844
+ schema: 1;
845
+ profile: CapacityProfile;
846
+ relaySlots: number;
847
+ /** Slots that only control-plane relays (the tunnel and the daemon) may take. */
848
+ reservedRelaySlots: number;
849
+ relayMaxBytes: number;
850
+ diskBytesPerSecond: number;
851
+ diskIops: number;
852
+ }
853
+
697
854
  interface NetworkEndpoint {
698
855
  host: string;
699
856
  ports: number[];
@@ -706,6 +863,8 @@ interface ResolvedNetworkHost {
706
863
  /** Host-resolved, pinned, globally-routable IPv4 address. */
707
864
  address: string;
708
865
  ports: number[];
866
+ /** A control-plane destination, whose relays may use the reserved slots. */
867
+ reserve?: "control";
709
868
  }
710
869
  interface GuestSrvRecord {
711
870
  name: string;
@@ -719,6 +878,13 @@ interface ResolvedNetwork {
719
878
  hosts: ResolvedNetworkHost[];
720
879
  /** Static SRV answers needed by cloudflared's documented edge discovery. */
721
880
  srv: GuestSrvRecord[];
881
+ /**
882
+ * Daemon-only aliases of the Conveyor API. Guest DNS never serves them, so
883
+ * only the daemon, which dials them by address, can use the reserved slots.
884
+ */
885
+ control?: ResolvedNetworkHost[];
886
+ /** What the host enforces. Absent in a document an older CLI wrote. */
887
+ limits?: CapacityLimits;
722
888
  }
723
889
  /** Alias used by the VM host manager. */
724
890
  type NetworkPolicy = ResolvedNetwork;
@@ -776,11 +942,72 @@ interface QemuNetworkPolicy extends ResolvedNetwork {
776
942
  /** Host-only information used to tie QEMU file endpoints to one Lima instance. */
777
943
  interface QemuPolicyContext {
778
944
  instanceDir?: string;
945
+ /** The Lima instance name, which Lima passes as `-name lima-<name>`. */
946
+ vmName?: string;
947
+ /** The boot-disk throttle, from the network policy's capacity limits. */
948
+ disk?: {
949
+ bps: number;
950
+ iops: number;
951
+ };
779
952
  }
780
- /** Rewrites only a complete, known Lima QEMU profile; every unknown input fails closed. */
781
- declare function rewriteQemuArgs(args: string[], network: QemuNetworkPolicy, ncPath?: string, context?: QemuPolicyContext): string[];
953
+ /**
954
+ * Rewrites only a complete, known Lima QEMU profile; every unknown input fails
955
+ * closed. The network's capacity limits set the boot-disk throttle.
956
+ */
957
+ declare function rewriteQemuArgs(args: string[], network: QemuNetworkPolicy, ncPath?: string, hostContext?: QemuPolicyContext): string[];
782
958
 
959
+ /** The largest whole-GiB guest whose host cost (see vmHostMemoryGiB) fits in the budget. */
960
+ declare function maximumGuestMemoryGiBWithin(budgetGiB: number, relaySlots?: number): number;
783
961
  /** Reserve 25% of physical host RAM after accounting for VM overhead. */
784
- declare function maximumGuestMemoryGiB(hostBytes: number): number;
962
+ declare function maximumGuestMemoryGiB(hostBytes: number, relaySlots?: number): number;
963
+
964
+ /** `--instance` omitted or `default`: the original VM, whose paths never move. */
965
+ declare const DEFAULT_INSTANCE = "default";
966
+ /** The `--instance` flag's value. Omitted or `default` selects the default VM. */
967
+ declare function parseInstanceName(value: unknown): string;
968
+
969
+ /** One VM's host paths. The default VM keeps its original root. */
970
+ declare function hostPaths(instance?: string): {
971
+ root: string;
972
+ limaHome: string;
973
+ state: string;
974
+ network: string;
975
+ template: string;
976
+ wrapper: string;
977
+ wrapperJs: string;
978
+ applied: string;
979
+ guard: string;
980
+ startRequest: string;
981
+ policyTransaction: string;
982
+ lock: string;
983
+ /** Written by an owner stop, so a supervising service does not restart the VM. */
984
+ stopIntent: string;
985
+ instance: string;
986
+ vmName: string;
987
+ instanceDir: string;
988
+ limaConfig: string;
989
+ };
990
+ /** Every host path of one VM under `root`. Pure, so tests inject a temporary root. */
991
+ declare function pathsForRoot(root: string, instance?: string): {
992
+ root: string;
993
+ limaHome: string;
994
+ state: string;
995
+ network: string;
996
+ template: string;
997
+ wrapper: string;
998
+ wrapperJs: string;
999
+ applied: string;
1000
+ guard: string;
1001
+ startRequest: string;
1002
+ policyTransaction: string;
1003
+ lock: string;
1004
+ /** Written by an owner stop, so a supervising service does not restart the VM. */
1005
+ stopIntent: string;
1006
+ instance: string;
1007
+ vmName: string;
1008
+ instanceDir: string;
1009
+ limaConfig: string;
1010
+ };
1011
+ type HostPaths = ReturnType<typeof pathsForRoot>;
785
1012
 
786
- export { ClusterConnection, type ClusterConnectionConfig, ClusterDaemon, CommandError, type CrictlImage, type DaemonPorts, type Executor, type FileSystemPort$1 as FileSystemPort, type GuestDnsProvision, type GuestSrvRecord, HELP, type ImageInventory, K3S_DISABLED_COMPONENTS, K3S_INSTALL_EXEC, type K3sUpgradeDecision, type MachineState, type NetworkEndpoint, type NetworkPolicy, type QemuNetworkPolicy, type ReconcileContext, type ReconcileResult, type RegistriesFile, type ResolvedNetwork, type ResolvedNetworkHost, type SRVLookup, type UninstallResult, type UnitRenderOptions, VERBS, type Verb, cloudflaredSrvRecords, collectHeartbeat, configuredAuthHosts, countRunningConveyorPods, createMemoryFileSystem, decideK3sUpgrade, isGloballyRoutableIPv4, isVerb, maximumGuestMemoryGiB, meetsK3sMinimum, missingAuthHosts, nodeFileSystem, ownerConfiguredKeys, ownerConfiguredKubeletFlags, parseArgs, parseK3sVersion, parseRegistries, pruneStrandedImages, readImageInventory, readState, reconcile, registryAdd, registryList, renderAllUnits, renderGuestDnsProvision, renderK3sDropIn, renderKubeletDropIn, renderRegistryAuthSummary, resolveCloudflaredSrvRecords, resolveNetwork, rewriteQemuArgs, runOrThrow, runUpdate, selectStrandedImages, serializeRegistries, spawnExecutor, uninstall, upsertRegistryAuth, warmImages, writeState };
1013
+ export { ClusterConnection, type ClusterConnectionConfig, ClusterDaemon, CommandError, type CrictlImage, DEFAULT_INSTANCE, type DaemonPorts, type Executor, type FileSystemPort$1 as FileSystemPort, type GuestDnsProvision, type GuestSrvRecord, HELP, type HostPaths, type ImageInventory, K3S_DISABLED_COMPONENTS, K3S_INSTALL_EXEC, type K3sUpgradeDecision, type MachineState, type NetworkEndpoint, type NetworkPolicy, type QemuNetworkPolicy, type ReconcileContext, type ReconcileResult, type RegistriesFile, type ResolvedNetwork, type ResolvedNetworkHost, type SRVLookup, type UninstallResult, type UnitRenderOptions, VERBS, type Verb, cloudflaredSrvRecords, collectHeartbeat, configuredAuthHosts, countRunningConveyorPods, createMemoryFileSystem, decideK3sUpgrade, hostPaths, isGloballyRoutableIPv4, isVerb, maximumGuestMemoryGiB, maximumGuestMemoryGiBWithin, meetsK3sMinimum, missingAuthHosts, nodeFileSystem, ownerConfiguredKeys, ownerConfiguredKubeletFlags, parseArgs, parseInstanceName, parseK3sVersion, parseRegistries, pruneStrandedImages, readImageInventory, readState, reconcile, registryAdd, registryList, renderAllUnits, renderGuestDnsProvision, renderK3sDropIn, renderKubeletDropIn, renderRegistryAuthSummary, resolveCloudflaredSrvRecords, resolveNetwork, rewriteQemuArgs, runOrThrow, runUpdate, selectStrandedImages, serializeRegistries, spawnExecutor, uninstall, upsertRegistryAuth, warmImages, writeState };