@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.
- package/Dockerfile +112 -41
- package/README.md +58 -59
- package/browser.ts +482 -0
- package/cli.ts +274 -0
- package/distribution.ts +731 -0
- package/docker.config +29 -8
- package/docs/artifactavailability.md +2 -2
- package/docs/browser.md +18 -1
- package/docs/{containerplatforms.2.1.0.md → containerplatforms210.md} +1 -1
- package/docs/{documentmap.1.8.12.md → documentmap1812.md} +1 -1
- package/docs/extension.md +13 -2
- package/docs/libraryapi.md +1 -1
- package/docs/{ninety.modalities.md → ninetymodalities.md} +87 -4
- package/docs/{planning.ninety.md → planningninety.md} +6 -6
- package/docs/{releasenotes.1.8.13.md → releasenotes1813.md} +2 -2
- package/docs/{releasenotes.1.8.14.md → releasenotes1814.md} +1 -1
- package/docs/{repo.synthesis.1.8.16.md → reposynthesis1816.md} +1 -1
- package/docs/todo.md +2 -2
- package/docs/{todo.1.8.15.md → todo1815.md} +1 -1
- package/docs/{todo.1.8.16.md → todo1816.md} +5 -5
- package/execution.ts +677 -0
- package/foundation.ts +314 -0
- package/index.ts +35 -27
- package/isolation.ts +84 -0
- package/modes.ts +170 -0
- package/mttg.config +1 -1
- package/orchestrator.ts +49 -15
- package/package.json +90 -75
- package/qemu.config +2 -2
- package/sandbox.ts +2094 -0
- package/scheduler.ts +1 -1
- package/server.ts +52 -0
- package/tsconfig.json +11 -12
- package/virtual.ts +1105 -0
- package/virtualgpu.ts +1 -1
- package/virtualization.ts +1 -1
- package/workloadlaw.ts +293 -0
- package/dist/App.d.ts +0 -5
- package/dist/App.js +0 -52
- package/dist/Sol/Sol.d.ts +0 -3
- package/dist/Sol/Sol.js +0 -46
- package/dist/Sol/agentbrowser/agentbrowser.d.ts +0 -9
- package/dist/Sol/agentbrowser/agentbrowser.js +0 -121
- package/dist/Sol/architecture/architecture.d.ts +0 -9
- package/dist/Sol/architecture/architecture.js +0 -93
- package/dist/Sol/compute/compute.d.ts +0 -9
- package/dist/Sol/compute/compute.js +0 -22
- package/dist/Sol/console/console.d.ts +0 -15
- package/dist/Sol/console/console.js +0 -292
- package/dist/Sol/dashboard/dashboard.d.ts +0 -10
- package/dist/Sol/dashboard/dashboard.js +0 -181
- package/dist/Sol/docs/docs.d.ts +0 -9
- package/dist/Sol/docs/docs.js +0 -28
- package/dist/Sol/home/home.d.ts +0 -35
- package/dist/Sol/home/home.js +0 -239
- package/dist/Sol/integrations/integrations.d.ts +0 -9
- package/dist/Sol/integrations/integrations.js +0 -83
- package/dist/Sol/intro/intro.d.ts +0 -7
- package/dist/Sol/intro/intro.js +0 -102
- package/dist/Sol/login/login.d.ts +0 -16
- package/dist/Sol/login/login.js +0 -49
- package/dist/Sol/notfound/notfound.d.ts +0 -9
- package/dist/Sol/notfound/notfound.js +0 -15
- package/dist/Sol/onboarding/onboarding.d.ts +0 -25
- package/dist/Sol/onboarding/onboarding.js +0 -60
- package/dist/Sol/playground/playground.d.ts +0 -9
- package/dist/Sol/playground/playground.js +0 -127
- package/dist/Sol/register/register.d.ts +0 -16
- package/dist/Sol/register/register.js +0 -193
- package/dist/Sol/shell/Shell.d.ts +0 -124
- package/dist/Sol/shell/Shell.js +0 -335
- package/dist/Sol/wingets/ComputeBootladder.d.ts +0 -6
- package/dist/Sol/wingets/ComputeBootladder.js +0 -71
- package/dist/Sol/wingets/ComputeModalitycatalog.d.ts +0 -7
- package/dist/Sol/wingets/ComputeModalitycatalog.js +0 -71
- package/dist/Sol/wingets/ConsoleApibadge.d.ts +0 -7
- package/dist/Sol/wingets/ConsoleApibadge.js +0 -5
- package/dist/Sol/wingets/ConsoleEdgelaw.d.ts +0 -6
- package/dist/Sol/wingets/ConsoleEdgelaw.js +0 -36
- package/dist/Sol/wingets/ConsoleEventstimeline.d.ts +0 -19
- package/dist/Sol/wingets/ConsoleEventstimeline.js +0 -12
- package/dist/Sol/wingets/ConsoleSnapshotcontrols.d.ts +0 -8
- package/dist/Sol/wingets/ConsoleSnapshotcontrols.js +0 -8
- package/dist/Sol/wingets/ConsoleSpecpanel.d.ts +0 -21
- package/dist/Sol/wingets/ConsoleSpecpanel.js +0 -75
- package/dist/Sol/wingets/ConsoleTerminal.d.ts +0 -22
- package/dist/Sol/wingets/ConsoleTerminal.js +0 -77
- package/dist/Sol/wingets/DashboardAccountpanel.d.ts +0 -18
- package/dist/Sol/wingets/DashboardAccountpanel.js +0 -65
- package/dist/Sol/wingets/DashboardAdminpanel.d.ts +0 -6
- package/dist/Sol/wingets/DashboardAdminpanel.js +0 -90
- package/dist/Sol/wingets/DashboardAuditlog.d.ts +0 -8
- package/dist/Sol/wingets/DashboardAuditlog.js +0 -50
- package/dist/Sol/wingets/DashboardCreatesandboxform.d.ts +0 -10
- package/dist/Sol/wingets/DashboardCreatesandboxform.js +0 -71
- package/dist/Sol/wingets/DashboardEventstimeline.d.ts +0 -8
- package/dist/Sol/wingets/DashboardEventstimeline.js +0 -83
- package/dist/Sol/wingets/DashboardGlobalsandboxestable.d.ts +0 -8
- package/dist/Sol/wingets/DashboardGlobalsandboxestable.js +0 -57
- package/dist/Sol/wingets/DashboardMeshnodestable.d.ts +0 -8
- package/dist/Sol/wingets/DashboardMeshnodestable.js +0 -103
- package/dist/Sol/wingets/DashboardSandboxlist.d.ts +0 -10
- package/dist/Sol/wingets/DashboardSandboxlist.js +0 -163
- package/dist/Sol/wingets/DashboardUserstable.d.ts +0 -8
- package/dist/Sol/wingets/DashboardUserstable.js +0 -78
- package/dist/Sol/wingets/HomeEntrygate.d.ts +0 -23
- package/dist/Sol/wingets/HomeEntrygate.js +0 -73
- package/dist/Sol/wingets/HomeMetricstrip.d.ts +0 -9
- package/dist/Sol/wingets/HomeMetricstrip.js +0 -4
- package/dist/Sol/wingets/HomeRuntimediagram.d.ts +0 -1
- package/dist/Sol/wingets/HomeRuntimediagram.js +0 -22
- package/dist/Sol/wingets/LoginForm.d.ts +0 -19
- package/dist/Sol/wingets/LoginForm.js +0 -141
- package/dist/Sol/wingets/RegisterPasswordconfirm.d.ts +0 -13
- package/dist/Sol/wingets/RegisterPasswordconfirm.js +0 -16
- package/dist/Sol/wingets/RegisterPasswordmeter.d.ts +0 -19
- package/dist/Sol/wingets/RegisterPasswordmeter.js +0 -65
- package/dist/Sol/wingets/RegisterUsernamerules.d.ts +0 -11
- package/dist/Sol/wingets/RegisterUsernamerules.js +0 -16
- package/dist/Sol/wingets/index.d.ts +0 -39
- package/dist/Sol/wingets/index.js +0 -39
- package/dist/alternatives.d.ts +0 -508
- package/dist/alternatives.js +0 -950
- package/dist/api.d.ts +0 -154
- package/dist/api.js +0 -174
- package/dist/auth.d.ts +0 -268
- package/dist/auth.js +0 -476
- package/dist/automation.d.ts +0 -425
- package/dist/automation.js +0 -566
- package/dist/browser.d.ts +0 -561
- package/dist/browser.js +0 -1058
- package/dist/capacitor.config.d.ts +0 -45
- package/dist/capacitor.config.js +0 -48
- package/dist/catalog.d.ts +0 -43
- package/dist/catalog.js +0 -44
- package/dist/cli.d.ts +0 -16
- package/dist/cli.js +0 -168
- package/dist/communication.d.ts +0 -181
- package/dist/communication.js +0 -376
- package/dist/compute.d.ts +0 -1160
- package/dist/compute.js +0 -2272
- package/dist/dashboardlib.d.ts +0 -58
- package/dist/dashboardlib.js +0 -161
- package/dist/db.d.ts +0 -569
- package/dist/db.js +0 -1080
- package/dist/distribution.d.ts +0 -528
- package/dist/distribution.js +0 -810
- package/dist/drizzle.config.d.ts +0 -20
- package/dist/drizzle.config.js +0 -18
- package/dist/edgelandes.d.ts +0 -70
- package/dist/edgelandes.host.d.ts +0 -26
- package/dist/edgelandes.host.js +0 -1089
- package/dist/edgelandes.inproc.d.ts +0 -27
- package/dist/edgelandes.inproc.js +0 -952
- package/dist/edgelandes.js +0 -1133
- package/dist/edgelandes.ninety.d.ts +0 -31
- package/dist/edgelandes.ninety.js +0 -174
- package/dist/edgelandes.virtual.d.ts +0 -25
- package/dist/edgelandes.virtual.js +0 -629
- package/dist/edgeplane.d.ts +0 -121
- package/dist/edgeplane.js +0 -341
- package/dist/examplelocaljob.d.ts +0 -1
- package/dist/examplelocaljob.js +0 -14
- package/dist/execution.d.ts +0 -529
- package/dist/execution.js +0 -760
- package/dist/family.d.ts +0 -16
- package/dist/family.js +0 -42
- package/dist/familyurl.d.ts +0 -21
- package/dist/familyurl.js +0 -24
- package/dist/format.d.ts +0 -18
- package/dist/format.js +0 -59
- package/dist/foundation.d.ts +0 -172
- package/dist/foundation.js +0 -312
- package/dist/index.d.ts +0 -735
- package/dist/index.js +0 -1020
- package/dist/integration.d.ts +0 -235
- package/dist/integration.js +0 -228
- package/dist/intelligence.d.ts +0 -93
- package/dist/intelligence.js +0 -144
- package/dist/isolation.d.ts +0 -128
- package/dist/isolation.js +0 -95
- package/dist/labels.d.ts +0 -665
- package/dist/labels.js +0 -707
- package/dist/localauth.d.ts +0 -127
- package/dist/localauth.js +0 -413
- package/dist/media.d.ts +0 -343
- package/dist/media.js +0 -985
- package/dist/mesh.d.ts +0 -177
- package/dist/mesh.js +0 -468
- package/dist/modalityregistry.d.ts +0 -109
- package/dist/modalityregistry.js +0 -1386
- package/dist/modes.d.ts +0 -358
- package/dist/modes.js +0 -133
- package/dist/onboardingstate.d.ts +0 -79
- package/dist/onboardingstate.js +0 -282
- package/dist/orchestrator.d.ts +0 -1338
- package/dist/orchestrator.js +0 -4310
- package/dist/packager/targetcli.js +0 -4
- package/dist/paths.d.ts +0 -2
- package/dist/paths.js +0 -14
- package/dist/performance.d.ts +0 -415
- package/dist/performance.js +0 -826
- package/dist/quantum.d.ts +0 -809
- package/dist/quantum.js +0 -2072
- package/dist/render.d.ts +0 -380
- package/dist/render.js +0 -1072
- package/dist/runall.d.ts +0 -2
- package/dist/runall.js +0 -39
- package/dist/runtimeprobe.d.ts +0 -4
- package/dist/runtimeprobe.js +0 -8
- package/dist/sandbox.d.ts +0 -355
- package/dist/sandbox.js +0 -1721
- package/dist/sandboxenvprofile.d.ts +0 -29
- package/dist/sandboxenvprofile.js +0 -89
- package/dist/sandboxprofile.d.ts +0 -137
- package/dist/sandboxprofile.js +0 -241
- package/dist/sandboxqueue.d.ts +0 -108
- package/dist/sandboxqueue.js +0 -201
- package/dist/sandboxquota.d.ts +0 -121
- package/dist/sandboxquota.js +0 -236
- package/dist/scheduler.d.ts +0 -595
- package/dist/scheduler.js +0 -1258
- package/dist/scriptlanes.d.ts +0 -182
- package/dist/scriptlanes.js +0 -660
- package/dist/scripts.buildengine.d.ts +0 -1
- package/dist/scripts.buildengine.js +0 -39
- package/dist/scripts.securitygate.d.ts +0 -1
- package/dist/scripts.securitygate.js +0 -68
- package/dist/scripts.validateflatnative.d.ts +0 -1
- package/dist/scripts.validateflatnative.js +0 -72
- package/dist/security.d.ts +0 -500
- package/dist/security.js +0 -1169
- package/dist/seedcatalog.d.ts +0 -13
- package/dist/seedcatalog.js +0 -46
- package/dist/server.d.ts +0 -20
- package/dist/server.js +0 -53
- package/dist/speccatalog.d.ts +0 -31
- package/dist/speccatalog.js +0 -163
- package/dist/tiers.d.ts +0 -1001
- package/dist/tiers.js +0 -2169
- package/dist/utils.d.ts +0 -2
- package/dist/utils.js +0 -5
- package/dist/vaulthandoff.d.ts +0 -141
- package/dist/vaulthandoff.js +0 -444
- package/dist/virtual.d.ts +0 -640
- package/dist/virtual.js +0 -1284
- package/dist/virtualcpu.d.ts +0 -315
- package/dist/virtualcpu.js +0 -1180
- package/dist/virtualgpu.d.ts +0 -344
- package/dist/virtualgpu.js +0 -1000
- package/dist/virtualization.d.ts +0 -606
- package/dist/virtualization.js +0 -1186
- package/dist/virtualmemory.d.ts +0 -817
- package/dist/virtualmemory.js +0 -1795
- package/dist/vite.config.d.ts +0 -3
- package/dist/vite.config.js +0 -48
- package/dist/vitest.config.d.ts +0 -2
- package/dist/vitest.config.js +0 -25
- package/dist/webserver.d.ts +0 -166
- package/dist/webserver.js +0 -2226
- package/dist/workloadlaw.d.ts +0 -133
- package/dist/workloadlaw.js +0 -193
- package/manifest.json +0 -20
- package/media.ts +0 -1337
- /package/docs/{architecture.1.8.19.md → architecture1819.md} +0 -0
- /package/docs/{architecture.1.8.19.mmd → architecture1819.mmd} +0 -0
- /package/docs/{artifactresearch.1.8.9.md → artifactresearch189.md} +0 -0
- /package/docs/{batches.1816.to.1820.md → batches1816to1820.md} +0 -0
- /package/docs/{brancharchive.2026.08.18.md → brancharchive20260818.md} +0 -0
- /package/docs/{containerplatforms.1.8.17.md → containerplatforms1817.md} +0 -0
- /package/docs/{e2ugh.engine.md → e2ughengine.md} +0 -0
- /package/docs/{flatbuildresearch.1.8.11.md → flatbuildresearch1811.md} +0 -0
- /package/docs/{licenseaudit.1.8.12.md → licenseaudit1812.md} +0 -0
- /package/docs/{mobileconversionresearch.1.8.10.md → mobileconversionresearch1810.md} +0 -0
- /package/docs/{nativeidentityresearch.1.8.11.md → nativeidentityresearch1811.md} +0 -0
- /package/docs/{native.wrappers.md → nativewrappers.md} +0 -0
- /package/docs/{planning.1.8.18.md → planning1818.md} +0 -0
- /package/docs/{planning.1.8.19.md → planning1819.md} +0 -0
- /package/docs/{planning.engine.md → planningengine.md} +0 -0
- /package/docs/{releasenotes.1.8.12.md → releasenotes1812.md} +0 -0
- /package/docs/{releasenotes.1.8.15.md → releasenotes1815.md} +0 -0
- /package/docs/{releasenotes.1.8.16.md → releasenotes1816.md} +0 -0
- /package/docs/{releasenotes.1.8.17.md → releasenotes1817.md} +0 -0
- /package/docs/{releasenotes.1.8.18.md → releasenotes1818.md} +0 -0
- /package/docs/{releasenotes.1.8.19.md → releasenotes1819.md} +0 -0
- /package/docs/{reorganization.1.8.8.md → reorganization188.md} +0 -0
- /package/docs/{reorganization.1.8.9.md → reorganization189.md} +0 -0
- /package/docs/{repo.analysis.1.8.21.json → repoanalysis1821.json} +0 -0
- /package/docs/{repo.candidates.1.8.21.jsonl → repocandidates1821.jsonl} +0 -0
- /package/docs/{repo.research.1.8.16.md → reporesearch1816.md} +0 -0
- /package/docs/{repo.research.1.8.21.md → reporesearch1821.md} +0 -0
- /package/docs/{repo.selected.1.8.21.json → reposelected1821.json} +0 -0
- /package/docs/{repo.synthesis.1.8.21.md → reposynthesis1821.md} +0 -0
- /package/docs/{research.1.8.18.isolation.md → research1818isolation.md} +0 -0
- /package/docs/{research.1.8.19.virtual.browser.md → research1819virtualbrowser.md} +0 -0
- /package/docs/{scanningresearch.1.8.11.md → scanningresearch1811.md} +0 -0
- /package/docs/{securityaudit.1.8.7.md → securityaudit187.md} +0 -0
- /package/docs/{securityreferences.1.8.11.md → securityreferences1811.md} +0 -0
- /package/docs/{securityresearch.1.8.11.md → securityresearch1811.md} +0 -0
- /package/docs/{signingresearch.1.8.12.md → signingresearch1812.md} +0 -0
- /package/docs/{tauri.glib.risk.1.8.11.md → tauriglibrisk1811.md} +0 -0
- /package/docs/{todo.1.8.18.md → todo1818.md} +0 -0
- /package/docs/{todo.1.8.19.md → todo1819.md} +0 -0
- /package/docs/{toolchainresearch.1.8.9.md → toolchainresearch189.md} +0 -0
- /package/docs/{web.duplicatecleanup.md → webduplicatecleanup.md} +0 -0
- /package/docs/{web.ideas.md → webideas.md} +0 -0
- /package/docs/{workflowimprovements.2026.08.19.md → workflowimprovements20260819.md} +0 -0
package/dist/scriptlanes.js
DELETED
|
@@ -1,660 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* scriptlanes.ts — the script lanes of the saddle engine: sh execution
|
|
3
|
-
* through the universal runner, yml and yaml workflow descriptors
|
|
4
|
-
* interpreted and compiled through babel into executable job plans,
|
|
5
|
-
* background execution through the house job queue, and docker-ready
|
|
6
|
-
* descriptors for the container lane.
|
|
7
|
-
*
|
|
8
|
-
* The lanes this file opens answer the modalities the registry declares
|
|
9
|
-
* (runner-universal for the bash lane, runner-babel for the compile lane,
|
|
10
|
-
* container-docker for the descriptor lane) and keep the owner's PR15
|
|
11
|
-
* mandate verbatim: "Implemente execução de SHs / interpretação de YML /
|
|
12
|
-
* YAML com compilação via Babel para permitir execução em segundo plano /
|
|
13
|
-
* Docker".
|
|
14
|
-
*
|
|
15
|
-
* The laws every lane below keeps:
|
|
16
|
-
* - sh scripts execute server-side through the engine (the bash lane),
|
|
17
|
-
* never the visitor machine: every public run entry refuses a scope
|
|
18
|
-
* that carries window or document (assertservercontext);
|
|
19
|
-
* - yml and yaml descriptors interpret into executable job plans through
|
|
20
|
-
* the house yaml subset (documented at the parser), and the babel lane
|
|
21
|
-
* compiles the descriptor through @babel/core into a regenerated
|
|
22
|
-
* literal the engine parses back through node:vm — the roundtrip is
|
|
23
|
-
* validated, never presumed;
|
|
24
|
-
* - background execution rides the house jobqueue and the runstatuses
|
|
25
|
-
* vocabulary (created, submitted, running, succeeded, failed,
|
|
26
|
-
* cancelled): the lane kicks the run detached from the caller and the
|
|
27
|
-
* status stays observable through scriptRunStatus;
|
|
28
|
-
* - honest execution only: no faked results, a {code, message} shape on
|
|
29
|
-
* every fallible path, and deadlines enforced with a timeout kill on
|
|
30
|
-
* the child process.
|
|
31
|
-
*/
|
|
32
|
-
import { spawn } from "node:child_process";
|
|
33
|
-
import { runInNewContext } from "node:vm";
|
|
34
|
-
import { jobqueue, runrecord, runstatuses, transitionrun } from "./execution.js";
|
|
35
|
-
import { aserror, idfactory, saddleerror, validationerror } from "./foundation.js";
|
|
36
|
-
/** refuses a scope that carries window or document: the script lanes
|
|
37
|
-
* execute server-side only, and the visitor machine is never touched
|
|
38
|
-
* (the never-the-visitor-browser law). every public run entry calls
|
|
39
|
-
* this before any work. */
|
|
40
|
-
export function assertservercontext(scope = globalThis) {
|
|
41
|
-
if (scope.window !== undefined || scope.document !== undefined) {
|
|
42
|
-
throw saddleerror("the script lanes execute server-side only — the visitor browser context is refused", {
|
|
43
|
-
code: "VISITOR_CONTEXT_REFUSED",
|
|
44
|
-
details: { window: scope.window !== undefined, document: scope.document !== undefined },
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
/** validates a script plan honestly: a non-empty steps array where every
|
|
49
|
-
* step carries a non-empty string name, a string run and an optional
|
|
50
|
-
* positive finite timeoutms, with a known source and compile lane. */
|
|
51
|
-
function validateplan(plan) {
|
|
52
|
-
if (!plan || typeof plan !== "object")
|
|
53
|
-
throw validationerror("the script plan must be an object");
|
|
54
|
-
const candidate = plan;
|
|
55
|
-
if (!Array.isArray(candidate.steps) || candidate.steps.length === 0) {
|
|
56
|
-
throw validationerror("the script plan must carry a non-empty steps array");
|
|
57
|
-
}
|
|
58
|
-
for (const step of candidate.steps) {
|
|
59
|
-
if (!step || typeof step !== "object")
|
|
60
|
-
throw validationerror("every script step must be an object");
|
|
61
|
-
const record = step;
|
|
62
|
-
if (typeof record.name !== "string" || record.name.trim().length === 0) {
|
|
63
|
-
throw validationerror("every script step requires a non-empty string name");
|
|
64
|
-
}
|
|
65
|
-
if (typeof record.run !== "string")
|
|
66
|
-
throw validationerror(`the step "${record.name}" requires a string run command`);
|
|
67
|
-
if (record.timeoutms !== undefined &&
|
|
68
|
-
(typeof record.timeoutms !== "number" || !Number.isFinite(record.timeoutms) || record.timeoutms < 1)) {
|
|
69
|
-
throw validationerror(`the step "${record.name}" timeoutms must be a positive finite number`);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
if (!["sh", "yml", "yaml"].includes(candidate.source ?? "")) {
|
|
73
|
-
throw validationerror("the script plan source must be sh, yml or yaml");
|
|
74
|
-
}
|
|
75
|
-
if (!["babel", "direct"].includes(candidate.compiledvia ?? "")) {
|
|
76
|
-
throw validationerror("the script plan compile lane must be babel or direct");
|
|
77
|
-
}
|
|
78
|
-
return candidate;
|
|
79
|
-
}
|
|
80
|
-
/** validates the shared deadline option: absent passes as undefined, a
|
|
81
|
-
* present value must be a positive finite number of milliseconds. */
|
|
82
|
-
function validatedeadline(value) {
|
|
83
|
-
if (value === undefined)
|
|
84
|
-
return undefined;
|
|
85
|
-
if (typeof value !== "number" || !Number.isFinite(value) || value < 1) {
|
|
86
|
-
throw validationerror("the script run deadlinems must be a positive finite number of milliseconds");
|
|
87
|
-
}
|
|
88
|
-
return value;
|
|
89
|
-
}
|
|
90
|
-
/* ════════════════════════════════════════════════════════════════════ */
|
|
91
|
-
/* Section 2: the interpretation lanes — the sh wrap and the house yaml subset parser. */
|
|
92
|
-
/* ════════════════════════════════════════════════════════════════════ */
|
|
93
|
-
/**
|
|
94
|
-
* the interpretation lanes: interpretsh wraps a raw sh script as one
|
|
95
|
-
* direct step, and the house yaml subset parser reads the workflow
|
|
96
|
-
* descriptors (yml and yaml spellings) into step lists.
|
|
97
|
-
*/
|
|
98
|
-
/** wraps a raw sh script as a one-step plan: the script rides the bash
|
|
99
|
-
* lane directly, no compile step, so the plan declares the direct lane. */
|
|
100
|
-
export function interpretsh(script) {
|
|
101
|
-
if (typeof script !== "string" || script.trim().length === 0) {
|
|
102
|
-
throw validationerror("the sh script must be a non-empty string");
|
|
103
|
-
}
|
|
104
|
-
return { steps: [{ name: "script", run: script }], source: "sh", compiledvia: "direct" };
|
|
105
|
-
}
|
|
106
|
-
/** the yaml error factory: every interpret failure throws the honest
|
|
107
|
-
* house shape with the YAML_INTERPRET_FAILED code. */
|
|
108
|
-
function yamlerror(message) {
|
|
109
|
-
return saddleerror(`the yml interpretation failed: ${message}`, { code: "YAML_INTERPRET_FAILED" });
|
|
110
|
-
}
|
|
111
|
-
/** strips a `#` comment the way yaml does: outside quotes, and only at
|
|
112
|
-
* line start or after whitespace (a `#` glued to a word is content). */
|
|
113
|
-
function striphyamlcomment(line) {
|
|
114
|
-
let quote = null;
|
|
115
|
-
for (let index = 0; index < line.length; index += 1) {
|
|
116
|
-
const char = line[index];
|
|
117
|
-
if (quote) {
|
|
118
|
-
if (char === quote)
|
|
119
|
-
quote = null;
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
if (char === '"' || char === "'") {
|
|
123
|
-
quote = char;
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
if (char === "#" && (index === 0 || line[index - 1] === " " || line[index - 1] === "\t")) {
|
|
127
|
-
return line.slice(0, index);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
return line;
|
|
131
|
-
}
|
|
132
|
-
/** parses one scalar of the house subset: a plain string, a double- or
|
|
133
|
-
* single-quoted string, an integer or decimal, or one of the two
|
|
134
|
-
* booleans. the subset never invents types beyond these (nulls, dates
|
|
135
|
-
* and exponents stay plain strings — documented, not silently coerced). */
|
|
136
|
-
function parsehousescalar(raw, lineno) {
|
|
137
|
-
const text = raw.trim();
|
|
138
|
-
if (text.length === 0) {
|
|
139
|
-
throw yamlerror(`line ${lineno}: the value after the colon is empty (block scalars are outside the house subset)`);
|
|
140
|
-
}
|
|
141
|
-
if (text.length >= 2 && text.startsWith('"') && text.endsWith('"')) {
|
|
142
|
-
const inner = text.slice(1, -1);
|
|
143
|
-
if (inner.includes('"'))
|
|
144
|
-
throw yamlerror(`line ${lineno}: an unescaped double quote rides inside a quoted scalar`);
|
|
145
|
-
return inner;
|
|
146
|
-
}
|
|
147
|
-
if (text.length >= 2 && text.startsWith("'") && text.endsWith("'")) {
|
|
148
|
-
return text.slice(1, -1).replace(/''/g, "'");
|
|
149
|
-
}
|
|
150
|
-
if (text === "true")
|
|
151
|
-
return true;
|
|
152
|
-
if (text === "false")
|
|
153
|
-
return false;
|
|
154
|
-
if (/^-?\d+$/.test(text) || /^-?\d+\.\d+$/.test(text))
|
|
155
|
-
return Number(text);
|
|
156
|
-
return text;
|
|
157
|
-
}
|
|
158
|
-
/** the house yaml subset the lane interprets, documented honestly:
|
|
159
|
-
* exactly one top-level mapping key — `steps:` — whose value is an
|
|
160
|
-
* indented list of flat mappings; every list item carries `name:` (a
|
|
161
|
-
* string), `run:` (a string) and the optional `timeoutms:` (a positive
|
|
162
|
-
* number); values are single-line scalars (plain or quoted strings,
|
|
163
|
-
* integers, decimals, true and false); comments start at a `#` outside
|
|
164
|
-
* quotes after whitespace; every list line shares one indentation and
|
|
165
|
-
* every key of one item shares a deeper one — a dedent or a mismatch is
|
|
166
|
-
* an honest refusal, never a guess. unknown keys, duplicate keys,
|
|
167
|
-
* document markers, nested mappings and block scalars are all outside
|
|
168
|
-
* the subset and refuse with YAML_INTERPRET_FAILED. */
|
|
169
|
-
function parsehousesubset(text) {
|
|
170
|
-
if (typeof text !== "string")
|
|
171
|
-
throw validationerror("the yml descriptor must be a string");
|
|
172
|
-
const steps = [];
|
|
173
|
-
const lines = text.split(/\r?\n/);
|
|
174
|
-
let sawsteps = false;
|
|
175
|
-
let itemindent = -1;
|
|
176
|
-
let fieldindent = -1;
|
|
177
|
-
let current = null;
|
|
178
|
-
function closecurrent(lineno) {
|
|
179
|
-
if (!current)
|
|
180
|
-
return;
|
|
181
|
-
const name = current.get("name");
|
|
182
|
-
const run = current.get("run");
|
|
183
|
-
const timeoutms = current.get("timeoutms");
|
|
184
|
-
if (typeof name !== "string" || name.trim().length === 0) {
|
|
185
|
-
throw yamlerror(`line ${lineno}: every step requires a non-empty string name`);
|
|
186
|
-
}
|
|
187
|
-
if (typeof run !== "string")
|
|
188
|
-
throw yamlerror(`line ${lineno}: the step "${name}" requires a string run command`);
|
|
189
|
-
if (timeoutms !== undefined && (typeof timeoutms !== "number" || !Number.isFinite(timeoutms) || timeoutms < 1)) {
|
|
190
|
-
throw yamlerror(`line ${lineno}: the step "${name}" timeoutms must be a positive number`);
|
|
191
|
-
}
|
|
192
|
-
steps.push(timeoutms === undefined ? { name, run } : { name, run, timeoutms });
|
|
193
|
-
current = null;
|
|
194
|
-
}
|
|
195
|
-
function parsefield(content, lineno) {
|
|
196
|
-
const match = /^([A-Za-z][A-Za-z0-9_-]*):(?:[ \t]+(.*))?$/.exec(content);
|
|
197
|
-
if (!match)
|
|
198
|
-
throw yamlerror(`line ${lineno}: the house subset speaks "key: value" lines — malformed: ${content}`);
|
|
199
|
-
const key = match[1];
|
|
200
|
-
if (!["name", "run", "timeoutms"].includes(key)) {
|
|
201
|
-
throw yamlerror(`line ${lineno}: unknown key "${key}" — the house subset carries name, run and timeoutms only`);
|
|
202
|
-
}
|
|
203
|
-
if (current?.has(key))
|
|
204
|
-
throw yamlerror(`line ${lineno}: the key "${key}" appears twice in one step`);
|
|
205
|
-
if (match[2] === undefined || match[2].trim().length === 0) {
|
|
206
|
-
throw yamlerror(`line ${lineno}: the value for "${key}" is missing (block scalars are outside the house subset)`);
|
|
207
|
-
}
|
|
208
|
-
current?.set(key, parsehousescalar(match[2], lineno));
|
|
209
|
-
}
|
|
210
|
-
for (let index = 0; index < lines.length; index += 1) {
|
|
211
|
-
const lineno = index + 1;
|
|
212
|
-
const stripped = striphyamlcomment(lines[index]).replace(/\s+$/, "");
|
|
213
|
-
if (stripped.trim().length === 0)
|
|
214
|
-
continue;
|
|
215
|
-
const indent = stripped.length - stripped.replace(/^ +/, "").length;
|
|
216
|
-
const content = stripped.trim();
|
|
217
|
-
if (!sawsteps) {
|
|
218
|
-
if (indent !== 0)
|
|
219
|
-
throw yamlerror(`line ${lineno}: the descriptor must open with the top-level steps key at column zero`);
|
|
220
|
-
if (content !== "steps:") {
|
|
221
|
-
throw yamlerror(`line ${lineno}: the house subset carries exactly one top-level key (steps) — found: ${content}`);
|
|
222
|
-
}
|
|
223
|
-
sawsteps = true;
|
|
224
|
-
continue;
|
|
225
|
-
}
|
|
226
|
-
if (content === "-" || content.startsWith("- ")) {
|
|
227
|
-
closecurrent(lineno);
|
|
228
|
-
if (itemindent === -1) {
|
|
229
|
-
if (indent === 0)
|
|
230
|
-
throw yamlerror(`line ${lineno}: the steps list must be indented under the steps key`);
|
|
231
|
-
itemindent = indent;
|
|
232
|
-
}
|
|
233
|
-
else if (indent !== itemindent) {
|
|
234
|
-
throw yamlerror(`line ${lineno}: the steps list items must share one indentation (expected column ${itemindent}, found ${indent})`);
|
|
235
|
-
}
|
|
236
|
-
current = new Map();
|
|
237
|
-
fieldindent = -1;
|
|
238
|
-
const inline = content === "-" ? "" : content.slice(2).trim();
|
|
239
|
-
if (inline.length > 0) {
|
|
240
|
-
fieldindent = stripped.indexOf(inline);
|
|
241
|
-
parsefield(inline, lineno);
|
|
242
|
-
}
|
|
243
|
-
continue;
|
|
244
|
-
}
|
|
245
|
-
if (!current)
|
|
246
|
-
throw yamlerror(`line ${lineno}: a steps entry must start with a list dash (found: ${content})`);
|
|
247
|
-
if (indent === 0) {
|
|
248
|
-
throw yamlerror(`line ${lineno}: a second top-level key arrived after the steps list: ${content}`);
|
|
249
|
-
}
|
|
250
|
-
if (fieldindent === -1) {
|
|
251
|
-
if (indent <= itemindent)
|
|
252
|
-
throw yamlerror(`line ${lineno}: a bare list dash must be followed by deeper-indented keys`);
|
|
253
|
-
fieldindent = indent;
|
|
254
|
-
}
|
|
255
|
-
if (indent !== fieldindent) {
|
|
256
|
-
throw yamlerror(`line ${lineno}: bad indentation — the keys of one step must share column ${fieldindent} (found column ${indent})`);
|
|
257
|
-
}
|
|
258
|
-
parsefield(content, lineno);
|
|
259
|
-
}
|
|
260
|
-
closecurrent(lines.length);
|
|
261
|
-
if (!sawsteps)
|
|
262
|
-
throw yamlerror("the descriptor never carries the top-level steps key");
|
|
263
|
-
if (steps.length === 0)
|
|
264
|
-
throw yamlerror("the steps list is empty — the descriptor must carry at least one step");
|
|
265
|
-
return steps;
|
|
266
|
-
}
|
|
267
|
-
/** interprets a yml workflow descriptor into an executable job plan
|
|
268
|
-
* through the house yaml subset; the plan declares the babel compile
|
|
269
|
-
* lane (compilewithbabel performs it). a parse failure throws the honest
|
|
270
|
-
* YAML_INTERPRET_FAILED code. */
|
|
271
|
-
export function interpretyml(text) {
|
|
272
|
-
return { steps: parsehousesubset(text), source: "yml", compiledvia: "babel" };
|
|
273
|
-
}
|
|
274
|
-
/** interprets a yaml workflow descriptor — the same house subset under
|
|
275
|
-
* the yaml spelling, so both file extensions ride one lane. */
|
|
276
|
-
export function interpretyaml(text) {
|
|
277
|
-
return { steps: parsehousesubset(text), source: "yaml", compiledvia: "babel" };
|
|
278
|
-
}
|
|
279
|
-
/* ════════════════════════════════════════════════════════════════════ */
|
|
280
|
-
/* Section 3: the babel compile lane — the descriptor roundtrips through @babel/core and node:vm. */
|
|
281
|
-
/* ════════════════════════════════════════════════════════════════════ */
|
|
282
|
-
/**
|
|
283
|
-
* the babel compile lane: the plan descriptor is serialized to a
|
|
284
|
-
* javascript literal, regenerated through @babel/core transformSync (no
|
|
285
|
-
* presets — the transform parses and re-emits the source), and parsed
|
|
286
|
-
* back through node:vm runInNewContext in a fresh context that carries
|
|
287
|
-
* no globals, so the compiled text is data, never executable logic. the
|
|
288
|
-
* roundtrip is validated against the input plan — a loss refuses with
|
|
289
|
-
* BABEL_COMPILE_FAILED instead of degrading, and a missing @babel/core
|
|
290
|
-
* refuses with BABEL_RUNTIME_ABSENT.
|
|
291
|
-
*/
|
|
292
|
-
/** the honest refusal when @babel/core is not resolvable at runtime. */
|
|
293
|
-
function babelabsenterror(cause) {
|
|
294
|
-
return saddleerror("the babel compile lane requires @babel/core at runtime and it is absent in this install — the compile refuses instead of degrading", { code: "BABEL_RUNTIME_ABSENT", cause, details: { lane: "runner-babel" } });
|
|
295
|
-
}
|
|
296
|
-
/** compiles a script plan through the babel lane: serialize the plan to
|
|
297
|
-
* a javascript literal, transform it through @babel/core (no presets,
|
|
298
|
-
* no config files — the minimal options the runtime carries), parse the
|
|
299
|
-
* regenerated source back through node:vm, validate the roundtrip kept
|
|
300
|
-
* every step and the source, and return the plan stamped
|
|
301
|
-
* compiledvia "babel". */
|
|
302
|
-
export async function compilewithbabel(plan) {
|
|
303
|
-
assertservercontext();
|
|
304
|
-
validateplan(plan);
|
|
305
|
-
let transform = null;
|
|
306
|
-
try {
|
|
307
|
-
const mod = (await import("@babel/core"));
|
|
308
|
-
const candidate = mod.transformSync ?? mod.default?.transformSync;
|
|
309
|
-
if (typeof candidate === "function") {
|
|
310
|
-
transform = candidate;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
catch (error) {
|
|
314
|
-
throw babelabsenterror(error);
|
|
315
|
-
}
|
|
316
|
-
if (!transform)
|
|
317
|
-
throw babelabsenterror();
|
|
318
|
-
const serialized = `(${JSON.stringify({ steps: plan.steps, source: plan.source }, null, 2)});`;
|
|
319
|
-
let compiled;
|
|
320
|
-
try {
|
|
321
|
-
const result = transform(serialized, { configFile: false, babelrc: false, filename: "scriptplan.js" });
|
|
322
|
-
if (result === null || typeof result.code !== "string")
|
|
323
|
-
throw new Error("the babel transform returned no code");
|
|
324
|
-
compiled = result.code;
|
|
325
|
-
}
|
|
326
|
-
catch (error) {
|
|
327
|
-
const failure = error;
|
|
328
|
-
throw saddleerror(`the babel compile of the ${plan.source} descriptor failed: ${String(failure?.message ?? failure)}`, {
|
|
329
|
-
code: "BABEL_COMPILE_FAILED",
|
|
330
|
-
cause: error,
|
|
331
|
-
});
|
|
332
|
-
}
|
|
333
|
-
let parsed;
|
|
334
|
-
try {
|
|
335
|
-
parsed = runInNewContext(compiled, {}, { timeout: 5000 });
|
|
336
|
-
}
|
|
337
|
-
catch (error) {
|
|
338
|
-
throw saddleerror("the compiled descriptor did not parse back through the vm lane", {
|
|
339
|
-
code: "BABEL_COMPILE_FAILED",
|
|
340
|
-
cause: error,
|
|
341
|
-
});
|
|
342
|
-
}
|
|
343
|
-
const roundtripped = parsed;
|
|
344
|
-
if (JSON.stringify(roundtripped?.steps) !== JSON.stringify(plan.steps) || roundtripped?.source !== plan.source) {
|
|
345
|
-
throw saddleerror("the babel roundtrip lost plan content — the compile is refused, never faked", {
|
|
346
|
-
code: "BABEL_COMPILE_FAILED",
|
|
347
|
-
});
|
|
348
|
-
}
|
|
349
|
-
/* the validated plan steps ride onward (the vm objects live in the
|
|
350
|
-
fresh context and are the proof of the roundtrip, not the payload) */
|
|
351
|
-
return { steps: plan.steps, source: plan.source, compiledvia: "babel" };
|
|
352
|
-
}
|
|
353
|
-
/* ════════════════════════════════════════════════════════════════════ */
|
|
354
|
-
/* Section 4: the execution lane — sequential server-side bash with enforced deadlines. */
|
|
355
|
-
/* ════════════════════════════════════════════════════════════════════ */
|
|
356
|
-
/**
|
|
357
|
-
* the execution lane: every step runs server-side through `bash -c` on
|
|
358
|
-
* the engine machine (the runner-universal lane), with the exit code,
|
|
359
|
-
* both streams and the duration captured honestly. a failing step fails
|
|
360
|
-
* the plan and the later steps are skipped — reported as skipped, never
|
|
361
|
-
* silently dropped.
|
|
362
|
-
*/
|
|
363
|
-
/** the default per-step deadline when neither the step nor the caller
|
|
364
|
-
* carries one: ten seconds, the house default the mandate names. */
|
|
365
|
-
const DEFAULTSTEPTIMEOUTMS = 10000;
|
|
366
|
-
/** the stream capture cap (about one megabyte per stream): the lane
|
|
367
|
-
* keeps draining past the cap but stops accumulating, and flags the
|
|
368
|
-
* truncation on the step record so the report never lies about size. */
|
|
369
|
-
const STREAMCAP = 1_000_000;
|
|
370
|
-
/** builds the honest failure entry for a step that could not spawn. */
|
|
371
|
-
function spawnfailure(step, error) {
|
|
372
|
-
const failure = error;
|
|
373
|
-
return {
|
|
374
|
-
code: "SCRIPT_SPAWN_FAILED",
|
|
375
|
-
message: `the bash lane could not spawn the step "${step.name}": ${String(failure?.message ?? failure)}`,
|
|
376
|
-
};
|
|
377
|
-
}
|
|
378
|
-
/** runs one step through `bash -c` with a timeout kill: the child is
|
|
379
|
-
* spawned with piped streams, the deadline timer sends SIGKILL when it
|
|
380
|
-
* fires, and the record carries the exit code, the signal, both captured
|
|
381
|
-
* streams (capped honestly), the duration and the failure entry. */
|
|
382
|
-
function runstep(step, timeoutms) {
|
|
383
|
-
return new Promise((resolve) => {
|
|
384
|
-
const startedat = Date.now();
|
|
385
|
-
let stdout = "";
|
|
386
|
-
let stderr = "";
|
|
387
|
-
let stdouttruncated = false;
|
|
388
|
-
let stderrtruncated = false;
|
|
389
|
-
let timedout = false;
|
|
390
|
-
let settled = false;
|
|
391
|
-
const finish = (record) => {
|
|
392
|
-
if (settled)
|
|
393
|
-
return;
|
|
394
|
-
settled = true;
|
|
395
|
-
resolve(record);
|
|
396
|
-
};
|
|
397
|
-
let child;
|
|
398
|
-
try {
|
|
399
|
-
child = spawn("bash", ["-c", step.run], { stdio: ["ignore", "pipe", "pipe"] });
|
|
400
|
-
}
|
|
401
|
-
catch (error) {
|
|
402
|
-
finish({
|
|
403
|
-
name: step.name,
|
|
404
|
-
exitcode: null,
|
|
405
|
-
signal: null,
|
|
406
|
-
stdout: "",
|
|
407
|
-
stderr: "",
|
|
408
|
-
durationms: Date.now() - startedat,
|
|
409
|
-
error: spawnfailure(step, error),
|
|
410
|
-
});
|
|
411
|
-
return;
|
|
412
|
-
}
|
|
413
|
-
const timer = setTimeout(() => {
|
|
414
|
-
timedout = true;
|
|
415
|
-
child.kill("SIGKILL");
|
|
416
|
-
}, timeoutms);
|
|
417
|
-
const capture = (chunk, target) => {
|
|
418
|
-
const text = chunk.toString("utf8");
|
|
419
|
-
if (target === "stdout") {
|
|
420
|
-
if (stdout.length < STREAMCAP)
|
|
421
|
-
stdout += text;
|
|
422
|
-
else
|
|
423
|
-
stdouttruncated = true;
|
|
424
|
-
}
|
|
425
|
-
else if (stderr.length < STREAMCAP)
|
|
426
|
-
stderr += text;
|
|
427
|
-
else
|
|
428
|
-
stderrtruncated = true;
|
|
429
|
-
};
|
|
430
|
-
child.stdout?.on("data", (chunk) => capture(chunk, "stdout"));
|
|
431
|
-
child.stderr?.on("data", (chunk) => capture(chunk, "stderr"));
|
|
432
|
-
child.on("error", (error) => {
|
|
433
|
-
clearTimeout(timer);
|
|
434
|
-
finish({
|
|
435
|
-
name: step.name,
|
|
436
|
-
exitcode: null,
|
|
437
|
-
signal: null,
|
|
438
|
-
stdout,
|
|
439
|
-
stderr,
|
|
440
|
-
durationms: Date.now() - startedat,
|
|
441
|
-
error: spawnfailure(step, error),
|
|
442
|
-
});
|
|
443
|
-
});
|
|
444
|
-
child.on("close", (exitcode, signal) => {
|
|
445
|
-
clearTimeout(timer);
|
|
446
|
-
const ok = exitcode === 0 && !timedout;
|
|
447
|
-
finish({
|
|
448
|
-
name: step.name,
|
|
449
|
-
exitcode,
|
|
450
|
-
signal,
|
|
451
|
-
stdout,
|
|
452
|
-
stderr,
|
|
453
|
-
durationms: Date.now() - startedat,
|
|
454
|
-
timedout: timedout || undefined,
|
|
455
|
-
truncated: stdouttruncated || stderrtruncated || undefined,
|
|
456
|
-
error: ok
|
|
457
|
-
? undefined
|
|
458
|
-
: timedout
|
|
459
|
-
? {
|
|
460
|
-
code: "STEP_DEADLINE_EXCEEDED",
|
|
461
|
-
message: `the step "${step.name}" exceeded its ${timeoutms}ms deadline and was killed`,
|
|
462
|
-
}
|
|
463
|
-
: {
|
|
464
|
-
code: "SCRIPT_STEP_FAILED",
|
|
465
|
-
message: `the step "${step.name}" exited with code ${exitcode}`,
|
|
466
|
-
},
|
|
467
|
-
});
|
|
468
|
-
});
|
|
469
|
-
});
|
|
470
|
-
}
|
|
471
|
-
/** executes a script plan sequentially server-side: each step runs
|
|
472
|
-
* through `bash -c` with the deadline `step.timeoutms ?? the caller
|
|
473
|
-
* deadlinems ?? ten seconds`, the exit code and both streams are
|
|
474
|
-
* captured, a failing step fails the plan honestly and every later step
|
|
475
|
-
* is skipped with a null exit code and the skipped flag. the storage
|
|
476
|
-
* option is carried for the engine commit lane — this lane executes and
|
|
477
|
-
* answers, it never pretends a commit. */
|
|
478
|
-
export async function runscript(plan, options = {}) {
|
|
479
|
-
assertservercontext();
|
|
480
|
-
validateplan(plan);
|
|
481
|
-
const deadlinems = validatedeadline(options.deadlinems);
|
|
482
|
-
const steps = [];
|
|
483
|
-
let failure;
|
|
484
|
-
for (const step of plan.steps) {
|
|
485
|
-
if (failure) {
|
|
486
|
-
steps.push({
|
|
487
|
-
name: step.name,
|
|
488
|
-
exitcode: null,
|
|
489
|
-
signal: null,
|
|
490
|
-
stdout: "",
|
|
491
|
-
stderr: "",
|
|
492
|
-
durationms: 0,
|
|
493
|
-
skipped: true,
|
|
494
|
-
});
|
|
495
|
-
continue;
|
|
496
|
-
}
|
|
497
|
-
const timeoutms = step.timeoutms ?? deadlinems ?? DEFAULTSTEPTIMEOUTMS;
|
|
498
|
-
const record = await runstep(step, timeoutms);
|
|
499
|
-
steps.push(record);
|
|
500
|
-
if (record.exitcode !== 0) {
|
|
501
|
-
failure = record.error ?? { code: "SCRIPT_STEP_FAILED", message: `the step "${step.name}" failed` };
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
return failure === undefined ? { status: "succeeded", steps } : { status: "failed", steps, error: failure };
|
|
505
|
-
}
|
|
506
|
-
/* ════════════════════════════════════════════════════════════════════ */
|
|
507
|
-
/* Section 5: the background lane — detached runs in the house job queue with observable statuses. */
|
|
508
|
-
/* ════════════════════════════════════════════════════════════════════ */
|
|
509
|
-
/**
|
|
510
|
-
* the background lane: runscriptbackground registers the run as a house
|
|
511
|
-
* run record (the runstatuses vocabulary), queues the execution through
|
|
512
|
-
* the execution.ts jobqueue (one attempt — script side effects never
|
|
513
|
-
* replay), kicks it without awaiting and returns immediately; the
|
|
514
|
-
* statuses stay observable through scriptRunStatus until the detached
|
|
515
|
-
* run settles with its result.
|
|
516
|
-
*/
|
|
517
|
-
/** the id factory the background lane stamps run ids with. */
|
|
518
|
-
const scriptrunids = idfactory();
|
|
519
|
-
/** the house job queue the detached script runs ride: concurrency two
|
|
520
|
-
* (background lanes may overlap), one attempt per run (a failed script
|
|
521
|
-
* is reported, never silently retried — honest execution only). */
|
|
522
|
-
const scriptlanequeue = jobqueue({ concurrency: 2, maxattempts: 1, retryall: false });
|
|
523
|
-
/** the registry of background runs this process carries: runid to the
|
|
524
|
-
* mutable record holder with the result once the run settles. */
|
|
525
|
-
const scriptruns = new Map();
|
|
526
|
-
/** settles one background run: walks the legal transition and stores the
|
|
527
|
-
* result. the transition from running to succeeded or failed is the one
|
|
528
|
-
* legal close of the lane, so a refusal here keeps the record as it was
|
|
529
|
-
* and never invents a status. */
|
|
530
|
-
function finalizescriptrun(runid, status, result) {
|
|
531
|
-
const holder = scriptruns.get(runid);
|
|
532
|
-
if (!holder)
|
|
533
|
-
return;
|
|
534
|
-
try {
|
|
535
|
-
holder.record = transitionrun(holder.record, status, {
|
|
536
|
-
reason: `the detached run settled as ${result.status}`,
|
|
537
|
-
});
|
|
538
|
-
}
|
|
539
|
-
catch {
|
|
540
|
-
/* the transition refused — the record stays where it was and the
|
|
541
|
-
result below still tells the truth about the execution */
|
|
542
|
-
}
|
|
543
|
-
holder.result = result;
|
|
544
|
-
}
|
|
545
|
-
/** kicks a script plan in the background: the run is registered as a
|
|
546
|
-
* house run record, queued through the house job queue and executed
|
|
547
|
-
* without awaiting — the call returns {runid, status} immediately and
|
|
548
|
-
* the statuses stay observable through scriptRunStatus (created,
|
|
549
|
-
* running, succeeded, failed — the runstatuses vocabulary). */
|
|
550
|
-
export function runscriptbackground(plan, options = {}) {
|
|
551
|
-
assertservercontext();
|
|
552
|
-
validateplan(plan);
|
|
553
|
-
validatedeadline(options.deadlinems);
|
|
554
|
-
const runid = scriptrunids.next("scriptrun");
|
|
555
|
-
let record = runrecord({
|
|
556
|
-
requestid: runid,
|
|
557
|
-
name: `scriptlane-${plan.source}`,
|
|
558
|
-
status: "created",
|
|
559
|
-
runid,
|
|
560
|
-
metadata: { source: plan.source, compiledvia: plan.compiledvia, stepcount: plan.steps.length },
|
|
561
|
-
});
|
|
562
|
-
const holder = { record };
|
|
563
|
-
scriptruns.set(runid, holder);
|
|
564
|
-
record = transitionrun(record, "submitted", {
|
|
565
|
-
runid,
|
|
566
|
-
reason: "the script lane queued the run in the house job queue",
|
|
567
|
-
});
|
|
568
|
-
record = transitionrun(record, "running", {
|
|
569
|
-
reason: "the script lane kicked the run detached from the caller",
|
|
570
|
-
});
|
|
571
|
-
holder.record = record;
|
|
572
|
-
void scriptlanequeue
|
|
573
|
-
.add(plan, async () => runscript(plan, options), { id: runid })
|
|
574
|
-
.then((result) => {
|
|
575
|
-
finalizescriptrun(runid, result.status === "succeeded" ? "succeeded" : "failed", result);
|
|
576
|
-
}, (error) => {
|
|
577
|
-
const failure = aserror(error, runid);
|
|
578
|
-
finalizescriptrun(runid, "failed", {
|
|
579
|
-
status: "failed",
|
|
580
|
-
steps: [],
|
|
581
|
-
error: { code: failure.code, message: failure.message },
|
|
582
|
-
});
|
|
583
|
-
});
|
|
584
|
-
return { runid, status: record.status };
|
|
585
|
-
}
|
|
586
|
-
/** reads the observable record of one background run: the runstatuses
|
|
587
|
-
* vocabulary status, the auditable transition history, and the result
|
|
588
|
-
* once the detached run settled. an unknown runid refuses with the
|
|
589
|
-
* honest SCRIPT_RUN_NOT_FOUND code. */
|
|
590
|
-
export function scriptRunStatus(runid) {
|
|
591
|
-
assertservercontext();
|
|
592
|
-
const key = String(runid ?? "");
|
|
593
|
-
const holder = scriptruns.get(key);
|
|
594
|
-
if (!holder) {
|
|
595
|
-
throw saddleerror(`the script run is unknown: ${key}`, { code: "SCRIPT_RUN_NOT_FOUND", details: { runid: key } });
|
|
596
|
-
}
|
|
597
|
-
if (!runstatuses.includes(holder.record.status)) {
|
|
598
|
-
throw saddleerror(`the script run record carries an out-of-vocabulary status: ${holder.record.status}`, {
|
|
599
|
-
code: "SCRIPT_RUN_CORRUPT",
|
|
600
|
-
details: { runid: key },
|
|
601
|
-
});
|
|
602
|
-
}
|
|
603
|
-
return { ...holder.record, history: [...holder.record.history], result: holder.result };
|
|
604
|
-
}
|
|
605
|
-
/* ════════════════════════════════════════════════════════════════════ */
|
|
606
|
-
/* Section 6: the docker lane — docker-ready descriptors for the container modality. */
|
|
607
|
-
/* ════════════════════════════════════════════════════════════════════ */
|
|
608
|
-
/**
|
|
609
|
-
* the docker lane: dockerdescriptor emits the docker-ready descriptor
|
|
610
|
-
* the container modality (container-docker) consumes — a Dockerfile on
|
|
611
|
-
* the house node base with every step as an escaped RUN line, plus the
|
|
612
|
-
* verbatim plan script and the sh entrypoint.
|
|
613
|
-
*/
|
|
614
|
-
/** the house node base image the descriptor pins: the Dockerfile
|
|
615
|
-
* saddle-runtime stage rides FROM node:26.8.1-slim (the node-engine
|
|
616
|
-
* service image), so the descriptor asks for the base the house
|
|
617
|
-
* actually pins rather than a floating tag. */
|
|
618
|
-
export const SCRIPTLANES_NODE_BASE = "node:26.8.1-slim";
|
|
619
|
-
/** the heredoc sentinel of the plan script block: a quoted delimiter
|
|
620
|
-
* keeps the content verbatim (no expansion, quotes survive), and a
|
|
621
|
-
* descriptor whose script carries the sentinel itself refuses honestly
|
|
622
|
-
* instead of corrupting the block. */
|
|
623
|
-
const PLANHEREDOCSENTINEL = "SCRIPTLANES-PLAN-EOF";
|
|
624
|
-
/** escapes a step command for one dockerfile RUN line: the backslash
|
|
625
|
-
* first (so the other escapes cannot be forged), then the double quote
|
|
626
|
-
* and the dollar sign (the container shell never expands the plan), and
|
|
627
|
-
* a newline folds to an escaped \n so every step rides exactly one RUN
|
|
628
|
-
* line — the multi-line safety of the lane. */
|
|
629
|
-
function dockerescape(text) {
|
|
630
|
-
return text.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\$/g, "\\$").replace(/\r?\n/g, "\\n");
|
|
631
|
-
}
|
|
632
|
-
/** emits the docker-ready descriptor for the container lane: a
|
|
633
|
-
* Dockerfile on the house node base carrying every step as an escaped
|
|
634
|
-
* RUN line (bash -c with the quotes and dollars escaped), the verbatim
|
|
635
|
-
* plan script written through a quoted heredoc COPY, and the sh
|
|
636
|
-
* entrypoint that runs it. the returned entrypoint matches the
|
|
637
|
-
* ENTRYPOINT line of the emitted Dockerfile. */
|
|
638
|
-
export function dockerdescriptor(plan) {
|
|
639
|
-
assertservercontext();
|
|
640
|
-
validateplan(plan);
|
|
641
|
-
const runlines = plan.steps.map((step) => `RUN bash -c "${dockerescape(step.run)}"`);
|
|
642
|
-
const planscript = plan.steps.map((step) => `# ${step.name}\n${step.run}`).join("\n");
|
|
643
|
-
if (planscript.split(/\r?\n/).some((line) => line.trim() === PLANHEREDOCSENTINEL)) {
|
|
644
|
-
throw saddleerror("the plan script carries the heredoc sentinel — the docker descriptor refuses instead of corrupting", {
|
|
645
|
-
code: "SCRIPTLANES_DESCRIPTOR_UNSAFE",
|
|
646
|
-
});
|
|
647
|
-
}
|
|
648
|
-
const dockerfile = [
|
|
649
|
-
`# docker-ready descriptor generated by the saddle script lanes (source: ${plan.source}, compile lane: ${plan.compiledvia}).`,
|
|
650
|
-
`FROM ${SCRIPTLANES_NODE_BASE}`,
|
|
651
|
-
"WORKDIR /app",
|
|
652
|
-
...runlines,
|
|
653
|
-
`COPY <<'${PLANHEREDOCSENTINEL}' /app/plan.sh`,
|
|
654
|
-
planscript,
|
|
655
|
-
PLANHEREDOCSENTINEL,
|
|
656
|
-
'ENTRYPOINT ["bash", "/app/plan.sh"]',
|
|
657
|
-
"",
|
|
658
|
-
].join("\n");
|
|
659
|
-
return { dockerfile, entrypoint: "bash /app/plan.sh" };
|
|
660
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|