@descryy/runtime-controller 0.3.8 → 0.3.9
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/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 +9 -0
- package/dist/capability-registry.d.ts.map +1 -0
- package/dist/capability-registry.js +32 -0
- package/dist/capability-registry.js.map +1 -0
- package/dist/collector-version.d.ts +3 -0
- package/dist/collector-version.d.ts.map +1 -0
- package/dist/collector-version.js +5 -0
- package/dist/collector-version.js.map +1 -0
- package/dist/container-sandbox.d.ts +104 -0
- package/dist/container-sandbox.d.ts.map +1 -0
- package/dist/container-sandbox.js +152 -0
- package/dist/container-sandbox.js.map +1 -0
- package/dist/controller.d.ts +138 -0
- package/dist/controller.d.ts.map +1 -0
- package/dist/controller.js +449 -0
- package/dist/controller.js.map +1 -0
- package/dist/dependency-version-check.d.ts +36 -0
- package/dist/dependency-version-check.d.ts.map +1 -0
- package/dist/dependency-version-check.js +72 -0
- package/dist/dependency-version-check.js.map +1 -0
- package/dist/env.d.ts +10 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +12 -0
- package/dist/env.js.map +1 -0
- package/dist/environment-metadata.d.ts +15 -0
- package/dist/environment-metadata.d.ts.map +1 -0
- package/dist/environment-metadata.js +63 -0
- package/dist/environment-metadata.js.map +1 -0
- package/dist/environment-version-check.d.ts +40 -0
- package/dist/environment-version-check.d.ts.map +1 -0
- package/dist/environment-version-check.js +176 -0
- package/dist/environment-version-check.js.map +1 -0
- package/dist/execution-safety.d.ts +96 -0
- package/dist/execution-safety.d.ts.map +1 -0
- package/dist/execution-safety.js +140 -0
- package/dist/execution-safety.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/orchestration.d.ts +55 -0
- package/dist/orchestration.d.ts.map +1 -0
- package/dist/orchestration.js +219 -0
- package/dist/orchestration.js.map +1 -0
- package/dist/preflight.d.ts +122 -0
- package/dist/preflight.d.ts.map +1 -0
- package/dist/preflight.js +177 -0
- package/dist/preflight.js.map +1 -0
- package/dist/process-collector.d.ts +20 -0
- package/dist/process-collector.d.ts.map +1 -0
- package/dist/process-collector.js +116 -0
- package/dist/process-collector.js.map +1 -0
- package/dist/process-identity.d.ts +46 -0
- package/dist/process-identity.d.ts.map +1 -0
- package/dist/process-identity.js +186 -0
- package/dist/process-identity.js.map +1 -0
- package/dist/process-manager.d.ts +137 -0
- package/dist/process-manager.d.ts.map +1 -0
- package/dist/process-manager.js +365 -0
- package/dist/process-manager.js.map +1 -0
- package/dist/readiness.d.ts +122 -0
- package/dist/readiness.d.ts.map +1 -0
- package/dist/readiness.js +214 -0
- package/dist/readiness.js.map +1 -0
- package/dist/sandbox.d.ts +94 -0
- package/dist/sandbox.d.ts.map +1 -0
- package/dist/sandbox.js +228 -0
- package/dist/sandbox.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attach fencing (RT-N3): the boundary between "content this run watched happen"
|
|
3
|
+
* and "content that already existed when this run started watching it."
|
|
4
|
+
*
|
|
5
|
+
* Both `orchestrator/src/attach-to-running-process.ts` and this package's own
|
|
6
|
+
* `attachManagedProcess` read a target's ENTIRE log file on every poll —
|
|
7
|
+
* necessary, since a plain file offers no delta API — which means the very
|
|
8
|
+
* first read hands back everything the target had already written before
|
|
9
|
+
* attach, indistinguishable from something that happens during this run
|
|
10
|
+
* unless something records the boundary.
|
|
11
|
+
*
|
|
12
|
+
* Real incident (`documents/plans/descry-uat-phase-4.md`, finding N3): a
|
|
13
|
+
* killed, orphaned FIRST process took its full timeout to exit and kept
|
|
14
|
+
* writing to the SAME log path a SECOND, successful process also used;
|
|
15
|
+
* attaching to the second process's pid read the first process's crash as if
|
|
16
|
+
* it were the second's own. Two independent fences close two independent
|
|
17
|
+
* parts of that gap:
|
|
18
|
+
*
|
|
19
|
+
* - `computeAttachFence` records, at the moment of attach, how much of the
|
|
20
|
+
* log file already existed (`attachOffsetBytes`, `preExistingLineCount`)
|
|
21
|
+
* and the target's own process start time (best-effort, Linux only) — the
|
|
22
|
+
* information a caller needs to mark already-written content instead of
|
|
23
|
+
* treating it as freshly observed.
|
|
24
|
+
* - `detectOtherLogWriters` answers a different question: is some OTHER
|
|
25
|
+
* process, right now, also holding this same log file open for writing?
|
|
26
|
+
* On Linux only, by scanning `/proc/*\/fd`. A genuinely shared log (e.g. a
|
|
27
|
+
* `docker compose` stack's combined stream) is a legitimate answer here,
|
|
28
|
+
* not refused — this function reports who else has the file open, and it
|
|
29
|
+
* is the caller's job to decide what that means for a given target.
|
|
30
|
+
*
|
|
31
|
+
* Neither function makes attach perfectly correct — a log file carries no
|
|
32
|
+
* per-line writer-pid metadata, so "verify every line against the declared
|
|
33
|
+
* pid" is not implementable over a plain text file. What these two give a
|
|
34
|
+
* caller together: the boundary of what this run actually watched happen,
|
|
35
|
+
* and a best-effort, honestly-labelled answer to "is anything else writing
|
|
36
|
+
* here right now."
|
|
37
|
+
*/
|
|
38
|
+
export type ProcessStartTimeResult = {
|
|
39
|
+
readonly ok: true;
|
|
40
|
+
readonly startTimeMs: number;
|
|
41
|
+
} | {
|
|
42
|
+
readonly ok: false;
|
|
43
|
+
readonly reason: string;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* `pid`'s own start time, as wall-clock epoch milliseconds — Linux only.
|
|
47
|
+
* `/proc/<pid>/stat` field 22 (`starttime`) is clock ticks since boot, not
|
|
48
|
+
* since epoch; converted via `/proc/uptime`'s boot-relative uptime, itself
|
|
49
|
+
* converted to an epoch instant from `Date.now()` at read time (so this is
|
|
50
|
+
* only as precise as the gap between the two reads, sub-millisecond in
|
|
51
|
+
* practice).
|
|
52
|
+
*
|
|
53
|
+
* The `comm` field (`/proc/pid/stat` field 2) can itself contain spaces and
|
|
54
|
+
* parentheses — parsed by finding the LAST `)` on the line, matching every
|
|
55
|
+
* real `/proc/pid/stat` parser: the kernel guarantees the true comm field
|
|
56
|
+
* ends at the last `)`, whatever it contains.
|
|
57
|
+
*/
|
|
58
|
+
export declare function getProcessStartTimeMs(pid: number, platform?: NodeJS.Platform): ProcessStartTimeResult;
|
|
59
|
+
export type OtherLogWriterCheck = {
|
|
60
|
+
readonly checked: true;
|
|
61
|
+
readonly otherWriterPids: readonly number[];
|
|
62
|
+
} | {
|
|
63
|
+
readonly checked: false;
|
|
64
|
+
readonly reason: string;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Every OTHER process (not `targetPid`) currently holding an open file
|
|
68
|
+
* descriptor to `logFilePath`, by scanning `/proc/*\/fd` and comparing each
|
|
69
|
+
* descriptor's real target against `logFilePath`'s own real path. Linux only.
|
|
70
|
+
*
|
|
71
|
+
* A non-empty result is not by itself a problem — a deliberately shared log
|
|
72
|
+
* (several processes in one `docker compose` stack writing one combined
|
|
73
|
+
* stream) is legitimate multi-writer use. This function only answers "who
|
|
74
|
+
* else has it open right now"; a caller decides what that means for a given
|
|
75
|
+
* target.
|
|
76
|
+
*
|
|
77
|
+
* `checked: false` (never a thrown exception) only for what stops the scan
|
|
78
|
+
* from running AT ALL: non-Linux, `/proc` itself unreadable, or the log path
|
|
79
|
+
* can't be resolved to a real path. A single pid's `/proc/<pid>/fd` being
|
|
80
|
+
* unreadable (exited mid-scan, or a permission boundary) is NOT that —
|
|
81
|
+
* skipped for that one pid only, the same benign-race tolerance
|
|
82
|
+
* `inferRespawnCommandFromProc` already applies to `/proc` reads elsewhere
|
|
83
|
+
* in this codebase.
|
|
84
|
+
*/
|
|
85
|
+
export declare function detectOtherLogWriters(logFilePath: string, targetPid: number, platform?: NodeJS.Platform): OtherLogWriterCheck;
|
|
86
|
+
export interface AttachFence {
|
|
87
|
+
/** The log file's size in bytes at the moment of attach — rotation detection compares this (or a later high-water mark) against the file's current size. */
|
|
88
|
+
readonly attachOffsetBytes: number;
|
|
89
|
+
/**
|
|
90
|
+
* How many complete lines already existed in the log file at the moment of
|
|
91
|
+
* attach. A source that yields lines in file order marks the first
|
|
92
|
+
* this-many `preExisting: true`. See `countCompleteLines` for the one
|
|
93
|
+
* disclosed edge case (a line torn exactly at the attach instant).
|
|
94
|
+
*/
|
|
95
|
+
readonly preExistingLineCount: number;
|
|
96
|
+
/** Best-effort, Linux-only; `ok: false` on any other platform or read failure — never blocks the attach itself. */
|
|
97
|
+
readonly processStartTime: ProcessStartTimeResult;
|
|
98
|
+
/** See `detectOtherLogWriters` — computed once, at the moment of attach, against the pid this fence was built for. */
|
|
99
|
+
readonly otherWriters: OtherLogWriterCheck;
|
|
100
|
+
readonly attachedAt: string;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Snapshots the fence at the moment of attach. Never throws: a log file that
|
|
104
|
+
* doesn't exist yet at attach time (a target that hasn't written anything) is
|
|
105
|
+
* an honest zero-backlog fence, not an error — the file-tail machinery
|
|
106
|
+
* downstream already tolerates a not-yet-existing path the same way.
|
|
107
|
+
*/
|
|
108
|
+
export declare function computeAttachFence(pid: number, logFilePath: string, platform?: NodeJS.Platform): AttachFence;
|
|
109
|
+
//# sourceMappingURL=attach-fencing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attach-fencing.d.ts","sourceRoot":"","sources":["../src/attach-fencing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAsBH,MAAM,MAAM,sBAAsB,GAAG;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAwB3I;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAAG,sBAAsB,CAiCvH;AAED,MAAM,MAAM,mBAAmB,GAC3B;IAAE,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,eAAe,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,GACvE;IAAE,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAAG,mBAAmB,CA+C/I;AAED,MAAM,WAAW,WAAW;IAC1B,4JAA4J;IAC5J,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC;;;;;OAKG;IACH,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAC;IACtC,mHAAmH;IACnH,QAAQ,CAAC,gBAAgB,EAAE,sBAAsB,CAAC;IAClD,sHAAsH;IACtH,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAC;IAC3C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAAG,WAAW,CAgB9H"}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attach fencing (RT-N3): the boundary between "content this run watched happen"
|
|
3
|
+
* and "content that already existed when this run started watching it."
|
|
4
|
+
*
|
|
5
|
+
* Both `orchestrator/src/attach-to-running-process.ts` and this package's own
|
|
6
|
+
* `attachManagedProcess` read a target's ENTIRE log file on every poll —
|
|
7
|
+
* necessary, since a plain file offers no delta API — which means the very
|
|
8
|
+
* first read hands back everything the target had already written before
|
|
9
|
+
* attach, indistinguishable from something that happens during this run
|
|
10
|
+
* unless something records the boundary.
|
|
11
|
+
*
|
|
12
|
+
* Real incident (`documents/plans/descry-uat-phase-4.md`, finding N3): a
|
|
13
|
+
* killed, orphaned FIRST process took its full timeout to exit and kept
|
|
14
|
+
* writing to the SAME log path a SECOND, successful process also used;
|
|
15
|
+
* attaching to the second process's pid read the first process's crash as if
|
|
16
|
+
* it were the second's own. Two independent fences close two independent
|
|
17
|
+
* parts of that gap:
|
|
18
|
+
*
|
|
19
|
+
* - `computeAttachFence` records, at the moment of attach, how much of the
|
|
20
|
+
* log file already existed (`attachOffsetBytes`, `preExistingLineCount`)
|
|
21
|
+
* and the target's own process start time (best-effort, Linux only) — the
|
|
22
|
+
* information a caller needs to mark already-written content instead of
|
|
23
|
+
* treating it as freshly observed.
|
|
24
|
+
* - `detectOtherLogWriters` answers a different question: is some OTHER
|
|
25
|
+
* process, right now, also holding this same log file open for writing?
|
|
26
|
+
* On Linux only, by scanning `/proc/*\/fd`. A genuinely shared log (e.g. a
|
|
27
|
+
* `docker compose` stack's combined stream) is a legitimate answer here,
|
|
28
|
+
* not refused — this function reports who else has the file open, and it
|
|
29
|
+
* is the caller's job to decide what that means for a given target.
|
|
30
|
+
*
|
|
31
|
+
* Neither function makes attach perfectly correct — a log file carries no
|
|
32
|
+
* per-line writer-pid metadata, so "verify every line against the declared
|
|
33
|
+
* pid" is not implementable over a plain text file. What these two give a
|
|
34
|
+
* caller together: the boundary of what this run actually watched happen,
|
|
35
|
+
* and a best-effort, honestly-labelled answer to "is anything else writing
|
|
36
|
+
* here right now."
|
|
37
|
+
*/
|
|
38
|
+
import { execFileSync } from "node:child_process";
|
|
39
|
+
import { readFileSync, readdirSync, readlinkSync, realpathSync, statSync } from "node:fs";
|
|
40
|
+
/**
|
|
41
|
+
* Complete (newline-terminated) lines in `text` — the same rule `LineSplitter`
|
|
42
|
+
* (`@descryy/runtime-backend-observation`) applies, reimplemented here rather
|
|
43
|
+
* than imported: this package does not depend on that one (the dependency
|
|
44
|
+
* runs the other way), and the rule is three lines. A trailing, unterminated
|
|
45
|
+
* partial line is deliberately NOT counted — see `computeAttachFence`'s doc
|
|
46
|
+
* for the one edge case that leaves disclosed rather than solved.
|
|
47
|
+
*/
|
|
48
|
+
function countCompleteLines(text) {
|
|
49
|
+
if (text === "")
|
|
50
|
+
return 0;
|
|
51
|
+
const parts = text.split(/\r\n|\r|\n/);
|
|
52
|
+
// split() always yields at least one element; the LAST one is whatever
|
|
53
|
+
// followed the final terminator (empty, when text ends with one — itself
|
|
54
|
+
// not a line — or a real trailing partial otherwise; neither is counted).
|
|
55
|
+
return parts.length - 1;
|
|
56
|
+
}
|
|
57
|
+
let cachedClockTicksPerSecond = null;
|
|
58
|
+
/**
|
|
59
|
+
* `sysconf(_SC_CLK_TCK)` has no Node binding; `getconf CLK_TCK` is the same
|
|
60
|
+
* syscall via the POSIX utility every Linux ships. Cached process-wide — it
|
|
61
|
+
* cannot change while this process runs. Falls back to 100 (`USER_HZ`, the
|
|
62
|
+
* value on every Linux kernel/architecture this has been measured against)
|
|
63
|
+
* if `getconf` itself is unavailable, rather than failing the whole
|
|
64
|
+
* start-time computation over a missing convenience binary.
|
|
65
|
+
*/
|
|
66
|
+
function clockTicksPerSecond() {
|
|
67
|
+
if (cachedClockTicksPerSecond !== null)
|
|
68
|
+
return cachedClockTicksPerSecond;
|
|
69
|
+
try {
|
|
70
|
+
const output = execFileSync("getconf", ["CLK_TCK"], { encoding: "utf8" }).trim();
|
|
71
|
+
const parsed = Number(output);
|
|
72
|
+
cachedClockTicksPerSecond = Number.isFinite(parsed) && parsed > 0 ? parsed : 100;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
cachedClockTicksPerSecond = 100;
|
|
76
|
+
}
|
|
77
|
+
return cachedClockTicksPerSecond;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* `pid`'s own start time, as wall-clock epoch milliseconds — Linux only.
|
|
81
|
+
* `/proc/<pid>/stat` field 22 (`starttime`) is clock ticks since boot, not
|
|
82
|
+
* since epoch; converted via `/proc/uptime`'s boot-relative uptime, itself
|
|
83
|
+
* converted to an epoch instant from `Date.now()` at read time (so this is
|
|
84
|
+
* only as precise as the gap between the two reads, sub-millisecond in
|
|
85
|
+
* practice).
|
|
86
|
+
*
|
|
87
|
+
* The `comm` field (`/proc/pid/stat` field 2) can itself contain spaces and
|
|
88
|
+
* parentheses — parsed by finding the LAST `)` on the line, matching every
|
|
89
|
+
* real `/proc/pid/stat` parser: the kernel guarantees the true comm field
|
|
90
|
+
* ends at the last `)`, whatever it contains.
|
|
91
|
+
*/
|
|
92
|
+
export function getProcessStartTimeMs(pid, platform = process.platform) {
|
|
93
|
+
if (platform !== "linux") {
|
|
94
|
+
return { ok: false, reason: `process start time via /proc is Linux-only; not available on "${platform}"` };
|
|
95
|
+
}
|
|
96
|
+
let statLine;
|
|
97
|
+
try {
|
|
98
|
+
statLine = readFileSync(`/proc/${String(pid)}/stat`, "utf8");
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
return { ok: false, reason: `could not read /proc/${String(pid)}/stat: ${error instanceof Error ? error.message : String(error)}` };
|
|
102
|
+
}
|
|
103
|
+
const afterComm = statLine.slice(statLine.lastIndexOf(")") + 1).trim();
|
|
104
|
+
const fields = afterComm.split(/\s+/);
|
|
105
|
+
// field 1 of `afterComm` is /proc/pid/stat's field 3 (state); starttime is
|
|
106
|
+
// field 22 overall, i.e. index 19 in this zero-indexed, comm-stripped array.
|
|
107
|
+
const starttimeTicks = Number(fields[19]);
|
|
108
|
+
if (!Number.isFinite(starttimeTicks)) {
|
|
109
|
+
return { ok: false, reason: `/proc/${String(pid)}/stat did not parse as expected — starttime field missing or non-numeric` };
|
|
110
|
+
}
|
|
111
|
+
let uptimeSeconds;
|
|
112
|
+
try {
|
|
113
|
+
const uptimeRaw = readFileSync("/proc/uptime", "utf8").trim().split(/\s+/)[0];
|
|
114
|
+
uptimeSeconds = Number(uptimeRaw);
|
|
115
|
+
if (!Number.isFinite(uptimeSeconds))
|
|
116
|
+
throw new Error("non-numeric");
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
return { ok: false, reason: `could not read /proc/uptime to convert starttime to wall-clock time: ${error instanceof Error ? error.message : String(error)}` };
|
|
120
|
+
}
|
|
121
|
+
const bootEpochMs = Date.now() - uptimeSeconds * 1000;
|
|
122
|
+
const startTimeMs = bootEpochMs + (starttimeTicks / clockTicksPerSecond()) * 1000;
|
|
123
|
+
return { ok: true, startTimeMs };
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Every OTHER process (not `targetPid`) currently holding an open file
|
|
127
|
+
* descriptor to `logFilePath`, by scanning `/proc/*\/fd` and comparing each
|
|
128
|
+
* descriptor's real target against `logFilePath`'s own real path. Linux only.
|
|
129
|
+
*
|
|
130
|
+
* A non-empty result is not by itself a problem — a deliberately shared log
|
|
131
|
+
* (several processes in one `docker compose` stack writing one combined
|
|
132
|
+
* stream) is legitimate multi-writer use. This function only answers "who
|
|
133
|
+
* else has it open right now"; a caller decides what that means for a given
|
|
134
|
+
* target.
|
|
135
|
+
*
|
|
136
|
+
* `checked: false` (never a thrown exception) only for what stops the scan
|
|
137
|
+
* from running AT ALL: non-Linux, `/proc` itself unreadable, or the log path
|
|
138
|
+
* can't be resolved to a real path. A single pid's `/proc/<pid>/fd` being
|
|
139
|
+
* unreadable (exited mid-scan, or a permission boundary) is NOT that —
|
|
140
|
+
* skipped for that one pid only, the same benign-race tolerance
|
|
141
|
+
* `inferRespawnCommandFromProc` already applies to `/proc` reads elsewhere
|
|
142
|
+
* in this codebase.
|
|
143
|
+
*/
|
|
144
|
+
export function detectOtherLogWriters(logFilePath, targetPid, platform = process.platform) {
|
|
145
|
+
if (platform !== "linux") {
|
|
146
|
+
return { checked: false, reason: `second-writer detection requires scanning /proc/*/fd, which is Linux-only; not available on "${platform}"` };
|
|
147
|
+
}
|
|
148
|
+
let target;
|
|
149
|
+
try {
|
|
150
|
+
target = realpathSync(logFilePath);
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
return {
|
|
154
|
+
checked: false,
|
|
155
|
+
reason: `could not resolve a real path for "${logFilePath}" to compare against other processes' open file descriptors: ${error instanceof Error ? error.message : String(error)}`,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
let pidEntries;
|
|
159
|
+
try {
|
|
160
|
+
pidEntries = readdirSync("/proc").filter((entry) => /^\d+$/.test(entry));
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
return { checked: false, reason: `could not list /proc to scan for other processes holding "${logFilePath}" open: ${error instanceof Error ? error.message : String(error)}` };
|
|
164
|
+
}
|
|
165
|
+
const otherWriterPids = [];
|
|
166
|
+
for (const entry of pidEntries) {
|
|
167
|
+
const pid = Number(entry);
|
|
168
|
+
if (pid === targetPid)
|
|
169
|
+
continue; // the attached target itself is expected to hold it open -- not "other"
|
|
170
|
+
let fds;
|
|
171
|
+
try {
|
|
172
|
+
fds = readdirSync(`/proc/${entry}/fd`);
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
continue; // exited mid-scan, or no permission -- a benign race, not this scan's failure
|
|
176
|
+
}
|
|
177
|
+
for (const fd of fds) {
|
|
178
|
+
try {
|
|
179
|
+
if (readlinkSync(`/proc/${entry}/fd/${fd}`) === target) {
|
|
180
|
+
otherWriterPids.push(pid);
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
// fd closed between readdir and readlink -- benign race, skip
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return { checked: true, otherWriterPids };
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Snapshots the fence at the moment of attach. Never throws: a log file that
|
|
193
|
+
* doesn't exist yet at attach time (a target that hasn't written anything) is
|
|
194
|
+
* an honest zero-backlog fence, not an error — the file-tail machinery
|
|
195
|
+
* downstream already tolerates a not-yet-existing path the same way.
|
|
196
|
+
*/
|
|
197
|
+
export function computeAttachFence(pid, logFilePath, platform = process.platform) {
|
|
198
|
+
let attachOffsetBytes = 0;
|
|
199
|
+
let preExistingLineCount = 0;
|
|
200
|
+
try {
|
|
201
|
+
attachOffsetBytes = statSync(logFilePath).size;
|
|
202
|
+
preExistingLineCount = countCompleteLines(readFileSync(logFilePath, "utf8"));
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
// Not yet created -- nothing pre-existing, the honest default.
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
attachOffsetBytes,
|
|
209
|
+
preExistingLineCount,
|
|
210
|
+
processStartTime: getProcessStartTimeMs(pid, platform),
|
|
211
|
+
otherWriters: detectOtherLogWriters(logFilePath, pid, platform),
|
|
212
|
+
attachedAt: new Date().toISOString(),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
//# sourceMappingURL=attach-fencing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attach-fencing.js","sourceRoot":"","sources":["../src/attach-fencing.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CapabilityStatus, CollectorCapabilities, EnvironmentTier, ExecutionCapabilities, FidelityLevel } from "@descryy/runtime-contracts";
|
|
2
|
+
export interface RegisteredCollectorCapabilities {
|
|
3
|
+
readonly collectorId: string;
|
|
4
|
+
readonly capabilities: CollectorCapabilities;
|
|
5
|
+
}
|
|
6
|
+
export declare function buildExecutionCapabilities(environmentTier: EnvironmentTier, fidelityLevel: FidelityLevel, collectors: readonly RegisteredCollectorCapabilities[]): ExecutionCapabilities;
|
|
7
|
+
export type CollectorCapabilityKey = keyof CollectorCapabilities;
|
|
8
|
+
export declare function bestCapabilityStatus(capabilities: ExecutionCapabilities, key: CollectorCapabilityKey): CapabilityStatus;
|
|
9
|
+
//# 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":"AAKA,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;AAgBjE,wBAAgB,oBAAoB,CAClC,YAAY,EAAE,qBAAqB,EACnC,GAAG,EAAE,sBAAsB,GAC1B,gBAAgB,CAWlB"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Capability registry (§16): assembles per-execution capabilities from the collectors
|
|
2
|
+
// actually present, so e.g. `distributedTrace: unavailable` is a first-class answer
|
|
3
|
+
// rather than a silent gap. Assembles and queries only — producing a
|
|
4
|
+
// `CollectorCapabilities` value is each collector's own `capabilities()`.
|
|
5
|
+
export function buildExecutionCapabilities(environmentTier, fidelityLevel, collectors) {
|
|
6
|
+
return { environmentTier, fidelityLevel, collectors };
|
|
7
|
+
}
|
|
8
|
+
const AVAILABILITY_RANK = {
|
|
9
|
+
unavailable: 0,
|
|
10
|
+
degraded: 1,
|
|
11
|
+
available: 2,
|
|
12
|
+
};
|
|
13
|
+
const NO_COLLECTOR_STATUS = {
|
|
14
|
+
availability: "unavailable",
|
|
15
|
+
reason: "no collector registered for this execution",
|
|
16
|
+
};
|
|
17
|
+
// Best (most available) status for one capability across all registered collectors —
|
|
18
|
+
// "observable anywhere in this execution," not per-collector: two network collectors,
|
|
19
|
+
// one degraded one available, should read as available.
|
|
20
|
+
export function bestCapabilityStatus(capabilities, key) {
|
|
21
|
+
// Seeded from the first collector seen, not NO_COLLECTOR_STATUS — seeding from the
|
|
22
|
+
// placeholder would let a real "unavailable" tie the placeholder and lose its reason.
|
|
23
|
+
let best = null;
|
|
24
|
+
for (const collector of capabilities.collectors) {
|
|
25
|
+
const status = collector.capabilities[key];
|
|
26
|
+
if (best === null || AVAILABILITY_RANK[status.availability] > AVAILABILITY_RANK[best.availability]) {
|
|
27
|
+
best = status;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return best ?? NO_COLLECTOR_STATUS;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=capability-registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capability-registry.js","sourceRoot":"","sources":["../src/capability-registry.ts"],"names":[],"mappings":"AAAA,sFAAsF;AACtF,oFAAoF;AACpF,qEAAqE;AACrE,0EAA0E;AAgB1E,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,qFAAqF;AACrF,sFAAsF;AACtF,wDAAwD;AACxD,MAAM,UAAU,oBAAoB,CAClC,YAAmC,EACnC,GAA2B;IAE3B,mFAAmF;IACnF,sFAAsF;IACtF,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,3 @@
|
|
|
1
|
+
/** `Evidence.collectorVersion` for `ProcessCollector`: read from package.json itself so it can't drift from what shipped. Same pattern as `nodeVersion` (`process.version`), applied to a package instead of the runtime. */
|
|
2
|
+
export declare const COLLECTOR_VERSION: string;
|
|
3
|
+
//# 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,6NAA6N;AAM7N,eAAO,MAAM,iBAAiB,EAAE,MAA6E,CAAC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** `Evidence.collectorVersion` for `ProcessCollector`: read from package.json itself so it can't drift from what shipped. Same pattern as `nodeVersion` (`process.version`), applied to a package instead of the runtime. */
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
const require = createRequire(import.meta.url);
|
|
4
|
+
export const COLLECTOR_VERSION = require("../package.json").version;
|
|
5
|
+
//# sourceMappingURL=collector-version.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collector-version.js","sourceRoot":"","sources":["../src/collector-version.ts"],"names":[],"mappings":"AAAA,6NAA6N;AAE7N,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,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* §21's execution boundary, container backend -- the macOS/Windows half of the
|
|
3
|
+
* sandbox-isolation lane (`sandbox.ts` covers Linux via bubblewrap).
|
|
4
|
+
*
|
|
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.
|
|
10
|
+
*
|
|
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.
|
|
14
|
+
*
|
|
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.
|
|
20
|
+
*
|
|
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.
|
|
29
|
+
*
|
|
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.
|
|
34
|
+
*
|
|
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.
|
|
43
|
+
*
|
|
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.
|
|
51
|
+
*/
|
|
52
|
+
import type { CapabilityStatus, FilesystemPolicy, NetworkPolicy } from "@descryy/runtime-contracts";
|
|
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." */
|
|
54
|
+
export declare function containerRuntimeCapability(env?: NodeJS.ProcessEnv): CapabilityStatus;
|
|
55
|
+
export declare function containerFilesystemIsolationCapability(env?: NodeJS.ProcessEnv): CapabilityStatus;
|
|
56
|
+
export declare function containerNetworkIsolationCapability(env?: NodeJS.ProcessEnv): CapabilityStatus;
|
|
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. */
|
|
58
|
+
export declare function unsupportedContainerNetworkPolicyReason(policy: NetworkPolicy): string | null;
|
|
59
|
+
export declare function resolveContainerImage(interpreterCommand: string): string | null;
|
|
60
|
+
export interface ContainerSandboxOptions {
|
|
61
|
+
/** Resolves a default base image via `resolveContainerImage` when `containerImage` is not given. */
|
|
62
|
+
readonly interpreterCommand: string;
|
|
63
|
+
/** Command to exec inside the container -- resolved against the image's own PATH, not the host's. */
|
|
64
|
+
readonly command: string;
|
|
65
|
+
readonly args: readonly string[];
|
|
66
|
+
/** Always bind-mounted at the identical path (`-v cwd:cwd`) -- mirrors sandbox.ts's "cwd always bound" default. */
|
|
67
|
+
readonly cwd: string;
|
|
68
|
+
readonly filesystemPolicy?: FilesystemPolicy;
|
|
69
|
+
readonly networkPolicy?: NetworkPolicy;
|
|
70
|
+
/**
|
|
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`).
|
|
76
|
+
*/
|
|
77
|
+
readonly processEnv?: Readonly<Record<string, string | undefined>>;
|
|
78
|
+
/** Env for running the `docker` CLI itself (PATH, `DOCKER_HOST`). Defaults to `process.env`. */
|
|
79
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
80
|
+
/** Explicit override, bypasses `resolveContainerImage`'s name-based guess. */
|
|
81
|
+
readonly containerImage?: string;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
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`.
|
|
88
|
+
*
|
|
89
|
+
* **Throws rather than silently spawning unconstrained** when a policy can't
|
|
90
|
+
* actually be backed (no working Docker, unimplemented `networkPolicy` shape).
|
|
91
|
+
*
|
|
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.
|
|
98
|
+
*/
|
|
99
|
+
export declare function applyContainerSandbox(options: ContainerSandboxOptions): {
|
|
100
|
+
readonly command: string;
|
|
101
|
+
readonly args: readonly string[];
|
|
102
|
+
readonly containerName?: string;
|
|
103
|
+
};
|
|
104
|
+
//# 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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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"}
|