@outerlayer/cli 0.20.0 → 0.22.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 +26 -0
- package/README.md +15 -5
- package/dist/{agent-setup-AM42SA2D.js → agent-setup-XAJH6MPW.js} +13 -12
- package/dist/build-GTMEADQD.js +12 -0
- package/dist/build-command-ND2ENDBS.js +50 -0
- package/dist/build-info.json +1 -1
- package/dist/{builder-C4QNTYMS.js → builder-OAUSTZHL.js} +7 -5
- package/dist/{check-BR5D27CX.js → check-3YHMJQ3P.js} +48 -50
- package/dist/{chunk-AD3EYI7P.js → chunk-22DYXLU7.js} +259 -56
- package/dist/{chunk-P53VEAOF.js → chunk-2CMJLYV6.js} +1 -1
- package/dist/{chunk-FBTGMSQF.js → chunk-2HT2XGJX.js} +16 -9
- package/dist/{chunk-S5PCA3SN.js → chunk-2J5JGLW5.js} +1 -1
- package/dist/{chunk-R22NZSGA.js → chunk-34XORKFD.js} +9 -9
- package/dist/{chunk-QOZKPLVJ.js → chunk-3W333NFO.js} +40 -1
- package/dist/{chunk-HOKXGFDR.js → chunk-4JHVPFH6.js} +22 -16
- package/dist/{chunk-AMFWYESY.js → chunk-4NM3I3VV.js} +1 -1
- package/dist/{chunk-PIZRPNKR.js → chunk-4QKLMNI3.js} +5 -5
- package/dist/{chunk-NNLA7LLQ.js → chunk-5OCIHFGE.js} +29 -5
- package/dist/{chunk-PSN33ERW.js → chunk-6IO62VJS.js} +3 -3
- package/dist/{chunk-7YP7T6GV.js → chunk-7V7KG3YM.js} +2 -2
- package/dist/{chunk-QNLX477Q.js → chunk-AEKSX4EY.js} +165 -72
- package/dist/{chunk-JHGEBPLE.js → chunk-ATRIVTRE.js} +3 -3
- package/dist/{chunk-X5H2WYMB.js → chunk-BLW6GPBZ.js} +1 -1
- package/dist/{chunk-MLAAT4S6.js → chunk-C3HKR67W.js} +3 -3
- package/dist/{chunk-XZ4H4X5F.js → chunk-C3OVGKVJ.js} +15 -8
- package/dist/{chunk-BM63HB2M.js → chunk-EAQSOE62.js} +2 -2
- package/dist/{chunk-LW7G5TJB.js → chunk-EXV37PKS.js} +2 -2
- package/dist/{chunk-B6SCN5NY.js → chunk-FGW2774C.js} +175 -44
- package/dist/{chunk-KYS3TQAG.js → chunk-G2WME4RG.js} +4 -4
- package/dist/{chunk-LRG3ZMZD.js → chunk-G6DZCKA6.js} +4 -4
- package/dist/chunk-GISNHSYL.js +41 -0
- package/dist/{chunk-G2YNJIGF.js → chunk-HCVLP7XA.js} +10 -1
- package/dist/{chunk-QEFKA2QD.js → chunk-I66MQDXH.js} +2 -2
- package/dist/{chunk-3CWUOFV6.js → chunk-IRBNIRQJ.js} +12 -7
- package/dist/{chunk-CG36IONK.js → chunk-JRCSET55.js} +2 -2
- package/dist/{chunk-CGGPS7SI.js → chunk-JVB63NIB.js} +5 -5
- package/dist/{chunk-M2ZWWTWZ.js → chunk-KDHYLBRR.js} +1 -1
- package/dist/{chunk-N2PXGQRL.js → chunk-KIGSGZBB.js} +1 -1
- package/dist/{chunk-EAEELQVQ.js → chunk-KOQELWQ6.js} +5 -5
- package/dist/{chunk-MTP2QEVO.js → chunk-KPBQYEND.js} +1 -1
- package/dist/{chunk-VYAL7PUE.js → chunk-LOB2SUM3.js} +1 -1
- package/dist/{chunk-GGOMT6KF.js → chunk-PHHBYAL6.js} +1 -1
- package/dist/{chunk-IF2OSI5T.js → chunk-QOALB6ZD.js} +2 -2
- package/dist/{chunk-ZX4I2MEK.js → chunk-TPKYTORP.js} +1 -1
- package/dist/{chunk-XNGMQ27R.js → chunk-UD3EXMKB.js} +2 -2
- package/dist/{chunk-7UWVLZXD.js → chunk-USWVXQIH.js} +1 -1
- package/dist/{chunk-BQXFBI7T.js → chunk-V7MQKOZY.js} +32 -7
- package/dist/{chunk-6X4SQ5MY.js → chunk-V7SAQ645.js} +1 -1
- package/dist/{chunk-TWF3RICI.js → chunk-VSUXCMMH.js} +110 -11
- package/dist/{chunk-FVMYKL6L.js → chunk-VWLOQ6FI.js} +1 -1
- package/dist/{chunk-3ZR7Y23Y.js → chunk-VX74ND77.js} +650 -70
- package/dist/{chunk-Z7K3L4FC.js → chunk-W3SWJ2TD.js} +2 -2
- package/dist/chunk-WERZMIKA.js +718 -0
- package/dist/{chunk-GEMQLBMW.js → chunk-WMHYIBJW.js} +6 -6
- package/dist/{chunk-5VML45HH.js → chunk-XCRZTXRL.js} +2 -2
- package/dist/{chunk-ECODQ7II.js → chunk-ZFLOGMUC.js} +1 -1
- package/dist/chunk-ZNPE7FW5.js +137 -0
- package/dist/claude-launcher-4BQNXYAF.js +267 -0
- package/dist/{cli-MKF7TU7X.js → cli-NMGVZQPB.js} +161 -120
- package/dist/{command-ZQNEPDS6.js → command-GE62AJZY.js} +32 -34
- package/dist/{config-CLSGABD5.js → config-7Q52COZ3.js} +3 -2
- package/dist/{connect-cmd-UNYKGDAL.js → connect-cmd-ZFOK6HEL.js} +5 -5
- package/dist/{context-cleanup-CHEWNYU6.js → context-cleanup-DSA3HGQU.js} +1 -1
- package/dist/{control-plane-launch-34LNRJWR.js → control-plane-launch-SZTE4TUS.js} +62 -28
- package/dist/{dist-WO7NOJNG.js → dist-7IH56ELA.js} +1 -1
- package/dist/{docker-BFU3AQGY.js → docker-2OVBKVRK.js} +7 -7
- package/dist/{doctor-DHUM5DBB.js → doctor-3SQGZVYP.js} +35 -36
- package/dist/{emit-artifact-cmd-XBHM647X.js → emit-artifact-cmd-R42BE6ID.js} +19 -18
- package/dist/{emit-cmd-W55AEBCA.js → emit-cmd-3W5IO5ET.js} +3 -3
- package/dist/{emit-criteria-cmd-UNMWHOUY.js → emit-criteria-cmd-MQZE7W4Y.js} +13 -12
- package/dist/{emit-finding-cmd-7JHVRYOP.js → emit-finding-cmd-ZS5JEGGM.js} +13 -12
- package/dist/{emit-result-cmd-5NZMLCJK.js → emit-result-cmd-FUE3DXBW.js} +19 -18
- package/dist/{emit-step-cmd-347XNGPQ.js → emit-step-cmd-T7PRA44X.js} +14 -13
- package/dist/{exec-client-7UMESIE2.js → exec-client-YIDTWXMZ.js} +4 -3
- package/dist/{governing-source-HAA4OW57.js → governing-source-TORXL4I4.js} +19 -18
- package/dist/{guest-init-KPBQOFE5.js → guest-init-TO6HAWIT.js} +4 -3
- package/dist/{hook-fast-RL5E5OXE.js → hook-fast-WFB2ZMRB.js} +2 -2
- package/dist/host-key-2LXQDSBO.js +9 -0
- package/dist/{import-capture-cmd-TK4XY7RD.js → import-capture-cmd-Q56DDZVO.js} +1 -1
- package/dist/index.js +5 -5
- package/dist/init-2CRAVIUU.js +52 -0
- package/dist/{init-V6FOMD2V.js → init-AHDZCMDC.js} +32 -33
- package/dist/{init-cmd-Z4O4PPLZ.js → init-cmd-IXDL2ZSL.js} +25 -23
- package/dist/{init-update-7DVAKWW3.js → init-update-YB3PODDH.js} +69 -24
- package/dist/{install-4SEFIIW2.js → install-CZUIW2AF.js} +5 -4
- package/dist/{install-cmd-QDOTR4OM.js → install-cmd-Q7RJGFUO.js} +6 -5
- package/dist/{login-browser-DX35MARZ.js → login-browser-BN5UKPHS.js} +3 -3
- package/dist/{logout-cmd-5V5MMBSY.js → logout-cmd-RZFWTNYD.js} +2 -2
- package/dist/{logs-AMLKC3VP.js → logs-RJPYTVQE.js} +3 -2
- package/dist/loop-ZDW6TRAA.js +75 -0
- package/dist/machine-JCOHLASP.js +47 -0
- package/dist/{mcp-install-cmd-GGWB3JJ2.js → mcp-install-cmd-GD4QUP7Z.js} +2 -2
- package/dist/{mcp-serve-cmd-N3UTY6EE.js → mcp-serve-cmd-IULQPVHL.js} +6 -5
- package/dist/{paths-7XP63OAX.js → paths-N3LU4O2Z.js} +3 -2
- package/dist/pending-GRCV3ZR4.js +9 -0
- package/dist/{pidfile-3DM4OBKO.js → pidfile-IF3TCL5A.js} +4 -3
- package/dist/receive-files-T2BIUBQL.js +254 -0
- package/dist/{record-3WP6V2K5.js → record-73PQE77B.js} +4 -3
- package/dist/relay-PSZ4LGMW.js +210 -0
- package/dist/relay-protocol-K7KP2SPI.js +6 -0
- package/dist/{runner-env-FRZFFHDF.js → runner-env-LDOBJESN.js} +6 -5
- package/dist/runner-identity-RWCBTY2A.js +14 -0
- package/dist/{runner-key-5FOWCW7J.js → runner-key-HMYLP6WN.js} +32 -33
- package/dist/{session-sender-U2BBUWNO.js → session-sender-2G25JYHI.js} +20 -19
- package/dist/{spec-cmd-FVLJOFGC.js → spec-cmd-LBVXM6XS.js} +13 -12
- package/dist/{starter-pack-6LQVLBIA.js → starter-pack-5EQYGSJF.js} +2 -2
- package/dist/{status-BINH7MBG.js → status-QZB2GWXJ.js} +36 -38
- package/dist/{statusline-fast-KN3BX67H.js → statusline-fast-2OK3DVAA.js} +2 -2
- package/dist/{sync-cmd-VXOUFS4I.js → sync-cmd-TQQIR4PN.js} +19 -18
- package/dist/{watch-OH645CV2.js → watch-4JES4PCS.js} +19 -18
- package/dist/{work-claim-cmd-DRQCHCOK.js → work-claim-cmd-XG6O7B63.js} +17 -16
- package/dist/{work-cmd-5I3J2XQQ.js → work-cmd-L6WMPAA2.js} +13 -12
- package/dist/{work-comment-cmd-UCCPWUSR.js → work-comment-cmd-SNTDCPGH.js} +13 -12
- package/dist/{work-logs-cmd-7352CJPG.js → work-logs-cmd-3S7IAVII.js} +13 -12
- package/dist/{work-open-pr-cmd-LSH4VHJM.js → work-open-pr-cmd-E3WMO5XT.js} +15 -14
- package/dist/{work-pr-cmd-H4OQR26N.js → work-pr-cmd-22T6FG4T.js} +14 -13
- package/dist/{work-stop-cmd-CRDHXQVW.js → work-stop-cmd-WR7ESS4Z.js} +13 -12
- package/package.json +2 -2
- package/skill-pack/maintained/outerlayer-build/SKILL.md +347 -0
- package/skill-pack/{template/build → maintained/outerlayer-build}/references/evidence.md +2 -2
- package/skill-pack/{template/build → maintained/outerlayer-build}/references/release.md +4 -4
- package/skill-pack/{template/build → maintained/outerlayer-build}/references/review-loop.md +3 -1
- package/skill-pack/shipped.json +2 -0
- package/skill-pack/template/build/SKILL.md +4 -253
- package/skill-pack/validators/acceptance-criteria.yaml +1 -1
- package/dist/build-TQ6X5PYN.js +0 -12
- package/dist/chunk-3UZTIRLF.js +0 -320
- package/dist/chunk-CX22S5BA.js +0 -45
- package/dist/chunk-LLM3I5YU.js +0 -89
- package/dist/chunk-SADUQSO4.js +0 -50
- package/dist/chunk-VMJPFQNE.js +0 -261
- package/dist/claude-launcher-J5M44PFS.js +0 -115
- package/dist/host-key-U4B5QETR.js +0 -8
- package/dist/init-XG47KCI4.js +0 -53
- package/dist/loop-EAZQMAE2.js +0 -77
- package/dist/machine-TRHBT5NE.js +0 -48
- package/dist/pending-532ABZYC.js +0 -8
- package/dist/relay-L76HDX72.js +0 -46
- package/dist/{chunk-YVZJ6GKF.js → chunk-DIB2WLBL.js} +1 -1
- package/dist/{chunk-GOSFARMS.js → chunk-F35VMF5M.js} +1 -1
- package/dist/{chunk-DSF2FWAZ.js → chunk-IZSVXRDR.js} +1 -1
- /package/skill-pack/{template/build → maintained/outerlayer-build}/references/agent-briefs.md +0 -0
- /package/skill-pack/{template/build → maintained/outerlayer-build}/references/criteria-judge.md +0 -0
- /package/skill-pack/{template/build → maintained/outerlayer-build}/references/review-briefs.md +0 -0
- /package/skill-pack/{template/build → maintained/outerlayer-build}/scripts/criterion-citations.mjs +0 -0
- /package/skill-pack/{template/build → maintained/outerlayer-build}/scripts/record-criteria.mjs +0 -0
|
@@ -1,33 +1,33 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
|
+
import { readDefaultBranchHead, cloneRepository, buildRepositoryImage } from './chunk-IRBNIRQJ.js';
|
|
3
4
|
import { currentCliBuild } from './chunk-OGFGZQA3.js';
|
|
4
|
-
import { createDockerHost, childEnvironment, spawnRun } from './chunk-
|
|
5
|
-
import {
|
|
6
|
-
import { execCapture } from './chunk-
|
|
7
|
-
import { GUEST_CID } from './chunk-
|
|
8
|
-
import { describeCallFailure, controlPlaneTokenFailure, refusalCode, resolveGoverningSource, removeStagedContext, stageGovernedContext, pinnedSourceOf, commandWithShim, shimScript } from './chunk-
|
|
9
|
-
import { WorkCommandError } from './chunk-
|
|
5
|
+
import { createDockerHost, builderSpec, runnerLabelValue, RUNNER_LABEL, BUILD_LABEL, prepareRunArgs, containerRunArgs, HASH_ARGV, parseProgramHash, JUDGE_PROGRAM, containerExecArgs, childEnvironment, JUDGE_DIR, JUDGE_REPORT, MAX_REPORT_BYTES, spawnRun } from './chunk-WERZMIKA.js';
|
|
6
|
+
import { RecipeError, 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-3W333NFO.js';
|
|
7
|
+
import { execCapture } from './chunk-PHHBYAL6.js';
|
|
8
|
+
import { GUEST_CID } from './chunk-BLW6GPBZ.js';
|
|
9
|
+
import { describeCallFailure, controlPlaneTokenFailure, refusalCode, resolveGoverningSource, removeStagedContext, stageGovernedContext, pinnedSourceOf, commandWithShim, shimScript } from './chunk-C3OVGKVJ.js';
|
|
10
|
+
import { WorkCommandError } from './chunk-2HT2XGJX.js';
|
|
10
11
|
import { hostAllowed } from './chunk-UXYLWVTQ.js';
|
|
11
12
|
import { parseDuKilobytes } from './chunk-7RQ73DLO.js';
|
|
12
|
-
import { readDefaultBranchHead, cloneRepository, buildRepositoryImage } from './chunk-3CWUOFV6.js';
|
|
13
|
-
import { RecipeError } from './chunk-QOZKPLVJ.js';
|
|
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';
|
|
15
13
|
import { startTimeOf, isSameProcess } from './chunk-JBJTC3UU.js';
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
14
|
+
import { sharedSkillNames, skillConflictMessage } from './chunk-ZNPE7FW5.js';
|
|
15
|
+
import { versionAtLeast, MINIMUM_CLAUDE_VERSION } from './chunk-HCVLP7XA.js';
|
|
16
|
+
import { setCustomScrubbers, setLocalIdentity, setRepoRoot, parseTranscript, scrubDeep, scrubText } from './chunk-AEKSX4EY.js';
|
|
19
17
|
import { orderSessions, extractFacts, sessionsToTimeline } from './chunk-WFSUUUAN.js';
|
|
20
18
|
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-
|
|
22
|
-
import { repositoriesOf, primaryOf, RELAY_PORT, repositoryPath, CHECKOUT_DIR, lifecycleWorkdir, claudeCredential, buildEnvironment, startBuildServices, checkEnvironment, AGENT_CREDENTIAL_MISSING, checkoutPath, JOB_MOUNT } from './chunk-
|
|
19
|
+
import { rootfsPathFor, diskTemplatesDir, RUNNER_FILE_MODE, deliveryDir, ghConfigDir, runnerDirOfBuild, spoolDir, mirrorDir, buildDir, RUNNER_DIR_MODE, SHIM_DIR, vmAssetsDir } from './chunk-KIGSGZBB.js';
|
|
20
|
+
import { repositoriesOf, primaryOf, RELAY_PORT, repositoryPath, CHECKOUT_DIR, lifecycleWorkdir, claudeCredential, buildEnvironment, startBuildServices, checkEnvironment, AGENT_CREDENTIAL_MISSING, checkoutPath, JOB_MOUNT } from './chunk-V7MQKOZY.js';
|
|
21
|
+
import { RELAY_USER, RELAY_ID, UNKNOWN_TRANSPORT_REASON, MAX_MALFORMED_SESSIONS, MALFORMED_REASON, MAX_OPEN_STREAMS, STREAM_WINDOW, SESSION_WINDOW, targetOfPath } from './chunk-GISNHSYL.js';
|
|
23
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';
|
|
24
23
|
import { join, dirname, basename } from 'path';
|
|
25
24
|
import { spawnSync, spawn } from 'child_process';
|
|
26
25
|
import { availableParallelism, networkInterfaces } from 'os';
|
|
27
26
|
import { randomBytes, createHash } from 'crypto';
|
|
27
|
+
import http2 from 'http2';
|
|
28
|
+
import { Duplex, Transform } from 'stream';
|
|
28
29
|
import { createServer, request } from 'http';
|
|
29
30
|
import { request as request$1 } from 'https';
|
|
30
|
-
import { Transform } from 'stream';
|
|
31
31
|
import { pipeline, finished } from 'stream/promises';
|
|
32
32
|
import { createGunzip, gunzipSync, constants as constants$1 } from 'zlib';
|
|
33
33
|
import { isIP, connect, createServer as createServer$1 } from 'net';
|
|
@@ -306,6 +306,39 @@ function createCpuAllocator(cores) {
|
|
|
306
306
|
}
|
|
307
307
|
};
|
|
308
308
|
}
|
|
309
|
+
|
|
310
|
+
// src/runner/container/lease-channel.ts
|
|
311
|
+
var LEASE_SECONDS = 900;
|
|
312
|
+
function leaseExpiresAtMs(job) {
|
|
313
|
+
const base = Date.parse(job.lastRenewAt ?? job.startedAt);
|
|
314
|
+
return (Number.isFinite(base) ? base : Date.now()) + LEASE_SECONDS * 1e3;
|
|
315
|
+
}
|
|
316
|
+
function createLeaseChannel(expiryMs) {
|
|
317
|
+
const open = /* @__PURE__ */ new Set();
|
|
318
|
+
let stopped = false;
|
|
319
|
+
const write = (stream) => {
|
|
320
|
+
if (!stopped && !stream.destroyed && stream.writable) stream.write(`${Math.floor(expiryMs())}
|
|
321
|
+
`);
|
|
322
|
+
};
|
|
323
|
+
return {
|
|
324
|
+
accept(stream) {
|
|
325
|
+
stream.on("error", () => void 0);
|
|
326
|
+
open.add(stream);
|
|
327
|
+
stream.once("close", () => open.delete(stream));
|
|
328
|
+
queueMicrotask(() => write(stream));
|
|
329
|
+
},
|
|
330
|
+
publish() {
|
|
331
|
+
for (const stream of open) write(stream);
|
|
332
|
+
},
|
|
333
|
+
stop() {
|
|
334
|
+
stopped = true;
|
|
335
|
+
},
|
|
336
|
+
close() {
|
|
337
|
+
for (const stream of [...open]) stream.destroy();
|
|
338
|
+
open.clear();
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
}
|
|
309
342
|
var AssetError = class extends Error {
|
|
310
343
|
constructor(file, message) {
|
|
311
344
|
super(message);
|
|
@@ -396,8 +429,7 @@ async function ensureAssets(options) {
|
|
|
396
429
|
const helper = await ensureFile(dir, `outerlayer-guest-${arch}`, pins.helper, fetchImpl, 493);
|
|
397
430
|
return { firecracker, kernel, helper, helperSha256: pins.helper.sha256 };
|
|
398
431
|
}
|
|
399
|
-
|
|
400
|
-
// src/runner/container/docker.ts
|
|
432
|
+
var STOP_GRACE_SECONDS = 15;
|
|
401
433
|
function parseImageConfig(json) {
|
|
402
434
|
const config = JSON.parse(json);
|
|
403
435
|
const env = {};
|
|
@@ -435,10 +467,37 @@ function createContainerHost(deps = {}) {
|
|
|
435
467
|
exec(input, opts) {
|
|
436
468
|
const env = {};
|
|
437
469
|
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 });
|
|
470
|
+
return run("docker", ["exec", ...containerExecArgs(input)], { ...opts, env, ...input.stdin === void 0 ? {} : { input: input.stdin } });
|
|
471
|
+
},
|
|
472
|
+
execDetached(input) {
|
|
473
|
+
const env = {};
|
|
474
|
+
for (const [name, value] of Object.entries(input.env.passed)) if (value !== void 0) env[name] = value;
|
|
475
|
+
return run("docker", ["exec", ...containerExecArgs({ ...input, detached: true })], { env });
|
|
476
|
+
},
|
|
477
|
+
async state(container) {
|
|
478
|
+
const result = await run("docker", ["inspect", "--format", "{{.State.Running}}", container]);
|
|
479
|
+
if (result.status !== 0) return /no such (object|container)/i.test(result.stderr) ? "missing" : "unknown";
|
|
480
|
+
return result.stdout.trim() === "true" ? "running" : "stopped";
|
|
481
|
+
},
|
|
482
|
+
execStream(input) {
|
|
483
|
+
const child = (deps.spawn ?? spawn)("docker", ["exec", ...containerExecArgs({ ...input, interactive: true })], {
|
|
484
|
+
env: childEnvironment(process.env),
|
|
485
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
486
|
+
});
|
|
487
|
+
let stderr = "";
|
|
488
|
+
child.stderr?.on("data", (chunk) => {
|
|
489
|
+
stderr = (stderr + chunk.toString()).slice(-400);
|
|
490
|
+
});
|
|
491
|
+
child.stderr?.on("error", () => void 0);
|
|
492
|
+
const exited = new Promise((resolve) => {
|
|
493
|
+
child.once("error", () => resolve(-1));
|
|
494
|
+
child.once("close", (code) => resolve(code ?? -1));
|
|
495
|
+
});
|
|
496
|
+
child.stdin.on("error", () => void 0);
|
|
497
|
+
return { stdin: child.stdin, stdout: child.stdout, exited, kill: () => void child.kill("SIGKILL"), stderrTail: () => stderr };
|
|
439
498
|
},
|
|
440
499
|
stop(container) {
|
|
441
|
-
return run("docker", ["stop", container]);
|
|
500
|
+
return run("docker", ["stop", "--time", String(STOP_GRACE_SECONDS), container]);
|
|
442
501
|
},
|
|
443
502
|
start(container) {
|
|
444
503
|
return run("docker", ["start", container]);
|
|
@@ -713,7 +772,7 @@ var DELIVER_SCRIPT = [
|
|
|
713
772
|
'src="$1"; dest="$2"',
|
|
714
773
|
'if [ -e "$dest" ]; then chmod -R u+w "$dest"; rm -rf "$dest"; fi',
|
|
715
774
|
'mkdir -p "$dest"',
|
|
716
|
-
"for entry in AGENTS.md CLAUDE.md settings.json .claude .outerlayer; do",
|
|
775
|
+
"for entry in AGENTS.md CLAUDE.md settings.json .claude .outerlayer .agents; do",
|
|
717
776
|
' if [ -e "$src/$entry" ]; then cp -R "$src/$entry" "$dest/"; fi',
|
|
718
777
|
"done",
|
|
719
778
|
'chmod -R a-w "$dest"'
|
|
@@ -723,23 +782,33 @@ var WRITE_SHIM_SCRIPT = 'mkdir -p "$1" && printf "%s" "$2" > "$1/claude" && chmo
|
|
|
723
782
|
function failure(what, result) {
|
|
724
783
|
return `context_delivery_failed: ${what} (exit ${result.status})${firstLine3(result.stderr) === "" ? "" : `: ${firstLine3(result.stderr)}`}`;
|
|
725
784
|
}
|
|
785
|
+
async function repositorySkills(input, folder) {
|
|
786
|
+
const listed = await input.exec(["ls", "-1", folder], CHECKOUT_DIR, { quiet: true });
|
|
787
|
+
if (listed.status !== 0) return /* @__PURE__ */ new Set();
|
|
788
|
+
return new Set(listed.stdout.split("\n").map((name) => name.trim()));
|
|
789
|
+
}
|
|
726
790
|
async function deliveryRefusal(input, staged) {
|
|
727
791
|
if (input.agentVersion !== void 0 && !versionAtLeast(input.agentVersion)) {
|
|
728
792
|
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
793
|
}
|
|
730
|
-
const own =
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
794
|
+
const own = /* @__PURE__ */ new Map([
|
|
795
|
+
[".claude/skills", await repositorySkills(input, ".claude/skills")],
|
|
796
|
+
[".agents/skills", await repositorySkills(input, ".agents/skills")]
|
|
797
|
+
]);
|
|
798
|
+
const clash = sharedSkillNames(join(staged.dir, "tree")).find((name) => [...own.values()].some((names) => names.has(name)));
|
|
734
799
|
if (clash === void 0) return void 0;
|
|
735
|
-
|
|
800
|
+
const folder = own.get(".claude/skills").has(clash) ? ".claude/skills" : ".agents/skills";
|
|
801
|
+
return `context_skill_conflict: ${skillConflictMessage({ skill: clash, folder, repository: input.repository, controlPlane: staged.repository })}, and would replace it in this build; rename or remove it from the repository`;
|
|
802
|
+
}
|
|
803
|
+
async function writeShim(input) {
|
|
804
|
+
const shim = await input.exec(["sh", "-c", WRITE_SHIM_SCRIPT, "shim", SHIM_DIR, shimScript()], "/build/work");
|
|
805
|
+
if (shim.status !== 0) return { ok: false, reason: failure("writing the claude shim failed", shim) };
|
|
806
|
+
return { ok: true };
|
|
736
807
|
}
|
|
737
808
|
async function deliver(input, staged) {
|
|
738
809
|
const dir = deliveryDir(staged.repository, staged.sha);
|
|
739
810
|
const copied = await input.exec(["sh", "-c", DELIVER_SCRIPT, "deliver", STAGED_TREE, dir], "/build/work");
|
|
740
811
|
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
812
|
const record = JSON.stringify({ schemaVersion: 1, repository: staged.repository, ref: staged.ref, sha: staged.sha, dir });
|
|
744
813
|
const recorded = await input.exec(["sh", "-c", RECORD_SCRIPT, "record", record], CHECKOUT_DIR);
|
|
745
814
|
if (recorded.status !== 0) return { ok: false, reason: failure("recording the delivery failed", recorded) };
|
|
@@ -813,6 +882,11 @@ async function runBuildSteps(input) {
|
|
|
813
882
|
const staged = await input.stageContext();
|
|
814
883
|
if (staged.kind === "refused") return { ok: false, reason: staged.reason };
|
|
815
884
|
let controlPlane;
|
|
885
|
+
const shimmed = await writeShim(input);
|
|
886
|
+
if (!shimmed.ok) {
|
|
887
|
+
if (staged.kind === "staged") input.removeStagedContext();
|
|
888
|
+
return shimmed;
|
|
889
|
+
}
|
|
816
890
|
if (staged.kind === "staged") {
|
|
817
891
|
try {
|
|
818
892
|
const refusal = await deliveryRefusal(input, staged);
|
|
@@ -1048,17 +1122,190 @@ function copyJudgeTree(from, to, limits2 = { files: MAX_TREE_FILES, bytes: MAX_T
|
|
|
1048
1122
|
|
|
1049
1123
|
// src/runner/container/agent-user.ts
|
|
1050
1124
|
var ADDED_USER = "ol-agent";
|
|
1125
|
+
var RELAY_REFUSAL = `the agent's user cannot be \`${RELAY_USER}\``;
|
|
1051
1126
|
var AgentUserError = class extends Error {
|
|
1052
1127
|
};
|
|
1053
1128
|
async function resolveAgentUser(input) {
|
|
1054
1129
|
const named = input.remoteUser !== void 0 && input.remoteUser !== "" ? input.remoteUser : input.imageUser;
|
|
1055
1130
|
const candidate = (named ?? "").split(":")[0];
|
|
1056
1131
|
if (candidate === "" || candidate === "root") return ADDED_USER;
|
|
1057
|
-
if (
|
|
1132
|
+
if (candidate === RELAY_USER) throw new AgentUserError(RELAY_REFUSAL);
|
|
1133
|
+
if (/^\d+$/.test(candidate)) {
|
|
1134
|
+
if (Number(candidate) === RELAY_ID) throw new AgentUserError(RELAY_REFUSAL);
|
|
1135
|
+
return Number(candidate) === 0 ? ADDED_USER : candidate;
|
|
1136
|
+
}
|
|
1058
1137
|
const uid = await input.probe(candidate);
|
|
1059
1138
|
if (uid === null) throw new AgentUserError(`the user "${candidate}" is not in the image, so no command can run as it`);
|
|
1139
|
+
if (uid === RELAY_ID) throw new AgentUserError(RELAY_REFUSAL);
|
|
1060
1140
|
return uid === 0 ? ADDED_USER : candidate;
|
|
1061
1141
|
}
|
|
1142
|
+
var noop = () => void 0;
|
|
1143
|
+
var FRAME_HEADER_BYTES = 9;
|
|
1144
|
+
var GOAWAY_FRAME = 7;
|
|
1145
|
+
function watchSentGoaway(onError) {
|
|
1146
|
+
let head = Buffer.alloc(0);
|
|
1147
|
+
let want = FRAME_HEADER_BYTES;
|
|
1148
|
+
let skip = 0;
|
|
1149
|
+
return (chunk) => {
|
|
1150
|
+
let at = 0;
|
|
1151
|
+
while (at < chunk.length) {
|
|
1152
|
+
if (skip > 0) {
|
|
1153
|
+
const n = Math.min(skip, chunk.length - at);
|
|
1154
|
+
skip -= n;
|
|
1155
|
+
at += n;
|
|
1156
|
+
continue;
|
|
1157
|
+
}
|
|
1158
|
+
const take = Math.min(want - head.length, chunk.length - at);
|
|
1159
|
+
head = Buffer.concat([head, chunk.subarray(at, at + take)]);
|
|
1160
|
+
at += take;
|
|
1161
|
+
if (head.length < want) return;
|
|
1162
|
+
const length = head.readUIntBE(0, 3);
|
|
1163
|
+
if (want === FRAME_HEADER_BYTES && head[3] === GOAWAY_FRAME && length >= 8) {
|
|
1164
|
+
want = FRAME_HEADER_BYTES + 8;
|
|
1165
|
+
continue;
|
|
1166
|
+
}
|
|
1167
|
+
if (want > FRAME_HEADER_BYTES && head.readUInt32BE(FRAME_HEADER_BYTES + 4) !== http2.constants.NGHTTP2_NO_ERROR) onError();
|
|
1168
|
+
skip = length - (want - FRAME_HEADER_BYTES);
|
|
1169
|
+
head = Buffer.alloc(0);
|
|
1170
|
+
want = FRAME_HEADER_BYTES;
|
|
1171
|
+
}
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
function serveSession(transport, services, opts = {}) {
|
|
1175
|
+
const maxStreams = opts.maxStreams ?? MAX_OPEN_STREAMS;
|
|
1176
|
+
let open = 0;
|
|
1177
|
+
let served = 0;
|
|
1178
|
+
let malformed = false;
|
|
1179
|
+
transport.on("error", noop);
|
|
1180
|
+
const watch = watchSentGoaway(() => {
|
|
1181
|
+
malformed = true;
|
|
1182
|
+
setImmediate(() => session.destroy());
|
|
1183
|
+
});
|
|
1184
|
+
const outbound = new Transform({
|
|
1185
|
+
transform(chunk, _encoding, done) {
|
|
1186
|
+
watch(chunk);
|
|
1187
|
+
done(null, chunk);
|
|
1188
|
+
}
|
|
1189
|
+
});
|
|
1190
|
+
outbound.on("error", noop);
|
|
1191
|
+
outbound.pipe(transport);
|
|
1192
|
+
const wire = Duplex.from({ readable: transport, writable: outbound });
|
|
1193
|
+
wire.on("error", noop);
|
|
1194
|
+
const session = http2.performServerHandshake(wire, { settings: { initialWindowSize: STREAM_WINDOW, enablePush: false } });
|
|
1195
|
+
try {
|
|
1196
|
+
session.setLocalWindowSize(SESSION_WINDOW);
|
|
1197
|
+
} catch {
|
|
1198
|
+
}
|
|
1199
|
+
const ended = new Promise((resolve) => {
|
|
1200
|
+
session.on("error", (err) => {
|
|
1201
|
+
if (typeof err.code === "string" && err.code.startsWith("ERR_HTTP2_")) malformed = true;
|
|
1202
|
+
});
|
|
1203
|
+
session.on("close", () => resolve({ malformed, served }));
|
|
1204
|
+
});
|
|
1205
|
+
session.on("stream", (stream, headers) => {
|
|
1206
|
+
stream.on("error", noop);
|
|
1207
|
+
const target = headers[":method"] === "POST" ? targetOfPath(headers[":path"]) : void 0;
|
|
1208
|
+
if (target === void 0 || open >= maxStreams) {
|
|
1209
|
+
stream.close(http2.constants.NGHTTP2_REFUSED_STREAM);
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
open++;
|
|
1213
|
+
stream.once("close", () => {
|
|
1214
|
+
open--;
|
|
1215
|
+
});
|
|
1216
|
+
if (!services.accept(target, stream)) {
|
|
1217
|
+
stream.close(http2.constants.NGHTTP2_REFUSED_STREAM);
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
served++;
|
|
1221
|
+
if (!stream.headersSent && !stream.destroyed) stream.respond({ ":status": 200 });
|
|
1222
|
+
});
|
|
1223
|
+
return {
|
|
1224
|
+
ended,
|
|
1225
|
+
// A session the peer is closing gracefully waits for its open streams, which a stopped container never ends: destroy it whatever its state.
|
|
1226
|
+
close: () => {
|
|
1227
|
+
if (!session.destroyed) session.destroy();
|
|
1228
|
+
}
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
var FIRST_BACKOFF_MS = 200;
|
|
1232
|
+
var LONGEST_BACKOFF_MS = 5e3;
|
|
1233
|
+
var JOINED_AFTER_MS = 2e3;
|
|
1234
|
+
var EXIT_WAIT_MS = 5e3;
|
|
1235
|
+
function exitedWithin(stream, ms) {
|
|
1236
|
+
let timer;
|
|
1237
|
+
const late = new Promise((resolve) => {
|
|
1238
|
+
timer = setTimeout(() => resolve(void 0), ms);
|
|
1239
|
+
timer.unref();
|
|
1240
|
+
});
|
|
1241
|
+
return Promise.race([stream.exited, late]).finally(() => clearTimeout(timer));
|
|
1242
|
+
}
|
|
1243
|
+
function superviseRelay(input) {
|
|
1244
|
+
const sleep = input.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
1245
|
+
const exitWaitMs = input.exitWaitMs ?? EXIT_WAIT_MS;
|
|
1246
|
+
let closed = false;
|
|
1247
|
+
let fatal;
|
|
1248
|
+
let running;
|
|
1249
|
+
let wake;
|
|
1250
|
+
const loop = (async () => {
|
|
1251
|
+
let malformedInARow = 0;
|
|
1252
|
+
let backoff = FIRST_BACKOFF_MS;
|
|
1253
|
+
let failing = false;
|
|
1254
|
+
while (!closed && fatal === void 0) {
|
|
1255
|
+
const startedMs = Date.now();
|
|
1256
|
+
const stream = input.execStream({
|
|
1257
|
+
container: input.container,
|
|
1258
|
+
user: RELAY_USER,
|
|
1259
|
+
env: { literals: {}, passed: {} },
|
|
1260
|
+
argv: ["outerlayer", "broker-relay", "join"],
|
|
1261
|
+
interactive: true
|
|
1262
|
+
});
|
|
1263
|
+
stream.stdin.on("error", noop);
|
|
1264
|
+
stream.stdout.on("error", noop);
|
|
1265
|
+
const session = serveSession(Duplex.from({ readable: stream.stdout, writable: stream.stdin }), input.services);
|
|
1266
|
+
running = { stream, session };
|
|
1267
|
+
void stream.exited.then(() => session.close());
|
|
1268
|
+
const end = await session.ended;
|
|
1269
|
+
stream.kill();
|
|
1270
|
+
const status = await exitedWithin(stream, exitWaitMs);
|
|
1271
|
+
running = void 0;
|
|
1272
|
+
if (status === void 0) input.log?.(`the relay's exec into ${input.container} did not end when killed \xB7 going on without it`);
|
|
1273
|
+
const joined = end.served > 0 || Date.now() - startedMs >= JOINED_AFTER_MS;
|
|
1274
|
+
if (joined) {
|
|
1275
|
+
backoff = FIRST_BACKOFF_MS;
|
|
1276
|
+
failing = false;
|
|
1277
|
+
if (!closed) input.log?.(`the session with ${input.container} ended \xB7 joining again`);
|
|
1278
|
+
}
|
|
1279
|
+
if (!end.malformed && end.served > 0) malformedInARow = 0;
|
|
1280
|
+
if (end.malformed) {
|
|
1281
|
+
malformedInARow++;
|
|
1282
|
+
if (malformedInARow >= MAX_MALFORMED_SESSIONS) {
|
|
1283
|
+
fatal = MALFORMED_REASON;
|
|
1284
|
+
input.log?.(`the session with ${input.container} broke HTTP/2 framing ${malformedInARow} times in a row \xB7 ending the build`);
|
|
1285
|
+
input.onFatal?.(fatal);
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
} else if (!joined && !failing) {
|
|
1289
|
+
failing = true;
|
|
1290
|
+
const reason = stream.stderrTail?.().trim() ?? "";
|
|
1291
|
+
input.log?.(`could not join the relay in ${input.container}${status === 0 || status === void 0 ? "" : ` (exec exited ${status})`}${reason === "" ? "" : `: ${reason}`} \xB7 trying again`);
|
|
1292
|
+
}
|
|
1293
|
+
if (closed) return;
|
|
1294
|
+
await Promise.race([sleep(backoff), new Promise((resolve) => wake = resolve)]);
|
|
1295
|
+
backoff = Math.min(backoff * 2, LONGEST_BACKOFF_MS);
|
|
1296
|
+
}
|
|
1297
|
+
})();
|
|
1298
|
+
return {
|
|
1299
|
+
fatal: () => fatal,
|
|
1300
|
+
close: async () => {
|
|
1301
|
+
closed = true;
|
|
1302
|
+
wake?.();
|
|
1303
|
+
running?.session.close();
|
|
1304
|
+
running?.stream.kill();
|
|
1305
|
+
await loop;
|
|
1306
|
+
}
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1062
1309
|
var FILE_MODE = 420;
|
|
1063
1310
|
var DIR_MODE = 493;
|
|
1064
1311
|
function replaceFile(path, text) {
|
|
@@ -1086,6 +1333,140 @@ function writeGhConfig(dir, token) {
|
|
|
1086
1333
|
replaceFile(join(dir, "hosts.yml"), ghHostsYml(token));
|
|
1087
1334
|
replaceFile(join(dir, "config.yml"), GH_CONFIG_YML);
|
|
1088
1335
|
}
|
|
1336
|
+
var DeliveryFailure = class extends Error {
|
|
1337
|
+
};
|
|
1338
|
+
var BLOCK = 512;
|
|
1339
|
+
var DELIVERY_TIMEOUT_MS = 5 * 6e4;
|
|
1340
|
+
var RECEIVE_ARGV = ["/opt/outerlayer/bin/node", "/opt/outerlayer/cli/dist/index.js", "receive-files"];
|
|
1341
|
+
var REASON_LINE_MAX = 240;
|
|
1342
|
+
var STEPS = {
|
|
1343
|
+
gh: "the build's gh configuration",
|
|
1344
|
+
"git-hooks": "the commit hooks",
|
|
1345
|
+
context: "the staged context",
|
|
1346
|
+
"eval-input": "the checks' input"
|
|
1347
|
+
};
|
|
1348
|
+
function field(header, at, length, text) {
|
|
1349
|
+
header.write(text, at, length, "latin1");
|
|
1350
|
+
}
|
|
1351
|
+
function octalField(header, at, length, value) {
|
|
1352
|
+
field(header, at, length, `${value.toString(8).padStart(length - 1, "0")}\0`);
|
|
1353
|
+
}
|
|
1354
|
+
function tarHeader(name, type, mode, size) {
|
|
1355
|
+
const header = Buffer.alloc(BLOCK);
|
|
1356
|
+
header.write(name, 0, 100, "utf8");
|
|
1357
|
+
octalField(header, 100, 8, mode);
|
|
1358
|
+
octalField(header, 108, 8, 0);
|
|
1359
|
+
octalField(header, 116, 8, 0);
|
|
1360
|
+
octalField(header, 124, 12, size);
|
|
1361
|
+
octalField(header, 136, 12, 0);
|
|
1362
|
+
field(header, 148, 8, " ");
|
|
1363
|
+
header.write(type, 156, 1, "latin1");
|
|
1364
|
+
field(header, 257, 6, "ustar\0");
|
|
1365
|
+
field(header, 263, 2, "00");
|
|
1366
|
+
let sum = 0;
|
|
1367
|
+
for (const byte of header) sum += byte;
|
|
1368
|
+
field(header, 148, 8, `${sum.toString(8).padStart(6, "0")}\0 `);
|
|
1369
|
+
return header;
|
|
1370
|
+
}
|
|
1371
|
+
function padded(data) {
|
|
1372
|
+
const rest = data.length % BLOCK;
|
|
1373
|
+
return rest === 0 ? data : Buffer.concat([data, Buffer.alloc(BLOCK - rest)]);
|
|
1374
|
+
}
|
|
1375
|
+
function paxRecord(key, value) {
|
|
1376
|
+
const body = Buffer.from(` ${key}=${value}
|
|
1377
|
+
`, "utf8");
|
|
1378
|
+
let length = body.length + 1;
|
|
1379
|
+
while (String(length).length + body.length !== length) length = String(length).length + body.length;
|
|
1380
|
+
return Buffer.concat([Buffer.from(String(length), "latin1"), body]);
|
|
1381
|
+
}
|
|
1382
|
+
function packTar(entries) {
|
|
1383
|
+
const blocks = [];
|
|
1384
|
+
for (const entry of entries) {
|
|
1385
|
+
const data = entry.type === "file" ? entry.data ?? Buffer.alloc(0) : Buffer.alloc(0);
|
|
1386
|
+
const name = entry.type === "dir" && !entry.path.endsWith("/") ? `${entry.path}/` : entry.path;
|
|
1387
|
+
if (Buffer.byteLength(name, "utf8") > 100) {
|
|
1388
|
+
const record = paxRecord("path", name);
|
|
1389
|
+
blocks.push(tarHeader("PaxHeader", "x", 420, record.length), padded(record));
|
|
1390
|
+
}
|
|
1391
|
+
blocks.push(tarHeader(name, entry.type === "dir" ? "5" : "0", entry.mode, data.length), padded(data));
|
|
1392
|
+
}
|
|
1393
|
+
blocks.push(Buffer.alloc(BLOCK * 2));
|
|
1394
|
+
return Buffer.concat(blocks);
|
|
1395
|
+
}
|
|
1396
|
+
function fileMode(hostMode) {
|
|
1397
|
+
return 292 | hostMode & 128 | ((hostMode & 73) === 0 ? 0 : 73);
|
|
1398
|
+
}
|
|
1399
|
+
function packFolder(dir, options = {}) {
|
|
1400
|
+
const entries = options.rootMode === void 0 ? [] : [{ path: "./", type: "dir", mode: options.rootMode }];
|
|
1401
|
+
const walk = (current, prefix) => {
|
|
1402
|
+
for (const name of readdirSync(current).sort()) {
|
|
1403
|
+
const path = join(current, name);
|
|
1404
|
+
const stat = lstatSync(path);
|
|
1405
|
+
const rel = prefix === "" ? name : `${prefix}/${name}`;
|
|
1406
|
+
if (stat.isDirectory()) {
|
|
1407
|
+
entries.push({ path: rel, type: "dir", mode: options.dirModes?.[rel] ?? 493 });
|
|
1408
|
+
walk(path, rel);
|
|
1409
|
+
} else if (stat.isFile()) {
|
|
1410
|
+
entries.push({ path: rel, type: "file", mode: fileMode(stat.mode), data: readFileSync(path) });
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
};
|
|
1414
|
+
walk(dir, "");
|
|
1415
|
+
return packTar(entries);
|
|
1416
|
+
}
|
|
1417
|
+
function scrubDelivery(text, secrets) {
|
|
1418
|
+
let out = text.replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, "").replace(/[\u0000-\u001f\u007f-\u009f]/g, " ");
|
|
1419
|
+
for (const secret of secrets()) if (secret !== "") out = out.split(secret).join("[redacted]");
|
|
1420
|
+
out = out.replace(/\b(?:gh[pousr]_|github_pat_)[A-Za-z0-9_]+/g, "[redacted]");
|
|
1421
|
+
out = out.trim();
|
|
1422
|
+
return out.length > REASON_LINE_MAX ? `${out.slice(0, REASON_LINE_MAX)}...` : out;
|
|
1423
|
+
}
|
|
1424
|
+
async function deliverTar(runtime, state, input) {
|
|
1425
|
+
try {
|
|
1426
|
+
const result = await runtime.host.exec(
|
|
1427
|
+
{
|
|
1428
|
+
container: state.container,
|
|
1429
|
+
user: RELAY_USER,
|
|
1430
|
+
env: { literals: {}, passed: {} },
|
|
1431
|
+
argv: [...RECEIVE_ARGV, "--into", input.into, ...input.replace === true ? ["--replace"] : []],
|
|
1432
|
+
stdin: input.tar
|
|
1433
|
+
},
|
|
1434
|
+
{ timeoutMs: DELIVERY_TIMEOUT_MS }
|
|
1435
|
+
);
|
|
1436
|
+
if (result.status === 0) return { ok: true };
|
|
1437
|
+
const line = scrubDelivery(result.stderr.trim().split("\n")[0] ?? "", input.secrets);
|
|
1438
|
+
return { ok: false, reason: `${input.step} could not be delivered (exit ${result.status})${line === "" ? "" : `: ${line}`}` };
|
|
1439
|
+
} catch (err) {
|
|
1440
|
+
return { ok: false, reason: `${input.step} could not be delivered: ${scrubDelivery(describeCallFailure(err), input.secrets)}` };
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
async function deliverJobFiles(runtime, state, parts, secrets) {
|
|
1444
|
+
for (const part of parts) {
|
|
1445
|
+
const dir = join(state.buildDir, part === "eval-input" ? EVAL_INPUT_NAME : part);
|
|
1446
|
+
if (!existsSync(dir)) continue;
|
|
1447
|
+
let tar;
|
|
1448
|
+
try {
|
|
1449
|
+
tar = packFolder(dir);
|
|
1450
|
+
} catch (err) {
|
|
1451
|
+
return { ok: false, reason: `${STEPS[part]} could not be read on the host: ${scrubDelivery(describeCallFailure(err), secrets)}` };
|
|
1452
|
+
}
|
|
1453
|
+
const delivered = await deliverTar(runtime, state, { step: STEPS[part], tar, into: `${JOB_MOUNT}/${part}`, replace: part === "eval-input", secrets });
|
|
1454
|
+
if (!delivered.ok) return delivered;
|
|
1455
|
+
}
|
|
1456
|
+
return { ok: true };
|
|
1457
|
+
}
|
|
1458
|
+
function deliverGhConfig(runtime, state, secrets) {
|
|
1459
|
+
return deliverJobFiles(runtime, state, ["gh"], secrets);
|
|
1460
|
+
}
|
|
1461
|
+
function deliverEvalInput(runtime, state, secrets) {
|
|
1462
|
+
return deliverJobFiles(runtime, state, ["eval-input"], secrets);
|
|
1463
|
+
}
|
|
1464
|
+
async function deliverJudgeInputs(runtime, state, dir, secrets) {
|
|
1465
|
+
return deliverTar(runtime, state, { step: "the judge's inputs", tar: packFolder(dir, { rootMode: 1023, dirModes: { home: 511, "home/tmp": 511 } }), into: JUDGE_DIR, replace: true, secrets });
|
|
1466
|
+
}
|
|
1467
|
+
function clearJudgeFolder(runtime, state, secrets) {
|
|
1468
|
+
return deliverTar(runtime, state, { step: "clearing the judge's folder", tar: packTar([]), into: JUDGE_DIR, replace: true, secrets });
|
|
1469
|
+
}
|
|
1089
1470
|
function gitEnv(extra) {
|
|
1090
1471
|
const env = {};
|
|
1091
1472
|
for (const [name, value] of Object.entries(process.env)) if (!name.startsWith("GIT_")) env[name] = value;
|
|
@@ -1898,17 +2279,20 @@ ${body}`);
|
|
|
1898
2279
|
});
|
|
1899
2280
|
req.pipe(upstream);
|
|
1900
2281
|
}
|
|
1901
|
-
if (
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
2282
|
+
if (opts.socket !== false) {
|
|
2283
|
+
if (existsSync(socketPath)) unlinkSync(socketPath);
|
|
2284
|
+
await new Promise((resolveListen, reject) => {
|
|
2285
|
+
server.once("error", reject);
|
|
2286
|
+
server.listen(socketPath, () => resolveListen());
|
|
2287
|
+
});
|
|
2288
|
+
chmodSync(socketPath, 438);
|
|
2289
|
+
}
|
|
1907
2290
|
const tcpServers = [];
|
|
1908
2291
|
return {
|
|
1909
2292
|
socketPath,
|
|
1910
2293
|
record,
|
|
1911
2294
|
destinationUsed,
|
|
2295
|
+
acceptStream: (stream) => void server.emit("connection", stream),
|
|
1912
2296
|
async listenTcp(address) {
|
|
1913
2297
|
const tcp = createServer$1((socket) => server.emit("connection", socket));
|
|
1914
2298
|
await new Promise((resolveListen, reject) => {
|
|
@@ -1939,6 +2323,9 @@ function createContainerRuntime(opts = {}) {
|
|
|
1939
2323
|
host: opts.host ?? createContainerHost(),
|
|
1940
2324
|
cpus: opts.cpus ?? createCpuAllocator(usableCpus()),
|
|
1941
2325
|
tunnels: opts.tunnels ?? /* @__PURE__ */ new Map(),
|
|
2326
|
+
relays: opts.relays ?? /* @__PURE__ */ new Map(),
|
|
2327
|
+
relayFatal: opts.relayFatal ?? /* @__PURE__ */ new Map(),
|
|
2328
|
+
leases: opts.leases ?? /* @__PURE__ */ new Map(),
|
|
1942
2329
|
buildImage: opts.buildImage ?? ((build) => buildRepositoryImage(
|
|
1943
2330
|
{
|
|
1944
2331
|
repository: build.repository,
|
|
@@ -1967,13 +2354,47 @@ function createContainerRuntime(opts = {}) {
|
|
|
1967
2354
|
cliBuild: () => cli ?? currentCliBuild()
|
|
1968
2355
|
};
|
|
1969
2356
|
}
|
|
1970
|
-
|
|
2357
|
+
var REPORT_READ_TIMEOUT_MS = 6e4;
|
|
2358
|
+
var GH_CONFIG_RETRY_MS = 3e4;
|
|
2359
|
+
var GH_CONFIG_RETRIES = 5;
|
|
2360
|
+
function secretsOf(runtime, jobDir) {
|
|
2361
|
+
return () => jobDir === void 0 ? [] : runtime.credentials.get(jobDir)?.issuedTokens() ?? [];
|
|
2362
|
+
}
|
|
2363
|
+
function filesAreCopied(state) {
|
|
2364
|
+
return state.jobFiles === "copied";
|
|
2365
|
+
}
|
|
2366
|
+
function keepGhConfigCurrent(tokens, buildDir2, onError, copy) {
|
|
2367
|
+
let last = copy?.delivered;
|
|
2368
|
+
let queued;
|
|
2369
|
+
let queue = Promise.resolve();
|
|
2370
|
+
const deliver2 = (token, attemptsLeft) => {
|
|
2371
|
+
queued = token;
|
|
2372
|
+
queue = queue.then(async () => {
|
|
2373
|
+
if (copy === void 0 || queued !== token) return;
|
|
2374
|
+
const delivered = await deliverGhConfig(copy.runtime, copy.state, copy.secrets);
|
|
2375
|
+
if (delivered.ok) {
|
|
2376
|
+
last = token;
|
|
2377
|
+
queued = void 0;
|
|
2378
|
+
return;
|
|
2379
|
+
}
|
|
2380
|
+
copy.onFailure(delivered.reason);
|
|
2381
|
+
queued = void 0;
|
|
2382
|
+
if (attemptsLeft <= 0) return;
|
|
2383
|
+
const timer = setTimeout(() => {
|
|
2384
|
+
if (queued === void 0 && last !== token) deliver2(token, attemptsLeft - 1);
|
|
2385
|
+
}, GH_CONFIG_RETRY_MS);
|
|
2386
|
+
timer.unref();
|
|
2387
|
+
});
|
|
2388
|
+
};
|
|
1971
2389
|
tokens.onRead((token) => {
|
|
1972
2390
|
try {
|
|
1973
2391
|
writeGhConfig(ghConfigDir(buildDir2), token);
|
|
1974
2392
|
} catch (err) {
|
|
1975
2393
|
onError(err);
|
|
2394
|
+
return;
|
|
1976
2395
|
}
|
|
2396
|
+
if (copy === void 0 || token === last || token === queued) return;
|
|
2397
|
+
deliver2(token, GH_CONFIG_RETRIES);
|
|
1977
2398
|
});
|
|
1978
2399
|
}
|
|
1979
2400
|
function gitRemoteFor(runtime, input) {
|
|
@@ -1997,12 +2418,43 @@ function firstLine5(text) {
|
|
|
1997
2418
|
return text.trim().split("\n")[0] ?? "";
|
|
1998
2419
|
}
|
|
1999
2420
|
var NO_ENV = { literals: {}, passed: {} };
|
|
2421
|
+
function transportOf(state) {
|
|
2422
|
+
const recorded = state.transport;
|
|
2423
|
+
if (recorded === void 0 || recorded === "sockets") return "sockets";
|
|
2424
|
+
return recorded === "exec" ? "exec" : "unknown";
|
|
2425
|
+
}
|
|
2426
|
+
function relayUserOf(state) {
|
|
2427
|
+
return transportOf(state) === "exec" ? RELAY_USER : ADDED_USER;
|
|
2428
|
+
}
|
|
2429
|
+
function joinBuild(runtime, state, services, log) {
|
|
2430
|
+
runtime.relays.set(
|
|
2431
|
+
state.buildId,
|
|
2432
|
+
superviseRelay({
|
|
2433
|
+
container: state.container,
|
|
2434
|
+
services: services.streams,
|
|
2435
|
+
execStream: (input) => runtime.host.execStream(input),
|
|
2436
|
+
log,
|
|
2437
|
+
onFatal: (reason) => runtime.relayFatal.set(state.buildId, reason)
|
|
2438
|
+
})
|
|
2439
|
+
);
|
|
2440
|
+
}
|
|
2441
|
+
async function leaveBuild(runtime, state) {
|
|
2442
|
+
const relay = runtime.relays.get(state.buildId);
|
|
2443
|
+
if (relay === void 0) return;
|
|
2444
|
+
runtime.relays.delete(state.buildId);
|
|
2445
|
+
await relay.close();
|
|
2446
|
+
}
|
|
2447
|
+
function rejoinBuild(runtime, state, log) {
|
|
2448
|
+
const services = runtime.tunnels.get(state.buildId);
|
|
2449
|
+
if (services?.streams === void 0 || runtime.relays.has(state.buildId) || transportOf(state) !== "exec") return;
|
|
2450
|
+
joinBuild(runtime, state, { streams: services.streams }, log);
|
|
2451
|
+
}
|
|
2000
2452
|
var RELAY_PROBE = `const s=require("net").connect(${RELAY_PORT},"127.0.0.1");s.on("connect",()=>process.exit(0));s.on("error",()=>process.exit(1));setTimeout(()=>process.exit(1),2000)`;
|
|
2001
2453
|
async function waitForRelay(runtime, state) {
|
|
2002
2454
|
for (let attempt = 0; attempt < RELAY_ATTEMPTS; attempt++) {
|
|
2003
2455
|
const probe = await runtime.host.exec({
|
|
2004
2456
|
container: state.container,
|
|
2005
|
-
user:
|
|
2457
|
+
user: relayUserOf(state),
|
|
2006
2458
|
env: NO_ENV,
|
|
2007
2459
|
argv: ["/opt/outerlayer/bin/node", "-e", RELAY_PROBE]
|
|
2008
2460
|
});
|
|
@@ -2116,8 +2568,11 @@ async function provisionContainer(input) {
|
|
|
2116
2568
|
builtIns: true,
|
|
2117
2569
|
lifecycle: report.apply.lifecycle,
|
|
2118
2570
|
...governingSource.kind === "governed" ? { governing: { repository: governingSource.repository, ref: governingSource.ref, commit: governingSource.sha } } : {},
|
|
2119
|
-
cpuset: formatCpuList(runtime.cpus.acquire(buildId, config.build?.cpus ?? 4))
|
|
2571
|
+
cpuset: formatCpuList(runtime.cpus.acquire(buildId, config.build?.cpus ?? 4)),
|
|
2572
|
+
transport: "exec",
|
|
2573
|
+
jobFiles: "copied"
|
|
2120
2574
|
};
|
|
2575
|
+
const secrets = secretsOf(runtime, jobDir);
|
|
2121
2576
|
const envInput = {
|
|
2122
2577
|
config,
|
|
2123
2578
|
item: item.number,
|
|
@@ -2152,10 +2607,11 @@ async function provisionContainer(input) {
|
|
|
2152
2607
|
const dockerFailed = (what, result) => bail(`${what} failed (exit ${result.status})${firstLine5(result.stderr) === "" ? "" : `: ${firstLine5(result.stderr)}`}`);
|
|
2153
2608
|
if (input.aborted()) return bail("", true);
|
|
2154
2609
|
mkdirSync(buildDir2, { recursive: true });
|
|
2155
|
-
chmodSync(buildDir2, 493);
|
|
2156
2610
|
const ghDir = ghConfigDir(buildDir2);
|
|
2611
|
+
let readToken;
|
|
2157
2612
|
try {
|
|
2158
|
-
|
|
2613
|
+
readToken = await tokens.token("read");
|
|
2614
|
+
writeGhConfig(ghDir, readToken);
|
|
2159
2615
|
} catch (err) {
|
|
2160
2616
|
return bail(
|
|
2161
2617
|
err instanceof WorkCommandError ? `the build's read token could not be had: ${refusalCode(err)}` : `the build's gh configuration could not be written: ${describeCallFailure(err)}`
|
|
@@ -2172,9 +2628,11 @@ async function provisionContainer(input) {
|
|
|
2172
2628
|
keepGhConfigCurrent(tokens, buildDir2, (err) => log.error(`${tag} the read token could not be written for gh: ${describeCallFailure(err)}`));
|
|
2173
2629
|
tokens.markUsed("read");
|
|
2174
2630
|
environment.credentials = tokens.used();
|
|
2631
|
+
let services;
|
|
2175
2632
|
try {
|
|
2176
2633
|
const gitRemote = gitRemoteFor(runtime, { repositories, tokens, buildDir: buildDir2, log: (line) => log.line(`${tag} ${line}`) });
|
|
2177
|
-
|
|
2634
|
+
services = await startServicesFor(runtime, config, buildDir2, jobDir, itemKey, true, gitRemote, "stream", input.leaseExpiryMs === void 0 ? void 0 : leaseFor(runtime, buildId, input.leaseExpiryMs));
|
|
2635
|
+
runtime.tunnels.set(buildId, services);
|
|
2178
2636
|
} catch (err) {
|
|
2179
2637
|
return bail(`the tunnel could not start: ${describeCallFailure(err)}`);
|
|
2180
2638
|
}
|
|
@@ -2183,28 +2641,35 @@ async function provisionContainer(input) {
|
|
|
2183
2641
|
const volume = await host.createVolume(name, { [BUILD_LABEL]: buildId, [RUNNER_LABEL]: runnerId });
|
|
2184
2642
|
if (volume.status !== 0) return dockerFailed("creating the build's volume", volume);
|
|
2185
2643
|
if (input.aborted()) return bail("", true);
|
|
2186
|
-
const judgeDir = join(jobDir, "judge");
|
|
2187
|
-
mkdirSync(judgeDir, { recursive: true });
|
|
2188
|
-
chmodSync(judgeDir, 493);
|
|
2189
2644
|
const runInput = {
|
|
2190
2645
|
buildId,
|
|
2191
2646
|
runnerId,
|
|
2192
2647
|
container: name,
|
|
2193
2648
|
volume: name,
|
|
2194
|
-
buildDir: buildDir2,
|
|
2195
2649
|
image: report.imageId,
|
|
2196
2650
|
user: ADDED_USER,
|
|
2197
2651
|
limits: limits2,
|
|
2198
2652
|
destinations: config.build?.destinations?.length ?? 0,
|
|
2199
2653
|
builtIns: true,
|
|
2200
|
-
|
|
2654
|
+
transport: "exec",
|
|
2655
|
+
jobFiles: "copied"
|
|
2201
2656
|
};
|
|
2202
2657
|
const prepared = await host.run(prepareRunArgs(runInput), { logPath });
|
|
2203
2658
|
if (prepared.status !== 0) return dockerFailed("preparing the build's volume", prepared);
|
|
2204
2659
|
const started = await host.run(containerRunArgs(runInput), { logPath });
|
|
2205
2660
|
if (started.status !== 0) return dockerFailed("starting the build's container", started);
|
|
2661
|
+
joinBuild(runtime, state, services, (line) => log.line(`${tag} ${line}`));
|
|
2206
2662
|
if (input.aborted()) return bail("", true);
|
|
2207
2663
|
if (!await waitForRelay(runtime, state)) return bail("the relay in the build's container did not start");
|
|
2664
|
+
const ghDelivered = await deliverGhConfig(runtime, state, secrets);
|
|
2665
|
+
if (!ghDelivered.ok) return bail(ghDelivered.reason);
|
|
2666
|
+
keepGhConfigCurrent(tokens, buildDir2, (err) => log.error(`${tag} the read token could not be written for gh: ${describeCallFailure(err)}`), {
|
|
2667
|
+
runtime,
|
|
2668
|
+
state,
|
|
2669
|
+
secrets,
|
|
2670
|
+
delivered: readToken,
|
|
2671
|
+
onFailure: (reason) => log.error(`${tag} ${reason}`)
|
|
2672
|
+
});
|
|
2208
2673
|
const imageConfig = await host.imageConfig(report.imageId);
|
|
2209
2674
|
state.imageEnv = imageConfig.env;
|
|
2210
2675
|
let agentUser;
|
|
@@ -2243,7 +2708,7 @@ async function provisionContainer(input) {
|
|
|
2243
2708
|
log,
|
|
2244
2709
|
tag,
|
|
2245
2710
|
aborted: input.aborted,
|
|
2246
|
-
// The control plane is read on the host, with a token the gateway issues (or the host's git login when it has no GitHub App). The
|
|
2711
|
+
// The control plane is read on the host, with a token the gateway issues (or the host's git login when it has no GitHub App). The staged files are copied into the container.
|
|
2247
2712
|
stageContext: () => stageGovernedContext({
|
|
2248
2713
|
config,
|
|
2249
2714
|
repository,
|
|
@@ -2256,6 +2721,10 @@ async function provisionContainer(input) {
|
|
|
2256
2721
|
log,
|
|
2257
2722
|
tag
|
|
2258
2723
|
}),
|
|
2724
|
+
deliverContext: async () => {
|
|
2725
|
+
const delivered = await deliverJobFiles(runtime, state, ["context"], secrets);
|
|
2726
|
+
return delivered.ok ? delivered : { ok: false, reason: `context_unavailable: ${delivered.reason}` };
|
|
2727
|
+
},
|
|
2259
2728
|
removeStagedContext: () => removeStagedContext(buildDir2)
|
|
2260
2729
|
});
|
|
2261
2730
|
if (!steps.ok) return bail(steps.reason, steps.aborted === true);
|
|
@@ -2274,7 +2743,7 @@ function agentVersionOf(toolsJson) {
|
|
|
2274
2743
|
function shellQuote(word) {
|
|
2275
2744
|
return /^[A-Za-z0-9_@%+=:,./-]+$/.test(word) ? word : `'${word.replace(/'/g, `'\\''`)}'`;
|
|
2276
2745
|
}
|
|
2277
|
-
function
|
|
2746
|
+
function containerCommandExec(input) {
|
|
2278
2747
|
const state = input.job.container;
|
|
2279
2748
|
const env = buildEnvironment({
|
|
2280
2749
|
config: input.config,
|
|
@@ -2289,16 +2758,13 @@ function containerCommandLine(input) {
|
|
|
2289
2758
|
identity: input.job.commitIdentity,
|
|
2290
2759
|
imageEnv: state.imageEnv
|
|
2291
2760
|
});
|
|
2292
|
-
|
|
2761
|
+
return {
|
|
2293
2762
|
container: state.container,
|
|
2294
2763
|
user: state.agentUser ?? ADDED_USER,
|
|
2295
2764
|
workdir: CHECKOUT_DIR,
|
|
2296
2765
|
env,
|
|
2297
|
-
argv: ["/bin/sh", "-c", commandWithShim(input.job, input.cmdline)]
|
|
2298
|
-
|
|
2299
|
-
return {
|
|
2300
|
-
line: ["docker", "exec", ...args].map(shellQuote).join(" "),
|
|
2301
|
-
env: { ...childEnvironment(input.runtime.hostEnv), ...env.passed }
|
|
2766
|
+
argv: ["outerlayer", "build-command", "--", "/bin/sh", "-c", commandWithShim(input.job, input.cmdline)],
|
|
2767
|
+
detached: true
|
|
2302
2768
|
};
|
|
2303
2769
|
}
|
|
2304
2770
|
function containerSenderCommand(input) {
|
|
@@ -2329,15 +2795,22 @@ function senderSignalScript(pidfile, signal) {
|
|
|
2329
2795
|
const file = shellQuote(pidfile);
|
|
2330
2796
|
return `p="$(cat ${file})" && [ -n "$p" ] && grep -qa session-sender "/proc/$p/cmdline" && kill -${signal} "$p"`;
|
|
2331
2797
|
}
|
|
2798
|
+
function senderAliveScript(pidfile) {
|
|
2799
|
+
const file = shellQuote(pidfile);
|
|
2800
|
+
return `p="$(cat ${file})" && [ -n "$p" ] && grep -qa session-sender "/proc/$p/cmdline" && echo alive`;
|
|
2801
|
+
}
|
|
2332
2802
|
async function signalInContainer(runtime, state, pidfile, signal) {
|
|
2333
|
-
await runtime.host.exec({
|
|
2803
|
+
const { status } = await runtime.host.exec({
|
|
2334
2804
|
container: state.container,
|
|
2335
2805
|
user: state.agentUser ?? ADDED_USER,
|
|
2336
2806
|
env: NO_ENV,
|
|
2337
2807
|
argv: ["/bin/sh", "-c", senderSignalScript(pidfile, signal)]
|
|
2338
2808
|
});
|
|
2809
|
+
return status === 0;
|
|
2339
2810
|
}
|
|
2340
2811
|
async function stopContainer(runtime, state) {
|
|
2812
|
+
runtime.leases.get(state.buildId)?.stop();
|
|
2813
|
+
await leaveBuild(runtime, state);
|
|
2341
2814
|
await runtime.host.stop(state.container);
|
|
2342
2815
|
state.stopped = true;
|
|
2343
2816
|
}
|
|
@@ -2350,7 +2823,7 @@ async function measureDisk(runtime, state) {
|
|
|
2350
2823
|
});
|
|
2351
2824
|
return result.status === 0 ? parseDuKilobytes(result.stdout) : void 0;
|
|
2352
2825
|
}
|
|
2353
|
-
async function startServicesFor(runtime, config, buildDir2, jobDir, itemKey, builtIns, gitRemote) {
|
|
2826
|
+
async function startServicesFor(runtime, config, buildDir2, jobDir, itemKey, builtIns, gitRemote, serve = "socket", lease) {
|
|
2354
2827
|
return startBuildServices({
|
|
2355
2828
|
startTunnel: runtime.startTunnel,
|
|
2356
2829
|
tunnelOptions: { ...runtime.tunnelOptions, ...gitRemote === void 0 ? {} : { gitRemote } },
|
|
@@ -2358,28 +2831,58 @@ async function startServicesFor(runtime, config, buildDir2, jobDir, itemKey, bui
|
|
|
2358
2831
|
jobDir,
|
|
2359
2832
|
destinations: config.build?.destinations ?? [],
|
|
2360
2833
|
allowHosts: config.build?.allowHosts,
|
|
2361
|
-
serve
|
|
2834
|
+
serve,
|
|
2835
|
+
...lease === void 0 ? {} : { lease },
|
|
2362
2836
|
...builtIns ? { builtIns: { gatewayUrl: config.url, itemKey, claudeUpstream: runtime.claudeUpstream } } : {},
|
|
2363
2837
|
hostEnv: runtime.hostEnv,
|
|
2364
2838
|
destinationOptions: runtime.destinationOptions
|
|
2365
2839
|
});
|
|
2366
2840
|
}
|
|
2367
|
-
|
|
2841
|
+
function leaseFor(runtime, buildId, expiryMs) {
|
|
2842
|
+
let channel = runtime.leases.get(buildId);
|
|
2843
|
+
if (channel === void 0) {
|
|
2844
|
+
channel = createLeaseChannel(expiryMs);
|
|
2845
|
+
runtime.leases.set(buildId, channel);
|
|
2846
|
+
}
|
|
2847
|
+
return channel;
|
|
2848
|
+
}
|
|
2849
|
+
async function ensureTunnel(runtime, state, jobDir, config, itemKey, branch, repositories, log, leaseExpiryMs) {
|
|
2368
2850
|
if (runtime.tunnels.has(state.buildId)) return;
|
|
2851
|
+
const transport = transportOf(state);
|
|
2852
|
+
if (transport === "unknown") {
|
|
2853
|
+
runtime.relayFatal.set(state.buildId, UNKNOWN_TRANSPORT_REASON);
|
|
2854
|
+
return;
|
|
2855
|
+
}
|
|
2369
2856
|
const tokens = runtime.credentials.get(jobDir);
|
|
2370
2857
|
const served = repositoriesOf({ repositories, repository: state.repository, branch });
|
|
2371
2858
|
const gitRemote = tokens === void 0 || served.length === 0 ? void 0 : gitRemoteFor(runtime, { repositories: served, tokens, buildDir: state.buildDir });
|
|
2372
|
-
|
|
2859
|
+
const lease = transport === "exec" && leaseExpiryMs !== void 0 ? leaseFor(runtime, state.buildId, leaseExpiryMs) : void 0;
|
|
2860
|
+
if (state.stopped === true) lease?.stop();
|
|
2861
|
+
const services = await startServicesFor(runtime, config, state.buildDir, jobDir, itemKey, state.builtIns === true, gitRemote, transport === "exec" ? "stream" : "socket", lease);
|
|
2862
|
+
runtime.tunnels.set(state.buildId, services);
|
|
2863
|
+
if (transport === "exec") joinBuild(runtime, state, services, log);
|
|
2864
|
+
}
|
|
2865
|
+
async function redeliver(runtime, state, jobDir) {
|
|
2866
|
+
if (!filesAreCopied(state)) return { ok: true };
|
|
2867
|
+
const parts = ["gh", "git-hooks", "context"];
|
|
2868
|
+
return deliverJobFiles(runtime, state, parts, secretsOf(runtime, jobDir));
|
|
2869
|
+
}
|
|
2870
|
+
async function deliverContainerHooks(runtime, state, jobDir) {
|
|
2871
|
+
if (!filesAreCopied(state)) return { ok: true };
|
|
2872
|
+
return deliverJobFiles(runtime, state, ["git-hooks"], secretsOf(runtime, jobDir));
|
|
2373
2873
|
}
|
|
2374
2874
|
async function syncInContainer(input) {
|
|
2375
2875
|
const { runtime, job } = input;
|
|
2376
2876
|
const state = job.container;
|
|
2377
2877
|
try {
|
|
2378
|
-
await ensureTunnel(runtime, state, input.jobDir, input.config, job.itemKey, job.branch, job.repositories);
|
|
2878
|
+
await ensureTunnel(runtime, state, input.jobDir, input.config, job.itemKey, job.branch, job.repositories, input.log, () => leaseExpiresAtMs(job));
|
|
2379
2879
|
if (state.stopped) {
|
|
2380
2880
|
await runtime.host.start(state.container);
|
|
2381
2881
|
state.stopped = false;
|
|
2382
2882
|
if (!await waitForRelay(runtime, state)) return { ok: false, reason: "the relay did not start again" };
|
|
2883
|
+
rejoinBuild(runtime, state, input.log);
|
|
2884
|
+
const delivered = await redeliver(runtime, state, input.jobDir);
|
|
2885
|
+
if (!delivered.ok) return { ok: false, reason: delivered.reason };
|
|
2383
2886
|
}
|
|
2384
2887
|
const env = buildEnvironment({
|
|
2385
2888
|
config: input.config,
|
|
@@ -2430,6 +2933,10 @@ async function copyHookErrors(runtime, state, jobDir) {
|
|
|
2430
2933
|
async function cleanupContainer(runtime, state, jobDir) {
|
|
2431
2934
|
const failures = [];
|
|
2432
2935
|
if (jobDir !== void 0) await copyHookErrors(runtime, state, jobDir);
|
|
2936
|
+
await leaveBuild(runtime, state);
|
|
2937
|
+
runtime.relayFatal.delete(state.buildId);
|
|
2938
|
+
runtime.leases.get(state.buildId)?.close();
|
|
2939
|
+
runtime.leases.delete(state.buildId);
|
|
2433
2940
|
const removedContainer = await runtime.host.remove(state.container);
|
|
2434
2941
|
if (removedContainer.status !== 0 && !/no such container/i.test(removedContainer.stderr)) {
|
|
2435
2942
|
failures.push(`container ${state.container} was not removed: ${removedContainer.stderr.trim()}`);
|
|
@@ -2457,12 +2964,15 @@ function containerJudgeEnvironment(input) {
|
|
|
2457
2964
|
const folder = join(input.jobDir, "judge");
|
|
2458
2965
|
return {
|
|
2459
2966
|
stop: async () => {
|
|
2460
|
-
if (input.config !== void 0) await ensureTunnel(runtime, state, input.jobDir, input.config, job.itemKey, job.branch, job.repositories);
|
|
2967
|
+
if (input.config !== void 0) await ensureTunnel(runtime, state, input.jobDir, input.config, job.itemKey, job.branch, job.repositories, input.log);
|
|
2461
2968
|
await stopContainer(runtime, state);
|
|
2462
2969
|
const started = await runtime.host.start(state.container);
|
|
2463
2970
|
if (started.status !== 0) throw new Error(`the build's container could not be started again (exit ${started.status})`);
|
|
2464
2971
|
state.stopped = false;
|
|
2465
2972
|
if (!await waitForRelay(runtime, state)) throw new Error("the relay in the build's container did not start again");
|
|
2973
|
+
rejoinBuild(runtime, state, input.log);
|
|
2974
|
+
const delivered = await redeliver(runtime, state, input.jobDir);
|
|
2975
|
+
if (!delivered.ok) throw new Error(delivered.reason);
|
|
2466
2976
|
},
|
|
2467
2977
|
hashProgram: async () => {
|
|
2468
2978
|
const ran = await runtime.host.exec({ container: state.container, user: ADDED_USER, env: NO_ENV, argv: [...HASH_ARGV] });
|
|
@@ -2470,6 +2980,20 @@ function containerJudgeEnvironment(input) {
|
|
|
2470
2980
|
return sha256 === void 0 ? { ok: false, reason: firstLine5(ran.stderr) || `the hash could not be taken (exit ${ran.status})` } : { ok: true, sha256 };
|
|
2471
2981
|
},
|
|
2472
2982
|
prepare: async (files, tree) => {
|
|
2983
|
+
if (filesAreCopied(state)) {
|
|
2984
|
+
const staging = join(input.jobDir, "judge-input");
|
|
2985
|
+
rmSync(staging, { recursive: true, force: true });
|
|
2986
|
+
mkdirSync(join(staging, "home", "tmp"), { recursive: true });
|
|
2987
|
+
try {
|
|
2988
|
+
for (const [fileName, text] of Object.entries(files)) writeFileSync(join(staging, fileName), text, { mode: 420 });
|
|
2989
|
+
if (tree !== void 0) copyJudgeTree(tree, join(staging, "tree"));
|
|
2990
|
+
const delivered = await deliverJudgeInputs(runtime, state, staging, secretsOf(runtime, input.jobDir));
|
|
2991
|
+
if (!delivered.ok) throw new Error(delivered.reason);
|
|
2992
|
+
} finally {
|
|
2993
|
+
rmSync(staging, { recursive: true, force: true });
|
|
2994
|
+
}
|
|
2995
|
+
return;
|
|
2996
|
+
}
|
|
2473
2997
|
mkdirSync(folder, { recursive: true });
|
|
2474
2998
|
for (const entry of readdirSync(folder)) rmSync(join(folder, entry), { recursive: true, force: true });
|
|
2475
2999
|
mkdirSync(join(folder, "home", "tmp"), { recursive: true });
|
|
@@ -2480,12 +3004,26 @@ function containerJudgeEnvironment(input) {
|
|
|
2480
3004
|
chmodSync(folder, 1023);
|
|
2481
3005
|
},
|
|
2482
3006
|
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 () =>
|
|
3007
|
+
readReport: async () => {
|
|
3008
|
+
if (!filesAreCopied(state)) return readRegularFile(join(folder, "report.html"), MAX_REPORT_BYTES);
|
|
3009
|
+
const file = JUDGE_REPORT;
|
|
3010
|
+
const read = await runtime.host.exec({
|
|
3011
|
+
container: state.container,
|
|
3012
|
+
user,
|
|
3013
|
+
env: NO_ENV,
|
|
3014
|
+
argv: ["/bin/sh", "-c", `[ -f ${file} ] && [ ! -L ${file} ] && head -c ${MAX_REPORT_BYTES + 1} ${file}`]
|
|
3015
|
+
}, { timeoutMs: REPORT_READ_TIMEOUT_MS });
|
|
3016
|
+
if (read.status > 1) throw new Error(`reading the judge report exited ${read.status}: ${firstLine5(read.stderr)}`);
|
|
3017
|
+
return read.status === 0 && Buffer.byteLength(read.stdout) <= MAX_REPORT_BYTES ? read.stdout : void 0;
|
|
3018
|
+
},
|
|
2484
3019
|
// 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
3020
|
// 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
3021
|
clear: async () => {
|
|
2487
3022
|
const removed = await runtime.host.exec({ container: state.container, user, env: NO_ENV, argv: ["find", JUDGE_DIR, "-mindepth", "1", "-user", user, "-delete"] });
|
|
2488
3023
|
if (removed.status !== 0) throw new Error(`find exited ${removed.status}: ${firstLine5(removed.stderr)}`);
|
|
3024
|
+
if (!filesAreCopied(state)) return;
|
|
3025
|
+
const emptied = await clearJudgeFolder(runtime, state, secretsOf(runtime, input.jobDir));
|
|
3026
|
+
if (!emptied.ok) throw new Error(emptied.reason);
|
|
2489
3027
|
}
|
|
2490
3028
|
};
|
|
2491
3029
|
}
|
|
@@ -2511,7 +3049,21 @@ async function startCheckContainer(input) {
|
|
|
2511
3049
|
const buildDir2 = buildDir(config.configPath, buildId);
|
|
2512
3050
|
const agentUser = built.agentUser ?? ADDED_USER;
|
|
2513
3051
|
const cpuset = built.cpuset ?? formatCpuList(runtime.cpus.acquire(buildId, config.build?.cpus ?? 4));
|
|
2514
|
-
const
|
|
3052
|
+
const transport = transportOf(built);
|
|
3053
|
+
if (transport === "unknown") return { ok: false, reason: UNKNOWN_TRANSPORT_REASON };
|
|
3054
|
+
const state = {
|
|
3055
|
+
buildId,
|
|
3056
|
+
container: name,
|
|
3057
|
+
volume: name,
|
|
3058
|
+
buildDir: buildDir2,
|
|
3059
|
+
repository,
|
|
3060
|
+
imageId: built.imageId,
|
|
3061
|
+
imageEnv: built.imageEnv,
|
|
3062
|
+
agentUser,
|
|
3063
|
+
cpuset,
|
|
3064
|
+
transport,
|
|
3065
|
+
...transport === "exec" ? { jobFiles: "copied" } : {}
|
|
3066
|
+
};
|
|
2515
3067
|
const checksDir = join(jobDir, "checks");
|
|
2516
3068
|
mkdirSync(checksDir, { recursive: true });
|
|
2517
3069
|
const logPath = join(checksDir, "environment.log");
|
|
@@ -2526,7 +3078,8 @@ async function startCheckContainer(input) {
|
|
|
2526
3078
|
};
|
|
2527
3079
|
const dockerFailed = (what, result) => fail(`${what} failed (exit ${result.status})${firstLine5(result.stderr) === "" ? "" : `: ${firstLine5(result.stderr)}`}`);
|
|
2528
3080
|
mkdirSync(buildDir2, { recursive: true });
|
|
2529
|
-
chmodSync(buildDir2, 493);
|
|
3081
|
+
if (!filesAreCopied(state)) chmodSync(buildDir2, 493);
|
|
3082
|
+
let services;
|
|
2530
3083
|
try {
|
|
2531
3084
|
const gitRemote = gitRemoteFor(runtime, {
|
|
2532
3085
|
repositories: repositoriesOf({ repositories: job.repositories, repository, branch: job.branch }),
|
|
@@ -2535,7 +3088,8 @@ async function startCheckContainer(input) {
|
|
|
2535
3088
|
log: (line) => log.line(`${tag} ${line}`),
|
|
2536
3089
|
readOnly: true
|
|
2537
3090
|
});
|
|
2538
|
-
|
|
3091
|
+
services = await startServicesFor(runtime, config, buildDir2, checksDir, void 0, false, gitRemote, transport === "exec" ? "stream" : "socket");
|
|
3092
|
+
runtime.tunnels.set(buildId, services);
|
|
2539
3093
|
} catch (err) {
|
|
2540
3094
|
return fail(`the tunnel could not start: ${describeCallFailure(err)}`);
|
|
2541
3095
|
}
|
|
@@ -2548,33 +3102,59 @@ async function startCheckContainer(input) {
|
|
|
2548
3102
|
runnerId,
|
|
2549
3103
|
container: name,
|
|
2550
3104
|
volume: name,
|
|
2551
|
-
buildDir: buildDir2,
|
|
3105
|
+
...filesAreCopied(state) ? { jobFiles: "copied" } : { buildDir: buildDir2 },
|
|
2552
3106
|
image: built.imageId,
|
|
2553
3107
|
user: ADDED_USER,
|
|
2554
3108
|
limits: limits2,
|
|
2555
3109
|
destinations: config.build?.destinations?.length ?? 0,
|
|
2556
|
-
builtIns: false
|
|
3110
|
+
builtIns: false,
|
|
3111
|
+
transport
|
|
2557
3112
|
};
|
|
2558
3113
|
const prepared = await host.run(prepareRunArgs(runInput), { logPath });
|
|
2559
3114
|
if (prepared.status !== 0) return dockerFailed("preparing the check environment's volume", prepared);
|
|
2560
3115
|
const started = await host.run(containerRunArgs(runInput), { logPath });
|
|
2561
3116
|
if (started.status !== 0) return dockerFailed("starting the check environment's container", started);
|
|
3117
|
+
if (transport === "exec") joinBuild(runtime, state, services, (line) => log.line(`${tag} ${line}`));
|
|
2562
3118
|
if (!await waitForRelay(runtime, state)) return fail("the relay in the check environment's container did not start");
|
|
2563
3119
|
const env = checkEnvironment({ config, item: job.item, repository, hostEnv: runtime.hostEnv, containerEnv: built.containerEnv ?? {}, imageEnv: built.imageEnv });
|
|
3120
|
+
const secrets = () => tokens.issuedTokens();
|
|
3121
|
+
let delivered = false;
|
|
3122
|
+
const deliverInput = async () => {
|
|
3123
|
+
const result = await deliverEvalInput(runtime, state, secrets);
|
|
3124
|
+
if (result.ok) delivered = true;
|
|
3125
|
+
return result;
|
|
3126
|
+
};
|
|
2564
3127
|
return {
|
|
2565
3128
|
ok: true,
|
|
2566
3129
|
container: {
|
|
2567
3130
|
state,
|
|
2568
|
-
//
|
|
2569
|
-
evalInput: {
|
|
3131
|
+
// A container that mounts the build's directory already has the folder the runner wrote there; any other is given a copy.
|
|
3132
|
+
evalInput: {
|
|
3133
|
+
dir: join(buildDir2, EVAL_INPUT_NAME),
|
|
3134
|
+
deliver: async () => {
|
|
3135
|
+
if (!filesAreCopied(state)) return;
|
|
3136
|
+
const result = await deliverInput();
|
|
3137
|
+
if (!result.ok) throw new DeliveryFailure(result.reason);
|
|
3138
|
+
}
|
|
3139
|
+
},
|
|
2570
3140
|
exec: (argv, workdir, opts) => host.exec({ container: name, user: agentUser, workdir, env, argv }, opts?.logPath === void 0 ? void 0 : { logPath: opts.logPath }),
|
|
2571
3141
|
restart: async () => {
|
|
3142
|
+
await leaveBuild(runtime, state);
|
|
2572
3143
|
await host.stop(name);
|
|
2573
3144
|
const again = await host.start(name);
|
|
2574
|
-
|
|
3145
|
+
if (again.status !== 0 || !await waitForRelay(runtime, state)) return false;
|
|
3146
|
+
rejoinBuild(runtime, state, (line) => log.line(`${tag} ${line}`));
|
|
3147
|
+
if (delivered) {
|
|
3148
|
+
const again2 = await deliverInput();
|
|
3149
|
+
if (!again2.ok) {
|
|
3150
|
+
log.error(`${tag} ${again2.reason}`);
|
|
3151
|
+
return false;
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
3154
|
+
return true;
|
|
2575
3155
|
}
|
|
2576
3156
|
}
|
|
2577
3157
|
};
|
|
2578
3158
|
}
|
|
2579
3159
|
|
|
2580
|
-
export { ADDED_USER, AgentUserError, AssetError, EVAL_INPUT_NAME, GH_CONFIG_YML, MIN_ENGINE_MAJOR, cleanupContainer,
|
|
3160
|
+
export { ADDED_USER, AgentUserError, AssetError, DeliveryFailure, EVAL_INPUT_NAME, GH_CONFIG_YML, LEASE_SECONDS, MIN_ENGINE_MAJOR, cleanupContainer, containerCommandExec, containerJudgeEnvironment, containerSenderCommand, copyJudgeTree, copyTranscripts, copyTranscriptsFromContainer, createBuildDisk, createContainerRuntime, deleteBuildDisk, deliverContainerHooks, ensureRootfs, ensureTunnel, filesAreCopied, ghHostsYml, gitRemoteFor, isLiteralAddress, keepGhConfigCurrent, leaseExpiresAtMs, lifecycleFailureReason, lifecycleSteps, machineConfig, measureDisk, obtainImage, outOfMemoryReason, parseCpuList, prepareBranch, provisionContainer, resolveAgentUser, runBuildSteps, senderAliveScript, senderSignalScript, setUpRunnerCgroup, shellQuote, signalInContainer, startCheckContainer, startServicesFor, stopContainer, syncInContainer, writeEvalInput };
|