@descryy/runtime-controller 0.2.1 → 0.3.1
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/attach-fencing.d.ts +109 -0
- package/dist/attach-fencing.d.ts.map +1 -0
- package/dist/attach-fencing.js +215 -0
- package/dist/attach-fencing.js.map +1 -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 +2 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- 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 +65 -80
- package/dist/process-manager.d.ts.map +1 -1
- package/dist/process-manager.js +80 -103
- 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 +8 -3
|
@@ -1,184 +1,100 @@
|
|
|
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 type { CapabilityStatus, FilesystemPolicy, NetworkPolicy } from "@descryy/runtime-contracts";
|
|
99
|
-
/**
|
|
100
|
-
* Real capability check: is a working Docker CLI + reachable daemon
|
|
101
|
-
* actually present, not merely "does a `docker` binary exist on PATH."
|
|
102
|
-
* `docker version` talks to the daemon; a CLI with no running daemon behind
|
|
103
|
-
* it (Docker Desktop not started, dockerd not running) fails this the same
|
|
104
|
-
* way a missing binary does -- both are "cannot enforce," and the caller
|
|
105
|
-
* does not need to tell them apart to make the right decision (refuse).
|
|
106
|
-
*/
|
|
53
|
+
/** 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." */
|
|
107
54
|
export declare function containerRuntimeCapability(env?: NodeJS.ProcessEnv): CapabilityStatus;
|
|
108
|
-
/**
|
|
109
|
-
* Mirrors `sandbox.ts`'s `filesystemIsolationCapability`/
|
|
110
|
-
* `networkIsolationCapability` split: one real mechanism underneath
|
|
111
|
-
* (a container boundary), two named capabilities because the two policies
|
|
112
|
-
* are declared, refused, and reasoned about independently at the call
|
|
113
|
-
* site.
|
|
114
|
-
*/
|
|
115
55
|
export declare function containerFilesystemIsolationCapability(env?: NodeJS.ProcessEnv): CapabilityStatus;
|
|
116
56
|
export declare function containerNetworkIsolationCapability(env?: NodeJS.ProcessEnv): CapabilityStatus;
|
|
117
|
-
/**
|
|
118
|
-
* Same posture as `sandbox.ts`'s `unsupportedNetworkPolicyReason`, matched
|
|
119
|
-
* deliberately rather than reinvented: only full denial
|
|
120
|
-
* (`{ mode: "allow", hosts: [] }`, `docker run --network none`) is
|
|
121
|
-
* enforced. A shape bwrap already discloses as unsupported is not silently
|
|
122
|
-
* claimed as supported here just because the underlying mechanism changed.
|
|
123
|
-
*/
|
|
57
|
+
/** 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. */
|
|
124
58
|
export declare function unsupportedContainerNetworkPolicyReason(policy: NetworkPolicy): string | null;
|
|
125
59
|
export declare function resolveContainerImage(interpreterCommand: string): string | null;
|
|
126
60
|
export interface ContainerSandboxOptions {
|
|
127
|
-
/**
|
|
61
|
+
/** Resolves a default base image via `resolveContainerImage` when `containerImage` is not given. */
|
|
128
62
|
readonly interpreterCommand: string;
|
|
129
|
-
/**
|
|
63
|
+
/** Command to exec inside the container -- resolved against the image's own PATH, not the host's. */
|
|
130
64
|
readonly command: string;
|
|
131
65
|
readonly args: readonly string[];
|
|
132
|
-
/**
|
|
66
|
+
/** Always bind-mounted at the identical path (`-v cwd:cwd`) -- mirrors sandbox.ts's "cwd always bound" default. */
|
|
133
67
|
readonly cwd: string;
|
|
134
68
|
readonly filesystemPolicy?: FilesystemPolicy;
|
|
135
69
|
readonly networkPolicy?: NetworkPolicy;
|
|
136
70
|
/**
|
|
137
|
-
* Env vars the
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
* interpreter happens to live), and forwarding the host's `PATH` would
|
|
143
|
-
* silently break that resolution or -- if it named a path that happens to
|
|
144
|
-
* exist for a different binary inside the image -- run the wrong thing.
|
|
145
|
-
* Every other variable is forwarded unfiltered, matching the same
|
|
146
|
-
* unfiltered-inheritance precedent `process-manager.ts` already
|
|
147
|
-
* established for the bwrap path (bwrap does not `--clearenv` either).
|
|
71
|
+
* Env vars the containerized process needs, forwarded via `-e KEY=VALUE`. `PATH` is
|
|
72
|
+
* never forwarded: the container must resolve `command` against its own image
|
|
73
|
+
* layout (`/usr/local/bin/node`, not the host's), and the host's PATH would break
|
|
74
|
+
* that or run the wrong binary. Everything else forwards unfiltered, matching
|
|
75
|
+
* bwrap's own unfiltered-inheritance precedent (no `--clearenv`).
|
|
148
76
|
*/
|
|
149
77
|
readonly processEnv?: Readonly<Record<string, string | undefined>>;
|
|
150
|
-
/** Env
|
|
78
|
+
/** Env for running the `docker` CLI itself (PATH, `DOCKER_HOST`). Defaults to `process.env`. */
|
|
151
79
|
readonly env?: NodeJS.ProcessEnv;
|
|
152
|
-
/** Explicit override
|
|
80
|
+
/** Explicit override, bypasses `resolveContainerImage`'s name-based guess. */
|
|
153
81
|
readonly containerImage?: string;
|
|
154
82
|
}
|
|
155
83
|
/**
|
|
156
|
-
* Wraps `command`/`args` with `docker run` so a
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
* bwrap, applied here too: a caller who never opts into a policy is
|
|
161
|
-
* completely unaffected by this module's existence.
|
|
84
|
+
* Wraps `command`/`args` with `docker run` so a container boundary enforces
|
|
85
|
+
* `filesystemPolicy`/`networkPolicy`. Neither declared returns them unchanged and
|
|
86
|
+
* never invokes docker -- same non-regression contract as `sandbox.ts`'s
|
|
87
|
+
* `applySandbox`.
|
|
162
88
|
*
|
|
163
|
-
* **Throws rather than silently spawning unconstrained** when a policy
|
|
164
|
-
*
|
|
165
|
-
* `networkPolicy` shape this mechanism doesn't implement -- matching
|
|
166
|
-
* `applySandbox`'s own refuse-rather-than-guess precedent exactly.
|
|
89
|
+
* **Throws rather than silently spawning unconstrained** when a policy can't
|
|
90
|
+
* actually be backed (no working Docker, unimplemented `networkPolicy` shape).
|
|
167
91
|
*
|
|
168
|
-
* **Returns a real, unique `containerName` whenever it wraps.**
|
|
169
|
-
*
|
|
170
|
-
* not
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
* the container runs under `dockerd`, not as a child of the local `docker`
|
|
175
|
-
* CLI, so killing the CLI's process group does not reliably stop the
|
|
176
|
-
* container -- observed directly (a `kill()` call left a real orphaned
|
|
177
|
-
* `node:22-slim` container running after both the CLI process and its
|
|
178
|
-
* grace period were gone). `containerName` lets the caller
|
|
179
|
-
* (`process-manager.ts`) issue a real, explicit `docker stop <name>`
|
|
180
|
-
* against the container itself as the actual mechanism, not the CLI
|
|
181
|
-
* process, for the container backend.
|
|
92
|
+
* **Returns a real, unique `containerName` whenever it wraps.** Found via real
|
|
93
|
+
* escape testing: `process-manager.ts`'s group-kill targets the local `docker` CLI
|
|
94
|
+
* process's group, which is right for bwrap but not Docker -- the container runs
|
|
95
|
+
* under `dockerd`, not as the CLI's child, so killing the CLI's group left an
|
|
96
|
+
* orphaned `node:22-slim` container running in testing. `containerName` lets the
|
|
97
|
+
* caller issue an explicit `docker stop <name>` against the container itself.
|
|
182
98
|
*/
|
|
183
99
|
export declare function applyContainerSandbox(options: ContainerSandboxOptions): {
|
|
184
100
|
readonly command: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"container-sandbox.d.ts","sourceRoot":"","sources":["../src/container-sandbox.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"container-sandbox.d.ts","sourceRoot":"","sources":["../src/container-sandbox.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AAKH,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAEpG,2OAA2O;AAC3O,wBAAgB,0BAA0B,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,gBAAgB,CAUjG;AAKD,wBAAgB,sCAAsC,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,gBAAgB,CAE7G;AAED,wBAAgB,mCAAmC,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,gBAAgB,CAE1G;AAED,+NAA+N;AAC/N,wBAAgB,uCAAuC,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,GAAG,IAAI,CAO5F;AAYD,wBAAgB,qBAAqB,CAAC,kBAAkB,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAE/E;AAED,MAAM,WAAW,uBAAuB;IACtC,oGAAoG;IACpG,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,qGAAqG;IACrG,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,mHAAmH;IACnH,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;IAC7C,QAAQ,CAAC,aAAa,CAAC,EAAE,aAAa,CAAC;IACvC;;;;;;OAMG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;IACnE,gGAAgG;IAChG,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACjC,8EAA8E;IAC9E,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;CAClC;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,uBAAuB,GAC/B;IAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IAAC,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAA;CAAE,CAkDjG"}
|
|
@@ -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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"}
|