@outerlayer/cli 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +72 -0
- package/README.md +36 -2
- package/dist/agent-setup-TPR4BBFV.js +27 -0
- package/dist/{artifact-link-cmd-2F47LT7F.js → artifact-link-cmd-W7XF5BFB.js} +4 -3
- package/dist/{build-BG2YENGK.js → build-TQ6X5PYN.js} +5 -4
- package/dist/build-info.json +1 -1
- package/dist/{builder-2D3WSOOY.js → builder-C4QNTYMS.js} +5 -6
- package/dist/{check-5XE7XMC4.js → check-UWQAL2MN.js} +56 -52
- package/dist/{chunk-R7PF5OOU.js → chunk-3CWUOFV6.js} +21 -14
- package/dist/{chunk-4NPZVMJU.js → chunk-3UZTIRLF.js} +60 -4
- package/dist/chunk-452TM7J4.js +502 -0
- package/dist/{chunk-NNRTPG7X.js → chunk-5GHGAF6P.js} +4 -4
- package/dist/{chunk-WVUIRTWA.js → chunk-5MYFOW5V.js} +196 -37
- package/dist/{chunk-HGECYFEI.js → chunk-5TN7GLYX.js} +138 -109
- package/dist/{chunk-VQYBXPI7.js → chunk-6D7GT22T.js} +5 -5
- package/dist/{chunk-G5NPRL2K.js → chunk-6U3MV7TZ.js} +687 -76
- package/dist/{chunk-42ZI52JW.js → chunk-6X4SQ5MY.js} +4 -3
- package/dist/{chunk-MAK5LYRF.js → chunk-7KSSBQTS.js} +278 -535
- package/dist/{chunk-NWQTTSBB.js → chunk-7SJPQWBM.js} +41 -16
- package/dist/{chunk-J5M7BSJQ.js → chunk-7VUK5RFS.js} +23 -4
- package/dist/{chunk-OZGHL5Y5.js → chunk-7YP7T6GV.js} +2 -2
- package/dist/{chunk-TI7B7UEO.js → chunk-AMFWYESY.js} +1 -1
- package/dist/{chunk-YRYMNBGQ.js → chunk-ANPNT6PM.js} +2 -1
- package/dist/{chunk-4KSQKQXE.js → chunk-BM63HB2M.js} +20 -16
- package/dist/{chunk-4Q7OMKAT.js → chunk-BQXFBI7T.js} +81 -5
- package/dist/{chunk-ADJRGVZC.js → chunk-BWSUPRDK.js} +1 -1
- package/dist/{chunk-3NSNIIBE.js → chunk-CG36IONK.js} +18 -17
- package/dist/chunk-CJJDJ7AT.js +56 -0
- package/dist/chunk-CRB3POP4.js +34 -0
- package/dist/{chunk-LWSJQJC6.js → chunk-DADWEUH3.js} +7 -7
- package/dist/{chunk-KG5ECZGJ.js → chunk-DJ3ZUSC3.js} +11 -11
- package/dist/{chunk-F47JBIAW.js → chunk-DSF2FWAZ.js} +2 -1
- package/dist/{chunk-N5FOE5PS.js → chunk-ECODQ7II.js} +4 -33
- package/dist/{chunk-HHJDN6AU.js → chunk-ESVW4MJY.js} +1 -1
- package/dist/{chunk-YO4PSLVB.js → chunk-FVMYKL6L.js} +1 -1
- package/dist/chunk-G2YNJIGF.js +44 -0
- package/dist/{chunk-WUYTWNMA.js → chunk-G3IJ3N6F.js} +5 -3
- package/dist/{chunk-QKFB4K7X.js → chunk-GGOMT6KF.js} +1 -1
- package/dist/{chunk-N6LURUEF.js → chunk-GOSFARMS.js} +1 -1
- package/dist/{chunk-S3NYLI2P.js → chunk-HPJNLETG.js} +93 -38
- package/dist/{chunk-IP7BIBXJ.js → chunk-IF2OSI5T.js} +35 -1
- package/dist/{chunk-C5Y5ZJJZ.js → chunk-IXYZUUYQ.js} +5 -4
- package/dist/chunk-JBJTC3UU.js +35 -0
- package/dist/chunk-KYS3TQAG.js +296 -0
- package/dist/chunk-LLM3I5YU.js +89 -0
- package/dist/{chunk-YU3VKZ5F.js → chunk-LRG3ZMZD.js} +6 -7
- package/dist/{chunk-V7NIVOFD.js → chunk-M2ZWWTWZ.js} +1 -1
- package/dist/{chunk-ERI5GBNI.js → chunk-MLAAT4S6.js} +3 -3
- package/dist/{chunk-O765JR4W.js → chunk-MTP2QEVO.js} +1 -1
- package/dist/{chunk-ZM2IMMYN.js → chunk-N2PXGQRL.js} +9 -1
- package/dist/{chunk-BWQ66M6N.js → chunk-NFIR3GEE.js} +98 -24
- package/dist/{chunk-POJMEEJJ.js → chunk-NNLA7LLQ.js} +1 -1
- package/dist/{chunk-PLMTLGYH.js → chunk-NTRCK4NH.js} +5 -4
- package/dist/{chunk-22RMZBGW.js → chunk-P53VEAOF.js} +1 -1
- package/dist/{chunk-RLYZLUQ7.js → chunk-QEFKA2QD.js} +2 -2
- package/dist/{chunk-JXYVA45C.js → chunk-QNLX477Q.js} +1200 -322
- package/dist/{chunk-UGP76IZ5.js → chunk-S5PCA3SN.js} +1 -1
- package/dist/{chunk-RMWLBUCJ.js → chunk-SADUQSO4.js} +1 -1
- package/dist/{chunk-GQD22KKN.js → chunk-T2Q7ISPE.js} +2 -1
- package/dist/chunk-TUSYOL2O.js +113 -0
- package/dist/{chunk-NBGDBTO2.js → chunk-TWF3RICI.js} +9 -8
- package/dist/{chunk-ONKJUM6R.js → chunk-UVG35MWF.js} +1 -1
- package/dist/{chunk-KMDLFI4P.js → chunk-VMJPFQNE.js} +1 -1
- package/dist/chunk-VRRGENS4.js +144 -0
- package/dist/{chunk-PTW6W5NR.js → chunk-VYAL7PUE.js} +1 -1
- package/dist/{chunk-OHVJA253.js → chunk-WFSUUUAN.js} +47 -30
- package/dist/{chunk-RSNSZ2LJ.js → chunk-X5H2WYMB.js} +1 -1
- package/dist/{chunk-HYZRY4KZ.js → chunk-XNGMQ27R.js} +16 -6
- package/dist/chunk-XSZD4AZ6.js +490 -0
- package/dist/chunk-YQR6E6OV.js +109 -0
- package/dist/{chunk-XCK2YAY5.js → chunk-YVZJ6GKF.js} +3 -2
- package/dist/{chunk-2MNPHL23.js → chunk-ZHAUSEEN.js} +38 -15
- package/dist/claude-launcher-FPPHZPMR.js +115 -0
- package/dist/{cli-A76AXELQ.js → cli-HROW5T7X.js} +567 -285
- package/dist/{command-R2QCVX55.js → command-OEAV7BVX.js} +42 -40
- package/dist/{config-N3C6DFQA.js → config-CLSGABD5.js} +2 -3
- package/dist/{connect-cmd-JBKAQNSF.js → connect-cmd-UNYKGDAL.js} +10 -9
- package/dist/context-cleanup-HSVYDGMI.js +9 -0
- package/dist/control-plane-launch-NYVDJESK.js +216 -0
- package/dist/{dist-LKDWQWZ7.js → dist-WO7NOJNG.js} +2 -2
- package/dist/{docker-4J77Q2BF.js → docker-BFU3AQGY.js} +7 -8
- package/dist/{doctor-4TAIARIG.js → doctor-23BBJWBX.js} +48 -42
- package/dist/{emit-artifact-cmd-MPZVR56N.js → emit-artifact-cmd-O6SCM7TV.js} +27 -24
- package/dist/emit-cmd-7SP3I6UZ.js +14 -0
- package/dist/{emit-criteria-cmd-EFGYI23V.js → emit-criteria-cmd-LQNERZRV.js} +17 -16
- package/dist/{emit-finding-cmd-5GF3V4LR.js → emit-finding-cmd-AJPKHIOV.js} +18 -17
- package/dist/{emit-result-cmd-I4OADKMD.js → emit-result-cmd-RUICOKWQ.js} +22 -19
- package/dist/emit-step-cmd-347XNGPQ.js +26 -0
- package/dist/{exec-client-CQV746UF.js → exec-client-7UMESIE2.js} +3 -4
- package/dist/governing-source-V5EEGPTA.js +34 -0
- package/dist/guest-init-KPBQOFE5.js +9 -0
- package/dist/{hook-fast-R4FVSZ2H.js → hook-fast-YAZG5MDX.js} +5 -2
- package/dist/host-key-U4B5QETR.js +8 -0
- package/dist/{import-capture-cmd-EQ35EARA.js → import-capture-cmd-KKFFOGWP.js} +7 -5
- package/dist/{import-ruler-cmd-BGAZXCQM.js → import-ruler-cmd-5P6JOXQN.js} +5 -3
- package/dist/index.js +8 -4
- package/dist/{init-PWU5YVL7.js → init-LWHDMZQG.js} +40 -38
- package/dist/init-MOINY2DD.js +53 -0
- package/dist/{init-cmd-XAVO7HYZ.js → init-cmd-HSMRID6A.js} +75 -30
- package/dist/{init-update-CVCWD6VS.js → init-update-LWX5MPPR.js} +29 -29
- package/dist/{install-HDBIPHTX.js → install-4SEFIIW2.js} +4 -5
- package/dist/{install-cmd-5HYMXLEW.js → install-cmd-QDOTR4OM.js} +5 -4
- package/dist/{login-browser-QZCS3WW6.js → login-browser-DX35MARZ.js} +5 -4
- package/dist/{logout-cmd-QAUFDJ6J.js → logout-cmd-5V5MMBSY.js} +3 -2
- package/dist/{logs-H55NWW4H.js → logs-AMLKC3VP.js} +3 -1
- package/dist/loop-SYYPVFYW.js +77 -0
- package/dist/machine-6X63G2KH.js +48 -0
- package/dist/{mcp-install-cmd-FDTPQQS6.js → mcp-install-cmd-GGWB3JJ2.js} +3 -2
- package/dist/{mcp-serve-cmd-3KVYEUXG.js → mcp-serve-cmd-N3UTY6EE.js} +11 -9
- package/dist/paths-7XP63OAX.js +7 -0
- package/dist/pending-532ABZYC.js +8 -0
- package/dist/{pidfile-UZRH774M.js → pidfile-3DM4OBKO.js} +4 -2
- package/dist/{record-C43U4PKS.js → record-3WP6V2K5.js} +3 -2
- package/dist/{runner-env-EN7YE7QX.js → runner-env-FRZFFHDF.js} +5 -6
- package/dist/{runner-key-6SLBI7PP.js → runner-key-276SJU5R.js} +40 -38
- package/dist/session-sender-GGIY5OAX.js +36 -0
- package/dist/{pending-MXLSRYMO.js → shell-function-NRD4KVUK.js} +1 -2
- package/dist/spec-cmd-SIZBRN7D.js +115 -0
- package/dist/{starter-pack-KF66HGZR.js → starter-pack-6LQVLBIA.js} +7 -5
- package/dist/{status-7VNQN5PW.js → status-NAK6DYSF.js} +46 -42
- package/dist/{statusline-fast-SVTMBMD7.js → statusline-fast-KN3BX67H.js} +2 -1
- package/dist/sync-cmd-G7GF4UTW.js +35 -0
- package/dist/{watch-23HSAEI6.js → watch-EVM5GSMP.js} +22 -15
- package/dist/{work-claim-cmd-LKL3GHFQ.js → work-claim-cmd-3J3QW56U.js} +26 -29
- package/dist/work-cmd-5EPPD33Y.js +24 -0
- package/dist/{work-comment-cmd-AWHP56M2.js → work-comment-cmd-2ACOZQAE.js} +16 -15
- package/dist/{work-launch-R5HTAP6X.js → work-launch-LEQ2MPM3.js} +3 -1
- package/dist/work-logs-cmd-QDD2NMJL.js +60 -0
- package/dist/{work-open-pr-cmd-IOM4AVQQ.js → work-open-pr-cmd-XNCEI4ZM.js} +18 -17
- package/dist/work-pr-cmd-H4OQR26N.js +25 -0
- package/package.json +3 -2
- package/skill-pack/maintained/outerlayer-amend/SKILL.md +4 -2
- package/skill-pack/maintained/outerlayer-cli/SKILL.md +9 -2
- package/skill-pack/maintained/outerlayer-cli/references/find-out-why-a-build-ended.md +76 -0
- package/skill-pack/shipped.json +18 -2
- package/skill-pack/template/build/SKILL.md +23 -10
- package/skill-pack/template/build/references/agent-briefs.md +3 -1
- package/skill-pack/template/build/references/criteria-judge.md +27 -0
- package/skill-pack/template/build/references/release.md +22 -29
- package/skill-pack/template/spec/SKILL.md +35 -10
- package/skill-pack/validators/acceptance-criteria.yaml +25 -26
- package/dist/agent-setup-T624CWPD.js +0 -27
- package/dist/chunk-6Y5RUTHO.js +0 -39
- package/dist/chunk-BYT5O5RV.js +0 -61
- package/dist/chunk-WTE3MQML.js +0 -133
- package/dist/chunk-XCCLFVXM.js +0 -401
- package/dist/chunk-YZ5F4PIS.js +0 -797
- package/dist/context-adopt-NFHCLVUB.js +0 -108
- package/dist/context-materialize-IV2TUUFO.js +0 -22
- package/dist/emit-cmd-IJ3GVRFD.js +0 -12
- package/dist/guest-init-LJP5GPZF.js +0 -9
- package/dist/host-key-YMLXY6QB.js +0 -7
- package/dist/init-6XPDRDTC.js +0 -51
- package/dist/loop-FK3LPMMZ.js +0 -73
- package/dist/machine-SQXEBAM2.js +0 -46
- package/dist/paths-OYKMVYJP.js +0 -6
- package/dist/session-sender-FO3LCREV.js +0 -29
- package/dist/sync-cmd-GVPPFZ3G.js +0 -28
- package/dist/work-cmd-Z2WXLZPH.js +0 -23
- package/dist/work-pr-cmd-HNEUIEN7.js +0 -24
|
@@ -1,28 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
|
-
import { createDockerHost, childEnvironment, spawnRun } from './chunk-KMDLFI4P.js';
|
|
4
|
-
import { execCapture } from './chunk-QKFB4K7X.js';
|
|
5
|
-
import { GUEST_CID } from './chunk-RSNSZ2LJ.js';
|
|
6
|
-
import { WorkCommandError, postJson } from './chunk-S3NYLI2P.js';
|
|
7
|
-
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
3
|
import { currentCliBuild } from './chunk-OGFGZQA3.js';
|
|
12
|
-
import {
|
|
4
|
+
import { createDockerHost, childEnvironment, spawnRun } from './chunk-VMJPFQNE.js';
|
|
5
|
+
import { builderSpec, runnerLabelValue, RUNNER_LABEL, BUILD_LABEL, prepareRunArgs, containerRunArgs, HASH_ARGV, parseProgramHash, JUDGE_PROGRAM, containerExecArgs, JUDGE_DIR, MAX_REPORT_BYTES } from './chunk-3UZTIRLF.js';
|
|
6
|
+
import { execCapture } from './chunk-GGOMT6KF.js';
|
|
7
|
+
import { GUEST_CID } from './chunk-X5H2WYMB.js';
|
|
8
|
+
import { describeCallFailure, controlPlaneTokenFailure, refusalCode, resolveGoverningSource, removeStagedContext, stageGovernedContext, pinnedSourceOf, commandWithShim, shimScript } from './chunk-XSZD4AZ6.js';
|
|
9
|
+
import { WorkCommandError } from './chunk-HPJNLETG.js';
|
|
10
|
+
import { hostAllowed } from './chunk-UXYLWVTQ.js';
|
|
13
11
|
import { parseDuKilobytes } from './chunk-7RQ73DLO.js';
|
|
14
|
-
import {
|
|
12
|
+
import { readDefaultBranchHead, cloneRepository, buildRepositoryImage } from './chunk-3CWUOFV6.js';
|
|
13
|
+
import { RecipeError } from './chunk-QOZKPLVJ.js';
|
|
15
14
|
import { CLAUDE_CODE_VERSION, firecrackerArch, MICROVM_ARCHES, DEFAULT_BASE_IMAGE, GUEST_HELPER_SHA256, GUEST_ASSETS_BASE_URL, GUEST_KERNEL_SHA256, FIRECRACKER_BINARY_SHA256, FIRECRACKER_SHA256, FIRECRACKER_VERSION, GUEST_ASSETS_VERSION } from './chunk-CX22S5BA.js';
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import { orderSessions, extractFacts, sessionsToTimeline } from './chunk-
|
|
21
|
-
import { downconvertSession } from './chunk-
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
15
|
+
import { startTimeOf, isSameProcess } from './chunk-JBJTC3UU.js';
|
|
16
|
+
import { setCustomScrubbers, setLocalIdentity, setRepoRoot, parseTranscript, scrubDeep, scrubText } from './chunk-QNLX477Q.js';
|
|
17
|
+
import { sharedSkillNames, skillConflictMessage } from './chunk-LLM3I5YU.js';
|
|
18
|
+
import { versionAtLeast, MINIMUM_CLAUDE_VERSION } from './chunk-G2YNJIGF.js';
|
|
19
|
+
import { orderSessions, extractFacts, sessionsToTimeline } from './chunk-WFSUUUAN.js';
|
|
20
|
+
import { downconvertSession } from './chunk-7VUK5RFS.js';
|
|
21
|
+
import { rootfsPathFor, diskTemplatesDir, RUNNER_FILE_MODE, deliveryDir, ghConfigDir, runnerDirOfBuild, spoolDir, mirrorDir, buildDir, RUNNER_DIR_MODE, SHIM_DIR, vmAssetsDir } from './chunk-N2PXGQRL.js';
|
|
22
|
+
import { repositoriesOf, primaryOf, RELAY_PORT, repositoryPath, CHECKOUT_DIR, lifecycleWorkdir, claudeCredential, buildEnvironment, startBuildServices, checkEnvironment, AGENT_CREDENTIAL_MISSING, checkoutPath, JOB_MOUNT } from './chunk-BQXFBI7T.js';
|
|
23
|
+
import { readFileSync, existsSync, mkdirSync, chmodSync, truncateSync, rmSync, writeFileSync, readdirSync, lstatSync, copyFileSync, accessSync, constants, statSync, rmdirSync, renameSync, unlinkSync, openSync, closeSync, mkdtempSync, createWriteStream, createReadStream } from 'fs';
|
|
24
|
+
import { join, dirname, basename } from 'path';
|
|
26
25
|
import { spawnSync, spawn } from 'child_process';
|
|
27
26
|
import { availableParallelism, networkInterfaces } from 'os';
|
|
28
27
|
import { randomBytes, createHash } from 'crypto';
|
|
@@ -35,294 +34,6 @@ import { isIP, connect, createServer as createServer$1 } from 'net';
|
|
|
35
34
|
import { lookup } from 'dns/promises';
|
|
36
35
|
|
|
37
36
|
createRequire(import.meta.url);
|
|
38
|
-
function logStamp(at) {
|
|
39
|
-
return at.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
40
|
-
}
|
|
41
|
-
function sameDestination(stat = fstatSync) {
|
|
42
|
-
try {
|
|
43
|
-
const out = stat(1);
|
|
44
|
-
const err = stat(2);
|
|
45
|
-
return out.dev === err.dev && out.ino === err.ino;
|
|
46
|
-
} catch {
|
|
47
|
-
return false;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
function createRunnerLog(now = () => /* @__PURE__ */ new Date(), io = { stdout: process.stdout, stderr: process.stderr, joined: () => sameDestination() }) {
|
|
51
|
-
const write = (stream, text) => {
|
|
52
|
-
stream.write(`${logStamp(now())} ${text}
|
|
53
|
-
`);
|
|
54
|
-
};
|
|
55
|
-
return {
|
|
56
|
-
line: (text) => write(io.stdout, text),
|
|
57
|
-
error: (text) => {
|
|
58
|
-
write(io.stdout, text);
|
|
59
|
-
if (!io.joined()) write(io.stderr, text);
|
|
60
|
-
}
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
function formatDuration(ms) {
|
|
64
|
-
const total = Math.max(0, Math.round(ms / 1e3));
|
|
65
|
-
const hours = Math.floor(total / 3600);
|
|
66
|
-
const minutes = Math.floor(total % 3600 / 60);
|
|
67
|
-
const seconds = total % 60;
|
|
68
|
-
if (hours > 0) return `${hours}h ${minutes}m`;
|
|
69
|
-
if (minutes > 0) return `${minutes}m ${String(seconds).padStart(2, "0")}s`;
|
|
70
|
-
return `${seconds}s`;
|
|
71
|
-
}
|
|
72
|
-
function formatClock(at, today) {
|
|
73
|
-
const hhmm = `${String(at.getUTCHours()).padStart(2, "0")}:${String(at.getUTCMinutes()).padStart(2, "0")} UTC`;
|
|
74
|
-
const sameDay = at.getUTCFullYear() === today.getUTCFullYear() && at.getUTCMonth() === today.getUTCMonth() && at.getUTCDate() === today.getUTCDate();
|
|
75
|
-
if (sameDay) return hhmm;
|
|
76
|
-
const month = at.toLocaleString("en-US", { month: "short", timeZone: "UTC" });
|
|
77
|
-
return `${month} ${at.getUTCDate()} ${hhmm}`;
|
|
78
|
-
}
|
|
79
|
-
function shortClaim(claimId) {
|
|
80
|
-
return claimId.length <= 8 ? claimId : `${claimId.slice(0, 8)}\u2026`;
|
|
81
|
-
}
|
|
82
|
-
function relativeToRunner(runnerDirPath, path) {
|
|
83
|
-
const rel = relative(runnerDirPath, path);
|
|
84
|
-
return rel === "" || rel.startsWith("..") ? path : rel;
|
|
85
|
-
}
|
|
86
|
-
function describeCallFailure(err) {
|
|
87
|
-
if (err instanceof Error) {
|
|
88
|
-
const status = err.status;
|
|
89
|
-
return status === void 0 ? err.message : `${err.message} (${status})`;
|
|
90
|
-
}
|
|
91
|
-
return String(err);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// src/runner/retry.ts
|
|
95
|
-
var RETRY_WINDOW_MS = 12e4;
|
|
96
|
-
var FIRST_BACKOFF_MS = 1e3;
|
|
97
|
-
var MAX_BACKOFF_MS = 15e3;
|
|
98
|
-
function isRetryable(err) {
|
|
99
|
-
if (err instanceof WorkCommandError) return err.retryable;
|
|
100
|
-
return true;
|
|
101
|
-
}
|
|
102
|
-
async function retrySend(send, deps = {}) {
|
|
103
|
-
const now = deps.now ?? (() => /* @__PURE__ */ new Date());
|
|
104
|
-
const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
105
|
-
const windowMs = deps.windowMs ?? RETRY_WINDOW_MS;
|
|
106
|
-
const startedMs = now().getTime();
|
|
107
|
-
let attempts = 0;
|
|
108
|
-
let backoffMs = FIRST_BACKOFF_MS;
|
|
109
|
-
for (; ; ) {
|
|
110
|
-
attempts++;
|
|
111
|
-
try {
|
|
112
|
-
return { ok: true, value: await send(), attempts };
|
|
113
|
-
} catch (err) {
|
|
114
|
-
const reason = describeCallFailure(err);
|
|
115
|
-
const code = err instanceof WorkCommandError ? err.code : void 0;
|
|
116
|
-
const status = err instanceof WorkCommandError ? err.status : void 0;
|
|
117
|
-
const message = err instanceof WorkCommandError ? err.message : void 0;
|
|
118
|
-
if (!isRetryable(err)) return { ok: false, attempts, reason, retryable: false, code, status, message };
|
|
119
|
-
const elapsedMs = now().getTime() - startedMs;
|
|
120
|
-
if (elapsedMs + backoffMs >= windowMs) return { ok: false, attempts, reason, retryable: true, code, status, message };
|
|
121
|
-
await sleep(backoffMs);
|
|
122
|
-
backoffMs = Math.min(backoffMs * 2, MAX_BACKOFF_MS);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// src/runner/build-tokens.ts
|
|
128
|
-
var ISSUE_RETRY_WINDOW_MS = 3e4;
|
|
129
|
-
var REVOKE_TIMEOUT_MS = 15e3;
|
|
130
|
-
function isDue(expiresAtMs, nowMs) {
|
|
131
|
-
return nowMs >= expiresAtMs - 10 * 6e4;
|
|
132
|
-
}
|
|
133
|
-
function classifyRefusal(err) {
|
|
134
|
-
if (!(err instanceof WorkCommandError)) return { kind: "retry" };
|
|
135
|
-
if (err.status === 401 || err.code === "claim_expired" || err.code === "claim_not_held") return { kind: "lease_lost" };
|
|
136
|
-
if (err.code === "github_unavailable") return { kind: "retry" };
|
|
137
|
-
if (err.code === "github_request_failed") return { kind: "failed", reason: refusalCode(err) };
|
|
138
|
-
if (err.retryable && err.code !== "repository_tokens_unavailable") return { kind: "retry" };
|
|
139
|
-
return { kind: "failed", reason: refusalCode(err) };
|
|
140
|
-
}
|
|
141
|
-
function refusalCode(err) {
|
|
142
|
-
if (!(err instanceof WorkCommandError)) return "unavailable";
|
|
143
|
-
return err.code ?? `http_${err.status ?? "unknown"}`;
|
|
144
|
-
}
|
|
145
|
-
function keyOf(kind, repository) {
|
|
146
|
-
return repository === void 0 ? kind : `${kind}:${repository}`;
|
|
147
|
-
}
|
|
148
|
-
var BuildTokens = class {
|
|
149
|
-
constructor(options) {
|
|
150
|
-
this.options = options;
|
|
151
|
-
this.held = /* @__PURE__ */ new Map();
|
|
152
|
-
this.asking = /* @__PURE__ */ new Map();
|
|
153
|
-
this.issued = [];
|
|
154
|
-
this.usedKinds = /* @__PURE__ */ new Set();
|
|
155
|
-
this.revoked = false;
|
|
156
|
-
this.controlPlaneRetry = false;
|
|
157
|
-
}
|
|
158
|
-
now() {
|
|
159
|
-
return (this.options.now ?? (() => /* @__PURE__ */ new Date()))();
|
|
160
|
-
}
|
|
161
|
-
/** `repository` as the holder keys it: `undefined` for the primary, which is the one a request names no repository for. */
|
|
162
|
-
secondary(repository) {
|
|
163
|
-
if (repository === void 0) return void 0;
|
|
164
|
-
const primary = this.options.primary;
|
|
165
|
-
return primary !== void 0 && primary.toLowerCase() === repository.toLowerCase() ? void 0 : repository;
|
|
166
|
-
}
|
|
167
|
-
/**
|
|
168
|
-
* The token of `kind` for `repository`, or for the primary when none is
|
|
169
|
-
* named, asking the gateway for a new one when the build holds none or its
|
|
170
|
-
* token is within ten minutes of expiry.
|
|
171
|
-
*/
|
|
172
|
-
token(kind, repository) {
|
|
173
|
-
const other = this.secondary(repository);
|
|
174
|
-
const key = keyOf(kind, other);
|
|
175
|
-
const held = this.held.get(key);
|
|
176
|
-
if (held !== void 0 && !isDue(held.expiresAtMs, this.now().getTime())) return Promise.resolve(held.token);
|
|
177
|
-
let asking = this.asking.get(key);
|
|
178
|
-
if (asking === void 0) {
|
|
179
|
-
asking = this.issueOrKeep(kind, other).finally(() => this.asking.delete(key));
|
|
180
|
-
this.asking.set(key, asking);
|
|
181
|
-
}
|
|
182
|
-
return asking;
|
|
183
|
-
}
|
|
184
|
-
/** A due token that has not yet expired is kept when the gateway cannot issue a new one. */
|
|
185
|
-
async issueOrKeep(kind, repository) {
|
|
186
|
-
try {
|
|
187
|
-
return await this.issue(kind, repository);
|
|
188
|
-
} catch (err) {
|
|
189
|
-
const held = this.held.get(keyOf(kind, repository));
|
|
190
|
-
if (held !== void 0 && this.now().getTime() < held.expiresAtMs && classifyRefusal(err).kind === "retry") return held.token;
|
|
191
|
-
throw err;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
/** Asks the gateway for a token with `body`, retrying a failure about the moment. */
|
|
195
|
-
ask(body) {
|
|
196
|
-
const { config, item, fetchImpl } = this.options;
|
|
197
|
-
return retrySend(
|
|
198
|
-
async () => {
|
|
199
|
-
try {
|
|
200
|
-
return await postJson(
|
|
201
|
-
config.url,
|
|
202
|
-
`/v1/work-items/${item}/claim/tokens`,
|
|
203
|
-
config.apiKey,
|
|
204
|
-
config.appId,
|
|
205
|
-
body,
|
|
206
|
-
fetchImpl,
|
|
207
|
-
void 0,
|
|
208
|
-
await signerFor(config.configPath)
|
|
209
|
-
);
|
|
210
|
-
} catch (err) {
|
|
211
|
-
if (err instanceof WorkCommandError && (err.code === "repository_tokens_unavailable" || err.code === "github_request_failed")) {
|
|
212
|
-
throw new WorkCommandError(err.message, false, err.status, err.code);
|
|
213
|
-
}
|
|
214
|
-
throw err;
|
|
215
|
-
}
|
|
216
|
-
},
|
|
217
|
-
{ now: this.options.now, sleep: this.options.sleep, windowMs: ISSUE_RETRY_WINDOW_MS }
|
|
218
|
-
);
|
|
219
|
-
}
|
|
220
|
-
/**
|
|
221
|
-
* A read-only token for the repository that governs the item's, which the
|
|
222
|
-
* runner uses on the host to fetch its context. It is revoked with the
|
|
223
|
-
* others when the attempt ends. It is not held or renewed: nothing uses it
|
|
224
|
-
* after the fetch. A refusal is thrown with the gateway's code. A lost
|
|
225
|
-
* lease is kept for `refusal()`, and a failure about the moment for
|
|
226
|
-
* `controlPlaneUnavailable()`, as the read token's are.
|
|
227
|
-
*/
|
|
228
|
-
async controlPlane() {
|
|
229
|
-
const asked = await this.ask({ access: "read", target: "control_plane" });
|
|
230
|
-
if (!asked.ok) {
|
|
231
|
-
const err = new WorkCommandError(asked.message ?? asked.reason, asked.retryable, asked.status, asked.code);
|
|
232
|
-
const refusal = classifyRefusal(err);
|
|
233
|
-
if (refusal.kind === "lease_lost") this.failure = refusal;
|
|
234
|
-
if (refusal.kind === "retry") this.controlPlaneRetry = true;
|
|
235
|
-
throw err;
|
|
236
|
-
}
|
|
237
|
-
const { token, expiresAt, repository } = asked.value.data;
|
|
238
|
-
if (typeof token !== "string" || token === "" || typeof expiresAt !== "string" || typeof repository !== "string" || repository === "") {
|
|
239
|
-
throw new WorkCommandError("the gateway's token answer carried no token, expiry or repository", false, void 0, "malformed_token_response");
|
|
240
|
-
}
|
|
241
|
-
if (this.revoked) {
|
|
242
|
-
await this.revoke([token]);
|
|
243
|
-
throw new WorkCommandError("the build's tokens were already revoked", false, void 0, "tokens_revoked");
|
|
244
|
-
}
|
|
245
|
-
this.issued.push(token);
|
|
246
|
-
return { token, expiresAt, repository };
|
|
247
|
-
}
|
|
248
|
-
async issue(kind, repository) {
|
|
249
|
-
const asked = await this.ask({ access: kind, ...repository === void 0 ? {} : { repository } });
|
|
250
|
-
let err;
|
|
251
|
-
if (!asked.ok) {
|
|
252
|
-
err = new WorkCommandError(asked.message ?? asked.reason, asked.retryable, asked.status, asked.code);
|
|
253
|
-
} else {
|
|
254
|
-
const { token, expiresAt } = asked.value.data;
|
|
255
|
-
const expiresAtMs = typeof expiresAt === "string" ? Date.parse(expiresAt) : Number.NaN;
|
|
256
|
-
if (typeof token === "string" && token !== "" && Number.isFinite(expiresAtMs)) {
|
|
257
|
-
if (this.revoked) {
|
|
258
|
-
await this.revoke([token]);
|
|
259
|
-
throw new WorkCommandError("the build's tokens were already revoked", false, void 0, "tokens_revoked");
|
|
260
|
-
}
|
|
261
|
-
this.held.set(keyOf(kind, repository), { token, expiresAtMs });
|
|
262
|
-
this.issued.push(token);
|
|
263
|
-
if (kind === "read" && repository === void 0) this.readSink?.(token);
|
|
264
|
-
return token;
|
|
265
|
-
}
|
|
266
|
-
err = new WorkCommandError("the gateway's token answer carried no token or no expiry", false, void 0, "malformed_token_response");
|
|
267
|
-
}
|
|
268
|
-
const refusal = classifyRefusal(err);
|
|
269
|
-
if (refusal.kind !== "retry") this.failure = refusal;
|
|
270
|
-
throw err;
|
|
271
|
-
}
|
|
272
|
-
/** Asks for a new read token when the build holds one within ten minutes of expiry. A refusal is kept for `refusal()`, not thrown. */
|
|
273
|
-
async renewReadIfDue() {
|
|
274
|
-
const held = this.held.get("read");
|
|
275
|
-
if (held === void 0 || !isDue(held.expiresAtMs, this.now().getTime())) return;
|
|
276
|
-
await this.token("read").catch(() => void 0);
|
|
277
|
-
}
|
|
278
|
-
/** Calls `sink` with the read token now, and again with each one that replaces it. */
|
|
279
|
-
onRead(sink) {
|
|
280
|
-
this.readSink = sink;
|
|
281
|
-
const held = this.held.get("read");
|
|
282
|
-
if (held !== void 0) sink(held.token);
|
|
283
|
-
}
|
|
284
|
-
markUsed(kind) {
|
|
285
|
-
if (this.usedKinds.has(kind)) return;
|
|
286
|
-
this.usedKinds.add(kind);
|
|
287
|
-
this.options.onUsed?.(this.used());
|
|
288
|
-
}
|
|
289
|
-
/** The kinds of token the build has used, read before push. */
|
|
290
|
-
used() {
|
|
291
|
-
return ["read", "push"].filter((kind) => this.usedKinds.has(kind));
|
|
292
|
-
}
|
|
293
|
-
/** The last refusal that ends the build, if a renewal or a push's token request met one. */
|
|
294
|
-
refusal() {
|
|
295
|
-
return this.failure;
|
|
296
|
-
}
|
|
297
|
-
/** Whether a control plane token request failed for a reason about the moment, which a later attempt may not meet. */
|
|
298
|
-
controlPlaneUnavailable() {
|
|
299
|
-
return this.controlPlaneRetry;
|
|
300
|
-
}
|
|
301
|
-
/** 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. */
|
|
302
|
-
async revokeAll() {
|
|
303
|
-
this.revoked = true;
|
|
304
|
-
const tokens = this.issued.splice(0);
|
|
305
|
-
this.held.clear();
|
|
306
|
-
await this.revoke(tokens);
|
|
307
|
-
}
|
|
308
|
-
async revoke(tokens) {
|
|
309
|
-
const apiUrl = this.options.apiUrl ?? "https://api.github.com";
|
|
310
|
-
await Promise.allSettled(
|
|
311
|
-
tokens.map(
|
|
312
|
-
(token) => this.options.fetchImpl(`${apiUrl}/installation/token`, {
|
|
313
|
-
method: "DELETE",
|
|
314
|
-
headers: {
|
|
315
|
-
authorization: `Bearer ${token}`,
|
|
316
|
-
accept: "application/vnd.github+json",
|
|
317
|
-
"x-github-api-version": "2022-11-28",
|
|
318
|
-
"user-agent": "outerlayer-runner"
|
|
319
|
-
},
|
|
320
|
-
signal: AbortSignal.timeout(REVOKE_TIMEOUT_MS)
|
|
321
|
-
})
|
|
322
|
-
)
|
|
323
|
-
);
|
|
324
|
-
}
|
|
325
|
-
};
|
|
326
37
|
var BUILD_PREFIX = "build-";
|
|
327
38
|
var LEAF = "runner";
|
|
328
39
|
function serviceDirectory(selfCgroup, mount) {
|
|
@@ -685,6 +396,60 @@ async function ensureAssets(options) {
|
|
|
685
396
|
const helper = await ensureFile(dir, `outerlayer-guest-${arch}`, pins.helper, fetchImpl, 493);
|
|
686
397
|
return { firecracker, kernel, helper, helperSha256: pins.helper.sha256 };
|
|
687
398
|
}
|
|
399
|
+
|
|
400
|
+
// src/runner/container/docker.ts
|
|
401
|
+
function parseImageConfig(json) {
|
|
402
|
+
const config = JSON.parse(json);
|
|
403
|
+
const env = {};
|
|
404
|
+
for (const entry of config.Env ?? []) {
|
|
405
|
+
const eq = entry.indexOf("=");
|
|
406
|
+
if (eq > 0) env[entry.slice(0, eq)] = entry.slice(eq + 1);
|
|
407
|
+
}
|
|
408
|
+
return { user: config.User ?? "", env };
|
|
409
|
+
}
|
|
410
|
+
function createContainerHost(deps = {}) {
|
|
411
|
+
const run = deps.run ?? spawnRun;
|
|
412
|
+
return {
|
|
413
|
+
async version() {
|
|
414
|
+
const result = await run("docker", ["version", "--format", "{{.Server.Version}}"]);
|
|
415
|
+
return result.status === 0 && result.stdout.trim() !== "" ? result.stdout.trim() : null;
|
|
416
|
+
},
|
|
417
|
+
async operatingSystem() {
|
|
418
|
+
const result = await run("docker", ["info", "--format", "{{.OperatingSystem}}"]);
|
|
419
|
+
return result.status === 0 && result.stdout.trim() !== "" ? result.stdout.trim() : null;
|
|
420
|
+
},
|
|
421
|
+
async imageConfig(imageId) {
|
|
422
|
+
const result = await run("docker", ["image", "inspect", "--format", "{{json .Config}}", imageId]);
|
|
423
|
+
return result.status === 0 ? parseImageConfig(result.stdout) : { user: "", env: {} };
|
|
424
|
+
},
|
|
425
|
+
createVolume(name, labels) {
|
|
426
|
+
const flags = Object.entries(labels).flatMap(([key, value]) => ["--label", `${key}=${value}`]);
|
|
427
|
+
return run("docker", ["volume", "create", ...flags, name]);
|
|
428
|
+
},
|
|
429
|
+
removeVolume(name) {
|
|
430
|
+
return run("docker", ["volume", "rm", "--force", name]);
|
|
431
|
+
},
|
|
432
|
+
run(args, opts) {
|
|
433
|
+
return run("docker", ["run", ...args], opts);
|
|
434
|
+
},
|
|
435
|
+
exec(input, opts) {
|
|
436
|
+
const env = {};
|
|
437
|
+
for (const [name, value] of Object.entries(input.env.passed)) if (value !== void 0) env[name] = value;
|
|
438
|
+
return run("docker", ["exec", ...containerExecArgs(input)], { ...opts, env });
|
|
439
|
+
},
|
|
440
|
+
stop(container) {
|
|
441
|
+
return run("docker", ["stop", container]);
|
|
442
|
+
},
|
|
443
|
+
start(container) {
|
|
444
|
+
return run("docker", ["start", container]);
|
|
445
|
+
},
|
|
446
|
+
remove(container) {
|
|
447
|
+
return run("docker", ["rm", "--force", "--volumes", container]);
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// src/runner/microvm/disks.ts
|
|
688
453
|
function converterScript() {
|
|
689
454
|
return [
|
|
690
455
|
"set -eu",
|
|
@@ -751,13 +516,13 @@ function exportInto(container, converter) {
|
|
|
751
516
|
exporter.once("error", reject);
|
|
752
517
|
writer.once("error", reject);
|
|
753
518
|
let exits = 0;
|
|
754
|
-
let
|
|
519
|
+
let failure2;
|
|
755
520
|
const done = (name, code) => {
|
|
756
|
-
if (code !== 0 &&
|
|
521
|
+
if (code !== 0 && failure2 === void 0) failure2 = `${name} exited ${code}`;
|
|
757
522
|
exits += 1;
|
|
758
523
|
if (exits < 2) return;
|
|
759
|
-
if (
|
|
760
|
-
else reject(new Error(`${
|
|
524
|
+
if (failure2 === void 0) resolve();
|
|
525
|
+
else reject(new Error(`${failure2}: ${firstLine2(errors.join("\n"))}`));
|
|
761
526
|
};
|
|
762
527
|
exporter.once("exit", (code) => done("docker export", code));
|
|
763
528
|
writer.once("exit", (code) => done("the converter", code));
|
|
@@ -783,13 +548,7 @@ function createRootfsHost() {
|
|
|
783
548
|
async imageConfig(imageId) {
|
|
784
549
|
const inspected = await spawnRun("docker", ["image", "inspect", "--format", "{{json .Config}}", imageId], { env: env() });
|
|
785
550
|
if (inspected.status !== 0) throw new Error(`docker image inspect ${imageId} failed: ${firstLine2(inspected.stderr)}`);
|
|
786
|
-
|
|
787
|
-
const variables = {};
|
|
788
|
-
for (const entry of config.Env ?? []) {
|
|
789
|
-
const eq = entry.indexOf("=");
|
|
790
|
-
if (eq > 0) variables[entry.slice(0, eq)] = entry.slice(eq + 1);
|
|
791
|
-
}
|
|
792
|
-
return { env: variables, user: config.User ?? "" };
|
|
551
|
+
return parseImageConfig(inspected.stdout);
|
|
793
552
|
}
|
|
794
553
|
};
|
|
795
554
|
}
|
|
@@ -940,8 +699,6 @@ function createMicroVmPorts(over = {}) {
|
|
|
940
699
|
exec: over.exec ?? execCapture
|
|
941
700
|
};
|
|
942
701
|
}
|
|
943
|
-
|
|
944
|
-
// src/runner/build-steps.ts
|
|
945
702
|
var aborted = () => ({ ok: false, reason: "", aborted: true });
|
|
946
703
|
function firstLine3(text) {
|
|
947
704
|
return text.trim().split("\n")[0] ?? "";
|
|
@@ -950,14 +707,43 @@ function gitErrorLine(text) {
|
|
|
950
707
|
const lines = text.split("\n").map((line) => line.trim()).filter((line) => line !== "");
|
|
951
708
|
return lines.find((line) => /^(fatal|error):/.test(line)) ?? lines.at(-1) ?? "";
|
|
952
709
|
}
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
710
|
+
var STAGED_TREE = `${JOB_MOUNT}/context/tree`;
|
|
711
|
+
var DELIVER_SCRIPT = [
|
|
712
|
+
"set -e",
|
|
713
|
+
'src="$1"; dest="$2"',
|
|
714
|
+
'if [ -e "$dest" ]; then chmod -R u+w "$dest"; rm -rf "$dest"; fi',
|
|
715
|
+
'mkdir -p "$dest"',
|
|
716
|
+
"for entry in AGENTS.md CLAUDE.md settings.json .claude .outerlayer; do",
|
|
717
|
+
' if [ -e "$src/$entry" ]; then cp -R "$src/$entry" "$dest/"; fi',
|
|
718
|
+
"done",
|
|
719
|
+
'chmod -R a-w "$dest"'
|
|
720
|
+
].join("\n");
|
|
721
|
+
var RECORD_SCRIPT = 'd="$(git rev-parse --git-common-dir)" && mkdir -p "$d/outerlayer" && printf "%s" "$1" > "$d/outerlayer/delivery.json"';
|
|
722
|
+
var WRITE_SHIM_SCRIPT = 'mkdir -p "$1" && printf "%s" "$2" > "$1/claude" && chmod 755 "$1/claude"';
|
|
723
|
+
function failure(what, result) {
|
|
724
|
+
return `context_delivery_failed: ${what} (exit ${result.status})${firstLine3(result.stderr) === "" ? "" : `: ${firstLine3(result.stderr)}`}`;
|
|
725
|
+
}
|
|
726
|
+
async function deliveryRefusal(input, staged) {
|
|
727
|
+
if (input.agentVersion !== void 0 && !versionAtLeast(input.agentVersion)) {
|
|
728
|
+
return `context_agent_too_old: the build image's Claude Code is ${input.agentVersion}, and a governed build needs ${MINIMUM_CLAUDE_VERSION} or later to load its control plane's instructions; rebuild the image`;
|
|
729
|
+
}
|
|
730
|
+
const own = await input.exec(["ls", "-1", ".claude/skills"], CHECKOUT_DIR, { quiet: true });
|
|
731
|
+
if (own.status !== 0) return void 0;
|
|
732
|
+
const ownNames = new Set(own.stdout.split("\n").map((name) => name.trim()));
|
|
733
|
+
const clash = sharedSkillNames(join(staged.dir, "tree")).find((name) => ownNames.has(name));
|
|
734
|
+
if (clash === void 0) return void 0;
|
|
735
|
+
return `context_skill_conflict: ${skillConflictMessage({ skill: clash, repository: input.repository, controlPlane: staged.repository })}, and would replace it in this build; rename or remove it from the repository`;
|
|
736
|
+
}
|
|
737
|
+
async function deliver(input, staged) {
|
|
738
|
+
const dir = deliveryDir(staged.repository, staged.sha);
|
|
739
|
+
const copied = await input.exec(["sh", "-c", DELIVER_SCRIPT, "deliver", STAGED_TREE, dir], "/build/work");
|
|
740
|
+
if (copied.status !== 0) return { ok: false, reason: failure("copying the control plane into the build failed", copied) };
|
|
741
|
+
const shim = await input.exec(["sh", "-c", WRITE_SHIM_SCRIPT, "shim", SHIM_DIR, shimScript()], "/build/work");
|
|
742
|
+
if (shim.status !== 0) return { ok: false, reason: failure("writing the claude shim failed", shim) };
|
|
743
|
+
const record = JSON.stringify({ schemaVersion: 1, repository: staged.repository, ref: staged.ref, sha: staged.sha, dir });
|
|
744
|
+
const recorded = await input.exec(["sh", "-c", RECORD_SCRIPT, "record", record], CHECKOUT_DIR);
|
|
745
|
+
if (recorded.status !== 0) return { ok: false, reason: failure("recording the delivery failed", recorded) };
|
|
746
|
+
return { ok: true };
|
|
961
747
|
}
|
|
962
748
|
async function checkOut(input, entry) {
|
|
963
749
|
const { exec, log, tag } = input;
|
|
@@ -983,6 +769,18 @@ async function checkOut(input, entry) {
|
|
|
983
769
|
throw err;
|
|
984
770
|
}
|
|
985
771
|
}
|
|
772
|
+
var DEVCONTAINER_FILES = [".devcontainer/devcontainer.json", ".devcontainer.json"];
|
|
773
|
+
async function noteIgnoredDevcontainers(input, repositories, governing) {
|
|
774
|
+
for (const entry of repositories) {
|
|
775
|
+
if (entry.repository.toLowerCase() === governing.toLowerCase()) continue;
|
|
776
|
+
for (const file of DEVCONTAINER_FILES) {
|
|
777
|
+
const probe = await input.exec(["test", "-f", file], checkoutPath(entry), { quiet: true });
|
|
778
|
+
if (probe.status !== 0) continue;
|
|
779
|
+
input.log.line(`${input.tag} ${entry.repository} has its own ${file}; it is ignored, and the control plane's ${governing} devcontainer is used instead`);
|
|
780
|
+
break;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
986
784
|
async function runBuildSteps(input) {
|
|
987
785
|
const { exec, log, tag, repository } = input;
|
|
988
786
|
let startCommit = "";
|
|
@@ -993,33 +791,44 @@ async function runBuildSteps(input) {
|
|
|
993
791
|
const first = await checkOut(input, { ...primary, primary: true });
|
|
994
792
|
if (!first.ok) return first;
|
|
995
793
|
startCommit = first.startCommit;
|
|
794
|
+
if (repositories.length > 1) {
|
|
795
|
+
const linkPath = repositoryPath(primary.repository);
|
|
796
|
+
const linked = await exec(["sh", "-c", 'mkdir -p "$(dirname "$1")" && ln -s "$2" "$1"', "link", linkPath, CHECKOUT_DIR], "/build/work");
|
|
797
|
+
if (linked.status !== 0) return { ok: false, reason: `linking ${primary.repository} at ${linkPath} failed (exit ${linked.status})` };
|
|
798
|
+
}
|
|
996
799
|
for (const entry of repositories.filter((candidate) => candidate.repository !== primary.repository)) {
|
|
997
800
|
if (input.aborted()) return aborted();
|
|
998
801
|
const checked = await checkOut(input, { ...entry, primary: false });
|
|
999
802
|
if (!checked.ok) return checked;
|
|
1000
803
|
}
|
|
804
|
+
if (input.governing !== void 0) await noteIgnoredDevcontainers(input, repositories, input.governing.repository);
|
|
805
|
+
const lifecycleDir = lifecycleWorkdir(repositories, input.governing?.repository);
|
|
1001
806
|
for (const step of lifecycleSteps(input.lifecycle)) {
|
|
1002
807
|
if (input.aborted()) return aborted();
|
|
1003
808
|
log.line(`${tag} lifecycle ${step.name} (${step.source})`);
|
|
1004
|
-
const ran = await exec(step.argv,
|
|
809
|
+
const ran = await exec(step.argv, lifecycleDir);
|
|
1005
810
|
if (ran.status !== 0) return { ok: false, reason: lifecycleFailureReason(step, ran.status) };
|
|
1006
811
|
}
|
|
1007
812
|
if (input.aborted()) return aborted();
|
|
1008
813
|
const staged = await input.stageContext();
|
|
1009
814
|
if (staged.kind === "refused") return { ok: false, reason: staged.reason };
|
|
815
|
+
let controlPlane;
|
|
1010
816
|
if (staged.kind === "staged") {
|
|
1011
817
|
try {
|
|
1012
|
-
const
|
|
1013
|
-
if (
|
|
1014
|
-
const
|
|
1015
|
-
if (
|
|
1016
|
-
|
|
818
|
+
const refusal = await deliveryRefusal(input, staged);
|
|
819
|
+
if (refusal !== void 0) return { ok: false, reason: refusal };
|
|
820
|
+
const handed = await input.deliverContext?.(staged) ?? { ok: true };
|
|
821
|
+
if (!handed.ok) return { ok: false, reason: handed.reason };
|
|
822
|
+
const delivered = await deliver(input, staged);
|
|
823
|
+
if (!delivered.ok) return delivered;
|
|
824
|
+
controlPlane = { repository: staged.repository, commit: staged.sha };
|
|
825
|
+
log.line(`${tag} governed context ${staged.sha.slice(0, 7)} delivered to ${deliveryDir(staged.repository, staged.sha)} (${staged.files.length} files)`);
|
|
1017
826
|
} finally {
|
|
1018
827
|
input.removeStagedContext();
|
|
1019
828
|
}
|
|
1020
829
|
if (input.aborted()) return aborted();
|
|
1021
830
|
}
|
|
1022
|
-
return { ok: true, startCommit };
|
|
831
|
+
return { ok: true, startCommit, ...controlPlane === void 0 ? {} : { controlPlane } };
|
|
1023
832
|
}
|
|
1024
833
|
var EVAL_INPUT_NAME = "eval-input";
|
|
1025
834
|
var TRANSCRIPT_ROOT = "/build/home/.claude/projects";
|
|
@@ -1214,6 +1023,28 @@ async function writeEvalInput(request) {
|
|
|
1214
1023
|
writeReadOnly(join(dir, "activity.json"), `${JSON.stringify(activity, null, 2)}
|
|
1215
1024
|
`);
|
|
1216
1025
|
}
|
|
1026
|
+
var MAX_TREE_FILES = 2e4;
|
|
1027
|
+
var MAX_TREE_FILE_BYTES = 1024 * 1024;
|
|
1028
|
+
function copyJudgeTree(from, to, limits2 = { files: MAX_TREE_FILES, bytes: MAX_TREE_FILE_BYTES }) {
|
|
1029
|
+
let copied = 0;
|
|
1030
|
+
const walk = (src, dest, top) => {
|
|
1031
|
+
mkdirSync(dest);
|
|
1032
|
+
chmodSync(dest, 493);
|
|
1033
|
+
for (const name of readdirSync(src).sort()) {
|
|
1034
|
+
if (copied >= limits2.files) return;
|
|
1035
|
+
if (top && name === ".git") continue;
|
|
1036
|
+
const path = join(src, name);
|
|
1037
|
+
const stat = lstatSync(path);
|
|
1038
|
+
if (stat.isDirectory()) walk(path, join(dest, name), false);
|
|
1039
|
+
else if (stat.isFile() && stat.size <= limits2.bytes) {
|
|
1040
|
+
copyFileSync(path, join(dest, name));
|
|
1041
|
+
chmodSync(join(dest, name), 420);
|
|
1042
|
+
copied += 1;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
};
|
|
1046
|
+
walk(from, to, true);
|
|
1047
|
+
}
|
|
1217
1048
|
|
|
1218
1049
|
// src/runner/container/agent-user.ts
|
|
1219
1050
|
var ADDED_USER = "ol-agent";
|
|
@@ -1228,189 +1059,6 @@ async function resolveAgentUser(input) {
|
|
|
1228
1059
|
if (uid === null) throw new AgentUserError(`the user "${candidate}" is not in the image, so no command can run as it`);
|
|
1229
1060
|
return uid === 0 ? ADDED_USER : candidate;
|
|
1230
1061
|
}
|
|
1231
|
-
var TokenRefused = class extends Error {
|
|
1232
|
-
constructor(code, detail) {
|
|
1233
|
-
super(code);
|
|
1234
|
-
this.code = code;
|
|
1235
|
-
this.detail = detail;
|
|
1236
|
-
}
|
|
1237
|
-
};
|
|
1238
|
-
var NoControlPlane = class extends Error {
|
|
1239
|
-
};
|
|
1240
|
-
var GatewayConflict = class extends Error {
|
|
1241
|
-
};
|
|
1242
|
-
var unavailable = (why) => ({ kind: "refused", reason: `context_unavailable: ${why}` });
|
|
1243
|
-
var DESIGNATION_LOOKUP = { timeoutMs: 1e4, attempts: 2 };
|
|
1244
|
-
function lookupFailureReason(failure, repository) {
|
|
1245
|
-
switch (failure?.kind) {
|
|
1246
|
-
case "timeout":
|
|
1247
|
-
return `the gateway did not answer which repository governs ${repository} within ${failure.timeoutMs / 1e3} seconds, twice`;
|
|
1248
|
-
case "status":
|
|
1249
|
-
return `the gateway answered ${failure.status} when asked which repository governs ${repository}`;
|
|
1250
|
-
case "network":
|
|
1251
|
-
return `the request asking which repository governs ${repository} failed: ${failure.message}`;
|
|
1252
|
-
default:
|
|
1253
|
-
return `the gateway did not say which repository governs ${repository}`;
|
|
1254
|
-
}
|
|
1255
|
-
}
|
|
1256
|
-
function makeReadable(tree, dir, files) {
|
|
1257
|
-
chmodSync(dir, 493);
|
|
1258
|
-
chmodSync(tree, 493);
|
|
1259
|
-
const directories = /* @__PURE__ */ new Set();
|
|
1260
|
-
for (const rel of files) {
|
|
1261
|
-
for (let current = dirname(join(tree, rel)); current !== tree; current = dirname(current)) directories.add(current);
|
|
1262
|
-
chmodSync(join(tree, rel), (lstatSync(join(tree, rel)).mode & 73) !== 0 ? 493 : 420);
|
|
1263
|
-
}
|
|
1264
|
-
for (const directory of directories) chmodSync(directory, 493);
|
|
1265
|
-
}
|
|
1266
|
-
async function stageGovernedContext(input) {
|
|
1267
|
-
const git2 = input.git ?? defaultGitRunner;
|
|
1268
|
-
const dir = stagedContextDir(input.buildDir);
|
|
1269
|
-
const tree = join(dir, "tree");
|
|
1270
|
-
const finish = (outcome) => {
|
|
1271
|
-
removeStagedContext(input.buildDir);
|
|
1272
|
-
return outcome;
|
|
1273
|
-
};
|
|
1274
|
-
try {
|
|
1275
|
-
mkdirSync(tree, { recursive: true });
|
|
1276
|
-
for (const args of [
|
|
1277
|
-
["init", "-q", "--initial-branch=main", tree],
|
|
1278
|
-
["-C", tree, "remote", "add", "origin", `https://github.com/${input.repository}.git`]
|
|
1279
|
-
]) {
|
|
1280
|
-
const done = git2(args, { cwd: dir, timeoutMs: 1e4 });
|
|
1281
|
-
if (!done.ok) return finish(unavailable(`the staging directory could not be prepared: ${done.stderr.trim().split("\n")[0] ?? ""}`));
|
|
1282
|
-
}
|
|
1283
|
-
const { url, apiKey, appId, configPath } = input.config;
|
|
1284
|
-
let tokenRepository;
|
|
1285
|
-
const credentialFor = (repository) => tokenRepository !== void 0 && tokenRepository.toLowerCase() === repository.toLowerCase() ? "the token the gateway issued" : "the host's git login";
|
|
1286
|
-
const unreadable = (repositories) => repositories.map((repository) => `${credentialFor(repository)} cannot read ${repository}`).join(" and ");
|
|
1287
|
-
const result = await materializeContext({
|
|
1288
|
-
cwd: tree,
|
|
1289
|
-
sessionId: `build-${input.buildId}`,
|
|
1290
|
-
home: contextHomeDir(configPath),
|
|
1291
|
-
cloud: { url, apiKey, appId },
|
|
1292
|
-
fetchImpl: signingFetchFor(input.fetchImpl, configPath, apiKey),
|
|
1293
|
-
designationLookup: DESIGNATION_LOOKUP,
|
|
1294
|
-
git: git2,
|
|
1295
|
-
now: input.now,
|
|
1296
|
-
resolveControlPlaneUrl: (repository) => input.controlPlaneUrl(repository),
|
|
1297
|
-
controlPlaneAccess: input.controlPlaneToken === void 0 ? void 0 : async (candidates) => {
|
|
1298
|
-
const access = await controlPlaneAccess(input.controlPlaneToken, candidates, input.repository, input.log, input.tag);
|
|
1299
|
-
tokenRepository = access?.repository;
|
|
1300
|
-
return access;
|
|
1301
|
-
}
|
|
1302
|
-
});
|
|
1303
|
-
switch (result.status) {
|
|
1304
|
-
case "materialized": {
|
|
1305
|
-
if (result.unfetched !== void 0 && result.unfetched.length > 0) {
|
|
1306
|
-
return finish(unavailable(`${unreadable(result.unfetched)}, a control plane named for ${input.repository}, so it cannot be ruled out as a second claimant`));
|
|
1307
|
-
}
|
|
1308
|
-
const manifest = readContextManifest(join(tree, ".git"));
|
|
1309
|
-
const source = readSourceCache(join(tree, ".git"));
|
|
1310
|
-
if (result.sha === null || manifest === void 0 || source?.status !== "governed" || source.repository === void 0 || source.ref === void 0) {
|
|
1311
|
-
return finish(unavailable(`the context of ${input.repository} was materialized without a record of it`));
|
|
1312
|
-
}
|
|
1313
|
-
writeFileSync(
|
|
1314
|
-
join(dir, "staged.json"),
|
|
1315
|
-
JSON.stringify({ schemaVersion: 1, sha: result.sha, repository: source.repository, ref: source.ref, files: manifest.files }) + "\n"
|
|
1316
|
-
);
|
|
1317
|
-
makeReadable(tree, dir, manifest.files);
|
|
1318
|
-
chmodSync(join(dir, "staged.json"), 420);
|
|
1319
|
-
return { kind: "staged", dir, sha: result.sha, files: manifest.files };
|
|
1320
|
-
}
|
|
1321
|
-
case "self":
|
|
1322
|
-
if (result.unfetched !== void 0 && result.unfetched.length > 0) {
|
|
1323
|
-
return finish(unavailable(`${unreadable(result.unfetched)}, the control plane named for ${input.repository}`));
|
|
1324
|
-
}
|
|
1325
|
-
if (result.designation === "answered") return finish({ kind: "none" });
|
|
1326
|
-
if (result.authRejected === true) {
|
|
1327
|
-
return finish(unavailable(`the gateway rejected the runner's API key (401/403) when asked which repository governs ${input.repository}`));
|
|
1328
|
-
}
|
|
1329
|
-
return finish(unavailable(lookupFailureReason(result.lookupFailure, input.repository)));
|
|
1330
|
-
case "conflict":
|
|
1331
|
-
return finish({ kind: "refused", reason: `context_conflict: ${result.message ?? "two control planes claim this repository"}` });
|
|
1332
|
-
default:
|
|
1333
|
-
return finish(unavailable(result.message ?? `the context of ${input.repository} could not be materialized (${result.status})`));
|
|
1334
|
-
}
|
|
1335
|
-
} catch (err) {
|
|
1336
|
-
if (err instanceof NoControlPlane) return finish({ kind: "none" });
|
|
1337
|
-
if (err instanceof GatewayConflict) return finish({ kind: "refused", reason: `context_conflict: ${err.message}` });
|
|
1338
|
-
if (err instanceof TokenRefused) {
|
|
1339
|
-
return finish(
|
|
1340
|
-
unavailable(
|
|
1341
|
-
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}`
|
|
1342
|
-
)
|
|
1343
|
-
);
|
|
1344
|
-
}
|
|
1345
|
-
return finish(unavailable(`the context of ${input.repository} could not be staged: ${err instanceof Error ? err.message : String(err)}`));
|
|
1346
|
-
}
|
|
1347
|
-
}
|
|
1348
|
-
async function controlPlaneAccess(ask, candidates, repository, log, tag) {
|
|
1349
|
-
let answer;
|
|
1350
|
-
try {
|
|
1351
|
-
answer = await ask();
|
|
1352
|
-
} catch (err) {
|
|
1353
|
-
const code = refusalCode(err);
|
|
1354
|
-
if (code === "repository_tokens_unavailable") return void 0;
|
|
1355
|
-
if (code === "no_control_plane") throw new NoControlPlane(code);
|
|
1356
|
-
if (code === "context_conflict") throw new GatewayConflict(err instanceof Error ? err.message : code);
|
|
1357
|
-
throw new TokenRefused(code, err instanceof WorkCommandError && err.code === void 0 && err.status === void 0 ? describeCallFailure(err) : void 0);
|
|
1358
|
-
}
|
|
1359
|
-
const named = candidates.some((candidate) => candidate.repository.toLowerCase() === answer.repository.toLowerCase());
|
|
1360
|
-
if (!named) {
|
|
1361
|
-
log?.line(
|
|
1362
|
-
`${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`
|
|
1363
|
-
);
|
|
1364
|
-
return void 0;
|
|
1365
|
-
}
|
|
1366
|
-
return answer;
|
|
1367
|
-
}
|
|
1368
|
-
function removeStagedContext(buildDir2) {
|
|
1369
|
-
rmSync(stagedContextDir(buildDir2), { recursive: true, force: true });
|
|
1370
|
-
}
|
|
1371
|
-
|
|
1372
|
-
// src/runner/container/docker.ts
|
|
1373
|
-
function createContainerHost(deps = {}) {
|
|
1374
|
-
const run = deps.run ?? spawnRun;
|
|
1375
|
-
return {
|
|
1376
|
-
async version() {
|
|
1377
|
-
const result = await run("docker", ["version", "--format", "{{.Server.Version}}"]);
|
|
1378
|
-
return result.status === 0 && result.stdout.trim() !== "" ? result.stdout.trim() : null;
|
|
1379
|
-
},
|
|
1380
|
-
async operatingSystem() {
|
|
1381
|
-
const result = await run("docker", ["info", "--format", "{{.OperatingSystem}}"]);
|
|
1382
|
-
return result.status === 0 && result.stdout.trim() !== "" ? result.stdout.trim() : null;
|
|
1383
|
-
},
|
|
1384
|
-
async imageUser(imageId) {
|
|
1385
|
-
const result = await run("docker", ["image", "inspect", "--format", "{{.Config.User}}", imageId]);
|
|
1386
|
-
return result.status === 0 ? result.stdout.trim() : "";
|
|
1387
|
-
},
|
|
1388
|
-
createVolume(name, labels) {
|
|
1389
|
-
const flags = Object.entries(labels).flatMap(([key, value]) => ["--label", `${key}=${value}`]);
|
|
1390
|
-
return run("docker", ["volume", "create", ...flags, name]);
|
|
1391
|
-
},
|
|
1392
|
-
removeVolume(name) {
|
|
1393
|
-
return run("docker", ["volume", "rm", "--force", name]);
|
|
1394
|
-
},
|
|
1395
|
-
run(args, opts) {
|
|
1396
|
-
return run("docker", ["run", ...args], opts);
|
|
1397
|
-
},
|
|
1398
|
-
exec(input, opts) {
|
|
1399
|
-
const env = {};
|
|
1400
|
-
for (const [name, value] of Object.entries(input.env.passed)) if (value !== void 0) env[name] = value;
|
|
1401
|
-
return run("docker", ["exec", ...containerExecArgs(input)], { ...opts, env });
|
|
1402
|
-
},
|
|
1403
|
-
stop(container) {
|
|
1404
|
-
return run("docker", ["stop", container]);
|
|
1405
|
-
},
|
|
1406
|
-
start(container) {
|
|
1407
|
-
return run("docker", ["start", container]);
|
|
1408
|
-
},
|
|
1409
|
-
remove(container) {
|
|
1410
|
-
return run("docker", ["rm", "--force", "--volumes", container]);
|
|
1411
|
-
}
|
|
1412
|
-
};
|
|
1413
|
-
}
|
|
1414
1062
|
var FILE_MODE = 420;
|
|
1415
1063
|
var DIR_MODE = 493;
|
|
1416
1064
|
function replaceFile(path, text) {
|
|
@@ -2298,6 +1946,7 @@ function createContainerRuntime(opts = {}) {
|
|
|
2298
1946
|
configPath: build.configPath,
|
|
2299
1947
|
cli: cli ?? currentCliBuild(),
|
|
2300
1948
|
readToken: build.readToken,
|
|
1949
|
+
pinned: build.pinned,
|
|
2301
1950
|
guestHelper: build.guestHelper,
|
|
2302
1951
|
log: build.log
|
|
2303
1952
|
},
|
|
@@ -2368,8 +2017,9 @@ async function obtainImage(input) {
|
|
|
2368
2017
|
if (input.aborted()) return { ok: false, reason: "ended before provision finished", aborted: true };
|
|
2369
2018
|
let readToken;
|
|
2370
2019
|
try {
|
|
2371
|
-
readToken = await tokens.token("read");
|
|
2020
|
+
readToken = input.pinned === void 0 ? await tokens.token("read") : await controlPlaneReadToken(tokens, repository);
|
|
2372
2021
|
} catch (err) {
|
|
2022
|
+
if (input.pinned !== void 0) return { ok: false, reason: controlPlaneTokenFailure(err, input.governedRepository ?? repository) };
|
|
2373
2023
|
return {
|
|
2374
2024
|
ok: false,
|
|
2375
2025
|
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)}`
|
|
@@ -2383,6 +2033,7 @@ async function obtainImage(input) {
|
|
|
2383
2033
|
configPath: config.configPath,
|
|
2384
2034
|
buildMemory: config.build?.memory ?? "8g",
|
|
2385
2035
|
readToken,
|
|
2036
|
+
pinned: input.pinned,
|
|
2386
2037
|
guestHelper: input.guestHelper,
|
|
2387
2038
|
log: (line) => log.line(`${tag} ${line}`)
|
|
2388
2039
|
})
|
|
@@ -2402,6 +2053,16 @@ async function obtainImage(input) {
|
|
|
2402
2053
|
}
|
|
2403
2054
|
}
|
|
2404
2055
|
}
|
|
2056
|
+
async function controlPlaneReadToken(tokens, controlPlane) {
|
|
2057
|
+
try {
|
|
2058
|
+
const issued = await tokens.controlPlane();
|
|
2059
|
+
return issued.repository.toLowerCase() === controlPlane.toLowerCase() ? issued.token : void 0;
|
|
2060
|
+
} catch (err) {
|
|
2061
|
+
const code = refusalCode(err);
|
|
2062
|
+
if (code === "repository_tokens_unavailable" || code === "no_control_plane" || code === "context_conflict") return void 0;
|
|
2063
|
+
throw err;
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2405
2066
|
async function provisionContainer(input) {
|
|
2406
2067
|
const { config, runtime, item, itemKey, jobDir, log, tokens } = input;
|
|
2407
2068
|
const { host } = runtime;
|
|
@@ -2421,7 +2082,23 @@ async function provisionContainer(input) {
|
|
|
2421
2082
|
if (/docker desktop/i.test(await host.operatingSystem() ?? "")) {
|
|
2422
2083
|
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");
|
|
2423
2084
|
}
|
|
2424
|
-
const
|
|
2085
|
+
const governing = await resolveGoverningSource({ config, repository, fetchImpl: input.fetchImpl });
|
|
2086
|
+
if (!governing.ok) return fail(governing.reason);
|
|
2087
|
+
const governingSource = governing.source;
|
|
2088
|
+
const imageRepository = governingSource.kind === "governed" ? governingSource.repository : repository;
|
|
2089
|
+
const obtained = await obtainImage({
|
|
2090
|
+
config,
|
|
2091
|
+
runtime,
|
|
2092
|
+
tokens,
|
|
2093
|
+
repository: imageRepository,
|
|
2094
|
+
pinned: governingSource.kind === "governed" ? { commit: governingSource.sha, ref: governingSource.ref } : void 0,
|
|
2095
|
+
governedRepository: repository,
|
|
2096
|
+
tag,
|
|
2097
|
+
log,
|
|
2098
|
+
aborted: input.aborted,
|
|
2099
|
+
deadlineMs: input.deadlineMs,
|
|
2100
|
+
now: input.now
|
|
2101
|
+
});
|
|
2425
2102
|
if (!obtained.ok) return obtained.aborted === true ? { ok: false, reason: obtained.reason, aborted: true } : fail(obtained.reason);
|
|
2426
2103
|
const built = obtained.built;
|
|
2427
2104
|
const { report } = built;
|
|
@@ -2438,9 +2115,10 @@ async function provisionContainer(input) {
|
|
|
2438
2115
|
containerEnv: report.apply.containerEnv,
|
|
2439
2116
|
builtIns: true,
|
|
2440
2117
|
lifecycle: report.apply.lifecycle,
|
|
2118
|
+
...governingSource.kind === "governed" ? { governing: { repository: governingSource.repository, ref: governingSource.ref, commit: governingSource.sha } } : {},
|
|
2441
2119
|
cpuset: formatCpuList(runtime.cpus.acquire(buildId, config.build?.cpus ?? 4))
|
|
2442
2120
|
};
|
|
2443
|
-
const
|
|
2121
|
+
const envInput = {
|
|
2444
2122
|
config,
|
|
2445
2123
|
item: item.number,
|
|
2446
2124
|
itemKey,
|
|
@@ -2449,7 +2127,8 @@ async function provisionContainer(input) {
|
|
|
2449
2127
|
hostEnv: runtime.hostEnv,
|
|
2450
2128
|
containerEnv: report.apply.containerEnv,
|
|
2451
2129
|
builtIns: true
|
|
2452
|
-
}
|
|
2130
|
+
};
|
|
2131
|
+
const env = buildEnvironment(envInput);
|
|
2453
2132
|
if (env.skippedTokenVariables.length > 0) {
|
|
2454
2133
|
log.line(`${tag} not passing ${env.skippedTokenVariables.join(", ")} from runner.build.variables into the build: its repository token comes from the runner`);
|
|
2455
2134
|
}
|
|
@@ -2504,6 +2183,9 @@ async function provisionContainer(input) {
|
|
|
2504
2183
|
const volume = await host.createVolume(name, { [BUILD_LABEL]: buildId, [RUNNER_LABEL]: runnerId });
|
|
2505
2184
|
if (volume.status !== 0) return dockerFailed("creating the build's volume", volume);
|
|
2506
2185
|
if (input.aborted()) return bail("", true);
|
|
2186
|
+
const judgeDir = join(jobDir, "judge");
|
|
2187
|
+
mkdirSync(judgeDir, { recursive: true });
|
|
2188
|
+
chmodSync(judgeDir, 493);
|
|
2507
2189
|
const runInput = {
|
|
2508
2190
|
buildId,
|
|
2509
2191
|
runnerId,
|
|
@@ -2514,7 +2196,8 @@ async function provisionContainer(input) {
|
|
|
2514
2196
|
user: ADDED_USER,
|
|
2515
2197
|
limits: limits2,
|
|
2516
2198
|
destinations: config.build?.destinations?.length ?? 0,
|
|
2517
|
-
builtIns: true
|
|
2199
|
+
builtIns: true,
|
|
2200
|
+
judgeDir
|
|
2518
2201
|
};
|
|
2519
2202
|
const prepared = await host.run(prepareRunArgs(runInput), { logPath });
|
|
2520
2203
|
if (prepared.status !== 0) return dockerFailed("preparing the build's volume", prepared);
|
|
@@ -2522,12 +2205,13 @@ async function provisionContainer(input) {
|
|
|
2522
2205
|
if (started.status !== 0) return dockerFailed("starting the build's container", started);
|
|
2523
2206
|
if (input.aborted()) return bail("", true);
|
|
2524
2207
|
if (!await waitForRelay(runtime, state)) return bail("the relay in the build's container did not start");
|
|
2525
|
-
const
|
|
2208
|
+
const imageConfig = await host.imageConfig(report.imageId);
|
|
2209
|
+
state.imageEnv = imageConfig.env;
|
|
2526
2210
|
let agentUser;
|
|
2527
2211
|
try {
|
|
2528
2212
|
agentUser = await resolveAgentUser({
|
|
2529
2213
|
remoteUser: report.apply.remoteUser,
|
|
2530
|
-
imageUser,
|
|
2214
|
+
imageUser: imageConfig.user,
|
|
2531
2215
|
probe: async (user) => {
|
|
2532
2216
|
const probe = await host.exec({ container: name, user: ADDED_USER, env: NO_ENV, argv: ["id", "-u", user] });
|
|
2533
2217
|
const uid = Number(probe.stdout);
|
|
@@ -2540,13 +2224,20 @@ async function provisionContainer(input) {
|
|
|
2540
2224
|
}
|
|
2541
2225
|
state.agentUser = agentUser;
|
|
2542
2226
|
const tools = await host.exec({ container: name, user: ADDED_USER, env: NO_ENV, argv: ["cat", "/opt/outerlayer/tools.json"] });
|
|
2543
|
-
|
|
2227
|
+
const agentVersion = agentVersionOf(tools.status === 0 ? tools.stdout : "");
|
|
2228
|
+
environment.agentVersion = agentVersion;
|
|
2229
|
+
const program = await host.exec({ container: name, user: ADDED_USER, env: NO_ENV, argv: [...HASH_ARGV] });
|
|
2230
|
+
const hashed = program.status === 0 ? parseProgramHash(program.stdout) : void 0;
|
|
2231
|
+
if (hashed !== void 0) state.judgeProgram = { path: JUDGE_PROGRAM, sha256: hashed };
|
|
2544
2232
|
input.save(state, environment);
|
|
2545
|
-
const
|
|
2233
|
+
const execEnv = buildEnvironment({ ...envInput, imageEnv: state.imageEnv });
|
|
2234
|
+
const exec = (argv, workdir, opts) => host.exec({ container: name, user: agentUser, workdir, env: execEnv, argv }, opts?.quiet === true ? void 0 : { logPath });
|
|
2546
2235
|
const steps = await runBuildSteps({
|
|
2547
2236
|
repository,
|
|
2548
2237
|
branch: primary.branch,
|
|
2549
2238
|
repositories,
|
|
2239
|
+
...governingSource.kind === "governed" ? { governing: { repository: governingSource.repository } } : {},
|
|
2240
|
+
agentVersion,
|
|
2550
2241
|
lifecycle: report.apply.lifecycle,
|
|
2551
2242
|
exec,
|
|
2552
2243
|
log,
|
|
@@ -2556,6 +2247,7 @@ async function provisionContainer(input) {
|
|
|
2556
2247
|
stageContext: () => stageGovernedContext({
|
|
2557
2248
|
config,
|
|
2558
2249
|
repository,
|
|
2250
|
+
source: pinnedSourceOf(governingSource),
|
|
2559
2251
|
buildId,
|
|
2560
2252
|
buildDir: buildDir2,
|
|
2561
2253
|
fetchImpl: input.fetchImpl,
|
|
@@ -2568,6 +2260,7 @@ async function provisionContainer(input) {
|
|
|
2568
2260
|
});
|
|
2569
2261
|
if (!steps.ok) return bail(steps.reason, steps.aborted === true);
|
|
2570
2262
|
state.startCommit = steps.startCommit;
|
|
2263
|
+
if (steps.controlPlane !== void 0) environment.controlPlane = steps.controlPlane;
|
|
2571
2264
|
return { ok: true, workdir: CHECKOUT_DIR, state, environment };
|
|
2572
2265
|
}
|
|
2573
2266
|
function agentVersionOf(toolsJson) {
|
|
@@ -2593,14 +2286,15 @@ function containerCommandLine(input) {
|
|
|
2593
2286
|
hostEnv: input.runtime.hostEnv,
|
|
2594
2287
|
containerEnv: state.containerEnv ?? {},
|
|
2595
2288
|
builtIns: state.builtIns === true,
|
|
2596
|
-
identity: input.job.commitIdentity
|
|
2289
|
+
identity: input.job.commitIdentity,
|
|
2290
|
+
imageEnv: state.imageEnv
|
|
2597
2291
|
});
|
|
2598
2292
|
const args = containerExecArgs({
|
|
2599
2293
|
container: state.container,
|
|
2600
2294
|
user: state.agentUser ?? ADDED_USER,
|
|
2601
2295
|
workdir: CHECKOUT_DIR,
|
|
2602
2296
|
env,
|
|
2603
|
-
argv: ["/bin/sh", "-c", input.cmdline]
|
|
2297
|
+
argv: ["/bin/sh", "-c", commandWithShim(input.job, input.cmdline)]
|
|
2604
2298
|
});
|
|
2605
2299
|
return {
|
|
2606
2300
|
line: ["docker", "exec", ...args].map(shellQuote).join(" "),
|
|
@@ -2619,7 +2313,8 @@ function containerSenderCommand(input) {
|
|
|
2619
2313
|
hostEnv: input.runtime.hostEnv,
|
|
2620
2314
|
containerEnv: state.containerEnv ?? {},
|
|
2621
2315
|
builtIns: state.builtIns === true,
|
|
2622
|
-
identity: input.job.commitIdentity
|
|
2316
|
+
identity: input.job.commitIdentity,
|
|
2317
|
+
imageEnv: state.imageEnv
|
|
2623
2318
|
});
|
|
2624
2319
|
const args = containerExecArgs({
|
|
2625
2320
|
container: state.container,
|
|
@@ -2696,7 +2391,8 @@ async function syncInContainer(input) {
|
|
|
2696
2391
|
hostEnv: runtime.hostEnv,
|
|
2697
2392
|
containerEnv: state.containerEnv ?? {},
|
|
2698
2393
|
builtIns: state.builtIns === true,
|
|
2699
|
-
identity: job.commitIdentity
|
|
2394
|
+
identity: job.commitIdentity,
|
|
2395
|
+
imageEnv: state.imageEnv
|
|
2700
2396
|
});
|
|
2701
2397
|
const result = await runtime.host.exec(
|
|
2702
2398
|
{ container: state.container, user: state.agentUser ?? ADDED_USER, workdir: job.workdir ?? CHECKOUT_DIR, env, argv: ["outerlayer", "sync"] },
|
|
@@ -2754,6 +2450,53 @@ async function cleanupContainer(runtime, state, jobDir) {
|
|
|
2754
2450
|
runtime.cpus.release(state.buildId);
|
|
2755
2451
|
if (failures.length > 0) throw new Error(failures.join("; "));
|
|
2756
2452
|
}
|
|
2453
|
+
function containerJudgeEnvironment(input) {
|
|
2454
|
+
const { runtime, job } = input;
|
|
2455
|
+
const state = job.container;
|
|
2456
|
+
const user = state.agentUser ?? ADDED_USER;
|
|
2457
|
+
const folder = join(input.jobDir, "judge");
|
|
2458
|
+
return {
|
|
2459
|
+
stop: async () => {
|
|
2460
|
+
if (input.config !== void 0) await ensureTunnel(runtime, state, input.jobDir, input.config, job.itemKey, job.branch, job.repositories);
|
|
2461
|
+
await stopContainer(runtime, state);
|
|
2462
|
+
const started = await runtime.host.start(state.container);
|
|
2463
|
+
if (started.status !== 0) throw new Error(`the build's container could not be started again (exit ${started.status})`);
|
|
2464
|
+
state.stopped = false;
|
|
2465
|
+
if (!await waitForRelay(runtime, state)) throw new Error("the relay in the build's container did not start again");
|
|
2466
|
+
},
|
|
2467
|
+
hashProgram: async () => {
|
|
2468
|
+
const ran = await runtime.host.exec({ container: state.container, user: ADDED_USER, env: NO_ENV, argv: [...HASH_ARGV] });
|
|
2469
|
+
const sha256 = ran.status === 0 ? parseProgramHash(ran.stdout) : void 0;
|
|
2470
|
+
return sha256 === void 0 ? { ok: false, reason: firstLine5(ran.stderr) || `the hash could not be taken (exit ${ran.status})` } : { ok: true, sha256 };
|
|
2471
|
+
},
|
|
2472
|
+
prepare: async (files, tree) => {
|
|
2473
|
+
mkdirSync(folder, { recursive: true });
|
|
2474
|
+
for (const entry of readdirSync(folder)) rmSync(join(folder, entry), { recursive: true, force: true });
|
|
2475
|
+
mkdirSync(join(folder, "home", "tmp"), { recursive: true });
|
|
2476
|
+
for (const [fileName, text] of Object.entries(files)) writeFileSync(join(folder, fileName), text, { mode: 420 });
|
|
2477
|
+
if (tree !== void 0) copyJudgeTree(tree, join(folder, "tree"));
|
|
2478
|
+
chmodSync(join(folder, "home"), 511);
|
|
2479
|
+
chmodSync(join(folder, "home", "tmp"), 511);
|
|
2480
|
+
chmodSync(folder, 1023);
|
|
2481
|
+
},
|
|
2482
|
+
run: (command, opts) => runtime.host.exec({ container: state.container, user, workdir: command.cwd, env: { literals: command.env, passed: {} }, argv: command.argv }, { logPath: opts.logPath }),
|
|
2483
|
+
readReport: async () => readRegularFile(join(folder, "report.html"), MAX_REPORT_BYTES),
|
|
2484
|
+
// The judge's files belong to the build's user, and Claude Code makes some of its folders private to it, so the runner's user cannot remove them from the
|
|
2485
|
+
// host. Every capability is dropped in the container, so root there could not either; the build's user removes its own, and the runner removes the rest.
|
|
2486
|
+
clear: async () => {
|
|
2487
|
+
const removed = await runtime.host.exec({ container: state.container, user, env: NO_ENV, argv: ["find", JUDGE_DIR, "-mindepth", "1", "-user", user, "-delete"] });
|
|
2488
|
+
if (removed.status !== 0) throw new Error(`find exited ${removed.status}: ${firstLine5(removed.stderr)}`);
|
|
2489
|
+
}
|
|
2490
|
+
};
|
|
2491
|
+
}
|
|
2492
|
+
function readRegularFile(path, limit) {
|
|
2493
|
+
try {
|
|
2494
|
+
const stat = lstatSync(path);
|
|
2495
|
+
return stat.isFile() && stat.size <= limit ? readFileSync(path, "utf8") : void 0;
|
|
2496
|
+
} catch {
|
|
2497
|
+
return void 0;
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2757
2500
|
async function startCheckContainer(input) {
|
|
2758
2501
|
const { config, runtime, job, jobDir, tokens, log } = input;
|
|
2759
2502
|
const { host } = runtime;
|
|
@@ -2768,7 +2511,7 @@ async function startCheckContainer(input) {
|
|
|
2768
2511
|
const buildDir2 = buildDir(config.configPath, buildId);
|
|
2769
2512
|
const agentUser = built.agentUser ?? ADDED_USER;
|
|
2770
2513
|
const cpuset = built.cpuset ?? formatCpuList(runtime.cpus.acquire(buildId, config.build?.cpus ?? 4));
|
|
2771
|
-
const state = { buildId, container: name, volume: name, buildDir: buildDir2, repository, imageId: built.imageId, agentUser, cpuset };
|
|
2514
|
+
const state = { buildId, container: name, volume: name, buildDir: buildDir2, repository, imageId: built.imageId, imageEnv: built.imageEnv, agentUser, cpuset };
|
|
2772
2515
|
const checksDir = join(jobDir, "checks");
|
|
2773
2516
|
mkdirSync(checksDir, { recursive: true });
|
|
2774
2517
|
const logPath = join(checksDir, "environment.log");
|
|
@@ -2817,7 +2560,7 @@ async function startCheckContainer(input) {
|
|
|
2817
2560
|
const started = await host.run(containerRunArgs(runInput), { logPath });
|
|
2818
2561
|
if (started.status !== 0) return dockerFailed("starting the check environment's container", started);
|
|
2819
2562
|
if (!await waitForRelay(runtime, state)) return fail("the relay in the check environment's container did not start");
|
|
2820
|
-
const env = checkEnvironment({ config, item: job.item, repository, hostEnv: runtime.hostEnv, containerEnv: built.containerEnv ?? {} });
|
|
2563
|
+
const env = checkEnvironment({ config, item: job.item, repository, hostEnv: runtime.hostEnv, containerEnv: built.containerEnv ?? {}, imageEnv: built.imageEnv });
|
|
2821
2564
|
return {
|
|
2822
2565
|
ok: true,
|
|
2823
2566
|
container: {
|
|
@@ -2834,4 +2577,4 @@ async function startCheckContainer(input) {
|
|
|
2834
2577
|
};
|
|
2835
2578
|
}
|
|
2836
2579
|
|
|
2837
|
-
export { ADDED_USER, AgentUserError, AssetError,
|
|
2580
|
+
export { ADDED_USER, AgentUserError, AssetError, EVAL_INPUT_NAME, GH_CONFIG_YML, MIN_ENGINE_MAJOR, cleanupContainer, containerCommandLine, containerJudgeEnvironment, containerSenderCommand, copyJudgeTree, copyTranscripts, copyTranscriptsFromContainer, createBuildDisk, createContainerRuntime, deleteBuildDisk, ensureRootfs, ensureTunnel, ghHostsYml, gitRemoteFor, isLiteralAddress, keepGhConfigCurrent, lifecycleFailureReason, lifecycleSteps, machineConfig, measureDisk, obtainImage, outOfMemoryReason, parseCpuList, prepareBranch, provisionContainer, resolveAgentUser, runBuildSteps, senderSignalScript, setUpRunnerCgroup, shellQuote, signalInContainer, startCheckContainer, startServicesFor, stopContainer, syncInContainer, writeEvalInput };
|