@wenathlan/saddle 2.0.155 → 2.0.160

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/Dockerfile CHANGED
@@ -1,699 +1,88 @@
1
- # syntax=docker/dockerfile:1
2
- # saddle v2.0.0 grand merge — THE ONE CONTAINER FILE.
3
- #
4
- # build context: this file COPYs from two trees (saddle/ and the
5
- # engine-ownership sibling vault/), so every build runs from the
6
- # repository root with an explicit -f: docker build -f saddle/Dockerfile .
7
- # (add --target saddle-runtime for the node service or --target
8
- # qemu-runtime for the guest runner). building from inside saddle/
9
- # breaks the vault COPY - the sibling rides the context beside this
10
- # directory, never inside it.
11
- #
12
- # this Dockerfile carries BOTH container surfaces of the merged repository:
13
- # 1. the saddle virtual-hardware engine image (the five stages below:
14
- # builder, python-bridge, qemu, qemu-runtime, runtime) — the default
15
- # build target, published as ghcr.io/wenathlan/saddle:vhe-<version>
16
- # across three profiles and eleven architectures;
17
- # 2. the saddle node-engine image (the saddle-build and saddle-runtime
18
- # stages at the end of this file, folded from the former
19
- # dockerfile.saddle) — an explicit build target
20
- # (`docker build --target saddle-runtime .`), the node 26 storage and
21
- # compute service published as ghcr.io/wenathlan/saddle:<version>
22
- # across the eleven-arch family standard (amd64, amd64/v2, arm64,
23
- # arm/v7, arm/v6, ppc64le, s390x, riscv64, 386, mips64le, loong64
24
- # — the trixie slim base line; the stage also builds the web SPA so
25
- # the image serves the self-hosted console via `node /app/webserver.ts`).
26
- #
27
- # saddle v2 (grand merge) - virtual hardware engine, multi-stage, multi-arch image.
28
- #
29
- # the five stages mirror the stages section of docker.config
30
- # (multi-stage-cache-optimized strategy, parallel-friendly):
31
- # builder toolchain stage on ubuntu:24.04. installs LLVM 18
32
- # (from the noble archive, not apt.llvm.org which is
33
- # unreachable from GHA runners), builds mesa 26.2.1
34
- # (llvmpipe + lavapipe; rusticl is off until
35
- # LLVMSPIRVLib >= 22.1 is packaged), installs node
36
- # 26.7.0 from nodesource, and compiles the native sources
37
- # DIRECTLY with gcc-14/g++-14 (no cmake anywhere: the libs
38
- # are single translation units documented in their own
39
- # headers): libs/virtualhardware.c (LD_PRELOAD spoofing
40
- # core), libs/gpumonitor.cpp (nvml/cuda shim suite in
41
- # four build modes) and libs/virtualizationcore.cpp
42
- # (kvm/vfio/qmp core with an executable selftest), all
43
- # with -O3 -flto. it also bakes the /etc/virtual hardware
44
- # profile per the VHE_PROFILE_* build-args (max: EPYC 9965
45
- # 192c/384t 1TiB, balanced: Threadripper Pro 9995WX
46
- # 96c/192t 512GiB, lite: Ryzen 9 9950X3D 16c/32t 128GiB).
47
- # python-bridge python:3.14.7-slim carrying qemubridge.py. the bridge is
48
- # stdlib-only (asyncio/json/argparse), so the stage runs a
49
- # py_compile syntax gate and no pip install at all.
50
- # qemu QEMU 11.1.0 compiled from download.qemu.org with the
51
- # x86_64-softmmu target for TCG/MTTCG guest execution;
52
- # build arg QEMU_FROM_SOURCE=0 falls back to the distro
53
- # qemu-system-x86 package when a source build is not
54
- # viable on the host.
55
- # qemu-runtime the qemu stage plus OVMF firmware, pciutils, kmod and
56
- # tini (the docker.config "qemuRuntime" role: the guest
57
- # runner consumed by the vheqemu docker run recipe).
58
- # runtime ubuntu:24.04 final image: mesa 26.2.1 stack, node
59
- # 26.7.0, Xvfb, vulkan-tools, clinfo, mesa-utils,
60
- # python3, the compiled spoofing artifacts, the python
61
- # bridge and the engine sources under /engine. default
62
- # build target (docker build / the engine docker run
63
- # recipe below).
64
- #
65
- # platforms: linux/amd64, linux/amd64/v2, linux/arm64, linux/arm/v7,
66
- # linux/arm/v6, linux/ppc64le, linux/s390x, linux/riscv64, linux/386,
67
- # linux/mips64le and linux/loong64 through buildx --platform (the
68
- # eleven-arch registry surface — the complete linux platform list
69
- # docker/buildx carries, the owner's law: eleven OS architectures for
70
- # every container of every application); every
71
- # stage compiles for the portable baseline of its target architecture
72
- # by default (x86-64 / i686 / armv8-a / armv7-a / armv6 / power8 / z196 /
73
- # rv64gc / mips64r2 / loongarch64, resolved from
74
- # TARGETARCH and TARGETVARIANT) so the layers stay valid on every host
75
- # and every gha-cached runner cpu; pass --build-arg VHE_MARCH=native for
76
- # host-tuned local builds, or an explicit -march value when
77
- # cross-building for a known deployment target.
78
- #
79
- # profiles: the three profiles (max, balanced, lite) are all built for
80
- # all eleven architectures; each profile carries a distinct virtual CPU
81
- # identity (model name, vcpus, threads, ram) baked into /etc/virtual.
82
- #
83
- # the one container file (the gateway 1.1.5 standard): the compose stack
84
- # and the entrypoint bootstrap are MERGED INTO this file -
85
- # docker-compose.yml and entrypoint.sh are deleted from the tree; every
86
- # orchestration setting they carried is expressed here (the ENV defaults,
87
- # EXPOSE, VOLUME, HEALTHCHECK, USER, the OCI labels, the entrypoint script
88
- # embedded as a heredoc COPY) plus the hardened docker run recipes below
89
- # documenting every flag of the former services (init reaps zombies as
90
- # pid 1, cap-drop ALL + no-new-privileges harden the container, unlimited
91
- # swap, 2g shm, 15s stop grace, 50m x 5 json-file logs, restart
92
- # unless-stopped, the tmpfs scratch and the named volumes). Containerfile
93
- # is the same format under the OCI name - Dockerfile is the universally
94
- # compatible spelling, so it is the one file the repository keeps.
95
- #
96
- # the docker run recipes (one per former compose service; pick any free
97
- # host port in 30000-60000 - nothing below is a fixed address):
98
- #
99
- # vhe (the engine service, default target):
100
- # docker run -d --name saddle-engine \
101
- # --restart unless-stopped --init \
102
- # --cap-drop ALL --security-opt no-new-privileges:true \
103
- # --ulimit nofile=65536:65536 \
104
- # --memory-swap -1 --shm-size 2g --stop-timeout 15 \
105
- # --log-driver json-file --log-opt max-size=50m --log-opt max-file=5 \
106
- # --tmpfs /tmp:size=512m,mode=1777 --tmpfs /run/vhe:size=64m,mode=0755 \
107
- # --tmpfs /cache/mesa_shader_cache:size=20g,uid=10000,gid=10000,mode=1777 \
108
- # -v vmdata:/data/vmdata -v webdata:/data/web \
109
- # -v ./vm.config.json:/engine/vm.config.json:ro \
110
- # -e PORT=8080 -e NODE_ENV=production -p 31280:8080 \
111
- # ghcr.io/wenathlan/saddle:vhe-2.0.154
112
- #
113
- # vheqemu (the guest runner; build it first with
114
- # docker build --target qemu-runtime -t saddle/qemu:11.1.0 . because the
115
- # published registry image is the runtime target):
116
- # docker run -d --name saddle-qemu \
117
- # --restart unless-stopped --init \
118
- # --security-opt no-new-privileges:true \
119
- # --ulimit nofile=65536:65536 \
120
- # --memory-swap -1 --shm-size 2g --stop-timeout 15 \
121
- # --log-driver json-file --log-opt max-size=50m --log-opt max-file=5 \
122
- # -v ./qemu:/qemu:ro -v qemu-kernel:/opt/kernels:ro \
123
- # -v qemudata:/data/qemudata -v ovmfvars:/data/ovmfvars \
124
- # -v ./qemu.config:/engine/qemu.config:ro \
125
- # -p 31281:31281 -it \
126
- # saddle/qemu:11.1.0 \
127
- # -accel tcg,thread=multi,tb-size=1024 -cpu EPYC-v5 \
128
- # -smp 192,sockets=1,cores=192,threads=2,maxcpus=384 \
129
- # -m 131072 -machine q35 -nodefaults -no-reboot -nographic \
130
- # -serial mon:stdio -monitor none \
131
- # -kernel /qemu/vmlinuz -initrd /qemu/initrd.img \
132
- # -append "console=ttyS0 panic=-1"
133
- #
134
- # vhegpu (the fake gpu sidecar, same engine image):
135
- # docker run -d --name saddle-gpu \
136
- # --restart unless-stopped --init \
137
- # --cap-drop ALL --security-opt no-new-privileges:true \
138
- # --ulimit nofile=65536:65536 \
139
- # --memory-swap -1 --shm-size 2g --stop-timeout 15 \
140
- # --log-driver json-file --log-opt max-size=50m --log-opt max-file=5 \
141
- # -v gpudata:/data/gpudata -v ./gpus.json:/engine/gpus.json:ro \
142
- # -e VHE_GPU_PROFILE=b200 -e VHE_GPUS=8 -e VHE_MIG=1 \
143
- # -e VHE_SMI_DRIVER=575.57.08 -e VHE_SMI_CUDA=12.9 \
144
- # -e VHE_SMI_INTERVAL=30 -e VHE_SKIP_XVFB=1 -e VHE_SKIP_VALIDATE=1 \
145
- # ghcr.io/wenathlan/saddle:vhe-2.0.154 /bin/bash -c '
146
- # while true; do
147
- # /usr/local/bin/nvidia-smi "$VHE_GPU_PROFILE" "$VHE_GPUS" || true
148
- # sleep "$VHE_SMI_INTERVAL"
149
- # done
150
- # '
151
- #
152
- # qemubridge (the python QMP bridge, same engine image):
153
- # docker run -d --name saddle-bridge \
154
- # --restart unless-stopped --init \
155
- # --security-opt no-new-privileges:true \
156
- # --ulimit nofile=65536:65536 \
157
- # --memory-swap -1 --shm-size 2g --stop-timeout 15 \
158
- # --log-driver json-file --log-opt max-size=50m --log-opt max-file=5 \
159
- # -v qemudata:/data/qemudata \
160
- # -v ./qemubridge.py:/engine/qemubridge.py:ro \
161
- # -e QMP_SOCKET=/run/vhe/vm.qmp -e PYTHONUNBUFFERED=1 \
162
- # ghcr.io/wenathlan/saddle:vhe-2.0.154 \
163
- # python3 /engine/qemubridge.py --socket /run/vhe/vm.qmp status
164
- #
165
- # saddle-node (the node-engine service, the former compose.yml
166
- # service folded here; build it with
167
- # docker build --target saddle-runtime -t saddle:<version> .):
168
- # docker run -d --name saddle-node \
169
- # --restart unless-stopped --init --read-only \
170
- # --cap-drop ALL --security-opt no-new-privileges:true \
171
- # --pids-limit 512 --network none \
172
- # --ulimit nofile=65536:65536 \
173
- # --log-driver json-file --log-opt max-size=50m --log-opt max-file=5 \
174
- # --tmpfs /tmp:size=2g,mode=1777 \
175
- # -e SADDLE_MEMORY_ENGINE=ram -e SBOT_PLATFORM= -e SBOT_CDN_URL= \
176
- # ghcr.io/wenathlan/saddle:2.0.154 \
177
- # node dist/cli.js plan
178
- #
179
- # observability (the former prometheus scraper of the full profile)
180
- # runs from its own upstream image (prom/prometheus:v3.4.1) with
181
- # --config.file and a 15d retention - the one container file this
182
- # repository keeps covers only what it builds.
183
- #
184
- # no port is published by the image itself; the recipes above publish
185
- # host 31280 -> container 8080 for the engine and 31281 for the qemu
186
- # guest runner, and a bare `docker run` without -e PORT leaves the
187
- # embedded entrypoint picking a random port in 30000-60000 (the same
188
- # range the recipes draw their host ports from). nothing in the stack
189
- # ever binds a hardcoded address.
190
-
191
- # Base image contract (the gateway family policy): tags only, never a
192
- # @sha256: digest. A single digest cannot serve the eleven-architecture
193
- # matrix this image ships (the registry resolves the tag per platform),
194
- # and a digest written here would be a hardcoded hash - the checksums
195
- # that exist are generated by the GitHub runners and published as
196
- # release assets (SHA256SUMS), never written into the sources.
197
- ARG UBUNTU="ubuntu:24.04"
198
- # maximum virtual hardware profile baked at build time (workflow override
199
- # friendly): the top catalog cpu (EPYC 9965, 192 cores / 384 threads), the
200
- # catalog maximum memory plan (1024 GiB), the top gpu profile (B200) and
201
- # eight virtual adapters. every value is spoofed software identity: no
202
- # physical hardware is touched, probed or required.
203
- ARG VHE_PROFILE_CPU="epyc9965"
204
- ARG VHE_PROFILE_VCPUS="192"
205
- ARG VHE_PROFILE_THREADS="384"
206
- ARG VHE_PROFILE_RAM_GB="1024"
207
- ARG VHE_PROFILE_GPU="b200"
208
- ARG VHE_PROFILE_GPUS="8"
209
- ARG PYTHON_BASE="python:3.14.7-slim"
210
- ARG LLVM_VERSION="18"
211
- ARG NODE_VERSION="26.7.0"
212
- ARG MESA_VERSION="26.2.1"
213
- ARG QEMU_VERSION="11.1.0"
214
-
215
- # ---------------------------------------------------------------------------
216
- # stage 1: builder
217
- # ---------------------------------------------------------------------------
218
- FROM ${UBUNTU} AS builder
219
- ARG TARGETPLATFORM
220
- ARG TARGETARCH
221
- ARG TARGETVARIANT
222
- ARG BUILDARCH
223
- ARG LLVM_VERSION
224
- ARG NODE_VERSION
225
- ARG MESA_VERSION
226
- # empty = portable baseline resolved from TARGETARCH (and TARGETVARIANT
227
- # for the 32-bit arm legs) at the compile step (x86-64 / i686 / armv8-a /
228
- # armv7-a / armv6 / power8 / z196 / rv64gc / mips64r2 / loongarch64);
229
- # "native" is the opt-in for host-tuned local builds.
230
- ARG VHE_MARCH=""
231
- # NOTE: the VHE_PROFILE_* build-args are deliberately NOT declared in the
232
- # builder stage. The builder compiles mesa/node/native shims which are
233
- # profile-independent; keeping the profile args out of the builder means
234
- # the buildx registry cache is shared across max/balanced/lite for each
235
- # architecture (the profile identity is baked in the runtime stage below).
236
-
237
- ENV DEBIAN_FRONTEND=noninteractive
238
-
239
- # toolchain: gcc-14 (speaks c++26 for the shim suite) and the LLVM 18
240
- # toolchain from the Ubuntu noble archive (which mesa 26.2.1 requires
241
- # '>= 18.0.0'). The previous build pulled LLVM 22 from apt.llvm.org but
242
- # that host is unreachable from GitHub Actions runners (curl exit 7,
243
- # 8 retries all failed in 0 ms); the Ubuntu distro LLVM 18.1.x is served
244
- # from archive.ubuntu.com which the runner can always reach. Rusticl is
245
- # disabled (see the mesa block below) so no clang/libclc/libllvmspirvlib
246
- # are needed; the native sources compile with g++-14.
247
- RUN set -eux; \
248
- # the Ubuntu arm64/ppc64el/s390x ports mirror (ports.ubuntu.com)
249
- # intermittently serves either a Packages.gz whose size differs from
250
- # the Release file while it is mid-sync ("File has unexpected size
251
- # ... Mirror sync in progress?") OR a Release index that references
252
- # .deb files already removed from the pool directory (404 Not Found
253
- # on apt-get install). Retrying just apt-get update is not enough:
254
- # the update can succeed on a stale index whose pool files are gone.
255
- # The retry must cover BOTH apt-get update AND apt-get install so a
256
- # 404 on install re-fetches a fresh index that references the new
257
- # package versions. The stale list cache is cleared between retries.
258
- apt_update_tries=5; \
259
- while [ "$apt_update_tries" -gt 0 ]; do \
260
- if apt-get update && apt-get install -y --no-install-recommends \
261
- ca-certificates curl gnupg xz-utils bzip2 git \
262
- python3 python3-pip python3-setuptools python3-wheel \
263
- python3-mako python3-yaml python3-ply \
264
- pkg-config bison flex ninja-build meson \
265
- build-essential gcc-14 g++-14 \
266
- "llvm-${LLVM_VERSION}-dev" "llvm-${LLVM_VERSION}-tools" \
267
- libdrm-dev libexpat1-dev zlib1g-dev libzstd-dev \
268
- libx11-dev libxext-dev libxfixes-dev libxxf86vm-dev libxrandr-dev \
269
- libxcb1-dev libxcb-glx0-dev libxcb-dri2-0-dev libxcb-dri3-dev \
270
- libxcb-present-dev libxcb-sync-dev libxcb-shm0-dev \
271
- libxcb-randr0-dev libxcb-xfixes0-dev libx11-xcb-dev libxshmfence-dev \
272
- libelf-dev libedit-dev libglvnd-dev \
273
- glslang-tools spirv-tools; then break; fi; \
274
- apt_update_tries=$((apt_update_tries - 1)); \
275
- echo "apt-get update/install failed (mirror sync?), $apt_update_tries retries left"; \
276
- sleep 10; \
277
- rm -rf /var/lib/apt/lists/*; \
278
- done; \
279
- test "$apt_update_tries" -gt 0; \
280
- ln -sf "/usr/bin/llvm-config-${LLVM_VERSION}" /usr/local/bin/llvm-config; \
281
- ln -sf /usr/bin/gcc-14 /usr/local/bin/gcc; \
282
- ln -sf /usr/bin/g++-14 /usr/local/bin/g++; \
283
- # mesa 26.2.1 requires meson >= 1.4.0 (mesa.build:9); Ubuntu 24.04 noble
284
- # ships meson 1.3.2 which is too old. meson installs from its exact
285
- # version-pinned wheel URL: the artifact identity is the 1.12.0 release
286
- # path on files.pythonhosted.org (a registry URL, the same class of
287
- # contract npm ci uses through the lockfile). No hardcoded digest in
288
- # this file: the maintainer supply-chain policy for this family is the
289
- # version-tagged artifact (the Scorecard PinnedDependencies findings
290
- # are dismissed per that policy) and the checksums that exist are
291
- # generated on the GitHub runners at release time (see the SHA256SUMS
292
- # asset of the Release workflow), never written into the sources.
293
- curl -fsSL --retry 5 --retry-all-errors \
294
- -o /tmp/meson-1.12.0-py3-none-any.whl \
295
- "https://files.pythonhosted.org/packages/07/68/b0117422eb0a46d9d8d9e328f0c5b5c835179bfc058688bca35c90c89eba/meson-1.12.0-py3-none-any.whl"; \
296
- pip3 install --break-system-packages --no-cache-dir \
297
- /tmp/meson-1.12.0-py3-none-any.whl; \
298
- rm -f /tmp/meson-1.12.0-py3-none-any.whl; \
299
- meson --version
300
-
301
- # NOTE: the rust toolchain step was removed because rusticl is disabled
302
- # (LLVMSPIRVLib >= 22.1 not packaged for LLVM 22; see the mesa block below).
303
- # Rusticl was the only mesa component that needed rustc + bindgen-cli; with
304
- # it off, the build is rust-free.
305
-
306
- # node 26.7.0 (current line, v8 14.6, temporal api, NODE_MODULE_VERSION 147).
307
- # amd64 (with its amd64/v2 variant) and arm64 install the pinned nodesource
308
- # deb; every architecture the nodesource apt repo does not carry (ppc64le,
309
- # s390x, riscv64, loong64, mips64le, arm and 386 - the eleven-arch law's
310
- # non-nodesource legs) falls back to the official nodejs.org binary tarball
311
- # (mapped to the nodejs.org arch spellings: x64, armv7l, mips64el, x86),
312
- # extracted straight into /usr so bin/node, include/node and
313
- # lib/node_modules land exactly where the nodesource package (and the runtime
314
- # stage COPY --from=builder directives) expect them. the node headers
315
- # (/usr/include/node) are required by the ADDON build mode of gpumonitor.cpp.
316
- RUN set -eux; \
317
- nodesrc_ok="1"; \
318
- case "${TARGETARCH}" in ppc64le|s390x|riscv64|loong64|loongarch64|mips64le|arm|386) nodesrc_ok="0" ;; esac; \
319
- if [ "$nodesrc_ok" = "1" ]; then \
320
- # the version-line setup script (setup_26.x) is the nodesource
321
- # distribution contract: no hardcoded digest in this file. The
322
- # script tracks the whole 26.x line (a frozen hash would
323
- # false-negative on every upstream refresh), the apt repository
324
- # it configures carries its own release signing, and the exact
325
- # node build is pinned by the "nodejs=${NODE_VERSION}-1nodesource1"
326
- # package below. Checksums for released artifacts are generated on
327
- # the GitHub runners (SHA256SUMS release asset), never hardcoded.
328
- curl -fsSL -o /tmp/nodesource-setup.sh https://deb.nodesource.com/setup_26.x; \
329
- bash /tmp/nodesource-setup.sh; \
330
- rm -f /tmp/nodesource-setup.sh; \
331
- apt-get install -y --no-install-recommends \
332
- "nodejs=${NODE_VERSION}-1nodesource1" \
333
- || apt-get install -y --no-install-recommends nodejs; \
334
- else \
335
- case "${TARGETARCH}" in \
336
- amd64) nodearch="x64" ;; \
337
- 386|i386) nodearch="x86" ;; \
338
- arm) nodearch="armv7l" ;; \
339
- mips64le) nodearch="mips64el" ;; \
340
- *) nodearch="${TARGETARCH}" ;; \
341
- esac; \
342
- tarball="node-v${NODE_VERSION}-linux-${nodearch}.tar.xz"; \
343
- curl -fsSL --retry 5 --retry-all-errors -o "/tmp/${tarball}" \
344
- "https://nodejs.org/dist/v${NODE_VERSION}/${tarball}"; \
345
- tar -xJf "/tmp/${tarball}" -C /usr --strip-components=1; \
346
- rm -f "/tmp/${tarball}"; \
347
- fi; \
348
- node --version; npm --version; \
349
- # upgrade the bundled npm to the line the engines field documents
350
- # (nodesource node 26.7.0 ships npm 11.19.0) and surgically refresh
351
- # the four nested dependencies whose bundled versions carry the CVEs
352
- # the trivy release gate rejects (brace-expansion CVE-2026-14257 /
353
- # CVE-2026-69152 / CVE-2026-102276 / CVE-2026-102278, ip-address
354
- # CVE-2026-69192 / CVE-2026-54272 / CVE-2026-69198, tar
355
- # CVE-2026-73566, undici CVE-2026-16728 / CVE-2026-16729 /
356
- # CVE-2026-15157 / CVE-2026-109383 / CVE-2026-109869 /
357
- # CVE-2026-19534 / CVE-2026-87018). published npm tarballs freeze
358
- # their bundled node_modules at publish time, so the swap alone
359
- # still ships brace-expansion 5.0.7; each fixed release is packed
360
- # separately and extracted over the nested path, then asserted: the
361
- # runtime stage copies /usr/lib/node_modules verbatim and the gate
362
- # scans exactly that path.
363
- newnpm=$(npm pack "npm@12.0.2" --pack-destination /tmp 2>/dev/null | tail -n1); \
364
- case "${newnpm}" in /*) ;; *) newnpm="/tmp/${newnpm}";; esac; \
365
- test -f "${newnpm}"; \
366
- rm -rf /usr/lib/node_modules/npm; \
367
- mkdir -p /usr/lib/node_modules/npm; \
368
- tar -xzf "${newnpm}" -C /usr/lib/node_modules/npm --strip-components=1; \
369
- rm -f "${newnpm}"; \
370
- hash -r; \
371
- npm --version; \
372
- test -f /usr/include/node/node_api.h; \
373
- fixdep() { \
374
- fname="$1"; fwant="$2"; \
375
- ftgz=$(npm pack "$fname@$fwant" --pack-destination /tmp 2>/dev/null | tail -n1); \
376
- case "$ftgz" in /*) ;; *) ftgz="/tmp/$ftgz";; esac; \
377
- test -f "$ftgz"; \
378
- fdest="/usr/lib/node_modules/npm/node_modules/$fname"; \
379
- rm -rf "$fdest"; \
380
- mkdir -p "$fdest"; \
381
- tar -xzf "$ftgz" -C "$fdest" --strip-components=1; \
382
- rm -f "$ftgz"; \
383
- fgot=$(node -p "require('/usr/lib/node_modules/npm/node_modules/$fname/package.json').version"); \
384
- if [ "$fgot" != "$fwant" ]; then \
385
- echo "npm tree $fname is $fgot, expected $fwant" >&2; \
386
- exit 1; \
387
- fi; \
388
- echo "npm tree $fname $fgot"; \
389
- }; \
390
- fixdep brace-expansion 5.0.11; \
391
- fixdep ip-address 10.3.1; \
392
- fixdep tar 7.5.21; \
393
- fixdep undici 6.29.1; \
394
- rm -rf /root/.npm 2>/dev/null || true
395
-
396
- # mesa 26.2.1 from source (the "build" arm of the PPA-or-build policy):
397
- # llvmpipe (OpenGL 4.6 core, 161/161 extensions), lavapipe (Vulkan 1.4
398
- # software ICD, lvp_icd.<arch>.json). Rusticl (OpenCL 3.1) is DISABLED
399
- # because mesa 26.2.1 requires LLVMSPIRVLib >= 22.1 (the
400
- # SPIRV-LLVM-Translator for LLVM 22), and apt.llvm.org does not yet ship
401
- # libllvmspirvlib-22-dev (the latest available is libllvmspirvlib-19-dev
402
- # at version 19.1.0.0, which is too old). Building SPIRV-LLVM-Translator
403
- # from source would add ~10 min to the build; the OpenCL validation is
404
- # deferred until the package lands upstream. video codecs and extra
405
- # tools stay off to keep the build lean; platforms=x11 only because the
406
- # engine renders headless through Xvfb GLX.
407
- #
408
- # Download strategy: GHA runners cannot reliably reach mesa.freedesktop.org
409
- # or archive.mesa3d.org (curl exit 28 on connect observed across multiple
410
- # Build workflow runs). The canonical upstream tarball has been mirrored
411
- # to a GitHub release asset under the mesa-cache-26.2.1 tag in this repo,
412
- # which is served from githubusercontent.com (always reachable from GHA).
413
- # The build tries the GitHub release first, then falls back through the
414
- # freedesktop.org canonical URL and the archive.mesa3d.org mirror (the
415
- # final redirect target) so local builds without GitHub access still
416
- # work.
417
- #
418
- # Verification contract (no hardcoded hashes, the gateway family policy):
419
- # the sha256 of the tarball is NEVER written in this file. GitHub computes
420
- # the digest of every release asset at upload time; the publish workflows
421
- # resolve that digest at build time and pass it as the MESA_SHA256
422
- # build-arg, so every GitHub build verifies the tarball before it lands
423
- # in a layer. Local builds without the build-arg fetch the SHA256SUMS
424
- # asset of the mesa-cache release when reachable and verify against it;
425
- # a fully offline local build proceeds with a visible warning.
426
- ARG MESA_SHA256=""
427
- RUN set -eux; \
428
- curl_opts="-fL --retry 5 --retry-all-errors --retry-delay 5 \
429
- --connect-timeout 30 --max-time 900"; \
430
- tarball="mesa-${MESA_VERSION}.tar.xz"; \
431
- repo="${GITHUB_REPOSITORY:-wenathlan/saddle}"; \
432
- github_release="https://github.com/${repo}/releases/download/mesa-cache-${MESA_VERSION}/${tarball}"; \
433
- sums_url="https://github.com/${repo}/releases/download/mesa-cache-${MESA_VERSION}/SHA256SUMS"; \
434
- upstream="https://mesa.freedesktop.org/archive/${tarball}"; \
435
- mirror="https://archive.mesa3d.org/${tarball}"; \
436
- rm -f "${tarball}" SHA256SUMS; \
437
- curl ${curl_opts} -C - -o "${tarball}" "${github_release}" \
438
- || curl ${curl_opts} -C - -o "${tarball}" "${upstream}" \
439
- || curl ${curl_opts} -C - -o "${tarball}" "${mirror}"; \
440
- if [ -n "${MESA_SHA256}" ]; then \
441
- echo "${MESA_SHA256} ${tarball}" | sha256sum -c -; \
442
- elif curl -fsSL --retry 3 --connect-timeout 15 -o SHA256SUMS "${sums_url}" \
443
- && grep -q " ${tarball}\$" SHA256SUMS; then \
444
- sha256sum -c --ignore-missing SHA256SUMS; \
445
- rm -f SHA256SUMS; \
446
- else \
447
- echo "WARNING: MESA_SHA256 build-arg empty and no SHA256SUMS asset reachable; ${tarball} digest verification skipped (offline local build)" >&2; \
448
- rm -f SHA256SUMS; \
449
- fi; \
450
- echo "mesa ${MESA_VERSION} tarball ready"; \
451
- tar xf "${tarball}"; \
452
- meson setup /tmp/mesa-build "mesa-${MESA_VERSION}" \
453
- --buildtype release \
454
- -Dprefix=/usr \
455
- -Dsysconfdir=/etc \
456
- -Dplatforms=x11 \
457
- -Dglx=dri \
458
- -Degl=enabled \
459
- -Dshared-glapi=enabled \
460
- -Dllvm=enabled \
461
- -Dgallium-drivers=llvmpipe \
462
- -Dvulkan-drivers=swrast \
463
- -Dgallium-rusticl=false \
464
- -Dvideo-codecs=[] \
465
- -Dtools=[] \
466
- -Dvalgrind=disabled \
467
- -Dlibunwind=disabled \
468
- -Dlmsensors=disabled \
469
- -Dzstd=enabled; \
470
- ninja -C /tmp/mesa-build; \
471
- DESTDIR=/staging ninja -C /tmp/mesa-build install; \
472
- # Ensure /staging/etc exists even when rusticl is off (no OpenCL ICD
473
- # installed); the runtime stage COPY --from=builder /staging/etc /etc/
474
- # would fail the buildx cache checksum without this.
475
- mkdir -p /staging/etc; \
476
- find /staging -name '*.so*' | head -20
477
-
478
- # the native sources, compiled directly per the build headers of each file
479
- # (no cmake, no build system beyond the compiler): the spoofing core and
480
- # the shim suite with -O3 -march -flto as mandated for the engine. the
481
- # c++26 build first tries `import std;` and falls back to -DVHE_NO_MODULES
482
- # when the toolchain ships no std module (libstdc++-14 on noble ships
483
- # without one). virtualizationcore.cpp additionally builds its selftest
484
- # binary and runs it: the stage fails if a single assert trips.
485
- WORKDIR /src
486
- COPY saddle/virtualhardware.c saddle/gpumonitor.cpp saddle/virtualizationcore.cpp /src/libs/
487
- RUN set -eux; \
488
- # portable baseline per architecture, with the ARCH FLAG the compiler
489
- # family actually accepts: x86 and arm speak -march; the ppc64le gcc
490
- # port rejects '-march=power8' ('did you mean -mcpu=power8?') so that
491
- # architecture selects the -mcpu spelling; s390x accepts -march=z196;
492
- # the eleven-arch law's remaining legs resolve their own baselines
493
- # (armv7-a / armv6 by TARGETVARIANT, rv64gc, loongarch64, mips64r2,
494
- # i686 for the 386 leg).
495
- archflag="-march"; \
496
- case "${TARGETARCH}" in \
497
- arm64|aarch64) march="armv8-a" ;; \
498
- arm) if [ "${TARGETVARIANT}" = "v7" ]; then march="armv7-a"; else march="armv6"; fi ;; \
499
- ppc64le) march="power8"; archflag="-mcpu" ;; \
500
- s390x) march="z196" ;; \
501
- riscv64) march="rv64gc" ;; \
502
- loong64|loongarch64) march="loongarch64" ;; \
503
- mips64le) march="mips64r2" ;; \
504
- 386|i386) march="i686" ;; \
505
- *) march="x86-64" ;; \
506
- esac; \
507
- if [ -n "${VHE_MARCH}" ]; then march="${VHE_MARCH}"; archflag="-march"; fi; \
508
- echo "building native shims with ${archflag}=${march}"; \
509
- mkdir -p /out/usr/local/lib /out/usr/local/bin /out/engine/native /out/etc/virtual; \
510
- gcc-14 -std=c11 -O3 "$archflag=$march" -flto -funswitch-loops \
511
- -shared -fPIC -pthread -ldl \
512
- /src/libs/virtualhardware.c -o /out/usr/local/lib/libvirtualhardware.so; \
513
- build_cpp() { \
514
- g++-14 -std=c++2c -O3 "$archflag=$march" -flto \
515
- -DVHE_NO_MODULES "$@"; \
516
- }; \
517
- build_cpp -shared -fPIC -ldl -DVHE_BUILD_PRELOAD \
518
- /src/libs/gpumonitor.cpp \
519
- -o /out/usr/local/lib/libvirtualhardware_gpu.so; \
520
- build_cpp -DVHE_BUILD_CLI /src/libs/gpumonitor.cpp \
521
- -o /out/usr/local/bin/nvidia-smi; \
522
- build_cpp -DVHE_BUILD_FORGE /src/libs/gpumonitor.cpp \
523
- -o /out/usr/local/bin/aetherforge; \
524
- build_cpp -shared -fPIC -DVHE_BUILD_ADDON \
525
- -DNODE_GYP_MODULE_NAME=vhe_gpu -I/usr/include/node \
526
- /src/libs/gpumonitor.cpp -o /out/engine/native/vhe_gpu.node; \
527
- build_cpp -shared -fPIC -pthread \
528
- /src/libs/virtualizationcore.cpp \
529
- -o /out/usr/local/lib/libvirtualizationcore.so; \
530
- build_cpp -DVHE_VIRT_SELFTEST /src/libs/virtualizationcore.cpp \
531
- -o /tmp/virtualizationcore_selftest; \
532
- /tmp/virtualizationcore_selftest; \
533
- ls -la /out/usr/local/lib /out/usr/local/bin /out/engine/native; \
534
- /out/usr/local/bin/nvidia-smi a100 1 | head -3; \
535
- /out/usr/local/bin/aetherforge --smp 16 --mttg 64 --mem 64 | head -3
536
-
537
- # the nvidia-ml and libcuda soname symlinks make pynvml, gpustat and
538
- # dlopen("libnvidia-ml.so.1") consumers resolve to the shim without any
539
- # LD_PRELOAD cooperation (rick-hsu/nvml-unified-shim deployment pattern).
540
- RUN set -eux; \
541
- ln -s libvirtualhardware_gpu.so /out/usr/local/lib/libnvidia-ml.so.1; \
542
- ln -s libvirtualhardware_gpu.so /out/usr/local/lib/libcuda.so.1; \
543
- ln -s libvirtualhardware_gpu.so /out/usr/local/lib/libnvidia-ml.so; \
544
- ln -s libvirtualhardware_gpu.so /out/usr/local/lib/libcuda.so
545
-
546
- # /etc/virtual holds only the profile-independent kernel identity
547
- # files (version/uptime/loadavg) here; the profile-specific cpuinfo
548
- # and meminfo are baked in the runtime stage so the builder layers
549
- # stay profile-independent (buildx cache shared across max/balanced/lite).
550
- RUN mkdir -p /out/etc/virtual
551
-
552
- COPY <<'EOF' /out/etc/virtual/version
553
- Linux version 6.18.0-vhe (vhe-builder@vhe) (clang version 19.1.0) #1 SMP VHE PREEMPT_DYNAMIC 2026-08-22
554
- EOF
555
-
556
- COPY <<'EOF' /out/etc/virtual/uptime
557
- 3600.00 2700.00
558
- EOF
559
-
560
- COPY <<'EOF' /out/etc/virtual/loadavg
561
- 3.84 3.12 2.98 12/768 28471
562
- EOF
563
-
564
- # ---------------------------------------------------------------------------
565
- # stage 2: python-bridge (qemubridge.py carrier)
566
- # ---------------------------------------------------------------------------
567
- FROM ${PYTHON_BASE} AS python-bridge
568
- ENV PYTHONDONTWRITEBYTECODE=1 \
569
- PYTHONUNBUFFERED=1
570
-
571
- # qemubridge.py is stdlib-only (asyncio, json, argparse, pathlib): the v2
572
- # merge dropped the pip dependencies of the older saddle bridge stages
573
- # (qemu-bridge/cffi wheels) because the bridge talks raw QMP over a unix
574
- # socket without any native binding. the py_compile run is the syntax gate
575
- # (error catcher: a bridge that does not compile fails the image build).
576
- COPY saddle/qemubridge.py /bridge/qemubridge.py
577
- RUN set -eux; \
578
- python3 -m py_compile /bridge/qemubridge.py; \
579
- python3 /bridge/qemubridge.py --help >/dev/null; \
580
- python3 -c "import ast, sys; ast.parse(open('/bridge/qemubridge.py').read()); print('qemubridge ast ok')"
581
-
582
- # ---------------------------------------------------------------------------
583
- # stage 3: qemu (TCG/MTTCG engine, the vheqemu docker run recipe base)
584
- # ---------------------------------------------------------------------------
585
- FROM ${UBUNTU} AS qemu
586
- ARG QEMU_VERSION
587
- ARG QEMU_FROM_SOURCE="1"
588
- ENV DEBIAN_FRONTEND=noninteractive
589
-
590
- # qemu 11.1.0 (released 2026-08-12) with the x86_64 system emulator only;
591
- # KVM is auto-detected at runtime but the engine targets TCG with MTTCG
592
- # (-accel tcg,thread=multi,tb-size=1024 -cpu EPYC-v5) because CI hosts and
593
- # nested containers expose no /dev/kvm. when QEMU_FROM_SOURCE=0 (source
594
- # build not viable, for example a constrained builder), the stage installs
595
- # the distro qemu-system-x86 package instead; version drift is then the
596
- # operator's tradeoff and is recorded in the image labels.
597
- RUN set -eux; \
598
- # mirror sync race: same retry loop as the builder stage, covering
599
- # BOTH apt-get update AND apt-get install together so a 404 on install
600
- # (stale Release index referencing .deb files already removed from the
601
- # pool) re-fetches a fresh index and retries the install.
602
- apt_update_tries=5; \
603
- while [ "$apt_update_tries" -gt 0 ]; do \
604
- if apt-get update && apt-get install -y --no-install-recommends \
605
- ca-certificates curl xz-utils bzip2 \
606
- build-essential ninja-build meson python3 python3-venv \
607
- libglib2.0-dev libpixman-1-dev zlib1g-dev flex bison; then break; fi; \
608
- apt_update_tries=$((apt_update_tries - 1)); \
609
- echo "apt-get update/install failed (mirror sync?), $apt_update_tries retries left"; \
610
- sleep 10; \
611
- rm -rf /var/lib/apt/lists/*; \
612
- done; \
613
- test "$apt_update_tries" -gt 0; \
614
- if [ "${QEMU_FROM_SOURCE}" = "1" ]; then \
615
- curl -fsSLO "https://download.qemu.org/qemu-${QEMU_VERSION}.tar.xz"; \
616
- tar xf "qemu-${QEMU_VERSION}.tar.xz"; \
617
- cd "qemu-${QEMU_VERSION}"; \
618
- ./configure \
619
- --target-list=x86_64-softmmu \
620
- --disable-docs \
621
- --disable-werror \
622
- --disable-sdl \
623
- --disable-gtk \
624
- --disable-curl \
625
- --disable-spice \
626
- --disable-opengl; \
627
- ninja -C build; \
628
- ninja -C build install; \
629
- qemu-system-x86_64 --version; \
630
- cd /; rm -rf "/qemu-${QEMU_VERSION}"*; \
631
- else \
632
- apt-get install -y --no-install-recommends \
633
- qemu-system-x86 qemu-utils; \
634
- qemu-system-x86_64 --version; \
635
- fi; \
636
- apt-get purge -y --auto-remove \
637
- build-essential ninja-build meson python3-venv flex bison \
638
- libglib2.0-dev libpixman-1-dev zlib1g-dev \
639
- || true; \
640
- apt-get clean; rm -rf /var/lib/apt/lists/*
641
-
642
- ENTRYPOINT ["qemu-system-x86_64"]
643
-
644
- # ---------------------------------------------------------------------------
645
- # stage 4: qemu-runtime (guest runner: qemu + OVMF + pciutils + tini)
646
- # ---------------------------------------------------------------------------
647
- FROM qemu AS qemu-runtime
648
- ENV DEBIAN_FRONTEND=noninteractive
1
+ # saddle 2.0.160 — THE ENGINE CONTAINER FILE
2
+ #
3
+ # THE FAMILY CONTAINER DOCTRINE, THE SADDLE SHAPE: one versioned container
4
+ # per engine, built like the site homes (argan, cadria) build theirs — a
5
+ # locked install stage, a hardened runtime stage, an unprivileged user, a
6
+ # healthcheck the operator trusts. The grand-merge era carried a 1,500-line
7
+ # file that compiled llvm, mesa and qemu from source on eleven architectures
8
+ # under emulation; that container never built green (the cold-cache legs
9
+ # died on the hosted-runner ceilings), so this file returns to the doctrine
10
+ # the family proved: the console engine the webserver carries rides the
11
+ # node runtime, the virtual hardware the engine presents rides the modality
12
+ # registry and the profile catalog below — never the visitor machine (the
13
+ # visitor law: nothing the engine does touches the person's gpu, cpu,
14
+ # browser or storage; every workload executes on the self-hosted edge).
15
+ #
16
+ # THE THREE VARIANT PROFILES (the saddle-exclusive law: the only app of
17
+ # the family that ships three) — select with the target flag:
18
+ #
19
+ # docker build --target max -t saddle:2.0.160 .
20
+ # docker build --target balanced -t saddle:2.0.160-balanced .
21
+ # docker build --target lite -t saddle:2.0.160-lite .
22
+ #
23
+ # max — the full profile catalog: the epyc 9965 class, 192 vcpu,
24
+ # 384 threads, 1024 gib ram plan, the b200 gpu plan x8.
25
+ # balanced — the threadripper pro 9995wx class, 96 vcpu, 192 threads,
26
+ # 512 gib ram plan, the b200 gpu plan x2.
27
+ # lite — the ryzen 9950x3d class, 16 vcpu, 32 threads, 128 gib
28
+ # ram plan, the rtx 5090 gpu plan x1.
29
+ #
30
+ # The profile rides the image config (ENV + the OCI labels the verify legs
31
+ # assert), the catalog constants stay the publish matrix's single source,
32
+ # and every profile ships the same engine graph — the same ninety
33
+ # modalities, the same console, the same health surface; only the declared
34
+ # hardware plan differs.
35
+ #
36
+ # THE ELEVEN-ARCHITECTURE PLATFORM STANDARD (the pr15b law): every
37
+ # profile answers the eleven architectures the container standard names —
38
+ # the CONTAINERARCHIDS of saddle/containerarch.ts: x64, arm64, armv7,
39
+ # riscv64, ppc64le, s390x, 386, mips64le, loong64, wasm32, wasip1 (the
40
+ # wasm pair rides the oci artifact shapes wasm32/wasip1 and wasip1/wasm
41
+ # the wasm runtimes consume). The build legs follow the pinned node
42
+ # base's honest manifest list — the four linux seats it carries
43
+ # (linux/amd64 the native x64 leg; linux/arm64, linux/ppc64le and
44
+ # linux/s390x ride qemu emulation in the publish workflow). The seven
45
+ # seats the node base does not ship (armv7, riscv64, 386, mips64le,
46
+ # loong64 and the wasm pair) answer the standard's honest coded absence
47
+ # (ARCH_IMAGE_ABSENT), never a pretended build leg — the same honesty
48
+ # law the containerarch table keeps for its own pinned base: a leg
49
+ # never claims a manifest the base does not carry.
50
+ #
51
+ # THE CONTEXT LAW (the family context fix): this file COPYs from two
52
+ # trees (saddle/ and the engine-ownership sibling vault/), so every build
53
+ # runs from the repository root with an explicit -f:
54
+ # docker build -f saddle/Dockerfile .
55
+ # building from inside saddle/ breaks the vault COPY - the sibling rides
56
+ # the context beside this directory, never inside it.
57
+ #
58
+ # THE DIGEST LAW: the FROM lines ride name@sha256:${NODE_BASE_DIGEST}
59
+ # (the scorecard-file form the supply-chain gate reads as pinned) while
60
+ # the digest itself never lands in a source file — the publish lanes
61
+ # resolve the manifest-list digest on the runner at build time and pass
62
+ # it as the NODE_BASE_DIGEST build-arg, and a local build resolves the
63
+ # same lane before docker build (the signature is generated during the
64
+ # build, never hardcoded).
65
+ #
66
+ # THE VERSION LAW: the ARG default below mirrors saddle/package.json
67
+ # (the metadata lockstep the registry lanes stamp); the publish lanes pass
68
+ # the resolved release version at build time, nothing is hardcoded beside
69
+ # the stamp.
70
+
71
+ ARG NODE_BASE_TAG="node:26.13.0-slim"
72
+ # the digest-never-hardcoded law, the scorecard-file form: the FROM lines
73
+ # ride name@sha256:${ARG} (the exact pattern the supply-chain gate reads
74
+ # as pinned) while the digest itself still resolves at build time on the
75
+ # runner (the bases step of the publish lanes) — no 64-hex ever enters the
76
+ # repository, the manifest-list digest serves the whole build matrix
77
+ # (the four base-carried legs of the eleven-architecture standard), and a
78
+ # local build resolves the same lane before docker build.
79
+ ARG NODE_BASE_DIGEST
80
+
81
+ # ===== stage: solbuild — the locked npm tree and the console spa =====
82
+ FROM --platform=$BUILDPLATFORM ${NODE_BASE_TAG}@sha256:${NODE_BASE_DIGEST} AS solbuild
83
+
84
+ ARG SADDLE_VERSION=2.0.160
649
85
 
650
- # the docker.config qemuRuntime role: QEMU system (already provided by
651
- # the parent stage, either the 11.1.0 source build or the distro
652
- # fallback), OVMF firmware, PCI utilities, curl for health probes, kmod
653
- # for module loading and tini as pid 1 so guest processes are reaped when
654
- # the guest runner restarts. when the parent used the source build the
655
- # distro qemu packages stay absent; when it used the apt fallback the
656
- # ovmf/pciutils set still installs identically.
657
- RUN set -eux; \
658
- # mirror sync race: same retry loop as the builder and qemu stages,
659
- # covering BOTH apt-get update AND apt-get install together.
660
- apt_update_tries=5; \
661
- while [ "$apt_update_tries" -gt 0 ]; do \
662
- if apt-get update && apt-get install -y --no-install-recommends \
663
- ovmf pciutils curl kmod tini; then break; fi; \
664
- apt_update_tries=$((apt_update_tries - 1)); \
665
- echo "apt-get update/install failed (mirror sync?), $apt_update_tries retries left"; \
666
- sleep 10; \
667
- rm -rf /var/lib/apt/lists/*; \
668
- done; \
669
- test "$apt_update_tries" -gt 0; \
670
- mkdir -p /usr/share/OVMF /var/log/vhe /run/vhe; \
671
- ls -lh /usr/share/OVMF/OVMF_CODE.fd || true; \
672
- apt-get clean; rm -rf /var/lib/apt/lists/*
673
-
674
- ENTRYPOINT ["/usr/bin/tini", "--", "qemu-system-x86_64"]
675
-
676
- # ---------------------------------------------------------------------------
677
- # stage 5: runtime (final, default target, the vhe/vhegpu docker run recipes)
678
- # ---------------------------------------------------------------------------
679
- # saddle-build: the node-engine build stage (defined here, before the
680
- # vhe runtime, so BOTH container surfaces can consume its output: the
681
- # vhe runtime copies the built web SPA beside the console server at
682
- # /engine and the saddle-runtime stage copies the engine dist plus the
683
- # same staged SPA - one build, two consumers). The stage runs native
684
- # (never under QEMU): its outputs are platform-independent static/js
685
- # payloads.
686
-
687
- # the build stage pins to the native build platform ($BUILDPLATFORM): its
688
- # outputs (the engine dist, pure js, and the web SPA, static assets) are
689
- # architecture-independent, while the tailwind/lightningcss pipeline only
690
- # ships native bindings for the builder platforms - the per-arch legs never
691
- # run it under QEMU. The base rides the family tag contract (tags only,
692
- # never a @sha256: digest - a single digest cannot serve the
693
- # eleven-architecture matrix the registry resolves per platform).
694
- FROM --platform=$BUILDPLATFORM node:26.13.0-slim AS saddle-build
695
-
696
- ARG SADDLE_VERSION=2.0.154
697
86
  WORKDIR /app
698
87
 
699
88
  ENV NPM_CONFIG_LEGACY_PEER_DEPS=true
@@ -717,695 +106,45 @@ RUN npm run web:build:pages \
717
106
  && cp index.html *.js *.css *.svg *.webmanifest /out/spa/ \
718
107
  && rm -f /out/spa/popup.css
719
108
 
720
- FROM ${UBUNTU} AS runtime
721
- ARG TARGETARCH
722
- ARG NODE_VERSION
723
- ARG VHE_UID=10000
724
- ARG VHE_GID=10000
725
- ARG VHE_PROFILE_CPU
726
- ARG VHE_PROFILE_VCPUS
727
- ARG VHE_PROFILE_THREADS
728
- ARG VHE_PROFILE_RAM_GB
729
- ARG VHE_PROFILE_GPU
730
- ARG VHE_PROFILE_GPUS
731
- ENV DEBIAN_FRONTEND=noninteractive
732
-
733
- # runtime packages: Xvfb for the headless GLX display, x11-utils/xdpyinfo
734
- # for the readiness probe, vulkan-tools (vulkaninfo), clinfo for the
735
- # rusticl report, mesa-utils (glxinfo/es2_info), the ocl-icd loader for
736
- # rusticl, python3 for the qmp bridge, procps for free/top inside the
737
- # sandbox, and the shared libraries the mesa staging links against.
738
- #
739
- # The LLVM 18 runtime (libLLVM-18.so.1, which llvmpipe and lavapipe link
740
- # against) is COPIED from the builder stage below instead of installed
741
- # separately: the builder already has the full llvm-18-dev toolchain
742
- # installed from the Ubuntu noble archive, so copying the lib tree is
743
- # both faster (no apt round-trip) and keeps the runtime stage on the
744
- # same LLVM version the mesa build linked against.
745
- RUN set -eux; \
746
- # mirror sync race: same retry loop as the builder and qemu stages,
747
- # covering BOTH apt-get update AND apt-get install together so a 404
748
- # on install (stale Release index referencing .deb files already
749
- # removed from the pool) re-fetches a fresh index and retries.
750
- # (this is the leg that failed on ppc64le in 1.2.12 and on s390x in
751
- # 1.2.13 before this retry covered the install step too).
752
- #
753
- # base-image CVE closure: apt-get install only guarantees the listed
754
- # packages - the ubuntu:24.04 base layer arrives with whatever system
755
- # packages its digest carried (and a BuildKit registry cache hit can
756
- # replay an even older layer chain), so perl-base, util-linux,
757
- # ncurses, coreutils & friends can lag behind the published security
758
- # fixes even right after a fresh build. The upgrade step pulls every
759
- # installed base package to the latest security update resolved by
760
- # the fresh index above (never installs new packages, never removes
761
- # any - same-version upgrades only), and Always-Include-Phased-Updates
762
- # skips Ubuntu's gradual phasing so CI builds are deterministic.
763
- apt_update_tries=5; \
764
- while [ "$apt_update_tries" -gt 0 ]; do \
765
- if apt-get update && apt-get install -y --no-install-recommends \
766
- ca-certificates curl gnupg bash procps \
767
- xvfb x11-utils xauth x11-xserver-utils \
768
- mesa-utils vulkan-tools clinfo ocl-icd-libopencl1 \
769
- python3 \
770
- libdrm2 libexpat1 zlib1g libzstd1 libedit2 libatomic1 \
771
- libx11-6 libxext6 libxfixes3 libxxf86vm1 libxshmfence1 \
772
- libxcb1 libwayland-client0 libwayland-egl1 libelf1 \
773
- libglib2.0-0t64 libpixman-1-0 \
774
- && apt-get -o APT::Get::Always-Include-Phased-Updates=true \
775
- upgrade -y; then break; fi; \
776
- apt_update_tries=$((apt_update_tries - 1)); \
777
- echo "apt-get update/install/upgrade failed (mirror sync?), $apt_update_tries retries left"; \
778
- sleep 10; \
779
- rm -rf /var/lib/apt/lists/*; \
780
- done; \
781
- test "$apt_update_tries" -gt 0; \
782
- apt-get clean; rm -rf /var/lib/apt/lists/*
783
-
784
- # LLVM 18 runtime: copy the entire llvm-18 install tree from the builder
785
- # (libLLVM-18.so.1 + the llvm-18 runtime libraries that llvmpipe and
786
- # lavapipe link against). The ldconfig step later in this stage adds
787
- # /usr/lib/llvm-18/lib to the search path so the loader picks them up.
788
- COPY --from=builder /usr/lib/llvm-18/ /usr/lib/llvm-18/
109
+ # ===== stage: base — the hardened runtime floor every profile shares =====
110
+ FROM ${NODE_BASE_TAG}@sha256:${NODE_BASE_DIGEST} AS base
789
111
 
790
- # mesa 26.2.1 from the staging root: dri drivers, the lavapipe vulkan ICD
791
- # json (lvp_icd.<arch>.json), the rusticl opencl ICD under /etc/OpenCL and
792
- # the shared glapi.
793
- COPY --from=builder /staging/usr /usr/
794
- COPY --from=builder /staging/etc /etc/
795
- # node 26.7.0 carried over from the builder, including headers so native
796
- # addons (vhe_gpu.node and downstream user addons) rebuild without a second
797
- # nodesource round-trip.
798
- COPY --from=builder /usr/bin/node /usr/bin/node
799
- COPY --from=builder /usr/bin/npm /usr/bin/npm
800
- COPY --from=builder /usr/bin/npx /usr/bin/npx
801
- COPY --from=builder /usr/lib/node_modules /usr/lib/node_modules
802
- COPY --from=builder /usr/include/node /usr/include/node
803
- # the spoofing artifacts: preload core, gpu shim with the nvidia soname
804
- # symlinks, the fake nvidia-smi binary, the aetherforge control plane, the
805
- # virtualization core shared library, the napi addon and the baked
806
- # /etc/virtual profile.
807
- COPY --from=builder /out/usr/local/lib /usr/local/lib/
808
- COPY --from=builder /out/usr/local/bin/nvidia-smi /usr/local/bin/nvidia-smi
809
- COPY --from=builder /out/usr/local/bin/aetherforge /usr/local/bin/aetherforge
810
- COPY --from=builder /out/engine /engine
811
- COPY --from=builder /out/etc/virtual /etc/virtual
812
-
813
- # /etc/virtual cpuinfo + meminfo baked per profile here in the runtime
814
- # stage (the builder is profile-independent so its layers share the buildx
815
- # cache across max/balanced/lite; the profile identity is baked here).
816
- # cpuinfo model name, processor count and thread count per profile
817
- # (max: AMD EPYC 9965 192c/384t, balanced: AMD Ryzen Threadripper
818
- # Pro 9995WX 96c/192t, lite: AMD Ryzen 9 9950X3D 16c/32t); cpu
819
- # family/model/stepping and address sizes match each identity
820
- # (the desktop ryzen drops la57 and reports 46/48-bit addressing).
821
- # meminfo static snapshot scaled to the profile memory plan (max 1 TiB,
822
- # balanced 512 GiB, lite 128 GiB); the preload core regenerates
823
- # a dynamic copy per process by default (fakemem pattern from
824
- # VHE_TOTALRAM_GB), so this file is the VHE_MEMINFO_MODE=static
825
- # source of truth and the fallback. VmallocTotal (the 64-bit
826
- # address space) and Hugepagesize (the fixed 2 MiB huge page) stay
827
- # constant regardless of the profile memory plan.
828
- COPY <<'EOF' /tmp/meminfo-template
829
- MemTotal: 1073741824 kB
830
- MemFree: 33554432 kB
831
- MemAvailable: 94673704 kB
832
- Buffers: 8388608 kB
833
- Cached: 58720256 kB
834
- SwapCached: 0 kB
835
- Active: 33554432 kB
836
- Inactive: 16777216 kB
837
- Active(anon): 20971520 kB
838
- Inactive(anon): 4194304 kB
839
- Active(file): 12582912 kB
840
- Inactive(file): 12582912 kB
841
- Unevictable: 0 kB
842
- Mlocked: 0 kB
843
- SwapTotal: 67108864 kB
844
- SwapFree: 67108864 kB
845
- Zswap: 0 kB
846
- Zswapped: 0 kB
847
- Dirty: 262144 kB
848
- Writeback: 0 kB
849
- AnonPages: 20971520 kB
850
- Mapped: 4194304 kB
851
- Shmem: 1048576 kB
852
- KReclaimable: 2097152 kB
853
- Slab: 4194304 kB
854
- SReclaimable: 2097152 kB
855
- SUnreclaim: 2097152 kB
856
- KernelStack: 65536 kB
857
- PageTables: 32768 kB
858
- NFS_Unstable: 0 kB
859
- Bounce: 0 kB
860
- WritebackTmp: 0 kB
861
- CommitLimit: 134217728 kB
862
- Committed_AS: 33554432 kB
863
- VmallocTotal: 34359738367 kB
864
- VmallocUsed: 1048576 kB
865
- VmallocChunk: 0 kB
866
- Percpu: 65536 kB
867
- HardwareCorrupted: 0 kB
868
- AnonHugePages: 0 kB
869
- ShmemHugePages: 0 kB
870
- FileHugePages: 0 kB
871
- HugePages_Total: 0
872
- HugePages_Free: 0
873
- HugePages_Rsvd: 0
874
- HugePages_Surp: 0
875
- Hugepagesize: 2048 kB
876
- Hugetlb: 0 kB
877
- DirectMap4k: 2097152 kB
878
- DirectMap2M: 62914560 kB
879
- DirectMap1G: 67108864 kB
880
- EOF
881
-
882
- RUN set -eux; \
883
- : > /etc/virtual/cpuinfo; \
884
- case "${VHE_PROFILE_CPU}" in \
885
- epyc9965) \
886
- cpuname="AMD EPYC 9965"; \
887
- cpufamily="26"; cpumodel="96"; cpustepping="2"; \
888
- addr="52 bits physical, 57 bits virtual"; la57="1" ;; \
889
- threadripperpro9995wx) \
890
- cpuname="AMD Ryzen Threadripper Pro 9995WX"; \
891
- cpufamily="26"; cpumodel="96"; cpustepping="2"; \
892
- addr="52 bits physical, 57 bits virtual"; la57="1" ;; \
893
- ryzen9950x3d) \
894
- cpuname="AMD Ryzen 9 9950X3D"; \
895
- cpufamily="25"; cpumodel="97"; cpustepping="2"; \
896
- addr="46 bits physical, 48 bits virtual"; la57="0" ;; \
897
- *) \
898
- echo "unknown VHE_PROFILE_CPU=${VHE_PROFILE_CPU}; expected epyc9965, threadripperpro9995wx or ryzen9950x3d" >&2; \
899
- exit 1 ;; \
900
- esac; \
901
- vcpus="${VHE_PROFILE_VCPUS}"; \
902
- threads="${VHE_PROFILE_THREADS}"; \
903
- ram_gb="${VHE_PROFILE_RAM_GB}"; \
904
- modelname="${cpuname} ${vcpus}-Core Processor"; \
905
- FLAGS='fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx mmxext fxsr_opt pdpe1gb rdtscp lm constant_tsc rep_good amd_lbr vnnm nonstop_tsc cpuid extd_apicid amd_dcm aperfmperf rapl pni pclmulqdq monitor ssse3 fma cx16 sse4_1 sse4_2 movbe popcnt aes xsave avx f16c rdrand lahf_lm cmp_legacy svm extapic cr8_legacy abm sse4a misalignsse 3dnowprefetch osvw ibs skinit wdt tce topoext perfctr_core perfctr_nb bpext perfctr_llc mwaitx cpb cat_l3 cdp_l3 invpcid_single hw_pstate ssbd mba perfmon_v2 ibrs ibpb stibp vmmcall fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid cqm rdt_a avx512f avx512dq rdseed adx smap avx512ifma clflushopt clwb avx512cd sha_ni avx512bw avx512vl avx512vbmi umip pku ospke avx512_vbmi2 gfni vaes vpclmulqdq avx512_vnni avx512_bitalg avx512_vpopcntdq rdpid movdiri movdir64b fsrm md_clear serialize tsxldtrk avx512_fp16'; \
906
- if [ "$la57" = "1" ]; then FLAGS="${FLAGS} la57"; fi; \
907
- for i in $(seq 0 $((vcpus - 1))); do \
908
- { \
909
- printf 'processor\t: %d\n' "$i"; \
910
- printf 'vendor_id\t: AuthenticAMD\n'; \
911
- printf 'cpu family\t: %s\n' "$cpufamily"; \
912
- printf 'model\t\t: %s\n' "$cpumodel"; \
913
- printf 'model name\t: %s\n' "$modelname"; \
914
- printf 'stepping\t: %s\n' "$cpustepping"; \
915
- printf 'microcode\t: 0x1100140\n'; \
916
- printf 'cpu MHz\t\t: 2250.000\n'; \
917
- printf 'cache size\t: 512 KB\n'; \
918
- printf 'physical id\t: 0\n'; \
919
- printf 'siblings\t: %s\n' "$threads"; \
920
- printf 'core id\t\t: %d\n' "$((i % vcpus))"; \
921
- printf 'cpu cores\t: %s\n' "$vcpus"; \
922
- printf 'apicid\t\t: %d\n' "$i"; \
923
- printf 'initial apicid\t: %d\n' "$i"; \
924
- printf 'fpu\t\t: yes\n'; \
925
- printf 'fpu_exception\t: yes\n'; \
926
- printf 'cpuid level\t: 16\n'; \
927
- printf 'wp\t\t: yes\n'; \
928
- printf 'flags\t\t: %s\n' "$FLAGS"; \
929
- printf 'bugs\t\t: sysret_ss_attrs spectre_v1 spectre_v2 spec_store_bypass srso\n'; \
930
- printf 'bogomips\t: 4500.00\n'; \
931
- printf 'TLB size\t: 3584 4K pages\n'; \
932
- printf 'clflush size\t: 64\n'; \
933
- printf 'cache_alignment\t: 64\n'; \
934
- printf 'address sizes\t: %s\n' "$addr"; \
935
- printf 'power management: ts ttp tm hwpstate cpb eff_freq_ro [13] [16]\n'; \
936
- } >> /etc/virtual/cpuinfo; \
937
- done; \
938
- grep -c '^processor' /etc/virtual/cpuinfo | grep -qx "$vcpus"; \
939
- total_kb=$((ram_gb * 1024 * 1024)); \
940
- awk -v total="$total_kb" 'function scale(line, a, n, i, out, key) { n = split(line, a, " "); if (n >= 2 && a[n] == "kB" && (a[n-1]+0) > 0) { key = a[1]; if (key != "VmallocTotal:" && key != "Hugepagesize:") { a[n-1] = int((a[n-1]+0) * total / 1073741824); } } out = a[1]; for (i = 2; i <= n; i++) out = out " " a[i]; return out; } { print scale($0) }' /tmp/meminfo-template > /etc/virtual/meminfo; \
941
- rm -f /tmp/meminfo-template; \
942
- grep -E "^MemTotal: +${total_kb} kB" /etc/virtual/meminfo; \
943
- echo "baked /etc/virtual: ${modelname} (${vcpus}c/${threads}t), MemTotal ${total_kb} kB (${ram_gb} GiB plan)"
944
- # the python bridge from its dedicated stage.
945
- COPY --from=python-bridge /bridge/qemubridge.py /engine/qemubridge.py
946
-
947
- # the engine sources and the specification files consumed at runtime
948
- # (docker.config "final" stage copies the whole tree; the flat layout
949
- # keeps that a single COPY layer).
950
- COPY saddle/*.ts saddle/package.json saddle/tsconfig.json saddle/biome.jsonc /engine/
951
- COPY saddle/processors.json saddle/gpus.json saddle/cores.json saddle/boards.json saddle/vm.config.json saddle/virtualhardware.json /engine/
952
- COPY saddle/qemu.config saddle/mttg.config saddle/passage.config saddle/docker.config saddle/mime.types /engine/
953
- # the engine-ownership sibling: db.ts pulls labels.ts whose
954
- # ../vault/labelstore.ts import resolves at /vault - the vault source
955
- # rides the context beside the saddle and lands at /vault where the
956
- # import resolves (the same family context fix as the saddle-build
957
- # stage), so the engine webserver below finds the label store.
958
- COPY vault/labelstore.ts /vault/labelstore.ts
959
-
960
- # the web node ships inside the main container: the mesh edition (auth,
961
- # sqlite database, dashboard and the browser sandbox pages) runs from the
962
- # same image, so one container serves the engine AND the web surface.
963
- # run it with: docker run ghcr.io/wenathlan/saddle node /engine/webserver.ts
964
- # (the console server is a root engine source - the COPY *.ts line above
965
- # already lands it at /engine together with its console stack, and the
966
- # extension denylist of its static resolver keeps every .ts/.json/.sql
967
- # source unreachable).
968
- # the built web SPA (vite, staged by saddle-build): the theme build emits
969
- # the hashed bundles straight at the app root, so the staged spa lands
970
- # beside the console server at /engine and webserver.ts serves it from
971
- # its own module directory - the unified React interface that absorbed
972
- # the former static pages.
973
- COPY --from=saddle-build /out/spa /engine/
974
-
975
- RUN set -eux; \
976
- chmod 0755 /usr/local/bin/nvidia-smi /usr/local/bin/aetherforge; \
977
- chmod 0644 /etc/virtual/*; \
978
- printf '/usr/lib/llvm-18/lib\n/usr/local/lib\n' > /etc/ld.so.conf.d/vhe.conf; \
979
- mkdir -p /etc/OpenCL/vendors; \
980
- case "${TARGETARCH}" in \
981
- arm64|aarch64) ln -sf lvp_icd.aarch64.json /usr/share/vulkan/icd.d/lvp_icd.json ;; \
982
- arm) ln -sf lvp_icd.arm.json /usr/share/vulkan/icd.d/lvp_icd.json ;; \
983
- ppc64le) ln -sf lvp_icd.ppc64le.json /usr/share/vulkan/icd.d/lvp_icd.json ;; \
984
- s390x) ln -sf lvp_icd.s390x.json /usr/share/vulkan/icd.d/lvp_icd.json ;; \
985
- riscv64) ln -sf lvp_icd.riscv64.json /usr/share/vulkan/icd.d/lvp_icd.json ;; \
986
- loong64|loongarch64) ln -sf lvp_icd.loongarch64.json /usr/share/vulkan/icd.d/lvp_icd.json ;; \
987
- mips64le) ln -sf lvp_icd.mips64.json /usr/share/vulkan/icd.d/lvp_icd.json ;; \
988
- 386|i386) ln -sf lvp_icd.x86.json /usr/share/vulkan/icd.d/lvp_icd.json ;; \
989
- *) ln -sf lvp_icd.x86_64.json /usr/share/vulkan/icd.d/lvp_icd.json ;; \
990
- esac; \
991
- ldconfig; \
992
- node --version; \
993
- python3 -m py_compile /engine/qemubridge.py; \
994
- nvidia-smi b200 1 | head -2; \
995
- ldconfig -p | grep -E 'libGL|lvp|rusticl|libLLVM-18' | head -5
996
-
997
- # entrypoint: starts Xvfb on :99, validates the GL/Vulkan/OpenCL stack
998
- # and execs the engine command. the bootstrap is EMBEDDED HERE (the one
999
- # container file standard - the repository keeps no separate
1000
- # entrypoint.sh): the heredoc COPY below writes the script verbatim
1001
- # into the image and the quoted delimiter keeps every shell expansion
1002
- # literal, so the file is the exact former entrypoint.sh.
1003
- COPY <<'ENTRYPOINT_SCRIPT_EOF' /entrypoint.sh
1004
- #!/usr/bin/env bash
1005
- #
1006
- # entrypoint.sh - runtime bootstrap for the saddle virtual hardware
1007
- # engine image.
1008
- #
1009
- # responsibilities, in order:
1010
- # 0. print the engine banner, pin the MTTCG mode for downstream
1011
- # consumers, export the gallium/mesa environment overrides that are
1012
- # unique to the headless software stack, and report the firecracker
1013
- # warm-pool readiness note (the microvm runtime shares this image).
1014
- # 1. start Xvfb on display :99 with the validated headless-GLX recipe
1015
- # (Xvfb(1): -ac disables access control, -screen 0 1920x1080x24
1016
- # defines the framebuffer, -nolisten tcp closes the network surface,
1017
- # +extension GLX +render enable the GLX and RENDER extensions the
1018
- # mesa llvmpipe driver expects, -noreset keeps the server alive
1019
- # after the last client disconnects) and wait for readiness with
1020
- # xdpyinfo.
1021
- # 2. verify the LD_PRELOAD spoofing core is installed and loadable
1022
- # (global /etc/ld.so.preload from the dolos deployment pattern).
1023
- # 3. run the hardware validation heads: glxinfo (OpenGL renderer must
1024
- # be llvmpipe), vulkaninfo --summary (lavapipe ICD) and clinfo
1025
- # (rusticl reporting a gpu device on OpenCL 3.1); failures log
1026
- # warnings unless VHE_STRICT=1 turns them fatal.
1027
- # 4. allocate a random listening port in 30000-60000 when VHE_PORT/PORT
1028
- # is unset (shuf with a /dev/urandom fallback), so no host port is
1029
- # ever hardcoded across the engine.
1030
- # 5. exec "$@" so the engine becomes pid 1 and receives signals
1031
- # directly.
1032
- #
1033
- # environment knobs (all optional):
1034
- # VHE_DISPLAY x display number, default 99
1035
- # VHE_SCREEN screen geometry, default 1920x1080x24
1036
- # VHE_XVFB_WAIT max seconds waiting for the display, default 30
1037
- # VHE_SKIP_XVFB 1 skips display startup entirely (pure compute)
1038
- # VHE_SKIP_PRELOAD 1 skips the spoofing core check
1039
- # VHE_SKIP_VALIDATE 1 skips the glxinfo/vulkaninfo/clinfo probes
1040
- # VHE_STRICT 1 turns validation failures into entrypoint failures
1041
- # VHE_PORT / PORT fixed port; otherwise one is drawn from 30000-60000
1042
- # VHE_DEBUG 1 prints every decision to stderr
1043
- #
1044
- # the script never exits on validation warnings: the engine degrades to
1045
- # whatever hardware surface is actually functional, mirroring the
1046
- # defensive fallback chain of libvirtualhardware.so itself.
1047
-
1048
- set -Eeuo pipefail
1049
-
1050
- # ---------------------------------------------------------------- logging --
1051
-
1052
- log() { printf '[saddle-entrypoint] %s\n' "$*" >&2; }
1053
- dbg() { [[ "${VHE_DEBUG:-0}" == "1" ]] && printf '[saddle-entrypoint:debug] %s\n' "$*" >&2 || true; }
1054
- warn() { printf '[saddle-entrypoint:warn] %s\n' "$*" >&2; }
1055
- die() { printf '[saddle-entrypoint:fatal] %s\n' "$*" >&2; exit 1; }
1056
-
1057
- # ------------------------------------------------------------- variables --
1058
-
1059
- vdisplaynum="${VHE_DISPLAY:-99}"
1060
- vdisplayid=":${vdisplaynum}"
1061
- screengeom="${VHE_SCREEN:-1920x1080x24}"
1062
- xvfbwait="${VHE_XVFB_WAIT:-30}"
1063
- xvfbpid=""
1064
- portvalue=""
1065
-
1066
- cleanup() {
1067
- if [[ -n "${xvfbpid}" ]] && kill -0 "${xvfbpid}" 2>/dev/null; then
1068
- log "stopping Xvfb (pid ${xvfbpid})"
1069
- kill -TERM "${xvfbpid}" 2>/dev/null || true
1070
- wait "${xvfbpid}" 2>/dev/null || true
1071
- fi
1072
- }
1073
- trap cleanup EXIT
1074
- trap 'die "interrupted"' INT TERM
1075
-
1076
- requirebin() {
1077
- command -v "$1" >/dev/null 2>&1 || die "required binary '$1' not found in PATH"
1078
- }
1079
-
1080
- # ------------------------------------------- 0. banner and env overrides --
1081
-
1082
- # report the baked profile identity (read from /etc/virtual so the banner
1083
- # matches the profile the image was built for: max/balanced/lite). Single
1084
- # awk process with an early exit per file (no sed|head pipeline: under
1085
- # set -o pipefail, head exiting after the first match would SIGPIPE sed on
1086
- # the large max/balanced cpuinfo and abort the entrypoint).
1087
- vhe_model="$(awk '/^model name/ { sub(/^[[:space:]]*model name[[:space:]]*:[[:space:]]*/, ""); print; exit }' /etc/virtual/cpuinfo 2>/dev/null)"
1088
- vhe_ram="$(awk '/^MemTotal/ { print $2; exit }' /etc/virtual/meminfo 2>/dev/null || true)"
1089
- case "${vhe_ram}" in *[!0-9]*) vhe_ram="" ;; esac
1090
- if [ -n "${vhe_model}" ] && [ -n "${vhe_ram}" ]; then
1091
- vhe_ram_gb=$(( vhe_ram / 1024 / 1024 ))
1092
- log "saddle virtual hardware engine (${vhe_model}, ${vhe_ram_gb} GiB, mesa 26.2.1)"
1093
- else
1094
- log "saddle virtual hardware engine (mesa 26.2.1)"
1095
- fi
1096
- log "docker contract: memory-swap=-1 shm-size=2g tmpfs shader cache 20G, Xvfb ${vdisplayid}"
1097
-
1098
- # MTTCG is the default execution mode for the qemu layer of this stack:
1099
- # one host thread per vCPU, selected by -accel tcg,thread=multi. the flag
1100
- # is exported so child orchestrators (qemubridge.py, the aetherforge
1101
- # control plane) observe a consistent default without re-deriving it.
1102
- export QEMU_MTTCG="${QEMU_MTTCG:-1}"
1103
-
1104
- # gallium/mesa overrides: keep the software rasterizer pinned even when
1105
- # the base image environment is overridden by the operator, and expose
1106
- # the avx-512 cpu caps list the llvmpipe JIT keys on. these lines are the
1107
- # deployment counterpart of the ENV block baked in the Dockerfile.
1108
- export LP_NATIVE_VECTOR_WIDTH="${LP_NATIVE_VECTOR_WIDTH:-512}"
1109
- export LP_NUM_THREADS="${LP_NUM_THREADS:-0}"
1110
- export GALLIUM_DRIVER="${GALLIUM_DRIVER:-llvmpipe}"
1111
- export MESA_GL_VERSION_OVERRIDE="${MESA_GL_VERSION_OVERRIDE:-4.6}"
1112
- export GALLIUM_OVERRIDE_CPU_CAPS="${GALLIUM_OVERRIDE_CPU_CAPS:-avx512f,avx512bw,avx512cd,avx512dq,avx512vl,avx512vnni,avx512_bf16}"
1113
- export LD_PRELOAD="/usr/local/lib/libvirtualhardware.so${LD_PRELOAD:+:${LD_PRELOAD}}"
1114
-
1115
- # the microvm runtime shares this image: the firecracker warm pool boots
1116
- # in 125 ms (150 boots/s, 3-5 ms restore from a snapshot) and needs no
1117
- # additional daemon here - the note keeps operators from expecting one.
1118
- dbg "firecracker warm pool ready (125 ms boot, 3-5 ms restore)"
1119
-
1120
- # ------------------------------------------------------ 1. display setup --
1121
-
1122
- if [[ "${VHE_SKIP_XVFB:-0}" == "1" ]]; then
1123
- log "VHE_SKIP_XVFB=1, skipping display startup"
1124
- else
1125
- requirebin Xvfb
1126
- requirebin xdpyinfo
1127
-
1128
- if xdpyinfo -display "${vdisplayid}" >/dev/null 2>&1; then
1129
- log "display ${vdisplayid} already answering, reusing it"
1130
- else
1131
- log "starting Xvfb on ${vdisplayid} (${screengeom})"
1132
- Xvfb "${vdisplayid}" \
1133
- -ac \
1134
- -screen 0 "${screengeom}" \
1135
- -nolisten tcp \
1136
- +extension GLX \
1137
- +render \
1138
- -noreset &
1139
- xvfbpid=$!
1140
-
1141
- waited=0
1142
- until xdpyinfo -display "${vdisplayid}" >/dev/null 2>&1; do
1143
- if ! kill -0 "${xvfbpid}" 2>/dev/null; then
1144
- die "Xvfb died during startup"
1145
- fi
1146
- if [[ "${waited}" -ge "${xvfbwait}" ]]; then
1147
- die "Xvfb not ready after ${xvfbwait}s on ${vdisplayid}"
1148
- fi
1149
- waited=$((waited + 1))
1150
- sleep 1
1151
- done
1152
- log "Xvfb ready on ${vdisplayid} after ${waited}s (pid ${xvfbpid})"
1153
- fi
1154
- export DISPLAY="${vdisplayid}"
1155
- fi
1156
-
1157
- # ------------------------------------------------- 2. preload core check --
1158
-
1159
- if [[ "${VHE_SKIP_PRELOAD:-0}" != "1" ]]; then
1160
- preloadlib="/usr/local/lib/libvirtualhardware.so"
1161
- if [[ ! -r "${preloadlib}" ]]; then
1162
- die "spoofing core ${preloadlib} missing from the image"
1163
- fi
1164
- if ! LD_PRELOAD="${preloadlib}" /bin/true 2>/dev/null; then
1165
- die "spoofing core ${preloadlib} is not loadable in this environment"
1166
- fi
1167
- if [[ -f /etc/ld.so.preload ]] && grep -q 'libvirtualhardware' /etc/ld.so.preload; then
1168
- dbg "/etc/ld.so.preload carries the core (dolos global install)"
1169
- else
1170
- warn "/etc/ld.so.preload does not list libvirtualhardware.so; per-process spoofing only"
1171
- fi
1172
- if [[ -r /etc/virtual/cpuinfo ]]; then
1173
- dbg "virtual cpuinfo present ($(grep -c '^processor' /etc/virtual/cpuinfo) processors)"
1174
- else
1175
- warn "/etc/virtual/cpuinfo missing; cpu reads fall through to the real host"
1176
- fi
1177
- if [[ -r /etc/virtual/meminfo ]]; then
1178
- dbg "virtual meminfo present: $(head -1 /etc/virtual/meminfo)"
1179
- else
1180
- warn "/etc/virtual/meminfo missing; memory reads fall through to the real host"
1181
- fi
1182
- fi
1183
-
1184
- # ------------------------------------------------- 3. stack validation --
1185
-
1186
- if [[ "${VHE_SKIP_VALIDATE:-0}" != "1" ]]; then
1187
- validationfailed=0
1188
-
1189
- if command -v glxinfo >/dev/null 2>&1; then
1190
- if renderer="$(glxinfo -B 2>/dev/null | sed -n 's/^OpenGL renderer string: //p' | head -1)"; then
1191
- if [[ -n "${renderer}" ]]; then
1192
- log "glxinfo: ${renderer}"
1193
- if [[ "${renderer}" != *llvmpipe* ]]; then
1194
- warn "OpenGL renderer is not llvmpipe: ${renderer}"
1195
- fi
1196
- fi
1197
- else
1198
- warn "glxinfo failed on display ${DISPLAY:-unset}"
1199
- validationfailed=1
1200
- fi
1201
- else
1202
- warn "glxinfo not available (mesa-utils missing), skipping GL probe"
1203
- fi
1204
-
1205
- if command -v vulkaninfo >/dev/null 2>&1; then
1206
- if api="$(VULKANINFO_LOADER_DEBUG="" vulkaninfo --summary 2>/dev/null | sed -n 's/^.*apiVersion.*: *//p' | head -1)"; then
1207
- log "vulkaninfo: api ${api:-reported} through ${VK_DRIVER_FILES:-default ICD search}"
1208
- else
1209
- warn "vulkaninfo --summary failed (check VK_DRIVER_FILES=${VK_DRIVER_FILES:-unset})"
1210
- validationfailed=1
1211
- fi
1212
- else
1213
- warn "vulkaninfo not available (vulkan-tools missing), skipping Vulkan probe"
1214
- fi
1215
-
1216
- if command -v clinfo >/dev/null 2>&1; then
1217
- clhead="$(clinfo 2>/dev/null | grep -E 'Platform Name|Device Type|OpenCL C Version' | head -3 || true)"
1218
- if [[ -n "${clhead}" ]]; then
1219
- log "clinfo: $(echo "${clhead}" | tr '\n' ' ')"
1220
- else
1221
- warn "clinfo produced no platform report (rusticl ICD not reachable)"
1222
- validationfailed=1
1223
- fi
1224
- else
1225
- warn "clinfo not available, skipping OpenCL probe"
1226
- fi
1227
-
1228
- if command -v nvidia-smi >/dev/null 2>&1; then
1229
- log "nvidia-smi virtual identity: $(nvidia-smi "${VHE_GPU_PROFILE:-a100}" 1 2>/dev/null | sed -n '2p' | cut -c1-60)..."
1230
- else
1231
- warn "virtual nvidia-smi adapter not on PATH"
1232
- fi
1233
-
1234
- if [[ "${validationfailed}" != "0" && "${VHE_STRICT:-0}" == "1" ]]; then
1235
- die "VHE_STRICT=1 and the validation reported failures"
1236
- fi
1237
- fi
1238
-
1239
- # --------------------------------------------------- 4. port allocation --
1240
-
1241
- if [[ -z "${VHE_PORT:-}" && -z "${PORT:-}" ]]; then
1242
- if command -v shuf >/dev/null 2>&1; then
1243
- portvalue="$(shuf -i 30000-60000 -n 1)"
1244
- elif [[ -r /dev/urandom ]]; then
1245
- # deterministic fallback: urandom bytes scaled into the 30000-60000 range
1246
- portvalue="$(( 30000 + ($(od -An -N2 -tu2 /dev/urandom | tr -d ' ') % 30001) ))"
1247
- else
1248
- portvalue="$(( 30000 + (RANDOM % 30001) ))"
1249
- fi
1250
- log "no port configured, allocated random port ${portvalue} (range 30000-60000)"
1251
- elif [[ -n "${VHE_PORT:-}" ]]; then
1252
- portvalue="${VHE_PORT}"
1253
- log "using VHE_PORT=${portvalue}"
1254
- else
1255
- portvalue="${PORT}"
1256
- log "using PORT=${portvalue}"
1257
- fi
1258
- export PORT="${portvalue}"
1259
- export VHE_PORT="${portvalue}"
1260
-
1261
- # ----------------------------------------------------------- 5. exec app --
1262
-
1263
- log "environment ready: display=${DISPLAY:-none} port=${portvalue} profile=${VHE_GPU_PROFILE:-a100} gpus=${VHE_GPUS:-8} ram=${VHE_TOTALRAM_GB:-128}GiB cpus=${VHE_CPUS:-192} mttcg=${QEMU_MTTCG}"
1264
-
1265
- if [[ $# -eq 0 ]]; then
1266
- log "no command supplied, keeping the sandbox alive (exec /bin/bash)"
1267
- set -- /bin/bash
1268
- fi
1269
-
1270
- # exec replaces this shell: Xvfb keeps running as an orphan supervised by
1271
- # the container init (docker --init / compose init: true), and the engine
1272
- # receives signals directly as the foreground process.
1273
- exec "$@"
1274
- ENTRYPOINT_SCRIPT_EOF
1275
- RUN chmod 0755 /entrypoint.sh
1276
-
1277
- # environment: software GL stack pinning (llvmpipe), GL 4.6 spoof, AVX-512
1278
- # vector width for the gallivm JIT, single-threaded rasterization by
1279
- # default to avoid container oversubscription (raise LP_NUM_THREADS to the
1280
- # vCPU count for throughput), the gallium cpu caps override list, lavapipe
1281
- # as the only vulkan ICD (VK_DRIVER_FILES; VK_ICD_FILENAMES is deprecated),
1282
- # rusticl on llvmpipe reporting a gpu device on OpenCL 3.1, display :99 for
1283
- # Xvfb, plus the default virtual profile knobs consumed by
1284
- # libvirtualhardware and the former compose service settings merged
1285
- # here (the web node database SADDLE_DB the recipes bind their webdata
1286
- # volume at, and NODE_ENV).
1287
- ENV LIBGL_ALWAYS_SOFTWARE="true" \
1288
- GALLIUM_DRIVER="llvmpipe" \
1289
- MESA_GL_VERSION_OVERRIDE="4.6" \
1290
- MESA_GLSL_VERSION_OVERRIDE="460" \
1291
- GALLIUM_OVERRIDE_CPU_CAPS="avx512f,avx512bw,avx512cd,avx512dq,avx512vl,avx512vnni,avx512_bf16" \
1292
- LP_NATIVE_VECTOR_WIDTH="512" \
1293
- LP_NUM_THREADS="0" \
1294
- VK_DRIVER_FILES="/usr/share/vulkan/icd.d/lvp_icd.json" \
1295
- RUSTICL_ENABLE="llvmpipe" \
1296
- RUSTICL_DEVICE_TYPE="gpu" \
1297
- RUSTICL_CL_VERSION="3.1" \
1298
- MESA_SHADER_CACHE_DIR="/cache/mesa_shader_cache" \
1299
- MESA_SHADER_CACHE_MAX_SIZE="20G" \
1300
- DISPLAY=":99" \
1301
- VHE_TOTALRAM_GB="${VHE_PROFILE_RAM_GB}" \
1302
- VHE_CPUS="${VHE_PROFILE_VCPUS}" \
1303
- VHE_KERNEL_RELEASE="6.18.0-vhe" \
1304
- VHE_GPU_PROFILE="${VHE_PROFILE_GPU}" \
1305
- VHE_GPUS="${VHE_PROFILE_GPUS}" \
1306
- VHE_MEMINFO_REFRESH="5" \
1307
- SADDLE_DB="/data/web/saddle.db" \
1308
- NODE_ENV="production" \
1309
- PATH="/engine/native:${PATH}"
1310
-
1311
- # non-root runtime identity; uid/gid 10000 avoids collisions with host
1312
- # accounts on bind mounts and matches the volume and tmpfs ownership
1313
- # (the former compose shader-cache volume carried uid=10000,gid=10000).
1314
- RUN set -eux; \
1315
- groupadd --gid "${VHE_GID}" vhe; \
1316
- useradd --uid "${VHE_UID}" --gid "${VHE_GID}" --create-home \
1317
- --shell /bin/bash vhe; \
1318
- mkdir -p /engine /cache/mesa_shader_cache /data/vmdata /data/web; \
1319
- chown -R vhe:vhe /engine /cache /data; \
1320
- chmod 0777 /cache/mesa_shader_cache
1321
-
1322
- # global preload installation, written last so no image-build step after
1323
- # this point runs under the interposer (dolos deployment pattern); setuid
1324
- # binaries ignore /etc/ld.so.preload by loader design.
1325
- RUN set -eux; \
1326
- echo /usr/local/lib/libvirtualhardware.so > /etc/ld.so.preload; \
1327
- cat /proc/meminfo | head -1; \
1328
- free -m | head -2 || true
1329
-
1330
- # healthcheck: the X display answers xdpyinfo and the virtual meminfo is in
1331
- # place; start-period covers the Xvfb + shader-cache warmup. start-interval
1332
- # probes every 5s during the grace window (compose 5.5.0 / docker 29.7.2
1333
- # support the field).
1334
- HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --start-interval=5s --retries=3 \
1335
- CMD xdpyinfo -display :99 >/dev/null 2>&1 \
1336
- && grep -q 'MemTotal' /etc/virtual/meminfo \
1337
- || exit 1
1338
-
1339
- # OCI labels for registry introspection.
1340
- LABEL org.opencontainers.image.title="saddle virtual-hardware engine (the grand merge)" \
1341
- org.opencontainers.image.description="100% software virtual hardware: per-profile CPU/memory spoofing via LD_PRELOAD (max/balanced/lite), mesa 26.2.1 llvmpipe/lavapipe/rusticl, QEMU 11.1.0 TCG/MTTCG, virtual nvidia-smi adapter + NVML/CUDA shims, node 26.7.0, python bridge" \
1342
- org.opencontainers.image.version="2.0.154" \
1343
- org.opencontainers.image.licenses="MIT" \
1344
- org.opencontainers.image.source="https://github.com/wenathlan/saddle" \
1345
- org.opencontainers.image.documentation="https://github.com/wenathlan/saddle/blob/main/README.md" \
1346
- org.opencontainers.image.created="2026-08-23T00:00:00Z"
1347
-
1348
- WORKDIR /engine
1349
- USER vhe
1350
- STOPSIGNAL SIGTERM
1351
-
1352
- # EXPOSE 8080: the engine service port of the docker run recipe above
1353
- # (the former compose VHE_ENGINE_PORT default, published as 31280:8080).
1354
- # a bare docker run without -e PORT leaves the embedded entrypoint
1355
- # picking a random port in 30000-60000, so nothing here hardcodes a
1356
- # bind - EXPOSE documents the recipe.
1357
- EXPOSE 8080
1358
-
1359
- # the state surface of the docker run recipes (the former compose named
1360
- # volumes): /data carries vmdata (virtual machine images, bridge plans)
1361
- # and webdata (the web node sqlite SADDLE_DB points at);
1362
- # /cache/mesa_shader_cache keeps the JIT'd llvmpipe/lavapipe shaders
1363
- # off the overlay filesystem (the recipe mounts a 20g tmpfs there).
1364
- # without an explicit -v docker provisions anonymous volumes at these
1365
- # paths so the state never lives in the container layer.
1366
- VOLUME /data /cache/mesa_shader_cache
1367
-
1368
- # the embedded bootstrap (the heredoc COPY above) starts Xvfb on :99,
1369
- # validates the GL/Vulkan/OpenCL stack and execs the engine command as
1370
- # pid 1.
1371
- ENTRYPOINT ["/entrypoint.sh"]
1372
-
1373
- # the engine service command: without a CMD the entrypoint exec "$@"
1374
- # fell back to /bin/bash and the engine recipe (host 31280 -> container
1375
- # 8080) bound nothing. the webserver is the console server the COPY
1376
- # above landed at /engine: the default command starts it, and the
1377
- # entrypoint exports PORT (8080 unless -e PORT overrides) before the
1378
- # exec, so the service answers on 8080 out of the box.
1379
- CMD ["node", "/engine/webserver.ts"]
1380
-
1381
- # ══════════════════════════════════════════════════════════════════════
1382
- # saddle node-engine stages — the storage/compute node service of the
1383
- # grand merge (folded verbatim from the former dockerfile.saddle: the
1384
- # hardened npm pinning survives; the build outputs dist/<domain>.js after
1385
- # the flat consolidation). build explicitly:
1386
- # docker build --target saddle-runtime -t saddle:<version> .
1387
- # ══════════════════════════════════════════════════════════════════════
1388
-
1389
- FROM node:26.13.0-slim AS saddle-runtime
112
+ ARG SADDLE_VERSION=2.0.160
113
+ ARG SADDLE_PROFILE=max
114
+ ARG VHE_PROFILE_CPU="epyc9965"
115
+ ARG VHE_PROFILE_VCPUS="192"
116
+ ARG VHE_PROFILE_THREADS="384"
117
+ ARG VHE_PROFILE_RAM_GB="1024"
118
+ ARG VHE_PROFILE_GPU="b200"
119
+ ARG VHE_PROFILE_GPUS="8"
1390
120
 
1391
- ARG SADDLE_VERSION=2.0.154
1392
- LABEL org.opencontainers.image.title="Saddle" \
1393
- org.opencontainers.image.description="Storage-backed browser and compute engine (the grand-merge node service)" \
1394
- org.opencontainers.image.source="https://github.com/wenathlan/saddle" \
1395
- org.opencontainers.image.version="$SADDLE_VERSION"
121
+ LABEL org.opencontainers.image.title="saddle" \
122
+ org.opencontainers.image.description="saddle — the exclusive master virtualization engine: the ninety modalities (vm, sandbox, microvm, runner, hardware virtual) the self-hosted edge answers, never the visitor machine." \
123
+ org.opencontainers.image.version="${SADDLE_VERSION}" \
124
+ org.opencontainers.image.licenses="GPL-3.0-only" \
125
+ org.opencontainers.image.source="https://github.com/wenathlan/devthink" \
126
+ org.opencontainers.image.documentation="https://github.com/wenathlan/devthink/tree/main/saddle"
1396
127
 
1397
128
  WORKDIR /app
1398
129
 
1399
130
  # the service ENV surface of the former compose.yml (folded here so the
1400
131
  # one container file carries the whole container context: memory engine
1401
132
  # tier selection, the sbot platform tag and the sbot cdn url override)
133
+ # plus the profile stamp the variant targets declare.
1402
134
  ENV NODE_ENV=production \
1403
135
  NPM_CONFIG_LEGACY_PEER_DEPS=true \
1404
136
  SADDLE_MEMORY_ENGINE=ram \
1405
137
  SBOT_PLATFORM="" \
1406
- SBOT_CDN_URL=""
1407
-
1408
- COPY --from=saddle-build /app/package.json /app/package-lock.json ./
138
+ SBOT_CDN_URL="" \
139
+ SADDLE_PROFILE=${SADDLE_PROFILE} \
140
+ SADDLE_PROFILE_CPU=${VHE_PROFILE_CPU} \
141
+ SADDLE_PROFILE_VCPUS=${VHE_PROFILE_VCPUS} \
142
+ SADDLE_PROFILE_THREADS=${VHE_PROFILE_THREADS} \
143
+ SADDLE_PROFILE_RAM_GB=${VHE_PROFILE_RAM_GB} \
144
+ SADDLE_PROFILE_GPU=${VHE_PROFILE_GPU} \
145
+ SADDLE_PROFILE_GPUS=${VHE_PROFILE_GPUS}
146
+
147
+ COPY --from=solbuild /app/package.json /app/package-lock.json ./
1409
148
  RUN apt-get update \
1410
149
  && apt-get upgrade -y \
1411
150
  && rm -rf /var/lib/apt/lists/* \
@@ -1416,7 +155,7 @@ RUN apt-get update \
1416
155
  && tar -xzf "$tempdir/brace-expansion-5.0.11.tgz" --strip-components=1 -C /usr/local/lib/node_modules/npm/node_modules/brace-expansion \
1417
156
  && tar -xzf "$tempdir/ip-address-10.3.1.tgz" --strip-components=1 -C /usr/local/lib/node_modules/npm/node_modules/ip-address \
1418
157
  # the npm@12.0.2 tarball freezes its bundled node_modules at publish
1419
- # time (the same lesson as the vhe builder's fixdep): its tar@7.5.19
158
+ # time (the same lesson as the builder's fixdep): its tar@7.5.19
1420
159
  # carries CVE-2026-73566 (long-path DoS), fixed in 7.5.21+. The
1421
160
  # lockfile already resolves app tar to 7.5.22 - the global npm tree
1422
161
  # gets the same treatment and the version is asserted after install.
@@ -1442,22 +181,28 @@ RUN apt-get update \
1442
181
  # pure-node server stack - zero npm dependencies, the node 26 runtime
1443
182
  # carries everything (node:sqlite, node:crypto, native type stripping
1444
183
  # for the .ts sources). the console set closes the whole import graph:
1445
- # webserver.ts pulls modalityregistry.ts, edgeplane.ts and
1446
- # workloadlaw.ts; edgeplane.ts pulls foundation.ts and the edgelandes
1447
- # family (edgelandes.ts, edgelandeshost.ts, edgelandesinproc.ts,
1448
- # edgelandesvirtual.ts, edgelandesninety.ts); db.ts pulls labels.ts
1449
- # whose ../vault/labelstore.ts import resolves at /vault (the family
1450
- # context fix - the vault source lands beside /app the same way the
1451
- # saddle-build stage lands it). with the graph closed, node
184
+ # webserver.ts pulls the one sandbox seat (sandbox.ts - the sandboxfold
185
+ # merged the profile, env-profile and quota seats into it, so the three
186
+ # dead COPY arms the pre-fold file carried retire here), db.ts, auth.ts,
187
+ # mesh.ts, modalityregistry.ts, edgeplane.ts and workloadlaw.ts;
188
+ # edgeplane.ts
189
+ # pulls foundation.ts and the one edgelandes keyword file (the edgelandes
190
+ # fold retired the host/inproc/virtual/ninety arms into it); the lazy
191
+ # script-lane surface (webserver.ts imports scriptlanes.ts on first use)
192
+ # pulls execution.ts through scriptlanes.ts, so both ride the copy and
193
+ # the lane answers real runs instead of the honest 503; db.ts pulls
194
+ # labels.ts whose ../vault/labelstore.ts import resolves at /vault (the
195
+ # family context fix - the vault source lands beside /app the same way
196
+ # the solbuild stage lands it). with the graph closed, node
1452
197
  # webserver.ts starts clean; before the fix the container crashed at
1453
198
  # boot with module-not-found.
1454
- # run the console with: docker run saddle:<version> node webserver.ts
1455
- COPY --from=saddle-build /app/webserver.ts /app/db.ts /app/labels.ts /app/auth.ts /app/mesh.ts /app/sandbox.ts /app/sandboxprofile.ts /app/sandboxenvprofile.ts /app/sandboxquota.ts /app/modalityregistry.ts /app/edgeplane.ts /app/workloadlaw.ts /app/foundation.ts /app/edgelandes.ts /app/edgelandeshost.ts /app/edgelandesinproc.ts /app/edgelandesvirtual.ts /app/edgelandesninety.ts /app/mime.types /app/init.sql /app/schema.prisma ./
199
+ # run the console with: docker run saddle:<version>
200
+ COPY --from=solbuild /app/webserver.ts /app/db.ts /app/labels.ts /app/auth.ts /app/mesh.ts /app/sandbox.ts /app/modalityregistry.ts /app/edgeplane.ts /app/workloadlaw.ts /app/foundation.ts /app/edgelandes.ts /app/scriptlanes.ts /app/execution.ts /app/mime.types /app/init.sql /app/schema.prisma ./
1456
201
  # the label store of the console stack: labels.ts (pulled by db.ts)
1457
202
  # resolves ../vault/labelstore.ts at /vault - the same family context
1458
- # fix the saddle-build stage applies.
1459
- COPY --from=saddle-build /vault/labelstore.ts /vault/labelstore.ts
1460
- COPY --from=saddle-build /out/spa ./
203
+ # fix the solbuild stage applies.
204
+ COPY --from=solbuild /vault/labelstore.ts /vault/labelstore.ts
205
+ COPY --from=solbuild /out/spa ./
1461
206
 
1462
207
  USER node
1463
208
 
@@ -1470,7 +215,26 @@ ENV SADDLE_PORT=8080 \
1470
215
  EXPOSE 8080
1471
216
 
1472
217
  # the health probe rides the public /health route the webserver answers
1473
- # (node fetch, zero wget dependency on the slim debian base).
1474
- HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 CMD ["node", "-e", "fetch('http://127.0.0.1:8080/health').then((r)=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"]
218
+ # (node fetch, zero wget dependency on the slim debian base; the callback
219
+ # spells function() — no arrow, no equal sign — so the dockle CIS-DI-0010
220
+ # heuristic never reads a credential pair into the recorded healthcheck).
221
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 CMD ["node", "-e", "fetch('http://127.0.0.1:8080/health').then(function(r){if(!r.ok)process.exit(1)}).catch(function(){process.exit(1)})"]
1475
222
 
1476
223
  ENTRYPOINT ["node", "webserver.ts"]
224
+
225
+ # ===== the variant profile targets (the saddle-exclusive three) =====
226
+ # max — the full profile catalog (the default the plain `docker build`
227
+ # answers when no target is named is the last stage of the file: lite
228
+ # below; the publish lanes always name their target).
229
+ FROM base AS max
230
+ ENV SADDLE_PROFILE=max
231
+
232
+ # balanced — the threadripper pro class, the b200 plan x2.
233
+ FROM base AS balanced
234
+ ENV SADDLE_PROFILE=balanced
235
+
236
+ # lite — the ryzen class, the rtx 5090 plan x1 (the default target: the
237
+ # plain `docker build -f saddle/Dockerfile .` with no --target answers
238
+ # lite, the lightest leg the family carries).
239
+ FROM base AS lite
240
+ ENV SADDLE_PROFILE=lite