@outerlayer/cli 0.33.0-next.20261021171323 → 0.33.0-next.20261021194303

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/README.md +5 -1
  2. package/dist/{agent-setup-NSUX64RU.js → agent-setup-P5PWEGZM.js} +5 -5
  3. package/dist/{build-HU24C46Z.js → build-SK5BYBER.js} +4 -4
  4. package/dist/build-info.json +1 -1
  5. package/dist/{builder-VFREWHZA.js → builder-X4PIGYS6.js} +5 -5
  6. package/dist/{check-3TQJDGKM.js → check-DPVNDAQQ.js} +32 -32
  7. package/dist/chunk-2PGI6RDE.js +90 -0
  8. package/dist/{chunk-ZZSQYOY5.js → chunk-3VGDZ23W.js} +1 -1
  9. package/dist/{chunk-7LP3F2QH.js → chunk-4OKOLFPH.js} +3 -3
  10. package/dist/{chunk-NJS7JJYN.js → chunk-52C3SSNF.js} +2 -2
  11. package/dist/{chunk-IWYS4ERC.js → chunk-533QGXGZ.js} +1 -1
  12. package/dist/{chunk-54GBOMDW.js → chunk-543ZX5TM.js} +3 -3
  13. package/dist/{chunk-OX7CHZTJ.js → chunk-5ZUT3IK3.js} +1 -1
  14. package/dist/{chunk-ZBN4JGTJ.js → chunk-A3H27SYX.js} +1 -1
  15. package/dist/{chunk-P7BEZAEU.js → chunk-ALSE7YKX.js} +11 -3
  16. package/dist/{chunk-G7QBSH3X.js → chunk-BBK62GXM.js} +1256 -589
  17. package/dist/{chunk-NYKZC6HU.js → chunk-C4N5QRQJ.js} +3 -3
  18. package/dist/{chunk-CI6PRMWZ.js → chunk-CG5TWQ23.js} +5 -5
  19. package/dist/{chunk-TZQIEUDE.js → chunk-CNRMP6EN.js} +1 -1
  20. package/dist/{chunk-LAS3DOTW.js → chunk-ENV3AIWQ.js} +42 -18
  21. package/dist/{chunk-KP6R7CV3.js → chunk-ESP5VZBD.js} +4 -1
  22. package/dist/{chunk-RQGHHEM7.js → chunk-HBBCQ5NE.js} +2 -2
  23. package/dist/{chunk-T5TBGOT6.js → chunk-IPJ6HR7E.js} +1 -1
  24. package/dist/{chunk-APSYTJ6I.js → chunk-JT3YO5MH.js} +1 -1
  25. package/dist/{chunk-ZZWWKBLL.js → chunk-KUOY5UIR.js} +4 -4
  26. package/dist/{chunk-X7UUIHGH.js → chunk-LH3KULPL.js} +1 -1
  27. package/dist/{chunk-LK2S5XZH.js → chunk-LHYO4RDP.js} +9 -1
  28. package/dist/{chunk-LSMF3EET.js → chunk-LPIUIRHA.js} +10 -10
  29. package/dist/{chunk-BXA6JLHQ.js → chunk-LTZVMKQS.js} +5 -5
  30. package/dist/{chunk-WHSYPR3U.js → chunk-M5ZXOQYS.js} +1 -1
  31. package/dist/{chunk-VXYPD33W.js → chunk-NNNUME4Y.js} +5 -5
  32. package/dist/{chunk-KQON2T4A.js → chunk-OVSUSOHL.js} +23 -7
  33. package/dist/{chunk-NHXJU7CM.js → chunk-OXSFOLWE.js} +1 -1
  34. package/dist/{chunk-77F7NGAZ.js → chunk-TA6TFYAN.js} +3 -3
  35. package/dist/{chunk-EKMMX5A5.js → chunk-TAZJDD2U.js} +4 -4
  36. package/dist/{chunk-TI5Y242B.js → chunk-TGXYNEI7.js} +57 -89
  37. package/dist/{chunk-BSHUTMXV.js → chunk-TPGHCH2H.js} +45 -19
  38. package/dist/{chunk-AR6UB2BP.js → chunk-UVZ43SW3.js} +3 -3
  39. package/dist/{chunk-HPOW2VCB.js → chunk-VFW6I4GG.js} +2 -2
  40. package/dist/{chunk-F3EF56LQ.js → chunk-VQ2JGZSC.js} +1 -1
  41. package/dist/{chunk-D73OLAYE.js → chunk-WRX2MCDV.js} +1 -1
  42. package/dist/{chunk-LQFDTQC2.js → chunk-XQTT3222.js} +3 -3
  43. package/dist/{chunk-SOTJ2V3A.js → chunk-XRZGT54P.js} +6 -6
  44. package/dist/{chunk-GKNF4D62.js → chunk-XT5SOJRC.js} +48 -25
  45. package/dist/{chunk-H6Y5D6J7.js → chunk-YZIC5JXC.js} +1 -1
  46. package/dist/{chunk-JKXTG3BV.js → chunk-ZPW6PUNK.js} +3 -3
  47. package/dist/{claude-launcher-RNMUZZ3Y.js → claude-launcher-Q7YAPLY2.js} +1 -1
  48. package/dist/{cli-YQ64SSV3.js → cli-OGX6JNIN.js} +98 -98
  49. package/dist/{command-RFVCDSJV.js → command-UIAQUHHU.js} +22 -21
  50. package/dist/{config-3JRFDYZL.js → config-CUJ3Q4L2.js} +1 -1
  51. package/dist/{connect-cmd-HVHPNVAJ.js → connect-cmd-OLESV5O3.js} +5 -5
  52. package/dist/{container-start-DP3N72CK.js → container-start-MGY5VFHW.js} +22 -21
  53. package/dist/{control-plane-launch-DHMD5WBU.js → control-plane-launch-7PXJSMVM.js} +4 -4
  54. package/dist/{dist-MKY2BSYN.js → dist-DY2ZNEBS.js} +1 -1
  55. package/dist/{docker-6G6IF4YO.js → docker-XRUAUQIG.js} +5 -5
  56. package/dist/{doctor-ZE4LA7EZ.js → doctor-ZMKO3F7I.js} +31 -28
  57. package/dist/{emit-artifact-cmd-YRRXCZAI.js → emit-artifact-cmd-RI7HA37S.js} +10 -10
  58. package/dist/{emit-criteria-cmd-PSQORVHS.js → emit-criteria-cmd-T5HZZ7XJ.js} +6 -6
  59. package/dist/{emit-demo-cmd-RJPSISCC.js → emit-demo-cmd-MWRFZHSX.js} +13 -93
  60. package/dist/{emit-finding-cmd-6FVW3GGK.js → emit-finding-cmd-RN7LSGJ7.js} +7 -7
  61. package/dist/{emit-result-cmd-PFQ6HSVN.js → emit-result-cmd-72STGWMP.js} +9 -9
  62. package/dist/{emit-step-cmd-U32Y4PZJ.js → emit-step-cmd-POFAEM7G.js} +7 -7
  63. package/dist/{findings-cmd-AMYNXO3S.js → findings-cmd-TMURLT3A.js} +7 -7
  64. package/dist/{fleet-readiness-7ROJ54ZF.js → fleet-readiness-N6Y3F7R6.js} +4 -4
  65. package/dist/{governing-source-DYJQCHKU.js → governing-source-2R7IS7LZ.js} +9 -9
  66. package/dist/{hook-fast-WQQYEXO5.js → hook-fast-GO7UTNN5.js} +1 -1
  67. package/dist/{host-key-KAYMYRO3.js → host-key-UEWE4PXB.js} +2 -2
  68. package/dist/{image-tag-NM2YT2J2.js → image-tag-V2LXTKK7.js} +2 -2
  69. package/dist/index.js +4 -4
  70. package/dist/{init-QDUIHSTK.js → init-E3VWTJFQ.js} +21 -20
  71. package/dist/{init-CHWFDK23.js → init-YMU4QFRV.js} +21 -20
  72. package/dist/{init-cmd-STCU6RGY.js → init-cmd-WBPE3X7T.js} +10 -10
  73. package/dist/{init-update-OIFOUIAN.js → init-update-MONZSG7L.js} +8 -8
  74. package/dist/{install-OIRIEOS7.js → install-3DBYJITP.js} +3 -3
  75. package/dist/{install-cmd-IYWVR6WX.js → install-cmd-EELSKBSZ.js} +4 -4
  76. package/dist/{logs-C3RJ3ZTC.js → logs-ET75ZUCN.js} +1 -1
  77. package/dist/{loop-NUW7SSZY.js → loop-N3IUN4F3.js} +32 -32
  78. package/dist/{machine-BJ7TT3B7.js → machine-J7QFINBX.js} +17 -16
  79. package/dist/{mcp-serve-cmd-PB4UOTGL.js → mcp-serve-cmd-WYUP263T.js} +3 -3
  80. package/dist/paths-BN7X2YPM.js +9 -0
  81. package/dist/{pending-RKVRYOTF.js → pending-ROAIK5WO.js} +2 -2
  82. package/dist/{pidfile-BMFAUQAI.js → pidfile-52TGLGQH.js} +2 -2
  83. package/dist/{record-26AH6WDB.js → record-BSY4BKJ5.js} +2 -2
  84. package/dist/{runner-env-66RAFIDJ.js → runner-env-H6XLACTG.js} +4 -4
  85. package/dist/{runner-identity-FL67WQIA.js → runner-identity-MSMI2PD4.js} +5 -5
  86. package/dist/{runner-key-YHAJFEGN.js → runner-key-QTNYQ2I7.js} +21 -20
  87. package/dist/{session-sender-PHQ6PJVD.js → session-sender-USH6NOZL.js} +10 -10
  88. package/dist/{spec-cmd-SAMHN5FU.js → spec-cmd-ABGR2ZDH.js} +7 -7
  89. package/dist/{status-QWLX2CCV.js → status-FHEVPRWN.js} +22 -22
  90. package/dist/{sync-cmd-O6UIF4QJ.js → sync-cmd-GC2TBSUO.js} +9 -9
  91. package/dist/{watch-5HMOSQ4Z.js → watch-F3LCBTNO.js} +9 -9
  92. package/dist/{work-claim-cmd-6W7BW4O6.js → work-claim-cmd-X7KNYGU7.js} +10 -10
  93. package/dist/{work-cmd-3G26E3NL.js → work-cmd-AI3ZPCP7.js} +7 -7
  94. package/dist/{work-comment-cmd-S6EGHGHL.js → work-comment-cmd-6UXS342S.js} +7 -7
  95. package/dist/{work-logs-cmd-RQILRNHS.js → work-logs-cmd-TVKWMDOW.js} +7 -7
  96. package/dist/{work-open-pr-cmd-5CDP6KOF.js → work-open-pr-cmd-WZ7W2HZ7.js} +8 -8
  97. package/dist/{work-pr-cmd-D3PJJQOX.js → work-pr-cmd-KTNNR46X.js} +7 -7
  98. package/dist/{work-stop-cmd-AGZWXGRP.js → work-stop-cmd-2BUDSXUU.js} +7 -7
  99. package/package.json +1 -1
  100. package/dist/chunk-YYYRBYWG.js +0 -32
  101. package/dist/paths-OK2EZS7W.js +0 -9
@@ -1,20 +1,21 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'node:module';
3
- import { execCapture } from './chunk-QQGZ5THT.js';
4
- import { GUEST_CID } from './chunk-HIGJTX5U.js';
5
- import { keepStagedContext, describeCallFailure, controlPlaneTokenFailure, refusalCode, resolveGoverningSource, removeStagedContext, stageGovernedContext, pinnedSourceOf, commandWithShim, shimScript, WORKFLOW_PERMISSION_REASON } from './chunk-BXA6JLHQ.js';
6
- import { runnerImageVersion } from './chunk-IWYS4ERC.js';
3
+ import { sha256OfFile, downloadPinned, VoiceDownloadError } from './chunk-2PGI6RDE.js';
4
+ import { runExec, execCapture, waitForAgent } from './chunk-QQGZ5THT.js';
5
+ import { GUEST_CID, linkVsockSockets } from './chunk-HIGJTX5U.js';
6
+ import { describeCallFailure, keepStagedContext, controlPlaneTokenFailure, refusalCode, resolveGoverningSource, removeStagedContext, stageGovernedContext, pinnedSourceOf, commandWithShim, shimScript, WORKFLOW_PERMISSION_REASON } from './chunk-LTZVMKQS.js';
7
+ import { runnerImageVersion } from './chunk-533QGXGZ.js';
7
8
  import { hostAllowed } from './chunk-UXYLWVTQ.js';
8
- import { readDefaultBranchHead, cloneRepository, buildRepositoryImage } from './chunk-BSHUTMXV.js';
9
+ import { readDefaultBranchHead, cloneRepository, buildRepositoryImage, prepareImageBuild, KEY_PREFIX, FETCHED_DIR, toolsDockerfile, TAG_HOST, metadataFromLabels, writeRecord, FETCHED_FFMPEG, FETCHED_GH, FETCHED_TINI, FETCHED_VOICE_DIR } from './chunk-TPGHCH2H.js';
9
10
  import { startTimeOf, isSameProcess } from './chunk-JBJTC3UU.js';
10
11
  import { currentCliBuild } from './chunk-OGFGZQA3.js';
11
- import { createDockerHost, builderSpec, runnerLabelValue, RUNNER_LABEL, BUILD_LABEL, prepareRunArgs, containerRunArgs, HASH_ARGV, parseProgramHash, JUDGE_PROGRAM, containerExecArgs, childEnvironment, JUDGE_DIR, MAX_ITEMS_BYTES, JUDGE_ITEMS, MAX_REPORT_BYTES, JUDGE_REPORT, spawnRun } from './chunk-ZZWWKBLL.js';
12
+ import { createDockerHost, builderSpec, runnerLabelValue, RUNNER_LABEL, BUILD_LABEL, prepareRunArgs, containerRunArgs, HASH_ARGV, parseProgramHash, JUDGE_PROGRAM, containerExecArgs, childEnvironment, JUDGE_DIR, MAX_ITEMS_BYTES, JUDGE_ITEMS, MAX_REPORT_BYTES, JUDGE_REPORT, spawnRun } from './chunk-KUOY5UIR.js';
12
13
  import { readDu } from './chunk-YJLHUKYS.js';
13
- import { RecipeError, DOCKER_FEATURE } from './chunk-3Z3MPR55.js';
14
- import { FIRECRACKER_VERSION, hostHelperPinned, firecrackerArch, MICROVM_ARCHES, CLAUDE_CODE_VERSION, GUEST_HELPER_SHA256, GUEST_ASSETS_BASE_URL, GUEST_KERNEL_SHA256, JAILER_BINARY_SHA256, FIRECRACKER_BINARY_SHA256, FIRECRACKER_SHA256, GUEST_ASSETS_VERSION, DEFAULT_BASE_IMAGE } from './chunk-LK2S5XZH.js';
15
- import { WorkCommandError } from './chunk-SOTJ2V3A.js';
16
- import { setCustomScrubbers, setLocalIdentity, setRepoRoot, parseTranscript, scrubDeep, scrubText } from './chunk-GKNF4D62.js';
17
- import { vmAssetsDir, RUNNER_DIR_MODE, rootfsPathFor, diskTemplatesDir, RUNNER_FILE_MODE, deliveryDir, ghConfigDir, runnerDirOfBuild, spoolDir, mirrorDir, buildDir, SHIM_DIR } from './chunk-KP6R7CV3.js';
14
+ import { RecipeError, DOCKER_FEATURE, writeSanitizedRecipe, classifyProperties } from './chunk-3Z3MPR55.js';
15
+ import { FIRECRACKER_VERSION, hostHelperPinned, firecrackerArch, MICROVM_ARCHES, CLAUDE_CODE_VERSION, GUEST_HELPER_SHA256, GUEST_ASSETS_BASE_URL, GUEST_KERNEL_SHA256, JAILER_BINARY_SHA256, FIRECRACKER_BINARY_SHA256, FIRECRACKER_SHA256, GUEST_ASSETS_VERSION, BUILDER_DISK_VERSION, builderDiskPinned, FFMPEG_STATIC, FFMPEG_BUILD_TAG, GH_SHA256, GH_VERSION, TINI_SHA256, TINI_VERSION, VOICE_PACK_FILES, BUILDER_DISK_SHA256, BUILDER_DISK_BASE_URL, DEFAULT_BASE_IMAGE } from './chunk-LHYO4RDP.js';
16
+ import { WorkCommandError } from './chunk-XRZGT54P.js';
17
+ import { setCustomScrubbers, setLocalIdentity, setRepoRoot, parseTranscript, scrubDeep, scrubText } from './chunk-XT5SOJRC.js';
18
+ import { rootfsPathFor, diskTemplatesDir, RUNNER_FILE_MODE, buildsDir, buildDir, vmAssetsDir, RUNNER_DIR_MODE, deliveryDir, ghConfigDir, runnerDirOfBuild, spoolDir, mirrorDir, SHIM_DIR, runnerDir, vsockPath, builderCacheDiskPath } from './chunk-ESP5VZBD.js';
18
19
  import { repositoriesOf, primaryOf, RELAY_PORT, repositoryPath, CHECKOUT_DIR, lifecycleWorkdir, claudeCredential, buildEnvironment, startBuildServices, checkEnvironment, AGENT_CREDENTIAL_MISSING, checkoutPath, JOB_MOUNT } from './chunk-HDSDUTX4.js';
19
20
  import { sharedSkillNames, skillConflictMessage } from './chunk-FGF52V4V.js';
20
21
  import { versionAtLeast, MINIMUM_CLAUDE_VERSION } from './chunk-HCVLP7XA.js';
@@ -22,14 +23,14 @@ import { orderSessions, extractFacts, sessionsToTimeline } from './chunk-JUXATN4
22
23
  import { downconvertSession } from './chunk-3WW2TL7V.js';
23
24
  import { realFdStorePort } from './chunk-XD4ZCMA4.js';
24
25
  import { RELAY_USER, RELAY_ID, UNKNOWN_TRANSPORT_REASON, DOCKER_DESKTOP_REASON, MAX_MALFORMED_SESSIONS, MALFORMED_REASON, JOIN_WAIT_MS, MAX_OPEN_STREAMS, STREAM_RESET_RATE, STREAM_RESET_BURST, STREAM_WINDOW, SESSION_WINDOW, targetOfPath } from './chunk-MESOOLF4.js';
25
- import { readFileSync, mkdirSync, existsSync, mkdtempSync, rmSync, chmodSync, truncateSync, writeFileSync, readdirSync, lstatSync, copyFileSync, accessSync, constants, statSync, rmdirSync, renameSync, unlinkSync, openSync, closeSync, createWriteStream, createReadStream, writeSync } from 'fs';
26
- import { join, dirname, resolve, basename } from 'path';
26
+ import { statfsSync, readFileSync, existsSync, mkdirSync, chmodSync, truncateSync, rmSync, statSync, readdirSync, mkdtempSync, writeFileSync, lstatSync, copyFileSync, accessSync, constants, rmdirSync, renameSync, unlinkSync, linkSync, openSync, closeSync, cpSync, writeSync, appendFileSync, createWriteStream, createReadStream } from 'fs';
27
+ import { dirname, join, relative, resolve, basename } from 'path';
27
28
  import { isIP, createConnection, connect, createServer as createServer$1 } from 'net';
28
29
  import { spawnSync, spawn } from 'child_process';
29
- import { availableParallelism, networkInterfaces } from 'os';
30
30
  import { randomBytes, createHash } from 'crypto';
31
+ import { availableParallelism, networkInterfaces, tmpdir } from 'os';
32
+ import { Duplex, PassThrough, Transform } from 'stream';
31
33
  import http2 from 'http2';
32
- import { Duplex, Transform } from 'stream';
33
34
  import { createServer, request } from 'http';
34
35
  import { request as request$1 } from 'https';
35
36
  import { pipeline, finished } from 'stream/promises';
@@ -249,6 +250,978 @@ function jailedAlive(unit, cgroupRoot = "/sys/fs/cgroup") {
249
250
  function realHelperPort(env = process.env) {
250
251
  return { probe: () => probeHelper(env[HELPER_ENV]), alive: (unit) => jailedAlive(unit) };
251
252
  }
253
+ function freeBytes(path, statfs = statfsSync) {
254
+ let at = path;
255
+ for (; ; ) {
256
+ try {
257
+ const stats = statfs(at);
258
+ return Number(stats.bavail) * Number(stats.bsize);
259
+ } catch (err) {
260
+ if (err.code !== "ENOENT") return void 0;
261
+ const parent = dirname(at);
262
+ if (parent === at) return void 0;
263
+ at = parent;
264
+ }
265
+ }
266
+ }
267
+ var UNITS = ["B", "KB", "MB", "GB", "TB"];
268
+ function formatBytes(bytes) {
269
+ let value = Math.max(0, bytes);
270
+ let unit = 0;
271
+ while (value >= 1024 && unit < UNITS.length - 1) {
272
+ value /= 1024;
273
+ unit++;
274
+ }
275
+ return unit === 0 ? `${Math.round(value)} B` : `${value.toFixed(1)} ${UNITS[unit]}`;
276
+ }
277
+ var STOP_GRACE_SECONDS = 15;
278
+ function parseImageConfig(json) {
279
+ const config = JSON.parse(json);
280
+ const env = {};
281
+ for (const entry of config.Env ?? []) {
282
+ const eq = entry.indexOf("=");
283
+ if (eq > 0) env[entry.slice(0, eq)] = entry.slice(eq + 1);
284
+ }
285
+ return { user: config.User ?? "", env };
286
+ }
287
+ function createContainerHost(deps = {}) {
288
+ const run = deps.run ?? spawnRun;
289
+ return {
290
+ async version() {
291
+ const result = await run("docker", ["version", "--format", "{{.Server.Version}}"]);
292
+ return result.status === 0 && result.stdout.trim() !== "" ? result.stdout.trim() : null;
293
+ },
294
+ async operatingSystem() {
295
+ const result = await run("docker", ["info", "--format", "{{.OperatingSystem}}"]);
296
+ return result.status === 0 && result.stdout.trim() !== "" ? result.stdout.trim() : null;
297
+ },
298
+ async imageConfig(imageId) {
299
+ const result = await run("docker", ["image", "inspect", "--format", "{{json .Config}}", imageId]);
300
+ return result.status === 0 ? parseImageConfig(result.stdout) : { user: "", env: {} };
301
+ },
302
+ createVolume(name, labels) {
303
+ const flags = Object.entries(labels).flatMap(([key, value]) => ["--label", `${key}=${value}`]);
304
+ return run("docker", ["volume", "create", ...flags, name]);
305
+ },
306
+ removeVolume(name) {
307
+ return run("docker", ["volume", "rm", "--force", name]);
308
+ },
309
+ run(args, opts) {
310
+ return run("docker", ["run", ...args], opts);
311
+ },
312
+ exec(input, opts) {
313
+ const env = {};
314
+ for (const [name, value] of Object.entries(input.env.passed)) if (value !== void 0) env[name] = value;
315
+ return run("docker", ["exec", ...containerExecArgs(input)], { ...opts, env, ...input.stdin === void 0 ? {} : { input: input.stdin } });
316
+ },
317
+ execDetached(input) {
318
+ const env = {};
319
+ for (const [name, value] of Object.entries(input.env.passed)) if (value !== void 0) env[name] = value;
320
+ return run("docker", ["exec", ...containerExecArgs({ ...input, detached: true })], { env });
321
+ },
322
+ async state(container) {
323
+ const result = await run("docker", ["inspect", "--format", "{{.State.Running}}", container]);
324
+ if (result.status !== 0) return /no such (object|container)/i.test(result.stderr) ? "missing" : "unknown";
325
+ return result.stdout.trim() === "true" ? "running" : "stopped";
326
+ },
327
+ execStream(input) {
328
+ const child = (deps.spawn ?? spawn)("docker", ["exec", ...containerExecArgs({ ...input, interactive: true })], {
329
+ env: childEnvironment(process.env),
330
+ stdio: ["pipe", "pipe", "pipe"]
331
+ });
332
+ let stderr = "";
333
+ child.stderr?.on("data", (chunk) => {
334
+ stderr = (stderr + chunk.toString()).slice(-400);
335
+ });
336
+ child.stderr?.on("error", () => void 0);
337
+ const exited = new Promise((resolve2) => {
338
+ child.once("error", () => resolve2(-1));
339
+ child.once("close", (code) => resolve2(code ?? -1));
340
+ });
341
+ child.stdin.on("error", () => void 0);
342
+ return { stdin: child.stdin, stdout: child.stdout, exited, kill: () => void child.kill("SIGKILL"), stderrTail: () => stderr };
343
+ },
344
+ stop(container) {
345
+ return run("docker", ["stop", "--time", String(STOP_GRACE_SECONDS), container]);
346
+ },
347
+ start(container) {
348
+ return run("docker", ["start", container]);
349
+ },
350
+ remove(container) {
351
+ return run("docker", ["rm", "--force", "--volumes", container]);
352
+ }
353
+ };
354
+ }
355
+ function converterScript(workDir = "/tmp/rootfs") {
356
+ return [
357
+ "set -eu",
358
+ `rootfs=${workDir}`,
359
+ 'mkdir -p "$rootfs"',
360
+ 'tar -xf - -C "$rootfs"',
361
+ 'used_kb=$(du -sk "$rootfs" | cut -f1)',
362
+ 'files=$(find "$rootfs" | wc -l)',
363
+ // The file is made roomy and sparse, so that populating it cannot run out of space, and is shrunk to the smallest size that holds the files afterwards.
364
+ "total_kb=$(( used_kb * 2 + 262144 ))",
365
+ 'truncate -s "${total_kb}K" "/out/$OUT_NAME"',
366
+ 'mke2fs -q -F -t ext4 -m 0 -N $(( files + files / 4 + 4096 )) -d "$rootfs" "/out/$OUT_NAME"',
367
+ // e2fsck exits 1 when it repaired something; resize2fs -M needs the check to have run.
368
+ 'e2fsck -f -y "/out/$OUT_NAME" > /dev/null || [ $? -lt 4 ]',
369
+ 'resize2fs -M "/out/$OUT_NAME" > /dev/null 2>&1',
370
+ 'chown "$HOST_UID:$HOST_GID" "/out/$OUT_NAME"',
371
+ // In the builder microVM the extracted file system sits on the cache disk, which the next builder reuses.
372
+ ...workDir === "/tmp/rootfs" ? [] : ['rm -rf "$rootfs"']
373
+ ].join("\n");
374
+ }
375
+ function templateScript() {
376
+ return [
377
+ "set -eu",
378
+ 'truncate -s "$BYTES" "/out/$OUT_NAME"',
379
+ 'mke2fs -q -F -t ext4 -m 0 -E lazy_itable_init=1,lazy_journal_init=1,nodiscard "/out/$OUT_NAME"',
380
+ 'chown "$HOST_UID:$HOST_GID" "/out/$OUT_NAME"'
381
+ ].join("\n");
382
+ }
383
+ var ids = () => ({ uid: String(process.getuid?.() ?? 0), gid: String(process.getgid?.() ?? 0) });
384
+ function converterArgs(dest, memoryBytes, extra, script) {
385
+ const { uid, gid } = ids();
386
+ return [
387
+ "run",
388
+ "--rm",
389
+ "-i",
390
+ "--network",
391
+ "none",
392
+ "--memory",
393
+ String(memoryBytes),
394
+ "--memory-swap",
395
+ String(memoryBytes),
396
+ "--mount",
397
+ `type=bind,src=${dirname(dest)},dst=/out`,
398
+ "-e",
399
+ `OUT_NAME=${basename(dest)}`,
400
+ "-e",
401
+ `HOST_UID=${uid}`,
402
+ "-e",
403
+ `HOST_GID=${gid}`,
404
+ ...extra,
405
+ // The image that runs `mke2fs` as root is the default base image: Ubuntu holds e2fsprogs.
406
+ DEFAULT_BASE_IMAGE,
407
+ "sh",
408
+ "-c",
409
+ script
410
+ ];
411
+ }
412
+ var firstLine = (text) => text.trim().split("\n")[0] ?? "";
413
+ function exportInto(container, converter) {
414
+ return new Promise((resolve2, reject) => {
415
+ const exporter = spawn("docker", ["export", container], { stdio: ["ignore", "pipe", "pipe"], env: childEnvironment(process.env) });
416
+ const writer = spawn("docker", converter, { stdio: ["pipe", "ignore", "pipe"], env: childEnvironment(process.env) });
417
+ const errors = [];
418
+ exporter.stderr.on("data", (chunk) => errors.push(`docker export: ${chunk.toString("utf8")}`));
419
+ writer.stderr.on("data", (chunk) => errors.push(`converter: ${chunk.toString("utf8")}`));
420
+ writer.stdin.on("error", () => void 0);
421
+ exporter.stdout.on("error", () => void 0);
422
+ exporter.stdout.pipe(writer.stdin);
423
+ exporter.once("error", reject);
424
+ writer.once("error", reject);
425
+ let exits = 0;
426
+ let failure2;
427
+ const done = (name, code) => {
428
+ if (code !== 0 && failure2 === void 0) failure2 = `${name} exited ${code}`;
429
+ exits += 1;
430
+ if (exits < 2) return;
431
+ if (failure2 === void 0) resolve2();
432
+ else reject(new Error(`${failure2}: ${firstLine(errors.join("\n"))}`));
433
+ };
434
+ exporter.once("exit", (code) => done("docker export", code));
435
+ writer.once("exit", (code) => done("the converter", code));
436
+ });
437
+ }
438
+ function createRootfsHost() {
439
+ const env = () => childEnvironment(process.env);
440
+ return {
441
+ async convert({ imageId, dest, memoryBytes }) {
442
+ const created = await spawnRun("docker", ["create", imageId, "none"], { env: env() });
443
+ if (created.status !== 0) throw new Error(`docker create ${imageId} failed: ${firstLine(created.stderr)}`);
444
+ const container = created.stdout.trim();
445
+ try {
446
+ await exportInto(container, converterArgs(dest, memoryBytes, [], converterScript()));
447
+ } finally {
448
+ await spawnRun("docker", ["rm", "--force", "--volumes", container], { env: env() });
449
+ }
450
+ },
451
+ async makeTemplate({ bytes, dest, memoryBytes }) {
452
+ const made = await spawnRun("docker", converterArgs(dest, memoryBytes, ["-e", `BYTES=${bytes}`], templateScript()), { env: env() });
453
+ if (made.status !== 0) throw new Error(`making the empty build disk failed: ${firstLine(made.stderr)}`);
454
+ },
455
+ async imageConfig(imageId) {
456
+ const inspected = await spawnRun("docker", ["image", "inspect", "--format", "{{json .Config}}", imageId], { env: env() });
457
+ if (inspected.status !== 0) throw new Error(`docker image inspect ${imageId} failed: ${firstLine(inspected.stderr)}`);
458
+ return parseImageConfig(inspected.stdout);
459
+ }
460
+ };
461
+ }
462
+ function sweepStalePartials(dir) {
463
+ let names;
464
+ try {
465
+ names = readdirSync(dir);
466
+ } catch {
467
+ return;
468
+ }
469
+ for (const name of names) {
470
+ const pid = /\.partial-(\d+)-[0-9a-f]+$/.exec(name)?.[1];
471
+ if (pid === void 0 || Number(pid) === process.pid) continue;
472
+ try {
473
+ process.kill(Number(pid), 0);
474
+ } catch (err) {
475
+ if (err.code === "ESRCH") rmSync(join(dir, name), { force: true });
476
+ }
477
+ }
478
+ }
479
+ async function madeAtomically(path, make) {
480
+ mkdirSync(dirname(path), { recursive: true, mode: RUNNER_DIR_MODE });
481
+ const temp = `${path}.partial-${process.pid}-${randomBytes(3).toString("hex")}`;
482
+ sweepStalePartials(dirname(path));
483
+ try {
484
+ await make(temp);
485
+ renameSync(temp, path);
486
+ } catch (err) {
487
+ rmSync(temp, { force: true });
488
+ throw err;
489
+ }
490
+ }
491
+ async function ensureRootfs(input) {
492
+ const path = rootfsPathFor(input.runnerDir, input.imageId);
493
+ if (existsSync(path)) return path;
494
+ const { uid, gid } = ids();
495
+ await madeAtomically(path, (temp) => input.host.convert({ imageId: input.imageId, dest: temp, uid: Number(uid), gid: Number(gid), memoryBytes: input.memoryBytes }));
496
+ return path;
497
+ }
498
+ async function createBuildDisk(input) {
499
+ const template = join(diskTemplatesDir(input.runnerDir), `${input.bytes}.ext4`);
500
+ if (!existsSync(template)) {
501
+ const { uid, gid } = ids();
502
+ await madeAtomically(template, (temp) => input.host.makeTemplate({ bytes: input.bytes, dest: temp, uid: Number(uid), gid: Number(gid), memoryBytes: input.memoryBytes }));
503
+ }
504
+ mkdirSync(dirname(input.dest), { recursive: true });
505
+ if (input.host.copyTemplate) input.host.copyTemplate(template, input.dest);
506
+ else {
507
+ const copied = spawnSync("cp", ["--sparse=always", template, input.dest], { encoding: "utf8" });
508
+ if (copied.status !== 0) throw new Error(`copying the empty build disk failed: ${firstLine(copied.stderr)}`);
509
+ }
510
+ chmodSync(input.dest, RUNNER_FILE_MODE);
511
+ truncateSync(input.dest, input.bytes);
512
+ }
513
+ function deleteBuildDisk(path) {
514
+ rmSync(path, { force: true });
515
+ }
516
+ var CONSOLE_CUT_NOTE = "[outerlayer runner: cut the console at 8 MiB; the microVM keeps running]\n";
517
+ function pumpConsole(fd, streams) {
518
+ const cap = 8 * 1024 ** 2;
519
+ let written = 0;
520
+ let lastByte = 10;
521
+ let cut = false;
522
+ const append = (data) => {
523
+ try {
524
+ writeSync(fd, data);
525
+ } catch {
526
+ }
527
+ };
528
+ const onData = (chunk) => {
529
+ if (cut) return;
530
+ const room = cap - written;
531
+ if (chunk.length <= room) {
532
+ append(chunk);
533
+ written += chunk.length;
534
+ lastByte = chunk[chunk.length - 1] ?? lastByte;
535
+ return;
536
+ }
537
+ const head = chunk.subarray(0, room);
538
+ append(head);
539
+ if (head.length > 0) lastByte = head[head.length - 1];
540
+ append(Buffer.from(`${lastByte === 10 ? "" : "\n"}${CONSOLE_CUT_NOTE}`));
541
+ written = cap;
542
+ cut = true;
543
+ };
544
+ let open = streams.length;
545
+ for (const stream of streams) {
546
+ stream.on("data", onData);
547
+ stream.on("error", () => void 0);
548
+ stream.once("close", () => {
549
+ open -= 1;
550
+ if (open === 0) closeSync(fd);
551
+ });
552
+ }
553
+ }
554
+
555
+ // src/runner/microvm/firecracker.ts
556
+ var clearBit = (bit) => `0b${"x".repeat(31 - bit)}0${"x".repeat(bit)}`;
557
+ var CPU_TEMPLATE = {
558
+ cpuid_modifiers: [
559
+ { leaf: "0x1", subleaf: "0x0", flags: 0, modifiers: [{ register: "ecx", bitmap: clearBit(5) }] },
560
+ { leaf: "0x80000001", subleaf: "0x0", flags: 0, modifiers: [{ register: "ecx", bitmap: clearBit(2) }] }
561
+ ]
562
+ };
563
+ var MIB = 1024 ** 2;
564
+ var INIT = "/opt/outerlayer/bin/outerlayer-guest";
565
+ function machineConfig(limits2, paths) {
566
+ const bootArgs = [
567
+ "console=ttyS0",
568
+ "reboot=k",
569
+ "panic=1",
570
+ "pci=off",
571
+ "i8042.noaux",
572
+ "i8042.nomux",
573
+ "i8042.nopnp",
574
+ "i8042.dumbkbd",
575
+ // The pinned guest kernel builds pressure stall information default-disabled; the guest usage record reads it.
576
+ "psi=1",
577
+ "ro",
578
+ "root=/dev/vda",
579
+ "rootfstype=ext4",
580
+ `init=${INIT}`,
581
+ ...paths.relayPorts.length === 0 ? [] : [`outerlayer.relay=${paths.relayPorts.join(",")}`],
582
+ ...paths.leasePort === void 0 ? [] : [`outerlayer.lease=${paths.leasePort}`],
583
+ ...paths.docker === true ? ["outerlayer.docker=1"] : [],
584
+ // The kernel passes an argument of this form on to the init process as an environment variable.
585
+ ...paths.guestInit === void 0 ? [] : [`OUTERLAYER_GUEST_VM_INIT=${paths.guestInit}`]
586
+ ];
587
+ return {
588
+ "boot-source": { kernel_image_path: paths.kernel, boot_args: bootArgs.join(" ") },
589
+ drives: [
590
+ { drive_id: "rootfs", path_on_host: paths.rootfs, is_root_device: true, is_read_only: true },
591
+ { drive_id: "build", path_on_host: paths.buildDisk, is_root_device: false, is_read_only: false }
592
+ ],
593
+ "machine-config": { vcpu_count: limits2.cpus, mem_size_mib: Math.max(1, Math.floor(limits2.memoryBytes / MIB)), smt: false },
594
+ vsock: { guest_cid: GUEST_CID, uds_path: paths.vsock },
595
+ "cpu-config": CPU_TEMPLATE
596
+ };
597
+ }
598
+ function firecrackerCommand(input) {
599
+ const args = ["--no-api", "--config-file", input.configPath];
600
+ const binary = resolve(input.binary);
601
+ if (input.cgroup === void 0) return { command: binary, args };
602
+ return { command: "/bin/sh", args: ["-c", 'echo $$ > "$1/cgroup.procs" && shift && exec "$@"', "sh", input.cgroup, binary, ...args] };
603
+ }
604
+ var STOP_WAIT_MS = 1e4;
605
+ var POLL_MS = 20;
606
+ function isZombie(pid) {
607
+ try {
608
+ const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
609
+ return stat.slice(stat.lastIndexOf(")") + 2, stat.lastIndexOf(")") + 3) === "Z";
610
+ } catch {
611
+ return false;
612
+ }
613
+ }
614
+ function createFirecrackerHost(options = {}) {
615
+ const launch = options.launch ?? firecrackerCommand;
616
+ const spawnChild = options.spawnChild ?? spawn;
617
+ const exits = /* @__PURE__ */ new Map();
618
+ const alive = (vm) => isSameProcess(vm.pid, vm.start) && !isZombie(vm.pid);
619
+ return {
620
+ async spawn(input) {
621
+ const { command, args } = launch(input);
622
+ mkdirSync(dirname(input.consolePath), { recursive: true });
623
+ const consoleFd = openSync(input.consolePath, "a", 384);
624
+ let pumping = false;
625
+ try {
626
+ const child = spawnChild(command, args, { detached: true, stdio: ["ignore", "pipe", "pipe"], env: {} });
627
+ pumpConsole(consoleFd, [child.stdout, child.stderr]);
628
+ pumping = true;
629
+ for (const stream of [child.stdout, child.stderr]) stream.unref?.();
630
+ const failed = new Promise((_resolve, reject) => child.once("error", reject));
631
+ const pid = child.pid;
632
+ if (pid === void 0) return await failed;
633
+ exits.set(pid, new Promise((resolve2) => child.once("exit", () => resolve2())));
634
+ child.unref();
635
+ const start = startTimeOf(pid);
636
+ if (start === null) throw new Error(`${command} exited as soon as it started; its console is at ${input.consolePath}`);
637
+ return { pid, start };
638
+ } finally {
639
+ if (!pumping) closeSync(consoleFd);
640
+ }
641
+ },
642
+ async stop(vm) {
643
+ if (!alive(vm)) return;
644
+ try {
645
+ process.kill(vm.pid, "SIGKILL");
646
+ } catch {
647
+ }
648
+ const exited = exits.get(vm.pid);
649
+ const deadline = Date.now() + STOP_WAIT_MS;
650
+ if (exited !== void 0) await Promise.race([exited, new Promise((resolve2) => setTimeout(resolve2, STOP_WAIT_MS))]);
651
+ while (alive(vm) && Date.now() < deadline) await new Promise((resolve2) => setTimeout(resolve2, POLL_MS));
652
+ exits.delete(vm.pid);
653
+ },
654
+ alive
655
+ };
656
+ }
657
+ function unitNameFor(buildId) {
658
+ return `outerlayer-build-${buildId}`;
659
+ }
660
+ function linkIn(buildDir2, name, source) {
661
+ const target = join(buildDir2, name);
662
+ rmSync(target, { force: true });
663
+ try {
664
+ linkSync(source, target);
665
+ } catch (err) {
666
+ if (err.code === "EXDEV") {
667
+ throw new Error(`${source} and ${buildDir2} are on different file systems, so the build's files cannot be linked into its jail`);
668
+ }
669
+ throw err;
670
+ }
671
+ chmodSync(source, 292);
672
+ }
673
+ async function startJailed(input) {
674
+ linkIn(input.buildDir, "kernel", input.kernel);
675
+ linkIn(input.buildDir, "rootfs.ext4", input.rootfs);
676
+ for (const name of input.sockets) {
677
+ try {
678
+ chmodSync(join(input.buildDir, name), 438);
679
+ } catch (err) {
680
+ if (err.code !== "ENOENT") throw err;
681
+ }
682
+ }
683
+ return input.client.start({ build: input.buildId, kind: input.kind, ...input.timeLimitMinutes === void 0 ? {} : { timeLimitMinutes: input.timeLimitMinutes } });
684
+ }
685
+ async function startJailedInto(state, input, save) {
686
+ state.jailed = { unit: unitNameFor(state.buildId), chroot: "" };
687
+ save();
688
+ try {
689
+ const started = await startJailed({ ...input, buildId: state.buildId, buildDir: state.buildDir });
690
+ state.jailed = { unit: started.unit, chroot: started.chroot };
691
+ state.vsockPath = started.vsock;
692
+ } catch (err) {
693
+ if (err instanceof HelperRefusal) {
694
+ delete state.jailed;
695
+ save();
696
+ }
697
+ throw err;
698
+ }
699
+ save();
700
+ }
701
+ async function stopJailed(helper, state) {
702
+ const probe = await helper.probe();
703
+ if (!probe.ok) throw new Error(`the build's unit ${state.jailed?.unit ?? unitNameFor(state.buildId)} could not be stopped, because there is no helper to ask: ${probe.detail}`);
704
+ try {
705
+ await probe.client.stop(state.buildId);
706
+ } catch (err) {
707
+ throw new Error(`the build's unit ${state.jailed?.unit ?? unitNameFor(state.buildId)} was not stopped: ${describeCallFailure(err)}`);
708
+ }
709
+ }
710
+ function makeBuildsDirPrivate(buildDir2) {
711
+ const builds = dirname(buildDir2);
712
+ const stat = statSync(builds);
713
+ if (stat.uid === process.getuid?.() && (stat.mode & 18) !== 0) chmodSync(builds, 448);
714
+ }
715
+ var MANAGED_UNJAILED_REASON = "the root helper does not answer, and a managed runner starts no build without the jailer. The build did not start; restore the helper with `outerlayer runner install --service --replace`";
716
+
717
+ // src/runner/microvm/image-builder.ts
718
+ var ROOT_DISK_ID_PREFIX = "rootdisk:";
719
+ var BUILDER_INIT = "/opt/outerlayer/builder-init";
720
+ var TUNNEL_PORT = RELAY_PORT;
721
+ var ENGINE_PROXY = `http://127.0.0.1:${TUNNEL_PORT}`;
722
+ var BRIDGE_ADDRESS = "172.17.0.1";
723
+ var STEP_PROXY = `http://${BRIDGE_ADDRESS}:${TUNNEL_PORT}`;
724
+ var NO_PROXY = "localhost,127.0.0.1,::1";
725
+ var TUNNEL_SOCKET = "tunnel.sock";
726
+ var RECORD_FILE = "image-builder.json";
727
+ var BOOT_ATTEMPTS = 120;
728
+ var BOOT_POLL_MS = 500;
729
+ var ROOT_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
730
+ var KEEP_RECORDS = 3;
731
+ var LOG_TAIL_LINES = 30;
732
+ var MAX_PENDING_BYTES = 64 * 1024;
733
+ var MAX_LOG_BYTES = 64 * 1024 * 1024;
734
+ var RESULT_PREFIX = "OL-RESULT ";
735
+ var REFUSALS_NAMED = 5;
736
+ var GUEST_OUTPUT = "/out/rootfs.ext4";
737
+ var DOCKER_WAIT_SECONDS = 90;
738
+ var BUILD_CACHE_KEEP = "2GB";
739
+ function builderMachineConfig(limits2, paths) {
740
+ return machineConfig(limits2, { ...paths, guestInit: BUILDER_INIT });
741
+ }
742
+ function daemonConfig() {
743
+ return { "data-root": "/cache/docker", bip: `${BRIDGE_ADDRESS}/16`, proxies: { "http-proxy": ENGINE_PROXY, "https-proxy": ENGINE_PROXY, "no-proxy": NO_PROXY } };
744
+ }
745
+ function clientConfig() {
746
+ return { proxies: { default: { httpProxy: STEP_PROXY, httpsProxy: STEP_PROXY, noProxy: NO_PROXY } } };
747
+ }
748
+ function plain(name, value, allowed) {
749
+ if (!allowed.test(value)) throw new Error(`${name} holds a character the builder's script cannot carry safely: ${JSON.stringify(value)}`);
750
+ return value;
751
+ }
752
+ function builderGuestScript(parts) {
753
+ const recipeTag = plain("the recipe tag", parts.recipeTag, /^[A-Za-z0-9_.:@/-]+$/);
754
+ const buildTag = plain("the image tag", parts.buildTag, /^[A-Za-z0-9_.:@/-]+$/);
755
+ const build = parts.recipe.kind === "default" ? `docker build --progress plain --tag ${recipeTag} --file /work/recipe/Dockerfile /work/recipe` : `devcontainer build --workspace-folder /work/repo --config /work/repo/${plain("the recipe path", parts.recipe.config, /^[A-Za-z0-9_./-]+$/)} --image-name ${recipeTag} --log-format text`;
756
+ return [
757
+ "set -eu",
758
+ `export PATH=${ROOT_PATH}`,
759
+ // The devcontainer CLI, BuildKit and the engine read the environment; Node's fetch reads it only when asked.
760
+ `export HTTP_PROXY=${ENGINE_PROXY}`,
761
+ `export HTTPS_PROXY=${ENGINE_PROXY}`,
762
+ `export http_proxy=${ENGINE_PROXY}`,
763
+ `export https_proxy=${ENGINE_PROXY}`,
764
+ `export NO_PROXY=${NO_PROXY}`,
765
+ `export no_proxy=${NO_PROXY}`,
766
+ "export NODE_USE_ENV_PROXY=1",
767
+ "export DOCKER_CONFIG=/run/builder/docker DOCKER_BUILDKIT=1",
768
+ "mkdir -p /run/builder/docker /cache/docker",
769
+ // Whatever way the script ends, the cache keeps the recipe image and a bounded build cache, so a repository's cache disk cannot fill with the images of old recipes.
770
+ "trim() {",
771
+ ` docker image ls --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -vxF -e '<none>:<none>' -e ${recipeTag} | xargs -r docker rmi > /dev/null 2>&1 || true`,
772
+ " docker image prune --force > /dev/null 2>&1 || true",
773
+ ` docker builder prune --force --keep-storage ${BUILD_CACHE_KEEP} > /dev/null 2>&1 || true`,
774
+ "}",
775
+ "trap trim EXIT",
776
+ "cat > /run/builder/daemon.json <<'OL_DAEMON_JSON'",
777
+ JSON.stringify(daemonConfig(), null, 2),
778
+ "OL_DAEMON_JSON",
779
+ "cat > /run/builder/docker/config.json <<'OL_CLIENT_JSON'",
780
+ JSON.stringify(clientConfig(), null, 2),
781
+ "OL_CLIENT_JSON",
782
+ "cat > /run/builder/convert.sh <<'OL_CONVERT'",
783
+ converterScript("/cache/rootfs"),
784
+ "OL_CONVERT",
785
+ // The forwarder binds the bridge address before the engine makes the bridge.
786
+ "echo 1 > /proc/sys/net/ipv4/ip_nonlocal_bind",
787
+ "mount -t cgroup2 cgroup2 /sys/fs/cgroup 2>/dev/null || true",
788
+ `node /work/tools/dist/index.js vm-forward --listen ${BRIDGE_ADDRESS}:${TUNNEL_PORT} --to 127.0.0.1:${TUNNEL_PORT} > /run/builder/forward.log 2>&1 &`,
789
+ "dockerd --config-file /run/builder/daemon.json > /run/builder/dockerd.log 2>&1 &",
790
+ "waited=0",
791
+ "until docker info > /dev/null 2>&1; do",
792
+ " waited=$((waited + 1))",
793
+ ` if [ "$waited" -gt ${DOCKER_WAIT_SECONDS} ]; then echo "the Docker engine in the builder did not start:"; tail -n 20 /run/builder/dockerd.log; exit 1; fi`,
794
+ " sleep 1",
795
+ "done",
796
+ build,
797
+ `docker build --progress plain --tag ${buildTag} --file /work/tools/Dockerfile /work/tools`,
798
+ `docker image inspect --format '{{json .Config}}' ${buildTag} > /run/builder/config.json`,
799
+ // The conversion puts the extracted file system and the root disk beside the layers. The inputs and Docker's copies
800
+ // of the build contexts are not needed for it, and the tool files in them are large, so they go first.
801
+ "rm -rf /work/repo /work/recipe /work/tools/fetched",
802
+ "docker builder prune --force --filter type=source.local > /dev/null 2>&1 || true",
803
+ `container=$(docker create ${buildTag} none)`,
804
+ 'docker export "$container" | OUT_NAME=rootfs.ext4 HOST_UID=0 HOST_GID=0 sh /run/builder/convert.sh',
805
+ 'docker rm --force --volumes "$container" > /dev/null',
806
+ `printf '${RESULT_PREFIX}{"config":%s,"bytes":%s}\\n' "$(cat /run/builder/config.json)" "$(stat -c %s ${GUEST_OUTPUT})"`,
807
+ ""
808
+ ].join("\n");
809
+ }
810
+ function tarArchive(dir, exclude) {
811
+ const child = spawn("tar", ["-C", dir, ...exclude.map((name) => `--exclude=${name}`), "-cf", "-", "."], { stdio: ["ignore", "pipe", "pipe"] });
812
+ const errors = [];
813
+ child.stderr.on("data", (chunk) => errors.push(chunk));
814
+ const out = new PassThrough();
815
+ child.stdout.pipe(out, { end: false });
816
+ child.once("error", (err) => out.destroy(err));
817
+ out.once("close", () => child.kill());
818
+ child.once("close", (code) => {
819
+ if (code === 0) out.end();
820
+ else out.destroy(new Error(`tar could not pack ${dir}: ${Buffer.concat(errors).toString("utf8").trim().split("\n")[0] ?? `exit ${code}`}`));
821
+ });
822
+ return out;
823
+ }
824
+ function builderInput(parts, archive = tarArchive) {
825
+ return [
826
+ { target: "/work/repo", stream: archive(parts.checkoutDir, [".git"]) },
827
+ { target: "/work/tools", stream: archive(parts.toolsDir, []) },
828
+ ...parts.toolFilesDir === void 0 ? [] : [{ target: `/work/tools/${FETCHED_DIR}`, stream: archive(parts.toolFilesDir, []) }],
829
+ ...parts.recipeDir === void 0 ? [] : [{ target: "/work/recipe", stream: archive(parts.recipeDir, []) }]
830
+ ];
831
+ }
832
+ function parseCacheSpace(stdout) {
833
+ const fields = stdout.trim().split("\n").at(-1)?.trim().split(/\s+/) ?? [];
834
+ if (fields.length < 6 || fields[5] !== "/cache") return void 0;
835
+ const total = Number(fields[1]) * 1024;
836
+ const free = Number(fields[3]) * 1024;
837
+ return Number.isFinite(total) && Number.isFinite(free) && total > 0 ? { free, total } : void 0;
838
+ }
839
+ var StepFailed = class extends Error {
840
+ };
841
+ function readStored(prepared) {
842
+ const path = join(prepared.recordDir, "rootfs.ext4");
843
+ const recordPath = join(prepared.recordDir, "rootdisk.json");
844
+ if (!existsSync(path) || !existsSync(recordPath)) return void 0;
845
+ try {
846
+ const stored = JSON.parse(readFileSync(recordPath, "utf8"));
847
+ if (typeof stored.user !== "string" || typeof stored.env !== "object" || stored.env === null || typeof stored.report !== "object" || stored.report === null) return void 0;
848
+ if (statSync(path).size !== stored.bytes) return void 0;
849
+ return {
850
+ reused: true,
851
+ recipeBuilt: false,
852
+ toolsBuilt: false,
853
+ checkedCommit: prepared.checkout.commit,
854
+ recordDir: prepared.recordDir,
855
+ report: stored.report,
856
+ removed: [],
857
+ rootfs: { path, env: stored.env, user: stored.user }
858
+ };
859
+ } catch {
860
+ return void 0;
861
+ }
862
+ }
863
+ function writeBuilderRecord(dir, record) {
864
+ writeFileSync(join(dir, RECORD_FILE), `${JSON.stringify(record)}
865
+ `, { mode: 384 });
866
+ }
867
+ function refusalReason(refusals, logPath) {
868
+ const named = refusals.slice(0, REFUSALS_NAMED).map((r) => r.range === "not-allowed" ? `${r.host} (not in runner.build.allowHosts)` : `${r.host} (${r.range})`);
869
+ const more = refusals.length > REFUSALS_NAMED ? ` and ${refusals.length - REFUSALS_NAMED} more` : "";
870
+ return `the image build called ${named.join(", ")}${more}, which the tunnel refuses. An image build reaches only the public hosts in runner.build.allowHosts, and no private address. The build log is at ${logPath}`;
871
+ }
872
+ function pruneRecords(repoRecords, current) {
873
+ const recordNames = readdirSync(repoRecords).filter((name) => /^[0-9a-f]{12}-[0-9a-f]{12}$/.test(name) && join(repoRecords, name) !== current);
874
+ const aged = recordNames.map((name) => ({ name, at: statSync(join(repoRecords, name)).mtimeMs })).sort((a, b) => b.at - a.at);
875
+ for (const { name } of aged.slice(KEEP_RECORDS - 1)) rmSync(join(repoRecords, name), { recursive: true, force: true });
876
+ }
877
+ async function buildRootDiskInMicroVm(opts, deps) {
878
+ const prepared = await (deps.prepare ?? prepareImageBuild)(opts);
879
+ try {
880
+ const stored = readStored(prepared);
881
+ if (stored !== void 0) return stored;
882
+ return await buildInBuilder(opts, deps, prepared);
883
+ } finally {
884
+ prepared.release();
885
+ }
886
+ }
887
+ async function buildInBuilder(opts, deps, prepared) {
888
+ const { ports } = deps;
889
+ const sleep = deps.sleep ?? ((ms) => new Promise((resolve2) => setTimeout(resolve2, ms)));
890
+ const archive = deps.archive ?? tarArchive;
891
+ const now = opts.now ?? (() => /* @__PURE__ */ new Date());
892
+ const { repository, owner, name, checkout, location, recipe, repoRecords, recordDir, logPath } = prepared;
893
+ const rKey = prepared.recipeKey.slice(0, KEY_PREFIX);
894
+ const tKey = prepared.toolsKey.slice(0, KEY_PREFIX);
895
+ const recipeTag = `${TAG_HOST}/recipe/${repository}:${rKey}`;
896
+ const buildTag = `${TAG_HOST}/build/${repository}:${rKey}-${tKey}`;
897
+ const runner = runnerDir(opts.configPath);
898
+ const builderDisk = await ports.builderDisk(runner);
899
+ const toolFilesDir = await ports.toolFiles(runner);
900
+ const probe = await ports.helper.probe();
901
+ const helper = probe.ok ? probe.client : void 0;
902
+ if (helper === void 0 && opts.managed === true) throw new RecipeError("image_builder_unavailable", MANAGED_UNJAILED_REASON);
903
+ const id = randomBytes(6).toString("hex");
904
+ const dir = buildDir(opts.configPath, id);
905
+ const state = { buildId: id, buildDir: dir, vsockPath: vsockPath(dir) };
906
+ const record = { owner, name, id };
907
+ const cachePath = builderCacheDiskPath(opts.configPath, owner, name);
908
+ const diskPath = join(dir, "build.ext4");
909
+ const partial = join(recordDir, "rootfs.ext4.partial");
910
+ const scratchRoot = opts.tmpDir ?? tmpdir();
911
+ let scratch;
912
+ let tunnel;
913
+ let sanitized;
914
+ let timedOut = false;
915
+ const controller = new AbortController();
916
+ const timer = opts.deadlineMs === void 0 ? void 0 : setTimeout(() => {
917
+ timedOut = true;
918
+ controller.abort();
919
+ }, Math.max(0, opts.deadlineMs - Date.now()));
920
+ const tail = [];
921
+ let result;
922
+ let pending = "";
923
+ let logged = 0;
924
+ let hostFailure;
925
+ const failOnHost = (what, err) => {
926
+ hostFailure ??= `${what}: ${describeCallFailure(err)}`;
927
+ controller.abort();
928
+ };
929
+ const onOutput = (chunk) => {
930
+ if (hostFailure !== void 0) return;
931
+ if (logged < MAX_LOG_BYTES) {
932
+ const kept = chunk.subarray(0, MAX_LOG_BYTES - logged);
933
+ try {
934
+ appendFileSync(logPath, kept);
935
+ if (logged + kept.length >= MAX_LOG_BYTES) appendFileSync(logPath, "\n[the build log is cut here: it reached its size limit]\n");
936
+ } catch (err) {
937
+ failOnHost(`the build log ${logPath} could not be written`, err);
938
+ return;
939
+ }
940
+ logged += kept.length;
941
+ }
942
+ pending += chunk.toString("utf8");
943
+ const lines = pending.split("\n");
944
+ pending = lines.pop() ?? "";
945
+ if (pending.length > MAX_PENDING_BYTES) {
946
+ lines.push(pending);
947
+ pending = "";
948
+ }
949
+ for (const line of lines) {
950
+ if (line.startsWith(RESULT_PREFIX)) result = line.slice(RESULT_PREFIX.length);
951
+ else {
952
+ tail.push(line);
953
+ if (tail.length > LOG_TAIL_LINES) tail.shift();
954
+ }
955
+ }
956
+ };
957
+ const exec = (argv) => ({ argv, user: "root", cwd: "/", env: { PATH: ROOT_PATH } });
958
+ const check = (status, what) => {
959
+ if (hostFailure !== void 0) throw new StepFailed(`${what} stopped: ${hostFailure}`);
960
+ if (controller.signal.aborted) throw new StepFailed(`${what} was stopped at the time limit`);
961
+ if (status !== 0) throw new StepFailed(`${what} failed (exit ${status})`);
962
+ };
963
+ const cacheSpace = async () => {
964
+ try {
965
+ const df = await ports.exec(state.vsockPath, exec(["df", "-P", "-k", "/cache"]), { signal: controller.signal });
966
+ return df.status === 0 ? parseCacheSpace(df.stdout) : void 0;
967
+ } catch {
968
+ return void 0;
969
+ }
970
+ };
971
+ const spaceNote = async () => {
972
+ const space = await cacheSpace();
973
+ return space === void 0 ? "" : `; the builder's cache disk had ${formatBytes(space.free)} of ${formatBytes(space.total)} free (runner.build.disk)`;
974
+ };
975
+ let inputs = [];
976
+ let built;
977
+ let failure2;
978
+ try {
979
+ scratch = mkdtempSync(join(scratchRoot, "ol-builder-"));
980
+ mkdirSync(dir, { recursive: true, mode: 448 });
981
+ chmodSync(dir, 448);
982
+ makeBuildsDirPrivate(dir);
983
+ writeBuilderRecord(dir, record);
984
+ mkdirSync(dirname(logPath), { recursive: true, mode: RUNNER_DIR_MODE });
985
+ opts.log?.(`building the image in a builder microVM at ${opts.memory ?? "the default memory"}`);
986
+ mkdirSync(dirname(cachePath), { recursive: true, mode: RUNNER_DIR_MODE });
987
+ if (existsSync(cachePath) && statSync(cachePath).size !== opts.limits.diskBytes) rmSync(cachePath, { force: true });
988
+ if (existsSync(cachePath)) renameSync(cachePath, diskPath);
989
+ else await createBuildDisk({ host: ports.rootfs, runnerDir: runner, bytes: opts.limits.diskBytes, dest: diskPath, memoryBytes: opts.limits.memoryBytes });
990
+ tunnel = await deps.startTunnel(join(dir, TUNNEL_SOCKET), {
991
+ ...deps.tunnelOptions,
992
+ ...opts.allowHosts === void 0 ? {} : { allowHosts: opts.allowHosts },
993
+ recordPath: join(dir, "tunnel.json")
994
+ });
995
+ const configPath = join(dir, "vm.json");
996
+ if (helper === void 0) linkVsockSockets({ buildDir: dir, vsockPath: state.vsockPath, links: [{ port: TUNNEL_PORT, socket: TUNNEL_SOCKET }] });
997
+ writeFileSync(
998
+ configPath,
999
+ JSON.stringify(builderMachineConfig(opts.limits, { kernel: opts.kernel, rootfs: builderDisk, buildDisk: diskPath, vsock: state.vsockPath, relayPorts: [TUNNEL_PORT] }), null, 2),
1000
+ { mode: 384 }
1001
+ );
1002
+ if (helper !== void 0) {
1003
+ record.jailed = true;
1004
+ writeBuilderRecord(dir, record);
1005
+ await startJailedInto(state, { client: helper, kind: "build", kernel: opts.kernel, rootfs: builderDisk, sockets: [TUNNEL_SOCKET], timeLimitMinutes: opts.timeLimitMinutes }, () => void 0);
1006
+ } else {
1007
+ state.cgroup = ports.cgroup.create(id, opts.limits.memoryBytes);
1008
+ if (state.cgroup !== void 0) record.cgroup = state.cgroup;
1009
+ state.firecracker = await ports.firecracker.spawn({ binary: opts.firecracker, configPath, consolePath: join(dir, "console.log"), cgroup: state.cgroup });
1010
+ record.firecracker = state.firecracker;
1011
+ writeBuilderRecord(dir, record);
1012
+ }
1013
+ if (!await waitForAgent(state.vsockPath, { attempts: BOOT_ATTEMPTS, delayMs: BOOT_POLL_MS, sleep, onFailure: () => void 0 })) {
1014
+ throw new StepFailed(`the builder microVM did not start: its exec agent did not answer${state.jailed === void 0 ? ` (console in ${join(dir, "console.log")})` : `; its console output is in the journal of the unit ${state.jailed.unit}`}`);
1015
+ }
1016
+ const space = await cacheSpace();
1017
+ check(0, "checking the builder's cache disk");
1018
+ if (space !== void 0 && space.free * 2 < space.total) {
1019
+ check((await ports.exec(state.vsockPath, exec(["rm", "-rf", "/cache/docker"]), { signal: controller.signal })).status, "emptying the builder's layer cache");
1020
+ opts.log?.(`emptied the builder's layer cache: ${formatBytes(space.free)} of ${formatBytes(space.total)} was free`);
1021
+ }
1022
+ let recipeParts = { kind: "default" };
1023
+ const recipeDir = join(scratch, "recipe");
1024
+ mkdirSync(recipeDir);
1025
+ if (location.kind === "default") writeFileSync(join(recipeDir, "Dockerfile"), prepared.defaultText);
1026
+ else {
1027
+ sanitized = writeSanitizedRecipe(checkout.dir, location, recipe);
1028
+ recipeParts = { kind: "file", config: relative(checkout.dir, sanitized) };
1029
+ }
1030
+ const toolsDir = join(scratch, "tools");
1031
+ mkdirSync(toolsDir);
1032
+ cpSync(opts.cli.distDir, join(toolsDir, "dist"), { recursive: true });
1033
+ cpSync(join(opts.cli.distDir, "..", "package.json"), join(toolsDir, "package.json"));
1034
+ cpSync(join(opts.cli.distDir, "..", "skill-pack"), join(toolsDir, "skill-pack"), { recursive: true });
1035
+ mkdirSync(join(toolsDir, "guest"));
1036
+ cpSync(opts.guestHelper.path, join(toolsDir, "guest", "outerlayer-guest"));
1037
+ mkdirSync(join(toolsDir, FETCHED_DIR));
1038
+ writeFileSync(
1039
+ join(toolsDir, "Dockerfile"),
1040
+ toolsDockerfile({ recipeImage: recipeTag, nodeImage: prepared.nodeImage, claudeCodeVersion: prepared.claudeCodeVersion, buildId: opts.cli.buildId, guestHelper: true, fetched: true })
1041
+ );
1042
+ inputs = builderInput({ checkoutDir: checkout.dir, toolsDir, toolFilesDir, recipeDir }, archive);
1043
+ for (const { target, stream } of inputs) {
1044
+ let status;
1045
+ try {
1046
+ status = (await ports.exec(state.vsockPath, exec(["tar", "-x", "--no-same-owner", "-C", target]), { stdin: stream, signal: controller.signal })).status;
1047
+ } catch (err) {
1048
+ if (hostFailure !== void 0 || controller.signal.aborted) throw err;
1049
+ throw new StepFailed(`unpacking ${target} in the builder failed: ${err instanceof Error ? err.message : String(err)}${await spaceNote()}`);
1050
+ }
1051
+ if (status !== 0 && hostFailure === void 0 && !controller.signal.aborted) throw new StepFailed(`unpacking ${target} in the builder failed (exit ${status})${await spaceNote()}`);
1052
+ check(status, `unpacking ${target} in the builder`);
1053
+ }
1054
+ const script = builderGuestScript({ recipe: recipeParts, recipeTag, buildTag });
1055
+ const ran = await ports.execStream(state.vsockPath, exec(["/bin/sh", "-c", script]), { signal: controller.signal, onStdout: onOutput, onStderr: onOutput });
1056
+ check(ran.status, "the image build in the builder microVM");
1057
+ if (result === void 0) throw new StepFailed("the image build in the builder microVM ended without describing the image");
1058
+ const described = JSON.parse(result);
1059
+ const imageConfig = parseImageConfig(JSON.stringify(described.config));
1060
+ const labels = described.config.Labels ?? {};
1061
+ mkdirSync(recordDir, { recursive: true, mode: RUNNER_DIR_MODE });
1062
+ const fd = openSync(partial, "w", 384);
1063
+ let received = 0;
1064
+ try {
1065
+ const copied = await ports.execStream(state.vsockPath, exec(["cat", GUEST_OUTPUT]), {
1066
+ signal: controller.signal,
1067
+ onStdout: (chunk) => {
1068
+ if (hostFailure !== void 0) return;
1069
+ if (received + chunk.length > opts.limits.diskBytes) return failOnHost("the root disk is larger than build.disk", new Error(`more than ${opts.limits.diskBytes} bytes`));
1070
+ try {
1071
+ writeSync(fd, chunk);
1072
+ } catch (err) {
1073
+ return failOnHost(`the root disk ${partial} could not be written`, err);
1074
+ }
1075
+ received += chunk.length;
1076
+ },
1077
+ onStderr: onOutput
1078
+ });
1079
+ check(copied.status, "reading the root disk out of the builder microVM");
1080
+ } finally {
1081
+ closeSync(fd);
1082
+ }
1083
+ if (received !== described.bytes) throw new StepFailed(`the root disk arrived with ${received} bytes, and the builder made ${described.bytes}`);
1084
+ const report = {
1085
+ repository,
1086
+ sourceCommit: checkout.commit,
1087
+ defaultBranch: checkout.defaultBranch,
1088
+ recipe: location.kind === "default" ? "default" : location.path,
1089
+ recipeKey: prepared.recipeKey,
1090
+ toolsKey: prepared.toolsKey,
1091
+ imageId: `${ROOT_DISK_ID_PREFIX}${rKey}-${tKey}`,
1092
+ tag: buildTag,
1093
+ ...classifyProperties(recipe, metadataFromLabels(labels))
1094
+ };
1095
+ const path = join(recordDir, "rootfs.ext4");
1096
+ chmodSync(partial, 292);
1097
+ renameSync(partial, path);
1098
+ const stored = { env: imageConfig.env, user: imageConfig.user, bytes: received, report };
1099
+ writeFileSync(join(recordDir, "rootdisk.json"), `${JSON.stringify(stored, null, 2)}
1100
+ `, { mode: 384 });
1101
+ writeRecord(recordDir, report, now().toISOString());
1102
+ pruneRecords(repoRecords, recordDir);
1103
+ opts.log?.("root disk built in a builder microVM");
1104
+ built = { reused: false, recipeBuilt: true, toolsBuilt: true, checkedCommit: checkout.commit, recordDir, report, removed: [], rootfs: { path, env: imageConfig.env, user: imageConfig.user } };
1105
+ } catch (err) {
1106
+ failure2 = await explain(err);
1107
+ } finally {
1108
+ if (timer !== void 0) clearTimeout(timer);
1109
+ for (const { stream } of inputs) stream.destroy();
1110
+ const stopped = await stopBuilder();
1111
+ await tunnel?.close().catch(() => void 0);
1112
+ if (sanitized !== void 0) rmSync(sanitized, { force: true });
1113
+ if (scratch !== void 0) rmSync(scratch, { recursive: true, force: true });
1114
+ rmSync(partial, { force: true });
1115
+ if (stopped) {
1116
+ if (existsSync(diskPath)) renameSync(diskPath, cachePath);
1117
+ rmSync(dir, { recursive: true, force: true });
1118
+ } else {
1119
+ opts.log?.(`the builder microVM ${id} was not stopped, so its directory ${dir} stays for the runner's next start to remove`);
1120
+ }
1121
+ }
1122
+ if (failure2 !== void 0) throw failure2;
1123
+ return built;
1124
+ async function explain(err) {
1125
+ if (err instanceof RecipeError) return err;
1126
+ if (hostFailure !== void 0) return new RecipeError("image_builder_failed", `the image build stopped: ${hostFailure}`, logPath);
1127
+ if (timedOut) {
1128
+ const limit = opts.timeLimitMinutes === void 0 ? "runner.timeLimitMinutes" : `runner.timeLimitMinutes (${opts.timeLimitMinutes} minutes)`;
1129
+ return new RecipeError("image_builder_failed", `the image build ran past ${limit} and the builder microVM was stopped. The build log is at ${logPath}`, logPath);
1130
+ }
1131
+ try {
1132
+ if (state.cgroup !== void 0 && ports.cgroup.oomKills(state.cgroup) > 0 || helper !== void 0 && (await helper.state(id)).oomKills > 0) {
1133
+ return new RecipeError("image_build_out_of_memory", outOfMemoryReason(opts.memory), logPath);
1134
+ }
1135
+ } catch {
1136
+ }
1137
+ const refusals = tunnel?.record().refusals ?? [];
1138
+ if (err instanceof StepFailed && refusals.length > 0) return new RecipeError("image_builder_failed", refusalReason(refusals, logPath), logPath);
1139
+ const last = tail.filter((line) => line.trim() !== "").at(-1);
1140
+ const why = err instanceof StepFailed ? err.message : `the builder microVM failed: ${describeCallFailure(err)}`;
1141
+ return new RecipeError("image_builder_failed", `${why}${last === void 0 ? "" : `; the last line was: ${last}`}. The build log is at ${logPath}`, logPath);
1142
+ }
1143
+ async function stopBuilder() {
1144
+ let ok = true;
1145
+ if (state.jailed !== void 0) {
1146
+ try {
1147
+ await stopJailed(ports.helper, state);
1148
+ } catch (err) {
1149
+ ok = false;
1150
+ opts.log?.(describeCallFailure(err));
1151
+ }
1152
+ }
1153
+ if (state.firecracker !== void 0) {
1154
+ try {
1155
+ await ports.firecracker.stop(state.firecracker);
1156
+ if (ports.firecracker.alive(state.firecracker)) ok = false;
1157
+ } catch (err) {
1158
+ ok = false;
1159
+ opts.log?.(`the builder microVM (pid ${state.firecracker.pid}) was not stopped: ${describeCallFailure(err)}`);
1160
+ }
1161
+ }
1162
+ if (ok && state.cgroup !== void 0) {
1163
+ try {
1164
+ ports.cgroup.remove(state.cgroup);
1165
+ } catch (err) {
1166
+ opts.log?.(`the builder's cgroup ${state.cgroup} was not removed: ${describeCallFailure(err)}`);
1167
+ }
1168
+ }
1169
+ return ok;
1170
+ }
1171
+ }
1172
+ async function sweepImageBuilders(configPath, ports, onError) {
1173
+ const root = buildsDir(configPath);
1174
+ let names;
1175
+ try {
1176
+ names = readdirSync(root);
1177
+ } catch {
1178
+ return [];
1179
+ }
1180
+ const swept = [];
1181
+ for (const id of names) {
1182
+ const dir = buildDir(configPath, id);
1183
+ let record;
1184
+ try {
1185
+ record = JSON.parse(readFileSync(join(dir, RECORD_FILE), "utf8"));
1186
+ } catch {
1187
+ continue;
1188
+ }
1189
+ let stopped = true;
1190
+ if (record.jailed === true) {
1191
+ const probe = await ports.helper.probe();
1192
+ if (!probe.ok) {
1193
+ onError?.(`the builder microVM ${id} could not be stopped, because there is no helper to ask: ${probe.detail}`);
1194
+ continue;
1195
+ }
1196
+ try {
1197
+ if ((await probe.client.list()).includes(id)) await probe.client.stop(id);
1198
+ } catch (err) {
1199
+ onError?.(`the builder microVM ${id} was not stopped: ${describeCallFailure(err)}`);
1200
+ stopped = false;
1201
+ }
1202
+ }
1203
+ if (record.firecracker !== void 0) {
1204
+ try {
1205
+ if (ports.firecracker.alive(record.firecracker)) await ports.firecracker.stop(record.firecracker);
1206
+ if (ports.firecracker.alive(record.firecracker)) stopped = false;
1207
+ } catch (err) {
1208
+ onError?.(`the builder microVM ${id} (pid ${record.firecracker.pid}) was not stopped: ${describeCallFailure(err)}`);
1209
+ stopped = false;
1210
+ }
1211
+ }
1212
+ if (!stopped) continue;
1213
+ if (record.cgroup !== void 0) {
1214
+ try {
1215
+ ports.cgroup.remove(record.cgroup);
1216
+ } catch (err) {
1217
+ onError?.(`the builder's cgroup ${record.cgroup} was not removed: ${describeCallFailure(err)}`);
1218
+ }
1219
+ }
1220
+ rmSync(dir, { recursive: true, force: true });
1221
+ swept.push(id);
1222
+ }
1223
+ return swept;
1224
+ }
252
1225
  var BranchPrepareError = class extends Error {
253
1226
  };
254
1227
  var OWN_PREFIX = "outerlayer/";
@@ -270,13 +1243,13 @@ function localGit(cwd, args) {
270
1243
  }
271
1244
  return Promise.resolve({ status: result.status, stdout: result.stdout.trim(), stderr: result.stderr.trim() });
272
1245
  }
273
- function firstLine(stderr) {
1246
+ function firstLine2(stderr) {
274
1247
  return (stderr.split("\n")[0] ?? "").replace(/\/\/[^/@\s]+@/g, "//");
275
1248
  }
276
1249
  async function mustGit(git2, cwd, step, args) {
277
1250
  const result = await git2(cwd, args);
278
1251
  if (result.status !== 0) {
279
- const detail = firstLine(result.stderr);
1252
+ const detail = firstLine2(result.stderr);
280
1253
  throw new BranchPrepareError(`${step} failed${detail === "" ? "" : `: ${detail}`}`);
281
1254
  }
282
1255
  return result.stdout;
@@ -321,7 +1294,7 @@ async function isMerged(git2, workdir, branch, defaultBranch) {
321
1294
  const result = await git2(workdir, ["merge-base", "--is-ancestor", `origin/${branch}`, `origin/${defaultBranch}`]);
322
1295
  if (result.status === 0) return true;
323
1296
  if (result.status === 1) return false;
324
- const detail = firstLine(result.stderr);
1297
+ const detail = firstLine2(result.stderr);
325
1298
  throw new BranchPrepareError(`comparing ${branch} with ${defaultBranch} failed${detail === "" ? "" : `: ${detail}`}`);
326
1299
  }
327
1300
 
@@ -389,592 +1362,286 @@ function usableCpus(deps = {}) {
389
1362
  }
390
1363
  return Array.from({ length: Math.max(1, (deps.parallelism ?? availableParallelism)()) }, (_unused, core) => core);
391
1364
  }
392
- function createCpuAllocator(cores) {
393
- const usable = [...new Set(cores)].sort((a, b) => a - b);
394
- const held = /* @__PURE__ */ new Map();
395
- const holders = (core) => {
396
- let count = 0;
397
- for (const set of held.values()) if (set.includes(core)) count++;
398
- return count;
399
- };
400
- return {
401
- acquire(buildId, cpus) {
402
- const existing = held.get(buildId);
403
- if (existing !== void 0) return existing;
404
- const wanted = Math.min(Math.ceil(cpus), usable.length);
405
- const ranked = usable.map((core) => ({ core, count: holders(core) })).sort((a, b) => a.count - b.count);
406
- const chosen = ranked.slice(0, wanted).map(({ core }) => core).sort((a, b) => a - b);
407
- held.set(buildId, chosen);
408
- return chosen;
409
- },
410
- hold(buildId, set) {
411
- held.set(buildId, [...set]);
412
- },
413
- release(buildId) {
414
- held.delete(buildId);
415
- }
416
- };
417
- }
418
-
419
- // src/runner/container/lease-channel.ts
420
- var LEASE_SECONDS = 900;
421
- function leaseExpiresAtMs(job) {
422
- const base = Date.parse(job.lastRenewAt ?? job.startedAt);
423
- return (Number.isFinite(base) ? base : Date.now()) + LEASE_SECONDS * 1e3;
424
- }
425
- function leaseTimeLeftMs(job, nowMs) {
426
- return Math.max(0, leaseExpiresAtMs(job) - nowMs);
427
- }
428
- function createLeaseChannel(valueMs) {
429
- const open = /* @__PURE__ */ new Set();
430
- let stopped = false;
431
- const write = (stream) => {
432
- if (!stopped && !stream.destroyed && stream.writable) stream.write(`${Math.floor(valueMs())}
433
- `);
434
- };
435
- return {
436
- accept(stream) {
437
- stream.on("error", () => void 0);
438
- open.add(stream);
439
- stream.once("close", () => open.delete(stream));
440
- queueMicrotask(() => write(stream));
441
- },
442
- publish() {
443
- for (const stream of open) write(stream);
444
- },
445
- stop() {
446
- stopped = true;
447
- },
448
- close() {
449
- for (const stream of [...open]) stream.destroy();
450
- open.clear();
451
- }
452
- };
453
- }
454
- var AssetError = class extends Error {
455
- constructor(file, message) {
456
- super(message);
457
- this.file = file;
458
- }
459
- };
460
- var FIRECRACKER_RELEASES = "https://github.com/firecracker-microvm/firecracker/releases/download";
461
- var DOWNLOAD_TIMEOUT_MS = 10 * 6e4;
462
- function pinnedAssets(arch) {
463
- const version = FIRECRACKER_VERSION;
464
- return {
465
- firecracker: {
466
- url: `${FIRECRACKER_RELEASES}/v${version}/firecracker-v${version}-${arch}.tgz`,
467
- sha256: FIRECRACKER_SHA256[arch],
468
- binarySha256: FIRECRACKER_BINARY_SHA256[arch],
469
- jailerSha256: JAILER_BINARY_SHA256[arch]
470
- },
471
- kernel: { url: `${GUEST_ASSETS_BASE_URL}/vmlinux-${arch}`, sha256: GUEST_KERNEL_SHA256[arch] },
472
- helper: { url: `${GUEST_ASSETS_BASE_URL}/outerlayer-guest-${arch}`, sha256: GUEST_HELPER_SHA256[arch] }
473
- };
474
- }
475
- var sha256Of = (bytes) => createHash("sha256").update(bytes).digest("hex");
476
- function pinProblem(file, expected, actual, where) {
477
- const release = file.startsWith("vmlinux-") || file.startsWith("outerlayer-guest-") ? ` (guest assets release ${GUEST_ASSETS_VERSION})` : "";
478
- return new AssetError(file, `${file} ${where} has sha256 ${actual}, not the ${expected} this release pins${release}, so no microVM build starts`);
479
- }
480
- function writeAtomic(path, bytes, mode) {
481
- const temp = `${path}.${process.pid}.tmp`;
482
- writeFileSync(temp, bytes, { mode });
483
- renameSync(temp, path);
484
- }
485
- async function download(file, url, fetchImpl) {
486
- let response;
487
- try {
488
- response = await fetchImpl(url, { signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) });
489
- } catch (err) {
490
- throw new AssetError(file, `${file} could not be downloaded from ${url}: ${err instanceof Error ? err.message : String(err)}`);
491
- }
492
- if (!response.ok) throw new AssetError(file, `${file} could not be downloaded from ${url}: the server answered ${response.status}`);
493
- return Buffer.from(await response.arrayBuffer());
494
- }
495
- function keptPath(dir, sha256, name) {
496
- mkdirSync(join(dir, sha256), { recursive: true, mode: RUNNER_DIR_MODE });
497
- return join(dir, sha256, name);
498
- }
499
- async function ensureFile(dir, file, pin, fetchImpl, mode) {
500
- const path = keptPath(dir, pin.sha256, file);
501
- if (existsSync(path)) {
502
- const actual2 = sha256Of(readFileSync(path));
503
- if (actual2 !== pin.sha256) throw pinProblem(file, pin.sha256, actual2, `at ${path}`);
504
- return path;
505
- }
506
- const bytes = await download(file, pin.url, fetchImpl);
507
- const actual = sha256Of(bytes);
508
- if (actual !== pin.sha256) throw pinProblem(file, pin.sha256, actual, `downloaded from ${pin.url}`);
509
- writeAtomic(path, bytes, mode);
510
- return path;
511
- }
512
- function extractWithTar(arch, program = "firecracker") {
513
- const name = `${program}-v${FIRECRACKER_VERSION}-${arch}`;
514
- return async (tarball, destDir) => {
515
- const result = spawnSync("tar", ["-xzf", tarball, "-C", destDir, "--strip-components=1", `release-v${FIRECRACKER_VERSION}-${arch}/${name}`], { encoding: "utf8" });
516
- if (result.status !== 0) throw new Error(`tar could not unpack ${tarball}: ${result.stderr.trim()}`);
517
- return join(destDir, name);
518
- };
519
- }
520
- async function ensureFirecracker(input) {
521
- const { dir, arch, pins, fetchImpl } = input;
522
- const firecracker = keptPath(dir, pins.firecracker.binarySha256, "firecracker");
523
- if (existsSync(firecracker)) {
524
- const actual = sha256Of(readFileSync(firecracker));
525
- if (actual !== pins.firecracker.binarySha256) throw pinProblem("firecracker", pins.firecracker.binarySha256, actual, `at ${firecracker}`);
526
- return firecracker;
527
- }
528
- const tarballName = `firecracker-${arch}.tgz`;
529
- const tarball = await ensureFile(dir, tarballName, pins.firecracker, fetchImpl, RUNNER_FILE_MODE);
530
- const scratch = mkdtempSync(join(dir, ".extract-"));
531
- try {
532
- const extracted = await (input.extract ?? extractWithTar(arch))(tarball, scratch);
533
- const bytes = readFileSync(extracted);
534
- const actual = sha256Of(bytes);
535
- if (actual !== pins.firecracker.binarySha256) throw pinProblem("firecracker", pins.firecracker.binarySha256, actual, `unpacked from ${tarballName}`);
536
- writeAtomic(firecracker, bytes, 493);
537
- } finally {
538
- rmSync(scratch, { recursive: true, force: true });
539
- }
540
- return firecracker;
541
- }
542
- async function ensureFirecrackerAndJailer(options) {
543
- const arch = options.arch ?? firecrackerArch(process.arch);
544
- if (!MICROVM_ARCHES.includes(arch)) throw new AssetError("firecracker", `no Firecracker assets are pinned for ${arch}, so microVM builds run on ${MICROVM_ARCHES.join(" or ")} hosts only`);
545
- const pins = options.pins ?? pinnedAssets(arch);
546
- const dir = vmAssetsDir(options.runnerDir);
547
- mkdirSync(dir, { recursive: true, mode: RUNNER_DIR_MODE });
548
- const firecracker = await ensureFirecracker({ dir, arch, pins, fetchImpl: options.fetchImpl ?? fetch, extract: options.extractFirecracker });
549
- return { firecracker, jailer: await ensureJailer(options) };
550
- }
551
- async function ensureAssets(options) {
552
- const arch = options.arch ?? firecrackerArch(process.arch);
553
- if (!MICROVM_ARCHES.includes(arch)) throw new AssetError("firecracker", `no Firecracker or guest assets are pinned for ${arch}, so microVM builds run on ${MICROVM_ARCHES.join(" or ")} hosts only`);
554
- const pins = options.pins ?? pinnedAssets(arch);
555
- const fetchImpl = options.fetchImpl ?? fetch;
556
- const dir = vmAssetsDir(options.runnerDir);
557
- mkdirSync(dir, { recursive: true, mode: RUNNER_DIR_MODE });
558
- const firecracker = await ensureFirecracker({ dir, arch, pins, fetchImpl, extract: options.extractFirecracker });
559
- const kernel = await ensureFile(dir, `vmlinux-${arch}`, pins.kernel, fetchImpl, RUNNER_FILE_MODE);
560
- const helper = await ensureFile(dir, `outerlayer-guest-${arch}`, pins.helper, fetchImpl, 493);
561
- return { firecracker, kernel, helper, helperSha256: pins.helper.sha256 };
562
- }
563
- async function ensureJailer(options) {
564
- const arch = options.arch ?? firecrackerArch(process.arch);
565
- if (!MICROVM_ARCHES.includes(arch)) throw new AssetError("jailer", `no Firecracker assets are pinned for ${arch}, so microVM builds run on ${MICROVM_ARCHES.join(" or ")} hosts only`);
566
- const pins = options.pins ?? pinnedAssets(arch);
567
- const expected = pins.firecracker.jailerSha256;
568
- if (expected === void 0) throw new AssetError("jailer", "this release pins no hash for the jailer, so it cannot be had");
569
- const fetchImpl = options.fetchImpl ?? fetch;
570
- const dir = vmAssetsDir(options.runnerDir);
571
- mkdirSync(dir, { recursive: true, mode: RUNNER_DIR_MODE });
572
- const jailer = keptPath(dir, expected, "jailer");
573
- if (existsSync(jailer)) {
574
- const actual = sha256Of(readFileSync(jailer));
575
- if (actual !== expected) throw pinProblem("jailer", expected, actual, `at ${jailer}`);
576
- return jailer;
577
- }
578
- const tarballName = `firecracker-${arch}.tgz`;
579
- const tarball = await ensureFile(dir, tarballName, pins.firecracker, fetchImpl, RUNNER_FILE_MODE);
580
- const scratch = mkdtempSync(join(dir, ".extract-"));
581
- try {
582
- const extracted = await (options.extractJailer ?? extractWithTar(arch, "jailer"))(tarball, scratch);
583
- const bytes = readFileSync(extracted);
584
- const actual = sha256Of(bytes);
585
- if (actual !== expected) throw pinProblem("jailer", expected, actual, `unpacked from ${tarballName}`);
586
- writeAtomic(jailer, bytes, 493);
587
- } finally {
588
- rmSync(scratch, { recursive: true, force: true });
589
- }
590
- return jailer;
591
- }
592
- var STOP_GRACE_SECONDS = 15;
593
- function parseImageConfig(json) {
594
- const config = JSON.parse(json);
595
- const env = {};
596
- for (const entry of config.Env ?? []) {
597
- const eq = entry.indexOf("=");
598
- if (eq > 0) env[entry.slice(0, eq)] = entry.slice(eq + 1);
599
- }
600
- return { user: config.User ?? "", env };
601
- }
602
- function createContainerHost(deps = {}) {
603
- const run = deps.run ?? spawnRun;
1365
+ function createCpuAllocator(cores) {
1366
+ const usable = [...new Set(cores)].sort((a, b) => a - b);
1367
+ const held = /* @__PURE__ */ new Map();
1368
+ const holders = (core) => {
1369
+ let count = 0;
1370
+ for (const set of held.values()) if (set.includes(core)) count++;
1371
+ return count;
1372
+ };
604
1373
  return {
605
- async version() {
606
- const result = await run("docker", ["version", "--format", "{{.Server.Version}}"]);
607
- return result.status === 0 && result.stdout.trim() !== "" ? result.stdout.trim() : null;
608
- },
609
- async operatingSystem() {
610
- const result = await run("docker", ["info", "--format", "{{.OperatingSystem}}"]);
611
- return result.status === 0 && result.stdout.trim() !== "" ? result.stdout.trim() : null;
612
- },
613
- async imageConfig(imageId) {
614
- const result = await run("docker", ["image", "inspect", "--format", "{{json .Config}}", imageId]);
615
- return result.status === 0 ? parseImageConfig(result.stdout) : { user: "", env: {} };
616
- },
617
- createVolume(name, labels) {
618
- const flags = Object.entries(labels).flatMap(([key, value]) => ["--label", `${key}=${value}`]);
619
- return run("docker", ["volume", "create", ...flags, name]);
620
- },
621
- removeVolume(name) {
622
- return run("docker", ["volume", "rm", "--force", name]);
623
- },
624
- run(args, opts) {
625
- return run("docker", ["run", ...args], opts);
626
- },
627
- exec(input, opts) {
628
- const env = {};
629
- for (const [name, value] of Object.entries(input.env.passed)) if (value !== void 0) env[name] = value;
630
- return run("docker", ["exec", ...containerExecArgs(input)], { ...opts, env, ...input.stdin === void 0 ? {} : { input: input.stdin } });
631
- },
632
- execDetached(input) {
633
- const env = {};
634
- for (const [name, value] of Object.entries(input.env.passed)) if (value !== void 0) env[name] = value;
635
- return run("docker", ["exec", ...containerExecArgs({ ...input, detached: true })], { env });
636
- },
637
- async state(container) {
638
- const result = await run("docker", ["inspect", "--format", "{{.State.Running}}", container]);
639
- if (result.status !== 0) return /no such (object|container)/i.test(result.stderr) ? "missing" : "unknown";
640
- return result.stdout.trim() === "true" ? "running" : "stopped";
641
- },
642
- execStream(input) {
643
- const child = (deps.spawn ?? spawn)("docker", ["exec", ...containerExecArgs({ ...input, interactive: true })], {
644
- env: childEnvironment(process.env),
645
- stdio: ["pipe", "pipe", "pipe"]
646
- });
647
- let stderr = "";
648
- child.stderr?.on("data", (chunk) => {
649
- stderr = (stderr + chunk.toString()).slice(-400);
650
- });
651
- child.stderr?.on("error", () => void 0);
652
- const exited = new Promise((resolve2) => {
653
- child.once("error", () => resolve2(-1));
654
- child.once("close", (code) => resolve2(code ?? -1));
655
- });
656
- child.stdin.on("error", () => void 0);
657
- return { stdin: child.stdin, stdout: child.stdout, exited, kill: () => void child.kill("SIGKILL"), stderrTail: () => stderr };
658
- },
659
- stop(container) {
660
- return run("docker", ["stop", "--time", String(STOP_GRACE_SECONDS), container]);
1374
+ acquire(buildId, cpus) {
1375
+ const existing = held.get(buildId);
1376
+ if (existing !== void 0) return existing;
1377
+ const wanted = Math.min(Math.ceil(cpus), usable.length);
1378
+ const ranked = usable.map((core) => ({ core, count: holders(core) })).sort((a, b) => a.count - b.count);
1379
+ const chosen = ranked.slice(0, wanted).map(({ core }) => core).sort((a, b) => a - b);
1380
+ held.set(buildId, chosen);
1381
+ return chosen;
661
1382
  },
662
- start(container) {
663
- return run("docker", ["start", container]);
1383
+ hold(buildId, set) {
1384
+ held.set(buildId, [...set]);
664
1385
  },
665
- remove(container) {
666
- return run("docker", ["rm", "--force", "--volumes", container]);
1386
+ release(buildId) {
1387
+ held.delete(buildId);
667
1388
  }
668
1389
  };
669
1390
  }
670
1391
 
671
- // src/runner/microvm/disks.ts
672
- function converterScript() {
673
- return [
674
- "set -eu",
675
- "rootfs=/tmp/rootfs",
676
- 'mkdir -p "$rootfs"',
677
- 'tar -xf - -C "$rootfs"',
678
- 'used_kb=$(du -sk "$rootfs" | cut -f1)',
679
- 'files=$(find "$rootfs" | wc -l)',
680
- // Room for the file system's own metadata, and for an inode per file with some to spare.
681
- "total_kb=$(( used_kb + used_kb / 4 + 65536 ))",
682
- 'truncate -s "${total_kb}K" "/out/$OUT_NAME"',
683
- 'mke2fs -q -F -t ext4 -m 0 -N $(( files + files / 4 + 4096 )) -d "$rootfs" "/out/$OUT_NAME"',
684
- 'chown "$HOST_UID:$HOST_GID" "/out/$OUT_NAME"'
685
- ].join("\n");
686
- }
687
- function templateScript() {
688
- return [
689
- "set -eu",
690
- 'truncate -s "$BYTES" "/out/$OUT_NAME"',
691
- 'mke2fs -q -F -t ext4 -m 0 -E lazy_itable_init=1,lazy_journal_init=1,nodiscard "/out/$OUT_NAME"',
692
- 'chown "$HOST_UID:$HOST_GID" "/out/$OUT_NAME"'
693
- ].join("\n");
694
- }
695
- var ids = () => ({ uid: String(process.getuid?.() ?? 0), gid: String(process.getgid?.() ?? 0) });
696
- function converterArgs(dest, memoryBytes, extra, script) {
697
- const { uid, gid } = ids();
698
- return [
699
- "run",
700
- "--rm",
701
- "-i",
702
- "--network",
703
- "none",
704
- "--memory",
705
- String(memoryBytes),
706
- "--memory-swap",
707
- String(memoryBytes),
708
- "--mount",
709
- `type=bind,src=${dirname(dest)},dst=/out`,
710
- "-e",
711
- `OUT_NAME=${basename(dest)}`,
712
- "-e",
713
- `HOST_UID=${uid}`,
714
- "-e",
715
- `HOST_GID=${gid}`,
716
- ...extra,
717
- // The image that runs `mke2fs` as root is the default base image: Ubuntu holds e2fsprogs.
718
- DEFAULT_BASE_IMAGE,
719
- "sh",
720
- "-c",
721
- script
722
- ];
1392
+ // src/runner/container/lease-channel.ts
1393
+ var LEASE_SECONDS = 900;
1394
+ function leaseExpiresAtMs(job) {
1395
+ const base = Date.parse(job.lastRenewAt ?? job.startedAt);
1396
+ return (Number.isFinite(base) ? base : Date.now()) + LEASE_SECONDS * 1e3;
723
1397
  }
724
- var firstLine2 = (text) => text.trim().split("\n")[0] ?? "";
725
- function exportInto(container, converter) {
726
- return new Promise((resolve2, reject) => {
727
- const exporter = spawn("docker", ["export", container], { stdio: ["ignore", "pipe", "pipe"], env: childEnvironment(process.env) });
728
- const writer = spawn("docker", converter, { stdio: ["pipe", "ignore", "pipe"], env: childEnvironment(process.env) });
729
- const errors = [];
730
- exporter.stderr.on("data", (chunk) => errors.push(`docker export: ${chunk.toString("utf8")}`));
731
- writer.stderr.on("data", (chunk) => errors.push(`converter: ${chunk.toString("utf8")}`));
732
- writer.stdin.on("error", () => void 0);
733
- exporter.stdout.on("error", () => void 0);
734
- exporter.stdout.pipe(writer.stdin);
735
- exporter.once("error", reject);
736
- writer.once("error", reject);
737
- let exits = 0;
738
- let failure2;
739
- const done = (name, code) => {
740
- if (code !== 0 && failure2 === void 0) failure2 = `${name} exited ${code}`;
741
- exits += 1;
742
- if (exits < 2) return;
743
- if (failure2 === void 0) resolve2();
744
- else reject(new Error(`${failure2}: ${firstLine2(errors.join("\n"))}`));
745
- };
746
- exporter.once("exit", (code) => done("docker export", code));
747
- writer.once("exit", (code) => done("the converter", code));
748
- });
1398
+ function leaseTimeLeftMs(job, nowMs) {
1399
+ return Math.max(0, leaseExpiresAtMs(job) - nowMs);
749
1400
  }
750
- function createRootfsHost() {
751
- const env = () => childEnvironment(process.env);
1401
+ function createLeaseChannel(valueMs) {
1402
+ const open = /* @__PURE__ */ new Set();
1403
+ let stopped = false;
1404
+ const write = (stream) => {
1405
+ if (!stopped && !stream.destroyed && stream.writable) stream.write(`${Math.floor(valueMs())}
1406
+ `);
1407
+ };
752
1408
  return {
753
- async convert({ imageId, dest, memoryBytes }) {
754
- const created = await spawnRun("docker", ["create", imageId, "none"], { env: env() });
755
- if (created.status !== 0) throw new Error(`docker create ${imageId} failed: ${firstLine2(created.stderr)}`);
756
- const container = created.stdout.trim();
757
- try {
758
- await exportInto(container, converterArgs(dest, memoryBytes, [], converterScript()));
759
- } finally {
760
- await spawnRun("docker", ["rm", "--force", "--volumes", container], { env: env() });
761
- }
1409
+ accept(stream) {
1410
+ stream.on("error", () => void 0);
1411
+ open.add(stream);
1412
+ stream.once("close", () => open.delete(stream));
1413
+ queueMicrotask(() => write(stream));
762
1414
  },
763
- async makeTemplate({ bytes, dest, memoryBytes }) {
764
- const made = await spawnRun("docker", converterArgs(dest, memoryBytes, ["-e", `BYTES=${bytes}`], templateScript()), { env: env() });
765
- if (made.status !== 0) throw new Error(`making the empty build disk failed: ${firstLine2(made.stderr)}`);
1415
+ publish() {
1416
+ for (const stream of open) write(stream);
766
1417
  },
767
- async imageConfig(imageId) {
768
- const inspected = await spawnRun("docker", ["image", "inspect", "--format", "{{json .Config}}", imageId], { env: env() });
769
- if (inspected.status !== 0) throw new Error(`docker image inspect ${imageId} failed: ${firstLine2(inspected.stderr)}`);
770
- return parseImageConfig(inspected.stdout);
1418
+ stop() {
1419
+ stopped = true;
1420
+ },
1421
+ close() {
1422
+ for (const stream of [...open]) stream.destroy();
1423
+ open.clear();
771
1424
  }
772
1425
  };
773
1426
  }
774
- function sweepStalePartials(dir) {
775
- let names;
1427
+ var AssetError = class extends Error {
1428
+ constructor(file, message) {
1429
+ super(message);
1430
+ this.file = file;
1431
+ }
1432
+ };
1433
+ var FIRECRACKER_RELEASES = "https://github.com/firecracker-microvm/firecracker/releases/download";
1434
+ var DOWNLOAD_TIMEOUT_MS = 10 * 6e4;
1435
+ function pinnedAssets(arch) {
1436
+ const version = FIRECRACKER_VERSION;
1437
+ return {
1438
+ firecracker: {
1439
+ url: `${FIRECRACKER_RELEASES}/v${version}/firecracker-v${version}-${arch}.tgz`,
1440
+ sha256: FIRECRACKER_SHA256[arch],
1441
+ binarySha256: FIRECRACKER_BINARY_SHA256[arch],
1442
+ jailerSha256: JAILER_BINARY_SHA256[arch]
1443
+ },
1444
+ kernel: { url: `${GUEST_ASSETS_BASE_URL}/vmlinux-${arch}`, sha256: GUEST_KERNEL_SHA256[arch] },
1445
+ helper: { url: `${GUEST_ASSETS_BASE_URL}/outerlayer-guest-${arch}`, sha256: GUEST_HELPER_SHA256[arch] }
1446
+ };
1447
+ }
1448
+ function builderDiskPin(arch) {
1449
+ return { url: `${BUILDER_DISK_BASE_URL}/builder-rootfs-${arch}.ext4`, sha256: BUILDER_DISK_SHA256[arch] };
1450
+ }
1451
+ var sha256Of = (bytes) => createHash("sha256").update(bytes).digest("hex");
1452
+ function pinProblem(file, expected, actual, where) {
1453
+ const release = file.startsWith("vmlinux-") || file.startsWith("outerlayer-guest-") ? ` (guest assets release ${GUEST_ASSETS_VERSION})` : file.startsWith("builder-rootfs-") ? ` (builder root disk release ${BUILDER_DISK_VERSION})` : "";
1454
+ return new AssetError(file, `${file} ${where} has sha256 ${actual}, not the ${expected} this release pins${release}, so no microVM build starts`);
1455
+ }
1456
+ function writeAtomic(path, bytes, mode) {
1457
+ const temp = `${path}.${process.pid}.tmp`;
1458
+ writeFileSync(temp, bytes, { mode });
1459
+ renameSync(temp, path);
1460
+ }
1461
+ async function download(file, url, fetchImpl) {
1462
+ let response;
776
1463
  try {
777
- names = readdirSync(dir);
778
- } catch {
779
- return;
1464
+ response = await fetchImpl(url, { signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) });
1465
+ } catch (err) {
1466
+ throw new AssetError(file, `${file} could not be downloaded from ${url}: ${err instanceof Error ? err.message : String(err)}`);
780
1467
  }
781
- for (const name of names) {
782
- const pid = /\.partial-(\d+)-[0-9a-f]+$/.exec(name)?.[1];
783
- if (pid === void 0 || Number(pid) === process.pid) continue;
784
- try {
785
- process.kill(Number(pid), 0);
786
- } catch (err) {
787
- if (err.code === "ESRCH") rmSync(join(dir, name), { force: true });
788
- }
1468
+ if (!response.ok) throw new AssetError(file, `${file} could not be downloaded from ${url}: the server answered ${response.status}`);
1469
+ return Buffer.from(await response.arrayBuffer());
1470
+ }
1471
+ function keptPath(dir, sha256, name) {
1472
+ mkdirSync(join(dir, sha256), { recursive: true, mode: RUNNER_DIR_MODE });
1473
+ return join(dir, sha256, name);
1474
+ }
1475
+ async function ensureFile(dir, file, pin, fetchImpl, mode) {
1476
+ const path = keptPath(dir, pin.sha256, file);
1477
+ if (existsSync(path)) {
1478
+ const actual2 = sha256Of(readFileSync(path));
1479
+ if (actual2 !== pin.sha256) throw pinProblem(file, pin.sha256, actual2, `at ${path}`);
1480
+ return path;
789
1481
  }
1482
+ const bytes = await download(file, pin.url, fetchImpl);
1483
+ const actual = sha256Of(bytes);
1484
+ if (actual !== pin.sha256) throw pinProblem(file, pin.sha256, actual, `downloaded from ${pin.url}`);
1485
+ writeAtomic(path, bytes, mode);
1486
+ return path;
790
1487
  }
791
- async function madeAtomically(path, make) {
792
- mkdirSync(dirname(path), { recursive: true, mode: RUNNER_DIR_MODE });
793
- const temp = `${path}.partial-${process.pid}-${randomBytes(3).toString("hex")}`;
794
- sweepStalePartials(dirname(path));
1488
+ function extractWithTar(arch, program = "firecracker") {
1489
+ const name = `${program}-v${FIRECRACKER_VERSION}-${arch}`;
1490
+ return async (tarball, destDir) => {
1491
+ const result = spawnSync("tar", ["-xzf", tarball, "-C", destDir, "--strip-components=1", `release-v${FIRECRACKER_VERSION}-${arch}/${name}`], { encoding: "utf8" });
1492
+ if (result.status !== 0) throw new Error(`tar could not unpack ${tarball}: ${result.stderr.trim()}`);
1493
+ return join(destDir, name);
1494
+ };
1495
+ }
1496
+ async function ensureFirecracker(input) {
1497
+ const { dir, arch, pins, fetchImpl } = input;
1498
+ const firecracker = keptPath(dir, pins.firecracker.binarySha256, "firecracker");
1499
+ if (existsSync(firecracker)) {
1500
+ const actual = sha256Of(readFileSync(firecracker));
1501
+ if (actual !== pins.firecracker.binarySha256) throw pinProblem("firecracker", pins.firecracker.binarySha256, actual, `at ${firecracker}`);
1502
+ return firecracker;
1503
+ }
1504
+ const tarballName = `firecracker-${arch}.tgz`;
1505
+ const tarball = await ensureFile(dir, tarballName, pins.firecracker, fetchImpl, RUNNER_FILE_MODE);
1506
+ const scratch = mkdtempSync(join(dir, ".extract-"));
795
1507
  try {
796
- await make(temp);
797
- renameSync(temp, path);
798
- } catch (err) {
799
- rmSync(temp, { force: true });
800
- throw err;
1508
+ const extracted = await (input.extract ?? extractWithTar(arch))(tarball, scratch);
1509
+ const bytes = readFileSync(extracted);
1510
+ const actual = sha256Of(bytes);
1511
+ if (actual !== pins.firecracker.binarySha256) throw pinProblem("firecracker", pins.firecracker.binarySha256, actual, `unpacked from ${tarballName}`);
1512
+ writeAtomic(firecracker, bytes, 493);
1513
+ } finally {
1514
+ rmSync(scratch, { recursive: true, force: true });
801
1515
  }
1516
+ return firecracker;
802
1517
  }
803
- async function ensureRootfs(input) {
804
- const path = rootfsPathFor(input.runnerDir, input.imageId);
805
- if (existsSync(path)) return path;
806
- const { uid, gid } = ids();
807
- await madeAtomically(path, (temp) => input.host.convert({ imageId: input.imageId, dest: temp, uid: Number(uid), gid: Number(gid), memoryBytes: input.memoryBytes }));
808
- return path;
1518
+ async function ensureFirecrackerAndJailer(options) {
1519
+ const arch = options.arch ?? firecrackerArch(process.arch);
1520
+ if (!MICROVM_ARCHES.includes(arch)) throw new AssetError("firecracker", `no Firecracker assets are pinned for ${arch}, so microVM builds run on ${MICROVM_ARCHES.join(" or ")} hosts only`);
1521
+ const pins = options.pins ?? pinnedAssets(arch);
1522
+ const dir = vmAssetsDir(options.runnerDir);
1523
+ mkdirSync(dir, { recursive: true, mode: RUNNER_DIR_MODE });
1524
+ const firecracker = await ensureFirecracker({ dir, arch, pins, fetchImpl: options.fetchImpl ?? fetch, extract: options.extractFirecracker });
1525
+ return { firecracker, jailer: await ensureJailer(options) };
809
1526
  }
810
- async function createBuildDisk(input) {
811
- const template = join(diskTemplatesDir(input.runnerDir), `${input.bytes}.ext4`);
812
- if (!existsSync(template)) {
813
- const { uid, gid } = ids();
814
- await madeAtomically(template, (temp) => input.host.makeTemplate({ bytes: input.bytes, dest: temp, uid: Number(uid), gid: Number(gid), memoryBytes: input.memoryBytes }));
1527
+ async function ensureAssets(options) {
1528
+ const arch = options.arch ?? firecrackerArch(process.arch);
1529
+ if (!MICROVM_ARCHES.includes(arch)) throw new AssetError("firecracker", `no Firecracker or guest assets are pinned for ${arch}, so microVM builds run on ${MICROVM_ARCHES.join(" or ")} hosts only`);
1530
+ const pins = options.pins ?? pinnedAssets(arch);
1531
+ const fetchImpl = options.fetchImpl ?? fetch;
1532
+ const dir = vmAssetsDir(options.runnerDir);
1533
+ mkdirSync(dir, { recursive: true, mode: RUNNER_DIR_MODE });
1534
+ const firecracker = await ensureFirecracker({ dir, arch, pins, fetchImpl, extract: options.extractFirecracker });
1535
+ const kernel = await ensureFile(dir, `vmlinux-${arch}`, pins.kernel, fetchImpl, RUNNER_FILE_MODE);
1536
+ const helper = await ensureFile(dir, `outerlayer-guest-${arch}`, pins.helper, fetchImpl, 493);
1537
+ return { firecracker, kernel, helper, helperSha256: pins.helper.sha256 };
1538
+ }
1539
+ async function ensureJailer(options) {
1540
+ const arch = options.arch ?? firecrackerArch(process.arch);
1541
+ if (!MICROVM_ARCHES.includes(arch)) throw new AssetError("jailer", `no Firecracker assets are pinned for ${arch}, so microVM builds run on ${MICROVM_ARCHES.join(" or ")} hosts only`);
1542
+ const pins = options.pins ?? pinnedAssets(arch);
1543
+ const expected = pins.firecracker.jailerSha256;
1544
+ if (expected === void 0) throw new AssetError("jailer", "this release pins no hash for the jailer, so it cannot be had");
1545
+ const fetchImpl = options.fetchImpl ?? fetch;
1546
+ const dir = vmAssetsDir(options.runnerDir);
1547
+ mkdirSync(dir, { recursive: true, mode: RUNNER_DIR_MODE });
1548
+ const jailer = keptPath(dir, expected, "jailer");
1549
+ if (existsSync(jailer)) {
1550
+ const actual = sha256Of(readFileSync(jailer));
1551
+ if (actual !== expected) throw pinProblem("jailer", expected, actual, `at ${jailer}`);
1552
+ return jailer;
815
1553
  }
816
- mkdirSync(dirname(input.dest), { recursive: true });
817
- if (input.host.copyTemplate) input.host.copyTemplate(template, input.dest);
818
- else {
819
- const copied = spawnSync("cp", ["--sparse=always", template, input.dest], { encoding: "utf8" });
820
- if (copied.status !== 0) throw new Error(`copying the empty build disk failed: ${firstLine2(copied.stderr)}`);
1554
+ const tarballName = `firecracker-${arch}.tgz`;
1555
+ const tarball = await ensureFile(dir, tarballName, pins.firecracker, fetchImpl, RUNNER_FILE_MODE);
1556
+ const scratch = mkdtempSync(join(dir, ".extract-"));
1557
+ try {
1558
+ const extracted = await (options.extractJailer ?? extractWithTar(arch, "jailer"))(tarball, scratch);
1559
+ const bytes = readFileSync(extracted);
1560
+ const actual = sha256Of(bytes);
1561
+ if (actual !== expected) throw pinProblem("jailer", expected, actual, `unpacked from ${tarballName}`);
1562
+ writeAtomic(jailer, bytes, 493);
1563
+ } finally {
1564
+ rmSync(scratch, { recursive: true, force: true });
821
1565
  }
822
- chmodSync(input.dest, RUNNER_FILE_MODE);
823
- truncateSync(input.dest, input.bytes);
824
- }
825
- function deleteBuildDisk(path) {
826
- rmSync(path, { force: true });
1566
+ return jailer;
827
1567
  }
828
- var CONSOLE_CUT_NOTE = "[outerlayer runner: cut the console at 8 MiB; the microVM keeps running]\n";
829
- function pumpConsole(fd, streams) {
830
- const cap = 8 * 1024 ** 2;
831
- let written = 0;
832
- let lastByte = 10;
833
- let cut = false;
834
- const append = (data) => {
835
- try {
836
- writeSync(fd, data);
837
- } catch {
838
- }
839
- };
840
- const onData = (chunk) => {
841
- if (cut) return;
842
- const room = cap - written;
843
- if (chunk.length <= room) {
844
- append(chunk);
845
- written += chunk.length;
846
- lastByte = chunk[chunk.length - 1] ?? lastByte;
847
- return;
848
- }
849
- const head = chunk.subarray(0, room);
850
- append(head);
851
- if (head.length > 0) lastByte = head[head.length - 1];
852
- append(Buffer.from(`${lastByte === 10 ? "" : "\n"}${CONSOLE_CUT_NOTE}`));
853
- written = cap;
854
- cut = true;
855
- };
856
- let open = streams.length;
857
- for (const stream of streams) {
858
- stream.on("data", onData);
859
- stream.on("error", () => void 0);
860
- stream.once("close", () => {
861
- open -= 1;
862
- if (open === 0) closeSync(fd);
863
- });
1568
+ async function ensureBuilderDisk(options) {
1569
+ const arch = options.arch ?? firecrackerArch(process.arch);
1570
+ if (!MICROVM_ARCHES.includes(arch)) throw new AssetError("builder-rootfs", `no builder root disk is pinned for ${arch}, so microVM image builds run on ${MICROVM_ARCHES.join(" or ")} hosts only`);
1571
+ const file = `builder-rootfs-${arch}.ext4`;
1572
+ const pin = options.pin ?? builderDiskPin(arch);
1573
+ if (options.pin === void 0 && !builderDiskPinned(arch)) {
1574
+ throw new AssetError(file, `${file} is not published for this release of the CLI, so the image cannot be built in a builder microVM. Update the CLI, or set runner.build.imageBuild to host`);
864
1575
  }
1576
+ const dir = vmAssetsDir(options.runnerDir);
1577
+ mkdirSync(dir, { recursive: true, mode: RUNNER_DIR_MODE });
1578
+ return ensureFile(dir, file, pin, options.fetchImpl ?? fetch, RUNNER_FILE_MODE);
865
1579
  }
866
-
867
- // src/runner/microvm/firecracker.ts
868
- var clearBit = (bit) => `0b${"x".repeat(31 - bit)}0${"x".repeat(bit)}`;
869
- var CPU_TEMPLATE = {
870
- cpuid_modifiers: [
871
- { leaf: "0x1", subleaf: "0x0", flags: 0, modifiers: [{ register: "ecx", bitmap: clearBit(5) }] },
872
- { leaf: "0x80000001", subleaf: "0x0", flags: 0, modifiers: [{ register: "ecx", bitmap: clearBit(2) }] }
873
- ]
874
- };
875
- var MIB = 1024 ** 2;
876
- var INIT = "/opt/outerlayer/bin/outerlayer-guest";
877
- function machineConfig(limits2, paths) {
878
- const bootArgs = [
879
- "console=ttyS0",
880
- "reboot=k",
881
- "panic=1",
882
- "pci=off",
883
- "i8042.noaux",
884
- "i8042.nomux",
885
- "i8042.nopnp",
886
- "i8042.dumbkbd",
887
- // The pinned guest kernel builds pressure stall information default-disabled; the guest usage record reads it.
888
- "psi=1",
889
- "ro",
890
- "root=/dev/vda",
891
- "rootfstype=ext4",
892
- `init=${INIT}`,
893
- ...paths.relayPorts.length === 0 ? [] : [`outerlayer.relay=${paths.relayPorts.join(",")}`],
894
- ...paths.leasePort === void 0 ? [] : [`outerlayer.lease=${paths.leasePort}`],
895
- ...paths.docker === true ? ["outerlayer.docker=1"] : []
896
- ];
897
- return {
898
- "boot-source": { kernel_image_path: paths.kernel, boot_args: bootArgs.join(" ") },
899
- drives: [
900
- { drive_id: "rootfs", path_on_host: paths.rootfs, is_root_device: true, is_read_only: true },
901
- { drive_id: "build", path_on_host: paths.buildDisk, is_root_device: false, is_read_only: false }
902
- ],
903
- "machine-config": { vcpu_count: limits2.cpus, mem_size_mib: Math.max(1, Math.floor(limits2.memoryBytes / MIB)), smt: false },
904
- vsock: { guest_cid: GUEST_CID, uds_path: paths.vsock },
905
- "cpu-config": CPU_TEMPLATE
906
- };
907
- }
908
- function firecrackerCommand(input) {
909
- const args = ["--no-api", "--config-file", input.configPath];
910
- const binary = resolve(input.binary);
911
- if (input.cgroup === void 0) return { command: binary, args };
912
- return { command: "/bin/sh", args: ["-c", 'echo $$ > "$1/cgroup.procs" && shift && exec "$@"', "sh", input.cgroup, binary, ...args] };
1580
+ function dockerArch(nodeArch) {
1581
+ if (nodeArch === "x64") return "amd64";
1582
+ if (nodeArch === "arm64") return "arm64";
1583
+ throw new AssetError("tool files", `no tool files are pinned for ${nodeArch}`);
913
1584
  }
914
- var STOP_WAIT_MS = 1e4;
915
- var POLL_MS = 20;
916
- function isZombie(pid) {
917
- try {
918
- const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
919
- return stat.slice(stat.lastIndexOf(")") + 2, stat.lastIndexOf(")") + 3) === "Z";
920
- } catch {
921
- return false;
922
- }
1585
+ function toolFilePins(arch) {
1586
+ return [
1587
+ ...VOICE_PACK_FILES.map((file) => ({ path: `${FETCHED_VOICE_DIR}/${file.name}`, url: file.url, sha256: file.sha256 })),
1588
+ { path: FETCHED_FFMPEG, url: `https://github.com/BtbN/FFmpeg-Builds/releases/download/${FFMPEG_BUILD_TAG}/${FFMPEG_STATIC[arch].file}`, sha256: FFMPEG_STATIC[arch].sha256 },
1589
+ { path: FETCHED_GH, url: `https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_${arch}.tar.gz`, sha256: GH_SHA256[arch] },
1590
+ { path: FETCHED_TINI, url: `https://github.com/krallin/tini/releases/download/v${TINI_VERSION}/tini-static-${arch}`, sha256: TINI_SHA256[arch] }
1591
+ ];
923
1592
  }
924
- function createFirecrackerHost(options = {}) {
925
- const launch = options.launch ?? firecrackerCommand;
926
- const spawnChild = options.spawnChild ?? spawn;
927
- const exits = /* @__PURE__ */ new Map();
928
- const alive = (vm) => isSameProcess(vm.pid, vm.start) && !isZombie(vm.pid);
929
- return {
930
- async spawn(input) {
931
- const { command, args } = launch(input);
932
- mkdirSync(dirname(input.consolePath), { recursive: true });
933
- const consoleFd = openSync(input.consolePath, "a", 384);
934
- let pumping = false;
935
- try {
936
- const child = spawnChild(command, args, { detached: true, stdio: ["ignore", "pipe", "pipe"], env: {} });
937
- pumpConsole(consoleFd, [child.stdout, child.stderr]);
938
- pumping = true;
939
- for (const stream of [child.stdout, child.stderr]) stream.unref?.();
940
- const failed = new Promise((_resolve, reject) => child.once("error", reject));
941
- const pid = child.pid;
942
- if (pid === void 0) return await failed;
943
- exits.set(pid, new Promise((resolve2) => child.once("exit", () => resolve2())));
944
- child.unref();
945
- const start = startTimeOf(pid);
946
- if (start === null) throw new Error(`${command} exited as soon as it started; its console is at ${input.consolePath}`);
947
- return { pid, start };
948
- } finally {
949
- if (!pumping) closeSync(consoleFd);
950
- }
951
- },
952
- async stop(vm) {
953
- if (!alive(vm)) return;
954
- try {
955
- process.kill(vm.pid, "SIGKILL");
956
- } catch {
1593
+ function pinsDir(runnerDir2, pins) {
1594
+ const key = createHash("sha256").update(pins.map((pin) => `${pin.path} ${pin.sha256}`).join("\n")).digest("hex").slice(0, 16);
1595
+ const root = join(vmAssetsDir(runnerDir2), "tool-files");
1596
+ return { root, dir: join(root, key) };
1597
+ }
1598
+ var inFlight = /* @__PURE__ */ new Map();
1599
+ async function ensureToolFiles(options) {
1600
+ const pins = options.pins ?? toolFilePins(options.arch ?? dockerArch(process.arch));
1601
+ const { root, dir } = pinsDir(options.runnerDir, pins);
1602
+ const running = inFlight.get(dir);
1603
+ if (running !== void 0) return running;
1604
+ const fetching = fetchAll(root, dir, pins, options.fetchImpl ?? fetch).finally(() => inFlight.delete(dir));
1605
+ inFlight.set(dir, fetching);
1606
+ return fetching;
1607
+ }
1608
+ async function fetchAll(root, dir, pins, fetchImpl) {
1609
+ mkdirSync(dir, { recursive: true, mode: RUNNER_DIR_MODE });
1610
+ for (const pin of pins) {
1611
+ const path = join(dir, pin.path);
1612
+ if (existsSync(path)) {
1613
+ const actual = await sha256OfFile(path);
1614
+ if (actual !== pin.sha256) {
1615
+ throw new AssetError(pin.path, `the tool file ${path} has sha256 ${actual}, not the ${pin.sha256} this release pins, so no image build starts. Delete it to download it again`);
957
1616
  }
958
- const exited = exits.get(vm.pid);
959
- const deadline = Date.now() + STOP_WAIT_MS;
960
- if (exited !== void 0) await Promise.race([exited, new Promise((resolve2) => setTimeout(resolve2, STOP_WAIT_MS))]);
961
- while (alive(vm) && Date.now() < deadline) await new Promise((resolve2) => setTimeout(resolve2, POLL_MS));
962
- exits.delete(vm.pid);
963
- },
964
- alive
965
- };
1617
+ continue;
1618
+ }
1619
+ try {
1620
+ await downloadPinned({ url: pin.url, dest: path, sha256: pin.sha256, name: pin.path, fetchImpl, stopped: "no image build started" });
1621
+ } catch (err) {
1622
+ throw new AssetError(pin.path, `the tool file ${pin.path} could not be had: ${err instanceof VoiceDownloadError ? err.message : String(err)}`);
1623
+ }
1624
+ }
1625
+ for (const name of readdirSync(root)) {
1626
+ if (join(root, name) !== dir) rmSync(join(root, name), { recursive: true, force: true });
1627
+ }
1628
+ return dir;
966
1629
  }
967
1630
 
968
1631
  // src/runner/microvm/host.ts
969
1632
  function createMicroVmPorts(over = {}) {
970
1633
  return {
971
- assets: over.assets ?? ((runnerDir) => ensureAssets({ runnerDir })),
1634
+ assets: over.assets ?? ((runnerDir2) => ensureAssets({ runnerDir: runnerDir2 })),
972
1635
  rootfs: over.rootfs ?? createRootfsHost(),
973
1636
  firecracker: over.firecracker ?? createFirecrackerHost(),
974
1637
  cgroup: over.cgroup ?? realCgroupPort(),
975
1638
  helper: over.helper ?? realHelperPort(),
976
1639
  fdStore: over.fdStore ?? realFdStorePort(),
977
- exec: over.exec ?? execCapture
1640
+ exec: over.exec ?? execCapture,
1641
+ execStream: over.execStream ?? runExec,
1642
+ builderDisk: over.builderDisk ?? ((runnerDir2) => ensureBuilderDisk({ runnerDir: runnerDir2 })),
1643
+ builderDiskPinned: over.builderDiskPinned ?? (() => builderDiskPinned()),
1644
+ toolFiles: over.toolFiles ?? ((runnerDir2) => ensureToolFiles({ runnerDir: runnerDir2 }))
978
1645
  };
979
1646
  }
980
1647
  var aborted = () => ({ ok: false, reason: "", aborted: true });
@@ -1947,7 +2614,7 @@ function upstreamHeaders(incoming, authorization) {
1947
2614
  function isWorkflowRefusal(text) {
1948
2615
  return WORKFLOW_REFUSAL.test(text);
1949
2616
  }
1950
- function plain(res, status, text) {
2617
+ function plain2(res, status, text) {
1951
2618
  res.writeHead(status, { "content-type": "text/plain", "content-length": Buffer.byteLength(text), connection: "close" });
1952
2619
  res.end(text);
1953
2620
  }
@@ -2038,7 +2705,7 @@ function createGitRemote(options) {
2038
2705
  upstream.on("error", (err) => {
2039
2706
  log(`could not reach GitHub for ${route.kind}: ${err.message}`);
2040
2707
  if (res.headersSent) res.destroy();
2041
- else plain(res, 502, "the runner could not reach GitHub\n");
2708
+ else plain2(res, 502, "the runner could not reach GitHub\n");
2042
2709
  resolve2();
2043
2710
  });
2044
2711
  res.once("close", () => upstream.destroy());
@@ -2050,7 +2717,7 @@ function createGitRemote(options) {
2050
2717
  return await options.tokens.token(kind, repository);
2051
2718
  } catch (err) {
2052
2719
  log(`could not get a ${kind} token: ${refusalCode(err)}`);
2053
- plain(res, 502, `the runner could not get a ${kind} token: ${refusalCode(err)}
2720
+ plain2(res, 502, `the runner could not get a ${kind} token: ${refusalCode(err)}
2054
2721
  `);
2055
2722
  return void 0;
2056
2723
  }
@@ -2062,7 +2729,7 @@ function createGitRemote(options) {
2062
2729
  const refuse = async (reason) => {
2063
2730
  log(`push refused: ${reason}`);
2064
2731
  await drain(req);
2065
- plain(res, 400, `the runner refused this push: ${reason}
2732
+ plain2(res, 400, `the runner refused this push: ${reason}
2066
2733
  `);
2067
2734
  };
2068
2735
  if (encoding !== void 0 && encoding !== "gzip" && encoding !== "identity") {
@@ -2146,19 +2813,19 @@ function createGitRemote(options) {
2146
2813
  const route = classifyRequest(req.method ?? "", req.url ?? "");
2147
2814
  if (route === void 0) {
2148
2815
  req.resume();
2149
- return plain(res, 404, "not found\n");
2816
+ return plain2(res, 404, "not found\n");
2150
2817
  }
2151
2818
  if (options.readOnly === true && (route.kind === "receive-pack" || route.kind === "info-refs" && route.service === "git-receive-pack")) {
2152
2819
  log(`push refused: this remote is read-only`);
2153
2820
  req.resume();
2154
- return plain(res, 403, "this remote is read-only: a check cannot push\n");
2821
+ return plain2(res, 403, "this remote is read-only: a check cannot push\n");
2155
2822
  }
2156
2823
  const target = claimed.find((entry) => isOwnRepository(entry.repository, route.owner, route.name));
2157
2824
  const pushing = route.kind === "receive-pack" || route.kind === "info-refs" && route.service === "git-receive-pack";
2158
2825
  if (target === void 0 && pushing) {
2159
2826
  log(`push refused: ${route.owner}/${route.name} is not a repository of this build's claim`);
2160
2827
  req.resume();
2161
- return plain(res, 403, `the runner refused this push: ${route.owner}/${route.name} is not a repository of this build's claim
2828
+ return plain2(res, 403, `the runner refused this push: ${route.owner}/${route.name} is not a repository of this build's claim
2162
2829
  `);
2163
2830
  }
2164
2831
  if (target !== void 0 && route.kind === "receive-pack") return push(req, res, route, target);
@@ -2166,9 +2833,9 @@ function createGitRemote(options) {
2166
2833
  let body;
2167
2834
  if (route.kind === "lfs-batch") {
2168
2835
  body = await readBody(req, maxLfsBodyBytes);
2169
- if (body === void 0) return plain(res, 413, "the request is too large\n");
2836
+ if (body === void 0) return plain2(res, 413, "the request is too large\n");
2170
2837
  kind = isLfsUpload(body) ? "push" : "read";
2171
- if (options.readOnly === true && kind === "push") return plain(res, 403, "this remote is read-only: a check cannot upload\n");
2838
+ if (options.readOnly === true && kind === "push") return plain2(res, 403, "this remote is read-only: a check cannot upload\n");
2172
2839
  }
2173
2840
  let authorization;
2174
2841
  if (target !== void 0) {
@@ -2197,7 +2864,7 @@ function createGitRemote(options) {
2197
2864
  } catch (err) {
2198
2865
  log(`could not serve a request: ${err instanceof Error ? err.message : String(err)}`);
2199
2866
  if (res.headersSent) res.destroy();
2200
- else plain(res, 502, "the runner could not serve this request\n");
2867
+ else plain2(res, 502, "the runner could not serve this request\n");
2201
2868
  }
2202
2869
  };
2203
2870
  }
@@ -2692,6 +3359,8 @@ function withTunnelLog(opts, log) {
2692
3359
  }
2693
3360
  function createContainerRuntime(opts = {}) {
2694
3361
  const { cli, remoteFor } = opts;
3362
+ const vm = createMicroVmPorts(opts.vm);
3363
+ const sleep = opts.sleep ?? ((ms) => new Promise((resolve2) => setTimeout(resolve2, ms)));
2695
3364
  return {
2696
3365
  host: opts.host ?? createContainerHost(),
2697
3366
  cpus: opts.cpus ?? createCpuAllocator(usableCpus()),
@@ -2712,9 +3381,13 @@ function createContainerRuntime(opts = {}) {
2712
3381
  },
2713
3382
  { host: opts.imageHost ?? createDockerHost({ builder: builderSpec(build.configPath, build.buildMemory, opts.hostEnv ?? process.env) }) }
2714
3383
  )),
3384
+ buildRootDisk: opts.buildRootDisk ?? ((build) => buildRootDiskInMicroVm(
3385
+ { ...build, remote: remoteFor?.(build.repository), cli: cli ?? currentCliBuild() },
3386
+ { ports: vm, startTunnel: opts.startTunnel ?? startTunnel, tunnelOptions: opts.tunnelOptions, sleep }
3387
+ )),
2715
3388
  startTunnel: opts.startTunnel ?? startTunnel,
2716
3389
  tunnelOptions: opts.tunnelOptions,
2717
- sleep: opts.sleep ?? ((ms) => new Promise((resolve2) => setTimeout(resolve2, ms))),
3390
+ sleep,
2718
3391
  hostEnv: opts.hostEnv ?? process.env,
2719
3392
  platform: opts.platform ?? process.platform,
2720
3393
  credentials: opts.credentials ?? /* @__PURE__ */ new Map(),
@@ -2724,7 +3397,7 @@ function createContainerRuntime(opts = {}) {
2724
3397
  clone: opts.clone ?? ((input) => cloneRepository(input.remote, input.dest)),
2725
3398
  destinationOptions: opts.destinationOptions,
2726
3399
  claudeUpstream: opts.claudeUpstream,
2727
- vm: createMicroVmPorts(opts.vm),
3400
+ vm,
2728
3401
  cliBuild: () => cli ?? currentCliBuild()
2729
3402
  };
2730
3403
  }
@@ -2852,27 +3525,21 @@ async function obtainImage(input) {
2852
3525
  };
2853
3526
  }
2854
3527
  try {
2855
- return {
2856
- ok: true,
2857
- built: await runtime.buildImage({
2858
- repository,
2859
- configPath: config.configPath,
2860
- buildMemory: config.build?.memory ?? "8g",
2861
- readToken,
2862
- pinned: input.pinned,
2863
- guestHelper: input.guestHelper,
2864
- log: (line) => log.line(`${tag} ${line}`)
2865
- })
2866
- };
3528
+ const common = { repository, configPath: config.configPath, readToken, pinned: input.pinned, log: (line) => log.line(`${tag} ${line}`) };
3529
+ if (input.inBuilder !== void 0) {
3530
+ return { ok: true, built: await runtime.buildRootDisk({ ...input.inBuilder, ...common, guestHelper: input.guestHelper, deadlineMs: input.deadlineMs }) };
3531
+ }
3532
+ return { ok: true, built: await runtime.buildImage({ ...common, buildMemory: config.build?.memory ?? "8g", guestHelper: input.guestHelper }) };
2867
3533
  } catch (err) {
2868
3534
  if (err instanceof RecipeError && err.code === "build_in_progress" && (input.now?.() ?? /* @__PURE__ */ new Date()).getTime() < input.deadlineMs) {
2869
3535
  log.line(`${tag} waiting for another build of ${repository}'s image`);
2870
3536
  await runtime.sleep(BUSY_IMAGE_WAIT_MS);
2871
3537
  continue;
2872
3538
  }
3539
+ if (err instanceof AssetError) return { ok: false, reason: err.message };
2873
3540
  if (err instanceof RecipeError) {
2874
3541
  log.error(`${tag} image build for ${repository} failed: ${err.code}: ${err.message}`);
2875
- const says = err.code === "image_build_out_of_memory" || err.code === "image_builder_unavailable";
3542
+ const says = err.code === "image_build_out_of_memory" || err.code === "image_builder_unavailable" || err.code === "image_builder_failed";
2876
3543
  return { ok: false, reason: says ? err.message : err.code };
2877
3544
  }
2878
3545
  return { ok: false, reason: `the image build failed: ${describeCallFailure(err)}` };
@@ -3551,4 +4218,4 @@ async function startCheckContainer(input) {
3551
4218
  };
3552
4219
  }
3553
4220
 
3554
- export { ADDED_USER, AgentUserError, AssetError, DeliveryFailure, EVAL_INPUT_NAME, GH_CONFIG_YML, HELPER_ENV, HelperRefusal, LEASE_SECONDS, MIN_ENGINE_MAJOR, cleanupContainer, containerCommandExec, containerJudgeEnvironment, containerSenderCommand, copyJudgeTree, copyTranscripts, copyTranscriptsFromContainer, createBuildDisk, createContainerRuntime, createLeaseChannel, deleteBuildDisk, deliverContainerHooks, ensureFirecrackerAndJailer, ensureRootfs, ensureTunnel, filesAreCopied, ghHostsYml, gitRemoteFor, helperFirecrackerNote, helperSocketPath, isLiteralAddress, keepGhConfigCurrent, leaseExpiresAtMs, leaseTimeLeftMs, lifecycleFailureReason, lifecycleSteps, machineConfig, measureDisk, obtainImage, outOfMemoryReason, parseCpuList, prepareBranch, probeHelper, provisionContainer, resolveAgentUser, runBuildSteps, senderAliveScript, senderSignalScript, setUpRunnerCgroup, shellQuote, signalInContainer, startCheckContainer, startServicesFor, stopContainer, syncInContainer, withTunnelLog, writeEvalInput };
4221
+ export { ADDED_USER, AgentUserError, AssetError, DeliveryFailure, EVAL_INPUT_NAME, GH_CONFIG_YML, HELPER_ENV, LEASE_SECONDS, MANAGED_UNJAILED_REASON, MIN_ENGINE_MAJOR, ROOT_DISK_ID_PREFIX, cleanupContainer, containerCommandExec, containerJudgeEnvironment, containerSenderCommand, copyJudgeTree, copyTranscripts, copyTranscriptsFromContainer, createBuildDisk, createContainerRuntime, createLeaseChannel, deleteBuildDisk, deliverContainerHooks, ensureFirecrackerAndJailer, ensureRootfs, ensureTunnel, filesAreCopied, formatBytes, freeBytes, ghHostsYml, gitRemoteFor, helperFirecrackerNote, helperSocketPath, isLiteralAddress, keepGhConfigCurrent, leaseExpiresAtMs, leaseTimeLeftMs, lifecycleFailureReason, lifecycleSteps, machineConfig, makeBuildsDirPrivate, measureDisk, obtainImage, outOfMemoryReason, parseCpuList, prepareBranch, probeHelper, provisionContainer, resolveAgentUser, runBuildSteps, senderAliveScript, senderSignalScript, setUpRunnerCgroup, shellQuote, signalInContainer, startCheckContainer, startJailedInto, startServicesFor, stopContainer, stopJailed, sweepImageBuilders, syncInContainer, withTunnelLog, writeEvalInput };