@outerlayer/cli 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +72 -0
- package/README.md +36 -2
- package/dist/agent-setup-TPR4BBFV.js +27 -0
- package/dist/{artifact-link-cmd-2F47LT7F.js → artifact-link-cmd-W7XF5BFB.js} +4 -3
- package/dist/{build-BG2YENGK.js → build-TQ6X5PYN.js} +5 -4
- package/dist/build-info.json +1 -1
- package/dist/{builder-2D3WSOOY.js → builder-C4QNTYMS.js} +5 -6
- package/dist/{check-5XE7XMC4.js → check-UWQAL2MN.js} +56 -52
- package/dist/{chunk-R7PF5OOU.js → chunk-3CWUOFV6.js} +21 -14
- package/dist/{chunk-4NPZVMJU.js → chunk-3UZTIRLF.js} +60 -4
- package/dist/chunk-452TM7J4.js +502 -0
- package/dist/{chunk-NNRTPG7X.js → chunk-5GHGAF6P.js} +4 -4
- package/dist/{chunk-WVUIRTWA.js → chunk-5MYFOW5V.js} +196 -37
- package/dist/{chunk-HGECYFEI.js → chunk-5TN7GLYX.js} +138 -109
- package/dist/{chunk-VQYBXPI7.js → chunk-6D7GT22T.js} +5 -5
- package/dist/{chunk-G5NPRL2K.js → chunk-6U3MV7TZ.js} +687 -76
- package/dist/{chunk-42ZI52JW.js → chunk-6X4SQ5MY.js} +4 -3
- package/dist/{chunk-MAK5LYRF.js → chunk-7KSSBQTS.js} +278 -535
- package/dist/{chunk-NWQTTSBB.js → chunk-7SJPQWBM.js} +41 -16
- package/dist/{chunk-J5M7BSJQ.js → chunk-7VUK5RFS.js} +23 -4
- package/dist/{chunk-OZGHL5Y5.js → chunk-7YP7T6GV.js} +2 -2
- package/dist/{chunk-TI7B7UEO.js → chunk-AMFWYESY.js} +1 -1
- package/dist/{chunk-YRYMNBGQ.js → chunk-ANPNT6PM.js} +2 -1
- package/dist/{chunk-4KSQKQXE.js → chunk-BM63HB2M.js} +20 -16
- package/dist/{chunk-4Q7OMKAT.js → chunk-BQXFBI7T.js} +81 -5
- package/dist/{chunk-ADJRGVZC.js → chunk-BWSUPRDK.js} +1 -1
- package/dist/{chunk-3NSNIIBE.js → chunk-CG36IONK.js} +18 -17
- package/dist/chunk-CJJDJ7AT.js +56 -0
- package/dist/chunk-CRB3POP4.js +34 -0
- package/dist/{chunk-LWSJQJC6.js → chunk-DADWEUH3.js} +7 -7
- package/dist/{chunk-KG5ECZGJ.js → chunk-DJ3ZUSC3.js} +11 -11
- package/dist/{chunk-F47JBIAW.js → chunk-DSF2FWAZ.js} +2 -1
- package/dist/{chunk-N5FOE5PS.js → chunk-ECODQ7II.js} +4 -33
- package/dist/{chunk-HHJDN6AU.js → chunk-ESVW4MJY.js} +1 -1
- package/dist/{chunk-YO4PSLVB.js → chunk-FVMYKL6L.js} +1 -1
- package/dist/chunk-G2YNJIGF.js +44 -0
- package/dist/{chunk-WUYTWNMA.js → chunk-G3IJ3N6F.js} +5 -3
- package/dist/{chunk-QKFB4K7X.js → chunk-GGOMT6KF.js} +1 -1
- package/dist/{chunk-N6LURUEF.js → chunk-GOSFARMS.js} +1 -1
- package/dist/{chunk-S3NYLI2P.js → chunk-HPJNLETG.js} +93 -38
- package/dist/{chunk-IP7BIBXJ.js → chunk-IF2OSI5T.js} +35 -1
- package/dist/{chunk-C5Y5ZJJZ.js → chunk-IXYZUUYQ.js} +5 -4
- package/dist/chunk-JBJTC3UU.js +35 -0
- package/dist/chunk-KYS3TQAG.js +296 -0
- package/dist/chunk-LLM3I5YU.js +89 -0
- package/dist/{chunk-YU3VKZ5F.js → chunk-LRG3ZMZD.js} +6 -7
- package/dist/{chunk-V7NIVOFD.js → chunk-M2ZWWTWZ.js} +1 -1
- package/dist/{chunk-ERI5GBNI.js → chunk-MLAAT4S6.js} +3 -3
- package/dist/{chunk-O765JR4W.js → chunk-MTP2QEVO.js} +1 -1
- package/dist/{chunk-ZM2IMMYN.js → chunk-N2PXGQRL.js} +9 -1
- package/dist/{chunk-BWQ66M6N.js → chunk-NFIR3GEE.js} +98 -24
- package/dist/{chunk-POJMEEJJ.js → chunk-NNLA7LLQ.js} +1 -1
- package/dist/{chunk-PLMTLGYH.js → chunk-NTRCK4NH.js} +5 -4
- package/dist/{chunk-22RMZBGW.js → chunk-P53VEAOF.js} +1 -1
- package/dist/{chunk-RLYZLUQ7.js → chunk-QEFKA2QD.js} +2 -2
- package/dist/{chunk-JXYVA45C.js → chunk-QNLX477Q.js} +1200 -322
- package/dist/{chunk-UGP76IZ5.js → chunk-S5PCA3SN.js} +1 -1
- package/dist/{chunk-RMWLBUCJ.js → chunk-SADUQSO4.js} +1 -1
- package/dist/{chunk-GQD22KKN.js → chunk-T2Q7ISPE.js} +2 -1
- package/dist/chunk-TUSYOL2O.js +113 -0
- package/dist/{chunk-NBGDBTO2.js → chunk-TWF3RICI.js} +9 -8
- package/dist/{chunk-ONKJUM6R.js → chunk-UVG35MWF.js} +1 -1
- package/dist/{chunk-KMDLFI4P.js → chunk-VMJPFQNE.js} +1 -1
- package/dist/chunk-VRRGENS4.js +144 -0
- package/dist/{chunk-PTW6W5NR.js → chunk-VYAL7PUE.js} +1 -1
- package/dist/{chunk-OHVJA253.js → chunk-WFSUUUAN.js} +47 -30
- package/dist/{chunk-RSNSZ2LJ.js → chunk-X5H2WYMB.js} +1 -1
- package/dist/{chunk-HYZRY4KZ.js → chunk-XNGMQ27R.js} +16 -6
- package/dist/chunk-XSZD4AZ6.js +490 -0
- package/dist/chunk-YQR6E6OV.js +109 -0
- package/dist/{chunk-XCK2YAY5.js → chunk-YVZJ6GKF.js} +3 -2
- package/dist/{chunk-2MNPHL23.js → chunk-ZHAUSEEN.js} +38 -15
- package/dist/claude-launcher-FPPHZPMR.js +115 -0
- package/dist/{cli-A76AXELQ.js → cli-HROW5T7X.js} +567 -285
- package/dist/{command-R2QCVX55.js → command-OEAV7BVX.js} +42 -40
- package/dist/{config-N3C6DFQA.js → config-CLSGABD5.js} +2 -3
- package/dist/{connect-cmd-JBKAQNSF.js → connect-cmd-UNYKGDAL.js} +10 -9
- package/dist/context-cleanup-HSVYDGMI.js +9 -0
- package/dist/control-plane-launch-NYVDJESK.js +216 -0
- package/dist/{dist-LKDWQWZ7.js → dist-WO7NOJNG.js} +2 -2
- package/dist/{docker-4J77Q2BF.js → docker-BFU3AQGY.js} +7 -8
- package/dist/{doctor-4TAIARIG.js → doctor-23BBJWBX.js} +48 -42
- package/dist/{emit-artifact-cmd-MPZVR56N.js → emit-artifact-cmd-O6SCM7TV.js} +27 -24
- package/dist/emit-cmd-7SP3I6UZ.js +14 -0
- package/dist/{emit-criteria-cmd-EFGYI23V.js → emit-criteria-cmd-LQNERZRV.js} +17 -16
- package/dist/{emit-finding-cmd-5GF3V4LR.js → emit-finding-cmd-AJPKHIOV.js} +18 -17
- package/dist/{emit-result-cmd-I4OADKMD.js → emit-result-cmd-RUICOKWQ.js} +22 -19
- package/dist/emit-step-cmd-347XNGPQ.js +26 -0
- package/dist/{exec-client-CQV746UF.js → exec-client-7UMESIE2.js} +3 -4
- package/dist/governing-source-V5EEGPTA.js +34 -0
- package/dist/guest-init-KPBQOFE5.js +9 -0
- package/dist/{hook-fast-R4FVSZ2H.js → hook-fast-YAZG5MDX.js} +5 -2
- package/dist/host-key-U4B5QETR.js +8 -0
- package/dist/{import-capture-cmd-EQ35EARA.js → import-capture-cmd-KKFFOGWP.js} +7 -5
- package/dist/{import-ruler-cmd-BGAZXCQM.js → import-ruler-cmd-5P6JOXQN.js} +5 -3
- package/dist/index.js +8 -4
- package/dist/{init-PWU5YVL7.js → init-LWHDMZQG.js} +40 -38
- package/dist/init-MOINY2DD.js +53 -0
- package/dist/{init-cmd-XAVO7HYZ.js → init-cmd-HSMRID6A.js} +75 -30
- package/dist/{init-update-CVCWD6VS.js → init-update-LWX5MPPR.js} +29 -29
- package/dist/{install-HDBIPHTX.js → install-4SEFIIW2.js} +4 -5
- package/dist/{install-cmd-5HYMXLEW.js → install-cmd-QDOTR4OM.js} +5 -4
- package/dist/{login-browser-QZCS3WW6.js → login-browser-DX35MARZ.js} +5 -4
- package/dist/{logout-cmd-QAUFDJ6J.js → logout-cmd-5V5MMBSY.js} +3 -2
- package/dist/{logs-H55NWW4H.js → logs-AMLKC3VP.js} +3 -1
- package/dist/loop-SYYPVFYW.js +77 -0
- package/dist/machine-6X63G2KH.js +48 -0
- package/dist/{mcp-install-cmd-FDTPQQS6.js → mcp-install-cmd-GGWB3JJ2.js} +3 -2
- package/dist/{mcp-serve-cmd-3KVYEUXG.js → mcp-serve-cmd-N3UTY6EE.js} +11 -9
- package/dist/paths-7XP63OAX.js +7 -0
- package/dist/pending-532ABZYC.js +8 -0
- package/dist/{pidfile-UZRH774M.js → pidfile-3DM4OBKO.js} +4 -2
- package/dist/{record-C43U4PKS.js → record-3WP6V2K5.js} +3 -2
- package/dist/{runner-env-EN7YE7QX.js → runner-env-FRZFFHDF.js} +5 -6
- package/dist/{runner-key-6SLBI7PP.js → runner-key-276SJU5R.js} +40 -38
- package/dist/session-sender-GGIY5OAX.js +36 -0
- package/dist/{pending-MXLSRYMO.js → shell-function-NRD4KVUK.js} +1 -2
- package/dist/spec-cmd-SIZBRN7D.js +115 -0
- package/dist/{starter-pack-KF66HGZR.js → starter-pack-6LQVLBIA.js} +7 -5
- package/dist/{status-7VNQN5PW.js → status-NAK6DYSF.js} +46 -42
- package/dist/{statusline-fast-SVTMBMD7.js → statusline-fast-KN3BX67H.js} +2 -1
- package/dist/sync-cmd-G7GF4UTW.js +35 -0
- package/dist/{watch-23HSAEI6.js → watch-EVM5GSMP.js} +22 -15
- package/dist/{work-claim-cmd-LKL3GHFQ.js → work-claim-cmd-3J3QW56U.js} +26 -29
- package/dist/work-cmd-5EPPD33Y.js +24 -0
- package/dist/{work-comment-cmd-AWHP56M2.js → work-comment-cmd-2ACOZQAE.js} +16 -15
- package/dist/{work-launch-R5HTAP6X.js → work-launch-LEQ2MPM3.js} +3 -1
- package/dist/work-logs-cmd-QDD2NMJL.js +60 -0
- package/dist/{work-open-pr-cmd-IOM4AVQQ.js → work-open-pr-cmd-XNCEI4ZM.js} +18 -17
- package/dist/work-pr-cmd-H4OQR26N.js +25 -0
- package/package.json +3 -2
- package/skill-pack/maintained/outerlayer-amend/SKILL.md +4 -2
- package/skill-pack/maintained/outerlayer-cli/SKILL.md +9 -2
- package/skill-pack/maintained/outerlayer-cli/references/find-out-why-a-build-ended.md +76 -0
- package/skill-pack/shipped.json +18 -2
- package/skill-pack/template/build/SKILL.md +23 -10
- package/skill-pack/template/build/references/agent-briefs.md +3 -1
- package/skill-pack/template/build/references/criteria-judge.md +27 -0
- package/skill-pack/template/build/references/release.md +22 -29
- package/skill-pack/template/spec/SKILL.md +35 -10
- package/skill-pack/validators/acceptance-criteria.yaml +25 -26
- package/dist/agent-setup-T624CWPD.js +0 -27
- package/dist/chunk-6Y5RUTHO.js +0 -39
- package/dist/chunk-BYT5O5RV.js +0 -61
- package/dist/chunk-WTE3MQML.js +0 -133
- package/dist/chunk-XCCLFVXM.js +0 -401
- package/dist/chunk-YZ5F4PIS.js +0 -797
- package/dist/context-adopt-NFHCLVUB.js +0 -108
- package/dist/context-materialize-IV2TUUFO.js +0 -22
- package/dist/emit-cmd-IJ3GVRFD.js +0 -12
- package/dist/guest-init-LJP5GPZF.js +0 -9
- package/dist/host-key-YMLXY6QB.js +0 -7
- package/dist/init-6XPDRDTC.js +0 -51
- package/dist/loop-FK3LPMMZ.js +0 -73
- package/dist/machine-SQXEBAM2.js +0 -46
- package/dist/paths-OYKMVYJP.js +0 -6
- package/dist/session-sender-FO3LCREV.js +0 -29
- package/dist/sync-cmd-GVPPFZ3G.js +0 -28
- package/dist/work-cmd-Z2WXLZPH.js +0 -23
- package/dist/work-pr-cmd-HNEUIEN7.js +0 -24
|
@@ -1,33 +1,36 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
|
-
import { writeJobFile, readJobFile } from './chunk-
|
|
4
|
-
import { senderPidfile } from './chunk-
|
|
5
|
-
import { DOCKERD_LOG } from './chunk-
|
|
6
|
-
import { installScrubContext } from './chunk-
|
|
7
|
-
import { AssetError,
|
|
8
|
-
import { childEnvironment } from './chunk-
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
3
|
+
import { writeJobFile, readJobFile } from './chunk-SADUQSO4.js';
|
|
4
|
+
import { senderPidfile } from './chunk-5GHGAF6P.js';
|
|
5
|
+
import { DOCKERD_LOG, HELPER_PATH, HELPER_RECORD } from './chunk-XNGMQ27R.js';
|
|
6
|
+
import { installScrubContext } from './chunk-7SJPQWBM.js';
|
|
7
|
+
import { AssetError, obtainImage, ensureRootfs, gitRemoteFor, startServicesFor, createBuildDisk, machineConfig, resolveAgentUser, AgentUserError, runBuildSteps, shellQuote, createContainerRuntime, stopContainer, cleanupContainer, outOfMemoryReason, containerSenderCommand, syncInContainer, copyTranscriptsFromContainer, GH_CONFIG_YML, ghHostsYml, ADDED_USER, isLiteralAddress, signalInContainer, deleteBuildDisk, startCheckContainer, lifecycleSteps, lifecycleFailureReason, writeEvalInput, EVAL_INPUT_NAME, copyTranscripts, senderSignalScript, containerJudgeEnvironment, copyJudgeTree } from './chunk-7KSSBQTS.js';
|
|
8
|
+
import { childEnvironment } from './chunk-VMJPFQNE.js';
|
|
9
|
+
import { HASH_ARGV, parseProgramHash, JUDGE_PROGRAM, judgeCommand, MAX_REPORT_BYTES, JUDGE_MAX_TURNS, JUDGE_DIR } from './chunk-3UZTIRLF.js';
|
|
10
|
+
import { waitForAgent, vmExecArgs } from './chunk-GGOMT6KF.js';
|
|
11
|
+
import { socketPathProblem, vsockLinks, linkVsockSockets } from './chunk-X5H2WYMB.js';
|
|
12
|
+
import { describeCallFailure, resolveGoverningSource, refusalCode, removeStagedContext, stageGovernedContext, pinnedSourceOf, commandWithShim, logStamp, createRunnerLog, formatDuration, retrySend, formatClock } from './chunk-XSZD4AZ6.js';
|
|
13
|
+
import { WorkCommandError, postJson, isSignatureRefusal, getJson } from './chunk-HPJNLETG.js';
|
|
14
|
+
import { planArtifactUpload } from './chunk-G3IJ3N6F.js';
|
|
15
|
+
import { isBuiltinContainer, isBuiltinProcess, isBuiltinVm, DEFAULT_CHECK_TIME_LIMIT_MINUTES } from './chunk-P53VEAOF.js';
|
|
15
16
|
import { parseSize } from './chunk-7RQ73DLO.js';
|
|
16
|
-
import {
|
|
17
|
+
import { readBranchHead, fetchCommit, diffBetween } from './chunk-3CWUOFV6.js';
|
|
17
18
|
import { CLAUDE_CODE_VERSION } from './chunk-CX22S5BA.js';
|
|
18
|
-
import {
|
|
19
|
-
import { scrubText } from './chunk-
|
|
20
|
-
import {
|
|
21
|
-
import { parsePolicy } from './chunk-
|
|
19
|
+
import { isSameProcess, startTimeOf } from './chunk-JBJTC3UU.js';
|
|
20
|
+
import { scrubText } from './chunk-QNLX477Q.js';
|
|
21
|
+
import { resolvePackRoot } from './chunk-NNLA7LLQ.js';
|
|
22
|
+
import { parsePolicy } from './chunk-WFSUUUAN.js';
|
|
22
23
|
import { readCloudConfig } from './chunk-EABW6AJQ.js';
|
|
23
|
-
import { resolveVersion } from './chunk-RA3O54FC.js';
|
|
24
|
-
import { signerFor } from './chunk-V7NIVOFD.js';
|
|
25
|
-
import { runnerDir, buildDir, vsockPath, RUNNER_FILE_MODE } from './chunk-ZM2IMMYN.js';
|
|
26
24
|
import { homeFromEnv } from './chunk-VNVZDWO3.js';
|
|
25
|
+
import { resolveVersion } from './chunk-RA3O54FC.js';
|
|
26
|
+
import { signerFor } from './chunk-M2ZWWTWZ.js';
|
|
27
|
+
import { runnerDir, buildDir, vsockPath, RUNNER_FILE_MODE } from './chunk-N2PXGQRL.js';
|
|
28
|
+
import { claudeCredential, repositoriesOf, primaryOf, buildEnvironment, JOB_MOUNT, CHECKOUT_DIR, parseIsolation, proxyEnvironment, withProxyWarningHidden, containerLocalAddress, destinationEnvironment, CLAUDE_CREDENTIAL_VARIABLES, builtInEnvironment, withHooksPath, startBuildServices, BUILT_IN_NAMES, AGENT_CREDENTIAL_MISSING, sharedUserEnvironment, headerVariables, authorEnvironment, withTunnelRecord, commitIdentityRecord, RELAY_PORT, repositoryPath, lifecycleWorkdir, checkEnvironment, EVAL_INPUT_DIR } from './chunk-BQXFBI7T.js';
|
|
27
29
|
import { spawn, spawnSync } from 'child_process';
|
|
28
30
|
import { randomBytes, createHash } from 'crypto';
|
|
29
|
-
import { mkdirSync, chmodSync, writeFileSync, appendFileSync, statSync, accessSync, constants, openSync, existsSync, readFileSync, closeSync, rmSync, readdirSync, lstatSync, realpathSync } from 'fs';
|
|
31
|
+
import { mkdirSync, chmodSync, writeFileSync, appendFileSync, statSync, accessSync, constants, openSync, existsSync, readFileSync, closeSync, rmSync, readdirSync, lstatSync, fstatSync, readSync, realpathSync } from 'fs';
|
|
30
32
|
import { join, isAbsolute, dirname, delimiter, sep } from 'path';
|
|
33
|
+
import { pathToFileURL } from 'url';
|
|
31
34
|
|
|
32
35
|
createRequire(import.meta.url);
|
|
33
36
|
var DEFAULT_MEMORY = "8g";
|
|
@@ -103,11 +106,16 @@ async function provisionMicroVm(input) {
|
|
|
103
106
|
} catch (err) {
|
|
104
107
|
return fail(err instanceof AssetError ? err.message : `the microVM's assets could not be had: ${describeCallFailure(err)}`);
|
|
105
108
|
}
|
|
109
|
+
const governing = await resolveGoverningSource({ config, repository, fetchImpl: input.fetchImpl });
|
|
110
|
+
if (!governing.ok) return fail(governing.reason);
|
|
111
|
+
const governingSource = governing.source;
|
|
106
112
|
const obtained = await obtainImage({
|
|
107
113
|
config,
|
|
108
114
|
runtime,
|
|
109
115
|
tokens,
|
|
110
|
-
repository,
|
|
116
|
+
repository: governingSource.kind === "governed" ? governingSource.repository : repository,
|
|
117
|
+
pinned: governingSource.kind === "governed" ? { commit: governingSource.sha, ref: governingSource.ref } : void 0,
|
|
118
|
+
governedRepository: repository,
|
|
111
119
|
tag,
|
|
112
120
|
log,
|
|
113
121
|
aborted: input.aborted,
|
|
@@ -127,9 +135,10 @@ async function provisionMicroVm(input) {
|
|
|
127
135
|
containerEnv: report.apply.containerEnv,
|
|
128
136
|
builtIns: true,
|
|
129
137
|
lifecycle: report.apply.lifecycle,
|
|
138
|
+
...governingSource.kind === "governed" ? { governing: { repository: governingSource.repository, ref: governingSource.ref, commit: governingSource.sha } } : {},
|
|
130
139
|
...report.docker === true ? { docker: true } : {}
|
|
131
140
|
};
|
|
132
|
-
const
|
|
141
|
+
const envInput = {
|
|
133
142
|
config,
|
|
134
143
|
item: item.number,
|
|
135
144
|
itemKey,
|
|
@@ -138,7 +147,8 @@ async function provisionMicroVm(input) {
|
|
|
138
147
|
hostEnv: runtime.hostEnv,
|
|
139
148
|
containerEnv: report.apply.containerEnv,
|
|
140
149
|
builtIns: true
|
|
141
|
-
}
|
|
150
|
+
};
|
|
151
|
+
const env = buildEnvironment(envInput);
|
|
142
152
|
if (env.skippedTokenVariables.length > 0) {
|
|
143
153
|
log.line(`${tag} not passing ${env.skippedTokenVariables.join(", ")} from runner.build.variables into the build: its repository token comes from the runner`);
|
|
144
154
|
}
|
|
@@ -277,17 +287,24 @@ async function provisionMicroVm(input) {
|
|
|
277
287
|
}
|
|
278
288
|
state.agentUser = agentUser;
|
|
279
289
|
const tools = await asRoot(runtime, state, ["cat", "/opt/outerlayer/tools.json"]);
|
|
280
|
-
|
|
290
|
+
const agentVersion = agentVersionOf(tools.status === 0 ? tools.stdout : "");
|
|
291
|
+
environment.agentVersion = agentVersion;
|
|
292
|
+
const program = await asRoot(runtime, state, [...HASH_ARGV]);
|
|
293
|
+
const hashed = program.status === 0 ? parseProgramHash(program.stdout) : void 0;
|
|
294
|
+
if (hashed !== void 0) state.judgeProgram = { path: JUDGE_PROGRAM, sha256: hashed };
|
|
281
295
|
input.save(state, environment);
|
|
282
296
|
if (report.docker === true) {
|
|
283
297
|
const failure = await waitForDocker(runtime, state, agentUser, input.now ?? (() => /* @__PURE__ */ new Date()));
|
|
284
298
|
if (failure !== void 0) return bail(failure);
|
|
285
299
|
}
|
|
286
|
-
const
|
|
300
|
+
const execEnv = buildEnvironment({ ...envInput, imageEnv: state.imageEnv });
|
|
301
|
+
const request = (argv, workdir) => ({ argv, user: agentUser, cwd: workdir, env: execEnvironment(state.imageEnv, execEnv) });
|
|
287
302
|
const steps = await runBuildSteps({
|
|
288
303
|
repository,
|
|
289
304
|
branch: primary.branch,
|
|
290
305
|
repositories,
|
|
306
|
+
...governingSource.kind === "governed" ? { governing: { repository: governingSource.repository } } : {},
|
|
307
|
+
agentVersion,
|
|
291
308
|
lifecycle: report.apply.lifecycle,
|
|
292
309
|
exec: async (argv, workdir, opts) => {
|
|
293
310
|
const ran = await vm.exec(state.vsockPath, request(argv, workdir));
|
|
@@ -300,6 +317,7 @@ async function provisionMicroVm(input) {
|
|
|
300
317
|
stageContext: () => stageGovernedContext({
|
|
301
318
|
config,
|
|
302
319
|
repository,
|
|
320
|
+
source: pinnedSourceOf(governingSource),
|
|
303
321
|
buildId,
|
|
304
322
|
buildDir: buildDir2,
|
|
305
323
|
fetchImpl: input.fetchImpl,
|
|
@@ -321,6 +339,7 @@ async function provisionMicroVm(input) {
|
|
|
321
339
|
});
|
|
322
340
|
if (!steps.ok) return bail(steps.reason, steps.aborted === true);
|
|
323
341
|
state.startCommit = steps.startCommit;
|
|
342
|
+
if (steps.controlPlane !== void 0) environment.controlPlane = steps.controlPlane;
|
|
324
343
|
return { ok: true, workdir: CHECKOUT_DIR, state, environment };
|
|
325
344
|
}
|
|
326
345
|
async function waitForDocker(runtime, state, agentUser, now) {
|
|
@@ -365,7 +384,8 @@ function vmExecFor(input, argv) {
|
|
|
365
384
|
hostEnv: input.runtime.hostEnv,
|
|
366
385
|
containerEnv: state.containerEnv ?? {},
|
|
367
386
|
builtIns: state.builtIns === true,
|
|
368
|
-
identity: input.job.commitIdentity
|
|
387
|
+
identity: input.job.commitIdentity,
|
|
388
|
+
imageEnv: state.imageEnv
|
|
369
389
|
});
|
|
370
390
|
const options = {
|
|
371
391
|
socket: state.vsockPath,
|
|
@@ -378,7 +398,7 @@ function vmExecFor(input, argv) {
|
|
|
378
398
|
return { options, env: { ...childEnvironment(input.runtime.hostEnv), ...env.passed } };
|
|
379
399
|
}
|
|
380
400
|
function microVmCommandLine(input) {
|
|
381
|
-
const { options, env } = vmExecFor(input, ["/bin/sh", "-c", input.cmdline]);
|
|
401
|
+
const { options, env } = vmExecFor(input, ["/bin/sh", "-c", commandWithShim(input.job, input.cmdline)]);
|
|
382
402
|
return { line: [process.execPath, input.cli, "vm-exec", ...vmExecArgs(options)].map(shellQuote).join(" "), options, env };
|
|
383
403
|
}
|
|
384
404
|
function microVmSenderCommand(input) {
|
|
@@ -403,7 +423,8 @@ async function syncInMicroVm(input) {
|
|
|
403
423
|
hostEnv: runtime.hostEnv,
|
|
404
424
|
containerEnv: state.containerEnv ?? {},
|
|
405
425
|
builtIns: state.builtIns === true,
|
|
406
|
-
identity: job.commitIdentity
|
|
426
|
+
identity: job.commitIdentity,
|
|
427
|
+
imageEnv: state.imageEnv
|
|
407
428
|
});
|
|
408
429
|
const result = await runtime.vm.exec(state.vsockPath, {
|
|
409
430
|
argv: ["outerlayer", "sync"],
|
|
@@ -429,6 +450,98 @@ async function copyTranscriptsFromMicroVm(runtime, state) {
|
|
|
429
450
|
if (!isRunning(runtime, state)) return { ok: false, reason: "the build's microVM is gone" };
|
|
430
451
|
return copyTranscripts((argv) => asRoot(runtime, state, argv));
|
|
431
452
|
}
|
|
453
|
+
function guestStopScript(paths = { proc: "/proc", record: HELPER_RECORD }) {
|
|
454
|
+
return [
|
|
455
|
+
`record=${shellQuote(paths.record)}`,
|
|
456
|
+
'[ -r "$record" ] || exit 2',
|
|
457
|
+
'keep=" "',
|
|
458
|
+
'while read -r kpid kstart; do keep="$keep$kpid:$kstart "; done < "$record"',
|
|
459
|
+
"n=0",
|
|
460
|
+
"while [ $n -lt 50 ]; do",
|
|
461
|
+
' left=""',
|
|
462
|
+
// The list of /proc entries is made once per pass, so the commands this script runs are not in it.
|
|
463
|
+
` for p in ${shellQuote(paths.proc)}/[0-9]*; do`,
|
|
464
|
+
" pid=${p##*/}",
|
|
465
|
+
' [ "$pid" = 1 ] && continue',
|
|
466
|
+
' [ "$pid" = "$$" ] && continue',
|
|
467
|
+
' stat=$(cat "$p/stat" 2>/dev/null) || continue',
|
|
468
|
+
// Field 2 of stat is the command name, which can hold spaces, so the fields are counted from its closing ")":
|
|
469
|
+
// $1 is the state, $7 the flags, ${18} the thread count and ${20} the start time.
|
|
470
|
+
' set -- ${stat##*") "}',
|
|
471
|
+
// 0x200000 is PF_KTHREAD.
|
|
472
|
+
" [ $(($7 & 2097152)) -ne 0 ] && continue",
|
|
473
|
+
' [ "$1" = Z ] && [ "${18}" -le 1 ] && continue',
|
|
474
|
+
' case "$keep" in *" $pid:${20} "*) continue ;; esac',
|
|
475
|
+
' left="$left $pid"',
|
|
476
|
+
" done",
|
|
477
|
+
' [ -z "$left" ] && exit 0',
|
|
478
|
+
" for pid in $left; do kill -KILL $pid 2>/dev/null; done",
|
|
479
|
+
" n=$((n+1))",
|
|
480
|
+
" sleep 0.2",
|
|
481
|
+
"done",
|
|
482
|
+
"exit 1"
|
|
483
|
+
].join("\n");
|
|
484
|
+
}
|
|
485
|
+
var GUEST_JUDGE_DIR = JUDGE_DIR;
|
|
486
|
+
function microVmJudgeEnvironment(input) {
|
|
487
|
+
const { runtime, job } = input;
|
|
488
|
+
const state = job.microvm;
|
|
489
|
+
const agentUser = state.agentUser ?? ADDED_USER;
|
|
490
|
+
const needsVm = () => {
|
|
491
|
+
if (state.firecracker !== void 0 && !isRunning(runtime, state)) throw new Error("the build's microVM is gone");
|
|
492
|
+
};
|
|
493
|
+
return {
|
|
494
|
+
stop: async () => {
|
|
495
|
+
needsVm();
|
|
496
|
+
const stopped = await asRoot(runtime, state, ["/bin/sh", "-c", guestStopScript()]);
|
|
497
|
+
if (stopped.status === 2) throw new Error(`the guest has no record of the runner's helper processes at ${HELPER_RECORD}, so nothing was stopped`);
|
|
498
|
+
if (stopped.status !== 0) throw new Error(`processes of the build were still running after they were killed (exit ${stopped.status})`);
|
|
499
|
+
},
|
|
500
|
+
hashProgram: async () => {
|
|
501
|
+
needsVm();
|
|
502
|
+
const ran = await asRoot(runtime, state, [...HASH_ARGV]);
|
|
503
|
+
const sha256 = ran.status === 0 ? parseProgramHash(ran.stdout) : void 0;
|
|
504
|
+
return sha256 === void 0 ? { ok: false, reason: firstLine(ran.stderr) || `the hash could not be taken (exit ${ran.status})` } : { ok: true, sha256 };
|
|
505
|
+
},
|
|
506
|
+
prepare: async (files, tree) => {
|
|
507
|
+
needsVm();
|
|
508
|
+
const staging = join(input.jobDir, "judge-input");
|
|
509
|
+
rmSync(staging, { recursive: true, force: true });
|
|
510
|
+
mkdirSync(staging, { recursive: true });
|
|
511
|
+
try {
|
|
512
|
+
for (const [fileName, text2] of Object.entries(files)) writeFileSync(join(staging, fileName), text2);
|
|
513
|
+
if (tree !== void 0) copyJudgeTree(tree, join(staging, "tree"));
|
|
514
|
+
const dir = GUEST_JUDGE_DIR;
|
|
515
|
+
const script = [
|
|
516
|
+
"set -e",
|
|
517
|
+
`grep -q " ${dir} tmpfs " /proc/mounts || ${HELPER_PATH} mount --target ${dir} --source tmpfs --type tmpfs --options mode=0755`,
|
|
518
|
+
`find ${dir} -mindepth 1 -delete`,
|
|
519
|
+
`tar -x --no-same-owner -C ${dir}`,
|
|
520
|
+
`mkdir -p ${dir}/home/tmp`,
|
|
521
|
+
`chmod -R a+rX ${dir}`,
|
|
522
|
+
`chown -R ${shellQuote(agentUser)} ${dir}/home`,
|
|
523
|
+
`chmod 1777 ${dir}`
|
|
524
|
+
].join("\n");
|
|
525
|
+
const result = await asRoot(runtime, state, ["/bin/sh", "-c", script], tarOf(staging));
|
|
526
|
+
if (result.status !== 0) throw new Error(`preparing ${dir} in the guest failed (exit ${result.status}): ${firstLine(result.stderr)}`);
|
|
527
|
+
} finally {
|
|
528
|
+
rmSync(staging, { recursive: true, force: true });
|
|
529
|
+
}
|
|
530
|
+
},
|
|
531
|
+
run: async (command, opts) => {
|
|
532
|
+
needsVm();
|
|
533
|
+
const ran = await runtime.vm.exec(state.vsockPath, { argv: command.argv, user: agentUser, cwd: command.cwd, env: command.env });
|
|
534
|
+
appendFileSync(opts.logPath, ran.stdout + ran.stderr);
|
|
535
|
+
return ran;
|
|
536
|
+
},
|
|
537
|
+
readReport: async () => {
|
|
538
|
+
needsVm();
|
|
539
|
+
const file = `${GUEST_JUDGE_DIR}/report.html`;
|
|
540
|
+
const read = await asRoot(runtime, state, ["/bin/sh", "-c", `[ -f ${file} ] && [ ! -L ${file} ] && head -c ${MAX_REPORT_BYTES + 1} ${file}`]);
|
|
541
|
+
return read.status === 0 && Buffer.byteLength(read.stdout) <= MAX_REPORT_BYTES ? read.stdout : void 0;
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
}
|
|
432
545
|
async function startCheckMicroVm(input) {
|
|
433
546
|
const { config, runtime, job, jobDir, tokens, log } = input;
|
|
434
547
|
const { vm } = runtime;
|
|
@@ -548,7 +661,7 @@ async function startCheckMicroVm(input) {
|
|
|
548
661
|
const failed = await boot();
|
|
549
662
|
if (failed !== void 0) return fail(failed);
|
|
550
663
|
if (abandoned()) return fail(ABANDONED);
|
|
551
|
-
const env = checkEnvironment({ config, item: job.item, repository, hostEnv: runtime.hostEnv, containerEnv: built.containerEnv ?? {} });
|
|
664
|
+
const env = checkEnvironment({ config, item: job.item, repository, hostEnv: runtime.hostEnv, containerEnv: built.containerEnv ?? {}, imageEnv: built.imageEnv });
|
|
552
665
|
const evalInput = { dir: join(checksDir, EVAL_INPUT_NAME), delivered: false };
|
|
553
666
|
const deliverEvalInput = async () => {
|
|
554
667
|
await unpackIntoGuest(runtime, state, evalInput.dir, EVAL_INPUT_DIR, { readOnly: true });
|
|
@@ -734,6 +847,7 @@ function hostCommandEnvironment(base, build, config, hooksPath) {
|
|
|
734
847
|
Object.assign(env, proxyEnvironment(proxyUrlFor(build), brokerHost));
|
|
735
848
|
} else if (build.proxyPort !== void 0) {
|
|
736
849
|
Object.assign(env, proxyEnvironment(`http://127.0.0.1:${build.proxyPort}`));
|
|
850
|
+
env.NODE_OPTIONS = withProxyWarningHidden(env.NODE_OPTIONS);
|
|
737
851
|
}
|
|
738
852
|
const ports = build.destinations ?? [];
|
|
739
853
|
const portOf = (name) => ports.find((entry) => entry.name === name)?.port;
|
|
@@ -875,11 +989,123 @@ async function settleVerdict(deps) {
|
|
|
875
989
|
await deps.sleep(everyMs);
|
|
876
990
|
}
|
|
877
991
|
}
|
|
992
|
+
var RUNNER_LOG_FILE = "runner.log";
|
|
993
|
+
var RUNNER_LOG_KEEP_BYTES = 65536;
|
|
994
|
+
function isTailedStage(stage) {
|
|
995
|
+
const tailed = ["provision", "sync", "cleanup", "report"];
|
|
996
|
+
return tailed.includes(stage);
|
|
997
|
+
}
|
|
998
|
+
var READ_LIMIT_BYTES = 1048576;
|
|
999
|
+
var ITEM_LINE = /^#(\d+)\b/;
|
|
1000
|
+
var AttemptLogBook = class {
|
|
1001
|
+
constructor(now = () => /* @__PURE__ */ new Date()) {
|
|
1002
|
+
this.now = now;
|
|
1003
|
+
this.dirs = /* @__PURE__ */ new Map();
|
|
1004
|
+
this.lastByKey = /* @__PURE__ */ new Map();
|
|
1005
|
+
}
|
|
1006
|
+
/** Starts keeping lines for `item` in `jobDirPath`. Idempotent. */
|
|
1007
|
+
open(item, jobDirPath) {
|
|
1008
|
+
this.dirs.set(Number(item), jobDirPath);
|
|
1009
|
+
}
|
|
1010
|
+
/** Stops keeping lines for `item`; its file stays on disk until housekeeping removes the job directory. */
|
|
1011
|
+
close(item) {
|
|
1012
|
+
const number = Number(item);
|
|
1013
|
+
this.dirs.delete(number);
|
|
1014
|
+
for (const key of [...this.lastByKey.keys()]) {
|
|
1015
|
+
if (key.startsWith(`${number}:`)) this.lastByKey.delete(key);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
/** The log with every line it writes for an open item also kept in that item's file. */
|
|
1019
|
+
wrap(log) {
|
|
1020
|
+
return {
|
|
1021
|
+
line: (text2) => {
|
|
1022
|
+
log.line(text2);
|
|
1023
|
+
this.keepItemLine(text2);
|
|
1024
|
+
},
|
|
1025
|
+
error: (text2) => {
|
|
1026
|
+
log.error(text2);
|
|
1027
|
+
this.keepItemLine(text2);
|
|
1028
|
+
}
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
/** One line for `item` that does not itself start with its number. */
|
|
1032
|
+
note(item, text2) {
|
|
1033
|
+
this.append(Number(item), text2);
|
|
1034
|
+
}
|
|
1035
|
+
/** One line, written only when `text` differs from the last one written under `key` for the item. */
|
|
1036
|
+
noteChanged(item, key, text2) {
|
|
1037
|
+
const number = Number(item);
|
|
1038
|
+
const slot = `${number}:${key}`;
|
|
1039
|
+
if (this.lastByKey.get(slot) === text2) return;
|
|
1040
|
+
this.lastByKey.set(slot, text2);
|
|
1041
|
+
this.append(number, text2);
|
|
1042
|
+
}
|
|
1043
|
+
keepItemLine(text2) {
|
|
1044
|
+
const match = ITEM_LINE.exec(text2);
|
|
1045
|
+
if (match !== null) this.append(Number(match[1]), text2);
|
|
1046
|
+
}
|
|
1047
|
+
append(item, text2) {
|
|
1048
|
+
const dir = this.dirs.get(item);
|
|
1049
|
+
if (dir === void 0) return;
|
|
1050
|
+
try {
|
|
1051
|
+
appendFileSync(join(dir, RUNNER_LOG_FILE), `${logStamp(this.now())} ${text2.replace(/[\r\n]+/g, " ")}
|
|
1052
|
+
`, { mode: 384 });
|
|
1053
|
+
} catch {
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
};
|
|
1057
|
+
function readEnd(path, maxBytes) {
|
|
1058
|
+
let fd;
|
|
1059
|
+
try {
|
|
1060
|
+
fd = openSync(path, "r");
|
|
1061
|
+
const size = fstatSync(fd).size;
|
|
1062
|
+
const length = Math.min(size, maxBytes);
|
|
1063
|
+
const buffer = Buffer.alloc(length);
|
|
1064
|
+
readSync(fd, buffer, 0, length, size - length);
|
|
1065
|
+
return buffer.toString("utf8");
|
|
1066
|
+
} catch {
|
|
1067
|
+
return void 0;
|
|
1068
|
+
} finally {
|
|
1069
|
+
if (fd !== void 0) closeSync(fd);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
function lastLines(text2, count) {
|
|
1073
|
+
const lines = text2.split("\n");
|
|
1074
|
+
if (lines.at(-1) === "") lines.pop();
|
|
1075
|
+
return lines.slice(-200);
|
|
1076
|
+
}
|
|
1077
|
+
function capRunnerLog(text2) {
|
|
1078
|
+
const bytes = Buffer.from(text2, "utf8");
|
|
1079
|
+
if (bytes.length <= RUNNER_LOG_KEEP_BYTES) return text2;
|
|
1080
|
+
let start = bytes.length - RUNNER_LOG_KEEP_BYTES;
|
|
1081
|
+
if (bytes[start - 1] !== 10) {
|
|
1082
|
+
const newline = bytes.indexOf(10, start);
|
|
1083
|
+
if (newline + 1 < bytes.length) {
|
|
1084
|
+
start = newline + 1;
|
|
1085
|
+
} else {
|
|
1086
|
+
while ((bytes[start] & 192) === 128) start++;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
return `[runner log truncated: the first ${start} bytes were cut]
|
|
1090
|
+
${bytes.subarray(start).toString("utf8")}`;
|
|
1091
|
+
}
|
|
1092
|
+
function buildReleaseLog(jobDirPath, endStage, scrub) {
|
|
1093
|
+
const parts = [];
|
|
1094
|
+
const own = readEnd(join(jobDirPath, RUNNER_LOG_FILE), READ_LIMIT_BYTES);
|
|
1095
|
+
if (own !== void 0 && own.trim() !== "") parts.push(own.endsWith("\n") ? own.slice(0, -1) : own);
|
|
1096
|
+
if (isTailedStage(endStage)) {
|
|
1097
|
+
const file = `${endStage}.log`;
|
|
1098
|
+
const stage = readEnd(join(jobDirPath, file), READ_LIMIT_BYTES);
|
|
1099
|
+
const tail = stage === void 0 ? [] : lastLines(stage);
|
|
1100
|
+
if (tail.length > 0) parts.push(`--- the last ${tail.length} lines of ${file} ---`, ...tail);
|
|
1101
|
+
}
|
|
1102
|
+
if (parts.length === 0) return void 0;
|
|
1103
|
+
return capRunnerLog(scrub(parts.join("\n")) + "\n");
|
|
1104
|
+
}
|
|
878
1105
|
var ENDED = "ended before provision finished";
|
|
879
1106
|
async function provisionProcess(input) {
|
|
880
|
-
const { config, runtime, item, jobDir } = input;
|
|
1107
|
+
const { config, runtime, item, repository, jobDir } = input;
|
|
881
1108
|
if (claudeCredential(runtime.hostEnv) === void 0) return { ok: false, reason: AGENT_CREDENTIAL_MISSING };
|
|
882
|
-
const repository = item.specRepository;
|
|
883
1109
|
if (repository === void 0 || repository === null || repository === "") {
|
|
884
1110
|
return { ok: false, reason: "the work item names no repository to clone" };
|
|
885
1111
|
}
|
|
@@ -921,6 +1147,9 @@ var MAX_VALIDATOR_FILES = 20;
|
|
|
921
1147
|
var VALIDATORS_DIR = ".outerlayer/validators";
|
|
922
1148
|
var POLICY_FILE = ".outerlayer/policy.yaml";
|
|
923
1149
|
var LIMIT_KEY_TAIL = " (runner.checks.timeLimitMinutes)";
|
|
1150
|
+
function isJudgeCheck(check) {
|
|
1151
|
+
return "judge" in check;
|
|
1152
|
+
}
|
|
924
1153
|
function lastLine(text2) {
|
|
925
1154
|
return text2.split(/\r\n|\r|\n/).map((line) => line.trim()).filter((line) => line !== "").at(-1);
|
|
926
1155
|
}
|
|
@@ -956,17 +1185,24 @@ function checksIn(root, log, tag) {
|
|
|
956
1185
|
const policy = readInside(root, POLICY_FILE);
|
|
957
1186
|
const loaded = parsePolicy(policy === void 0 ? null : { path: POLICY_FILE, content: policy }, validatorFilesIn(root));
|
|
958
1187
|
for (const problem of loaded.problems) log.error(`${tag} ${problem.file}: ${problem.problem}`);
|
|
959
|
-
const hostChecks = loaded.customs.filter((def) => def.runWhere === "host" && def.level !== "off" && def.declaresEmit !== null
|
|
960
|
-
|
|
1188
|
+
const hostChecks = loaded.customs.filter((def) => def.runWhere === "host" && def.level !== "off" && def.declaresEmit !== null);
|
|
1189
|
+
const checks = [];
|
|
1190
|
+
for (const def of hostChecks) {
|
|
1191
|
+
if (def.judge !== void 0) checks.push({ name: def.declaresEmit, judge: { brief: def.judge.brief, text: readInside(root, def.judge.brief) } });
|
|
1192
|
+
else if (def.command !== void 0) checks.push({ name: def.declaresEmit, command: def.command });
|
|
1193
|
+
}
|
|
1194
|
+
return checks;
|
|
961
1195
|
}
|
|
962
1196
|
async function readDefinitions(input, repository, scratch, token, tokens) {
|
|
963
1197
|
const { config, job, deps } = input;
|
|
964
1198
|
const tag = `#${job.item}`;
|
|
965
1199
|
const stage = deps.stage ?? stageGovernedContext;
|
|
966
1200
|
const controlPlaneUrl = (controlPlane) => deps.runtime.remoteFor?.(controlPlane) ?? `${deps.runtime.github.gitUrl}/${controlPlane}.git`;
|
|
1201
|
+
const governing = (job.container ?? job.microvm)?.governing;
|
|
967
1202
|
const staged = await stage({
|
|
968
1203
|
config,
|
|
969
1204
|
repository,
|
|
1205
|
+
...governing === void 0 ? {} : { source: { repository: governing.repository, ref: governing.ref, sha: governing.commit } },
|
|
970
1206
|
buildId: `checks-${randomBytes(4).toString("hex")}`,
|
|
971
1207
|
buildDir: scratch,
|
|
972
1208
|
fetchImpl: deps.fetchImpl,
|
|
@@ -1004,6 +1240,33 @@ function noCleanEnvironment(job) {
|
|
|
1004
1240
|
const ran = job.build?.workDir !== void 0 ? "builtin:process" : "a provision hook of the host's own";
|
|
1005
1241
|
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`;
|
|
1006
1242
|
}
|
|
1243
|
+
function makeRecorder(input) {
|
|
1244
|
+
const { config, job, deps } = input;
|
|
1245
|
+
const tag = `#${job.item}`;
|
|
1246
|
+
const now = deps.now ?? (() => /* @__PURE__ */ new Date());
|
|
1247
|
+
let signer;
|
|
1248
|
+
return async (name, result, sentence, opts = {}) => {
|
|
1249
|
+
signer ??= await signerFor(config.configPath);
|
|
1250
|
+
const keyTail = opts.keyTail ?? "";
|
|
1251
|
+
const body = sentence === void 0 ? void 0 : cutReasonLine(`${scrubReason(sentence, config) ?? "check failed"}${keyTail}`);
|
|
1252
|
+
const clientEmitId = `host-${createHash("sha256").update(`${job.claimId}:${job.startedAt}:${name}${opts.idTag === void 0 ? "" : `:${opts.idTag}`}`).digest("hex").slice(0, 40)}`;
|
|
1253
|
+
const sent = await retrySend(
|
|
1254
|
+
() => postJson(
|
|
1255
|
+
config.url,
|
|
1256
|
+
"/v1/emitted-results",
|
|
1257
|
+
config.apiKey,
|
|
1258
|
+
config.appId,
|
|
1259
|
+
{ schemaVersion: 1, emit: { clientEmitId, name, result, ...body === void 0 ? {} : { body }, ...opts.link === void 0 ? {} : { link: opts.link }, emittedAt: now().toISOString(), item: { number: Number(job.item) } } },
|
|
1260
|
+
deps.fetchImpl,
|
|
1261
|
+
void 0,
|
|
1262
|
+
signer
|
|
1263
|
+
),
|
|
1264
|
+
{ now: deps.now }
|
|
1265
|
+
);
|
|
1266
|
+
if (sent.ok) deps.log.line(`${tag} check ${name} ${result}${body === void 0 ? "" : `: ${body}`}`);
|
|
1267
|
+
else deps.log.error(`${tag} check ${name} ${result} could not be recorded: ${sent.reason}`);
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1007
1270
|
async function runHostChecks(input) {
|
|
1008
1271
|
const { config, job, jobDir, deps } = input;
|
|
1009
1272
|
const { runtime, log } = deps;
|
|
@@ -1035,34 +1298,15 @@ async function runHostChecks(input) {
|
|
|
1035
1298
|
rmSync(scratch, { recursive: true, force: true });
|
|
1036
1299
|
return;
|
|
1037
1300
|
}
|
|
1038
|
-
const checks = definitions.checks;
|
|
1301
|
+
const checks = definitions.checks.filter((check) => !isJudgeCheck(check));
|
|
1039
1302
|
if (checks.length === 0) {
|
|
1040
1303
|
rmSync(scratch, { recursive: true, force: true });
|
|
1041
1304
|
return;
|
|
1042
1305
|
}
|
|
1043
1306
|
log.line(`${tag} checks started (${checks.map((check) => check.name).join(", ")})`);
|
|
1044
1307
|
const startedMs = now().getTime();
|
|
1045
|
-
|
|
1046
|
-
const record =
|
|
1047
|
-
signer ??= await signerFor(config.configPath);
|
|
1048
|
-
const body = sentence === void 0 ? void 0 : cutReasonLine(`${scrubReason(sentence, config) ?? "check failed"}${keyTail}`);
|
|
1049
|
-
const clientEmitId = `host-${createHash("sha256").update(`${job.claimId}:${job.startedAt}:${check.name}`).digest("hex").slice(0, 40)}`;
|
|
1050
|
-
const sent = await retrySend(
|
|
1051
|
-
() => postJson(
|
|
1052
|
-
config.url,
|
|
1053
|
-
"/v1/emitted-results",
|
|
1054
|
-
config.apiKey,
|
|
1055
|
-
config.appId,
|
|
1056
|
-
{ schemaVersion: 1, emit: { clientEmitId, name: check.name, result, ...body === void 0 ? {} : { body }, emittedAt: now().toISOString(), item: { number: Number(job.item) } } },
|
|
1057
|
-
deps.fetchImpl,
|
|
1058
|
-
void 0,
|
|
1059
|
-
signer
|
|
1060
|
-
),
|
|
1061
|
-
{ now: deps.now }
|
|
1062
|
-
);
|
|
1063
|
-
if (sent.ok) log.line(`${tag} check ${check.name} ${result}${body === void 0 ? "" : `: ${body}`}`);
|
|
1064
|
-
else log.error(`${tag} check ${check.name} ${result} could not be recorded: ${sent.reason}`);
|
|
1065
|
-
};
|
|
1308
|
+
const recorder = makeRecorder({ config, job, deps });
|
|
1309
|
+
const record = (check, result, sentence, keyTail = "") => recorder(check.name, result, sentence, { keyTail });
|
|
1066
1310
|
const failAll = async (reason, keyTail = "") => {
|
|
1067
1311
|
log.error(`${tag} checks failed: ${reason}${keyTail}`);
|
|
1068
1312
|
for (const check of checks) await record(check, "fail", reason, keyTail);
|
|
@@ -1070,12 +1314,36 @@ async function runHostChecks(input) {
|
|
|
1070
1314
|
try {
|
|
1071
1315
|
if (job.container === void 0 && job.microvm === void 0) return await failAll(noCleanEnvironment(job));
|
|
1072
1316
|
if (tokens === void 0) return await failAll("the build's repository tokens are no longer held, so the branch cannot be fetched");
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
const
|
|
1077
|
-
|
|
1078
|
-
|
|
1317
|
+
if (job.branch === void 0) return await failAll("the build pushed no branch: the claim names none");
|
|
1318
|
+
const claimed = repositoriesOf({ repositories: job.repositories, repository, branch: job.branch });
|
|
1319
|
+
if (claimed.length === 0) return await failAll("the claim names no repository to check out");
|
|
1320
|
+
const remoteOf = (name) => runtime.remoteFor?.(name) ?? `${runtime.github.gitUrl}/${name}.git`;
|
|
1321
|
+
const remote = remoteOf(repository);
|
|
1322
|
+
const pushed = /* @__PURE__ */ new Map();
|
|
1323
|
+
const unpushed = [];
|
|
1324
|
+
let primaryHead;
|
|
1325
|
+
for (const entry of claimed) {
|
|
1326
|
+
let entryToken = readToken;
|
|
1327
|
+
if (!entry.primary) {
|
|
1328
|
+
try {
|
|
1329
|
+
entryToken = await tokens.token("read", entry.repository);
|
|
1330
|
+
} catch (err) {
|
|
1331
|
+
return await failAll(`no read token for ${entry.repository}: ${describeCallFailure(err)}`);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
const read = await (deps.readBranch ?? readBranchHead)({ remote: remoteOf(entry.repository), dir: scratch, branch: entry.branch, token: entryToken });
|
|
1335
|
+
if (read.kind === "unreadable") return await failAll(`the branch ${entry.branch} could not be read${entry.primary ? "" : ` on ${entry.repository}`}: ${read.reason}`);
|
|
1336
|
+
if (read.kind === "absent") {
|
|
1337
|
+
unpushed.push(entry);
|
|
1338
|
+
continue;
|
|
1339
|
+
}
|
|
1340
|
+
pushed.set(entry.repository, entry.branch);
|
|
1341
|
+
if (entry.primary) primaryHead = read.sha;
|
|
1342
|
+
}
|
|
1343
|
+
if (pushed.size === 0) {
|
|
1344
|
+
return await failAll(`the build pushed no branch: ${unpushed.map((entry) => `${entry.branch} does not exist on ${entry.repository}`).join("; ")}`);
|
|
1345
|
+
}
|
|
1346
|
+
const repositoriesToCheckOut = claimed;
|
|
1079
1347
|
const limitMinutes = config.checks?.timeLimitMinutes ?? DEFAULT_CHECK_TIME_LIMIT_MINUTES;
|
|
1080
1348
|
const minutes = `${limitMinutes} ${limitMinutes === 1 ? "minute" : "minutes"}`;
|
|
1081
1349
|
const environmentLog = join(jobDir, "checks", "environment.log");
|
|
@@ -1130,20 +1398,33 @@ async function runHostChecks(input) {
|
|
|
1130
1398
|
workdir: "/build/work",
|
|
1131
1399
|
failed: (status) => `configuring git failed (exit ${status})`
|
|
1132
1400
|
},
|
|
1133
|
-
{
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1401
|
+
...repositoriesToCheckOut.flatMap((entry) => {
|
|
1402
|
+
const branch = pushed.get(entry.repository);
|
|
1403
|
+
const path = entry.primary ? CHECKOUT_DIR : repositoryPath(entry.repository);
|
|
1404
|
+
const clone = {
|
|
1405
|
+
argv: ["git", "clone", "--filter=blob:none", "--single-branch", ...branch === void 0 ? [] : ["--branch", branch], `https://github.com/${entry.repository}.git`, path],
|
|
1406
|
+
workdir: "/build/work",
|
|
1407
|
+
failed: (status) => `the clone of ${branch ?? `the default branch of ${entry.repository}`} failed (exit ${status})`
|
|
1408
|
+
};
|
|
1409
|
+
return entry.primary && repositoriesToCheckOut.length > 1 ? [
|
|
1410
|
+
clone,
|
|
1411
|
+
{
|
|
1412
|
+
argv: ["sh", "-c", 'mkdir -p "$(dirname "$1")" && ln -s "$2" "$1"', "link", repositoryPath(entry.repository), CHECKOUT_DIR],
|
|
1413
|
+
workdir: "/build/work",
|
|
1414
|
+
failed: (status) => `linking ${entry.repository} at ${repositoryPath(entry.repository)} failed (exit ${status})`
|
|
1415
|
+
}
|
|
1416
|
+
] : [clone];
|
|
1417
|
+
})
|
|
1138
1418
|
];
|
|
1139
1419
|
for (const step of steps) {
|
|
1140
1420
|
if (abandoned) return "the check environment setup ran out of time";
|
|
1141
1421
|
const ran = await made.exec(step.argv, step.workdir, { logPath: environmentLog });
|
|
1142
1422
|
if (ran.status !== 0) return `the check environment could not be prepared: ${step.failed(ran.status)}`;
|
|
1143
1423
|
}
|
|
1424
|
+
const lifecycleDir = lifecycleWorkdir(repositoriesToCheckOut, (job.container ?? job.microvm)?.governing?.repository);
|
|
1144
1425
|
for (const step of lifecycleSteps((job.container ?? job.microvm)?.lifecycle ?? {})) {
|
|
1145
1426
|
log.line(`${tag} checks lifecycle ${step.name} (${step.source})`);
|
|
1146
|
-
const ran = await made.exec(step.argv,
|
|
1427
|
+
const ran = await made.exec(step.argv, lifecycleDir, { logPath: environmentLog });
|
|
1147
1428
|
if (ran.status !== 0) return `the check environment could not be prepared: ${lifecycleFailureReason(step, ran.status)}`;
|
|
1148
1429
|
}
|
|
1149
1430
|
return made;
|
|
@@ -1172,7 +1453,7 @@ async function runHostChecks(input) {
|
|
|
1172
1453
|
scratch,
|
|
1173
1454
|
remote,
|
|
1174
1455
|
readToken,
|
|
1175
|
-
branchHead:
|
|
1456
|
+
branchHead: primaryHead ?? (job.container ?? job.microvm)?.startCommit ?? "",
|
|
1176
1457
|
transcripts,
|
|
1177
1458
|
scrub: transcriptScrub(config, [config.apiKey, job.itemKey, readToken]),
|
|
1178
1459
|
readWorkItem: () => readWorkItem(config, job.item, deps.fetchImpl),
|
|
@@ -1239,6 +1520,296 @@ async function runHostChecks(input) {
|
|
|
1239
1520
|
}
|
|
1240
1521
|
}
|
|
1241
1522
|
|
|
1523
|
+
// src/runner/judge-verdict.ts
|
|
1524
|
+
function mechanicalFailures(criteria, citations) {
|
|
1525
|
+
return criteria.filter((criterion) => criterion.proof === null && (citations[criterion.id]?.length ?? 0) === 0).map((criterion) => criterion.id);
|
|
1526
|
+
}
|
|
1527
|
+
function firstLine2(text2) {
|
|
1528
|
+
return text2.trim().split(/\r\n|\r|\n/)[0]?.trim() ?? "";
|
|
1529
|
+
}
|
|
1530
|
+
function parseOutput(stdout) {
|
|
1531
|
+
try {
|
|
1532
|
+
const parsed = JSON.parse(stdout);
|
|
1533
|
+
if (typeof parsed !== "object" || parsed === null) return void 0;
|
|
1534
|
+
const { subtype, result } = parsed;
|
|
1535
|
+
return { subtype: typeof subtype === "string" ? subtype : void 0, result: typeof result === "string" ? result : "" };
|
|
1536
|
+
} catch {
|
|
1537
|
+
return void 0;
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
var RESULT_LINE = /^result:\s*(pass|fail)\s*$/i;
|
|
1541
|
+
var UNMET_LINE = /^unmet:\s*(\S+)\s*$/i;
|
|
1542
|
+
function decideVerdict(input) {
|
|
1543
|
+
const { run, criteria } = input;
|
|
1544
|
+
if (run.kind === "timeout") return { result: "fail", reason: `the judge ran out of time after ${input.minutes}`, limitKey: true };
|
|
1545
|
+
const output = parseOutput(run.stdout);
|
|
1546
|
+
if (output === void 0) {
|
|
1547
|
+
const said = firstLine2(run.stderr);
|
|
1548
|
+
return { result: "fail", reason: `the judge exited ${run.status} and printed no result${said === "" ? "" : `: ${said}`}` };
|
|
1549
|
+
}
|
|
1550
|
+
if (output.subtype === "error_max_turns") return { result: "fail", reason: `the judge reached the turn limit of ${JUDGE_MAX_TURNS} turns before it gave a result` };
|
|
1551
|
+
const lines = output.result.split(/\r\n|\r|\n/).map((line) => line.trim()).filter((line) => line !== "");
|
|
1552
|
+
const verdict = RESULT_LINE.exec(lines.at(-1) ?? "")?.[1]?.toLowerCase();
|
|
1553
|
+
if (verdict === void 0) return { result: "fail", reason: "the judge gave no `result:` line" };
|
|
1554
|
+
const named = new Set(lines.flatMap((line) => UNMET_LINE.exec(line)?.[1] ?? []));
|
|
1555
|
+
const mechanical = new Set(mechanicalFailures(criteria, input.citations));
|
|
1556
|
+
const unmet = criteria.map((criterion) => criterion.id).filter((id) => named.has(id) || mechanical.has(id));
|
|
1557
|
+
if (verdict === "pass" && unmet.length === 0) return { result: "pass" };
|
|
1558
|
+
const ruled = unmet.filter((id) => mechanical.has(id));
|
|
1559
|
+
const rule = ruled.length === 0 ? "" : `: the mechanical rule found no proof annotation and no citing test for ${ruled.join(", ")}`;
|
|
1560
|
+
if (unmet.length === 0) return { result: "fail", reason: "the judge failed the change and named no criterion" };
|
|
1561
|
+
return { result: "fail", reason: `${unmet.length} of ${criteria.length} criteria not satisfied, first ${unmet[0]}${rule}` };
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
// src/runner/judge.ts
|
|
1565
|
+
var LIMIT_KEY_TAIL2 = " (runner.checks.timeLimitMinutes)";
|
|
1566
|
+
var MAX_DIFF_BYTES = 4 * 1024 * 1024;
|
|
1567
|
+
var MAX_TEST_FILES = 2e4;
|
|
1568
|
+
var MAX_TEST_FILE_BYTES = 1024 * 1024;
|
|
1569
|
+
var NO_CLAUDE = "the image has no Claude Code for the judge";
|
|
1570
|
+
var REPORT_CAPTION = "The host's criteria judge, each criterion judged against the test that cites it";
|
|
1571
|
+
async function loadCitationFinder() {
|
|
1572
|
+
return await import(pathToFileURL(join(resolvePackRoot(), "template", "build", "scripts", "criterion-citations.mjs")).href);
|
|
1573
|
+
}
|
|
1574
|
+
function testFilesIn(root, isTest, limits = { files: MAX_TEST_FILES, bytes: MAX_TEST_FILE_BYTES }) {
|
|
1575
|
+
const files = [];
|
|
1576
|
+
const walk = (dir, prefix) => {
|
|
1577
|
+
for (const name of readdirSync(dir).sort()) {
|
|
1578
|
+
if (files.length >= limits.files) return;
|
|
1579
|
+
if (prefix === "" && name === ".git") continue;
|
|
1580
|
+
const path = join(dir, name);
|
|
1581
|
+
const rel = prefix === "" ? name : `${prefix}/${name}`;
|
|
1582
|
+
const stat = lstatSync(path);
|
|
1583
|
+
if (stat.isDirectory()) walk(path, rel);
|
|
1584
|
+
else if (stat.isFile() && isTest.test(rel) && stat.size <= limits.bytes) files.push({ path: rel, contents: readFileSync(path, "utf8") });
|
|
1585
|
+
}
|
|
1586
|
+
};
|
|
1587
|
+
walk(root, "");
|
|
1588
|
+
return files;
|
|
1589
|
+
}
|
|
1590
|
+
async function hostPassedNames(config, item, fetchImpl) {
|
|
1591
|
+
const workItemId = Object(await readWorkItem(config, item, fetchImpl)).workItemId;
|
|
1592
|
+
if (typeof workItemId !== "string") throw new WorkCommandError("the work item read named no id");
|
|
1593
|
+
const signer = await signerFor(config.configPath);
|
|
1594
|
+
const latest = /* @__PURE__ */ new Map();
|
|
1595
|
+
let since = null;
|
|
1596
|
+
do {
|
|
1597
|
+
const query = since === null ? "" : `?since=${encodeURIComponent(since)}`;
|
|
1598
|
+
const page = await getJson(config.url, `/v1/work-items/${encodeURIComponent(workItemId)}/changes${query}`, config.apiKey, config.appId, fetchImpl, signer);
|
|
1599
|
+
for (const entry of Array.isArray(page.data) ? page.data : []) {
|
|
1600
|
+
const { name, result, source } = Object(entry);
|
|
1601
|
+
if (source === "host" && typeof name === "string" && typeof result === "string") latest.set(name, result);
|
|
1602
|
+
}
|
|
1603
|
+
since = page.pagination?.cursor ?? null;
|
|
1604
|
+
} while (since !== null);
|
|
1605
|
+
return [...latest].filter(([, result]) => result === "pass").map(([name]) => name);
|
|
1606
|
+
}
|
|
1607
|
+
function criteriaOf(workItem) {
|
|
1608
|
+
const criteria = Object(Object(workItem).criteria);
|
|
1609
|
+
if (!Array.isArray(criteria?.items)) return void 0;
|
|
1610
|
+
const drafted = criteria.drafted === true;
|
|
1611
|
+
const items = [];
|
|
1612
|
+
for (const entry of criteria.items) {
|
|
1613
|
+
const { id, text: text2, proof } = Object(entry);
|
|
1614
|
+
if (typeof id !== "string" || typeof text2 !== "string") continue;
|
|
1615
|
+
items.push({ id, text: text2, proof: typeof proof === "string" ? proof : null, drafted });
|
|
1616
|
+
}
|
|
1617
|
+
return items.length === 0 ? void 0 : items;
|
|
1618
|
+
}
|
|
1619
|
+
function environmentFor(input) {
|
|
1620
|
+
const { config, job, jobDir, deps } = input;
|
|
1621
|
+
if (deps.judgeEnvironment !== void 0) return deps.judgeEnvironment;
|
|
1622
|
+
if (job.container !== void 0) return containerJudgeEnvironment({ runtime: deps.runtime, config, job, jobDir });
|
|
1623
|
+
if (job.microvm !== void 0) return microVmJudgeEnvironment({ runtime: deps.runtime, job, jobDir });
|
|
1624
|
+
return void 0;
|
|
1625
|
+
}
|
|
1626
|
+
async function runJudge(input) {
|
|
1627
|
+
const { config, job, jobDir, deps } = input;
|
|
1628
|
+
const { runtime, log } = deps;
|
|
1629
|
+
const tag = `#${job.item}`;
|
|
1630
|
+
const now = deps.now ?? (() => /* @__PURE__ */ new Date());
|
|
1631
|
+
const repository = job.repository ?? job.container?.repository;
|
|
1632
|
+
if (repository === void 0) return;
|
|
1633
|
+
const repo = repository;
|
|
1634
|
+
const tokens = runtime.credentials.get(jobDir);
|
|
1635
|
+
let readToken;
|
|
1636
|
+
try {
|
|
1637
|
+
readToken = await tokens?.token("read");
|
|
1638
|
+
} catch (err) {
|
|
1639
|
+
log.error(`${tag} no read token for the judge: ${describeCallFailure(err)}`);
|
|
1640
|
+
}
|
|
1641
|
+
const scratch = join(jobDir, "judge-scratch");
|
|
1642
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
1643
|
+
const definitionsDir = join(scratch, "definitions");
|
|
1644
|
+
mkdirSync(definitionsDir, { recursive: true });
|
|
1645
|
+
let definitions;
|
|
1646
|
+
try {
|
|
1647
|
+
definitions = await readDefinitions(input, repository, definitionsDir, readToken, tokens);
|
|
1648
|
+
} catch (err) {
|
|
1649
|
+
definitions = { ok: false, reason: describeCallFailure(err) };
|
|
1650
|
+
} finally {
|
|
1651
|
+
removeStagedContext(definitionsDir);
|
|
1652
|
+
}
|
|
1653
|
+
if (!definitions.ok) {
|
|
1654
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
1655
|
+
log.line(`${tag} judge not run: the definitions could not be read: ${definitions.reason}`);
|
|
1656
|
+
const passed = await retrySend(() => hostPassedNames(config, job.item, deps.fetchImpl), { now: deps.now });
|
|
1657
|
+
if (!passed.ok) {
|
|
1658
|
+
log.error(`${tag} the item's host results could not be read, so none was failed: ${passed.reason}`);
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
const record2 = makeRecorder({ config, job, deps });
|
|
1662
|
+
for (const name of passed.value) await record2(name, "fail", `the definitions could not be read: ${definitions.reason}`, { idTag: "judge-unread" });
|
|
1663
|
+
return;
|
|
1664
|
+
}
|
|
1665
|
+
const checks = definitions.checks.filter(isJudgeCheck);
|
|
1666
|
+
if (checks.length === 0) {
|
|
1667
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
1668
|
+
return;
|
|
1669
|
+
}
|
|
1670
|
+
log.line(`${tag} judge started (${checks.map((check) => check.name).join(", ")})`);
|
|
1671
|
+
const startedMs = now().getTime();
|
|
1672
|
+
const record = makeRecorder({ config, job, deps });
|
|
1673
|
+
const fail = (check, reason) => {
|
|
1674
|
+
log.error(`${tag} judge failed: ${reason}`);
|
|
1675
|
+
return record(check.name, "fail", reason);
|
|
1676
|
+
};
|
|
1677
|
+
try {
|
|
1678
|
+
for (const check of checks) {
|
|
1679
|
+
try {
|
|
1680
|
+
await judgeOne(check);
|
|
1681
|
+
} catch (err) {
|
|
1682
|
+
await fail(check, `the judge could not be run: ${describeCallFailure(err)}`);
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
} finally {
|
|
1686
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
1687
|
+
log.line(`${tag} judge done (${formatDuration(now().getTime() - startedMs)})`);
|
|
1688
|
+
}
|
|
1689
|
+
async function judgeOne(check) {
|
|
1690
|
+
const environment = environmentFor(input);
|
|
1691
|
+
if (environment === void 0) return await fail(check, `${NO_CLAUDE}: this build ran outside a container or microVM`);
|
|
1692
|
+
const expected = (job.container ?? job.microvm)?.judgeProgram;
|
|
1693
|
+
if (expected === void 0) return await fail(check, NO_CLAUDE);
|
|
1694
|
+
if (check.judge.text === void 0) return await fail(check, `the brief ${check.judge.brief} is not in the definitions repository`);
|
|
1695
|
+
if (tokens === void 0) return await fail(check, "the build's repository tokens are no longer held, so the branch cannot be fetched");
|
|
1696
|
+
const credential = claudeCredential(runtime.hostEnv);
|
|
1697
|
+
if (credential === void 0) return await fail(check, "the host holds no Claude credential for the judge");
|
|
1698
|
+
const branch = job.branch;
|
|
1699
|
+
if (branch === void 0) return await fail(check, "the build pushed no branch: the claim names none");
|
|
1700
|
+
const remote = runtime.remoteFor?.(repo) ?? `${runtime.github.gitUrl}/${repo}.git`;
|
|
1701
|
+
try {
|
|
1702
|
+
await environment.stop();
|
|
1703
|
+
} catch (err) {
|
|
1704
|
+
return await fail(check, `the build's processes could not be stopped: ${describeCallFailure(err)}`);
|
|
1705
|
+
}
|
|
1706
|
+
const hashed = await environment.hashProgram();
|
|
1707
|
+
if (!hashed.ok) return await fail(check, `${NO_CLAUDE}: ${hashed.reason}`);
|
|
1708
|
+
if (hashed.sha256 !== expected.sha256) return await fail(check, "the judge program changed during the build");
|
|
1709
|
+
const read = await (deps.readBranch ?? readBranchHead)({ remote, dir: scratch, branch, token: readToken });
|
|
1710
|
+
if (read.kind === "absent") return await fail(check, `the build pushed no branch: ${branch} does not exist on ${repo}`);
|
|
1711
|
+
if (read.kind === "unreadable") return await fail(check, `the branch ${branch} could not be read: ${read.reason}`);
|
|
1712
|
+
const head = read.sha;
|
|
1713
|
+
let criteria;
|
|
1714
|
+
try {
|
|
1715
|
+
criteria = criteriaOf(await readWorkItem(config, job.item, deps.fetchImpl));
|
|
1716
|
+
} catch (err) {
|
|
1717
|
+
return await fail(check, `the item's criteria could not be read: ${describeCallFailure(err)}`);
|
|
1718
|
+
}
|
|
1719
|
+
if (criteria === void 0) return await fail(check, "no criteria are recorded for the item");
|
|
1720
|
+
const branchDir = join(scratch, "branch");
|
|
1721
|
+
const fetched = await (deps.fetchCommit ?? fetchCommit)({ remote, dir: branchDir, commit: head, token: readToken });
|
|
1722
|
+
if (!fetched.ok) return await fail(check, `the build's tests could not be fetched: ${fetched.reason}`);
|
|
1723
|
+
const finder = await loadCitationFinder();
|
|
1724
|
+
const citations = finder.buildCitations({ ids: criteria.map((criterion) => criterion.id), files: testFilesIn(branchDir, finder.TEST_FILE), repository: repo, sha: head });
|
|
1725
|
+
let diff = "";
|
|
1726
|
+
const startCommit = (job.container ?? job.microvm)?.startCommit;
|
|
1727
|
+
if (startCommit !== void 0) {
|
|
1728
|
+
const output = join(scratch, "diff.patch");
|
|
1729
|
+
const made = await (deps.fetchDiff ?? ((request) => diffBetween(request)))({ remote, dir: join(scratch, "diff"), base: startCommit, head, token: readToken, output });
|
|
1730
|
+
if (!made.ok) return await fail(check, `the build's diff could not be read: ${made.reason}`);
|
|
1731
|
+
const bytes = readFileSync(output);
|
|
1732
|
+
diff = bytes.length > MAX_DIFF_BYTES ? `${bytes.subarray(0, MAX_DIFF_BYTES).toString("utf8")}
|
|
1733
|
+
[the diff was cut at ${MAX_DIFF_BYTES} bytes]
|
|
1734
|
+
` : bytes.toString("utf8");
|
|
1735
|
+
}
|
|
1736
|
+
await environment.prepare({
|
|
1737
|
+
"criteria.json": `${JSON.stringify({ criteria }, null, 2)}
|
|
1738
|
+
`,
|
|
1739
|
+
"citations.json": `${JSON.stringify(citations, null, 2)}
|
|
1740
|
+
`,
|
|
1741
|
+
"diff.patch": diff
|
|
1742
|
+
}, branchDir);
|
|
1743
|
+
const limitMinutes = config.checks?.timeLimitMinutes ?? DEFAULT_CHECK_TIME_LIMIT_MINUTES;
|
|
1744
|
+
const minutes = `${limitMinutes} ${limitMinutes === 1 ? "minute" : "minutes"}`;
|
|
1745
|
+
const clear = async () => {
|
|
1746
|
+
await environment.clear?.().catch((err) => log.error(`${tag} what the judge left in its folder could not be removed: ${describeCallFailure(err)}`));
|
|
1747
|
+
};
|
|
1748
|
+
const running = environment.run(judgeCommand({ brief: check.judge.text, credential }), { logPath: join(jobDir, "judge.log") });
|
|
1749
|
+
running.catch(() => void 0);
|
|
1750
|
+
const timer = timeoutAfter(limitMinutes * 6e4, deps.sleep);
|
|
1751
|
+
let outcome;
|
|
1752
|
+
try {
|
|
1753
|
+
outcome = await Promise.race([running, timer.done]);
|
|
1754
|
+
} catch (err) {
|
|
1755
|
+
await clear();
|
|
1756
|
+
return await fail(check, `the judge could not be run: ${describeCallFailure(err)}`);
|
|
1757
|
+
} finally {
|
|
1758
|
+
timer.cancel();
|
|
1759
|
+
}
|
|
1760
|
+
if (outcome === "timeout") await environment.stop().catch((err) => log.error(`${tag} the judge could not be stopped after its time ran out: ${describeCallFailure(err)}`));
|
|
1761
|
+
const decision = decideVerdict({
|
|
1762
|
+
run: outcome === "timeout" ? { kind: "timeout" } : { kind: "exited", status: outcome.status, stdout: outcome.stdout, stderr: outcome.stderr },
|
|
1763
|
+
criteria,
|
|
1764
|
+
citations,
|
|
1765
|
+
minutes
|
|
1766
|
+
});
|
|
1767
|
+
let report;
|
|
1768
|
+
try {
|
|
1769
|
+
report = await environment.readReport();
|
|
1770
|
+
} catch (err) {
|
|
1771
|
+
log.error(`${tag} the judge's report could not be read: ${describeCallFailure(err)}`);
|
|
1772
|
+
}
|
|
1773
|
+
await clear();
|
|
1774
|
+
const link = report === void 0 ? void 0 : await uploadReport(check, report, { branch, head });
|
|
1775
|
+
await record(check.name, decision.result, decision.reason, { keyTail: decision.limitKey === true ? LIMIT_KEY_TAIL2 : "", link });
|
|
1776
|
+
}
|
|
1777
|
+
async function uploadReport(check, report, at) {
|
|
1778
|
+
const bytes = Buffer.from(report, "utf8");
|
|
1779
|
+
if (bytes.length === 0 || bytes.length > MAX_REPORT_BYTES) {
|
|
1780
|
+
log.error(`${tag} the judge's report was not uploaded: it is ${bytes.length === 0 ? "empty" : `larger than ${MAX_REPORT_BYTES} bytes`}`);
|
|
1781
|
+
return void 0;
|
|
1782
|
+
}
|
|
1783
|
+
const clientArtifactId = `host-judge-${createHash("sha256").update(`${job.claimId}:${job.startedAt}:${check.name}`).digest("hex").slice(0, 40)}`;
|
|
1784
|
+
const request = {
|
|
1785
|
+
schemaVersion: 1,
|
|
1786
|
+
artifact: {
|
|
1787
|
+
clientArtifactId,
|
|
1788
|
+
filename: "criteria-judge-report.html",
|
|
1789
|
+
mediaType: "text/html",
|
|
1790
|
+
bytes: bytes.length,
|
|
1791
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
1792
|
+
caption: REPORT_CAPTION,
|
|
1793
|
+
criterionId: check.name,
|
|
1794
|
+
emittedAt: now().toISOString(),
|
|
1795
|
+
repository: repo,
|
|
1796
|
+
gitRepo: repo,
|
|
1797
|
+
gitBranch: at.branch,
|
|
1798
|
+
commitSha: at.head
|
|
1799
|
+
},
|
|
1800
|
+
blob: { data: bytes.toString("base64") }
|
|
1801
|
+
};
|
|
1802
|
+
const signer = await signerFor(config.configPath);
|
|
1803
|
+
const sent = await retrySend(() => postJson(config.url, "/v1/artifacts", config.apiKey, config.appId, request, deps.fetchImpl, void 0, signer), { now: deps.now });
|
|
1804
|
+
if (!sent.ok) {
|
|
1805
|
+
log.error(`${tag} the judge's report could not be uploaded: ${sent.reason}`);
|
|
1806
|
+
return void 0;
|
|
1807
|
+
}
|
|
1808
|
+
const url = sent.value.data?.url;
|
|
1809
|
+
return typeof url === "string" && /^https?:\/\/\S+$/.test(url) ? url : void 0;
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1242
1813
|
// src/runner/job.ts
|
|
1243
1814
|
var LEASE_SECONDS = 900;
|
|
1244
1815
|
var RENEW_INTERVAL_SECONDS = 300;
|
|
@@ -1440,6 +2011,14 @@ function transcriptScrub(config, secrets) {
|
|
|
1440
2011
|
function stripUnsendableReasonChars(text2) {
|
|
1441
2012
|
return text2.replace(/\u0000/g, "").replace(/\p{Cs}/gu, "");
|
|
1442
2013
|
}
|
|
2014
|
+
function stripTerminalControls(text2) {
|
|
2015
|
+
return text2.replace(/\u001b\[[0-?]*[ -\/]*[@-~]/g, "").replace(/\u001b\][^\u0007\u001b\r\n]*(?:\u0007|\u001b\\)?/g, "").replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g, "");
|
|
2016
|
+
}
|
|
2017
|
+
function releaseLogFor(jobDirPath, job, config) {
|
|
2018
|
+
const home = homeFromEnv();
|
|
2019
|
+
installScrubContext(home, transcriptScrub(config, [job.itemKey, config.apiKey]));
|
|
2020
|
+
return buildReleaseLog(jobDirPath, job.endStage, (text2) => scrubText(stripTerminalControls(stripUnsendableReasonChars(text2))));
|
|
2021
|
+
}
|
|
1443
2022
|
function readReasonLine(jobDirPath, config, home = homeFromEnv()) {
|
|
1444
2023
|
let raw;
|
|
1445
2024
|
try {
|
|
@@ -1574,28 +2153,34 @@ async function renewLease(config, item, fetchImpl) {
|
|
|
1574
2153
|
}
|
|
1575
2154
|
var ALREADY_RELEASED = "already released";
|
|
1576
2155
|
async function releaseLease(config, item, outcome, detail, fetchImpl) {
|
|
1577
|
-
const send = async (environment2, sent2) => postJson(
|
|
2156
|
+
const send = async (environment2, sent2, log2) => postJson(
|
|
1578
2157
|
config.url,
|
|
1579
2158
|
`/v1/work-items/${item}/claim/release`,
|
|
1580
2159
|
config.apiKey,
|
|
1581
2160
|
config.appId,
|
|
1582
|
-
{ host: config.host, outcome: sent2, stage: detail.stage, exitCode: detail.exitCode, reason: detail.reason, environment: environment2 },
|
|
2161
|
+
{ host: config.host, outcome: sent2, stage: detail.stage, exitCode: detail.exitCode, reason: detail.reason, environment: environment2, log: log2 },
|
|
1583
2162
|
fetchImpl,
|
|
1584
2163
|
void 0,
|
|
1585
2164
|
await signerFor(config.configPath)
|
|
1586
2165
|
);
|
|
1587
2166
|
let environment = detail.environment;
|
|
1588
2167
|
let sent = outcome;
|
|
2168
|
+
let log = detail.log;
|
|
1589
2169
|
let data;
|
|
1590
2170
|
for (; ; ) {
|
|
1591
2171
|
try {
|
|
1592
|
-
({ data } = await send(environment, sent));
|
|
2172
|
+
({ data } = await send(environment, sent, log));
|
|
1593
2173
|
break;
|
|
1594
2174
|
} catch (err) {
|
|
1595
2175
|
if (!(err instanceof WorkCommandError) || err.status !== 400 || err.code !== "invalid_request_body") throw err;
|
|
1596
2176
|
const namesOutcome = /\boutcome\b/.test(err.message);
|
|
1597
2177
|
if (sent === "incomplete" && namesOutcome) {
|
|
1598
2178
|
sent = "ok";
|
|
2179
|
+
} else if (log !== void 0 && /\blog\b/.test(err.message)) {
|
|
2180
|
+
log = void 0;
|
|
2181
|
+
} else if (environment?.controlPlane !== void 0) {
|
|
2182
|
+
const { controlPlane: _omittedPlane, ...withoutPlane } = environment;
|
|
2183
|
+
environment = withoutPlane;
|
|
1599
2184
|
} else if (environment?.commitIdentity !== void 0) {
|
|
1600
2185
|
const { commitIdentity: _omitted, ...withoutIdentity } = environment;
|
|
1601
2186
|
environment = withoutIdentity;
|
|
@@ -1689,6 +2274,7 @@ async function finishJob(job, jobDirPath, outcome, config, deps, ending) {
|
|
|
1689
2274
|
const jobFilePath = join(jobDirPath, "job.json");
|
|
1690
2275
|
const logPath = join(jobDirPath, "command.log");
|
|
1691
2276
|
const tag = `#${job.item}`;
|
|
2277
|
+
deps.attemptLogs?.open(job.item, jobDirPath);
|
|
1692
2278
|
if (job.sender !== void 0) {
|
|
1693
2279
|
await stopSender({ job, deps: { spawnFn, runtime: deps.container, log } });
|
|
1694
2280
|
delete job.sender;
|
|
@@ -1706,7 +2292,7 @@ async function finishJob(job, jobDirPath, outcome, config, deps, ending) {
|
|
|
1706
2292
|
await stopProcessGroup(job.pid);
|
|
1707
2293
|
}
|
|
1708
2294
|
if (outcome !== void 0 && job.endStage === void 0) {
|
|
1709
|
-
const TEARDOWN_STAGES = ["checks", "sync", "cleanup", "report"];
|
|
2295
|
+
const TEARDOWN_STAGES = ["judge", "checks", "sync", "cleanup", "report"];
|
|
1710
2296
|
const staleTeardownStage = job.stage !== void 0 && TEARDOWN_STAGES.includes(job.stage);
|
|
1711
2297
|
job.endStage = ending?.stage ?? (staleTeardownStage ? job.exitCode !== void 0 ? "command" : void 0 : job.stage);
|
|
1712
2298
|
job.endExitCode = ending?.exitCode ?? (outcome === "ok" || outcome === "failed" ? job.exitCode : void 0);
|
|
@@ -1738,6 +2324,7 @@ async function finishJob(job, jobDirPath, outcome, config, deps, ending) {
|
|
|
1738
2324
|
}
|
|
1739
2325
|
}
|
|
1740
2326
|
};
|
|
2327
|
+
let buildVmStopLogged = false;
|
|
1741
2328
|
const endBuildMicroVm = vmBuild === void 0 ? void 0 : async () => {
|
|
1742
2329
|
if (job.workdir && job.sessionsSynced !== true) {
|
|
1743
2330
|
await uploadSessions(job.workdir);
|
|
@@ -1745,6 +2332,8 @@ async function finishJob(job, jobDirPath, outcome, config, deps, ending) {
|
|
|
1745
2332
|
writeJobFile(jobFilePath, job);
|
|
1746
2333
|
}
|
|
1747
2334
|
await stopBuildMicroVm(vmBuild.runtime, vmBuild.state, jobDirPath);
|
|
2335
|
+
log.line(`${tag} build microVM stopped`);
|
|
2336
|
+
buildVmStopLogged = true;
|
|
1748
2337
|
};
|
|
1749
2338
|
const collectSessions = build !== void 0 ? () => copyTranscriptsFromContainer(build.runtime, build.state) : vmBuild !== void 0 ? () => copyTranscriptsFromMicroVm(vmBuild.runtime, vmBuild.state) : void 0;
|
|
1750
2339
|
if (job.checkContainer !== void 0 && job.checksRan === true) {
|
|
@@ -1765,6 +2354,21 @@ async function finishJob(job, jobDirPath, outcome, config, deps, ending) {
|
|
|
1765
2354
|
log.error(`${tag} removing an earlier attempt's check environment failed: ${describeCallFailure(err)} \xB7 continuing`);
|
|
1766
2355
|
}
|
|
1767
2356
|
}
|
|
2357
|
+
if (outcome === "ok" && !job.cleanupRan && job.judgeRan !== true) {
|
|
2358
|
+
setStage(job, jobDirPath, "judge");
|
|
2359
|
+
job.judgeRan = true;
|
|
2360
|
+
writeJobFile(jobFilePath, job);
|
|
2361
|
+
try {
|
|
2362
|
+
await runJudge({
|
|
2363
|
+
config,
|
|
2364
|
+
job,
|
|
2365
|
+
jobDir: jobDirPath,
|
|
2366
|
+
deps: { fetchImpl: deps.fetchImpl, runtime: deps.container ?? createContainerRuntime(), log, now: deps.now, endBuildMicroVm, collectSessions, ...deps.hostChecks }
|
|
2367
|
+
});
|
|
2368
|
+
} catch (err) {
|
|
2369
|
+
log.error(`${tag} the host's judge stopped: ${describeCallFailure(err)} \xB7 continuing`);
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
1768
2372
|
if (outcome === "ok" && !job.cleanupRan && job.checksRan !== true) {
|
|
1769
2373
|
setStage(job, jobDirPath, "checks");
|
|
1770
2374
|
job.checksRan = true;
|
|
@@ -1819,6 +2423,7 @@ async function finishJob(job, jobDirPath, outcome, config, deps, ending) {
|
|
|
1819
2423
|
cleanedUp = false;
|
|
1820
2424
|
log.error(`${tag} cleanup failed: ${describeCallFailure(err)} \xB7 continuing`);
|
|
1821
2425
|
}
|
|
2426
|
+
if (cleanedUp && !buildVmStopLogged) log.line(`${tag} build microVM stopped`);
|
|
1822
2427
|
if (cleanedUp) log.line(`${tag} cleanup ok (${formatDuration(now().getTime() - cleanupStartedMs)})`);
|
|
1823
2428
|
} else if (build !== void 0 || !hostMade && isBuiltinContainer(config.hooks.cleanup)) {
|
|
1824
2429
|
let cleanedUp = true;
|
|
@@ -1944,6 +2549,9 @@ async function finishJob(job, jobDirPath, outcome, config, deps, ending) {
|
|
|
1944
2549
|
} else if (ending?.reason === void 0) {
|
|
1945
2550
|
job.reason = (outcome === "ok" ? void 0 : held?.reason) ?? (isolated ? fromHook ?? job.reason : fromHook);
|
|
1946
2551
|
}
|
|
2552
|
+
if (outcome === "failed" && job.reason === void 0 && job.commandEndReason !== void 0) {
|
|
2553
|
+
job.reason = scrubReason(job.commandEndReason, config);
|
|
2554
|
+
}
|
|
1947
2555
|
const cliVersion = deps.cliVersion ?? resolveVersion();
|
|
1948
2556
|
const base = job.environment ?? (job.microvm !== void 0 ? { isolation: "vm", cliVersion } : job.container === void 0 ? sharedUserEnvironment(cliVersion) : { isolation: "container", cliVersion });
|
|
1949
2557
|
if (isolated || job.build !== void 0) {
|
|
@@ -1956,7 +2564,8 @@ async function finishJob(job, jobDirPath, outcome, config, deps, ending) {
|
|
|
1956
2564
|
writeJobFile(jobFilePath, job);
|
|
1957
2565
|
}
|
|
1958
2566
|
const sentAtMs = now().getTime();
|
|
1959
|
-
const
|
|
2567
|
+
const runnerLog = releaseLogFor(jobDirPath, job, config);
|
|
2568
|
+
const detail = outcome === void 0 ? { log: runnerLog } : { stage: job.endStage, exitCode: job.endExitCode, reason: job.reason, environment: job.environment, log: runnerLog };
|
|
1960
2569
|
const release = await retrySend(() => releaseLease(config, job.item, outcome, detail, deps.fetchImpl), {
|
|
1961
2570
|
now: deps.now,
|
|
1962
2571
|
sleep: deps.sleep
|
|
@@ -1967,6 +2576,7 @@ async function finishJob(job, jobDirPath, outcome, config, deps, ending) {
|
|
|
1967
2576
|
job.endedAt = endedAt.toISOString();
|
|
1968
2577
|
job.releaseFailed = release.ok ? alreadyReleased ? ALREADY_RELEASED : void 0 : release.reason;
|
|
1969
2578
|
writeJobFile(jobFilePath, job);
|
|
2579
|
+
deps.attemptLogs?.close(job.item);
|
|
1970
2580
|
const total = formatDuration(endedAt.getTime() - new Date(job.startedAt).getTime());
|
|
1971
2581
|
if (alreadyReleased) {
|
|
1972
2582
|
log.line(
|
|
@@ -2012,7 +2622,8 @@ async function retryFailedReleases(jobsDirPath, config, deps) {
|
|
|
2012
2622
|
if (!job || job.releaseFailed === void 0 || job.releaseFailed === ALREADY_RELEASED || isUnfinished(job)) {
|
|
2013
2623
|
continue;
|
|
2014
2624
|
}
|
|
2015
|
-
const
|
|
2625
|
+
const runnerLog = releaseLogFor(dirPath, job, config);
|
|
2626
|
+
const detail = job.outcome === void 0 ? { log: runnerLog } : { stage: job.endStage, exitCode: job.endExitCode, reason: job.reason, environment: job.environment, log: runnerLog };
|
|
2016
2627
|
const release = await retrySend(() => releaseLease(config, job.item, job.outcome, detail, deps.fetchImpl), {
|
|
2017
2628
|
now: deps.now,
|
|
2018
2629
|
sleep: deps.sleep
|
|
@@ -2185,4 +2796,4 @@ async function stopSender(input) {
|
|
|
2185
2796
|
}
|
|
2186
2797
|
}
|
|
2187
2798
|
|
|
2188
|
-
export { ALREADY_RELEASED, LEASE_SECONDS, RENEW_INTERVAL_SECONDS, deliverCommitHooks, describeHookEnd, execArgv, execCommandLine, finishJob, hookLimitsFor, hostCommandEnvironment, hostEnvironment, isGroupAlive, isIdle, isTimedOut, isUnfinished, jobEnv, listAdoptableJobs, listenOnBroker, microVmCommandLine, needsRenew, outcomeFromExitCode, provisionEnvironment, provisionMicroVm, provisionProcess, readCpuUsec, readProvisionOut, recoverLostJobs, releaseLease, renewLease, resolveHostBuild, retryFailedReleases, runCommand, runHook, setStage, startHostServices, startSender, stopOrphanedMicroVms, unfinishedJobs, workdirProblem };
|
|
2799
|
+
export { ALREADY_RELEASED, AttemptLogBook, LEASE_SECONDS, RENEW_INTERVAL_SECONDS, deliverCommitHooks, describeHookEnd, execArgv, execCommandLine, finishJob, hookLimitsFor, hostCommandEnvironment, hostEnvironment, isGroupAlive, isIdle, isTimedOut, isUnfinished, jobEnv, listAdoptableJobs, listenOnBroker, microVmCommandLine, needsRenew, outcomeFromExitCode, provisionEnvironment, provisionMicroVm, provisionProcess, readCpuUsec, readProvisionOut, recoverLostJobs, releaseLease, renewLease, resolveHostBuild, retryFailedReleases, runCommand, runHook, setStage, startHostServices, startSender, stopOrphanedMicroVms, unfinishedJobs, workdirProblem };
|