@outerlayer/cli 0.34.0-next.20261026232906 → 0.34.0-next.20261027013606
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/README.md +4 -3
- package/dist/{build-M4UYZAFH.js → build-YSOVDTAN.js} +4 -3
- package/dist/{build-disk-ZDVSSHO2.js → build-disk-RZ7AZKTY.js} +3 -3
- package/dist/build-info.json +1 -1
- package/dist/{builder-IORYS76D.js → builder-MQIL24HI.js} +2 -2
- package/dist/{check-V66LZDNK.js → check-JD3GVQFP.js} +15 -14
- package/dist/chunk-7GY3DVIL.js +255 -0
- package/dist/{chunk-MIRCFK4X.js → chunk-7PCA37ZS.js} +1 -1
- package/dist/{chunk-TLIZ7FY4.js → chunk-7QF2EXAN.js} +2 -2
- package/dist/{chunk-XDJ376OP.js → chunk-7X2G463B.js} +5 -169
- package/dist/{chunk-DFBY6LX3.js → chunk-AHAYHJNN.js} +10 -7
- package/dist/{chunk-6NXBXWOP.js → chunk-BLSEHKZ4.js} +1 -1
- package/dist/{chunk-Q3L5AC75.js → chunk-DEJCZ43H.js} +3 -3
- package/dist/{chunk-F5H3LVHR.js → chunk-DM4EEV73.js} +2 -2
- package/dist/{chunk-IZVDQXTC.js → chunk-EP3WBNMC.js} +9 -9
- package/dist/{chunk-JZLMFYFM.js → chunk-FQGC22IP.js} +1 -1
- package/dist/{chunk-TGBST2QL.js → chunk-FQOUIYBL.js} +6 -6
- package/dist/{chunk-T7HUNNJ2.js → chunk-GK6OYFMU.js} +3 -3
- package/dist/{chunk-KOZ6IBEI.js → chunk-GR7ZVGNI.js} +1 -1
- package/dist/{chunk-PN7RMFPP.js → chunk-JM3YQ4LC.js} +2 -2
- package/dist/{chunk-T6HSNQAT.js → chunk-MPNASRYB.js} +4 -4
- package/dist/{chunk-63A6NUEZ.js → chunk-Q2KGDDUX.js} +1 -1
- package/dist/{chunk-4UPET3R7.js → chunk-QUOITPN7.js} +21 -2
- package/dist/{chunk-XLJXO4QC.js → chunk-VUJURIPJ.js} +5 -5
- package/dist/{cli-HQWQEHXJ.js → cli-3BAUWQCS.js} +45 -45
- package/dist/{command-TONUMZ2A.js → command-UVGNJILP.js} +9 -8
- package/dist/{config-4K6OG2SZ.js → config-NGCQD6SN.js} +4 -4
- package/dist/{container-start-R74IO7CI.js → container-start-YBENDAOQ.js} +12 -11
- package/dist/{demo-record-cmd-7HJ5C7KM.js → demo-record-cmd-C2C4P2EQ.js} +10 -1
- package/dist/{demo-voice-cmd-MBK3BGHZ.js → demo-voice-cmd-RWTRJUAX.js} +2 -2
- package/dist/{docker-XWQM4F7B.js → docker-TRGA5QRL.js} +2 -2
- package/dist/{doctor-SF4GAWAO.js → doctor-QENPDYBD.js} +10 -9
- package/dist/{emit-demo-cmd-6VTA5BSJ.js → emit-demo-cmd-QMRDQGB6.js} +2 -2
- package/dist/{find-unit-I4GWKY4T.js → find-unit-IOCVYPNW.js} +6 -6
- package/dist/{host-checks-FKLQST3J.js → host-checks-UNEQTQ6L.js} +10 -9
- package/dist/index.js +1 -1
- package/dist/{init-TDLUTI6F.js → init-6UEOOD2J.js} +9 -8
- package/dist/{init-3MO5WCU3.js → init-JR62PVKC.js} +9 -8
- package/dist/{init-cmd-MQ5R35O2.js → init-cmd-34YWWGQZ.js} +1 -1
- package/dist/{loop-NNNDBFAS.js → loop-WHK6EVUE.js} +14 -13
- package/dist/{machine-GCW2GGI2.js → machine-ZZOQ7LLV.js} +9 -8
- package/dist/{runner-env-LVVLUHY2.js → runner-env-DH7YA4BU.js} +6 -6
- package/dist/{runner-identity-73F2IWBT.js → runner-identity-GLL56XY7.js} +2 -2
- package/dist/{runner-key-ASPZ7TQH.js → runner-key-4ASVLQGM.js} +9 -8
- package/dist/{status-35Z3TW42.js → status-7YP2C2QK.js} +10 -9
- package/dist/{update-steps-ZTNIK7LA.js → update-steps-J54P3SBL.js} +11 -10
- package/dist/{work-claim-cmd-QPYCZUMI.js → work-claim-cmd-JF5GGDFZ.js} +4 -4
- package/dist/{work-proofs-cmd-56EY7O45.js → work-proofs-cmd-DTFQ3FSB.js} +1 -1
- package/dist/{work-proofs-record-SPGI47OS.js → work-proofs-record-XG544MGV.js} +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -234,7 +234,8 @@ content:
|
|
|
234
234
|
from GitHub, a pinned static `ffmpeg` build from GitHub, and the narration
|
|
235
235
|
voice pack from Hugging Face and the npm registry, checking each against a
|
|
236
236
|
digest. It also installs the CLI's pinned Playwright release from the npm
|
|
237
|
-
registry,
|
|
237
|
+
registry, and that release's headless Chromium and video encoder from
|
|
238
|
+
cdn.playwright.dev, checked against a digest. The image build runs outside
|
|
238
239
|
any work item and sends nothing to your factory. `outerlayer runner start`
|
|
239
240
|
builds each build's image the same way, before the build starts. That image
|
|
240
241
|
build uses the host's network: the build's tunnel does not carry it, and
|
|
@@ -251,8 +252,8 @@ content:
|
|
|
251
252
|
the host. An image build in a builder microVM also needs the files of the
|
|
252
253
|
runner's tools layer, which the runner downloads on the host. It gets `gh`,
|
|
253
254
|
`tini` and FFmpeg from github.com. It gets the narration voice pack from
|
|
254
|
-
huggingface.co and registry.npmjs.org
|
|
255
|
-
pinned in the CLI. The microVM reaches the network only
|
|
255
|
+
huggingface.co and registry.npmjs.org, and the demo browser from
|
|
256
|
+
cdn.playwright.dev. Each file is checked against a sha256 pinned in the CLI. The microVM reaches the network only
|
|
256
257
|
through the same tunnel a container build uses, which the runner serves on
|
|
257
258
|
Unix sockets and the guest reaches over vsock. In a build with Docker, the
|
|
258
259
|
guest forwards the proxy connections of containers and `docker build` steps
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
|
-
export { KEY_PREFIX, TAG_HOST, buildRepositoryImage, metadataFromLabels, prepareImageBuild, writeRecord } from './chunk-
|
|
4
|
-
import './chunk-
|
|
5
|
-
import './chunk-
|
|
3
|
+
export { KEY_PREFIX, TAG_HOST, buildRepositoryImage, metadataFromLabels, prepareImageBuild, writeRecord } from './chunk-7X2G463B.js';
|
|
4
|
+
import './chunk-7GY3DVIL.js';
|
|
5
|
+
import './chunk-GR7ZVGNI.js';
|
|
6
6
|
export { RecipeError } from './chunk-HDLON3RJ.js';
|
|
7
|
+
import './chunk-QUOITPN7.js';
|
|
7
8
|
import './chunk-LQHCNXPK.js';
|
|
8
9
|
import './chunk-JBJTC3UU.js';
|
|
9
10
|
import './chunk-L5VHGRQE.js';
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
|
-
export { DEFAULT_BUILD_DISK, DEFAULT_CACHE_DISK, DOCKER_CACHE_MS, allocatedBytes, checkDiskFit, realBuildDiskProbe, runDocker, volumeBytes } from './chunk-
|
|
4
|
-
import './chunk-
|
|
5
|
-
import './chunk-4UPET3R7.js';
|
|
3
|
+
export { DEFAULT_BUILD_DISK, DEFAULT_CACHE_DISK, DOCKER_CACHE_MS, allocatedBytes, checkDiskFit, realBuildDiskProbe, runDocker, volumeBytes } from './chunk-BLSEHKZ4.js';
|
|
4
|
+
import './chunk-GR7ZVGNI.js';
|
|
6
5
|
import './chunk-HDLON3RJ.js';
|
|
6
|
+
import './chunk-QUOITPN7.js';
|
|
7
7
|
import './chunk-L5VHGRQE.js';
|
|
8
8
|
import './chunk-OOD5NDQA.js';
|
|
9
9
|
import './chunk-DINRXUQA.js';
|
package/dist/build-info.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"id":"2026-10-
|
|
1
|
+
{"id":"2026-10-27T01:37:43.355Z+a8ae7a2","root":"/home/runner/_work/outerlayer/outerlayer","pkg":"outerlayer"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
|
-
export { MIN_BUILDX_VERSION, builderName, builderSpec, buildxState, ensureBuilder, ensureFailureCount, isOutOfMemory, proxyEnvironment, trackImageBuild } from './chunk-
|
|
4
|
-
import './chunk-4UPET3R7.js';
|
|
3
|
+
export { MIN_BUILDX_VERSION, builderName, builderSpec, buildxState, ensureBuilder, ensureFailureCount, isOutOfMemory, proxyEnvironment, trackImageBuild } from './chunk-GR7ZVGNI.js';
|
|
5
4
|
import './chunk-HDLON3RJ.js';
|
|
5
|
+
import './chunk-QUOITPN7.js';
|
|
6
6
|
import './chunk-L5VHGRQE.js';
|
|
7
7
|
import './chunk-OOD5NDQA.js';
|
|
8
8
|
import './chunk-DINRXUQA.js';
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
|
-
import { probeCredentials } from './chunk-
|
|
3
|
+
import { probeCredentials } from './chunk-EP3WBNMC.js';
|
|
4
4
|
import './chunk-3JVYMD4G.js';
|
|
5
|
-
import './chunk-
|
|
5
|
+
import './chunk-DM4EEV73.js';
|
|
6
6
|
import './chunk-E7R2GIAW.js';
|
|
7
7
|
import './chunk-MH6QLXUO.js';
|
|
8
8
|
import './chunk-HOUFCUMH.js';
|
|
9
|
-
import './chunk-
|
|
9
|
+
import './chunk-JM3YQ4LC.js';
|
|
10
10
|
import './chunk-TS3EFB7S.js';
|
|
11
|
-
import './chunk-
|
|
11
|
+
import './chunk-FQOUIYBL.js';
|
|
12
12
|
import './chunk-6CK2OC4Q.js';
|
|
13
13
|
import './chunk-ZVB57WI6.js';
|
|
14
14
|
import './chunk-P3NPTFLF.js';
|
|
@@ -18,27 +18,28 @@ import './chunk-QRJIBTNQ.js';
|
|
|
18
18
|
import './chunk-7DK2FOZ4.js';
|
|
19
19
|
import './chunk-ESVW4MJY.js';
|
|
20
20
|
import './chunk-CFFVCI2I.js';
|
|
21
|
-
import { managedProblems, managedFactsOf } from './chunk-
|
|
21
|
+
import { managedProblems, managedFactsOf } from './chunk-VUJURIPJ.js';
|
|
22
22
|
import './chunk-YL3RWW7X.js';
|
|
23
23
|
import './chunk-TBT347UY.js';
|
|
24
|
-
import { findServiceUnit } from './chunk-
|
|
24
|
+
import { findServiceUnit } from './chunk-FQGC22IP.js';
|
|
25
25
|
import './chunk-TZMDRHWB.js';
|
|
26
|
-
import { placeholderQueues, placeholderProblem, permissionModeProblems, permissionModeProblem, HELPER_ENV, helperSocketPath, probeHelper, needsClaudeCredential, claudeCredentialCheck, claimPermissionProblem, formerSkillQueues, formerSkillProblem } from './chunk-
|
|
26
|
+
import { placeholderQueues, placeholderProblem, permissionModeProblems, permissionModeProblem, HELPER_ENV, helperSocketPath, probeHelper, needsClaudeCredential, claudeCredentialCheck, claimPermissionProblem, formerSkillQueues, formerSkillProblem } from './chunk-AHAYHJNN.js';
|
|
27
27
|
import './chunk-2PGI6RDE.js';
|
|
28
|
-
import './chunk-
|
|
28
|
+
import './chunk-7X2G463B.js';
|
|
29
|
+
import './chunk-7GY3DVIL.js';
|
|
29
30
|
import './chunk-OGFGZQA3.js';
|
|
30
31
|
import './chunk-3VZTNXK5.js';
|
|
31
32
|
import './chunk-6KFJIU73.js';
|
|
32
33
|
import './chunk-77VBS2RT.js';
|
|
33
34
|
import './chunk-SDELQEG4.js';
|
|
34
35
|
import './chunk-TQMI335S.js';
|
|
35
|
-
import './chunk-
|
|
36
|
-
import './chunk-
|
|
37
|
-
import { loadRunnerConfig, RunnerConfigError, checkHooks } from './chunk-
|
|
38
|
-
import './chunk-
|
|
39
|
-
import './chunk-
|
|
40
|
-
import './chunk-4UPET3R7.js';
|
|
36
|
+
import './chunk-Q2KGDDUX.js';
|
|
37
|
+
import './chunk-7QF2EXAN.js';
|
|
38
|
+
import { loadRunnerConfig, RunnerConfigError, checkHooks } from './chunk-DEJCZ43H.js';
|
|
39
|
+
import './chunk-BLSEHKZ4.js';
|
|
40
|
+
import './chunk-GR7ZVGNI.js';
|
|
41
41
|
import './chunk-HDLON3RJ.js';
|
|
42
|
+
import './chunk-QUOITPN7.js';
|
|
42
43
|
import './chunk-LQHCNXPK.js';
|
|
43
44
|
import './chunk-JBJTC3UU.js';
|
|
44
45
|
import './chunk-KT4CN6OU.js';
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { GH_VERSION, VOICE_PACK_FILES, VOICE_PACK_VERSION, FFMPEG_STATIC, FFMPEG_BUILD_TAG, GH_SHA256, TINI_SHA256, TINI_VERSION, HEADLESS_SHELL_REVISION, HEADLESS_SHELL_BASE_URL, PLAYWRIGHT_FFMPEG_BASE_URL, PLAYWRIGHT_FFMPEG_REVISION, HEADLESS_SHELL, PLAYWRIGHT_FFMPEG, PLAYWRIGHT_VERSION } from './chunk-QUOITPN7.js';
|
|
4
|
+
import { PROXY_WARNING_FLAG } from './chunk-SDAEXUIX.js';
|
|
5
|
+
import { RELAY_ID, RELAY_USER } from './chunk-MESOOLF4.js';
|
|
6
|
+
|
|
7
|
+
createRequire(import.meta.url);
|
|
8
|
+
|
|
9
|
+
// src/runner/image/dockerfiles.ts
|
|
10
|
+
var FETCHED_DIR = "fetched";
|
|
11
|
+
var FETCHED_VOICE_DIR = "voice";
|
|
12
|
+
var FETCHED_FFMPEG = "ffmpeg.tar.xz";
|
|
13
|
+
var FETCHED_GH = "gh.tgz";
|
|
14
|
+
var FETCHED_TINI = "tini";
|
|
15
|
+
var FETCHED_HEADLESS_SHELL = "headless-shell.zip";
|
|
16
|
+
var FETCHED_PLAYWRIGHT_FFMPEG = "playwright-ffmpeg.zip";
|
|
17
|
+
var IMAGE_BROWSER = {
|
|
18
|
+
dir: "/opt/outerlayer/browser",
|
|
19
|
+
playwright: "/opt/outerlayer/browser/playwright",
|
|
20
|
+
fontConfig: "/opt/outerlayer/browser/fonts.conf"
|
|
21
|
+
};
|
|
22
|
+
function plain(name, value, allowed) {
|
|
23
|
+
if (!allowed.test(value)) throw new Error(`${name} holds a character a Dockerfile line cannot carry safely: ${JSON.stringify(value)}`);
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
var DEFAULT_PACKAGES = ["git", "ca-certificates", "curl", "build-essential", "python3", "python3-pip", "python3-venv", "unzip", "jq"];
|
|
27
|
+
function defaultDockerfile(baseImage, nodeImage) {
|
|
28
|
+
return [
|
|
29
|
+
ubuntuWithNode(baseImage, nodeImage, DEFAULT_PACKAGES),
|
|
30
|
+
// The browsers live in one shared folder so any user finds them. A build container's root filesystem is
|
|
31
|
+
// read-only, so the build user cannot add to this folder: a repository that needs another Playwright version
|
|
32
|
+
// installs that version's Chromium into a writable folder by setting PLAYWRIGHT_BROWSERS_PATH.
|
|
33
|
+
// `install --with-deps` runs apt-get as root and works on amd64 and arm64.
|
|
34
|
+
"ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright",
|
|
35
|
+
"RUN mkdir -p /ms-playwright \\",
|
|
36
|
+
` && npx -y playwright@${plain("the Playwright version", PLAYWRIGHT_VERSION, /^[0-9.]+$/)} install --with-deps chromium \\`,
|
|
37
|
+
" && chmod -R a+rX /ms-playwright \\",
|
|
38
|
+
" && rm -rf /var/lib/apt/lists/* /root/.npm",
|
|
39
|
+
""
|
|
40
|
+
].join("\n");
|
|
41
|
+
}
|
|
42
|
+
function ubuntuWithNode(baseImage, nodeImage, packages) {
|
|
43
|
+
return [
|
|
44
|
+
`FROM ${baseImage}`,
|
|
45
|
+
"ENV DEBIAN_FRONTEND=noninteractive",
|
|
46
|
+
"RUN apt-get update \\",
|
|
47
|
+
` && apt-get install -y --no-install-recommends ${packages.join(" ")} \\`,
|
|
48
|
+
" && rm -rf /var/lib/apt/lists/*",
|
|
49
|
+
`COPY --from=${nodeImage} /usr/local/bin/node /usr/local/bin/node`,
|
|
50
|
+
`COPY --from=${nodeImage} /usr/local/lib/node_modules /usr/local/lib/node_modules`,
|
|
51
|
+
"RUN ln -s ../lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \\",
|
|
52
|
+
" && ln -s ../lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx \\",
|
|
53
|
+
" && ln -s ../lib/node_modules/corepack/dist/corepack.js /usr/local/bin/corepack",
|
|
54
|
+
""
|
|
55
|
+
].join("\n");
|
|
56
|
+
}
|
|
57
|
+
function toolsDockerfile(parts) {
|
|
58
|
+
const recipeImage = plain("the recipe image", parts.recipeImage, /^[A-Za-z0-9_.:@/-]+$/);
|
|
59
|
+
const nodeImage = plain("the Node image", parts.nodeImage, /^[A-Za-z0-9_.:@/-]+$/);
|
|
60
|
+
const version = plain("the Claude Code version", parts.claudeCodeVersion, /^[A-Za-z0-9.+-]+$/);
|
|
61
|
+
const buildId = plain("the build id", parts.buildId, /^[A-Za-z0-9_.:+-]+$/);
|
|
62
|
+
const tools = JSON.stringify({ outerlayerBuildId: buildId, claudeCodeVersion: version, ghVersion: GH_VERSION });
|
|
63
|
+
const managedSettings = JSON.stringify({
|
|
64
|
+
hooks: Object.fromEntries(
|
|
65
|
+
["SessionStart", "SessionEnd", "Stop", "PostToolUse"].map((event) => [event, [{ hooks: [{ type: "command", command: `outerlayer hook ${event}` }] }]])
|
|
66
|
+
)
|
|
67
|
+
});
|
|
68
|
+
const download = `NODE_USE_ENV_PROXY=1 node ${PROXY_WARNING_FLAG} -e 'fetch(process.argv[1]).then((r)=>{if(!r.ok)throw new Error(process.argv[1]+" "+r.status);return r.arrayBuffer()}).then((b)=>require("fs").writeFileSync(process.argv[2],Buffer.from(b)))'`;
|
|
69
|
+
const stream = `NODE_USE_ENV_PROXY=1 node ${PROXY_WARNING_FLAG} -e 'const{pipeline}=require("stream/promises"),{Readable}=require("stream"),fs=require("fs");fetch(process.argv[1]).then((r)=>{if(!r.ok)throw new Error(process.argv[1]+" "+r.status);return pipeline(Readable.fromWeb(r.body),fs.createWriteStream(process.argv[2]))})'`;
|
|
70
|
+
const voiceDir = `/out/opt/outerlayer/voice/${VOICE_PACK_VERSION}`;
|
|
71
|
+
const checks = VOICE_PACK_FILES.map((file) => `echo "${file.sha256} ${voiceDir}/${file.name}" | sha256sum -c -`);
|
|
72
|
+
const fetched = parts.fetched === true;
|
|
73
|
+
const voice = fetched ? [`FROM ${nodeImage} AS voice`, `COPY ${FETCHED_DIR}/${FETCHED_VOICE_DIR}/ ${voiceDir}/`, `RUN ${checks.join(" \\\n && ")} \\`, " && chmod -R a+rX /out"] : [
|
|
74
|
+
// The voice pack streams to disk, one pinned file at a time, because a 300 MB model must not sit in a build's memory. Its own stage keeps it cached when the CLI changes.
|
|
75
|
+
`FROM ${nodeImage} AS voice`,
|
|
76
|
+
`RUN mkdir -p ${voiceDir} \\`,
|
|
77
|
+
...VOICE_PACK_FILES.map((file) => ` && ${stream} ${file.url} ${voiceDir}/${file.name} \\`),
|
|
78
|
+
...checks.map((check) => ` && ${check} \\`),
|
|
79
|
+
" && chmod -R a+rX /out"
|
|
80
|
+
];
|
|
81
|
+
return [
|
|
82
|
+
...voice,
|
|
83
|
+
// ffmpeg and ffprobe are static LGPL builds. espeak-ng comes from the stage image's own package, with the libraries and loader it needs, and runs as a separate program.
|
|
84
|
+
`FROM ${nodeImage} AS media`,
|
|
85
|
+
"ARG TARGETARCH",
|
|
86
|
+
"RUN apt-get update \\",
|
|
87
|
+
" && apt-get install -y --no-install-recommends ca-certificates espeak-ng xz-utils \\",
|
|
88
|
+
" && rm -rf /var/lib/apt/lists/*",
|
|
89
|
+
...fetched ? [`COPY ${FETCHED_DIR}/${FETCHED_FFMPEG} /tmp/ffmpeg.tar.xz`] : [],
|
|
90
|
+
"RUN set -eu \\",
|
|
91
|
+
` && case "$TARGETARCH" in \\`,
|
|
92
|
+
` amd64) ffmpeg_file=${FFMPEG_STATIC.amd64.file}; ffmpeg_sha=${FFMPEG_STATIC.amd64.sha256} ;; \\`,
|
|
93
|
+
` arm64) ffmpeg_file=${FFMPEG_STATIC.arm64.file}; ffmpeg_sha=${FFMPEG_STATIC.arm64.sha256} ;; \\`,
|
|
94
|
+
` *) echo "no pinned ffmpeg for $TARGETARCH" >&2; exit 1 ;; \\`,
|
|
95
|
+
" esac \\",
|
|
96
|
+
...fetched ? [] : [` && ${download} https://github.com/BtbN/FFmpeg-Builds/releases/download/${FFMPEG_BUILD_TAG}/$ffmpeg_file /tmp/ffmpeg.tar.xz \\`],
|
|
97
|
+
' && echo "$ffmpeg_sha /tmp/ffmpeg.tar.xz" | sha256sum -c - \\',
|
|
98
|
+
" && tar -xJf /tmp/ffmpeg.tar.xz -C /tmp \\",
|
|
99
|
+
" && mkdir -p /out/usr/local/bin /out/opt/outerlayer/media/lib \\",
|
|
100
|
+
" && install -m 0755 /tmp/ffmpeg-*/bin/ffmpeg /tmp/ffmpeg-*/bin/ffprobe /out/usr/local/bin/ \\",
|
|
101
|
+
" && install -m 0755 /usr/bin/espeak-ng /out/opt/outerlayer/media/espeak-ng \\",
|
|
102
|
+
` && for lib in $(ldd /usr/bin/espeak-ng | awk '$3 ~ /^\\// {print $3}'); do cp -L "$lib" /out/opt/outerlayer/media/lib/; done \\`,
|
|
103
|
+
" && cp -L $(ldd /usr/bin/espeak-ng | awk '$1 ~ /ld-linux/ {print $1}') /out/opt/outerlayer/media/lib/ld.so \\",
|
|
104
|
+
" && cp -r /usr/lib/*/espeak-ng-data /out/opt/outerlayer/media/espeak-ng-data \\",
|
|
105
|
+
` && printf '%s\\n' '#!/bin/sh' 'ESPEAK_DATA_PATH=/opt/outerlayer/media' 'export ESPEAK_DATA_PATH' 'exec /opt/outerlayer/media/lib/ld.so --library-path /opt/outerlayer/media/lib /opt/outerlayer/media/espeak-ng "$@"' > /out/usr/local/bin/espeak-ng \\`,
|
|
106
|
+
" && chmod 0755 /out/usr/local/bin/espeak-ng \\",
|
|
107
|
+
" && chmod -R a+rX /out",
|
|
108
|
+
...browserStage({ nodeImage, fetched, download, stream }),
|
|
109
|
+
`FROM ${recipeImage} AS recipe`,
|
|
110
|
+
`FROM ${nodeImage} AS stage`,
|
|
111
|
+
"ARG TARGETARCH",
|
|
112
|
+
`RUN npm install --global --prefix /out/opt/outerlayer/claude @anthropic-ai/claude-code@${version}`,
|
|
113
|
+
"COPY dist /out/opt/outerlayer/cli/dist",
|
|
114
|
+
"COPY package.json /out/opt/outerlayer/cli/package.json",
|
|
115
|
+
// `outerlayer demo record` runs a demo on the Playwright release this CLI depends on, found beside the CLI as an npm
|
|
116
|
+
// install puts it. Only the library, with no install scripts: its browser is the browser stage's, which no other
|
|
117
|
+
// Playwright in the image looks for.
|
|
118
|
+
"RUN mkdir -p /tmp/cli-deps && cd /tmp/cli-deps && echo '{}' > package.json \\",
|
|
119
|
+
` && npm install --no-audit --no-fund --ignore-scripts @playwright/test@${plain("the Playwright version", PLAYWRIGHT_VERSION, /^[0-9.]+$/)} \\`,
|
|
120
|
+
" && mv node_modules /out/opt/outerlayer/cli/node_modules",
|
|
121
|
+
// `outerlayer claude` installs the maintained skills from this pack into the build's home folder before the agent starts.
|
|
122
|
+
"COPY skill-pack /out/opt/outerlayer/cli/skill-pack",
|
|
123
|
+
"RUN mkdir -p /out/opt/outerlayer/bin /out/usr/local/bin /out/etc/claude-code /out/build \\",
|
|
124
|
+
" && cp /usr/local/bin/node /out/opt/outerlayer/bin/node \\",
|
|
125
|
+
` && printf '%s\\n' '#!/bin/sh' 'exec /opt/outerlayer/bin/node /opt/outerlayer/cli/dist/index.js "$@"' > /out/usr/local/bin/outerlayer \\`,
|
|
126
|
+
` && printf '%s\\n' '#!/bin/sh' 'PATH=/opt/outerlayer/bin:$PATH' 'export PATH' 'exec /opt/outerlayer/claude/bin/claude "$@"' > /out/usr/local/bin/claude \\`,
|
|
127
|
+
` && printf '%s\\n' '${tools}' > /out/opt/outerlayer/tools.json \\`,
|
|
128
|
+
` && printf '%s\\n' '${managedSettings}' > /out/etc/claude-code/managed-settings.json \\`,
|
|
129
|
+
" && chmod 0755 /out/usr/local/bin/outerlayer /out/usr/local/bin/claude \\",
|
|
130
|
+
// Every build, whatever user runs it, writes here: the first mount of an empty volume copies this mode.
|
|
131
|
+
" && chmod 1777 /out/build",
|
|
132
|
+
...parts.guestHelper === true ? [
|
|
133
|
+
"COPY guest/outerlayer-guest /out/opt/outerlayer/bin/outerlayer-guest",
|
|
134
|
+
// The guest mounts the build's tmpfs, the kernel's file systems and the run directory over these. The judge's tmpfs is mounted over its directory when the judge runs.
|
|
135
|
+
"RUN chmod 0755 /out/opt/outerlayer/bin/outerlayer-guest \\",
|
|
136
|
+
" && mkdir -p /out/outerlayer/job /out/outerlayer/judge /out/run /out/proc /out/sys /out/dev"
|
|
137
|
+
] : [],
|
|
138
|
+
...fetched ? [`COPY ${FETCHED_DIR}/${FETCHED_TINI} /tmp/tini`, `COPY ${FETCHED_DIR}/${FETCHED_GH} /tmp/gh.tgz`] : [],
|
|
139
|
+
`RUN set -eu \\`,
|
|
140
|
+
` && case "$TARGETARCH" in \\`,
|
|
141
|
+
` amd64) gh_sha=${GH_SHA256.amd64}; tini_sha=${TINI_SHA256.amd64} ;; \\`,
|
|
142
|
+
` arm64) gh_sha=${GH_SHA256.arm64}; tini_sha=${TINI_SHA256.arm64} ;; \\`,
|
|
143
|
+
` *) echo "no pinned gh and tini for $TARGETARCH" >&2; exit 1 ;; \\`,
|
|
144
|
+
" esac \\",
|
|
145
|
+
...fetched ? [] : [` && ${download} https://github.com/krallin/tini/releases/download/v${TINI_VERSION}/tini-static-$TARGETARCH /tmp/tini \\`],
|
|
146
|
+
' && echo "$tini_sha /tmp/tini" | sha256sum -c - \\',
|
|
147
|
+
" && install -m 0755 /tmp/tini /out/opt/outerlayer/bin/tini \\",
|
|
148
|
+
...fetched ? [] : [` && ${download} https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_$TARGETARCH.tar.gz /tmp/gh.tgz \\`],
|
|
149
|
+
' && echo "$gh_sha /tmp/gh.tgz" | sha256sum -c - \\',
|
|
150
|
+
" && tar -xzf /tmp/gh.tgz -C /tmp \\",
|
|
151
|
+
` && install -m 0755 /tmp/gh_${GH_VERSION}_linux_$TARGETARCH/bin/gh /out/usr/local/bin/gh`,
|
|
152
|
+
"COPY --from=recipe /etc/passwd /tmp/recipe-passwd",
|
|
153
|
+
"COPY --from=recipe /etc/group /tmp/recipe-group",
|
|
154
|
+
`RUN uid=$(awk -F: 'BEGIN{m=1499} $3>=1500 && $3<60000 && $3>m{m=$3} END{print m+1}' /tmp/recipe-passwd) \\`,
|
|
155
|
+
` && gid=$(awk -F: 'BEGIN{m=1499} $3>=1500 && $3<60000 && $3>m{m=$3} END{print m+1}' /tmp/recipe-group) \\`,
|
|
156
|
+
" && { grep -v -e '^ol-agent:' -e '^ol-relay:' /tmp/recipe-passwd > /out/etc/passwd || true; } \\",
|
|
157
|
+
" && { grep -v -e '^ol-agent:' -e '^ol-relay:' /tmp/recipe-group > /out/etc/group || true; } \\",
|
|
158
|
+
// The relay's id is fixed because the runner names it in the container's `--tmpfs` options before the container exists.
|
|
159
|
+
` && if awk -F: '$3==${RELAY_ID}{found=1} END{exit !found}' /out/etc/passwd; then echo "uid ${RELAY_ID} is taken by a user of the recipe image, and the relay needs it" >&2; exit 1; fi \\`,
|
|
160
|
+
` && if awk -F: '$3==${RELAY_ID}{found=1} END{exit !found}' /out/etc/group; then echo "gid ${RELAY_ID} is taken by a group of the recipe image, and the relay needs it" >&2; exit 1; fi \\`,
|
|
161
|
+
` && printf '%s\\n' "ol-agent:x:$uid:$gid::/build/home:/bin/sh" >> /out/etc/passwd \\`,
|
|
162
|
+
` && printf '%s\\n' "ol-agent:x:$gid:" >> /out/etc/group \\`,
|
|
163
|
+
` && printf '%s\\n' "${RELAY_USER}:x:${RELAY_ID}:${RELAY_ID}::/nonexistent:/usr/sbin/nologin" >> /out/etc/passwd \\`,
|
|
164
|
+
` && printf '%s\\n' "${RELAY_USER}:x:${RELAY_ID}:" >> /out/etc/group \\`,
|
|
165
|
+
" && chmod 0644 /out/etc/passwd /out/etc/group \\",
|
|
166
|
+
" && chmod -R a+rX /out",
|
|
167
|
+
`FROM ${recipeImage}`,
|
|
168
|
+
"COPY --from=stage /out/ /",
|
|
169
|
+
"COPY --from=voice /out/ /",
|
|
170
|
+
"COPY --from=media /out/ /",
|
|
171
|
+
"COPY --from=browser /out/ /",
|
|
172
|
+
"ENV DISABLE_AUTOUPDATER=1",
|
|
173
|
+
// espeak-ng's audio library makes a private pulse-* folder for the user who runs it, in the temp folder unless XDG_RUNTIME_DIR names another.
|
|
174
|
+
// Made here, as root, it would ship in the image where the build user cannot read it, and the runner's disk check fails on it.
|
|
175
|
+
"RUN check=$(mktemp -d /tmp/ol-check.XXXXXX) \\",
|
|
176
|
+
" && outerlayer --version && claude --version && gh --version && ffmpeg -version && ffprobe -version \\",
|
|
177
|
+
' && XDG_RUNTIME_DIR="$check" espeak-ng --version \\',
|
|
178
|
+
` && ${IMAGE_BROWSER.playwright}/chromium_headless_shell-${HEADLESS_SHELL_REVISION}/chrome-headless-shell-linux*/chrome-headless-shell --version \\`,
|
|
179
|
+
' && rm -rf "$check"',
|
|
180
|
+
""
|
|
181
|
+
].join("\n");
|
|
182
|
+
}
|
|
183
|
+
function browserStage(parts) {
|
|
184
|
+
const { fetched } = parts;
|
|
185
|
+
const libraries = [
|
|
186
|
+
"libasound2",
|
|
187
|
+
"libatk-bridge2.0-0",
|
|
188
|
+
"libatk1.0-0",
|
|
189
|
+
"libatspi2.0-0",
|
|
190
|
+
"libdbus-1-3",
|
|
191
|
+
"libexpat1",
|
|
192
|
+
"libgbm1",
|
|
193
|
+
"libglib2.0-0",
|
|
194
|
+
"libnspr4",
|
|
195
|
+
"libnss3",
|
|
196
|
+
"libudev1",
|
|
197
|
+
"libx11-6",
|
|
198
|
+
"libxcb1",
|
|
199
|
+
"libxcomposite1",
|
|
200
|
+
"libxdamage1",
|
|
201
|
+
"libxext6",
|
|
202
|
+
"libxfixes3",
|
|
203
|
+
"libxkbcommon0",
|
|
204
|
+
"libxrandr2"
|
|
205
|
+
];
|
|
206
|
+
const fonts = ["fonts-liberation", "fonts-dejavu-core", "fonts-wqy-zenhei", "fonts-noto-color-emoji"];
|
|
207
|
+
const lib = `${IMAGE_BROWSER.dir}/lib`;
|
|
208
|
+
const browsers = `/out${IMAGE_BROWSER.playwright}`;
|
|
209
|
+
const fontSettings = [
|
|
210
|
+
'<?xml version="1.0"?>',
|
|
211
|
+
'<!DOCTYPE fontconfig SYSTEM "urn:fontconfig:fonts.dtd">',
|
|
212
|
+
"<fontconfig>",
|
|
213
|
+
` <dir>${IMAGE_BROWSER.dir}/fonts</dir>`,
|
|
214
|
+
" <dir>/usr/share/fonts</dir>",
|
|
215
|
+
" <dir>/usr/local/share/fonts</dir>",
|
|
216
|
+
' <dir prefix="xdg">fonts</dir>',
|
|
217
|
+
` <include ignore_missing="yes">${IMAGE_BROWSER.dir}/conf.d</include>`,
|
|
218
|
+
' <cachedir prefix="xdg">fontconfig</cachedir>',
|
|
219
|
+
"</fontconfig>"
|
|
220
|
+
];
|
|
221
|
+
const archive = (arch) => ` ${arch}) shell_file=${HEADLESS_SHELL[arch].file}; shell_sha=${HEADLESS_SHELL[arch].sha256}; encoder_file=${PLAYWRIGHT_FFMPEG[arch].file}; encoder_sha=${PLAYWRIGHT_FFMPEG[arch].sha256} ;; \\`;
|
|
222
|
+
return [
|
|
223
|
+
`FROM ${parts.nodeImage} AS browser`,
|
|
224
|
+
"ARG TARGETARCH",
|
|
225
|
+
"RUN apt-get update \\",
|
|
226
|
+
` && apt-get install -y --no-install-recommends unzip patchelf fontconfig-config ${fonts.join(" ")} ${libraries.join(" ")} \\`,
|
|
227
|
+
" && rm -rf /var/lib/apt/lists/*",
|
|
228
|
+
...fetched ? [`COPY ${FETCHED_DIR}/${FETCHED_HEADLESS_SHELL} /tmp/shell.zip`, `COPY ${FETCHED_DIR}/${FETCHED_PLAYWRIGHT_FFMPEG} /tmp/encoder.zip`] : [],
|
|
229
|
+
"RUN set -eu \\",
|
|
230
|
+
` && case "$TARGETARCH" in \\`,
|
|
231
|
+
archive("amd64"),
|
|
232
|
+
archive("arm64"),
|
|
233
|
+
` *) echo "no pinned browser for $TARGETARCH" >&2; exit 1 ;; \\`,
|
|
234
|
+
" esac \\",
|
|
235
|
+
...fetched ? [] : [` && ${parts.stream} ${HEADLESS_SHELL_BASE_URL}/$shell_file /tmp/shell.zip \\`, ` && ${parts.download} ${PLAYWRIGHT_FFMPEG_BASE_URL}/$encoder_file /tmp/encoder.zip \\`],
|
|
236
|
+
' && echo "$shell_sha /tmp/shell.zip" | sha256sum -c - \\',
|
|
237
|
+
' && echo "$encoder_sha /tmp/encoder.zip" | sha256sum -c - \\',
|
|
238
|
+
` && mkdir -p ${browsers} /out${lib} /out${IMAGE_BROWSER.dir}/fonts /out${IMAGE_BROWSER.dir}/conf.d \\`,
|
|
239
|
+
` && unzip -q /tmp/shell.zip -d ${browsers}/chromium_headless_shell-${HEADLESS_SHELL_REVISION} \\`,
|
|
240
|
+
` && unzip -q /tmp/encoder.zip -d ${browsers}/ffmpeg-${PLAYWRIGHT_FFMPEG_REVISION} \\`,
|
|
241
|
+
` && shell=$(echo ${browsers}/chromium_headless_shell-${HEADLESS_SHELL_REVISION}/chrome-headless-shell-linux*/chrome-headless-shell) \\`,
|
|
242
|
+
' && find "$(dirname "$shell")/locales" -type f ! -name en-US.pak -delete \\',
|
|
243
|
+
" && nss=$(dpkg -L libnss3 | grep '\\.so$') \\",
|
|
244
|
+
` && if ldd "$shell" $nss | grep 'not found'; then echo "the browser needs a library this stage does not install" >&2; exit 1; fi \\`,
|
|
245
|
+
` && cp -L $(for file in "$shell" $nss; do ldd "$file" | awk '$3 ~ /^\\// {print $3}'; done | sort -u) $nss /out${lib}/ \\`,
|
|
246
|
+
` && cp -L $(ldd "$shell" | awk '$1 ~ /ld-linux/ {print $1}') /out${lib}/ld.so \\`,
|
|
247
|
+
` && patchelf --set-interpreter ${lib}/ld.so --force-rpath --set-rpath ${lib} "$shell" \\`,
|
|
248
|
+
` && cp /usr/share/fonts/truetype/liberation/*.ttf /usr/share/fonts/truetype/dejavu/*.ttf /usr/share/fonts/truetype/wqy/wqy-zenhei.ttc /usr/share/fonts/truetype/noto/NotoColorEmoji.ttf /out${IMAGE_BROWSER.dir}/fonts/ \\`,
|
|
249
|
+
` && cp -L /etc/fonts/conf.d/*.conf /out${IMAGE_BROWSER.dir}/conf.d/ \\`,
|
|
250
|
+
` && printf '%s\\n' ${fontSettings.map((line) => `'${line}'`).join(" ")} > /out${IMAGE_BROWSER.fontConfig} \\`,
|
|
251
|
+
" && chmod -R a+rX /out"
|
|
252
|
+
];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export { FETCHED_DIR, FETCHED_FFMPEG, FETCHED_GH, FETCHED_HEADLESS_SHELL, FETCHED_PLAYWRIGHT_FFMPEG, FETCHED_TINI, FETCHED_VOICE_DIR, IMAGE_BROWSER, defaultDockerfile, toolsDockerfile };
|
|
@@ -3,7 +3,7 @@ import { createRequire } from 'node:module';
|
|
|
3
3
|
import { readNewestDemo } from './chunk-ER6KQCVJ.js';
|
|
4
4
|
import { runEmitArtifact } from './chunk-QWXBWOVV.js';
|
|
5
5
|
import { VoiceDownloadError, downloadPinned, sha256OfFile } from './chunk-2PGI6RDE.js';
|
|
6
|
-
import { VOICE_PACK_FILES, VOICE_PACK_VERSION } from './chunk-
|
|
6
|
+
import { VOICE_PACK_FILES, VOICE_PACK_VERSION } from './chunk-QUOITPN7.js';
|
|
7
7
|
import { detectActiveSession } from './chunk-IP7BIBXJ.js';
|
|
8
8
|
import { autoConnect } from './chunk-OKGHWCSK.js';
|
|
9
9
|
import { C } from './chunk-ANPNT6PM.js';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
|
-
import { builtinKind, keepsHostBuilder } from './chunk-
|
|
4
|
-
import { parseSize } from './chunk-
|
|
3
|
+
import { builtinKind, keepsHostBuilder } from './chunk-DEJCZ43H.js';
|
|
4
|
+
import { parseSize } from './chunk-GR7ZVGNI.js';
|
|
5
5
|
import { runnerDir } from './chunk-GJ4KBHBD.js';
|
|
6
6
|
import { FD_SLOTS_PER_BUILD, FD_STORE_ENV, BUILD_SLICE_MEMORY_ENV, CLAUDE_CREDENTIAL_ID, CLAUDE_ENV_FILE, credentialLineIn, withoutCredentialLines } from './chunk-SDAEXUIX.js';
|
|
7
7
|
import { existsSync, readFileSync, writeFileSync, chmodSync, renameSync } from 'fs';
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { defaultDockerfile, toolsDockerfile } from './chunk-7GY3DVIL.js';
|
|
4
|
+
import { runnerLabelValue } from './chunk-GR7ZVGNI.js';
|
|
5
5
|
import { RecipeError, locateRecipe, readRecipe, assertBuildable, writeSanitizedRecipe, classifyProperties, SANITIZED_NAME } from './chunk-HDLON3RJ.js';
|
|
6
|
+
import { CLAUDE_CODE_VERSION, NODE_IMAGE, DEFAULT_BASE_IMAGE, MEDIA_PINS, TINI_VERSION, GH_VERSION } from './chunk-QUOITPN7.js';
|
|
6
7
|
import { claimPidFile, RunnerAlreadyRunningError, releasePidFile } from './chunk-LQHCNXPK.js';
|
|
7
8
|
import { startTimeOf } from './chunk-JBJTC3UU.js';
|
|
8
9
|
import { RUNNER_DIR_MODE, RUNNER_FILE_MODE, repoCacheDir, imagesDir, imageBuildLogsDir, attemptStamp, runnerDir, rootfsPathFor } from './chunk-GJ4KBHBD.js';
|
|
9
|
-
import {
|
|
10
|
-
import { RELAY_ID, RELAY_USER } from './chunk-MESOOLF4.js';
|
|
10
|
+
import { withGitConfig, tokenHeader } from './chunk-SDAEXUIX.js';
|
|
11
11
|
import { mkdirSync, rmSync, writeFileSync, existsSync, mkdtempSync, cpSync, readFileSync, readdirSync, lstatSync, readlinkSync } from 'fs';
|
|
12
12
|
import { tmpdir } from 'os';
|
|
13
13
|
import { dirname, join } from 'path';
|
|
@@ -146,170 +146,6 @@ async function cloneRepository(remote, dest, timeoutMs = CLONE_TIMEOUT_MS) {
|
|
|
146
146
|
return { ok: false, reason: `the clone of ${remote} failed: ${detail}` };
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
|
-
|
|
150
|
-
// src/runner/image/dockerfiles.ts
|
|
151
|
-
var FETCHED_DIR = "fetched";
|
|
152
|
-
var FETCHED_VOICE_DIR = "voice";
|
|
153
|
-
var FETCHED_FFMPEG = "ffmpeg.tar.xz";
|
|
154
|
-
var FETCHED_GH = "gh.tgz";
|
|
155
|
-
var FETCHED_TINI = "tini";
|
|
156
|
-
function plain(name, value, allowed) {
|
|
157
|
-
if (!allowed.test(value)) throw new Error(`${name} holds a character a Dockerfile line cannot carry safely: ${JSON.stringify(value)}`);
|
|
158
|
-
return value;
|
|
159
|
-
}
|
|
160
|
-
var DEFAULT_PACKAGES = ["git", "ca-certificates", "curl", "build-essential", "python3", "python3-pip", "python3-venv", "unzip", "jq"];
|
|
161
|
-
function defaultDockerfile(baseImage, nodeImage) {
|
|
162
|
-
return [
|
|
163
|
-
ubuntuWithNode(baseImage, nodeImage, DEFAULT_PACKAGES),
|
|
164
|
-
// The browsers live in one shared folder so any user finds them. A build container's root filesystem is
|
|
165
|
-
// read-only, so the build user cannot add to this folder: a repository that needs another Playwright version
|
|
166
|
-
// installs that version's Chromium into a writable folder by setting PLAYWRIGHT_BROWSERS_PATH.
|
|
167
|
-
// `install --with-deps` runs apt-get as root and works on amd64 and arm64.
|
|
168
|
-
"ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright",
|
|
169
|
-
"RUN mkdir -p /ms-playwright \\",
|
|
170
|
-
` && npx -y playwright@${plain("the Playwright version", PLAYWRIGHT_VERSION, /^[0-9.]+$/)} install --with-deps chromium \\`,
|
|
171
|
-
" && chmod -R a+rX /ms-playwright \\",
|
|
172
|
-
" && rm -rf /var/lib/apt/lists/* /root/.npm",
|
|
173
|
-
""
|
|
174
|
-
].join("\n");
|
|
175
|
-
}
|
|
176
|
-
function ubuntuWithNode(baseImage, nodeImage, packages) {
|
|
177
|
-
return [
|
|
178
|
-
`FROM ${baseImage}`,
|
|
179
|
-
"ENV DEBIAN_FRONTEND=noninteractive",
|
|
180
|
-
"RUN apt-get update \\",
|
|
181
|
-
` && apt-get install -y --no-install-recommends ${packages.join(" ")} \\`,
|
|
182
|
-
" && rm -rf /var/lib/apt/lists/*",
|
|
183
|
-
`COPY --from=${nodeImage} /usr/local/bin/node /usr/local/bin/node`,
|
|
184
|
-
`COPY --from=${nodeImage} /usr/local/lib/node_modules /usr/local/lib/node_modules`,
|
|
185
|
-
"RUN ln -s ../lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \\",
|
|
186
|
-
" && ln -s ../lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx \\",
|
|
187
|
-
" && ln -s ../lib/node_modules/corepack/dist/corepack.js /usr/local/bin/corepack",
|
|
188
|
-
""
|
|
189
|
-
].join("\n");
|
|
190
|
-
}
|
|
191
|
-
function toolsDockerfile(parts) {
|
|
192
|
-
const recipeImage = plain("the recipe image", parts.recipeImage, /^[A-Za-z0-9_.:@/-]+$/);
|
|
193
|
-
const nodeImage = plain("the Node image", parts.nodeImage, /^[A-Za-z0-9_.:@/-]+$/);
|
|
194
|
-
const version = plain("the Claude Code version", parts.claudeCodeVersion, /^[A-Za-z0-9.+-]+$/);
|
|
195
|
-
const buildId = plain("the build id", parts.buildId, /^[A-Za-z0-9_.:+-]+$/);
|
|
196
|
-
const tools = JSON.stringify({ outerlayerBuildId: buildId, claudeCodeVersion: version, ghVersion: GH_VERSION });
|
|
197
|
-
const managedSettings = JSON.stringify({
|
|
198
|
-
hooks: Object.fromEntries(
|
|
199
|
-
["SessionStart", "SessionEnd", "Stop", "PostToolUse"].map((event) => [event, [{ hooks: [{ type: "command", command: `outerlayer hook ${event}` }] }]])
|
|
200
|
-
)
|
|
201
|
-
});
|
|
202
|
-
const download = `NODE_USE_ENV_PROXY=1 node ${PROXY_WARNING_FLAG} -e 'fetch(process.argv[1]).then((r)=>{if(!r.ok)throw new Error(process.argv[1]+" "+r.status);return r.arrayBuffer()}).then((b)=>require("fs").writeFileSync(process.argv[2],Buffer.from(b)))'`;
|
|
203
|
-
const stream = `NODE_USE_ENV_PROXY=1 node ${PROXY_WARNING_FLAG} -e 'const{pipeline}=require("stream/promises"),{Readable}=require("stream"),fs=require("fs");fetch(process.argv[1]).then((r)=>{if(!r.ok)throw new Error(process.argv[1]+" "+r.status);return pipeline(Readable.fromWeb(r.body),fs.createWriteStream(process.argv[2]))})'`;
|
|
204
|
-
const voiceDir = `/out/opt/outerlayer/voice/${VOICE_PACK_VERSION}`;
|
|
205
|
-
const checks = VOICE_PACK_FILES.map((file) => `echo "${file.sha256} ${voiceDir}/${file.name}" | sha256sum -c -`);
|
|
206
|
-
const fetched = parts.fetched === true;
|
|
207
|
-
const voice = fetched ? [`FROM ${nodeImage} AS voice`, `COPY ${FETCHED_DIR}/${FETCHED_VOICE_DIR}/ ${voiceDir}/`, `RUN ${checks.join(" \\\n && ")} \\`, " && chmod -R a+rX /out"] : [
|
|
208
|
-
// The voice pack streams to disk, one pinned file at a time, because a 300 MB model must not sit in a build's memory. Its own stage keeps it cached when the CLI changes.
|
|
209
|
-
`FROM ${nodeImage} AS voice`,
|
|
210
|
-
`RUN mkdir -p ${voiceDir} \\`,
|
|
211
|
-
...VOICE_PACK_FILES.map((file) => ` && ${stream} ${file.url} ${voiceDir}/${file.name} \\`),
|
|
212
|
-
...checks.map((check) => ` && ${check} \\`),
|
|
213
|
-
" && chmod -R a+rX /out"
|
|
214
|
-
];
|
|
215
|
-
return [
|
|
216
|
-
...voice,
|
|
217
|
-
// ffmpeg and ffprobe are static LGPL builds. espeak-ng comes from the stage image's own package, with the libraries and loader it needs, and runs as a separate program.
|
|
218
|
-
`FROM ${nodeImage} AS media`,
|
|
219
|
-
"ARG TARGETARCH",
|
|
220
|
-
"RUN apt-get update \\",
|
|
221
|
-
" && apt-get install -y --no-install-recommends ca-certificates espeak-ng xz-utils \\",
|
|
222
|
-
" && rm -rf /var/lib/apt/lists/*",
|
|
223
|
-
...fetched ? [`COPY ${FETCHED_DIR}/${FETCHED_FFMPEG} /tmp/ffmpeg.tar.xz`] : [],
|
|
224
|
-
"RUN set -eu \\",
|
|
225
|
-
` && case "$TARGETARCH" in \\`,
|
|
226
|
-
` amd64) ffmpeg_file=${FFMPEG_STATIC.amd64.file}; ffmpeg_sha=${FFMPEG_STATIC.amd64.sha256} ;; \\`,
|
|
227
|
-
` arm64) ffmpeg_file=${FFMPEG_STATIC.arm64.file}; ffmpeg_sha=${FFMPEG_STATIC.arm64.sha256} ;; \\`,
|
|
228
|
-
` *) echo "no pinned ffmpeg for $TARGETARCH" >&2; exit 1 ;; \\`,
|
|
229
|
-
" esac \\",
|
|
230
|
-
...fetched ? [] : [` && ${download} https://github.com/BtbN/FFmpeg-Builds/releases/download/${FFMPEG_BUILD_TAG}/$ffmpeg_file /tmp/ffmpeg.tar.xz \\`],
|
|
231
|
-
' && echo "$ffmpeg_sha /tmp/ffmpeg.tar.xz" | sha256sum -c - \\',
|
|
232
|
-
" && tar -xJf /tmp/ffmpeg.tar.xz -C /tmp \\",
|
|
233
|
-
" && mkdir -p /out/usr/local/bin /out/opt/outerlayer/media/lib \\",
|
|
234
|
-
" && install -m 0755 /tmp/ffmpeg-*/bin/ffmpeg /tmp/ffmpeg-*/bin/ffprobe /out/usr/local/bin/ \\",
|
|
235
|
-
" && install -m 0755 /usr/bin/espeak-ng /out/opt/outerlayer/media/espeak-ng \\",
|
|
236
|
-
` && for lib in $(ldd /usr/bin/espeak-ng | awk '$3 ~ /^\\// {print $3}'); do cp -L "$lib" /out/opt/outerlayer/media/lib/; done \\`,
|
|
237
|
-
" && cp -L $(ldd /usr/bin/espeak-ng | awk '$1 ~ /ld-linux/ {print $1}') /out/opt/outerlayer/media/lib/ld.so \\",
|
|
238
|
-
" && cp -r /usr/lib/*/espeak-ng-data /out/opt/outerlayer/media/espeak-ng-data \\",
|
|
239
|
-
` && printf '%s\\n' '#!/bin/sh' 'ESPEAK_DATA_PATH=/opt/outerlayer/media' 'export ESPEAK_DATA_PATH' 'exec /opt/outerlayer/media/lib/ld.so --library-path /opt/outerlayer/media/lib /opt/outerlayer/media/espeak-ng "$@"' > /out/usr/local/bin/espeak-ng \\`,
|
|
240
|
-
" && chmod 0755 /out/usr/local/bin/espeak-ng \\",
|
|
241
|
-
" && chmod -R a+rX /out",
|
|
242
|
-
`FROM ${recipeImage} AS recipe`,
|
|
243
|
-
`FROM ${nodeImage} AS stage`,
|
|
244
|
-
"ARG TARGETARCH",
|
|
245
|
-
`RUN npm install --global --prefix /out/opt/outerlayer/claude @anthropic-ai/claude-code@${version}`,
|
|
246
|
-
"COPY dist /out/opt/outerlayer/cli/dist",
|
|
247
|
-
"COPY package.json /out/opt/outerlayer/cli/package.json",
|
|
248
|
-
// `outerlayer demo record` runs a demo on the Playwright release this CLI depends on, found beside the CLI as an npm
|
|
249
|
-
// install puts it. Only the library, with no install scripts: the layer adds no browser to a repository's own image.
|
|
250
|
-
"RUN mkdir -p /tmp/cli-deps && cd /tmp/cli-deps && echo '{}' > package.json \\",
|
|
251
|
-
` && npm install --no-audit --no-fund --ignore-scripts @playwright/test@${plain("the Playwright version", PLAYWRIGHT_VERSION, /^[0-9.]+$/)} \\`,
|
|
252
|
-
" && mv node_modules /out/opt/outerlayer/cli/node_modules",
|
|
253
|
-
// `outerlayer claude` installs the maintained skills from this pack into the build's home folder before the agent starts.
|
|
254
|
-
"COPY skill-pack /out/opt/outerlayer/cli/skill-pack",
|
|
255
|
-
"RUN mkdir -p /out/opt/outerlayer/bin /out/usr/local/bin /out/etc/claude-code /out/build \\",
|
|
256
|
-
" && cp /usr/local/bin/node /out/opt/outerlayer/bin/node \\",
|
|
257
|
-
` && printf '%s\\n' '#!/bin/sh' 'exec /opt/outerlayer/bin/node /opt/outerlayer/cli/dist/index.js "$@"' > /out/usr/local/bin/outerlayer \\`,
|
|
258
|
-
` && printf '%s\\n' '#!/bin/sh' 'PATH=/opt/outerlayer/bin:$PATH' 'export PATH' 'exec /opt/outerlayer/claude/bin/claude "$@"' > /out/usr/local/bin/claude \\`,
|
|
259
|
-
` && printf '%s\\n' '${tools}' > /out/opt/outerlayer/tools.json \\`,
|
|
260
|
-
` && printf '%s\\n' '${managedSettings}' > /out/etc/claude-code/managed-settings.json \\`,
|
|
261
|
-
" && chmod 0755 /out/usr/local/bin/outerlayer /out/usr/local/bin/claude \\",
|
|
262
|
-
// Every build, whatever user runs it, writes here: the first mount of an empty volume copies this mode.
|
|
263
|
-
" && chmod 1777 /out/build",
|
|
264
|
-
...parts.guestHelper === true ? [
|
|
265
|
-
"COPY guest/outerlayer-guest /out/opt/outerlayer/bin/outerlayer-guest",
|
|
266
|
-
// The guest mounts the build's tmpfs, the kernel's file systems and the run directory over these. The judge's tmpfs is mounted over its directory when the judge runs.
|
|
267
|
-
"RUN chmod 0755 /out/opt/outerlayer/bin/outerlayer-guest \\",
|
|
268
|
-
" && mkdir -p /out/outerlayer/job /out/outerlayer/judge /out/run /out/proc /out/sys /out/dev"
|
|
269
|
-
] : [],
|
|
270
|
-
...fetched ? [`COPY ${FETCHED_DIR}/${FETCHED_TINI} /tmp/tini`, `COPY ${FETCHED_DIR}/${FETCHED_GH} /tmp/gh.tgz`] : [],
|
|
271
|
-
`RUN set -eu \\`,
|
|
272
|
-
` && case "$TARGETARCH" in \\`,
|
|
273
|
-
` amd64) gh_sha=${GH_SHA256.amd64}; tini_sha=${TINI_SHA256.amd64} ;; \\`,
|
|
274
|
-
` arm64) gh_sha=${GH_SHA256.arm64}; tini_sha=${TINI_SHA256.arm64} ;; \\`,
|
|
275
|
-
` *) echo "no pinned gh and tini for $TARGETARCH" >&2; exit 1 ;; \\`,
|
|
276
|
-
" esac \\",
|
|
277
|
-
...fetched ? [] : [` && ${download} https://github.com/krallin/tini/releases/download/v${TINI_VERSION}/tini-static-$TARGETARCH /tmp/tini \\`],
|
|
278
|
-
' && echo "$tini_sha /tmp/tini" | sha256sum -c - \\',
|
|
279
|
-
" && install -m 0755 /tmp/tini /out/opt/outerlayer/bin/tini \\",
|
|
280
|
-
...fetched ? [] : [` && ${download} https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_$TARGETARCH.tar.gz /tmp/gh.tgz \\`],
|
|
281
|
-
' && echo "$gh_sha /tmp/gh.tgz" | sha256sum -c - \\',
|
|
282
|
-
" && tar -xzf /tmp/gh.tgz -C /tmp \\",
|
|
283
|
-
` && install -m 0755 /tmp/gh_${GH_VERSION}_linux_$TARGETARCH/bin/gh /out/usr/local/bin/gh`,
|
|
284
|
-
"COPY --from=recipe /etc/passwd /tmp/recipe-passwd",
|
|
285
|
-
"COPY --from=recipe /etc/group /tmp/recipe-group",
|
|
286
|
-
`RUN uid=$(awk -F: 'BEGIN{m=1499} $3>=1500 && $3<60000 && $3>m{m=$3} END{print m+1}' /tmp/recipe-passwd) \\`,
|
|
287
|
-
` && gid=$(awk -F: 'BEGIN{m=1499} $3>=1500 && $3<60000 && $3>m{m=$3} END{print m+1}' /tmp/recipe-group) \\`,
|
|
288
|
-
" && { grep -v -e '^ol-agent:' -e '^ol-relay:' /tmp/recipe-passwd > /out/etc/passwd || true; } \\",
|
|
289
|
-
" && { grep -v -e '^ol-agent:' -e '^ol-relay:' /tmp/recipe-group > /out/etc/group || true; } \\",
|
|
290
|
-
// The relay's id is fixed because the runner names it in the container's `--tmpfs` options before the container exists.
|
|
291
|
-
` && if awk -F: '$3==${RELAY_ID}{found=1} END{exit !found}' /out/etc/passwd; then echo "uid ${RELAY_ID} is taken by a user of the recipe image, and the relay needs it" >&2; exit 1; fi \\`,
|
|
292
|
-
` && if awk -F: '$3==${RELAY_ID}{found=1} END{exit !found}' /out/etc/group; then echo "gid ${RELAY_ID} is taken by a group of the recipe image, and the relay needs it" >&2; exit 1; fi \\`,
|
|
293
|
-
` && printf '%s\\n' "ol-agent:x:$uid:$gid::/build/home:/bin/sh" >> /out/etc/passwd \\`,
|
|
294
|
-
` && printf '%s\\n' "ol-agent:x:$gid:" >> /out/etc/group \\`,
|
|
295
|
-
` && printf '%s\\n' "${RELAY_USER}:x:${RELAY_ID}:${RELAY_ID}::/nonexistent:/usr/sbin/nologin" >> /out/etc/passwd \\`,
|
|
296
|
-
` && printf '%s\\n' "${RELAY_USER}:x:${RELAY_ID}:" >> /out/etc/group \\`,
|
|
297
|
-
" && chmod 0644 /out/etc/passwd /out/etc/group \\",
|
|
298
|
-
" && chmod -R a+rX /out",
|
|
299
|
-
`FROM ${recipeImage}`,
|
|
300
|
-
"COPY --from=stage /out/ /",
|
|
301
|
-
"COPY --from=voice /out/ /",
|
|
302
|
-
"COPY --from=media /out/ /",
|
|
303
|
-
"ENV DISABLE_AUTOUPDATER=1",
|
|
304
|
-
// espeak-ng's audio library makes a private pulse-* folder for the user who runs it, in the temp folder unless XDG_RUNTIME_DIR names another.
|
|
305
|
-
// Made here, as root, it would ship in the image where the build user cannot read it, and the runner's disk check fails on it.
|
|
306
|
-
"RUN check=$(mktemp -d /tmp/ol-check.XXXXXX) \\",
|
|
307
|
-
" && outerlayer --version && claude --version && gh --version && ffmpeg -version && ffprobe -version \\",
|
|
308
|
-
' && XDG_RUNTIME_DIR="$check" espeak-ng --version \\',
|
|
309
|
-
' && rm -rf "$check"',
|
|
310
|
-
""
|
|
311
|
-
].join("\n");
|
|
312
|
-
}
|
|
313
149
|
function sha256(...chunks) {
|
|
314
150
|
const hash = createHash("sha256");
|
|
315
151
|
for (const chunk of chunks) hash.update(chunk);
|
|
@@ -612,4 +448,4 @@ async function prune(host, repository, repoRecords, keep, runnerDirPath, runnerL
|
|
|
612
448
|
return removed;
|
|
613
449
|
}
|
|
614
450
|
|
|
615
|
-
export {
|
|
451
|
+
export { KEY_PREFIX, TAG_HOST, buildRepositoryImage, cloneRepository, diffBetween, fetchCommit, metadataFromLabels, prepareImageBuild, readBranchHead, readDefaultBranchHead, writeRecord };
|