@descryy/runtime-controller 0.0.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.
Files changed (61) hide show
  1. package/dist/capability-registry.d.ts +24 -0
  2. package/dist/capability-registry.d.ts.map +1 -0
  3. package/dist/capability-registry.js +42 -0
  4. package/dist/capability-registry.js.map +1 -0
  5. package/dist/collector-version.d.ts +10 -0
  6. package/dist/collector-version.d.ts.map +1 -0
  7. package/dist/collector-version.js +12 -0
  8. package/dist/collector-version.js.map +1 -0
  9. package/dist/container-sandbox.d.ts +188 -0
  10. package/dist/container-sandbox.d.ts.map +1 -0
  11. package/dist/container-sandbox.js +233 -0
  12. package/dist/container-sandbox.js.map +1 -0
  13. package/dist/controller.d.ts +162 -0
  14. package/dist/controller.d.ts.map +1 -0
  15. package/dist/controller.js +433 -0
  16. package/dist/controller.js.map +1 -0
  17. package/dist/dependency-version-check.d.ts +90 -0
  18. package/dist/dependency-version-check.d.ts.map +1 -0
  19. package/dist/dependency-version-check.js +121 -0
  20. package/dist/dependency-version-check.js.map +1 -0
  21. package/dist/env.d.ts +16 -0
  22. package/dist/env.d.ts.map +1 -0
  23. package/dist/env.js +18 -0
  24. package/dist/env.js.map +1 -0
  25. package/dist/environment-metadata.d.ts +23 -0
  26. package/dist/environment-metadata.d.ts.map +1 -0
  27. package/dist/environment-metadata.js +80 -0
  28. package/dist/environment-metadata.js.map +1 -0
  29. package/dist/environment-version-check.d.ts +83 -0
  30. package/dist/environment-version-check.d.ts.map +1 -0
  31. package/dist/environment-version-check.js +218 -0
  32. package/dist/environment-version-check.js.map +1 -0
  33. package/dist/execution-safety.d.ts +154 -0
  34. package/dist/execution-safety.d.ts.map +1 -0
  35. package/dist/execution-safety.js +194 -0
  36. package/dist/execution-safety.js.map +1 -0
  37. package/dist/index.d.ts +35 -0
  38. package/dist/index.d.ts.map +1 -0
  39. package/dist/index.js +14 -0
  40. package/dist/index.js.map +1 -0
  41. package/dist/orchestration.d.ts +70 -0
  42. package/dist/orchestration.d.ts.map +1 -0
  43. package/dist/orchestration.js +255 -0
  44. package/dist/orchestration.js.map +1 -0
  45. package/dist/process-collector.d.ts +34 -0
  46. package/dist/process-collector.d.ts.map +1 -0
  47. package/dist/process-collector.js +148 -0
  48. package/dist/process-collector.js.map +1 -0
  49. package/dist/process-manager.d.ts +133 -0
  50. package/dist/process-manager.d.ts.map +1 -0
  51. package/dist/process-manager.js +333 -0
  52. package/dist/process-manager.js.map +1 -0
  53. package/dist/readiness.d.ts +120 -0
  54. package/dist/readiness.d.ts.map +1 -0
  55. package/dist/readiness.js +179 -0
  56. package/dist/readiness.js.map +1 -0
  57. package/dist/sandbox.d.ts +139 -0
  58. package/dist/sandbox.d.ts.map +1 -0
  59. package/dist/sandbox.js +271 -0
  60. package/dist/sandbox.js.map +1 -0
  61. package/package.json +29 -0
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Capability registry (plan §16): assembles per-execution capabilities
3
+ * from the collectors actually present, so `distributedTrace: unavailable`
4
+ * is a first-class answer the AI planner can read rather than a silent
5
+ * gap. Does not know how to *produce* a `CollectorCapabilities` value —
6
+ * that's each collector's own `capabilities()` (Agents 2-4's concern) —
7
+ * only how to assemble and query what's been registered.
8
+ */
9
+ import type { CapabilityStatus, CollectorCapabilities, EnvironmentTier, ExecutionCapabilities, FidelityLevel } from "@descryy/runtime-contracts";
10
+ export interface RegisteredCollectorCapabilities {
11
+ readonly collectorId: string;
12
+ readonly capabilities: CollectorCapabilities;
13
+ }
14
+ export declare function buildExecutionCapabilities(environmentTier: EnvironmentTier, fidelityLevel: FidelityLevel, collectors: readonly RegisteredCollectorCapabilities[]): ExecutionCapabilities;
15
+ export type CollectorCapabilityKey = keyof CollectorCapabilities;
16
+ /**
17
+ * The best (most available) status for one capability across every
18
+ * registered collector — "is this observable *anywhere* in this
19
+ * execution," not per-collector. An execution with two network collectors
20
+ * where one degraded and one is fully available should read as available,
21
+ * not degraded.
22
+ */
23
+ export declare function bestCapabilityStatus(capabilities: ExecutionCapabilities, key: CollectorCapabilityKey): CapabilityStatus;
24
+ //# sourceMappingURL=capability-registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capability-registry.d.ts","sourceRoot":"","sources":["../src/capability-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAEV,gBAAgB,EAChB,qBAAqB,EACrB,eAAe,EACf,qBAAqB,EACrB,aAAa,EACd,MAAM,4BAA4B,CAAC;AAEpC,MAAM,WAAW,+BAA+B;IAC9C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,YAAY,EAAE,qBAAqB,CAAC;CAC9C;AAED,wBAAgB,0BAA0B,CACxC,eAAe,EAAE,eAAe,EAChC,aAAa,EAAE,aAAa,EAC5B,UAAU,EAAE,SAAS,+BAA+B,EAAE,GACrD,qBAAqB,CAEvB;AAED,MAAM,MAAM,sBAAsB,GAAG,MAAM,qBAAqB,CAAC;AAajE;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,YAAY,EAAE,qBAAqB,EACnC,GAAG,EAAE,sBAAsB,GAC1B,gBAAgB,CAalB"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Capability registry (plan §16): assembles per-execution capabilities
3
+ * from the collectors actually present, so `distributedTrace: unavailable`
4
+ * is a first-class answer the AI planner can read rather than a silent
5
+ * gap. Does not know how to *produce* a `CollectorCapabilities` value —
6
+ * that's each collector's own `capabilities()` (Agents 2-4's concern) —
7
+ * only how to assemble and query what's been registered.
8
+ */
9
+ export function buildExecutionCapabilities(environmentTier, fidelityLevel, collectors) {
10
+ return { environmentTier, fidelityLevel, collectors };
11
+ }
12
+ const AVAILABILITY_RANK = {
13
+ unavailable: 0,
14
+ degraded: 1,
15
+ available: 2,
16
+ };
17
+ const NO_COLLECTOR_STATUS = {
18
+ availability: "unavailable",
19
+ reason: "no collector registered for this execution",
20
+ };
21
+ /**
22
+ * The best (most available) status for one capability across every
23
+ * registered collector — "is this observable *anywhere* in this
24
+ * execution," not per-collector. An execution with two network collectors
25
+ * where one degraded and one is fully available should read as available,
26
+ * not degraded.
27
+ */
28
+ export function bestCapabilityStatus(capabilities, key) {
29
+ // Seeded from the first collector actually seen, not from
30
+ // NO_COLLECTOR_STATUS -- seeding from the placeholder would make a
31
+ // single collector's genuine "unavailable" tie the placeholder's rank
32
+ // and lose its specific reason to the generic one.
33
+ let best = null;
34
+ for (const collector of capabilities.collectors) {
35
+ const status = collector.capabilities[key];
36
+ if (best === null || AVAILABILITY_RANK[status.availability] > AVAILABILITY_RANK[best.availability]) {
37
+ best = status;
38
+ }
39
+ }
40
+ return best ?? NO_COLLECTOR_STATUS;
41
+ }
42
+ //# sourceMappingURL=capability-registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capability-registry.js","sourceRoot":"","sources":["../src/capability-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAgBH,MAAM,UAAU,0BAA0B,CACxC,eAAgC,EAChC,aAA4B,EAC5B,UAAsD;IAEtD,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,UAAU,EAAE,CAAC;AACxD,CAAC;AAID,MAAM,iBAAiB,GAAqD;IAC1E,WAAW,EAAE,CAAC;IACd,QAAQ,EAAE,CAAC;IACX,SAAS,EAAE,CAAC;CACb,CAAC;AAEF,MAAM,mBAAmB,GAAqB;IAC5C,YAAY,EAAE,aAAa;IAC3B,MAAM,EAAE,4CAA4C;CACrD,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB,CAClC,YAAmC,EACnC,GAA2B;IAE3B,0DAA0D;IAC1D,mEAAmE;IACnE,sEAAsE;IACtE,mDAAmD;IACnD,IAAI,IAAI,GAA4B,IAAI,CAAC;IACzC,KAAK,MAAM,SAAS,IAAI,YAAY,CAAC,UAAU,EAAE,CAAC;QAChD,MAAM,MAAM,GAAG,SAAS,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC;QAC3C,IAAI,IAAI,KAAK,IAAI,IAAI,iBAAiB,CAAC,MAAM,CAAC,YAAY,CAAC,GAAG,iBAAiB,CAAC,IAAI,CAAC,YAAY,CAAC,EAAE,CAAC;YACnG,IAAI,GAAG,MAAM,CAAC;QAChB,CAAC;IACH,CAAC;IACD,OAAO,IAAI,IAAI,mBAAmB,CAAC;AACrC,CAAC"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * `Evidence.collectorVersion`'s source for `ProcessCollector`: this
3
+ * package's own `package.json` "version", read from the artifact itself
4
+ * rather than hand-duplicated into a second string that can drift from
5
+ * what actually shipped. Same idea as `nodeVersion` elsewhere in this repo
6
+ * (`process.version`, always known, never probed) applied to a package
7
+ * instead of the Node runtime.
8
+ */
9
+ export declare const COLLECTOR_VERSION: string;
10
+ //# sourceMappingURL=collector-version.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collector-version.d.ts","sourceRoot":"","sources":["../src/collector-version.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,eAAO,MAAM,iBAAiB,EAAE,MAA6E,CAAC"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * `Evidence.collectorVersion`'s source for `ProcessCollector`: this
3
+ * package's own `package.json` "version", read from the artifact itself
4
+ * rather than hand-duplicated into a second string that can drift from
5
+ * what actually shipped. Same idea as `nodeVersion` elsewhere in this repo
6
+ * (`process.version`, always known, never probed) applied to a package
7
+ * instead of the Node runtime.
8
+ */
9
+ import { createRequire } from "node:module";
10
+ const require = createRequire(import.meta.url);
11
+ export const COLLECTOR_VERSION = require("../package.json").version;
12
+ //# sourceMappingURL=collector-version.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collector-version.js","sourceRoot":"","sources":["../src/collector-version.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAE/C,MAAM,CAAC,MAAM,iBAAiB,GAAY,OAAO,CAAC,iBAAiB,CAAkC,CAAC,OAAO,CAAC"}
@@ -0,0 +1,188 @@
1
+ /**
2
+ * §21's execution boundary, container backend -- the macOS/Windows half of
3
+ * the sandbox-isolation lane. `sandbox.ts` closed this gap for Linux via
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.
14
+ *
15
+ * **Mechanism: route the spawned process through a real container instead.**
16
+ * This runtime's actual targets -- JVM/Spring, Node, Python backends -- are
17
+ * already containerizable, so this reuses the *Linux* sandbox mechanism
18
+ * bwrap already proved, by running the target process inside a `docker run`
19
+ * invocation. The container's own rootfs IS the "outside allowedRoots is
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.
24
+ *
25
+ * **What was actually verified, and what was not -- stated precisely,
26
+ * matching `sandbox.ts`'s own "researched and disclosed rather than
27
+ * guessed" discipline:**
28
+ *
29
+ * REAL-TESTED, on this Linux development machine, via a real installed
30
+ * Docker daemon (server 29.6.1) and real `docker run` invocations -- not
31
+ * mocked, not asserted from documentation: a file outside a container's
32
+ * bind-mounts is genuinely unreachable (`ENOENT` from inside the running
33
+ * container); a network call under `--network none` is genuinely refused
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.
38
+ *
39
+ * **IMPLEMENTATION-NOT-DONE, not a "gap" and not a coverage caveat: the
40
+ * macOS-Docker-Desktop-VM-specific and Windows-WSL2-specific integration
41
+ * paths are UNVERIFIED on real macOS/Windows hardware.** No such hardware
42
+ * is available in this environment. Concretely unverified: detecting
43
+ * whether Docker Desktop is actually running before attempting a spawn;
44
+ * path translation across the Docker Desktop VM boundary on macOS; WSL2
45
+ * path translation on Windows (a Windows host path and the path Linux
46
+ * containers see are not the same string). This module's bind-mount
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.
56
+ *
57
+ * **Explicit scope boundary, same treatment attach-mode already gets in
58
+ * this codebase:** a target that cannot run containerized at all --deep
59
+ * native OS integration, GUI-dependent processes, OS-specific native
60
+ * dependencies that don't exist inside a Linux container image-- is out of
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.
64
+ *
65
+ * **Real escape testing on the container boundary is still required and
66
+ * was done** -- "the container starts successfully" is not treated as
67
+ * sufficient evidence here, the same bar `sandbox.ts`'s bwrap path was held
68
+ * to. A mutation check on the first version of that testing (temporarily
69
+ * removing `--network none`, rerunning) surfaced a genuine behavioral
70
+ * difference from bwrap worth stating precisely rather than glossing over:
71
+ * **Docker gives every container its own network namespace by default,
72
+ * with or without `--network none`** -- unlike bwrap, which shares the
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.
82
+ *
83
+ * **What this module does NOT attempt to solve, disclosed rather than
84
+ * silently dropped:** composition with `ResourceLimits` (`prlimit`) --
85
+ * `sandbox.ts` composes bwrap with `applyResourceLimits` by wrapping
86
+ * outside it; this module does not attempt the equivalent composition with
87
+ * Docker's own `--memory`/`--cpus`/`--pids-limit` flags in this pass. A
88
+ * `resourceLimits`-declared execution routed through the container backend
89
+ * runs without OS-enforced resource limits inside the container. Selective
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.
97
+ */
98
+ 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
+ */
107
+ 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
+ export declare function containerFilesystemIsolationCapability(env?: NodeJS.ProcessEnv): CapabilityStatus;
116
+ 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
+ */
124
+ export declare function unsupportedContainerNetworkPolicyReason(policy: NetworkPolicy): string | null;
125
+ export declare function resolveContainerImage(interpreterCommand: string): string | null;
126
+ export interface ContainerSandboxOptions {
127
+ /** Used only to resolve a default base image via `resolveContainerImage` when `containerImage` is not given. */
128
+ readonly interpreterCommand: string;
129
+ /** The command to exec *inside* the container -- resolved against the image's own PATH, not the host's (see `processEnv`'s own comment for why `PATH` is never forwarded). */
130
+ readonly command: string;
131
+ readonly args: readonly string[];
132
+ /** Bind-mounted into the container at the identical path (`-v cwd:cwd`), always -- mirrors `sandbox.ts`'s "cwd always bound" default, and is the one directory the target process is guaranteed to need. */
133
+ readonly cwd: string;
134
+ readonly filesystemPolicy?: FilesystemPolicy;
135
+ readonly networkPolicy?: NetworkPolicy;
136
+ /**
137
+ * Env vars the CONTAINERIZED PROCESS itself needs (e.g. `PORT`, a
138
+ * service's declared `env`) -- forwarded into the container via `-e
139
+ * KEY=VALUE`. **`PATH` is deliberately never forwarded**: the container
140
+ * must resolve `command` against its own image's filesystem layout
141
+ * (`/usr/local/bin/node` in `node:22-slim`, not wherever the host's
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).
148
+ */
149
+ readonly processEnv?: Readonly<Record<string, string | undefined>>;
150
+ /** Env used only to run the `docker` CLI itself (PATH lookup for `docker`, `DOCKER_HOST`, etc.) -- plays the same role `SandboxOptions.env` plays for bwrap's own capability check. Defaults to `process.env`. */
151
+ readonly env?: NodeJS.ProcessEnv;
152
+ /** Explicit override -- bypasses `resolveContainerImage`'s name-based guess entirely. */
153
+ readonly containerImage?: string;
154
+ }
155
+ /**
156
+ * Wraps `command`/`args` with `docker run` so a real container boundary
157
+ * enforces `filesystemPolicy`/`networkPolicy`. Neither declared returns
158
+ * `command`/`args` unchanged and never invokes docker at all -- the same
159
+ * non-regression contract `sandbox.ts`'s `applySandbox` established for
160
+ * bwrap, applied here too: a caller who never opts into a policy is
161
+ * completely unaffected by this module's existence.
162
+ *
163
+ * **Throws rather than silently spawning unconstrained** when a policy is
164
+ * requested and cannot actually be backed -- no working Docker, or a
165
+ * `networkPolicy` shape this mechanism doesn't implement -- matching
166
+ * `applySandbox`'s own refuse-rather-than-guess precedent exactly.
167
+ *
168
+ * **Returns a real, unique `containerName` whenever it wraps.** This was
169
+ * added after a genuine finding from this module's own real escape tests,
170
+ * not assumed up front: `process-manager.ts`'s existing group-kill
171
+ * (`process.kill(-pid, signal)`) targets the *local* `docker` CLI process's
172
+ * group -- exactly right for bwrap, which execs the sandboxed process
173
+ * directly and never creates a second process group. Docker is different:
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.
182
+ */
183
+ export declare function applyContainerSandbox(options: ContainerSandboxOptions): {
184
+ readonly command: string;
185
+ readonly args: readonly string[];
186
+ readonly containerName?: string;
187
+ };
188
+ //# sourceMappingURL=container-sandbox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"container-sandbox.d.ts","sourceRoot":"","sources":["../src/container-sandbox.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgGG;AAKH,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAEpG;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,gBAAgB,CAUjG;AAED;;;;;;GAMG;AACH,wBAAgB,sCAAsC,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,gBAAgB,CAE7G;AAED,wBAAgB,mCAAmC,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,gBAAgB,CAE1G;AAED;;;;;;GAMG;AACH,wBAAgB,uCAAuC,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,GAAG,IAAI,CAO5F;AAkBD,wBAAgB,qBAAqB,CAAC,kBAAkB,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAE/E;AAED,MAAM,WAAW,uBAAuB;IACtC,gHAAgH;IAChH,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,8KAA8K;IAC9K,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,4MAA4M;IAC5M,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;IAC7C,QAAQ,CAAC,aAAa,CAAC,EAAE,aAAa,CAAC;IACvC;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;IACnE,kNAAkN;IAClN,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACjC,yFAAyF;IACzF,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;CAClC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;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"}
@@ -0,0 +1,233 @@
1
+ /**
2
+ * §21's execution boundary, container backend -- the macOS/Windows half of
3
+ * the sandbox-isolation lane. `sandbox.ts` closed this gap for Linux via
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.
14
+ *
15
+ * **Mechanism: route the spawned process through a real container instead.**
16
+ * This runtime's actual targets -- JVM/Spring, Node, Python backends -- are
17
+ * already containerizable, so this reuses the *Linux* sandbox mechanism
18
+ * bwrap already proved, by running the target process inside a `docker run`
19
+ * invocation. The container's own rootfs IS the "outside allowedRoots is
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.
24
+ *
25
+ * **What was actually verified, and what was not -- stated precisely,
26
+ * matching `sandbox.ts`'s own "researched and disclosed rather than
27
+ * guessed" discipline:**
28
+ *
29
+ * REAL-TESTED, on this Linux development machine, via a real installed
30
+ * Docker daemon (server 29.6.1) and real `docker run` invocations -- not
31
+ * mocked, not asserted from documentation: a file outside a container's
32
+ * bind-mounts is genuinely unreachable (`ENOENT` from inside the running
33
+ * container); a network call under `--network none` is genuinely refused
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.
38
+ *
39
+ * **IMPLEMENTATION-NOT-DONE, not a "gap" and not a coverage caveat: the
40
+ * macOS-Docker-Desktop-VM-specific and Windows-WSL2-specific integration
41
+ * paths are UNVERIFIED on real macOS/Windows hardware.** No such hardware
42
+ * is available in this environment. Concretely unverified: detecting
43
+ * whether Docker Desktop is actually running before attempting a spawn;
44
+ * path translation across the Docker Desktop VM boundary on macOS; WSL2
45
+ * path translation on Windows (a Windows host path and the path Linux
46
+ * containers see are not the same string). This module's bind-mount
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.
56
+ *
57
+ * **Explicit scope boundary, same treatment attach-mode already gets in
58
+ * this codebase:** a target that cannot run containerized at all --deep
59
+ * native OS integration, GUI-dependent processes, OS-specific native
60
+ * dependencies that don't exist inside a Linux container image-- is out of
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.
64
+ *
65
+ * **Real escape testing on the container boundary is still required and
66
+ * was done** -- "the container starts successfully" is not treated as
67
+ * sufficient evidence here, the same bar `sandbox.ts`'s bwrap path was held
68
+ * to. A mutation check on the first version of that testing (temporarily
69
+ * removing `--network none`, rerunning) surfaced a genuine behavioral
70
+ * difference from bwrap worth stating precisely rather than glossing over:
71
+ * **Docker gives every container its own network namespace by default,
72
+ * with or without `--network none`** -- unlike bwrap, which shares the
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.
82
+ *
83
+ * **What this module does NOT attempt to solve, disclosed rather than
84
+ * silently dropped:** composition with `ResourceLimits` (`prlimit`) --
85
+ * `sandbox.ts` composes bwrap with `applyResourceLimits` by wrapping
86
+ * outside it; this module does not attempt the equivalent composition with
87
+ * Docker's own `--memory`/`--cpus`/`--pids-limit` flags in this pass. A
88
+ * `resourceLimits`-declared execution routed through the container backend
89
+ * runs without OS-enforced resource limits inside the container. Selective
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.
97
+ */
98
+ import { execFileSync } from "node:child_process";
99
+ import { randomUUID } from "node:crypto";
100
+ 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
+ */
109
+ export function containerRuntimeCapability(env = process.env) {
110
+ try {
111
+ execFileSync("docker", ["version", "--format", "{{.Server.Version}}"], { env, stdio: "ignore" });
112
+ }
113
+ catch {
114
+ return {
115
+ availability: "unavailable",
116
+ reason: "docker is not on PATH, or no Docker daemon is reachable -- container-based isolation cannot be enforced without a real, running Docker",
117
+ };
118
+ }
119
+ return { availability: "available", reason: null };
120
+ }
121
+ /**
122
+ * Mirrors `sandbox.ts`'s `filesystemIsolationCapability`/
123
+ * `networkIsolationCapability` split: one real mechanism underneath
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
+ */
128
+ export function containerFilesystemIsolationCapability(env = process.env) {
129
+ return containerRuntimeCapability(env);
130
+ }
131
+ export function containerNetworkIsolationCapability(env = process.env) {
132
+ return containerRuntimeCapability(env);
133
+ }
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
+ */
141
+ export function unsupportedContainerNetworkPolicyReason(policy) {
142
+ if (policy.mode === "allow" && policy.hosts.length === 0)
143
+ return null;
144
+ return ('only full network denial ({ mode: "allow", hosts: [] }) is enforced -- selective allow- or deny-listing of specific ' +
145
+ "hosts would need a custom docker network with DNS interception and IP filtering inside it, not built in this " +
146
+ "iteration (the same posture the bwrap backend already declares for the same shapes)");
147
+ }
148
+ /**
149
+ * Minimal, official, `-slim`/`-jre` base images for this runtime's actual
150
+ * fixture targets (JVM/Spring, Node, Python backends -- see
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
+ */
157
+ const KNOWN_RUNTIME_IMAGES = {
158
+ node: "node:22-slim",
159
+ python: "python:3.12-slim",
160
+ python3: "python:3.12-slim",
161
+ java: "eclipse-temurin:21-jre",
162
+ };
163
+ export function resolveContainerImage(interpreterCommand) {
164
+ return KNOWN_RUNTIME_IMAGES[basename(interpreterCommand)] ?? null;
165
+ }
166
+ /**
167
+ * Wraps `command`/`args` with `docker run` so a real container boundary
168
+ * enforces `filesystemPolicy`/`networkPolicy`. Neither declared returns
169
+ * `command`/`args` unchanged and never invokes docker at all -- the same
170
+ * non-regression contract `sandbox.ts`'s `applySandbox` established for
171
+ * bwrap, applied here too: a caller who never opts into a policy is
172
+ * completely unaffected by this module's existence.
173
+ *
174
+ * **Throws rather than silently spawning unconstrained** when a policy is
175
+ * requested and cannot actually be backed -- no working Docker, or a
176
+ * `networkPolicy` shape this mechanism doesn't implement -- matching
177
+ * `applySandbox`'s own refuse-rather-than-guess precedent exactly.
178
+ *
179
+ * **Returns a real, unique `containerName` whenever it wraps.** This was
180
+ * added after a genuine finding from this module's own real escape tests,
181
+ * not assumed up front: `process-manager.ts`'s existing group-kill
182
+ * (`process.kill(-pid, signal)`) targets the *local* `docker` CLI process's
183
+ * group -- exactly right for bwrap, which execs the sandboxed process
184
+ * directly and never creates a second process group. Docker is different:
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.
193
+ */
194
+ export function applyContainerSandbox(options) {
195
+ const { filesystemPolicy, networkPolicy } = options;
196
+ if (filesystemPolicy === undefined && networkPolicy === undefined) {
197
+ return { command: options.command, args: options.args };
198
+ }
199
+ const env = options.env ?? process.env;
200
+ if (networkPolicy !== undefined) {
201
+ const shapeReason = unsupportedContainerNetworkPolicyReason(networkPolicy);
202
+ if (shapeReason !== null) {
203
+ throw new Error(`networkPolicy was configured but cannot be enforced: ${shapeReason}`);
204
+ }
205
+ }
206
+ const capability = containerRuntimeCapability(env);
207
+ if (capability.availability !== "available") {
208
+ throw new Error(`container sandbox was requested but cannot be enforced: ${capability.reason}`);
209
+ }
210
+ const image = options.containerImage ?? resolveContainerImage(options.interpreterCommand);
211
+ if (image === null) {
212
+ throw new Error(`container sandbox was requested but no base image is known for interpreter "${options.interpreterCommand}" -- pass containerImage explicitly`);
213
+ }
214
+ const containerName = `descry-sandbox-${randomUUID()}`;
215
+ const dockerArgs = ["run", "--rm", "--name", containerName];
216
+ if (networkPolicy !== undefined) {
217
+ dockerArgs.push("--network", "none");
218
+ }
219
+ dockerArgs.push("-v", `${options.cwd}:${options.cwd}`, "-w", options.cwd);
220
+ if (filesystemPolicy !== undefined) {
221
+ for (const root of filesystemPolicy.allowedRoots) {
222
+ dockerArgs.push("-v", `${root}:${root}`);
223
+ }
224
+ }
225
+ for (const [key, value] of Object.entries(options.processEnv ?? {})) {
226
+ if (key === "PATH" || value === undefined)
227
+ continue;
228
+ dockerArgs.push("-e", `${key}=${value}`);
229
+ }
230
+ dockerArgs.push(image, options.command, ...options.args);
231
+ return { command: "docker", args: dockerArgs, containerName };
232
+ }
233
+ //# sourceMappingURL=container-sandbox.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"container-sandbox.js","sourceRoot":"","sources":["../src/container-sandbox.ts"],"names":[],"mappings":"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"}