@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,28 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
|
-
import { createDockerHost, childEnvironment, spawnRun } from './chunk-
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
3
|
+
import { createDockerHost, childEnvironment, spawnRun } from './chunk-L2USZT6T.js';
|
|
4
|
+
import { builderSpec, runnerLabelValue, RUNNER_LABEL, BUILD_LABEL, prepareRunArgs, containerRunArgs, HASH_ARGV, parseProgramHash, JUDGE_PROGRAM, containerExecArgs, JUDGE_DIR, MAX_REPORT_BYTES } from './chunk-S26M3YBJ.js';
|
|
5
|
+
import { execCapture } from './chunk-GGOMT6KF.js';
|
|
6
|
+
import { GUEST_CID } from './chunk-X5H2WYMB.js';
|
|
7
|
+
import { describeCallFailure, controlPlaneTokenFailure, refusalCode, resolveGoverningSource, removeStagedContext, stageGovernedContext, pinnedSourceOf, commandWithShim, sharedSkillNames, shimScript } from './chunk-6CHIYEZZ.js';
|
|
8
|
+
import { WorkCommandError } from './chunk-KK4VBOH6.js';
|
|
7
9
|
import { hostAllowed } from './chunk-UXYLWVTQ.js';
|
|
8
|
-
import { readDefaultBranchHead, cloneRepository, buildRepositoryImage } from './chunk-R7PF5OOU.js';
|
|
9
|
-
import { RecipeError } from './chunk-QOZKPLVJ.js';
|
|
10
|
-
import { startTimeOf, isSameProcess } from './chunk-N5FOE5PS.js';
|
|
11
|
-
import { currentCliBuild } from './chunk-OGFGZQA3.js';
|
|
12
|
-
import { builderSpec, runnerLabelValue, RUNNER_LABEL, BUILD_LABEL, prepareRunArgs, containerRunArgs, containerExecArgs } from './chunk-4NPZVMJU.js';
|
|
13
10
|
import { parseDuKilobytes } from './chunk-7RQ73DLO.js';
|
|
14
|
-
import {
|
|
11
|
+
import { readDefaultBranchHead, cloneRepository, buildRepositoryImage } from './chunk-3CWUOFV6.js';
|
|
12
|
+
import { RecipeError } from './chunk-QOZKPLVJ.js';
|
|
15
13
|
import { CLAUDE_CODE_VERSION, firecrackerArch, MICROVM_ARCHES, DEFAULT_BASE_IMAGE, GUEST_HELPER_SHA256, GUEST_ASSETS_BASE_URL, GUEST_KERNEL_SHA256, FIRECRACKER_BINARY_SHA256, FIRECRACKER_SHA256, FIRECRACKER_VERSION, GUEST_ASSETS_VERSION } from './chunk-CX22S5BA.js';
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
14
|
+
import { startTimeOf, isSameProcess } from './chunk-JBJTC3UU.js';
|
|
15
|
+
import { currentCliBuild } from './chunk-OGFGZQA3.js';
|
|
16
|
+
import { setCustomScrubbers, setLocalIdentity, setRepoRoot, parseTranscript, scrubDeep, scrubText } from './chunk-BMYGAGBJ.js';
|
|
17
|
+
import { orderSessions, extractFacts, sessionsToTimeline } from './chunk-X5N6KC6V.js';
|
|
18
|
+
import { downconvertSession } from './chunk-N3CUDFCW.js';
|
|
19
|
+
import { rootfsPathFor, diskTemplatesDir, RUNNER_FILE_MODE, deliveryDir, ghConfigDir, runnerDirOfBuild, spoolDir, mirrorDir, buildDir, RUNNER_DIR_MODE, SHIM_DIR, vmAssetsDir } from './chunk-N2PXGQRL.js';
|
|
20
|
+
import { repositoriesOf, primaryOf, RELAY_PORT, repositoryPath, CHECKOUT_DIR, lifecycleWorkdir, claudeCredential, buildEnvironment, startBuildServices, checkEnvironment, AGENT_CREDENTIAL_MISSING, checkoutPath, JOB_MOUNT } from './chunk-BQXFBI7T.js';
|
|
21
|
+
import { versionAtLeast, MINIMUM_CLAUDE_VERSION } from './chunk-G2YNJIGF.js';
|
|
22
|
+
import { readFileSync, existsSync, mkdirSync, chmodSync, truncateSync, rmSync, writeFileSync, readdirSync, lstatSync, copyFileSync, accessSync, constants, statSync, rmdirSync, renameSync, unlinkSync, openSync, closeSync, mkdtempSync, createWriteStream, createReadStream } from 'fs';
|
|
23
|
+
import { join, dirname, basename } from 'path';
|
|
26
24
|
import { spawnSync, spawn } from 'child_process';
|
|
27
25
|
import { availableParallelism, networkInterfaces } from 'os';
|
|
28
26
|
import { randomBytes, createHash } from 'crypto';
|
|
@@ -35,285 +33,6 @@ import { isIP, connect, createServer as createServer$1 } from 'net';
|
|
|
35
33
|
import { lookup } from 'dns/promises';
|
|
36
34
|
|
|
37
35
|
createRequire(import.meta.url);
|
|
38
|
-
function logStamp(at) {
|
|
39
|
-
return at.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
40
|
-
}
|
|
41
|
-
function createRunnerLog(now = () => /* @__PURE__ */ new Date()) {
|
|
42
|
-
const write = (stream, text) => {
|
|
43
|
-
stream.write(`${logStamp(now())} ${text}
|
|
44
|
-
`);
|
|
45
|
-
};
|
|
46
|
-
return {
|
|
47
|
-
line: (text) => write(process.stdout, text),
|
|
48
|
-
error: (text) => {
|
|
49
|
-
write(process.stdout, text);
|
|
50
|
-
write(process.stderr, text);
|
|
51
|
-
}
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
function formatDuration(ms) {
|
|
55
|
-
const total = Math.max(0, Math.round(ms / 1e3));
|
|
56
|
-
const hours = Math.floor(total / 3600);
|
|
57
|
-
const minutes = Math.floor(total % 3600 / 60);
|
|
58
|
-
const seconds = total % 60;
|
|
59
|
-
if (hours > 0) return `${hours}h ${minutes}m`;
|
|
60
|
-
if (minutes > 0) return `${minutes}m ${String(seconds).padStart(2, "0")}s`;
|
|
61
|
-
return `${seconds}s`;
|
|
62
|
-
}
|
|
63
|
-
function formatClock(at, today) {
|
|
64
|
-
const hhmm = `${String(at.getUTCHours()).padStart(2, "0")}:${String(at.getUTCMinutes()).padStart(2, "0")} UTC`;
|
|
65
|
-
const sameDay = at.getUTCFullYear() === today.getUTCFullYear() && at.getUTCMonth() === today.getUTCMonth() && at.getUTCDate() === today.getUTCDate();
|
|
66
|
-
if (sameDay) return hhmm;
|
|
67
|
-
const month = at.toLocaleString("en-US", { month: "short", timeZone: "UTC" });
|
|
68
|
-
return `${month} ${at.getUTCDate()} ${hhmm}`;
|
|
69
|
-
}
|
|
70
|
-
function shortClaim(claimId) {
|
|
71
|
-
return claimId.length <= 8 ? claimId : `${claimId.slice(0, 8)}\u2026`;
|
|
72
|
-
}
|
|
73
|
-
function relativeToRunner(runnerDirPath, path) {
|
|
74
|
-
const rel = relative(runnerDirPath, path);
|
|
75
|
-
return rel === "" || rel.startsWith("..") ? path : rel;
|
|
76
|
-
}
|
|
77
|
-
function describeCallFailure(err) {
|
|
78
|
-
if (err instanceof Error) {
|
|
79
|
-
const status = err.status;
|
|
80
|
-
return status === void 0 ? err.message : `${err.message} (${status})`;
|
|
81
|
-
}
|
|
82
|
-
return String(err);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// src/runner/retry.ts
|
|
86
|
-
var RETRY_WINDOW_MS = 12e4;
|
|
87
|
-
var FIRST_BACKOFF_MS = 1e3;
|
|
88
|
-
var MAX_BACKOFF_MS = 15e3;
|
|
89
|
-
function isRetryable(err) {
|
|
90
|
-
if (err instanceof WorkCommandError) return err.retryable;
|
|
91
|
-
return true;
|
|
92
|
-
}
|
|
93
|
-
async function retrySend(send, deps = {}) {
|
|
94
|
-
const now = deps.now ?? (() => /* @__PURE__ */ new Date());
|
|
95
|
-
const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
96
|
-
const windowMs = deps.windowMs ?? RETRY_WINDOW_MS;
|
|
97
|
-
const startedMs = now().getTime();
|
|
98
|
-
let attempts = 0;
|
|
99
|
-
let backoffMs = FIRST_BACKOFF_MS;
|
|
100
|
-
for (; ; ) {
|
|
101
|
-
attempts++;
|
|
102
|
-
try {
|
|
103
|
-
return { ok: true, value: await send(), attempts };
|
|
104
|
-
} catch (err) {
|
|
105
|
-
const reason = describeCallFailure(err);
|
|
106
|
-
const code = err instanceof WorkCommandError ? err.code : void 0;
|
|
107
|
-
const status = err instanceof WorkCommandError ? err.status : void 0;
|
|
108
|
-
const message = err instanceof WorkCommandError ? err.message : void 0;
|
|
109
|
-
if (!isRetryable(err)) return { ok: false, attempts, reason, retryable: false, code, status, message };
|
|
110
|
-
const elapsedMs = now().getTime() - startedMs;
|
|
111
|
-
if (elapsedMs + backoffMs >= windowMs) return { ok: false, attempts, reason, retryable: true, code, status, message };
|
|
112
|
-
await sleep(backoffMs);
|
|
113
|
-
backoffMs = Math.min(backoffMs * 2, MAX_BACKOFF_MS);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// src/runner/build-tokens.ts
|
|
119
|
-
var ISSUE_RETRY_WINDOW_MS = 3e4;
|
|
120
|
-
var REVOKE_TIMEOUT_MS = 15e3;
|
|
121
|
-
function isDue(expiresAtMs, nowMs) {
|
|
122
|
-
return nowMs >= expiresAtMs - 10 * 6e4;
|
|
123
|
-
}
|
|
124
|
-
function classifyRefusal(err) {
|
|
125
|
-
if (!(err instanceof WorkCommandError)) return { kind: "retry" };
|
|
126
|
-
if (err.status === 401 || err.code === "claim_expired" || err.code === "claim_not_held") return { kind: "lease_lost" };
|
|
127
|
-
if (err.code === "github_unavailable") return { kind: "retry" };
|
|
128
|
-
if (err.code === "github_request_failed") return { kind: "failed", reason: refusalCode(err) };
|
|
129
|
-
if (err.retryable && err.code !== "repository_tokens_unavailable") return { kind: "retry" };
|
|
130
|
-
return { kind: "failed", reason: refusalCode(err) };
|
|
131
|
-
}
|
|
132
|
-
function refusalCode(err) {
|
|
133
|
-
if (!(err instanceof WorkCommandError)) return "unavailable";
|
|
134
|
-
return err.code ?? `http_${err.status ?? "unknown"}`;
|
|
135
|
-
}
|
|
136
|
-
function keyOf(kind, repository) {
|
|
137
|
-
return repository === void 0 ? kind : `${kind}:${repository}`;
|
|
138
|
-
}
|
|
139
|
-
var BuildTokens = class {
|
|
140
|
-
constructor(options) {
|
|
141
|
-
this.options = options;
|
|
142
|
-
this.held = /* @__PURE__ */ new Map();
|
|
143
|
-
this.asking = /* @__PURE__ */ new Map();
|
|
144
|
-
this.issued = [];
|
|
145
|
-
this.usedKinds = /* @__PURE__ */ new Set();
|
|
146
|
-
this.revoked = false;
|
|
147
|
-
this.controlPlaneRetry = false;
|
|
148
|
-
}
|
|
149
|
-
now() {
|
|
150
|
-
return (this.options.now ?? (() => /* @__PURE__ */ new Date()))();
|
|
151
|
-
}
|
|
152
|
-
/** `repository` as the holder keys it: `undefined` for the primary, which is the one a request names no repository for. */
|
|
153
|
-
secondary(repository) {
|
|
154
|
-
if (repository === void 0) return void 0;
|
|
155
|
-
const primary = this.options.primary;
|
|
156
|
-
return primary !== void 0 && primary.toLowerCase() === repository.toLowerCase() ? void 0 : repository;
|
|
157
|
-
}
|
|
158
|
-
/**
|
|
159
|
-
* The token of `kind` for `repository`, or for the primary when none is
|
|
160
|
-
* named, asking the gateway for a new one when the build holds none or its
|
|
161
|
-
* token is within ten minutes of expiry.
|
|
162
|
-
*/
|
|
163
|
-
token(kind, repository) {
|
|
164
|
-
const other = this.secondary(repository);
|
|
165
|
-
const key = keyOf(kind, other);
|
|
166
|
-
const held = this.held.get(key);
|
|
167
|
-
if (held !== void 0 && !isDue(held.expiresAtMs, this.now().getTime())) return Promise.resolve(held.token);
|
|
168
|
-
let asking = this.asking.get(key);
|
|
169
|
-
if (asking === void 0) {
|
|
170
|
-
asking = this.issueOrKeep(kind, other).finally(() => this.asking.delete(key));
|
|
171
|
-
this.asking.set(key, asking);
|
|
172
|
-
}
|
|
173
|
-
return asking;
|
|
174
|
-
}
|
|
175
|
-
/** A due token that has not yet expired is kept when the gateway cannot issue a new one. */
|
|
176
|
-
async issueOrKeep(kind, repository) {
|
|
177
|
-
try {
|
|
178
|
-
return await this.issue(kind, repository);
|
|
179
|
-
} catch (err) {
|
|
180
|
-
const held = this.held.get(keyOf(kind, repository));
|
|
181
|
-
if (held !== void 0 && this.now().getTime() < held.expiresAtMs && classifyRefusal(err).kind === "retry") return held.token;
|
|
182
|
-
throw err;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
/** Asks the gateway for a token with `body`, retrying a failure about the moment. */
|
|
186
|
-
ask(body) {
|
|
187
|
-
const { config, item, fetchImpl } = this.options;
|
|
188
|
-
return retrySend(
|
|
189
|
-
async () => {
|
|
190
|
-
try {
|
|
191
|
-
return await postJson(
|
|
192
|
-
config.url,
|
|
193
|
-
`/v1/work-items/${item}/claim/tokens`,
|
|
194
|
-
config.apiKey,
|
|
195
|
-
config.appId,
|
|
196
|
-
body,
|
|
197
|
-
fetchImpl,
|
|
198
|
-
void 0,
|
|
199
|
-
await signerFor(config.configPath)
|
|
200
|
-
);
|
|
201
|
-
} catch (err) {
|
|
202
|
-
if (err instanceof WorkCommandError && (err.code === "repository_tokens_unavailable" || err.code === "github_request_failed")) {
|
|
203
|
-
throw new WorkCommandError(err.message, false, err.status, err.code);
|
|
204
|
-
}
|
|
205
|
-
throw err;
|
|
206
|
-
}
|
|
207
|
-
},
|
|
208
|
-
{ now: this.options.now, sleep: this.options.sleep, windowMs: ISSUE_RETRY_WINDOW_MS }
|
|
209
|
-
);
|
|
210
|
-
}
|
|
211
|
-
/**
|
|
212
|
-
* A read-only token for the repository that governs the item's, which the
|
|
213
|
-
* runner uses on the host to fetch its context. It is revoked with the
|
|
214
|
-
* others when the attempt ends. It is not held or renewed: nothing uses it
|
|
215
|
-
* after the fetch. A refusal is thrown with the gateway's code. A lost
|
|
216
|
-
* lease is kept for `refusal()`, and a failure about the moment for
|
|
217
|
-
* `controlPlaneUnavailable()`, as the read token's are.
|
|
218
|
-
*/
|
|
219
|
-
async controlPlane() {
|
|
220
|
-
const asked = await this.ask({ access: "read", target: "control_plane" });
|
|
221
|
-
if (!asked.ok) {
|
|
222
|
-
const err = new WorkCommandError(asked.message ?? asked.reason, asked.retryable, asked.status, asked.code);
|
|
223
|
-
const refusal = classifyRefusal(err);
|
|
224
|
-
if (refusal.kind === "lease_lost") this.failure = refusal;
|
|
225
|
-
if (refusal.kind === "retry") this.controlPlaneRetry = true;
|
|
226
|
-
throw err;
|
|
227
|
-
}
|
|
228
|
-
const { token, expiresAt, repository } = asked.value.data;
|
|
229
|
-
if (typeof token !== "string" || token === "" || typeof expiresAt !== "string" || typeof repository !== "string" || repository === "") {
|
|
230
|
-
throw new WorkCommandError("the gateway's token answer carried no token, expiry or repository", false, void 0, "malformed_token_response");
|
|
231
|
-
}
|
|
232
|
-
if (this.revoked) {
|
|
233
|
-
await this.revoke([token]);
|
|
234
|
-
throw new WorkCommandError("the build's tokens were already revoked", false, void 0, "tokens_revoked");
|
|
235
|
-
}
|
|
236
|
-
this.issued.push(token);
|
|
237
|
-
return { token, expiresAt, repository };
|
|
238
|
-
}
|
|
239
|
-
async issue(kind, repository) {
|
|
240
|
-
const asked = await this.ask({ access: kind, ...repository === void 0 ? {} : { repository } });
|
|
241
|
-
let err;
|
|
242
|
-
if (!asked.ok) {
|
|
243
|
-
err = new WorkCommandError(asked.message ?? asked.reason, asked.retryable, asked.status, asked.code);
|
|
244
|
-
} else {
|
|
245
|
-
const { token, expiresAt } = asked.value.data;
|
|
246
|
-
const expiresAtMs = typeof expiresAt === "string" ? Date.parse(expiresAt) : Number.NaN;
|
|
247
|
-
if (typeof token === "string" && token !== "" && Number.isFinite(expiresAtMs)) {
|
|
248
|
-
if (this.revoked) {
|
|
249
|
-
await this.revoke([token]);
|
|
250
|
-
throw new WorkCommandError("the build's tokens were already revoked", false, void 0, "tokens_revoked");
|
|
251
|
-
}
|
|
252
|
-
this.held.set(keyOf(kind, repository), { token, expiresAtMs });
|
|
253
|
-
this.issued.push(token);
|
|
254
|
-
if (kind === "read" && repository === void 0) this.readSink?.(token);
|
|
255
|
-
return token;
|
|
256
|
-
}
|
|
257
|
-
err = new WorkCommandError("the gateway's token answer carried no token or no expiry", false, void 0, "malformed_token_response");
|
|
258
|
-
}
|
|
259
|
-
const refusal = classifyRefusal(err);
|
|
260
|
-
if (refusal.kind !== "retry") this.failure = refusal;
|
|
261
|
-
throw err;
|
|
262
|
-
}
|
|
263
|
-
/** Asks for a new read token when the build holds one within ten minutes of expiry. A refusal is kept for `refusal()`, not thrown. */
|
|
264
|
-
async renewReadIfDue() {
|
|
265
|
-
const held = this.held.get("read");
|
|
266
|
-
if (held === void 0 || !isDue(held.expiresAtMs, this.now().getTime())) return;
|
|
267
|
-
await this.token("read").catch(() => void 0);
|
|
268
|
-
}
|
|
269
|
-
/** Calls `sink` with the read token now, and again with each one that replaces it. */
|
|
270
|
-
onRead(sink) {
|
|
271
|
-
this.readSink = sink;
|
|
272
|
-
const held = this.held.get("read");
|
|
273
|
-
if (held !== void 0) sink(held.token);
|
|
274
|
-
}
|
|
275
|
-
markUsed(kind) {
|
|
276
|
-
if (this.usedKinds.has(kind)) return;
|
|
277
|
-
this.usedKinds.add(kind);
|
|
278
|
-
this.options.onUsed?.(this.used());
|
|
279
|
-
}
|
|
280
|
-
/** The kinds of token the build has used, read before push. */
|
|
281
|
-
used() {
|
|
282
|
-
return ["read", "push"].filter((kind) => this.usedKinds.has(kind));
|
|
283
|
-
}
|
|
284
|
-
/** The last refusal that ends the build, if a renewal or a push's token request met one. */
|
|
285
|
-
refusal() {
|
|
286
|
-
return this.failure;
|
|
287
|
-
}
|
|
288
|
-
/** Whether a control plane token request failed for a reason about the moment, which a later attempt may not meet. */
|
|
289
|
-
controlPlaneUnavailable() {
|
|
290
|
-
return this.controlPlaneRetry;
|
|
291
|
-
}
|
|
292
|
-
/** Revokes every token this holder was issued, the superseded ones too, at GitHub with the token itself. A failure is not an error: the token lapses within the hour. */
|
|
293
|
-
async revokeAll() {
|
|
294
|
-
this.revoked = true;
|
|
295
|
-
const tokens = this.issued.splice(0);
|
|
296
|
-
this.held.clear();
|
|
297
|
-
await this.revoke(tokens);
|
|
298
|
-
}
|
|
299
|
-
async revoke(tokens) {
|
|
300
|
-
const apiUrl = this.options.apiUrl ?? "https://api.github.com";
|
|
301
|
-
await Promise.allSettled(
|
|
302
|
-
tokens.map(
|
|
303
|
-
(token) => this.options.fetchImpl(`${apiUrl}/installation/token`, {
|
|
304
|
-
method: "DELETE",
|
|
305
|
-
headers: {
|
|
306
|
-
authorization: `Bearer ${token}`,
|
|
307
|
-
accept: "application/vnd.github+json",
|
|
308
|
-
"x-github-api-version": "2022-11-28",
|
|
309
|
-
"user-agent": "outerlayer-runner"
|
|
310
|
-
},
|
|
311
|
-
signal: AbortSignal.timeout(REVOKE_TIMEOUT_MS)
|
|
312
|
-
})
|
|
313
|
-
)
|
|
314
|
-
);
|
|
315
|
-
}
|
|
316
|
-
};
|
|
317
36
|
var BUILD_PREFIX = "build-";
|
|
318
37
|
var LEAF = "runner";
|
|
319
38
|
function serviceDirectory(selfCgroup, mount) {
|
|
@@ -676,6 +395,60 @@ async function ensureAssets(options) {
|
|
|
676
395
|
const helper = await ensureFile(dir, `outerlayer-guest-${arch}`, pins.helper, fetchImpl, 493);
|
|
677
396
|
return { firecracker, kernel, helper, helperSha256: pins.helper.sha256 };
|
|
678
397
|
}
|
|
398
|
+
|
|
399
|
+
// src/runner/container/docker.ts
|
|
400
|
+
function parseImageConfig(json) {
|
|
401
|
+
const config = JSON.parse(json);
|
|
402
|
+
const env = {};
|
|
403
|
+
for (const entry of config.Env ?? []) {
|
|
404
|
+
const eq = entry.indexOf("=");
|
|
405
|
+
if (eq > 0) env[entry.slice(0, eq)] = entry.slice(eq + 1);
|
|
406
|
+
}
|
|
407
|
+
return { user: config.User ?? "", env };
|
|
408
|
+
}
|
|
409
|
+
function createContainerHost(deps = {}) {
|
|
410
|
+
const run = deps.run ?? spawnRun;
|
|
411
|
+
return {
|
|
412
|
+
async version() {
|
|
413
|
+
const result = await run("docker", ["version", "--format", "{{.Server.Version}}"]);
|
|
414
|
+
return result.status === 0 && result.stdout.trim() !== "" ? result.stdout.trim() : null;
|
|
415
|
+
},
|
|
416
|
+
async operatingSystem() {
|
|
417
|
+
const result = await run("docker", ["info", "--format", "{{.OperatingSystem}}"]);
|
|
418
|
+
return result.status === 0 && result.stdout.trim() !== "" ? result.stdout.trim() : null;
|
|
419
|
+
},
|
|
420
|
+
async imageConfig(imageId) {
|
|
421
|
+
const result = await run("docker", ["image", "inspect", "--format", "{{json .Config}}", imageId]);
|
|
422
|
+
return result.status === 0 ? parseImageConfig(result.stdout) : { user: "", env: {} };
|
|
423
|
+
},
|
|
424
|
+
createVolume(name, labels) {
|
|
425
|
+
const flags = Object.entries(labels).flatMap(([key, value]) => ["--label", `${key}=${value}`]);
|
|
426
|
+
return run("docker", ["volume", "create", ...flags, name]);
|
|
427
|
+
},
|
|
428
|
+
removeVolume(name) {
|
|
429
|
+
return run("docker", ["volume", "rm", "--force", name]);
|
|
430
|
+
},
|
|
431
|
+
run(args, opts) {
|
|
432
|
+
return run("docker", ["run", ...args], opts);
|
|
433
|
+
},
|
|
434
|
+
exec(input, opts) {
|
|
435
|
+
const env = {};
|
|
436
|
+
for (const [name, value] of Object.entries(input.env.passed)) if (value !== void 0) env[name] = value;
|
|
437
|
+
return run("docker", ["exec", ...containerExecArgs(input)], { ...opts, env });
|
|
438
|
+
},
|
|
439
|
+
stop(container) {
|
|
440
|
+
return run("docker", ["stop", container]);
|
|
441
|
+
},
|
|
442
|
+
start(container) {
|
|
443
|
+
return run("docker", ["start", container]);
|
|
444
|
+
},
|
|
445
|
+
remove(container) {
|
|
446
|
+
return run("docker", ["rm", "--force", "--volumes", container]);
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// src/runner/microvm/disks.ts
|
|
679
452
|
function converterScript() {
|
|
680
453
|
return [
|
|
681
454
|
"set -eu",
|
|
@@ -742,13 +515,13 @@ function exportInto(container, converter) {
|
|
|
742
515
|
exporter.once("error", reject);
|
|
743
516
|
writer.once("error", reject);
|
|
744
517
|
let exits = 0;
|
|
745
|
-
let
|
|
518
|
+
let failure2;
|
|
746
519
|
const done = (name, code) => {
|
|
747
|
-
if (code !== 0 &&
|
|
520
|
+
if (code !== 0 && failure2 === void 0) failure2 = `${name} exited ${code}`;
|
|
748
521
|
exits += 1;
|
|
749
522
|
if (exits < 2) return;
|
|
750
|
-
if (
|
|
751
|
-
else reject(new Error(`${
|
|
523
|
+
if (failure2 === void 0) resolve();
|
|
524
|
+
else reject(new Error(`${failure2}: ${firstLine2(errors.join("\n"))}`));
|
|
752
525
|
};
|
|
753
526
|
exporter.once("exit", (code) => done("docker export", code));
|
|
754
527
|
writer.once("exit", (code) => done("the converter", code));
|
|
@@ -774,13 +547,7 @@ function createRootfsHost() {
|
|
|
774
547
|
async imageConfig(imageId) {
|
|
775
548
|
const inspected = await spawnRun("docker", ["image", "inspect", "--format", "{{json .Config}}", imageId], { env: env() });
|
|
776
549
|
if (inspected.status !== 0) throw new Error(`docker image inspect ${imageId} failed: ${firstLine2(inspected.stderr)}`);
|
|
777
|
-
|
|
778
|
-
const variables = {};
|
|
779
|
-
for (const entry of config.Env ?? []) {
|
|
780
|
-
const eq = entry.indexOf("=");
|
|
781
|
-
if (eq > 0) variables[entry.slice(0, eq)] = entry.slice(eq + 1);
|
|
782
|
-
}
|
|
783
|
-
return { env: variables, user: config.User ?? "" };
|
|
550
|
+
return parseImageConfig(inspected.stdout);
|
|
784
551
|
}
|
|
785
552
|
};
|
|
786
553
|
}
|
|
@@ -931,8 +698,6 @@ function createMicroVmPorts(over = {}) {
|
|
|
931
698
|
exec: over.exec ?? execCapture
|
|
932
699
|
};
|
|
933
700
|
}
|
|
934
|
-
|
|
935
|
-
// src/runner/build-steps.ts
|
|
936
701
|
var aborted = () => ({ ok: false, reason: "", aborted: true });
|
|
937
702
|
function firstLine3(text) {
|
|
938
703
|
return text.trim().split("\n")[0] ?? "";
|
|
@@ -941,14 +706,43 @@ function gitErrorLine(text) {
|
|
|
941
706
|
const lines = text.split("\n").map((line) => line.trim()).filter((line) => line !== "");
|
|
942
707
|
return lines.find((line) => /^(fatal|error):/.test(line)) ?? lines.at(-1) ?? "";
|
|
943
708
|
}
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
709
|
+
var STAGED_TREE = `${JOB_MOUNT}/context/tree`;
|
|
710
|
+
var DELIVER_SCRIPT = [
|
|
711
|
+
"set -e",
|
|
712
|
+
'src="$1"; dest="$2"',
|
|
713
|
+
'if [ -e "$dest" ]; then chmod -R u+w "$dest"; rm -rf "$dest"; fi',
|
|
714
|
+
'mkdir -p "$dest"',
|
|
715
|
+
"for entry in AGENTS.md CLAUDE.md settings.json .claude .outerlayer; do",
|
|
716
|
+
' if [ -e "$src/$entry" ]; then cp -R "$src/$entry" "$dest/"; fi',
|
|
717
|
+
"done",
|
|
718
|
+
'chmod -R a-w "$dest"'
|
|
719
|
+
].join("\n");
|
|
720
|
+
var RECORD_SCRIPT = 'd="$(git rev-parse --git-common-dir)" && mkdir -p "$d/outerlayer" && printf "%s" "$1" > "$d/outerlayer/delivery.json"';
|
|
721
|
+
var WRITE_SHIM_SCRIPT = 'mkdir -p "$1" && printf "%s" "$2" > "$1/claude" && chmod 755 "$1/claude"';
|
|
722
|
+
function failure(what, result) {
|
|
723
|
+
return `context_delivery_failed: ${what} (exit ${result.status})${firstLine3(result.stderr) === "" ? "" : `: ${firstLine3(result.stderr)}`}`;
|
|
724
|
+
}
|
|
725
|
+
async function deliveryRefusal(input, staged) {
|
|
726
|
+
if (input.agentVersion !== void 0 && !versionAtLeast(input.agentVersion)) {
|
|
727
|
+
return `context_agent_too_old: the build image's Claude Code is ${input.agentVersion}, and a governed build needs ${MINIMUM_CLAUDE_VERSION} or later to load its control plane's instructions; rebuild the image`;
|
|
728
|
+
}
|
|
729
|
+
const own = await input.exec(["ls", "-1", ".claude/skills"], CHECKOUT_DIR, { quiet: true });
|
|
730
|
+
if (own.status !== 0) return void 0;
|
|
731
|
+
const ownNames = new Set(own.stdout.split("\n").map((name) => name.trim()));
|
|
732
|
+
const clash = sharedSkillNames(join(staged.dir, "tree")).find((name) => ownNames.has(name));
|
|
733
|
+
if (clash === void 0) return void 0;
|
|
734
|
+
return `context_skill_conflict: .claude/skills/${clash} in ${input.repository} has the same name as the shared skill ${clash} from ${staged.repository}, and would replace it in this build; rename or remove it from the repository`;
|
|
735
|
+
}
|
|
736
|
+
async function deliver(input, staged) {
|
|
737
|
+
const dir = deliveryDir(staged.repository, staged.sha);
|
|
738
|
+
const copied = await input.exec(["sh", "-c", DELIVER_SCRIPT, "deliver", STAGED_TREE, dir], "/build/work");
|
|
739
|
+
if (copied.status !== 0) return { ok: false, reason: failure("copying the control plane into the build failed", copied) };
|
|
740
|
+
const shim = await input.exec(["sh", "-c", WRITE_SHIM_SCRIPT, "shim", SHIM_DIR, shimScript()], "/build/work");
|
|
741
|
+
if (shim.status !== 0) return { ok: false, reason: failure("writing the claude shim failed", shim) };
|
|
742
|
+
const record = JSON.stringify({ schemaVersion: 1, repository: staged.repository, ref: staged.ref, sha: staged.sha, dir });
|
|
743
|
+
const recorded = await input.exec(["sh", "-c", RECORD_SCRIPT, "record", record], CHECKOUT_DIR);
|
|
744
|
+
if (recorded.status !== 0) return { ok: false, reason: failure("recording the delivery failed", recorded) };
|
|
745
|
+
return { ok: true };
|
|
952
746
|
}
|
|
953
747
|
async function checkOut(input, entry) {
|
|
954
748
|
const { exec, log, tag } = input;
|
|
@@ -974,6 +768,18 @@ async function checkOut(input, entry) {
|
|
|
974
768
|
throw err;
|
|
975
769
|
}
|
|
976
770
|
}
|
|
771
|
+
var DEVCONTAINER_FILES = [".devcontainer/devcontainer.json", ".devcontainer.json"];
|
|
772
|
+
async function noteIgnoredDevcontainers(input, repositories, governing) {
|
|
773
|
+
for (const entry of repositories) {
|
|
774
|
+
if (entry.repository.toLowerCase() === governing.toLowerCase()) continue;
|
|
775
|
+
for (const file of DEVCONTAINER_FILES) {
|
|
776
|
+
const probe = await input.exec(["test", "-f", file], checkoutPath(entry), { quiet: true });
|
|
777
|
+
if (probe.status !== 0) continue;
|
|
778
|
+
input.log.line(`${input.tag} ${entry.repository} has its own ${file}; it is ignored, and the control plane's ${governing} devcontainer is used instead`);
|
|
779
|
+
break;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
977
783
|
async function runBuildSteps(input) {
|
|
978
784
|
const { exec, log, tag, repository } = input;
|
|
979
785
|
let startCommit = "";
|
|
@@ -984,33 +790,44 @@ async function runBuildSteps(input) {
|
|
|
984
790
|
const first = await checkOut(input, { ...primary, primary: true });
|
|
985
791
|
if (!first.ok) return first;
|
|
986
792
|
startCommit = first.startCommit;
|
|
793
|
+
if (repositories.length > 1) {
|
|
794
|
+
const linkPath = repositoryPath(primary.repository);
|
|
795
|
+
const linked = await exec(["sh", "-c", 'mkdir -p "$(dirname "$1")" && ln -s "$2" "$1"', "link", linkPath, CHECKOUT_DIR], "/build/work");
|
|
796
|
+
if (linked.status !== 0) return { ok: false, reason: `linking ${primary.repository} at ${linkPath} failed (exit ${linked.status})` };
|
|
797
|
+
}
|
|
987
798
|
for (const entry of repositories.filter((candidate) => candidate.repository !== primary.repository)) {
|
|
988
799
|
if (input.aborted()) return aborted();
|
|
989
800
|
const checked = await checkOut(input, { ...entry, primary: false });
|
|
990
801
|
if (!checked.ok) return checked;
|
|
991
802
|
}
|
|
803
|
+
if (input.governing !== void 0) await noteIgnoredDevcontainers(input, repositories, input.governing.repository);
|
|
804
|
+
const lifecycleDir = lifecycleWorkdir(repositories, input.governing?.repository);
|
|
992
805
|
for (const step of lifecycleSteps(input.lifecycle)) {
|
|
993
806
|
if (input.aborted()) return aborted();
|
|
994
807
|
log.line(`${tag} lifecycle ${step.name} (${step.source})`);
|
|
995
|
-
const ran = await exec(step.argv,
|
|
808
|
+
const ran = await exec(step.argv, lifecycleDir);
|
|
996
809
|
if (ran.status !== 0) return { ok: false, reason: lifecycleFailureReason(step, ran.status) };
|
|
997
810
|
}
|
|
998
811
|
if (input.aborted()) return aborted();
|
|
999
812
|
const staged = await input.stageContext();
|
|
1000
813
|
if (staged.kind === "refused") return { ok: false, reason: staged.reason };
|
|
814
|
+
let controlPlane;
|
|
1001
815
|
if (staged.kind === "staged") {
|
|
1002
816
|
try {
|
|
1003
|
-
const
|
|
1004
|
-
if (
|
|
1005
|
-
const
|
|
1006
|
-
if (
|
|
1007
|
-
|
|
817
|
+
const refusal = await deliveryRefusal(input, staged);
|
|
818
|
+
if (refusal !== void 0) return { ok: false, reason: refusal };
|
|
819
|
+
const handed = await input.deliverContext?.(staged) ?? { ok: true };
|
|
820
|
+
if (!handed.ok) return { ok: false, reason: handed.reason };
|
|
821
|
+
const delivered = await deliver(input, staged);
|
|
822
|
+
if (!delivered.ok) return delivered;
|
|
823
|
+
controlPlane = { repository: staged.repository, commit: staged.sha };
|
|
824
|
+
log.line(`${tag} governed context ${staged.sha.slice(0, 7)} delivered to ${deliveryDir(staged.repository, staged.sha)} (${staged.files.length} files)`);
|
|
1008
825
|
} finally {
|
|
1009
826
|
input.removeStagedContext();
|
|
1010
827
|
}
|
|
1011
828
|
if (input.aborted()) return aborted();
|
|
1012
829
|
}
|
|
1013
|
-
return { ok: true, startCommit };
|
|
830
|
+
return { ok: true, startCommit, ...controlPlane === void 0 ? {} : { controlPlane } };
|
|
1014
831
|
}
|
|
1015
832
|
var EVAL_INPUT_NAME = "eval-input";
|
|
1016
833
|
var TRANSCRIPT_ROOT = "/build/home/.claude/projects";
|
|
@@ -1205,6 +1022,28 @@ async function writeEvalInput(request) {
|
|
|
1205
1022
|
writeReadOnly(join(dir, "activity.json"), `${JSON.stringify(activity, null, 2)}
|
|
1206
1023
|
`);
|
|
1207
1024
|
}
|
|
1025
|
+
var MAX_TREE_FILES = 2e4;
|
|
1026
|
+
var MAX_TREE_FILE_BYTES = 1024 * 1024;
|
|
1027
|
+
function copyJudgeTree(from, to, limits2 = { files: MAX_TREE_FILES, bytes: MAX_TREE_FILE_BYTES }) {
|
|
1028
|
+
let copied = 0;
|
|
1029
|
+
const walk = (src, dest, top) => {
|
|
1030
|
+
mkdirSync(dest);
|
|
1031
|
+
chmodSync(dest, 493);
|
|
1032
|
+
for (const name of readdirSync(src).sort()) {
|
|
1033
|
+
if (copied >= limits2.files) return;
|
|
1034
|
+
if (top && name === ".git") continue;
|
|
1035
|
+
const path = join(src, name);
|
|
1036
|
+
const stat = lstatSync(path);
|
|
1037
|
+
if (stat.isDirectory()) walk(path, join(dest, name), false);
|
|
1038
|
+
else if (stat.isFile() && stat.size <= limits2.bytes) {
|
|
1039
|
+
copyFileSync(path, join(dest, name));
|
|
1040
|
+
chmodSync(join(dest, name), 420);
|
|
1041
|
+
copied += 1;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
};
|
|
1045
|
+
walk(from, to, true);
|
|
1046
|
+
}
|
|
1208
1047
|
|
|
1209
1048
|
// src/runner/container/agent-user.ts
|
|
1210
1049
|
var ADDED_USER = "ol-agent";
|
|
@@ -1219,189 +1058,6 @@ async function resolveAgentUser(input) {
|
|
|
1219
1058
|
if (uid === null) throw new AgentUserError(`the user "${candidate}" is not in the image, so no command can run as it`);
|
|
1220
1059
|
return uid === 0 ? ADDED_USER : candidate;
|
|
1221
1060
|
}
|
|
1222
|
-
var TokenRefused = class extends Error {
|
|
1223
|
-
constructor(code, detail) {
|
|
1224
|
-
super(code);
|
|
1225
|
-
this.code = code;
|
|
1226
|
-
this.detail = detail;
|
|
1227
|
-
}
|
|
1228
|
-
};
|
|
1229
|
-
var NoControlPlane = class extends Error {
|
|
1230
|
-
};
|
|
1231
|
-
var GatewayConflict = class extends Error {
|
|
1232
|
-
};
|
|
1233
|
-
var unavailable = (why) => ({ kind: "refused", reason: `context_unavailable: ${why}` });
|
|
1234
|
-
var DESIGNATION_LOOKUP = { timeoutMs: 1e4, attempts: 2 };
|
|
1235
|
-
function lookupFailureReason(failure, repository) {
|
|
1236
|
-
switch (failure?.kind) {
|
|
1237
|
-
case "timeout":
|
|
1238
|
-
return `the gateway did not answer which repository governs ${repository} within ${failure.timeoutMs / 1e3} seconds, twice`;
|
|
1239
|
-
case "status":
|
|
1240
|
-
return `the gateway answered ${failure.status} when asked which repository governs ${repository}`;
|
|
1241
|
-
case "network":
|
|
1242
|
-
return `the request asking which repository governs ${repository} failed: ${failure.message}`;
|
|
1243
|
-
default:
|
|
1244
|
-
return `the gateway did not say which repository governs ${repository}`;
|
|
1245
|
-
}
|
|
1246
|
-
}
|
|
1247
|
-
function makeReadable(tree, dir, files) {
|
|
1248
|
-
chmodSync(dir, 493);
|
|
1249
|
-
chmodSync(tree, 493);
|
|
1250
|
-
const directories = /* @__PURE__ */ new Set();
|
|
1251
|
-
for (const rel of files) {
|
|
1252
|
-
for (let current = dirname(join(tree, rel)); current !== tree; current = dirname(current)) directories.add(current);
|
|
1253
|
-
chmodSync(join(tree, rel), (lstatSync(join(tree, rel)).mode & 73) !== 0 ? 493 : 420);
|
|
1254
|
-
}
|
|
1255
|
-
for (const directory of directories) chmodSync(directory, 493);
|
|
1256
|
-
}
|
|
1257
|
-
async function stageGovernedContext(input) {
|
|
1258
|
-
const git2 = input.git ?? defaultGitRunner;
|
|
1259
|
-
const dir = stagedContextDir(input.buildDir);
|
|
1260
|
-
const tree = join(dir, "tree");
|
|
1261
|
-
const finish = (outcome) => {
|
|
1262
|
-
removeStagedContext(input.buildDir);
|
|
1263
|
-
return outcome;
|
|
1264
|
-
};
|
|
1265
|
-
try {
|
|
1266
|
-
mkdirSync(tree, { recursive: true });
|
|
1267
|
-
for (const args of [
|
|
1268
|
-
["init", "-q", "--initial-branch=main", tree],
|
|
1269
|
-
["-C", tree, "remote", "add", "origin", `https://github.com/${input.repository}.git`]
|
|
1270
|
-
]) {
|
|
1271
|
-
const done = git2(args, { cwd: dir, timeoutMs: 1e4 });
|
|
1272
|
-
if (!done.ok) return finish(unavailable(`the staging directory could not be prepared: ${done.stderr.trim().split("\n")[0] ?? ""}`));
|
|
1273
|
-
}
|
|
1274
|
-
const { url, apiKey, appId, configPath } = input.config;
|
|
1275
|
-
let tokenRepository;
|
|
1276
|
-
const credentialFor = (repository) => tokenRepository !== void 0 && tokenRepository.toLowerCase() === repository.toLowerCase() ? "the token the gateway issued" : "the host's git login";
|
|
1277
|
-
const unreadable = (repositories) => repositories.map((repository) => `${credentialFor(repository)} cannot read ${repository}`).join(" and ");
|
|
1278
|
-
const result = await materializeContext({
|
|
1279
|
-
cwd: tree,
|
|
1280
|
-
sessionId: `build-${input.buildId}`,
|
|
1281
|
-
home: contextHomeDir(configPath),
|
|
1282
|
-
cloud: { url, apiKey, appId },
|
|
1283
|
-
fetchImpl: signingFetchFor(input.fetchImpl, configPath, apiKey),
|
|
1284
|
-
designationLookup: DESIGNATION_LOOKUP,
|
|
1285
|
-
git: git2,
|
|
1286
|
-
now: input.now,
|
|
1287
|
-
resolveControlPlaneUrl: (repository) => input.controlPlaneUrl(repository),
|
|
1288
|
-
controlPlaneAccess: input.controlPlaneToken === void 0 ? void 0 : async (candidates) => {
|
|
1289
|
-
const access = await controlPlaneAccess(input.controlPlaneToken, candidates, input.repository, input.log, input.tag);
|
|
1290
|
-
tokenRepository = access?.repository;
|
|
1291
|
-
return access;
|
|
1292
|
-
}
|
|
1293
|
-
});
|
|
1294
|
-
switch (result.status) {
|
|
1295
|
-
case "materialized": {
|
|
1296
|
-
if (result.unfetched !== void 0 && result.unfetched.length > 0) {
|
|
1297
|
-
return finish(unavailable(`${unreadable(result.unfetched)}, a control plane named for ${input.repository}, so it cannot be ruled out as a second claimant`));
|
|
1298
|
-
}
|
|
1299
|
-
const manifest = readContextManifest(join(tree, ".git"));
|
|
1300
|
-
const source = readSourceCache(join(tree, ".git"));
|
|
1301
|
-
if (result.sha === null || manifest === void 0 || source?.status !== "governed" || source.repository === void 0 || source.ref === void 0) {
|
|
1302
|
-
return finish(unavailable(`the context of ${input.repository} was materialized without a record of it`));
|
|
1303
|
-
}
|
|
1304
|
-
writeFileSync(
|
|
1305
|
-
join(dir, "staged.json"),
|
|
1306
|
-
JSON.stringify({ schemaVersion: 1, sha: result.sha, repository: source.repository, ref: source.ref, files: manifest.files }) + "\n"
|
|
1307
|
-
);
|
|
1308
|
-
makeReadable(tree, dir, manifest.files);
|
|
1309
|
-
chmodSync(join(dir, "staged.json"), 420);
|
|
1310
|
-
return { kind: "staged", dir, sha: result.sha, files: manifest.files };
|
|
1311
|
-
}
|
|
1312
|
-
case "self":
|
|
1313
|
-
if (result.unfetched !== void 0 && result.unfetched.length > 0) {
|
|
1314
|
-
return finish(unavailable(`${unreadable(result.unfetched)}, the control plane named for ${input.repository}`));
|
|
1315
|
-
}
|
|
1316
|
-
if (result.designation === "answered") return finish({ kind: "none" });
|
|
1317
|
-
if (result.authRejected === true) {
|
|
1318
|
-
return finish(unavailable(`the gateway rejected the runner's API key (401/403) when asked which repository governs ${input.repository}`));
|
|
1319
|
-
}
|
|
1320
|
-
return finish(unavailable(lookupFailureReason(result.lookupFailure, input.repository)));
|
|
1321
|
-
case "conflict":
|
|
1322
|
-
return finish({ kind: "refused", reason: `context_conflict: ${result.message ?? "two control planes claim this repository"}` });
|
|
1323
|
-
default:
|
|
1324
|
-
return finish(unavailable(result.message ?? `the context of ${input.repository} could not be materialized (${result.status})`));
|
|
1325
|
-
}
|
|
1326
|
-
} catch (err) {
|
|
1327
|
-
if (err instanceof NoControlPlane) return finish({ kind: "none" });
|
|
1328
|
-
if (err instanceof GatewayConflict) return finish({ kind: "refused", reason: `context_conflict: ${err.message}` });
|
|
1329
|
-
if (err instanceof TokenRefused) {
|
|
1330
|
-
return finish(
|
|
1331
|
-
unavailable(
|
|
1332
|
-
err.detail === void 0 ? `the gateway refused a token for the control plane named for ${input.repository} (${err.code})` : `the token request for the control plane named for ${input.repository} failed: ${err.detail}`
|
|
1333
|
-
)
|
|
1334
|
-
);
|
|
1335
|
-
}
|
|
1336
|
-
return finish(unavailable(`the context of ${input.repository} could not be staged: ${err instanceof Error ? err.message : String(err)}`));
|
|
1337
|
-
}
|
|
1338
|
-
}
|
|
1339
|
-
async function controlPlaneAccess(ask, candidates, repository, log, tag) {
|
|
1340
|
-
let answer;
|
|
1341
|
-
try {
|
|
1342
|
-
answer = await ask();
|
|
1343
|
-
} catch (err) {
|
|
1344
|
-
const code = refusalCode(err);
|
|
1345
|
-
if (code === "repository_tokens_unavailable") return void 0;
|
|
1346
|
-
if (code === "no_control_plane") throw new NoControlPlane(code);
|
|
1347
|
-
if (code === "context_conflict") throw new GatewayConflict(err instanceof Error ? err.message : code);
|
|
1348
|
-
throw new TokenRefused(code, err instanceof WorkCommandError && err.code === void 0 && err.status === void 0 ? describeCallFailure(err) : void 0);
|
|
1349
|
-
}
|
|
1350
|
-
const named = candidates.some((candidate) => candidate.repository.toLowerCase() === answer.repository.toLowerCase());
|
|
1351
|
-
if (!named) {
|
|
1352
|
-
log?.line(
|
|
1353
|
-
`${tag === void 0 ? "" : `${tag} `}the gateway issued a control plane token for ${answer.repository}, which is none of the repositories named for ${repository} (${candidates.map((candidate) => candidate.repository).join(", ")}) \xB7 fetching with the host's git login`
|
|
1354
|
-
);
|
|
1355
|
-
return void 0;
|
|
1356
|
-
}
|
|
1357
|
-
return answer;
|
|
1358
|
-
}
|
|
1359
|
-
function removeStagedContext(buildDir2) {
|
|
1360
|
-
rmSync(stagedContextDir(buildDir2), { recursive: true, force: true });
|
|
1361
|
-
}
|
|
1362
|
-
|
|
1363
|
-
// src/runner/container/docker.ts
|
|
1364
|
-
function createContainerHost(deps = {}) {
|
|
1365
|
-
const run = deps.run ?? spawnRun;
|
|
1366
|
-
return {
|
|
1367
|
-
async version() {
|
|
1368
|
-
const result = await run("docker", ["version", "--format", "{{.Server.Version}}"]);
|
|
1369
|
-
return result.status === 0 && result.stdout.trim() !== "" ? result.stdout.trim() : null;
|
|
1370
|
-
},
|
|
1371
|
-
async operatingSystem() {
|
|
1372
|
-
const result = await run("docker", ["info", "--format", "{{.OperatingSystem}}"]);
|
|
1373
|
-
return result.status === 0 && result.stdout.trim() !== "" ? result.stdout.trim() : null;
|
|
1374
|
-
},
|
|
1375
|
-
async imageUser(imageId) {
|
|
1376
|
-
const result = await run("docker", ["image", "inspect", "--format", "{{.Config.User}}", imageId]);
|
|
1377
|
-
return result.status === 0 ? result.stdout.trim() : "";
|
|
1378
|
-
},
|
|
1379
|
-
createVolume(name, labels) {
|
|
1380
|
-
const flags = Object.entries(labels).flatMap(([key, value]) => ["--label", `${key}=${value}`]);
|
|
1381
|
-
return run("docker", ["volume", "create", ...flags, name]);
|
|
1382
|
-
},
|
|
1383
|
-
removeVolume(name) {
|
|
1384
|
-
return run("docker", ["volume", "rm", "--force", name]);
|
|
1385
|
-
},
|
|
1386
|
-
run(args, opts) {
|
|
1387
|
-
return run("docker", ["run", ...args], opts);
|
|
1388
|
-
},
|
|
1389
|
-
exec(input, opts) {
|
|
1390
|
-
const env = {};
|
|
1391
|
-
for (const [name, value] of Object.entries(input.env.passed)) if (value !== void 0) env[name] = value;
|
|
1392
|
-
return run("docker", ["exec", ...containerExecArgs(input)], { ...opts, env });
|
|
1393
|
-
},
|
|
1394
|
-
stop(container) {
|
|
1395
|
-
return run("docker", ["stop", container]);
|
|
1396
|
-
},
|
|
1397
|
-
start(container) {
|
|
1398
|
-
return run("docker", ["start", container]);
|
|
1399
|
-
},
|
|
1400
|
-
remove(container) {
|
|
1401
|
-
return run("docker", ["rm", "--force", "--volumes", container]);
|
|
1402
|
-
}
|
|
1403
|
-
};
|
|
1404
|
-
}
|
|
1405
1061
|
var FILE_MODE = 420;
|
|
1406
1062
|
var DIR_MODE = 493;
|
|
1407
1063
|
function replaceFile(path, text) {
|
|
@@ -2289,6 +1945,7 @@ function createContainerRuntime(opts = {}) {
|
|
|
2289
1945
|
configPath: build.configPath,
|
|
2290
1946
|
cli: cli ?? currentCliBuild(),
|
|
2291
1947
|
readToken: build.readToken,
|
|
1948
|
+
pinned: build.pinned,
|
|
2292
1949
|
guestHelper: build.guestHelper,
|
|
2293
1950
|
log: build.log
|
|
2294
1951
|
},
|
|
@@ -2359,8 +2016,9 @@ async function obtainImage(input) {
|
|
|
2359
2016
|
if (input.aborted()) return { ok: false, reason: "ended before provision finished", aborted: true };
|
|
2360
2017
|
let readToken;
|
|
2361
2018
|
try {
|
|
2362
|
-
readToken = await tokens.token("read");
|
|
2019
|
+
readToken = input.pinned === void 0 ? await tokens.token("read") : await controlPlaneReadToken(tokens, repository);
|
|
2363
2020
|
} catch (err) {
|
|
2021
|
+
if (input.pinned !== void 0) return { ok: false, reason: controlPlaneTokenFailure(err, input.governedRepository ?? repository) };
|
|
2364
2022
|
return {
|
|
2365
2023
|
ok: false,
|
|
2366
2024
|
reason: err instanceof WorkCommandError ? `the build's read token could not be had: ${refusalCode(err)}` : `the build's read token could not be had: ${describeCallFailure(err)}`
|
|
@@ -2374,6 +2032,7 @@ async function obtainImage(input) {
|
|
|
2374
2032
|
configPath: config.configPath,
|
|
2375
2033
|
buildMemory: config.build?.memory ?? "8g",
|
|
2376
2034
|
readToken,
|
|
2035
|
+
pinned: input.pinned,
|
|
2377
2036
|
guestHelper: input.guestHelper,
|
|
2378
2037
|
log: (line) => log.line(`${tag} ${line}`)
|
|
2379
2038
|
})
|
|
@@ -2393,6 +2052,16 @@ async function obtainImage(input) {
|
|
|
2393
2052
|
}
|
|
2394
2053
|
}
|
|
2395
2054
|
}
|
|
2055
|
+
async function controlPlaneReadToken(tokens, controlPlane) {
|
|
2056
|
+
try {
|
|
2057
|
+
const issued = await tokens.controlPlane();
|
|
2058
|
+
return issued.repository.toLowerCase() === controlPlane.toLowerCase() ? issued.token : void 0;
|
|
2059
|
+
} catch (err) {
|
|
2060
|
+
const code = refusalCode(err);
|
|
2061
|
+
if (code === "repository_tokens_unavailable" || code === "no_control_plane" || code === "context_conflict") return void 0;
|
|
2062
|
+
throw err;
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2396
2065
|
async function provisionContainer(input) {
|
|
2397
2066
|
const { config, runtime, item, itemKey, jobDir, log, tokens } = input;
|
|
2398
2067
|
const { host } = runtime;
|
|
@@ -2412,7 +2081,23 @@ async function provisionContainer(input) {
|
|
|
2412
2081
|
if (/docker desktop/i.test(await host.operatingSystem() ?? "")) {
|
|
2413
2082
|
return fail("Docker Desktop cannot pass the tunnel's socket into a container, so container builds need Docker Engine: install it (on Windows, inside WSL2) or use builtin:process");
|
|
2414
2083
|
}
|
|
2415
|
-
const
|
|
2084
|
+
const governing = await resolveGoverningSource({ config, repository, fetchImpl: input.fetchImpl });
|
|
2085
|
+
if (!governing.ok) return fail(governing.reason);
|
|
2086
|
+
const governingSource = governing.source;
|
|
2087
|
+
const imageRepository = governingSource.kind === "governed" ? governingSource.repository : repository;
|
|
2088
|
+
const obtained = await obtainImage({
|
|
2089
|
+
config,
|
|
2090
|
+
runtime,
|
|
2091
|
+
tokens,
|
|
2092
|
+
repository: imageRepository,
|
|
2093
|
+
pinned: governingSource.kind === "governed" ? { commit: governingSource.sha, ref: governingSource.ref } : void 0,
|
|
2094
|
+
governedRepository: repository,
|
|
2095
|
+
tag,
|
|
2096
|
+
log,
|
|
2097
|
+
aborted: input.aborted,
|
|
2098
|
+
deadlineMs: input.deadlineMs,
|
|
2099
|
+
now: input.now
|
|
2100
|
+
});
|
|
2416
2101
|
if (!obtained.ok) return obtained.aborted === true ? { ok: false, reason: obtained.reason, aborted: true } : fail(obtained.reason);
|
|
2417
2102
|
const built = obtained.built;
|
|
2418
2103
|
const { report } = built;
|
|
@@ -2429,9 +2114,10 @@ async function provisionContainer(input) {
|
|
|
2429
2114
|
containerEnv: report.apply.containerEnv,
|
|
2430
2115
|
builtIns: true,
|
|
2431
2116
|
lifecycle: report.apply.lifecycle,
|
|
2117
|
+
...governingSource.kind === "governed" ? { governing: { repository: governingSource.repository, ref: governingSource.ref, commit: governingSource.sha } } : {},
|
|
2432
2118
|
cpuset: formatCpuList(runtime.cpus.acquire(buildId, config.build?.cpus ?? 4))
|
|
2433
2119
|
};
|
|
2434
|
-
const
|
|
2120
|
+
const envInput = {
|
|
2435
2121
|
config,
|
|
2436
2122
|
item: item.number,
|
|
2437
2123
|
itemKey,
|
|
@@ -2440,7 +2126,8 @@ async function provisionContainer(input) {
|
|
|
2440
2126
|
hostEnv: runtime.hostEnv,
|
|
2441
2127
|
containerEnv: report.apply.containerEnv,
|
|
2442
2128
|
builtIns: true
|
|
2443
|
-
}
|
|
2129
|
+
};
|
|
2130
|
+
const env = buildEnvironment(envInput);
|
|
2444
2131
|
if (env.skippedTokenVariables.length > 0) {
|
|
2445
2132
|
log.line(`${tag} not passing ${env.skippedTokenVariables.join(", ")} from runner.build.variables into the build: its repository token comes from the runner`);
|
|
2446
2133
|
}
|
|
@@ -2495,6 +2182,9 @@ async function provisionContainer(input) {
|
|
|
2495
2182
|
const volume = await host.createVolume(name, { [BUILD_LABEL]: buildId, [RUNNER_LABEL]: runnerId });
|
|
2496
2183
|
if (volume.status !== 0) return dockerFailed("creating the build's volume", volume);
|
|
2497
2184
|
if (input.aborted()) return bail("", true);
|
|
2185
|
+
const judgeDir = join(jobDir, "judge");
|
|
2186
|
+
mkdirSync(judgeDir, { recursive: true });
|
|
2187
|
+
chmodSync(judgeDir, 493);
|
|
2498
2188
|
const runInput = {
|
|
2499
2189
|
buildId,
|
|
2500
2190
|
runnerId,
|
|
@@ -2505,7 +2195,8 @@ async function provisionContainer(input) {
|
|
|
2505
2195
|
user: ADDED_USER,
|
|
2506
2196
|
limits: limits2,
|
|
2507
2197
|
destinations: config.build?.destinations?.length ?? 0,
|
|
2508
|
-
builtIns: true
|
|
2198
|
+
builtIns: true,
|
|
2199
|
+
judgeDir
|
|
2509
2200
|
};
|
|
2510
2201
|
const prepared = await host.run(prepareRunArgs(runInput), { logPath });
|
|
2511
2202
|
if (prepared.status !== 0) return dockerFailed("preparing the build's volume", prepared);
|
|
@@ -2513,12 +2204,13 @@ async function provisionContainer(input) {
|
|
|
2513
2204
|
if (started.status !== 0) return dockerFailed("starting the build's container", started);
|
|
2514
2205
|
if (input.aborted()) return bail("", true);
|
|
2515
2206
|
if (!await waitForRelay(runtime, state)) return bail("the relay in the build's container did not start");
|
|
2516
|
-
const
|
|
2207
|
+
const imageConfig = await host.imageConfig(report.imageId);
|
|
2208
|
+
state.imageEnv = imageConfig.env;
|
|
2517
2209
|
let agentUser;
|
|
2518
2210
|
try {
|
|
2519
2211
|
agentUser = await resolveAgentUser({
|
|
2520
2212
|
remoteUser: report.apply.remoteUser,
|
|
2521
|
-
imageUser,
|
|
2213
|
+
imageUser: imageConfig.user,
|
|
2522
2214
|
probe: async (user) => {
|
|
2523
2215
|
const probe = await host.exec({ container: name, user: ADDED_USER, env: NO_ENV, argv: ["id", "-u", user] });
|
|
2524
2216
|
const uid = Number(probe.stdout);
|
|
@@ -2531,13 +2223,20 @@ async function provisionContainer(input) {
|
|
|
2531
2223
|
}
|
|
2532
2224
|
state.agentUser = agentUser;
|
|
2533
2225
|
const tools = await host.exec({ container: name, user: ADDED_USER, env: NO_ENV, argv: ["cat", "/opt/outerlayer/tools.json"] });
|
|
2534
|
-
|
|
2226
|
+
const agentVersion = agentVersionOf(tools.status === 0 ? tools.stdout : "");
|
|
2227
|
+
environment.agentVersion = agentVersion;
|
|
2228
|
+
const program = await host.exec({ container: name, user: ADDED_USER, env: NO_ENV, argv: [...HASH_ARGV] });
|
|
2229
|
+
const hashed = program.status === 0 ? parseProgramHash(program.stdout) : void 0;
|
|
2230
|
+
if (hashed !== void 0) state.judgeProgram = { path: JUDGE_PROGRAM, sha256: hashed };
|
|
2535
2231
|
input.save(state, environment);
|
|
2536
|
-
const
|
|
2232
|
+
const execEnv = buildEnvironment({ ...envInput, imageEnv: state.imageEnv });
|
|
2233
|
+
const exec = (argv, workdir, opts) => host.exec({ container: name, user: agentUser, workdir, env: execEnv, argv }, opts?.quiet === true ? void 0 : { logPath });
|
|
2537
2234
|
const steps = await runBuildSteps({
|
|
2538
2235
|
repository,
|
|
2539
2236
|
branch: primary.branch,
|
|
2540
2237
|
repositories,
|
|
2238
|
+
...governingSource.kind === "governed" ? { governing: { repository: governingSource.repository } } : {},
|
|
2239
|
+
agentVersion,
|
|
2541
2240
|
lifecycle: report.apply.lifecycle,
|
|
2542
2241
|
exec,
|
|
2543
2242
|
log,
|
|
@@ -2547,6 +2246,7 @@ async function provisionContainer(input) {
|
|
|
2547
2246
|
stageContext: () => stageGovernedContext({
|
|
2548
2247
|
config,
|
|
2549
2248
|
repository,
|
|
2249
|
+
source: pinnedSourceOf(governingSource),
|
|
2550
2250
|
buildId,
|
|
2551
2251
|
buildDir: buildDir2,
|
|
2552
2252
|
fetchImpl: input.fetchImpl,
|
|
@@ -2559,6 +2259,7 @@ async function provisionContainer(input) {
|
|
|
2559
2259
|
});
|
|
2560
2260
|
if (!steps.ok) return bail(steps.reason, steps.aborted === true);
|
|
2561
2261
|
state.startCommit = steps.startCommit;
|
|
2262
|
+
if (steps.controlPlane !== void 0) environment.controlPlane = steps.controlPlane;
|
|
2562
2263
|
return { ok: true, workdir: CHECKOUT_DIR, state, environment };
|
|
2563
2264
|
}
|
|
2564
2265
|
function agentVersionOf(toolsJson) {
|
|
@@ -2584,14 +2285,15 @@ function containerCommandLine(input) {
|
|
|
2584
2285
|
hostEnv: input.runtime.hostEnv,
|
|
2585
2286
|
containerEnv: state.containerEnv ?? {},
|
|
2586
2287
|
builtIns: state.builtIns === true,
|
|
2587
|
-
identity: input.job.commitIdentity
|
|
2288
|
+
identity: input.job.commitIdentity,
|
|
2289
|
+
imageEnv: state.imageEnv
|
|
2588
2290
|
});
|
|
2589
2291
|
const args = containerExecArgs({
|
|
2590
2292
|
container: state.container,
|
|
2591
2293
|
user: state.agentUser ?? ADDED_USER,
|
|
2592
2294
|
workdir: CHECKOUT_DIR,
|
|
2593
2295
|
env,
|
|
2594
|
-
argv: ["/bin/sh", "-c", input.cmdline]
|
|
2296
|
+
argv: ["/bin/sh", "-c", commandWithShim(input.job, input.cmdline)]
|
|
2595
2297
|
});
|
|
2596
2298
|
return {
|
|
2597
2299
|
line: ["docker", "exec", ...args].map(shellQuote).join(" "),
|
|
@@ -2610,7 +2312,8 @@ function containerSenderCommand(input) {
|
|
|
2610
2312
|
hostEnv: input.runtime.hostEnv,
|
|
2611
2313
|
containerEnv: state.containerEnv ?? {},
|
|
2612
2314
|
builtIns: state.builtIns === true,
|
|
2613
|
-
identity: input.job.commitIdentity
|
|
2315
|
+
identity: input.job.commitIdentity,
|
|
2316
|
+
imageEnv: state.imageEnv
|
|
2614
2317
|
});
|
|
2615
2318
|
const args = containerExecArgs({
|
|
2616
2319
|
container: state.container,
|
|
@@ -2687,7 +2390,8 @@ async function syncInContainer(input) {
|
|
|
2687
2390
|
hostEnv: runtime.hostEnv,
|
|
2688
2391
|
containerEnv: state.containerEnv ?? {},
|
|
2689
2392
|
builtIns: state.builtIns === true,
|
|
2690
|
-
identity: job.commitIdentity
|
|
2393
|
+
identity: job.commitIdentity,
|
|
2394
|
+
imageEnv: state.imageEnv
|
|
2691
2395
|
});
|
|
2692
2396
|
const result = await runtime.host.exec(
|
|
2693
2397
|
{ container: state.container, user: state.agentUser ?? ADDED_USER, workdir: job.workdir ?? CHECKOUT_DIR, env, argv: ["outerlayer", "sync"] },
|
|
@@ -2745,6 +2449,53 @@ async function cleanupContainer(runtime, state, jobDir) {
|
|
|
2745
2449
|
runtime.cpus.release(state.buildId);
|
|
2746
2450
|
if (failures.length > 0) throw new Error(failures.join("; "));
|
|
2747
2451
|
}
|
|
2452
|
+
function containerJudgeEnvironment(input) {
|
|
2453
|
+
const { runtime, job } = input;
|
|
2454
|
+
const state = job.container;
|
|
2455
|
+
const user = state.agentUser ?? ADDED_USER;
|
|
2456
|
+
const folder = join(input.jobDir, "judge");
|
|
2457
|
+
return {
|
|
2458
|
+
stop: async () => {
|
|
2459
|
+
if (input.config !== void 0) await ensureTunnel(runtime, state, input.jobDir, input.config, job.itemKey, job.branch, job.repositories);
|
|
2460
|
+
await stopContainer(runtime, state);
|
|
2461
|
+
const started = await runtime.host.start(state.container);
|
|
2462
|
+
if (started.status !== 0) throw new Error(`the build's container could not be started again (exit ${started.status})`);
|
|
2463
|
+
state.stopped = false;
|
|
2464
|
+
if (!await waitForRelay(runtime, state)) throw new Error("the relay in the build's container did not start again");
|
|
2465
|
+
},
|
|
2466
|
+
hashProgram: async () => {
|
|
2467
|
+
const ran = await runtime.host.exec({ container: state.container, user: ADDED_USER, env: NO_ENV, argv: [...HASH_ARGV] });
|
|
2468
|
+
const sha256 = ran.status === 0 ? parseProgramHash(ran.stdout) : void 0;
|
|
2469
|
+
return sha256 === void 0 ? { ok: false, reason: firstLine5(ran.stderr) || `the hash could not be taken (exit ${ran.status})` } : { ok: true, sha256 };
|
|
2470
|
+
},
|
|
2471
|
+
prepare: async (files, tree) => {
|
|
2472
|
+
mkdirSync(folder, { recursive: true });
|
|
2473
|
+
for (const entry of readdirSync(folder)) rmSync(join(folder, entry), { recursive: true, force: true });
|
|
2474
|
+
mkdirSync(join(folder, "home", "tmp"), { recursive: true });
|
|
2475
|
+
for (const [fileName, text] of Object.entries(files)) writeFileSync(join(folder, fileName), text, { mode: 420 });
|
|
2476
|
+
if (tree !== void 0) copyJudgeTree(tree, join(folder, "tree"));
|
|
2477
|
+
chmodSync(join(folder, "home"), 511);
|
|
2478
|
+
chmodSync(join(folder, "home", "tmp"), 511);
|
|
2479
|
+
chmodSync(folder, 1023);
|
|
2480
|
+
},
|
|
2481
|
+
run: (command, opts) => runtime.host.exec({ container: state.container, user, workdir: command.cwd, env: { literals: command.env, passed: {} }, argv: command.argv }, { logPath: opts.logPath }),
|
|
2482
|
+
readReport: async () => readRegularFile(join(folder, "report.html"), MAX_REPORT_BYTES),
|
|
2483
|
+
// The judge's files belong to the build's user, and Claude Code makes some of its folders private to it, so the runner's user cannot remove them from the
|
|
2484
|
+
// host. Every capability is dropped in the container, so root there could not either; the build's user removes its own, and the runner removes the rest.
|
|
2485
|
+
clear: async () => {
|
|
2486
|
+
const removed = await runtime.host.exec({ container: state.container, user, env: NO_ENV, argv: ["find", JUDGE_DIR, "-mindepth", "1", "-user", user, "-delete"] });
|
|
2487
|
+
if (removed.status !== 0) throw new Error(`find exited ${removed.status}: ${firstLine5(removed.stderr)}`);
|
|
2488
|
+
}
|
|
2489
|
+
};
|
|
2490
|
+
}
|
|
2491
|
+
function readRegularFile(path, limit) {
|
|
2492
|
+
try {
|
|
2493
|
+
const stat = lstatSync(path);
|
|
2494
|
+
return stat.isFile() && stat.size <= limit ? readFileSync(path, "utf8") : void 0;
|
|
2495
|
+
} catch {
|
|
2496
|
+
return void 0;
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2748
2499
|
async function startCheckContainer(input) {
|
|
2749
2500
|
const { config, runtime, job, jobDir, tokens, log } = input;
|
|
2750
2501
|
const { host } = runtime;
|
|
@@ -2759,7 +2510,7 @@ async function startCheckContainer(input) {
|
|
|
2759
2510
|
const buildDir2 = buildDir(config.configPath, buildId);
|
|
2760
2511
|
const agentUser = built.agentUser ?? ADDED_USER;
|
|
2761
2512
|
const cpuset = built.cpuset ?? formatCpuList(runtime.cpus.acquire(buildId, config.build?.cpus ?? 4));
|
|
2762
|
-
const state = { buildId, container: name, volume: name, buildDir: buildDir2, repository, imageId: built.imageId, agentUser, cpuset };
|
|
2513
|
+
const state = { buildId, container: name, volume: name, buildDir: buildDir2, repository, imageId: built.imageId, imageEnv: built.imageEnv, agentUser, cpuset };
|
|
2763
2514
|
const checksDir = join(jobDir, "checks");
|
|
2764
2515
|
mkdirSync(checksDir, { recursive: true });
|
|
2765
2516
|
const logPath = join(checksDir, "environment.log");
|
|
@@ -2808,7 +2559,7 @@ async function startCheckContainer(input) {
|
|
|
2808
2559
|
const started = await host.run(containerRunArgs(runInput), { logPath });
|
|
2809
2560
|
if (started.status !== 0) return dockerFailed("starting the check environment's container", started);
|
|
2810
2561
|
if (!await waitForRelay(runtime, state)) return fail("the relay in the check environment's container did not start");
|
|
2811
|
-
const env = checkEnvironment({ config, item: job.item, repository, hostEnv: runtime.hostEnv, containerEnv: built.containerEnv ?? {} });
|
|
2562
|
+
const env = checkEnvironment({ config, item: job.item, repository, hostEnv: runtime.hostEnv, containerEnv: built.containerEnv ?? {}, imageEnv: built.imageEnv });
|
|
2812
2563
|
return {
|
|
2813
2564
|
ok: true,
|
|
2814
2565
|
container: {
|
|
@@ -2825,4 +2576,4 @@ async function startCheckContainer(input) {
|
|
|
2825
2576
|
};
|
|
2826
2577
|
}
|
|
2827
2578
|
|
|
2828
|
-
export { ADDED_USER, AgentUserError, AssetError,
|
|
2579
|
+
export { ADDED_USER, AgentUserError, AssetError, EVAL_INPUT_NAME, GH_CONFIG_YML, MIN_ENGINE_MAJOR, cleanupContainer, containerCommandLine, containerJudgeEnvironment, containerSenderCommand, copyJudgeTree, copyTranscripts, copyTranscriptsFromContainer, createBuildDisk, createContainerRuntime, deleteBuildDisk, ensureRootfs, ensureTunnel, ghHostsYml, gitRemoteFor, isLiteralAddress, keepGhConfigCurrent, lifecycleFailureReason, lifecycleSteps, machineConfig, measureDisk, obtainImage, outOfMemoryReason, parseCpuList, prepareBranch, provisionContainer, resolveAgentUser, runBuildSteps, senderSignalScript, setUpRunnerCgroup, shellQuote, signalInContainer, startCheckContainer, startServicesFor, stopContainer, syncInContainer, writeEvalInput };
|