@outerlayer/cli 0.20.0 → 0.22.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 (146) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +15 -5
  3. package/dist/{agent-setup-AM42SA2D.js → agent-setup-XAJH6MPW.js} +13 -12
  4. package/dist/build-GTMEADQD.js +12 -0
  5. package/dist/build-command-ND2ENDBS.js +50 -0
  6. package/dist/build-info.json +1 -1
  7. package/dist/{builder-C4QNTYMS.js → builder-OAUSTZHL.js} +7 -5
  8. package/dist/{check-BR5D27CX.js → check-3YHMJQ3P.js} +48 -50
  9. package/dist/{chunk-AD3EYI7P.js → chunk-22DYXLU7.js} +259 -56
  10. package/dist/{chunk-P53VEAOF.js → chunk-2CMJLYV6.js} +1 -1
  11. package/dist/{chunk-FBTGMSQF.js → chunk-2HT2XGJX.js} +16 -9
  12. package/dist/{chunk-S5PCA3SN.js → chunk-2J5JGLW5.js} +1 -1
  13. package/dist/{chunk-R22NZSGA.js → chunk-34XORKFD.js} +9 -9
  14. package/dist/{chunk-QOZKPLVJ.js → chunk-3W333NFO.js} +40 -1
  15. package/dist/{chunk-HOKXGFDR.js → chunk-4JHVPFH6.js} +22 -16
  16. package/dist/{chunk-AMFWYESY.js → chunk-4NM3I3VV.js} +1 -1
  17. package/dist/{chunk-PIZRPNKR.js → chunk-4QKLMNI3.js} +5 -5
  18. package/dist/{chunk-NNLA7LLQ.js → chunk-5OCIHFGE.js} +29 -5
  19. package/dist/{chunk-PSN33ERW.js → chunk-6IO62VJS.js} +3 -3
  20. package/dist/{chunk-7YP7T6GV.js → chunk-7V7KG3YM.js} +2 -2
  21. package/dist/{chunk-QNLX477Q.js → chunk-AEKSX4EY.js} +165 -72
  22. package/dist/{chunk-JHGEBPLE.js → chunk-ATRIVTRE.js} +3 -3
  23. package/dist/{chunk-X5H2WYMB.js → chunk-BLW6GPBZ.js} +1 -1
  24. package/dist/{chunk-MLAAT4S6.js → chunk-C3HKR67W.js} +3 -3
  25. package/dist/{chunk-XZ4H4X5F.js → chunk-C3OVGKVJ.js} +15 -8
  26. package/dist/{chunk-BM63HB2M.js → chunk-EAQSOE62.js} +2 -2
  27. package/dist/{chunk-LW7G5TJB.js → chunk-EXV37PKS.js} +2 -2
  28. package/dist/{chunk-B6SCN5NY.js → chunk-FGW2774C.js} +175 -44
  29. package/dist/{chunk-KYS3TQAG.js → chunk-G2WME4RG.js} +4 -4
  30. package/dist/{chunk-LRG3ZMZD.js → chunk-G6DZCKA6.js} +4 -4
  31. package/dist/chunk-GISNHSYL.js +41 -0
  32. package/dist/{chunk-G2YNJIGF.js → chunk-HCVLP7XA.js} +10 -1
  33. package/dist/{chunk-QEFKA2QD.js → chunk-I66MQDXH.js} +2 -2
  34. package/dist/{chunk-3CWUOFV6.js → chunk-IRBNIRQJ.js} +12 -7
  35. package/dist/{chunk-CG36IONK.js → chunk-JRCSET55.js} +2 -2
  36. package/dist/{chunk-CGGPS7SI.js → chunk-JVB63NIB.js} +5 -5
  37. package/dist/{chunk-M2ZWWTWZ.js → chunk-KDHYLBRR.js} +1 -1
  38. package/dist/{chunk-N2PXGQRL.js → chunk-KIGSGZBB.js} +1 -1
  39. package/dist/{chunk-EAEELQVQ.js → chunk-KOQELWQ6.js} +5 -5
  40. package/dist/{chunk-MTP2QEVO.js → chunk-KPBQYEND.js} +1 -1
  41. package/dist/{chunk-VYAL7PUE.js → chunk-LOB2SUM3.js} +1 -1
  42. package/dist/{chunk-GGOMT6KF.js → chunk-PHHBYAL6.js} +1 -1
  43. package/dist/{chunk-IF2OSI5T.js → chunk-QOALB6ZD.js} +2 -2
  44. package/dist/{chunk-ZX4I2MEK.js → chunk-TPKYTORP.js} +1 -1
  45. package/dist/{chunk-XNGMQ27R.js → chunk-UD3EXMKB.js} +2 -2
  46. package/dist/{chunk-7UWVLZXD.js → chunk-USWVXQIH.js} +1 -1
  47. package/dist/{chunk-BQXFBI7T.js → chunk-V7MQKOZY.js} +32 -7
  48. package/dist/{chunk-6X4SQ5MY.js → chunk-V7SAQ645.js} +1 -1
  49. package/dist/{chunk-TWF3RICI.js → chunk-VSUXCMMH.js} +110 -11
  50. package/dist/{chunk-FVMYKL6L.js → chunk-VWLOQ6FI.js} +1 -1
  51. package/dist/{chunk-3ZR7Y23Y.js → chunk-VX74ND77.js} +650 -70
  52. package/dist/{chunk-Z7K3L4FC.js → chunk-W3SWJ2TD.js} +2 -2
  53. package/dist/chunk-WERZMIKA.js +718 -0
  54. package/dist/{chunk-GEMQLBMW.js → chunk-WMHYIBJW.js} +6 -6
  55. package/dist/{chunk-5VML45HH.js → chunk-XCRZTXRL.js} +2 -2
  56. package/dist/{chunk-ECODQ7II.js → chunk-ZFLOGMUC.js} +1 -1
  57. package/dist/chunk-ZNPE7FW5.js +137 -0
  58. package/dist/claude-launcher-4BQNXYAF.js +267 -0
  59. package/dist/{cli-MKF7TU7X.js → cli-NMGVZQPB.js} +161 -120
  60. package/dist/{command-ZQNEPDS6.js → command-GE62AJZY.js} +32 -34
  61. package/dist/{config-CLSGABD5.js → config-7Q52COZ3.js} +3 -2
  62. package/dist/{connect-cmd-UNYKGDAL.js → connect-cmd-ZFOK6HEL.js} +5 -5
  63. package/dist/{context-cleanup-CHEWNYU6.js → context-cleanup-DSA3HGQU.js} +1 -1
  64. package/dist/{control-plane-launch-34LNRJWR.js → control-plane-launch-SZTE4TUS.js} +62 -28
  65. package/dist/{dist-WO7NOJNG.js → dist-7IH56ELA.js} +1 -1
  66. package/dist/{docker-BFU3AQGY.js → docker-2OVBKVRK.js} +7 -7
  67. package/dist/{doctor-DHUM5DBB.js → doctor-3SQGZVYP.js} +35 -36
  68. package/dist/{emit-artifact-cmd-XBHM647X.js → emit-artifact-cmd-R42BE6ID.js} +19 -18
  69. package/dist/{emit-cmd-W55AEBCA.js → emit-cmd-3W5IO5ET.js} +3 -3
  70. package/dist/{emit-criteria-cmd-UNMWHOUY.js → emit-criteria-cmd-MQZE7W4Y.js} +13 -12
  71. package/dist/{emit-finding-cmd-7JHVRYOP.js → emit-finding-cmd-ZS5JEGGM.js} +13 -12
  72. package/dist/{emit-result-cmd-5NZMLCJK.js → emit-result-cmd-FUE3DXBW.js} +19 -18
  73. package/dist/{emit-step-cmd-347XNGPQ.js → emit-step-cmd-T7PRA44X.js} +14 -13
  74. package/dist/{exec-client-7UMESIE2.js → exec-client-YIDTWXMZ.js} +4 -3
  75. package/dist/{governing-source-HAA4OW57.js → governing-source-TORXL4I4.js} +19 -18
  76. package/dist/{guest-init-KPBQOFE5.js → guest-init-TO6HAWIT.js} +4 -3
  77. package/dist/{hook-fast-RL5E5OXE.js → hook-fast-WFB2ZMRB.js} +2 -2
  78. package/dist/host-key-2LXQDSBO.js +9 -0
  79. package/dist/{import-capture-cmd-TK4XY7RD.js → import-capture-cmd-Q56DDZVO.js} +1 -1
  80. package/dist/index.js +5 -5
  81. package/dist/init-2CRAVIUU.js +52 -0
  82. package/dist/{init-V6FOMD2V.js → init-AHDZCMDC.js} +32 -33
  83. package/dist/{init-cmd-Z4O4PPLZ.js → init-cmd-IXDL2ZSL.js} +25 -23
  84. package/dist/{init-update-7DVAKWW3.js → init-update-YB3PODDH.js} +69 -24
  85. package/dist/{install-4SEFIIW2.js → install-CZUIW2AF.js} +5 -4
  86. package/dist/{install-cmd-QDOTR4OM.js → install-cmd-Q7RJGFUO.js} +6 -5
  87. package/dist/{login-browser-DX35MARZ.js → login-browser-BN5UKPHS.js} +3 -3
  88. package/dist/{logout-cmd-5V5MMBSY.js → logout-cmd-RZFWTNYD.js} +2 -2
  89. package/dist/{logs-AMLKC3VP.js → logs-RJPYTVQE.js} +3 -2
  90. package/dist/loop-ZDW6TRAA.js +75 -0
  91. package/dist/machine-JCOHLASP.js +47 -0
  92. package/dist/{mcp-install-cmd-GGWB3JJ2.js → mcp-install-cmd-GD4QUP7Z.js} +2 -2
  93. package/dist/{mcp-serve-cmd-N3UTY6EE.js → mcp-serve-cmd-IULQPVHL.js} +6 -5
  94. package/dist/{paths-7XP63OAX.js → paths-N3LU4O2Z.js} +3 -2
  95. package/dist/pending-GRCV3ZR4.js +9 -0
  96. package/dist/{pidfile-3DM4OBKO.js → pidfile-IF3TCL5A.js} +4 -3
  97. package/dist/receive-files-T2BIUBQL.js +254 -0
  98. package/dist/{record-3WP6V2K5.js → record-73PQE77B.js} +4 -3
  99. package/dist/relay-PSZ4LGMW.js +210 -0
  100. package/dist/relay-protocol-K7KP2SPI.js +6 -0
  101. package/dist/{runner-env-FRZFFHDF.js → runner-env-LDOBJESN.js} +6 -5
  102. package/dist/runner-identity-RWCBTY2A.js +14 -0
  103. package/dist/{runner-key-5FOWCW7J.js → runner-key-HMYLP6WN.js} +32 -33
  104. package/dist/{session-sender-U2BBUWNO.js → session-sender-2G25JYHI.js} +20 -19
  105. package/dist/{spec-cmd-FVLJOFGC.js → spec-cmd-LBVXM6XS.js} +13 -12
  106. package/dist/{starter-pack-6LQVLBIA.js → starter-pack-5EQYGSJF.js} +2 -2
  107. package/dist/{status-BINH7MBG.js → status-QZB2GWXJ.js} +36 -38
  108. package/dist/{statusline-fast-KN3BX67H.js → statusline-fast-2OK3DVAA.js} +2 -2
  109. package/dist/{sync-cmd-VXOUFS4I.js → sync-cmd-TQQIR4PN.js} +19 -18
  110. package/dist/{watch-OH645CV2.js → watch-4JES4PCS.js} +19 -18
  111. package/dist/{work-claim-cmd-DRQCHCOK.js → work-claim-cmd-XG6O7B63.js} +17 -16
  112. package/dist/{work-cmd-5I3J2XQQ.js → work-cmd-L6WMPAA2.js} +13 -12
  113. package/dist/{work-comment-cmd-UCCPWUSR.js → work-comment-cmd-SNTDCPGH.js} +13 -12
  114. package/dist/{work-logs-cmd-7352CJPG.js → work-logs-cmd-3S7IAVII.js} +13 -12
  115. package/dist/{work-open-pr-cmd-LSH4VHJM.js → work-open-pr-cmd-E3WMO5XT.js} +15 -14
  116. package/dist/{work-pr-cmd-H4OQR26N.js → work-pr-cmd-22T6FG4T.js} +14 -13
  117. package/dist/{work-stop-cmd-CRDHXQVW.js → work-stop-cmd-WR7ESS4Z.js} +13 -12
  118. package/package.json +2 -2
  119. package/skill-pack/maintained/outerlayer-build/SKILL.md +347 -0
  120. package/skill-pack/{template/build → maintained/outerlayer-build}/references/evidence.md +2 -2
  121. package/skill-pack/{template/build → maintained/outerlayer-build}/references/release.md +4 -4
  122. package/skill-pack/{template/build → maintained/outerlayer-build}/references/review-loop.md +3 -1
  123. package/skill-pack/shipped.json +2 -0
  124. package/skill-pack/template/build/SKILL.md +4 -253
  125. package/skill-pack/validators/acceptance-criteria.yaml +1 -1
  126. package/dist/build-TQ6X5PYN.js +0 -12
  127. package/dist/chunk-3UZTIRLF.js +0 -320
  128. package/dist/chunk-CX22S5BA.js +0 -45
  129. package/dist/chunk-LLM3I5YU.js +0 -89
  130. package/dist/chunk-SADUQSO4.js +0 -50
  131. package/dist/chunk-VMJPFQNE.js +0 -261
  132. package/dist/claude-launcher-J5M44PFS.js +0 -115
  133. package/dist/host-key-U4B5QETR.js +0 -8
  134. package/dist/init-XG47KCI4.js +0 -53
  135. package/dist/loop-EAZQMAE2.js +0 -77
  136. package/dist/machine-TRHBT5NE.js +0 -48
  137. package/dist/pending-532ABZYC.js +0 -8
  138. package/dist/relay-L76HDX72.js +0 -46
  139. package/dist/{chunk-YVZJ6GKF.js → chunk-DIB2WLBL.js} +1 -1
  140. package/dist/{chunk-GOSFARMS.js → chunk-F35VMF5M.js} +1 -1
  141. package/dist/{chunk-DSF2FWAZ.js → chunk-IZSVXRDR.js} +1 -1
  142. /package/skill-pack/{template/build → maintained/outerlayer-build}/references/agent-briefs.md +0 -0
  143. /package/skill-pack/{template/build → maintained/outerlayer-build}/references/criteria-judge.md +0 -0
  144. /package/skill-pack/{template/build → maintained/outerlayer-build}/references/review-briefs.md +0 -0
  145. /package/skill-pack/{template/build → maintained/outerlayer-build}/scripts/criterion-citations.mjs +0 -0
  146. /package/skill-pack/{template/build → maintained/outerlayer-build}/scripts/record-criteria.mjs +0 -0
@@ -1,33 +1,33 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'node:module';
3
+ import { readDefaultBranchHead, cloneRepository, buildRepositoryImage } from './chunk-IRBNIRQJ.js';
3
4
  import { currentCliBuild } from './chunk-OGFGZQA3.js';
4
- import { createDockerHost, childEnvironment, spawnRun } from './chunk-VMJPFQNE.js';
5
- import { builderSpec, runnerLabelValue, RUNNER_LABEL, BUILD_LABEL, prepareRunArgs, containerRunArgs, HASH_ARGV, parseProgramHash, JUDGE_PROGRAM, containerExecArgs, JUDGE_DIR, MAX_REPORT_BYTES } from './chunk-3UZTIRLF.js';
6
- import { execCapture } from './chunk-GGOMT6KF.js';
7
- import { GUEST_CID } from './chunk-X5H2WYMB.js';
8
- import { describeCallFailure, controlPlaneTokenFailure, refusalCode, resolveGoverningSource, removeStagedContext, stageGovernedContext, pinnedSourceOf, commandWithShim, shimScript } from './chunk-XZ4H4X5F.js';
9
- import { WorkCommandError } from './chunk-FBTGMSQF.js';
5
+ import { createDockerHost, builderSpec, runnerLabelValue, RUNNER_LABEL, BUILD_LABEL, prepareRunArgs, containerRunArgs, HASH_ARGV, parseProgramHash, JUDGE_PROGRAM, containerExecArgs, childEnvironment, JUDGE_DIR, JUDGE_REPORT, MAX_REPORT_BYTES, spawnRun } from './chunk-WERZMIKA.js';
6
+ import { RecipeError, 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-3W333NFO.js';
7
+ import { execCapture } from './chunk-PHHBYAL6.js';
8
+ import { GUEST_CID } from './chunk-BLW6GPBZ.js';
9
+ import { describeCallFailure, controlPlaneTokenFailure, refusalCode, resolveGoverningSource, removeStagedContext, stageGovernedContext, pinnedSourceOf, commandWithShim, shimScript } from './chunk-C3OVGKVJ.js';
10
+ import { WorkCommandError } from './chunk-2HT2XGJX.js';
10
11
  import { hostAllowed } from './chunk-UXYLWVTQ.js';
11
12
  import { parseDuKilobytes } from './chunk-7RQ73DLO.js';
12
- import { readDefaultBranchHead, cloneRepository, buildRepositoryImage } from './chunk-3CWUOFV6.js';
13
- import { RecipeError } from './chunk-QOZKPLVJ.js';
14
- 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';
15
13
  import { startTimeOf, isSameProcess } from './chunk-JBJTC3UU.js';
16
- import { setCustomScrubbers, setLocalIdentity, setRepoRoot, parseTranscript, scrubDeep, scrubText } from './chunk-QNLX477Q.js';
17
- import { sharedSkillNames, skillConflictMessage } from './chunk-LLM3I5YU.js';
18
- import { versionAtLeast, MINIMUM_CLAUDE_VERSION } from './chunk-G2YNJIGF.js';
14
+ import { sharedSkillNames, skillConflictMessage } from './chunk-ZNPE7FW5.js';
15
+ import { versionAtLeast, MINIMUM_CLAUDE_VERSION } from './chunk-HCVLP7XA.js';
16
+ import { setCustomScrubbers, setLocalIdentity, setRepoRoot, parseTranscript, scrubDeep, scrubText } from './chunk-AEKSX4EY.js';
19
17
  import { orderSessions, extractFacts, sessionsToTimeline } from './chunk-WFSUUUAN.js';
20
18
  import { downconvertSession } from './chunk-7VUK5RFS.js';
21
- import { rootfsPathFor, diskTemplatesDir, RUNNER_FILE_MODE, deliveryDir, ghConfigDir, runnerDirOfBuild, spoolDir, mirrorDir, buildDir, RUNNER_DIR_MODE, SHIM_DIR, vmAssetsDir } from './chunk-N2PXGQRL.js';
22
- import { repositoriesOf, primaryOf, RELAY_PORT, repositoryPath, CHECKOUT_DIR, lifecycleWorkdir, claudeCredential, buildEnvironment, startBuildServices, checkEnvironment, AGENT_CREDENTIAL_MISSING, checkoutPath, JOB_MOUNT } from './chunk-BQXFBI7T.js';
19
+ import { rootfsPathFor, diskTemplatesDir, RUNNER_FILE_MODE, deliveryDir, ghConfigDir, runnerDirOfBuild, spoolDir, mirrorDir, buildDir, RUNNER_DIR_MODE, SHIM_DIR, vmAssetsDir } from './chunk-KIGSGZBB.js';
20
+ import { repositoriesOf, primaryOf, RELAY_PORT, repositoryPath, CHECKOUT_DIR, lifecycleWorkdir, claudeCredential, buildEnvironment, startBuildServices, checkEnvironment, AGENT_CREDENTIAL_MISSING, checkoutPath, JOB_MOUNT } from './chunk-V7MQKOZY.js';
21
+ import { RELAY_USER, RELAY_ID, UNKNOWN_TRANSPORT_REASON, MAX_MALFORMED_SESSIONS, MALFORMED_REASON, MAX_OPEN_STREAMS, STREAM_WINDOW, SESSION_WINDOW, targetOfPath } from './chunk-GISNHSYL.js';
23
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';
24
23
  import { join, dirname, basename } from 'path';
25
24
  import { spawnSync, spawn } from 'child_process';
26
25
  import { availableParallelism, networkInterfaces } from 'os';
27
26
  import { randomBytes, createHash } from 'crypto';
27
+ import http2 from 'http2';
28
+ import { Duplex, Transform } from 'stream';
28
29
  import { createServer, request } from 'http';
29
30
  import { request as request$1 } from 'https';
30
- import { Transform } from 'stream';
31
31
  import { pipeline, finished } from 'stream/promises';
32
32
  import { createGunzip, gunzipSync, constants as constants$1 } from 'zlib';
33
33
  import { isIP, connect, createServer as createServer$1 } from 'net';
@@ -306,6 +306,39 @@ function createCpuAllocator(cores) {
306
306
  }
307
307
  };
308
308
  }
309
+
310
+ // src/runner/container/lease-channel.ts
311
+ var LEASE_SECONDS = 900;
312
+ function leaseExpiresAtMs(job) {
313
+ const base = Date.parse(job.lastRenewAt ?? job.startedAt);
314
+ return (Number.isFinite(base) ? base : Date.now()) + LEASE_SECONDS * 1e3;
315
+ }
316
+ function createLeaseChannel(expiryMs) {
317
+ const open = /* @__PURE__ */ new Set();
318
+ let stopped = false;
319
+ const write = (stream) => {
320
+ if (!stopped && !stream.destroyed && stream.writable) stream.write(`${Math.floor(expiryMs())}
321
+ `);
322
+ };
323
+ return {
324
+ accept(stream) {
325
+ stream.on("error", () => void 0);
326
+ open.add(stream);
327
+ stream.once("close", () => open.delete(stream));
328
+ queueMicrotask(() => write(stream));
329
+ },
330
+ publish() {
331
+ for (const stream of open) write(stream);
332
+ },
333
+ stop() {
334
+ stopped = true;
335
+ },
336
+ close() {
337
+ for (const stream of [...open]) stream.destroy();
338
+ open.clear();
339
+ }
340
+ };
341
+ }
309
342
  var AssetError = class extends Error {
310
343
  constructor(file, message) {
311
344
  super(message);
@@ -396,8 +429,7 @@ async function ensureAssets(options) {
396
429
  const helper = await ensureFile(dir, `outerlayer-guest-${arch}`, pins.helper, fetchImpl, 493);
397
430
  return { firecracker, kernel, helper, helperSha256: pins.helper.sha256 };
398
431
  }
399
-
400
- // src/runner/container/docker.ts
432
+ var STOP_GRACE_SECONDS = 15;
401
433
  function parseImageConfig(json) {
402
434
  const config = JSON.parse(json);
403
435
  const env = {};
@@ -435,10 +467,37 @@ function createContainerHost(deps = {}) {
435
467
  exec(input, opts) {
436
468
  const env = {};
437
469
  for (const [name, value] of Object.entries(input.env.passed)) if (value !== void 0) env[name] = value;
438
- return run("docker", ["exec", ...containerExecArgs(input)], { ...opts, env });
470
+ return run("docker", ["exec", ...containerExecArgs(input)], { ...opts, env, ...input.stdin === void 0 ? {} : { input: input.stdin } });
471
+ },
472
+ execDetached(input) {
473
+ const env = {};
474
+ for (const [name, value] of Object.entries(input.env.passed)) if (value !== void 0) env[name] = value;
475
+ return run("docker", ["exec", ...containerExecArgs({ ...input, detached: true })], { env });
476
+ },
477
+ async state(container) {
478
+ const result = await run("docker", ["inspect", "--format", "{{.State.Running}}", container]);
479
+ if (result.status !== 0) return /no such (object|container)/i.test(result.stderr) ? "missing" : "unknown";
480
+ return result.stdout.trim() === "true" ? "running" : "stopped";
481
+ },
482
+ execStream(input) {
483
+ const child = (deps.spawn ?? spawn)("docker", ["exec", ...containerExecArgs({ ...input, interactive: true })], {
484
+ env: childEnvironment(process.env),
485
+ stdio: ["pipe", "pipe", "pipe"]
486
+ });
487
+ let stderr = "";
488
+ child.stderr?.on("data", (chunk) => {
489
+ stderr = (stderr + chunk.toString()).slice(-400);
490
+ });
491
+ child.stderr?.on("error", () => void 0);
492
+ const exited = new Promise((resolve) => {
493
+ child.once("error", () => resolve(-1));
494
+ child.once("close", (code) => resolve(code ?? -1));
495
+ });
496
+ child.stdin.on("error", () => void 0);
497
+ return { stdin: child.stdin, stdout: child.stdout, exited, kill: () => void child.kill("SIGKILL"), stderrTail: () => stderr };
439
498
  },
440
499
  stop(container) {
441
- return run("docker", ["stop", container]);
500
+ return run("docker", ["stop", "--time", String(STOP_GRACE_SECONDS), container]);
442
501
  },
443
502
  start(container) {
444
503
  return run("docker", ["start", container]);
@@ -713,7 +772,7 @@ var DELIVER_SCRIPT = [
713
772
  'src="$1"; dest="$2"',
714
773
  'if [ -e "$dest" ]; then chmod -R u+w "$dest"; rm -rf "$dest"; fi',
715
774
  'mkdir -p "$dest"',
716
- "for entry in AGENTS.md CLAUDE.md settings.json .claude .outerlayer; do",
775
+ "for entry in AGENTS.md CLAUDE.md settings.json .claude .outerlayer .agents; do",
717
776
  ' if [ -e "$src/$entry" ]; then cp -R "$src/$entry" "$dest/"; fi',
718
777
  "done",
719
778
  'chmod -R a-w "$dest"'
@@ -723,23 +782,33 @@ var WRITE_SHIM_SCRIPT = 'mkdir -p "$1" && printf "%s" "$2" > "$1/claude" && chmo
723
782
  function failure(what, result) {
724
783
  return `context_delivery_failed: ${what} (exit ${result.status})${firstLine3(result.stderr) === "" ? "" : `: ${firstLine3(result.stderr)}`}`;
725
784
  }
785
+ async function repositorySkills(input, folder) {
786
+ const listed = await input.exec(["ls", "-1", folder], CHECKOUT_DIR, { quiet: true });
787
+ if (listed.status !== 0) return /* @__PURE__ */ new Set();
788
+ return new Set(listed.stdout.split("\n").map((name) => name.trim()));
789
+ }
726
790
  async function deliveryRefusal(input, staged) {
727
791
  if (input.agentVersion !== void 0 && !versionAtLeast(input.agentVersion)) {
728
792
  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`;
729
793
  }
730
- const own = await input.exec(["ls", "-1", ".claude/skills"], CHECKOUT_DIR, { quiet: true });
731
- if (own.status !== 0) return void 0;
732
- const ownNames = new Set(own.stdout.split("\n").map((name) => name.trim()));
733
- const clash = sharedSkillNames(join(staged.dir, "tree")).find((name) => ownNames.has(name));
794
+ const own = /* @__PURE__ */ new Map([
795
+ [".claude/skills", await repositorySkills(input, ".claude/skills")],
796
+ [".agents/skills", await repositorySkills(input, ".agents/skills")]
797
+ ]);
798
+ const clash = sharedSkillNames(join(staged.dir, "tree")).find((name) => [...own.values()].some((names) => names.has(name)));
734
799
  if (clash === void 0) return void 0;
735
- return `context_skill_conflict: ${skillConflictMessage({ skill: clash, repository: input.repository, controlPlane: staged.repository })}, and would replace it in this build; rename or remove it from the repository`;
800
+ const folder = own.get(".claude/skills").has(clash) ? ".claude/skills" : ".agents/skills";
801
+ 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`;
802
+ }
803
+ async function writeShim(input) {
804
+ const shim = await input.exec(["sh", "-c", WRITE_SHIM_SCRIPT, "shim", SHIM_DIR, shimScript()], "/build/work");
805
+ if (shim.status !== 0) return { ok: false, reason: failure("writing the claude shim failed", shim) };
806
+ return { ok: true };
736
807
  }
737
808
  async function deliver(input, staged) {
738
809
  const dir = deliveryDir(staged.repository, staged.sha);
739
810
  const copied = await input.exec(["sh", "-c", DELIVER_SCRIPT, "deliver", STAGED_TREE, dir], "/build/work");
740
811
  if (copied.status !== 0) return { ok: false, reason: failure("copying the control plane into the build failed", copied) };
741
- const shim = await input.exec(["sh", "-c", WRITE_SHIM_SCRIPT, "shim", SHIM_DIR, shimScript()], "/build/work");
742
- if (shim.status !== 0) return { ok: false, reason: failure("writing the claude shim failed", shim) };
743
812
  const record = JSON.stringify({ schemaVersion: 1, repository: staged.repository, ref: staged.ref, sha: staged.sha, dir });
744
813
  const recorded = await input.exec(["sh", "-c", RECORD_SCRIPT, "record", record], CHECKOUT_DIR);
745
814
  if (recorded.status !== 0) return { ok: false, reason: failure("recording the delivery failed", recorded) };
@@ -813,6 +882,11 @@ async function runBuildSteps(input) {
813
882
  const staged = await input.stageContext();
814
883
  if (staged.kind === "refused") return { ok: false, reason: staged.reason };
815
884
  let controlPlane;
885
+ const shimmed = await writeShim(input);
886
+ if (!shimmed.ok) {
887
+ if (staged.kind === "staged") input.removeStagedContext();
888
+ return shimmed;
889
+ }
816
890
  if (staged.kind === "staged") {
817
891
  try {
818
892
  const refusal = await deliveryRefusal(input, staged);
@@ -1048,17 +1122,190 @@ function copyJudgeTree(from, to, limits2 = { files: MAX_TREE_FILES, bytes: MAX_T
1048
1122
 
1049
1123
  // src/runner/container/agent-user.ts
1050
1124
  var ADDED_USER = "ol-agent";
1125
+ var RELAY_REFUSAL = `the agent's user cannot be \`${RELAY_USER}\``;
1051
1126
  var AgentUserError = class extends Error {
1052
1127
  };
1053
1128
  async function resolveAgentUser(input) {
1054
1129
  const named = input.remoteUser !== void 0 && input.remoteUser !== "" ? input.remoteUser : input.imageUser;
1055
1130
  const candidate = (named ?? "").split(":")[0];
1056
1131
  if (candidate === "" || candidate === "root") return ADDED_USER;
1057
- if (/^\d+$/.test(candidate)) return Number(candidate) === 0 ? ADDED_USER : candidate;
1132
+ if (candidate === RELAY_USER) throw new AgentUserError(RELAY_REFUSAL);
1133
+ if (/^\d+$/.test(candidate)) {
1134
+ if (Number(candidate) === RELAY_ID) throw new AgentUserError(RELAY_REFUSAL);
1135
+ return Number(candidate) === 0 ? ADDED_USER : candidate;
1136
+ }
1058
1137
  const uid = await input.probe(candidate);
1059
1138
  if (uid === null) throw new AgentUserError(`the user "${candidate}" is not in the image, so no command can run as it`);
1139
+ if (uid === RELAY_ID) throw new AgentUserError(RELAY_REFUSAL);
1060
1140
  return uid === 0 ? ADDED_USER : candidate;
1061
1141
  }
1142
+ var noop = () => void 0;
1143
+ var FRAME_HEADER_BYTES = 9;
1144
+ var GOAWAY_FRAME = 7;
1145
+ function watchSentGoaway(onError) {
1146
+ let head = Buffer.alloc(0);
1147
+ let want = FRAME_HEADER_BYTES;
1148
+ let skip = 0;
1149
+ return (chunk) => {
1150
+ let at = 0;
1151
+ while (at < chunk.length) {
1152
+ if (skip > 0) {
1153
+ const n = Math.min(skip, chunk.length - at);
1154
+ skip -= n;
1155
+ at += n;
1156
+ continue;
1157
+ }
1158
+ const take = Math.min(want - head.length, chunk.length - at);
1159
+ head = Buffer.concat([head, chunk.subarray(at, at + take)]);
1160
+ at += take;
1161
+ if (head.length < want) return;
1162
+ const length = head.readUIntBE(0, 3);
1163
+ if (want === FRAME_HEADER_BYTES && head[3] === GOAWAY_FRAME && length >= 8) {
1164
+ want = FRAME_HEADER_BYTES + 8;
1165
+ continue;
1166
+ }
1167
+ if (want > FRAME_HEADER_BYTES && head.readUInt32BE(FRAME_HEADER_BYTES + 4) !== http2.constants.NGHTTP2_NO_ERROR) onError();
1168
+ skip = length - (want - FRAME_HEADER_BYTES);
1169
+ head = Buffer.alloc(0);
1170
+ want = FRAME_HEADER_BYTES;
1171
+ }
1172
+ };
1173
+ }
1174
+ function serveSession(transport, services, opts = {}) {
1175
+ const maxStreams = opts.maxStreams ?? MAX_OPEN_STREAMS;
1176
+ let open = 0;
1177
+ let served = 0;
1178
+ let malformed = false;
1179
+ transport.on("error", noop);
1180
+ const watch = watchSentGoaway(() => {
1181
+ malformed = true;
1182
+ setImmediate(() => session.destroy());
1183
+ });
1184
+ const outbound = new Transform({
1185
+ transform(chunk, _encoding, done) {
1186
+ watch(chunk);
1187
+ done(null, chunk);
1188
+ }
1189
+ });
1190
+ outbound.on("error", noop);
1191
+ outbound.pipe(transport);
1192
+ const wire = Duplex.from({ readable: transport, writable: outbound });
1193
+ wire.on("error", noop);
1194
+ const session = http2.performServerHandshake(wire, { settings: { initialWindowSize: STREAM_WINDOW, enablePush: false } });
1195
+ try {
1196
+ session.setLocalWindowSize(SESSION_WINDOW);
1197
+ } catch {
1198
+ }
1199
+ const ended = new Promise((resolve) => {
1200
+ session.on("error", (err) => {
1201
+ if (typeof err.code === "string" && err.code.startsWith("ERR_HTTP2_")) malformed = true;
1202
+ });
1203
+ session.on("close", () => resolve({ malformed, served }));
1204
+ });
1205
+ session.on("stream", (stream, headers) => {
1206
+ stream.on("error", noop);
1207
+ const target = headers[":method"] === "POST" ? targetOfPath(headers[":path"]) : void 0;
1208
+ if (target === void 0 || open >= maxStreams) {
1209
+ stream.close(http2.constants.NGHTTP2_REFUSED_STREAM);
1210
+ return;
1211
+ }
1212
+ open++;
1213
+ stream.once("close", () => {
1214
+ open--;
1215
+ });
1216
+ if (!services.accept(target, stream)) {
1217
+ stream.close(http2.constants.NGHTTP2_REFUSED_STREAM);
1218
+ return;
1219
+ }
1220
+ served++;
1221
+ if (!stream.headersSent && !stream.destroyed) stream.respond({ ":status": 200 });
1222
+ });
1223
+ return {
1224
+ ended,
1225
+ // A session the peer is closing gracefully waits for its open streams, which a stopped container never ends: destroy it whatever its state.
1226
+ close: () => {
1227
+ if (!session.destroyed) session.destroy();
1228
+ }
1229
+ };
1230
+ }
1231
+ var FIRST_BACKOFF_MS = 200;
1232
+ var LONGEST_BACKOFF_MS = 5e3;
1233
+ var JOINED_AFTER_MS = 2e3;
1234
+ var EXIT_WAIT_MS = 5e3;
1235
+ function exitedWithin(stream, ms) {
1236
+ let timer;
1237
+ const late = new Promise((resolve) => {
1238
+ timer = setTimeout(() => resolve(void 0), ms);
1239
+ timer.unref();
1240
+ });
1241
+ return Promise.race([stream.exited, late]).finally(() => clearTimeout(timer));
1242
+ }
1243
+ function superviseRelay(input) {
1244
+ const sleep = input.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
1245
+ const exitWaitMs = input.exitWaitMs ?? EXIT_WAIT_MS;
1246
+ let closed = false;
1247
+ let fatal;
1248
+ let running;
1249
+ let wake;
1250
+ const loop = (async () => {
1251
+ let malformedInARow = 0;
1252
+ let backoff = FIRST_BACKOFF_MS;
1253
+ let failing = false;
1254
+ while (!closed && fatal === void 0) {
1255
+ const startedMs = Date.now();
1256
+ const stream = input.execStream({
1257
+ container: input.container,
1258
+ user: RELAY_USER,
1259
+ env: { literals: {}, passed: {} },
1260
+ argv: ["outerlayer", "broker-relay", "join"],
1261
+ interactive: true
1262
+ });
1263
+ stream.stdin.on("error", noop);
1264
+ stream.stdout.on("error", noop);
1265
+ const session = serveSession(Duplex.from({ readable: stream.stdout, writable: stream.stdin }), input.services);
1266
+ running = { stream, session };
1267
+ void stream.exited.then(() => session.close());
1268
+ const end = await session.ended;
1269
+ stream.kill();
1270
+ const status = await exitedWithin(stream, exitWaitMs);
1271
+ running = void 0;
1272
+ if (status === void 0) input.log?.(`the relay's exec into ${input.container} did not end when killed \xB7 going on without it`);
1273
+ const joined = end.served > 0 || Date.now() - startedMs >= JOINED_AFTER_MS;
1274
+ if (joined) {
1275
+ backoff = FIRST_BACKOFF_MS;
1276
+ failing = false;
1277
+ if (!closed) input.log?.(`the session with ${input.container} ended \xB7 joining again`);
1278
+ }
1279
+ if (!end.malformed && end.served > 0) malformedInARow = 0;
1280
+ if (end.malformed) {
1281
+ malformedInARow++;
1282
+ if (malformedInARow >= MAX_MALFORMED_SESSIONS) {
1283
+ fatal = MALFORMED_REASON;
1284
+ input.log?.(`the session with ${input.container} broke HTTP/2 framing ${malformedInARow} times in a row \xB7 ending the build`);
1285
+ input.onFatal?.(fatal);
1286
+ return;
1287
+ }
1288
+ } else if (!joined && !failing) {
1289
+ failing = true;
1290
+ const reason = stream.stderrTail?.().trim() ?? "";
1291
+ input.log?.(`could not join the relay in ${input.container}${status === 0 || status === void 0 ? "" : ` (exec exited ${status})`}${reason === "" ? "" : `: ${reason}`} \xB7 trying again`);
1292
+ }
1293
+ if (closed) return;
1294
+ await Promise.race([sleep(backoff), new Promise((resolve) => wake = resolve)]);
1295
+ backoff = Math.min(backoff * 2, LONGEST_BACKOFF_MS);
1296
+ }
1297
+ })();
1298
+ return {
1299
+ fatal: () => fatal,
1300
+ close: async () => {
1301
+ closed = true;
1302
+ wake?.();
1303
+ running?.session.close();
1304
+ running?.stream.kill();
1305
+ await loop;
1306
+ }
1307
+ };
1308
+ }
1062
1309
  var FILE_MODE = 420;
1063
1310
  var DIR_MODE = 493;
1064
1311
  function replaceFile(path, text) {
@@ -1086,6 +1333,140 @@ function writeGhConfig(dir, token) {
1086
1333
  replaceFile(join(dir, "hosts.yml"), ghHostsYml(token));
1087
1334
  replaceFile(join(dir, "config.yml"), GH_CONFIG_YML);
1088
1335
  }
1336
+ var DeliveryFailure = class extends Error {
1337
+ };
1338
+ var BLOCK = 512;
1339
+ var DELIVERY_TIMEOUT_MS = 5 * 6e4;
1340
+ var RECEIVE_ARGV = ["/opt/outerlayer/bin/node", "/opt/outerlayer/cli/dist/index.js", "receive-files"];
1341
+ var REASON_LINE_MAX = 240;
1342
+ var STEPS = {
1343
+ gh: "the build's gh configuration",
1344
+ "git-hooks": "the commit hooks",
1345
+ context: "the staged context",
1346
+ "eval-input": "the checks' input"
1347
+ };
1348
+ function field(header, at, length, text) {
1349
+ header.write(text, at, length, "latin1");
1350
+ }
1351
+ function octalField(header, at, length, value) {
1352
+ field(header, at, length, `${value.toString(8).padStart(length - 1, "0")}\0`);
1353
+ }
1354
+ function tarHeader(name, type, mode, size) {
1355
+ const header = Buffer.alloc(BLOCK);
1356
+ header.write(name, 0, 100, "utf8");
1357
+ octalField(header, 100, 8, mode);
1358
+ octalField(header, 108, 8, 0);
1359
+ octalField(header, 116, 8, 0);
1360
+ octalField(header, 124, 12, size);
1361
+ octalField(header, 136, 12, 0);
1362
+ field(header, 148, 8, " ");
1363
+ header.write(type, 156, 1, "latin1");
1364
+ field(header, 257, 6, "ustar\0");
1365
+ field(header, 263, 2, "00");
1366
+ let sum = 0;
1367
+ for (const byte of header) sum += byte;
1368
+ field(header, 148, 8, `${sum.toString(8).padStart(6, "0")}\0 `);
1369
+ return header;
1370
+ }
1371
+ function padded(data) {
1372
+ const rest = data.length % BLOCK;
1373
+ return rest === 0 ? data : Buffer.concat([data, Buffer.alloc(BLOCK - rest)]);
1374
+ }
1375
+ function paxRecord(key, value) {
1376
+ const body = Buffer.from(` ${key}=${value}
1377
+ `, "utf8");
1378
+ let length = body.length + 1;
1379
+ while (String(length).length + body.length !== length) length = String(length).length + body.length;
1380
+ return Buffer.concat([Buffer.from(String(length), "latin1"), body]);
1381
+ }
1382
+ function packTar(entries) {
1383
+ const blocks = [];
1384
+ for (const entry of entries) {
1385
+ const data = entry.type === "file" ? entry.data ?? Buffer.alloc(0) : Buffer.alloc(0);
1386
+ const name = entry.type === "dir" && !entry.path.endsWith("/") ? `${entry.path}/` : entry.path;
1387
+ if (Buffer.byteLength(name, "utf8") > 100) {
1388
+ const record = paxRecord("path", name);
1389
+ blocks.push(tarHeader("PaxHeader", "x", 420, record.length), padded(record));
1390
+ }
1391
+ blocks.push(tarHeader(name, entry.type === "dir" ? "5" : "0", entry.mode, data.length), padded(data));
1392
+ }
1393
+ blocks.push(Buffer.alloc(BLOCK * 2));
1394
+ return Buffer.concat(blocks);
1395
+ }
1396
+ function fileMode(hostMode) {
1397
+ return 292 | hostMode & 128 | ((hostMode & 73) === 0 ? 0 : 73);
1398
+ }
1399
+ function packFolder(dir, options = {}) {
1400
+ const entries = options.rootMode === void 0 ? [] : [{ path: "./", type: "dir", mode: options.rootMode }];
1401
+ const walk = (current, prefix) => {
1402
+ for (const name of readdirSync(current).sort()) {
1403
+ const path = join(current, name);
1404
+ const stat = lstatSync(path);
1405
+ const rel = prefix === "" ? name : `${prefix}/${name}`;
1406
+ if (stat.isDirectory()) {
1407
+ entries.push({ path: rel, type: "dir", mode: options.dirModes?.[rel] ?? 493 });
1408
+ walk(path, rel);
1409
+ } else if (stat.isFile()) {
1410
+ entries.push({ path: rel, type: "file", mode: fileMode(stat.mode), data: readFileSync(path) });
1411
+ }
1412
+ }
1413
+ };
1414
+ walk(dir, "");
1415
+ return packTar(entries);
1416
+ }
1417
+ function scrubDelivery(text, secrets) {
1418
+ let out = text.replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, "").replace(/[\u0000-\u001f\u007f-\u009f]/g, " ");
1419
+ for (const secret of secrets()) if (secret !== "") out = out.split(secret).join("[redacted]");
1420
+ out = out.replace(/\b(?:gh[pousr]_|github_pat_)[A-Za-z0-9_]+/g, "[redacted]");
1421
+ out = out.trim();
1422
+ return out.length > REASON_LINE_MAX ? `${out.slice(0, REASON_LINE_MAX)}...` : out;
1423
+ }
1424
+ async function deliverTar(runtime, state, input) {
1425
+ try {
1426
+ const result = await runtime.host.exec(
1427
+ {
1428
+ container: state.container,
1429
+ user: RELAY_USER,
1430
+ env: { literals: {}, passed: {} },
1431
+ argv: [...RECEIVE_ARGV, "--into", input.into, ...input.replace === true ? ["--replace"] : []],
1432
+ stdin: input.tar
1433
+ },
1434
+ { timeoutMs: DELIVERY_TIMEOUT_MS }
1435
+ );
1436
+ if (result.status === 0) return { ok: true };
1437
+ const line = scrubDelivery(result.stderr.trim().split("\n")[0] ?? "", input.secrets);
1438
+ return { ok: false, reason: `${input.step} could not be delivered (exit ${result.status})${line === "" ? "" : `: ${line}`}` };
1439
+ } catch (err) {
1440
+ return { ok: false, reason: `${input.step} could not be delivered: ${scrubDelivery(describeCallFailure(err), input.secrets)}` };
1441
+ }
1442
+ }
1443
+ async function deliverJobFiles(runtime, state, parts, secrets) {
1444
+ for (const part of parts) {
1445
+ const dir = join(state.buildDir, part === "eval-input" ? EVAL_INPUT_NAME : part);
1446
+ if (!existsSync(dir)) continue;
1447
+ let tar;
1448
+ try {
1449
+ tar = packFolder(dir);
1450
+ } catch (err) {
1451
+ return { ok: false, reason: `${STEPS[part]} could not be read on the host: ${scrubDelivery(describeCallFailure(err), secrets)}` };
1452
+ }
1453
+ const delivered = await deliverTar(runtime, state, { step: STEPS[part], tar, into: `${JOB_MOUNT}/${part}`, replace: part === "eval-input", secrets });
1454
+ if (!delivered.ok) return delivered;
1455
+ }
1456
+ return { ok: true };
1457
+ }
1458
+ function deliverGhConfig(runtime, state, secrets) {
1459
+ return deliverJobFiles(runtime, state, ["gh"], secrets);
1460
+ }
1461
+ function deliverEvalInput(runtime, state, secrets) {
1462
+ return deliverJobFiles(runtime, state, ["eval-input"], secrets);
1463
+ }
1464
+ async function deliverJudgeInputs(runtime, state, dir, secrets) {
1465
+ return deliverTar(runtime, state, { step: "the judge's inputs", tar: packFolder(dir, { rootMode: 1023, dirModes: { home: 511, "home/tmp": 511 } }), into: JUDGE_DIR, replace: true, secrets });
1466
+ }
1467
+ function clearJudgeFolder(runtime, state, secrets) {
1468
+ return deliverTar(runtime, state, { step: "clearing the judge's folder", tar: packTar([]), into: JUDGE_DIR, replace: true, secrets });
1469
+ }
1089
1470
  function gitEnv(extra) {
1090
1471
  const env = {};
1091
1472
  for (const [name, value] of Object.entries(process.env)) if (!name.startsWith("GIT_")) env[name] = value;
@@ -1898,17 +2279,20 @@ ${body}`);
1898
2279
  });
1899
2280
  req.pipe(upstream);
1900
2281
  }
1901
- if (existsSync(socketPath)) unlinkSync(socketPath);
1902
- await new Promise((resolveListen, reject) => {
1903
- server.once("error", reject);
1904
- server.listen(socketPath, () => resolveListen());
1905
- });
1906
- chmodSync(socketPath, 438);
2282
+ if (opts.socket !== false) {
2283
+ if (existsSync(socketPath)) unlinkSync(socketPath);
2284
+ await new Promise((resolveListen, reject) => {
2285
+ server.once("error", reject);
2286
+ server.listen(socketPath, () => resolveListen());
2287
+ });
2288
+ chmodSync(socketPath, 438);
2289
+ }
1907
2290
  const tcpServers = [];
1908
2291
  return {
1909
2292
  socketPath,
1910
2293
  record,
1911
2294
  destinationUsed,
2295
+ acceptStream: (stream) => void server.emit("connection", stream),
1912
2296
  async listenTcp(address) {
1913
2297
  const tcp = createServer$1((socket) => server.emit("connection", socket));
1914
2298
  await new Promise((resolveListen, reject) => {
@@ -1939,6 +2323,9 @@ function createContainerRuntime(opts = {}) {
1939
2323
  host: opts.host ?? createContainerHost(),
1940
2324
  cpus: opts.cpus ?? createCpuAllocator(usableCpus()),
1941
2325
  tunnels: opts.tunnels ?? /* @__PURE__ */ new Map(),
2326
+ relays: opts.relays ?? /* @__PURE__ */ new Map(),
2327
+ relayFatal: opts.relayFatal ?? /* @__PURE__ */ new Map(),
2328
+ leases: opts.leases ?? /* @__PURE__ */ new Map(),
1942
2329
  buildImage: opts.buildImage ?? ((build) => buildRepositoryImage(
1943
2330
  {
1944
2331
  repository: build.repository,
@@ -1967,13 +2354,47 @@ function createContainerRuntime(opts = {}) {
1967
2354
  cliBuild: () => cli ?? currentCliBuild()
1968
2355
  };
1969
2356
  }
1970
- function keepGhConfigCurrent(tokens, buildDir2, onError) {
2357
+ var REPORT_READ_TIMEOUT_MS = 6e4;
2358
+ var GH_CONFIG_RETRY_MS = 3e4;
2359
+ var GH_CONFIG_RETRIES = 5;
2360
+ function secretsOf(runtime, jobDir) {
2361
+ return () => jobDir === void 0 ? [] : runtime.credentials.get(jobDir)?.issuedTokens() ?? [];
2362
+ }
2363
+ function filesAreCopied(state) {
2364
+ return state.jobFiles === "copied";
2365
+ }
2366
+ function keepGhConfigCurrent(tokens, buildDir2, onError, copy) {
2367
+ let last = copy?.delivered;
2368
+ let queued;
2369
+ let queue = Promise.resolve();
2370
+ const deliver2 = (token, attemptsLeft) => {
2371
+ queued = token;
2372
+ queue = queue.then(async () => {
2373
+ if (copy === void 0 || queued !== token) return;
2374
+ const delivered = await deliverGhConfig(copy.runtime, copy.state, copy.secrets);
2375
+ if (delivered.ok) {
2376
+ last = token;
2377
+ queued = void 0;
2378
+ return;
2379
+ }
2380
+ copy.onFailure(delivered.reason);
2381
+ queued = void 0;
2382
+ if (attemptsLeft <= 0) return;
2383
+ const timer = setTimeout(() => {
2384
+ if (queued === void 0 && last !== token) deliver2(token, attemptsLeft - 1);
2385
+ }, GH_CONFIG_RETRY_MS);
2386
+ timer.unref();
2387
+ });
2388
+ };
1971
2389
  tokens.onRead((token) => {
1972
2390
  try {
1973
2391
  writeGhConfig(ghConfigDir(buildDir2), token);
1974
2392
  } catch (err) {
1975
2393
  onError(err);
2394
+ return;
1976
2395
  }
2396
+ if (copy === void 0 || token === last || token === queued) return;
2397
+ deliver2(token, GH_CONFIG_RETRIES);
1977
2398
  });
1978
2399
  }
1979
2400
  function gitRemoteFor(runtime, input) {
@@ -1997,12 +2418,43 @@ function firstLine5(text) {
1997
2418
  return text.trim().split("\n")[0] ?? "";
1998
2419
  }
1999
2420
  var NO_ENV = { literals: {}, passed: {} };
2421
+ function transportOf(state) {
2422
+ const recorded = state.transport;
2423
+ if (recorded === void 0 || recorded === "sockets") return "sockets";
2424
+ return recorded === "exec" ? "exec" : "unknown";
2425
+ }
2426
+ function relayUserOf(state) {
2427
+ return transportOf(state) === "exec" ? RELAY_USER : ADDED_USER;
2428
+ }
2429
+ function joinBuild(runtime, state, services, log) {
2430
+ runtime.relays.set(
2431
+ state.buildId,
2432
+ superviseRelay({
2433
+ container: state.container,
2434
+ services: services.streams,
2435
+ execStream: (input) => runtime.host.execStream(input),
2436
+ log,
2437
+ onFatal: (reason) => runtime.relayFatal.set(state.buildId, reason)
2438
+ })
2439
+ );
2440
+ }
2441
+ async function leaveBuild(runtime, state) {
2442
+ const relay = runtime.relays.get(state.buildId);
2443
+ if (relay === void 0) return;
2444
+ runtime.relays.delete(state.buildId);
2445
+ await relay.close();
2446
+ }
2447
+ function rejoinBuild(runtime, state, log) {
2448
+ const services = runtime.tunnels.get(state.buildId);
2449
+ if (services?.streams === void 0 || runtime.relays.has(state.buildId) || transportOf(state) !== "exec") return;
2450
+ joinBuild(runtime, state, { streams: services.streams }, log);
2451
+ }
2000
2452
  var RELAY_PROBE = `const s=require("net").connect(${RELAY_PORT},"127.0.0.1");s.on("connect",()=>process.exit(0));s.on("error",()=>process.exit(1));setTimeout(()=>process.exit(1),2000)`;
2001
2453
  async function waitForRelay(runtime, state) {
2002
2454
  for (let attempt = 0; attempt < RELAY_ATTEMPTS; attempt++) {
2003
2455
  const probe = await runtime.host.exec({
2004
2456
  container: state.container,
2005
- user: ADDED_USER,
2457
+ user: relayUserOf(state),
2006
2458
  env: NO_ENV,
2007
2459
  argv: ["/opt/outerlayer/bin/node", "-e", RELAY_PROBE]
2008
2460
  });
@@ -2116,8 +2568,11 @@ async function provisionContainer(input) {
2116
2568
  builtIns: true,
2117
2569
  lifecycle: report.apply.lifecycle,
2118
2570
  ...governingSource.kind === "governed" ? { governing: { repository: governingSource.repository, ref: governingSource.ref, commit: governingSource.sha } } : {},
2119
- cpuset: formatCpuList(runtime.cpus.acquire(buildId, config.build?.cpus ?? 4))
2571
+ cpuset: formatCpuList(runtime.cpus.acquire(buildId, config.build?.cpus ?? 4)),
2572
+ transport: "exec",
2573
+ jobFiles: "copied"
2120
2574
  };
2575
+ const secrets = secretsOf(runtime, jobDir);
2121
2576
  const envInput = {
2122
2577
  config,
2123
2578
  item: item.number,
@@ -2152,10 +2607,11 @@ async function provisionContainer(input) {
2152
2607
  const dockerFailed = (what, result) => bail(`${what} failed (exit ${result.status})${firstLine5(result.stderr) === "" ? "" : `: ${firstLine5(result.stderr)}`}`);
2153
2608
  if (input.aborted()) return bail("", true);
2154
2609
  mkdirSync(buildDir2, { recursive: true });
2155
- chmodSync(buildDir2, 493);
2156
2610
  const ghDir = ghConfigDir(buildDir2);
2611
+ let readToken;
2157
2612
  try {
2158
- writeGhConfig(ghDir, await tokens.token("read"));
2613
+ readToken = await tokens.token("read");
2614
+ writeGhConfig(ghDir, readToken);
2159
2615
  } catch (err) {
2160
2616
  return bail(
2161
2617
  err instanceof WorkCommandError ? `the build's read token could not be had: ${refusalCode(err)}` : `the build's gh configuration could not be written: ${describeCallFailure(err)}`
@@ -2172,9 +2628,11 @@ async function provisionContainer(input) {
2172
2628
  keepGhConfigCurrent(tokens, buildDir2, (err) => log.error(`${tag} the read token could not be written for gh: ${describeCallFailure(err)}`));
2173
2629
  tokens.markUsed("read");
2174
2630
  environment.credentials = tokens.used();
2631
+ let services;
2175
2632
  try {
2176
2633
  const gitRemote = gitRemoteFor(runtime, { repositories, tokens, buildDir: buildDir2, log: (line) => log.line(`${tag} ${line}`) });
2177
- runtime.tunnels.set(buildId, await startServicesFor(runtime, config, buildDir2, jobDir, itemKey, true, gitRemote));
2634
+ services = await startServicesFor(runtime, config, buildDir2, jobDir, itemKey, true, gitRemote, "stream", input.leaseExpiryMs === void 0 ? void 0 : leaseFor(runtime, buildId, input.leaseExpiryMs));
2635
+ runtime.tunnels.set(buildId, services);
2178
2636
  } catch (err) {
2179
2637
  return bail(`the tunnel could not start: ${describeCallFailure(err)}`);
2180
2638
  }
@@ -2183,28 +2641,35 @@ async function provisionContainer(input) {
2183
2641
  const volume = await host.createVolume(name, { [BUILD_LABEL]: buildId, [RUNNER_LABEL]: runnerId });
2184
2642
  if (volume.status !== 0) return dockerFailed("creating the build's volume", volume);
2185
2643
  if (input.aborted()) return bail("", true);
2186
- const judgeDir = join(jobDir, "judge");
2187
- mkdirSync(judgeDir, { recursive: true });
2188
- chmodSync(judgeDir, 493);
2189
2644
  const runInput = {
2190
2645
  buildId,
2191
2646
  runnerId,
2192
2647
  container: name,
2193
2648
  volume: name,
2194
- buildDir: buildDir2,
2195
2649
  image: report.imageId,
2196
2650
  user: ADDED_USER,
2197
2651
  limits: limits2,
2198
2652
  destinations: config.build?.destinations?.length ?? 0,
2199
2653
  builtIns: true,
2200
- judgeDir
2654
+ transport: "exec",
2655
+ jobFiles: "copied"
2201
2656
  };
2202
2657
  const prepared = await host.run(prepareRunArgs(runInput), { logPath });
2203
2658
  if (prepared.status !== 0) return dockerFailed("preparing the build's volume", prepared);
2204
2659
  const started = await host.run(containerRunArgs(runInput), { logPath });
2205
2660
  if (started.status !== 0) return dockerFailed("starting the build's container", started);
2661
+ joinBuild(runtime, state, services, (line) => log.line(`${tag} ${line}`));
2206
2662
  if (input.aborted()) return bail("", true);
2207
2663
  if (!await waitForRelay(runtime, state)) return bail("the relay in the build's container did not start");
2664
+ const ghDelivered = await deliverGhConfig(runtime, state, secrets);
2665
+ if (!ghDelivered.ok) return bail(ghDelivered.reason);
2666
+ keepGhConfigCurrent(tokens, buildDir2, (err) => log.error(`${tag} the read token could not be written for gh: ${describeCallFailure(err)}`), {
2667
+ runtime,
2668
+ state,
2669
+ secrets,
2670
+ delivered: readToken,
2671
+ onFailure: (reason) => log.error(`${tag} ${reason}`)
2672
+ });
2208
2673
  const imageConfig = await host.imageConfig(report.imageId);
2209
2674
  state.imageEnv = imageConfig.env;
2210
2675
  let agentUser;
@@ -2243,7 +2708,7 @@ async function provisionContainer(input) {
2243
2708
  log,
2244
2709
  tag,
2245
2710
  aborted: input.aborted,
2246
- // The control plane is read on the host, with a token the gateway issues (or the host's git login when it has no GitHub App). The container reads the staged files from its mount of the build's directory.
2711
+ // The control plane is read on the host, with a token the gateway issues (or the host's git login when it has no GitHub App). The staged files are copied into the container.
2247
2712
  stageContext: () => stageGovernedContext({
2248
2713
  config,
2249
2714
  repository,
@@ -2256,6 +2721,10 @@ async function provisionContainer(input) {
2256
2721
  log,
2257
2722
  tag
2258
2723
  }),
2724
+ deliverContext: async () => {
2725
+ const delivered = await deliverJobFiles(runtime, state, ["context"], secrets);
2726
+ return delivered.ok ? delivered : { ok: false, reason: `context_unavailable: ${delivered.reason}` };
2727
+ },
2259
2728
  removeStagedContext: () => removeStagedContext(buildDir2)
2260
2729
  });
2261
2730
  if (!steps.ok) return bail(steps.reason, steps.aborted === true);
@@ -2274,7 +2743,7 @@ function agentVersionOf(toolsJson) {
2274
2743
  function shellQuote(word) {
2275
2744
  return /^[A-Za-z0-9_@%+=:,./-]+$/.test(word) ? word : `'${word.replace(/'/g, `'\\''`)}'`;
2276
2745
  }
2277
- function containerCommandLine(input) {
2746
+ function containerCommandExec(input) {
2278
2747
  const state = input.job.container;
2279
2748
  const env = buildEnvironment({
2280
2749
  config: input.config,
@@ -2289,16 +2758,13 @@ function containerCommandLine(input) {
2289
2758
  identity: input.job.commitIdentity,
2290
2759
  imageEnv: state.imageEnv
2291
2760
  });
2292
- const args = containerExecArgs({
2761
+ return {
2293
2762
  container: state.container,
2294
2763
  user: state.agentUser ?? ADDED_USER,
2295
2764
  workdir: CHECKOUT_DIR,
2296
2765
  env,
2297
- argv: ["/bin/sh", "-c", commandWithShim(input.job, input.cmdline)]
2298
- });
2299
- return {
2300
- line: ["docker", "exec", ...args].map(shellQuote).join(" "),
2301
- env: { ...childEnvironment(input.runtime.hostEnv), ...env.passed }
2766
+ argv: ["outerlayer", "build-command", "--", "/bin/sh", "-c", commandWithShim(input.job, input.cmdline)],
2767
+ detached: true
2302
2768
  };
2303
2769
  }
2304
2770
  function containerSenderCommand(input) {
@@ -2329,15 +2795,22 @@ function senderSignalScript(pidfile, signal) {
2329
2795
  const file = shellQuote(pidfile);
2330
2796
  return `p="$(cat ${file})" && [ -n "$p" ] && grep -qa session-sender "/proc/$p/cmdline" && kill -${signal} "$p"`;
2331
2797
  }
2798
+ function senderAliveScript(pidfile) {
2799
+ const file = shellQuote(pidfile);
2800
+ return `p="$(cat ${file})" && [ -n "$p" ] && grep -qa session-sender "/proc/$p/cmdline" && echo alive`;
2801
+ }
2332
2802
  async function signalInContainer(runtime, state, pidfile, signal) {
2333
- await runtime.host.exec({
2803
+ const { status } = await runtime.host.exec({
2334
2804
  container: state.container,
2335
2805
  user: state.agentUser ?? ADDED_USER,
2336
2806
  env: NO_ENV,
2337
2807
  argv: ["/bin/sh", "-c", senderSignalScript(pidfile, signal)]
2338
2808
  });
2809
+ return status === 0;
2339
2810
  }
2340
2811
  async function stopContainer(runtime, state) {
2812
+ runtime.leases.get(state.buildId)?.stop();
2813
+ await leaveBuild(runtime, state);
2341
2814
  await runtime.host.stop(state.container);
2342
2815
  state.stopped = true;
2343
2816
  }
@@ -2350,7 +2823,7 @@ async function measureDisk(runtime, state) {
2350
2823
  });
2351
2824
  return result.status === 0 ? parseDuKilobytes(result.stdout) : void 0;
2352
2825
  }
2353
- async function startServicesFor(runtime, config, buildDir2, jobDir, itemKey, builtIns, gitRemote) {
2826
+ async function startServicesFor(runtime, config, buildDir2, jobDir, itemKey, builtIns, gitRemote, serve = "socket", lease) {
2354
2827
  return startBuildServices({
2355
2828
  startTunnel: runtime.startTunnel,
2356
2829
  tunnelOptions: { ...runtime.tunnelOptions, ...gitRemote === void 0 ? {} : { gitRemote } },
@@ -2358,28 +2831,58 @@ async function startServicesFor(runtime, config, buildDir2, jobDir, itemKey, bui
2358
2831
  jobDir,
2359
2832
  destinations: config.build?.destinations ?? [],
2360
2833
  allowHosts: config.build?.allowHosts,
2361
- serve: "socket",
2834
+ serve,
2835
+ ...lease === void 0 ? {} : { lease },
2362
2836
  ...builtIns ? { builtIns: { gatewayUrl: config.url, itemKey, claudeUpstream: runtime.claudeUpstream } } : {},
2363
2837
  hostEnv: runtime.hostEnv,
2364
2838
  destinationOptions: runtime.destinationOptions
2365
2839
  });
2366
2840
  }
2367
- async function ensureTunnel(runtime, state, jobDir, config, itemKey, branch, repositories) {
2841
+ function leaseFor(runtime, buildId, expiryMs) {
2842
+ let channel = runtime.leases.get(buildId);
2843
+ if (channel === void 0) {
2844
+ channel = createLeaseChannel(expiryMs);
2845
+ runtime.leases.set(buildId, channel);
2846
+ }
2847
+ return channel;
2848
+ }
2849
+ async function ensureTunnel(runtime, state, jobDir, config, itemKey, branch, repositories, log, leaseExpiryMs) {
2368
2850
  if (runtime.tunnels.has(state.buildId)) return;
2851
+ const transport = transportOf(state);
2852
+ if (transport === "unknown") {
2853
+ runtime.relayFatal.set(state.buildId, UNKNOWN_TRANSPORT_REASON);
2854
+ return;
2855
+ }
2369
2856
  const tokens = runtime.credentials.get(jobDir);
2370
2857
  const served = repositoriesOf({ repositories, repository: state.repository, branch });
2371
2858
  const gitRemote = tokens === void 0 || served.length === 0 ? void 0 : gitRemoteFor(runtime, { repositories: served, tokens, buildDir: state.buildDir });
2372
- runtime.tunnels.set(state.buildId, await startServicesFor(runtime, config, state.buildDir, jobDir, itemKey, state.builtIns === true, gitRemote));
2859
+ const lease = transport === "exec" && leaseExpiryMs !== void 0 ? leaseFor(runtime, state.buildId, leaseExpiryMs) : void 0;
2860
+ if (state.stopped === true) lease?.stop();
2861
+ const services = await startServicesFor(runtime, config, state.buildDir, jobDir, itemKey, state.builtIns === true, gitRemote, transport === "exec" ? "stream" : "socket", lease);
2862
+ runtime.tunnels.set(state.buildId, services);
2863
+ if (transport === "exec") joinBuild(runtime, state, services, log);
2864
+ }
2865
+ async function redeliver(runtime, state, jobDir) {
2866
+ if (!filesAreCopied(state)) return { ok: true };
2867
+ const parts = ["gh", "git-hooks", "context"];
2868
+ return deliverJobFiles(runtime, state, parts, secretsOf(runtime, jobDir));
2869
+ }
2870
+ async function deliverContainerHooks(runtime, state, jobDir) {
2871
+ if (!filesAreCopied(state)) return { ok: true };
2872
+ return deliverJobFiles(runtime, state, ["git-hooks"], secretsOf(runtime, jobDir));
2373
2873
  }
2374
2874
  async function syncInContainer(input) {
2375
2875
  const { runtime, job } = input;
2376
2876
  const state = job.container;
2377
2877
  try {
2378
- await ensureTunnel(runtime, state, input.jobDir, input.config, job.itemKey, job.branch, job.repositories);
2878
+ await ensureTunnel(runtime, state, input.jobDir, input.config, job.itemKey, job.branch, job.repositories, input.log, () => leaseExpiresAtMs(job));
2379
2879
  if (state.stopped) {
2380
2880
  await runtime.host.start(state.container);
2381
2881
  state.stopped = false;
2382
2882
  if (!await waitForRelay(runtime, state)) return { ok: false, reason: "the relay did not start again" };
2883
+ rejoinBuild(runtime, state, input.log);
2884
+ const delivered = await redeliver(runtime, state, input.jobDir);
2885
+ if (!delivered.ok) return { ok: false, reason: delivered.reason };
2383
2886
  }
2384
2887
  const env = buildEnvironment({
2385
2888
  config: input.config,
@@ -2430,6 +2933,10 @@ async function copyHookErrors(runtime, state, jobDir) {
2430
2933
  async function cleanupContainer(runtime, state, jobDir) {
2431
2934
  const failures = [];
2432
2935
  if (jobDir !== void 0) await copyHookErrors(runtime, state, jobDir);
2936
+ await leaveBuild(runtime, state);
2937
+ runtime.relayFatal.delete(state.buildId);
2938
+ runtime.leases.get(state.buildId)?.close();
2939
+ runtime.leases.delete(state.buildId);
2433
2940
  const removedContainer = await runtime.host.remove(state.container);
2434
2941
  if (removedContainer.status !== 0 && !/no such container/i.test(removedContainer.stderr)) {
2435
2942
  failures.push(`container ${state.container} was not removed: ${removedContainer.stderr.trim()}`);
@@ -2457,12 +2964,15 @@ function containerJudgeEnvironment(input) {
2457
2964
  const folder = join(input.jobDir, "judge");
2458
2965
  return {
2459
2966
  stop: async () => {
2460
- if (input.config !== void 0) await ensureTunnel(runtime, state, input.jobDir, input.config, job.itemKey, job.branch, job.repositories);
2967
+ if (input.config !== void 0) await ensureTunnel(runtime, state, input.jobDir, input.config, job.itemKey, job.branch, job.repositories, input.log);
2461
2968
  await stopContainer(runtime, state);
2462
2969
  const started = await runtime.host.start(state.container);
2463
2970
  if (started.status !== 0) throw new Error(`the build's container could not be started again (exit ${started.status})`);
2464
2971
  state.stopped = false;
2465
2972
  if (!await waitForRelay(runtime, state)) throw new Error("the relay in the build's container did not start again");
2973
+ rejoinBuild(runtime, state, input.log);
2974
+ const delivered = await redeliver(runtime, state, input.jobDir);
2975
+ if (!delivered.ok) throw new Error(delivered.reason);
2466
2976
  },
2467
2977
  hashProgram: async () => {
2468
2978
  const ran = await runtime.host.exec({ container: state.container, user: ADDED_USER, env: NO_ENV, argv: [...HASH_ARGV] });
@@ -2470,6 +2980,20 @@ function containerJudgeEnvironment(input) {
2470
2980
  return sha256 === void 0 ? { ok: false, reason: firstLine5(ran.stderr) || `the hash could not be taken (exit ${ran.status})` } : { ok: true, sha256 };
2471
2981
  },
2472
2982
  prepare: async (files, tree) => {
2983
+ if (filesAreCopied(state)) {
2984
+ const staging = join(input.jobDir, "judge-input");
2985
+ rmSync(staging, { recursive: true, force: true });
2986
+ mkdirSync(join(staging, "home", "tmp"), { recursive: true });
2987
+ try {
2988
+ for (const [fileName, text] of Object.entries(files)) writeFileSync(join(staging, fileName), text, { mode: 420 });
2989
+ if (tree !== void 0) copyJudgeTree(tree, join(staging, "tree"));
2990
+ const delivered = await deliverJudgeInputs(runtime, state, staging, secretsOf(runtime, input.jobDir));
2991
+ if (!delivered.ok) throw new Error(delivered.reason);
2992
+ } finally {
2993
+ rmSync(staging, { recursive: true, force: true });
2994
+ }
2995
+ return;
2996
+ }
2473
2997
  mkdirSync(folder, { recursive: true });
2474
2998
  for (const entry of readdirSync(folder)) rmSync(join(folder, entry), { recursive: true, force: true });
2475
2999
  mkdirSync(join(folder, "home", "tmp"), { recursive: true });
@@ -2480,12 +3004,26 @@ function containerJudgeEnvironment(input) {
2480
3004
  chmodSync(folder, 1023);
2481
3005
  },
2482
3006
  run: (command, opts) => runtime.host.exec({ container: state.container, user, workdir: command.cwd, env: { literals: command.env, passed: {} }, argv: command.argv }, { logPath: opts.logPath }),
2483
- readReport: async () => readRegularFile(join(folder, "report.html"), MAX_REPORT_BYTES),
3007
+ readReport: async () => {
3008
+ if (!filesAreCopied(state)) return readRegularFile(join(folder, "report.html"), MAX_REPORT_BYTES);
3009
+ const file = JUDGE_REPORT;
3010
+ const read = await runtime.host.exec({
3011
+ container: state.container,
3012
+ user,
3013
+ env: NO_ENV,
3014
+ argv: ["/bin/sh", "-c", `[ -f ${file} ] && [ ! -L ${file} ] && head -c ${MAX_REPORT_BYTES + 1} ${file}`]
3015
+ }, { timeoutMs: REPORT_READ_TIMEOUT_MS });
3016
+ if (read.status > 1) throw new Error(`reading the judge report exited ${read.status}: ${firstLine5(read.stderr)}`);
3017
+ return read.status === 0 && Buffer.byteLength(read.stdout) <= MAX_REPORT_BYTES ? read.stdout : void 0;
3018
+ },
2484
3019
  // 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
2485
3020
  // 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.
2486
3021
  clear: async () => {
2487
3022
  const removed = await runtime.host.exec({ container: state.container, user, env: NO_ENV, argv: ["find", JUDGE_DIR, "-mindepth", "1", "-user", user, "-delete"] });
2488
3023
  if (removed.status !== 0) throw new Error(`find exited ${removed.status}: ${firstLine5(removed.stderr)}`);
3024
+ if (!filesAreCopied(state)) return;
3025
+ const emptied = await clearJudgeFolder(runtime, state, secretsOf(runtime, input.jobDir));
3026
+ if (!emptied.ok) throw new Error(emptied.reason);
2489
3027
  }
2490
3028
  };
2491
3029
  }
@@ -2511,7 +3049,21 @@ async function startCheckContainer(input) {
2511
3049
  const buildDir2 = buildDir(config.configPath, buildId);
2512
3050
  const agentUser = built.agentUser ?? ADDED_USER;
2513
3051
  const cpuset = built.cpuset ?? formatCpuList(runtime.cpus.acquire(buildId, config.build?.cpus ?? 4));
2514
- const state = { buildId, container: name, volume: name, buildDir: buildDir2, repository, imageId: built.imageId, imageEnv: built.imageEnv, agentUser, cpuset };
3052
+ const transport = transportOf(built);
3053
+ if (transport === "unknown") return { ok: false, reason: UNKNOWN_TRANSPORT_REASON };
3054
+ const state = {
3055
+ buildId,
3056
+ container: name,
3057
+ volume: name,
3058
+ buildDir: buildDir2,
3059
+ repository,
3060
+ imageId: built.imageId,
3061
+ imageEnv: built.imageEnv,
3062
+ agentUser,
3063
+ cpuset,
3064
+ transport,
3065
+ ...transport === "exec" ? { jobFiles: "copied" } : {}
3066
+ };
2515
3067
  const checksDir = join(jobDir, "checks");
2516
3068
  mkdirSync(checksDir, { recursive: true });
2517
3069
  const logPath = join(checksDir, "environment.log");
@@ -2526,7 +3078,8 @@ async function startCheckContainer(input) {
2526
3078
  };
2527
3079
  const dockerFailed = (what, result) => fail(`${what} failed (exit ${result.status})${firstLine5(result.stderr) === "" ? "" : `: ${firstLine5(result.stderr)}`}`);
2528
3080
  mkdirSync(buildDir2, { recursive: true });
2529
- chmodSync(buildDir2, 493);
3081
+ if (!filesAreCopied(state)) chmodSync(buildDir2, 493);
3082
+ let services;
2530
3083
  try {
2531
3084
  const gitRemote = gitRemoteFor(runtime, {
2532
3085
  repositories: repositoriesOf({ repositories: job.repositories, repository, branch: job.branch }),
@@ -2535,7 +3088,8 @@ async function startCheckContainer(input) {
2535
3088
  log: (line) => log.line(`${tag} ${line}`),
2536
3089
  readOnly: true
2537
3090
  });
2538
- runtime.tunnels.set(buildId, await startServicesFor(runtime, config, buildDir2, checksDir, void 0, false, gitRemote));
3091
+ services = await startServicesFor(runtime, config, buildDir2, checksDir, void 0, false, gitRemote, transport === "exec" ? "stream" : "socket");
3092
+ runtime.tunnels.set(buildId, services);
2539
3093
  } catch (err) {
2540
3094
  return fail(`the tunnel could not start: ${describeCallFailure(err)}`);
2541
3095
  }
@@ -2548,33 +3102,59 @@ async function startCheckContainer(input) {
2548
3102
  runnerId,
2549
3103
  container: name,
2550
3104
  volume: name,
2551
- buildDir: buildDir2,
3105
+ ...filesAreCopied(state) ? { jobFiles: "copied" } : { buildDir: buildDir2 },
2552
3106
  image: built.imageId,
2553
3107
  user: ADDED_USER,
2554
3108
  limits: limits2,
2555
3109
  destinations: config.build?.destinations?.length ?? 0,
2556
- builtIns: false
3110
+ builtIns: false,
3111
+ transport
2557
3112
  };
2558
3113
  const prepared = await host.run(prepareRunArgs(runInput), { logPath });
2559
3114
  if (prepared.status !== 0) return dockerFailed("preparing the check environment's volume", prepared);
2560
3115
  const started = await host.run(containerRunArgs(runInput), { logPath });
2561
3116
  if (started.status !== 0) return dockerFailed("starting the check environment's container", started);
3117
+ if (transport === "exec") joinBuild(runtime, state, services, (line) => log.line(`${tag} ${line}`));
2562
3118
  if (!await waitForRelay(runtime, state)) return fail("the relay in the check environment's container did not start");
2563
3119
  const env = checkEnvironment({ config, item: job.item, repository, hostEnv: runtime.hostEnv, containerEnv: built.containerEnv ?? {}, imageEnv: built.imageEnv });
3120
+ const secrets = () => tokens.issuedTokens();
3121
+ let delivered = false;
3122
+ const deliverInput = async () => {
3123
+ const result = await deliverEvalInput(runtime, state, secrets);
3124
+ if (result.ok) delivered = true;
3125
+ return result;
3126
+ };
2564
3127
  return {
2565
3128
  ok: true,
2566
3129
  container: {
2567
3130
  state,
2568
- // The directory the container sees read-only at `JOB_MOUNT` is the build's directory on the host, so a folder written there is already in place.
2569
- evalInput: { dir: join(buildDir2, EVAL_INPUT_NAME), deliver: async () => void 0 },
3131
+ // A container that mounts the build's directory already has the folder the runner wrote there; any other is given a copy.
3132
+ evalInput: {
3133
+ dir: join(buildDir2, EVAL_INPUT_NAME),
3134
+ deliver: async () => {
3135
+ if (!filesAreCopied(state)) return;
3136
+ const result = await deliverInput();
3137
+ if (!result.ok) throw new DeliveryFailure(result.reason);
3138
+ }
3139
+ },
2570
3140
  exec: (argv, workdir, opts) => host.exec({ container: name, user: agentUser, workdir, env, argv }, opts?.logPath === void 0 ? void 0 : { logPath: opts.logPath }),
2571
3141
  restart: async () => {
3142
+ await leaveBuild(runtime, state);
2572
3143
  await host.stop(name);
2573
3144
  const again = await host.start(name);
2574
- return again.status === 0 && await waitForRelay(runtime, state);
3145
+ if (again.status !== 0 || !await waitForRelay(runtime, state)) return false;
3146
+ rejoinBuild(runtime, state, (line) => log.line(`${tag} ${line}`));
3147
+ if (delivered) {
3148
+ const again2 = await deliverInput();
3149
+ if (!again2.ok) {
3150
+ log.error(`${tag} ${again2.reason}`);
3151
+ return false;
3152
+ }
3153
+ }
3154
+ return true;
2575
3155
  }
2576
3156
  }
2577
3157
  };
2578
3158
  }
2579
3159
 
2580
- 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 };
3160
+ export { ADDED_USER, AgentUserError, AssetError, DeliveryFailure, EVAL_INPUT_NAME, GH_CONFIG_YML, LEASE_SECONDS, MIN_ENGINE_MAJOR, cleanupContainer, containerCommandExec, containerJudgeEnvironment, containerSenderCommand, copyJudgeTree, copyTranscripts, copyTranscriptsFromContainer, createBuildDisk, createContainerRuntime, deleteBuildDisk, deliverContainerHooks, ensureRootfs, ensureTunnel, filesAreCopied, ghHostsYml, gitRemoteFor, isLiteralAddress, keepGhConfigCurrent, leaseExpiresAtMs, lifecycleFailureReason, lifecycleSteps, machineConfig, measureDisk, obtainImage, outOfMemoryReason, parseCpuList, prepareBranch, provisionContainer, resolveAgentUser, runBuildSteps, senderAliveScript, senderSignalScript, setUpRunnerCgroup, shellQuote, signalInContainer, startCheckContainer, startServicesFor, stopContainer, syncInContainer, writeEvalInput };