@descryy/runtime-controller 0.2.1 → 0.3.0
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/LICENSE +6 -0
- package/dist/capability-registry.d.ts +0 -15
- package/dist/capability-registry.d.ts.map +1 -1
- package/dist/capability-registry.js +9 -19
- package/dist/capability-registry.js.map +1 -1
- package/dist/collector-version.d.ts +1 -8
- package/dist/collector-version.d.ts.map +1 -1
- package/dist/collector-version.js +1 -8
- package/dist/collector-version.js.map +1 -1
- package/dist/container-sandbox.d.ts +66 -150
- package/dist/container-sandbox.d.ts.map +1 -1
- package/dist/container-sandbox.js +62 -143
- package/dist/container-sandbox.js.map +1 -1
- package/dist/controller.d.ts +49 -106
- package/dist/controller.d.ts.map +1 -1
- package/dist/controller.js +65 -123
- package/dist/controller.js.map +1 -1
- package/dist/dependency-version-check.d.ts +19 -73
- package/dist/dependency-version-check.d.ts.map +1 -1
- package/dist/dependency-version-check.js +18 -67
- package/dist/dependency-version-check.js.map +1 -1
- package/dist/env.d.ts +4 -10
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +4 -10
- package/dist/env.js.map +1 -1
- package/dist/environment-metadata.d.ts +9 -17
- package/dist/environment-metadata.d.ts.map +1 -1
- package/dist/environment-metadata.js +12 -29
- package/dist/environment-metadata.js.map +1 -1
- package/dist/environment-version-check.d.ts +22 -65
- package/dist/environment-version-check.d.ts.map +1 -1
- package/dist/environment-version-check.js +24 -66
- package/dist/environment-version-check.js.map +1 -1
- package/dist/execution-safety.d.ts +54 -112
- package/dist/execution-safety.d.ts.map +1 -1
- package/dist/execution-safety.js +48 -102
- package/dist/execution-safety.js.map +1 -1
- package/dist/index.d.ts +0 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/orchestration.d.ts +24 -39
- package/dist/orchestration.d.ts.map +1 -1
- package/dist/orchestration.js +39 -75
- package/dist/orchestration.js.map +1 -1
- package/dist/process-collector.d.ts +8 -22
- package/dist/process-collector.d.ts.map +1 -1
- package/dist/process-collector.js +23 -55
- package/dist/process-collector.js.map +1 -1
- package/dist/process-manager.d.ts +45 -80
- package/dist/process-manager.d.ts.map +1 -1
- package/dist/process-manager.js +51 -101
- package/dist/process-manager.js.map +1 -1
- package/dist/readiness.d.ts +28 -70
- package/dist/readiness.d.ts.map +1 -1
- package/dist/readiness.js +73 -95
- package/dist/readiness.js.map +1 -1
- package/dist/sandbox.d.ts +60 -105
- package/dist/sandbox.d.ts.map +1 -1
- package/dist/sandbox.js +78 -121
- package/dist/sandbox.js.map +1 -1
- package/package.json +7 -2
|
@@ -1,111 +1,58 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* §21's execution boundary, container backend -- the macOS/Windows half of
|
|
3
|
-
*
|
|
4
|
-
* bubblewrap; this module closes it for macOS and Windows, but NOT by
|
|
5
|
-
* building a second and third native OS sandbox. That path was researched
|
|
6
|
-
* (`sandbox.ts`'s own original module doc named `sandbox-exec`/Seatbelt on
|
|
7
|
-
* macOS and Job Objects/AppContainer on Windows as candidates) and
|
|
8
|
-
* explicitly rejected as an architecture decision: `sandbox-exec` is
|
|
9
|
-
* deprecated (no public API replacement), the Endpoint Security Framework
|
|
10
|
-
* needs a special Apple entitlement this project does not have, and
|
|
11
|
-
* hand-building Job Objects + AppContainer + WFP network enforcement on
|
|
12
|
-
* Windows is a second and third bespoke native sandbox to build and
|
|
13
|
-
* maintain for the same problem already solved once.
|
|
2
|
+
* §21's execution boundary, container backend -- the macOS/Windows half of the
|
|
3
|
+
* sandbox-isolation lane (`sandbox.ts` covers Linux via bubblewrap).
|
|
14
4
|
*
|
|
15
|
-
* **
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* unreachable" boundary; nothing inside the container needs bwrap. On
|
|
21
|
-
* macOS, `docker run` targets Docker Desktop's Linux VM; on Windows, WSL2 /
|
|
22
|
-
* Docker Desktop's Windows backend. Both ultimately run the same Linux
|
|
23
|
-
* container runtime this module already drives directly.
|
|
5
|
+
* **Not a second native sandbox.** `sandbox-exec`/Seatbelt (macOS) and Job
|
|
6
|
+
* Objects/AppContainer/WFP (Windows) were researched and rejected: `sandbox-exec` is
|
|
7
|
+
* deprecated with no public API replacement, Endpoint Security needs an Apple
|
|
8
|
+
* entitlement this project doesn't have, and hand-building Windows equivalents is a
|
|
9
|
+
* second and third bespoke sandbox for a problem already solved once.
|
|
24
10
|
*
|
|
25
|
-
* **
|
|
26
|
-
*
|
|
27
|
-
*
|
|
11
|
+
* **Mechanism: route the spawned process through a real container instead**, via
|
|
12
|
+
* `docker run` (Docker Desktop's Linux VM on macOS, WSL2/Docker Desktop on Windows).
|
|
13
|
+
* The container's own rootfs is the isolation boundary; no bwrap needed inside it.
|
|
28
14
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* against a real host listener; both together on one spawn; the same
|
|
35
|
-
* boundary through the full `ExecutionController.run()` path, not just this
|
|
36
|
-
* module's own function called directly. See `container-sandbox.test.ts`
|
|
37
|
-
* and `controller.test.ts`'s container-backend end-to-end test.
|
|
15
|
+
* **Verified for real** (Linux dev machine, real Docker daemon 29.6.1, not mocked):
|
|
16
|
+
* a file outside bind-mounts is unreachable (`ENOENT`); a network call under
|
|
17
|
+
* `--network none` is refused against a real listener; both together, through the
|
|
18
|
+
* full `ExecutionController.run()` path. See `container-sandbox.test.ts`,
|
|
19
|
+
* `controller.test.ts`'s container-backend e2e test.
|
|
38
20
|
*
|
|
39
|
-
* **
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* path
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* strategy assumes the host path and the in-container path are identical
|
|
48
|
-
* (`-v hostPath:hostPath`, mirroring `sandbox.ts`'s own `--bind root root`
|
|
49
|
-
* convention) -- true on Linux, true for Docker Desktop's Linux VM when the
|
|
50
|
-
* path is already inside the VM's shared-drive mapping, but the exact
|
|
51
|
-
* translation rules for an arbitrary Windows path are not exercised here at
|
|
52
|
-
* all. This is "the container mechanism exists and is tested where it can
|
|
53
|
-
* be, but full cross-platform verification needs hardware nobody here has"
|
|
54
|
-
* -- a distinct category from a real bug or an intentionally out-of-scope
|
|
55
|
-
* capability, and this module does not claim otherwise.
|
|
21
|
+
* **UNVERIFIED on real macOS/Windows hardware** (none available here): Docker
|
|
22
|
+
* Desktop running-detection before spawn, path translation across the Docker
|
|
23
|
+
* Desktop VM boundary (macOS) and WSL2 (Windows) -- this module's bind-mount assumes
|
|
24
|
+
* host path == in-container path (`-v hostPath:hostPath`), true on Linux and for
|
|
25
|
+
* Docker Desktop's Linux VM when already inside its shared-drive mapping, untested
|
|
26
|
+
* for an arbitrary Windows path. Mechanism exists and is tested where it can be;
|
|
27
|
+
* cross-platform verification needs hardware nobody here has -- not a bug, not
|
|
28
|
+
* claimed otherwise.
|
|
56
29
|
*
|
|
57
|
-
* **
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* scope for this approach, on any platform. There is no fallback to a
|
|
62
|
-
* native sandbox for such a target; it is simply not isolatable by this
|
|
63
|
-
* mechanism, and that is disclosed rather than silently unsupported.
|
|
30
|
+
* **Scope boundary** (same treatment as attach-mode elsewhere): a target that can't
|
|
31
|
+
* run containerized at all (deep native OS integration, GUI, native deps absent
|
|
32
|
+
* from a Linux image) is out of scope on any platform, with no native-sandbox
|
|
33
|
+
* fallback -- disclosed, not silently unsupported.
|
|
64
34
|
*
|
|
65
|
-
* **
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* host's network unless `networkPolicy` is declared. A containerized
|
|
74
|
-
* process can therefore never reach the HOST's own loopback either way
|
|
75
|
-
* (that alone is not evidence `--network none` specifically did anything),
|
|
76
|
-
* but it CAN still reach the public internet via Docker's default bridge
|
|
77
|
-
* NAT when no `networkPolicy` is declared -- only `--network none`
|
|
78
|
-
* additionally blocks that. `container-sandbox.test.ts`'s dedicated
|
|
79
|
-
* mutation-sensitive test isolates exactly this variable (an external
|
|
80
|
-
* host, not the host's own loopback) to prove the flag itself is what is
|
|
81
|
-
* being tested, not mere containerization.
|
|
35
|
+
* **Measured Docker/bwrap difference** (found via a mutation check removing
|
|
36
|
+
* `--network none` and rerunning): Docker gives every container its own network
|
|
37
|
+
* namespace regardless of `--network none` -- unlike bwrap, which shares the host's
|
|
38
|
+
* network unless `networkPolicy` is declared. So a container can never reach the
|
|
39
|
+
* HOST's loopback either way, but it CAN still reach the public internet via
|
|
40
|
+
* Docker's default bridge NAT unless `--network none` is set. The dedicated
|
|
41
|
+
* mutation-sensitive test in `container-sandbox.test.ts` checks reachability to an
|
|
42
|
+
* external host (not loopback) to isolate exactly this.
|
|
82
43
|
*
|
|
83
|
-
* **
|
|
84
|
-
*
|
|
85
|
-
* `
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* network allow-/deny-listing (`NetworkPolicy` shapes other than full
|
|
91
|
-
* denial) is unimplemented here for the same reason `sandbox.ts` doesn't
|
|
92
|
-
* implement it: it needs DNS interception and IP filtering this iteration
|
|
93
|
-
* does not build -- `unsupportedContainerNetworkPolicyReason` reports this
|
|
94
|
-
* the same way `sandbox.ts`'s own `unsupportedNetworkPolicyReason` does,
|
|
95
|
-
* deliberately matching posture rather than silently claiming broader
|
|
96
|
-
* support than the underlying mechanism has.
|
|
44
|
+
* **Not solved here, disclosed:** `ResourceLimits` composition -- `sandbox.ts` wraps
|
|
45
|
+
* bwrap with `applyResourceLimits`; this module does not wrap Docker's
|
|
46
|
+
* `--memory`/`--cpus`/`--pids-limit` equivalents, so a `resourceLimits`-declared
|
|
47
|
+
* execution on this backend runs unlimited inside the container. Selective network
|
|
48
|
+
* allow/deny-listing is unimplemented for the same reason as `sandbox.ts` (needs DNS
|
|
49
|
+
* interception + IP filtering) -- `unsupportedContainerNetworkPolicyReason` reports
|
|
50
|
+
* it the same way, matching posture rather than overclaiming.
|
|
97
51
|
*/
|
|
98
52
|
import { execFileSync } from "node:child_process";
|
|
99
53
|
import { randomUUID } from "node:crypto";
|
|
100
54
|
import { basename } from "node:path";
|
|
101
|
-
/**
|
|
102
|
-
* Real capability check: is a working Docker CLI + reachable daemon
|
|
103
|
-
* actually present, not merely "does a `docker` binary exist on PATH."
|
|
104
|
-
* `docker version` talks to the daemon; a CLI with no running daemon behind
|
|
105
|
-
* it (Docker Desktop not started, dockerd not running) fails this the same
|
|
106
|
-
* way a missing binary does -- both are "cannot enforce," and the caller
|
|
107
|
-
* does not need to tell them apart to make the right decision (refuse).
|
|
108
|
-
*/
|
|
55
|
+
/** Real check: working Docker CLI + reachable daemon, not just a `docker` binary on PATH. `docker version` talks to the daemon, so a CLI with no daemon running fails the same way a missing binary does -- both mean "cannot enforce." */
|
|
109
56
|
export function containerRuntimeCapability(env = process.env) {
|
|
110
57
|
try {
|
|
111
58
|
execFileSync("docker", ["version", "--format", "{{.Server.Version}}"], { env, stdio: "ignore" });
|
|
@@ -118,26 +65,16 @@ export function containerRuntimeCapability(env = process.env) {
|
|
|
118
65
|
}
|
|
119
66
|
return { availability: "available", reason: null };
|
|
120
67
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
* (a container boundary), two named capabilities because the two policies
|
|
125
|
-
* are declared, refused, and reasoned about independently at the call
|
|
126
|
-
* site.
|
|
127
|
-
*/
|
|
68
|
+
// Mirrors sandbox.ts's filesystem/network capability split: one real mechanism
|
|
69
|
+
// (container boundary), two named capabilities since the policies are declared
|
|
70
|
+
// and reasoned about independently.
|
|
128
71
|
export function containerFilesystemIsolationCapability(env = process.env) {
|
|
129
72
|
return containerRuntimeCapability(env);
|
|
130
73
|
}
|
|
131
74
|
export function containerNetworkIsolationCapability(env = process.env) {
|
|
132
75
|
return containerRuntimeCapability(env);
|
|
133
76
|
}
|
|
134
|
-
/**
|
|
135
|
-
* Same posture as `sandbox.ts`'s `unsupportedNetworkPolicyReason`, matched
|
|
136
|
-
* deliberately rather than reinvented: only full denial
|
|
137
|
-
* (`{ mode: "allow", hosts: [] }`, `docker run --network none`) is
|
|
138
|
-
* enforced. A shape bwrap already discloses as unsupported is not silently
|
|
139
|
-
* claimed as supported here just because the underlying mechanism changed.
|
|
140
|
-
*/
|
|
77
|
+
/** Same posture as `sandbox.ts`'s `unsupportedNetworkPolicyReason`: only full denial (`--network none`) is enforced. A shape bwrap discloses as unsupported isn't silently claimed here just because the mechanism changed. */
|
|
141
78
|
export function unsupportedContainerNetworkPolicyReason(policy) {
|
|
142
79
|
if (policy.mode === "allow" && policy.hosts.length === 0)
|
|
143
80
|
return null;
|
|
@@ -145,15 +82,9 @@ export function unsupportedContainerNetworkPolicyReason(policy) {
|
|
|
145
82
|
"hosts would need a custom docker network with DNS interception and IP filtering inside it, not built in this " +
|
|
146
83
|
"iteration (the same posture the bwrap backend already declares for the same shapes)");
|
|
147
84
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
* `packages/orchestrator/test/fixtures`), keyed by the interpreter's
|
|
152
|
-
* basename so a full interpreter path (`/home/x/.nvm/versions/node/vX/bin/node`)
|
|
153
|
-
* still resolves. Not a general-purpose image registry -- a caller with a
|
|
154
|
-
* different runtime need passes `containerImage` explicitly instead of
|
|
155
|
-
* extending this table with more special cases.
|
|
156
|
-
*/
|
|
85
|
+
// Minimal official -slim/-jre images for this runtime's fixture targets, keyed by
|
|
86
|
+
// interpreter basename so a full path still resolves. Not a general registry --
|
|
87
|
+
// other runtimes pass `containerImage` explicitly instead of extending this table.
|
|
157
88
|
const KNOWN_RUNTIME_IMAGES = {
|
|
158
89
|
node: "node:22-slim",
|
|
159
90
|
python: "python:3.12-slim",
|
|
@@ -164,32 +95,20 @@ export function resolveContainerImage(interpreterCommand) {
|
|
|
164
95
|
return KNOWN_RUNTIME_IMAGES[basename(interpreterCommand)] ?? null;
|
|
165
96
|
}
|
|
166
97
|
/**
|
|
167
|
-
* Wraps `command`/`args` with `docker run` so a
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
* bwrap, applied here too: a caller who never opts into a policy is
|
|
172
|
-
* completely unaffected by this module's existence.
|
|
98
|
+
* Wraps `command`/`args` with `docker run` so a container boundary enforces
|
|
99
|
+
* `filesystemPolicy`/`networkPolicy`. Neither declared returns them unchanged and
|
|
100
|
+
* never invokes docker -- same non-regression contract as `sandbox.ts`'s
|
|
101
|
+
* `applySandbox`.
|
|
173
102
|
*
|
|
174
|
-
* **Throws rather than silently spawning unconstrained** when a policy
|
|
175
|
-
*
|
|
176
|
-
* `networkPolicy` shape this mechanism doesn't implement -- matching
|
|
177
|
-
* `applySandbox`'s own refuse-rather-than-guess precedent exactly.
|
|
103
|
+
* **Throws rather than silently spawning unconstrained** when a policy can't
|
|
104
|
+
* actually be backed (no working Docker, unimplemented `networkPolicy` shape).
|
|
178
105
|
*
|
|
179
|
-
* **Returns a real, unique `containerName` whenever it wraps.**
|
|
180
|
-
*
|
|
181
|
-
* not
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
* the container runs under `dockerd`, not as a child of the local `docker`
|
|
186
|
-
* CLI, so killing the CLI's process group does not reliably stop the
|
|
187
|
-
* container -- observed directly (a `kill()` call left a real orphaned
|
|
188
|
-
* `node:22-slim` container running after both the CLI process and its
|
|
189
|
-
* grace period were gone). `containerName` lets the caller
|
|
190
|
-
* (`process-manager.ts`) issue a real, explicit `docker stop <name>`
|
|
191
|
-
* against the container itself as the actual mechanism, not the CLI
|
|
192
|
-
* process, for the container backend.
|
|
106
|
+
* **Returns a real, unique `containerName` whenever it wraps.** Found via real
|
|
107
|
+
* escape testing: `process-manager.ts`'s group-kill targets the local `docker` CLI
|
|
108
|
+
* process's group, which is right for bwrap but not Docker -- the container runs
|
|
109
|
+
* under `dockerd`, not as the CLI's child, so killing the CLI's group left an
|
|
110
|
+
* orphaned `node:22-slim` container running in testing. `containerName` lets the
|
|
111
|
+
* caller issue an explicit `docker stop <name>` against the container itself.
|
|
193
112
|
*/
|
|
194
113
|
export function applyContainerSandbox(options) {
|
|
195
114
|
const { filesystemPolicy, networkPolicy } = options;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"container-sandbox.js","sourceRoot":"","sources":["../src/container-sandbox.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"container-sandbox.js","sourceRoot":"","sources":["../src/container-sandbox.ts"],"names":[],"mappings":"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"}
|
package/dist/controller.d.ts
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Runtime controller (
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* (never "process exists"), and guarantees cleanup — including on partial
|
|
7
|
-
* or total failure and on timeout. Builds no collectors and knows nothing
|
|
8
|
-
* about any specific language or framework (plan principle 8).
|
|
2
|
+
* Runtime controller (§4/§5/§6/§45 "Core"). Owns Execution lifecycle, spawns every
|
|
3
|
+
* configured service, waits for each one's real readiness (never "process exists"),
|
|
4
|
+
* guarantees cleanup on partial/total failure and timeout. Knows nothing about any
|
|
5
|
+
* specific language or framework (principle 8).
|
|
9
6
|
*/
|
|
10
7
|
import { type Execution, type ProcessHandle } from "@descryy/runtime-contracts";
|
|
11
8
|
import { type ReadinessCheck, type ReadinessResult } from "./readiness.ts";
|
|
@@ -23,17 +20,13 @@ export interface ServiceReadinessSpec {
|
|
|
23
20
|
readonly timeoutMs: number;
|
|
24
21
|
}
|
|
25
22
|
export interface RunOptions {
|
|
26
|
-
/** One entry required per key in `configuration.services`
|
|
23
|
+
/** One entry required per key in `configuration.services` -- refuses rather than defaulting a missing one to "spawn success is enough" (RT-024/RT-032). */
|
|
27
24
|
readonly readiness: Readonly<Record<string, ServiceReadinessSpec>>;
|
|
28
25
|
/**
|
|
29
|
-
* Optional live observer for `ProcessLifecycleEvent
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* hang, may not silently swallow its own failure" discipline `Collector`
|
|
34
|
-
* already imposes on every callback shape in this repo; a caller wiring a
|
|
35
|
-
* `ProcessCollector`'s emit path in here that itself throws deserves to
|
|
36
|
-
* see that, not have it disappear into a promise this module owns.
|
|
26
|
+
* Optional live observer for `ProcessLifecycleEvent`, called synchronously at each
|
|
27
|
+
* transition for the lifetime of this execution (including after `run()` returns).
|
|
28
|
+
* A throwing observer is not caught here -- same "must not hang, may not silently
|
|
29
|
+
* swallow its own failure" discipline `Collector` imposes on every callback here.
|
|
37
30
|
*/
|
|
38
31
|
readonly onProcessLifecycleEvent?: (event: ProcessLifecycleEvent) => void;
|
|
39
32
|
}
|
|
@@ -41,31 +34,20 @@ export interface ServiceStartResult {
|
|
|
41
34
|
readonly serviceName: string;
|
|
42
35
|
readonly stage: "spawn" | "readiness" | "not-attempted";
|
|
43
36
|
readonly succeeded: boolean;
|
|
44
|
-
/** Set when `stage === "readiness"`. RT-024/RT-031 apply
|
|
37
|
+
/** Set when `stage === "readiness"`. RT-024/RT-031 apply per entry -- N of these is N liveness claims, never a joint identity proof. */
|
|
45
38
|
readonly readiness: ReadinessResult | null;
|
|
46
39
|
/** Set when `stage === "spawn"` and spawning itself threw. */
|
|
47
40
|
readonly error: string | null;
|
|
48
41
|
}
|
|
49
42
|
/**
|
|
50
|
-
* Fires at the exact moments `run()`
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* ignorant of Evidence/Collector as its own module doc already claims
|
|
54
|
-
* ("knows nothing about any specific language or framework") — nothing
|
|
55
|
-
* here decides whether a lifecycle transition becomes Evidence, only that
|
|
56
|
-
* a caller who wants to know *when* one happened, live, has a way to ask
|
|
57
|
-
* that isn't "reconstruct it later from `Execution.processes`."
|
|
43
|
+
* Fires at the exact moments `run()` observes a service's lifecycle -- spawned,
|
|
44
|
+
* readiness settled either way, reaped. A pure notification channel; nothing here
|
|
45
|
+
* decides whether a transition becomes Evidence.
|
|
58
46
|
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* `
|
|
62
|
-
*
|
|
63
|
-
* has not exited yet at that point — its own eventual `process-exited`
|
|
64
|
-
* event lies in the future relative to `run()` resolving. A consumer that
|
|
65
|
-
* only reads the final `Execution` after `run()` settles can see "started"
|
|
66
|
-
* and "ready" (or "ready-failed") for every service, but can never see
|
|
67
|
-
* "exited" for one still healthy when `run()` returned — only this live
|
|
68
|
-
* channel can.
|
|
47
|
+
* Needed because `Execution.processes`/`ServiceStartResults` are only complete once
|
|
48
|
+
* `run()` returns, but a service still running when `run()` resolves hasn't exited
|
|
49
|
+
* yet -- its eventual `process-exited` lies in the future. Only this live channel
|
|
50
|
+
* can observe that exit; reading the final `Execution` later cannot.
|
|
69
51
|
*/
|
|
70
52
|
export type ProcessLifecycleEvent = {
|
|
71
53
|
readonly kind: "process-started";
|
|
@@ -83,19 +65,11 @@ export type ProcessLifecycleEvent = {
|
|
|
83
65
|
readonly readiness: ReadinessResult;
|
|
84
66
|
}
|
|
85
67
|
/**
|
|
86
|
-
* `phase` distinguishes "
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
* "finished/was stopped after running successfully," which collapses
|
|
92
|
-
* exactly the distinction the execution lifecycle already exists to
|
|
93
|
-
* carry. Computed from THIS service's own readiness history, not from
|
|
94
|
-
* `Execution.state` at the moment of exit — a crash mid-readiness-poll
|
|
95
|
-
* fires before `run()` has itself observed the failure and moved the
|
|
96
|
-
* execution to `FAILED_START`, so reading execution state at exit time
|
|
97
|
-
* would misreport `"starting"` as whatever the execution's not-yet-
|
|
98
|
-
* updated state still says.
|
|
68
|
+
* `phase` distinguishes "readiness hadn't yet succeeded when it exited"
|
|
69
|
+
* (`"starting"`) from "had already reached READY" (`"running"`) -- the
|
|
70
|
+
* process-level echo of `FAILED_START` vs `FAILED` (§30). Computed from this
|
|
71
|
+
* service's own readiness history, not `Execution.state` at exit time -- a crash
|
|
72
|
+
* mid-readiness-poll fires before the execution's own state has caught up.
|
|
99
73
|
*/
|
|
100
74
|
| {
|
|
101
75
|
readonly kind: "process-exited";
|
|
@@ -103,82 +77,51 @@ export type ProcessLifecycleEvent = {
|
|
|
103
77
|
readonly handle: ProcessHandle;
|
|
104
78
|
readonly phase: "starting" | "running";
|
|
105
79
|
};
|
|
106
|
-
/**
|
|
107
|
-
* `run()`'s timeout timer and a caller's `stop()`/`cancel()` can both
|
|
108
|
-
* attempt to move a finishing execution to a terminal state at nearly the
|
|
109
|
-
* same instant. Every transition attempted after an `await` goes through
|
|
110
|
-
* `#tryTransition` (returns false instead of throwing) rather than the
|
|
111
|
-
* throwing `#transition`, so whichever path loses the race degrades to a
|
|
112
|
-
* no-op instead of an unhandled rejection from a fire-and-forget timer
|
|
113
|
-
* callback.
|
|
114
|
-
*/
|
|
115
80
|
export declare class ExecutionController {
|
|
116
81
|
#private;
|
|
117
82
|
private constructor();
|
|
118
83
|
static create(input: CreateExecutionInput): ExecutionController;
|
|
119
84
|
get execution(): Execution;
|
|
120
|
-
/** One entry per service
|
|
85
|
+
/** One entry per configured service, in spawn order, including never-attempted ones (`stage: "not-attempted"`). Empty until `run()` returns its first result. */
|
|
121
86
|
get serviceStartResults(): readonly ServiceStartResult[];
|
|
122
|
-
/** Non-null only when `run()` refused before spawning anything
|
|
87
|
+
/** Non-null only when `run()` refused before spawning anything. */
|
|
123
88
|
get validationError(): string | null;
|
|
124
89
|
/** Combined stdout+stderr of the named service's process so far, or "" if it was never spawned. */
|
|
125
90
|
readProcessOutput(serviceName: string): string;
|
|
126
91
|
/**
|
|
127
|
-
* Starts every service in
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
* failure (spawn/attach throw, or readiness never succeeding), every
|
|
134
|
-
* already-started service is killed in reverse order (a no-op for an
|
|
135
|
-
* attached one — see `attachManagedProcess`'s own doc) and every
|
|
136
|
-
* not-yet-attempted service is recorded as such — a caller can always
|
|
137
|
-
* tell "never tried" from "tried and failed" via `serviceStartResults`.
|
|
92
|
+
* Starts every service in dependency order, spawned (each on its own ephemeral
|
|
93
|
+
* port unless fixed) or attached (`service.attach`, for an already-running
|
|
94
|
+
* pid/log file). Reaches RUNNING only if every service starts and reaches
|
|
95
|
+
* readiness. On first failure, every started service is killed in reverse order
|
|
96
|
+
* (no-op for attached ones) and remaining services are recorded "not-attempted" --
|
|
97
|
+
* `serviceStartResults` always distinguishes "never tried" from "tried and failed".
|
|
138
98
|
*
|
|
139
|
-
* Configuration
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
* `RunOptions.readiness` entry refuses before anything is spawned, never
|
|
143
|
-
* discovered mid-sequence with other services already running.
|
|
99
|
+
* Configuration is validated atomically first -- a bad `dependsOn`, a cycle, an
|
|
100
|
+
* undeclared placeholder, ambiguous `command`/`attach`, or a missing readiness
|
|
101
|
+
* entry refuses before anything spawns.
|
|
144
102
|
*
|
|
145
|
-
* Every **spawned** service
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
* `ServiceConfiguration` whose command reads its listen port from
|
|
151
|
-
* anywhere other than `PORT` will not be reachable through readiness
|
|
152
|
-
* checks built from the same `port` this function resolves. An
|
|
153
|
-
* **attached** service receives no env and no port allocation — it is
|
|
154
|
-
* already running on whatever port it bound at its own launch, and
|
|
155
|
-
* `service.port` (when the caller supplies it) is a declaration of that
|
|
156
|
-
* fact, not a request this function can act on.
|
|
103
|
+
* Every **spawned** service gets `PORT` (its resolved port) unconditionally --
|
|
104
|
+
* load-bearing (matches the fixture apps), not incidental: a command reading its
|
|
105
|
+
* port from anywhere else won't be reachable via readiness checks built from the
|
|
106
|
+
* same `port`. An **attached** service gets no env/port allocation -- it's already
|
|
107
|
+
* running on whatever it bound at launch.
|
|
157
108
|
*/
|
|
158
109
|
run(options: RunOptions): Promise<Execution>;
|
|
159
110
|
stop(): Promise<Execution>;
|
|
160
111
|
/**
|
|
161
|
-
* Stop everything and record
|
|
162
|
-
*
|
|
163
|
-
* before this controller's own timer got round to saying so.
|
|
164
|
-
*
|
|
165
|
-
* **Why a caller needs this, and why `stop()` is wrong for the case.**
|
|
166
|
-
* `ExecutionConfiguration.timeoutMs` is a whole-execution budget, and the
|
|
167
|
-
* orchestrator clamps its observation window to whatever the budget has left.
|
|
168
|
-
* That clamp made `TIMED_OUT` a race: the window ends at the deadline, the
|
|
169
|
-
* timer fires a hair later, and whichever of `#onTimeout` and `stop()` gets
|
|
170
|
-
* there first decides what the run *says* happened. Measured: the same case
|
|
171
|
-
* reported `TIMED_OUT` at 1534ms and `COMPLETED` at 1515ms.
|
|
112
|
+
* Stop everything and record `TIMED_OUT` -- same transition `#onTimeout` makes,
|
|
113
|
+
* for a caller that knows the budget expired before the internal timer noticed.
|
|
172
114
|
*
|
|
173
|
-
* `
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
115
|
+
* **Why not `stop()`:** the orchestrator clamps its observation window to
|
|
116
|
+
* whatever's left of `timeoutMs`, making `TIMED_OUT` a race between `#onTimeout`
|
|
117
|
+
* and `stop()` at the deadline -- measured: the same case reported `TIMED_OUT` at
|
|
118
|
+
* 1534ms and `COMPLETED` at 1515ms. `execution.state` is what `observe_runtime`
|
|
119
|
+
* reads to choose `ok` vs `timed_out`, so losing that race silently hides a real
|
|
120
|
+
* cutoff. A caller that already decided it truncated the run shouldn't have to
|
|
121
|
+
* win a race to report it.
|
|
179
122
|
*
|
|
180
|
-
* Idempotent and terminal-safe
|
|
181
|
-
*
|
|
123
|
+
* Idempotent and terminal-safe like `cancel()`: if the timer fired first, returns
|
|
124
|
+
* the execution it already finished.
|
|
182
125
|
*/
|
|
183
126
|
timeout(): Promise<Execution>;
|
|
184
127
|
cancel(): Promise<Execution>;
|
package/dist/controller.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../src/controller.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../src/controller.ts"],"names":[],"mappings":"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"}
|