@outerlayer/cli 0.16.1 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +64 -0
- package/README.md +26 -11
- package/dist/agent-setup-RBTEACYU.js +28 -0
- package/dist/artifact-link-cmd-AVM2EKTV.js +25 -0
- package/dist/{build-BG2YENGK.js → build-TQ6X5PYN.js} +5 -4
- package/dist/build-info.json +1 -1
- package/dist/{builder-2D3WSOOY.js → builder-DUI2ZH2H.js} +5 -6
- package/dist/{check-CNTMSGOG.js → check-5C4YDZYV.js} +53 -50
- package/dist/{chunk-R7PF5OOU.js → chunk-3CWUOFV6.js} +21 -14
- package/dist/{chunk-R4ZQQ7U2.js → chunk-3OFZM4X6.js} +551 -72
- package/dist/{chunk-2MNPHL23.js → chunk-47TTLEDX.js} +6 -5
- package/dist/{chunk-PLMTLGYH.js → chunk-52NC2FIU.js} +4 -3
- package/dist/chunk-6CHIYEZZ.js +564 -0
- package/dist/{chunk-TI7B7UEO.js → chunk-AMFWYESY.js} +1 -1
- package/dist/{chunk-YRYMNBGQ.js → chunk-ANPNT6PM.js} +2 -1
- package/dist/{chunk-JJEGKEFV.js → chunk-B3UKB46N.js} +6 -6
- package/dist/{chunk-JXYVA45C.js → chunk-BMYGAGBJ.js} +1050 -304
- package/dist/{chunk-4Q7OMKAT.js → chunk-BQXFBI7T.js} +81 -5
- package/dist/chunk-CRB3POP4.js +34 -0
- package/dist/{chunk-F47JBIAW.js → chunk-DSF2FWAZ.js} +2 -1
- package/dist/{chunk-N5FOE5PS.js → chunk-ECODQ7II.js} +4 -33
- package/dist/{chunk-NBGDBTO2.js → chunk-ESC6UR76.js} +6 -5
- package/dist/{chunk-HHJDN6AU.js → chunk-ESVW4MJY.js} +1 -1
- package/dist/{chunk-42ZI52JW.js → chunk-F4XSVQJ4.js} +4 -3
- package/dist/{chunk-FWX735W7.js → chunk-FUB7LRUE.js} +7 -7
- package/dist/chunk-G2YNJIGF.js +44 -0
- package/dist/{chunk-QKFB4K7X.js → chunk-GGOMT6KF.js} +1 -1
- package/dist/{chunk-CA3XMYFN.js → chunk-GOBOJMOJ.js} +6 -6
- package/dist/{chunk-N6LURUEF.js → chunk-GOSFARMS.js} +1 -1
- package/dist/chunk-JBJTC3UU.js +35 -0
- package/dist/{chunk-U3HXXRTY.js → chunk-JF42URPU.js} +16 -15
- package/dist/{chunk-ADJRGVZC.js → chunk-KDVIU23F.js} +1 -1
- package/dist/{chunk-6QRMLNH3.js → chunk-KK4VBOH6.js} +93 -37
- package/dist/{chunk-KMDLFI4P.js → chunk-L2USZT6T.js} +1 -1
- package/dist/{chunk-WTE3MQML.js → chunk-LXAY4A2G.js} +3 -12
- package/dist/{chunk-V7NIVOFD.js → chunk-M2ZWWTWZ.js} +1 -1
- package/dist/{chunk-ERI5GBNI.js → chunk-MLAAT4S6.js} +3 -3
- package/dist/{chunk-POJMEEJJ.js → chunk-MNTSH4CL.js} +1 -1
- package/dist/{chunk-WUYTWNMA.js → chunk-MQJSXHV7.js} +1 -1
- package/dist/{chunk-O765JR4W.js → chunk-MTP2QEVO.js} +1 -1
- package/dist/{chunk-ZM2IMMYN.js → chunk-N2PXGQRL.js} +9 -1
- package/dist/{chunk-J5M7BSJQ.js → chunk-N3CUDFCW.js} +9 -3
- package/dist/{chunk-4KSQKQXE.js → chunk-NACDBXUG.js} +19 -15
- package/dist/{chunk-RSPXEVDX.js → chunk-NL4XPC7W.js} +1 -1
- package/dist/{chunk-EHAAPRWY.js → chunk-QQM2UJ6T.js} +31 -26
- package/dist/{chunk-5FEK6PNE.js → chunk-RAZTWYZN.js} +10 -9
- package/dist/{chunk-4NPZVMJU.js → chunk-S26M3YBJ.js} +60 -4
- package/dist/{chunk-UGP76IZ5.js → chunk-S5PCA3SN.js} +1 -1
- package/dist/{chunk-YDSMROTB.js → chunk-S76AYB4M.js} +25 -20
- package/dist/{chunk-RMWLBUCJ.js → chunk-SADUQSO4.js} +1 -1
- package/dist/{chunk-OZGHL5Y5.js → chunk-SWCTGCSC.js} +2 -2
- package/dist/{chunk-GQD22KKN.js → chunk-T2Q7ISPE.js} +2 -1
- package/dist/{chunk-C5Y5ZJJZ.js → chunk-T4Y6SCG3.js} +5 -4
- package/dist/{chunk-6JNURGOA.js → chunk-ULSIJGLS.js} +45 -13
- package/dist/{chunk-ONRCEE6D.js → chunk-UYOYTKE2.js} +4 -4
- package/dist/{chunk-PTW6W5NR.js → chunk-VYAL7PUE.js} +1 -1
- package/dist/{chunk-JEHRUE3F.js → chunk-W5VPCONO.js} +166 -196
- package/dist/{chunk-NPQWKGUA.js → chunk-WBT5X3NH.js} +5 -4
- package/dist/{chunk-RLYZLUQ7.js → chunk-WGAUY5LE.js} +2 -2
- package/dist/{chunk-RSNSZ2LJ.js → chunk-X5H2WYMB.js} +1 -1
- package/dist/{chunk-OHVJA253.js → chunk-X5N6KC6V.js} +47 -29
- package/dist/{chunk-22RMZBGW.js → chunk-XGP3VYOG.js} +1 -1
- package/dist/{chunk-HYZRY4KZ.js → chunk-XNGMQ27R.js} +16 -6
- package/dist/{chunk-W3EKCZSQ.js → chunk-Y5GE76XC.js} +180 -53
- package/dist/{chunk-EWORLJMO.js → chunk-Y5MXET4M.js} +277 -526
- package/dist/{chunk-XCK2YAY5.js → chunk-YVZJ6GKF.js} +3 -2
- package/dist/{chunk-YO4PSLVB.js → chunk-ZRL6HKBY.js} +1 -1
- package/dist/claude-launcher-YVXI76NX.js +102 -0
- package/dist/{cli-AEWH62UE.js → cli-C77H54BV.js} +489 -174
- package/dist/{command-HHPBR6CS.js → command-H5CILO4S.js} +40 -37
- package/dist/{config-N3C6DFQA.js → config-FXDG4NPJ.js} +2 -3
- package/dist/connect-cmd-ZW5FV3ZW.js +17 -0
- package/dist/{context-adopt-VMMGFRUZ.js → context-adopt-O5OFXMKI.js} +13 -12
- package/dist/context-materialize-Y5IJ4MQT.js +23 -0
- package/dist/{dist-LKDWQWZ7.js → dist-QTX4RSCL.js} +2 -2
- package/dist/{docker-4J77Q2BF.js → docker-RC3PRX3Q.js} +7 -8
- package/dist/{doctor-N6Y7HKK3.js → doctor-2NQQML6P.js} +47 -39
- package/dist/{emit-artifact-cmd-H5MH3AOP.js → emit-artifact-cmd-7MP63KXU.js} +25 -22
- package/dist/emit-cmd-PPN4QQF2.js +14 -0
- package/dist/{emit-criteria-cmd-HBP6XS5C.js → emit-criteria-cmd-3RXPDLNK.js} +16 -14
- package/dist/{emit-finding-cmd-ZXDRL6PG.js → emit-finding-cmd-OL7JB6H3.js} +18 -16
- package/dist/{emit-result-cmd-RFKALS5Z.js → emit-result-cmd-UZVMOCF4.js} +20 -17
- package/dist/{exec-client-CQV746UF.js → exec-client-7UMESIE2.js} +3 -4
- package/dist/governing-source-BUU4IABS.js +35 -0
- package/dist/guest-init-KPBQOFE5.js +9 -0
- package/dist/hidden-prompt-UXBFMQC5.js +68 -0
- package/dist/{hook-fast-D7HV4IXG.js → hook-fast-W5MKHMJT.js} +4 -1
- package/dist/host-key-U4B5QETR.js +8 -0
- package/dist/{import-capture-cmd-EQ35EARA.js → import-capture-cmd-ZO6KGHI2.js} +7 -5
- package/dist/{import-ruler-cmd-BGAZXCQM.js → import-ruler-cmd-KJMWL5V6.js} +5 -3
- package/dist/index.js +8 -4
- package/dist/init-VWXIZEAN.js +54 -0
- package/dist/{init-FBCA4DRA.js → init-WNXXYMO7.js} +80 -53
- package/dist/{init-cmd-CAG572UE.js → init-cmd-WS6TIRYT.js} +28 -24
- package/dist/{init-update-VDFF4K3B.js → init-update-E3J6TVIN.js} +27 -26
- package/dist/{install-HDBIPHTX.js → install-CBWIEF3R.js} +4 -5
- package/dist/{install-cmd-5HYMXLEW.js → install-cmd-QDOTR4OM.js} +5 -4
- package/dist/{login-browser-QZCS3WW6.js → login-browser-O63QZN7D.js} +4 -3
- 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-UGI2BJOT.js +76 -0
- package/dist/machine-JOW4RNZ7.js +49 -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-DOTY5FKY.js} +10 -8
- 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-4NB2GA7O.js} +5 -6
- package/dist/runner-key-TC3NFWFV.js +177 -0
- package/dist/{session-sender-24L5EW25.js → session-sender-NNVPSPY7.js} +17 -14
- package/dist/spec-cmd-OWVTBB4J.js +116 -0
- package/dist/{starter-pack-KF66HGZR.js → starter-pack-OPWUDOWX.js} +7 -5
- package/dist/{status-XHSGEYO5.js → status-TKCLLKFG.js} +42 -39
- package/dist/{statusline-fast-SVTMBMD7.js → statusline-fast-KN3BX67H.js} +2 -1
- package/dist/{sync-cmd-5G5EIEHD.js → sync-cmd-Q6VYNPWG.js} +16 -13
- package/dist/{watch-UTOXO4SE.js → watch-ARAGADTF.js} +16 -13
- package/dist/{work-claim-cmd-RKHYFE5Y.js → work-claim-cmd-34FERLYX.js} +25 -27
- package/dist/work-cmd-7S5B2H2P.js +25 -0
- package/dist/{work-comment-cmd-A55CUSYE.js → work-comment-cmd-SZJSPM5S.js} +15 -13
- package/dist/{work-launch-R5HTAP6X.js → work-launch-LEQ2MPM3.js} +3 -1
- package/dist/{work-open-pr-cmd-E6O53IJD.js → work-open-pr-cmd-RPXKCBWS.js} +17 -15
- package/dist/work-pr-cmd-5LGANL32.js +26 -0
- package/package.json +3 -2
- package/skill-pack/maintained/outerlayer-amend/SKILL.md +6 -2
- package/skill-pack/maintained/outerlayer-cli/SKILL.md +2 -0
- package/skill-pack/maintained/outerlayer-evidence/SKILL.md +4 -2
- package/skill-pack/maintained/outerlayer-findings/SKILL.md +2 -0
- package/skill-pack/shipped.json +27 -5
- package/skill-pack/template/build/SKILL.md +25 -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 +27 -34
- package/skill-pack/template/build/scripts/criterion-citations.mjs +8 -3
- package/skill-pack/template/build/scripts/record-criteria.mjs +8 -2
- package/skill-pack/template/spec/SKILL.md +37 -10
- package/skill-pack/validators/acceptance-criteria.yaml +25 -25
- package/skill-pack/validators/code-review.yaml +6 -5
- package/dist/agent-setup-K3ABU5NL.js +0 -27
- package/dist/chunk-BYT5O5RV.js +0 -61
- package/dist/connect-cmd-JUAW6MU4.js +0 -16
- package/dist/context-materialize-2WXTS4CW.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-MYX7YMIH.js +0 -51
- package/dist/loop-75J4YXLZ.js +0 -73
- package/dist/machine-A6ZCTUGS.js +0 -46
- package/dist/paths-OYKMVYJP.js +0 -6
- package/dist/pending-MXLSRYMO.js +0 -7
- package/dist/runner-key-W25ISHI3.js +0 -155
- package/dist/work-cmd-DJQJEZVB.js +0 -23
- package/dist/work-pr-cmd-L77K32WK.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 {
|
|
5
|
-
import {
|
|
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-UYOYTKE2.js';
|
|
5
|
+
import { DOCKERD_LOG, HELPER_PATH, HELPER_RECORD } from './chunk-XNGMQ27R.js';
|
|
6
|
+
import { installScrubContext } from './chunk-JF42URPU.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-Y5MXET4M.js';
|
|
8
|
+
import { childEnvironment } from './chunk-L2USZT6T.js';
|
|
9
|
+
import { HASH_ARGV, parseProgramHash, JUDGE_PROGRAM, judgeCommand, MAX_REPORT_BYTES, JUDGE_MAX_TURNS, JUDGE_DIR } from './chunk-S26M3YBJ.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, createRunnerLog, formatDuration, retrySend, formatClock } from './chunk-6CHIYEZZ.js';
|
|
13
|
+
import { WorkCommandError, postJson, isSignatureRefusal, getJson } from './chunk-KK4VBOH6.js';
|
|
14
|
+
import { planArtifactUpload } from './chunk-MQJSXHV7.js';
|
|
15
|
+
import { isBuiltinContainer, isBuiltinProcess, isBuiltinVm, DEFAULT_CHECK_TIME_LIMIT_MINUTES } from './chunk-XGP3VYOG.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-BMYGAGBJ.js';
|
|
21
|
+
import { resolvePackRoot } from './chunk-MNTSH4CL.js';
|
|
22
|
+
import { parsePolicy } from './chunk-X5N6KC6V.js';
|
|
22
23
|
import { readCloudConfig } from './chunk-EABW6AJQ.js';
|
|
23
24
|
import { resolveVersion } from './chunk-RA3O54FC.js';
|
|
24
|
-
import { signerFor } from './chunk-
|
|
25
|
-
import { runnerDir, buildDir, vsockPath, RUNNER_FILE_MODE } from './chunk-
|
|
25
|
+
import { signerFor } from './chunk-M2ZWWTWZ.js';
|
|
26
|
+
import { runnerDir, buildDir, vsockPath, RUNNER_FILE_MODE } from './chunk-N2PXGQRL.js';
|
|
27
|
+
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';
|
|
26
28
|
import { homeFromEnv } from './chunk-VNVZDWO3.js';
|
|
27
29
|
import { spawn, spawnSync } from 'child_process';
|
|
28
30
|
import { randomBytes, createHash } from 'crypto';
|
|
29
31
|
import { mkdirSync, chmodSync, writeFileSync, appendFileSync, statSync, accessSync, constants, openSync, existsSync, readFileSync, closeSync, rmSync, readdirSync, lstatSync, 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;
|
|
@@ -877,9 +991,8 @@ async function settleVerdict(deps) {
|
|
|
877
991
|
}
|
|
878
992
|
var ENDED = "ended before provision finished";
|
|
879
993
|
async function provisionProcess(input) {
|
|
880
|
-
const { config, runtime, item, jobDir } = input;
|
|
994
|
+
const { config, runtime, item, repository, jobDir } = input;
|
|
881
995
|
if (claudeCredential(runtime.hostEnv) === void 0) return { ok: false, reason: AGENT_CREDENTIAL_MISSING };
|
|
882
|
-
const repository = item.specRepository;
|
|
883
996
|
if (repository === void 0 || repository === null || repository === "") {
|
|
884
997
|
return { ok: false, reason: "the work item names no repository to clone" };
|
|
885
998
|
}
|
|
@@ -921,6 +1034,9 @@ var MAX_VALIDATOR_FILES = 20;
|
|
|
921
1034
|
var VALIDATORS_DIR = ".outerlayer/validators";
|
|
922
1035
|
var POLICY_FILE = ".outerlayer/policy.yaml";
|
|
923
1036
|
var LIMIT_KEY_TAIL = " (runner.checks.timeLimitMinutes)";
|
|
1037
|
+
function isJudgeCheck(check) {
|
|
1038
|
+
return "judge" in check;
|
|
1039
|
+
}
|
|
924
1040
|
function lastLine(text2) {
|
|
925
1041
|
return text2.split(/\r\n|\r|\n/).map((line) => line.trim()).filter((line) => line !== "").at(-1);
|
|
926
1042
|
}
|
|
@@ -956,17 +1072,24 @@ function checksIn(root, log, tag) {
|
|
|
956
1072
|
const policy = readInside(root, POLICY_FILE);
|
|
957
1073
|
const loaded = parsePolicy(policy === void 0 ? null : { path: POLICY_FILE, content: policy }, validatorFilesIn(root));
|
|
958
1074
|
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
|
-
|
|
1075
|
+
const hostChecks = loaded.customs.filter((def) => def.runWhere === "host" && def.level !== "off" && def.declaresEmit !== null);
|
|
1076
|
+
const checks = [];
|
|
1077
|
+
for (const def of hostChecks) {
|
|
1078
|
+
if (def.judge !== void 0) checks.push({ name: def.declaresEmit, judge: { brief: def.judge.brief, text: readInside(root, def.judge.brief) } });
|
|
1079
|
+
else if (def.command !== void 0) checks.push({ name: def.declaresEmit, command: def.command });
|
|
1080
|
+
}
|
|
1081
|
+
return checks;
|
|
961
1082
|
}
|
|
962
1083
|
async function readDefinitions(input, repository, scratch, token, tokens) {
|
|
963
1084
|
const { config, job, deps } = input;
|
|
964
1085
|
const tag = `#${job.item}`;
|
|
965
1086
|
const stage = deps.stage ?? stageGovernedContext;
|
|
966
1087
|
const controlPlaneUrl = (controlPlane) => deps.runtime.remoteFor?.(controlPlane) ?? `${deps.runtime.github.gitUrl}/${controlPlane}.git`;
|
|
1088
|
+
const governing = (job.container ?? job.microvm)?.governing;
|
|
967
1089
|
const staged = await stage({
|
|
968
1090
|
config,
|
|
969
1091
|
repository,
|
|
1092
|
+
...governing === void 0 ? {} : { source: { repository: governing.repository, ref: governing.ref, sha: governing.commit } },
|
|
970
1093
|
buildId: `checks-${randomBytes(4).toString("hex")}`,
|
|
971
1094
|
buildDir: scratch,
|
|
972
1095
|
fetchImpl: deps.fetchImpl,
|
|
@@ -1004,6 +1127,33 @@ function noCleanEnvironment(job) {
|
|
|
1004
1127
|
const ran = job.build?.workDir !== void 0 ? "builtin:process" : "a provision hook of the host's own";
|
|
1005
1128
|
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
1129
|
}
|
|
1130
|
+
function makeRecorder(input) {
|
|
1131
|
+
const { config, job, deps } = input;
|
|
1132
|
+
const tag = `#${job.item}`;
|
|
1133
|
+
const now = deps.now ?? (() => /* @__PURE__ */ new Date());
|
|
1134
|
+
let signer;
|
|
1135
|
+
return async (name, result, sentence, opts = {}) => {
|
|
1136
|
+
signer ??= await signerFor(config.configPath);
|
|
1137
|
+
const keyTail = opts.keyTail ?? "";
|
|
1138
|
+
const body = sentence === void 0 ? void 0 : cutReasonLine(`${scrubReason(sentence, config) ?? "check failed"}${keyTail}`);
|
|
1139
|
+
const clientEmitId = `host-${createHash("sha256").update(`${job.claimId}:${job.startedAt}:${name}${opts.idTag === void 0 ? "" : `:${opts.idTag}`}`).digest("hex").slice(0, 40)}`;
|
|
1140
|
+
const sent = await retrySend(
|
|
1141
|
+
() => postJson(
|
|
1142
|
+
config.url,
|
|
1143
|
+
"/v1/emitted-results",
|
|
1144
|
+
config.apiKey,
|
|
1145
|
+
config.appId,
|
|
1146
|
+
{ 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) } } },
|
|
1147
|
+
deps.fetchImpl,
|
|
1148
|
+
void 0,
|
|
1149
|
+
signer
|
|
1150
|
+
),
|
|
1151
|
+
{ now: deps.now }
|
|
1152
|
+
);
|
|
1153
|
+
if (sent.ok) deps.log.line(`${tag} check ${name} ${result}${body === void 0 ? "" : `: ${body}`}`);
|
|
1154
|
+
else deps.log.error(`${tag} check ${name} ${result} could not be recorded: ${sent.reason}`);
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1007
1157
|
async function runHostChecks(input) {
|
|
1008
1158
|
const { config, job, jobDir, deps } = input;
|
|
1009
1159
|
const { runtime, log } = deps;
|
|
@@ -1035,34 +1185,15 @@ async function runHostChecks(input) {
|
|
|
1035
1185
|
rmSync(scratch, { recursive: true, force: true });
|
|
1036
1186
|
return;
|
|
1037
1187
|
}
|
|
1038
|
-
const checks = definitions.checks;
|
|
1188
|
+
const checks = definitions.checks.filter((check) => !isJudgeCheck(check));
|
|
1039
1189
|
if (checks.length === 0) {
|
|
1040
1190
|
rmSync(scratch, { recursive: true, force: true });
|
|
1041
1191
|
return;
|
|
1042
1192
|
}
|
|
1043
1193
|
log.line(`${tag} checks started (${checks.map((check) => check.name).join(", ")})`);
|
|
1044
1194
|
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
|
-
};
|
|
1195
|
+
const recorder = makeRecorder({ config, job, deps });
|
|
1196
|
+
const record = (check, result, sentence, keyTail = "") => recorder(check.name, result, sentence, { keyTail });
|
|
1066
1197
|
const failAll = async (reason, keyTail = "") => {
|
|
1067
1198
|
log.error(`${tag} checks failed: ${reason}${keyTail}`);
|
|
1068
1199
|
for (const check of checks) await record(check, "fail", reason, keyTail);
|
|
@@ -1070,12 +1201,36 @@ async function runHostChecks(input) {
|
|
|
1070
1201
|
try {
|
|
1071
1202
|
if (job.container === void 0 && job.microvm === void 0) return await failAll(noCleanEnvironment(job));
|
|
1072
1203
|
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
|
-
|
|
1204
|
+
if (job.branch === void 0) return await failAll("the build pushed no branch: the claim names none");
|
|
1205
|
+
const claimed = repositoriesOf({ repositories: job.repositories, repository, branch: job.branch });
|
|
1206
|
+
if (claimed.length === 0) return await failAll("the claim names no repository to check out");
|
|
1207
|
+
const remoteOf = (name) => runtime.remoteFor?.(name) ?? `${runtime.github.gitUrl}/${name}.git`;
|
|
1208
|
+
const remote = remoteOf(repository);
|
|
1209
|
+
const pushed = /* @__PURE__ */ new Map();
|
|
1210
|
+
const unpushed = [];
|
|
1211
|
+
let primaryHead;
|
|
1212
|
+
for (const entry of claimed) {
|
|
1213
|
+
let entryToken = readToken;
|
|
1214
|
+
if (!entry.primary) {
|
|
1215
|
+
try {
|
|
1216
|
+
entryToken = await tokens.token("read", entry.repository);
|
|
1217
|
+
} catch (err) {
|
|
1218
|
+
return await failAll(`no read token for ${entry.repository}: ${describeCallFailure(err)}`);
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
const read = await (deps.readBranch ?? readBranchHead)({ remote: remoteOf(entry.repository), dir: scratch, branch: entry.branch, token: entryToken });
|
|
1222
|
+
if (read.kind === "unreadable") return await failAll(`the branch ${entry.branch} could not be read${entry.primary ? "" : ` on ${entry.repository}`}: ${read.reason}`);
|
|
1223
|
+
if (read.kind === "absent") {
|
|
1224
|
+
unpushed.push(entry);
|
|
1225
|
+
continue;
|
|
1226
|
+
}
|
|
1227
|
+
pushed.set(entry.repository, entry.branch);
|
|
1228
|
+
if (entry.primary) primaryHead = read.sha;
|
|
1229
|
+
}
|
|
1230
|
+
if (pushed.size === 0) {
|
|
1231
|
+
return await failAll(`the build pushed no branch: ${unpushed.map((entry) => `${entry.branch} does not exist on ${entry.repository}`).join("; ")}`);
|
|
1232
|
+
}
|
|
1233
|
+
const repositoriesToCheckOut = claimed;
|
|
1079
1234
|
const limitMinutes = config.checks?.timeLimitMinutes ?? DEFAULT_CHECK_TIME_LIMIT_MINUTES;
|
|
1080
1235
|
const minutes = `${limitMinutes} ${limitMinutes === 1 ? "minute" : "minutes"}`;
|
|
1081
1236
|
const environmentLog = join(jobDir, "checks", "environment.log");
|
|
@@ -1130,20 +1285,33 @@ async function runHostChecks(input) {
|
|
|
1130
1285
|
workdir: "/build/work",
|
|
1131
1286
|
failed: (status) => `configuring git failed (exit ${status})`
|
|
1132
1287
|
},
|
|
1133
|
-
{
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1288
|
+
...repositoriesToCheckOut.flatMap((entry) => {
|
|
1289
|
+
const branch = pushed.get(entry.repository);
|
|
1290
|
+
const path = entry.primary ? CHECKOUT_DIR : repositoryPath(entry.repository);
|
|
1291
|
+
const clone = {
|
|
1292
|
+
argv: ["git", "clone", "--filter=blob:none", "--single-branch", ...branch === void 0 ? [] : ["--branch", branch], `https://github.com/${entry.repository}.git`, path],
|
|
1293
|
+
workdir: "/build/work",
|
|
1294
|
+
failed: (status) => `the clone of ${branch ?? `the default branch of ${entry.repository}`} failed (exit ${status})`
|
|
1295
|
+
};
|
|
1296
|
+
return entry.primary && repositoriesToCheckOut.length > 1 ? [
|
|
1297
|
+
clone,
|
|
1298
|
+
{
|
|
1299
|
+
argv: ["sh", "-c", 'mkdir -p "$(dirname "$1")" && ln -s "$2" "$1"', "link", repositoryPath(entry.repository), CHECKOUT_DIR],
|
|
1300
|
+
workdir: "/build/work",
|
|
1301
|
+
failed: (status) => `linking ${entry.repository} at ${repositoryPath(entry.repository)} failed (exit ${status})`
|
|
1302
|
+
}
|
|
1303
|
+
] : [clone];
|
|
1304
|
+
})
|
|
1138
1305
|
];
|
|
1139
1306
|
for (const step of steps) {
|
|
1140
1307
|
if (abandoned) return "the check environment setup ran out of time";
|
|
1141
1308
|
const ran = await made.exec(step.argv, step.workdir, { logPath: environmentLog });
|
|
1142
1309
|
if (ran.status !== 0) return `the check environment could not be prepared: ${step.failed(ran.status)}`;
|
|
1143
1310
|
}
|
|
1311
|
+
const lifecycleDir = lifecycleWorkdir(repositoriesToCheckOut, (job.container ?? job.microvm)?.governing?.repository);
|
|
1144
1312
|
for (const step of lifecycleSteps((job.container ?? job.microvm)?.lifecycle ?? {})) {
|
|
1145
1313
|
log.line(`${tag} checks lifecycle ${step.name} (${step.source})`);
|
|
1146
|
-
const ran = await made.exec(step.argv,
|
|
1314
|
+
const ran = await made.exec(step.argv, lifecycleDir, { logPath: environmentLog });
|
|
1147
1315
|
if (ran.status !== 0) return `the check environment could not be prepared: ${lifecycleFailureReason(step, ran.status)}`;
|
|
1148
1316
|
}
|
|
1149
1317
|
return made;
|
|
@@ -1172,7 +1340,7 @@ async function runHostChecks(input) {
|
|
|
1172
1340
|
scratch,
|
|
1173
1341
|
remote,
|
|
1174
1342
|
readToken,
|
|
1175
|
-
branchHead:
|
|
1343
|
+
branchHead: primaryHead ?? (job.container ?? job.microvm)?.startCommit ?? "",
|
|
1176
1344
|
transcripts,
|
|
1177
1345
|
scrub: transcriptScrub(config, [config.apiKey, job.itemKey, readToken]),
|
|
1178
1346
|
readWorkItem: () => readWorkItem(config, job.item, deps.fetchImpl),
|
|
@@ -1239,6 +1407,296 @@ async function runHostChecks(input) {
|
|
|
1239
1407
|
}
|
|
1240
1408
|
}
|
|
1241
1409
|
|
|
1410
|
+
// src/runner/judge-verdict.ts
|
|
1411
|
+
function mechanicalFailures(criteria, citations) {
|
|
1412
|
+
return criteria.filter((criterion) => criterion.proof === null && (citations[criterion.id]?.length ?? 0) === 0).map((criterion) => criterion.id);
|
|
1413
|
+
}
|
|
1414
|
+
function firstLine2(text2) {
|
|
1415
|
+
return text2.trim().split(/\r\n|\r|\n/)[0]?.trim() ?? "";
|
|
1416
|
+
}
|
|
1417
|
+
function parseOutput(stdout) {
|
|
1418
|
+
try {
|
|
1419
|
+
const parsed = JSON.parse(stdout);
|
|
1420
|
+
if (typeof parsed !== "object" || parsed === null) return void 0;
|
|
1421
|
+
const { subtype, result } = parsed;
|
|
1422
|
+
return { subtype: typeof subtype === "string" ? subtype : void 0, result: typeof result === "string" ? result : "" };
|
|
1423
|
+
} catch {
|
|
1424
|
+
return void 0;
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
var RESULT_LINE = /^result:\s*(pass|fail)\s*$/i;
|
|
1428
|
+
var UNMET_LINE = /^unmet:\s*(\S+)\s*$/i;
|
|
1429
|
+
function decideVerdict(input) {
|
|
1430
|
+
const { run, criteria } = input;
|
|
1431
|
+
if (run.kind === "timeout") return { result: "fail", reason: `the judge ran out of time after ${input.minutes}`, limitKey: true };
|
|
1432
|
+
const output = parseOutput(run.stdout);
|
|
1433
|
+
if (output === void 0) {
|
|
1434
|
+
const said = firstLine2(run.stderr);
|
|
1435
|
+
return { result: "fail", reason: `the judge exited ${run.status} and printed no result${said === "" ? "" : `: ${said}`}` };
|
|
1436
|
+
}
|
|
1437
|
+
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` };
|
|
1438
|
+
const lines = output.result.split(/\r\n|\r|\n/).map((line) => line.trim()).filter((line) => line !== "");
|
|
1439
|
+
const verdict = RESULT_LINE.exec(lines.at(-1) ?? "")?.[1]?.toLowerCase();
|
|
1440
|
+
if (verdict === void 0) return { result: "fail", reason: "the judge gave no `result:` line" };
|
|
1441
|
+
const named = new Set(lines.flatMap((line) => UNMET_LINE.exec(line)?.[1] ?? []));
|
|
1442
|
+
const mechanical = new Set(mechanicalFailures(criteria, input.citations));
|
|
1443
|
+
const unmet = criteria.map((criterion) => criterion.id).filter((id) => named.has(id) || mechanical.has(id));
|
|
1444
|
+
if (verdict === "pass" && unmet.length === 0) return { result: "pass" };
|
|
1445
|
+
const ruled = unmet.filter((id) => mechanical.has(id));
|
|
1446
|
+
const rule = ruled.length === 0 ? "" : `: the mechanical rule found no proof annotation and no citing test for ${ruled.join(", ")}`;
|
|
1447
|
+
if (unmet.length === 0) return { result: "fail", reason: "the judge failed the change and named no criterion" };
|
|
1448
|
+
return { result: "fail", reason: `${unmet.length} of ${criteria.length} criteria not satisfied, first ${unmet[0]}${rule}` };
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
// src/runner/judge.ts
|
|
1452
|
+
var LIMIT_KEY_TAIL2 = " (runner.checks.timeLimitMinutes)";
|
|
1453
|
+
var MAX_DIFF_BYTES = 4 * 1024 * 1024;
|
|
1454
|
+
var MAX_TEST_FILES = 2e4;
|
|
1455
|
+
var MAX_TEST_FILE_BYTES = 1024 * 1024;
|
|
1456
|
+
var NO_CLAUDE = "the image has no Claude Code for the judge";
|
|
1457
|
+
var REPORT_CAPTION = "The host's criteria judge, each criterion judged against the test that cites it";
|
|
1458
|
+
async function loadCitationFinder() {
|
|
1459
|
+
return await import(pathToFileURL(join(resolvePackRoot(), "template", "build", "scripts", "criterion-citations.mjs")).href);
|
|
1460
|
+
}
|
|
1461
|
+
function testFilesIn(root, isTest, limits = { files: MAX_TEST_FILES, bytes: MAX_TEST_FILE_BYTES }) {
|
|
1462
|
+
const files = [];
|
|
1463
|
+
const walk = (dir, prefix) => {
|
|
1464
|
+
for (const name of readdirSync(dir).sort()) {
|
|
1465
|
+
if (files.length >= limits.files) return;
|
|
1466
|
+
if (prefix === "" && name === ".git") continue;
|
|
1467
|
+
const path = join(dir, name);
|
|
1468
|
+
const rel = prefix === "" ? name : `${prefix}/${name}`;
|
|
1469
|
+
const stat = lstatSync(path);
|
|
1470
|
+
if (stat.isDirectory()) walk(path, rel);
|
|
1471
|
+
else if (stat.isFile() && isTest.test(rel) && stat.size <= limits.bytes) files.push({ path: rel, contents: readFileSync(path, "utf8") });
|
|
1472
|
+
}
|
|
1473
|
+
};
|
|
1474
|
+
walk(root, "");
|
|
1475
|
+
return files;
|
|
1476
|
+
}
|
|
1477
|
+
async function hostPassedNames(config, item, fetchImpl) {
|
|
1478
|
+
const workItemId = Object(await readWorkItem(config, item, fetchImpl)).workItemId;
|
|
1479
|
+
if (typeof workItemId !== "string") throw new WorkCommandError("the work item read named no id");
|
|
1480
|
+
const signer = await signerFor(config.configPath);
|
|
1481
|
+
const latest = /* @__PURE__ */ new Map();
|
|
1482
|
+
let since = null;
|
|
1483
|
+
do {
|
|
1484
|
+
const query = since === null ? "" : `?since=${encodeURIComponent(since)}`;
|
|
1485
|
+
const page = await getJson(config.url, `/v1/work-items/${encodeURIComponent(workItemId)}/changes${query}`, config.apiKey, config.appId, fetchImpl, signer);
|
|
1486
|
+
for (const entry of Array.isArray(page.data) ? page.data : []) {
|
|
1487
|
+
const { name, result, source } = Object(entry);
|
|
1488
|
+
if (source === "host" && typeof name === "string" && typeof result === "string") latest.set(name, result);
|
|
1489
|
+
}
|
|
1490
|
+
since = page.pagination?.cursor ?? null;
|
|
1491
|
+
} while (since !== null);
|
|
1492
|
+
return [...latest].filter(([, result]) => result === "pass").map(([name]) => name);
|
|
1493
|
+
}
|
|
1494
|
+
function criteriaOf(workItem) {
|
|
1495
|
+
const criteria = Object(Object(workItem).criteria);
|
|
1496
|
+
if (!Array.isArray(criteria?.items)) return void 0;
|
|
1497
|
+
const drafted = criteria.drafted === true;
|
|
1498
|
+
const items = [];
|
|
1499
|
+
for (const entry of criteria.items) {
|
|
1500
|
+
const { id, text: text2, proof } = Object(entry);
|
|
1501
|
+
if (typeof id !== "string" || typeof text2 !== "string") continue;
|
|
1502
|
+
items.push({ id, text: text2, proof: typeof proof === "string" ? proof : null, drafted });
|
|
1503
|
+
}
|
|
1504
|
+
return items.length === 0 ? void 0 : items;
|
|
1505
|
+
}
|
|
1506
|
+
function environmentFor(input) {
|
|
1507
|
+
const { config, job, jobDir, deps } = input;
|
|
1508
|
+
if (deps.judgeEnvironment !== void 0) return deps.judgeEnvironment;
|
|
1509
|
+
if (job.container !== void 0) return containerJudgeEnvironment({ runtime: deps.runtime, config, job, jobDir });
|
|
1510
|
+
if (job.microvm !== void 0) return microVmJudgeEnvironment({ runtime: deps.runtime, job, jobDir });
|
|
1511
|
+
return void 0;
|
|
1512
|
+
}
|
|
1513
|
+
async function runJudge(input) {
|
|
1514
|
+
const { config, job, jobDir, deps } = input;
|
|
1515
|
+
const { runtime, log } = deps;
|
|
1516
|
+
const tag = `#${job.item}`;
|
|
1517
|
+
const now = deps.now ?? (() => /* @__PURE__ */ new Date());
|
|
1518
|
+
const repository = job.repository ?? job.container?.repository;
|
|
1519
|
+
if (repository === void 0) return;
|
|
1520
|
+
const repo = repository;
|
|
1521
|
+
const tokens = runtime.credentials.get(jobDir);
|
|
1522
|
+
let readToken;
|
|
1523
|
+
try {
|
|
1524
|
+
readToken = await tokens?.token("read");
|
|
1525
|
+
} catch (err) {
|
|
1526
|
+
log.error(`${tag} no read token for the judge: ${describeCallFailure(err)}`);
|
|
1527
|
+
}
|
|
1528
|
+
const scratch = join(jobDir, "judge-scratch");
|
|
1529
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
1530
|
+
const definitionsDir = join(scratch, "definitions");
|
|
1531
|
+
mkdirSync(definitionsDir, { recursive: true });
|
|
1532
|
+
let definitions;
|
|
1533
|
+
try {
|
|
1534
|
+
definitions = await readDefinitions(input, repository, definitionsDir, readToken, tokens);
|
|
1535
|
+
} catch (err) {
|
|
1536
|
+
definitions = { ok: false, reason: describeCallFailure(err) };
|
|
1537
|
+
} finally {
|
|
1538
|
+
removeStagedContext(definitionsDir);
|
|
1539
|
+
}
|
|
1540
|
+
if (!definitions.ok) {
|
|
1541
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
1542
|
+
log.line(`${tag} judge not run: the definitions could not be read: ${definitions.reason}`);
|
|
1543
|
+
const passed = await retrySend(() => hostPassedNames(config, job.item, deps.fetchImpl), { now: deps.now });
|
|
1544
|
+
if (!passed.ok) {
|
|
1545
|
+
log.error(`${tag} the item's host results could not be read, so none was failed: ${passed.reason}`);
|
|
1546
|
+
return;
|
|
1547
|
+
}
|
|
1548
|
+
const record2 = makeRecorder({ config, job, deps });
|
|
1549
|
+
for (const name of passed.value) await record2(name, "fail", `the definitions could not be read: ${definitions.reason}`, { idTag: "judge-unread" });
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
const checks = definitions.checks.filter(isJudgeCheck);
|
|
1553
|
+
if (checks.length === 0) {
|
|
1554
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1557
|
+
log.line(`${tag} judge started (${checks.map((check) => check.name).join(", ")})`);
|
|
1558
|
+
const startedMs = now().getTime();
|
|
1559
|
+
const record = makeRecorder({ config, job, deps });
|
|
1560
|
+
const fail = (check, reason) => {
|
|
1561
|
+
log.error(`${tag} judge failed: ${reason}`);
|
|
1562
|
+
return record(check.name, "fail", reason);
|
|
1563
|
+
};
|
|
1564
|
+
try {
|
|
1565
|
+
for (const check of checks) {
|
|
1566
|
+
try {
|
|
1567
|
+
await judgeOne(check);
|
|
1568
|
+
} catch (err) {
|
|
1569
|
+
await fail(check, `the judge could not be run: ${describeCallFailure(err)}`);
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
} finally {
|
|
1573
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
1574
|
+
log.line(`${tag} judge done (${formatDuration(now().getTime() - startedMs)})`);
|
|
1575
|
+
}
|
|
1576
|
+
async function judgeOne(check) {
|
|
1577
|
+
const environment = environmentFor(input);
|
|
1578
|
+
if (environment === void 0) return await fail(check, `${NO_CLAUDE}: this build ran outside a container or microVM`);
|
|
1579
|
+
const expected = (job.container ?? job.microvm)?.judgeProgram;
|
|
1580
|
+
if (expected === void 0) return await fail(check, NO_CLAUDE);
|
|
1581
|
+
if (check.judge.text === void 0) return await fail(check, `the brief ${check.judge.brief} is not in the definitions repository`);
|
|
1582
|
+
if (tokens === void 0) return await fail(check, "the build's repository tokens are no longer held, so the branch cannot be fetched");
|
|
1583
|
+
const credential = claudeCredential(runtime.hostEnv);
|
|
1584
|
+
if (credential === void 0) return await fail(check, "the host holds no Claude credential for the judge");
|
|
1585
|
+
const branch = job.branch;
|
|
1586
|
+
if (branch === void 0) return await fail(check, "the build pushed no branch: the claim names none");
|
|
1587
|
+
const remote = runtime.remoteFor?.(repo) ?? `${runtime.github.gitUrl}/${repo}.git`;
|
|
1588
|
+
try {
|
|
1589
|
+
await environment.stop();
|
|
1590
|
+
} catch (err) {
|
|
1591
|
+
return await fail(check, `the build's processes could not be stopped: ${describeCallFailure(err)}`);
|
|
1592
|
+
}
|
|
1593
|
+
const hashed = await environment.hashProgram();
|
|
1594
|
+
if (!hashed.ok) return await fail(check, `${NO_CLAUDE}: ${hashed.reason}`);
|
|
1595
|
+
if (hashed.sha256 !== expected.sha256) return await fail(check, "the judge program changed during the build");
|
|
1596
|
+
const read = await (deps.readBranch ?? readBranchHead)({ remote, dir: scratch, branch, token: readToken });
|
|
1597
|
+
if (read.kind === "absent") return await fail(check, `the build pushed no branch: ${branch} does not exist on ${repo}`);
|
|
1598
|
+
if (read.kind === "unreadable") return await fail(check, `the branch ${branch} could not be read: ${read.reason}`);
|
|
1599
|
+
const head = read.sha;
|
|
1600
|
+
let criteria;
|
|
1601
|
+
try {
|
|
1602
|
+
criteria = criteriaOf(await readWorkItem(config, job.item, deps.fetchImpl));
|
|
1603
|
+
} catch (err) {
|
|
1604
|
+
return await fail(check, `the item's criteria could not be read: ${describeCallFailure(err)}`);
|
|
1605
|
+
}
|
|
1606
|
+
if (criteria === void 0) return await fail(check, "no criteria are recorded for the item");
|
|
1607
|
+
const branchDir = join(scratch, "branch");
|
|
1608
|
+
const fetched = await (deps.fetchCommit ?? fetchCommit)({ remote, dir: branchDir, commit: head, token: readToken });
|
|
1609
|
+
if (!fetched.ok) return await fail(check, `the build's tests could not be fetched: ${fetched.reason}`);
|
|
1610
|
+
const finder = await loadCitationFinder();
|
|
1611
|
+
const citations = finder.buildCitations({ ids: criteria.map((criterion) => criterion.id), files: testFilesIn(branchDir, finder.TEST_FILE), repository: repo, sha: head });
|
|
1612
|
+
let diff = "";
|
|
1613
|
+
const startCommit = (job.container ?? job.microvm)?.startCommit;
|
|
1614
|
+
if (startCommit !== void 0) {
|
|
1615
|
+
const output = join(scratch, "diff.patch");
|
|
1616
|
+
const made = await (deps.fetchDiff ?? ((request) => diffBetween(request)))({ remote, dir: join(scratch, "diff"), base: startCommit, head, token: readToken, output });
|
|
1617
|
+
if (!made.ok) return await fail(check, `the build's diff could not be read: ${made.reason}`);
|
|
1618
|
+
const bytes = readFileSync(output);
|
|
1619
|
+
diff = bytes.length > MAX_DIFF_BYTES ? `${bytes.subarray(0, MAX_DIFF_BYTES).toString("utf8")}
|
|
1620
|
+
[the diff was cut at ${MAX_DIFF_BYTES} bytes]
|
|
1621
|
+
` : bytes.toString("utf8");
|
|
1622
|
+
}
|
|
1623
|
+
await environment.prepare({
|
|
1624
|
+
"criteria.json": `${JSON.stringify({ criteria }, null, 2)}
|
|
1625
|
+
`,
|
|
1626
|
+
"citations.json": `${JSON.stringify(citations, null, 2)}
|
|
1627
|
+
`,
|
|
1628
|
+
"diff.patch": diff
|
|
1629
|
+
}, branchDir);
|
|
1630
|
+
const limitMinutes = config.checks?.timeLimitMinutes ?? DEFAULT_CHECK_TIME_LIMIT_MINUTES;
|
|
1631
|
+
const minutes = `${limitMinutes} ${limitMinutes === 1 ? "minute" : "minutes"}`;
|
|
1632
|
+
const clear = async () => {
|
|
1633
|
+
await environment.clear?.().catch((err) => log.error(`${tag} what the judge left in its folder could not be removed: ${describeCallFailure(err)}`));
|
|
1634
|
+
};
|
|
1635
|
+
const running = environment.run(judgeCommand({ brief: check.judge.text, credential }), { logPath: join(jobDir, "judge.log") });
|
|
1636
|
+
running.catch(() => void 0);
|
|
1637
|
+
const timer = timeoutAfter(limitMinutes * 6e4, deps.sleep);
|
|
1638
|
+
let outcome;
|
|
1639
|
+
try {
|
|
1640
|
+
outcome = await Promise.race([running, timer.done]);
|
|
1641
|
+
} catch (err) {
|
|
1642
|
+
await clear();
|
|
1643
|
+
return await fail(check, `the judge could not be run: ${describeCallFailure(err)}`);
|
|
1644
|
+
} finally {
|
|
1645
|
+
timer.cancel();
|
|
1646
|
+
}
|
|
1647
|
+
if (outcome === "timeout") await environment.stop().catch((err) => log.error(`${tag} the judge could not be stopped after its time ran out: ${describeCallFailure(err)}`));
|
|
1648
|
+
const decision = decideVerdict({
|
|
1649
|
+
run: outcome === "timeout" ? { kind: "timeout" } : { kind: "exited", status: outcome.status, stdout: outcome.stdout, stderr: outcome.stderr },
|
|
1650
|
+
criteria,
|
|
1651
|
+
citations,
|
|
1652
|
+
minutes
|
|
1653
|
+
});
|
|
1654
|
+
let report;
|
|
1655
|
+
try {
|
|
1656
|
+
report = await environment.readReport();
|
|
1657
|
+
} catch (err) {
|
|
1658
|
+
log.error(`${tag} the judge's report could not be read: ${describeCallFailure(err)}`);
|
|
1659
|
+
}
|
|
1660
|
+
await clear();
|
|
1661
|
+
const link = report === void 0 ? void 0 : await uploadReport(check, report, { branch, head });
|
|
1662
|
+
await record(check.name, decision.result, decision.reason, { keyTail: decision.limitKey === true ? LIMIT_KEY_TAIL2 : "", link });
|
|
1663
|
+
}
|
|
1664
|
+
async function uploadReport(check, report, at) {
|
|
1665
|
+
const bytes = Buffer.from(report, "utf8");
|
|
1666
|
+
if (bytes.length === 0 || bytes.length > MAX_REPORT_BYTES) {
|
|
1667
|
+
log.error(`${tag} the judge's report was not uploaded: it is ${bytes.length === 0 ? "empty" : `larger than ${MAX_REPORT_BYTES} bytes`}`);
|
|
1668
|
+
return void 0;
|
|
1669
|
+
}
|
|
1670
|
+
const clientArtifactId = `host-judge-${createHash("sha256").update(`${job.claimId}:${job.startedAt}:${check.name}`).digest("hex").slice(0, 40)}`;
|
|
1671
|
+
const request = {
|
|
1672
|
+
schemaVersion: 1,
|
|
1673
|
+
artifact: {
|
|
1674
|
+
clientArtifactId,
|
|
1675
|
+
filename: "criteria-judge-report.html",
|
|
1676
|
+
mediaType: "text/html",
|
|
1677
|
+
bytes: bytes.length,
|
|
1678
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
1679
|
+
caption: REPORT_CAPTION,
|
|
1680
|
+
criterionId: check.name,
|
|
1681
|
+
emittedAt: now().toISOString(),
|
|
1682
|
+
repository: repo,
|
|
1683
|
+
gitRepo: repo,
|
|
1684
|
+
gitBranch: at.branch,
|
|
1685
|
+
commitSha: at.head
|
|
1686
|
+
},
|
|
1687
|
+
blob: { data: bytes.toString("base64") }
|
|
1688
|
+
};
|
|
1689
|
+
const signer = await signerFor(config.configPath);
|
|
1690
|
+
const sent = await retrySend(() => postJson(config.url, "/v1/artifacts", config.apiKey, config.appId, request, deps.fetchImpl, void 0, signer), { now: deps.now });
|
|
1691
|
+
if (!sent.ok) {
|
|
1692
|
+
log.error(`${tag} the judge's report could not be uploaded: ${sent.reason}`);
|
|
1693
|
+
return void 0;
|
|
1694
|
+
}
|
|
1695
|
+
const url = sent.value.data?.url;
|
|
1696
|
+
return typeof url === "string" && /^https?:\/\/\S+$/.test(url) ? url : void 0;
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1242
1700
|
// src/runner/job.ts
|
|
1243
1701
|
var LEASE_SECONDS = 900;
|
|
1244
1702
|
var RENEW_INTERVAL_SECONDS = 300;
|
|
@@ -1596,6 +2054,9 @@ async function releaseLease(config, item, outcome, detail, fetchImpl) {
|
|
|
1596
2054
|
const namesOutcome = /\boutcome\b/.test(err.message);
|
|
1597
2055
|
if (sent === "incomplete" && namesOutcome) {
|
|
1598
2056
|
sent = "ok";
|
|
2057
|
+
} else if (environment?.controlPlane !== void 0) {
|
|
2058
|
+
const { controlPlane: _omittedPlane, ...withoutPlane } = environment;
|
|
2059
|
+
environment = withoutPlane;
|
|
1599
2060
|
} else if (environment?.commitIdentity !== void 0) {
|
|
1600
2061
|
const { commitIdentity: _omitted, ...withoutIdentity } = environment;
|
|
1601
2062
|
environment = withoutIdentity;
|
|
@@ -1706,7 +2167,7 @@ async function finishJob(job, jobDirPath, outcome, config, deps, ending) {
|
|
|
1706
2167
|
await stopProcessGroup(job.pid);
|
|
1707
2168
|
}
|
|
1708
2169
|
if (outcome !== void 0 && job.endStage === void 0) {
|
|
1709
|
-
const TEARDOWN_STAGES = ["checks", "sync", "cleanup", "report"];
|
|
2170
|
+
const TEARDOWN_STAGES = ["judge", "checks", "sync", "cleanup", "report"];
|
|
1710
2171
|
const staleTeardownStage = job.stage !== void 0 && TEARDOWN_STAGES.includes(job.stage);
|
|
1711
2172
|
job.endStage = ending?.stage ?? (staleTeardownStage ? job.exitCode !== void 0 ? "command" : void 0 : job.stage);
|
|
1712
2173
|
job.endExitCode = ending?.exitCode ?? (outcome === "ok" || outcome === "failed" ? job.exitCode : void 0);
|
|
@@ -1738,6 +2199,7 @@ async function finishJob(job, jobDirPath, outcome, config, deps, ending) {
|
|
|
1738
2199
|
}
|
|
1739
2200
|
}
|
|
1740
2201
|
};
|
|
2202
|
+
let buildVmStopLogged = false;
|
|
1741
2203
|
const endBuildMicroVm = vmBuild === void 0 ? void 0 : async () => {
|
|
1742
2204
|
if (job.workdir && job.sessionsSynced !== true) {
|
|
1743
2205
|
await uploadSessions(job.workdir);
|
|
@@ -1745,6 +2207,8 @@ async function finishJob(job, jobDirPath, outcome, config, deps, ending) {
|
|
|
1745
2207
|
writeJobFile(jobFilePath, job);
|
|
1746
2208
|
}
|
|
1747
2209
|
await stopBuildMicroVm(vmBuild.runtime, vmBuild.state, jobDirPath);
|
|
2210
|
+
log.line(`${tag} build microVM stopped`);
|
|
2211
|
+
buildVmStopLogged = true;
|
|
1748
2212
|
};
|
|
1749
2213
|
const collectSessions = build !== void 0 ? () => copyTranscriptsFromContainer(build.runtime, build.state) : vmBuild !== void 0 ? () => copyTranscriptsFromMicroVm(vmBuild.runtime, vmBuild.state) : void 0;
|
|
1750
2214
|
if (job.checkContainer !== void 0 && job.checksRan === true) {
|
|
@@ -1765,6 +2229,21 @@ async function finishJob(job, jobDirPath, outcome, config, deps, ending) {
|
|
|
1765
2229
|
log.error(`${tag} removing an earlier attempt's check environment failed: ${describeCallFailure(err)} \xB7 continuing`);
|
|
1766
2230
|
}
|
|
1767
2231
|
}
|
|
2232
|
+
if (outcome === "ok" && !job.cleanupRan && job.judgeRan !== true) {
|
|
2233
|
+
setStage(job, jobDirPath, "judge");
|
|
2234
|
+
job.judgeRan = true;
|
|
2235
|
+
writeJobFile(jobFilePath, job);
|
|
2236
|
+
try {
|
|
2237
|
+
await runJudge({
|
|
2238
|
+
config,
|
|
2239
|
+
job,
|
|
2240
|
+
jobDir: jobDirPath,
|
|
2241
|
+
deps: { fetchImpl: deps.fetchImpl, runtime: deps.container ?? createContainerRuntime(), log, now: deps.now, endBuildMicroVm, collectSessions, ...deps.hostChecks }
|
|
2242
|
+
});
|
|
2243
|
+
} catch (err) {
|
|
2244
|
+
log.error(`${tag} the host's judge stopped: ${describeCallFailure(err)} \xB7 continuing`);
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
1768
2247
|
if (outcome === "ok" && !job.cleanupRan && job.checksRan !== true) {
|
|
1769
2248
|
setStage(job, jobDirPath, "checks");
|
|
1770
2249
|
job.checksRan = true;
|
|
@@ -1819,6 +2298,7 @@ async function finishJob(job, jobDirPath, outcome, config, deps, ending) {
|
|
|
1819
2298
|
cleanedUp = false;
|
|
1820
2299
|
log.error(`${tag} cleanup failed: ${describeCallFailure(err)} \xB7 continuing`);
|
|
1821
2300
|
}
|
|
2301
|
+
if (cleanedUp && !buildVmStopLogged) log.line(`${tag} build microVM stopped`);
|
|
1822
2302
|
if (cleanedUp) log.line(`${tag} cleanup ok (${formatDuration(now().getTime() - cleanupStartedMs)})`);
|
|
1823
2303
|
} else if (build !== void 0 || !hostMade && isBuiltinContainer(config.hooks.cleanup)) {
|
|
1824
2304
|
let cleanedUp = true;
|
|
@@ -2074,19 +2554,18 @@ async function startSender(input) {
|
|
|
2074
2554
|
const launch = launchFor(config, job, jobDir, deps);
|
|
2075
2555
|
const fd = openSync(join(jobDir, "sender.log"), "a", RUNNER_FILE_MODE);
|
|
2076
2556
|
let pid;
|
|
2557
|
+
let exitLogged;
|
|
2077
2558
|
try {
|
|
2078
2559
|
const child = spawnFn(launch.command, launch.args, { env: launch.env, cwd: launch.cwd, detached: true, stdio: ["ignore", fd, fd] });
|
|
2079
2560
|
child.on("error", (err) => deps.log.error(`${tag} session sender failed: ${describeCallFailure(err)}`));
|
|
2080
|
-
|
|
2561
|
+
exitLogged = new Promise((resolve) => {
|
|
2081
2562
|
child.on("exit", (code, signal) => {
|
|
2082
2563
|
deps.log.line(`${tag} session sender exited \xB7 ${signal ?? `code ${code}`} \xB7 see sender.log`);
|
|
2083
|
-
if (pid !== void 0) exitsLogged.delete(pid);
|
|
2084
2564
|
resolve();
|
|
2085
2565
|
});
|
|
2086
2566
|
});
|
|
2087
2567
|
child.unref?.();
|
|
2088
2568
|
pid = child.pid;
|
|
2089
|
-
if (pid !== void 0) exitsLogged.set(pid, exitLogged);
|
|
2090
2569
|
} finally {
|
|
2091
2570
|
closeSync(fd);
|
|
2092
2571
|
}
|
|
@@ -2094,6 +2573,7 @@ async function startSender(input) {
|
|
|
2094
2573
|
deps.log.line(`${tag} session sender did not start: no pid`);
|
|
2095
2574
|
return void 0;
|
|
2096
2575
|
}
|
|
2576
|
+
exitsLogged.set(pid, exitLogged);
|
|
2097
2577
|
deps.log.line(`${tag} session sender started \xB7 pid ${pid}`);
|
|
2098
2578
|
return { pid, start: startTimeOf(pid) ?? void 0 };
|
|
2099
2579
|
} catch (err) {
|
|
@@ -2148,7 +2628,6 @@ async function signalInGuest(runtime, state, pidfile, signal) {
|
|
|
2148
2628
|
}
|
|
2149
2629
|
async function exitReported(pid) {
|
|
2150
2630
|
const logged = exitsLogged.get(pid);
|
|
2151
|
-
if (logged === void 0) return;
|
|
2152
2631
|
let timer;
|
|
2153
2632
|
const bound = new Promise((resolve) => {
|
|
2154
2633
|
timer = setTimeout(resolve, EXIT_REPORT_WAIT_MS);
|