@outerlayer/cli 0.17.0 → 0.18.0

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 (144) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +15 -0
  3. package/dist/agent-setup-RBTEACYU.js +28 -0
  4. package/dist/{artifact-link-cmd-2F47LT7F.js → artifact-link-cmd-AVM2EKTV.js} +4 -3
  5. package/dist/{build-BG2YENGK.js → build-TQ6X5PYN.js} +5 -4
  6. package/dist/build-info.json +1 -1
  7. package/dist/{builder-2D3WSOOY.js → builder-DUI2ZH2H.js} +5 -6
  8. package/dist/{check-5XE7XMC4.js → check-5C4YDZYV.js} +53 -50
  9. package/dist/{chunk-R7PF5OOU.js → chunk-3CWUOFV6.js} +21 -14
  10. package/dist/{chunk-G5NPRL2K.js → chunk-3OFZM4X6.js} +548 -68
  11. package/dist/{chunk-2MNPHL23.js → chunk-47TTLEDX.js} +6 -5
  12. package/dist/{chunk-PLMTLGYH.js → chunk-52NC2FIU.js} +4 -3
  13. package/dist/chunk-6CHIYEZZ.js +564 -0
  14. package/dist/{chunk-TI7B7UEO.js → chunk-AMFWYESY.js} +1 -1
  15. package/dist/{chunk-YRYMNBGQ.js → chunk-ANPNT6PM.js} +2 -1
  16. package/dist/{chunk-YU3VKZ5F.js → chunk-B3UKB46N.js} +6 -6
  17. package/dist/{chunk-JXYVA45C.js → chunk-BMYGAGBJ.js} +1050 -304
  18. package/dist/{chunk-4Q7OMKAT.js → chunk-BQXFBI7T.js} +81 -5
  19. package/dist/chunk-CRB3POP4.js +34 -0
  20. package/dist/{chunk-F47JBIAW.js → chunk-DSF2FWAZ.js} +2 -1
  21. package/dist/{chunk-N5FOE5PS.js → chunk-ECODQ7II.js} +4 -33
  22. package/dist/{chunk-NBGDBTO2.js → chunk-ESC6UR76.js} +6 -5
  23. package/dist/{chunk-HHJDN6AU.js → chunk-ESVW4MJY.js} +1 -1
  24. package/dist/{chunk-42ZI52JW.js → chunk-F4XSVQJ4.js} +4 -3
  25. package/dist/{chunk-LWSJQJC6.js → chunk-FUB7LRUE.js} +7 -7
  26. package/dist/chunk-G2YNJIGF.js +44 -0
  27. package/dist/{chunk-QKFB4K7X.js → chunk-GGOMT6KF.js} +1 -1
  28. package/dist/{chunk-VQYBXPI7.js → chunk-GOBOJMOJ.js} +5 -5
  29. package/dist/{chunk-N6LURUEF.js → chunk-GOSFARMS.js} +1 -1
  30. package/dist/chunk-JBJTC3UU.js +35 -0
  31. package/dist/{chunk-NWQTTSBB.js → chunk-JF42URPU.js} +16 -15
  32. package/dist/{chunk-ADJRGVZC.js → chunk-KDVIU23F.js} +1 -1
  33. package/dist/{chunk-S3NYLI2P.js → chunk-KK4VBOH6.js} +93 -37
  34. package/dist/{chunk-KMDLFI4P.js → chunk-L2USZT6T.js} +1 -1
  35. package/dist/{chunk-WTE3MQML.js → chunk-LXAY4A2G.js} +3 -12
  36. package/dist/{chunk-V7NIVOFD.js → chunk-M2ZWWTWZ.js} +1 -1
  37. package/dist/{chunk-ERI5GBNI.js → chunk-MLAAT4S6.js} +3 -3
  38. package/dist/{chunk-POJMEEJJ.js → chunk-MNTSH4CL.js} +1 -1
  39. package/dist/{chunk-WUYTWNMA.js → chunk-MQJSXHV7.js} +1 -1
  40. package/dist/{chunk-O765JR4W.js → chunk-MTP2QEVO.js} +1 -1
  41. package/dist/{chunk-ZM2IMMYN.js → chunk-N2PXGQRL.js} +9 -1
  42. package/dist/{chunk-J5M7BSJQ.js → chunk-N3CUDFCW.js} +9 -3
  43. package/dist/{chunk-4KSQKQXE.js → chunk-NACDBXUG.js} +19 -15
  44. package/dist/{chunk-ONKJUM6R.js → chunk-NL4XPC7W.js} +1 -1
  45. package/dist/{chunk-WVUIRTWA.js → chunk-QQM2UJ6T.js} +31 -26
  46. package/dist/{chunk-KG5ECZGJ.js → chunk-RAZTWYZN.js} +10 -9
  47. package/dist/{chunk-4NPZVMJU.js → chunk-S26M3YBJ.js} +60 -4
  48. package/dist/{chunk-UGP76IZ5.js → chunk-S5PCA3SN.js} +1 -1
  49. package/dist/{chunk-3NSNIIBE.js → chunk-S76AYB4M.js} +17 -16
  50. package/dist/{chunk-RMWLBUCJ.js → chunk-SADUQSO4.js} +1 -1
  51. package/dist/{chunk-OZGHL5Y5.js → chunk-SWCTGCSC.js} +2 -2
  52. package/dist/{chunk-GQD22KKN.js → chunk-T2Q7ISPE.js} +2 -1
  53. package/dist/{chunk-C5Y5ZJJZ.js → chunk-T4Y6SCG3.js} +5 -4
  54. package/dist/{chunk-BWQ66M6N.js → chunk-ULSIJGLS.js} +45 -13
  55. package/dist/{chunk-NNRTPG7X.js → chunk-UYOYTKE2.js} +4 -4
  56. package/dist/{chunk-PTW6W5NR.js → chunk-VYAL7PUE.js} +1 -1
  57. package/dist/{chunk-YZ5F4PIS.js → chunk-W5VPCONO.js} +146 -247
  58. package/dist/{chunk-6Y5RUTHO.js → chunk-WBT5X3NH.js} +5 -4
  59. package/dist/{chunk-RLYZLUQ7.js → chunk-WGAUY5LE.js} +2 -2
  60. package/dist/{chunk-RSNSZ2LJ.js → chunk-X5H2WYMB.js} +1 -1
  61. package/dist/{chunk-OHVJA253.js → chunk-X5N6KC6V.js} +47 -29
  62. package/dist/{chunk-22RMZBGW.js → chunk-XGP3VYOG.js} +1 -1
  63. package/dist/{chunk-HYZRY4KZ.js → chunk-XNGMQ27R.js} +16 -6
  64. package/dist/{chunk-HGECYFEI.js → chunk-Y5GE76XC.js} +131 -54
  65. package/dist/{chunk-MAK5LYRF.js → chunk-Y5MXET4M.js} +277 -535
  66. package/dist/{chunk-XCK2YAY5.js → chunk-YVZJ6GKF.js} +3 -2
  67. package/dist/{chunk-YO4PSLVB.js → chunk-ZRL6HKBY.js} +1 -1
  68. package/dist/claude-launcher-YVXI76NX.js +102 -0
  69. package/dist/{cli-A76AXELQ.js → cli-C77H54BV.js} +468 -161
  70. package/dist/{command-R2QCVX55.js → command-H5CILO4S.js} +40 -37
  71. package/dist/{config-N3C6DFQA.js → config-FXDG4NPJ.js} +2 -3
  72. package/dist/{connect-cmd-JBKAQNSF.js → connect-cmd-ZW5FV3ZW.js} +9 -8
  73. package/dist/{context-adopt-NFHCLVUB.js → context-adopt-O5OFXMKI.js} +13 -12
  74. package/dist/context-materialize-Y5IJ4MQT.js +23 -0
  75. package/dist/{dist-LKDWQWZ7.js → dist-QTX4RSCL.js} +2 -2
  76. package/dist/{docker-4J77Q2BF.js → docker-RC3PRX3Q.js} +7 -8
  77. package/dist/{doctor-4TAIARIG.js → doctor-2NQQML6P.js} +46 -39
  78. package/dist/{emit-artifact-cmd-MPZVR56N.js → emit-artifact-cmd-7MP63KXU.js} +25 -22
  79. package/dist/emit-cmd-PPN4QQF2.js +14 -0
  80. package/dist/{emit-criteria-cmd-EFGYI23V.js → emit-criteria-cmd-3RXPDLNK.js} +16 -14
  81. package/dist/{emit-finding-cmd-5GF3V4LR.js → emit-finding-cmd-OL7JB6H3.js} +18 -16
  82. package/dist/{emit-result-cmd-I4OADKMD.js → emit-result-cmd-UZVMOCF4.js} +20 -17
  83. package/dist/{exec-client-CQV746UF.js → exec-client-7UMESIE2.js} +3 -4
  84. package/dist/governing-source-BUU4IABS.js +35 -0
  85. package/dist/guest-init-KPBQOFE5.js +9 -0
  86. package/dist/{hook-fast-R4FVSZ2H.js → hook-fast-W5MKHMJT.js} +4 -1
  87. package/dist/host-key-U4B5QETR.js +8 -0
  88. package/dist/{import-capture-cmd-EQ35EARA.js → import-capture-cmd-ZO6KGHI2.js} +7 -5
  89. package/dist/{import-ruler-cmd-BGAZXCQM.js → import-ruler-cmd-KJMWL5V6.js} +5 -3
  90. package/dist/index.js +8 -4
  91. package/dist/init-VWXIZEAN.js +54 -0
  92. package/dist/{init-PWU5YVL7.js → init-WNXXYMO7.js} +38 -35
  93. package/dist/{init-cmd-XAVO7HYZ.js → init-cmd-WS6TIRYT.js} +28 -24
  94. package/dist/{init-update-CVCWD6VS.js → init-update-E3J6TVIN.js} +27 -26
  95. package/dist/{install-HDBIPHTX.js → install-CBWIEF3R.js} +4 -5
  96. package/dist/{install-cmd-5HYMXLEW.js → install-cmd-QDOTR4OM.js} +5 -4
  97. package/dist/{login-browser-QZCS3WW6.js → login-browser-O63QZN7D.js} +4 -3
  98. package/dist/{logout-cmd-QAUFDJ6J.js → logout-cmd-5V5MMBSY.js} +3 -2
  99. package/dist/{logs-H55NWW4H.js → logs-AMLKC3VP.js} +3 -1
  100. package/dist/loop-UGI2BJOT.js +76 -0
  101. package/dist/machine-JOW4RNZ7.js +49 -0
  102. package/dist/{mcp-install-cmd-FDTPQQS6.js → mcp-install-cmd-GGWB3JJ2.js} +3 -2
  103. package/dist/{mcp-serve-cmd-3KVYEUXG.js → mcp-serve-cmd-DOTY5FKY.js} +10 -8
  104. package/dist/paths-7XP63OAX.js +7 -0
  105. package/dist/pending-532ABZYC.js +8 -0
  106. package/dist/{pidfile-UZRH774M.js → pidfile-3DM4OBKO.js} +4 -2
  107. package/dist/{record-C43U4PKS.js → record-3WP6V2K5.js} +3 -2
  108. package/dist/{runner-env-EN7YE7QX.js → runner-env-4NB2GA7O.js} +5 -6
  109. package/dist/{runner-key-6SLBI7PP.js → runner-key-TC3NFWFV.js} +38 -35
  110. package/dist/{session-sender-FO3LCREV.js → session-sender-NNVPSPY7.js} +17 -14
  111. package/dist/spec-cmd-OWVTBB4J.js +116 -0
  112. package/dist/{starter-pack-KF66HGZR.js → starter-pack-OPWUDOWX.js} +7 -5
  113. package/dist/{status-7VNQN5PW.js → status-TKCLLKFG.js} +42 -39
  114. package/dist/{statusline-fast-SVTMBMD7.js → statusline-fast-KN3BX67H.js} +2 -1
  115. package/dist/{sync-cmd-GVPPFZ3G.js → sync-cmd-Q6VYNPWG.js} +16 -13
  116. package/dist/{watch-23HSAEI6.js → watch-ARAGADTF.js} +16 -13
  117. package/dist/{work-claim-cmd-LKL3GHFQ.js → work-claim-cmd-34FERLYX.js} +25 -27
  118. package/dist/work-cmd-7S5B2H2P.js +25 -0
  119. package/dist/{work-comment-cmd-AWHP56M2.js → work-comment-cmd-SZJSPM5S.js} +15 -13
  120. package/dist/{work-launch-R5HTAP6X.js → work-launch-LEQ2MPM3.js} +3 -1
  121. package/dist/{work-open-pr-cmd-IOM4AVQQ.js → work-open-pr-cmd-RPXKCBWS.js} +17 -15
  122. package/dist/work-pr-cmd-5LGANL32.js +26 -0
  123. package/package.json +3 -2
  124. package/skill-pack/maintained/outerlayer-amend/SKILL.md +4 -2
  125. package/skill-pack/shipped.json +17 -2
  126. package/skill-pack/template/build/SKILL.md +23 -10
  127. package/skill-pack/template/build/references/agent-briefs.md +3 -1
  128. package/skill-pack/template/build/references/criteria-judge.md +27 -0
  129. package/skill-pack/template/build/references/release.md +19 -28
  130. package/skill-pack/template/spec/SKILL.md +35 -10
  131. package/skill-pack/validators/acceptance-criteria.yaml +25 -26
  132. package/dist/agent-setup-T624CWPD.js +0 -27
  133. package/dist/chunk-BYT5O5RV.js +0 -61
  134. package/dist/context-materialize-IV2TUUFO.js +0 -22
  135. package/dist/emit-cmd-IJ3GVRFD.js +0 -12
  136. package/dist/guest-init-LJP5GPZF.js +0 -9
  137. package/dist/host-key-YMLXY6QB.js +0 -7
  138. package/dist/init-6XPDRDTC.js +0 -51
  139. package/dist/loop-FK3LPMMZ.js +0 -73
  140. package/dist/machine-SQXEBAM2.js +0 -46
  141. package/dist/paths-OYKMVYJP.js +0 -6
  142. package/dist/pending-MXLSRYMO.js +0 -7
  143. package/dist/work-cmd-Z2WXLZPH.js +0 -23
  144. package/dist/work-pr-cmd-HNEUIEN7.js +0 -24
@@ -1,28 +1,26 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'node:module';
3
- import { createDockerHost, childEnvironment, spawnRun } from './chunk-KMDLFI4P.js';
4
- import { execCapture } from './chunk-QKFB4K7X.js';
5
- import { GUEST_CID } from './chunk-RSNSZ2LJ.js';
6
- import { WorkCommandError, postJson } from './chunk-S3NYLI2P.js';
3
+ import { createDockerHost, childEnvironment, spawnRun } from './chunk-L2USZT6T.js';
4
+ import { builderSpec, runnerLabelValue, RUNNER_LABEL, BUILD_LABEL, prepareRunArgs, containerRunArgs, HASH_ARGV, parseProgramHash, JUDGE_PROGRAM, containerExecArgs, JUDGE_DIR, MAX_REPORT_BYTES } from './chunk-S26M3YBJ.js';
5
+ import { execCapture } from './chunk-GGOMT6KF.js';
6
+ import { GUEST_CID } from './chunk-X5H2WYMB.js';
7
+ import { describeCallFailure, controlPlaneTokenFailure, refusalCode, resolveGoverningSource, removeStagedContext, stageGovernedContext, pinnedSourceOf, commandWithShim, sharedSkillNames, shimScript } from './chunk-6CHIYEZZ.js';
8
+ import { WorkCommandError } from './chunk-KK4VBOH6.js';
7
9
  import { hostAllowed } from './chunk-UXYLWVTQ.js';
8
- import { readDefaultBranchHead, cloneRepository, buildRepositoryImage } from './chunk-R7PF5OOU.js';
9
- import { RecipeError } from './chunk-QOZKPLVJ.js';
10
- import { startTimeOf, isSameProcess } from './chunk-N5FOE5PS.js';
11
- import { currentCliBuild } from './chunk-OGFGZQA3.js';
12
- import { builderSpec, runnerLabelValue, RUNNER_LABEL, BUILD_LABEL, prepareRunArgs, containerRunArgs, containerExecArgs } from './chunk-4NPZVMJU.js';
13
10
  import { parseDuKilobytes } from './chunk-7RQ73DLO.js';
14
- import { repositoriesOf, primaryOf, RELAY_PORT, CHECKOUT_DIR, JOB_MOUNT, claudeCredential, buildEnvironment, startBuildServices, checkEnvironment, AGENT_CREDENTIAL_MISSING, checkoutPath } from './chunk-4Q7OMKAT.js';
11
+ import { readDefaultBranchHead, cloneRepository, buildRepositoryImage } from './chunk-3CWUOFV6.js';
12
+ import { RecipeError } from './chunk-QOZKPLVJ.js';
15
13
  import { CLAUDE_CODE_VERSION, firecrackerArch, MICROVM_ARCHES, DEFAULT_BASE_IMAGE, GUEST_HELPER_SHA256, GUEST_ASSETS_BASE_URL, GUEST_KERNEL_SHA256, FIRECRACKER_BINARY_SHA256, FIRECRACKER_SHA256, FIRECRACKER_VERSION, GUEST_ASSETS_VERSION } from './chunk-CX22S5BA.js';
16
- import { setCustomScrubbers, setLocalIdentity, setRepoRoot, parseTranscript, scrubDeep, scrubText } from './chunk-JXYVA45C.js';
17
- import { defaultGitRunner, materializeContext } from './chunk-YZ5F4PIS.js';
18
- import { readContextManifest, readSourceCache } from './chunk-WTE3MQML.js';
19
- import { signingFetchFor } from './chunk-YO4PSLVB.js';
20
- import { orderSessions, extractFacts, sessionsToTimeline } from './chunk-OHVJA253.js';
21
- import { downconvertSession } from './chunk-J5M7BSJQ.js';
22
- import { signerFor } from './chunk-V7NIVOFD.js';
23
- import { rootfsPathFor, diskTemplatesDir, RUNNER_FILE_MODE, stagedContextDir, contextHomeDir, ghConfigDir, runnerDirOfBuild, spoolDir, mirrorDir, buildDir, RUNNER_DIR_MODE, vmAssetsDir } from './chunk-ZM2IMMYN.js';
24
- import { fstatSync, readFileSync, existsSync, mkdirSync, chmodSync, truncateSync, rmSync, writeFileSync, accessSync, constants, readdirSync, statSync, rmdirSync, renameSync, lstatSync, unlinkSync, openSync, closeSync, mkdtempSync, createWriteStream, createReadStream } from 'fs';
25
- import { relative, join, dirname, basename } from 'path';
14
+ import { startTimeOf, isSameProcess } from './chunk-JBJTC3UU.js';
15
+ import { currentCliBuild } from './chunk-OGFGZQA3.js';
16
+ import { setCustomScrubbers, setLocalIdentity, setRepoRoot, parseTranscript, scrubDeep, scrubText } from './chunk-BMYGAGBJ.js';
17
+ import { orderSessions, extractFacts, sessionsToTimeline } from './chunk-X5N6KC6V.js';
18
+ import { downconvertSession } from './chunk-N3CUDFCW.js';
19
+ import { rootfsPathFor, diskTemplatesDir, RUNNER_FILE_MODE, deliveryDir, ghConfigDir, runnerDirOfBuild, spoolDir, mirrorDir, buildDir, RUNNER_DIR_MODE, SHIM_DIR, vmAssetsDir } from './chunk-N2PXGQRL.js';
20
+ import { repositoriesOf, primaryOf, RELAY_PORT, repositoryPath, CHECKOUT_DIR, lifecycleWorkdir, claudeCredential, buildEnvironment, startBuildServices, checkEnvironment, AGENT_CREDENTIAL_MISSING, checkoutPath, JOB_MOUNT } from './chunk-BQXFBI7T.js';
21
+ import { versionAtLeast, MINIMUM_CLAUDE_VERSION } from './chunk-G2YNJIGF.js';
22
+ import { readFileSync, existsSync, mkdirSync, chmodSync, truncateSync, rmSync, writeFileSync, readdirSync, lstatSync, copyFileSync, accessSync, constants, statSync, rmdirSync, renameSync, unlinkSync, openSync, closeSync, mkdtempSync, createWriteStream, createReadStream } from 'fs';
23
+ import { join, dirname, basename } from 'path';
26
24
  import { spawnSync, spawn } from 'child_process';
27
25
  import { availableParallelism, networkInterfaces } from 'os';
28
26
  import { randomBytes, createHash } from 'crypto';
@@ -35,294 +33,6 @@ import { isIP, connect, createServer as createServer$1 } from 'net';
35
33
  import { lookup } from 'dns/promises';
36
34
 
37
35
  createRequire(import.meta.url);
38
- function logStamp(at) {
39
- return at.toISOString().replace(/\.\d{3}Z$/, "Z");
40
- }
41
- function sameDestination(stat = fstatSync) {
42
- try {
43
- const out = stat(1);
44
- const err = stat(2);
45
- return out.dev === err.dev && out.ino === err.ino;
46
- } catch {
47
- return false;
48
- }
49
- }
50
- function createRunnerLog(now = () => /* @__PURE__ */ new Date(), io = { stdout: process.stdout, stderr: process.stderr, joined: () => sameDestination() }) {
51
- const write = (stream, text) => {
52
- stream.write(`${logStamp(now())} ${text}
53
- `);
54
- };
55
- return {
56
- line: (text) => write(io.stdout, text),
57
- error: (text) => {
58
- write(io.stdout, text);
59
- if (!io.joined()) write(io.stderr, text);
60
- }
61
- };
62
- }
63
- function formatDuration(ms) {
64
- const total = Math.max(0, Math.round(ms / 1e3));
65
- const hours = Math.floor(total / 3600);
66
- const minutes = Math.floor(total % 3600 / 60);
67
- const seconds = total % 60;
68
- if (hours > 0) return `${hours}h ${minutes}m`;
69
- if (minutes > 0) return `${minutes}m ${String(seconds).padStart(2, "0")}s`;
70
- return `${seconds}s`;
71
- }
72
- function formatClock(at, today) {
73
- const hhmm = `${String(at.getUTCHours()).padStart(2, "0")}:${String(at.getUTCMinutes()).padStart(2, "0")} UTC`;
74
- const sameDay = at.getUTCFullYear() === today.getUTCFullYear() && at.getUTCMonth() === today.getUTCMonth() && at.getUTCDate() === today.getUTCDate();
75
- if (sameDay) return hhmm;
76
- const month = at.toLocaleString("en-US", { month: "short", timeZone: "UTC" });
77
- return `${month} ${at.getUTCDate()} ${hhmm}`;
78
- }
79
- function shortClaim(claimId) {
80
- return claimId.length <= 8 ? claimId : `${claimId.slice(0, 8)}\u2026`;
81
- }
82
- function relativeToRunner(runnerDirPath, path) {
83
- const rel = relative(runnerDirPath, path);
84
- return rel === "" || rel.startsWith("..") ? path : rel;
85
- }
86
- function describeCallFailure(err) {
87
- if (err instanceof Error) {
88
- const status = err.status;
89
- return status === void 0 ? err.message : `${err.message} (${status})`;
90
- }
91
- return String(err);
92
- }
93
-
94
- // src/runner/retry.ts
95
- var RETRY_WINDOW_MS = 12e4;
96
- var FIRST_BACKOFF_MS = 1e3;
97
- var MAX_BACKOFF_MS = 15e3;
98
- function isRetryable(err) {
99
- if (err instanceof WorkCommandError) return err.retryable;
100
- return true;
101
- }
102
- async function retrySend(send, deps = {}) {
103
- const now = deps.now ?? (() => /* @__PURE__ */ new Date());
104
- const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
105
- const windowMs = deps.windowMs ?? RETRY_WINDOW_MS;
106
- const startedMs = now().getTime();
107
- let attempts = 0;
108
- let backoffMs = FIRST_BACKOFF_MS;
109
- for (; ; ) {
110
- attempts++;
111
- try {
112
- return { ok: true, value: await send(), attempts };
113
- } catch (err) {
114
- const reason = describeCallFailure(err);
115
- const code = err instanceof WorkCommandError ? err.code : void 0;
116
- const status = err instanceof WorkCommandError ? err.status : void 0;
117
- const message = err instanceof WorkCommandError ? err.message : void 0;
118
- if (!isRetryable(err)) return { ok: false, attempts, reason, retryable: false, code, status, message };
119
- const elapsedMs = now().getTime() - startedMs;
120
- if (elapsedMs + backoffMs >= windowMs) return { ok: false, attempts, reason, retryable: true, code, status, message };
121
- await sleep(backoffMs);
122
- backoffMs = Math.min(backoffMs * 2, MAX_BACKOFF_MS);
123
- }
124
- }
125
- }
126
-
127
- // src/runner/build-tokens.ts
128
- var ISSUE_RETRY_WINDOW_MS = 3e4;
129
- var REVOKE_TIMEOUT_MS = 15e3;
130
- function isDue(expiresAtMs, nowMs) {
131
- return nowMs >= expiresAtMs - 10 * 6e4;
132
- }
133
- function classifyRefusal(err) {
134
- if (!(err instanceof WorkCommandError)) return { kind: "retry" };
135
- if (err.status === 401 || err.code === "claim_expired" || err.code === "claim_not_held") return { kind: "lease_lost" };
136
- if (err.code === "github_unavailable") return { kind: "retry" };
137
- if (err.code === "github_request_failed") return { kind: "failed", reason: refusalCode(err) };
138
- if (err.retryable && err.code !== "repository_tokens_unavailable") return { kind: "retry" };
139
- return { kind: "failed", reason: refusalCode(err) };
140
- }
141
- function refusalCode(err) {
142
- if (!(err instanceof WorkCommandError)) return "unavailable";
143
- return err.code ?? `http_${err.status ?? "unknown"}`;
144
- }
145
- function keyOf(kind, repository) {
146
- return repository === void 0 ? kind : `${kind}:${repository}`;
147
- }
148
- var BuildTokens = class {
149
- constructor(options) {
150
- this.options = options;
151
- this.held = /* @__PURE__ */ new Map();
152
- this.asking = /* @__PURE__ */ new Map();
153
- this.issued = [];
154
- this.usedKinds = /* @__PURE__ */ new Set();
155
- this.revoked = false;
156
- this.controlPlaneRetry = false;
157
- }
158
- now() {
159
- return (this.options.now ?? (() => /* @__PURE__ */ new Date()))();
160
- }
161
- /** `repository` as the holder keys it: `undefined` for the primary, which is the one a request names no repository for. */
162
- secondary(repository) {
163
- if (repository === void 0) return void 0;
164
- const primary = this.options.primary;
165
- return primary !== void 0 && primary.toLowerCase() === repository.toLowerCase() ? void 0 : repository;
166
- }
167
- /**
168
- * The token of `kind` for `repository`, or for the primary when none is
169
- * named, asking the gateway for a new one when the build holds none or its
170
- * token is within ten minutes of expiry.
171
- */
172
- token(kind, repository) {
173
- const other = this.secondary(repository);
174
- const key = keyOf(kind, other);
175
- const held = this.held.get(key);
176
- if (held !== void 0 && !isDue(held.expiresAtMs, this.now().getTime())) return Promise.resolve(held.token);
177
- let asking = this.asking.get(key);
178
- if (asking === void 0) {
179
- asking = this.issueOrKeep(kind, other).finally(() => this.asking.delete(key));
180
- this.asking.set(key, asking);
181
- }
182
- return asking;
183
- }
184
- /** A due token that has not yet expired is kept when the gateway cannot issue a new one. */
185
- async issueOrKeep(kind, repository) {
186
- try {
187
- return await this.issue(kind, repository);
188
- } catch (err) {
189
- const held = this.held.get(keyOf(kind, repository));
190
- if (held !== void 0 && this.now().getTime() < held.expiresAtMs && classifyRefusal(err).kind === "retry") return held.token;
191
- throw err;
192
- }
193
- }
194
- /** Asks the gateway for a token with `body`, retrying a failure about the moment. */
195
- ask(body) {
196
- const { config, item, fetchImpl } = this.options;
197
- return retrySend(
198
- async () => {
199
- try {
200
- return await postJson(
201
- config.url,
202
- `/v1/work-items/${item}/claim/tokens`,
203
- config.apiKey,
204
- config.appId,
205
- body,
206
- fetchImpl,
207
- void 0,
208
- await signerFor(config.configPath)
209
- );
210
- } catch (err) {
211
- if (err instanceof WorkCommandError && (err.code === "repository_tokens_unavailable" || err.code === "github_request_failed")) {
212
- throw new WorkCommandError(err.message, false, err.status, err.code);
213
- }
214
- throw err;
215
- }
216
- },
217
- { now: this.options.now, sleep: this.options.sleep, windowMs: ISSUE_RETRY_WINDOW_MS }
218
- );
219
- }
220
- /**
221
- * A read-only token for the repository that governs the item's, which the
222
- * runner uses on the host to fetch its context. It is revoked with the
223
- * others when the attempt ends. It is not held or renewed: nothing uses it
224
- * after the fetch. A refusal is thrown with the gateway's code. A lost
225
- * lease is kept for `refusal()`, and a failure about the moment for
226
- * `controlPlaneUnavailable()`, as the read token's are.
227
- */
228
- async controlPlane() {
229
- const asked = await this.ask({ access: "read", target: "control_plane" });
230
- if (!asked.ok) {
231
- const err = new WorkCommandError(asked.message ?? asked.reason, asked.retryable, asked.status, asked.code);
232
- const refusal = classifyRefusal(err);
233
- if (refusal.kind === "lease_lost") this.failure = refusal;
234
- if (refusal.kind === "retry") this.controlPlaneRetry = true;
235
- throw err;
236
- }
237
- const { token, expiresAt, repository } = asked.value.data;
238
- if (typeof token !== "string" || token === "" || typeof expiresAt !== "string" || typeof repository !== "string" || repository === "") {
239
- throw new WorkCommandError("the gateway's token answer carried no token, expiry or repository", false, void 0, "malformed_token_response");
240
- }
241
- if (this.revoked) {
242
- await this.revoke([token]);
243
- throw new WorkCommandError("the build's tokens were already revoked", false, void 0, "tokens_revoked");
244
- }
245
- this.issued.push(token);
246
- return { token, expiresAt, repository };
247
- }
248
- async issue(kind, repository) {
249
- const asked = await this.ask({ access: kind, ...repository === void 0 ? {} : { repository } });
250
- let err;
251
- if (!asked.ok) {
252
- err = new WorkCommandError(asked.message ?? asked.reason, asked.retryable, asked.status, asked.code);
253
- } else {
254
- const { token, expiresAt } = asked.value.data;
255
- const expiresAtMs = typeof expiresAt === "string" ? Date.parse(expiresAt) : Number.NaN;
256
- if (typeof token === "string" && token !== "" && Number.isFinite(expiresAtMs)) {
257
- if (this.revoked) {
258
- await this.revoke([token]);
259
- throw new WorkCommandError("the build's tokens were already revoked", false, void 0, "tokens_revoked");
260
- }
261
- this.held.set(keyOf(kind, repository), { token, expiresAtMs });
262
- this.issued.push(token);
263
- if (kind === "read" && repository === void 0) this.readSink?.(token);
264
- return token;
265
- }
266
- err = new WorkCommandError("the gateway's token answer carried no token or no expiry", false, void 0, "malformed_token_response");
267
- }
268
- const refusal = classifyRefusal(err);
269
- if (refusal.kind !== "retry") this.failure = refusal;
270
- throw err;
271
- }
272
- /** Asks for a new read token when the build holds one within ten minutes of expiry. A refusal is kept for `refusal()`, not thrown. */
273
- async renewReadIfDue() {
274
- const held = this.held.get("read");
275
- if (held === void 0 || !isDue(held.expiresAtMs, this.now().getTime())) return;
276
- await this.token("read").catch(() => void 0);
277
- }
278
- /** Calls `sink` with the read token now, and again with each one that replaces it. */
279
- onRead(sink) {
280
- this.readSink = sink;
281
- const held = this.held.get("read");
282
- if (held !== void 0) sink(held.token);
283
- }
284
- markUsed(kind) {
285
- if (this.usedKinds.has(kind)) return;
286
- this.usedKinds.add(kind);
287
- this.options.onUsed?.(this.used());
288
- }
289
- /** The kinds of token the build has used, read before push. */
290
- used() {
291
- return ["read", "push"].filter((kind) => this.usedKinds.has(kind));
292
- }
293
- /** The last refusal that ends the build, if a renewal or a push's token request met one. */
294
- refusal() {
295
- return this.failure;
296
- }
297
- /** Whether a control plane token request failed for a reason about the moment, which a later attempt may not meet. */
298
- controlPlaneUnavailable() {
299
- return this.controlPlaneRetry;
300
- }
301
- /** Revokes every token this holder was issued, the superseded ones too, at GitHub with the token itself. A failure is not an error: the token lapses within the hour. */
302
- async revokeAll() {
303
- this.revoked = true;
304
- const tokens = this.issued.splice(0);
305
- this.held.clear();
306
- await this.revoke(tokens);
307
- }
308
- async revoke(tokens) {
309
- const apiUrl = this.options.apiUrl ?? "https://api.github.com";
310
- await Promise.allSettled(
311
- tokens.map(
312
- (token) => this.options.fetchImpl(`${apiUrl}/installation/token`, {
313
- method: "DELETE",
314
- headers: {
315
- authorization: `Bearer ${token}`,
316
- accept: "application/vnd.github+json",
317
- "x-github-api-version": "2022-11-28",
318
- "user-agent": "outerlayer-runner"
319
- },
320
- signal: AbortSignal.timeout(REVOKE_TIMEOUT_MS)
321
- })
322
- )
323
- );
324
- }
325
- };
326
36
  var BUILD_PREFIX = "build-";
327
37
  var LEAF = "runner";
328
38
  function serviceDirectory(selfCgroup, mount) {
@@ -685,6 +395,60 @@ async function ensureAssets(options) {
685
395
  const helper = await ensureFile(dir, `outerlayer-guest-${arch}`, pins.helper, fetchImpl, 493);
686
396
  return { firecracker, kernel, helper, helperSha256: pins.helper.sha256 };
687
397
  }
398
+
399
+ // src/runner/container/docker.ts
400
+ function parseImageConfig(json) {
401
+ const config = JSON.parse(json);
402
+ const env = {};
403
+ for (const entry of config.Env ?? []) {
404
+ const eq = entry.indexOf("=");
405
+ if (eq > 0) env[entry.slice(0, eq)] = entry.slice(eq + 1);
406
+ }
407
+ return { user: config.User ?? "", env };
408
+ }
409
+ function createContainerHost(deps = {}) {
410
+ const run = deps.run ?? spawnRun;
411
+ return {
412
+ async version() {
413
+ const result = await run("docker", ["version", "--format", "{{.Server.Version}}"]);
414
+ return result.status === 0 && result.stdout.trim() !== "" ? result.stdout.trim() : null;
415
+ },
416
+ async operatingSystem() {
417
+ const result = await run("docker", ["info", "--format", "{{.OperatingSystem}}"]);
418
+ return result.status === 0 && result.stdout.trim() !== "" ? result.stdout.trim() : null;
419
+ },
420
+ async imageConfig(imageId) {
421
+ const result = await run("docker", ["image", "inspect", "--format", "{{json .Config}}", imageId]);
422
+ return result.status === 0 ? parseImageConfig(result.stdout) : { user: "", env: {} };
423
+ },
424
+ createVolume(name, labels) {
425
+ const flags = Object.entries(labels).flatMap(([key, value]) => ["--label", `${key}=${value}`]);
426
+ return run("docker", ["volume", "create", ...flags, name]);
427
+ },
428
+ removeVolume(name) {
429
+ return run("docker", ["volume", "rm", "--force", name]);
430
+ },
431
+ run(args, opts) {
432
+ return run("docker", ["run", ...args], opts);
433
+ },
434
+ exec(input, opts) {
435
+ const env = {};
436
+ for (const [name, value] of Object.entries(input.env.passed)) if (value !== void 0) env[name] = value;
437
+ return run("docker", ["exec", ...containerExecArgs(input)], { ...opts, env });
438
+ },
439
+ stop(container) {
440
+ return run("docker", ["stop", container]);
441
+ },
442
+ start(container) {
443
+ return run("docker", ["start", container]);
444
+ },
445
+ remove(container) {
446
+ return run("docker", ["rm", "--force", "--volumes", container]);
447
+ }
448
+ };
449
+ }
450
+
451
+ // src/runner/microvm/disks.ts
688
452
  function converterScript() {
689
453
  return [
690
454
  "set -eu",
@@ -751,13 +515,13 @@ function exportInto(container, converter) {
751
515
  exporter.once("error", reject);
752
516
  writer.once("error", reject);
753
517
  let exits = 0;
754
- let failure;
518
+ let failure2;
755
519
  const done = (name, code) => {
756
- if (code !== 0 && failure === void 0) failure = `${name} exited ${code}`;
520
+ if (code !== 0 && failure2 === void 0) failure2 = `${name} exited ${code}`;
757
521
  exits += 1;
758
522
  if (exits < 2) return;
759
- if (failure === void 0) resolve();
760
- else reject(new Error(`${failure}: ${firstLine2(errors.join("\n"))}`));
523
+ if (failure2 === void 0) resolve();
524
+ else reject(new Error(`${failure2}: ${firstLine2(errors.join("\n"))}`));
761
525
  };
762
526
  exporter.once("exit", (code) => done("docker export", code));
763
527
  writer.once("exit", (code) => done("the converter", code));
@@ -783,13 +547,7 @@ function createRootfsHost() {
783
547
  async imageConfig(imageId) {
784
548
  const inspected = await spawnRun("docker", ["image", "inspect", "--format", "{{json .Config}}", imageId], { env: env() });
785
549
  if (inspected.status !== 0) throw new Error(`docker image inspect ${imageId} failed: ${firstLine2(inspected.stderr)}`);
786
- const config = JSON.parse(inspected.stdout);
787
- const variables = {};
788
- for (const entry of config.Env ?? []) {
789
- const eq = entry.indexOf("=");
790
- if (eq > 0) variables[entry.slice(0, eq)] = entry.slice(eq + 1);
791
- }
792
- return { env: variables, user: config.User ?? "" };
550
+ return parseImageConfig(inspected.stdout);
793
551
  }
794
552
  };
795
553
  }
@@ -940,8 +698,6 @@ function createMicroVmPorts(over = {}) {
940
698
  exec: over.exec ?? execCapture
941
699
  };
942
700
  }
943
-
944
- // src/runner/build-steps.ts
945
701
  var aborted = () => ({ ok: false, reason: "", aborted: true });
946
702
  function firstLine3(text) {
947
703
  return text.trim().split("\n")[0] ?? "";
@@ -950,14 +706,43 @@ function gitErrorLine(text) {
950
706
  const lines = text.split("\n").map((line) => line.trim()).filter((line) => line !== "");
951
707
  return lines.find((line) => /^(fatal|error):/.test(line)) ?? lines.at(-1) ?? "";
952
708
  }
953
- function adoptionFailureReason(result) {
954
- const line = result.stdout.trim().split("\n").at(-1) ?? "";
955
- try {
956
- const answer = JSON.parse(line);
957
- if (typeof answer.code === "string" && typeof answer.message === "string") return `${answer.code}: ${answer.message}`;
958
- } catch {
959
- }
960
- return `context_adoption_failed: adopting the control plane's context failed (exit ${result.status})${firstLine3(result.stderr) === "" ? "" : `: ${firstLine3(result.stderr)}`}`;
709
+ var STAGED_TREE = `${JOB_MOUNT}/context/tree`;
710
+ var DELIVER_SCRIPT = [
711
+ "set -e",
712
+ 'src="$1"; dest="$2"',
713
+ 'if [ -e "$dest" ]; then chmod -R u+w "$dest"; rm -rf "$dest"; fi',
714
+ 'mkdir -p "$dest"',
715
+ "for entry in AGENTS.md CLAUDE.md settings.json .claude .outerlayer; do",
716
+ ' if [ -e "$src/$entry" ]; then cp -R "$src/$entry" "$dest/"; fi',
717
+ "done",
718
+ 'chmod -R a-w "$dest"'
719
+ ].join("\n");
720
+ var RECORD_SCRIPT = 'd="$(git rev-parse --git-common-dir)" && mkdir -p "$d/outerlayer" && printf "%s" "$1" > "$d/outerlayer/delivery.json"';
721
+ var WRITE_SHIM_SCRIPT = 'mkdir -p "$1" && printf "%s" "$2" > "$1/claude" && chmod 755 "$1/claude"';
722
+ function failure(what, result) {
723
+ return `context_delivery_failed: ${what} (exit ${result.status})${firstLine3(result.stderr) === "" ? "" : `: ${firstLine3(result.stderr)}`}`;
724
+ }
725
+ async function deliveryRefusal(input, staged) {
726
+ if (input.agentVersion !== void 0 && !versionAtLeast(input.agentVersion)) {
727
+ return `context_agent_too_old: the build image's Claude Code is ${input.agentVersion}, and a governed build needs ${MINIMUM_CLAUDE_VERSION} or later to load its control plane's instructions; rebuild the image`;
728
+ }
729
+ const own = await input.exec(["ls", "-1", ".claude/skills"], CHECKOUT_DIR, { quiet: true });
730
+ if (own.status !== 0) return void 0;
731
+ const ownNames = new Set(own.stdout.split("\n").map((name) => name.trim()));
732
+ const clash = sharedSkillNames(join(staged.dir, "tree")).find((name) => ownNames.has(name));
733
+ if (clash === void 0) return void 0;
734
+ return `context_skill_conflict: .claude/skills/${clash} in ${input.repository} has the same name as the shared skill ${clash} from ${staged.repository}, and would replace it in this build; rename or remove it from the repository`;
735
+ }
736
+ async function deliver(input, staged) {
737
+ const dir = deliveryDir(staged.repository, staged.sha);
738
+ const copied = await input.exec(["sh", "-c", DELIVER_SCRIPT, "deliver", STAGED_TREE, dir], "/build/work");
739
+ if (copied.status !== 0) return { ok: false, reason: failure("copying the control plane into the build failed", copied) };
740
+ const shim = await input.exec(["sh", "-c", WRITE_SHIM_SCRIPT, "shim", SHIM_DIR, shimScript()], "/build/work");
741
+ if (shim.status !== 0) return { ok: false, reason: failure("writing the claude shim failed", shim) };
742
+ const record = JSON.stringify({ schemaVersion: 1, repository: staged.repository, ref: staged.ref, sha: staged.sha, dir });
743
+ const recorded = await input.exec(["sh", "-c", RECORD_SCRIPT, "record", record], CHECKOUT_DIR);
744
+ if (recorded.status !== 0) return { ok: false, reason: failure("recording the delivery failed", recorded) };
745
+ return { ok: true };
961
746
  }
962
747
  async function checkOut(input, entry) {
963
748
  const { exec, log, tag } = input;
@@ -983,6 +768,18 @@ async function checkOut(input, entry) {
983
768
  throw err;
984
769
  }
985
770
  }
771
+ var DEVCONTAINER_FILES = [".devcontainer/devcontainer.json", ".devcontainer.json"];
772
+ async function noteIgnoredDevcontainers(input, repositories, governing) {
773
+ for (const entry of repositories) {
774
+ if (entry.repository.toLowerCase() === governing.toLowerCase()) continue;
775
+ for (const file of DEVCONTAINER_FILES) {
776
+ const probe = await input.exec(["test", "-f", file], checkoutPath(entry), { quiet: true });
777
+ if (probe.status !== 0) continue;
778
+ input.log.line(`${input.tag} ${entry.repository} has its own ${file}; it is ignored, and the control plane's ${governing} devcontainer is used instead`);
779
+ break;
780
+ }
781
+ }
782
+ }
986
783
  async function runBuildSteps(input) {
987
784
  const { exec, log, tag, repository } = input;
988
785
  let startCommit = "";
@@ -993,33 +790,44 @@ async function runBuildSteps(input) {
993
790
  const first = await checkOut(input, { ...primary, primary: true });
994
791
  if (!first.ok) return first;
995
792
  startCommit = first.startCommit;
793
+ if (repositories.length > 1) {
794
+ const linkPath = repositoryPath(primary.repository);
795
+ const linked = await exec(["sh", "-c", 'mkdir -p "$(dirname "$1")" && ln -s "$2" "$1"', "link", linkPath, CHECKOUT_DIR], "/build/work");
796
+ if (linked.status !== 0) return { ok: false, reason: `linking ${primary.repository} at ${linkPath} failed (exit ${linked.status})` };
797
+ }
996
798
  for (const entry of repositories.filter((candidate) => candidate.repository !== primary.repository)) {
997
799
  if (input.aborted()) return aborted();
998
800
  const checked = await checkOut(input, { ...entry, primary: false });
999
801
  if (!checked.ok) return checked;
1000
802
  }
803
+ if (input.governing !== void 0) await noteIgnoredDevcontainers(input, repositories, input.governing.repository);
804
+ const lifecycleDir = lifecycleWorkdir(repositories, input.governing?.repository);
1001
805
  for (const step of lifecycleSteps(input.lifecycle)) {
1002
806
  if (input.aborted()) return aborted();
1003
807
  log.line(`${tag} lifecycle ${step.name} (${step.source})`);
1004
- const ran = await exec(step.argv, CHECKOUT_DIR);
808
+ const ran = await exec(step.argv, lifecycleDir);
1005
809
  if (ran.status !== 0) return { ok: false, reason: lifecycleFailureReason(step, ran.status) };
1006
810
  }
1007
811
  if (input.aborted()) return aborted();
1008
812
  const staged = await input.stageContext();
1009
813
  if (staged.kind === "refused") return { ok: false, reason: staged.reason };
814
+ let controlPlane;
1010
815
  if (staged.kind === "staged") {
1011
816
  try {
1012
- const delivered = await input.deliverContext?.(staged) ?? { ok: true };
1013
- if (!delivered.ok) return { ok: false, reason: delivered.reason };
1014
- const adopted = await exec(["outerlayer", "context", "adopt", `${JOB_MOUNT}/context`, "--json"], CHECKOUT_DIR);
1015
- if (adopted.status !== 0) return { ok: false, reason: adoptionFailureReason(adopted) };
1016
- log.line(`${tag} governed context ${staged.sha.slice(0, 7)} adopted (${staged.files.length} files)`);
817
+ const refusal = await deliveryRefusal(input, staged);
818
+ if (refusal !== void 0) return { ok: false, reason: refusal };
819
+ const handed = await input.deliverContext?.(staged) ?? { ok: true };
820
+ if (!handed.ok) return { ok: false, reason: handed.reason };
821
+ const delivered = await deliver(input, staged);
822
+ if (!delivered.ok) return delivered;
823
+ controlPlane = { repository: staged.repository, commit: staged.sha };
824
+ log.line(`${tag} governed context ${staged.sha.slice(0, 7)} delivered to ${deliveryDir(staged.repository, staged.sha)} (${staged.files.length} files)`);
1017
825
  } finally {
1018
826
  input.removeStagedContext();
1019
827
  }
1020
828
  if (input.aborted()) return aborted();
1021
829
  }
1022
- return { ok: true, startCommit };
830
+ return { ok: true, startCommit, ...controlPlane === void 0 ? {} : { controlPlane } };
1023
831
  }
1024
832
  var EVAL_INPUT_NAME = "eval-input";
1025
833
  var TRANSCRIPT_ROOT = "/build/home/.claude/projects";
@@ -1214,6 +1022,28 @@ async function writeEvalInput(request) {
1214
1022
  writeReadOnly(join(dir, "activity.json"), `${JSON.stringify(activity, null, 2)}
1215
1023
  `);
1216
1024
  }
1025
+ var MAX_TREE_FILES = 2e4;
1026
+ var MAX_TREE_FILE_BYTES = 1024 * 1024;
1027
+ function copyJudgeTree(from, to, limits2 = { files: MAX_TREE_FILES, bytes: MAX_TREE_FILE_BYTES }) {
1028
+ let copied = 0;
1029
+ const walk = (src, dest, top) => {
1030
+ mkdirSync(dest);
1031
+ chmodSync(dest, 493);
1032
+ for (const name of readdirSync(src).sort()) {
1033
+ if (copied >= limits2.files) return;
1034
+ if (top && name === ".git") continue;
1035
+ const path = join(src, name);
1036
+ const stat = lstatSync(path);
1037
+ if (stat.isDirectory()) walk(path, join(dest, name), false);
1038
+ else if (stat.isFile() && stat.size <= limits2.bytes) {
1039
+ copyFileSync(path, join(dest, name));
1040
+ chmodSync(join(dest, name), 420);
1041
+ copied += 1;
1042
+ }
1043
+ }
1044
+ };
1045
+ walk(from, to, true);
1046
+ }
1217
1047
 
1218
1048
  // src/runner/container/agent-user.ts
1219
1049
  var ADDED_USER = "ol-agent";
@@ -1228,189 +1058,6 @@ async function resolveAgentUser(input) {
1228
1058
  if (uid === null) throw new AgentUserError(`the user "${candidate}" is not in the image, so no command can run as it`);
1229
1059
  return uid === 0 ? ADDED_USER : candidate;
1230
1060
  }
1231
- var TokenRefused = class extends Error {
1232
- constructor(code, detail) {
1233
- super(code);
1234
- this.code = code;
1235
- this.detail = detail;
1236
- }
1237
- };
1238
- var NoControlPlane = class extends Error {
1239
- };
1240
- var GatewayConflict = class extends Error {
1241
- };
1242
- var unavailable = (why) => ({ kind: "refused", reason: `context_unavailable: ${why}` });
1243
- var DESIGNATION_LOOKUP = { timeoutMs: 1e4, attempts: 2 };
1244
- function lookupFailureReason(failure, repository) {
1245
- switch (failure?.kind) {
1246
- case "timeout":
1247
- return `the gateway did not answer which repository governs ${repository} within ${failure.timeoutMs / 1e3} seconds, twice`;
1248
- case "status":
1249
- return `the gateway answered ${failure.status} when asked which repository governs ${repository}`;
1250
- case "network":
1251
- return `the request asking which repository governs ${repository} failed: ${failure.message}`;
1252
- default:
1253
- return `the gateway did not say which repository governs ${repository}`;
1254
- }
1255
- }
1256
- function makeReadable(tree, dir, files) {
1257
- chmodSync(dir, 493);
1258
- chmodSync(tree, 493);
1259
- const directories = /* @__PURE__ */ new Set();
1260
- for (const rel of files) {
1261
- for (let current = dirname(join(tree, rel)); current !== tree; current = dirname(current)) directories.add(current);
1262
- chmodSync(join(tree, rel), (lstatSync(join(tree, rel)).mode & 73) !== 0 ? 493 : 420);
1263
- }
1264
- for (const directory of directories) chmodSync(directory, 493);
1265
- }
1266
- async function stageGovernedContext(input) {
1267
- const git2 = input.git ?? defaultGitRunner;
1268
- const dir = stagedContextDir(input.buildDir);
1269
- const tree = join(dir, "tree");
1270
- const finish = (outcome) => {
1271
- removeStagedContext(input.buildDir);
1272
- return outcome;
1273
- };
1274
- try {
1275
- mkdirSync(tree, { recursive: true });
1276
- for (const args of [
1277
- ["init", "-q", "--initial-branch=main", tree],
1278
- ["-C", tree, "remote", "add", "origin", `https://github.com/${input.repository}.git`]
1279
- ]) {
1280
- const done = git2(args, { cwd: dir, timeoutMs: 1e4 });
1281
- if (!done.ok) return finish(unavailable(`the staging directory could not be prepared: ${done.stderr.trim().split("\n")[0] ?? ""}`));
1282
- }
1283
- const { url, apiKey, appId, configPath } = input.config;
1284
- let tokenRepository;
1285
- const credentialFor = (repository) => tokenRepository !== void 0 && tokenRepository.toLowerCase() === repository.toLowerCase() ? "the token the gateway issued" : "the host's git login";
1286
- const unreadable = (repositories) => repositories.map((repository) => `${credentialFor(repository)} cannot read ${repository}`).join(" and ");
1287
- const result = await materializeContext({
1288
- cwd: tree,
1289
- sessionId: `build-${input.buildId}`,
1290
- home: contextHomeDir(configPath),
1291
- cloud: { url, apiKey, appId },
1292
- fetchImpl: signingFetchFor(input.fetchImpl, configPath, apiKey),
1293
- designationLookup: DESIGNATION_LOOKUP,
1294
- git: git2,
1295
- now: input.now,
1296
- resolveControlPlaneUrl: (repository) => input.controlPlaneUrl(repository),
1297
- controlPlaneAccess: input.controlPlaneToken === void 0 ? void 0 : async (candidates) => {
1298
- const access = await controlPlaneAccess(input.controlPlaneToken, candidates, input.repository, input.log, input.tag);
1299
- tokenRepository = access?.repository;
1300
- return access;
1301
- }
1302
- });
1303
- switch (result.status) {
1304
- case "materialized": {
1305
- if (result.unfetched !== void 0 && result.unfetched.length > 0) {
1306
- return finish(unavailable(`${unreadable(result.unfetched)}, a control plane named for ${input.repository}, so it cannot be ruled out as a second claimant`));
1307
- }
1308
- const manifest = readContextManifest(join(tree, ".git"));
1309
- const source = readSourceCache(join(tree, ".git"));
1310
- if (result.sha === null || manifest === void 0 || source?.status !== "governed" || source.repository === void 0 || source.ref === void 0) {
1311
- return finish(unavailable(`the context of ${input.repository} was materialized without a record of it`));
1312
- }
1313
- writeFileSync(
1314
- join(dir, "staged.json"),
1315
- JSON.stringify({ schemaVersion: 1, sha: result.sha, repository: source.repository, ref: source.ref, files: manifest.files }) + "\n"
1316
- );
1317
- makeReadable(tree, dir, manifest.files);
1318
- chmodSync(join(dir, "staged.json"), 420);
1319
- return { kind: "staged", dir, sha: result.sha, files: manifest.files };
1320
- }
1321
- case "self":
1322
- if (result.unfetched !== void 0 && result.unfetched.length > 0) {
1323
- return finish(unavailable(`${unreadable(result.unfetched)}, the control plane named for ${input.repository}`));
1324
- }
1325
- if (result.designation === "answered") return finish({ kind: "none" });
1326
- if (result.authRejected === true) {
1327
- return finish(unavailable(`the gateway rejected the runner's API key (401/403) when asked which repository governs ${input.repository}`));
1328
- }
1329
- return finish(unavailable(lookupFailureReason(result.lookupFailure, input.repository)));
1330
- case "conflict":
1331
- return finish({ kind: "refused", reason: `context_conflict: ${result.message ?? "two control planes claim this repository"}` });
1332
- default:
1333
- return finish(unavailable(result.message ?? `the context of ${input.repository} could not be materialized (${result.status})`));
1334
- }
1335
- } catch (err) {
1336
- if (err instanceof NoControlPlane) return finish({ kind: "none" });
1337
- if (err instanceof GatewayConflict) return finish({ kind: "refused", reason: `context_conflict: ${err.message}` });
1338
- if (err instanceof TokenRefused) {
1339
- return finish(
1340
- unavailable(
1341
- err.detail === void 0 ? `the gateway refused a token for the control plane named for ${input.repository} (${err.code})` : `the token request for the control plane named for ${input.repository} failed: ${err.detail}`
1342
- )
1343
- );
1344
- }
1345
- return finish(unavailable(`the context of ${input.repository} could not be staged: ${err instanceof Error ? err.message : String(err)}`));
1346
- }
1347
- }
1348
- async function controlPlaneAccess(ask, candidates, repository, log, tag) {
1349
- let answer;
1350
- try {
1351
- answer = await ask();
1352
- } catch (err) {
1353
- const code = refusalCode(err);
1354
- if (code === "repository_tokens_unavailable") return void 0;
1355
- if (code === "no_control_plane") throw new NoControlPlane(code);
1356
- if (code === "context_conflict") throw new GatewayConflict(err instanceof Error ? err.message : code);
1357
- throw new TokenRefused(code, err instanceof WorkCommandError && err.code === void 0 && err.status === void 0 ? describeCallFailure(err) : void 0);
1358
- }
1359
- const named = candidates.some((candidate) => candidate.repository.toLowerCase() === answer.repository.toLowerCase());
1360
- if (!named) {
1361
- log?.line(
1362
- `${tag === void 0 ? "" : `${tag} `}the gateway issued a control plane token for ${answer.repository}, which is none of the repositories named for ${repository} (${candidates.map((candidate) => candidate.repository).join(", ")}) \xB7 fetching with the host's git login`
1363
- );
1364
- return void 0;
1365
- }
1366
- return answer;
1367
- }
1368
- function removeStagedContext(buildDir2) {
1369
- rmSync(stagedContextDir(buildDir2), { recursive: true, force: true });
1370
- }
1371
-
1372
- // src/runner/container/docker.ts
1373
- function createContainerHost(deps = {}) {
1374
- const run = deps.run ?? spawnRun;
1375
- return {
1376
- async version() {
1377
- const result = await run("docker", ["version", "--format", "{{.Server.Version}}"]);
1378
- return result.status === 0 && result.stdout.trim() !== "" ? result.stdout.trim() : null;
1379
- },
1380
- async operatingSystem() {
1381
- const result = await run("docker", ["info", "--format", "{{.OperatingSystem}}"]);
1382
- return result.status === 0 && result.stdout.trim() !== "" ? result.stdout.trim() : null;
1383
- },
1384
- async imageUser(imageId) {
1385
- const result = await run("docker", ["image", "inspect", "--format", "{{.Config.User}}", imageId]);
1386
- return result.status === 0 ? result.stdout.trim() : "";
1387
- },
1388
- createVolume(name, labels) {
1389
- const flags = Object.entries(labels).flatMap(([key, value]) => ["--label", `${key}=${value}`]);
1390
- return run("docker", ["volume", "create", ...flags, name]);
1391
- },
1392
- removeVolume(name) {
1393
- return run("docker", ["volume", "rm", "--force", name]);
1394
- },
1395
- run(args, opts) {
1396
- return run("docker", ["run", ...args], opts);
1397
- },
1398
- exec(input, opts) {
1399
- const env = {};
1400
- for (const [name, value] of Object.entries(input.env.passed)) if (value !== void 0) env[name] = value;
1401
- return run("docker", ["exec", ...containerExecArgs(input)], { ...opts, env });
1402
- },
1403
- stop(container) {
1404
- return run("docker", ["stop", container]);
1405
- },
1406
- start(container) {
1407
- return run("docker", ["start", container]);
1408
- },
1409
- remove(container) {
1410
- return run("docker", ["rm", "--force", "--volumes", container]);
1411
- }
1412
- };
1413
- }
1414
1061
  var FILE_MODE = 420;
1415
1062
  var DIR_MODE = 493;
1416
1063
  function replaceFile(path, text) {
@@ -2298,6 +1945,7 @@ function createContainerRuntime(opts = {}) {
2298
1945
  configPath: build.configPath,
2299
1946
  cli: cli ?? currentCliBuild(),
2300
1947
  readToken: build.readToken,
1948
+ pinned: build.pinned,
2301
1949
  guestHelper: build.guestHelper,
2302
1950
  log: build.log
2303
1951
  },
@@ -2368,8 +2016,9 @@ async function obtainImage(input) {
2368
2016
  if (input.aborted()) return { ok: false, reason: "ended before provision finished", aborted: true };
2369
2017
  let readToken;
2370
2018
  try {
2371
- readToken = await tokens.token("read");
2019
+ readToken = input.pinned === void 0 ? await tokens.token("read") : await controlPlaneReadToken(tokens, repository);
2372
2020
  } catch (err) {
2021
+ if (input.pinned !== void 0) return { ok: false, reason: controlPlaneTokenFailure(err, input.governedRepository ?? repository) };
2373
2022
  return {
2374
2023
  ok: false,
2375
2024
  reason: err instanceof WorkCommandError ? `the build's read token could not be had: ${refusalCode(err)}` : `the build's read token could not be had: ${describeCallFailure(err)}`
@@ -2383,6 +2032,7 @@ async function obtainImage(input) {
2383
2032
  configPath: config.configPath,
2384
2033
  buildMemory: config.build?.memory ?? "8g",
2385
2034
  readToken,
2035
+ pinned: input.pinned,
2386
2036
  guestHelper: input.guestHelper,
2387
2037
  log: (line) => log.line(`${tag} ${line}`)
2388
2038
  })
@@ -2402,6 +2052,16 @@ async function obtainImage(input) {
2402
2052
  }
2403
2053
  }
2404
2054
  }
2055
+ async function controlPlaneReadToken(tokens, controlPlane) {
2056
+ try {
2057
+ const issued = await tokens.controlPlane();
2058
+ return issued.repository.toLowerCase() === controlPlane.toLowerCase() ? issued.token : void 0;
2059
+ } catch (err) {
2060
+ const code = refusalCode(err);
2061
+ if (code === "repository_tokens_unavailable" || code === "no_control_plane" || code === "context_conflict") return void 0;
2062
+ throw err;
2063
+ }
2064
+ }
2405
2065
  async function provisionContainer(input) {
2406
2066
  const { config, runtime, item, itemKey, jobDir, log, tokens } = input;
2407
2067
  const { host } = runtime;
@@ -2421,7 +2081,23 @@ async function provisionContainer(input) {
2421
2081
  if (/docker desktop/i.test(await host.operatingSystem() ?? "")) {
2422
2082
  return fail("Docker Desktop cannot pass the tunnel's socket into a container, so container builds need Docker Engine: install it (on Windows, inside WSL2) or use builtin:process");
2423
2083
  }
2424
- const obtained = await obtainImage({ config, runtime, tokens, repository, tag, log, aborted: input.aborted, deadlineMs: input.deadlineMs, now: input.now });
2084
+ const governing = await resolveGoverningSource({ config, repository, fetchImpl: input.fetchImpl });
2085
+ if (!governing.ok) return fail(governing.reason);
2086
+ const governingSource = governing.source;
2087
+ const imageRepository = governingSource.kind === "governed" ? governingSource.repository : repository;
2088
+ const obtained = await obtainImage({
2089
+ config,
2090
+ runtime,
2091
+ tokens,
2092
+ repository: imageRepository,
2093
+ pinned: governingSource.kind === "governed" ? { commit: governingSource.sha, ref: governingSource.ref } : void 0,
2094
+ governedRepository: repository,
2095
+ tag,
2096
+ log,
2097
+ aborted: input.aborted,
2098
+ deadlineMs: input.deadlineMs,
2099
+ now: input.now
2100
+ });
2425
2101
  if (!obtained.ok) return obtained.aborted === true ? { ok: false, reason: obtained.reason, aborted: true } : fail(obtained.reason);
2426
2102
  const built = obtained.built;
2427
2103
  const { report } = built;
@@ -2438,9 +2114,10 @@ async function provisionContainer(input) {
2438
2114
  containerEnv: report.apply.containerEnv,
2439
2115
  builtIns: true,
2440
2116
  lifecycle: report.apply.lifecycle,
2117
+ ...governingSource.kind === "governed" ? { governing: { repository: governingSource.repository, ref: governingSource.ref, commit: governingSource.sha } } : {},
2441
2118
  cpuset: formatCpuList(runtime.cpus.acquire(buildId, config.build?.cpus ?? 4))
2442
2119
  };
2443
- const env = buildEnvironment({
2120
+ const envInput = {
2444
2121
  config,
2445
2122
  item: item.number,
2446
2123
  itemKey,
@@ -2449,7 +2126,8 @@ async function provisionContainer(input) {
2449
2126
  hostEnv: runtime.hostEnv,
2450
2127
  containerEnv: report.apply.containerEnv,
2451
2128
  builtIns: true
2452
- });
2129
+ };
2130
+ const env = buildEnvironment(envInput);
2453
2131
  if (env.skippedTokenVariables.length > 0) {
2454
2132
  log.line(`${tag} not passing ${env.skippedTokenVariables.join(", ")} from runner.build.variables into the build: its repository token comes from the runner`);
2455
2133
  }
@@ -2504,6 +2182,9 @@ async function provisionContainer(input) {
2504
2182
  const volume = await host.createVolume(name, { [BUILD_LABEL]: buildId, [RUNNER_LABEL]: runnerId });
2505
2183
  if (volume.status !== 0) return dockerFailed("creating the build's volume", volume);
2506
2184
  if (input.aborted()) return bail("", true);
2185
+ const judgeDir = join(jobDir, "judge");
2186
+ mkdirSync(judgeDir, { recursive: true });
2187
+ chmodSync(judgeDir, 493);
2507
2188
  const runInput = {
2508
2189
  buildId,
2509
2190
  runnerId,
@@ -2514,7 +2195,8 @@ async function provisionContainer(input) {
2514
2195
  user: ADDED_USER,
2515
2196
  limits: limits2,
2516
2197
  destinations: config.build?.destinations?.length ?? 0,
2517
- builtIns: true
2198
+ builtIns: true,
2199
+ judgeDir
2518
2200
  };
2519
2201
  const prepared = await host.run(prepareRunArgs(runInput), { logPath });
2520
2202
  if (prepared.status !== 0) return dockerFailed("preparing the build's volume", prepared);
@@ -2522,12 +2204,13 @@ async function provisionContainer(input) {
2522
2204
  if (started.status !== 0) return dockerFailed("starting the build's container", started);
2523
2205
  if (input.aborted()) return bail("", true);
2524
2206
  if (!await waitForRelay(runtime, state)) return bail("the relay in the build's container did not start");
2525
- const imageUser = await host.imageUser(report.imageId);
2207
+ const imageConfig = await host.imageConfig(report.imageId);
2208
+ state.imageEnv = imageConfig.env;
2526
2209
  let agentUser;
2527
2210
  try {
2528
2211
  agentUser = await resolveAgentUser({
2529
2212
  remoteUser: report.apply.remoteUser,
2530
- imageUser,
2213
+ imageUser: imageConfig.user,
2531
2214
  probe: async (user) => {
2532
2215
  const probe = await host.exec({ container: name, user: ADDED_USER, env: NO_ENV, argv: ["id", "-u", user] });
2533
2216
  const uid = Number(probe.stdout);
@@ -2540,13 +2223,20 @@ async function provisionContainer(input) {
2540
2223
  }
2541
2224
  state.agentUser = agentUser;
2542
2225
  const tools = await host.exec({ container: name, user: ADDED_USER, env: NO_ENV, argv: ["cat", "/opt/outerlayer/tools.json"] });
2543
- environment.agentVersion = agentVersionOf(tools.status === 0 ? tools.stdout : "");
2226
+ const agentVersion = agentVersionOf(tools.status === 0 ? tools.stdout : "");
2227
+ environment.agentVersion = agentVersion;
2228
+ const program = await host.exec({ container: name, user: ADDED_USER, env: NO_ENV, argv: [...HASH_ARGV] });
2229
+ const hashed = program.status === 0 ? parseProgramHash(program.stdout) : void 0;
2230
+ if (hashed !== void 0) state.judgeProgram = { path: JUDGE_PROGRAM, sha256: hashed };
2544
2231
  input.save(state, environment);
2545
- const exec = (argv, workdir, opts) => host.exec({ container: name, user: agentUser, workdir, env, argv }, opts?.quiet === true ? void 0 : { logPath });
2232
+ const execEnv = buildEnvironment({ ...envInput, imageEnv: state.imageEnv });
2233
+ const exec = (argv, workdir, opts) => host.exec({ container: name, user: agentUser, workdir, env: execEnv, argv }, opts?.quiet === true ? void 0 : { logPath });
2546
2234
  const steps = await runBuildSteps({
2547
2235
  repository,
2548
2236
  branch: primary.branch,
2549
2237
  repositories,
2238
+ ...governingSource.kind === "governed" ? { governing: { repository: governingSource.repository } } : {},
2239
+ agentVersion,
2550
2240
  lifecycle: report.apply.lifecycle,
2551
2241
  exec,
2552
2242
  log,
@@ -2556,6 +2246,7 @@ async function provisionContainer(input) {
2556
2246
  stageContext: () => stageGovernedContext({
2557
2247
  config,
2558
2248
  repository,
2249
+ source: pinnedSourceOf(governingSource),
2559
2250
  buildId,
2560
2251
  buildDir: buildDir2,
2561
2252
  fetchImpl: input.fetchImpl,
@@ -2568,6 +2259,7 @@ async function provisionContainer(input) {
2568
2259
  });
2569
2260
  if (!steps.ok) return bail(steps.reason, steps.aborted === true);
2570
2261
  state.startCommit = steps.startCommit;
2262
+ if (steps.controlPlane !== void 0) environment.controlPlane = steps.controlPlane;
2571
2263
  return { ok: true, workdir: CHECKOUT_DIR, state, environment };
2572
2264
  }
2573
2265
  function agentVersionOf(toolsJson) {
@@ -2593,14 +2285,15 @@ function containerCommandLine(input) {
2593
2285
  hostEnv: input.runtime.hostEnv,
2594
2286
  containerEnv: state.containerEnv ?? {},
2595
2287
  builtIns: state.builtIns === true,
2596
- identity: input.job.commitIdentity
2288
+ identity: input.job.commitIdentity,
2289
+ imageEnv: state.imageEnv
2597
2290
  });
2598
2291
  const args = containerExecArgs({
2599
2292
  container: state.container,
2600
2293
  user: state.agentUser ?? ADDED_USER,
2601
2294
  workdir: CHECKOUT_DIR,
2602
2295
  env,
2603
- argv: ["/bin/sh", "-c", input.cmdline]
2296
+ argv: ["/bin/sh", "-c", commandWithShim(input.job, input.cmdline)]
2604
2297
  });
2605
2298
  return {
2606
2299
  line: ["docker", "exec", ...args].map(shellQuote).join(" "),
@@ -2619,7 +2312,8 @@ function containerSenderCommand(input) {
2619
2312
  hostEnv: input.runtime.hostEnv,
2620
2313
  containerEnv: state.containerEnv ?? {},
2621
2314
  builtIns: state.builtIns === true,
2622
- identity: input.job.commitIdentity
2315
+ identity: input.job.commitIdentity,
2316
+ imageEnv: state.imageEnv
2623
2317
  });
2624
2318
  const args = containerExecArgs({
2625
2319
  container: state.container,
@@ -2696,7 +2390,8 @@ async function syncInContainer(input) {
2696
2390
  hostEnv: runtime.hostEnv,
2697
2391
  containerEnv: state.containerEnv ?? {},
2698
2392
  builtIns: state.builtIns === true,
2699
- identity: job.commitIdentity
2393
+ identity: job.commitIdentity,
2394
+ imageEnv: state.imageEnv
2700
2395
  });
2701
2396
  const result = await runtime.host.exec(
2702
2397
  { container: state.container, user: state.agentUser ?? ADDED_USER, workdir: job.workdir ?? CHECKOUT_DIR, env, argv: ["outerlayer", "sync"] },
@@ -2754,6 +2449,53 @@ async function cleanupContainer(runtime, state, jobDir) {
2754
2449
  runtime.cpus.release(state.buildId);
2755
2450
  if (failures.length > 0) throw new Error(failures.join("; "));
2756
2451
  }
2452
+ function containerJudgeEnvironment(input) {
2453
+ const { runtime, job } = input;
2454
+ const state = job.container;
2455
+ const user = state.agentUser ?? ADDED_USER;
2456
+ const folder = join(input.jobDir, "judge");
2457
+ return {
2458
+ stop: async () => {
2459
+ if (input.config !== void 0) await ensureTunnel(runtime, state, input.jobDir, input.config, job.itemKey, job.branch, job.repositories);
2460
+ await stopContainer(runtime, state);
2461
+ const started = await runtime.host.start(state.container);
2462
+ if (started.status !== 0) throw new Error(`the build's container could not be started again (exit ${started.status})`);
2463
+ state.stopped = false;
2464
+ if (!await waitForRelay(runtime, state)) throw new Error("the relay in the build's container did not start again");
2465
+ },
2466
+ hashProgram: async () => {
2467
+ const ran = await runtime.host.exec({ container: state.container, user: ADDED_USER, env: NO_ENV, argv: [...HASH_ARGV] });
2468
+ const sha256 = ran.status === 0 ? parseProgramHash(ran.stdout) : void 0;
2469
+ return sha256 === void 0 ? { ok: false, reason: firstLine5(ran.stderr) || `the hash could not be taken (exit ${ran.status})` } : { ok: true, sha256 };
2470
+ },
2471
+ prepare: async (files, tree) => {
2472
+ mkdirSync(folder, { recursive: true });
2473
+ for (const entry of readdirSync(folder)) rmSync(join(folder, entry), { recursive: true, force: true });
2474
+ mkdirSync(join(folder, "home", "tmp"), { recursive: true });
2475
+ for (const [fileName, text] of Object.entries(files)) writeFileSync(join(folder, fileName), text, { mode: 420 });
2476
+ if (tree !== void 0) copyJudgeTree(tree, join(folder, "tree"));
2477
+ chmodSync(join(folder, "home"), 511);
2478
+ chmodSync(join(folder, "home", "tmp"), 511);
2479
+ chmodSync(folder, 1023);
2480
+ },
2481
+ run: (command, opts) => runtime.host.exec({ container: state.container, user, workdir: command.cwd, env: { literals: command.env, passed: {} }, argv: command.argv }, { logPath: opts.logPath }),
2482
+ readReport: async () => readRegularFile(join(folder, "report.html"), MAX_REPORT_BYTES),
2483
+ // The judge's files belong to the build's user, and Claude Code makes some of its folders private to it, so the runner's user cannot remove them from the
2484
+ // host. Every capability is dropped in the container, so root there could not either; the build's user removes its own, and the runner removes the rest.
2485
+ clear: async () => {
2486
+ const removed = await runtime.host.exec({ container: state.container, user, env: NO_ENV, argv: ["find", JUDGE_DIR, "-mindepth", "1", "-user", user, "-delete"] });
2487
+ if (removed.status !== 0) throw new Error(`find exited ${removed.status}: ${firstLine5(removed.stderr)}`);
2488
+ }
2489
+ };
2490
+ }
2491
+ function readRegularFile(path, limit) {
2492
+ try {
2493
+ const stat = lstatSync(path);
2494
+ return stat.isFile() && stat.size <= limit ? readFileSync(path, "utf8") : void 0;
2495
+ } catch {
2496
+ return void 0;
2497
+ }
2498
+ }
2757
2499
  async function startCheckContainer(input) {
2758
2500
  const { config, runtime, job, jobDir, tokens, log } = input;
2759
2501
  const { host } = runtime;
@@ -2768,7 +2510,7 @@ async function startCheckContainer(input) {
2768
2510
  const buildDir2 = buildDir(config.configPath, buildId);
2769
2511
  const agentUser = built.agentUser ?? ADDED_USER;
2770
2512
  const cpuset = built.cpuset ?? formatCpuList(runtime.cpus.acquire(buildId, config.build?.cpus ?? 4));
2771
- const state = { buildId, container: name, volume: name, buildDir: buildDir2, repository, imageId: built.imageId, agentUser, cpuset };
2513
+ const state = { buildId, container: name, volume: name, buildDir: buildDir2, repository, imageId: built.imageId, imageEnv: built.imageEnv, agentUser, cpuset };
2772
2514
  const checksDir = join(jobDir, "checks");
2773
2515
  mkdirSync(checksDir, { recursive: true });
2774
2516
  const logPath = join(checksDir, "environment.log");
@@ -2817,7 +2559,7 @@ async function startCheckContainer(input) {
2817
2559
  const started = await host.run(containerRunArgs(runInput), { logPath });
2818
2560
  if (started.status !== 0) return dockerFailed("starting the check environment's container", started);
2819
2561
  if (!await waitForRelay(runtime, state)) return fail("the relay in the check environment's container did not start");
2820
- const env = checkEnvironment({ config, item: job.item, repository, hostEnv: runtime.hostEnv, containerEnv: built.containerEnv ?? {} });
2562
+ const env = checkEnvironment({ config, item: job.item, repository, hostEnv: runtime.hostEnv, containerEnv: built.containerEnv ?? {}, imageEnv: built.imageEnv });
2821
2563
  return {
2822
2564
  ok: true,
2823
2565
  container: {
@@ -2834,4 +2576,4 @@ async function startCheckContainer(input) {
2834
2576
  };
2835
2577
  }
2836
2578
 
2837
- export { ADDED_USER, AgentUserError, AssetError, BuildTokens, EVAL_INPUT_NAME, GH_CONFIG_YML, MIN_ENGINE_MAJOR, classifyRefusal, cleanupContainer, containerCommandLine, containerSenderCommand, copyTranscripts, copyTranscriptsFromContainer, createBuildDisk, createContainerRuntime, createRunnerLog, deleteBuildDisk, describeCallFailure, ensureRootfs, ensureTunnel, formatClock, formatDuration, ghHostsYml, gitRemoteFor, isLiteralAddress, keepGhConfigCurrent, lifecycleFailureReason, lifecycleSteps, machineConfig, measureDisk, obtainImage, outOfMemoryReason, parseCpuList, prepareBranch, provisionContainer, refusalCode, relativeToRunner, removeStagedContext, resolveAgentUser, retrySend, runBuildSteps, senderSignalScript, setUpRunnerCgroup, shellQuote, shortClaim, signalInContainer, stageGovernedContext, startCheckContainer, startServicesFor, stopContainer, syncInContainer, writeEvalInput };
2579
+ export { ADDED_USER, AgentUserError, AssetError, EVAL_INPUT_NAME, GH_CONFIG_YML, MIN_ENGINE_MAJOR, cleanupContainer, containerCommandLine, containerJudgeEnvironment, containerSenderCommand, copyJudgeTree, copyTranscripts, copyTranscriptsFromContainer, createBuildDisk, createContainerRuntime, deleteBuildDisk, ensureRootfs, ensureTunnel, ghHostsYml, gitRemoteFor, isLiteralAddress, keepGhConfigCurrent, lifecycleFailureReason, lifecycleSteps, machineConfig, measureDisk, obtainImage, outOfMemoryReason, parseCpuList, prepareBranch, provisionContainer, resolveAgentUser, runBuildSteps, senderSignalScript, setUpRunnerCgroup, shellQuote, signalInContainer, startCheckContainer, startServicesFor, stopContainer, syncInContainer, writeEvalInput };