@outerlayer/cli 0.33.0-next.20261022195547 → 0.33.0-next.20261023002325

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 (111) hide show
  1. package/README.md +5 -0
  2. package/dist/{agent-setup-RJOVRRHK.js → agent-setup-55ZDEXCS.js} +6 -6
  3. package/dist/{bridge-BCG2EB7F.js → bridge-NBCXFTT4.js} +2 -2
  4. package/dist/{build-GKCVFHF4.js → build-G4GUF6LT.js} +4 -4
  5. package/dist/{build-disk-6QAXMA2J.js → build-disk-3GL3KKEB.js} +5 -5
  6. package/dist/build-info.json +1 -1
  7. package/dist/{builder-DPNHJXG3.js → builder-3CUKTVSY.js} +4 -4
  8. package/dist/{check-64UX323V.js → check-64FD5G3X.js} +35 -37
  9. package/dist/{chunk-3MXPS7ED.js → chunk-2N67C6IV.js} +5 -4
  10. package/dist/{chunk-ZQOSIMKU.js → chunk-2WMZPJVE.js} +6 -3
  11. package/dist/{chunk-5BNWAIHN.js → chunk-3DCB3GOV.js} +41 -3
  12. package/dist/{chunk-UOFBHIOZ.js → chunk-57L2YAJA.js} +1 -1
  13. package/dist/{chunk-6NWS455T.js → chunk-5THSC6BD.js} +1 -1
  14. package/dist/{chunk-QL3LPZ67.js → chunk-6LBSAW6I.js} +1 -1
  15. package/dist/{chunk-WPGBDY73.js → chunk-6RKD7YZ6.js} +1 -1
  16. package/dist/{chunk-77B36F5K.js → chunk-6Z4SZHV2.js} +4 -3
  17. package/dist/{chunk-DETKDTNL.js → chunk-AU346ENF.js} +3 -2
  18. package/dist/chunk-B2JURP6A.js +41 -0
  19. package/dist/{chunk-BCCI32ME.js → chunk-BALIE3BA.js} +36 -7
  20. package/dist/{chunk-BY36DVJV.js → chunk-BKADJZ7F.js} +3 -2
  21. package/dist/{chunk-H7A3IQH5.js → chunk-CANYZTD5.js} +39 -22
  22. package/dist/{chunk-L6JFFP3Q.js → chunk-DGI66PGY.js} +32 -20
  23. package/dist/{chunk-GN2R5Z7G.js → chunk-DXJTBU7A.js} +3 -3
  24. package/dist/{chunk-WCELNMXT.js → chunk-EUVE2EAE.js} +2 -2
  25. package/dist/{chunk-DDS2ISML.js → chunk-F5UL23I2.js} +1 -1
  26. package/dist/{chunk-43PV5KDB.js → chunk-GFVDC5S3.js} +3 -3
  27. package/dist/{chunk-JEKENLLA.js → chunk-GH246BGV.js} +1 -1
  28. package/dist/{chunk-W5DOK7BL.js → chunk-HVRQQOMH.js} +3 -3
  29. package/dist/{chunk-6IKSUAIA.js → chunk-HXA5GIHK.js} +61 -3
  30. package/dist/{chunk-2QT4JUW3.js → chunk-IP7BIBXJ.js} +1 -35
  31. package/dist/{chunk-KLXWS5Z7.js → chunk-JPG43G6Y.js} +3 -3
  32. package/dist/{chunk-D3HRAPNF.js → chunk-JRZCRIEO.js} +1044 -640
  33. package/dist/{chunk-LWTBE5J6.js → chunk-L26V6ILJ.js} +1 -1
  34. package/dist/{chunk-ZXBQ6E72.js → chunk-LJUIG5K4.js} +14 -9
  35. package/dist/{chunk-ZRRUWNJS.js → chunk-LYE3KB6L.js} +3 -3
  36. package/dist/{chunk-SK4TWCN3.js → chunk-MZBQTVFG.js} +1 -1
  37. package/dist/{chunk-YKYCE4HY.js → chunk-MZYOUIY5.js} +1 -1
  38. package/dist/{chunk-OWDSO3PE.js → chunk-NBZ7XBT6.js} +1 -1
  39. package/dist/{chunk-6C2YWM6N.js → chunk-OOCAF3ID.js} +1 -1
  40. package/dist/{chunk-AGZNCIIJ.js → chunk-PA6CK6WQ.js} +1 -1
  41. package/dist/{chunk-QDMMW5PC.js → chunk-Q2CMEQV7.js} +2 -2
  42. package/dist/{chunk-DNKUSXQU.js → chunk-QFQC5K4J.js} +6 -113
  43. package/dist/{chunk-DJWKUQLO.js → chunk-QPWFVE3X.js} +1 -1
  44. package/dist/{chunk-AA6LYZC4.js → chunk-SOI6K5DY.js} +1 -1
  45. package/dist/{chunk-35IY5HQY.js → chunk-SVDOA4W2.js} +1 -1
  46. package/dist/{chunk-RBJG5MM4.js → chunk-T5REPMNL.js} +5 -5
  47. package/dist/{chunk-6KLPC4U7.js → chunk-ULBNTSAD.js} +3 -2
  48. package/dist/{chunk-CMKY36NV.js → chunk-VT4TTJPG.js} +6 -5
  49. package/dist/{chunk-FAZDYI2D.js → chunk-XL2XWWU7.js} +3 -2
  50. package/dist/{chunk-P5FC7VUO.js → chunk-XWFSAOC7.js} +2 -2
  51. package/dist/{chunk-CUBIZBMF.js → chunk-YERR22SY.js} +3 -3
  52. package/dist/{chunk-GWDS42JC.js → chunk-Z5IW4DHH.js} +1 -1
  53. package/dist/{claude-launcher-66GKNIRZ.js → claude-launcher-NTYHJUJM.js} +1 -1
  54. package/dist/{cli-WICFAKFW.js → cli-WKRURGHC.js} +126 -102
  55. package/dist/{command-VGR24IPI.js → command-WG54AUHK.js} +23 -23
  56. package/dist/{config-DUY2PT7K.js → config-LZSFW2O3.js} +2 -3
  57. package/dist/{connect-cmd-ADRPR6ES.js → connect-cmd-P5GASVAF.js} +5 -5
  58. package/dist/{container-start-QX53HKXW.js → container-start-DQKFQ3CL.js} +21 -23
  59. package/dist/context-show-cmd-3EPJ7CJC.js +115 -0
  60. package/dist/{control-plane-launch-SMHL6MWW.js → control-plane-launch-A3YCHQNQ.js} +5 -5
  61. package/dist/{docker-GGRJHHBP.js → docker-4ERXWTRW.js} +4 -4
  62. package/dist/{doctor-N3CDJ66I.js → doctor-GRCEYJXA.js} +24 -26
  63. package/dist/{emit-artifact-cmd-73OWUBH6.js → emit-artifact-cmd-FDPJKGOO.js} +11 -10
  64. package/dist/{emit-criteria-cmd-D7OR356Y.js → emit-criteria-cmd-3SVGCMJN.js} +8 -7
  65. package/dist/{emit-demo-cmd-4AV74GMZ.js → emit-demo-cmd-JSVI56WY.js} +11 -10
  66. package/dist/{emit-finding-cmd-GEPRJ6KO.js → emit-finding-cmd-TLJWBGQE.js} +8 -7
  67. package/dist/{emit-result-cmd-N52IABK6.js → emit-result-cmd-CJNVDV5V.js} +10 -9
  68. package/dist/{emit-step-cmd-CC6MUJ2S.js → emit-step-cmd-347CZO7Y.js} +8 -7
  69. package/dist/{exec-client-KLI262IX.js → exec-client-7PT4W7JR.js} +3 -3
  70. package/dist/{findings-cmd-7I72XX2H.js → findings-cmd-LHTYEHYB.js} +8 -7
  71. package/dist/{fleet-readiness-EZAGL32M.js → fleet-readiness-KGR5AC2J.js} +5 -5
  72. package/dist/{governing-source-M7ZRJCGY.js → governing-source-JZQZUTMA.js} +10 -9
  73. package/dist/{guest-init-ZJIE6MU3.js → guest-init-J3F3AEEY.js} +4 -4
  74. package/dist/{hook-fast-WNKICZ6E.js → hook-fast-6WDMNZG5.js} +1 -1
  75. package/dist/{pending-I5TY6SQH.js → host-key-UERIQRE6.js} +3 -3
  76. package/dist/{image-tag-MORUQ4LF.js → image-tag-ADSLWXBA.js} +3 -3
  77. package/dist/index.js +4 -4
  78. package/dist/{init-XG4NRZX4.js → init-OKJAI77M.js} +20 -22
  79. package/dist/{init-VCWDBOT4.js → init-WNO4NAO7.js} +20 -22
  80. package/dist/{init-cmd-2CAMWMRP.js → init-cmd-PFW4XTG7.js} +11 -10
  81. package/dist/{init-update-NCM4GA3S.js → init-update-X7YLZL6H.js} +8 -8
  82. package/dist/{install-NLVYISAV.js → install-GQUCHIGE.js} +4 -5
  83. package/dist/{install-cmd-HFGINYEE.js → install-cmd-FV7IZ7XF.js} +5 -5
  84. package/dist/{logs-QSKJ4G6M.js → logs-HI46FO6Z.js} +2 -2
  85. package/dist/{loop-PXALY3VY.js → loop-2SITCQ6H.js} +35 -37
  86. package/dist/{machine-AZRL432D.js → machine-YHBCNRUO.js} +20 -18
  87. package/dist/{mcp-serve-cmd-DQSO6MOL.js → mcp-serve-cmd-7ABBV2QE.js} +4 -4
  88. package/dist/{paths-KHDFSBEI.js → paths-7HVTDE6J.js} +2 -2
  89. package/dist/{host-key-REZADWAM.js → pending-5HZACM2V.js} +3 -3
  90. package/dist/{pidfile-BSY63HIC.js → pidfile-5UDLYPMC.js} +3 -3
  91. package/dist/{record-7CNTGRLD.js → record-PPZEXAVA.js} +3 -3
  92. package/dist/{runner-env-26NLQI35.js → runner-env-QYE7KZFP.js} +5 -6
  93. package/dist/{runner-identity-WGH73DTV.js → runner-identity-FIHOECSH.js} +4 -4
  94. package/dist/{runner-key-6BIULOR4.js → runner-key-YUKU2MWK.js} +20 -22
  95. package/dist/{session-sender-MSHKJSBG.js → session-sender-7B3ZDWMC.js} +11 -10
  96. package/dist/{spec-cmd-AWIMO32E.js → spec-cmd-Q26NE22H.js} +9 -8
  97. package/dist/{status-BILERIXB.js → status-VBQJMFEX.js} +27 -25
  98. package/dist/{sync-cmd-BSUZ65CU.js → sync-cmd-SSUYTMLJ.js} +10 -9
  99. package/dist/{watch-UF6XCGSI.js → watch-L5NCO46E.js} +10 -9
  100. package/dist/{work-claim-cmd-4FXVAZWF.js → work-claim-cmd-MFND6Z4S.js} +12 -12
  101. package/dist/{work-cmd-NCP4637R.js → work-cmd-W7WEHMQJ.js} +8 -7
  102. package/dist/{work-comment-cmd-YNQYVHYS.js → work-comment-cmd-SSUOGVDD.js} +8 -7
  103. package/dist/{work-logs-cmd-HHWVYQBG.js → work-logs-cmd-WPMWMDX6.js} +8 -7
  104. package/dist/{work-open-pr-cmd-ALVD7KLY.js → work-open-pr-cmd-NO5YLOXD.js} +9 -8
  105. package/dist/{work-pr-cmd-KBNFVRB4.js → work-pr-cmd-37B5KECR.js} +8 -7
  106. package/dist/{work-stop-cmd-DBWNHZJO.js → work-stop-cmd-3BJLXUJ3.js} +8 -7
  107. package/package.json +1 -1
  108. package/skill-pack/maintained/outerlayer-build/SKILL.md +4 -2
  109. package/dist/chunk-I5LGKZ4M.js +0 -107
  110. package/dist/chunk-UXYLWVTQ.js +0 -43
  111. package/dist/chunk-WBZJWVT2.js +0 -146
@@ -1,34 +1,37 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'node:module';
3
3
  import { sha256OfFile, downloadPinned, VoiceDownloadError } from './chunk-2PGI6RDE.js';
4
- 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-KLXWS5Z7.js';
4
+ import { hasStoredCredential, AGENT_COMMANDS, storeClaudeCredential, credentialFromStdin, isAgentName } from './chunk-YERR22SY.js';
5
+ 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-JPG43G6Y.js';
5
6
  import { startTimeOf, isSameProcess } from './chunk-JBJTC3UU.js';
6
7
  import { currentCliBuild } from './chunk-OGFGZQA3.js';
7
- import { runExec, execCapture, waitForAgent } from './chunk-6NWS455T.js';
8
- import { GUEST_CID, linkVsockSockets } from './chunk-LWTBE5J6.js';
9
- import { describeCallFailure, keepStagedContext, controlPlaneTokenFailure, refusalCode, resolveGoverningSource, prefetchGovernedContext, removeStagedContext, stageGovernedContext, pinnedSourceOf, commandWithShim, shimScript, WORKFLOW_PERMISSION_REASON } from './chunk-RBJG5MM4.js';
10
- import { runnerImageVersion } from './chunk-SK4TWCN3.js';
8
+ import { runExec, execCapture, waitForAgent } from './chunk-5THSC6BD.js';
9
+ import { GUEST_CID, linkVsockSockets } from './chunk-L26V6ILJ.js';
10
+ import { describeCallFailure, keepStagedContext, controlPlaneTokenFailure, refusalCode, resolveGoverningSource, prefetchGovernedContext, removeStagedContext, stageGovernedContext, pinnedSourceOf, commandWithShim, shimScript, WORKFLOW_PERMISSION_REASON } from './chunk-T5REPMNL.js';
11
+ import { runnerImageVersion } from './chunk-MZBQTVFG.js';
11
12
  import { formatBytes } from './chunk-YYYRBYWG.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-43PV5KDB.js';
13
+ 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-GFVDC5S3.js';
13
14
  import { RecipeError, DOCKER_FEATURE, writeSanitizedRecipe, classifyProperties } 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, 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-TGHBOXKJ.js';
15
- import { hostAllowed } from './chunk-UXYLWVTQ.js';
15
+ import { firecrackerArch, MICROVM_ARCHES, FIRECRACKER_VERSION, hostHelperPinned, CLAUDE_CODE_VERSION, guestAssetsPinned, 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-TGHBOXKJ.js';
16
+ import { runnerCredentialProblem, builtinKind, hostAllowed } from './chunk-3DCB3GOV.js';
16
17
  import { readDu } from './chunk-YJLHUKYS.js';
17
- import { WorkCommandError } from './chunk-CMKY36NV.js';
18
+ import { WorkCommandError, getJson } from './chunk-VT4TTJPG.js';
19
+ import { sanitizeTerminal } from './chunk-EWVYQLD3.js';
18
20
  import { setCustomScrubbers, setLocalIdentity, setRepoRoot, parseTranscript, scrubDeep, scrubText } from './chunk-5DNCFOXA.js';
19
- import { rootfsPathFor, diskTemplatesDir, RUNNER_FILE_MODE, buildsDir, buildDir, vmAssetsDir, RUNNER_DIR_MODE, deliveryDir, ghConfigDir, runnerDirOfBuild, spoolDir, mirrorDir, SHIM_DIR, runnerDir, vsockPath, builderCacheDiskPath, builderConsolePath } from './chunk-AGZNCIIJ.js';
20
- import { repositoriesOf, primaryOf, RELAY_PORT, repositoryPath, CHECKOUT_DIR, lifecycleWorkdir, claudeCredential, buildEnvironment, writeBuildMavenSettings, startBuildServices, checkEnvironment, AGENT_CREDENTIAL_MISSING, checkoutPath, JOB_MOUNT } from './chunk-BCCI32ME.js';
21
+ import { signerFor } from './chunk-SOI6K5DY.js';
22
+ import { vmAssetsDir, RUNNER_DIR_MODE, rootfsPathFor, diskTemplatesDir, RUNNER_FILE_MODE, buildsDir, buildDir, runnerDir, deliveryDir, ghConfigDir, runnerDirOfBuild, spoolDir, mirrorDir, SHIM_DIR, vsockPath, builderCacheDiskPath, builderConsolePath } from './chunk-PA6CK6WQ.js';
23
+ import { CLAUDE_CREDENTIAL_VARIABLES, claudeCredential, repositoriesOf, primaryOf, RELAY_PORT, repositoryPath, CHECKOUT_DIR, lifecycleWorkdir, buildEnvironment, writeBuildMavenSettings, startBuildServices, checkEnvironment, AGENT_CREDENTIAL_MISSING, checkoutPath, JOB_MOUNT } from './chunk-BALIE3BA.js';
21
24
  import { sharedSkillNames, skillConflictMessage } from './chunk-FGF52V4V.js';
22
25
  import { versionAtLeast, MINIMUM_CLAUDE_VERSION } from './chunk-HCVLP7XA.js';
23
26
  import { orderSessions, extractFacts, sessionsToTimeline } from './chunk-LJG6EMLJ.js';
24
27
  import { downconvertSession } from './chunk-E6CO3MJE.js';
25
28
  import { realFdStorePort } from './chunk-HACGUL52.js';
26
29
  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';
27
- import { 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';
28
- import { join, dirname, relative, resolve, basename } from 'path';
29
- import { isIP, createConnection, connect, createServer as createServer$1 } from 'net';
30
30
  import { spawnSync, spawn } from 'child_process';
31
+ import { mkdirSync, existsSync, readFileSync, mkdtempSync, rmSync, chmodSync, truncateSync, statSync, readdirSync, writeFileSync, lstatSync, copyFileSync, renameSync, accessSync, constants, rmdirSync, unlinkSync, linkSync, openSync, closeSync, cpSync, writeSync, appendFileSync, createWriteStream, createReadStream } from 'fs';
31
32
  import { randomBytes, createHash } from 'crypto';
33
+ import { join, dirname, relative, resolve, basename } from 'path';
34
+ import { isIP, createConnection, connect, createServer as createServer$1 } from 'net';
32
35
  import { availableParallelism, networkInterfaces, tmpdir } from 'os';
33
36
  import { Duplex, PassThrough, Transform } from 'stream';
34
37
  import http2 from 'http2';
@@ -39,227 +42,168 @@ import { createGunzip, gunzipSync, constants as constants$1 } from 'zlib';
39
42
  import { lookup } from 'dns/promises';
40
43
 
41
44
  createRequire(import.meta.url);
42
- var BUILD_PREFIX = "build-";
43
- var LEAF = "runner";
44
- function serviceDirectory(selfCgroup, mount) {
45
- const line = selfCgroup?.split("\n").find((l) => l.startsWith("0::"));
46
- if (line === void 0) return void 0;
47
- const path = line.slice(3).trim();
48
- if (!path.endsWith(".service")) return void 0;
49
- return join(mount, path);
50
- }
51
- function prepareRunnerCgroup(setup) {
52
- if (setup.invocationId === void 0 || setup.invocationId === "") return void 0;
53
- const root = serviceDirectory(setup.selfCgroup, setup.mount);
54
- if (root === void 0) return void 0;
55
- const { fs } = setup;
56
- if (!fs.exists(root) || !fs.writable(join(root, "cgroup.procs"))) return void 0;
57
- const controllers = (fs.read(join(root, "cgroup.controllers")) ?? "").split(/\s+/);
58
- if (!controllers.includes("memory")) return void 0;
59
- for (const name of fs.list(root)) {
60
- if (!name.startsWith(BUILD_PREFIX)) continue;
61
- try {
62
- fs.rmdir(join(root, name));
63
- } catch {
64
- }
45
+ var AssetError = class extends Error {
46
+ constructor(file, message) {
47
+ super(message);
48
+ this.file = file;
65
49
  }
66
- if (!fs.exists(join(root, LEAF))) fs.mkdir(join(root, LEAF));
67
- fs.write(join(root, LEAF, "cgroup.procs"), String(setup.pid));
68
- fs.write(join(root, "cgroup.subtree_control"), controllers.includes("io") ? "+memory +io" : "+memory");
69
- return { root };
50
+ };
51
+ var FIRECRACKER_RELEASES = "https://github.com/firecracker-microvm/firecracker/releases/download";
52
+ var DOWNLOAD_TIMEOUT_MS = 10 * 6e4;
53
+ function pinnedAssets(arch) {
54
+ const version = FIRECRACKER_VERSION;
55
+ return {
56
+ firecracker: {
57
+ url: `${FIRECRACKER_RELEASES}/v${version}/firecracker-v${version}-${arch}.tgz`,
58
+ sha256: FIRECRACKER_SHA256[arch],
59
+ binarySha256: FIRECRACKER_BINARY_SHA256[arch],
60
+ jailerSha256: JAILER_BINARY_SHA256[arch]
61
+ },
62
+ kernel: { url: `${GUEST_ASSETS_BASE_URL}/vmlinux-${arch}`, sha256: GUEST_KERNEL_SHA256[arch] },
63
+ helper: { url: `${GUEST_ASSETS_BASE_URL}/outerlayer-guest-${arch}`, sha256: GUEST_HELPER_SHA256[arch] }
64
+ };
70
65
  }
71
- var BUILD_RECLAIM_HEADROOM_BYTES = 256 * 1024 ** 2;
72
- var BUILD_KILL_HEADROOM_BYTES = 1024 ** 3;
73
- function createBuildCgroup(fs, root, buildId, buildMemoryBytes) {
74
- const path = join(root, `${BUILD_PREFIX}${buildId}`);
75
- fs.mkdir(path);
76
- try {
77
- fs.write(join(path, "memory.high"), String(buildMemoryBytes + BUILD_RECLAIM_HEADROOM_BYTES));
78
- fs.write(join(path, "memory.max"), String(buildMemoryBytes + BUILD_KILL_HEADROOM_BYTES));
79
- fs.write(join(path, "memory.swap.max"), "0");
80
- } catch (err) {
81
- removeBuildCgroup(fs, path);
82
- throw err;
83
- }
84
- return path;
66
+ function builderDiskPin(arch) {
67
+ return { url: `${BUILDER_DISK_BASE_URL}/builder-rootfs-${arch}.ext4`, sha256: BUILDER_DISK_SHA256[arch] };
85
68
  }
86
- function outOfMemoryReason(memory) {
87
- return `the build ran out of memory: the kernel killed its microVM at the limit of runner.build.memory (${memory ?? "8g"}) plus 1 GiB for Firecracker and its disks' page cache. Raise runner.build.memory, or lower runner.concurrency`;
69
+ var sha256Of = (bytes) => createHash("sha256").update(bytes).digest("hex");
70
+ function pinProblem(file, expected, actual, where) {
71
+ 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})` : "";
72
+ return new AssetError(file, `${file} ${where} has sha256 ${actual}, not the ${expected} this release pins${release}, so no microVM build starts`);
88
73
  }
89
- function oomKills(fs, path) {
90
- const match = /^oom_kill (\d+)$/m.exec(fs.read(join(path, "memory.events")) ?? "");
91
- return match === null ? 0 : Number(match[1]);
74
+ function writeAtomic(path, bytes, mode) {
75
+ const temp = `${path}.${process.pid}.tmp`;
76
+ writeFileSync(temp, bytes, { mode });
77
+ renameSync(temp, path);
92
78
  }
93
- function removeBuildCgroup(fs, path) {
79
+ async function download(file, url, fetchImpl) {
80
+ let response;
94
81
  try {
95
- fs.rmdir(path);
82
+ response = await fetchImpl(url, { signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) });
96
83
  } catch (err) {
97
- if (err.code !== "ENOENT") throw err;
84
+ throw new AssetError(file, `${file} could not be downloaded from ${url}: ${err instanceof Error ? err.message : String(err)}`);
98
85
  }
86
+ if (!response.ok) throw new AssetError(file, `${file} could not be downloaded from ${url}: the server answered ${response.status}`);
87
+ return Buffer.from(await response.arrayBuffer());
99
88
  }
100
- function realCgroupFs() {
101
- return {
102
- exists: (path) => existsSync(path),
103
- read: (path) => {
104
- try {
105
- return readFileSync(path, "utf8");
106
- } catch {
107
- return void 0;
108
- }
109
- },
110
- write: (path, data) => writeFileSync(path, data),
111
- mkdir: (path) => mkdirSync(path),
112
- rmdir: (path) => rmdirSync(path),
113
- list: (path) => readdirSync(path).filter((name) => statSync(join(path, name)).isDirectory()),
114
- writable: (path) => {
115
- try {
116
- accessSync(path, constants.W_OK);
117
- return true;
118
- } catch {
119
- return false;
120
- }
121
- }
89
+ function keptPath(dir, sha256, name) {
90
+ mkdirSync(join(dir, sha256), { recursive: true, mode: RUNNER_DIR_MODE });
91
+ return join(dir, sha256, name);
92
+ }
93
+ async function ensureFile(dir, file, pin, fetchImpl, mode) {
94
+ const path = keptPath(dir, pin.sha256, file);
95
+ if (existsSync(path)) {
96
+ const actual2 = sha256Of(readFileSync(path));
97
+ if (actual2 !== pin.sha256) throw pinProblem(file, pin.sha256, actual2, `at ${path}`);
98
+ return path;
99
+ }
100
+ const bytes = await download(file, pin.url, fetchImpl);
101
+ const actual = sha256Of(bytes);
102
+ if (actual !== pin.sha256) throw pinProblem(file, pin.sha256, actual, `downloaded from ${pin.url}`);
103
+ writeAtomic(path, bytes, mode);
104
+ return path;
105
+ }
106
+ function extractWithTar(arch, program = "firecracker") {
107
+ const name = `${program}-v${FIRECRACKER_VERSION}-${arch}`;
108
+ return async (tarball, destDir) => {
109
+ const result = spawnSync("tar", ["-xzf", tarball, "-C", destDir, "--strip-components=1", `release-v${FIRECRACKER_VERSION}-${arch}/${name}`], { encoding: "utf8" });
110
+ if (result.status !== 0) throw new Error(`tar could not unpack ${tarball}: ${result.stderr.trim()}`);
111
+ return join(destDir, name);
122
112
  };
123
113
  }
124
- var delegatedRoot;
125
- function setUpRunnerCgroup(over = {}) {
126
- let selfCgroup;
114
+ async function ensureFirecracker(input) {
115
+ const { dir, arch, pins, fetchImpl } = input;
116
+ const firecracker = keptPath(dir, pins.firecracker.binarySha256, "firecracker");
117
+ if (existsSync(firecracker)) {
118
+ const actual = sha256Of(readFileSync(firecracker));
119
+ if (actual !== pins.firecracker.binarySha256) throw pinProblem("firecracker", pins.firecracker.binarySha256, actual, `at ${firecracker}`);
120
+ return firecracker;
121
+ }
122
+ const tarballName = `firecracker-${arch}.tgz`;
123
+ const tarball = await ensureFile(dir, tarballName, pins.firecracker, fetchImpl, RUNNER_FILE_MODE);
124
+ const scratch = mkdtempSync(join(dir, ".extract-"));
127
125
  try {
128
- selfCgroup = readFileSync("/proc/self/cgroup", "utf8");
129
- } catch {
130
- selfCgroup = void 0;
126
+ const extracted = await (input.extract ?? extractWithTar(arch))(tarball, scratch);
127
+ const bytes = readFileSync(extracted);
128
+ const actual = sha256Of(bytes);
129
+ if (actual !== pins.firecracker.binarySha256) throw pinProblem("firecracker", pins.firecracker.binarySha256, actual, `unpacked from ${tarballName}`);
130
+ writeAtomic(firecracker, bytes, 493);
131
+ } finally {
132
+ rmSync(scratch, { recursive: true, force: true });
131
133
  }
132
- let refusal;
134
+ return firecracker;
135
+ }
136
+ async function ensureFirecrackerAndJailer(options) {
137
+ const arch = options.arch ?? firecrackerArch(process.arch);
138
+ 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`);
139
+ const pins = options.pins ?? pinnedAssets(arch);
140
+ const dir = vmAssetsDir(options.runnerDir);
141
+ mkdirSync(dir, { recursive: true, mode: RUNNER_DIR_MODE });
142
+ const firecracker = await ensureFirecracker({ dir, arch, pins, fetchImpl: options.fetchImpl ?? fetch, extract: options.extractFirecracker });
143
+ return { firecracker, jailer: await ensureJailer(options) };
144
+ }
145
+ async function ensureAssets(options) {
146
+ const arch = options.arch ?? firecrackerArch(process.arch);
147
+ 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`);
148
+ const pins = options.pins ?? pinnedAssets(arch);
149
+ const fetchImpl = options.fetchImpl ?? fetch;
150
+ const dir = vmAssetsDir(options.runnerDir);
151
+ mkdirSync(dir, { recursive: true, mode: RUNNER_DIR_MODE });
152
+ const firecracker = await ensureFirecracker({ dir, arch, pins, fetchImpl, extract: options.extractFirecracker });
153
+ const kernel = await ensureFile(dir, `vmlinux-${arch}`, pins.kernel, fetchImpl, RUNNER_FILE_MODE);
154
+ const helper = await ensureFile(dir, `outerlayer-guest-${arch}`, pins.helper, fetchImpl, 493);
155
+ return { firecracker, kernel, helper, helperSha256: pins.helper.sha256 };
156
+ }
157
+ async function ensureJailer(options) {
158
+ const arch = options.arch ?? firecrackerArch(process.arch);
159
+ 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`);
160
+ const pins = options.pins ?? pinnedAssets(arch);
161
+ const expected = pins.firecracker.jailerSha256;
162
+ if (expected === void 0) throw new AssetError("jailer", "this release pins no hash for the jailer, so it cannot be had");
163
+ const fetchImpl = options.fetchImpl ?? fetch;
164
+ const dir = vmAssetsDir(options.runnerDir);
165
+ mkdirSync(dir, { recursive: true, mode: RUNNER_DIR_MODE });
166
+ const jailer = keptPath(dir, expected, "jailer");
167
+ if (existsSync(jailer)) {
168
+ const actual = sha256Of(readFileSync(jailer));
169
+ if (actual !== expected) throw pinProblem("jailer", expected, actual, `at ${jailer}`);
170
+ return jailer;
171
+ }
172
+ const tarballName = `firecracker-${arch}.tgz`;
173
+ const tarball = await ensureFile(dir, tarballName, pins.firecracker, fetchImpl, RUNNER_FILE_MODE);
174
+ const scratch = mkdtempSync(join(dir, ".extract-"));
133
175
  try {
134
- const prepared = prepareRunnerCgroup({ fs: realCgroupFs(), selfCgroup, pid: process.pid, invocationId: process.env.INVOCATION_ID, mount: "/sys/fs/cgroup", ...over });
135
- delegatedRoot = prepared?.root;
136
- } catch (err) {
137
- delegatedRoot = void 0;
138
- refusal = err instanceof Error ? err.message : String(err);
176
+ const extracted = await (options.extractJailer ?? extractWithTar(arch, "jailer"))(tarball, scratch);
177
+ const bytes = readFileSync(extracted);
178
+ const actual = sha256Of(bytes);
179
+ if (actual !== expected) throw pinProblem("jailer", expected, actual, `unpacked from ${tarballName}`);
180
+ writeAtomic(jailer, bytes, 493);
181
+ } finally {
182
+ rmSync(scratch, { recursive: true, force: true });
139
183
  }
140
- return { delegated: delegatedRoot !== void 0, ...refusal === void 0 ? {} : { refusal } };
184
+ return jailer;
141
185
  }
142
- function realCgroupPort() {
143
- const fs = realCgroupFs();
144
- return {
145
- create: (buildId, bytes) => {
146
- if (delegatedRoot === void 0) return void 0;
147
- return createBuildCgroup(fs, delegatedRoot, buildId, bytes);
148
- },
149
- oomKills: (path) => oomKills(fs, path),
150
- remove: (path) => removeBuildCgroup(fs, path)
151
- };
186
+ async function ensureBuilderDisk(options) {
187
+ const arch = options.arch ?? firecrackerArch(process.arch);
188
+ 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`);
189
+ const file = `builder-rootfs-${arch}.ext4`;
190
+ const pin = options.pin ?? builderDiskPin(arch);
191
+ if (options.pin === void 0 && !builderDiskPinned(arch)) {
192
+ 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`);
193
+ }
194
+ const dir = vmAssetsDir(options.runnerDir);
195
+ mkdirSync(dir, { recursive: true, mode: RUNNER_DIR_MODE });
196
+ return ensureFile(dir, file, pin, options.fetchImpl ?? fetch, RUNNER_FILE_MODE);
152
197
  }
153
- var SUPPORTED_HELPER_PROTOCOLS = [1];
154
- var HELPER_ENV = "OUTERLAYER_HELPER_SOCKET";
155
- var HelperRefusal = class extends Error {
156
- constructor(code, reason) {
157
- super(reason);
158
- this.code = code;
198
+ var STOP_GRACE_SECONDS = 15;
199
+ function parseImageConfig(json) {
200
+ const config = JSON.parse(json);
201
+ const env = {};
202
+ for (const entry of config.Env ?? []) {
203
+ const eq = entry.indexOf("=");
204
+ if (eq > 0) env[entry.slice(0, eq)] = entry.slice(eq + 1);
159
205
  }
160
- };
161
- function helperSocketPath(unitName) {
162
- return `/run/${unitName}-helper/helper.sock`;
163
- }
164
- var START_TIMEOUT_MS = 12e4;
165
- var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
166
- var MAX_REPLY_BYTES = 1024 * 1024;
167
- function ask(socketPath, request, timeoutMs) {
168
- return new Promise((resolve2, reject) => {
169
- const conn = createConnection(socketPath);
170
- let text = "";
171
- let settled = false;
172
- const settle = (finish) => {
173
- if (settled) return;
174
- settled = true;
175
- conn.destroy();
176
- finish();
177
- };
178
- conn.setTimeout(timeoutMs, () => settle(() => reject(new Error(`the helper at ${socketPath} did not answer within ${Math.round(timeoutMs / 1e3)} seconds`))));
179
- conn.on("connect", () => conn.write(`${JSON.stringify(request)}
180
- `));
181
- conn.on("data", (chunk) => {
182
- text += chunk.toString("utf8");
183
- if (text.length > MAX_REPLY_BYTES) return settle(() => reject(new Error(`the helper at ${socketPath} sent a reply that is too long`)));
184
- const end = text.indexOf("\n");
185
- if (end === -1) return;
186
- settle(() => {
187
- let reply;
188
- try {
189
- reply = JSON.parse(text.slice(0, end));
190
- } catch {
191
- return reject(new Error(`the helper at ${socketPath} sent a reply that is not JSON`));
192
- }
193
- if (reply.status === "refused") return reject(new HelperRefusal(String(reply.code ?? "refused"), String(reply.reason ?? "the helper refused the request")));
194
- resolve2(reply);
195
- });
196
- });
197
- conn.on("error", (err) => settle(() => reject(new Error(`the helper at ${socketPath} did not answer: ${err.message}`))));
198
- conn.on("close", () => settle(() => reject(new Error(`the helper at ${socketPath} closed the connection without a reply`))));
199
- });
200
- }
201
- function helperClient(socketPath, options = {}) {
202
- const REQUEST_TIMEOUT_MS = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
203
- return {
204
- async start({ build, kind, timeLimitMinutes }) {
205
- const reply = await ask(socketPath, { v: 1, op: "start", build, kind, timeLimitMinutes }, START_TIMEOUT_MS);
206
- return { unit: String(reply.unit), chroot: String(reply.chroot), vsock: String(reply.vsock) };
207
- },
208
- async stop(build) {
209
- await ask(socketPath, { v: 1, op: "stop", build }, REQUEST_TIMEOUT_MS);
210
- },
211
- async state(build) {
212
- const reply = await ask(socketPath, { v: 1, op: "state", build }, REQUEST_TIMEOUT_MS);
213
- return { active: reply.active === true, oomKills: typeof reply.oomKills === "number" ? reply.oomKills : 0 };
214
- },
215
- async list() {
216
- const reply = await ask(socketPath, { v: 1, op: "list" }, REQUEST_TIMEOUT_MS);
217
- return Array.isArray(reply.builds) ? reply.builds.map(String) : [];
218
- },
219
- async relink(build) {
220
- await ask(socketPath, { v: 1, op: "relink", build }, REQUEST_TIMEOUT_MS);
221
- }
222
- };
223
- }
224
- function helperFirecrackerNote(helperFirecracker) {
225
- return `the root helper runs Firecracker ${helperFirecracker}, not the ${FIRECRACKER_VERSION} this release pins, so microVM builds run under the jailer with Firecracker ${helperFirecracker}`;
226
- }
227
- function reinstallMessage(protocol) {
228
- const spoken = protocol === void 0 ? "a protocol it does not name" : `protocol ${protocol}`;
229
- return `the root helper speaks ${spoken}, which this runner does not support, so microVM builds run without the jailer; run \`outerlayer runner install --service --replace\` to install the helper that goes with this release`;
230
- }
231
- async function probeHelper(socketPath, options = {}) {
232
- if (socketPath === void 0 || socketPath === "") return { ok: false, reason: "absent", detail: `${HELPER_ENV} is not set, so this runner was not started by a service that has a helper` };
233
- if (!(options.pinned ?? hostHelperPinned())) return { ok: false, reason: "unpinned", detail: "this release of the CLI pins no helper, so builds run without the jailer" };
234
- try {
235
- const reply = await ask(socketPath, { v: 1, op: "hello" }, options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS);
236
- const protocol = typeof reply.protocol === "number" ? reply.protocol : void 0;
237
- if (protocol === void 0 || !SUPPORTED_HELPER_PROTOCOLS.includes(protocol)) return { ok: false, reason: "unsupported", detail: reinstallMessage(protocol) };
238
- return { ok: true, client: helperClient(socketPath, { requestTimeoutMs: options.requestTimeoutMs }), firecracker: String(reply.firecracker ?? "") };
239
- } catch (err) {
240
- const message = err instanceof Error ? err.message : String(err);
241
- return { ok: false, reason: "absent", unreachable: true, detail: message.includes(socketPath) ? message : `the helper at ${socketPath} did not answer: ${message}` };
242
- }
243
- }
244
- function jailedAlive(unit, cgroupRoot = "/sys/fs/cgroup") {
245
- try {
246
- return /^populated 1$/m.test(readFileSync(join(cgroupRoot, "system.slice", `${unit}.service`, "cgroup.events"), "utf8"));
247
- } catch {
248
- return false;
249
- }
250
- }
251
- function realHelperPort(env = process.env) {
252
- return { probe: () => probeHelper(env[HELPER_ENV]), alive: (unit) => jailedAlive(unit) };
253
- }
254
- var STOP_GRACE_SECONDS = 15;
255
- function parseImageConfig(json) {
256
- const config = JSON.parse(json);
257
- const env = {};
258
- for (const entry of config.Env ?? []) {
259
- const eq = entry.indexOf("=");
260
- if (eq > 0) env[entry.slice(0, eq)] = entry.slice(eq + 1);
261
- }
262
- return { user: config.User ?? "", env };
206
+ return { user: config.User ?? "", env };
263
207
  }
264
208
  function createContainerHost(deps = {}) {
265
209
  const run = deps.run ?? spawnRun;
@@ -329,6 +273,8 @@ function createContainerHost(deps = {}) {
329
273
  }
330
274
  };
331
275
  }
276
+
277
+ // src/runner/microvm/disks.ts
332
278
  function converterScript(workDir = "/tmp/rootfs") {
333
279
  return [
334
280
  "set -eu",
@@ -490,6 +436,56 @@ async function createBuildDisk(input) {
490
436
  function deleteBuildDisk(path) {
491
437
  rmSync(path, { force: true });
492
438
  }
439
+ function dockerArch(nodeArch) {
440
+ if (nodeArch === "x64") return "amd64";
441
+ if (nodeArch === "arm64") return "arm64";
442
+ throw new AssetError("tool files", `no tool files are pinned for ${nodeArch}`);
443
+ }
444
+ function toolFilePins(arch) {
445
+ return [
446
+ ...VOICE_PACK_FILES.map((file) => ({ path: `${FETCHED_VOICE_DIR}/${file.name}`, url: file.url, sha256: file.sha256 })),
447
+ { path: FETCHED_FFMPEG, url: `https://github.com/BtbN/FFmpeg-Builds/releases/download/${FFMPEG_BUILD_TAG}/${FFMPEG_STATIC[arch].file}`, sha256: FFMPEG_STATIC[arch].sha256 },
448
+ { 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] },
449
+ { path: FETCHED_TINI, url: `https://github.com/krallin/tini/releases/download/v${TINI_VERSION}/tini-static-${arch}`, sha256: TINI_SHA256[arch] }
450
+ ];
451
+ }
452
+ function pinsDir(runnerDir2, pins) {
453
+ const key = createHash("sha256").update(pins.map((pin) => `${pin.path} ${pin.sha256}`).join("\n")).digest("hex").slice(0, 16);
454
+ const root = join(vmAssetsDir(runnerDir2), "tool-files");
455
+ return { root, dir: join(root, key) };
456
+ }
457
+ var inFlight = /* @__PURE__ */ new Map();
458
+ async function ensureToolFiles(options) {
459
+ const pins = options.pins ?? toolFilePins(options.arch ?? dockerArch(process.arch));
460
+ const { root, dir } = pinsDir(options.runnerDir, pins);
461
+ const running = inFlight.get(dir);
462
+ if (running !== void 0) return running;
463
+ const fetching = fetchAll(root, dir, pins, options.fetchImpl ?? fetch).finally(() => inFlight.delete(dir));
464
+ inFlight.set(dir, fetching);
465
+ return fetching;
466
+ }
467
+ async function fetchAll(root, dir, pins, fetchImpl) {
468
+ mkdirSync(dir, { recursive: true, mode: RUNNER_DIR_MODE });
469
+ for (const pin of pins) {
470
+ const path = join(dir, pin.path);
471
+ if (existsSync(path)) {
472
+ const actual = await sha256OfFile(path);
473
+ if (actual !== pin.sha256) {
474
+ 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`);
475
+ }
476
+ continue;
477
+ }
478
+ try {
479
+ await downloadPinned({ url: pin.url, dest: path, sha256: pin.sha256, name: pin.path, fetchImpl, stopped: "no image build started" });
480
+ } catch (err) {
481
+ throw new AssetError(pin.path, `the tool file ${pin.path} could not be had: ${err instanceof VoiceDownloadError ? err.message : String(err)}`);
482
+ }
483
+ }
484
+ for (const name of readdirSync(root)) {
485
+ if (join(root, name) !== dir) rmSync(join(root, name), { recursive: true, force: true });
486
+ }
487
+ return dir;
488
+ }
493
489
  var CONSOLE_CUT_NOTE = "[outerlayer runner: cut the console at 8 MiB; the microVM keeps running]\n";
494
490
  function pumpConsole(fd, streams) {
495
491
  const cap = 8 * 1024 ** 2;
@@ -631,63 +627,292 @@ function createFirecrackerHost(options = {}) {
631
627
  alive
632
628
  };
633
629
  }
634
- function unitNameFor(buildId) {
635
- return `outerlayer-build-${buildId}`;
636
- }
637
- function linkIn(buildDir2, name, source) {
638
- const target = join(buildDir2, name);
639
- rmSync(target, { force: true });
640
- try {
641
- linkSync(source, target);
642
- } catch (err) {
643
- if (err.code === "EXDEV") {
644
- throw new Error(`${source} and ${buildDir2} are on different file systems, so the build's files cannot be linked into its jail`);
645
- }
646
- throw err;
647
- }
648
- chmodSync(source, 292);
630
+ var BUILD_PREFIX = "build-";
631
+ var LEAF = "runner";
632
+ function serviceDirectory(selfCgroup, mount) {
633
+ const line = selfCgroup?.split("\n").find((l) => l.startsWith("0::"));
634
+ if (line === void 0) return void 0;
635
+ const path = line.slice(3).trim();
636
+ if (!path.endsWith(".service")) return void 0;
637
+ return join(mount, path);
649
638
  }
650
- async function startJailed(input) {
651
- linkIn(input.buildDir, "kernel", input.kernel);
652
- linkIn(input.buildDir, "rootfs.ext4", input.rootfs);
653
- for (const name of input.sockets) {
639
+ function prepareRunnerCgroup(setup) {
640
+ if (setup.invocationId === void 0 || setup.invocationId === "") return void 0;
641
+ const root = serviceDirectory(setup.selfCgroup, setup.mount);
642
+ if (root === void 0) return void 0;
643
+ const { fs } = setup;
644
+ if (!fs.exists(root) || !fs.writable(join(root, "cgroup.procs"))) return void 0;
645
+ const controllers = (fs.read(join(root, "cgroup.controllers")) ?? "").split(/\s+/);
646
+ if (!controllers.includes("memory")) return void 0;
647
+ for (const name of fs.list(root)) {
648
+ if (!name.startsWith(BUILD_PREFIX)) continue;
654
649
  try {
655
- chmodSync(join(input.buildDir, name), 438);
656
- } catch (err) {
657
- if (err.code !== "ENOENT") throw err;
650
+ fs.rmdir(join(root, name));
651
+ } catch {
658
652
  }
659
653
  }
660
- return input.client.start({ build: input.buildId, kind: input.kind, ...input.timeLimitMinutes === void 0 ? {} : { timeLimitMinutes: input.timeLimitMinutes } });
654
+ if (!fs.exists(join(root, LEAF))) fs.mkdir(join(root, LEAF));
655
+ fs.write(join(root, LEAF, "cgroup.procs"), String(setup.pid));
656
+ fs.write(join(root, "cgroup.subtree_control"), controllers.includes("io") ? "+memory +io" : "+memory");
657
+ return { root };
661
658
  }
662
- async function startJailedInto(state, input, save) {
663
- state.jailed = { unit: unitNameFor(state.buildId), chroot: "" };
664
- save();
659
+ var BUILD_RECLAIM_HEADROOM_BYTES = 256 * 1024 ** 2;
660
+ var BUILD_KILL_HEADROOM_BYTES = 1024 ** 3;
661
+ function createBuildCgroup(fs, root, buildId, buildMemoryBytes) {
662
+ const path = join(root, `${BUILD_PREFIX}${buildId}`);
663
+ fs.mkdir(path);
665
664
  try {
666
- const started = await startJailed({ ...input, buildId: state.buildId, buildDir: state.buildDir });
667
- state.jailed = { unit: started.unit, chroot: started.chroot };
668
- state.vsockPath = started.vsock;
665
+ fs.write(join(path, "memory.high"), String(buildMemoryBytes + BUILD_RECLAIM_HEADROOM_BYTES));
666
+ fs.write(join(path, "memory.max"), String(buildMemoryBytes + BUILD_KILL_HEADROOM_BYTES));
667
+ fs.write(join(path, "memory.swap.max"), "0");
669
668
  } catch (err) {
670
- if (err instanceof HelperRefusal) {
671
- delete state.jailed;
672
- save();
673
- }
669
+ removeBuildCgroup(fs, path);
674
670
  throw err;
675
671
  }
676
- save();
672
+ return path;
677
673
  }
678
- async function stopJailed(helper, state) {
679
- const probe = await helper.probe();
680
- 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}`);
674
+ function outOfMemoryReason(memory) {
675
+ return `the build ran out of memory: the kernel killed its microVM at the limit of runner.build.memory (${memory ?? "8g"}) plus 1 GiB for Firecracker and its disks' page cache. Raise runner.build.memory, or lower runner.concurrency`;
676
+ }
677
+ function oomKills(fs, path) {
678
+ const match = /^oom_kill (\d+)$/m.exec(fs.read(join(path, "memory.events")) ?? "");
679
+ return match === null ? 0 : Number(match[1]);
680
+ }
681
+ function removeBuildCgroup(fs, path) {
681
682
  try {
682
- await probe.client.stop(state.buildId);
683
+ fs.rmdir(path);
683
684
  } catch (err) {
684
- throw new Error(`the build's unit ${state.jailed?.unit ?? unitNameFor(state.buildId)} was not stopped: ${describeCallFailure(err)}`);
685
+ if (err.code !== "ENOENT") throw err;
685
686
  }
686
687
  }
687
- function makeBuildsDirPrivate(buildDir2) {
688
- const builds = dirname(buildDir2);
689
- const stat = statSync(builds);
690
- if (stat.uid === process.getuid?.() && (stat.mode & 18) !== 0) chmodSync(builds, 448);
688
+ function realCgroupFs() {
689
+ return {
690
+ exists: (path) => existsSync(path),
691
+ read: (path) => {
692
+ try {
693
+ return readFileSync(path, "utf8");
694
+ } catch {
695
+ return void 0;
696
+ }
697
+ },
698
+ write: (path, data) => writeFileSync(path, data),
699
+ mkdir: (path) => mkdirSync(path),
700
+ rmdir: (path) => rmdirSync(path),
701
+ list: (path) => readdirSync(path).filter((name) => statSync(join(path, name)).isDirectory()),
702
+ writable: (path) => {
703
+ try {
704
+ accessSync(path, constants.W_OK);
705
+ return true;
706
+ } catch {
707
+ return false;
708
+ }
709
+ }
710
+ };
711
+ }
712
+ var delegatedRoot;
713
+ function setUpRunnerCgroup(over = {}) {
714
+ let selfCgroup;
715
+ try {
716
+ selfCgroup = readFileSync("/proc/self/cgroup", "utf8");
717
+ } catch {
718
+ selfCgroup = void 0;
719
+ }
720
+ let refusal;
721
+ try {
722
+ const prepared = prepareRunnerCgroup({ fs: realCgroupFs(), selfCgroup, pid: process.pid, invocationId: process.env.INVOCATION_ID, mount: "/sys/fs/cgroup", ...over });
723
+ delegatedRoot = prepared?.root;
724
+ } catch (err) {
725
+ delegatedRoot = void 0;
726
+ refusal = err instanceof Error ? err.message : String(err);
727
+ }
728
+ return { delegated: delegatedRoot !== void 0, ...refusal === void 0 ? {} : { refusal } };
729
+ }
730
+ function realCgroupPort() {
731
+ const fs = realCgroupFs();
732
+ return {
733
+ create: (buildId, bytes) => {
734
+ if (delegatedRoot === void 0) return void 0;
735
+ return createBuildCgroup(fs, delegatedRoot, buildId, bytes);
736
+ },
737
+ oomKills: (path) => oomKills(fs, path),
738
+ remove: (path) => removeBuildCgroup(fs, path)
739
+ };
740
+ }
741
+ var SUPPORTED_HELPER_PROTOCOLS = [1];
742
+ var HELPER_ENV = "OUTERLAYER_HELPER_SOCKET";
743
+ var HelperRefusal = class extends Error {
744
+ constructor(code, reason) {
745
+ super(reason);
746
+ this.code = code;
747
+ }
748
+ };
749
+ function helperSocketPath(unitName) {
750
+ return `/run/${unitName}-helper/helper.sock`;
751
+ }
752
+ var START_TIMEOUT_MS = 12e4;
753
+ var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
754
+ var MAX_REPLY_BYTES = 1024 * 1024;
755
+ function ask(socketPath, request, timeoutMs) {
756
+ return new Promise((resolve2, reject) => {
757
+ const conn = createConnection(socketPath);
758
+ let text = "";
759
+ let settled = false;
760
+ const settle = (finish) => {
761
+ if (settled) return;
762
+ settled = true;
763
+ conn.destroy();
764
+ finish();
765
+ };
766
+ conn.setTimeout(timeoutMs, () => settle(() => reject(new Error(`the helper at ${socketPath} did not answer within ${Math.round(timeoutMs / 1e3)} seconds`))));
767
+ conn.on("connect", () => conn.write(`${JSON.stringify(request)}
768
+ `));
769
+ conn.on("data", (chunk) => {
770
+ text += chunk.toString("utf8");
771
+ if (text.length > MAX_REPLY_BYTES) return settle(() => reject(new Error(`the helper at ${socketPath} sent a reply that is too long`)));
772
+ const end = text.indexOf("\n");
773
+ if (end === -1) return;
774
+ settle(() => {
775
+ let reply;
776
+ try {
777
+ reply = JSON.parse(text.slice(0, end));
778
+ } catch {
779
+ return reject(new Error(`the helper at ${socketPath} sent a reply that is not JSON`));
780
+ }
781
+ if (reply.status === "refused") return reject(new HelperRefusal(String(reply.code ?? "refused"), String(reply.reason ?? "the helper refused the request")));
782
+ resolve2(reply);
783
+ });
784
+ });
785
+ conn.on("error", (err) => settle(() => reject(new Error(`the helper at ${socketPath} did not answer: ${err.message}`))));
786
+ conn.on("close", () => settle(() => reject(new Error(`the helper at ${socketPath} closed the connection without a reply`))));
787
+ });
788
+ }
789
+ function helperClient(socketPath, options = {}) {
790
+ const REQUEST_TIMEOUT_MS = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
791
+ return {
792
+ async start({ build, kind, timeLimitMinutes }) {
793
+ const reply = await ask(socketPath, { v: 1, op: "start", build, kind, timeLimitMinutes }, START_TIMEOUT_MS);
794
+ return { unit: String(reply.unit), chroot: String(reply.chroot), vsock: String(reply.vsock) };
795
+ },
796
+ async stop(build) {
797
+ await ask(socketPath, { v: 1, op: "stop", build }, REQUEST_TIMEOUT_MS);
798
+ },
799
+ async state(build) {
800
+ const reply = await ask(socketPath, { v: 1, op: "state", build }, REQUEST_TIMEOUT_MS);
801
+ return { active: reply.active === true, oomKills: typeof reply.oomKills === "number" ? reply.oomKills : 0 };
802
+ },
803
+ async list() {
804
+ const reply = await ask(socketPath, { v: 1, op: "list" }, REQUEST_TIMEOUT_MS);
805
+ return Array.isArray(reply.builds) ? reply.builds.map(String) : [];
806
+ },
807
+ async relink(build) {
808
+ await ask(socketPath, { v: 1, op: "relink", build }, REQUEST_TIMEOUT_MS);
809
+ }
810
+ };
811
+ }
812
+ function helperFirecrackerNote(helperFirecracker) {
813
+ return `the root helper runs Firecracker ${helperFirecracker}, not the ${FIRECRACKER_VERSION} this release pins, so microVM builds run under the jailer with Firecracker ${helperFirecracker}`;
814
+ }
815
+ function reinstallMessage(protocol) {
816
+ const spoken = protocol === void 0 ? "a protocol it does not name" : `protocol ${protocol}`;
817
+ return `the root helper speaks ${spoken}, which this runner does not support, so microVM builds run without the jailer; run \`outerlayer runner install --service --replace\` to install the helper that goes with this release`;
818
+ }
819
+ async function probeHelper(socketPath, options = {}) {
820
+ if (socketPath === void 0 || socketPath === "") return { ok: false, reason: "absent", detail: `${HELPER_ENV} is not set, so this runner was not started by a service that has a helper` };
821
+ if (!(options.pinned ?? hostHelperPinned())) return { ok: false, reason: "unpinned", detail: "this release of the CLI pins no helper, so builds run without the jailer" };
822
+ try {
823
+ const reply = await ask(socketPath, { v: 1, op: "hello" }, options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS);
824
+ const protocol = typeof reply.protocol === "number" ? reply.protocol : void 0;
825
+ if (protocol === void 0 || !SUPPORTED_HELPER_PROTOCOLS.includes(protocol)) return { ok: false, reason: "unsupported", detail: reinstallMessage(protocol) };
826
+ return { ok: true, client: helperClient(socketPath, { requestTimeoutMs: options.requestTimeoutMs }), firecracker: String(reply.firecracker ?? "") };
827
+ } catch (err) {
828
+ const message = err instanceof Error ? err.message : String(err);
829
+ return { ok: false, reason: "absent", unreachable: true, detail: message.includes(socketPath) ? message : `the helper at ${socketPath} did not answer: ${message}` };
830
+ }
831
+ }
832
+ function jailedAlive(unit, cgroupRoot = "/sys/fs/cgroup") {
833
+ try {
834
+ return /^populated 1$/m.test(readFileSync(join(cgroupRoot, "system.slice", `${unit}.service`, "cgroup.events"), "utf8"));
835
+ } catch {
836
+ return false;
837
+ }
838
+ }
839
+ function realHelperPort(env = process.env) {
840
+ return { probe: () => probeHelper(env[HELPER_ENV]), alive: (unit) => jailedAlive(unit) };
841
+ }
842
+
843
+ // src/runner/microvm/host.ts
844
+ function createMicroVmPorts(over = {}) {
845
+ return {
846
+ assets: over.assets ?? ((runnerDir2) => ensureAssets({ runnerDir: runnerDir2 })),
847
+ rootfs: over.rootfs ?? createRootfsHost(),
848
+ firecracker: over.firecracker ?? createFirecrackerHost(),
849
+ cgroup: over.cgroup ?? realCgroupPort(),
850
+ helper: over.helper ?? realHelperPort(),
851
+ fdStore: over.fdStore ?? realFdStorePort(),
852
+ exec: over.exec ?? execCapture,
853
+ execStream: over.execStream ?? runExec,
854
+ builderDisk: over.builderDisk ?? ((runnerDir2) => ensureBuilderDisk({ runnerDir: runnerDir2 })),
855
+ builderDiskPinned: over.builderDiskPinned ?? (() => builderDiskPinned()),
856
+ toolFiles: over.toolFiles ?? ((runnerDir2) => ensureToolFiles({ runnerDir: runnerDir2 }))
857
+ };
858
+ }
859
+ function unitNameFor(buildId) {
860
+ return `outerlayer-build-${buildId}`;
861
+ }
862
+ function linkIn(buildDir2, name, source) {
863
+ const target = join(buildDir2, name);
864
+ rmSync(target, { force: true });
865
+ try {
866
+ linkSync(source, target);
867
+ } catch (err) {
868
+ if (err.code === "EXDEV") {
869
+ throw new Error(`${source} and ${buildDir2} are on different file systems, so the build's files cannot be linked into its jail`);
870
+ }
871
+ throw err;
872
+ }
873
+ chmodSync(source, 292);
874
+ }
875
+ async function startJailed(input) {
876
+ linkIn(input.buildDir, "kernel", input.kernel);
877
+ linkIn(input.buildDir, "rootfs.ext4", input.rootfs);
878
+ for (const name of input.sockets) {
879
+ try {
880
+ chmodSync(join(input.buildDir, name), 438);
881
+ } catch (err) {
882
+ if (err.code !== "ENOENT") throw err;
883
+ }
884
+ }
885
+ return input.client.start({ build: input.buildId, kind: input.kind, ...input.timeLimitMinutes === void 0 ? {} : { timeLimitMinutes: input.timeLimitMinutes } });
886
+ }
887
+ async function startJailedInto(state, input, save) {
888
+ state.jailed = { unit: unitNameFor(state.buildId), chroot: "" };
889
+ save();
890
+ try {
891
+ const started = await startJailed({ ...input, buildId: state.buildId, buildDir: state.buildDir });
892
+ state.jailed = { unit: started.unit, chroot: started.chroot };
893
+ state.vsockPath = started.vsock;
894
+ } catch (err) {
895
+ if (err instanceof HelperRefusal) {
896
+ delete state.jailed;
897
+ save();
898
+ }
899
+ throw err;
900
+ }
901
+ save();
902
+ }
903
+ async function stopJailed(helper, state) {
904
+ const probe = await helper.probe();
905
+ 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}`);
906
+ try {
907
+ await probe.client.stop(state.buildId);
908
+ } catch (err) {
909
+ throw new Error(`the build's unit ${state.jailed?.unit ?? unitNameFor(state.buildId)} was not stopped: ${describeCallFailure(err)}`);
910
+ }
911
+ }
912
+ function makeBuildsDirPrivate(buildDir2) {
913
+ const builds = dirname(buildDir2);
914
+ const stat = statSync(builds);
915
+ if (stat.uid === process.getuid?.() && (stat.mode & 18) !== 0) chmodSync(builds, 448);
691
916
  }
692
917
  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`";
693
918
 
@@ -1224,428 +1449,374 @@ async function sweepImageBuilders(configPath, ports, onError) {
1224
1449
  }
1225
1450
  return swept;
1226
1451
  }
1227
- var BranchPrepareError = class extends Error {
1452
+ var RunnerInitError = class extends Error {
1228
1453
  };
1229
- var OWN_PREFIX = "outerlayer/";
1230
- var GIT_TIMEOUT_MS = 12e4;
1231
- var REPO_BINDING_GIT_VARS = new Set(
1232
- `GIT_ALTERNATE_OBJECT_DIRECTORIES GIT_OBJECT_DIRECTORY GIT_DIR GIT_WORK_TREE GIT_IMPLICIT_WORK_TREE
1233
- GIT_GRAFT_FILE GIT_INDEX_FILE GIT_NO_REPLACE_OBJECTS GIT_REPLACE_REF_BASE GIT_PREFIX
1234
- GIT_INTERNAL_SUPER_PREFIX GIT_SHALLOW_FILE GIT_COMMON_DIR GIT_NAMESPACE`.split(/\s+/)
1235
- );
1236
- function localGit(cwd, args) {
1237
- const env = {};
1238
- for (const [key, value] of Object.entries(process.env)) {
1239
- if (!REPO_BINDING_GIT_VARS.has(key)) env[key] = value;
1454
+ var UNMARKED_PLACEHOLDER_COMMAND = "echo 'edit runner.commands in this config file: it must start your agent on item $OUTERLAYER_WORK' >&2; exit 1";
1455
+ var PLACEHOLDER_MARKER = "outerlayer-placeholder";
1456
+ var PLACEHOLDER_COMMAND = `${UNMARKED_PLACEHOLDER_COMMAND} # ${PLACEHOLDER_MARKER}`;
1457
+ function isolationNote(machine) {
1458
+ const has = describeMachineDocker(machine);
1459
+ const reach = reachDockerStep(machine);
1460
+ const needs = reach !== void 0 ? `Isolated builds need Docker Engine ${MIN_ENGINE_MAJOR} or later: ${reach}, then set both hooks to builtin:container.` : machine.platform === "windows" ? `Isolated builds need Docker Engine ${MIN_ENGINE_MAJOR} or later inside WSL2: install it there, run the runner from a WSL2 shell, and set both hooks to builtin:container.` : `Isolated builds need Docker Engine ${MIN_ENGINE_MAJOR} or later on Linux or WSL2. Once it is installed, set both hooks to builtin:container.`;
1461
+ return `This machine has ${has}, so builds run as processes under this user (isolation: shared-user), through the tunnel. ${needs}`;
1462
+ }
1463
+ function processRefusal(machine) {
1464
+ const reach = reachDockerStep(machine);
1465
+ const needs = machine.platform === "darwin" ? "Isolated builds on a Mac run in the runner image on Docker Desktop (https://docs.outerlayer.ai/run-the-runner-in-a-container) or in a Linux VM: run this again with --vm." : reach !== void 0 ? `Isolated builds need Docker Engine ${MIN_ENGINE_MAJOR} or later: ${reach}, then run this again.` : machine.platform === "windows" ? `Isolated builds need Docker Engine ${MIN_ENGINE_MAJOR} or later inside WSL2: install it there, then run this again from a WSL2 shell.` : `Isolated builds need Docker Engine ${MIN_ENGINE_MAJOR} or later on Linux or WSL2: install it, then run this again.`;
1466
+ return `This machine has ${describeMachineDocker(machine)}, so it cannot isolate a build. ${needs} To run builds as processes under this user instead, where a build can read this user's files and the runner's Claude credential, run this again with --allow-process-builds.`;
1467
+ }
1468
+ function isPlaceholderCommand(command) {
1469
+ return command === UNMARKED_PLACEHOLDER_COMMAND || command.includes(PLACEHOLDER_MARKER);
1470
+ }
1471
+ function readConfigObject(configPath) {
1472
+ if (!existsSync(configPath)) {
1473
+ throw new RunnerInitError(
1474
+ `${configPath}: no config file. Run \`outerlayer login\` first, then this command.`
1475
+ );
1240
1476
  }
1241
- env.GIT_TERMINAL_PROMPT = "0";
1242
- const result = spawnSync("git", args, { cwd, env, encoding: "utf8", timeout: GIT_TIMEOUT_MS });
1243
- if (result.error !== void 0) {
1244
- return Promise.resolve({ status: null, stdout: "", stderr: result.error.message });
1477
+ try {
1478
+ return JSON.parse(readFileSync(configPath, "utf8"));
1479
+ } catch (err) {
1480
+ throw new RunnerInitError(`${configPath}: not valid JSON (${err instanceof Error ? err.message : String(err)})`);
1245
1481
  }
1246
- return Promise.resolve({ status: result.status, stdout: result.stdout.trim(), stderr: result.stderr.trim() });
1247
1482
  }
1248
- function firstLine2(stderr) {
1249
- return (stderr.split("\n")[0] ?? "").replace(/\/\/[^/@\s]+@/g, "//");
1483
+ function handedCredential(value) {
1484
+ if (value === void 0) return void 0;
1485
+ if (typeof value !== "string") throw new RunnerInitError("the config on stdin carries a claudeCredential that is not text.");
1486
+ return credentialFromStdin(value);
1250
1487
  }
1251
- async function mustGit(git2, cwd, step, args) {
1252
- const result = await git2(cwd, args);
1253
- if (result.status !== 0) {
1254
- const detail = firstLine2(result.stderr);
1255
- throw new BranchPrepareError(`${step} failed${detail === "" ? "" : `: ${detail}`}`);
1256
- }
1257
- return result.stdout;
1488
+ function handedAgent(value) {
1489
+ if (value === void 0) return void 0;
1490
+ if (!isAgentName(value)) throw new RunnerInitError("the config on stdin names an agent runner init does not know.");
1491
+ return value;
1258
1492
  }
1259
- async function remoteDefaultBranch(git2, workdir) {
1260
- const out = await mustGit(git2, workdir, "reading the remote's default branch", ["ls-remote", "--symref", "origin", "HEAD"]);
1261
- const match = /^ref: refs\/heads\/(\S+)\s+HEAD$/m.exec(out);
1262
- if (match === null) {
1263
- throw new BranchPrepareError("reading the remote's default branch failed: the remote names none");
1493
+ function parseStdinConfig(configPath, text) {
1494
+ if (existsSync(configPath)) {
1495
+ throw new RunnerInitError(
1496
+ `${configPath}: a config file is already here, so the runner key in it is not replaced. Remove it, or delete the VM and start again.`
1497
+ );
1264
1498
  }
1265
- return match[1];
1499
+ let config;
1500
+ try {
1501
+ config = JSON.parse(text);
1502
+ } catch (err) {
1503
+ throw new RunnerInitError(`the config on stdin is not valid JSON (${err instanceof Error ? err.message : String(err)})`);
1504
+ }
1505
+ const problem = runnerCredentialProblem(config);
1506
+ if (problem !== void 0) throw new RunnerInitError(`the config on stdin: ${problem.field} ${problem.reason}.`);
1507
+ const carried = config.runner;
1508
+ const credential = handedCredential(config.claudeCredential);
1509
+ const agent = handedAgent(config.agent);
1510
+ delete config.runner;
1511
+ delete config.claudeCredential;
1512
+ delete config.agent;
1513
+ return { config, carried, credential, agent };
1514
+ }
1515
+ function runRunnerInit(configPath, opts = {}) {
1516
+ return writeRunnerInit(prepareRunnerInit(configPath, opts));
1517
+ }
1518
+ function prepareRunnerInit(configPath, opts = {}) {
1519
+ let config;
1520
+ let carried;
1521
+ let credential = opts.credential;
1522
+ let agent = opts.agent;
1523
+ let needsKey = false;
1524
+ if (opts.configFromStdin === void 0) {
1525
+ config = opts.keyFromLogin === true && !existsSync(configPath) ? {} : readConfigObject(configPath);
1526
+ if (config.runner !== void 0) {
1527
+ throw new RunnerInitError(`${configPath}: a runner block is already here. Edit it, or remove it and run this again.`);
1528
+ }
1529
+ needsKey = opts.keyFromLogin === true && config.apiKey === void 0;
1530
+ const problem = needsKey ? void 0 : runnerCredentialProblem(config);
1531
+ if (problem !== void 0) throw new RunnerInitError(`${configPath}: ${problem.field} ${problem.reason}.`);
1532
+ } else {
1533
+ const handed = parseStdinConfig(configPath, opts.configFromStdin);
1534
+ ({ config, carried } = handed);
1535
+ credential ??= handed.credential;
1536
+ agent ??= handed.agent;
1537
+ }
1538
+ const machine = opts.machine ?? detectMachine();
1539
+ if (opts.requireContainer === true && !supportsContainerBuilds(machine)) {
1540
+ const fix = reachDockerStep(machine) ?? `install Docker Engine ${MIN_ENGINE_MAJOR} or later in the VM`;
1541
+ throw new RunnerInitError(`the VM has ${describeMachineDocker(machine)}, but the runner in it needs container builds: ${fix}, then run this again.`);
1542
+ }
1543
+ const hooks = opts.requireContainer !== true && supportsMicroVmBuilds(machine) ? "builtin:vm" : supportsContainerBuilds(machine) ? "builtin:container" : "builtin:process";
1544
+ if (hooks === "builtin:process" && opts.allowProcess !== true) throw new RunnerInitError(processRefusal(machine));
1545
+ if (agent === "claude" && credential === void 0 && opts.credentialInEnvironment !== true && !hasStoredCredential(configPath)) {
1546
+ throw new RunnerInitError(
1547
+ "--agent claude needs Claude's credential, and none was piped on stdin or is in the claude.env beside the config. Pipe a token from `claude setup-token`, or an Anthropic API key: printf '%s' \"$CLAUDE_CODE_OAUTH_TOKEN\" | outerlayer runner init --agent claude"
1548
+ );
1549
+ }
1550
+ return { configPath, config, carried, credential, agent, machine, hooks, needsKey, host: opts.host };
1266
1551
  }
1267
- async function remoteHasBranch(git2, workdir, branch) {
1268
- return await mustGit(git2, workdir, `looking for ${branch} on the remote`, ["ls-remote", "--heads", "origin", `refs/heads/${branch}`]) !== "";
1269
- }
1270
- async function fetchBranch(git2, workdir, branch) {
1271
- await mustGit(git2, workdir, `fetching ${branch}`, ["fetch", "--quiet", "origin", `+refs/heads/${branch}:refs/remotes/origin/${branch}`]);
1272
- }
1273
- async function prepareBranch(opts) {
1274
- const { workdir, branch } = opts;
1275
- const git2 = opts.git ?? localGit;
1276
- const defaultBranch = await remoteDefaultBranch(git2, workdir);
1277
- const exists = await remoteHasBranch(git2, workdir, branch);
1278
- await fetchBranch(git2, workdir, defaultBranch);
1279
- if (exists) await fetchBranch(git2, workdir, branch);
1280
- let start;
1281
- if (exists && !(branch.startsWith(OWN_PREFIX) && await isMerged(git2, workdir, branch, defaultBranch))) {
1282
- start = `origin/${branch}`;
1283
- } else if (branch.startsWith(OWN_PREFIX)) {
1284
- start = `origin/${defaultBranch}`;
1285
- } else {
1286
- throw new BranchPrepareError(`the pull request's branch ${branch} is not on the remote`);
1287
- }
1288
- await mustGit(git2, workdir, `checking out ${branch}`, ["checkout", "--quiet", "-B", branch, start]);
1289
- return {
1290
- branch,
1291
- head: await mustGit(git2, workdir, "reading the checked-out commit", ["rev-parse", "HEAD"]),
1292
- startCommit: await mustGit(git2, workdir, `reading the commit of ${defaultBranch}`, ["rev-parse", `origin/${defaultBranch}`])
1293
- };
1294
- }
1295
- async function isMerged(git2, workdir, branch, defaultBranch) {
1296
- const result = await git2(workdir, ["merge-base", "--is-ancestor", `origin/${branch}`, `origin/${defaultBranch}`]);
1297
- if (result.status === 0) return true;
1298
- if (result.status === 1) return false;
1299
- const detail = firstLine2(result.stderr);
1300
- throw new BranchPrepareError(`comparing ${branch} with ${defaultBranch} failed${detail === "" ? "" : `: ${detail}`}`);
1552
+ function writeRunnerInit(prepared, runnerKey) {
1553
+ const { configPath, config, carried, credential, agent, machine, hooks, host } = prepared;
1554
+ if (prepared.needsKey && runnerKey === void 0) {
1555
+ throw new RunnerInitError(`${configPath}: apiKey is missing, and no runner key was pasted.`);
1556
+ }
1557
+ if (runnerKey !== void 0) Object.assign(config, { url: runnerKey.url, apiKey: runnerKey.apiKey, appId: runnerKey.appId });
1558
+ mkdirSync(runnerDir(configPath), { recursive: true, mode: RUNNER_DIR_MODE });
1559
+ config.runner = {
1560
+ queues: ["implement", "amend"],
1561
+ concurrency: 1,
1562
+ pollSeconds: 60,
1563
+ timeLimitMinutes: 480,
1564
+ idleLimitMinutes: 30,
1565
+ // A placeholder that fails loudly. A command line that quietly did
1566
+ // nothing would claim an item, run, exit zero, and record `ok` —
1567
+ // every attempt reading as a success that produced nothing.
1568
+ commands: {
1569
+ implement: PLACEHOLDER_COMMAND,
1570
+ amend: PLACEHOLDER_COMMAND
1571
+ },
1572
+ ...carried,
1573
+ ...host === void 0 ? {} : { name: host },
1574
+ // The agent was named on this command line, so its commands win over any the hand-off carried.
1575
+ ...agent === void 0 ? {} : { commands: { ...AGENT_COMMANDS[agent] } },
1576
+ hooks: { provision: hooks, cleanup: hooks }
1577
+ };
1578
+ if (host !== void 0) delete config.runner.host;
1579
+ mkdirSync(dirname(configPath), { recursive: true });
1580
+ const stored = credential === void 0 ? void 0 : { variable: credential.variable, path: storeClaudeCredential(configPath, credential) };
1581
+ writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
1582
+ `, { mode: RUNNER_FILE_MODE });
1583
+ return { configPath, hooks, ...hooks === "builtin:process" ? { note: isolationNote(machine) } : {}, credential: stored };
1301
1584
  }
1302
1585
 
1303
- // src/runner/container/lifecycle.ts
1304
- var ORDER = ["onCreateCommand", "updateContentCommand", "postCreateCommand", "postStartCommand"];
1305
- function commandArgv(command) {
1306
- if (typeof command === "string") return command === "" ? void 0 : ["/bin/sh", "-c", command];
1307
- if (Array.isArray(command) && command.length > 0 && command.every((part) => typeof part === "string")) return command;
1308
- return void 0;
1586
+ // src/runner/host-setup.ts
1587
+ function placeholderQueues(config) {
1588
+ return config.queues.filter((queue) => {
1589
+ const command = config.commands[queue];
1590
+ return command !== void 0 && isPlaceholderCommand(command);
1591
+ });
1309
1592
  }
1310
- function lifecycleSteps(lifecycle) {
1311
- const steps = [];
1312
- for (const name of ORDER) {
1313
- const entries = lifecycle[name];
1314
- if (entries === void 0) continue;
1315
- for (const { source, command } of entries) {
1316
- if (typeof command === "object" && command !== null && !Array.isArray(command)) {
1317
- for (const [key, value] of Object.entries(command)) {
1318
- const argv2 = commandArgv(value);
1319
- if (argv2 !== void 0) steps.push({ name: `${name}[${key}]`, source, argv: argv2 });
1320
- }
1321
- continue;
1322
- }
1323
- const argv = commandArgv(command);
1324
- if (argv !== void 0) steps.push({ name, source, argv });
1593
+ function placeholderProblem(queues) {
1594
+ const keys = queues.map((queue) => `runner.commands.${queue}`).join(", ");
1595
+ return `${keys} ${queues.length === 1 ? "is" : "are"} still the placeholder \`runner init\` wrote, which fails every item. Edit ${queues.length === 1 ? "it" : "them"} to start your agent on item $OUTERLAYER_WORK`;
1596
+ }
1597
+ var CLAUDE_PERMISSION_MODES = ["acceptEdits", "auto", "bypassPermissions", "default", "dontAsk", "manual", "plan"];
1598
+ var CLAUDE_COMMAND = /(?:^|[;&|(])\s*(?:(?:exec|env|nohup|time)\s+)*(?:[A-Za-z_][A-Za-z0-9_]*=\S*\s+)*(?:\S*\/)?claude\s/;
1599
+ var PERMISSION_MODE_FLAG = /--permission-mode(?:=|\s+)(?:"([^"]*)"|'([^']*)'|([^\s;&|()]+))/g;
1600
+ function permissionModeProblems(config) {
1601
+ const problems = [];
1602
+ for (const queue of config.queues) {
1603
+ const command = config.commands[queue];
1604
+ if (command === void 0 || !CLAUDE_COMMAND.test(command)) continue;
1605
+ for (const match of command.matchAll(PERMISSION_MODE_FLAG)) {
1606
+ const value = match[1] ?? match[2] ?? match[3] ?? "";
1607
+ if (value.includes("$") || value.includes("`") || CLAUDE_PERMISSION_MODES.includes(value)) continue;
1608
+ problems.push({ queue, command, value });
1325
1609
  }
1326
1610
  }
1327
- return steps;
1611
+ return problems;
1328
1612
  }
1329
- function lifecycleFailureReason(step, exitCode) {
1330
- return `lifecycle command ${step.name} (${step.source}) exited ${exitCode}`;
1613
+ function permissionModeProblem(problems) {
1614
+ return problems.map(({ queue, command, value }) => `runner.commands.${queue} is \`${command}\`, and \`${value}\` is not a permission mode Claude Code accepts (${CLAUDE_PERMISSION_MODES.join(", ")}). The command would fail only when a build ran it`).join(". ");
1331
1615
  }
1332
- function parseCpuList(text) {
1333
- const cores = /* @__PURE__ */ new Set();
1334
- for (const part of text.trim().split(",")) {
1335
- const match = /^(\d+)(?:-(\d+))?$/.exec(part.trim());
1336
- if (match === null) return void 0;
1337
- const low = Number(match[1]);
1338
- const high = match[2] === void 0 ? low : Number(match[2]);
1339
- if (high < low) return void 0;
1340
- for (let core = low; core <= high; core++) cores.add(core);
1341
- }
1342
- return [...cores].sort((a, b) => a - b);
1616
+ var FORMER_AMEND_COMMAND = /(^|["'\s])\/amend(?=["'\s]|$)/g;
1617
+ function currentSkillCommand(command) {
1618
+ return command.replace(FORMER_AMEND_COMMAND, "$1/outerlayer-amend");
1343
1619
  }
1344
- function formatCpuList(cores) {
1345
- const sorted = [...new Set(cores)].sort((a, b) => a - b);
1346
- const parts = [];
1347
- for (let i = 0; i < sorted.length; i++) {
1348
- const start = sorted[i];
1349
- while (sorted[i + 1] === sorted[i] + 1) i++;
1350
- parts.push(start === sorted[i] ? String(start) : `${start}-${sorted[i]}`);
1351
- }
1352
- return parts.join(",");
1353
- }
1354
- function capCpus(wanted, cpuset) {
1355
- const cores = cpuset === void 0 ? void 0 : parseCpuList(cpuset);
1356
- return cores === void 0 ? wanted : Math.min(wanted, cores.length);
1357
- }
1358
- function usableCpus(deps = {}) {
1359
- try {
1360
- const status = (deps.readStatus ?? (() => readFileSync("/proc/self/status", "utf8")))();
1361
- const cores = parseCpuList(/^Cpus_allowed_list:\s*(.+)$/m.exec(status)?.[1] ?? "");
1362
- if (cores !== void 0) return cores;
1363
- } catch {
1364
- }
1365
- return Array.from({ length: Math.max(1, (deps.parallelism ?? availableParallelism)()) }, (_unused, core) => core);
1366
- }
1367
- function createCpuAllocator(cores) {
1368
- const usable = [...new Set(cores)].sort((a, b) => a - b);
1369
- const held = /* @__PURE__ */ new Map();
1370
- const holders = (core) => {
1371
- let count = 0;
1372
- for (const set of held.values()) if (set.includes(core)) count++;
1373
- return count;
1374
- };
1375
- return {
1376
- acquire(buildId, cpus) {
1377
- const existing = held.get(buildId);
1378
- if (existing !== void 0) return existing;
1379
- const wanted = Math.min(Math.ceil(cpus), usable.length);
1380
- const ranked = usable.map((core) => ({ core, count: holders(core) })).sort((a, b) => a.count - b.count);
1381
- const chosen = ranked.slice(0, wanted).map(({ core }) => core).sort((a, b) => a - b);
1382
- held.set(buildId, chosen);
1383
- return chosen;
1384
- },
1385
- hold(buildId, set) {
1386
- held.set(buildId, [...set]);
1387
- },
1388
- release(buildId) {
1389
- held.delete(buildId);
1390
- }
1391
- };
1620
+ function formerSkillQueues(config) {
1621
+ return config.queues.filter((queue) => {
1622
+ const command = config.commands[queue];
1623
+ return command !== void 0 && currentSkillCommand(command) !== command;
1624
+ });
1392
1625
  }
1393
-
1394
- // src/runner/container/lease-channel.ts
1395
- var LEASE_SECONDS = 900;
1396
- function leaseExpiresAtMs(job) {
1397
- const base = Date.parse(job.lastRenewAt ?? job.startedAt);
1398
- return (Number.isFinite(base) ? base : Date.now()) + LEASE_SECONDS * 1e3;
1626
+ function formerSkillProblem(queues) {
1627
+ const keys = queues.map((queue) => `runner.commands.${queue}`).join(", ");
1628
+ return `${keys} ${queues.length === 1 ? "starts" : "start"} /amend, which is now /outerlayer-amend. The runner runs /outerlayer-amend in its place; edit the config to say so`;
1399
1629
  }
1400
- function leaseTimeLeftMs(job, nowMs) {
1401
- return Math.max(0, leaseExpiresAtMs(job) - nowMs);
1630
+ function needsClaudeCredential(hooks) {
1631
+ return builtinKind(hooks.provision) !== "own";
1402
1632
  }
1403
- function createLeaseChannel(valueMs) {
1404
- const open = /* @__PURE__ */ new Set();
1405
- let stopped = false;
1406
- const write = (stream) => {
1407
- if (!stopped && !stream.destroyed && stream.writable) stream.write(`${Math.floor(valueMs())}
1408
- `);
1409
- };
1410
- return {
1411
- accept(stream) {
1412
- stream.on("error", () => void 0);
1413
- open.add(stream);
1414
- stream.once("close", () => open.delete(stream));
1415
- queueMicrotask(() => write(stream));
1416
- },
1417
- publish() {
1418
- for (const stream of open) write(stream);
1419
- },
1420
- stop() {
1421
- stopped = true;
1422
- },
1423
- close() {
1424
- for (const stream of [...open]) stream.destroy();
1425
- open.clear();
1633
+ var CREDENTIAL_FIX = "Run `claude setup-token`, then pipe the token it prints (or an Anthropic API key) to `outerlayer runner init --agent claude`, which stores it in the claude.env beside the config. Or start the runner with CLAUDE_CODE_OAUTH_TOKEN (or ANTHROPIC_API_KEY) set";
1634
+ function claudeCredentialCheck(env, options = {}) {
1635
+ const name = "Claude credential";
1636
+ if (options.service !== void 0) {
1637
+ const { unit, variables } = options.service;
1638
+ if (variables.length === 0) {
1639
+ return {
1640
+ name,
1641
+ status: "fail",
1642
+ detail: `the service ${unit} has no Claude credential: neither ${CLAUDE_CREDENTIAL_VARIABLES.slice(0, 2).join(" nor ")} is set in its claude.env or runner.env, so every build fails at provision as agent_credential_missing`,
1643
+ fix: `${CREDENTIAL_FIX}, then restart the service: sudo systemctl restart ${unit}.`
1644
+ };
1426
1645
  }
1427
- };
1428
- }
1429
- var AssetError = class extends Error {
1430
- constructor(file, message) {
1431
- super(message);
1432
- this.file = file;
1646
+ return { name, status: "pass", detail: `${variables[0]} is set in the claude.env or runner.env of the service ${unit}` };
1433
1647
  }
1434
- };
1435
- var FIRECRACKER_RELEASES = "https://github.com/firecracker-microvm/firecracker/releases/download";
1436
- var DOWNLOAD_TIMEOUT_MS = 10 * 6e4;
1437
- function pinnedAssets(arch) {
1438
- const version = FIRECRACKER_VERSION;
1439
- return {
1440
- firecracker: {
1441
- url: `${FIRECRACKER_RELEASES}/v${version}/firecracker-v${version}-${arch}.tgz`,
1442
- sha256: FIRECRACKER_SHA256[arch],
1443
- binarySha256: FIRECRACKER_BINARY_SHA256[arch],
1444
- jailerSha256: JAILER_BINARY_SHA256[arch]
1445
- },
1446
- kernel: { url: `${GUEST_ASSETS_BASE_URL}/vmlinux-${arch}`, sha256: GUEST_KERNEL_SHA256[arch] },
1447
- helper: { url: `${GUEST_ASSETS_BASE_URL}/outerlayer-guest-${arch}`, sha256: GUEST_HELPER_SHA256[arch] }
1448
- };
1648
+ const held = claudeCredential(env, { ignoreHeld: options.ignoreHeld === true });
1649
+ if (held === void 0) {
1650
+ return {
1651
+ name,
1652
+ status: "fail",
1653
+ detail: `neither ${CLAUDE_CREDENTIAL_VARIABLES.slice(0, 2).join(" nor ")} is set, so every build fails at provision as agent_credential_missing`,
1654
+ fix: `${CREDENTIAL_FIX}.`
1655
+ };
1656
+ }
1657
+ const sameVariables = options.shell === true ? "; the runner must be started with the same variable" : "";
1658
+ return { name, status: "pass", detail: `${held.variable} is set${sameVariables}` };
1449
1659
  }
1450
- function builderDiskPin(arch) {
1451
- return { url: `${BUILDER_DISK_BASE_URL}/builder-rootfs-${arch}.ext4`, sha256: BUILDER_DISK_SHA256[arch] };
1660
+ function missingPermissionOf(err) {
1661
+ if (!(err instanceof WorkCommandError) || err.status !== 403) return void 0;
1662
+ const permission = /missing required permission '([^']+)'/i.exec(err.message)?.[1];
1663
+ return permission === void 0 ? void 0 : sanitizeTerminal(permission);
1452
1664
  }
1453
- var sha256Of = (bytes) => createHash("sha256").update(bytes).digest("hex");
1454
- function pinProblem(file, expected, actual, where) {
1455
- 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})` : "";
1456
- return new AssetError(file, `${file} ${where} has sha256 ${actual}, not the ${expected} this release pins${release}, so no microVM build starts`);
1457
- }
1458
- function writeAtomic(path, bytes, mode) {
1459
- const temp = `${path}.${process.pid}.tmp`;
1460
- writeFileSync(temp, bytes, { mode });
1461
- renameSync(temp, path);
1665
+ function missingPermissionFix(permission) {
1666
+ return `use a key that holds ${permission}: create one with the Runner preset on the factory's API keys page, then run \`outerlayer login\` with it`;
1462
1667
  }
1463
- async function download(file, url, fetchImpl) {
1464
- let response;
1668
+ async function claimPermissionProblem(config, fetchImpl = fetch) {
1669
+ const unverified = (what, err) => ({
1670
+ status: "unverified",
1671
+ reason: sanitizeTerminal(`${what}: ${err instanceof Error ? describeCallFailure(err) : String(err)}`)
1672
+ });
1673
+ let signer;
1465
1674
  try {
1466
- response = await fetchImpl(url, { signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) });
1675
+ signer = await signerFor(config.configPath);
1467
1676
  } catch (err) {
1468
- throw new AssetError(file, `${file} could not be downloaded from ${url}: ${err instanceof Error ? err.message : String(err)}`);
1677
+ return unverified("this host's key could not be read, so whether the runner key can claim work was not checked", err);
1469
1678
  }
1470
- if (!response.ok) throw new AssetError(file, `${file} could not be downloaded from ${url}: the server answered ${response.status}`);
1471
- return Buffer.from(await response.arrayBuffer());
1472
- }
1473
- function keptPath(dir, sha256, name) {
1474
- mkdirSync(join(dir, sha256), { recursive: true, mode: RUNNER_DIR_MODE });
1475
- return join(dir, sha256, name);
1476
- }
1477
- async function ensureFile(dir, file, pin, fetchImpl, mode) {
1478
- const path = keptPath(dir, pin.sha256, file);
1479
- if (existsSync(path)) {
1480
- const actual2 = sha256Of(readFileSync(path));
1481
- if (actual2 !== pin.sha256) throw pinProblem(file, pin.sha256, actual2, `at ${path}`);
1482
- return path;
1679
+ try {
1680
+ await getJson(config.url, "/v1/runner/host-key", config.apiKey, config.appId, fetchImpl, signer);
1681
+ return { status: "ok" };
1682
+ } catch (err) {
1683
+ const permission = missingPermissionOf(err);
1684
+ if (permission === void 0) return unverified("the host-key call did not say whether the runner key can claim work", err);
1685
+ return { status: "lacks", problem: `the runner key lacks ${permission}, which a runner needs to claim work: ${missingPermissionFix(permission)}` };
1483
1686
  }
1484
- const bytes = await download(file, pin.url, fetchImpl);
1485
- const actual = sha256Of(bytes);
1486
- if (actual !== pin.sha256) throw pinProblem(file, pin.sha256, actual, `downloaded from ${pin.url}`);
1487
- writeAtomic(path, bytes, mode);
1488
- return path;
1489
1687
  }
1490
- function extractWithTar(arch, program = "firecracker") {
1491
- const name = `${program}-v${FIRECRACKER_VERSION}-${arch}`;
1492
- return async (tarball, destDir) => {
1493
- const result = spawnSync("tar", ["-xzf", tarball, "-C", destDir, "--strip-components=1", `release-v${FIRECRACKER_VERSION}-${arch}/${name}`], { encoding: "utf8" });
1494
- if (result.status !== 0) throw new Error(`tar could not unpack ${tarball}: ${result.stderr.trim()}`);
1495
- return join(destDir, name);
1496
- };
1688
+
1689
+ // src/runner/build-skill-check.ts
1690
+ var BUILD_SKILL_PATHS = [".agents/skills/build/SKILL.md", ".claude/skills/build/SKILL.md"];
1691
+ var BUILD_COMMAND = /(?:^|["']|(?:^|\s)(?:claude|-p|--print)\s+)\/build(?=["'\s]|$)/;
1692
+ var BUILD_DIRECTORY_USE = /(?:^|[\s;&|])(?:cd|pushd|--add-dir|--cwd|-C)\s+["']?\/build(?=["'\s;&|]|$)/g;
1693
+ function needsBuildSkill(queue, command) {
1694
+ if (queue !== "implement" || command === void 0) return false;
1695
+ return BUILD_COMMAND.test(currentSkillCommand(command).replace(BUILD_DIRECTORY_USE, " "));
1696
+ }
1697
+ function missingBuildSkillReason(governed, lookupFailure) {
1698
+ const checked = governed === void 0 ? BUILD_SKILL_PATHS.join(" and ") : `${BUILD_SKILL_PATHS.join(", ")} and the governed context from ${governed.repository}@${governed.commit.slice(0, 7)}`;
1699
+ const lookup2 = lookupFailure === void 0 ? "" : `, and the lookup of the governing repository failed (${lookupFailure})`;
1700
+ return `the build has no \`build\` skill, so /build would load nothing${lookup2}: checked ${checked}`;
1701
+ }
1702
+ function cloneHasBuildSkill(checkout) {
1703
+ return BUILD_SKILL_PATHS.some((path) => existsSync(join(checkout, path)));
1704
+ }
1705
+ async function decideHostBuildSkill(input) {
1706
+ const { requireBuildSkill, containerMode, vmMode, workdir, repository } = input;
1707
+ if (!requireBuildSkill || containerMode || vmMode || !existsSync(join(workdir, ".git")) || cloneHasBuildSkill(workdir)) return { kind: "start" };
1708
+ const governing = repository === void 0 ? void 0 : await input.lookup(repository);
1709
+ if (governing?.ok === true && governing.source.kind !== "self") return { kind: "warn" };
1710
+ const reason = missingBuildSkillReason(void 0, governing?.ok === false ? governing.reason : void 0);
1711
+ return { kind: "refuse", reason };
1497
1712
  }
1498
- async function ensureFirecracker(input) {
1499
- const { dir, arch, pins, fetchImpl } = input;
1500
- const firecracker = keptPath(dir, pins.firecracker.binarySha256, "firecracker");
1501
- if (existsSync(firecracker)) {
1502
- const actual = sha256Of(readFileSync(firecracker));
1503
- if (actual !== pins.firecracker.binarySha256) throw pinProblem("firecracker", pins.firecracker.binarySha256, actual, `at ${firecracker}`);
1504
- return firecracker;
1713
+ var BranchPrepareError = class extends Error {
1714
+ };
1715
+ var OWN_PREFIX = "outerlayer/";
1716
+ var GIT_TIMEOUT_MS = 12e4;
1717
+ var REPO_BINDING_GIT_VARS = new Set(
1718
+ `GIT_ALTERNATE_OBJECT_DIRECTORIES GIT_OBJECT_DIRECTORY GIT_DIR GIT_WORK_TREE GIT_IMPLICIT_WORK_TREE
1719
+ GIT_GRAFT_FILE GIT_INDEX_FILE GIT_NO_REPLACE_OBJECTS GIT_REPLACE_REF_BASE GIT_PREFIX
1720
+ GIT_INTERNAL_SUPER_PREFIX GIT_SHALLOW_FILE GIT_COMMON_DIR GIT_NAMESPACE`.split(/\s+/)
1721
+ );
1722
+ function localGit(cwd, args) {
1723
+ const env = {};
1724
+ for (const [key, value] of Object.entries(process.env)) {
1725
+ if (!REPO_BINDING_GIT_VARS.has(key)) env[key] = value;
1505
1726
  }
1506
- const tarballName = `firecracker-${arch}.tgz`;
1507
- const tarball = await ensureFile(dir, tarballName, pins.firecracker, fetchImpl, RUNNER_FILE_MODE);
1508
- const scratch = mkdtempSync(join(dir, ".extract-"));
1509
- try {
1510
- const extracted = await (input.extract ?? extractWithTar(arch))(tarball, scratch);
1511
- const bytes = readFileSync(extracted);
1512
- const actual = sha256Of(bytes);
1513
- if (actual !== pins.firecracker.binarySha256) throw pinProblem("firecracker", pins.firecracker.binarySha256, actual, `unpacked from ${tarballName}`);
1514
- writeAtomic(firecracker, bytes, 493);
1515
- } finally {
1516
- rmSync(scratch, { recursive: true, force: true });
1727
+ env.GIT_TERMINAL_PROMPT = "0";
1728
+ const result = spawnSync("git", args, { cwd, env, encoding: "utf8", timeout: GIT_TIMEOUT_MS });
1729
+ if (result.error !== void 0) {
1730
+ return Promise.resolve({ status: null, stdout: "", stderr: result.error.message });
1517
1731
  }
1518
- return firecracker;
1519
- }
1520
- async function ensureFirecrackerAndJailer(options) {
1521
- const arch = options.arch ?? firecrackerArch(process.arch);
1522
- 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`);
1523
- const pins = options.pins ?? pinnedAssets(arch);
1524
- const dir = vmAssetsDir(options.runnerDir);
1525
- mkdirSync(dir, { recursive: true, mode: RUNNER_DIR_MODE });
1526
- const firecracker = await ensureFirecracker({ dir, arch, pins, fetchImpl: options.fetchImpl ?? fetch, extract: options.extractFirecracker });
1527
- return { firecracker, jailer: await ensureJailer(options) };
1732
+ return Promise.resolve({ status: result.status, stdout: result.stdout.trim(), stderr: result.stderr.trim() });
1528
1733
  }
1529
- async function ensureAssets(options) {
1530
- const arch = options.arch ?? firecrackerArch(process.arch);
1531
- 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`);
1532
- const pins = options.pins ?? pinnedAssets(arch);
1533
- const fetchImpl = options.fetchImpl ?? fetch;
1534
- const dir = vmAssetsDir(options.runnerDir);
1535
- mkdirSync(dir, { recursive: true, mode: RUNNER_DIR_MODE });
1536
- const firecracker = await ensureFirecracker({ dir, arch, pins, fetchImpl, extract: options.extractFirecracker });
1537
- const kernel = await ensureFile(dir, `vmlinux-${arch}`, pins.kernel, fetchImpl, RUNNER_FILE_MODE);
1538
- const helper = await ensureFile(dir, `outerlayer-guest-${arch}`, pins.helper, fetchImpl, 493);
1539
- return { firecracker, kernel, helper, helperSha256: pins.helper.sha256 };
1734
+ function firstLine2(stderr) {
1735
+ return (stderr.split("\n")[0] ?? "").replace(/\/\/[^/@\s]+@/g, "//");
1540
1736
  }
1541
- async function ensureJailer(options) {
1542
- const arch = options.arch ?? firecrackerArch(process.arch);
1543
- 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`);
1544
- const pins = options.pins ?? pinnedAssets(arch);
1545
- const expected = pins.firecracker.jailerSha256;
1546
- if (expected === void 0) throw new AssetError("jailer", "this release pins no hash for the jailer, so it cannot be had");
1547
- const fetchImpl = options.fetchImpl ?? fetch;
1548
- const dir = vmAssetsDir(options.runnerDir);
1549
- mkdirSync(dir, { recursive: true, mode: RUNNER_DIR_MODE });
1550
- const jailer = keptPath(dir, expected, "jailer");
1551
- if (existsSync(jailer)) {
1552
- const actual = sha256Of(readFileSync(jailer));
1553
- if (actual !== expected) throw pinProblem("jailer", expected, actual, `at ${jailer}`);
1554
- return jailer;
1737
+ async function mustGit(git2, cwd, step, args) {
1738
+ const result = await git2(cwd, args);
1739
+ if (result.status !== 0) {
1740
+ const detail = firstLine2(result.stderr);
1741
+ throw new BranchPrepareError(`${step} failed${detail === "" ? "" : `: ${detail}`}`);
1555
1742
  }
1556
- const tarballName = `firecracker-${arch}.tgz`;
1557
- const tarball = await ensureFile(dir, tarballName, pins.firecracker, fetchImpl, RUNNER_FILE_MODE);
1558
- const scratch = mkdtempSync(join(dir, ".extract-"));
1559
- try {
1560
- const extracted = await (options.extractJailer ?? extractWithTar(arch, "jailer"))(tarball, scratch);
1561
- const bytes = readFileSync(extracted);
1562
- const actual = sha256Of(bytes);
1563
- if (actual !== expected) throw pinProblem("jailer", expected, actual, `unpacked from ${tarballName}`);
1564
- writeAtomic(jailer, bytes, 493);
1565
- } finally {
1566
- rmSync(scratch, { recursive: true, force: true });
1743
+ return result.stdout;
1744
+ }
1745
+ async function remoteDefaultBranch(git2, workdir) {
1746
+ const out = await mustGit(git2, workdir, "reading the remote's default branch", ["ls-remote", "--symref", "origin", "HEAD"]);
1747
+ const match = /^ref: refs\/heads\/(\S+)\s+HEAD$/m.exec(out);
1748
+ if (match === null) {
1749
+ throw new BranchPrepareError("reading the remote's default branch failed: the remote names none");
1567
1750
  }
1568
- return jailer;
1751
+ return match[1];
1569
1752
  }
1570
- async function ensureBuilderDisk(options) {
1571
- const arch = options.arch ?? firecrackerArch(process.arch);
1572
- 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`);
1573
- const file = `builder-rootfs-${arch}.ext4`;
1574
- const pin = options.pin ?? builderDiskPin(arch);
1575
- if (options.pin === void 0 && !builderDiskPinned(arch)) {
1576
- 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`);
1577
- }
1578
- const dir = vmAssetsDir(options.runnerDir);
1579
- mkdirSync(dir, { recursive: true, mode: RUNNER_DIR_MODE });
1580
- return ensureFile(dir, file, pin, options.fetchImpl ?? fetch, RUNNER_FILE_MODE);
1753
+ async function remoteHasBranch(git2, workdir, branch) {
1754
+ return await mustGit(git2, workdir, `looking for ${branch} on the remote`, ["ls-remote", "--heads", "origin", `refs/heads/${branch}`]) !== "";
1581
1755
  }
1582
- function dockerArch(nodeArch) {
1583
- if (nodeArch === "x64") return "amd64";
1584
- if (nodeArch === "arm64") return "arm64";
1585
- throw new AssetError("tool files", `no tool files are pinned for ${nodeArch}`);
1756
+ async function fetchBranch(git2, workdir, branch) {
1757
+ await mustGit(git2, workdir, `fetching ${branch}`, ["fetch", "--quiet", "origin", `+refs/heads/${branch}:refs/remotes/origin/${branch}`]);
1586
1758
  }
1587
- function toolFilePins(arch) {
1588
- return [
1589
- ...VOICE_PACK_FILES.map((file) => ({ path: `${FETCHED_VOICE_DIR}/${file.name}`, url: file.url, sha256: file.sha256 })),
1590
- { path: FETCHED_FFMPEG, url: `https://github.com/BtbN/FFmpeg-Builds/releases/download/${FFMPEG_BUILD_TAG}/${FFMPEG_STATIC[arch].file}`, sha256: FFMPEG_STATIC[arch].sha256 },
1591
- { 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] },
1592
- { path: FETCHED_TINI, url: `https://github.com/krallin/tini/releases/download/v${TINI_VERSION}/tini-static-${arch}`, sha256: TINI_SHA256[arch] }
1593
- ];
1759
+ async function prepareBranch(opts) {
1760
+ const { workdir, branch } = opts;
1761
+ const git2 = opts.git ?? localGit;
1762
+ const defaultBranch = await remoteDefaultBranch(git2, workdir);
1763
+ const exists = await remoteHasBranch(git2, workdir, branch);
1764
+ await fetchBranch(git2, workdir, defaultBranch);
1765
+ if (exists) await fetchBranch(git2, workdir, branch);
1766
+ let start;
1767
+ if (exists && !(branch.startsWith(OWN_PREFIX) && await isMerged(git2, workdir, branch, defaultBranch))) {
1768
+ start = `origin/${branch}`;
1769
+ } else if (branch.startsWith(OWN_PREFIX)) {
1770
+ start = `origin/${defaultBranch}`;
1771
+ } else {
1772
+ throw new BranchPrepareError(`the pull request's branch ${branch} is not on the remote`);
1773
+ }
1774
+ await mustGit(git2, workdir, `checking out ${branch}`, ["checkout", "--quiet", "-B", branch, start]);
1775
+ return {
1776
+ branch,
1777
+ head: await mustGit(git2, workdir, "reading the checked-out commit", ["rev-parse", "HEAD"]),
1778
+ startCommit: await mustGit(git2, workdir, `reading the commit of ${defaultBranch}`, ["rev-parse", `origin/${defaultBranch}`])
1779
+ };
1594
1780
  }
1595
- function pinsDir(runnerDir2, pins) {
1596
- const key = createHash("sha256").update(pins.map((pin) => `${pin.path} ${pin.sha256}`).join("\n")).digest("hex").slice(0, 16);
1597
- const root = join(vmAssetsDir(runnerDir2), "tool-files");
1598
- return { root, dir: join(root, key) };
1781
+ async function isMerged(git2, workdir, branch, defaultBranch) {
1782
+ const result = await git2(workdir, ["merge-base", "--is-ancestor", `origin/${branch}`, `origin/${defaultBranch}`]);
1783
+ if (result.status === 0) return true;
1784
+ if (result.status === 1) return false;
1785
+ const detail = firstLine2(result.stderr);
1786
+ throw new BranchPrepareError(`comparing ${branch} with ${defaultBranch} failed${detail === "" ? "" : `: ${detail}`}`);
1599
1787
  }
1600
- var inFlight = /* @__PURE__ */ new Map();
1601
- async function ensureToolFiles(options) {
1602
- const pins = options.pins ?? toolFilePins(options.arch ?? dockerArch(process.arch));
1603
- const { root, dir } = pinsDir(options.runnerDir, pins);
1604
- const running = inFlight.get(dir);
1605
- if (running !== void 0) return running;
1606
- const fetching = fetchAll(root, dir, pins, options.fetchImpl ?? fetch).finally(() => inFlight.delete(dir));
1607
- inFlight.set(dir, fetching);
1608
- return fetching;
1788
+
1789
+ // src/runner/container/lifecycle.ts
1790
+ var ORDER = ["onCreateCommand", "updateContentCommand", "postCreateCommand", "postStartCommand"];
1791
+ function commandArgv(command) {
1792
+ if (typeof command === "string") return command === "" ? void 0 : ["/bin/sh", "-c", command];
1793
+ if (Array.isArray(command) && command.length > 0 && command.every((part) => typeof part === "string")) return command;
1794
+ return void 0;
1609
1795
  }
1610
- async function fetchAll(root, dir, pins, fetchImpl) {
1611
- mkdirSync(dir, { recursive: true, mode: RUNNER_DIR_MODE });
1612
- for (const pin of pins) {
1613
- const path = join(dir, pin.path);
1614
- if (existsSync(path)) {
1615
- const actual = await sha256OfFile(path);
1616
- if (actual !== pin.sha256) {
1617
- 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`);
1796
+ function lifecycleSteps(lifecycle) {
1797
+ const steps = [];
1798
+ for (const name of ORDER) {
1799
+ const entries = lifecycle[name];
1800
+ if (entries === void 0) continue;
1801
+ for (const { source, command } of entries) {
1802
+ if (typeof command === "object" && command !== null && !Array.isArray(command)) {
1803
+ for (const [key, value] of Object.entries(command)) {
1804
+ const argv2 = commandArgv(value);
1805
+ if (argv2 !== void 0) steps.push({ name: `${name}[${key}]`, source, argv: argv2 });
1806
+ }
1807
+ continue;
1618
1808
  }
1619
- continue;
1620
- }
1621
- try {
1622
- await downloadPinned({ url: pin.url, dest: path, sha256: pin.sha256, name: pin.path, fetchImpl, stopped: "no image build started" });
1623
- } catch (err) {
1624
- throw new AssetError(pin.path, `the tool file ${pin.path} could not be had: ${err instanceof VoiceDownloadError ? err.message : String(err)}`);
1809
+ const argv = commandArgv(command);
1810
+ if (argv !== void 0) steps.push({ name, source, argv });
1625
1811
  }
1626
1812
  }
1627
- for (const name of readdirSync(root)) {
1628
- if (join(root, name) !== dir) rmSync(join(root, name), { recursive: true, force: true });
1629
- }
1630
- return dir;
1813
+ return steps;
1631
1814
  }
1632
-
1633
- // src/runner/microvm/host.ts
1634
- function createMicroVmPorts(over = {}) {
1635
- return {
1636
- assets: over.assets ?? ((runnerDir2) => ensureAssets({ runnerDir: runnerDir2 })),
1637
- rootfs: over.rootfs ?? createRootfsHost(),
1638
- firecracker: over.firecracker ?? createFirecrackerHost(),
1639
- cgroup: over.cgroup ?? realCgroupPort(),
1640
- helper: over.helper ?? realHelperPort(),
1641
- fdStore: over.fdStore ?? realFdStorePort(),
1642
- exec: over.exec ?? execCapture,
1643
- execStream: over.execStream ?? runExec,
1644
- builderDisk: over.builderDisk ?? ((runnerDir2) => ensureBuilderDisk({ runnerDir: runnerDir2 })),
1645
- builderDiskPinned: over.builderDiskPinned ?? (() => builderDiskPinned()),
1646
- toolFiles: over.toolFiles ?? ((runnerDir2) => ensureToolFiles({ runnerDir: runnerDir2 }))
1647
- };
1815
+ function lifecycleFailureReason(step, exitCode) {
1816
+ return `lifecycle command ${step.name} (${step.source}) exited ${exitCode}`;
1648
1817
  }
1818
+
1819
+ // src/runner/build-steps.ts
1649
1820
  var aborted = () => ({ ok: false, reason: "", aborted: true });
1650
1821
  function firstLine3(text) {
1651
1822
  return text.trim().split("\n")[0] ?? "";
@@ -1688,6 +1859,17 @@ async function deliveryRefusal(input, staged) {
1688
1859
  const folder = own.get(".claude/skills").has(clash) ? ".claude/skills" : ".agents/skills";
1689
1860
  return `context_skill_conflict: ${skillConflictMessage({ skill: clash, folder, repository: input.repository, controlPlane: staged.repository })}, and would replace it in this build; rename or remove it from the repository`;
1690
1861
  }
1862
+ async function checkoutHasBuildSkill(input) {
1863
+ for (const path of BUILD_SKILL_PATHS) {
1864
+ const probe = await input.exec(["test", "-f", path], CHECKOUT_DIR, { quiet: true });
1865
+ if (probe.status === 0) return true;
1866
+ if (probe.status !== 1) return { failed: failure(`checking for ${path} failed`, probe) };
1867
+ }
1868
+ return false;
1869
+ }
1870
+ function stagedHasBuildSkill(staged) {
1871
+ return existsSync(join(staged.dir, "tree", ".claude/skills/build/SKILL.md"));
1872
+ }
1691
1873
  async function writeShim(input) {
1692
1874
  const shim = await input.exec(["sh", "-c", WRITE_SHIM_SCRIPT, "shim", SHIM_DIR, shimScript()], "/build/work");
1693
1875
  if (shim.status !== 0) return { ok: false, reason: failure("writing the claude shim failed", shim) };
@@ -1770,6 +1952,14 @@ async function runBuildSteps(input) {
1770
1952
  const staged = await input.stageContext();
1771
1953
  if (staged.kind === "refused") return { ok: false, reason: staged.reason };
1772
1954
  let controlPlane;
1955
+ if (input.requireBuildSkill === true && !(staged.kind === "staged" && stagedHasBuildSkill(staged))) {
1956
+ const held = await checkoutHasBuildSkill(input);
1957
+ if (held !== true) {
1958
+ if (staged.kind === "staged") input.removeStagedContext();
1959
+ if (held !== false) return { ok: false, reason: held.failed };
1960
+ return { ok: false, reason: missingBuildSkillReason(staged.kind === "staged" ? { repository: staged.repository, commit: staged.sha } : void 0) };
1961
+ }
1962
+ }
1773
1963
  const shimmed = await writeShim(input);
1774
1964
  if (!shimmed.ok) {
1775
1965
  if (staged.kind === "staged") input.removeStagedContext();
@@ -2878,6 +3068,103 @@ function isLfsUpload(body) {
2878
3068
  return false;
2879
3069
  }
2880
3070
  }
3071
+ function parseCpuList(text) {
3072
+ const cores = /* @__PURE__ */ new Set();
3073
+ for (const part of text.trim().split(",")) {
3074
+ const match = /^(\d+)(?:-(\d+))?$/.exec(part.trim());
3075
+ if (match === null) return void 0;
3076
+ const low = Number(match[1]);
3077
+ const high = match[2] === void 0 ? low : Number(match[2]);
3078
+ if (high < low) return void 0;
3079
+ for (let core = low; core <= high; core++) cores.add(core);
3080
+ }
3081
+ return [...cores].sort((a, b) => a - b);
3082
+ }
3083
+ function formatCpuList(cores) {
3084
+ const sorted = [...new Set(cores)].sort((a, b) => a - b);
3085
+ const parts = [];
3086
+ for (let i = 0; i < sorted.length; i++) {
3087
+ const start = sorted[i];
3088
+ while (sorted[i + 1] === sorted[i] + 1) i++;
3089
+ parts.push(start === sorted[i] ? String(start) : `${start}-${sorted[i]}`);
3090
+ }
3091
+ return parts.join(",");
3092
+ }
3093
+ function capCpus(wanted, cpuset) {
3094
+ const cores = cpuset === void 0 ? void 0 : parseCpuList(cpuset);
3095
+ return cores === void 0 ? wanted : Math.min(wanted, cores.length);
3096
+ }
3097
+ function usableCpus(deps = {}) {
3098
+ try {
3099
+ const status = (deps.readStatus ?? (() => readFileSync("/proc/self/status", "utf8")))();
3100
+ const cores = parseCpuList(/^Cpus_allowed_list:\s*(.+)$/m.exec(status)?.[1] ?? "");
3101
+ if (cores !== void 0) return cores;
3102
+ } catch {
3103
+ }
3104
+ return Array.from({ length: Math.max(1, (deps.parallelism ?? availableParallelism)()) }, (_unused, core) => core);
3105
+ }
3106
+ function createCpuAllocator(cores) {
3107
+ const usable = [...new Set(cores)].sort((a, b) => a - b);
3108
+ const held = /* @__PURE__ */ new Map();
3109
+ const holders = (core) => {
3110
+ let count = 0;
3111
+ for (const set of held.values()) if (set.includes(core)) count++;
3112
+ return count;
3113
+ };
3114
+ return {
3115
+ acquire(buildId, cpus) {
3116
+ const existing = held.get(buildId);
3117
+ if (existing !== void 0) return existing;
3118
+ const wanted = Math.min(Math.ceil(cpus), usable.length);
3119
+ const ranked = usable.map((core) => ({ core, count: holders(core) })).sort((a, b) => a.count - b.count);
3120
+ const chosen = ranked.slice(0, wanted).map(({ core }) => core).sort((a, b) => a - b);
3121
+ held.set(buildId, chosen);
3122
+ return chosen;
3123
+ },
3124
+ hold(buildId, set) {
3125
+ held.set(buildId, [...set]);
3126
+ },
3127
+ release(buildId) {
3128
+ held.delete(buildId);
3129
+ }
3130
+ };
3131
+ }
3132
+
3133
+ // src/runner/container/lease-channel.ts
3134
+ var LEASE_SECONDS = 900;
3135
+ function leaseExpiresAtMs(job) {
3136
+ const base = Date.parse(job.lastRenewAt ?? job.startedAt);
3137
+ return (Number.isFinite(base) ? base : Date.now()) + LEASE_SECONDS * 1e3;
3138
+ }
3139
+ function leaseTimeLeftMs(job, nowMs) {
3140
+ return Math.max(0, leaseExpiresAtMs(job) - nowMs);
3141
+ }
3142
+ function createLeaseChannel(valueMs) {
3143
+ const open = /* @__PURE__ */ new Set();
3144
+ let stopped = false;
3145
+ const write = (stream) => {
3146
+ if (!stopped && !stream.destroyed && stream.writable) stream.write(`${Math.floor(valueMs())}
3147
+ `);
3148
+ };
3149
+ return {
3150
+ accept(stream) {
3151
+ stream.on("error", () => void 0);
3152
+ open.add(stream);
3153
+ stream.once("close", () => open.delete(stream));
3154
+ queueMicrotask(() => write(stream));
3155
+ },
3156
+ publish() {
3157
+ for (const stream of open) write(stream);
3158
+ },
3159
+ stop() {
3160
+ stopped = true;
3161
+ },
3162
+ close() {
3163
+ for (const stream of [...open]) stream.destroy();
3164
+ open.clear();
3165
+ }
3166
+ };
3167
+ }
2881
3168
 
2882
3169
  // src/runner/container/ranges.ts
2883
3170
  var V4 = [
@@ -3071,6 +3358,7 @@ async function startTunnel(socketPath, opts = {}) {
3071
3358
  const extraRefusalKeys = /* @__PURE__ */ new Set();
3072
3359
  let refusalsMore = 0;
3073
3360
  const destinations = [];
3361
+ const routes = /* @__PURE__ */ Object.create(null);
3074
3362
  if (opts.recordPath !== void 0 && existsSync(opts.recordPath)) {
3075
3363
  try {
3076
3364
  const saved = JSON.parse(readFileSync(opts.recordPath, "utf8"));
@@ -3080,6 +3368,7 @@ async function startTunnel(socketPath, opts = {}) {
3080
3368
  }
3081
3369
  hostsMore = saved.hostsMore ?? 0;
3082
3370
  for (const name of saved.destinations ?? []) destinations.push(name);
3371
+ for (const [name, counts] of Object.entries(saved.routes ?? {})) routes[name] = { requests: counts.requests, failures: counts.failures };
3083
3372
  refusalsMore = saved.refusalsMore ?? 0;
3084
3373
  for (const refusal of saved.refusals ?? []) {
3085
3374
  refusals.push(refusal);
@@ -3093,7 +3382,9 @@ async function startTunnel(socketPath, opts = {}) {
3093
3382
  hostsMore,
3094
3383
  refusalsMore,
3095
3384
  refusals: refusals.map((r) => ({ ...r })),
3096
- destinations: [...destinations]
3385
+ destinations: [...destinations],
3386
+ // Absent until a route is served, so a record of a tunnel that served none is the shape an older runner wrote.
3387
+ ...Object.keys(routes).length === 0 ? {} : { routes: Object.fromEntries(Object.entries(routes).map(([name, counts]) => [name, { ...counts }])) }
3097
3388
  });
3098
3389
  const persist = () => {
3099
3390
  if (opts.recordPath === void 0) return;
@@ -3107,6 +3398,17 @@ async function startTunnel(socketPath, opts = {}) {
3107
3398
  destinations.push(name);
3108
3399
  persist();
3109
3400
  };
3401
+ const routeServed = (name) => {
3402
+ if (routes[name] !== void 0) return;
3403
+ routes[name] = { requests: 0, failures: 0 };
3404
+ persist();
3405
+ };
3406
+ const routeFinished = (name, ok) => {
3407
+ const counts = routes[name] ??= { requests: 0, failures: 0 };
3408
+ counts.requests++;
3409
+ if (!ok) counts.failures++;
3410
+ persist();
3411
+ };
3110
3412
  const reached = (host) => {
3111
3413
  const name = host.toLowerCase();
3112
3414
  if (seen.has(name)) return;
@@ -3332,6 +3634,8 @@ ${body}`);
3332
3634
  socketPath,
3333
3635
  record,
3334
3636
  destinationUsed,
3637
+ routeServed,
3638
+ routeFinished,
3335
3639
  acceptStream: (stream) => void server.emit("connection", stream),
3336
3640
  listener: () => opts.socket === false ? void 0 : server,
3337
3641
  async listenTcp(address) {
@@ -3698,7 +4002,7 @@ async function provisionContainer(input) {
3698
4002
  let services;
3699
4003
  try {
3700
4004
  const gitRemote = gitRemoteFor(runtime, { repositories, tokens, buildDir: buildDir2, log: (line) => log.line(`${tag} ${line}`) });
3701
- services = await startServicesFor(runtime, config, buildDir2, jobDir, itemKey, true, gitRemote, "stream", input.leaseExpiryMs === void 0 ? void 0 : leaseFor(runtime, buildId, input.leaseExpiryMs));
4005
+ services = await startServicesFor(runtime, config, buildDir2, jobDir, itemKey, true, gitRemote, "stream", input.leaseExpiryMs === void 0 ? void 0 : leaseFor(runtime, buildId, input.leaseExpiryMs), void 0, (line) => log.line(`${tag} ${line}`));
3702
4006
  runtime.tunnels.set(buildId, services);
3703
4007
  } catch (err) {
3704
4008
  return bail(`the tunnel could not start: ${describeCallFailure(err)}`);
@@ -3794,7 +4098,8 @@ async function provisionContainer(input) {
3794
4098
  return delivered.ok ? delivered : { ok: false, reason: `context_unavailable: ${delivered.reason}` };
3795
4099
  },
3796
4100
  removeStagedContext: () => removeStagedContext(buildDir2),
3797
- keepIn: jobDir
4101
+ keepIn: jobDir,
4102
+ ...input.requireBuildSkill === true ? { requireBuildSkill: true } : {}
3798
4103
  });
3799
4104
  if (!steps.ok) return bail(steps.reason, steps.aborted === true);
3800
4105
  state.startCommit = steps.startCommit;
@@ -3904,7 +4209,7 @@ async function measureDisk(runtime, state) {
3904
4209
  clearTimeout(timer);
3905
4210
  }
3906
4211
  }
3907
- async function startServicesFor(runtime, config, buildDir2, jobDir, itemKey, builtIns, gitRemote, serve = "socket", lease, inherit) {
4212
+ async function startServicesFor(runtime, config, buildDir2, jobDir, itemKey, builtIns, gitRemote, serve = "socket", lease, inherit, routeLog) {
3908
4213
  return startBuildServices({
3909
4214
  startTunnel: runtime.startTunnel,
3910
4215
  tunnelOptions: { ...runtime.tunnelOptions, ...gitRemote === void 0 ? {} : { gitRemote } },
@@ -3915,6 +4220,7 @@ async function startServicesFor(runtime, config, buildDir2, jobDir, itemKey, bui
3915
4220
  serve,
3916
4221
  ...lease === void 0 ? {} : { lease },
3917
4222
  ...inherit === void 0 ? {} : { inherit },
4223
+ ...routeLog === void 0 ? {} : { log: routeLog },
3918
4224
  ...builtIns ? { builtIns: { gatewayUrl: config.url, itemKey, claudeUpstream: runtime.claudeUpstream } } : {},
3919
4225
  hostEnv: runtime.hostEnv,
3920
4226
  destinationOptions: runtime.destinationOptions
@@ -4240,4 +4546,102 @@ async function startCheckContainer(input) {
4240
4546
  };
4241
4547
  }
4242
4548
 
4243
- 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, 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 };
4549
+ // src/runner/machine.ts
4550
+ function groupName(gid) {
4551
+ try {
4552
+ for (const line of readFileSync("/etc/group", "utf8").split("\n")) {
4553
+ const [name, , id] = line.split(":");
4554
+ if (name !== void 0 && Number(id) === gid) return name;
4555
+ }
4556
+ } catch {
4557
+ }
4558
+ return String(gid);
4559
+ }
4560
+ function detectKvm(device) {
4561
+ if (!existsSync(device)) return { state: "absent" };
4562
+ try {
4563
+ accessSync(device, constants.R_OK | constants.W_OK);
4564
+ return { state: "usable" };
4565
+ } catch {
4566
+ return { state: "no-permission", group: groupName(statSync(device).gid) };
4567
+ }
4568
+ }
4569
+ function readProcVersion() {
4570
+ try {
4571
+ return readFileSync("/proc/version", "utf8");
4572
+ } catch {
4573
+ return void 0;
4574
+ }
4575
+ }
4576
+ function docker(args) {
4577
+ const result = spawnSync("docker", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 15e3 });
4578
+ const out = result.status === 0 ? result.stdout.trim() : "";
4579
+ return out === "" ? void 0 : out;
4580
+ }
4581
+ function dockerFailure() {
4582
+ const result = spawnSync("docker", ["version", "--format", "{{.Server.Version}}"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 15e3 });
4583
+ if (result.error?.code === "ENOENT") return void 0;
4584
+ return /permission denied/i.test(result.stderr ?? "") ? "permission" : "daemon";
4585
+ }
4586
+ function platformOf(probes) {
4587
+ const platform = probes.platform ?? process.platform;
4588
+ if (platform === "win32") return "windows";
4589
+ if (platform === "darwin") return "darwin";
4590
+ if (platform !== "linux") return "other";
4591
+ return /microsoft/i.test((probes.procVersion ?? readProcVersion)() ?? "") ? "wsl2" : "linux";
4592
+ }
4593
+ function detectMachine(probes = {}) {
4594
+ const platform = platformOf(probes);
4595
+ const kvm = (probes.kvm ?? (() => detectKvm(probes.kvmDevice ?? "/dev/kvm")))();
4596
+ const arch = firecrackerArch(probes.nodeArch ?? process.arch);
4597
+ const inImage = runnerImageVersion(probes.env ?? process.env) !== void 0 ? { inImage: true } : {};
4598
+ const version = (probes.dockerVersion ?? (() => docker(["version", "--format", "{{.Server.Version}}"])))();
4599
+ if (version === void 0) {
4600
+ const failure2 = (probes.dockerFailure ?? (probes.dockerVersion === void 0 ? dockerFailure : () => void 0))();
4601
+ return { platform, ...inImage, docker: failure2 === void 0 ? { kind: "none" } : { kind: "unreachable", reason: failure2 }, kvm, arch };
4602
+ }
4603
+ const system = (probes.dockerOperatingSystem ?? (() => docker(["info", "--format", "{{.OperatingSystem}}"])))() ?? "";
4604
+ return { platform, ...inImage, docker: { kind: /docker desktop/i.test(system) ? "desktop" : "engine", version }, kvm, arch };
4605
+ }
4606
+ function supportsContainerBuilds(machine) {
4607
+ if (machine.platform !== "linux" && machine.platform !== "wsl2") return false;
4608
+ const { kind, version } = machine.docker;
4609
+ if (kind !== "engine" && !(kind === "desktop" && machine.inImage === true)) return false;
4610
+ return Number.parseInt(version ?? "", 10) >= MIN_ENGINE_MAJOR;
4611
+ }
4612
+ function supportsMicroVmBuilds(machine) {
4613
+ return machine.platform === "linux" && machine.kvm?.state === "usable" && supportsContainerBuilds(machine) && microVmAssetsPinned(machine);
4614
+ }
4615
+ function microVmArchSupported(machine) {
4616
+ return MICROVM_ARCHES.includes(machine.arch ?? "");
4617
+ }
4618
+ function microVmAssetsPinned(machine) {
4619
+ return microVmArchSupported(machine) && guestAssetsPinned(machine.arch);
4620
+ }
4621
+ function reachDockerStep(machine) {
4622
+ if (machine.docker.kind !== "unreachable") return void 0;
4623
+ return machine.docker.reason === "permission" ? "give this user access to Docker Engine's socket (add it to the docker group, then log in again)" : "start Docker Engine's daemon (for example, sudo systemctl start docker)";
4624
+ }
4625
+ function describeMachineDocker(machine) {
4626
+ const { kind, version, reason } = machine.docker;
4627
+ const where = machine.platform === "windows" ? "Windows" : machine.platform === "darwin" ? "macOS" : machine.platform === "wsl2" ? "WSL2" : "Linux";
4628
+ if (kind === "desktop") {
4629
+ const engine = version === void 0 ? "" : ` with engine ${version}`;
4630
+ if (machine.inImage === true && (machine.platform === "linux" || machine.platform === "wsl2")) {
4631
+ const major = Number.parseInt(version ?? "", 10);
4632
+ if (Number.isNaN(major)) return `Docker Desktop${engine}, whose engine version is not readable`;
4633
+ if (major < MIN_ENGINE_MAJOR) return `Docker Desktop${engine}, older than the ${MIN_ENGINE_MAJOR} that container builds need`;
4634
+ return `Docker Desktop${engine}`;
4635
+ }
4636
+ return `Docker Desktop${engine} on ${where}, which a runner accepts only in the runner image`;
4637
+ }
4638
+ if (kind === "none") return `no Docker on ${where}`;
4639
+ if (kind === "unreachable") {
4640
+ const problem = reason === "permission" ? "this user has no permission on its socket" : "its daemon is not running";
4641
+ return `a docker command on ${where} that cannot reach Docker Engine (${problem})`;
4642
+ }
4643
+ if (machine.platform !== "linux" && machine.platform !== "wsl2") return `Docker Engine ${version ?? ""} on ${where}, which cannot isolate a build`.replace(" ", " ");
4644
+ return `Docker Engine ${version ?? "of no known version"}, older than the ${MIN_ENGINE_MAJOR} that container builds need`;
4645
+ }
4646
+
4647
+ export { ADDED_USER, AgentUserError, AssetError, DeliveryFailure, EVAL_INPUT_NAME, GH_CONFIG_YML, HELPER_ENV, LEASE_SECONDS, MANAGED_UNJAILED_REASON, MIN_ENGINE_MAJOR, PLACEHOLDER_MARKER, ROOT_DISK_ID_PREFIX, RunnerInitError, claimPermissionProblem, claudeCredentialCheck, cleanupContainer, containerCommandExec, containerJudgeEnvironment, containerSenderCommand, copyJudgeTree, copyTranscripts, copyTranscriptsFromContainer, createBuildDisk, createContainerRuntime, createLeaseChannel, currentSkillCommand, decideHostBuildSkill, deleteBuildDisk, deliverContainerHooks, describeMachineDocker, detectMachine, ensureFirecrackerAndJailer, ensureRootfs, ensureTunnel, filesAreCopied, formerSkillProblem, formerSkillQueues, ghHostsYml, gitRemoteFor, helperFirecrackerNote, helperSocketPath, isLiteralAddress, isPlaceholderCommand, keepGhConfigCurrent, leaseExpiresAtMs, leaseTimeLeftMs, lifecycleFailureReason, lifecycleSteps, machineConfig, makeBuildsDirPrivate, measureDisk, microVmArchSupported, microVmAssetsPinned, missingPermissionFix, missingPermissionOf, needsBuildSkill, needsClaudeCredential, obtainImage, outOfMemoryReason, parseCpuList, permissionModeProblem, permissionModeProblems, placeholderProblem, placeholderQueues, prepareBranch, prepareRunnerInit, probeHelper, provisionContainer, reachDockerStep, readConfigObject, resolveAgentUser, runBuildSteps, runRunnerInit, senderAliveScript, senderSignalScript, setUpRunnerCgroup, shellQuote, signalInContainer, startCheckContainer, startJailedInto, startServicesFor, stopContainer, stopJailed, supportsContainerBuilds, supportsMicroVmBuilds, sweepImageBuilders, syncInContainer, withTunnelLog, writeEvalInput, writeRunnerInit };