@kilogent/runner 0.1.6 → 0.1.8

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.
@@ -0,0 +1,877 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/provision/root.ts
4
+ import { spawnSync } from "node:child_process";
5
+ import { createHash } from "node:crypto";
6
+ import fs3 from "node:fs";
7
+ import os2 from "node:os";
8
+ import path2 from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+
11
+ // src/config.ts
12
+ import fs from "node:fs";
13
+ import os from "node:os";
14
+ import path from "node:path";
15
+
16
+ // src/version.ts
17
+ var RUNNER_PACKAGE = typeof __RUNNER_PACKAGE__ === "string" ? __RUNNER_PACKAGE__ : "@kilogent/runner-dev";
18
+ var RUNNER_BIN = typeof __RUNNER_BIN__ === "string" ? __RUNNER_BIN__ : "kilogent-runner-dev";
19
+
20
+ // src/config.ts
21
+ var IS_DEV_BUILD = RUNNER_PACKAGE.endsWith("-dev");
22
+ var LEGACY_DIR_NAME = ".crew-runner";
23
+ var DIR_NAME = `.${RUNNER_BIN}`;
24
+ var LUMI_DIR_NAME = ".lumi-runner";
25
+ var FALLBACK_DIR_NAMES = [LUMI_DIR_NAME, LEGACY_DIR_NAME];
26
+ function configDir() {
27
+ const override = process.env.LUMI_RUNNER_HOME || process.env.CREW_RUNNER_HOME;
28
+ if (override) return override;
29
+ const home = os.homedir();
30
+ const current = path.join(home, DIR_NAME);
31
+ if (fs.existsSync(current)) return current;
32
+ for (const name of FALLBACK_DIR_NAMES) {
33
+ const older = path.join(home, name);
34
+ if (fs.existsSync(older)) return older;
35
+ }
36
+ return current;
37
+ }
38
+ function fleetStatePath() {
39
+ return path.join(configDir(), "fleet-state.json");
40
+ }
41
+
42
+ // src/fleet/drain.ts
43
+ var DRAIN_REQUEST_FILE = "drain-request.json";
44
+ var DRAIN_REQUEST_MAX_MS = 2 * 60 * 6e4;
45
+ function drainProgress(state, input) {
46
+ if (!state || input.now - state.updatedAt > input.staleMs) return { state: "no-daemon" };
47
+ if (state.drainRequestAt !== input.requestedAt) return { state: "waiting", running: state.running.length };
48
+ return state.running.length === 0 ? { state: "drained" } : { state: "waiting", running: state.running.length };
49
+ }
50
+
51
+ // src/fleet/state.ts
52
+ import fs2 from "node:fs";
53
+ function readFleetState(file = fleetStatePath()) {
54
+ try {
55
+ const parsed = JSON.parse(fs2.readFileSync(file, "utf8"));
56
+ if (parsed?.version !== 1 || typeof parsed.serverId !== "string") return null;
57
+ return {
58
+ ...parsed,
59
+ running: Array.isArray(parsed.running) ? parsed.running : [],
60
+ engines: Array.isArray(parsed.engines) ? parsed.engines : []
61
+ };
62
+ } catch {
63
+ return null;
64
+ }
65
+ }
66
+ var FLEET_STATE_STALE_MS = 2 * 6e4;
67
+
68
+ // src/jobs/capacity.ts
69
+ var HOST_RESERVE_MB = 4096;
70
+
71
+ // src/launcher/memoryWatch.ts
72
+ var GROW_PLAN_BOUNDS = Object.freeze({
73
+ startMb: [256, 4096],
74
+ stepMb: [50, 1024],
75
+ maxMb: [256, 4096],
76
+ growAtPct: [50, 95],
77
+ pollMs: [100, 5e3]
78
+ });
79
+
80
+ // src/launcher/protocol.ts
81
+ var LAUNCHER_PATH = "/usr/local/lib/kilogent/kg-session-launcher.mjs";
82
+ var LAUNCHER_SETTINGS_PATH = "/etc/kilogent/launcher.json";
83
+ var MAX_REQUEST_BYTES = 1024 * 1024;
84
+ var SESSION_ENV_NAMES = Object.freeze([
85
+ "ANTHROPIC_BASE_URL",
86
+ "ANTHROPIC_AUTH_TOKEN",
87
+ "ANTHROPIC_API_KEY",
88
+ "CLAUDE_CODE_OAUTH_TOKEN",
89
+ "OPENAI_API_KEY",
90
+ "OPENAI_BASE_URL",
91
+ "GOOGLE_API_KEY",
92
+ "GH_TOKEN",
93
+ "GITHUB_TOKEN",
94
+ "GIT_CONFIG_COUNT",
95
+ "GIT_CONFIG_KEY_0",
96
+ "GIT_CONFIG_VALUE_0",
97
+ "GIT_CONFIG_KEY_1",
98
+ "GIT_CONFIG_VALUE_1",
99
+ "GIT_CONFIG_KEY_2",
100
+ "GIT_CONFIG_VALUE_2",
101
+ "CREW_JOB_ID",
102
+ "CREW_MODEL_INPUT"
103
+ ]);
104
+ var SESSION_ENV_SINCE = Object.freeze({ CREW_MODEL_INPUT: 1 });
105
+ var MAX_ARG_BYTES = 256 * 1024;
106
+
107
+ // src/provision/host.ts
108
+ function parseOsRelease(text) {
109
+ const values = {};
110
+ for (const line of text.split("\n")) {
111
+ const match = /^([A-Z_]+)=("?)(.*)\2$/.exec(line.trim());
112
+ if (match) values[match[1]] = match[3];
113
+ }
114
+ return { id: values.ID ?? "", versionId: values.VERSION_ID ?? "", codename: values.VERSION_CODENAME ?? "" };
115
+ }
116
+ var SUPPORTED_HOSTS = Object.freeze([
117
+ { id: "ubuntu", versionId: "22.04", codename: "jammy" },
118
+ { id: "ubuntu", versionId: "24.04", codename: "noble" },
119
+ { id: "debian", versionId: "12", codename: "bookworm" }
120
+ ]);
121
+ function supportedHost(os3) {
122
+ return SUPPORTED_HOSTS.some((h) => h.id === os3.id && h.versionId === os3.versionId && h.codename === os3.codename);
123
+ }
124
+ function unsupportedHostSentence(os3) {
125
+ const names = SUPPORTED_HOSTS.map((h) => `${h.id === "ubuntu" ? "Ubuntu" : "Debian"} ${h.versionId}`).join(", ");
126
+ return `This is ${os3.id || "an unknown system"} ${os3.versionId}; provision installs on ${names}. With Docker Engine and gVisor (runsc) installed by hand, it checks them instead.`;
127
+ }
128
+ var RUNSC_PATH = "/usr/bin/runsc";
129
+ function mergeDaemonJson(existing) {
130
+ let doc = {};
131
+ if (existing && existing.trim()) {
132
+ try {
133
+ const parsed = JSON.parse(existing);
134
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return { ok: false, reason: "/etc/docker/daemon.json is not a JSON object." };
135
+ doc = parsed;
136
+ } catch {
137
+ return { ok: false, reason: "/etc/docker/daemon.json is not valid JSON. Fix it by hand, then run provision again." };
138
+ }
139
+ }
140
+ const runtimes = doc.runtimes && typeof doc.runtimes === "object" ? doc.runtimes : {};
141
+ const runsc = runtimes.runsc;
142
+ if (runsc && runsc.path !== RUNSC_PATH) {
143
+ return { ok: false, reason: `daemon.json already registers runsc at ${String(runsc.path)}. Provision expects ${RUNSC_PATH}.` };
144
+ }
145
+ if ("live-restore" in doc && doc["live-restore"] !== true) {
146
+ return { ok: false, reason: "daemon.json sets live-restore to false. Provision needs it on; change it by hand if that is safe here." };
147
+ }
148
+ const changed = !runsc || doc["live-restore"] !== true;
149
+ const next = { ...doc, runtimes: { ...runtimes, runsc: { path: RUNSC_PATH } }, "live-restore": true };
150
+ return { ok: true, changed, text: `${JSON.stringify(next, null, 2)}
151
+ ` };
152
+ }
153
+ var SESSION_BRIDGE = "kg-sessions";
154
+ var FIREWALL_CHAIN = "KG-SESSIONS";
155
+ var BLOCKED_RANGES = Object.freeze(["169.254.0.0/16", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "100.64.0.0/10"]);
156
+ function firewallRules(input) {
157
+ const bridge = input.bridge ?? SESSION_BRIDGE;
158
+ const resolvers = [...new Set(input.resolvers.filter((r) => /^\d{1,3}(\.\d{1,3}){3}$/.test(r)))];
159
+ const lines = ["*filter", `:${FIREWALL_CHAIN} - [0:0]`, `-F ${FIREWALL_CHAIN}`];
160
+ lines.push(`-A ${FIREWALL_CHAIN} -m conntrack --ctstate ESTABLISHED,RELATED -j RETURN`);
161
+ for (const r of resolvers) {
162
+ lines.push(`-A ${FIREWALL_CHAIN} -i ${bridge} -d ${r}/32 -p udp --dport 53 -j RETURN`);
163
+ lines.push(`-A ${FIREWALL_CHAIN} -i ${bridge} -d ${r}/32 -p tcp --dport 53 -j RETURN`);
164
+ }
165
+ for (const range of BLOCKED_RANGES) lines.push(`-A ${FIREWALL_CHAIN} -i ${bridge} -d ${range} -j DROP`);
166
+ lines.push(`-A ${FIREWALL_CHAIN} -i ${bridge} -j RETURN`);
167
+ lines.push("COMMIT");
168
+ return `${lines.join("\n")}
169
+ `;
170
+ }
171
+ function firewallJumps(bridge = SESSION_BRIDGE) {
172
+ return [
173
+ { chain: "DOCKER-USER", rule: ["-i", bridge, "-j", FIREWALL_CHAIN] },
174
+ { chain: "INPUT", rule: ["-i", bridge, "-j", "DROP"] }
175
+ ];
176
+ }
177
+ function firewallUnit(rulesPath, bridge = SESSION_BRIDGE) {
178
+ const ensure = firewallJumps(bridge).map(({ chain, rule }) => `ExecStart=/bin/sh -c 'iptables -C ${chain} ${rule.join(" ")} 2>/dev/null || iptables -I ${chain} 1 ${rule.join(" ")}'`).join("\n");
179
+ return `[Unit]
180
+ Description=Kilogent session firewall
181
+ After=docker.service
182
+ PartOf=docker.service
183
+
184
+ [Service]
185
+ Type=oneshot
186
+ RemainAfterExit=yes
187
+ ExecStart=/usr/sbin/iptables-restore --noflush ${rulesPath}
188
+ ${ensure}
189
+
190
+ [Install]
191
+ WantedBy=docker.service
192
+ `;
193
+ }
194
+ function resolversFrom(resolvConf) {
195
+ return resolvConf.split("\n").map((l) => /^\s*nameserver\s+(\d{1,3}(?:\.\d{1,3}){3})\s*$/.exec(l)?.[1]).filter((r) => Boolean(r) && !r.startsWith("127."));
196
+ }
197
+ function sudoersLine(user, launcher) {
198
+ if (!/^[a-z_][a-z0-9_-]{0,31}$/.test(user)) throw new Error("Not a user name.");
199
+ if (!/^\/[A-Za-z0-9._/-]+$/.test(launcher)) throw new Error("Not an absolute path.");
200
+ return `${user} ALL=(root) NOPASSWD: ${launcher} ""
201
+ `;
202
+ }
203
+
204
+ // src/provision/plan.ts
205
+ var MIN_PROVISION_NODE_MAJOR = 18;
206
+ var SERVICE_NODE_MAJOR = 22;
207
+ var MIN_PROVISION_DISK_BYTES = 10 * 1024 ** 3;
208
+ var OWNER_USER = "kilogent";
209
+ var SESSION_USER = "kilogent-s";
210
+ var SESSIONS_ROOT = "/var/lib/kilogent/sessions";
211
+ var HOST_STATE_PATH = "/etc/kilogent/host.json";
212
+ var SUDOERS_PATH = "/etc/sudoers.d/kilogent";
213
+ var FIREWALL_UNIT = "kg-sessions-firewall.service";
214
+ function versionAtLeast(have, want) {
215
+ const parse = (v) => (/^(\d+)\.(\d+)\.(\d+)/.exec(v) ?? []).slice(1).map(Number);
216
+ const a = parse(have);
217
+ const b = parse(want);
218
+ if (a.length !== 3 || b.length !== 3) return false;
219
+ for (let i = 0; i < 3; i += 1) {
220
+ if (a[i] !== b[i]) return a[i] > b[i];
221
+ }
222
+ return true;
223
+ }
224
+ function planLinuxProvision(facts, options) {
225
+ const refusal = preflightRefusal(facts);
226
+ if (refusal) return { refusal, steps: [] };
227
+ const steps = [{ id: "preflight", as: "root", action: "check", why: "The host can run a Kilogent server." }];
228
+ const supported = supportedHost(facts.osRelease);
229
+ const add = (id, as, action, why) => steps.push({ id, as, action, why });
230
+ add(
231
+ "users",
232
+ "root",
233
+ facts.users.owner && facts.users.session ? "check" : "do",
234
+ facts.users.owner && facts.users.session ? `${OWNER_USER} and ${SESSION_USER} exist.` : `Create ${[!facts.users.owner && OWNER_USER, !facts.users.session && SESSION_USER].filter(Boolean).join(" and ")}.`
235
+ );
236
+ add(
237
+ "node",
238
+ "root",
239
+ facts.systemNodeMajor !== null && facts.systemNodeMajor >= SERVICE_NODE_MAJOR ? "check" : "do",
240
+ facts.systemNodeMajor !== null && facts.systemNodeMajor >= SERVICE_NODE_MAJOR ? `Node ${facts.systemNodeMajor} is installed.` : `Install Node ${SERVICE_NODE_MAJOR} from NodeSource's signed repository.`
241
+ );
242
+ if (!options.hostOnly) {
243
+ const runnerCurrent = facts.runnerVersion !== null && versionAtLeast(facts.runnerVersion, options.version);
244
+ add(
245
+ "runner",
246
+ "owner",
247
+ runnerCurrent ? "skip" : "do",
248
+ runnerCurrent ? `The runner ${facts.runnerVersion} is installed, which is this release or newer.` : `Install the runner ${options.version} into ${OWNER_USER}'s own prefix.`
249
+ );
250
+ const reEnrol = facts.enrolled && options.reEnrol === true;
251
+ add(
252
+ "enrol",
253
+ "owner",
254
+ facts.enrolled && !reEnrol ? "skip" : "do",
255
+ !facts.enrolled ? "Enrol with the one-time code." : reEnrol ? "Enrol again with the new code, replacing this server's key." : "This server is enrolled already."
256
+ );
257
+ add("service", "owner", "do", "Install and start the service. It takes no work until the host is ready.");
258
+ }
259
+ if (facts.docker.present) {
260
+ add("docker", "root", "check", `Docker is installed (${facts.docker.source ?? "unknown source"}), and is used as it is.`);
261
+ } else {
262
+ add("docker", "root", supported ? "do" : "check", supported ? "Install Docker Engine from Docker's signed repository." : "Docker Engine must be installed by hand here.");
263
+ }
264
+ if (facts.runsc.present) add("gvisor", "root", "check", "gVisor (runsc) is installed.");
265
+ else add("gvisor", "root", supported ? "do" : "check", supported ? "Install gVisor from gVisor's signed repository." : "gVisor must be installed by hand here.");
266
+ const merged = mergeDaemonJson(facts.daemonJson);
267
+ if (!merged.ok) add("daemon-json", "root", "check", merged.reason);
268
+ else add("daemon-json", "root", merged.changed ? "do" : "check", merged.changed ? "Register runsc and turn live-restore on, then reload Docker (never restart it)." : "runsc is registered and live-restore is on.");
269
+ if (facts.network.exists && facts.network.iccDisabled === true) {
270
+ add("network", "root", "check", "The kg-sessions network exists, and its sessions cannot reach each other.");
271
+ } else if (facts.network.exists && facts.network.attachedContainers > 0) {
272
+ add("network", "root", "check", "kg-sessions lets sessions reach each other, and has containers on it, so it is not recreated now.");
273
+ } else {
274
+ add("network", "root", "do", facts.network.exists ? "Recreate kg-sessions so its sessions cannot reach each other." : "Create the kg-sessions network.");
275
+ }
276
+ add("firewall", "root", "do", "Apply the session firewall, and a unit that applies it again whenever Docker starts.");
277
+ add(
278
+ "launcher",
279
+ "root",
280
+ facts.launcherSha256 === options.launcherSha256 ? "check" : "do",
281
+ facts.launcherSha256 === options.launcherSha256 ? "The session launcher is this release's." : "Install this release's session launcher and its settings."
282
+ );
283
+ add("sudoers", "root", facts.sudoers === options.expectedSudoers ? "check" : "do", facts.sudoers === options.expectedSudoers ? "sudo allows the launcher, and nothing else." : "Allow the service user the launcher, and nothing else.");
284
+ add("hold", "root", "do", "Hold the packages provision installed, so an unattended upgrade cannot restart Docker under a session.");
285
+ if (!options.hostOnly) add("wait", "owner", "do", "Wait for the server to report ready.");
286
+ return { refusal: null, steps };
287
+ }
288
+ function preflightRefusal(f) {
289
+ if (f.nodeMajor < MIN_PROVISION_NODE_MAJOR) {
290
+ return `Provision needs Node ${MIN_PROVISION_NODE_MAJOR} or newer to start; this is Node ${f.nodeMajor}. Install a newer nodejs, then run it again.`;
291
+ }
292
+ if (!f.systemd) return "This host has no systemd, so a service cannot be installed. Provision runs on a VM or a bare machine, not in a container.";
293
+ if (!f.cgroupV2) return "This host does not use cgroup v2, which the session limits need.";
294
+ if (f.diskFreeBytes !== null && f.diskFreeBytes < MIN_PROVISION_DISK_BYTES) {
295
+ return `This host has ${(f.diskFreeBytes / 1024 ** 3).toFixed(1)} GB free; a server needs at least ${MIN_PROVISION_DISK_BYTES / 1024 ** 3} GB.`;
296
+ }
297
+ if (!supportedHost(f.osRelease) && (!f.docker.present || !f.runsc.present)) return unsupportedHostSentence(f.osRelease);
298
+ if (f.docker.present && !f.docker.answers) {
299
+ return "Docker is installed and does not answer. Provision starts a stopped Docker; one that will not start is broken, and `systemctl status docker` says why. Fix that, then run provision again.";
300
+ }
301
+ return null;
302
+ }
303
+ function provisionExitCode(results) {
304
+ const serviceIndex = results.findIndex((r) => r.id === "service");
305
+ const firstFailure = results.findIndex((r) => !r.ok);
306
+ if (firstFailure < 0) return 0;
307
+ if (serviceIndex >= 0 && results[serviceIndex].ok && firstFailure > serviceIndex) return 2;
308
+ return 1;
309
+ }
310
+
311
+ // src/provision/root.ts
312
+ var ROOT_ENV = { PATH: "/usr/sbin:/usr/bin:/sbin:/bin", LANG: "C.UTF-8", DEBIAN_FRONTEND: "noninteractive" };
313
+ var OWNER_HOME = `/home/${OWNER_USER}`;
314
+ var OWNER_PREFIX = `${OWNER_HOME}/.local`;
315
+ var FIREWALL_RULES_PATH = "/etc/kilogent/firewall.rules";
316
+ var KEYRINGS = "/etc/apt/keyrings";
317
+ var APT_KEY_FINGERPRINTS = {
318
+ docker: ["9DC858229FC7DD38854AE2D88D81803C0EBFCD88"],
319
+ nodesource: ["6F71F525282841EEDAF851B42F59B5F99B1BE0B4"],
320
+ gvisor: ["6F1DF85E3A71C24918E727D56FC6D554E32BD943"]
321
+ };
322
+ var StepFailure = class extends Error {
323
+ };
324
+ function say(line) {
325
+ process.stdout.write(`${line}
326
+ `);
327
+ }
328
+ function run(command, args, opts = {}) {
329
+ const res = spawnSync(command, [...args], { env: ROOT_ENV, encoding: "utf8", timeout: 15 * 6e4, ...opts });
330
+ return { ok: res.status === 0, out: String(res.stdout ?? ""), err: String(res.stderr ?? "") || (res.error ? res.error.message : "") };
331
+ }
332
+ function must(command, args, what, opts = {}) {
333
+ const res = run(command, args, opts);
334
+ if (!res.ok) throw new StepFailure(`${what} failed: ${(res.err || res.out).trim().split("\n").slice(-3).join(" ").slice(0, 300)}`);
335
+ return res.out;
336
+ }
337
+ function parseArgs(argv) {
338
+ const get = (flag) => {
339
+ const i = argv.indexOf(flag);
340
+ return i >= 0 ? argv[i + 1] ?? null : null;
341
+ };
342
+ return {
343
+ project: get("--project") ?? "",
344
+ apiKey: get("--api-key") ?? "",
345
+ codeStdin: argv.includes("--code-stdin"),
346
+ yes: argv.includes("--yes"),
347
+ hostOnly: argv.includes("--host-only"),
348
+ planOnly: argv.includes("--plan"),
349
+ upgradeHost: argv.some((a) => a === "--upgrade-host" || a.startsWith("--upgrade-host=")),
350
+ upgradeOnly: (argv.find((a) => a.startsWith("--upgrade-host="))?.slice("--upgrade-host=".length) ?? "").split(",").map((name) => name.trim()).filter((name) => name.length > 0),
351
+ now: argv.includes("--now"),
352
+ reEnrol: argv.includes("--re-enrol"),
353
+ tarball: get("--tarball")
354
+ };
355
+ }
356
+ function thisRelease() {
357
+ const root = path2.dirname(path2.dirname(fileURLToPath(import.meta.url)));
358
+ const pkg = JSON.parse(fs3.readFileSync(path2.join(root, "package.json"), "utf8"));
359
+ const launcher = fs3.readFileSync(path2.join(root, "dist/kg-session-launcher.mjs"));
360
+ const bin = Object.keys(pkg.bin ?? {})[0];
361
+ if (!bin) throw new Error(`${path2.join(root, "package.json")} names no command, so it is not a runner release.`);
362
+ return {
363
+ root,
364
+ name: pkg.name,
365
+ version: pkg.version,
366
+ bin,
367
+ sessionImage: pkg.kilogent?.sessionImage ?? "",
368
+ launcherSha256: createHash("sha256").update(launcher).digest("hex")
369
+ };
370
+ }
371
+ function readText(file) {
372
+ try {
373
+ return fs3.readFileSync(file, "utf8");
374
+ } catch {
375
+ return null;
376
+ }
377
+ }
378
+ function idOf(user, flag) {
379
+ const res = run("id", [flag, user]);
380
+ const n = Number(res.out.trim());
381
+ return res.ok && Number.isInteger(n) ? n : null;
382
+ }
383
+ function gatherFacts(release) {
384
+ const systemNode = run("/usr/bin/node", ["--version"]);
385
+ const dockerPresent = run("sh", ["-c", "command -v docker"]).ok;
386
+ const dockerAnswers = dockerPresent && run("docker", ["version", "--format", "{{.Server.Version}}"], { timeout: 2e4 }).ok;
387
+ const source = !dockerPresent ? null : fs3.existsSync("/snap/bin/docker") ? "snap" : run("dpkg-query", ["-W", "-f=${Status}", "docker-ce"]).out.includes("installed") ? "docker-ce" : run("dpkg-query", ["-W", "-f=${Status}", "docker.io"]).out.includes("installed") ? "docker.io" : "other";
388
+ const running = dockerAnswers ? run("docker", ["ps", "--quiet"]).out.split("\n").filter(Boolean).length : 0;
389
+ const network = dockerAnswers ? run("docker", ["network", "inspect", SESSION_BRIDGE, "--format", "{{json .Options}}|{{len .Containers}}"]) : null;
390
+ let iccDisabled = null;
391
+ let attached = 0;
392
+ if (network?.ok) {
393
+ const [options, count] = network.out.trim().split("|");
394
+ try {
395
+ iccDisabled = JSON.parse(options)?.["com.docker.network.bridge.enable_icc"] === "false";
396
+ } catch {
397
+ iccDisabled = null;
398
+ }
399
+ attached = Number(count) || 0;
400
+ }
401
+ const launcher = fs3.existsSync(LAUNCHER_PATH) ? createHash("sha256").update(fs3.readFileSync(LAUNCHER_PATH)).digest("hex") : null;
402
+ const installed = readText(path2.join(OWNER_PREFIX, "lib/node_modules", release.name, "package.json"));
403
+ const configDir2 = path2.join(OWNER_HOME, `.${release.bin}`);
404
+ let diskFreeBytes = null;
405
+ try {
406
+ const st = fs3.statfsSync(fs3.existsSync("/var/lib") ? "/var/lib" : "/");
407
+ diskFreeBytes = Number(st.bavail) * Number(st.bsize);
408
+ } catch {
409
+ diskFreeBytes = null;
410
+ }
411
+ return {
412
+ osRelease: parseOsRelease(readText("/etc/os-release") ?? ""),
413
+ nodeMajor: Number(process.versions.node.split(".")[0]),
414
+ systemNodeMajor: systemNode.ok ? Number(/^v(\d+)/.exec(systemNode.out.trim())?.[1] ?? NaN) || null : null,
415
+ systemd: fs3.existsSync("/run/systemd/system"),
416
+ cgroupV2: fs3.existsSync("/sys/fs/cgroup/cgroup.controllers"),
417
+ diskFreeBytes,
418
+ docker: { present: dockerPresent, answers: dockerAnswers, source, runningContainers: running },
419
+ runsc: { present: run("sh", ["-c", "command -v runsc"]).ok },
420
+ daemonJson: readText("/etc/docker/daemon.json"),
421
+ users: { owner: idOf(OWNER_USER, "-u") !== null, session: idOf(SESSION_USER, "-u") !== null },
422
+ network: { exists: Boolean(network?.ok), iccDisabled, attachedContainers: attached },
423
+ launcherSha256: launcher,
424
+ sudoers: readText(SUDOERS_PATH),
425
+ runnerVersion: installed ? JSON.parse(installed).version ?? null : null,
426
+ enrolled: fs3.existsSync(path2.join(configDir2, "fleet.key")) && fs3.existsSync(path2.join(configDir2, "config.json"))
427
+ };
428
+ }
429
+ var aptListsFresh = false;
430
+ function aptUpdateOnce() {
431
+ if (aptListsFresh) return;
432
+ must("apt-get", ["update"], "apt-get update");
433
+ aptListsFresh = true;
434
+ }
435
+ async function installAptKey(name, url) {
436
+ aptUpdateOnce();
437
+ must("apt-get", ["install", "-y", "--no-install-recommends", "ca-certificates", "gnupg"], "Installing gnupg");
438
+ const res = await fetch(url, { signal: AbortSignal.timeout(3e4) });
439
+ if (!res.ok) throw new StepFailure(`Could not fetch the ${name} signing key (${res.status}).`);
440
+ const tmp = path2.join(os2.tmpdir(), `kg-${name}-${process.pid}.key`);
441
+ fs3.writeFileSync(tmp, Buffer.from(await res.arrayBuffer()), { mode: 384 });
442
+ try {
443
+ const shown = must("gpg", ["--show-keys", "--with-colons", tmp], `Reading the ${name} signing key`);
444
+ const fingerprints = shown.split("\n").filter((l) => l.startsWith("fpr:")).map((l) => l.split(":")[9]).filter(Boolean);
445
+ const trusted = APT_KEY_FINGERPRINTS[name];
446
+ if (fingerprints.length === 0) throw new StepFailure(`The ${name} signing key has no fingerprint.`);
447
+ if (trusted.length > 0 && !fingerprints.some((f) => trusted.includes(f))) {
448
+ throw new StepFailure(`The ${name} signing key (${fingerprints[0]}) is not one this release trusts.`);
449
+ }
450
+ fs3.mkdirSync(KEYRINGS, { recursive: true, mode: 493 });
451
+ const keyring = path2.join(KEYRINGS, `${name}.gpg`);
452
+ must("gpg", ["--batch", "--yes", "--dearmor", "-o", keyring, tmp], `Installing the ${name} keyring`);
453
+ fs3.chmodSync(keyring, 420);
454
+ return { keyring, fingerprints };
455
+ } finally {
456
+ fs3.rmSync(tmp, { force: true });
457
+ }
458
+ }
459
+ function aptSource(file, line) {
460
+ fs3.writeFileSync(path2.join("/etc/apt/sources.list.d", file), `${line}
461
+ `, { mode: 420 });
462
+ must("apt-get", ["update"], "apt-get update");
463
+ }
464
+ function ownerEnv(uid) {
465
+ return [
466
+ `HOME=${OWNER_HOME}`,
467
+ `PATH=${OWNER_PREFIX}/bin:/usr/bin:/bin`,
468
+ `XDG_RUNTIME_DIR=/run/user/${uid}`,
469
+ `DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/${uid}/bus`
470
+ ];
471
+ }
472
+ function asOwner(args, what, opts = {}) {
473
+ const uid = idOf(OWNER_USER, "-u");
474
+ if (uid === null) throw new StepFailure(`${OWNER_USER} does not exist.`);
475
+ return must("runuser", ["-u", OWNER_USER, "--", "setsid", "/usr/bin/env", "-i", ...ownerEnv(uid), ...args], what, {
476
+ cwd: OWNER_HOME,
477
+ ...opts.input !== void 0 ? { input: opts.input } : {}
478
+ });
479
+ }
480
+ function ownerCli(release) {
481
+ return path2.join(OWNER_PREFIX, "lib/node_modules", release.name, "dist/cli.js");
482
+ }
483
+ async function runStep(step, ctx) {
484
+ const { release, options } = ctx;
485
+ if (step.action === "skip") return step.why;
486
+ switch (step.id) {
487
+ case "preflight":
488
+ return step.why;
489
+ case "users": {
490
+ if (step.action === "do") {
491
+ if (idOf(OWNER_USER, "-u") === null) {
492
+ must("useradd", ["--create-home", "--home-dir", OWNER_HOME, "--shell", "/usr/sbin/nologin", "--user-group", OWNER_USER], `Creating ${OWNER_USER}`);
493
+ }
494
+ if (idOf(SESSION_USER, "-u") === null) {
495
+ must("useradd", ["--system", "--no-create-home", "--home-dir", "/nonexistent", "--shell", "/usr/sbin/nologin", "--user-group", SESSION_USER], `Creating ${SESSION_USER}`);
496
+ }
497
+ }
498
+ const groups = run("id", ["-Gn", OWNER_USER]).out.split(/\s+/);
499
+ if (groups.includes("docker")) must("gpasswd", ["-d", OWNER_USER, "docker"], `Removing ${OWNER_USER} from the docker group`);
500
+ must("loginctl", ["enable-linger", OWNER_USER], "Keeping the service user\u2019s services running without a login");
501
+ const uid = idOf(OWNER_USER, "-u");
502
+ for (let i = 0; i < 30 && !fs3.existsSync(`/run/user/${uid}/bus`); i += 1) spawnSync("sleep", ["1"]);
503
+ return step.why;
504
+ }
505
+ case "node": {
506
+ if (step.action === "check") return step.why;
507
+ const { keyring, fingerprints } = await installAptKey("nodesource", "https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key");
508
+ ctx.notes.nodesourceKey = fingerprints;
509
+ aptSource("nodesource.list", `deb [signed-by=${keyring}] https://deb.nodesource.com/node_${SERVICE_NODE_MAJOR}.x nodistro main`);
510
+ must("apt-get", ["install", "-y", "nodejs"], `Installing Node ${SERVICE_NODE_MAJOR}`);
511
+ ctx.installed.push("nodejs");
512
+ return `Node ${SERVICE_NODE_MAJOR} installed (key ${fingerprints[0]}).`;
513
+ }
514
+ case "runner": {
515
+ const tarball = options.tarball ?? (fs3.existsSync(path2.resolve("kg.tgz")) ? path2.resolve("kg.tgz") : null);
516
+ const staged = path2.join(os2.tmpdir(), `kg-runner-${release.version}-${process.pid}.tgz`);
517
+ if (tarball) fs3.copyFileSync(tarball, staged);
518
+ else must("npm", ["pack", "--pack-destination", os2.tmpdir(), release.root], "Packing the runner").trim();
519
+ if (!tarball) fs3.renameSync(path2.join(os2.tmpdir(), `${release.name.replace("@", "").replace("/", "-")}-${release.version}.tgz`), staged);
520
+ fs3.chmodSync(staged, 420);
521
+ try {
522
+ asOwner(["/usr/bin/npm", "install", "--global", "--prefix", OWNER_PREFIX, "--ignore-scripts", "--no-audit", "--no-fund", staged], "Installing the runner");
523
+ } finally {
524
+ fs3.rmSync(staged, { force: true });
525
+ }
526
+ return `${release.name}@${release.version} installed in ${OWNER_PREFIX}.`;
527
+ }
528
+ case "enrol": {
529
+ if (!ctx.code) throw new StepFailure("No enrolment code was given.");
530
+ const replace = options.reEnrol ? ["--replace"] : [];
531
+ const out = asOwner(["/usr/bin/node", ownerCli(release), "enrol", "--project", options.project, "--api-key", options.apiKey, ...replace], "Enrolling", {
532
+ input: `${ctx.code}
533
+ `
534
+ });
535
+ const tail = /key ends …?([0-9a-f]{4})/.exec(out)?.[1];
536
+ const approve = /Approve this server[^\n]*/.exec(out)?.[0];
537
+ return approve ? `Enrolled. ${approve}` : `Enrolled${tail ? `; the key ends \u2026${tail}` : ""}.`;
538
+ }
539
+ case "service":
540
+ asOwner(["/usr/bin/node", ownerCli(release), "--yes", "service", "install"], "Installing the service");
541
+ return "The service is installed and running. It takes no work until this server is ready.";
542
+ case "docker": {
543
+ if (step.action === "check") return step.why;
544
+ const id = parseOsRelease(readText("/etc/os-release") ?? "");
545
+ const arch = must("dpkg", ["--print-architecture"], "Reading the architecture").trim();
546
+ const { keyring } = await installAptKey("docker", `https://download.docker.com/linux/${id.id}/gpg`);
547
+ aptSource("docker.list", `deb [arch=${arch} signed-by=${keyring}] https://download.docker.com/linux/${id.id} ${id.codename} stable`);
548
+ must("apt-get", ["install", "-y", "docker-ce", "docker-ce-cli", "containerd.io"], "Installing Docker Engine");
549
+ ctx.installed.push("docker-ce", "docker-ce-cli", "containerd.io");
550
+ must("systemctl", ["enable", "--now", "docker"], "Starting Docker");
551
+ return "Docker Engine installed and started.";
552
+ }
553
+ case "gvisor": {
554
+ if (step.action === "check") return step.why;
555
+ const arch = must("dpkg", ["--print-architecture"], "Reading the architecture").trim();
556
+ const { keyring, fingerprints } = await installAptKey("gvisor", "https://gvisor.dev/archive.key");
557
+ ctx.notes.gvisorKey = fingerprints;
558
+ aptSource("gvisor.list", `deb [arch=${arch} signed-by=${keyring}] https://storage.googleapis.com/gvisor/releases release main`);
559
+ must("apt-get", ["install", "-y", "runsc"], "Installing gVisor");
560
+ ctx.installed.push("runsc");
561
+ return `gVisor installed (key ${fingerprints[0]}).`;
562
+ }
563
+ case "daemon-json": {
564
+ if (step.action === "check") {
565
+ const merged2 = mergeDaemonJson(readText("/etc/docker/daemon.json"));
566
+ if (!merged2.ok) throw new StepFailure(merged2.reason);
567
+ return step.why;
568
+ }
569
+ const current = readText("/etc/docker/daemon.json");
570
+ const merged = mergeDaemonJson(current);
571
+ if (!merged.ok) throw new StepFailure(merged.reason);
572
+ fs3.mkdirSync("/etc/docker", { recursive: true, mode: 493 });
573
+ const backup = current !== null ? `/etc/docker/daemon.json.kg-backup-${Date.now()}` : null;
574
+ if (backup) fs3.writeFileSync(backup, current, { mode: 420 });
575
+ fs3.writeFileSync("/etc/docker/daemon.json", merged.text, { mode: 420 });
576
+ const valid = run("dockerd", ["--validate", "--config-file", "/etc/docker/daemon.json"]);
577
+ if (!valid.ok) {
578
+ if (backup) fs3.copyFileSync(backup, "/etc/docker/daemon.json");
579
+ else fs3.rmSync("/etc/docker/daemon.json", { force: true });
580
+ throw new StepFailure(`Docker refused the merged daemon.json, so the old one was put back: ${valid.err.trim().slice(0, 200)}`);
581
+ }
582
+ must("systemctl", ["reload", "docker"], "Reloading Docker");
583
+ return `runsc registered and live-restore on${backup ? `; the old file is at ${backup}` : ""}.`;
584
+ }
585
+ case "network": {
586
+ if (step.action === "check") {
587
+ if (step.why.includes("has containers on it")) throw new StepFailure(step.why);
588
+ return step.why;
589
+ }
590
+ if (run("docker", ["network", "inspect", SESSION_BRIDGE]).ok) must("docker", ["network", "rm", SESSION_BRIDGE], "Removing the old session network");
591
+ must(
592
+ "docker",
593
+ ["network", "create", "--driver", "bridge", "--opt", `com.docker.network.bridge.name=${SESSION_BRIDGE}`, "--opt", "com.docker.network.bridge.enable_icc=false", SESSION_BRIDGE],
594
+ "Creating the session network"
595
+ );
596
+ return step.why;
597
+ }
598
+ case "firewall": {
599
+ const resolvers = [
600
+ ...resolversFrom(readText("/run/systemd/resolve/resolv.conf") ?? ""),
601
+ ...resolversFrom(readText("/etc/resolv.conf") ?? "")
602
+ ];
603
+ fs3.mkdirSync("/etc/kilogent", { recursive: true, mode: 493 });
604
+ fs3.writeFileSync(FIREWALL_RULES_PATH, firewallRules({ resolvers }), { mode: 420 });
605
+ fs3.writeFileSync(path2.join("/etc/systemd/system", FIREWALL_UNIT), firewallUnit(FIREWALL_RULES_PATH), { mode: 420 });
606
+ must("systemctl", ["daemon-reload"], "Loading the firewall unit");
607
+ must("systemctl", ["enable", FIREWALL_UNIT], "Enabling the firewall unit");
608
+ must("systemctl", ["restart", FIREWALL_UNIT], "Applying the session firewall");
609
+ return `Applied, with DNS to ${[...new Set(resolvers)].join(", ") || "no resolver found"}.`;
610
+ }
611
+ case "launcher": {
612
+ if (!release.sessionImage) throw new StepFailure("This package names no session image, so it is not a published release.");
613
+ const repository = release.sessionImage.split("@")[0];
614
+ const sessionUid = idOf(SESSION_USER, "-u");
615
+ const sessionGid = idOf(SESSION_USER, "-g");
616
+ const ownerUid = idOf(OWNER_USER, "-u");
617
+ if (sessionUid === null || sessionGid === null || ownerUid === null) throw new StepFailure("The service users do not exist.");
618
+ for (const dir of ["/usr/local/lib/kilogent", "/etc/kilogent", "/var/lib/kilogent", SESSIONS_ROOT]) {
619
+ fs3.mkdirSync(dir, { recursive: true, mode: 493 });
620
+ fs3.chownSync(dir, 0, 0);
621
+ fs3.chmodSync(dir, 493);
622
+ }
623
+ const staged = `${LAUNCHER_PATH}.new`;
624
+ fs3.copyFileSync(path2.join(release.root, "dist/kg-session-launcher.mjs"), staged);
625
+ fs3.chownSync(staged, 0, 0);
626
+ fs3.chmodSync(staged, 493);
627
+ fs3.renameSync(staged, LAUNCHER_PATH);
628
+ const settings = {
629
+ repository,
630
+ runtime: "runsc",
631
+ network: SESSION_BRIDGE,
632
+ sessionUid,
633
+ sessionGid,
634
+ ownerUid,
635
+ sessionsRoot: SESSIONS_ROOT,
636
+ requireRuntime: true,
637
+ // What sessions may REALLY use at once: the machine's memory less the host's own share. The launcher
638
+ // refuses a raise past it, so growing sessions never run the machine out of memory.
639
+ sessionCapMb: Math.max(1024, Math.floor(os2.totalmem() / (1024 * 1024)) - HOST_RESERVE_MB)
640
+ };
641
+ fs3.writeFileSync(LAUNCHER_SETTINGS_PATH, `${JSON.stringify(settings, null, 2)}
642
+ `, { mode: 420 });
643
+ return step.action === "check" ? `The session launcher is this release's; its settings were written again, for ${repository}.` : `Installed at ${LAUNCHER_PATH}, for ${repository}.`;
644
+ }
645
+ case "sudoers": {
646
+ if (step.action === "check") return step.why;
647
+ const staged = `${SUDOERS_PATH}.new`;
648
+ fs3.writeFileSync(staged, sudoersLine(OWNER_USER, LAUNCHER_PATH), { mode: 288 });
649
+ const checked = run("visudo", ["-cf", staged]);
650
+ if (!checked.ok) {
651
+ fs3.rmSync(staged, { force: true });
652
+ throw new StepFailure(`visudo refused the sudoers line: ${checked.err.trim().slice(0, 200)}`);
653
+ }
654
+ fs3.renameSync(staged, SUDOERS_PATH);
655
+ return step.why;
656
+ }
657
+ case "hold":
658
+ if (ctx.installed.length === 0) return "Nothing was installed this run; what an earlier run held stays held.";
659
+ must("apt-mark", ["hold", ...ctx.installed], "Holding the installed packages");
660
+ return `Held: ${ctx.installed.join(", ")}.`;
661
+ case "wait":
662
+ return waitForReadiness(release);
663
+ }
664
+ }
665
+ function waitForReadiness(release) {
666
+ const file = path2.join(OWNER_HOME, `.${release.bin}`, "readiness.json");
667
+ let last = "";
668
+ for (let i = 0; i < 60; i += 1) {
669
+ const doc = readText(file);
670
+ if (doc) {
671
+ try {
672
+ const run2 = JSON.parse(doc).run;
673
+ const notOk = (run2?.items ?? []).filter((item) => item.state !== "ok").map((item) => `${item.id} ${item.state}`);
674
+ const line = notOk.length ? `Not ready yet: ${notOk.join(", ")}.` : "Every readiness item is ok.";
675
+ if (line !== last) say(` ${line}`);
676
+ last = line;
677
+ if (!notOk.length) return "The server reports ready.";
678
+ } catch {
679
+ }
680
+ }
681
+ spawnSync("sleep", ["5"]);
682
+ }
683
+ return last ? `${last} See \`${release.bin} doctor\` as ${OWNER_USER}, and the Fleet page.` : "The server has not reported yet. See the Fleet page.";
684
+ }
685
+ function readCode(options) {
686
+ if (options.codeStdin) return fs3.readFileSync(0, "utf8").trim() || null;
687
+ let fd;
688
+ try {
689
+ fd = fs3.openSync("/dev/tty", "r+");
690
+ } catch {
691
+ return null;
692
+ }
693
+ fs3.writeSync(fd, "Enrolment code (from the Fleet page): ");
694
+ spawnSync("stty", ["-echo"], { stdio: [fd, "ignore", "ignore"] });
695
+ const buf = Buffer.alloc(1);
696
+ let code = "";
697
+ try {
698
+ while (fs3.readSync(fd, buf, 0, 1, null) === 1 && buf[0] !== 10) code += buf.toString("utf8");
699
+ } finally {
700
+ spawnSync("stty", ["echo"], { stdio: [fd, "ignore", "ignore"] });
701
+ fs3.writeSync(fd, "\n");
702
+ fs3.closeSync(fd);
703
+ }
704
+ return code.trim() || null;
705
+ }
706
+ function confirm() {
707
+ let fd;
708
+ try {
709
+ fd = fs3.openSync("/dev/tty", "r+");
710
+ } catch {
711
+ return false;
712
+ }
713
+ fs3.writeSync(fd, "Go ahead? [y/N] ");
714
+ const buf = Buffer.alloc(64);
715
+ const n = fs3.readSync(fd, buf, 0, buf.length, null);
716
+ fs3.closeSync(fd);
717
+ return /^y(es)?$/i.test(buf.subarray(0, n).toString("utf8").trim());
718
+ }
719
+ function writeHostState(release, results, notes) {
720
+ try {
721
+ fs3.mkdirSync(path2.dirname(HOST_STATE_PATH), { recursive: true, mode: 493 });
722
+ fs3.writeFileSync(HOST_STATE_PATH, `${JSON.stringify({ version: 1, release: release.version, at: Date.now(), steps: results, ...notes }, null, 2)}
723
+ `, { mode: 420 });
724
+ } catch {
725
+ }
726
+ }
727
+ var HOST_PACKAGES = ["docker-ce", "docker-ce-cli", "containerd.io", "runsc", "nodejs"];
728
+ function drainServer(release, now) {
729
+ const dir = path2.join(OWNER_HOME, `.${release.bin}`);
730
+ const uid = idOf(OWNER_USER, "-u");
731
+ const gid = idOf(OWNER_USER, "-g");
732
+ if (!fs3.existsSync(dir) || uid === null || gid === null) return null;
733
+ const file = path2.join(dir, DRAIN_REQUEST_FILE);
734
+ const request = { at: Date.now(), reason: "provision --upgrade-host" };
735
+ fs3.writeFileSync(file, `${JSON.stringify(request)}
736
+ `, { mode: 384 });
737
+ fs3.chownSync(file, uid, gid);
738
+ if (now) return file;
739
+ const stateFile = path2.join(dir, "fleet-state.json");
740
+ let said = "";
741
+ for (let waited = 0; waited < 45 * 6e4; waited += 5e3) {
742
+ const progress = drainProgress(readFleetState(stateFile), { requestedAt: request.at, now: Date.now(), staleMs: FLEET_STATE_STALE_MS });
743
+ if (progress.state !== "waiting") return file;
744
+ const line = `Waiting for ${progress.running} running job(s) to finish (--now does not wait)\u2026`;
745
+ if (line !== said) say(` ${line}`);
746
+ said = line;
747
+ spawnSync("sleep", ["5"]);
748
+ }
749
+ fs3.rmSync(file, { force: true });
750
+ throw new StepFailure("Jobs were still running after 45 minutes, so nothing was upgraded. Run it again, or with --now.");
751
+ }
752
+ function startStoppedDocker() {
753
+ if (!run("sh", ["-c", "command -v docker"]).ok) return;
754
+ if (run("docker", ["version", "--format", "{{.Server.Version}}"], { timeout: 2e4 }).ok) return;
755
+ if (!run("systemctl", ["cat", "docker.service"]).ok) return;
756
+ say("Docker is installed and stopped; starting it.");
757
+ run("systemctl", ["start", "docker"], { timeout: 12e4 });
758
+ for (let i = 0; i < 30; i += 1) {
759
+ if (run("docker", ["version", "--format", "{{.Server.Version}}"], { timeout: 1e4 }).ok) return;
760
+ spawnSync("sleep", ["1"]);
761
+ }
762
+ }
763
+ function upgradeHostPackages(only = []) {
764
+ const unknown = only.filter((name) => !HOST_PACKAGES.includes(name));
765
+ if (unknown.length) {
766
+ throw new StepFailure(`--upgrade-host names ${unknown.join(", ")}, which provision does not install. It installs: ${HOST_PACKAGES.join(", ")}.`);
767
+ }
768
+ const wanted = only.length ? HOST_PACKAGES.filter((pkg) => only.includes(pkg)) : HOST_PACKAGES;
769
+ const installed = wanted.filter((pkg) => run("dpkg-query", ["-W", "-f=${Status}", pkg]).out.includes("ok installed"));
770
+ if (installed.length === 0) {
771
+ return only.length ? `${only.join(", ")} ${only.length === 1 ? "is" : "are"} not installed here; nothing to upgrade.` : "None of the packages provision installs are here; nothing to upgrade.";
772
+ }
773
+ const held = run("apt-mark", ["showhold"]).out.split("\n").map((l) => l.trim()).filter((p) => installed.includes(p));
774
+ if (held.length) must("apt-mark", ["unhold", ...held], "Releasing the held packages");
775
+ try {
776
+ must("apt-get", ["update"], "apt-get update");
777
+ must("apt-get", ["install", "-y", "--only-upgrade", ...installed], "Upgrading the host packages");
778
+ } finally {
779
+ if (held.length) must("apt-mark", ["hold", ...held], "Holding the packages again");
780
+ }
781
+ return `Upgraded ${installed.join(", ")}.`;
782
+ }
783
+ async function main() {
784
+ if (process.platform !== "linux") {
785
+ say("provision-root runs on a Linux server. On the dev Mac, run `npx <package>@<version> provision` as the runner user.");
786
+ process.exit(1);
787
+ }
788
+ if (typeof process.getuid !== "function" || process.getuid() !== 0) {
789
+ say("Run this as root.");
790
+ process.exit(1);
791
+ }
792
+ process.umask(18);
793
+ const options = parseArgs(process.argv.slice(2));
794
+ const release = thisRelease();
795
+ let drainFile = null;
796
+ if (options.upgradeHost) {
797
+ options.hostOnly = true;
798
+ if (!options.yes && !confirm()) {
799
+ say("Nothing was changed.");
800
+ process.exit(1);
801
+ }
802
+ options.yes = true;
803
+ try {
804
+ drainFile = drainServer(release, options.now);
805
+ say(`\u2713 drain: ${drainFile ? "no jobs are running." : "no runner is set up here, so there is nothing to drain."}`);
806
+ say(`\u2713 upgrade: ${upgradeHostPackages(options.upgradeOnly)}`);
807
+ } catch (e) {
808
+ if (drainFile) fs3.rmSync(drainFile, { force: true });
809
+ say(`\u2717 ${e instanceof Error ? e.message : String(e)}`);
810
+ process.exit(1);
811
+ }
812
+ process.on("exit", () => {
813
+ if (drainFile) fs3.rmSync(drainFile, { force: true });
814
+ });
815
+ }
816
+ if (!options.planOnly) startStoppedDocker();
817
+ const facts = gatherFacts(release);
818
+ const plan = planLinuxProvision(facts, {
819
+ version: release.version,
820
+ launcherSha256: release.launcherSha256,
821
+ expectedSudoers: sudoersLine(OWNER_USER, LAUNCHER_PATH),
822
+ hostOnly: options.hostOnly,
823
+ reEnrol: options.reEnrol
824
+ });
825
+ say(`Kilogent server provisioning \u2014 ${release.name}@${release.version}`);
826
+ if (plan.refusal) {
827
+ say(`\u2717 ${plan.refusal}`);
828
+ say("Nothing was changed.");
829
+ process.exit(1);
830
+ }
831
+ for (const step of plan.steps) say(` ${step.action === "do" ? "\u2022" : step.action === "check" ? "\u2713" : "\u2013"} ${step.id}: ${step.why}`);
832
+ if (options.planOnly) {
833
+ say("That is the plan. Nothing was changed.");
834
+ process.exit(0);
835
+ }
836
+ if (!options.hostOnly && (!/^[a-z0-9-]{1,64}$/.test(options.project) || !/^[A-Za-z0-9_-]{1,128}$/.test(options.apiKey))) {
837
+ say("\u2717 --project and --api-key are required. The Fleet page prints the whole command.");
838
+ process.exit(1);
839
+ }
840
+ if (!options.yes && !confirm()) {
841
+ say("Nothing was changed.");
842
+ process.exit(1);
843
+ }
844
+ const enrols = plan.steps.some((s) => s.id === "enrol" && s.action === "do");
845
+ const code = enrols ? readCode(options) : null;
846
+ if (enrols && !code) {
847
+ say("\u2717 No enrolment code was given. Nothing was changed.");
848
+ process.exit(1);
849
+ }
850
+ const results = [];
851
+ const notes = {};
852
+ const ctx = { release, options, code, installed: [], notes };
853
+ for (const step of plan.steps) {
854
+ try {
855
+ const detail = await runStep(step, ctx);
856
+ results.push({ id: step.id, action: step.action, ok: true, detail });
857
+ say(`\u2713 ${step.id}: ${detail}`);
858
+ } catch (e) {
859
+ const detail = e instanceof Error ? e.message : String(e);
860
+ results.push({ id: step.id, action: step.action, ok: false, detail });
861
+ say(`\u2717 ${step.id}: ${detail}`);
862
+ writeHostState(release, results, notes);
863
+ if (!results.some((r) => r.id === "service" && r.ok) && !options.hostOnly) break;
864
+ if (options.hostOnly && ["docker", "gvisor", "daemon-json"].includes(step.id)) break;
865
+ }
866
+ writeHostState(release, results, notes);
867
+ }
868
+ const code_ = provisionExitCode(results);
869
+ say(
870
+ code_ === 0 ? "Done. This server is set up." : code_ === 2 ? `The server is running, and a host step failed. Fix what is named above, then run this again.` : "Provisioning stopped before the server was running. Fix what is named above, then run this again."
871
+ );
872
+ process.exit(code_);
873
+ }
874
+ void main();
875
+ export {
876
+ APT_KEY_FINGERPRINTS
877
+ };