@wenathlan/saddle 2.0.154 → 2.0.159

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