@outerlayer/cli 0.34.0-next.20261026145838 → 0.34.0-next.20261026182624

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 (125) hide show
  1. package/README.md +6 -0
  2. package/dist/{agent-setup-FNDKCPUP.js → agent-setup-WOEZELVB.js} +6 -6
  3. package/dist/{bridge-YJBB4YA3.js → bridge-F5FBQSPZ.js} +2 -2
  4. package/dist/{build-ZJK6CPNH.js → build-UDOBVX5J.js} +7 -7
  5. package/dist/{build-disk-7HS4UF7O.js → build-disk-C3WIAHCL.js} +6 -6
  6. package/dist/build-info.json +1 -1
  7. package/dist/{builder-MHZKDI4H.js → builder-YNXWVH2V.js} +5 -5
  8. package/dist/{check-4W4QUBMP.js → check-G2K76NEM.js} +39 -38
  9. package/dist/{chunk-2ILU7UAU.js → chunk-3VZTNXK5.js} +1 -1
  10. package/dist/{chunk-4YFMNF5Y.js → chunk-4J2FF5HZ.js} +3 -3
  11. package/dist/{chunk-CAQCDYKF.js → chunk-5JGPTYLT.js} +2 -2
  12. package/dist/chunk-6D7H5DMZ.js +152 -0
  13. package/dist/{chunk-OGPEHSNW.js → chunk-6KFJIU73.js} +1 -1
  14. package/dist/{chunk-3EJBI5JW.js → chunk-7FQYTM7M.js} +1 -1
  15. package/dist/{chunk-3N77LUQI.js → chunk-7QAJQDHS.js} +1 -1
  16. package/dist/{chunk-SI6WDF3U.js → chunk-CFFVCI2I.js} +1 -1
  17. package/dist/{chunk-7P3H4RY2.js → chunk-CNZRUVZ5.js} +102 -36
  18. package/dist/{chunk-WMNPKBX5.js → chunk-ELO5XEES.js} +4 -4
  19. package/dist/{chunk-PP6OX4OT.js → chunk-EPFFYDNX.js} +5 -5
  20. package/dist/{chunk-CY5TCVBS.js → chunk-ERTIEYDG.js} +6 -6
  21. package/dist/{chunk-35JMYSUD.js → chunk-EUQZAH2W.js} +2 -2
  22. package/dist/{chunk-WHDS6MEG.js → chunk-GHFI5GBF.js} +3 -3
  23. package/dist/{chunk-PXGU3GKY.js → chunk-GJ4KBHBD.js} +1 -1
  24. package/dist/chunk-GYVMMU3U.js +321 -0
  25. package/dist/{chunk-VVOSUGEF.js → chunk-H47VGSCW.js} +4 -4
  26. package/dist/{chunk-TFPNPS4Q.js → chunk-HOUFCUMH.js} +1 -1
  27. package/dist/{chunk-7JE2WFNK.js → chunk-HVPW272H.js} +3 -3
  28. package/dist/{chunk-M3ME6TLQ.js → chunk-HVQMIZ22.js} +4 -4
  29. package/dist/{chunk-7DXVL7TN.js → chunk-IO2BBKG4.js} +3 -3
  30. package/dist/{chunk-UONH72CJ.js → chunk-IWVGALDC.js} +2 -2
  31. package/dist/{chunk-ZP7GKJSM.js → chunk-JC6AQ6PC.js} +39 -6
  32. package/dist/{chunk-6JZXIWX7.js → chunk-JKNOJKVD.js} +34 -9
  33. package/dist/{chunk-GEQT3RPN.js → chunk-JSZRBQY3.js} +3 -3
  34. package/dist/{chunk-MOI2KDLH.js → chunk-L5VHGRQE.js} +1 -1
  35. package/dist/{chunk-SKVA4KWD.js → chunk-LD3BHLK7.js} +3 -3
  36. package/dist/{chunk-YNMVVQOQ.js → chunk-LQHCNXPK.js} +1 -1
  37. package/dist/{chunk-PWSUNS6Y.js → chunk-LTN2YFLR.js} +99 -31
  38. package/dist/{chunk-YHKU2NOZ.js → chunk-LTYBHCL4.js} +1 -1
  39. package/dist/{chunk-PAK5P2ND.js → chunk-MH6QLXUO.js} +1 -1
  40. package/dist/{chunk-OS7DJOQJ.js → chunk-MN3PP45X.js} +1 -1
  41. package/dist/{chunk-25GRRBTH.js → chunk-NHEU5CON.js} +1 -1
  42. package/dist/{chunk-IM6IL3F4.js → chunk-OQT7RT5I.js} +3 -3
  43. package/dist/{chunk-KRTD2F3I.js → chunk-P3CHDKXD.js} +3 -3
  44. package/dist/{chunk-63IVRRIO.js → chunk-P3NPTFLF.js} +3 -3
  45. package/dist/{chunk-GC5PVMIJ.js → chunk-RR3VDM7M.js} +3 -3
  46. package/dist/{chunk-G4W4B33M.js → chunk-SDAEXUIX.js} +25 -10
  47. package/dist/{chunk-SYVRBBLW.js → chunk-SDELQEG4.js} +1 -1
  48. package/dist/{chunk-AE3OTPUU.js → chunk-SZU4YCBH.js} +964 -1056
  49. package/dist/{chunk-FDJPDZSR.js → chunk-TS3EFB7S.js} +1 -1
  50. package/dist/{chunk-EMH2LNIE.js → chunk-V2VJYK46.js} +4 -4
  51. package/dist/{chunk-KAVZAB3E.js → chunk-VEJWLQ4G.js} +10 -10
  52. package/dist/{chunk-XTYYF6P7.js → chunk-VUQKGBIQ.js} +14 -7
  53. package/dist/{chunk-3RLKILPT.js → chunk-WE4C3ODP.js} +2 -2
  54. package/dist/{chunk-QEIA5Q35.js → chunk-XL2MPORM.js} +4 -4
  55. package/dist/{chunk-VGRCZE6U.js → chunk-XSFR76J7.js} +3 -3
  56. package/dist/{chunk-UMYYYF7B.js → chunk-YL3RWW7X.js} +1 -1
  57. package/dist/{chunk-GSSVJNP2.js → chunk-Z7WMU3RA.js} +3 -2
  58. package/dist/{chunk-3EHDZTRT.js → chunk-ZIKZZ4LK.js} +3 -3
  59. package/dist/{claude-launcher-OQZNW7XJ.js → claude-launcher-6KGANL7E.js} +1 -1
  60. package/dist/{cli-PFWJD5QM.js → cli-5KY3TE3S.js} +114 -112
  61. package/dist/{command-CM22HPN5.js → command-VQPQDPCZ.js} +24 -24
  62. package/dist/{config-W2XWNR4C.js → config-WSZIWPP4.js} +7 -7
  63. package/dist/{connect-cmd-TOWE4P45.js → connect-cmd-LI34HS5Q.js} +6 -6
  64. package/dist/{container-start-D2OEPMUO.js → container-start-XRR4VC5Z.js} +31 -30
  65. package/dist/{context-show-cmd-5UJUWBJL.js → context-show-cmd-X27GMOT6.js} +7 -7
  66. package/dist/{control-plane-launch-XBGHM7DB.js → control-plane-launch-74EIIB5Z.js} +5 -5
  67. package/dist/dist-HVXLUMRV.js +7 -0
  68. package/dist/{docker-GA63DVME.js → docker-KZ3OQTPW.js} +5 -5
  69. package/dist/{doctor-GVWQBHBC.js → doctor-KPKLFIJH.js} +24 -24
  70. package/dist/{emit-artifact-cmd-TUOLSGHB.js → emit-artifact-cmd-BJICHW2E.js} +11 -11
  71. package/dist/{emit-criteria-cmd-VEUMA7MZ.js → emit-criteria-cmd-MUWPT25P.js} +8 -8
  72. package/dist/{emit-demo-cmd-YLEZB6FG.js → emit-demo-cmd-YQN7VKQH.js} +12 -12
  73. package/dist/{emit-finding-cmd-BX3FWGSX.js → emit-finding-cmd-NPBWVY44.js} +8 -8
  74. package/dist/{emit-result-cmd-AYXGCIPR.js → emit-result-cmd-7CJZZQGK.js} +10 -10
  75. package/dist/{emit-step-cmd-CU6GQ3WW.js → emit-step-cmd-ZPLHZAMA.js} +8 -8
  76. package/dist/{exec-client-QB4OZRGM.js → exec-client-LXHTMVIB.js} +3 -3
  77. package/dist/find-unit-UKH6JH4S.js +18 -0
  78. package/dist/{findings-cmd-G4V4M3PO.js → findings-cmd-DZ4ESTHX.js} +8 -8
  79. package/dist/{fleet-readiness-NJYTQYTA.js → fleet-readiness-YSI4NOHT.js} +5 -5
  80. package/dist/{governing-source-JQV3ZKD7.js → governing-source-JPRMOB3O.js} +10 -10
  81. package/dist/{guest-init-IF5LXGUL.js → guest-init-WWL2WL7R.js} +4 -4
  82. package/dist/{hook-fast-GPUL7T77.js → hook-fast-6EW2MGA2.js} +1 -1
  83. package/dist/host-checks-NDXNYOMA.js +75 -0
  84. package/dist/{pending-ERXWVLLG.js → host-key-TSOKVSZX.js} +3 -3
  85. package/dist/{image-tag-SEMBVB5D.js → image-tag-BTXKIH73.js} +3 -3
  86. package/dist/index.js +4 -4
  87. package/dist/{init-FY3HP73H.js → init-MN3MNG7V.js} +21 -21
  88. package/dist/{init-3NT4YX2X.js → init-V6Y2VNKW.js} +21 -21
  89. package/dist/{init-cmd-RXHHEAOT.js → init-cmd-QCWLJDIF.js} +11 -11
  90. package/dist/{init-update-RHS6HWLI.js → init-update-GTFLPDEL.js} +9 -9
  91. package/dist/{install-cmd-5H6M2KJ7.js → install-cmd-RGO7NWKP.js} +5 -5
  92. package/dist/{logs-6CMB3DO6.js → logs-LEVH6VAI.js} +2 -2
  93. package/dist/{loop-V3GED2WX.js → loop-NGFNQF3Y.js} +38 -37
  94. package/dist/{machine-HLZLH327.js → machine-XG54XYYZ.js} +21 -21
  95. package/dist/{mcp-serve-cmd-MWGIN6YL.js → mcp-serve-cmd-4O5INM77.js} +4 -4
  96. package/dist/{paths-T2DEBI62.js → paths-AQ47ZRHK.js} +2 -2
  97. package/dist/{host-key-5RU322O3.js → pending-UWFDVFOA.js} +3 -3
  98. package/dist/{pidfile-EAD2OEM7.js → pidfile-PCG2H5T4.js} +3 -3
  99. package/dist/{record-IMXQNHPB.js → record-SVCNFCGQ.js} +3 -3
  100. package/dist/{runner-env-V52JUOD5.js → runner-env-YPXWTNP3.js} +9 -9
  101. package/dist/{runner-identity-J4NCBSBU.js → runner-identity-XPZO3TG4.js} +5 -5
  102. package/dist/{runner-key-ZUHMQCHZ.js → runner-key-B5EJH25Z.js} +21 -21
  103. package/dist/{session-sender-XDKMK4SF.js → session-sender-XED7CKHP.js} +11 -11
  104. package/dist/{spec-cmd-XQV7EIJW.js → spec-cmd-TRXAGQYM.js} +9 -9
  105. package/dist/{status-ZJJEJ52D.js → status-WQ53MWC3.js} +29 -28
  106. package/dist/{sync-cmd-BG2M536W.js → sync-cmd-UQVENF25.js} +10 -10
  107. package/dist/{update-steps-I5JLERXS.js → update-steps-IQVAL64Y.js} +30 -29
  108. package/dist/{watch-7A63565I.js → watch-EZGDDRTQ.js} +10 -10
  109. package/dist/{work-claim-cmd-OVPMBIRR.js → work-claim-cmd-YSAJ7DAK.js} +14 -14
  110. package/dist/{work-cmd-KVFRK24I.js → work-cmd-V5FHLWJM.js} +8 -8
  111. package/dist/{work-comment-cmd-GC3WAFM7.js → work-comment-cmd-KLEMDYI4.js} +9 -9
  112. package/dist/{work-demo-cmd-IOB5UDGQ.js → work-demo-cmd-XYRKFP4G.js} +8 -8
  113. package/dist/{work-logs-cmd-7II6D2JJ.js → work-logs-cmd-DOGPK7Q6.js} +8 -8
  114. package/dist/{work-open-pr-cmd-FYKRKCDL.js → work-open-pr-cmd-UAUEEP3V.js} +9 -9
  115. package/dist/{work-pr-cmd-KP3RI7TV.js → work-pr-cmd-LYWL7ZLF.js} +8 -8
  116. package/dist/{work-proofs-cmd-TVU6QFZN.js → work-proofs-cmd-YMMDAA6W.js} +11 -9
  117. package/dist/{work-proofs-record-XANZE3K4.js → work-proofs-record-NUCWR7ZJ.js} +17 -15
  118. package/dist/{work-stop-cmd-5V3DV3HJ.js → work-stop-cmd-MRLJEQRP.js} +8 -8
  119. package/package.json +1 -1
  120. package/skill-pack/maintained/outerlayer-amend/SKILL.md +2 -1
  121. package/skill-pack/maintained/outerlayer-build/SKILL.md +3 -1
  122. package/skill-pack/maintained/outerlayer-build/references/evidence.md +9 -3
  123. package/dist/chunk-YZ375YBI.js +0 -151
  124. package/dist/dist-IOPJIQWK.js +0 -7
  125. package/dist/find-unit-GGBRYOSO.js +0 -18
@@ -1,45 +1,65 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'node:module';
3
3
  import { readsJobState, READ_JOB_STATE_FORMATS, jobStateFormatOf } from './chunk-ZVB57WI6.js';
4
- import { DOCKERD_LOG, HELPER_PATH, HELPER_RECORD } from './chunk-63IVRRIO.js';
4
+ import { itemsSentence, criteriaOf, hostItemsOf, loadCitationFinder, MAX_TEST_FILE_BYTES, MAX_TEST_FILES, criterionInputs, planCriteria, parseJudgeAnswer, inCriteriaOrder, oneLineReason, judgedItems } from './chunk-6D7H5DMZ.js';
5
+ import { DOCKERD_LOG, HELPER_PATH, HELPER_RECORD } from './chunk-P3NPTFLF.js';
5
6
  import { zodProblem } from './chunk-I3LTIAOR.js';
6
- import { senderPidfile } from './chunk-SKVA4KWD.js';
7
- import { installScrubContext } from './chunk-XTYYF6P7.js';
8
- import { writeDockerClientConfig } from './chunk-SI6WDF3U.js';
7
+ import { senderPidfile } from './chunk-LD3BHLK7.js';
8
+ import { installScrubContext } from './chunk-VUQKGBIQ.js';
9
+ import { writeDockerClientConfig } from './chunk-CFFVCI2I.js';
9
10
  import { upgradedStarterText } from './chunk-XKJOIQ2Q.js';
10
- import { ADDED_USER, shellQuote, GH_CONFIG_YML, ghHostsYml, hasClaudeCredential, AssetError, obtainImage, ensureRootfs, makeBuildsDirPrivate, gitRemoteFor, startServicesFor, createLeaseChannel, createBuildDisk, machineConfig, startJailedInto, resolveAgentUser, AgentUserError, runBuildSteps, startHostServices, LEASE_SECONDS, createContainerRuntime, stopContainer, cleanupContainer, outOfMemoryReason, closeHostBuild, dropManagedClaudeKey, containerSenderCommand, hostCommandEnvironment, execArgv, signalInContainer, syncInContainer, copyTranscriptsFromContainer, MANAGED_UNJAILED_REASON, senderAliveScript, helperFirecrackerNote, managedClaudeKeyValues, stopJailed, deleteBuildDisk, managedClaudeKeyFor, startCheckContainer, lifecycleSteps, lifecycleFailureReason, writeEvalInput, DeliveryFailure, EVAL_INPUT_NAME, copyTranscripts, senderSignalScript, containerJudgeEnvironment, ROOT_DISK_ID_PREFIX, copyJudgeTree } from './chunk-7P3H4RY2.js';
11
- import { readBranchHead, fetchCommit, diffBetween } from './chunk-VVOSUGEF.js';
12
- import { waitForAgent, vmExecArgs } from './chunk-2ILU7UAU.js';
13
- import { socketPathProblem, vsockLinks, LEASE_LINK, linkVsockSockets } from './chunk-OGPEHSNW.js';
14
- import { describeCallFailure, resolveGoverningSource, prefetchGovernedContext, refusalCode, removeStagedContext, stageGovernedContext, pinnedSourceOf, commandWithShim, logStamp, createRunnerLog, removeKeptContext, formatDuration, retrySend, formatClock, WORKFLOW_PERMISSION_REASON, keptContextTree } from './chunk-PP6OX4OT.js';
15
- import { imageBuildOf, isBuiltinContainer, isBuiltinProcess, isBuiltinVm, DEFAULT_CHECK_TIME_LIMIT_MINUTES } from './chunk-3EHDZTRT.js';
16
- import { DEFAULT_CACHE_DISK, formatBytes, allocatedBytes } from './chunk-UONH72CJ.js';
17
- import { parseSize, HASH_ARGV, parseProgramHash, JUDGE_PROGRAM, writeJobFile, readJobFile, childEnvironment, withAnswerProtocol, judgeCommand, MAX_REPORT_BYTES, JUDGE_MAX_TURNS, JUDGE_DIR, COMMAND_DIR, MAX_ITEMS_BYTES } from './chunk-M3ME6TLQ.js';
11
+ import { ADDED_USER, shellQuote, GH_CONFIG_YML, ghHostsYml, hasClaudeCredential, AssetError, obtainImage, ensureRootfs, makeBuildsDirPrivate, gitRemoteFor, startServicesFor, createLeaseChannel, createBuildDisk, machineConfig, startJailedInto, resolveAgentUser, AgentUserError, runBuildSteps, createContainerRuntime, containerSenderCommand, hostCommandEnvironment, execArgv, signalInContainer, startHostServices, LEASE_SECONDS, managedBuildSettingsFor, stopContainer, cleanupContainer, outOfMemoryReason, closeHostBuild, dropManagedClaudeKey, dropManagedBuildSettings, startCheckContainer, lifecycleSteps, lifecycleFailureReason, writeEvalInput, DeliveryFailure, EVAL_INPUT_NAME, syncInContainer, copyTranscriptsFromContainer, MANAGED_UNJAILED_REASON, senderAliveScript, managedMachineLimits, withFactoryHosts, helperFirecrackerNote, managedClaudeKeyValues, managedBuildSettingValues, stopJailed, deleteBuildDisk, managedClaudeKeyFor, ROOT_DISK_ID_PREFIX, copyTranscripts, containerJudgeEnvironment, senderSignalScript, copyJudgeTree } from './chunk-CNZRUVZ5.js';
12
+ import { fetchCommit, readBranchHead, diffBetween } from './chunk-H47VGSCW.js';
13
+ import { waitForAgent, vmExecArgs } from './chunk-3VZTNXK5.js';
14
+ import { socketPathProblem, vsockLinks, LEASE_LINK, linkVsockSockets } from './chunk-6KFJIU73.js';
15
+ import { describeCallFailure, resolveGoverningSource, prefetchGovernedContext, refusalCode, removeStagedContext, stageGovernedContext, pinnedSourceOf, commandWithShim, logStamp, createRunnerLog, removeKeptContext, formatDuration, retrySend, formatClock, keptContextTree, WORKFLOW_PERMISSION_REASON } from './chunk-EPFFYDNX.js';
16
+ import { imageBuildOf, isBuiltinContainer, isBuiltinProcess, isBuiltinVm, DEFAULT_CHECK_TIME_LIMIT_MINUTES } from './chunk-ZIKZZ4LK.js';
17
+ import { formatBytes, DEFAULT_CACHE_DISK, allocatedBytes } from './chunk-IWVGALDC.js';
18
+ import { HASH_ARGV, parseProgramHash, JUDGE_PROGRAM, writeJobFile, readJobFile, parseSize, childEnvironment, withAnswerProtocol, judgeCommand, MAX_REPORT_BYTES, JUDGE_MAX_TURNS, JUDGE_DIR, COMMAND_DIR, MAX_ITEMS_BYTES } from './chunk-HVQMIZ22.js';
18
19
  import { CLAUDE_CODE_VERSION, FIRECRACKER_VERSION } from './chunk-4UPET3R7.js';
19
20
  import { isSameProcess, startTimeOf } from './chunk-JBJTC3UU.js';
20
- import { WorkCommandError, postJson, isSignatureRefusal, getJson } from './chunk-6JZXIWX7.js';
21
+ import { WorkCommandError, postJson, isSignatureRefusal, getJson } from './chunk-JKNOJKVD.js';
21
22
  import { planArtifactUpload } from './chunk-MECW77XP.js';
22
23
  import { readCloudConfig } from './chunk-PJDQPF5N.js';
23
- import { signerFor } from './chunk-MOI2KDLH.js';
24
- import { resolvePackRoot, AGENTS_SKILLS_BASE, OUTERLAYER_SKILLS_BASE } from './chunk-ZQSANKS4.js';
25
- import { scrubText } from './chunk-GSSVJNP2.js';
24
+ import { signerFor } from './chunk-L5VHGRQE.js';
25
+ import { AGENTS_SKILLS_BASE, OUTERLAYER_SKILLS_BASE, resolvePackRoot } from './chunk-ZQSANKS4.js';
26
+ import { scrubText } from './chunk-Z7WMU3RA.js';
26
27
  import { MAINTAINED_SKILLS } from './chunk-CDDED64E.js';
27
28
  import { resolveVersion } from './chunk-RA3O54FC.js';
28
29
  import { parsePolicy } from './chunk-FGXT5OXS.js';
29
- import { CheckItemsSchema, CHECK_ITEM_REASON_MAX_LENGTH } from './chunk-EOTMDIYC.js';
30
+ import { CheckItemsSchema } from './chunk-EOTMDIYC.js';
30
31
  import { homeFromEnv } from './chunk-VNVZDWO3.js';
31
- import { RUNNER_FILE_MODE, runnerDir, buildDir, vsockPath, jobConsolePath } from './chunk-PXGU3GKY.js';
32
- import { JOB_MOUNT, repositoriesOf, primaryOf, buildEnvironment, CHECKOUT_DIR, claudeCredential, AGENT_CREDENTIAL_MISSING, sharedUserEnvironment, headerVariables, authorEnvironment, withTunnelRecord, commitIdentityRecord, fdName, buildMavenSettingsXml, CLAUDE_CREDENTIAL_VARIABLES, currentClaudeCredential, removedClaudeCredentialValues, managedClaudeCredential, RELAY_PORT, repositoryPath, lifecycleWorkdir, CHECK_ITEMS_FILE, MAVEN_SETTINGS_FILE, checkEnvironment, EVAL_INPUT_DIR } from './chunk-G4W4B33M.js';
32
+ import { RUNNER_FILE_MODE, runnerDir, buildDir, vsockPath, jobConsolePath } from './chunk-GJ4KBHBD.js';
33
+ import { JOB_MOUNT, repositoriesOf, primaryOf, buildEnvironment, CHECKOUT_DIR, claudeCredential, AGENT_CREDENTIAL_MISSING, sharedUserEnvironment, headerVariables, authorEnvironment, withTunnelRecord, commitIdentityRecord, RELAY_PORT, repositoryPath, lifecycleWorkdir, CHECK_ITEMS_FILE, fdName, buildMavenSettingsXml, CLAUDE_CREDENTIAL_VARIABLES, currentClaudeCredential, removedClaudeCredentialValues, managedClaudeCredential, checkEnvironment, MAVEN_SETTINGS_FILE, EVAL_INPUT_DIR } from './chunk-SDAEXUIX.js';
33
34
  import { RUNNER_WORDED_REASONS, UNKNOWN_TRACKING_REASON } from './chunk-MESOOLF4.js';
34
- import { existsSync, statSync, appendFileSync, rmSync, mkdirSync, chmodSync, writeFileSync, openSync, readFileSync, closeSync, readdirSync, lstatSync, fstatSync, readSync, realpathSync } from 'fs';
35
+ import { randomBytes, createHash } from 'crypto';
36
+ import { rmSync, existsSync, statSync, appendFileSync, mkdirSync, chmodSync, writeFileSync, openSync, closeSync, readFileSync, readdirSync, lstatSync, realpathSync, fstatSync, readSync } from 'fs';
35
37
  import { join, dirname, delimiter, sep } from 'path';
38
+ import { spawn, spawnSync } from 'child_process';
36
39
  import { once } from 'events';
37
40
  import { createServer } from 'net';
38
- import { spawn, spawnSync } from 'child_process';
39
- import { randomBytes, createHash } from 'crypto';
40
- import { pathToFileURL } from 'url';
41
41
 
42
42
  createRequire(import.meta.url);
43
+ async function serveLease(socketPath, channel, makeServer = createServer, listenOn) {
44
+ const server = makeServer((socket) => channel.accept(socket));
45
+ if (listenOn === void 0) {
46
+ rmSync(socketPath, { force: true });
47
+ server.listen(socketPath);
48
+ await once(server, "listening");
49
+ } else {
50
+ await listenOn(server);
51
+ }
52
+ server.on("error", () => void 0);
53
+ return {
54
+ ...channel,
55
+ listener: server,
56
+ close() {
57
+ channel.close();
58
+ server.close();
59
+ rmSync(socketPath, { force: true });
60
+ }
61
+ };
62
+ }
43
63
  var PROBE_TIMEOUT_MS = 3e4;
44
64
  var PROBE_BYTES = 256 * 1024;
45
65
  function probeScript(dir, alive) {
@@ -104,26 +124,6 @@ function judgeProbe(probe, missedBefore) {
104
124
  if (probe.alive) return { kind: "running" };
105
125
  return missedBefore ? { kind: "lost" } : { kind: "recheck" };
106
126
  }
107
- async function serveLease(socketPath, channel, makeServer = createServer, listenOn) {
108
- const server = makeServer((socket) => channel.accept(socket));
109
- if (listenOn === void 0) {
110
- rmSync(socketPath, { force: true });
111
- server.listen(socketPath);
112
- await once(server, "listening");
113
- } else {
114
- await listenOn(server);
115
- }
116
- server.on("error", () => void 0);
117
- return {
118
- ...channel,
119
- listener: server,
120
- close() {
121
- channel.close();
122
- server.close();
123
- rmSync(socketPath, { force: true });
124
- }
125
- };
126
- }
127
127
 
128
128
  // src/runner/microvm/command-state.ts
129
129
  var MICROVM_COMMAND_DIR = "/build/.command";
@@ -175,6 +175,8 @@ async function stopMicroVmCommand(runtime, state) {
175
175
  (signal) => runtime.vm.exec(state.vsockPath, { argv: ["/bin/sh", "-c", STOP_COMMAND_SCRIPT], user: "root", cwd: "/", env: { PATH: GUEST_PATH } }, { signal })
176
176
  );
177
177
  }
178
+
179
+ // src/runner/microvm/hooks.ts
178
180
  var MANAGED_NO_BUILDER_DISK_REASON = "this release of the CLI pins no builder root disk, and a managed runner builds its images in a builder microVM, never on the host. The build did not start; update the CLI";
179
181
  var DEFAULT_MEMORY = "8g";
180
182
  var DEFAULT_DISK = "40g";
@@ -193,6 +195,37 @@ var GUEST_MAVEN_DIR = `${JOB_MOUNT}/maven`;
193
195
  function buildMemoryBytes(config) {
194
196
  return parseSize(config.build?.memory ?? DEFAULT_MEMORY) ?? parseSize(DEFAULT_MEMORY);
195
197
  }
198
+ function machineLimits(config, jobDir) {
199
+ const settings = managedBuildSettingsFor(jobDir);
200
+ if (settings !== void 0) return managedMachineLimits(settings);
201
+ return {
202
+ cpus: config.build?.cpus ?? DEFAULT_CPUS,
203
+ memoryBytes: buildMemoryBytes(config),
204
+ diskBytes: parseSize(config.build?.disk ?? DEFAULT_DISK) ?? parseSize(DEFAULT_DISK)
205
+ };
206
+ }
207
+ function recordedLimits(limits) {
208
+ return { memoryBytes: limits.memoryBytes, diskBytes: limits.diskBytes };
209
+ }
210
+ function memoryTextOf(config, jobDir, limits) {
211
+ return managedBuildSettingsFor(jobDir) === void 0 ? config.build?.memory : `${limits.memoryBytes / 1024 ** 3}g`;
212
+ }
213
+ function builderMachineFor(config, jobDir, assets, limits) {
214
+ return {
215
+ kernel: assets.kernel,
216
+ firecracker: assets.firecracker,
217
+ limits: { ...limits, cacheDiskBytes: parseSize(config.build?.cacheDisk ?? DEFAULT_CACHE_DISK) ?? parseSize(DEFAULT_CACHE_DISK) },
218
+ memory: memoryTextOf(config, jobDir, limits),
219
+ allowHosts: withFactoryHosts(config.build?.allowHosts, managedBuildSettingsFor(jobDir)),
220
+ managed: config.managed === true,
221
+ timeLimitMinutes: config.timeLimitMinutes
222
+ };
223
+ }
224
+ function factoryVariablesOf(config, jobDir) {
225
+ const held = managedBuildSettingsFor(jobDir)?.variables;
226
+ if (held !== void 0) return held;
227
+ return config.managed === true ? [] : void 0;
228
+ }
196
229
  function firstLine(text2) {
197
230
  return text2.trim().split("\n")[0] ?? "";
198
231
  }
@@ -327,11 +360,8 @@ async function provisionMicroVm(input) {
327
360
  });
328
361
  if (unfetched !== void 0) return fail(unfetched);
329
362
  }
330
- const limits = {
331
- cpus: config.build?.cpus ?? DEFAULT_CPUS,
332
- memoryBytes: buildMemoryBytes(config),
333
- diskBytes: parseSize(config.build?.disk ?? DEFAULT_DISK) ?? parseSize(DEFAULT_DISK)
334
- };
363
+ const limits = machineLimits(config, jobDir);
364
+ const memoryText = memoryTextOf(config, jobDir, limits);
335
365
  let buildsInBuilder = imageBuildOf(config) === "vm";
336
366
  if (buildsInBuilder && !vm.builderDiskPinned()) {
337
367
  if (config.managed === true) return fail(MANAGED_NO_BUILDER_DISK_REASON);
@@ -351,17 +381,7 @@ async function provisionMicroVm(input) {
351
381
  deadlineMs: input.deadlineMs,
352
382
  now: input.now,
353
383
  guestHelper: { path: assets.helper, sha256: assets.helperSha256 },
354
- ...buildsInBuilder ? {
355
- inBuilder: {
356
- kernel: assets.kernel,
357
- firecracker: assets.firecracker,
358
- limits: { ...limits, cacheDiskBytes: parseSize(config.build?.cacheDisk ?? DEFAULT_CACHE_DISK) ?? parseSize(DEFAULT_CACHE_DISK) },
359
- memory: config.build?.memory,
360
- allowHosts: config.build?.allowHosts,
361
- managed: config.managed === true,
362
- timeLimitMinutes: config.timeLimitMinutes
363
- }
364
- } : {}
384
+ ...buildsInBuilder ? { inBuilder: builderMachineFor(config, jobDir, assets, limits) } : {}
365
385
  });
366
386
  if (!obtained.ok) return obtained.aborted === true ? { ok: false, reason: obtained.reason, aborted: true } : fail(obtained.reason);
367
387
  const { report } = obtained.built;
@@ -375,6 +395,7 @@ async function provisionMicroVm(input) {
375
395
  imageId: report.imageId,
376
396
  containerEnv: report.apply.containerEnv,
377
397
  builtIns: true,
398
+ limits: recordedLimits(limits),
378
399
  lifecycle: report.apply.lifecycle,
379
400
  ...governingSource.kind === "governed" ? { governing: { repository: governingSource.repository, ref: governingSource.ref, commit: governingSource.sha } } : {},
380
401
  ...report.docker === true ? { docker: true } : {}
@@ -387,7 +408,8 @@ async function provisionMicroVm(input) {
387
408
  repositories,
388
409
  hostEnv: runtime.hostEnv,
389
410
  containerEnv: report.apply.containerEnv,
390
- builtIns: true
411
+ builtIns: true,
412
+ factoryVariables: factoryVariablesOf(config, jobDir)
391
413
  };
392
414
  const env = buildEnvironment(envInput);
393
415
  if (env.skippedTokenVariables.length > 0) {
@@ -406,7 +428,7 @@ async function provisionMicroVm(input) {
406
428
  let why = reason;
407
429
  if (!aborted && (state.cgroup !== void 0 || state.jailed !== void 0)) {
408
430
  try {
409
- if (await microVmOomKills(runtime, state) > 0) why = outOfMemoryReason(config.build?.memory);
431
+ if (await microVmOomKills(runtime, state) > 0) why = outOfMemoryReason(memoryText);
410
432
  } catch (err) {
411
433
  log.error(`${tag} could not read the build's memory events: ${describeCallFailure(err)} \xB7 continuing`);
412
434
  }
@@ -425,7 +447,7 @@ async function provisionMicroVm(input) {
425
447
  imageConfig = { env: madeInBuilder.env, user: madeInBuilder.user };
426
448
  } else {
427
449
  try {
428
- state.rootfs = await ensureRootfs({ host: vm.rootfs, runnerDir: runnerDirPath, imageId: report.imageId, memoryBytes: buildMemoryBytes(config) });
450
+ state.rootfs = await ensureRootfs({ host: vm.rootfs, runnerDir: runnerDirPath, imageId: report.imageId, memoryBytes: limits.memoryBytes });
429
451
  imageConfig = await vm.rootfs.imageConfig(report.imageId);
430
452
  } catch (err) {
431
453
  return bail(`the image could not be converted to a root disk: ${describeCallFailure(err)}`);
@@ -678,7 +700,8 @@ function vmExecFor(input, argv) {
678
700
  containerEnv: state.containerEnv ?? {},
679
701
  builtIns: state.builtIns === true,
680
702
  identity: input.job.commitIdentity,
681
- imageEnv: state.imageEnv
703
+ imageEnv: state.imageEnv,
704
+ factoryVariables: factoryVariablesOf(input.config, input.jobDir)
682
705
  });
683
706
  const options = {
684
707
  socket: state.vsockPath,
@@ -707,7 +730,8 @@ async function startMicroVmCommand(input) {
707
730
  containerEnv: state.containerEnv ?? {},
708
731
  builtIns: state.builtIns === true,
709
732
  identity: input.job.commitIdentity,
710
- imageEnv: state.imageEnv
733
+ imageEnv: state.imageEnv,
734
+ factoryVariables: factoryVariablesOf(input.config, input.jobDir)
711
735
  });
712
736
  const started = await input.runtime.vm.exec(state.vsockPath, {
713
737
  argv: ["/bin/sh", "-c", commandWithShim(input.job, input.cmdline)],
@@ -749,7 +773,8 @@ async function syncInMicroVm(input) {
749
773
  containerEnv: state.containerEnv ?? {},
750
774
  builtIns: state.builtIns === true,
751
775
  identity: job.commitIdentity,
752
- imageEnv: state.imageEnv
776
+ imageEnv: state.imageEnv,
777
+ factoryVariables: factoryVariablesOf(input.config, input.jobDir)
753
778
  });
754
779
  const result = await runtime.vm.exec(state.vsockPath, {
755
780
  argv: ["outerlayer", "sync"],
@@ -873,6 +898,7 @@ async function startCheckMicroVm(input) {
873
898
  const { config, runtime, job, jobDir, tokens, log } = input;
874
899
  const { vm } = runtime;
875
900
  const tag = `#${job.item}`;
901
+ const limits = machineLimits(config, jobDir);
876
902
  const built = job.microvm;
877
903
  const repository = built?.repository ?? job.repository;
878
904
  if (built === void 0 || built.imageId === void 0 || repository === void 0 || job.branch === void 0) {
@@ -922,7 +948,7 @@ async function startCheckMicroVm(input) {
922
948
  state.rootfs = built.rootfs;
923
949
  } else {
924
950
  try {
925
- state.rootfs = await ensureRootfs({ host: vm.rootfs, runnerDir: runnerDirPath, imageId: built.imageId, memoryBytes: buildMemoryBytes(config) });
951
+ state.rootfs = await ensureRootfs({ host: vm.rootfs, runnerDir: runnerDirPath, imageId: built.imageId, memoryBytes: limits.memoryBytes });
926
952
  } catch (err) {
927
953
  return fail(`the image could not be converted to a root disk: ${describeCallFailure(err)}`);
928
954
  }
@@ -940,16 +966,11 @@ async function startCheckMicroVm(input) {
940
966
  log: (line) => log.line(`${tag} ${line}`),
941
967
  readOnly: true
942
968
  });
943
- runtime.tunnels.set(buildId, await startServicesFor(runtime, config, buildDir2, checksDir, void 0, false, gitRemote));
969
+ runtime.tunnels.set(buildId, await startServicesFor(runtime, config, buildDir2, checksDir, void 0, false, gitRemote, "socket", void 0, void 0, void 0, jobDir));
944
970
  } catch (err) {
945
971
  return fail(`the tunnel could not start: ${describeCallFailure(err)}`);
946
972
  }
947
973
  if (abandoned()) return fail(ABANDONED);
948
- const limits = {
949
- cpus: config.build?.cpus ?? DEFAULT_CPUS,
950
- memoryBytes: buildMemoryBytes(config),
951
- diskBytes: parseSize(config.build?.disk ?? DEFAULT_DISK) ?? parseSize(DEFAULT_DISK)
952
- };
953
974
  const links = vsockLinks({ destinations: config.build?.destinations?.length ?? 0, builtIns: false });
954
975
  const configPath = join(buildDir2, "vm.json");
955
976
  const helper = await helperFor(runtime, (line) => log.line(`${tag} ${line}`), config.managed === true);
@@ -1009,7 +1030,16 @@ async function startCheckMicroVm(input) {
1009
1030
  const failed = await boot();
1010
1031
  if (failed !== void 0) return fail(failed);
1011
1032
  if (abandoned()) return fail(ABANDONED);
1012
- const env = checkEnvironment({ config, item: job.item, repository, hostEnv: runtime.hostEnv, containerEnv: built.containerEnv ?? {}, imageEnv: built.imageEnv });
1033
+ const env = checkEnvironment({
1034
+ config,
1035
+ item: job.item,
1036
+ repository,
1037
+ hostEnv: runtime.hostEnv,
1038
+ containerEnv: built.containerEnv ?? {},
1039
+ imageEnv: built.imageEnv,
1040
+ // The checks of a managed build get its factory's variables, as its command did, and none of the runner's own.
1041
+ factoryVariables: factoryVariablesOf(config, jobDir)
1042
+ });
1013
1043
  const evalInput = { dir: join(checksDir, EVAL_INPUT_NAME), delivered: false };
1014
1044
  const deliverEvalInput = async () => {
1015
1045
  await unpackIntoGuest(runtime, state, evalInput.dir, EVAL_INPUT_DIR, { readOnly: true });
@@ -1149,82 +1179,6 @@ async function stopOrphanedMicroVms(input) {
1149
1179
  return stopped;
1150
1180
  }
1151
1181
 
1152
- // src/runner/microvm/guest-usage.ts
1153
- var GUEST_USAGE_INTERVAL_MS = 15e3;
1154
- var MS_PER_TICK = 10;
1155
- function usageScript(procDir = "/proc") {
1156
- return [
1157
- `echo OL-USAGE-MEMINFO; cat ${procDir}/meminfo`,
1158
- `echo OL-USAGE-STAT; cat ${procDir}/stat`,
1159
- `if [ -r ${procDir}/pressure/memory ]; then echo OL-USAGE-PSI-MEMORY; cat ${procDir}/pressure/memory; fi`,
1160
- `if [ -r ${procDir}/pressure/cpu ]; then echo OL-USAGE-PSI-CPU; cat ${procDir}/pressure/cpu; fi`
1161
- ].join("; ");
1162
- }
1163
- function section(stdout, name) {
1164
- const lines = stdout.split("\n");
1165
- const start = lines.indexOf(`OL-USAGE-${name}`);
1166
- if (start === -1) return void 0;
1167
- const end = lines.findIndex((line, index) => index > start && line.startsWith("OL-USAGE-"));
1168
- return lines.slice(start + 1, end === -1 ? void 0 : end).join("\n");
1169
- }
1170
- function meminfoKb(text2, key) {
1171
- const match = new RegExp(`^${key}:\\s+(\\d+)\\s+kB\\s*$`, "m").exec(text2);
1172
- return match === null ? void 0 : Number(match[1]);
1173
- }
1174
- function pressureTotal(text2) {
1175
- if (text2 === void 0) return void 0;
1176
- const match = /^some\s.*\btotal=(\d+)\s*$/m.exec(text2);
1177
- return match === null ? void 0 : Number(match[1]);
1178
- }
1179
- function parseGuestSample(stdout) {
1180
- const meminfo = section(stdout, "MEMINFO");
1181
- const stat = section(stdout, "STAT");
1182
- if (meminfo === void 0 || stat === void 0) return void 0;
1183
- const totalKb = meminfoKb(meminfo, "MemTotal");
1184
- const availableKb = meminfoKb(meminfo, "MemAvailable");
1185
- const aggregate = /^cpu\s+(\d+(?:\s+\d+){7,})\s*$/m.exec(stat);
1186
- if (totalKb === void 0 || availableKb === void 0 || totalKb <= 0 || availableKb > totalKb || aggregate === null) return void 0;
1187
- const [user, nice, system, , , irq, softirq, steal] = aggregate[1].split(/\s+/).map(Number);
1188
- const vcpus = stat.split("\n").filter((line) => /^cpu\d+\s/.test(line)).length;
1189
- if (vcpus < 1) return void 0;
1190
- const memoryPressureUs = pressureTotal(section(stdout, "PSI-MEMORY"));
1191
- const cpuPressureUs = pressureTotal(section(stdout, "PSI-CPU"));
1192
- return {
1193
- memoryBytes: totalKb * 1024,
1194
- vcpus,
1195
- usedBytes: (totalKb - availableKb) * 1024,
1196
- cpuBusyMs: (user + nice + system + irq + softirq + steal) * MS_PER_TICK,
1197
- ...memoryPressureUs === void 0 ? {} : { memoryPressureUs },
1198
- ...cpuPressureUs === void 0 ? {} : { cpuPressureUs }
1199
- };
1200
- }
1201
- function foldSample(usage, sample) {
1202
- return { peakMemoryBytes: Math.max(usage?.peakMemoryBytes ?? 0, sample.usedBytes), last: sample };
1203
- }
1204
- function usageRecord(usage) {
1205
- if (usage === void 0) return void 0;
1206
- const { last } = usage;
1207
- return {
1208
- guestMemoryBytes: last.memoryBytes,
1209
- guestVcpus: last.vcpus,
1210
- peakMemoryBytes: usage.peakMemoryBytes,
1211
- cpuBusyMs: last.cpuBusyMs,
1212
- ...last.memoryPressureUs === void 0 ? {} : { memoryPressureUs: last.memoryPressureUs },
1213
- ...last.cpuPressureUs === void 0 ? {} : { cpuPressureUs: last.cpuPressureUs }
1214
- };
1215
- }
1216
- async function sampleGuest(runtime, state) {
1217
- if (state.vsockPath === void 0) return void 0;
1218
- try {
1219
- const result = await withinTime(
1220
- (signal) => runtime.vm.exec(state.vsockPath, { argv: ["/bin/sh", "-c", usageScript()], user: "root", cwd: "/", env: { PATH: GUEST_PATH } }, { signal })
1221
- );
1222
- return result.status === 0 ? parseGuestSample(result.stdout) : void 0;
1223
- } catch {
1224
- return void 0;
1225
- }
1226
- }
1227
-
1228
1182
  // src/runner/verdict.ts
1229
1183
  var VERDICT_SETTLE_MS = 5 * 6e4;
1230
1184
  var VERDICT_READ_EVERY_MS = 3e4;
@@ -1310,722 +1264,397 @@ async function settleVerdict(deps) {
1310
1264
  await deps.sleep(everyMs);
1311
1265
  }
1312
1266
  }
1313
- var RUNNER_LOG_FILE = "runner.log";
1314
- var RUNNER_LOG_KEEP_BYTES = 65536;
1315
- function isTailedStage(stage) {
1316
- const tailed = ["provision", "sync", "cleanup", "report"];
1317
- return tailed.includes(stage);
1267
+
1268
+ // src/runner/microvm/guest-usage.ts
1269
+ var GUEST_USAGE_INTERVAL_MS = 15e3;
1270
+ var MS_PER_TICK = 10;
1271
+ function usageScript(procDir = "/proc") {
1272
+ return [
1273
+ `echo OL-USAGE-MEMINFO; cat ${procDir}/meminfo`,
1274
+ `echo OL-USAGE-STAT; cat ${procDir}/stat`,
1275
+ `if [ -r ${procDir}/pressure/memory ]; then echo OL-USAGE-PSI-MEMORY; cat ${procDir}/pressure/memory; fi`,
1276
+ `if [ -r ${procDir}/pressure/cpu ]; then echo OL-USAGE-PSI-CPU; cat ${procDir}/pressure/cpu; fi`
1277
+ ].join("; ");
1318
1278
  }
1319
- var PROVISION_PROBLEM = /\b(warn(ing)?|error|fail(ed|ure)?|refused)\b/i;
1320
- var READ_LIMIT_BYTES = 1048576;
1321
- var ITEM_LINE = /^#(\d+)\b/;
1322
- var AttemptLogBook = class {
1323
- constructor(now = () => /* @__PURE__ */ new Date()) {
1324
- this.now = now;
1325
- this.dirs = /* @__PURE__ */ new Map();
1326
- this.lastByKey = /* @__PURE__ */ new Map();
1327
- }
1328
- /** Starts keeping lines for `item` in `jobDirPath`. Idempotent. */
1329
- open(item, jobDirPath) {
1330
- this.dirs.set(Number(item), jobDirPath);
1331
- }
1332
- /** Stops keeping lines for `item`; its file stays on disk until housekeeping removes the job directory. */
1333
- close(item) {
1334
- const number = Number(item);
1335
- this.dirs.delete(number);
1336
- for (const key of [...this.lastByKey.keys()]) {
1337
- if (key.startsWith(`${number}:`)) this.lastByKey.delete(key);
1338
- }
1339
- }
1340
- /** The log with every line it writes for an open item also kept in that item's file. */
1341
- wrap(log) {
1342
- return {
1343
- line: (text2) => {
1344
- log.line(text2);
1345
- this.keepItemLine(text2);
1346
- },
1347
- error: (text2) => {
1348
- log.error(text2);
1349
- this.keepItemLine(text2);
1350
- }
1351
- };
1352
- }
1353
- /** One line for `item` that does not itself start with its number. */
1354
- note(item, text2) {
1355
- this.append(Number(item), text2);
1356
- }
1357
- /** One line, written only when `text` differs from the last one written under `key` for the item. */
1358
- noteChanged(item, key, text2) {
1359
- const number = Number(item);
1360
- const slot = `${number}:${key}`;
1361
- if (this.lastByKey.get(slot) === text2) return;
1362
- this.lastByKey.set(slot, text2);
1363
- this.append(number, text2);
1364
- }
1365
- keepItemLine(text2) {
1366
- const match = ITEM_LINE.exec(text2);
1367
- if (match !== null) this.append(Number(match[1]), text2);
1368
- }
1369
- append(item, text2) {
1370
- const dir = this.dirs.get(item);
1371
- if (dir === void 0) return;
1372
- try {
1373
- appendFileSync(join(dir, RUNNER_LOG_FILE), `${logStamp(this.now())} ${text2.replace(/[\r\n]+/g, " ")}
1374
- `, { mode: 384 });
1375
- } catch {
1376
- }
1377
- }
1378
- };
1379
- function readEnd(path, maxBytes) {
1380
- let fd;
1381
- try {
1382
- fd = openSync(path, "r");
1383
- const size = fstatSync(fd).size;
1384
- const length = Math.min(size, maxBytes);
1385
- const buffer = Buffer.alloc(length);
1386
- readSync(fd, buffer, 0, length, size - length);
1387
- return buffer.toString("utf8");
1388
- } catch {
1389
- return void 0;
1390
- } finally {
1391
- if (fd !== void 0) closeSync(fd);
1392
- }
1279
+ function section(stdout, name) {
1280
+ const lines = stdout.split("\n");
1281
+ const start = lines.indexOf(`OL-USAGE-${name}`);
1282
+ if (start === -1) return void 0;
1283
+ const end = lines.findIndex((line, index) => index > start && line.startsWith("OL-USAGE-"));
1284
+ return lines.slice(start + 1, end === -1 ? void 0 : end).join("\n");
1393
1285
  }
1394
- function lastLines(text2, count) {
1395
- const lines = text2.split("\n");
1396
- if (lines.at(-1) === "") lines.pop();
1397
- return lines.slice(-200);
1286
+ function meminfoKb(text2, key) {
1287
+ const match = new RegExp(`^${key}:\\s+(\\d+)\\s+kB\\s*$`, "m").exec(text2);
1288
+ return match === null ? void 0 : Number(match[1]);
1398
1289
  }
1399
- function capRunnerLog(text2) {
1400
- const bytes = Buffer.from(text2, "utf8");
1401
- if (bytes.length <= RUNNER_LOG_KEEP_BYTES) return text2;
1402
- let start = bytes.length - RUNNER_LOG_KEEP_BYTES;
1403
- if (bytes[start - 1] !== 10) {
1404
- const newline = bytes.indexOf(10, start);
1405
- if (newline + 1 < bytes.length) {
1406
- start = newline + 1;
1407
- } else {
1408
- while ((bytes[start] & 192) === 128) start++;
1409
- }
1410
- }
1411
- return `[runner log truncated: the first ${start} bytes were cut]
1412
- ${bytes.subarray(start).toString("utf8")}`;
1413
- }
1414
- function buildReleaseLog(jobDirPath, endStage, scrub) {
1415
- const parts = [];
1416
- const own = readEnd(join(jobDirPath, RUNNER_LOG_FILE), READ_LIMIT_BYTES);
1417
- if (own !== void 0 && own.trim() !== "") parts.push(own.endsWith("\n") ? own.slice(0, -1) : own);
1418
- if (isTailedStage(endStage)) {
1419
- const file = `${endStage}.log`;
1420
- const stage = readEnd(join(jobDirPath, file), READ_LIMIT_BYTES);
1421
- const tail = stage === void 0 ? [] : lastLines(stage);
1422
- if (tail.length > 0) parts.push(`--- the last ${tail.length} lines of ${file} ---`, ...tail);
1423
- }
1424
- if (endStage !== "provision") {
1425
- const provision = readEnd(join(jobDirPath, "provision.log"), READ_LIMIT_BYTES);
1426
- const problems = provision === void 0 ? [] : provision.split("\n").filter((line) => PROVISION_PROBLEM.test(line)).slice(-50);
1427
- if (problems.length > 0) parts.push(`--- provision problems (${problems.length} lines of provision.log) ---`, ...problems);
1428
- }
1429
- if (parts.length === 0) return void 0;
1430
- return capRunnerLog(scrub(parts.join("\n")) + "\n");
1290
+ function pressureTotal(text2) {
1291
+ if (text2 === void 0) return void 0;
1292
+ const match = /^some\s.*\btotal=(\d+)\s*$/m.exec(text2);
1293
+ return match === null ? void 0 : Number(match[1]);
1431
1294
  }
1432
- var ENDED = "ended before provision finished";
1433
- async function provisionProcess(input) {
1434
- const { config, runtime, item, repository, jobDir } = input;
1435
- if (claudeCredential(runtime.hostEnv) === void 0) return { ok: false, reason: AGENT_CREDENTIAL_MISSING };
1436
- if (repository === void 0 || repository === null || repository === "") {
1437
- return { ok: false, reason: "the work item names no repository to clone" };
1438
- }
1439
- const buildId = randomBytes(6).toString("hex");
1440
- const buildDir2 = buildDir(config.configPath, buildId);
1441
- const workDir = join(jobDir, "work");
1442
- const state = { buildId, buildDir: buildDir2, workDir };
1443
- const environment = sharedUserEnvironment(input.cliVersion);
1444
- input.save(state, environment);
1445
- const bail = async (reason) => {
1446
- try {
1447
- await cleanupProcess(runtime, state);
1448
- } catch (err) {
1449
- input.log.error(`#${item.number} cleanup failed: ${describeCallFailure(err)}`);
1450
- }
1451
- return { ok: false, reason };
1295
+ function parseGuestSample(stdout) {
1296
+ const meminfo = section(stdout, "MEMINFO");
1297
+ const stat = section(stdout, "STAT");
1298
+ if (meminfo === void 0 || stat === void 0) return void 0;
1299
+ const totalKb = meminfoKb(meminfo, "MemTotal");
1300
+ const availableKb = meminfoKb(meminfo, "MemAvailable");
1301
+ const aggregate = /^cpu\s+(\d+(?:\s+\d+){7,})\s*$/m.exec(stat);
1302
+ if (totalKb === void 0 || availableKb === void 0 || totalKb <= 0 || availableKb > totalKb || aggregate === null) return void 0;
1303
+ const [user, nice, system, , , irq, softirq, steal] = aggregate[1].split(/\s+/).map(Number);
1304
+ const vcpus = stat.split("\n").filter((line) => /^cpu\d+\s/.test(line)).length;
1305
+ if (vcpus < 1) return void 0;
1306
+ const memoryPressureUs = pressureTotal(section(stdout, "PSI-MEMORY"));
1307
+ const cpuPressureUs = pressureTotal(section(stdout, "PSI-CPU"));
1308
+ return {
1309
+ memoryBytes: totalKb * 1024,
1310
+ vcpus,
1311
+ usedBytes: (totalKb - availableKb) * 1024,
1312
+ cpuBusyMs: (user + nice + system + irq + softirq + steal) * MS_PER_TICK,
1313
+ ...memoryPressureUs === void 0 ? {} : { memoryPressureUs },
1314
+ ...cpuPressureUs === void 0 ? {} : { cpuPressureUs }
1452
1315
  };
1453
- if (input.aborted()) return bail(ENDED);
1454
- mkdirSync(buildDir2, { recursive: true });
1455
- chmodSync(buildDir2, 493);
1456
- try {
1457
- await startHostServices({ runtime, config, build: state, jobDir, itemKey: input.itemKey, process: true, routeLog: (line) => input.log.line(`#${item.number} ${line}`) });
1458
- } catch (err) {
1459
- return bail(`the tunnel could not start: ${describeCallFailure(err)}`);
1460
- }
1461
- input.save(state, environment);
1462
- if (input.aborted()) return bail(ENDED);
1463
- const checkout = join(workDir, "checkout");
1464
- const remote = runtime.remoteFor?.(repository) ?? `https://github.com/${repository}.git`;
1465
- const cloned = await runtime.clone({ remote, dest: checkout });
1466
- if (!cloned.ok) return bail(cloned.reason);
1467
- if (input.aborted()) return bail(ENDED);
1468
- return { ok: true, workdir: checkout, state, environment };
1469
1316
  }
1470
- function cleanupProcess(runtime, state) {
1471
- return closeHostBuild(runtime, state);
1317
+ function foldSample(usage, sample) {
1318
+ return { peakMemoryBytes: Math.max(usage?.peakMemoryBytes ?? 0, sample.usedBytes), last: sample };
1472
1319
  }
1473
- var ARTIFACT_PROOF_REASON = "Its declared artifact proves it; the judge does not judge artifacts.";
1474
- var NO_CITING_TEST_REASON = "No test cites this criterion, and it declares no artifact proof.";
1475
- var NO_VERDICT_REASON = "The judge gave no result for this criterion.";
1476
- function criterionInputs(input) {
1477
- const paths = [...new Set((input.citations[input.criterion.id] ?? []).map((citation) => citation.path))].sort();
1478
- const hash = createHash("sha256");
1479
- const part = (text2) => {
1480
- hash.update(`${Buffer.byteLength(text2)}:`);
1481
- hash.update(text2);
1320
+ function usageRecord(usage) {
1321
+ if (usage === void 0) return void 0;
1322
+ const { last } = usage;
1323
+ return {
1324
+ guestMemoryBytes: last.memoryBytes,
1325
+ guestVcpus: last.vcpus,
1326
+ peakMemoryBytes: usage.peakMemoryBytes,
1327
+ cpuBusyMs: last.cpuBusyMs,
1328
+ ...last.memoryPressureUs === void 0 ? {} : { memoryPressureUs: last.memoryPressureUs },
1329
+ ...last.cpuPressureUs === void 0 ? {} : { cpuPressureUs: last.cpuPressureUs }
1482
1330
  };
1483
- part("judge-criterion-v1");
1484
- part(input.brief);
1485
- part(input.criterion.id);
1486
- part(input.criterion.text);
1487
- part(input.criterion.proof ?? "");
1488
- for (const path of paths) {
1489
- part(path);
1490
- part(input.testFiles.get(path) ?? "");
1491
- }
1492
- return hash.digest("hex");
1493
- }
1494
- var HTTPS_LINK = /^https:\/\/\S+$/;
1495
- function refsFor(id, citations) {
1496
- const refs = (citations[id] ?? []).slice(0, 20).map((citation) => ({
1497
- path: citation.path,
1498
- line: citation.line,
1499
- // Only an https link becomes an href on a page; any other is left off rather than refusing the check.
1500
- ...HTTPS_LINK.test(citation.href ?? "") ? { href: citation.href } : {}
1501
- }));
1502
- return refs.length === 0 ? void 0 : refs;
1503
- }
1504
- function planCriteria(input) {
1505
- const previousById = new Map(input.previous.map((item) => [item.id, item]));
1506
- const settled = [];
1507
- const toJudge = [];
1508
- for (const criterion of input.criteria) {
1509
- const inputs = input.inputs.get(criterion.id);
1510
- const base = { id: criterion.id, criterion: criterion.id, ...inputs === void 0 ? {} : { inputs } };
1511
- const refs = refsFor(criterion.id, input.citations);
1512
- if (criterion.proof !== null) {
1513
- settled.push({ ...base, result: "skipped", reason: ARTIFACT_PROOF_REASON });
1514
- continue;
1515
- }
1516
- if (refs === void 0) {
1517
- settled.push({ ...base, result: "fail", reason: NO_CITING_TEST_REASON });
1518
- continue;
1519
- }
1520
- const kept = previousById.get(criterion.id);
1521
- if (kept !== void 0 && inputs !== void 0 && kept.inputs === inputs && (kept.result === "pass" || kept.result === "fail")) {
1522
- settled.push({ ...base, result: kept.result, ...kept.reason === void 0 ? {} : { reason: kept.reason }, refs });
1523
- continue;
1524
- }
1525
- toJudge.push(criterion);
1526
- }
1527
- return { settled, toJudge };
1528
- }
1529
- var MAX_ANSWER_ENTRIES = 500;
1530
- function readAnswer(raw) {
1531
- if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return { ok: false, problem: "expected an object" };
1532
- const { result, items } = raw;
1533
- if (result !== void 0 && result !== "pass" && result !== "fail") return { ok: false, problem: 'result: must be "pass" or "fail"' };
1534
- if (items !== void 0 && !Array.isArray(items)) return { ok: false, problem: "items: must be an array" };
1535
- const entries = items ?? [];
1536
- if (entries.length > MAX_ANSWER_ENTRIES) return { ok: false, problem: `items: at most ${MAX_ANSWER_ENTRIES} entries` };
1537
- const read = [];
1538
- for (const [index, entry] of entries.entries()) {
1539
- const { id, result: itemResult, reason } = typeof entry === "object" && entry !== null ? entry : {};
1540
- if (typeof id !== "string" || id === "" || id.length > 200) return { ok: false, problem: `items.${index}.id: must be text of 1 to 200 characters` };
1541
- if (itemResult !== "pass" && itemResult !== "fail") return { ok: false, problem: `items.${index}.result: must be "pass" or "fail"` };
1542
- if (reason !== void 0 && typeof reason !== "string") return { ok: false, problem: `items.${index}.reason: must be text` };
1543
- read.push({ id, result: itemResult, ...reason === void 0 ? {} : { reason } });
1544
- }
1545
- return { ok: true, answer: { ...result === void 0 ? {} : { result }, items: read } };
1546
- }
1547
- function parseJudgeAnswer(text2) {
1548
- if (text2 === void 0 || text2.trim() === "") return void 0;
1549
- let raw;
1331
+ }
1332
+ async function sampleGuest(runtime, state) {
1333
+ if (state.vsockPath === void 0) return void 0;
1550
1334
  try {
1551
- raw = JSON.parse(text2);
1335
+ const result = await withinTime(
1336
+ (signal) => runtime.vm.exec(state.vsockPath, { argv: ["/bin/sh", "-c", usageScript()], user: "root", cwd: "/", env: { PATH: GUEST_PATH } }, { signal })
1337
+ );
1338
+ return result.status === 0 ? parseGuestSample(result.stdout) : void 0;
1552
1339
  } catch {
1553
- return { ok: false, reason: "the judge's items.json is not JSON" };
1340
+ return void 0;
1554
1341
  }
1555
- const read = readAnswer(raw);
1556
- return read.ok ? read : { ok: false, reason: `the judge's items.json does not match its shape: ${read.problem}` };
1557
- }
1558
- function oneLineReason(text2) {
1559
- if (text2 === void 0) return void 0;
1560
- const line = text2.replace(/\s+/g, " ").trim();
1561
- if (line === "") return void 0;
1562
- return line.length <= CHECK_ITEM_REASON_MAX_LENGTH ? line : `${line.slice(0, CHECK_ITEM_REASON_MAX_LENGTH - 1)}\u2026`;
1563
- }
1564
- function judgedItems(input) {
1565
- return input.toJudge.map((criterion) => {
1566
- const said = input.results.get(criterion.id);
1567
- const inputs = input.inputs.get(criterion.id);
1568
- const refs = refsFor(criterion.id, input.citations);
1569
- const reason = said === void 0 ? NO_VERDICT_REASON : oneLineReason(said.reason);
1570
- return {
1571
- id: criterion.id,
1572
- criterion: criterion.id,
1573
- result: said?.result ?? "fail",
1574
- ...reason === void 0 ? {} : { reason },
1575
- ...refs === void 0 ? {} : { refs },
1576
- ...inputs === void 0 || said === void 0 ? {} : { inputs }
1577
- };
1578
- });
1579
- }
1580
- function inCriteriaOrder(items, criteria) {
1581
- const order = new Map(criteria.map((criterion, index) => [criterion.id, index]));
1582
- return [...items].sort((a, b) => (order.get(a.id) ?? Number.MAX_SAFE_INTEGER) - (order.get(b.id) ?? Number.MAX_SAFE_INTEGER));
1583
1342
  }
1584
- function itemsSentence(items, noun) {
1585
- const failing = items.filter((item) => item.result === "fail").map((item) => item.id);
1586
- if (failing.length === 0) return void 0;
1587
- return `${failing.length} of ${items.length} ${items.length === 1 ? noun.one : noun.many} not satisfied: ${failing.join(", ")}`;
1588
- }
1589
-
1590
- // src/runner/host-checks.ts
1591
- var MAX_VALIDATOR_FILES = 20;
1592
- var VALIDATORS_DIR = ".outerlayer/validators";
1593
- var POLICY_FILE = ".outerlayer/policy.yaml";
1594
- var LIMIT_KEY_TAIL = " (runner.checks.timeLimitMinutes)";
1595
- var MAX_CHECK_ITEMS_BYTES = 1024 * 1024;
1596
- function commandItems(read, scrub) {
1597
- if (read.status !== 0 || read.stdout.trim() === "") return { ok: true, items: void 0 };
1598
- if (Buffer.byteLength(read.stdout) > MAX_CHECK_ITEMS_BYTES) return { ok: false, reason: `its check items file is longer than ${MAX_CHECK_ITEMS_BYTES} bytes` };
1599
- let raw;
1343
+ var STOP_WAIT_MS = 3e4;
1344
+ var POLL_MS = 200;
1345
+ var ASK_AGAIN_MS = 1e3;
1346
+ var EXIT_REPORT_WAIT_MS = 2e3;
1347
+ var exitsLogged = /* @__PURE__ */ new Map();
1348
+ var BUILD_HOME = "/build/home";
1349
+ async function startSender(input) {
1350
+ const { config, job, jobDir, deps } = input;
1351
+ const tag = `#${job.item}`;
1352
+ if (job.itemKey === void 0) return void 0;
1353
+ const spawnFn = deps.spawnFn ?? spawn;
1600
1354
  try {
1601
- raw = JSON.parse(read.stdout);
1602
- } catch {
1603
- return { ok: false, reason: "its check items file (OUTERLAYER_CHECK_ITEMS) is not JSON" };
1604
- }
1605
- const parsed = CheckItemsSchema.safeParse(raw);
1606
- if (!parsed.success) {
1607
- return { ok: false, reason: `its check items file does not match the check item shape: ${zodProblem(parsed.error.issues)}` };
1355
+ const launch = launchFor(config, job, jobDir, deps);
1356
+ const fd = openSync(join(jobDir, "sender.log"), "a", RUNNER_FILE_MODE);
1357
+ let pid;
1358
+ let exitLogged;
1359
+ try {
1360
+ const child = spawnFn(launch.command, launch.args, { env: launch.env, cwd: launch.cwd, detached: true, stdio: ["ignore", fd, fd] });
1361
+ child.on("error", (err) => deps.log.error(`${tag} session sender failed: ${describeCallFailure(err)}`));
1362
+ exitLogged = new Promise((resolve) => {
1363
+ child.on("exit", (code, signal) => {
1364
+ deps.log.line(`${tag} session sender exited \xB7 ${signal ?? `code ${code}`} \xB7 see sender.log`);
1365
+ resolve();
1366
+ });
1367
+ });
1368
+ child.unref?.();
1369
+ pid = child.pid;
1370
+ } finally {
1371
+ closeSync(fd);
1372
+ }
1373
+ if (pid === void 0) {
1374
+ deps.log.line(`${tag} session sender did not start: no pid`);
1375
+ return void 0;
1376
+ }
1377
+ exitsLogged.set(pid, exitLogged);
1378
+ deps.log.line(`${tag} session sender started \xB7 pid ${pid}`);
1379
+ return { pid, start: startTimeOf(pid) ?? void 0 };
1380
+ } catch (err) {
1381
+ deps.log.error(`${tag} session sender did not start: ${describeCallFailure(err)} \xB7 continuing`);
1382
+ return void 0;
1608
1383
  }
1609
- return { ok: true, items: parsed.data.map((item) => item.reason === void 0 ? item : { ...item, reason: scrub(item.reason) }) };
1610
1384
  }
1611
- function isJudgeCheck(check) {
1612
- return "judge" in check;
1385
+ function launchFor(config, job, jobDir, deps) {
1386
+ if (job.microvm !== void 0) {
1387
+ const runtime = deps.runtime ?? createContainerRuntime();
1388
+ const cli = deps.cliPath ?? join(runtime.cliBuild().distDir, "index.js");
1389
+ return { ...microVmSenderCommand({ job, jobDir, runtime, config, cli }), cwd: jobDir };
1390
+ }
1391
+ if (job.container !== void 0) {
1392
+ return { ...containerSenderCommand({ job, runtime: deps.runtime ?? createContainerRuntime(), config }), cwd: jobDir };
1393
+ }
1394
+ const env = jobEnv(config, job.item, job.itemKey, jobDir, process.env, job.branch, job.commitIdentity?.author);
1395
+ const build = job.build;
1396
+ const hostEnv = build === void 0 ? env : hostCommandEnvironment(env, build, config, job.commitIdentity?.hooksPath);
1397
+ if (build?.exec !== void 0) {
1398
+ return { command: build.exec, args: execArgv(job.workdir, ["outerlayer", "session-sender"]), env: hostEnv, cwd: jobDir };
1399
+ }
1400
+ return { command: process.execPath, args: [deps.cliPath ?? process.argv[1] ?? "outerlayer", "session-sender"], env: hostEnv, cwd: job.workdir ?? jobDir };
1613
1401
  }
1614
- function lastLine(text2) {
1615
- return text2.split(/\r\n|\r|\n/).map((line) => line.trim()).filter((line) => line !== "").at(-1);
1402
+ async function restartSender(input) {
1403
+ const { job, deps } = input;
1404
+ const state = job.container;
1405
+ if (state !== void 0 && job.itemKey !== void 0) {
1406
+ const runtime = deps.runtime ?? createContainerRuntime();
1407
+ const pidfile = senderPidfile(BUILD_HOME, Number(job.item));
1408
+ const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
1409
+ const running = async () => {
1410
+ const probe = await runtime.host.exec({ container: state.container, user: state.agentUser ?? ADDED_USER, env: { literals: {}, passed: {} }, argv: ["/bin/sh", "-c", senderAliveScript(pidfile)] });
1411
+ return probe.status === 0 && probe.stdout.trim() === "alive";
1412
+ };
1413
+ try {
1414
+ await signalInContainer(runtime, state, pidfile, "TERM");
1415
+ const deadline = (deps.waitMs ?? STOP_WAIT_MS) / POLL_MS;
1416
+ for (let waited = 0; await running() && waited < deadline; waited += 1) await sleep(POLL_MS);
1417
+ if (await running()) {
1418
+ deps.log.error(`#${job.item} the session sender an earlier runner left did not stop in time \xB7 killing it`);
1419
+ await signalInContainer(runtime, state, pidfile, "KILL");
1420
+ }
1421
+ } catch (err) {
1422
+ deps.log.error(`#${job.item} stopping the session sender an earlier runner left failed: ${describeCallFailure(err)} \xB7 continuing`);
1423
+ }
1424
+ }
1425
+ return startSender(input);
1616
1426
  }
1617
- function readInside(root, rel) {
1618
- const path = join(root, rel);
1427
+ function isLive(sender) {
1428
+ if (sender.start !== void 0) return isSameProcess(sender.pid, sender.start);
1619
1429
  try {
1620
- if (!lstatSync(path).isFile()) return void 0;
1621
- const inside = realpathSync(path);
1622
- const base = realpathSync(root);
1623
- return inside.startsWith(base + sep) ? readFileSync(inside, "utf8") : void 0;
1430
+ process.kill(sender.pid, 0);
1431
+ return true;
1624
1432
  } catch {
1625
- return void 0;
1433
+ return false;
1626
1434
  }
1627
1435
  }
1628
- var MAINTAINED_BRIEF = /^\.(?:agents|outerlayer)\/skills\/(outerlayer-[a-z]+)\/([A-Za-z0-9_./-]+)$/;
1629
- function readBrief(root, brief) {
1630
- const own = readInside(root, brief);
1631
- if (own !== void 0) return own;
1632
- const match = MAINTAINED_BRIEF.exec(brief);
1633
- if (match === null || match[2].split("/").includes("..") || !MAINTAINED_SKILLS.includes(match[1])) return void 0;
1436
+ function signalHost(sender, signal) {
1437
+ if (!isLive(sender)) return;
1634
1438
  try {
1635
- return readFileSync(join(resolvePackRoot(), "maintained", match[1], match[2]), "utf8");
1439
+ process.kill(-sender.pid, signal);
1636
1440
  } catch {
1637
- return void 0;
1441
+ try {
1442
+ process.kill(sender.pid, signal);
1443
+ } catch {
1444
+ }
1638
1445
  }
1639
1446
  }
1640
- function validatorFilesIn(root) {
1641
- const dir = join(root, VALIDATORS_DIR);
1642
- let names;
1643
- try {
1644
- if (!lstatSync(dir).isDirectory()) return [];
1645
- names = readdirSync(dir);
1646
- } catch {
1647
- return [];
1648
- }
1649
- const files = [];
1650
- for (const name of names.filter((entry) => /\.ya?ml$/.test(entry)).sort().slice(0, MAX_VALIDATOR_FILES)) {
1651
- const path = `${VALIDATORS_DIR}/${name}`;
1652
- const content = readInside(root, path);
1653
- if (content !== void 0) files.push({ path, content });
1654
- }
1655
- return files;
1656
- }
1657
- var CRITERIA_CHECK = "acceptance-criteria";
1658
- function asHostReads(file, base, log, tag) {
1659
- const upgraded = upgradedStarterText(file.path, file.content, { base });
1660
- if (upgraded === void 0) return file;
1661
- log.line(`${tag} ${file.path} is an unedited copy an earlier release wrote, so it is read as this CLI writes it`);
1662
- return { path: file.path, content: upgraded };
1663
- }
1664
- function checksIn(root, log, tag) {
1665
- const base = existsSync(join(root, AGENTS_SKILLS_BASE)) ? AGENTS_SKILLS_BASE : OUTERLAYER_SKILLS_BASE;
1666
- const policyText = readInside(root, POLICY_FILE);
1667
- const policy = policyText === void 0 ? null : asHostReads({ path: POLICY_FILE, content: policyText }, base, log, tag);
1668
- const loaded = parsePolicy(policy, validatorFilesIn(root).map((file) => asHostReads(file, base, log, tag)));
1669
- for (const problem of loaded.problems) log.error(`${tag} ${problem.file}: ${problem.problem}`);
1670
- const hostChecks = loaded.customs.filter((def) => def.runWhere === "host" && def.level !== "off" && def.declaresEmit !== null);
1671
- const checks = [];
1672
- for (const def of hostChecks) {
1673
- if (def.judge !== void 0) checks.push({ name: def.declaresEmit, judge: { brief: def.judge.brief, text: readBrief(root, def.judge.brief), subjects: def.judge.subjects } });
1674
- else if (def.command !== void 0) checks.push({ name: def.declaresEmit, command: def.command });
1675
- }
1676
- const unrecorded = loaded.customs.filter((def) => def.level !== "off" && def.declaresEmit === CRITERIA_CHECK && !checks.some((check) => check.name === CRITERIA_CHECK)).map((def) => def.id);
1677
- return { checks, unrecorded };
1678
- }
1679
- async function readDefinitions(input, repository, scratch, token, tokens) {
1680
- const { config, job, deps } = input;
1681
- const tag = `#${job.item}`;
1682
- const stage = deps.stage ?? stageGovernedContext;
1683
- const controlPlaneUrl = (controlPlane) => deps.runtime.remoteFor?.(controlPlane) ?? `${deps.runtime.github.gitUrl}/${controlPlane}.git`;
1684
- const governing = (job.container ?? job.microvm)?.governing;
1685
- const kept = keptContextTree(input.jobDir, governing);
1686
- if (kept !== void 0) return { ok: true, ...checksIn(kept, deps.log, tag) };
1687
- const staged = await stage({
1688
- config,
1689
- repository,
1690
- ...governing === void 0 ? {} : { source: { repository: governing.repository, ref: governing.ref, sha: governing.commit } },
1691
- buildId: `checks-${randomBytes(4).toString("hex")}`,
1692
- buildDir: scratch,
1693
- fetchImpl: deps.fetchImpl,
1694
- controlPlaneUrl,
1695
- controlPlaneToken: tokens === void 0 ? void 0 : () => tokens.controlPlane(),
1696
- log: deps.log,
1697
- tag
1447
+ async function signalInGuest(runtime, state, pidfile, signal) {
1448
+ const { status } = await runtime.vm.exec(state.vsockPath, {
1449
+ argv: ["/bin/sh", "-c", senderSignalScript(pidfile, signal)],
1450
+ user: state.agentUser ?? ADDED_USER,
1451
+ cwd: "/",
1452
+ env: { PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" }
1698
1453
  });
1699
- if (staged.kind === "refused") return { ok: false, reason: staged.reason };
1700
- if (staged.kind === "staged") {
1701
- try {
1702
- return { ok: true, ...checksIn(join(staged.dir, "tree"), deps.log, tag) };
1703
- } finally {
1704
- rmSync(staged.dir, { recursive: true, force: true });
1705
- }
1706
- }
1707
- const repo = join(scratch, "repo");
1708
- const remote = deps.runtime.remoteFor?.(repository) ?? `${deps.runtime.github.gitUrl}/${repository}.git`;
1709
- const started = (job.container ?? job.microvm)?.startCommit;
1710
- const commit = started !== void 0 && started !== "" ? started : job.environment?.recipe?.sourceCommit ?? await deps.runtime.readHead(remote, scratch);
1711
- if (commit === void 0) return { ok: false, reason: "the repository's default branch could not be read" };
1712
- const fetched = await (deps.fetchCommit ?? fetchCommit)({ remote, dir: repo, commit, token });
1713
- if (!fetched.ok) return { ok: false, reason: fetched.reason };
1714
- deps.log.line(`${tag} check definitions read at ${commit.slice(0, 12)}`);
1715
- return { ok: true, ...checksIn(repo, deps.log, tag) };
1454
+ return status === 0;
1716
1455
  }
1717
- function timeoutAfter(ms, sleep) {
1718
- if (sleep !== void 0) return { done: sleep(ms).then(() => "timeout"), cancel: () => void 0 };
1456
+ async function exitReported(pid) {
1457
+ const logged = exitsLogged.get(pid);
1719
1458
  let timer;
1720
- const done = new Promise((resolve) => {
1721
- timer = setTimeout(() => resolve("timeout"), ms);
1722
- timer.unref();
1459
+ const bound = new Promise((resolve) => {
1460
+ timer = setTimeout(resolve, EXIT_REPORT_WAIT_MS);
1461
+ timer.unref?.();
1723
1462
  });
1724
- return { done, cancel: () => clearTimeout(timer) };
1725
- }
1726
- function noCleanEnvironment(job) {
1727
- const ran = job.build?.workDir !== void 0 ? "builtin:process" : "a provision hook of the host's own";
1728
- return `the host has no clean environment for checks: this build ran under ${ran}, and checks run only in a new container or microVM from a build's image`;
1463
+ await Promise.race([logged, bound]);
1464
+ clearTimeout(timer);
1465
+ exitsLogged.delete(pid);
1729
1466
  }
1730
- function makeRecorder(input) {
1731
- const { config, job, deps } = input;
1467
+ async function stopSender(input) {
1468
+ const { job, deps } = input;
1469
+ const sender = job.sender;
1470
+ if (sender === void 0) return;
1732
1471
  const tag = `#${job.item}`;
1733
- const now = deps.now ?? (() => /* @__PURE__ */ new Date());
1734
- let signer;
1735
- return async (name, result, sentence, opts = {}) => {
1736
- signer ??= await signerFor(config.configPath);
1737
- const keyTail = opts.keyTail ?? "";
1738
- const body = sentence === void 0 ? void 0 : cutReasonLine(`${scrubReason(sentence, config) ?? "check failed"}${keyTail}`);
1739
- const clientEmitId = `host-${createHash("sha256").update(`${job.claimId}:${job.startedAt}:${name}${opts.idTag === void 0 ? "" : `:${opts.idTag}`}`).digest("hex").slice(0, 40)}`;
1740
- const sent = await retrySend(
1741
- () => postJson(
1742
- config.url,
1743
- "/v1/emitted-results",
1744
- config.apiKey,
1745
- config.appId,
1746
- {
1747
- schemaVersion: 1,
1748
- // A field left undefined is not sent: the request is JSON.
1749
- emit: { clientEmitId, name, result, body, link: opts.link, items: opts.items, emittedAt: now().toISOString(), item: { number: Number(job.item) } }
1750
- },
1751
- deps.fetchImpl,
1752
- void 0,
1753
- signer
1754
- ),
1755
- { now: deps.now }
1756
- );
1757
- if (sent.ok) deps.log.line(`${tag} check ${name} ${result}${body === void 0 ? "" : `: ${body}`}`);
1758
- else deps.log.error(`${tag} check ${name} ${result} could not be recorded: ${sent.reason}`);
1472
+ const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
1473
+ const inContainer = job.container === void 0 ? void 0 : { state: job.container, runtime: deps.runtime ?? createContainerRuntime(), pidfile: senderPidfile(BUILD_HOME, Number(job.item)) };
1474
+ const inGuest = job.microvm === void 0 ? void 0 : { state: job.microvm, runtime: deps.runtime ?? createContainerRuntime(), pidfile: senderPidfile(BUILD_HOME, Number(job.item)) };
1475
+ const signalInside = async (signal) => {
1476
+ if (inContainer !== void 0) return signalInContainer(inContainer.runtime, inContainer.state, inContainer.pidfile, signal).catch(() => void 0);
1477
+ if (inGuest !== void 0) return signalInGuest(inGuest.runtime, inGuest.state, inGuest.pidfile, signal).catch(() => void 0);
1478
+ return void 0;
1759
1479
  };
1760
- }
1761
- async function runHostChecks(input) {
1762
- const { config, job, jobDir, deps } = input;
1763
- const { runtime, log } = deps;
1764
- const tag = `#${job.item}`;
1765
- const now = deps.now ?? (() => /* @__PURE__ */ new Date());
1766
- const repository = job.repository ?? job.container?.repository;
1767
- if (repository === void 0) return;
1768
- const jobFilePath = join(jobDir, "job.json");
1769
- const tokens = runtime.credentials.get(jobDir);
1770
- let readToken;
1771
1480
  try {
1772
- readToken = await tokens?.token("read");
1481
+ let reached = true;
1482
+ if (inContainer !== void 0 || inGuest !== void 0) reached = await signalInside("TERM");
1483
+ else signalHost(sender, "SIGTERM");
1484
+ const deadline = (deps.waitMs ?? STOP_WAIT_MS) / POLL_MS;
1485
+ for (let waited = 0; isLive(sender) && waited < deadline; waited += 1) {
1486
+ await sleep(POLL_MS);
1487
+ if (!reached && (waited + 1) % (ASK_AGAIN_MS / POLL_MS) === 0) reached = await signalInside("TERM");
1488
+ }
1489
+ if (isLive(sender)) {
1490
+ deps.log.error(`${tag} session sender did not stop in time \xB7 killing it`);
1491
+ await signalInside("KILL");
1492
+ signalHost(sender, "SIGKILL");
1493
+ }
1494
+ await exitReported(sender.pid);
1773
1495
  } catch (err) {
1774
- log.error(`${tag} no read token for the checks: ${describeCallFailure(err)}`);
1496
+ deps.log.error(`${tag} stopping the session sender failed: ${describeCallFailure(err)} \xB7 continuing`);
1775
1497
  }
1776
- const scratch = join(jobDir, "checks", "definitions");
1777
- rmSync(scratch, { recursive: true, force: true });
1778
- mkdirSync(scratch, { recursive: true });
1779
- let definitions;
1780
- try {
1781
- definitions = await readDefinitions(input, repository, scratch, readToken, tokens);
1782
- } catch (err) {
1783
- definitions = { ok: false, reason: describeCallFailure(err) };
1784
- } finally {
1785
- removeStagedContext(scratch);
1498
+ }
1499
+ var RUNNER_LOG_FILE = "runner.log";
1500
+ var RUNNER_LOG_KEEP_BYTES = 65536;
1501
+ function isTailedStage(stage) {
1502
+ const tailed = ["provision", "sync", "cleanup", "report"];
1503
+ return tailed.includes(stage);
1504
+ }
1505
+ var PROVISION_PROBLEM = /\b(warn(ing)?|error|fail(ed|ure)?|refused)\b/i;
1506
+ var READ_LIMIT_BYTES = 1048576;
1507
+ var ITEM_LINE = /^#(\d+)\b/;
1508
+ var AttemptLogBook = class {
1509
+ constructor(now = () => /* @__PURE__ */ new Date()) {
1510
+ this.now = now;
1511
+ this.dirs = /* @__PURE__ */ new Map();
1512
+ this.lastByKey = /* @__PURE__ */ new Map();
1786
1513
  }
1787
- if (!definitions.ok) {
1788
- log.line(`${tag} checks not run: the definitions could not be read: ${definitions.reason}`);
1789
- rmSync(scratch, { recursive: true, force: true });
1790
- return;
1514
+ /** Starts keeping lines for `item` in `jobDirPath`. Idempotent. */
1515
+ open(item, jobDirPath) {
1516
+ this.dirs.set(Number(item), jobDirPath);
1791
1517
  }
1792
- const checks = definitions.checks.filter((check) => !isJudgeCheck(check));
1793
- if (checks.length === 0) {
1794
- rmSync(scratch, { recursive: true, force: true });
1795
- return;
1518
+ /** Stops keeping lines for `item`; its file stays on disk until housekeeping removes the job directory. */
1519
+ close(item) {
1520
+ const number = Number(item);
1521
+ this.dirs.delete(number);
1522
+ for (const key of [...this.lastByKey.keys()]) {
1523
+ if (key.startsWith(`${number}:`)) this.lastByKey.delete(key);
1524
+ }
1796
1525
  }
1797
- log.line(`${tag} checks started (${checks.map((check) => check.name).join(", ")})`);
1798
- const startedMs = now().getTime();
1799
- const recorder = makeRecorder({ config, job, deps });
1800
- const record = (check, result, sentence, keyTail = "", items) => recorder(check.name, result, sentence, { keyTail, items });
1801
- const failAll = async (reason, keyTail = "") => {
1802
- log.error(`${tag} checks failed: ${reason}${keyTail}`);
1803
- for (const check of checks) await record(check, "fail", reason, keyTail);
1804
- };
1805
- try {
1806
- if (job.container === void 0 && job.microvm === void 0) return await failAll(noCleanEnvironment(job));
1807
- if (tokens === void 0) return await failAll("the build's repository tokens are no longer held, so the branch cannot be fetched");
1808
- if (job.branch === void 0) return await failAll("the build pushed no branch: the claim names none");
1809
- const claimed = repositoriesOf({ repositories: job.repositories, repository, branch: job.branch });
1810
- if (claimed.length === 0) return await failAll("the claim names no repository to check out");
1811
- const remoteOf = (name) => runtime.remoteFor?.(name) ?? `${runtime.github.gitUrl}/${name}.git`;
1812
- const remote = remoteOf(repository);
1813
- const pushed = /* @__PURE__ */ new Map();
1814
- const unpushed = [];
1815
- let primaryHead;
1816
- for (const entry of claimed) {
1817
- let entryToken = readToken;
1818
- if (!entry.primary) {
1819
- try {
1820
- entryToken = await tokens.token("read", entry.repository);
1821
- } catch (err) {
1822
- return await failAll(`no read token for ${entry.repository}: ${describeCallFailure(err)}`);
1823
- }
1824
- }
1825
- const read = await (deps.readBranch ?? readBranchHead)({ remote: remoteOf(entry.repository), dir: scratch, branch: entry.branch, token: entryToken });
1826
- if (read.kind === "unreadable") return await failAll(`the branch ${entry.branch} could not be read${entry.primary ? "" : ` on ${entry.repository}`}: ${read.reason}`);
1827
- if (read.kind === "absent") {
1828
- unpushed.push(entry);
1829
- continue;
1526
+ /** The log with every line it writes for an open item also kept in that item's file. */
1527
+ wrap(log) {
1528
+ return {
1529
+ line: (text2) => {
1530
+ log.line(text2);
1531
+ this.keepItemLine(text2);
1532
+ },
1533
+ error: (text2) => {
1534
+ log.error(text2);
1535
+ this.keepItemLine(text2);
1830
1536
  }
1831
- pushed.set(entry.repository, entry.branch);
1832
- if (entry.primary) primaryHead = read.sha;
1833
- }
1834
- if (pushed.size === 0) {
1835
- return await failAll(`the build pushed no branch: ${unpushed.map((entry) => `${entry.branch} does not exist on ${entry.repository}`).join("; ")}`);
1836
- }
1837
- const repositoriesToCheckOut = claimed;
1838
- const limitMinutes = config.checks?.timeLimitMinutes ?? DEFAULT_CHECK_TIME_LIMIT_MINUTES;
1839
- const minutes = `${limitMinutes} ${limitMinutes === 1 ? "minute" : "minutes"}`;
1840
- const environmentLog = join(jobDir, "checks", "environment.log");
1841
- let transcripts = { ok: false, reason: "the runner holds no way to read the build's sessions" };
1842
- if (deps.collectSessions !== void 0) {
1843
- try {
1844
- transcripts = await deps.collectSessions();
1845
- } catch (err) {
1846
- transcripts = { ok: false, reason: describeCallFailure(err) };
1847
- }
1848
- }
1849
- if (job.microvm !== void 0) {
1850
- if (deps.endBuildMicroVm === void 0) return await failAll("the runner has no way to stop the build's microVM, so no check environment was made");
1851
- try {
1852
- await deps.endBuildMicroVm();
1853
- } catch (err) {
1854
- return await failAll(`the build's microVM could not be stopped, so no check environment was made: ${describeCallFailure(err)}`);
1855
- }
1856
- }
1857
- let abandoned = false;
1858
- const setup = async () => {
1859
- const started = job.microvm !== void 0 ? await startCheckMicroVm({
1860
- config,
1861
- runtime,
1862
- job,
1863
- jobDir,
1864
- tokens,
1865
- log,
1866
- aborted: () => abandoned,
1867
- save: (state) => {
1868
- if (abandoned) return;
1869
- job.checkMicrovm = state;
1870
- writeJobFile(jobFilePath, job);
1871
- }
1872
- }) : await startCheckContainer({
1873
- config,
1874
- runtime,
1875
- job,
1876
- jobDir,
1877
- tokens,
1878
- log,
1879
- save: (state) => {
1880
- job.checkContainer = state;
1881
- writeJobFile(jobFilePath, job);
1882
- }
1883
- });
1884
- if (!started.ok) return `the check environment could not be made: ${started.reason}`;
1885
- const made = "container" in started ? started.container : started.environment;
1886
- const steps = [
1887
- {
1888
- argv: ["git", "config", "--global", `url.http://127.0.0.1:${RELAY_PORT}/github.com/.insteadOf`, "https://github.com/"],
1889
- workdir: "/build/work",
1890
- failed: (status) => `configuring git failed (exit ${status})`
1891
- },
1892
- ...repositoriesToCheckOut.flatMap((entry) => {
1893
- const branch = pushed.get(entry.repository);
1894
- const path = entry.primary ? CHECKOUT_DIR : repositoryPath(entry.repository);
1895
- const clone = {
1896
- argv: ["git", "clone", "--filter=blob:none", "--single-branch", ...branch === void 0 ? [] : ["--branch", branch], `https://github.com/${entry.repository}.git`, path],
1897
- workdir: "/build/work",
1898
- failed: (status) => `the clone of ${branch ?? `the default branch of ${entry.repository}`} failed (exit ${status})`
1899
- };
1900
- return entry.primary && repositoriesToCheckOut.length > 1 ? [
1901
- clone,
1902
- {
1903
- argv: ["sh", "-c", 'mkdir -p "$(dirname "$1")" && ln -s "$2" "$1"', "link", repositoryPath(entry.repository), CHECKOUT_DIR],
1904
- workdir: "/build/work",
1905
- failed: (status) => `linking ${entry.repository} at ${repositoryPath(entry.repository)} failed (exit ${status})`
1906
- }
1907
- ] : [clone];
1908
- })
1909
- ];
1910
- for (const step of steps) {
1911
- if (abandoned) return "the check environment setup ran out of time";
1912
- const ran = await made.exec(step.argv, step.workdir, { logPath: environmentLog });
1913
- if (ran.status !== 0) return `the check environment could not be prepared: ${step.failed(ran.status)}`;
1914
- }
1915
- const lifecycleDir = lifecycleWorkdir(repositoriesToCheckOut, (job.container ?? job.microvm)?.governing?.repository);
1916
- for (const step of lifecycleSteps((job.container ?? job.microvm)?.lifecycle ?? {})) {
1917
- log.line(`${tag} checks lifecycle ${step.name} (${step.source})`);
1918
- const ran = await made.exec(step.argv, lifecycleDir, { logPath: environmentLog });
1919
- if (ran.status !== 0) return `the check environment could not be prepared: ${lifecycleFailureReason(step, ran.status)}`;
1920
- }
1921
- return made;
1922
1537
  };
1923
- const setupTimer = timeoutAfter(limitMinutes * 6e4, deps.sleep);
1924
- let settled;
1538
+ }
1539
+ /** One line for `item` that does not itself start with its number. */
1540
+ note(item, text2) {
1541
+ this.append(Number(item), text2);
1542
+ }
1543
+ /** One line, written only when `text` differs from the last one written under `key` for the item. */
1544
+ noteChanged(item, key, text2) {
1545
+ const number = Number(item);
1546
+ const slot = `${number}:${key}`;
1547
+ if (this.lastByKey.get(slot) === text2) return;
1548
+ this.lastByKey.set(slot, text2);
1549
+ this.append(number, text2);
1550
+ }
1551
+ keepItemLine(text2) {
1552
+ const match = ITEM_LINE.exec(text2);
1553
+ if (match !== null) this.append(Number(match[1]), text2);
1554
+ }
1555
+ append(item, text2) {
1556
+ const dir = this.dirs.get(item);
1557
+ if (dir === void 0) return;
1925
1558
  try {
1926
- const outcome = await Promise.race([
1927
- setup().catch((err) => `the check environment could not be made: ${describeCallFailure(err)}`),
1928
- setupTimer.done
1929
- ]);
1930
- if (outcome === "timeout") {
1931
- abandoned = true;
1932
- return await failAll(`the check environment setup ran out of time after ${minutes}`, LIMIT_KEY_TAIL);
1933
- }
1934
- settled = outcome;
1935
- } finally {
1936
- setupTimer.cancel();
1559
+ appendFileSync(join(dir, RUNNER_LOG_FILE), `${logStamp(this.now())} ${text2.replace(/[\r\n]+/g, " ")}
1560
+ `, { mode: 384 });
1561
+ } catch {
1937
1562
  }
1938
- if (typeof settled === "string") return await failAll(settled);
1939
- const ready = settled;
1940
- try {
1941
- await writeEvalInput({
1942
- job,
1943
- dir: ready.evalInput.dir,
1944
- scratch,
1945
- remote,
1946
- readToken,
1947
- branchHead: primaryHead ?? (job.container ?? job.microvm)?.startCommit ?? "",
1948
- transcripts,
1949
- scrub: transcriptScrub(config, [config.apiKey, job.itemKey, readToken]),
1950
- readWorkItem: () => readWorkItem(config, job.item, deps.fetchImpl),
1951
- fetchDiff: deps.fetchDiff ?? ((input2) => diffBetween(input2)),
1952
- log
1953
- });
1954
- } catch (err) {
1955
- return await failAll(`the checks' input could not be written: ${describeCallFailure(err)}`);
1563
+ }
1564
+ };
1565
+ function readEnd(path, maxBytes) {
1566
+ let fd;
1567
+ try {
1568
+ fd = openSync(path, "r");
1569
+ const size = fstatSync(fd).size;
1570
+ const length = Math.min(size, maxBytes);
1571
+ const buffer = Buffer.alloc(length);
1572
+ readSync(fd, buffer, 0, length, size - length);
1573
+ return buffer.toString("utf8");
1574
+ } catch {
1575
+ return void 0;
1576
+ } finally {
1577
+ if (fd !== void 0) closeSync(fd);
1578
+ }
1579
+ }
1580
+ function lastLines(text2, count) {
1581
+ const lines = text2.split("\n");
1582
+ if (lines.at(-1) === "") lines.pop();
1583
+ return lines.slice(-200);
1584
+ }
1585
+ function capRunnerLog(text2) {
1586
+ const bytes = Buffer.from(text2, "utf8");
1587
+ if (bytes.length <= RUNNER_LOG_KEEP_BYTES) return text2;
1588
+ let start = bytes.length - RUNNER_LOG_KEEP_BYTES;
1589
+ if (bytes[start - 1] !== 10) {
1590
+ const newline = bytes.indexOf(10, start);
1591
+ if (newline + 1 < bytes.length) {
1592
+ start = newline + 1;
1593
+ } else {
1594
+ while ((bytes[start] & 192) === 128) start++;
1956
1595
  }
1596
+ }
1597
+ return `[runner log truncated: the first ${start} bytes were cut]
1598
+ ${bytes.subarray(start).toString("utf8")}`;
1599
+ }
1600
+ function buildReleaseLog(jobDirPath, endStage, scrub) {
1601
+ const parts = [];
1602
+ const own = readEnd(join(jobDirPath, RUNNER_LOG_FILE), READ_LIMIT_BYTES);
1603
+ if (own !== void 0 && own.trim() !== "") parts.push(own.endsWith("\n") ? own.slice(0, -1) : own);
1604
+ if (isTailedStage(endStage)) {
1605
+ const file = `${endStage}.log`;
1606
+ const stage = readEnd(join(jobDirPath, file), READ_LIMIT_BYTES);
1607
+ const tail = stage === void 0 ? [] : lastLines(stage);
1608
+ if (tail.length > 0) parts.push(`--- the last ${tail.length} lines of ${file} ---`, ...tail);
1609
+ }
1610
+ if (endStage !== "provision") {
1611
+ const provision = readEnd(join(jobDirPath, "provision.log"), READ_LIMIT_BYTES);
1612
+ const problems = provision === void 0 ? [] : provision.split("\n").filter((line) => PROVISION_PROBLEM.test(line)).slice(-50);
1613
+ if (problems.length > 0) parts.push(`--- provision problems (${problems.length} lines of provision.log) ---`, ...problems);
1614
+ }
1615
+ if (parts.length === 0) return void 0;
1616
+ return capRunnerLog(scrub(parts.join("\n")) + "\n");
1617
+ }
1618
+ var ENDED = "ended before provision finished";
1619
+ async function provisionProcess(input) {
1620
+ const { config, runtime, item, repository, jobDir } = input;
1621
+ if (claudeCredential(runtime.hostEnv) === void 0) return { ok: false, reason: AGENT_CREDENTIAL_MISSING };
1622
+ if (repository === void 0 || repository === null || repository === "") {
1623
+ return { ok: false, reason: "the work item names no repository to clone" };
1624
+ }
1625
+ const buildId = randomBytes(6).toString("hex");
1626
+ const buildDir2 = buildDir(config.configPath, buildId);
1627
+ const workDir = join(jobDir, "work");
1628
+ const state = { buildId, buildDir: buildDir2, workDir };
1629
+ const environment = sharedUserEnvironment(input.cliVersion);
1630
+ input.save(state, environment);
1631
+ const bail = async (reason) => {
1957
1632
  try {
1958
- await ready.evalInput.deliver();
1633
+ await cleanupProcess(runtime, state);
1959
1634
  } catch (err) {
1960
- return await failAll(err instanceof DeliveryFailure ? err.message : `the checks' input could not be written: ${describeCallFailure(err)}`);
1961
- }
1962
- let usable = true;
1963
- for (const check of checks) {
1964
- if (!usable) {
1965
- await record(check, "fail", "the check environment could not be restarted after an earlier check ran out of time");
1966
- continue;
1967
- }
1968
- const checkStartedMs = now().getTime();
1969
- const logPath = join(jobDir, "checks", `${check.name}.log`);
1970
- await ready.exec(["rm", "-f", CHECK_ITEMS_FILE], CHECKOUT_DIR, { logPath });
1971
- const running = ready.exec(["/bin/sh", "-c", check.command], CHECKOUT_DIR, { logPath });
1972
- running.catch(() => void 0);
1973
- const timer = timeoutAfter(limitMinutes * 6e4, deps.sleep);
1974
- let outcome;
1975
- try {
1976
- outcome = await Promise.race([running, timer.done]);
1977
- } catch (err) {
1978
- timer.cancel();
1979
- await record(check, "fail", `the command could not be run: ${describeCallFailure(err)}`);
1980
- continue;
1981
- }
1982
- timer.cancel();
1983
- if (outcome === "timeout") {
1984
- await record(check, "fail", `ran out of time after ${minutes}`, LIMIT_KEY_TAIL);
1985
- const restarting = ready.restart().catch(() => false);
1986
- const restartTimer = timeoutAfter(limitMinutes * 6e4, deps.sleep);
1987
- usable = await Promise.race([restarting, restartTimer.done]) === true;
1988
- restartTimer.cancel();
1989
- continue;
1990
- }
1991
- const read = await ready.exec(["head", "-c", String(MAX_CHECK_ITEMS_BYTES + 1), CHECK_ITEMS_FILE], CHECKOUT_DIR);
1992
- const items = commandItems(read, (text2) => scrubReason(text2, config) ?? text2);
1993
- if (!items.ok) {
1994
- await record(check, "fail", items.reason);
1995
- continue;
1996
- }
1997
- const failing = items.items === void 0 ? void 0 : itemsSentence(items.items, { one: "check item", many: "check items" });
1998
- if (outcome.status === 0 && failing === void 0) {
1999
- log.line(`${tag} check ${check.name} ran (${formatDuration(now().getTime() - checkStartedMs)})`);
2000
- await record(check, "pass", void 0, "", items.items);
2001
- continue;
2002
- }
2003
- if (outcome.status === 0) {
2004
- await record(check, "fail", failing, "", items.items);
2005
- continue;
2006
- }
2007
- const last = lastLine(outcome.stderr) ?? lastLine(outcome.stdout);
2008
- const scrubbed = last === void 0 ? void 0 : scrubReason(last, config);
2009
- await record(check, "fail", `exited ${outcome.status}${scrubbed === void 0 ? " with no output" : `: ${cutReasonLine(scrubbed, 140)}`}`, "", items.items);
1635
+ input.log.error(`#${item.number} cleanup failed: ${describeCallFailure(err)}`);
2010
1636
  }
1637
+ return { ok: false, reason };
1638
+ };
1639
+ if (input.aborted()) return bail(ENDED);
1640
+ mkdirSync(buildDir2, { recursive: true });
1641
+ chmodSync(buildDir2, 493);
1642
+ try {
1643
+ await startHostServices({ runtime, config, build: state, jobDir, itemKey: input.itemKey, process: true, routeLog: (line) => input.log.line(`#${item.number} ${line}`) });
2011
1644
  } catch (err) {
2012
- log.error(`${tag} checks stopped: ${describeCallFailure(err)}`);
2013
- } finally {
2014
- if (job.checkContainer !== void 0) {
2015
- await cleanupContainer(runtime, job.checkContainer).catch((err) => log.error(`${tag} removing the check environment failed: ${describeCallFailure(err)}`));
2016
- delete job.checkContainer;
2017
- writeJobFile(jobFilePath, job);
2018
- }
2019
- if (job.checkMicrovm !== void 0) {
2020
- await cleanupMicroVm(runtime, job.checkMicrovm).catch((err) => log.error(`${tag} removing the check environment failed: ${describeCallFailure(err)}`));
2021
- delete job.checkMicrovm;
2022
- writeJobFile(jobFilePath, job);
2023
- }
2024
- rmSync(scratch, { recursive: true, force: true });
2025
- rmSync(join(jobDir, "checks", EVAL_INPUT_NAME), { recursive: true, force: true });
2026
- if (existsSync(scratch)) log.error(`${tag} the checks' scratch directory could not be removed`);
2027
- log.line(`${tag} checks done (${formatDuration(now().getTime() - startedMs)})`);
1645
+ return bail(`the tunnel could not start: ${describeCallFailure(err)}`);
2028
1646
  }
1647
+ input.save(state, environment);
1648
+ if (input.aborted()) return bail(ENDED);
1649
+ const checkout = join(workDir, "checkout");
1650
+ const remote = runtime.remoteFor?.(repository) ?? `https://github.com/${repository}.git`;
1651
+ const cloned = await runtime.clone({ remote, dest: checkout });
1652
+ if (!cloned.ok) return bail(cloned.reason);
1653
+ if (input.aborted()) return bail(ENDED);
1654
+ return { ok: true, workdir: checkout, state, environment };
1655
+ }
1656
+ function cleanupProcess(runtime, state) {
1657
+ return closeHostBuild(runtime, state);
2029
1658
  }
2030
1659
 
2031
1660
  // src/runner/judge-verdict.ts
@@ -2109,15 +1738,10 @@ ${rows}
2109
1738
  }
2110
1739
 
2111
1740
  // src/runner/judge.ts
2112
- var LIMIT_KEY_TAIL2 = " (runner.checks.timeLimitMinutes)";
1741
+ var LIMIT_KEY_TAIL = " (runner.checks.timeLimitMinutes)";
2113
1742
  var MAX_DIFF_BYTES = 4 * 1024 * 1024;
2114
- var MAX_TEST_FILES = 2e4;
2115
- var MAX_TEST_FILE_BYTES = 1024 * 1024;
2116
1743
  var NO_CLAUDE = "the image has no Claude Code for the judge";
2117
1744
  var REPORT_CAPTION = "The host's criteria judge, each criterion judged against the test that cites it";
2118
- async function loadCitationFinder() {
2119
- return await import(pathToFileURL(join(resolvePackRoot(), "maintained", "outerlayer-build", "scripts", "criterion-citations.mjs")).href);
2120
- }
2121
1745
  function testFilesIn(root, isTest, limits = { files: MAX_TEST_FILES, bytes: MAX_TEST_FILE_BYTES }) {
2122
1746
  const files = [];
2123
1747
  const walk = (dir, prefix) => {
@@ -2151,26 +1775,6 @@ async function hostPassedNames(config, item, fetchImpl) {
2151
1775
  } while (since !== null);
2152
1776
  return [...latest].filter(([, result]) => result === "pass").map(([name]) => name);
2153
1777
  }
2154
- function criteriaOf(workItem) {
2155
- const criteria = Object(Object(workItem).criteria);
2156
- if (!Array.isArray(criteria?.items)) return void 0;
2157
- const drafted = criteria.drafted === true;
2158
- const items = [];
2159
- for (const entry of criteria.items) {
2160
- const { id, text: text2, proof } = Object(entry);
2161
- if (typeof id !== "string" || typeof text2 !== "string") continue;
2162
- items.push({ id, text: text2, proof: typeof proof === "string" ? proof : null, drafted });
2163
- }
2164
- return items.length === 0 ? void 0 : items;
2165
- }
2166
- function hostItemsOf(workItem, name) {
2167
- const checks = Object(workItem).checks;
2168
- if (!Array.isArray(checks)) return [];
2169
- const check = checks.find((entry) => Object(entry).name === name);
2170
- if (check?.source !== "host") return [];
2171
- const parsed = CheckItemsSchema.safeParse(check.items);
2172
- return parsed.success ? parsed.data : [];
2173
- }
2174
1778
  function environmentFor(input) {
2175
1779
  const { config, job, jobDir, deps } = input;
2176
1780
  if (deps.judgeEnvironment !== void 0) return deps.judgeEnvironment;
@@ -2391,7 +1995,7 @@ async function runJudge(input) {
2391
1995
  const reason = at.reason ?? failing;
2392
1996
  const report = at.judgeReport !== void 0 && at.judgeReport !== "" ? at.judgeReport : renderJudgeReport({ check: check.name, result, reason, items, commit: at.head, extraHead: at.finder.renderBlock(at.citations) });
2393
1997
  const link = await uploadReport(check, report, { branch: at.branch, head: at.head });
2394
- await record(check.name, result, reason, { keyTail: at.limitKey === true ? LIMIT_KEY_TAIL2 : "", link, items: sendable(items) });
1998
+ await record(check.name, result, reason, { keyTail: at.limitKey === true ? LIMIT_KEY_TAIL : "", link, items: sendable(items) });
2395
1999
  }
2396
2000
  async function finishWhole(check, at) {
2397
2001
  const where = { head: at.head, branch: at.branch };
@@ -2414,7 +2018,7 @@ async function runJudge(input) {
2414
2018
  }
2415
2019
  async function recordWhole(check, at) {
2416
2020
  const link = at.report === void 0 ? void 0 : await uploadReport(check, at.report, { branch: at.branch, head: at.head });
2417
- await record(check.name, at.result, at.reason, { keyTail: at.limitKey === true ? LIMIT_KEY_TAIL2 : "", link, items: sendable(at.items) });
2021
+ await record(check.name, at.result, at.reason, { keyTail: at.limitKey === true ? LIMIT_KEY_TAIL : "", link, items: sendable(at.items) });
2418
2022
  }
2419
2023
  function sendable(items) {
2420
2024
  if (items === void 0) return void 0;
@@ -2650,7 +2254,7 @@ function mergeScrubConfig(a, b) {
2650
2254
  function releaseScrub(config, home) {
2651
2255
  const merged = mergeScrubConfig(config.scrub, readCloudConfig(home).scrub);
2652
2256
  const secrets = [...headerVariables(config.build?.destinations), ...CLAUDE_CREDENTIAL_VARIABLES].map((name) => process.env[name] ?? "");
2653
- const held = [currentClaudeCredential(process.env)?.value, ...removedClaudeCredentialValues(), ...managedClaudeKeyValues()].filter((value) => value !== void 0);
2257
+ const held = [currentClaudeCredential(process.env)?.value, ...removedClaudeCredentialValues(), ...managedClaudeKeyValues(), ...managedBuildSettingValues()].filter((value) => value !== void 0);
2654
2258
  return { literals: [...scrubList(merged?.literals), ...secrets, ...held], patterns: scrubList(merged?.patterns) };
2655
2259
  }
2656
2260
  function transcriptScrub(config, secrets) {
@@ -2927,6 +2531,15 @@ function readTunnelRecord(path) {
2927
2531
  return void 0;
2928
2532
  }
2929
2533
  }
2534
+ var VALUES_NOT_HELD = "the runner restarted and could not read this build's variables from its factory again, so it cannot remove their values from what it uploads";
2535
+ var SCRUBBED_RELEASE_LOG = "release-log.txt";
2536
+ function readScrubbedReleaseLog(jobDirPath) {
2537
+ try {
2538
+ return readFileSync(join(jobDirPath, SCRUBBED_RELEASE_LOG), "utf8");
2539
+ } catch {
2540
+ return void 0;
2541
+ }
2542
+ }
2930
2543
  async function finishJob(job, jobDirPath, outcome, config, deps, ending) {
2931
2544
  const spawnFn = deps.spawnFn ?? spawn;
2932
2545
  const now = deps.now ?? (() => /* @__PURE__ */ new Date());
@@ -2935,6 +2548,7 @@ async function finishJob(job, jobDirPath, outcome, config, deps, ending) {
2935
2548
  const logPath = join(jobDirPath, "command.log");
2936
2549
  const tag = `#${job.item}`;
2937
2550
  deps.attemptLogs?.open(job.item, jobDirPath);
2551
+ const valuesNotHeld = (job.managedVariables?.length ?? 0) > 0 && managedBuildSettingsFor(jobDirPath) === void 0;
2938
2552
  if (job.sender !== void 0) {
2939
2553
  await stopSender({ job, deps: { spawnFn, runtime: deps.container, log } });
2940
2554
  delete job.sender;
@@ -2978,6 +2592,10 @@ async function finishJob(job, jobDirPath, outcome, config, deps, ending) {
2978
2592
  writeJobFile(jobFilePath, job);
2979
2593
  }
2980
2594
  const uploadSessions = async (workdir) => {
2595
+ if (valuesNotHeld) {
2596
+ log.error(`${tag} sync skipped: ${VALUES_NOT_HELD}`);
2597
+ return;
2598
+ }
2981
2599
  if (job.container === void 0 && job.microvm === void 0 && job.build?.exec === void 0 && workdirProblem(workdir) !== void 0) {
2982
2600
  log.line(`${tag} sync skipped: workdir ${workdir} is gone`);
2983
2601
  } else if (job.itemKey === void 0) {
@@ -3036,7 +2654,8 @@ async function finishJob(job, jobDirPath, outcome, config, deps, ending) {
3036
2654
  log.error(`${tag} removing an earlier attempt's check environment failed: ${describeCallFailure(err)} \xB7 continuing`);
3037
2655
  }
3038
2656
  }
3039
- if (outcome === "ok" && !job.cleanupRan && job.judgeRan !== true) {
2657
+ if (valuesNotHeld && outcome === "ok" && !job.cleanupRan) log.error(`${tag} the host's judge and checks skipped: ${VALUES_NOT_HELD}`);
2658
+ if (outcome === "ok" && !job.cleanupRan && job.judgeRan !== true && !valuesNotHeld) {
3040
2659
  setStage(job, jobDirPath, "judge");
3041
2660
  job.judgeRan = true;
3042
2661
  writeJobFile(jobFilePath, job);
@@ -3051,7 +2670,7 @@ async function finishJob(job, jobDirPath, outcome, config, deps, ending) {
3051
2670
  log.error(`${tag} the host's judge stopped: ${describeCallFailure(err)} \xB7 continuing`);
3052
2671
  }
3053
2672
  }
3054
- if (outcome === "ok" && !job.cleanupRan && job.checksRan !== true) {
2673
+ if (outcome === "ok" && !job.cleanupRan && job.checksRan !== true && !valuesNotHeld) {
3055
2674
  setStage(job, jobDirPath, "checks");
3056
2675
  job.checksRan = true;
3057
2676
  writeJobFile(jobFilePath, job);
@@ -3265,12 +2884,17 @@ async function finishJob(job, jobDirPath, outcome, config, deps, ending) {
3265
2884
  writeJobFile(jobFilePath, job);
3266
2885
  }
3267
2886
  const sentAtMs = now().getTime();
3268
- const runnerLog = releaseLogFor(jobDirPath, job, config);
2887
+ const runnerLog = valuesNotHeld ? `The runner sends no log for this attempt: ${VALUES_NOT_HELD}.
2888
+ ` : releaseLogFor(jobDirPath, job, config);
3269
2889
  const detail = outcome === void 0 ? { log: runnerLog } : { stage: job.endStage, exitCode: job.endExitCode, reason: job.reason, environment: job.environment, log: runnerLog };
3270
2890
  const release = await retrySend(() => releaseLease(config, job.item, outcome, detail, deps.fetchImpl), {
3271
2891
  now: deps.now,
3272
2892
  sleep: deps.sleep
3273
2893
  });
2894
+ if (!release.ok && release.retryable && job.managedVariables !== void 0 && runnerLog !== void 0) {
2895
+ writeFileSync(join(jobDirPath, SCRUBBED_RELEASE_LOG), runnerLog, { mode: RUNNER_FILE_MODE });
2896
+ }
2897
+ dropManagedBuildSettings(jobDirPath);
3274
2898
  const alreadyReleased = release.ok && wasAlreadyReleased(release.value, outcome, sentAtMs);
3275
2899
  const endedAt = now();
3276
2900
  job.outcome = outcome;
@@ -3323,244 +2947,528 @@ async function retryFailedReleases(jobsDirPath, config, deps) {
3323
2947
  if (!job || job.releaseFailed === void 0 || job.releaseFailed === ALREADY_RELEASED || isUnfinished(job)) {
3324
2948
  continue;
3325
2949
  }
3326
- const runnerLog = releaseLogFor(dirPath, job, config);
2950
+ const runnerLog = readScrubbedReleaseLog(dirPath) ?? releaseLogFor(dirPath, job, config);
3327
2951
  const detail = job.outcome === void 0 ? { log: runnerLog } : { stage: job.endStage, exitCode: job.endExitCode, reason: job.reason, environment: job.environment, log: runnerLog };
3328
2952
  const release = await retrySend(() => releaseLease(config, job.item, job.outcome, detail, deps.fetchImpl), {
3329
2953
  now: deps.now,
3330
2954
  sleep: deps.sleep
3331
2955
  });
3332
2956
  if (release.ok || !release.retryable) {
2957
+ rmSync(join(dirPath, SCRUBBED_RELEASE_LOG), { force: true });
3333
2958
  job.releaseFailed = void 0;
3334
2959
  writeJobFile(jobFilePath, job);
3335
2960
  } else {
3336
2961
  job.releaseFailed = release.reason;
3337
2962
  writeJobFile(jobFilePath, job);
3338
2963
  }
3339
- results.push({ job, released: release.ok });
3340
- }
3341
- return results;
3342
- }
3343
- function isUnfinished(job) {
3344
- return job.outcome === void 0 && job.endedAt === void 0;
3345
- }
3346
- function unfinishedJobs(jobsDirPath) {
3347
- if (!existsSync(jobsDirPath)) return [];
3348
- const found = [];
3349
- for (const entry of readdirSync(jobsDirPath, { withFileTypes: true })) {
3350
- if (!entry.isDirectory()) continue;
3351
- const dirPath = join(jobsDirPath, entry.name);
3352
- const job = readJobFile(join(dirPath, "job.json"));
3353
- if (job && isUnfinished(job)) found.push({ dirPath, job });
2964
+ results.push({ job, released: release.ok });
2965
+ }
2966
+ return results;
2967
+ }
2968
+ function isUnfinished(job) {
2969
+ return job.outcome === void 0 && job.endedAt === void 0;
2970
+ }
2971
+ function unfinishedJobs(jobsDirPath) {
2972
+ if (!existsSync(jobsDirPath)) return [];
2973
+ const found = [];
2974
+ for (const entry of readdirSync(jobsDirPath, { withFileTypes: true })) {
2975
+ if (!entry.isDirectory()) continue;
2976
+ const dirPath = join(jobsDirPath, entry.name);
2977
+ const job = readJobFile(join(dirPath, "job.json"));
2978
+ if (job && isUnfinished(job)) found.push({ dirPath, job });
2979
+ }
2980
+ return found;
2981
+ }
2982
+ function isContainerTracked(job) {
2983
+ return job.container?.tracking === "container";
2984
+ }
2985
+ function isMicroVmTracked(job) {
2986
+ return job.microvm?.tracking === "microvm";
2987
+ }
2988
+ function isTracked(job) {
2989
+ return isContainerTracked(job) || isMicroVmTracked(job);
2990
+ }
2991
+ function trackingOf(job) {
2992
+ return job.container?.tracking ?? job.microvm?.tracking;
2993
+ }
2994
+ function isReadable(job) {
2995
+ return readsJobState(job, READ_JOB_STATE_FORMATS);
2996
+ }
2997
+ function listUnreadableJobs(jobsDirPath) {
2998
+ return unfinishedJobs(jobsDirPath).flatMap(({ dirPath, job }) => isReadable(job) ? [] : [{ dirPath, job, format: jobStateFormatOf(job) }]);
2999
+ }
3000
+ function listTrackedJobs(jobsDirPath) {
3001
+ return unfinishedJobs(jobsDirPath).flatMap(({ dirPath, job }) => {
3002
+ if (!isReadable(job)) return [];
3003
+ const tracking = trackingOf(job);
3004
+ if (tracking === void 0) return [];
3005
+ if ((tracking === "container" || tracking === "microvm") && inTeardown(job)) return [];
3006
+ return [{ dirPath, job, known: tracking === "container" || tracking === "microvm" }];
3007
+ });
3008
+ }
3009
+ function listAdoptableJobs(jobsDirPath) {
3010
+ return unfinishedJobs(jobsDirPath).filter(
3011
+ ({ job }) => isReadable(job) && job.microvm === void 0 && job.pid !== void 0 && job.start !== void 0 && isSameProcess(job.pid, job.start)
3012
+ );
3013
+ }
3014
+ async function recoverLostJobs(jobsDirPath, config, deps) {
3015
+ const recovered = [];
3016
+ for (const { dirPath, job } of unfinishedJobs(jobsDirPath)) {
3017
+ if (!isReadable(job)) continue;
3018
+ const tracking = trackingOf(job);
3019
+ if (tracking !== void 0 && tracking !== "container" && tracking !== "microvm") {
3020
+ recovered.push(await finishJob(job, dirPath, "interrupted", config, deps, { reason: UNKNOWN_TRACKING_REASON }));
3021
+ continue;
3022
+ }
3023
+ if ((tracking === "container" || tracking === "microvm") && !inTeardown(job)) continue;
3024
+ const alive = job.pid !== void 0 && job.start !== void 0 && isSameProcess(job.pid, job.start);
3025
+ if (alive) continue;
3026
+ const recorded = outcomeFromExitCode(dirPath);
3027
+ if (recorded !== void 0) job.exitCode = recorded.code;
3028
+ recovered.push(await finishJob(job, dirPath, recorded?.outcome ?? "interrupted", config, deps));
3029
+ }
3030
+ return recovered;
3031
+ }
3032
+
3033
+ // src/runner/host-checks.ts
3034
+ var MAX_VALIDATOR_FILES = 20;
3035
+ var VALIDATORS_DIR = ".outerlayer/validators";
3036
+ var POLICY_FILE = ".outerlayer/policy.yaml";
3037
+ var LIMIT_KEY_TAIL2 = " (runner.checks.timeLimitMinutes)";
3038
+ var MAX_CHECK_ITEMS_BYTES = 1024 * 1024;
3039
+ function commandItems(read, scrub) {
3040
+ if (read.status !== 0 || read.stdout.trim() === "") return { ok: true, items: void 0 };
3041
+ if (Buffer.byteLength(read.stdout) > MAX_CHECK_ITEMS_BYTES) return { ok: false, reason: `its check items file is longer than ${MAX_CHECK_ITEMS_BYTES} bytes` };
3042
+ let raw;
3043
+ try {
3044
+ raw = JSON.parse(read.stdout);
3045
+ } catch {
3046
+ return { ok: false, reason: "its check items file (OUTERLAYER_CHECK_ITEMS) is not JSON" };
3047
+ }
3048
+ const parsed = CheckItemsSchema.safeParse(raw);
3049
+ if (!parsed.success) {
3050
+ return { ok: false, reason: `its check items file does not match the check item shape: ${zodProblem(parsed.error.issues)}` };
3051
+ }
3052
+ return { ok: true, items: parsed.data.map((item) => item.reason === void 0 ? item : { ...item, reason: scrub(item.reason) }) };
3053
+ }
3054
+ function isJudgeCheck(check) {
3055
+ return "judge" in check;
3056
+ }
3057
+ function lastLine(text2) {
3058
+ return text2.split(/\r\n|\r|\n/).map((line) => line.trim()).filter((line) => line !== "").at(-1);
3059
+ }
3060
+ function readInside(root, rel) {
3061
+ const path = join(root, rel);
3062
+ try {
3063
+ if (!lstatSync(path).isFile()) return void 0;
3064
+ const inside = realpathSync(path);
3065
+ const base = realpathSync(root);
3066
+ return inside.startsWith(base + sep) ? readFileSync(inside, "utf8") : void 0;
3067
+ } catch {
3068
+ return void 0;
3069
+ }
3070
+ }
3071
+ var MAINTAINED_BRIEF = /^\.(?:agents|outerlayer)\/skills\/(outerlayer-[a-z]+)\/([A-Za-z0-9_./-]+)$/;
3072
+ function readBrief(root, brief) {
3073
+ const own = readInside(root, brief);
3074
+ if (own !== void 0) return own;
3075
+ const match = MAINTAINED_BRIEF.exec(brief);
3076
+ if (match === null || match[2].split("/").includes("..") || !MAINTAINED_SKILLS.includes(match[1])) return void 0;
3077
+ try {
3078
+ return readFileSync(join(resolvePackRoot(), "maintained", match[1], match[2]), "utf8");
3079
+ } catch {
3080
+ return void 0;
3081
+ }
3082
+ }
3083
+ function validatorFilesIn(root) {
3084
+ const dir = join(root, VALIDATORS_DIR);
3085
+ let names;
3086
+ try {
3087
+ if (!lstatSync(dir).isDirectory()) return [];
3088
+ names = readdirSync(dir);
3089
+ } catch {
3090
+ return [];
3091
+ }
3092
+ const files = [];
3093
+ for (const name of names.filter((entry) => /\.ya?ml$/.test(entry)).sort().slice(0, MAX_VALIDATOR_FILES)) {
3094
+ const path = `${VALIDATORS_DIR}/${name}`;
3095
+ const content = readInside(root, path);
3096
+ if (content !== void 0) files.push({ path, content });
3097
+ }
3098
+ return files;
3099
+ }
3100
+ var CRITERIA_CHECK = "acceptance-criteria";
3101
+ function asHostReads(file, base, log, tag) {
3102
+ const upgraded = upgradedStarterText(file.path, file.content, { base });
3103
+ if (upgraded === void 0) return file;
3104
+ log.line(`${tag} ${file.path} is an unedited copy an earlier release wrote, so it is read as this CLI writes it`);
3105
+ return { path: file.path, content: upgraded };
3106
+ }
3107
+ function checksIn(root, log, tag) {
3108
+ const base = existsSync(join(root, AGENTS_SKILLS_BASE)) ? AGENTS_SKILLS_BASE : OUTERLAYER_SKILLS_BASE;
3109
+ const policyText = readInside(root, POLICY_FILE);
3110
+ const policy = policyText === void 0 ? null : asHostReads({ path: POLICY_FILE, content: policyText }, base, log, tag);
3111
+ const loaded = parsePolicy(policy, validatorFilesIn(root).map((file) => asHostReads(file, base, log, tag)));
3112
+ for (const problem of loaded.problems) log.error(`${tag} ${problem.file}: ${problem.problem}`);
3113
+ const hostChecks = loaded.customs.filter((def) => def.runWhere === "host" && def.level !== "off" && def.declaresEmit !== null);
3114
+ const checks = [];
3115
+ for (const def of hostChecks) {
3116
+ if (def.judge !== void 0) checks.push({ name: def.declaresEmit, judge: { brief: def.judge.brief, text: readBrief(root, def.judge.brief), subjects: def.judge.subjects } });
3117
+ else if (def.command !== void 0) checks.push({ name: def.declaresEmit, command: def.command });
3118
+ }
3119
+ const unrecorded = loaded.customs.filter((def) => def.level !== "off" && def.declaresEmit === CRITERIA_CHECK && !checks.some((check) => check.name === CRITERIA_CHECK)).map((def) => def.id);
3120
+ return { checks, unrecorded };
3121
+ }
3122
+ async function readDefinitions(input, repository, scratch, token, tokens) {
3123
+ const { config, job, deps } = input;
3124
+ const tag = `#${job.item}`;
3125
+ const stage = deps.stage ?? stageGovernedContext;
3126
+ const controlPlaneUrl = (controlPlane) => deps.runtime.remoteFor?.(controlPlane) ?? `${deps.runtime.github.gitUrl}/${controlPlane}.git`;
3127
+ const governing = (job.container ?? job.microvm)?.governing;
3128
+ const kept = keptContextTree(input.jobDir, governing);
3129
+ if (kept !== void 0) return { ok: true, ...checksIn(kept, deps.log, tag) };
3130
+ const staged = await stage({
3131
+ config,
3132
+ repository,
3133
+ ...governing === void 0 ? {} : { source: { repository: governing.repository, ref: governing.ref, sha: governing.commit } },
3134
+ buildId: `checks-${randomBytes(4).toString("hex")}`,
3135
+ buildDir: scratch,
3136
+ fetchImpl: deps.fetchImpl,
3137
+ controlPlaneUrl,
3138
+ controlPlaneToken: tokens === void 0 ? void 0 : () => tokens.controlPlane(),
3139
+ log: deps.log,
3140
+ tag
3141
+ });
3142
+ if (staged.kind === "refused") return { ok: false, reason: staged.reason };
3143
+ if (staged.kind === "staged") {
3144
+ try {
3145
+ return { ok: true, ...checksIn(join(staged.dir, "tree"), deps.log, tag) };
3146
+ } finally {
3147
+ rmSync(staged.dir, { recursive: true, force: true });
3148
+ }
3354
3149
  }
3355
- return found;
3356
- }
3357
- function isContainerTracked(job) {
3358
- return job.container?.tracking === "container";
3359
- }
3360
- function isMicroVmTracked(job) {
3361
- return job.microvm?.tracking === "microvm";
3362
- }
3363
- function isTracked(job) {
3364
- return isContainerTracked(job) || isMicroVmTracked(job);
3365
- }
3366
- function trackingOf(job) {
3367
- return job.container?.tracking ?? job.microvm?.tracking;
3368
- }
3369
- function isReadable(job) {
3370
- return readsJobState(job, READ_JOB_STATE_FORMATS);
3371
- }
3372
- function listUnreadableJobs(jobsDirPath) {
3373
- return unfinishedJobs(jobsDirPath).flatMap(({ dirPath, job }) => isReadable(job) ? [] : [{ dirPath, job, format: jobStateFormatOf(job) }]);
3150
+ const repo = join(scratch, "repo");
3151
+ const remote = deps.runtime.remoteFor?.(repository) ?? `${deps.runtime.github.gitUrl}/${repository}.git`;
3152
+ const started = (job.container ?? job.microvm)?.startCommit;
3153
+ const commit = started !== void 0 && started !== "" ? started : job.environment?.recipe?.sourceCommit ?? await deps.runtime.readHead(remote, scratch);
3154
+ if (commit === void 0) return { ok: false, reason: "the repository's default branch could not be read" };
3155
+ const fetched = await (deps.fetchCommit ?? fetchCommit)({ remote, dir: repo, commit, token });
3156
+ if (!fetched.ok) return { ok: false, reason: fetched.reason };
3157
+ deps.log.line(`${tag} check definitions read at ${commit.slice(0, 12)}`);
3158
+ return { ok: true, ...checksIn(repo, deps.log, tag) };
3374
3159
  }
3375
- function listTrackedJobs(jobsDirPath) {
3376
- return unfinishedJobs(jobsDirPath).flatMap(({ dirPath, job }) => {
3377
- if (!isReadable(job)) return [];
3378
- const tracking = trackingOf(job);
3379
- if (tracking === void 0) return [];
3380
- if ((tracking === "container" || tracking === "microvm") && inTeardown(job)) return [];
3381
- return [{ dirPath, job, known: tracking === "container" || tracking === "microvm" }];
3160
+ function timeoutAfter(ms, sleep) {
3161
+ if (sleep !== void 0) return { done: sleep(ms).then(() => "timeout"), cancel: () => void 0 };
3162
+ let timer;
3163
+ const done = new Promise((resolve) => {
3164
+ timer = setTimeout(() => resolve("timeout"), ms);
3165
+ timer.unref();
3382
3166
  });
3167
+ return { done, cancel: () => clearTimeout(timer) };
3383
3168
  }
3384
- function listAdoptableJobs(jobsDirPath) {
3385
- return unfinishedJobs(jobsDirPath).filter(
3386
- ({ job }) => isReadable(job) && job.microvm === void 0 && job.pid !== void 0 && job.start !== void 0 && isSameProcess(job.pid, job.start)
3387
- );
3169
+ function noCleanEnvironment(job) {
3170
+ const ran = job.build?.workDir !== void 0 ? "builtin:process" : "a provision hook of the host's own";
3171
+ return `the host has no clean environment for checks: this build ran under ${ran}, and checks run only in a new container or microVM from a build's image`;
3388
3172
  }
3389
- async function recoverLostJobs(jobsDirPath, config, deps) {
3390
- const recovered = [];
3391
- for (const { dirPath, job } of unfinishedJobs(jobsDirPath)) {
3392
- if (!isReadable(job)) continue;
3393
- const tracking = trackingOf(job);
3394
- if (tracking !== void 0 && tracking !== "container" && tracking !== "microvm") {
3395
- recovered.push(await finishJob(job, dirPath, "interrupted", config, deps, { reason: UNKNOWN_TRACKING_REASON }));
3396
- continue;
3397
- }
3398
- if ((tracking === "container" || tracking === "microvm") && !inTeardown(job)) continue;
3399
- const alive = job.pid !== void 0 && job.start !== void 0 && isSameProcess(job.pid, job.start);
3400
- if (alive) continue;
3401
- const recorded = outcomeFromExitCode(dirPath);
3402
- if (recorded !== void 0) job.exitCode = recorded.code;
3403
- recovered.push(await finishJob(job, dirPath, recorded?.outcome ?? "interrupted", config, deps));
3404
- }
3405
- return recovered;
3173
+ function makeRecorder(input) {
3174
+ const { config, job, deps } = input;
3175
+ const tag = `#${job.item}`;
3176
+ const now = deps.now ?? (() => /* @__PURE__ */ new Date());
3177
+ let signer;
3178
+ return async (name, result, sentence, opts = {}) => {
3179
+ signer ??= await signerFor(config.configPath);
3180
+ const keyTail = opts.keyTail ?? "";
3181
+ const body = sentence === void 0 ? void 0 : cutReasonLine(`${scrubReason(sentence, config) ?? "check failed"}${keyTail}`);
3182
+ const clientEmitId = `host-${createHash("sha256").update(`${job.claimId}:${job.startedAt}:${name}${opts.idTag === void 0 ? "" : `:${opts.idTag}`}`).digest("hex").slice(0, 40)}`;
3183
+ const sent = await retrySend(
3184
+ () => postJson(
3185
+ config.url,
3186
+ "/v1/emitted-results",
3187
+ config.apiKey,
3188
+ config.appId,
3189
+ {
3190
+ schemaVersion: 1,
3191
+ // A field left undefined is not sent: the request is JSON.
3192
+ emit: { clientEmitId, name, result, body, link: opts.link, items: opts.items, emittedAt: now().toISOString(), item: { number: Number(job.item) } }
3193
+ },
3194
+ deps.fetchImpl,
3195
+ void 0,
3196
+ signer
3197
+ ),
3198
+ { now: deps.now }
3199
+ );
3200
+ if (sent.ok) deps.log.line(`${tag} check ${name} ${result}${body === void 0 ? "" : `: ${body}`}`);
3201
+ else deps.log.error(`${tag} check ${name} ${result} could not be recorded: ${sent.reason}`);
3202
+ };
3406
3203
  }
3407
-
3408
- // src/runner/sender.ts
3409
- var STOP_WAIT_MS = 3e4;
3410
- var POLL_MS = 200;
3411
- var ASK_AGAIN_MS = 1e3;
3412
- var EXIT_REPORT_WAIT_MS = 2e3;
3413
- var exitsLogged = /* @__PURE__ */ new Map();
3414
- var BUILD_HOME = "/build/home";
3415
- async function startSender(input) {
3204
+ async function runHostChecks(input) {
3416
3205
  const { config, job, jobDir, deps } = input;
3206
+ const { runtime, log } = deps;
3417
3207
  const tag = `#${job.item}`;
3418
- if (job.itemKey === void 0) return void 0;
3419
- const spawnFn = deps.spawnFn ?? spawn;
3208
+ const now = deps.now ?? (() => /* @__PURE__ */ new Date());
3209
+ const repository = job.repository ?? job.container?.repository;
3210
+ if (repository === void 0) return;
3211
+ const jobFilePath = join(jobDir, "job.json");
3212
+ const tokens = runtime.credentials.get(jobDir);
3213
+ let readToken;
3420
3214
  try {
3421
- const launch = launchFor(config, job, jobDir, deps);
3422
- const fd = openSync(join(jobDir, "sender.log"), "a", RUNNER_FILE_MODE);
3423
- let pid;
3424
- let exitLogged;
3425
- try {
3426
- const child = spawnFn(launch.command, launch.args, { env: launch.env, cwd: launch.cwd, detached: true, stdio: ["ignore", fd, fd] });
3427
- child.on("error", (err) => deps.log.error(`${tag} session sender failed: ${describeCallFailure(err)}`));
3428
- exitLogged = new Promise((resolve) => {
3429
- child.on("exit", (code, signal) => {
3430
- deps.log.line(`${tag} session sender exited \xB7 ${signal ?? `code ${code}`} \xB7 see sender.log`);
3431
- resolve();
3432
- });
3215
+ readToken = await tokens?.token("read");
3216
+ } catch (err) {
3217
+ log.error(`${tag} no read token for the checks: ${describeCallFailure(err)}`);
3218
+ }
3219
+ const scratch = join(jobDir, "checks", "definitions");
3220
+ rmSync(scratch, { recursive: true, force: true });
3221
+ mkdirSync(scratch, { recursive: true });
3222
+ let definitions;
3223
+ try {
3224
+ definitions = await readDefinitions(input, repository, scratch, readToken, tokens);
3225
+ } catch (err) {
3226
+ definitions = { ok: false, reason: describeCallFailure(err) };
3227
+ } finally {
3228
+ removeStagedContext(scratch);
3229
+ }
3230
+ if (!definitions.ok) {
3231
+ log.line(`${tag} checks not run: the definitions could not be read: ${definitions.reason}`);
3232
+ rmSync(scratch, { recursive: true, force: true });
3233
+ return;
3234
+ }
3235
+ const checks = definitions.checks.filter((check) => !isJudgeCheck(check));
3236
+ if (checks.length === 0) {
3237
+ rmSync(scratch, { recursive: true, force: true });
3238
+ return;
3239
+ }
3240
+ log.line(`${tag} checks started (${checks.map((check) => check.name).join(", ")})`);
3241
+ const startedMs = now().getTime();
3242
+ const recorder = makeRecorder({ config, job, deps });
3243
+ const record = (check, result, sentence, keyTail = "", items) => recorder(check.name, result, sentence, { keyTail, items });
3244
+ const failAll = async (reason, keyTail = "") => {
3245
+ log.error(`${tag} checks failed: ${reason}${keyTail}`);
3246
+ for (const check of checks) await record(check, "fail", reason, keyTail);
3247
+ };
3248
+ try {
3249
+ if (job.container === void 0 && job.microvm === void 0) return await failAll(noCleanEnvironment(job));
3250
+ if (tokens === void 0) return await failAll("the build's repository tokens are no longer held, so the branch cannot be fetched");
3251
+ if (job.branch === void 0) return await failAll("the build pushed no branch: the claim names none");
3252
+ const claimed = repositoriesOf({ repositories: job.repositories, repository, branch: job.branch });
3253
+ if (claimed.length === 0) return await failAll("the claim names no repository to check out");
3254
+ const remoteOf = (name) => runtime.remoteFor?.(name) ?? `${runtime.github.gitUrl}/${name}.git`;
3255
+ const remote = remoteOf(repository);
3256
+ const pushed = /* @__PURE__ */ new Map();
3257
+ const unpushed = [];
3258
+ let primaryHead;
3259
+ for (const entry of claimed) {
3260
+ let entryToken = readToken;
3261
+ if (!entry.primary) {
3262
+ try {
3263
+ entryToken = await tokens.token("read", entry.repository);
3264
+ } catch (err) {
3265
+ return await failAll(`no read token for ${entry.repository}: ${describeCallFailure(err)}`);
3266
+ }
3267
+ }
3268
+ const read = await (deps.readBranch ?? readBranchHead)({ remote: remoteOf(entry.repository), dir: scratch, branch: entry.branch, token: entryToken });
3269
+ if (read.kind === "unreadable") return await failAll(`the branch ${entry.branch} could not be read${entry.primary ? "" : ` on ${entry.repository}`}: ${read.reason}`);
3270
+ if (read.kind === "absent") {
3271
+ unpushed.push(entry);
3272
+ continue;
3273
+ }
3274
+ pushed.set(entry.repository, entry.branch);
3275
+ if (entry.primary) primaryHead = read.sha;
3276
+ }
3277
+ if (pushed.size === 0) {
3278
+ return await failAll(`the build pushed no branch: ${unpushed.map((entry) => `${entry.branch} does not exist on ${entry.repository}`).join("; ")}`);
3279
+ }
3280
+ const repositoriesToCheckOut = claimed;
3281
+ const limitMinutes = config.checks?.timeLimitMinutes ?? DEFAULT_CHECK_TIME_LIMIT_MINUTES;
3282
+ const minutes = `${limitMinutes} ${limitMinutes === 1 ? "minute" : "minutes"}`;
3283
+ const environmentLog = join(jobDir, "checks", "environment.log");
3284
+ let transcripts = { ok: false, reason: "the runner holds no way to read the build's sessions" };
3285
+ if (deps.collectSessions !== void 0) {
3286
+ try {
3287
+ transcripts = await deps.collectSessions();
3288
+ } catch (err) {
3289
+ transcripts = { ok: false, reason: describeCallFailure(err) };
3290
+ }
3291
+ }
3292
+ if (job.microvm !== void 0) {
3293
+ if (deps.endBuildMicroVm === void 0) return await failAll("the runner has no way to stop the build's microVM, so no check environment was made");
3294
+ try {
3295
+ await deps.endBuildMicroVm();
3296
+ } catch (err) {
3297
+ return await failAll(`the build's microVM could not be stopped, so no check environment was made: ${describeCallFailure(err)}`);
3298
+ }
3299
+ }
3300
+ let abandoned = false;
3301
+ const setup = async () => {
3302
+ const started = job.microvm !== void 0 ? await startCheckMicroVm({
3303
+ config,
3304
+ runtime,
3305
+ job,
3306
+ jobDir,
3307
+ tokens,
3308
+ log,
3309
+ aborted: () => abandoned,
3310
+ save: (state) => {
3311
+ if (abandoned) return;
3312
+ job.checkMicrovm = state;
3313
+ writeJobFile(jobFilePath, job);
3314
+ }
3315
+ }) : await startCheckContainer({
3316
+ config,
3317
+ runtime,
3318
+ job,
3319
+ jobDir,
3320
+ tokens,
3321
+ log,
3322
+ save: (state) => {
3323
+ job.checkContainer = state;
3324
+ writeJobFile(jobFilePath, job);
3325
+ }
3433
3326
  });
3434
- child.unref?.();
3435
- pid = child.pid;
3436
- } finally {
3437
- closeSync(fd);
3438
- }
3439
- if (pid === void 0) {
3440
- deps.log.line(`${tag} session sender did not start: no pid`);
3441
- return void 0;
3442
- }
3443
- exitsLogged.set(pid, exitLogged);
3444
- deps.log.line(`${tag} session sender started \xB7 pid ${pid}`);
3445
- return { pid, start: startTimeOf(pid) ?? void 0 };
3446
- } catch (err) {
3447
- deps.log.error(`${tag} session sender did not start: ${describeCallFailure(err)} \xB7 continuing`);
3448
- return void 0;
3449
- }
3450
- }
3451
- function launchFor(config, job, jobDir, deps) {
3452
- if (job.microvm !== void 0) {
3453
- const runtime = deps.runtime ?? createContainerRuntime();
3454
- const cli = deps.cliPath ?? join(runtime.cliBuild().distDir, "index.js");
3455
- return { ...microVmSenderCommand({ job, runtime, config, cli }), cwd: jobDir };
3456
- }
3457
- if (job.container !== void 0) {
3458
- return { ...containerSenderCommand({ job, runtime: deps.runtime ?? createContainerRuntime(), config }), cwd: jobDir };
3459
- }
3460
- const env = jobEnv(config, job.item, job.itemKey, jobDir, process.env, job.branch, job.commitIdentity?.author);
3461
- const build = job.build;
3462
- const hostEnv = build === void 0 ? env : hostCommandEnvironment(env, build, config, job.commitIdentity?.hooksPath);
3463
- if (build?.exec !== void 0) {
3464
- return { command: build.exec, args: execArgv(job.workdir, ["outerlayer", "session-sender"]), env: hostEnv, cwd: jobDir };
3465
- }
3466
- return { command: process.execPath, args: [deps.cliPath ?? process.argv[1] ?? "outerlayer", "session-sender"], env: hostEnv, cwd: job.workdir ?? jobDir };
3467
- }
3468
- async function restartSender(input) {
3469
- const { job, deps } = input;
3470
- const state = job.container;
3471
- if (state !== void 0 && job.itemKey !== void 0) {
3472
- const runtime = deps.runtime ?? createContainerRuntime();
3473
- const pidfile = senderPidfile(BUILD_HOME, Number(job.item));
3474
- const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
3475
- const running = async () => {
3476
- const probe = await runtime.host.exec({ container: state.container, user: state.agentUser ?? ADDED_USER, env: { literals: {}, passed: {} }, argv: ["/bin/sh", "-c", senderAliveScript(pidfile)] });
3477
- return probe.status === 0 && probe.stdout.trim() === "alive";
3327
+ if (!started.ok) return `the check environment could not be made: ${started.reason}`;
3328
+ const made = "container" in started ? started.container : started.environment;
3329
+ const steps = [
3330
+ {
3331
+ argv: ["git", "config", "--global", `url.http://127.0.0.1:${RELAY_PORT}/github.com/.insteadOf`, "https://github.com/"],
3332
+ workdir: "/build/work",
3333
+ failed: (status) => `configuring git failed (exit ${status})`
3334
+ },
3335
+ ...repositoriesToCheckOut.flatMap((entry) => {
3336
+ const branch = pushed.get(entry.repository);
3337
+ const path = entry.primary ? CHECKOUT_DIR : repositoryPath(entry.repository);
3338
+ const clone = {
3339
+ argv: ["git", "clone", "--filter=blob:none", "--single-branch", ...branch === void 0 ? [] : ["--branch", branch], `https://github.com/${entry.repository}.git`, path],
3340
+ workdir: "/build/work",
3341
+ failed: (status) => `the clone of ${branch ?? `the default branch of ${entry.repository}`} failed (exit ${status})`
3342
+ };
3343
+ return entry.primary && repositoriesToCheckOut.length > 1 ? [
3344
+ clone,
3345
+ {
3346
+ argv: ["sh", "-c", 'mkdir -p "$(dirname "$1")" && ln -s "$2" "$1"', "link", repositoryPath(entry.repository), CHECKOUT_DIR],
3347
+ workdir: "/build/work",
3348
+ failed: (status) => `linking ${entry.repository} at ${repositoryPath(entry.repository)} failed (exit ${status})`
3349
+ }
3350
+ ] : [clone];
3351
+ })
3352
+ ];
3353
+ for (const step of steps) {
3354
+ if (abandoned) return "the check environment setup ran out of time";
3355
+ const ran = await made.exec(step.argv, step.workdir, { logPath: environmentLog });
3356
+ if (ran.status !== 0) return `the check environment could not be prepared: ${step.failed(ran.status)}`;
3357
+ }
3358
+ const lifecycleDir = lifecycleWorkdir(repositoriesToCheckOut, (job.container ?? job.microvm)?.governing?.repository);
3359
+ for (const step of lifecycleSteps((job.container ?? job.microvm)?.lifecycle ?? {})) {
3360
+ log.line(`${tag} checks lifecycle ${step.name} (${step.source})`);
3361
+ const ran = await made.exec(step.argv, lifecycleDir, { logPath: environmentLog });
3362
+ if (ran.status !== 0) return `the check environment could not be prepared: ${lifecycleFailureReason(step, ran.status)}`;
3363
+ }
3364
+ return made;
3478
3365
  };
3366
+ const setupTimer = timeoutAfter(limitMinutes * 6e4, deps.sleep);
3367
+ let settled;
3479
3368
  try {
3480
- await signalInContainer(runtime, state, pidfile, "TERM");
3481
- const deadline = (deps.waitMs ?? STOP_WAIT_MS) / POLL_MS;
3482
- for (let waited = 0; await running() && waited < deadline; waited += 1) await sleep(POLL_MS);
3483
- if (await running()) {
3484
- deps.log.error(`#${job.item} the session sender an earlier runner left did not stop in time \xB7 killing it`);
3485
- await signalInContainer(runtime, state, pidfile, "KILL");
3369
+ const outcome = await Promise.race([
3370
+ setup().catch((err) => `the check environment could not be made: ${describeCallFailure(err)}`),
3371
+ setupTimer.done
3372
+ ]);
3373
+ if (outcome === "timeout") {
3374
+ abandoned = true;
3375
+ return await failAll(`the check environment setup ran out of time after ${minutes}`, LIMIT_KEY_TAIL2);
3486
3376
  }
3487
- } catch (err) {
3488
- deps.log.error(`#${job.item} stopping the session sender an earlier runner left failed: ${describeCallFailure(err)} \xB7 continuing`);
3377
+ settled = outcome;
3378
+ } finally {
3379
+ setupTimer.cancel();
3489
3380
  }
3490
- }
3491
- return startSender(input);
3492
- }
3493
- function isLive(sender) {
3494
- if (sender.start !== void 0) return isSameProcess(sender.pid, sender.start);
3495
- try {
3496
- process.kill(sender.pid, 0);
3497
- return true;
3498
- } catch {
3499
- return false;
3500
- }
3501
- }
3502
- function signalHost(sender, signal) {
3503
- if (!isLive(sender)) return;
3504
- try {
3505
- process.kill(-sender.pid, signal);
3506
- } catch {
3381
+ if (typeof settled === "string") return await failAll(settled);
3382
+ const ready = settled;
3507
3383
  try {
3508
- process.kill(sender.pid, signal);
3509
- } catch {
3384
+ await writeEvalInput({
3385
+ job,
3386
+ dir: ready.evalInput.dir,
3387
+ scratch,
3388
+ remote,
3389
+ readToken,
3390
+ branchHead: primaryHead ?? (job.container ?? job.microvm)?.startCommit ?? "",
3391
+ transcripts,
3392
+ scrub: transcriptScrub(config, [config.apiKey, job.itemKey, readToken]),
3393
+ readWorkItem: () => readWorkItem(config, job.item, deps.fetchImpl),
3394
+ fetchDiff: deps.fetchDiff ?? ((input2) => diffBetween(input2)),
3395
+ log
3396
+ });
3397
+ } catch (err) {
3398
+ return await failAll(`the checks' input could not be written: ${describeCallFailure(err)}`);
3510
3399
  }
3511
- }
3512
- }
3513
- async function signalInGuest(runtime, state, pidfile, signal) {
3514
- const { status } = await runtime.vm.exec(state.vsockPath, {
3515
- argv: ["/bin/sh", "-c", senderSignalScript(pidfile, signal)],
3516
- user: state.agentUser ?? ADDED_USER,
3517
- cwd: "/",
3518
- env: { PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" }
3519
- });
3520
- return status === 0;
3521
- }
3522
- async function exitReported(pid) {
3523
- const logged = exitsLogged.get(pid);
3524
- let timer;
3525
- const bound = new Promise((resolve) => {
3526
- timer = setTimeout(resolve, EXIT_REPORT_WAIT_MS);
3527
- timer.unref?.();
3528
- });
3529
- await Promise.race([logged, bound]);
3530
- clearTimeout(timer);
3531
- exitsLogged.delete(pid);
3532
- }
3533
- async function stopSender(input) {
3534
- const { job, deps } = input;
3535
- const sender = job.sender;
3536
- if (sender === void 0) return;
3537
- const tag = `#${job.item}`;
3538
- const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
3539
- const inContainer = job.container === void 0 ? void 0 : { state: job.container, runtime: deps.runtime ?? createContainerRuntime(), pidfile: senderPidfile(BUILD_HOME, Number(job.item)) };
3540
- const inGuest = job.microvm === void 0 ? void 0 : { state: job.microvm, runtime: deps.runtime ?? createContainerRuntime(), pidfile: senderPidfile(BUILD_HOME, Number(job.item)) };
3541
- const signalInside = async (signal) => {
3542
- if (inContainer !== void 0) return signalInContainer(inContainer.runtime, inContainer.state, inContainer.pidfile, signal).catch(() => void 0);
3543
- if (inGuest !== void 0) return signalInGuest(inGuest.runtime, inGuest.state, inGuest.pidfile, signal).catch(() => void 0);
3544
- return void 0;
3545
- };
3546
- try {
3547
- let reached = true;
3548
- if (inContainer !== void 0 || inGuest !== void 0) reached = await signalInside("TERM");
3549
- else signalHost(sender, "SIGTERM");
3550
- const deadline = (deps.waitMs ?? STOP_WAIT_MS) / POLL_MS;
3551
- for (let waited = 0; isLive(sender) && waited < deadline; waited += 1) {
3552
- await sleep(POLL_MS);
3553
- if (!reached && (waited + 1) % (ASK_AGAIN_MS / POLL_MS) === 0) reached = await signalInside("TERM");
3400
+ try {
3401
+ await ready.evalInput.deliver();
3402
+ } catch (err) {
3403
+ return await failAll(err instanceof DeliveryFailure ? err.message : `the checks' input could not be written: ${describeCallFailure(err)}`);
3554
3404
  }
3555
- if (isLive(sender)) {
3556
- deps.log.error(`${tag} session sender did not stop in time \xB7 killing it`);
3557
- await signalInside("KILL");
3558
- signalHost(sender, "SIGKILL");
3405
+ let usable = true;
3406
+ for (const check of checks) {
3407
+ if (!usable) {
3408
+ await record(check, "fail", "the check environment could not be restarted after an earlier check ran out of time");
3409
+ continue;
3410
+ }
3411
+ const checkStartedMs = now().getTime();
3412
+ const logPath = join(jobDir, "checks", `${check.name}.log`);
3413
+ await ready.exec(["rm", "-f", CHECK_ITEMS_FILE], CHECKOUT_DIR, { logPath });
3414
+ const running = ready.exec(["/bin/sh", "-c", check.command], CHECKOUT_DIR, { logPath });
3415
+ running.catch(() => void 0);
3416
+ const timer = timeoutAfter(limitMinutes * 6e4, deps.sleep);
3417
+ let outcome;
3418
+ try {
3419
+ outcome = await Promise.race([running, timer.done]);
3420
+ } catch (err) {
3421
+ timer.cancel();
3422
+ await record(check, "fail", `the command could not be run: ${describeCallFailure(err)}`);
3423
+ continue;
3424
+ }
3425
+ timer.cancel();
3426
+ if (outcome === "timeout") {
3427
+ await record(check, "fail", `ran out of time after ${minutes}`, LIMIT_KEY_TAIL2);
3428
+ const restarting = ready.restart().catch(() => false);
3429
+ const restartTimer = timeoutAfter(limitMinutes * 6e4, deps.sleep);
3430
+ usable = await Promise.race([restarting, restartTimer.done]) === true;
3431
+ restartTimer.cancel();
3432
+ continue;
3433
+ }
3434
+ const read = await ready.exec(["head", "-c", String(MAX_CHECK_ITEMS_BYTES + 1), CHECK_ITEMS_FILE], CHECKOUT_DIR);
3435
+ const items = commandItems(read, (text2) => scrubReason(text2, config) ?? text2);
3436
+ if (!items.ok) {
3437
+ await record(check, "fail", items.reason);
3438
+ continue;
3439
+ }
3440
+ const failing = items.items === void 0 ? void 0 : itemsSentence(items.items, { one: "check item", many: "check items" });
3441
+ if (outcome.status === 0 && failing === void 0) {
3442
+ log.line(`${tag} check ${check.name} ran (${formatDuration(now().getTime() - checkStartedMs)})`);
3443
+ await record(check, "pass", void 0, "", items.items);
3444
+ continue;
3445
+ }
3446
+ if (outcome.status === 0) {
3447
+ await record(check, "fail", failing, "", items.items);
3448
+ continue;
3449
+ }
3450
+ const last = lastLine(outcome.stderr) ?? lastLine(outcome.stdout);
3451
+ const scrubbed = last === void 0 ? void 0 : scrubReason(last, config);
3452
+ await record(check, "fail", `exited ${outcome.status}${scrubbed === void 0 ? " with no output" : `: ${cutReasonLine(scrubbed, 140)}`}`, "", items.items);
3559
3453
  }
3560
- await exitReported(sender.pid);
3561
3454
  } catch (err) {
3562
- deps.log.error(`${tag} stopping the session sender failed: ${describeCallFailure(err)} \xB7 continuing`);
3455
+ log.error(`${tag} checks stopped: ${describeCallFailure(err)}`);
3456
+ } finally {
3457
+ if (job.checkContainer !== void 0) {
3458
+ await cleanupContainer(runtime, job.checkContainer).catch((err) => log.error(`${tag} removing the check environment failed: ${describeCallFailure(err)}`));
3459
+ delete job.checkContainer;
3460
+ writeJobFile(jobFilePath, job);
3461
+ }
3462
+ if (job.checkMicrovm !== void 0) {
3463
+ await cleanupMicroVm(runtime, job.checkMicrovm).catch((err) => log.error(`${tag} removing the check environment failed: ${describeCallFailure(err)}`));
3464
+ delete job.checkMicrovm;
3465
+ writeJobFile(jobFilePath, job);
3466
+ }
3467
+ rmSync(scratch, { recursive: true, force: true });
3468
+ rmSync(join(jobDir, "checks", EVAL_INPUT_NAME), { recursive: true, force: true });
3469
+ if (existsSync(scratch)) log.error(`${tag} the checks' scratch directory could not be removed`);
3470
+ log.line(`${tag} checks done (${formatDuration(now().getTime() - startedMs)})`);
3563
3471
  }
3564
3472
  }
3565
3473
 
3566
- export { ALREADY_RELEASED, AttemptLogBook, EXIT_CODE_FILE, GUEST_USAGE_INTERVAL_MS, MICROVM_COMMAND_DIR, RENEW_INTERVAL_SECONDS, appendCommandLog, commandLogOffset, deliverCommitHooks, describeHookEnd, finishJob, foldSample, hookLimitsFor, inTeardown, isContainerTracked, isGroupAlive, isIdle, isMicroVmTracked, isReadable, isTimedOut, isTracked, isUnfinished, jobEnv, judgeProbe, listAdoptableJobs, listTrackedJobs, listUnreadableJobs, microVmCommandLine, microVmOomKills, needsRenew, outcomeFromExitCode, probeCommand, probeMicroVmCommand, provisionMicroVm, provisionProcess, readCpuUsec, readProvisionOut, recoverLostJobs, releaseLease, renewLease, restartSender, retryFailedReleases, runCommand, runHook, sampleGuest, scrubReason, serveLease, setStage, startMicroVmCommand, startSender, stopOrphanedMicroVms, stripTerminalControls, unfinishedJobs, vmAlive, workdirProblem, writeGuestGhConfig };
3474
+ export { ALREADY_RELEASED, AttemptLogBook, EXIT_CODE_FILE, GUEST_USAGE_INTERVAL_MS, MAX_CHECK_ITEMS_BYTES, MICROVM_COMMAND_DIR, RENEW_INTERVAL_SECONDS, appendCommandLog, checksIn, commandItems, commandLogOffset, deliverCommitHooks, describeHookEnd, finishJob, foldSample, hookLimitsFor, inTeardown, isContainerTracked, isGroupAlive, isIdle, isJudgeCheck, isMicroVmTracked, isReadable, isTimedOut, isTracked, isUnfinished, jobEnv, judgeProbe, listAdoptableJobs, listTrackedJobs, listUnreadableJobs, makeRecorder, microVmCommandLine, microVmOomKills, needsRenew, outcomeFromExitCode, probeCommand, probeMicroVmCommand, provisionMicroVm, provisionProcess, readCpuUsec, readDefinitions, readProvisionOut, recoverLostJobs, releaseLease, renewLease, restartSender, retryFailedReleases, runCommand, runHook, runHostChecks, sampleGuest, scrubReason, serveLease, setStage, startMicroVmCommand, startSender, stopOrphanedMicroVms, stripTerminalControls, timeoutAfter, unfinishedJobs, vmAlive, workdirProblem, writeGuestGhConfig };