@descryy/runtime-controller 0.2.0 → 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 +54 -87
- package/dist/controller.d.ts.map +1 -1
- package/dist/controller.js +77 -104
- 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
package/dist/readiness.js
CHANGED
|
@@ -1,65 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Readiness (
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* — two names for the same shape, a caller-provided async predicate,
|
|
7
|
-
* consolidated into one mechanism rather than two near-identical ones).
|
|
2
|
+
* Readiness (§6): "readiness must not equal 'process exists.'" Mechanisms: HTTP
|
|
3
|
+
* check, TCP/port check, log pattern, subprocess command, or a caller hook
|
|
4
|
+
* (consolidates the plan's "framework readiness hook" and "user-defined readiness"
|
|
5
|
+
* into one predicate shape).
|
|
8
6
|
*
|
|
9
|
-
* **`command` is not `custom-hook` (RT-193).**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* while `command` runs an actual subprocess and reads its actual exit
|
|
14
|
-
* code — the same "declared, not inferred" shape `ServiceConfiguration`'s
|
|
15
|
-
* own `command` field already uses, one layer over. A caller with a real
|
|
16
|
-
* CLI health check (`pg_isready`, a framework's own readiness script) had
|
|
17
|
-
* no way to express that without either wrapping it in a hand-written
|
|
18
|
-
* `custom-hook` predicate that shells out itself, or reimplementing the
|
|
19
|
-
* subprocess plumbing per caller — `command` is that plumbing, built once.
|
|
7
|
+
* **`command` is not `custom-hook` (RT-193).** `custom-hook` runs a caller's
|
|
8
|
+
* in-process predicate; `command` runs a real subprocess and reads its exit code --
|
|
9
|
+
* built once so a caller with a real CLI health check (`pg_isready`) doesn't have to
|
|
10
|
+
* shell out from inside a hand-written hook.
|
|
20
11
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
12
|
+
* **What `ready: true` does not claim (RT-024).** All mechanisms answer "is
|
|
13
|
+
* something listening/responding," none confirm it's the process *this* spawn
|
|
14
|
+
* produced -- a stale listener from an already-dead run satisfies `http`/`tcp-port`
|
|
15
|
+
* identically. `log-pattern` only carries the stronger this-run guarantee if `read`
|
|
16
|
+
* is wired to this execution's own `readOutput()` (caller discipline, not enforced
|
|
17
|
+
* by the type). An ephemeral port closes this gap by construction; a fixed,
|
|
18
|
+
* guessable port doesn't. Found via a real 30s collector timeout that looked like a
|
|
19
|
+
* Playwright bug and was actually a wait on an orphaned process from a dead session
|
|
20
|
+
* (RT-024).
|
|
23
21
|
*
|
|
24
|
-
* **
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* `
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* this-run-specific guarantee when its `read` is wired to *this* execution's
|
|
31
|
-
* own `ManagedProcess.readOutput()` — the type does not enforce that wiring,
|
|
32
|
-
* it is a caller discipline. `custom-hook` inherits whatever the caller's
|
|
33
|
-
* predicate actually checks.
|
|
34
|
-
*
|
|
35
|
-
* Callers that bind a fixed, guessable host:port are exposed to this gap;
|
|
36
|
-
* an ephemeral port closes it by construction (nothing else can already be
|
|
37
|
-
* bound there) rather than by detection, and is the default recommendation
|
|
38
|
-
* over a startup-nonce scheme, which only earns its extra machinery when a
|
|
39
|
-
* fixed port is genuinely unavoidable (a contract with an external tool).
|
|
40
|
-
* See `descry-runtime`'s `DECISIONS.md` RT-024 for how this was found —
|
|
41
|
-
* a real 30-second collector timeout that looked like a Playwright/DOM bug
|
|
42
|
-
* and was actually a correct wait against an orphaned process from a
|
|
43
|
-
* different, already-dead session.
|
|
44
|
-
*
|
|
45
|
-
* **A single probe attempt cannot outlive `timeoutMs` (RT-030).** `http`
|
|
46
|
-
* and `tcp-port` each bind their I/O to the *remaining* time until
|
|
47
|
-
* `deadline`, not a fresh per-attempt budget — a target that accepts a
|
|
48
|
-
* connection and never responds no longer lets one probe attempt run past
|
|
49
|
-
* the caller's own deadline. Measured, not theoretical: an unbound `fetch`
|
|
50
|
-
* here let `awaitReadiness` run past its own `timeoutMs` (>8s observed
|
|
51
|
-
* against a 3s budget). `command` honors the same discipline, the same
|
|
52
|
-
* way: `execFile`'s own `timeout` option is set to the remaining time until
|
|
53
|
-
* `deadline`, so a command that hangs instead of exiting is killed rather
|
|
54
|
-
* than left to run past the caller's own budget — it owns a real OS
|
|
55
|
-
* resource (a child process) exactly like the http/tcp-port sockets do, so
|
|
56
|
-
* it gets the same treatment. `custom-hook` is not wrapped the same way — a
|
|
57
|
-
* caller-supplied predicate owns its own timeout behavior, same as every
|
|
58
|
-
* other `Collector`-shaped callback contract in this repo (`collector.ts`'s
|
|
59
|
-
* own docs put "must resolve, not hang" on the implementer); imposing a
|
|
60
|
-
* surprise external cancellation on arbitrary caller code would be a
|
|
61
|
-
* different, larger decision than fixing the mechanisms that own a real OS
|
|
62
|
-
* resource this module can actually clean up.
|
|
22
|
+
* **A single probe attempt cannot outlive `timeoutMs` (RT-030).** `http`/`tcp-port`
|
|
23
|
+
* bind I/O to the remaining time until `deadline`, not a fresh per-attempt budget --
|
|
24
|
+
* measured: an unbound `fetch` here let `awaitReadiness` run 8s+ past a 3s budget.
|
|
25
|
+
* `command` gets the same treatment via `execFile`'s `timeout`. `custom-hook` is not
|
|
26
|
+
* wrapped this way -- a caller predicate owns its own timeout, same as every other
|
|
27
|
+
* `Collector` callback contract here.
|
|
63
28
|
*/
|
|
64
29
|
import { createConnection } from "node:net";
|
|
65
30
|
import { execFile } from "node:child_process";
|
|
@@ -72,35 +37,34 @@ export async function awaitReadiness(checks, options) {
|
|
|
72
37
|
if (checks.length === 0) {
|
|
73
38
|
return { ready: false, mechanism: null, elapsedMs: 0, reason: "no readiness checks configured" };
|
|
74
39
|
}
|
|
40
|
+
// C-F4: last real diagnostic any probe captured before giving up (chiefly
|
|
41
|
+
// `command`'s stderr/exit code -- http/tcp-port have nothing richer than
|
|
42
|
+
// "connection refused"). Kept as "last seen", most likely to explain the timeout.
|
|
43
|
+
let lastDetail = null;
|
|
75
44
|
do {
|
|
76
45
|
for (const check of checks) {
|
|
77
|
-
|
|
46
|
+
const result = await probe(check, deadline);
|
|
47
|
+
if (result.ok) {
|
|
78
48
|
return { ready: true, mechanism: check.kind, elapsedMs: Date.now() - startedAt, reason: null };
|
|
79
49
|
}
|
|
50
|
+
if (result.detail !== null)
|
|
51
|
+
lastDetail = `${check.kind}: ${result.detail}`;
|
|
80
52
|
}
|
|
81
53
|
await sleep(pollIntervalMs);
|
|
82
54
|
} while (Date.now() < deadline);
|
|
83
|
-
// Names what was actually probed, not just that nothing answered
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
// own bind, the child never comes up and the *only* symptom is this
|
|
89
|
-
// result. "No readiness mechanism succeeded" sends the reader to look at
|
|
90
|
-
// the application; "nothing answered http://127.0.0.1:41237/health"
|
|
91
|
-
// sends them to look at the port, which is where the problem is. The race
|
|
92
|
-
// itself cannot be closed without binding the real service directly to
|
|
93
|
-
// the probe socket, which the child-process model does not support — so
|
|
94
|
-
// making its one symptom legible is the honest remainder.
|
|
55
|
+
// Names what was actually probed, not just that nothing answered -- the only
|
|
56
|
+
// cheap mitigation for the ephemeral-port race in `allocateEphemeralPort`
|
|
57
|
+
// (bind 0, read, close, spawn): if another process grabs the port first, the
|
|
58
|
+
// child never comes up and this message is the only symptom. Naming the exact
|
|
59
|
+
// address probed sends the reader to the port, not the application.
|
|
95
60
|
return {
|
|
96
61
|
ready: false,
|
|
97
62
|
mechanism: null,
|
|
98
63
|
elapsedMs: Date.now() - startedAt,
|
|
99
|
-
// "timeout" stays in the wording deliberately
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
reason: `no readiness mechanism succeeded within the ${options.timeoutMs}ms timeout — probed ${checks.map(describeCheck).join(", ")}`,
|
|
64
|
+
// "timeout" stays in the wording deliberately -- §41's hung-process test
|
|
65
|
+
// matches on that word.
|
|
66
|
+
reason: `no readiness mechanism succeeded within the ${options.timeoutMs}ms timeout — probed ${checks.map(describeCheck).join(", ")}` +
|
|
67
|
+
(lastDetail !== null ? `; last observed failure — ${lastDetail}` : ""),
|
|
104
68
|
};
|
|
105
69
|
}
|
|
106
70
|
function describeCheck(check) {
|
|
@@ -117,6 +81,12 @@ function describeCheck(check) {
|
|
|
117
81
|
return "custom hook";
|
|
118
82
|
}
|
|
119
83
|
}
|
|
84
|
+
function ok() {
|
|
85
|
+
return { ok: true, detail: null };
|
|
86
|
+
}
|
|
87
|
+
function notReady(detail = null) {
|
|
88
|
+
return { ok: false, detail };
|
|
89
|
+
}
|
|
120
90
|
async function probe(check, deadline) {
|
|
121
91
|
switch (check.kind) {
|
|
122
92
|
case "http":
|
|
@@ -124,52 +94,60 @@ async function probe(check, deadline) {
|
|
|
124
94
|
case "tcp-port":
|
|
125
95
|
return probeTcpPort(check, deadline);
|
|
126
96
|
case "log-pattern":
|
|
127
|
-
return check.pattern.test(check.read());
|
|
97
|
+
return check.pattern.test(check.read()) ? ok() : notReady();
|
|
128
98
|
case "command":
|
|
129
99
|
return probeCommand(check, deadline);
|
|
130
100
|
case "custom-hook":
|
|
131
|
-
return check.check();
|
|
101
|
+
return (await check.check()) ? ok() : notReady();
|
|
132
102
|
}
|
|
133
103
|
}
|
|
134
104
|
async function probeHttp(check, deadline) {
|
|
135
105
|
try {
|
|
136
106
|
const response = await fetch(check.url, { signal: AbortSignal.timeout(Math.max(0, deadline - Date.now())) });
|
|
137
|
-
|
|
107
|
+
if (response.status === (check.expectedStatus ?? 200))
|
|
108
|
+
return ok();
|
|
109
|
+
return notReady(`responded with status ${String(response.status)}, expected ${String(check.expectedStatus ?? 200)}`);
|
|
138
110
|
}
|
|
139
|
-
catch {
|
|
140
|
-
return
|
|
111
|
+
catch (error) {
|
|
112
|
+
return notReady(error instanceof Error ? error.message : String(error));
|
|
141
113
|
}
|
|
142
114
|
}
|
|
143
115
|
function probeTcpPort(check, deadline) {
|
|
144
116
|
return new Promise((resolve) => {
|
|
145
117
|
const socket = createConnection({ host: check.host, port: check.port });
|
|
146
118
|
socket.setTimeout(Math.max(0, deadline - Date.now()));
|
|
147
|
-
const finish = (
|
|
119
|
+
const finish = (result) => {
|
|
148
120
|
socket.destroy();
|
|
149
|
-
resolve(
|
|
121
|
+
resolve(result);
|
|
150
122
|
};
|
|
151
|
-
socket.once("connect", () => finish(
|
|
152
|
-
socket.once("error", () => finish(
|
|
153
|
-
socket.once("timeout", () => finish(
|
|
123
|
+
socket.once("connect", () => finish(ok()));
|
|
124
|
+
socket.once("error", (error) => finish(notReady(error.message)));
|
|
125
|
+
socket.once("timeout", () => finish(notReady("connection attempt timed out")));
|
|
154
126
|
});
|
|
155
127
|
}
|
|
156
128
|
/**
|
|
157
|
-
* Ready when `check.command` exits 0 within the remaining time until
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
129
|
+
* Ready when `check.command` exits 0 within the remaining time until `deadline`
|
|
130
|
+
* (RT-030). Non-zero exit, spawn failure, and timeout kill are all "not ready yet" --
|
|
131
|
+
* same as every other mechanism's `false`.
|
|
132
|
+
*
|
|
133
|
+
* C-F4: stdout/stderr are the actual health-check output, the richest diagnostic
|
|
134
|
+
* available here. Preferring stderr, then stdout, then the bare error message
|
|
135
|
+
* mirrors `runAttacher`'s ordering in `attach-to-running-jvm-process.ts`.
|
|
163
136
|
*/
|
|
164
137
|
function probeCommand(check, deadline) {
|
|
165
138
|
return new Promise((resolve) => {
|
|
166
139
|
const remainingMs = Math.max(0, deadline - Date.now());
|
|
167
140
|
if (remainingMs === 0) {
|
|
168
|
-
resolve(
|
|
141
|
+
resolve(notReady("no time remaining before the readiness deadline"));
|
|
169
142
|
return;
|
|
170
143
|
}
|
|
171
|
-
execFile(check.command, check.args ?? [], { cwd: check.cwd, timeout: remainingMs }, (error) => {
|
|
172
|
-
|
|
144
|
+
execFile(check.command, check.args ?? [], { cwd: check.cwd, timeout: remainingMs }, (error, stdout, stderr) => {
|
|
145
|
+
if (error === null) {
|
|
146
|
+
resolve(ok());
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const detail = stderr.trim() || stdout.trim() || error.message;
|
|
150
|
+
resolve(notReady(detail));
|
|
173
151
|
});
|
|
174
152
|
});
|
|
175
153
|
}
|
package/dist/readiness.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"readiness.js","sourceRoot":"","sources":["../src/readiness.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"readiness.js","sourceRoot":"","sources":["../src/readiness.ts"],"names":[],"mappings":"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"}
|
package/dist/sandbox.d.ts
CHANGED
|
@@ -1,110 +1,70 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* §21's execution boundary, real isolation half
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* reading any file the invoking user could read, or reaching any host the
|
|
8
|
-
* invoking user's network could reach. This module closes that gap for
|
|
9
|
-
* real, on Linux, for processes this runtime SPAWNS.
|
|
2
|
+
* §21's execution boundary, real isolation half. RT-070/RT-193 gave this runtime a
|
|
3
|
+
* *named* boundary (trusted-local, resource limits, privilege reporting) but not an
|
|
4
|
+
* *isolating* one -- nothing stopped a spawned process from reading any file, or
|
|
5
|
+
* reaching any host, the invoking user could. This module closes that gap for real,
|
|
6
|
+
* on Linux, for processes this runtime SPAWNS.
|
|
10
7
|
*
|
|
11
|
-
* **
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* generalizable"). Spawn-mode gets real isolation via this module.
|
|
19
|
-
* Attach-mode stays permanently and explicitly unsandboxed --
|
|
20
|
-
* `SANDBOX_BOUNDARY_DISCLOSURE` in `execution-safety.ts` says so in-product,
|
|
21
|
-
* and this module does not (and structurally cannot) change that.
|
|
8
|
+
* **Architectural constraint this module cannot solve around:** isolation can only
|
|
9
|
+
* wrap a process Descry itself launches. Attach-mode reaches a process already
|
|
10
|
+
* running, unconfined, before Descry touched it -- there is no honest way to
|
|
11
|
+
* retroactively contain it (ptrace-based interception was investigated and rejected
|
|
12
|
+
* elsewhere as not honestly generalizable). Attach-mode stays permanently and
|
|
13
|
+
* explicitly unsandboxed -- `SANDBOX_BOUNDARY_DISCLOSURE` in `execution-safety.ts`
|
|
14
|
+
* discloses this in-product; this module does not and structurally cannot change it.
|
|
22
15
|
*
|
|
23
|
-
* **Mechanism: bubblewrap (bwrap), not raw `unshare`/`clone`.** bwrap is
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* `process-manager.ts` reaches it and everything it started, unchanged.
|
|
36
|
-
* The one dependency this buys: `bwrap` must be on `PATH`. Reported as a
|
|
37
|
-
* capability, exactly like `prlimit` -- refused, never silently
|
|
38
|
-
* unconstrained, when requested and absent.
|
|
16
|
+
* **Mechanism: bubblewrap (bwrap), not raw `unshare`/`clone`.** bwrap is the
|
|
17
|
+
* unprivileged sandboxing helper behind Flatpak -- small, auditable, actively
|
|
18
|
+
* maintained, not a bespoke native module this repo builds per-platform. Runs
|
|
19
|
+
* setuid-free on any kernel with unprivileged user namespaces enabled (checked
|
|
20
|
+
* below, never assumed) and gives one process a new mount namespace (real
|
|
21
|
+
* filesystem visibility control) and network namespace (real network denial) in one
|
|
22
|
+
* exec. Composes with `applyResourceLimits`'s `prlimit` wrap and with
|
|
23
|
+
* `process-manager.ts`'s process-group kill -- verified directly: bwrap does not
|
|
24
|
+
* create a new process group unless asked, so the existing group-kill reaches it
|
|
25
|
+
* unchanged. The one dependency this buys: `bwrap` must be on PATH, reported as a
|
|
26
|
+
* capability exactly like `prlimit` -- refused, never silently unconstrained, when
|
|
27
|
+
* requested and absent.
|
|
39
28
|
*
|
|
40
|
-
* **
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* Linux VM on macOS, WSL2/Docker Desktop's Windows backend on Windows) --
|
|
52
|
-
* see that module's own doc comment for exactly what is real-tested there
|
|
53
|
-
* versus unverified on real macOS/Windows hardware. `filesystemIsolationCapability`/
|
|
54
|
-
* `networkIsolationCapability` below still report `unavailable` with a
|
|
55
|
-
* reason on non-Linux platforms -- this module never silently claims to
|
|
56
|
-
* cover them; `container-sandbox.ts` is the module a non-Linux caller
|
|
57
|
-
* actually wants.
|
|
29
|
+
* **NOT attempted, deliberately: a native macOS or Windows sandbox.** `sandbox-exec`/
|
|
30
|
+
* Seatbelt is deprecated with no public API replacement; the Endpoint Security
|
|
31
|
+
* Framework needs an Apple entitlement this project doesn't have; Windows Job
|
|
32
|
+
* Objects + AppContainer + WFP would be a second and third bespoke native sandbox.
|
|
33
|
+
* This is an explicit decision not to build native sandboxes for those platforms at
|
|
34
|
+
* all -- `../container-sandbox.ts` closes the gap instead, routing macOS/Windows
|
|
35
|
+
* through a real Docker container that reuses this module's proven Linux mechanism
|
|
36
|
+
* (see that module's doc for what's real-tested there vs unverified on real
|
|
37
|
+
* hardware). `filesystemIsolationCapability`/`networkIsolationCapability` below
|
|
38
|
+
* still report `unavailable` with a reason on non-Linux -- never silently claimed
|
|
39
|
+
* as covered.
|
|
58
40
|
*/
|
|
59
41
|
import type { CapabilityStatus, FilesystemPolicy, NetworkPolicy } from "@descryy/runtime-contracts";
|
|
60
|
-
/**
|
|
61
|
-
* Real capability check, matching `resourceLimitCapability`'s own shape and
|
|
62
|
-
* precedent exactly: existence on `PATH` first (fast, matches
|
|
63
|
-
* `prlimit --version`'s pattern), then the one Linux-specific precondition
|
|
64
|
-
* that existence alone doesn't prove -- unprivileged user namespaces
|
|
65
|
-
* actually being usable, not just bwrap being installed.
|
|
66
|
-
*/
|
|
42
|
+
/** Real capability check, same shape as `resourceLimitCapability`: PATH existence first, then the Linux-specific precondition existence alone doesn't prove -- unprivileged user namespaces actually usable, not just bwrap installed. */
|
|
67
43
|
export declare function filesystemIsolationCapability(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): CapabilityStatus;
|
|
68
|
-
/**
|
|
69
|
-
* The same underlying mechanism as `filesystemIsolationCapability` --
|
|
70
|
-
* bwrap building one namespace set covers both a restricted mount view and
|
|
71
|
-
* a network namespace in the same exec -- so this delegates rather than
|
|
72
|
-
* duplicating the check. Kept as its own named function because the two
|
|
73
|
-
* policies are declared, refused, and reasoned about independently
|
|
74
|
-
* (`NetworkPolicy` only supports one shape; see `unsupportedNetworkPolicyReason`),
|
|
75
|
-
* even though today they share one mechanism underneath.
|
|
76
|
-
*/
|
|
44
|
+
/** Same underlying mechanism as `filesystemIsolationCapability` (one bwrap namespace set covers both) -- delegates rather than duplicating. Kept as its own function because the two policies are declared and reasoned about independently. */
|
|
77
45
|
export declare function networkIsolationCapability(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): CapabilityStatus;
|
|
78
46
|
/**
|
|
79
|
-
* `NetworkPolicy`'s general shape (arbitrary allow/deny host lists) is not
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* itself is otherwise available, and the two refusals are different
|
|
85
|
-
* claims ("nothing here can enforce any network policy" vs. "this specific
|
|
86
|
-
* policy asks for something the real mechanism doesn't do yet").
|
|
47
|
+
* `NetworkPolicy`'s general shape (arbitrary allow/deny host lists) is not what
|
|
48
|
+
* this mechanism enforces -- only full denial. Returns the reason a policy can't be
|
|
49
|
+
* applied, or `null` if it's the one shape that can. Checked independently of
|
|
50
|
+
* `networkIsolationCapability`: "nothing here can enforce any policy" and "this
|
|
51
|
+
* specific policy asks for something not built yet" are different claims.
|
|
87
52
|
*/
|
|
88
53
|
export declare function unsupportedNetworkPolicyReason(policy: NetworkPolicy): string | null;
|
|
89
54
|
/**
|
|
90
55
|
* Resolves the real, symlink-followed directory containing the executable
|
|
91
|
-
* `command` would run as -- the one
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* is, which is exactly the kind of per-language special case §IR-boundary
|
|
98
|
-
* rules out. Returns `null` when `command` cannot be resolved (already
|
|
99
|
-
* absolute-looking and missing, or not found on `PATH`) -- the caller
|
|
100
|
-
* proceeds without an extra bind in that case; the exec itself will fail
|
|
101
|
-
* inside the sandbox the same honest way it would outside one.
|
|
56
|
+
* `command` would run as -- the one extra host path a sandboxed process needs
|
|
57
|
+
* beyond standard OS dirs and whatever the caller declared. Computed generically,
|
|
58
|
+
* not per-language: an interpreter installed outside `/usr` (nvm, rbenv, pyenv,
|
|
59
|
+
* sdkman) would otherwise be invisible inside the sandbox regardless of language.
|
|
60
|
+
* Returns `null` when unresolvable -- the caller proceeds without an extra bind;
|
|
61
|
+
* the exec fails inside the sandbox the same honest way it would outside one.
|
|
102
62
|
*/
|
|
103
63
|
export declare function resolveExecutableDirectory(command: string, cwd: string, env: NodeJS.ProcessEnv): string | null;
|
|
104
64
|
export interface SandboxOptions {
|
|
105
|
-
/**
|
|
65
|
+
/** Original, un-resource-limit-wrapped command (e.g. "node") -- used only to resolve which extra directory the interpreter needs bound in. */
|
|
106
66
|
readonly interpreterCommand: string;
|
|
107
|
-
/**
|
|
67
|
+
/** Command to actually exec inside the sandbox -- already passed through `applyResourceLimits`, so may be "prlimit" with the real command in `args`. */
|
|
108
68
|
readonly command: string;
|
|
109
69
|
readonly args: readonly string[];
|
|
110
70
|
readonly cwd: string;
|
|
@@ -114,23 +74,18 @@ export interface SandboxOptions {
|
|
|
114
74
|
readonly platform?: NodeJS.Platform;
|
|
115
75
|
}
|
|
116
76
|
/**
|
|
117
|
-
* Wraps `command`/`args` with `bwrap` so the OS enforces `filesystemPolicy
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
* before unless it opts in, the same non-regression contract
|
|
121
|
-
* `applyResourceLimits` already established for resource limits.
|
|
77
|
+
* Wraps `command`/`args` with `bwrap` so the OS enforces `filesystemPolicy`/
|
|
78
|
+
* `networkPolicy`. Neither declared returns them unchanged -- same non-regression
|
|
79
|
+
* contract as `applyResourceLimits`.
|
|
122
80
|
*
|
|
123
|
-
* **Throws rather than silently spawning unconstrained**
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
* platform has no mechanism at all or because the declared shape is one
|
|
127
|
-
* this mechanism doesn't implement (`unsupportedNetworkPolicyReason`).
|
|
81
|
+
* **Throws rather than silently spawning unconstrained** when a policy is
|
|
82
|
+
* requested and the real mechanism can't back it -- no mechanism on this platform,
|
|
83
|
+
* or a declared shape this mechanism doesn't implement.
|
|
128
84
|
*
|
|
129
|
-
* Composes with `applyResourceLimits
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
* every sandboxed process gets.
|
|
85
|
+
* Composes with `applyResourceLimits` by wrapping *outside* it (`bwrap … --
|
|
86
|
+
* prlimit … -- realCommand`) -- the mount namespace must exist before `prlimit` or
|
|
87
|
+
* the real command run inside it; `prlimit` (in `/usr/bin`) is reachable through
|
|
88
|
+
* the same base OS binding every sandboxed process gets.
|
|
134
89
|
*/
|
|
135
90
|
export declare function applySandbox(options: SandboxOptions): {
|
|
136
91
|
readonly command: string;
|
package/dist/sandbox.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sandbox.d.ts","sourceRoot":"","sources":["../src/sandbox.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"sandbox.d.ts","sourceRoot":"","sources":["../src/sandbox.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAKH,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAkCpG,0OAA0O;AAC1O,wBAAgB,6BAA6B,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,EAAE,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAAG,gBAAgB,CAoBlJ;AAED,gPAAgP;AAChP,wBAAgB,0BAA0B,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,EAAE,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAAG,gBAAgB,CAE/I;AAED;;;;;;GAMG;AACH,wBAAgB,8BAA8B,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,GAAG,IAAI,CAOnF;AAOD;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG,MAAM,GAAG,IAAI,CAwB9G;AAED,MAAM,WAAW,cAAc;IAC7B,8IAA8I;IAC9I,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,wJAAwJ;IACxJ,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;IAC7C,QAAQ,CAAC,aAAa,CAAC,EAAE,aAAa,CAAC;IACvC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACjC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;CACrC;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,cAAc,GAAG;IAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,CAkFpH"}
|