@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,1047 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/launcher/main.ts
4
+ import { spawnSync } from "node:child_process";
5
+ import fs2 from "node:fs";
6
+
7
+ // src/launcher/handler.ts
8
+ import { spawn } from "node:child_process";
9
+ import fs from "node:fs";
10
+ import path from "node:path";
11
+
12
+ // src/engines/runCommand.ts
13
+ import { execFile } from "node:child_process";
14
+ var COMMAND_TIMEOUT_MS = 1e4;
15
+ function runCommand(command, args, opts = {}) {
16
+ return new Promise((resolve) => {
17
+ try {
18
+ const child = execFile(
19
+ command,
20
+ [...args],
21
+ { env: opts.env ?? process.env, timeout: opts.timeoutMs ?? COMMAND_TIMEOUT_MS, maxBuffer: 4 * 1024 * 1024, encoding: "utf8" },
22
+ (error, stdout, stderr) => {
23
+ resolve({ ok: !error, stdout: String(stdout ?? ""), stderr: String(stderr ?? "") || (error ? error.message : "") });
24
+ }
25
+ );
26
+ if (opts.input !== void 0) {
27
+ child.stdin?.on("error", () => {
28
+ });
29
+ child.stdin?.end(opts.input);
30
+ }
31
+ } catch (e) {
32
+ resolve({ ok: false, stdout: "", stderr: e instanceof Error ? e.message : String(e) });
33
+ }
34
+ });
35
+ }
36
+
37
+ // src/image/dockerApi.ts
38
+ import http from "node:http";
39
+
40
+ // src/image/ref.ts
41
+ var DIGEST_REF = /^([a-z0-9-]+-docker\.pkg\.dev)\/([a-z0-9][a-z0-9-]{4,62}\/[a-z0-9][a-z0-9._-]*(?:\/[a-z0-9][a-z0-9._-]*)*)@(sha256:[a-f0-9]{64})$/;
42
+ var LOCAL_DIGEST_REF = /^((?:127\.0\.0\.1|localhost):\d{2,5})\/([a-z0-9][a-z0-9._-]*(?:\/[a-z0-9][a-z0-9._-]*)*)@(sha256:[a-f0-9]{64})$/;
43
+ function parseDigestRef(value, opts = {}) {
44
+ if (typeof value !== "string") return null;
45
+ const match = DIGEST_REF.exec(value) ?? (opts.allowLocalRegistry ? LOCAL_DIGEST_REF.exec(value) : null);
46
+ if (!match) return null;
47
+ return { registry: match[1], repository: match[2], digest: match[3], ref: value };
48
+ }
49
+ function registryAuthHeader(token, registry) {
50
+ return Buffer.from(JSON.stringify({ registrytoken: token, serveraddress: registry })).toString("base64url");
51
+ }
52
+ function classifyPullError(message, code) {
53
+ if (code === "ECONNREFUSED" || code === "ENOENT" || code === "EACCES" || /cannot connect to the docker daemon/i.test(message)) {
54
+ return "docker";
55
+ }
56
+ if (/unauthori[sz]ed|denied|forbidden|\b401\b|\b403\b|authentication required/i.test(message)) return "auth";
57
+ if (/manifest unknown|not found|no such manifest|name unknown|\b404\b/i.test(message)) return "registry";
58
+ return "network";
59
+ }
60
+
61
+ // src/image/dockerApi.ts
62
+ function request(socketPath, method, apiPath, opts) {
63
+ return new Promise((resolve, reject) => {
64
+ const body = opts.json === void 0 ? null : JSON.stringify(opts.json);
65
+ const headers = { ...opts.headers ?? {}, ...body === null ? {} : { "Content-Type": "application/json", "Content-Length": String(Buffer.byteLength(body)) } };
66
+ const req = http.request({ socketPath, method, path: apiPath, headers, ...opts.signal ? { signal: opts.signal } : {} }, (res) => {
67
+ let body2 = "";
68
+ res.setEncoding("utf8");
69
+ res.on("data", (chunk) => {
70
+ opts.onChunk?.(chunk);
71
+ body2 = (body2 + chunk).slice(-64e3);
72
+ });
73
+ res.on("end", () => resolve({ status: res.statusCode ?? 0, body: body2 }));
74
+ res.on("error", reject);
75
+ });
76
+ req.setTimeout(opts.timeoutMs, () => req.destroy(Object.assign(new Error(`Docker did not answer within ${opts.timeoutMs} ms`), { code: "ETIMEDOUT" })));
77
+ req.on("error", reject);
78
+ req.end(body ?? void 0);
79
+ });
80
+ }
81
+ async function imagePresent(socketPath, image2) {
82
+ try {
83
+ const res = await request(socketPath, "GET", `/images/${encodeURIComponent(image2)}/json`, { timeoutMs: 1e4 });
84
+ if (res.status === 200) return true;
85
+ if (res.status === 404) return false;
86
+ return null;
87
+ } catch {
88
+ return null;
89
+ }
90
+ }
91
+ async function listRepositoryImages(socketPath, repository) {
92
+ try {
93
+ const filters = encodeURIComponent(JSON.stringify({ reference: [repository] }));
94
+ const res = await request(socketPath, "GET", `/images/json?filters=${filters}`, { timeoutMs: 15e3 });
95
+ if (res.status !== 200) return [];
96
+ const images = JSON.parse(res.body);
97
+ return images.flatMap((image2) => Array.isArray(image2.RepoDigests) ? image2.RepoDigests : []).filter((ref) => typeof ref === "string" && ref.startsWith(`${repository}@sha256:`));
98
+ } catch {
99
+ return [];
100
+ }
101
+ }
102
+ async function removeImage(socketPath, ref) {
103
+ try {
104
+ const res = await request(socketPath, "DELETE", `/images/${encodeURIComponent(ref)}`, { timeoutMs: 6e4 });
105
+ return res.status === 200;
106
+ } catch {
107
+ return false;
108
+ }
109
+ }
110
+ async function pullByDigest(input) {
111
+ const query = `fromImage=${encodeURIComponent(`${input.image.registry}/${input.image.repository}@${input.image.digest}`)}`;
112
+ try {
113
+ const res = await request(input.socketPath, "POST", `/images/create?${query}`, {
114
+ headers: { "X-Registry-Auth": registryAuthHeader(input.token, input.image.registry) },
115
+ timeoutMs: input.timeoutMs ?? 15 * 6e4
116
+ });
117
+ const streamed = lastStreamError(res.body);
118
+ if (res.status !== 200 || streamed) {
119
+ const message = streamed ?? (res.body.trim().slice(0, 300) || `Docker answered ${res.status}`);
120
+ return { ok: false, kind: classifyPullError(message), message };
121
+ }
122
+ return { ok: true };
123
+ } catch (e) {
124
+ const err = e;
125
+ return { ok: false, kind: classifyPullError(err.message ?? String(e), err.code), message: err.message ?? String(e) };
126
+ }
127
+ }
128
+ function lastStreamError(body) {
129
+ let found = null;
130
+ for (const line of body.split("\n")) {
131
+ const trimmed = line.trim();
132
+ if (!trimmed.startsWith("{")) continue;
133
+ try {
134
+ const doc = JSON.parse(trimmed);
135
+ const message = typeof doc.errorDetail?.message === "string" ? doc.errorDetail.message : typeof doc.error === "string" ? doc.error : null;
136
+ if (message) found = message;
137
+ } catch {
138
+ }
139
+ }
140
+ return found;
141
+ }
142
+ var MIB = 1024 * 1024;
143
+ function memoryFromStats(body) {
144
+ let doc;
145
+ try {
146
+ doc = JSON.parse(body);
147
+ } catch {
148
+ return null;
149
+ }
150
+ const m = doc.memory_stats;
151
+ if (!m || typeof m.usage !== "number") return null;
152
+ const inactive = typeof m.stats?.inactive_file === "number" ? m.stats.inactive_file : typeof m.stats?.total_inactive_file === "number" ? m.stats.total_inactive_file : 0;
153
+ return {
154
+ usedMb: Math.max(0, (m.usage - inactive) / MIB),
155
+ limitMb: typeof m.limit === "number" ? m.limit / MIB : 0
156
+ };
157
+ }
158
+ async function containerMemory(socketPath, container) {
159
+ try {
160
+ const res = await request(socketPath, "GET", `/containers/${encodeURIComponent(container)}/stats?stream=false&one-shot=true`, { timeoutMs: 5e3 });
161
+ return res.status === 200 ? memoryFromStats(res.body) : null;
162
+ } catch {
163
+ return null;
164
+ }
165
+ }
166
+ async function updateContainerMemory(socketPath, container, limitMb) {
167
+ const bytes = Math.round(limitMb) * MIB;
168
+ try {
169
+ const res = await request(socketPath, "POST", `/containers/${encodeURIComponent(container)}/update`, {
170
+ timeoutMs: 1e4,
171
+ json: { Memory: bytes, MemorySwap: bytes }
172
+ });
173
+ return res.status === 200;
174
+ } catch {
175
+ return false;
176
+ }
177
+ }
178
+ function watchContainerOom(socketPath, container, signal) {
179
+ let seen = false;
180
+ const filters = encodeURIComponent(JSON.stringify({ type: ["container"], container: [container], event: ["oom"] }));
181
+ request(socketPath, "GET", `/events?filters=${filters}`, {
182
+ timeoutMs: 24 * 60 * 6e4,
183
+ signal,
184
+ onChunk: (text) => {
185
+ if (/"(status|Action)"\s*:\s*"oom"/.test(text)) seen = true;
186
+ }
187
+ }).catch(() => void 0);
188
+ return { seen: () => seen };
189
+ }
190
+
191
+ // src/image/retention.ts
192
+ function repositoryOf(ref) {
193
+ const at = ref.indexOf("@sha256:");
194
+ return at > 0 && /^[a-f0-9]{64}$/.test(ref.slice(at + "@sha256:".length)) ? ref.slice(0, at) : null;
195
+ }
196
+ function imagesToRemove(present, keep) {
197
+ const repositories = new Set(keep.map(repositoryOf).filter((r) => r !== null));
198
+ return present.filter((ref) => {
199
+ const repository = repositoryOf(ref);
200
+ return repository !== null && repositories.has(repository) && !keep.includes(ref);
201
+ });
202
+ }
203
+
204
+ // src/launcher/memoryWatch.ts
205
+ var RUNSC_LIVE_MEMORY_MIN = "release-20260202.0";
206
+ var MAX_RAISE_FAILURES = 5;
207
+ var OOM_NEAR_LIMIT_PCT = 90;
208
+ var GROW_PLAN_BOUNDS = Object.freeze({
209
+ startMb: [256, 4096],
210
+ stepMb: [50, 1024],
211
+ maxMb: [256, 4096],
212
+ growAtPct: [50, 95],
213
+ pollMs: [100, 5e3]
214
+ });
215
+ function parseGrowPlan(raw, ceilingMb) {
216
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
217
+ const d = raw;
218
+ const out = {};
219
+ for (const [key, [min, max]] of Object.entries(GROW_PLAN_BOUNDS)) {
220
+ const v = d[key];
221
+ if (typeof v !== "number" || !Number.isInteger(v) || v < min || v > max) return null;
222
+ out[key] = v;
223
+ }
224
+ const plan = out;
225
+ if (plan.maxMb > ceilingMb || plan.startMb > plan.maxMb) return null;
226
+ return plan;
227
+ }
228
+ function nextLimit(usedMb, appliedMb, plan) {
229
+ if (!Number.isFinite(usedMb) || appliedMb >= plan.maxMb) return null;
230
+ const over = (limit) => usedMb >= limit * plan.growAtPct / 100;
231
+ if (!over(appliedMb)) return null;
232
+ let next = appliedMb + plan.stepMb;
233
+ while (next < plan.maxMb && over(next)) next += plan.stepMb;
234
+ return Math.min(plan.maxMb, next);
235
+ }
236
+ function nodeHeapMb(plan) {
237
+ return Math.max(128, Math.min(Math.round(plan.maxMb * 0.75), plan.maxMb - 128));
238
+ }
239
+ function nodeOptionsFor(plan) {
240
+ return `--max-old-space-size=${nodeHeapMb(plan)}`;
241
+ }
242
+ function classifyExit(input) {
243
+ const nearLimit = input.limitMb > 0 && input.lastUsedMb >= input.limitMb * OOM_NEAR_LIMIT_PCT / 100;
244
+ const killed = input.oomEvent || input.exitCode === 137 && !input.stopped && nearLimit;
245
+ if (!killed) return "none";
246
+ return input.limitMb >= input.maxMb ? "limitAtMax" : "limitBelowMax";
247
+ }
248
+ function initialMemoryState(input) {
249
+ const startMb = input.plan ? input.plan.startMb : input.ceilingMb;
250
+ const maxMb = input.plan ? input.plan.maxMb : input.ceilingMb;
251
+ return {
252
+ tier: input.tier,
253
+ growable: input.plan !== null,
254
+ reason: input.plan ? null : input.reason,
255
+ startMb,
256
+ limitMb: startMb,
257
+ maxMb,
258
+ usedMb: 0,
259
+ peakMb: 0,
260
+ raises: 0,
261
+ refused: 0,
262
+ atMax: startMb >= maxMb,
263
+ running: true,
264
+ oom: "none",
265
+ updatedAt: input.now
266
+ };
267
+ }
268
+ var RECORD_EVERY_MS = 2e3;
269
+ async function watchMemory(input) {
270
+ const { plan, port, signal } = input;
271
+ const state = { ...input.state };
272
+ let failures = 0;
273
+ let lastRecorded = 0;
274
+ const record = (force) => {
275
+ const now = port.now();
276
+ if (!force && now - lastRecorded < RECORD_EVERY_MS) return;
277
+ lastRecorded = now;
278
+ state.updatedAt = now;
279
+ try {
280
+ port.onChange({ ...state });
281
+ } catch {
282
+ }
283
+ };
284
+ record(true);
285
+ while (!signal.aborted) {
286
+ let changed = false;
287
+ const sample = await port.sample().catch(() => null);
288
+ if (sample && !signal.aborted) {
289
+ state.usedMb = Math.max(0, Math.round(sample.usedMb));
290
+ state.peakMb = Math.max(state.peakMb, state.usedMb);
291
+ if (plan && failures < MAX_RAISE_FAILURES) {
292
+ if (sample.limitMb > 0 && Math.abs(sample.limitMb - state.limitMb) >= 1) {
293
+ if (await port.apply(state.limitMb).catch(() => false)) failures = 0;
294
+ else failures += 1;
295
+ }
296
+ const next = nextLimit(state.usedMb, state.limitMb, plan);
297
+ if (next !== null) {
298
+ const room = port.mayRaise ? await port.mayRaise(state.limitMb, next).catch(() => false) : true;
299
+ if (!room) {
300
+ state.refused += 1;
301
+ changed = true;
302
+ } else if (await port.apply(next).catch(() => false)) {
303
+ state.limitMb = next;
304
+ state.raises += 1;
305
+ state.atMax = next >= state.maxMb;
306
+ failures = 0;
307
+ changed = true;
308
+ } else {
309
+ failures += 1;
310
+ }
311
+ }
312
+ if (failures >= MAX_RAISE_FAILURES && !state.reason) {
313
+ state.reason = "Docker refused its raises, so this session stopped growing.";
314
+ changed = true;
315
+ }
316
+ }
317
+ }
318
+ record(changed);
319
+ await port.sleep(plan ? plan.pollMs : input.idlePollMs ?? 1e3, signal).catch(() => void 0);
320
+ }
321
+ return state;
322
+ }
323
+ function abortableSleep(ms, signal) {
324
+ return new Promise((resolve) => {
325
+ if (signal.aborted) return resolve();
326
+ const timer = setTimeout(done, ms);
327
+ timer.unref?.();
328
+ function done() {
329
+ clearTimeout(timer);
330
+ signal.removeEventListener("abort", done);
331
+ resolve();
332
+ }
333
+ signal.addEventListener("abort", done, { once: true });
334
+ });
335
+ }
336
+
337
+ // src/launcher/protocol.ts
338
+ var LAUNCHER_PROTOCOL = 2;
339
+ var LAUNCHER_SECURITY = 1;
340
+ var MEMORY_WATCH_REVISION = 1;
341
+ var SESSION_ENV_REVISION = 1;
342
+ var LAUNCHER_SETTINGS_PATH = "/etc/kilogent/launcher.json";
343
+ var MAX_REQUEST_BYTES = 1024 * 1024;
344
+ var SESSION_ENV_NAMES = Object.freeze([
345
+ "ANTHROPIC_BASE_URL",
346
+ "ANTHROPIC_AUTH_TOKEN",
347
+ "ANTHROPIC_API_KEY",
348
+ "CLAUDE_CODE_OAUTH_TOKEN",
349
+ "OPENAI_API_KEY",
350
+ "OPENAI_BASE_URL",
351
+ "GOOGLE_API_KEY",
352
+ "GH_TOKEN",
353
+ "GITHUB_TOKEN",
354
+ "GIT_CONFIG_COUNT",
355
+ "GIT_CONFIG_KEY_0",
356
+ "GIT_CONFIG_VALUE_0",
357
+ "GIT_CONFIG_KEY_1",
358
+ "GIT_CONFIG_VALUE_1",
359
+ "GIT_CONFIG_KEY_2",
360
+ "GIT_CONFIG_VALUE_2",
361
+ "CREW_JOB_ID",
362
+ "CREW_MODEL_INPUT"
363
+ ]);
364
+ var SESSION_ENV_SINCE = Object.freeze({ CREW_MODEL_INPUT: 1 });
365
+ var TIERS = {
366
+ "no-shell": { memoryMb: 1024, cpus: 0.5, pids: 256 },
367
+ shell: { memoryMb: 4096, cpus: 1, pids: 2048 }
368
+ };
369
+ var LauncherRefusal = class extends Error {
370
+ constructor(message) {
371
+ super(message);
372
+ this.name = "LauncherRefusal";
373
+ }
374
+ };
375
+ var LEASE = /^[a-f0-9]{20}$/;
376
+ var ENV_NAME = /^[A-Z_][A-Z0-9_]*$/;
377
+ var MAX_ARGS = 256;
378
+ var MAX_ARG_BYTES = 256 * 1024;
379
+ function refuse(message) {
380
+ throw new LauncherRefusal(message);
381
+ }
382
+ function onlyKeys(doc, allowed) {
383
+ for (const key of Object.keys(doc)) if (!allowed.includes(key)) refuse(`Unknown field '${key}'.`);
384
+ }
385
+ function lease(value) {
386
+ return typeof value === "string" && LEASE.test(value) ? value : refuse("A lease id is 20 lowercase hex characters.");
387
+ }
388
+ function image(value, settings) {
389
+ if (typeof value !== "string") refuse("An image is required.");
390
+ const expected = new RegExp(`^${escapeRegExp(settings.repository)}@sha256:[a-f0-9]{64}$`);
391
+ return expected.test(value) ? value : refuse(`The image must be ${settings.repository}@sha256:<digest>.`);
392
+ }
393
+ function parseRequest(raw, settings) {
394
+ if (Buffer.byteLength(raw, "utf8") > MAX_REQUEST_BYTES) refuse("The request is larger than 1 MiB.");
395
+ let doc;
396
+ try {
397
+ doc = JSON.parse(raw);
398
+ } catch {
399
+ refuse("The request is not JSON.");
400
+ }
401
+ if (!doc || typeof doc !== "object" || Array.isArray(doc)) refuse("The request must be a JSON object.");
402
+ const d = doc;
403
+ switch (d.op) {
404
+ case "info":
405
+ case "reap":
406
+ onlyKeys(d, ["op"]);
407
+ return { op: d.op };
408
+ case "prepare":
409
+ case "stop":
410
+ case "kill":
411
+ case "cleanup":
412
+ onlyKeys(d, ["op", "lease"]);
413
+ return { op: d.op, lease: lease(d.lease) };
414
+ case "inspect":
415
+ onlyKeys(d, ["op", "image"]);
416
+ return { op: "inspect", image: image(d.image, settings) };
417
+ case "prune": {
418
+ onlyKeys(d, ["op", "keep"]);
419
+ if (!Array.isArray(d.keep) || d.keep.length < 1 || d.keep.length > 4) refuse("keep must list one to four images.");
420
+ return { op: "prune", keep: d.keep.map((k) => image(k, settings)) };
421
+ }
422
+ case "pull":
423
+ onlyKeys(d, ["op", "image", "token"]);
424
+ if (typeof d.token !== "string" || !/^[A-Za-z0-9._-]{16,4096}$/.test(d.token)) refuse("A pull token is required.");
425
+ return { op: "pull", image: image(d.image, settings), token: d.token };
426
+ case "probe": {
427
+ onlyKeys(d, ["op", "image", "mcpUrl"]);
428
+ if (typeof d.mcpUrl !== "string" || !/^https?:\/\/[A-Za-z0-9.-]+(:\d+)?(\/[A-Za-z0-9._~/-]*)?$/.test(d.mcpUrl)) refuse("The MCP URL is not a plain URL.");
429
+ return { op: "probe", image: image(d.image, settings), mcpUrl: d.mcpUrl };
430
+ }
431
+ case "run": {
432
+ onlyKeys(d, ["op", "lease", "image", "bin", "args", "env", "tier", "memory"]);
433
+ const bin = d.bin === "claude" || d.bin === "deepagents-session" ? d.bin : refuse("The engine must be claude or deepagents-session.");
434
+ const tier = d.tier === "shell" || d.tier === "no-shell" ? d.tier : refuse("The tier must be shell or no-shell.");
435
+ if (!Array.isArray(d.args) || d.args.length > MAX_ARGS || !d.args.every((a) => typeof a === "string")) refuse("The args must be a list of strings.");
436
+ const args = d.args;
437
+ if (args.reduce((n, a) => n + Buffer.byteLength(a, "utf8"), 0) > MAX_ARG_BYTES) refuse("The args are too long.");
438
+ if (!d.env || typeof d.env !== "object" || Array.isArray(d.env)) refuse("The env must be an object.");
439
+ const env = {};
440
+ for (const [name, value] of Object.entries(d.env)) {
441
+ if (!ENV_NAME.test(name) || !SESSION_ENV_NAMES.includes(name)) refuse(`A session may not be given ${name}.`);
442
+ if (typeof value !== "string") refuse(`The value of ${name} must be a string.`);
443
+ env[name] = value;
444
+ }
445
+ let memory;
446
+ if (d.memory !== void 0) {
447
+ memory = parseGrowPlan(d.memory, TIERS[tier].memoryMb) ?? refuse(`The memory plan is not sound for a ${tier} session.`);
448
+ }
449
+ return { op: "run", lease: lease(d.lease), image: image(d.image, settings), bin, args, env, tier, ...memory ? { memory } : {} };
450
+ }
451
+ default:
452
+ refuse("Unknown op.");
453
+ }
454
+ }
455
+ function leaseDirs(settings, lease2) {
456
+ const base = `${settings.sessionsRoot.replace(/\/+$/, "")}/${settings.ownerUid}/${lease2}`;
457
+ return { base, work: `${base}/work`, cfg: `${base}/cfg`, home: `${base}/home`, resolv: `${base}/resolv.conf` };
458
+ }
459
+ var CONTAINER_WORK = "/session";
460
+ var CONTAINER_CFG = "/session-config";
461
+ var CONTAINER_HOME = "/home/session";
462
+ function containerName(settings, lease2) {
463
+ return `kg-${settings.ownerUid}-${lease2}`;
464
+ }
465
+ function buildRunArgv(req, settings, opts = { resolvConf: false }) {
466
+ if (settings.requireRuntime && settings.runtime !== "runsc") refuse("This server requires gVisor, and its launcher is not set to runsc.");
467
+ const dirs = leaseDirs(settings, req.lease);
468
+ const tier = TIERS[req.tier];
469
+ const memoryMb = opts.startMb !== void 0 ? Math.min(tier.memoryMb, opts.startMb) : tier.memoryMb;
470
+ const argv = [
471
+ "run",
472
+ "--rm",
473
+ "--init",
474
+ "--pull",
475
+ "never",
476
+ "--name",
477
+ containerName(settings, req.lease),
478
+ "--label",
479
+ "com.kilogent.session=1",
480
+ "--label",
481
+ `com.kilogent.owner=${settings.ownerUid}`,
482
+ "--label",
483
+ `com.kilogent.lease=${req.lease}`,
484
+ "--runtime",
485
+ settings.runtime,
486
+ "--network",
487
+ settings.network,
488
+ "--user",
489
+ `${settings.sessionUid}:${settings.sessionGid}`,
490
+ "--memory",
491
+ `${memoryMb}m`,
492
+ "--memory-swap",
493
+ `${memoryMb}m`,
494
+ // If the MACHINE runs short, the kernel takes a session before the launcher, the daemon or Docker.
495
+ "--oom-score-adj",
496
+ "500",
497
+ "--cpus",
498
+ String(tier.cpus),
499
+ "--pids-limit",
500
+ String(tier.pids),
501
+ "--security-opt",
502
+ "no-new-privileges",
503
+ "--cap-drop",
504
+ "ALL"
505
+ ];
506
+ for (const name of Object.keys(req.env).sort()) argv.push("-e", name);
507
+ if (req.memory && opts.startMb !== void 0) argv.push("-e", `NODE_OPTIONS=${nodeOptionsFor(req.memory)}`);
508
+ argv.push("-e", `HOME=${CONTAINER_HOME}`, "-v", `${dirs.home}:${CONTAINER_HOME}`);
509
+ if (opts.resolvConf) argv.push("-v", `${dirs.resolv}:/etc/resolv.conf:ro`);
510
+ argv.push("-v", `${dirs.work}:${CONTAINER_WORK}`, "-v", `${dirs.cfg}:${CONTAINER_CFG}:ro`, "-w", CONTAINER_WORK);
511
+ const remap = (a) => a.split(dirs.cfg).join(CONTAINER_CFG).split(dirs.work).join(CONTAINER_WORK);
512
+ argv.push(req.image, req.bin, ...req.args.map(remap));
513
+ return argv;
514
+ }
515
+ function parseSettings(text) {
516
+ let doc;
517
+ try {
518
+ doc = JSON.parse(text);
519
+ } catch {
520
+ refuse("launcher.json is not JSON.");
521
+ }
522
+ const int = (v, what) => Number.isInteger(v) && v > 0 ? v : refuse(`launcher.json: ${what} must be a positive integer.`);
523
+ const str = (v, re, what) => typeof v === "string" && re.test(v) ? v : refuse(`launcher.json: ${what} is not valid.`);
524
+ return {
525
+ repository: str(doc.repository, /^[a-z0-9.-]+(:\d+)?\/[a-z0-9._/-]+$/, "repository"),
526
+ runtime: str(doc.runtime, /^[a-z0-9-]{1,32}$/, "runtime"),
527
+ network: str(doc.network, /^[a-z0-9_-]{1,64}$/, "network"),
528
+ sessionUid: int(doc.sessionUid, "sessionUid"),
529
+ sessionGid: int(doc.sessionGid, "sessionGid"),
530
+ ownerUid: int(doc.ownerUid, "ownerUid"),
531
+ sessionsRoot: str(doc.sessionsRoot, /^\/[A-Za-z0-9._/-]+$/, "sessionsRoot"),
532
+ requireRuntime: doc.requireRuntime === true,
533
+ ...Number.isInteger(doc.sessionCapMb) && doc.sessionCapMb > 0 ? { sessionCapMb: doc.sessionCapMb } : {}
534
+ };
535
+ }
536
+ function runscOlder(version, min) {
537
+ const parse = (v) => {
538
+ const m = /^release-(\d{8})\.(\d+)$/.exec(v.trim());
539
+ return m ? [Number(m[1]), Number(m[2])] : null;
540
+ };
541
+ const a = parse(version);
542
+ const b = parse(min);
543
+ if (!a || !b) return null;
544
+ return a[0] < b[0] || a[0] === b[0] && a[1] < b[1];
545
+ }
546
+ function escapeRegExp(s) {
547
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
548
+ }
549
+
550
+ // src/engines/sandbox.ts
551
+ var DOCKER_CLI_ENV = [
552
+ "PATH",
553
+ "HOME",
554
+ "DOCKER_HOST",
555
+ "DOCKER_CONTEXT",
556
+ "DOCKER_CONFIG",
557
+ "DOCKER_CERT_PATH",
558
+ "DOCKER_TLS_VERIFY"
559
+ ];
560
+ function dockerCliEnv(hostEnv) {
561
+ return Object.fromEntries(
562
+ DOCKER_CLI_ENV.filter((k) => hostEnv[k] !== void 0).map((k) => [k, hostEnv[k]])
563
+ );
564
+ }
565
+
566
+ // src/image/probe.ts
567
+ function probeScript(input) {
568
+ return `
569
+ const out = { cli: null, home: null, mcp: null, isolation: null };
570
+ const cp = require('node:child_process');
571
+ const fs = require('node:fs');
572
+ const net = require('node:net');
573
+ const dns = require('node:dns');
574
+ try { cp.execFileSync('claude', ['--version'], { stdio: 'pipe', timeout: 60000 }); out.cli = true; } catch { out.cli = false; }
575
+ try { const f = (process.env.HOME || '/nonexistent') + '/.kg-probe'; fs.writeFileSync(f, 'x'); fs.unlinkSync(f); out.home = true; } catch { out.home = false; }
576
+ const reach = (host, port) => new Promise((resolve) => {
577
+ const s = net.connect({ host, port, timeout: 2500 });
578
+ s.on('connect', () => { s.destroy(); resolve(true); });
579
+ s.on('timeout', () => { s.destroy(); resolve(false); });
580
+ s.on('error', () => resolve(false));
581
+ });
582
+ (async () => {
583
+ try {
584
+ const url = new URL(${JSON.stringify(input.mcpUrl)});
585
+ await dns.promises.lookup(url.hostname);
586
+ const res = await fetch(url, { method: 'GET', signal: AbortSignal.timeout(10000) });
587
+ out.mcp = res.status > 0;
588
+ } catch { out.mcp = false; }
589
+ if (${input.isolation ? "true" : "false"}) {
590
+ const gateway = (() => { try { const line = fs.readFileSync('/proc/net/route', 'utf8').split('\\n').find((l) => l.split(/\\s+/)[1] === '00000000'); const hex = line.split(/\\s+/)[2]; return [3, 2, 1, 0].map((i) => parseInt(hex.slice(i * 2, i * 2 + 2), 16)).join('.'); } catch { return '172.17.0.1'; } })();
591
+ const reached = await Promise.all([reach('169.254.169.254', 80), reach('10.0.0.1', 80), reach('192.168.0.1', 80), reach(gateway, 22), reach(gateway, 80)]);
592
+ out.isolation = reached.every((r) => !r);
593
+ }
594
+ process.stdout.write(JSON.stringify(out) + '\\n');
595
+ })();
596
+ `;
597
+ }
598
+ function parseProbeOutput(stdout, now) {
599
+ const line = stdout.trim().split("\n").reverse().find((l) => l.trim().startsWith("{"));
600
+ if (!line) return null;
601
+ try {
602
+ const doc = JSON.parse(line);
603
+ const tri = (v) => v === true ? true : v === false ? false : null;
604
+ return { cli: tri(doc.cli), home: tri(doc.home), mcp: tri(doc.mcp), isolation: tri(doc.isolation), at: now, detail: "" };
605
+ } catch {
606
+ return null;
607
+ }
608
+ }
609
+ async function probeImage(input) {
610
+ const now = input.now ?? Date.now;
611
+ const args = [
612
+ "run",
613
+ "--rm",
614
+ "--init",
615
+ "--pull",
616
+ "never",
617
+ "--label",
618
+ "com.kilogent.probe=1",
619
+ "--user",
620
+ input.user,
621
+ "--memory",
622
+ "512m",
623
+ "--memory-swap",
624
+ "512m",
625
+ ...input.runtime ? ["--runtime", input.runtime] : [],
626
+ ...input.network ? ["--network", input.network] : [],
627
+ ...input.extraArgs ?? [],
628
+ input.image,
629
+ "node",
630
+ "-e",
631
+ probeScript({ mcpUrl: input.mcpUrl, isolation: input.isolation })
632
+ ];
633
+ const result = await runCommand("docker", args, { env: dockerCliEnv(input.env ?? process.env), timeoutMs: 12e4 });
634
+ const parsed = parseProbeOutput(result.stdout, now());
635
+ if (parsed) return parsed;
636
+ return {
637
+ cli: false,
638
+ home: null,
639
+ mcp: null,
640
+ isolation: null,
641
+ at: now(),
642
+ detail: (result.stderr || "The probe container did not answer.").trim().split("\n")[0]?.slice(0, 200) ?? ""
643
+ };
644
+ }
645
+
646
+ // src/provision/host.ts
647
+ var SUPPORTED_HOSTS = Object.freeze([
648
+ { id: "ubuntu", versionId: "22.04", codename: "jammy" },
649
+ { id: "ubuntu", versionId: "24.04", codename: "noble" },
650
+ { id: "debian", versionId: "12", codename: "bookworm" }
651
+ ]);
652
+ var SESSION_BRIDGE = "kg-sessions";
653
+ var FIREWALL_CHAIN = "KG-SESSIONS";
654
+ 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"]);
655
+ function firewallStatus(iptablesS, resolvers, bridge = SESSION_BRIDGE) {
656
+ const rules = iptablesS.split("\n").map((l) => l.trim().split(/\s+/)).filter((t) => t.length > 1);
657
+ const has = (tokens, flag, value) => tokens.some((t, i) => t === flag && tokens[i + 1] === value);
658
+ const chain = rules.some((t) => t[0] === "-N" && t[1] === FIREWALL_CHAIN);
659
+ const ruleIn = (chain2, target) => rules.some((t) => t[0] === "-A" && t[1] === chain2 && has(t, "-i", bridge) && has(t, "-j", target));
660
+ const own = rules.filter((t) => t[0] === "-A" && t[1] === FIREWALL_CHAIN);
661
+ const dnsFor = (r, proto) => own.some((t) => has(t, "-d", `${r}/32`) && has(t, "-p", proto) && has(t, "--dport", "53") && has(t, "-j", "RETURN"));
662
+ const drops = BLOCKED_RANGES.every((range) => own.some((t) => has(t, "-d", range) && has(t, "-j", "DROP")));
663
+ return {
664
+ chain: chain && drops,
665
+ jumps: ruleIn("DOCKER-USER", FIREWALL_CHAIN) && ruleIn("INPUT", "DROP"),
666
+ dns: resolvers.length > 0 && resolvers.every((r) => dnsFor(r, "udp") && dnsFor(r, "tcp"))
667
+ };
668
+ }
669
+ function resolversFrom(resolvConf) {
670
+ 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."));
671
+ }
672
+ function sessionResolvConf(resolvers) {
673
+ const unique = [...new Set(resolvers)].slice(0, 3);
674
+ return unique.length ? `${unique.map((r) => `nameserver ${r}`).join("\n")}
675
+ options ndots:0
676
+ ` : null;
677
+ }
678
+
679
+ // src/launcher/handler.ts
680
+ function rootOwnedDir(dir) {
681
+ try {
682
+ const st = fs.lstatSync(dir);
683
+ return st.isDirectory() && !st.isSymbolicLink() && st.uid === 0 && (st.mode & 18) === 0;
684
+ } catch {
685
+ return false;
686
+ }
687
+ }
688
+ var HandlerFailure = class extends Error {
689
+ };
690
+ function prepare(settings, lease2, ctx) {
691
+ if (ctx.root && !rootOwnedDir(settings.sessionsRoot)) {
692
+ throw new HandlerFailure(`${settings.sessionsRoot} must be a root-owned directory that only root can write.`);
693
+ }
694
+ if (!ctx.root) fs.mkdirSync(settings.sessionsRoot, { recursive: true, mode: 493 });
695
+ const ownerDir = path.join(settings.sessionsRoot, String(settings.ownerUid));
696
+ if (!fs.existsSync(ownerDir)) fs.mkdirSync(ownerDir, { mode: 493 });
697
+ if (ctx.root && !rootOwnedDir(ownerDir)) throw new HandlerFailure(`${ownerDir} must be a root-owned directory that only root can write.`);
698
+ const dirs = leaseDirs(settings, lease2);
699
+ fs.rmSync(dirs.base, { recursive: true, force: true });
700
+ fs.mkdirSync(dirs.base, { mode: 493 });
701
+ fs.mkdirSync(dirs.work);
702
+ fs.mkdirSync(dirs.cfg);
703
+ fs.mkdirSync(dirs.home);
704
+ fs.chmodSync(dirs.home, 448);
705
+ fs.chmodSync(dirs.work, 1528);
706
+ fs.chmodSync(dirs.cfg, 1512);
707
+ if (ctx.root) {
708
+ fs.chownSync(dirs.work, settings.sessionUid, ctx.ownerGid);
709
+ fs.chownSync(dirs.cfg, settings.ownerUid, settings.sessionGid);
710
+ fs.chownSync(dirs.home, settings.sessionUid, settings.sessionGid);
711
+ }
712
+ writeResolvConf(dirs.resolv, ctx);
713
+ return { workdir: dirs.work, configDir: dirs.cfg };
714
+ }
715
+ function writeResolvConf(file, ctx) {
716
+ let resolvers = [];
717
+ try {
718
+ resolvers = resolversFrom(ctx.resolvConf());
719
+ } catch {
720
+ resolvers = [];
721
+ }
722
+ const text = sessionResolvConf(resolvers);
723
+ if (!text) return false;
724
+ fs.writeFileSync(file, text, { mode: 420 });
725
+ return true;
726
+ }
727
+ async function docker(args, ctx, timeoutMs = 6e4) {
728
+ return runCommand("docker", args, { env: ctx.dockerEnv, timeoutMs });
729
+ }
730
+ function goneOrRemoved(result) {
731
+ return result.ok || /no such container/i.test(result.stderr);
732
+ }
733
+ async function info(settings, ctx) {
734
+ const out = {
735
+ protocol: LAUNCHER_PROTOCOL,
736
+ security: LAUNCHER_SECURITY,
737
+ memoryWatch: MEMORY_WATCH_REVISION,
738
+ sessionEnv: SESSION_ENV_REVISION,
739
+ runtime: settings.runtime,
740
+ network: settings.network,
741
+ repository: settings.repository,
742
+ requireRuntime: settings.requireRuntime,
743
+ docker: { answers: false, detail: "" },
744
+ firewall: null,
745
+ runsc: null
746
+ };
747
+ const dockerInfo = await docker(["info", "--format", "{{json .}}"], ctx, 15e3);
748
+ if (!dockerInfo.ok) {
749
+ out.docker = { answers: false, detail: dockerInfo.stderr.trim().split("\n")[0]?.slice(0, 200) || "docker info did not answer." };
750
+ } else {
751
+ let doc = {};
752
+ try {
753
+ doc = JSON.parse(dockerInfo.stdout);
754
+ } catch {
755
+ }
756
+ out.docker = {
757
+ answers: true,
758
+ detail: "Docker answered.",
759
+ ...typeof doc.ServerVersion === "string" ? { serverVersion: doc.ServerVersion } : {},
760
+ ...doc.Runtimes && typeof doc.Runtimes === "object" ? { runtimes: Object.keys(doc.Runtimes).sort() } : {},
761
+ ...typeof doc.LiveRestoreEnabled === "boolean" ? { liveRestore: doc.LiveRestoreEnabled } : {},
762
+ ...typeof doc.MemTotal === "number" ? { memTotalBytes: doc.MemTotal } : {}
763
+ };
764
+ const version = await docker(["version", "--format", "{{json .Server.Components}}"], ctx, 15e3);
765
+ try {
766
+ const components = JSON.parse(version.stdout);
767
+ const containerd = components?.find((c) => c.Name === "containerd")?.Version;
768
+ if (containerd) out.docker.containerd = containerd.replace(/^v/, "");
769
+ } catch {
770
+ }
771
+ const network = await docker(["network", "inspect", settings.network, "--format", "{{json .Options}}"], ctx, 15e3);
772
+ out.docker.sessionNetwork = network.ok;
773
+ if (network.ok) {
774
+ try {
775
+ const options = JSON.parse(network.stdout);
776
+ out.docker.iccDisabled = options?.["com.docker.network.bridge.enable_icc"] === "false";
777
+ } catch {
778
+ out.docker.iccDisabled = false;
779
+ }
780
+ }
781
+ }
782
+ if (ctx.root) {
783
+ const saved = await runCommand("iptables", ["-S"], { env: ctx.dockerEnv, timeoutMs: 15e3 });
784
+ if (saved.ok) {
785
+ let resolvers = [];
786
+ try {
787
+ resolvers = resolversFrom(ctx.resolvConf());
788
+ } catch {
789
+ resolvers = [];
790
+ }
791
+ out.firewall = firewallStatus(saved.stdout, resolvers, settings.network);
792
+ }
793
+ }
794
+ const runsc = await runCommand("runsc", ["--version"], { env: ctx.dockerEnv, timeoutMs: 15e3 });
795
+ const match = runsc.ok ? /runsc version (\S+)/.exec(runsc.stdout) : null;
796
+ out.runsc = match?.[1] ?? null;
797
+ return out;
798
+ }
799
+ function memoryFilePath(settings, lease2) {
800
+ return path.join(leaseDirs(settings, lease2).base, "memory.json");
801
+ }
802
+ function writeMemoryFile(file, state) {
803
+ const tmp = `${file}.${process.pid}.tmp`;
804
+ try {
805
+ fs.rmSync(tmp, { force: true });
806
+ const fd = fs.openSync(tmp, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_NOFOLLOW, 420);
807
+ try {
808
+ fs.writeSync(fd, `${JSON.stringify(state)}
809
+ `);
810
+ } finally {
811
+ fs.closeSync(fd);
812
+ }
813
+ fs.renameSync(tmp, file);
814
+ } catch {
815
+ fs.rmSync(tmp, { force: true });
816
+ }
817
+ }
818
+ var MEMORY_FILE_FRESH_MS = 3e4;
819
+ function sessionsUsedMb(settings, now) {
820
+ const owner = path.join(settings.sessionsRoot, String(settings.ownerUid));
821
+ let sum = 0;
822
+ let leases = [];
823
+ try {
824
+ leases = fs.readdirSync(owner);
825
+ } catch {
826
+ return 0;
827
+ }
828
+ for (const lease2 of leases) {
829
+ try {
830
+ const doc = JSON.parse(fs.readFileSync(path.join(owner, lease2, "memory.json"), "utf8"));
831
+ if (doc.running !== true || typeof doc.updatedAt !== "number" || now - doc.updatedAt > MEMORY_FILE_FRESH_MS) continue;
832
+ sum += typeof doc.peakMb === "number" && doc.peakMb > 0 ? Number(doc.usedMb) || 0 : Number(doc.startMb) || 0;
833
+ } catch {
834
+ }
835
+ }
836
+ return sum;
837
+ }
838
+ async function growDecision(req, settings, ctx) {
839
+ if (!req.memory) return { plan: null, reason: "Sessions do not grow on this server." };
840
+ if (settings.runtime === "runsc") {
841
+ const out = await runCommand("runsc", ["--version"], { env: ctx.dockerEnv, timeoutMs: 15e3 });
842
+ const version = out.ok ? /runsc version (\S+)/.exec(out.stdout)?.[1] : void 0;
843
+ if (!version || runscOlder(version, RUNSC_LIVE_MEMORY_MIN) !== false) {
844
+ return { plan: null, reason: `gVisor ${version ?? "(unknown)"} cannot raise a running session's memory; it needs ${RUNSC_LIVE_MEMORY_MIN}.` };
845
+ }
846
+ }
847
+ return { plan: req.memory, reason: null };
848
+ }
849
+ async function run(req, settings, ctx) {
850
+ const decision = await growDecision(req, settings, ctx);
851
+ const argv = buildRunArgv(req, settings, {
852
+ resolvConf: fs.existsSync(leaseDirs(settings, req.lease).resolv),
853
+ ...decision.plan ? { startMb: decision.plan.startMb } : {}
854
+ });
855
+ const name = containerName(settings, req.lease);
856
+ const file = memoryFilePath(settings, req.lease);
857
+ const state = initialMemoryState({ tier: req.tier, plan: decision.plan, ceilingMb: TIERS[req.tier].memoryMb, reason: decision.reason, now: Date.now() });
858
+ writeMemoryFile(file, state);
859
+ const controller = new AbortController();
860
+ const oom = watchContainerOom(ctx.dockerSocket, name, controller.signal);
861
+ let stopped = false;
862
+ const cap = settings.sessionCapMb;
863
+ const watching = watchMemory({
864
+ state,
865
+ plan: decision.plan,
866
+ signal: controller.signal,
867
+ port: {
868
+ sample: () => containerMemory(ctx.dockerSocket, name),
869
+ apply: (mb) => updateContainerMemory(ctx.dockerSocket, name, mb),
870
+ ...cap ? { mayRaise: async (fromMb, toMb) => toMb <= fromMb || sessionsUsedMb(settings, Date.now()) + (toMb - fromMb) <= cap } : {},
871
+ onChange: (next) => writeMemoryFile(file, next),
872
+ now: Date.now,
873
+ sleep: abortableSleep
874
+ }
875
+ });
876
+ const code = await new Promise((resolve) => {
877
+ const child = spawn("docker", argv, { stdio: ["ignore", "inherit", "inherit"], env: { ...ctx.dockerEnv, ...req.env } });
878
+ const forward = (signal) => () => {
879
+ stopped = true;
880
+ child.kill(signal);
881
+ };
882
+ for (const signal of ["SIGTERM", "SIGINT", "SIGHUP"]) process.on(signal, forward(signal));
883
+ child.on("error", (e) => {
884
+ process.stderr.write(`kg-session-launcher: docker did not start: ${e.message}
885
+ `);
886
+ resolve(127);
887
+ });
888
+ child.on("exit", (exit, signal) => resolve(exit ?? (signal ? 143 : 1)));
889
+ });
890
+ controller.abort();
891
+ const last = await watching;
892
+ writeMemoryFile(file, {
893
+ ...last,
894
+ running: false,
895
+ oom: classifyExit({ exitCode: code, oomEvent: oom.seen(), stopped, lastUsedMb: last.usedMb, limitMb: last.limitMb, maxMb: last.maxMb }),
896
+ updatedAt: Date.now()
897
+ });
898
+ return code;
899
+ }
900
+ async function handleRequest(req, settings, ctx, out) {
901
+ try {
902
+ switch (req.op) {
903
+ case "info":
904
+ out.write(JSON.stringify(await info(settings, ctx)));
905
+ return 0;
906
+ case "prepare":
907
+ out.write(JSON.stringify(prepare(settings, req.lease, ctx)));
908
+ return 0;
909
+ case "run": {
910
+ await docker(["rm", "--force", containerName(settings, req.lease)], ctx);
911
+ return await run(req, settings, ctx);
912
+ }
913
+ case "stop":
914
+ return goneOrRemoved(await docker(["stop", "--time", "10", containerName(settings, req.lease)], ctx)) ? 0 : 1;
915
+ case "kill":
916
+ return goneOrRemoved(await docker(["rm", "--force", containerName(settings, req.lease)], ctx)) ? 0 : 1;
917
+ case "reap": {
918
+ const listed = await docker(["ps", "--all", "--quiet", "--filter", "label=com.kilogent.session=1", "--filter", `label=com.kilogent.owner=${settings.ownerUid}`], ctx);
919
+ const ids = listed.stdout.split("\n").map((l) => l.trim()).filter((l) => /^[a-f0-9]{12,64}$/.test(l));
920
+ const removed = ids.length ? await docker(["rm", "--force", ...ids], ctx) : { ok: true };
921
+ out.write(JSON.stringify({ removed: removed.ok ? ids.length : 0, found: ids.length }));
922
+ return listed.ok && removed.ok ? 0 : 1;
923
+ }
924
+ case "cleanup":
925
+ fs.rmSync(leaseDirs(settings, req.lease).base, { recursive: true, force: true });
926
+ return 0;
927
+ case "inspect": {
928
+ const present = await imagePresent(ctx.dockerSocket, req.image);
929
+ out.write(JSON.stringify({ present }));
930
+ return present === null ? 1 : 0;
931
+ }
932
+ case "prune": {
933
+ const present = await listRepositoryImages(ctx.dockerSocket, settings.repository);
934
+ let removed = 0;
935
+ for (const ref of imagesToRemove(present, req.keep)) if (await removeImage(ctx.dockerSocket, ref)) removed += 1;
936
+ out.write(JSON.stringify({ removed }));
937
+ return 0;
938
+ }
939
+ case "pull": {
940
+ const ref = parseDigestRef(req.image, { allowLocalRegistry: ctx.allowLocalRegistry });
941
+ if (!ref) {
942
+ out.write(JSON.stringify({ ok: false, kind: "registry", message: "The image is not a digest reference." }));
943
+ return 1;
944
+ }
945
+ const result = await pullByDigest({ socketPath: ctx.dockerSocket, image: ref, token: req.token });
946
+ out.write(JSON.stringify(result));
947
+ return result.ok ? 0 : 1;
948
+ }
949
+ case "probe": {
950
+ const home = path.join(settings.sessionsRoot, String(settings.ownerUid), `probe-home-${process.pid}`);
951
+ fs.rmSync(home, { recursive: true, force: true });
952
+ fs.mkdirSync(home, { recursive: true, mode: 448 });
953
+ fs.chmodSync(home, 448);
954
+ if (ctx.root) fs.chownSync(home, settings.sessionUid, settings.sessionGid);
955
+ const resolv = `${home}.resolv.conf`;
956
+ const dns = writeResolvConf(resolv, ctx) ? ["-v", `${resolv}:/etc/resolv.conf:ro`] : [];
957
+ try {
958
+ const result = await probeImage({
959
+ image: req.image,
960
+ mcpUrl: req.mcpUrl,
961
+ user: `${settings.sessionUid}:${settings.sessionGid}`,
962
+ runtime: settings.runtime,
963
+ network: settings.network,
964
+ isolation: true,
965
+ env: ctx.dockerEnv,
966
+ extraArgs: ["-e", `HOME=${CONTAINER_HOME}`, "-v", `${home}:${CONTAINER_HOME}`, ...dns]
967
+ });
968
+ out.write(JSON.stringify(result));
969
+ return 0;
970
+ } finally {
971
+ fs.rmSync(home, { recursive: true, force: true });
972
+ fs.rmSync(resolv, { force: true });
973
+ }
974
+ }
975
+ }
976
+ } catch (e) {
977
+ process.stderr.write(`kg-session-launcher: ${e instanceof Error ? e.message : String(e)}
978
+ `);
979
+ return 2;
980
+ }
981
+ }
982
+ async function readRequestText(max) {
983
+ const chunks = [];
984
+ let total = 0;
985
+ for await (const chunk of process.stdin) {
986
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
987
+ total += buf.length;
988
+ if (total > max) throw new HandlerFailure("The request is larger than 1 MiB.");
989
+ chunks.push(buf);
990
+ }
991
+ return Buffer.concat(chunks).toString("utf8");
992
+ }
993
+
994
+ // src/launcher/main.ts
995
+ var ROOT_PATH = "/usr/sbin:/usr/bin:/sbin:/bin";
996
+ function fail(message, code = 2) {
997
+ process.stderr.write(`kg-session-launcher: ${message}
998
+ `);
999
+ process.exit(code);
1000
+ }
1001
+ function ownerGid(uid) {
1002
+ const out = spawnSync("id", ["-g", String(uid)], { encoding: "utf8", env: { PATH: ROOT_PATH } });
1003
+ const gid = Number(out.stdout.trim());
1004
+ return Number.isInteger(gid) && gid > 0 ? gid : fail("Could not read the service user\u2019s group.");
1005
+ }
1006
+ async function main() {
1007
+ if (typeof process.getuid !== "function" || process.getuid() !== 0) fail("This runs as root, through sudo.");
1008
+ process.umask(18);
1009
+ let settings;
1010
+ try {
1011
+ settings = parseSettings(fs2.readFileSync(LAUNCHER_SETTINGS_PATH, "utf8"));
1012
+ } catch (e) {
1013
+ fail(e instanceof Error ? e.message : String(e));
1014
+ }
1015
+ const caller = Number(process.env.SUDO_UID);
1016
+ if (!Number.isInteger(caller) || caller !== settings.ownerUid) fail("Only the Kilogent service user may ask this launcher for anything.");
1017
+ let req;
1018
+ try {
1019
+ req = parseRequest(await readRequestText(MAX_REQUEST_BYTES), settings);
1020
+ } catch (e) {
1021
+ fail(e instanceof LauncherRefusal ? e.message : e instanceof Error ? e.message : String(e));
1022
+ }
1023
+ const code = await handleRequest(
1024
+ req,
1025
+ settings,
1026
+ {
1027
+ root: true,
1028
+ dockerSocket: "/var/run/docker.sock",
1029
+ dockerEnv: { PATH: ROOT_PATH },
1030
+ allowLocalRegistry: false,
1031
+ ownerGid: ownerGid(settings.ownerUid),
1032
+ // The same two files provision reads: on Ubuntu `/etc/resolv.conf` names only systemd-resolved's local
1033
+ // stub, and the real upstream resolvers are in the other one.
1034
+ resolvConf: () => ["/run/systemd/resolve/resolv.conf", "/etc/resolv.conf"].map((file) => {
1035
+ try {
1036
+ return fs2.readFileSync(file, "utf8");
1037
+ } catch {
1038
+ return "";
1039
+ }
1040
+ }).join("\n")
1041
+ },
1042
+ { write: (line) => process.stdout.write(`${line}
1043
+ `) }
1044
+ );
1045
+ process.exit(code);
1046
+ }
1047
+ void main();