@wenathlan/saddle 2.0.154 → 2.0.155

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 (307) hide show
  1. package/Dockerfile +112 -41
  2. package/README.md +58 -59
  3. package/browser.ts +482 -0
  4. package/cli.ts +274 -0
  5. package/distribution.ts +731 -0
  6. package/docker.config +29 -8
  7. package/docs/artifactavailability.md +2 -2
  8. package/docs/browser.md +18 -1
  9. package/docs/{containerplatforms.2.1.0.md → containerplatforms210.md} +1 -1
  10. package/docs/{documentmap.1.8.12.md → documentmap1812.md} +1 -1
  11. package/docs/extension.md +13 -2
  12. package/docs/libraryapi.md +1 -1
  13. package/docs/{ninety.modalities.md → ninetymodalities.md} +87 -4
  14. package/docs/{planning.ninety.md → planningninety.md} +6 -6
  15. package/docs/{releasenotes.1.8.13.md → releasenotes1813.md} +2 -2
  16. package/docs/{releasenotes.1.8.14.md → releasenotes1814.md} +1 -1
  17. package/docs/{repo.synthesis.1.8.16.md → reposynthesis1816.md} +1 -1
  18. package/docs/todo.md +2 -2
  19. package/docs/{todo.1.8.15.md → todo1815.md} +1 -1
  20. package/docs/{todo.1.8.16.md → todo1816.md} +5 -5
  21. package/execution.ts +677 -0
  22. package/foundation.ts +314 -0
  23. package/index.ts +35 -27
  24. package/isolation.ts +84 -0
  25. package/modes.ts +170 -0
  26. package/mttg.config +1 -1
  27. package/orchestrator.ts +49 -15
  28. package/package.json +90 -75
  29. package/qemu.config +2 -2
  30. package/sandbox.ts +2094 -0
  31. package/scheduler.ts +1 -1
  32. package/server.ts +52 -0
  33. package/tsconfig.json +11 -12
  34. package/virtual.ts +1105 -0
  35. package/virtualgpu.ts +1 -1
  36. package/virtualization.ts +1 -1
  37. package/workloadlaw.ts +293 -0
  38. package/dist/App.d.ts +0 -5
  39. package/dist/App.js +0 -52
  40. package/dist/Sol/Sol.d.ts +0 -3
  41. package/dist/Sol/Sol.js +0 -46
  42. package/dist/Sol/agentbrowser/agentbrowser.d.ts +0 -9
  43. package/dist/Sol/agentbrowser/agentbrowser.js +0 -121
  44. package/dist/Sol/architecture/architecture.d.ts +0 -9
  45. package/dist/Sol/architecture/architecture.js +0 -93
  46. package/dist/Sol/compute/compute.d.ts +0 -9
  47. package/dist/Sol/compute/compute.js +0 -22
  48. package/dist/Sol/console/console.d.ts +0 -15
  49. package/dist/Sol/console/console.js +0 -292
  50. package/dist/Sol/dashboard/dashboard.d.ts +0 -10
  51. package/dist/Sol/dashboard/dashboard.js +0 -181
  52. package/dist/Sol/docs/docs.d.ts +0 -9
  53. package/dist/Sol/docs/docs.js +0 -28
  54. package/dist/Sol/home/home.d.ts +0 -35
  55. package/dist/Sol/home/home.js +0 -239
  56. package/dist/Sol/integrations/integrations.d.ts +0 -9
  57. package/dist/Sol/integrations/integrations.js +0 -83
  58. package/dist/Sol/intro/intro.d.ts +0 -7
  59. package/dist/Sol/intro/intro.js +0 -102
  60. package/dist/Sol/login/login.d.ts +0 -16
  61. package/dist/Sol/login/login.js +0 -49
  62. package/dist/Sol/notfound/notfound.d.ts +0 -9
  63. package/dist/Sol/notfound/notfound.js +0 -15
  64. package/dist/Sol/onboarding/onboarding.d.ts +0 -25
  65. package/dist/Sol/onboarding/onboarding.js +0 -60
  66. package/dist/Sol/playground/playground.d.ts +0 -9
  67. package/dist/Sol/playground/playground.js +0 -127
  68. package/dist/Sol/register/register.d.ts +0 -16
  69. package/dist/Sol/register/register.js +0 -193
  70. package/dist/Sol/shell/Shell.d.ts +0 -124
  71. package/dist/Sol/shell/Shell.js +0 -335
  72. package/dist/Sol/wingets/ComputeBootladder.d.ts +0 -6
  73. package/dist/Sol/wingets/ComputeBootladder.js +0 -71
  74. package/dist/Sol/wingets/ComputeModalitycatalog.d.ts +0 -7
  75. package/dist/Sol/wingets/ComputeModalitycatalog.js +0 -71
  76. package/dist/Sol/wingets/ConsoleApibadge.d.ts +0 -7
  77. package/dist/Sol/wingets/ConsoleApibadge.js +0 -5
  78. package/dist/Sol/wingets/ConsoleEdgelaw.d.ts +0 -6
  79. package/dist/Sol/wingets/ConsoleEdgelaw.js +0 -36
  80. package/dist/Sol/wingets/ConsoleEventstimeline.d.ts +0 -19
  81. package/dist/Sol/wingets/ConsoleEventstimeline.js +0 -12
  82. package/dist/Sol/wingets/ConsoleSnapshotcontrols.d.ts +0 -8
  83. package/dist/Sol/wingets/ConsoleSnapshotcontrols.js +0 -8
  84. package/dist/Sol/wingets/ConsoleSpecpanel.d.ts +0 -21
  85. package/dist/Sol/wingets/ConsoleSpecpanel.js +0 -75
  86. package/dist/Sol/wingets/ConsoleTerminal.d.ts +0 -22
  87. package/dist/Sol/wingets/ConsoleTerminal.js +0 -77
  88. package/dist/Sol/wingets/DashboardAccountpanel.d.ts +0 -18
  89. package/dist/Sol/wingets/DashboardAccountpanel.js +0 -65
  90. package/dist/Sol/wingets/DashboardAdminpanel.d.ts +0 -6
  91. package/dist/Sol/wingets/DashboardAdminpanel.js +0 -90
  92. package/dist/Sol/wingets/DashboardAuditlog.d.ts +0 -8
  93. package/dist/Sol/wingets/DashboardAuditlog.js +0 -50
  94. package/dist/Sol/wingets/DashboardCreatesandboxform.d.ts +0 -10
  95. package/dist/Sol/wingets/DashboardCreatesandboxform.js +0 -71
  96. package/dist/Sol/wingets/DashboardEventstimeline.d.ts +0 -8
  97. package/dist/Sol/wingets/DashboardEventstimeline.js +0 -83
  98. package/dist/Sol/wingets/DashboardGlobalsandboxestable.d.ts +0 -8
  99. package/dist/Sol/wingets/DashboardGlobalsandboxestable.js +0 -57
  100. package/dist/Sol/wingets/DashboardMeshnodestable.d.ts +0 -8
  101. package/dist/Sol/wingets/DashboardMeshnodestable.js +0 -103
  102. package/dist/Sol/wingets/DashboardSandboxlist.d.ts +0 -10
  103. package/dist/Sol/wingets/DashboardSandboxlist.js +0 -163
  104. package/dist/Sol/wingets/DashboardUserstable.d.ts +0 -8
  105. package/dist/Sol/wingets/DashboardUserstable.js +0 -78
  106. package/dist/Sol/wingets/HomeEntrygate.d.ts +0 -23
  107. package/dist/Sol/wingets/HomeEntrygate.js +0 -73
  108. package/dist/Sol/wingets/HomeMetricstrip.d.ts +0 -9
  109. package/dist/Sol/wingets/HomeMetricstrip.js +0 -4
  110. package/dist/Sol/wingets/HomeRuntimediagram.d.ts +0 -1
  111. package/dist/Sol/wingets/HomeRuntimediagram.js +0 -22
  112. package/dist/Sol/wingets/LoginForm.d.ts +0 -19
  113. package/dist/Sol/wingets/LoginForm.js +0 -141
  114. package/dist/Sol/wingets/RegisterPasswordconfirm.d.ts +0 -13
  115. package/dist/Sol/wingets/RegisterPasswordconfirm.js +0 -16
  116. package/dist/Sol/wingets/RegisterPasswordmeter.d.ts +0 -19
  117. package/dist/Sol/wingets/RegisterPasswordmeter.js +0 -65
  118. package/dist/Sol/wingets/RegisterUsernamerules.d.ts +0 -11
  119. package/dist/Sol/wingets/RegisterUsernamerules.js +0 -16
  120. package/dist/Sol/wingets/index.d.ts +0 -39
  121. package/dist/Sol/wingets/index.js +0 -39
  122. package/dist/alternatives.d.ts +0 -508
  123. package/dist/alternatives.js +0 -950
  124. package/dist/api.d.ts +0 -154
  125. package/dist/api.js +0 -174
  126. package/dist/auth.d.ts +0 -268
  127. package/dist/auth.js +0 -476
  128. package/dist/automation.d.ts +0 -425
  129. package/dist/automation.js +0 -566
  130. package/dist/browser.d.ts +0 -561
  131. package/dist/browser.js +0 -1058
  132. package/dist/capacitor.config.d.ts +0 -45
  133. package/dist/capacitor.config.js +0 -48
  134. package/dist/catalog.d.ts +0 -43
  135. package/dist/catalog.js +0 -44
  136. package/dist/cli.d.ts +0 -16
  137. package/dist/cli.js +0 -168
  138. package/dist/communication.d.ts +0 -181
  139. package/dist/communication.js +0 -376
  140. package/dist/compute.d.ts +0 -1160
  141. package/dist/compute.js +0 -2272
  142. package/dist/dashboardlib.d.ts +0 -58
  143. package/dist/dashboardlib.js +0 -161
  144. package/dist/db.d.ts +0 -569
  145. package/dist/db.js +0 -1080
  146. package/dist/distribution.d.ts +0 -528
  147. package/dist/distribution.js +0 -810
  148. package/dist/drizzle.config.d.ts +0 -20
  149. package/dist/drizzle.config.js +0 -18
  150. package/dist/edgelandes.d.ts +0 -70
  151. package/dist/edgelandes.host.d.ts +0 -26
  152. package/dist/edgelandes.host.js +0 -1089
  153. package/dist/edgelandes.inproc.d.ts +0 -27
  154. package/dist/edgelandes.inproc.js +0 -952
  155. package/dist/edgelandes.js +0 -1133
  156. package/dist/edgelandes.ninety.d.ts +0 -31
  157. package/dist/edgelandes.ninety.js +0 -174
  158. package/dist/edgelandes.virtual.d.ts +0 -25
  159. package/dist/edgelandes.virtual.js +0 -629
  160. package/dist/edgeplane.d.ts +0 -121
  161. package/dist/edgeplane.js +0 -341
  162. package/dist/examplelocaljob.d.ts +0 -1
  163. package/dist/examplelocaljob.js +0 -14
  164. package/dist/execution.d.ts +0 -529
  165. package/dist/execution.js +0 -760
  166. package/dist/family.d.ts +0 -16
  167. package/dist/family.js +0 -42
  168. package/dist/familyurl.d.ts +0 -21
  169. package/dist/familyurl.js +0 -24
  170. package/dist/format.d.ts +0 -18
  171. package/dist/format.js +0 -59
  172. package/dist/foundation.d.ts +0 -172
  173. package/dist/foundation.js +0 -312
  174. package/dist/index.d.ts +0 -735
  175. package/dist/index.js +0 -1020
  176. package/dist/integration.d.ts +0 -235
  177. package/dist/integration.js +0 -228
  178. package/dist/intelligence.d.ts +0 -93
  179. package/dist/intelligence.js +0 -144
  180. package/dist/isolation.d.ts +0 -128
  181. package/dist/isolation.js +0 -95
  182. package/dist/labels.d.ts +0 -665
  183. package/dist/labels.js +0 -707
  184. package/dist/localauth.d.ts +0 -127
  185. package/dist/localauth.js +0 -413
  186. package/dist/media.d.ts +0 -343
  187. package/dist/media.js +0 -985
  188. package/dist/mesh.d.ts +0 -177
  189. package/dist/mesh.js +0 -468
  190. package/dist/modalityregistry.d.ts +0 -109
  191. package/dist/modalityregistry.js +0 -1386
  192. package/dist/modes.d.ts +0 -358
  193. package/dist/modes.js +0 -133
  194. package/dist/onboardingstate.d.ts +0 -79
  195. package/dist/onboardingstate.js +0 -282
  196. package/dist/orchestrator.d.ts +0 -1338
  197. package/dist/orchestrator.js +0 -4310
  198. package/dist/packager/targetcli.js +0 -4
  199. package/dist/paths.d.ts +0 -2
  200. package/dist/paths.js +0 -14
  201. package/dist/performance.d.ts +0 -415
  202. package/dist/performance.js +0 -826
  203. package/dist/quantum.d.ts +0 -809
  204. package/dist/quantum.js +0 -2072
  205. package/dist/render.d.ts +0 -380
  206. package/dist/render.js +0 -1072
  207. package/dist/runall.d.ts +0 -2
  208. package/dist/runall.js +0 -39
  209. package/dist/runtimeprobe.d.ts +0 -4
  210. package/dist/runtimeprobe.js +0 -8
  211. package/dist/sandbox.d.ts +0 -355
  212. package/dist/sandbox.js +0 -1721
  213. package/dist/sandboxenvprofile.d.ts +0 -29
  214. package/dist/sandboxenvprofile.js +0 -89
  215. package/dist/sandboxprofile.d.ts +0 -137
  216. package/dist/sandboxprofile.js +0 -241
  217. package/dist/sandboxqueue.d.ts +0 -108
  218. package/dist/sandboxqueue.js +0 -201
  219. package/dist/sandboxquota.d.ts +0 -121
  220. package/dist/sandboxquota.js +0 -236
  221. package/dist/scheduler.d.ts +0 -595
  222. package/dist/scheduler.js +0 -1258
  223. package/dist/scriptlanes.d.ts +0 -182
  224. package/dist/scriptlanes.js +0 -660
  225. package/dist/scripts.buildengine.d.ts +0 -1
  226. package/dist/scripts.buildengine.js +0 -39
  227. package/dist/scripts.securitygate.d.ts +0 -1
  228. package/dist/scripts.securitygate.js +0 -68
  229. package/dist/scripts.validateflatnative.d.ts +0 -1
  230. package/dist/scripts.validateflatnative.js +0 -72
  231. package/dist/security.d.ts +0 -500
  232. package/dist/security.js +0 -1169
  233. package/dist/seedcatalog.d.ts +0 -13
  234. package/dist/seedcatalog.js +0 -46
  235. package/dist/server.d.ts +0 -20
  236. package/dist/server.js +0 -53
  237. package/dist/speccatalog.d.ts +0 -31
  238. package/dist/speccatalog.js +0 -163
  239. package/dist/tiers.d.ts +0 -1001
  240. package/dist/tiers.js +0 -2169
  241. package/dist/utils.d.ts +0 -2
  242. package/dist/utils.js +0 -5
  243. package/dist/vaulthandoff.d.ts +0 -141
  244. package/dist/vaulthandoff.js +0 -444
  245. package/dist/virtual.d.ts +0 -640
  246. package/dist/virtual.js +0 -1284
  247. package/dist/virtualcpu.d.ts +0 -315
  248. package/dist/virtualcpu.js +0 -1180
  249. package/dist/virtualgpu.d.ts +0 -344
  250. package/dist/virtualgpu.js +0 -1000
  251. package/dist/virtualization.d.ts +0 -606
  252. package/dist/virtualization.js +0 -1186
  253. package/dist/virtualmemory.d.ts +0 -817
  254. package/dist/virtualmemory.js +0 -1795
  255. package/dist/vite.config.d.ts +0 -3
  256. package/dist/vite.config.js +0 -48
  257. package/dist/vitest.config.d.ts +0 -2
  258. package/dist/vitest.config.js +0 -25
  259. package/dist/webserver.d.ts +0 -166
  260. package/dist/webserver.js +0 -2226
  261. package/dist/workloadlaw.d.ts +0 -133
  262. package/dist/workloadlaw.js +0 -193
  263. package/manifest.json +0 -20
  264. package/media.ts +0 -1337
  265. /package/docs/{architecture.1.8.19.md → architecture1819.md} +0 -0
  266. /package/docs/{architecture.1.8.19.mmd → architecture1819.mmd} +0 -0
  267. /package/docs/{artifactresearch.1.8.9.md → artifactresearch189.md} +0 -0
  268. /package/docs/{batches.1816.to.1820.md → batches1816to1820.md} +0 -0
  269. /package/docs/{brancharchive.2026.08.18.md → brancharchive20260818.md} +0 -0
  270. /package/docs/{containerplatforms.1.8.17.md → containerplatforms1817.md} +0 -0
  271. /package/docs/{e2ugh.engine.md → e2ughengine.md} +0 -0
  272. /package/docs/{flatbuildresearch.1.8.11.md → flatbuildresearch1811.md} +0 -0
  273. /package/docs/{licenseaudit.1.8.12.md → licenseaudit1812.md} +0 -0
  274. /package/docs/{mobileconversionresearch.1.8.10.md → mobileconversionresearch1810.md} +0 -0
  275. /package/docs/{nativeidentityresearch.1.8.11.md → nativeidentityresearch1811.md} +0 -0
  276. /package/docs/{native.wrappers.md → nativewrappers.md} +0 -0
  277. /package/docs/{planning.1.8.18.md → planning1818.md} +0 -0
  278. /package/docs/{planning.1.8.19.md → planning1819.md} +0 -0
  279. /package/docs/{planning.engine.md → planningengine.md} +0 -0
  280. /package/docs/{releasenotes.1.8.12.md → releasenotes1812.md} +0 -0
  281. /package/docs/{releasenotes.1.8.15.md → releasenotes1815.md} +0 -0
  282. /package/docs/{releasenotes.1.8.16.md → releasenotes1816.md} +0 -0
  283. /package/docs/{releasenotes.1.8.17.md → releasenotes1817.md} +0 -0
  284. /package/docs/{releasenotes.1.8.18.md → releasenotes1818.md} +0 -0
  285. /package/docs/{releasenotes.1.8.19.md → releasenotes1819.md} +0 -0
  286. /package/docs/{reorganization.1.8.8.md → reorganization188.md} +0 -0
  287. /package/docs/{reorganization.1.8.9.md → reorganization189.md} +0 -0
  288. /package/docs/{repo.analysis.1.8.21.json → repoanalysis1821.json} +0 -0
  289. /package/docs/{repo.candidates.1.8.21.jsonl → repocandidates1821.jsonl} +0 -0
  290. /package/docs/{repo.research.1.8.16.md → reporesearch1816.md} +0 -0
  291. /package/docs/{repo.research.1.8.21.md → reporesearch1821.md} +0 -0
  292. /package/docs/{repo.selected.1.8.21.json → reposelected1821.json} +0 -0
  293. /package/docs/{repo.synthesis.1.8.21.md → reposynthesis1821.md} +0 -0
  294. /package/docs/{research.1.8.18.isolation.md → research1818isolation.md} +0 -0
  295. /package/docs/{research.1.8.19.virtual.browser.md → research1819virtualbrowser.md} +0 -0
  296. /package/docs/{scanningresearch.1.8.11.md → scanningresearch1811.md} +0 -0
  297. /package/docs/{securityaudit.1.8.7.md → securityaudit187.md} +0 -0
  298. /package/docs/{securityreferences.1.8.11.md → securityreferences1811.md} +0 -0
  299. /package/docs/{securityresearch.1.8.11.md → securityresearch1811.md} +0 -0
  300. /package/docs/{signingresearch.1.8.12.md → signingresearch1812.md} +0 -0
  301. /package/docs/{tauri.glib.risk.1.8.11.md → tauriglibrisk1811.md} +0 -0
  302. /package/docs/{todo.1.8.18.md → todo1818.md} +0 -0
  303. /package/docs/{todo.1.8.19.md → todo1819.md} +0 -0
  304. /package/docs/{toolchainresearch.1.8.9.md → toolchainresearch189.md} +0 -0
  305. /package/docs/{web.duplicatecleanup.md → webduplicatecleanup.md} +0 -0
  306. /package/docs/{web.ideas.md → webideas.md} +0 -0
  307. /package/docs/{workflowimprovements.2026.08.19.md → workflowimprovements20260819.md} +0 -0
@@ -1,1089 +0,0 @@
1
- // # edgelandes.host.ts — the host binary lane executors of the ninety
2
- // modalities wave, group a: twenty-five planned sandbox modalities
3
- // wired as honest host-binary execution lanes — the sandbox jails, the
4
- // container extensions, the runtimes, the screens, the mobile seat, the
5
- // builds, the ci runners, the network tunnels and the storage daemons
6
- // the engine host may carry. Every lane follows the spawn pattern of
7
- // edgelandes.ts: the runtime is probed first (an absent runtime answers
8
- // the coded HOST_RUNTIME_ABSENT naming the runtime — a lane never fakes
9
- // a run), the code field lands in a private temp directory, the inputs
10
- // ride inputs.json through the SADDLE_EDGE_INPUTS env, the runtime
11
- // spawns under the caller's hard deadline with both streams capped at
12
- // 256 kb, and the temp directory dies in the finally no matter how the
13
- // run answered (the owner's law: the lanes act for the system
14
- // server-side, the visitor browser, gpu, cpu and storage are never
15
- // touched, and no lane leaves bytes behind on the host). The shared
16
- // helpers are local on purpose: edgelandes.ts exports its contract
17
- // pieces only, so this file carries its own spawn, deadline and temp
18
- // surface — the same law with its own seat. All twenty-five lanes are
19
- // kind live: the executor exists and drives the runtime when present,
20
- // exactly like language-ruby rides live though a host may not carry it.
21
- import { spawn } from "node:child_process";
22
- import { existsSync } from "node:fs";
23
- import { copyFile, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
24
- import { createRequire } from "node:module";
25
- import { connect } from "node:net";
26
- import { tmpdir } from "node:os";
27
- import { basename, dirname, join } from "node:path";
28
- import process from "node:process";
29
- import { edgelaneerror, probehostbinary } from "./edgelandes.ts";
30
- /* ════════════════════════════════════════════════════════════════════ */
31
- /* Section 2: the shared host execution surface — spawn, deadline, temp. */
32
- /* ════════════════════════════════════════════════════════════════════ */
33
- /** the per stream capture ceiling: 256 kb of stdout or stderr, the honest
34
- * bound every host lane reports under. */
35
- const OUTPUTCAPBYTES = 262144;
36
- /** spawns one host runtime under a hard deadline, captures both streams
37
- * (capped), optionally feeds a stdin payload, kills on timeout and
38
- * answers the coded failures honestly: a missing binary answers
39
- * HOST_RUNTIME_ABSENT, a nonzero exit answers RUN_EXITED_NONZERO
40
- * carrying the stderr, a blown deadline answers RUN_DEADLINE_EXCEEDED. */
41
- function runhostprocess(command, args, options) {
42
- return new Promise((resolve, reject) => {
43
- // detached makes the child a process group leader: the deadline kill
44
- // takes the whole group down (podman spawns conmon and the container
45
- // shims that would otherwise outlive the cli and hold the pipes open —
46
- // the close event would never fire and the lane would hang past its
47
- // own budget)
48
- const child = spawn(command, args, {
49
- cwd: options.cwd,
50
- env: options.env === undefined ? process.env : { ...process.env, ...options.env },
51
- stdio: [options.stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"],
52
- detached: true,
53
- });
54
- if (options.stdin !== undefined && child.stdin !== null) {
55
- // a payload through /dev/stdin: an early exit must not crash the
56
- // engine over a broken pipe, the run answers the exit it got
57
- child.stdin.on("error", () => {
58
- // the stream error is the child's own honest outcome
59
- });
60
- child.stdin.end(options.stdin);
61
- }
62
- const outchunks = [];
63
- const errchunks = [];
64
- let timedout = false;
65
- const makecollector = (chunks) => {
66
- let used = 0;
67
- return (chunk) => {
68
- if (used < OUTPUTCAPBYTES) {
69
- chunks.push(chunk);
70
- used += chunk.byteLength;
71
- }
72
- };
73
- };
74
- child.stdout?.on("data", makecollector(outchunks));
75
- child.stderr?.on("data", makecollector(errchunks));
76
- const timer = setTimeout(() => {
77
- timedout = true;
78
- try {
79
- process.kill(-child.pid, "SIGKILL");
80
- }
81
- catch {
82
- child.kill("SIGKILL");
83
- }
84
- }, options.deadline);
85
- child.on("error", (error) => {
86
- clearTimeout(timer);
87
- reject(edgelaneerror("HOST_RUNTIME_ABSENT", `the host runtime ${command} is absent: ${errormessage(error)}`));
88
- });
89
- child.on("close", (code, signal) => {
90
- clearTimeout(timer);
91
- const stdout = Buffer.concat(outchunks).toString("utf8");
92
- const stderr = Buffer.concat(errchunks).toString("utf8");
93
- if (timedout) {
94
- reject(edgelaneerror("RUN_DEADLINE_EXCEEDED", `the lane exceeded its ${options.deadline} ms deadline and was killed`, {
95
- stderr,
96
- }));
97
- }
98
- else if (code !== 0) {
99
- reject(edgelaneerror("RUN_EXITED_NONZERO", `the lane process ${command} exited with code ${code ?? signal}`, {
100
- stderr,
101
- }));
102
- }
103
- else {
104
- // the hard cap law: the collector admits chunks under the ceiling
105
- // but one large chunk may ride past the line — the joined buffer
106
- // truncates to the ceiling so the capture never exceeds it
107
- resolve({
108
- stdout: stdout.length > OUTPUTCAPBYTES ? stdout.slice(0, OUTPUTCAPBYTES) : stdout,
109
- stderr: stderr.length > OUTPUTCAPBYTES ? stderr.slice(0, OUTPUTCAPBYTES) : stderr,
110
- });
111
- }
112
- });
113
- });
114
- }
115
- /** runs fn inside one private temp directory (mkdtemp under the os
116
- * tmpdir, the saddleedgehost prefix) and removes it no matter how fn
117
- * answers — the host lanes never leave workspace bytes behind. */
118
- async function withtempdirhost(fn) {
119
- const dir = await mkdtemp(join(tmpdir(), "saddleedgehost"));
120
- try {
121
- return await fn(dir);
122
- }
123
- finally {
124
- await rm(dir, { recursive: true, force: true });
125
- }
126
- }
127
- /** writes the run inputs as inputs.json beside the program and answers
128
- * the env overlay pointing at it (SADDLE_EDGE_INPUTS); host lane
129
- * programs read their inputs from that file when the caller sent any. */
130
- async function writeinputsfile(dir, inputs) {
131
- if (inputs === undefined)
132
- return undefined;
133
- const path = join(dir, "inputs.json");
134
- await writeFile(path, JSON.stringify(inputs ?? null), "utf8");
135
- return { SADDLE_EDGE_INPUTS: path };
136
- }
137
- /** writes the code as script.sh inside the private dir and runs one
138
- * command with the inputs surface and the extra env merged in: the
139
- * shared recipe of the bash-shaped host lanes. */
140
- async function runscript(command, args, dir, code, inputs, deadline, extraenv = {}) {
141
- await writeFile(join(dir, "script.sh"), code, "utf8");
142
- const inputenv = (await writeinputsfile(dir, inputs)) ?? {};
143
- const run = await runhostprocess(command, args, { cwd: dir, deadline, env: { ...inputenv, ...extraenv } });
144
- return { stdout: run.stdout, stderr: run.stderr };
145
- }
146
- /** the safe message of an unknown thrown value (no stack ever rides a lane answer). */
147
- function errormessage(error) {
148
- return error instanceof Error ? error.message : String(error ?? "unknown lane failure");
149
- }
150
- /** the coded absence of one host runtime: the lane never fakes a run
151
- * over a binary the host does not carry — the message names the runtime
152
- * and the lane, the code rides the machine surface. */
153
- function absentruntime(laneid, runtime, note = "") {
154
- throw edgelaneerror("HOST_RUNTIME_ABSENT", `the host runtime ${runtime} is absent: the lane ${laneid} requires it on PATH${note.length > 0 ? ` ${note}` : ""}`);
155
- }
156
- /** whether the engine process runs as root: the chroot and the oci
157
- * runtimes need the seat for their namespace work, and the lane answers
158
- * the coded privilege refusal instead of a wrapped errno. */
159
- function isroot() {
160
- return typeof process.getuid === "function" && process.getuid() === 0;
161
- }
162
- /** the monotonic lane id counter: deterministic container ids without
163
- * crypto — the ids only need uniqueness per engine process. */
164
- let laneidseat = 0;
165
- function nextlaneid(prefix) {
166
- laneidseat += 1;
167
- return `${prefix}-${process.pid}-${laneidseat}`;
168
- }
169
- /** waits for one loopback tcp port to accept a connection within the
170
- * budget: the honest readiness probe of the tunnel lanes (a port that
171
- * never opens answers the deadline refusal, never a fake tunnel). */
172
- function waitforport(port, budgetms) {
173
- return new Promise((resolve) => {
174
- const endsat = Date.now() + budgetms;
175
- const attempt = () => {
176
- if (Date.now() > endsat) {
177
- resolve(false);
178
- return;
179
- }
180
- const socket = connect(port, "127.0.0.1");
181
- const settle = (answer) => {
182
- socket.removeAllListeners();
183
- socket.destroy();
184
- if (answer) {
185
- resolve(true);
186
- return;
187
- }
188
- if (Date.now() >= endsat) {
189
- resolve(false);
190
- return;
191
- }
192
- setTimeout(attempt, 100);
193
- };
194
- socket.once("connect", () => settle(true));
195
- socket.once("error", () => settle(false));
196
- };
197
- attempt();
198
- });
199
- }
200
- /** waits for one spawned child to exit within the budget (the daemon
201
- * teardown of the tunnel lanes; a false answer hands the caller the
202
- * kill decision). */
203
- function waitforexit(child, budgetms) {
204
- return new Promise((resolve) => {
205
- if (child.exitCode !== null || child.signalCode !== null) {
206
- resolve(true);
207
- return;
208
- }
209
- let settled = false;
210
- const finish = (answer) => {
211
- if (settled)
212
- return;
213
- settled = true;
214
- clearTimeout(timer);
215
- resolve(answer);
216
- };
217
- const timer = setTimeout(() => finish(false), budgetms);
218
- child.once("exit", () => finish(true));
219
- });
220
- }
221
- /** probes the docker surface of the host: the binary on PATH or the
222
- * daemon socket (the same honest seat the engine's docker lane rides). */
223
- function probedockersurface() {
224
- return probehostbinary("docker") || existsSync("/var/run/docker.sock");
225
- }
226
- /* ════════════════════════════════════════════════════════════════════ */
227
- /* Section 3: the sandbox jail lanes — bubblewrap, nsjail and chroot. */
228
- /* ════════════════════════════════════════════════════════════════════ */
229
- /** sandbox-bubblewrap: the user-namespace jail lane. The code rides a
230
- * temp script.sh executed inside bwrap with the host root bound
231
- * read-only, every namespace unshared and the jail dying with the
232
- * engine parent — the honest bubblewrap seat this host may carry. */
233
- async function sandboxbubblewraplane(code, inputs, deadline) {
234
- if (!probehostbinary("bwrap")) {
235
- absentruntime("sandbox-bubblewrap", "bwrap");
236
- }
237
- return withtempdirhost((dir) => runscript("bwrap", ["--ro-bind", "/", "/", "--unshare-all", "--die-with-parent", "/bin/bash", "script.sh"], dir, code, inputs, deadline));
238
- }
239
- /** sandbox-nsjail: the process jail lane. The lane writes a minimal
240
- * nsjail config (mode ONCE, the deadline as time_limit, the temp cwd,
241
- * a read-only root bind and a writable tmpfs scratch) and runs the
242
- * script through the jail — the config and the script live and die in
243
- * the private temp dir. */
244
- async function sandboxnsjaillane(code, inputs, deadline) {
245
- if (!probehostbinary("nsjail")) {
246
- absentruntime("sandbox-nsjail", "nsjail");
247
- }
248
- return withtempdirhost(async (dir) => {
249
- const config = [
250
- "# the saddle edge nsjail lane: one process under the jail, the",
251
- "# deadline rides time_limit, the root bind keeps the system visible",
252
- "# read-only and the tmpfs at /scratch is the writable seat.",
253
- 'name: "saddle-edge-nsjail"',
254
- "mode: ONCE",
255
- `time_limit: ${Math.max(1, Math.ceil(deadline / 1000))}`,
256
- `cwd: "${dir}"`,
257
- "mount {",
258
- ' src: "/"',
259
- ' dst: "/"',
260
- " is_bind: true",
261
- "}",
262
- "mount {",
263
- ' dst: "/scratch"',
264
- ' fstype: "tmpfs"',
265
- " rw: true",
266
- "}",
267
- "",
268
- ].join("\n");
269
- await writeFile(join(dir, "config.nsjail"), config, "utf8");
270
- return await runscript("nsjail", ["--config", "config.nsjail", "--", "/bin/bash", "script.sh"], dir, code, inputs, deadline);
271
- });
272
- }
273
- /** builds the minimal chroot tree: /bin/bash with its ldd dependency
274
- * set (best effort — fakechroot maps the real filesystem through its
275
- * preload shim, the copies serve the real chroot seat), the lane script
276
- * at /script.sh and the inputs at /inputs.json so the jailed program
277
- * reads them at their in-tree paths. */
278
- async function buildchroottree(root, code, inputs) {
279
- if (!existsSync("/bin/bash")) {
280
- throw edgelaneerror("RUNTIME_BUNDLE_UNAVAILABLE", "the lane sandbox-chroot cannot build its tree: /bin/bash is absent from the host (the lane never fakes a root)");
281
- }
282
- await mkdir(join(root, "bin"), { recursive: true });
283
- await mkdir(join(root, "tmp"), { recursive: true });
284
- await copyFile("/bin/bash", join(root, "bin", "bash"));
285
- try {
286
- const deps = await runhostprocess("ldd", ["/bin/bash"], { cwd: root, deadline: 2000 });
287
- for (const line of deps.stdout.split("\n")) {
288
- for (const token of line.split(/\s+/)) {
289
- if (!token.startsWith("/") || !token.includes(".so"))
290
- continue;
291
- try {
292
- await mkdir(join(root, dirname(token)), { recursive: true });
293
- await copyFile(token, join(root, token));
294
- }
295
- catch {
296
- // an optional dependency copy that fails leaves the runtime's
297
- // own honest loader error at run time — the tree never fakes a lib
298
- }
299
- }
300
- }
301
- }
302
- catch {
303
- // ldd absent or failing: the fakechroot seat maps the real fs and
304
- // needs no copies, the real chroot seat answers its own loader error
305
- }
306
- await writeFile(join(root, "script.sh"), code, "utf8");
307
- if (inputs !== undefined) {
308
- await writeFile(join(root, "inputs.json"), JSON.stringify(inputs ?? null), "utf8");
309
- }
310
- }
311
- /** sandbox-chroot: the chroot jail lane with the honest privilege law:
312
- * a real chroot needs root, so a non-root engine requires fakechroot —
313
- * a host carrying neither root nor fakechroot answers the coded
314
- * privilege refusal, never a wrapped errno or a fake root. */
315
- async function sandboxchrootlane(code, inputs, deadline) {
316
- const chrootpresent = probehostbinary("chroot");
317
- const fakepresent = probehostbinary("fakechroot");
318
- if (!chrootpresent && !fakepresent) {
319
- absentruntime("sandbox-chroot", "chroot", "(and fakechroot, the unprivileged seat, is absent too)");
320
- }
321
- if (!fakepresent && !isroot()) {
322
- throw edgelaneerror("HOST_PRIVILEGE_REQUIRED", "the lane sandbox-chroot requires root for a real chroot and the host carries no fakechroot (the lane never fakes a root)");
323
- }
324
- return withtempdirhost(async (dir) => {
325
- const tree = join(dir, "root");
326
- await buildchroottree(tree, code, inputs);
327
- const env = {};
328
- if (inputs !== undefined)
329
- env.SADDLE_EDGE_INPUTS = "/inputs.json";
330
- const command = fakepresent ? "fakechroot" : "chroot";
331
- const args = fakepresent ? ["chroot", tree, "/bin/bash", "/script.sh"] : [tree, "/bin/bash", "/script.sh"];
332
- const run = await runhostprocess(command, args, { cwd: dir, deadline, env });
333
- return { stdout: run.stdout, stderr: run.stderr };
334
- });
335
- }
336
- /* ════════════════════════════════════════════════════════════════════ */
337
- /* Section 4: the container extension lanes — podman to ghcr. */
338
- /* ════════════════════════════════════════════════════════════════════ */
339
- /** container-podman: the rootless container lane. The script mounts at
340
- * /work and runs through the busybox image — the pull happens inside
341
- * the runtime itself under the caller's deadline, the lane never
342
- * pre-pulls anything outside its own process. */
343
- async function containerpodmanlane(code, inputs, deadline) {
344
- if (!probehostbinary("podman")) {
345
- absentruntime("container-podman", "podman");
346
- }
347
- return withtempdirhost(async (dir) => {
348
- await writeFile(join(dir, "script.sh"), code, "utf8");
349
- const inputenv = await writeinputsfile(dir, inputs);
350
- const env = inputenv === undefined ? undefined : { ...inputenv, SADDLE_EDGE_INPUTS: "/work/inputs.json" };
351
- const run = await runhostprocess("podman", [
352
- "run",
353
- "--rm",
354
- "-v",
355
- `${dir}:/work`,
356
- "-w",
357
- "/work",
358
- "quay.io/libpod/busybox:latest",
359
- "/bin/sh",
360
- "/work/script.sh",
361
- ], { cwd: dir, deadline, env });
362
- return { stdout: run.stdout, stderr: run.stderr };
363
- });
364
- }
365
- /** container-containerd: the containerd supervisor lane. The code field
366
- * is the shell command echoed through busybox's /bin/sh inside one
367
- * containerd task under the native snapshotter — the task id rides the
368
- * monotonic counter, deterministic without crypto. */
369
- async function containercontainerdlane(code, inputs, deadline) {
370
- if (!probehostbinary("ctr")) {
371
- absentruntime("container-containerd", "ctr");
372
- }
373
- return withtempdirhost(async (dir) => {
374
- const inputenv = await writeinputsfile(dir, inputs);
375
- const run = await runhostprocess("ctr", [
376
- "run",
377
- "--rm",
378
- "--snapshotter",
379
- "native",
380
- "docker.io/library/busybox:latest",
381
- nextlaneid("saddleedge-ctr"),
382
- "/bin/sh",
383
- "-c",
384
- code,
385
- ], { cwd: dir, deadline, env: inputenv });
386
- return { stdout: run.stdout, stderr: run.stderr };
387
- });
388
- }
389
- /** builds one minimal oci bundle for the runc and crun seats: a
390
- * spec.json (process args /bin/sh -c, cwd /, a PATH env, the pid,
391
- * network, ipc, uts and mount namespaces, a read-only root) and a
392
- * rootfs seeded with busybox — without busybox on PATH there is no
393
- * honest bundle and the lane answers RUNTIME_BUNDLE_UNAVAILABLE. */
394
- async function buildocibundle(dir, laneid, code) {
395
- if (!probehostbinary("busybox")) {
396
- throw edgelaneerror("RUNTIME_BUNDLE_UNAVAILABLE", `the lane ${laneid} cannot build its rootfs: busybox is absent from PATH (the lane never fakes a bundle)`);
397
- }
398
- const bundle = join(dir, "bundle");
399
- const rootfs = join(bundle, "rootfs");
400
- await mkdir(join(rootfs, "bin"), { recursive: true });
401
- await mkdir(join(rootfs, "tmp"), { recursive: true });
402
- const seat = await runhostprocess("bash", ["-c", "command -v busybox"], { cwd: dir, deadline: 2000 });
403
- const busybox = seat.stdout.trim();
404
- await copyFile(busybox, join(rootfs, "bin", "sh"));
405
- await copyFile(busybox, join(rootfs, "bin", "busybox"));
406
- const spec = {
407
- ociVersion: "1.0.2",
408
- process: {
409
- terminal: false,
410
- user: { uid: 0, gid: 0 },
411
- args: ["/bin/sh", "-c", code],
412
- env: ["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],
413
- cwd: "/",
414
- },
415
- root: { path: "rootfs", readonly: true },
416
- hostname: "saddleedge",
417
- linux: {
418
- namespaces: [{ type: "pid" }, { type: "network" }, { type: "ipc" }, { type: "uts" }, { type: "mount" }],
419
- },
420
- };
421
- await writeFile(join(bundle, "spec.json"), `${JSON.stringify(spec, null, 2)}\n`, "utf8");
422
- return bundle;
423
- }
424
- /** the shared oci runtime seat of container-runc and container-crun:
425
- * the probe, the root privilege law (the minimal spec carries no user
426
- * namespace mappings, so the namespace work needs root), the bundle
427
- * build and the run under the deadline. */
428
- async function runocilane(laneid, runtime, code, inputs, deadline) {
429
- if (!probehostbinary(runtime)) {
430
- absentruntime(laneid, runtime);
431
- }
432
- if (!isroot()) {
433
- throw edgelaneerror("HOST_PRIVILEGE_REQUIRED", `the lane ${laneid} requires root to create its ${runtime} namespaces (the lane never fakes a container)`);
434
- }
435
- return withtempdirhost(async (dir) => {
436
- const bundle = await buildocibundle(dir, laneid, code);
437
- const inputenv = await writeinputsfile(dir, inputs);
438
- const run = await runhostprocess(runtime, ["run", nextlaneid(`saddleedge-${runtime}`)], {
439
- cwd: bundle,
440
- deadline,
441
- env: inputenv,
442
- });
443
- return { stdout: run.stdout, stderr: run.stderr };
444
- });
445
- }
446
- /** container-runc: the oci process lane — spec.json plus a busybox
447
- * rootfs, run by runc under the deadline. */
448
- async function containerrunclane(code, inputs, deadline) {
449
- return runocilane("container-runc", "runc", code, inputs, deadline);
450
- }
451
- /** container-crun: the same oci bundle law through the crun runtime. */
452
- async function containercrunlane(code, inputs, deadline) {
453
- return runocilane("container-crun", "crun", code, inputs, deadline);
454
- }
455
- /** runs the code through one docker profile: the image and the
456
- * hardening flags ride the profile args, the script mounts at /work and
457
- * the inputs ride /work/inputs.json inside the container. */
458
- async function rundockerprofile(dir, code, inputs, deadline, profileargs) {
459
- await writeFile(join(dir, "script.sh"), code, "utf8");
460
- const inputenv = await writeinputsfile(dir, inputs);
461
- const env = inputenv === undefined ? undefined : { ...inputenv, SADDLE_EDGE_INPUTS: "/work/inputs.json" };
462
- const run = await runhostprocess("docker", ["run", "--rm", "-v", `${dir}:/work`, "-w", "/work", ...profileargs, "/bin/bash", "/work/script.sh"], { cwd: dir, deadline, env });
463
- return { stdout: run.stdout, stderr: run.stderr };
464
- }
465
- /** container-max: the docker max profile — no network, every capability
466
- * dropped, the pid limit raised to 256 and the memory swap left open:
467
- * more resources than the lite seat, the same honest spawn law. */
468
- async function containermaxlane(code, inputs, deadline) {
469
- if (!probedockersurface()) {
470
- absentruntime("container-max", "docker", "(no docker binary and no /var/run/docker.sock)");
471
- }
472
- return withtempdirhost((dir) => rundockerprofile(dir, code, inputs, deadline, [
473
- "--network",
474
- "none",
475
- "--cap-drop",
476
- "ALL",
477
- "--pids-limit",
478
- "256",
479
- "--memory-swap",
480
- "-1",
481
- "docker.io/library/bash:5",
482
- ]));
483
- }
484
- /** container-lite: the docker lite profile — no network, every
485
- * capability dropped, a read-only root, the pid limit at 64, 256 mb of
486
- * memory and a tmpfs at /tmp: the tightest honest container seat. */
487
- async function containerlitelane(code, inputs, deadline) {
488
- if (!probedockersurface()) {
489
- absentruntime("container-lite", "docker", "(no docker binary and no /var/run/docker.sock)");
490
- }
491
- return withtempdirhost((dir) => rundockerprofile(dir, code, inputs, deadline, [
492
- "--network",
493
- "none",
494
- "--cap-drop",
495
- "ALL",
496
- "--read-only",
497
- "--pids-limit",
498
- "64",
499
- "--memory",
500
- "256m",
501
- "--tmpfs",
502
- "/tmp",
503
- "docker.io/library/bash:5",
504
- ]));
505
- }
506
- /** container-ghcr: the immutable ghcr lane. The image rides the inputs
507
- * field {image, script} — without it the lane runs the distroless
508
- * default ghcr.io/distroless/cc:latest and echoes the script through
509
- * the container's /dev/stdin (docker -i): the lane feeds the payload,
510
- * the image answers what it answers, nothing is faked. */
511
- async function containerghcrlane(code, inputs, deadline) {
512
- if (!probedockersurface()) {
513
- absentruntime("container-ghcr", "docker", "(no docker binary and no /var/run/docker.sock)");
514
- }
515
- return withtempdirhost(async (dir) => {
516
- const record = (inputs ?? {});
517
- const image = typeof record.image === "string" && record.image.length > 0 ? record.image : "ghcr.io/distroless/cc:latest";
518
- const script = typeof record.script === "string" ? record.script : code;
519
- const inputenv = await writeinputsfile(dir, inputs);
520
- const run = await runhostprocess("docker", ["run", "--rm", "-i", "--network", "none", "--cap-drop", "ALL", "--read-only", "--pull=missing", image], { cwd: dir, deadline, env: inputenv, stdin: script });
521
- return { stdout: run.stdout, stderr: run.stderr };
522
- });
523
- }
524
- /* ════════════════════════════════════════════════════════════════════ */
525
- /* Section 5: the runtime lanes — the clr and workerd seats. */
526
- /* ════════════════════════════════════════════════════════════════════ */
527
- /** runtime-clr: the dotnet lane. When the dotnet-script tool rides the
528
- * host the program runs as program.csx; otherwise the lane falls back
529
- * to a minimal net8.0 console csproj whose Program.cs is the code field
530
- * — both seats spawn under the deadline, the compile answers honestly. */
531
- async function runtimeclrlane(code, inputs, deadline) {
532
- if (!probehostbinary("dotnet")) {
533
- absentruntime("runtime-clr", "dotnet");
534
- }
535
- const scripttool = probehostbinary("dotnet-script");
536
- return withtempdirhost(async (dir) => {
537
- const inputenv = await writeinputsfile(dir, inputs);
538
- if (scripttool) {
539
- await writeFile(join(dir, "program.csx"), code, "utf8");
540
- const run = await runhostprocess("dotnet", ["script", "program.csx"], { cwd: dir, deadline, env: inputenv });
541
- return { stdout: run.stdout, stderr: run.stderr };
542
- }
543
- await writeFile(join(dir, "Program.cs"), code, "utf8");
544
- await writeFile(join(dir, "lane.csproj"), [
545
- '<Project Sdk="Microsoft.NET.Sdk">',
546
- " <PropertyGroup>",
547
- " <OutputType>Exe</OutputType>",
548
- " <TargetFramework>net8.0</TargetFramework>",
549
- " <ImplicitUsings>disable</ImplicitUsings>",
550
- " <Nullable>disable</Nullable>",
551
- " </PropertyGroup>",
552
- "</Project>",
553
- "",
554
- ].join("\n"), "utf8");
555
- const run = await runhostprocess("dotnet", ["run", "--project", "lane.csproj"], {
556
- cwd: dir,
557
- deadline,
558
- env: inputenv,
559
- });
560
- return { stdout: run.stdout, stderr: run.stderr };
561
- });
562
- }
563
- /** runtime-workerd: the cloudflare runtime lane. The lane writes the
564
- * worker as worker.js plus a minimal capnp config (one loopback http
565
- * socket, one worker whose module tree is the temp dir, sourceUrl
566
- * workerd:main) and serves it under the deadline. workerd serve is a
567
- * server: a healthy worker outlives the run window and answers the
568
- * honest RUN_DEADLINE_EXCEEDED with its startup logs riding the
569
- * streams — a broken config answers RUN_EXITED_NONZERO, never a fake. */
570
- async function runtimeworkerdlane(code, inputs, deadline) {
571
- if (!probehostbinary("workerd")) {
572
- absentruntime("runtime-workerd", "workerd");
573
- }
574
- return withtempdirhost(async (dir) => {
575
- await writeFile(join(dir, "worker.js"), code, "utf8");
576
- await writeFile(join(dir, "workerd.capnp"), [
577
- "# the saddle edge lane workerd config: one loopback http socket and",
578
- '# one worker whose module tree is this directory (sourceUrl "workerd:main").',
579
- 'using Workerd = import "/workerd/io/workerd.capnp";',
580
- "",
581
- "const config :Workerd.Server = (",
582
- ' services = [ (name = "main", worker = .mainWorker ) ],',
583
- ' sockets = [ (name = "http", address = "127.0.0.1:0", http = (), service = "main" ) ],',
584
- ");",
585
- "",
586
- "const mainWorker :Workerd.Worker = (",
587
- ' compatibilityDate = "2024-04-03",',
588
- ' sourceUrl = "workerd:main",',
589
- ' modules = "./",',
590
- ");",
591
- "",
592
- ].join("\n"), "utf8");
593
- const inputenv = await writeinputsfile(dir, inputs);
594
- const run = await runhostprocess("workerd", ["serve", "workerd.capnp"], { cwd: dir, deadline, env: inputenv });
595
- return { stdout: run.stdout, stderr: run.stderr };
596
- });
597
- }
598
- /* ════════════════════════════════════════════════════════════════════ */
599
- /* Section 6: the screen lanes — the virtual displays, never the visitor's. */
600
- /* ════════════════════════════════════════════════════════════════════ */
601
- /** screen-xvfb: the virtual x display lane. The script runs under
602
- * xvfb-run with an auto-picked display and a 1280x800x24 screen — the
603
- * display is a virtual server the lane spawns and kills, the visitor's
604
- * screen is never touched (the engine host owns this surface). */
605
- async function screenxvfblane(code, inputs, deadline) {
606
- if (!probehostbinary("xvfb-run")) {
607
- absentruntime("screen-xvfb", "xvfb-run");
608
- }
609
- return withtempdirhost((dir) => runscript("xvfb-run", ["-a", "--server-args=-screen 0 1280x800x24", "/bin/bash", "script.sh"], dir, code, inputs, deadline));
610
- }
611
- /** screen-wayland: the headless compositor lane. cage is the first seat
612
- * (the wlroots headless backend, the runtime dir as the xdg seat so the
613
- * socket lands inside the private temp dir); weston is the fallback —
614
- * one bash spawn drives the compositor and the script in sequence, the
615
- * deadline covers the whole session and the compositor dies with the
616
- * script. A host carrying neither answers the coded absence. */
617
- async function screenwaylandlane(code, inputs, deadline) {
618
- const cagepresent = probehostbinary("cage");
619
- const westonpresent = probehostbinary("weston");
620
- if (!cagepresent && !westonpresent) {
621
- throw edgelaneerror("HOST_RUNTIME_ABSENT", "the host runtime cage is absent: the lane screen-wayland requires it on PATH (weston, the alternative seat, is absent too)");
622
- }
623
- return withtempdirhost(async (dir) => {
624
- if (cagepresent) {
625
- return await runscript("cage", ["--", "/bin/bash", "script.sh"], dir, code, inputs, deadline, {
626
- XDG_RUNTIME_DIR: dir,
627
- WLR_BACKENDS: "headless",
628
- });
629
- }
630
- await writeFile(join(dir, "script.sh"), code, "utf8");
631
- const inputenv = (await writeinputsfile(dir, inputs)) ?? {};
632
- const session = [
633
- `XDG_RUNTIME_DIR="${dir}" weston --backend=headless-backend.so --socket=wayland-0 >/dev/null 2>&1 &`,
634
- "westonpid=$!",
635
- "sleep 0.3",
636
- `WAYLAND_DISPLAY=wayland-0 XDG_RUNTIME_DIR="${dir}" /bin/bash script.sh`,
637
- "status=$?",
638
- 'kill "$westonpid" 2>/dev/null',
639
- 'wait "$westonpid" 2>/dev/null',
640
- "exit $status",
641
- ].join("\n");
642
- const run = await runhostprocess("bash", ["-c", session], { cwd: dir, deadline, env: inputenv });
643
- return { stdout: run.stdout, stderr: run.stderr };
644
- });
645
- }
646
- /* ════════════════════════════════════════════════════════════════════ */
647
- /* Section 7: the mobile lane — the termux seat. */
648
- /* ════════════════════════════════════════════════════════════════════ */
649
- /** mobile-termux: the honest mobile lane. A linux engine host never
650
- * carries termux — the lane answers the coded absence; a termux host
651
- * (the engine running inside the android sandbox) runs the script
652
- * through the termux prefix environment (TERMUX_PREFIX with the prefix
653
- * bin on PATH), the honest emulation-free seat. */
654
- async function mobiletermuxlane(code, inputs, deadline) {
655
- if (!probehostbinary("termux")) {
656
- absentruntime("mobile-termux", "termux", "(the termux host prefix exists only inside a termux device host)");
657
- }
658
- return withtempdirhost(async (dir) => {
659
- const record = (inputs ?? {});
660
- const prefix = typeof record.termuxprefix === "string" && record.termuxprefix.length > 0
661
- ? record.termuxprefix
662
- : "/data/data/com.termux/files/usr";
663
- const path = process.env.PATH ?? "";
664
- return await runscript("bash", ["script.sh"], dir, code, inputs, deadline, {
665
- TERMUX_PREFIX: prefix,
666
- PATH: `${prefix}/bin:${path}`,
667
- });
668
- });
669
- }
670
- /* ════════════════════════════════════════════════════════════════════ */
671
- /* Section 8: the build lanes — bazel, buck2 and turborepo. */
672
- /* ════════════════════════════════════════════════════════════════════ */
673
- /** build-bazel: the bazel lane. A minimal BUILD file wires the script
674
- * as one sh_binary and bazel runs it in batch mode with the system and
675
- * home rc files refused, HOME pointed at the temp dir and the output
676
- * user root inside it — the build caches the engine host may own are
677
- * never touched, nothing leaks outside the private dir. */
678
- async function buildbazellane(code, inputs, deadline) {
679
- if (!probehostbinary("bazel")) {
680
- absentruntime("build-bazel", "bazel");
681
- }
682
- return withtempdirhost(async (dir) => {
683
- await writeFile(join(dir, "MODULE.bazel"), "", "utf8");
684
- await writeFile(join(dir, "BUILD"), 'sh_binary(\n name = "lane",\n srcs = ["script.sh"],\n)\n', "utf8");
685
- return await runscript("bazel", ["--batch", "--nosystem_rc", "--nohome_rc", `--output_user_root=${join(dir, "out")}`, "run", "//:lane"], dir, code, inputs, deadline, { HOME: dir });
686
- });
687
- }
688
- /** build-buck: the buck2 lane. A minimal BUCK file wires the script as
689
- * one sh_binary and buck2 runs it with HOME at the temp dir so the
690
- * buck caches stay inside the private seat. */
691
- async function buildbucklane(code, inputs, deadline) {
692
- if (!probehostbinary("buck2")) {
693
- absentruntime("build-buck", "buck2");
694
- }
695
- return withtempdirhost(async (dir) => {
696
- await writeFile(join(dir, ".buckconfig"), "[repositories]\nroot = .\n", "utf8");
697
- await writeFile(join(dir, "BUCK"), 'sh_binary(\n name = "lane",\n srcs = ["script.sh"],\n)\n', "utf8");
698
- return await runscript("buck2", ["run", "//:lane"], dir, code, inputs, deadline, { HOME: dir });
699
- });
700
- }
701
- /** the turbo seat: the binary on PATH or the engine module graph's
702
- * turbo package (the bin shim it ships) — the honest probe answers
703
- * present only when one of the two seats resolves. */
704
- function turboseat() {
705
- if (probehostbinary("turbo")) {
706
- return { present: true, binpath: "turbo", viabinary: true };
707
- }
708
- try {
709
- const resolved = createRequire(import.meta.url).resolve("turbo");
710
- let dir = dirname(resolved);
711
- while (dir !== dirname(dir)) {
712
- if (basename(dir) === "turbo" && basename(dirname(dir)) === "node_modules") {
713
- const binpath = join(dir, "bin", "turbo");
714
- if (existsSync(binpath)) {
715
- return { present: true, binpath, viabinary: false };
716
- }
717
- }
718
- dir = dirname(dir);
719
- }
720
- }
721
- catch {
722
- // the module seat is absent: the binary seat already answered
723
- }
724
- return { present: false, binpath: "", viabinary: false };
725
- }
726
- /** build-turborepo: the turbo lane. A minimal package.json whose build
727
- * script is the code field plus a minimal turbo.json, then turbo run
728
- * build — through the binary seat when the host carries it, through the
729
- * engine module seat's bin shim (spawned under node) when the repo
730
- * carries turbo only as a dependency. */
731
- async function buildturborepolane(code, inputs, deadline) {
732
- const seat = turboseat();
733
- if (!seat.present) {
734
- throw edgelaneerror("HOST_RUNTIME_ABSENT", "the host runtime turbo is absent: the lane build-turborepo requires it on PATH (neither the turbo binary nor the engine module seat resolved)");
735
- }
736
- return withtempdirhost(async (dir) => {
737
- const manifest = {
738
- name: "saddle-edge-turbo-lane",
739
- private: true,
740
- version: "0.0.0",
741
- scripts: { build: code },
742
- };
743
- await writeFile(join(dir, "package.json"), `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
744
- await writeFile(join(dir, "turbo.json"), `${JSON.stringify({ $schema: "https://turbo.build/schema.json", tasks: { build: { outputs: [] } } }, null, 2)}\n`, "utf8");
745
- const inputenv = (await writeinputsfile(dir, inputs)) ?? {};
746
- const command = seat.viabinary ? "turbo" : "node";
747
- const args = seat.viabinary ? ["run", "build"] : [seat.binpath, "run", "build"];
748
- const run = await runhostprocess(command, args, { cwd: dir, deadline, env: { ...inputenv, HOME: dir } });
749
- return { stdout: run.stdout, stderr: run.stderr };
750
- });
751
- }
752
- /* ════════════════════════════════════════════════════════════════════ */
753
- /* Section 9: the ci runner lanes — the actions and gitlab seats. */
754
- /* ════════════════════════════════════════════════════════════════════ */
755
- /** the actions runner root seat: the ACTIONS_RUNNER_ROOT env pointing
756
- * at a real directory, or the empty string when the host carries none. */
757
- function actionsrunnerroot() {
758
- const root = process.env.ACTIONS_RUNNER_ROOT;
759
- return typeof root === "string" && root.length > 0 && existsSync(root) ? root : "";
760
- }
761
- /** ci-actions: the github actions runner lane. The probe is the
762
- * Runner.Listener binary on PATH or the ACTIONS_RUNNER_ROOT directory;
763
- * when present the lane runs the listener in once mode through its own
764
- * root and answers its streams — the job the listener pulls answers the
765
- * configured service, the code and the inputs ride the env surface for
766
- * the job side to pick up, nothing is faked. */
767
- async function ciactionslane(code, inputs, deadline) {
768
- const root = actionsrunnerroot();
769
- if (root.length === 0 && !probehostbinary("Runner.Listener")) {
770
- absentruntime("ci-actions", "actions-runner", "(no Runner.Listener on PATH and no ACTIONS_RUNNER_ROOT directory)");
771
- }
772
- return withtempdirhost(async (dir) => {
773
- let runnerroot = root;
774
- if (runnerroot.length === 0) {
775
- const seat = await runhostprocess("bash", ["-c", "command -v Runner.Listener"], {
776
- cwd: dir,
777
- deadline: Math.min(deadline, 2000),
778
- });
779
- runnerroot = dirname(seat.stdout.trim());
780
- }
781
- await writeFile(join(dir, "script.sh"), code, "utf8");
782
- const inputenv = await writeinputsfile(dir, inputs);
783
- const run = await runhostprocess("./Runner.Listener", ["run", "--once"], {
784
- cwd: runnerroot,
785
- deadline,
786
- env: inputenv,
787
- });
788
- return { stdout: run.stdout, stderr: run.stderr };
789
- });
790
- }
791
- /** ci-gitlab: the gitlab runner lane. The job script rides a minimal
792
- * .gitlab-ci.yml (one job, one block scalar script) and the runner
793
- * executes it through `exec docker`; a modern runner that removed the
794
- * exec command answers RUNTIME_CAPABILITY_ABSENT with the message
795
- * naming the removal — the runtime exists, the capability is gone. */
796
- async function cigitlablane(code, inputs, deadline) {
797
- if (!probehostbinary("gitlab-runner")) {
798
- absentruntime("ci-gitlab", "gitlab-runner");
799
- }
800
- return withtempdirhost(async (dir) => {
801
- const body = code
802
- .split("\n")
803
- .map((line) => ` ${line}`)
804
- .join("\n");
805
- await writeFile(join(dir, ".gitlab-ci.yml"), `lane:\n script:\n - |\n${body}\n`, "utf8");
806
- const inputenv = await writeinputsfile(dir, inputs);
807
- try {
808
- const run = await runhostprocess("gitlab-runner", ["exec", "docker", "lane"], {
809
- cwd: dir,
810
- deadline,
811
- env: inputenv,
812
- });
813
- return { stdout: run.stdout, stderr: run.stderr };
814
- }
815
- catch (error) {
816
- const stderr = error.stderr ?? "";
817
- if (typeof error.code === "string" && /unknown command/i.test(stderr)) {
818
- throw edgelaneerror("RUNTIME_CAPABILITY_ABSENT", "the host runtime gitlab-runner refused the exec command: exec was removed from the modern runners (the lane ci-gitlab requires a runner that still carries exec)", { stderr });
819
- }
820
- throw error;
821
- }
822
- });
823
- }
824
- /* ════════════════════════════════════════════════════════════════════ */
825
- /* Section 10: the network tunnel lanes — tor, i2p and wireguard. */
826
- /* ════════════════════════════════════════════════════════════════════ */
827
- /** network-tor: the onion lane. The lane starts tor inside the private
828
- * temp dir (its data directory and logs stay there), waits up to three
829
- * seconds for the socks port to accept a connection, runs the code as
830
- * a bash script with ALL_PROXY pointing at the circuit and tears tor
831
- * down in the finally — no daemon outlives the lane, and a circuit that
832
- * never opens answers the deadline refusal, never a fake tunnel. */
833
- async function networktorlane(code, inputs, deadline) {
834
- if (!probehostbinary("tor")) {
835
- absentruntime("network-tor", "tor");
836
- }
837
- return withtempdirhost(async (dir) => {
838
- const tordata = join(dir, "tordata");
839
- await mkdir(tordata, { recursive: true });
840
- const daemon = spawn("tor", [
841
- "--SocksPort",
842
- "9050",
843
- "--DataDirectory",
844
- tordata,
845
- "--ControlPort",
846
- "0",
847
- "--Log",
848
- `notice file ${join(dir, "tor.log")}`,
849
- ], { cwd: dir, stdio: "ignore", env: process.env });
850
- try {
851
- const opened = await waitforport(9050, Math.min(3000, Math.max(250, deadline)));
852
- if (!opened) {
853
- throw edgelaneerror("RUN_DEADLINE_EXCEEDED", "the tor socks port 9050 never opened within its 3 s wait (the lane network-tor refuses to run a script without the circuit)");
854
- }
855
- return await runscript("bash", ["script.sh"], dir, code, inputs, deadline, {
856
- ALL_PROXY: "socks5://127.0.0.1:9050",
857
- });
858
- }
859
- finally {
860
- daemon.kill("SIGTERM");
861
- const exited = await waitforexit(daemon, 1500);
862
- if (!exited) {
863
- daemon.kill("SIGKILL");
864
- await waitforexit(daemon, 1500);
865
- }
866
- }
867
- });
868
- }
869
- /** network-i2p: the i2p tunnel lane. The runtime seat is the i2prouter
870
- * binary: when the router console (7657) already answers, the script
871
- * rides the existing tunnel; otherwise the lane starts the router,
872
- * waits up to five seconds for the console and runs the script with
873
- * the i2p proxy env (I2P_PROXY_HOST/I2P_PROXY_PORT at the 4444 http
874
- * proxy). The router the lane started gets its graceful stop in the
875
- * finally — a console that never opens answers the deadline refusal. */
876
- async function networki2plane(code, inputs, deadline) {
877
- if (!probehostbinary("i2prouter")) {
878
- absentruntime("network-i2p", "i2p", "(the i2prouter binary is absent and the router console 7657 cannot be managed)");
879
- }
880
- return withtempdirhost(async (dir) => {
881
- let startedrouter = false;
882
- try {
883
- const consoleopen = await waitforport(7657, 300);
884
- if (!consoleopen) {
885
- await runhostprocess("i2prouter", ["start"], { cwd: dir, deadline: Math.min(deadline, 8000) });
886
- startedrouter = true;
887
- const opened = await waitforport(7657, Math.min(5000, Math.max(250, deadline)));
888
- if (!opened) {
889
- throw edgelaneerror("RUN_DEADLINE_EXCEEDED", "the i2p router console 7657 never opened within its 5 s wait (the lane network-i2p refuses to run a script without the tunnel)");
890
- }
891
- }
892
- return await runscript("bash", ["script.sh"], dir, code, inputs, deadline, {
893
- I2P_PROXY_HOST: "127.0.0.1",
894
- I2P_PROXY_PORT: "4444",
895
- });
896
- }
897
- finally {
898
- if (startedrouter) {
899
- try {
900
- await runhostprocess("i2prouter", ["graceful"], { cwd: dir, deadline: Math.min(deadline, 8000) });
901
- }
902
- catch {
903
- // the graceful stop is the honest best effort: the router owns
904
- // its own daemon lifecycle and answers its own error here
905
- }
906
- }
907
- }
908
- });
909
- }
910
- /** network-wireguard: the interface handshake lane. An empty code field
911
- * answers the honest report — the wg show capture plus one wg genkey |
912
- * wg pubkey pair generation on stdout; a non-empty code field runs as a
913
- * bash script with the wg tools on PATH, and a script that asks for a
914
- * wg-quick interface bring-up needs root: a non-root engine answers
915
- * the coded privilege refusal, never a wrapped errno. */
916
- async function networkwireguardlane(code, inputs, deadline) {
917
- if (!probehostbinary("wg")) {
918
- absentruntime("network-wireguard", "wg");
919
- }
920
- return withtempdirhost(async (dir) => {
921
- const inputenv = (await writeinputsfile(dir, inputs)) ?? {};
922
- if (code.trim().length === 0) {
923
- let report = "";
924
- let reporterr = "";
925
- try {
926
- const show = await runhostprocess("wg", ["show"], { cwd: dir, deadline, env: inputenv });
927
- report = show.stdout;
928
- reporterr = show.stderr;
929
- }
930
- catch (error) {
931
- // wg show on a host without interfaces or privileges answers
932
- // nonzero: the report is informational, the failure rides stderr
933
- reporterr = `${errormessage(error)}${error.stderr ?? ""}`;
934
- }
935
- const pair = await runhostprocess("bash", ["-c", "wg genkey | wg pubkey"], {
936
- cwd: dir,
937
- deadline,
938
- env: inputenv,
939
- });
940
- return { stdout: `${report}${pair.stdout}`.trim(), stderr: `${reporterr}${pair.stderr}` };
941
- }
942
- if (code.trimStart().startsWith("wg-quick") && !isroot()) {
943
- throw edgelaneerror("HOST_PRIVILEGE_REQUIRED", "the lane network-wireguard requires root to bring a wg-quick interface up (the lane never fakes a handshake)");
944
- }
945
- await writeFile(join(dir, "script.sh"), code, "utf8");
946
- const run = await runhostprocess("bash", ["script.sh"], { cwd: dir, deadline, env: inputenv });
947
- return { stdout: run.stdout, stderr: run.stderr };
948
- });
949
- }
950
- /* ════════════════════════════════════════════════════════════════════ */
951
- /* Section 11: the storage daemon lanes — the minio and garage clients. */
952
- /* ════════════════════════════════════════════════════════════════════ */
953
- /** storage-minio: the minio client lane. The code field is a shell
954
- * script riding the mc CLI; the inputs {alias, url, accesskey,
955
- * secretkey} (all optional) derive the MC_HOST_<alias> env so the
956
- * script addresses the vault — without them mc answers its own honest
957
- * error and the lane wraps it, never invents a vault. */
958
- async function storageminiolane(code, inputs, deadline) {
959
- if (!probehostbinary("mc")) {
960
- absentruntime("storage-minio", "mc");
961
- }
962
- return withtempdirhost(async (dir) => {
963
- const record = (inputs ?? {});
964
- const alias = typeof record.alias === "string" && record.alias.length > 0 ? record.alias : "lane";
965
- const extraenv = {};
966
- if (typeof record.url === "string" && record.url.length > 0) {
967
- const accesskey = typeof record.accesskey === "string" ? record.accesskey : "";
968
- const secretkey = typeof record.secretkey === "string" ? record.secretkey : "";
969
- extraenv[`MC_HOST_${alias}`] = `https://${accesskey}:${secretkey}@${record.url}`;
970
- }
971
- return await runscript("bash", ["script.sh"], dir, code, inputs, deadline, extraenv);
972
- });
973
- }
974
- /** storage-garage: the garage client lane. The same script law — the
975
- * code field rides bash with the garage CLI on PATH, and the inputs
976
- * {rpchost, rpcsecret} derive the GARAGE_RPC_HOST and
977
- * GARAGE_RPC_SECRET env when the caller sent them. */
978
- async function storagegaragelane(code, inputs, deadline) {
979
- if (!probehostbinary("garage")) {
980
- absentruntime("storage-garage", "garage");
981
- }
982
- return withtempdirhost(async (dir) => {
983
- const record = (inputs ?? {});
984
- const extraenv = {};
985
- if (typeof record.rpchost === "string" && record.rpchost.length > 0) {
986
- extraenv.GARAGE_RPC_HOST = record.rpchost;
987
- }
988
- if (typeof record.rpcsecret === "string" && record.rpcsecret.length > 0) {
989
- extraenv.GARAGE_RPC_SECRET = record.rpcsecret;
990
- }
991
- return await runscript("bash", ["script.sh"], dir, code, inputs, deadline, extraenv);
992
- });
993
- }
994
- /* ════════════════════════════════════════════════════════════════════ */
995
- /* Section 12: the lane table — the specs, the executors, the ids. */
996
- /* ════════════════════════════════════════════════════════════════════ */
997
- /** the host lane table of the ninety modalities wave, group a: the
998
- * twenty-five host binary lanes, every one kind live — the executor
999
- * exists and drives the runtime when the host carries it. The ids are
1000
- * pinned — the dispatcher merge and the api surface read them exactly. */
1001
- export const hostlanespecs = Object.freeze([
1002
- { modality: "sandbox-bubblewrap", kind: "live", runtime: "bwrap", present: () => probehostbinary("bwrap") },
1003
- { modality: "sandbox-nsjail", kind: "live", runtime: "nsjail", present: () => probehostbinary("nsjail") },
1004
- {
1005
- modality: "sandbox-chroot",
1006
- kind: "live",
1007
- runtime: "chroot",
1008
- present: () => probehostbinary("chroot") || probehostbinary("fakechroot"),
1009
- },
1010
- { modality: "container-podman", kind: "live", runtime: "podman", present: () => probehostbinary("podman") },
1011
- { modality: "container-containerd", kind: "live", runtime: "ctr", present: () => probehostbinary("ctr") },
1012
- { modality: "container-runc", kind: "live", runtime: "runc", present: () => probehostbinary("runc") },
1013
- { modality: "container-crun", kind: "live", runtime: "crun", present: () => probehostbinary("crun") },
1014
- { modality: "container-max", kind: "live", runtime: "docker", present: probedockersurface },
1015
- { modality: "container-lite", kind: "live", runtime: "docker", present: probedockersurface },
1016
- { modality: "container-ghcr", kind: "live", runtime: "docker", present: probedockersurface },
1017
- { modality: "runtime-clr", kind: "live", runtime: "dotnet", present: () => probehostbinary("dotnet") },
1018
- { modality: "runtime-workerd", kind: "live", runtime: "workerd", present: () => probehostbinary("workerd") },
1019
- { modality: "screen-xvfb", kind: "live", runtime: "xvfb-run", present: () => probehostbinary("xvfb-run") },
1020
- {
1021
- modality: "screen-wayland",
1022
- kind: "live",
1023
- runtime: "cage",
1024
- present: () => probehostbinary("cage") || probehostbinary("weston"),
1025
- },
1026
- { modality: "mobile-termux", kind: "live", runtime: "termux", present: () => probehostbinary("termux") },
1027
- { modality: "build-bazel", kind: "live", runtime: "bazel", present: () => probehostbinary("bazel") },
1028
- { modality: "build-buck", kind: "live", runtime: "buck2", present: () => probehostbinary("buck2") },
1029
- { modality: "build-turborepo", kind: "live", runtime: "turbo", present: () => turboseat().present },
1030
- {
1031
- modality: "ci-actions",
1032
- kind: "live",
1033
- runtime: "actions-runner",
1034
- present: () => probehostbinary("Runner.Listener") || actionsrunnerroot().length > 0,
1035
- },
1036
- { modality: "ci-gitlab", kind: "live", runtime: "gitlab-runner", present: () => probehostbinary("gitlab-runner") },
1037
- { modality: "network-tor", kind: "live", runtime: "tor", present: () => probehostbinary("tor") },
1038
- { modality: "network-i2p", kind: "live", runtime: "i2p", present: () => probehostbinary("i2prouter") },
1039
- { modality: "network-wireguard", kind: "live", runtime: "wg", present: () => probehostbinary("wg") },
1040
- { modality: "storage-minio", kind: "live", runtime: "mc", present: () => probehostbinary("mc") },
1041
- { modality: "storage-garage", kind: "live", runtime: "garage", present: () => probehostbinary("garage") },
1042
- ]);
1043
- /** the host lane executor table: one executor per lane id, keyed by the
1044
- * modality id in the same order as the spec table; the dispatch plane
1045
- * refuses to boot over a lane it cannot wire. */
1046
- export const edgelaneexecutorshost = Object.freeze({
1047
- "sandbox-bubblewrap": sandboxbubblewraplane,
1048
- "sandbox-nsjail": sandboxnsjaillane,
1049
- "sandbox-chroot": sandboxchrootlane,
1050
- "container-podman": containerpodmanlane,
1051
- "container-containerd": containercontainerdlane,
1052
- "container-runc": containerrunclane,
1053
- "container-crun": containercrunlane,
1054
- "container-max": containermaxlane,
1055
- "container-lite": containerlitelane,
1056
- "container-ghcr": containerghcrlane,
1057
- "runtime-clr": runtimeclrlane,
1058
- "runtime-workerd": runtimeworkerdlane,
1059
- "screen-xvfb": screenxvfblane,
1060
- "screen-wayland": screenwaylandlane,
1061
- "mobile-termux": mobiletermuxlane,
1062
- "build-bazel": buildbazellane,
1063
- "build-buck": buildbucklane,
1064
- "build-turborepo": buildturborepolane,
1065
- "ci-actions": ciactionslane,
1066
- "ci-gitlab": cigitlablane,
1067
- "network-tor": networktorlane,
1068
- "network-i2p": networki2plane,
1069
- "network-wireguard": networkwireguardlane,
1070
- "storage-minio": storageminiolane,
1071
- "storage-garage": storagegaragelane,
1072
- });
1073
- /** the host lane ids in the spec order — the pinned contract the
1074
- * dispatcher merge and the api surface read exactly. */
1075
- export const hostlaneids = Object.freeze(hostlanespecs.map((lane) => lane.modality));
1076
- /* the wiring self-check: the host lane table refuses to load over a gap
1077
- * between the specs and the executors (a spec without an executor is a
1078
- * miswired lane, and miswiring must fail loud at boot, never silently
1079
- * at run time). */
1080
- for (const lane of hostlanespecs) {
1081
- if (edgelaneexecutorshost[lane.modality] === undefined) {
1082
- throw new Error(`the host lane plane is miswired: no executor for the lane ${lane.modality}`);
1083
- }
1084
- }
1085
- for (const id of Object.keys(edgelaneexecutorshost)) {
1086
- if (!hostlaneids.includes(id)) {
1087
- throw new Error(`the host lane plane is miswired: the executor ${id} carries no spec`);
1088
- }
1089
- }