@descryy/runtime-controller 0.3.8 → 0.4.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/dist/attach-discovery.d.ts +94 -0
- package/dist/attach-discovery.d.ts.map +1 -0
- package/dist/attach-discovery.js +238 -0
- package/dist/attach-discovery.js.map +1 -0
- package/dist/attach-fencing.d.ts +109 -0
- package/dist/attach-fencing.d.ts.map +1 -0
- package/dist/attach-fencing.js +215 -0
- package/dist/attach-fencing.js.map +1 -0
- package/dist/capability-registry.d.ts +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 +152 -0
- package/dist/controller.d.ts.map +1 -0
- package/dist/controller.js +464 -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 +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -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 +382 -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 +2 -2
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ready check before a run (RT-263). A broken setup used to be discovered only after a
|
|
3
|
+
* full readiness wait (up to `RunOptions.readiness[*].timeoutMs`, often ~30s-2min) had
|
|
4
|
+
* already failed. This runs a short, read-only pass first — the same declared checks a
|
|
5
|
+
* run is about to depend on, single-shot rather than polled to a full timeout — and
|
|
6
|
+
* reports a plain-words problem and fix before the caller starts anything.
|
|
7
|
+
*
|
|
8
|
+
* Deliberately generic: this module owns the engine (status, timing, disclosure), never
|
|
9
|
+
* a specific check's mechanism it would have to import a language, a browser, a graph
|
|
10
|
+
* store or a database driver to perform. `customCheck` is the seam every one of those
|
|
11
|
+
* uses — `chromiumCanLaunch`, `graphExists`, and any future "database reachable" check
|
|
12
|
+
* are built by the caller that already depends on the thing being checked, and handed in
|
|
13
|
+
* as a probe. Nothing here decides what a broken setup is, only how many of the caller's
|
|
14
|
+
* own answers it takes to call one "ready", "partial" or "broken".
|
|
15
|
+
*
|
|
16
|
+
* **Read-only, always.** Every probe built here (`portFreeCheck`, `portAnswersCheck`,
|
|
17
|
+
* `readinessTargetCheck`) only ever connects or asks; none binds a port, starts a
|
|
18
|
+
* process or writes anything. A `customCheck` probe is the caller's own code and this
|
|
19
|
+
* module cannot enforce that discipline on it, but every caller in this codebase that
|
|
20
|
+
* builds one must hold to it too (Build A step 4).
|
|
21
|
+
*/
|
|
22
|
+
import { createConnection } from "node:net";
|
|
23
|
+
import { awaitReadiness } from "./readiness.js";
|
|
24
|
+
export const PREFLIGHT_CHECK_KINDS = ["port", "readiness", "chromium", "graph", "database", "custom"];
|
|
25
|
+
export const PREFLIGHT_STATUSES = ["ready", "partial", "broken"];
|
|
26
|
+
/**
|
|
27
|
+
* Bounds one check's own probe — not the whole preflight. Every check in a call runs
|
|
28
|
+
* concurrently (`Promise.all`), so a caller with several checks still finishes near this
|
|
29
|
+
* bound rather than their sum. Starting value, not a measurement: it is what lets a
|
|
30
|
+
* `broken` setup report within the lane's 5s target while still giving a slow-but-honest
|
|
31
|
+
* TCP probe (a loaded machine, a container cold-starting) more than an instant to answer.
|
|
32
|
+
*/
|
|
33
|
+
export const PREFLIGHT_CHECK_TIMEOUT_MS = 4_000;
|
|
34
|
+
/**
|
|
35
|
+
* Runs every check concurrently and folds the results into one status. Never throws —
|
|
36
|
+
* a probe that rejects or hangs past its own bound is recorded as a failed check
|
|
37
|
+
* (`runOne` below), not a crash of the preflight itself, because "the check broke" and
|
|
38
|
+
* "the setup is broken" are different facts the caller still needs told apart... except
|
|
39
|
+
* they aren't told apart here: a check that cannot even answer is exactly as blocking as
|
|
40
|
+
* one that answered "no", which is why it is folded in as `!ok` rather than a third
|
|
41
|
+
* outcome. What is preserved is the message, which always names which check and why.
|
|
42
|
+
*/
|
|
43
|
+
export async function runPreflight(checks) {
|
|
44
|
+
const startedAt = Date.now();
|
|
45
|
+
const results = await Promise.all(checks.map((check) => runOne(check)));
|
|
46
|
+
const elapsedMs = Date.now() - startedAt;
|
|
47
|
+
const brokenChecks = results.filter((result) => result.required && !result.ok);
|
|
48
|
+
const gaps = results.filter((result) => !result.required && !result.ok);
|
|
49
|
+
const status = brokenChecks.length > 0 ? "broken" : gaps.length > 0 ? "partial" : "ready";
|
|
50
|
+
return { status, elapsedMs, checks: results, disclosures: gaps.map((gap) => gap.message) };
|
|
51
|
+
}
|
|
52
|
+
async function runOne(check) {
|
|
53
|
+
try {
|
|
54
|
+
const outcome = await withTimeout(check.probe(), PREFLIGHT_CHECK_TIMEOUT_MS, check.label);
|
|
55
|
+
return { kind: check.kind, label: check.label, required: check.required, ok: outcome.ok, message: outcome.message };
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
return {
|
|
59
|
+
kind: check.kind,
|
|
60
|
+
label: check.label,
|
|
61
|
+
required: check.required,
|
|
62
|
+
ok: false,
|
|
63
|
+
message: `${check.label}: the check itself did not complete — ${error instanceof Error ? error.message : String(error)}`,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function withTimeout(promise, ms, label) {
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
const timer = setTimeout(() => reject(new Error(`no answer within ${String(ms)}ms while checking ${label}`)), ms);
|
|
70
|
+
promise.then((value) => {
|
|
71
|
+
clearTimeout(timer);
|
|
72
|
+
resolve(value);
|
|
73
|
+
}, (error) => {
|
|
74
|
+
clearTimeout(timer);
|
|
75
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
function tcpAnswers(host, port) {
|
|
80
|
+
return new Promise((resolve) => {
|
|
81
|
+
const socket = createConnection({ host, port });
|
|
82
|
+
socket.setTimeout(PREFLIGHT_CHECK_TIMEOUT_MS);
|
|
83
|
+
const finish = (ok) => {
|
|
84
|
+
socket.destroy();
|
|
85
|
+
resolve(ok);
|
|
86
|
+
};
|
|
87
|
+
socket.once("connect", () => finish(true));
|
|
88
|
+
socket.once("error", () => finish(false));
|
|
89
|
+
socket.once("timeout", () => finish(false));
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Boot precondition: nothing may already be listening at `host:port` before this run
|
|
94
|
+
* spawns its own process there — a stale listener from a dead session would otherwise
|
|
95
|
+
* be mistaken for this run's own (the same RT-024 hazard `readiness.ts` names). `ready`
|
|
96
|
+
* is `!answered` — the inverse polarity of `portAnswersCheck` below, for the attach case.
|
|
97
|
+
*/
|
|
98
|
+
export function portFreeCheck(host, port, label) {
|
|
99
|
+
const checkLabel = label ?? `port ${String(port)} on ${host} is free`;
|
|
100
|
+
return {
|
|
101
|
+
kind: "port",
|
|
102
|
+
label: checkLabel,
|
|
103
|
+
required: true,
|
|
104
|
+
probe: async () => {
|
|
105
|
+
const answered = await tcpAnswers(host, port);
|
|
106
|
+
return answered
|
|
107
|
+
? {
|
|
108
|
+
ok: false,
|
|
109
|
+
message: `Something is already listening on ${host}:${String(port)}. Stop it, or configure a different port, before starting this run.`,
|
|
110
|
+
}
|
|
111
|
+
: { ok: true, message: "ok" };
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Attach precondition: the caller is attaching to an app it expects to already be
|
|
117
|
+
* running, so something must already answer at `host:port`. `ready` is `answered` —
|
|
118
|
+
* the inverse polarity of `portFreeCheck` above, for the boot case.
|
|
119
|
+
*/
|
|
120
|
+
export function portAnswersCheck(host, port, label) {
|
|
121
|
+
const checkLabel = label ?? `${host}:${String(port)} answers`;
|
|
122
|
+
return {
|
|
123
|
+
kind: "port",
|
|
124
|
+
label: checkLabel,
|
|
125
|
+
required: true,
|
|
126
|
+
probe: async () => {
|
|
127
|
+
const answered = await tcpAnswers(host, port);
|
|
128
|
+
return answered ? { ok: true, message: "ok" } : { ok: false, message: `Nothing is listening on ${host}:${String(port)}. Is your app running?` };
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Wraps a run's own declared `ReadinessCheck`s as a single-shot preflight probe — one
|
|
134
|
+
* pass at `PREFLIGHT_CHECK_TIMEOUT_MS`, never the full poll loop `awaitReadiness` runs
|
|
135
|
+
* once the run actually starts. Reuses `awaitReadiness` itself rather than a second
|
|
136
|
+
* probing mechanism that could drift from what the real readiness wait checks.
|
|
137
|
+
*
|
|
138
|
+
* Required by default: a target this run declared and cannot reach is this run being
|
|
139
|
+
* broken, not a gap it can disclose and continue past. A caller may mark a secondary
|
|
140
|
+
* target `required: false` when the run can genuinely proceed without it.
|
|
141
|
+
*/
|
|
142
|
+
export function readinessTargetCheck(label, checks, options = {}) {
|
|
143
|
+
return {
|
|
144
|
+
kind: "readiness",
|
|
145
|
+
label,
|
|
146
|
+
required: options.required ?? true,
|
|
147
|
+
probe: async () => {
|
|
148
|
+
const result = await awaitReadiness(checks, { timeoutMs: PREFLIGHT_CHECK_TIMEOUT_MS, pollIntervalMs: 200 });
|
|
149
|
+
return result.ready ? { ok: true, message: "ok" } : { ok: false, message: result.reason ?? `${label} did not answer` };
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
export function databaseReachableCheck(host, port, label) {
|
|
154
|
+
const checkLabel = label ?? `database at ${host}:${String(port)} answers`;
|
|
155
|
+
return {
|
|
156
|
+
kind: "database",
|
|
157
|
+
label: checkLabel,
|
|
158
|
+
required: true,
|
|
159
|
+
probe: async () => {
|
|
160
|
+
const answered = await tcpAnswers(host, port);
|
|
161
|
+
return answered
|
|
162
|
+
? { ok: true, message: "ok" }
|
|
163
|
+
: { ok: false, message: `Nothing is listening on ${host}:${String(port)}. Is your database running?` };
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* The seam for anything this package must not name (rule 1's dependency-direction
|
|
169
|
+
* cousin: `@descryy/runtime-controller` has no browser, graph or database dependency,
|
|
170
|
+
* and must not gain one just to phrase a preflight check). A caller that already
|
|
171
|
+
* depends on the thing being checked — `@descryy/runtime-browser` for Chromium,
|
|
172
|
+
* `@descryy/mcp` for the graph — builds the probe and hands it in with this.
|
|
173
|
+
*/
|
|
174
|
+
export function customCheck(kind, label, required, probe) {
|
|
175
|
+
return { kind, label, required, probe };
|
|
176
|
+
}
|
|
177
|
+
//# sourceMappingURL=preflight.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preflight.js","sourceRoot":"","sources":["../src/preflight.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ProcessCollector`: turns `ProcessLifecycleEvent` (`controller.ts`) into
|
|
3
|
+
* `PROCESS_STARTED` / `PROCESS_READY` / `PROCESS_EXITED` Evidence. A plain translator
|
|
4
|
+
* with no lifecycle logic of its own — `ExecutionController` decides when each
|
|
5
|
+
* transition happens and calls `RunOptions.onProcessLifecycleEvent`; this collector
|
|
6
|
+
* just turns that call into one `context.emit()`.
|
|
7
|
+
*
|
|
8
|
+
* **Wiring order is load-bearing.** `start(context)` must run before
|
|
9
|
+
* `onProcessLifecycleEvent` can fire. `handleLifecycleEvent` throws rather than
|
|
10
|
+
* silently dropping an event on violation — a lost PROCESS_STARTED would otherwise
|
|
11
|
+
* read downstream as "never observed," a worse, quieter failure than a thrown error.
|
|
12
|
+
*/
|
|
13
|
+
import type { Collector } from "@descryy/runtime-contracts";
|
|
14
|
+
import type { ProcessLifecycleEvent } from "./controller.ts";
|
|
15
|
+
export interface ProcessCollector extends Collector {
|
|
16
|
+
/** Wire as `RunOptions.onProcessLifecycleEvent` — see the module doc for the ordering this depends on. */
|
|
17
|
+
handleLifecycleEvent(event: ProcessLifecycleEvent): void;
|
|
18
|
+
}
|
|
19
|
+
export declare function createProcessCollector(): ProcessCollector;
|
|
20
|
+
//# sourceMappingURL=process-collector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"process-collector.d.ts","sourceRoot":"","sources":["../src/process-collector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAmF,MAAM,4BAA4B,CAAC;AAC7I,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAG7D,MAAM,WAAW,gBAAiB,SAAQ,SAAS;IACjD,0GAA0G;IAC1G,oBAAoB,CAAC,KAAK,EAAE,qBAAqB,GAAG,IAAI,CAAC;CAC1D;AA6DD,wBAAgB,sBAAsB,IAAI,gBAAgB,CAoDzD"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ProcessCollector`: turns `ProcessLifecycleEvent` (`controller.ts`) into
|
|
3
|
+
* `PROCESS_STARTED` / `PROCESS_READY` / `PROCESS_EXITED` Evidence. A plain translator
|
|
4
|
+
* with no lifecycle logic of its own — `ExecutionController` decides when each
|
|
5
|
+
* transition happens and calls `RunOptions.onProcessLifecycleEvent`; this collector
|
|
6
|
+
* just turns that call into one `context.emit()`.
|
|
7
|
+
*
|
|
8
|
+
* **Wiring order is load-bearing.** `start(context)` must run before
|
|
9
|
+
* `onProcessLifecycleEvent` can fire. `handleLifecycleEvent` throws rather than
|
|
10
|
+
* silently dropping an event on violation — a lost PROCESS_STARTED would otherwise
|
|
11
|
+
* read downstream as "never observed," a worse, quieter failure than a thrown error.
|
|
12
|
+
*/
|
|
13
|
+
import { COLLECTOR_VERSION } from "./collector-version.js";
|
|
14
|
+
// None of `CollectorCapabilities`' fields is "process lifecycle observation" — the
|
|
15
|
+
// one thing this collector does. Reported `unavailable` on the others with an honest
|
|
16
|
+
// reason rather than inventing a field outside this lane's boundary (`contracts`).
|
|
17
|
+
// This collector doing its job while capabilities() shows all-unavailable is a
|
|
18
|
+
// disclosed gap in the capability model, not a bug here.
|
|
19
|
+
function computeCapabilities() {
|
|
20
|
+
const reason = "ProcessCollector observes process lifecycle (started/ready/exited) only — the capability model has no field for that yet.";
|
|
21
|
+
const status = { availability: "unavailable", reason };
|
|
22
|
+
return {
|
|
23
|
+
domObservation: status,
|
|
24
|
+
consoleObservation: status,
|
|
25
|
+
networkObservation: status,
|
|
26
|
+
backendLogAccess: status,
|
|
27
|
+
distributedTrace: status,
|
|
28
|
+
sourceMapping: status,
|
|
29
|
+
stackCapture: status,
|
|
30
|
+
processLifecycle: { availability: "available", reason: null },
|
|
31
|
+
databaseObservation: status,
|
|
32
|
+
externalServiceObservation: status,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function eventTypeOf(kind) {
|
|
36
|
+
switch (kind) {
|
|
37
|
+
case "process-started":
|
|
38
|
+
return "PROCESS_STARTED";
|
|
39
|
+
case "process-ready":
|
|
40
|
+
case "process-ready-failed":
|
|
41
|
+
return "PROCESS_READY";
|
|
42
|
+
case "process-exited":
|
|
43
|
+
return "PROCESS_EXITED";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// `process-ready-failed` still emits `PROCESS_READY` — no separate vocabulary type,
|
|
47
|
+
// and adding one here is the wrong layer. `payload.ready: false` plus the full
|
|
48
|
+
// `ReadinessResult` lets a reader distinguish success from failure via payload, not
|
|
49
|
+
// event type (same shape as `ServiceStartResult`'s `succeeded` + `stage`).
|
|
50
|
+
function payloadOf(event) {
|
|
51
|
+
switch (event.kind) {
|
|
52
|
+
case "process-started":
|
|
53
|
+
return { processId: event.handle.processId, serviceName: event.serviceName, pid: event.handle.pid, command: event.handle.command, startedAt: event.handle.startedAt };
|
|
54
|
+
case "process-ready":
|
|
55
|
+
case "process-ready-failed":
|
|
56
|
+
return { processId: event.handle.processId, serviceName: event.serviceName, ready: event.readiness.ready, readiness: event.readiness };
|
|
57
|
+
case "process-exited":
|
|
58
|
+
// `phase` distinguishes "crashed on boot" from "finished/stopped after
|
|
59
|
+
// running" — see `ProcessLifecycleEvent` in controller.ts.
|
|
60
|
+
return {
|
|
61
|
+
processId: event.handle.processId,
|
|
62
|
+
serviceName: event.serviceName,
|
|
63
|
+
exitCode: event.handle.exitCode,
|
|
64
|
+
signal: event.handle.signal,
|
|
65
|
+
exitedAt: event.handle.exitedAt,
|
|
66
|
+
phase: event.phase,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export function createProcessCollector() {
|
|
71
|
+
const capabilities = computeCapabilities();
|
|
72
|
+
let context = null;
|
|
73
|
+
return {
|
|
74
|
+
collectorId: "process-collector",
|
|
75
|
+
start(ctx) {
|
|
76
|
+
context = ctx;
|
|
77
|
+
return Promise.resolve({ available: true });
|
|
78
|
+
},
|
|
79
|
+
stop() {
|
|
80
|
+
// Idempotent per the Collector contract; nothing to release -- owns
|
|
81
|
+
// only a reference to the emit context, no process/socket/file handle.
|
|
82
|
+
context = null;
|
|
83
|
+
return Promise.resolve();
|
|
84
|
+
},
|
|
85
|
+
capabilities() {
|
|
86
|
+
return capabilities;
|
|
87
|
+
},
|
|
88
|
+
handleLifecycleEvent(event) {
|
|
89
|
+
if (context === null) {
|
|
90
|
+
throw new Error(`ProcessCollector.handleLifecycleEvent("${event.kind}") called before start() — ` +
|
|
91
|
+
`wire handleLifecycleEvent as RunOptions.onProcessLifecycleEvent only after start() has resolved, ` +
|
|
92
|
+
`or this event (and everything downstream of it) is silently unobserved rather than loudly wrong.`);
|
|
93
|
+
}
|
|
94
|
+
context.emit({
|
|
95
|
+
timestamp: new Date().toISOString(),
|
|
96
|
+
source: event.kind === "process-exited" ? "process-exit" : "backend-process",
|
|
97
|
+
service: event.serviceName,
|
|
98
|
+
process: event.handle.processId,
|
|
99
|
+
eventType: eventTypeOf(event.kind),
|
|
100
|
+
payload: payloadOf(event),
|
|
101
|
+
traceId: null,
|
|
102
|
+
requestId: null,
|
|
103
|
+
correlationId: null,
|
|
104
|
+
graphNodeId: null,
|
|
105
|
+
sourceLocation: null,
|
|
106
|
+
stackTrace: null,
|
|
107
|
+
// Direct observation of our own orchestration state, never inferred --
|
|
108
|
+
// same basis as LogCollector's verbatim-capture confidence of 1.
|
|
109
|
+
confidence: 1,
|
|
110
|
+
redactionStatus: "pending-redaction",
|
|
111
|
+
collectorVersion: COLLECTOR_VERSION,
|
|
112
|
+
});
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=process-collector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"process-collector.js","sourceRoot":"","sources":["../src/process-collector.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RT-024's named hole, closed for the two mechanisms that can carry it:
|
|
3
|
+
* "none of readiness's five mechanisms confirm the listener is THIS run's
|
|
4
|
+
* process -- a stale listener from a dead run satisfies http/tcp-port
|
|
5
|
+
* identically." Ephemeral-by-default (`allocateEphemeralPort`) closes the
|
|
6
|
+
* common case by construction; this closes the fixed-port case by
|
|
7
|
+
* detection, for real, via `/proc` -- not a heuristic guess.
|
|
8
|
+
*
|
|
9
|
+
* **Mechanism.** A TCP listening socket on Linux is a row in `/proc/net/tcp`
|
|
10
|
+
* (or `/proc/net/tcp6`) carrying an inode; a process's open file descriptors
|
|
11
|
+
* are symlinks under `/proc/<pid>/fd/*` named `socket:[<inode>]` when that fd
|
|
12
|
+
* is a socket. If the expected pid's fd table holds a socket whose inode
|
|
13
|
+
* matches a LISTEN row for the target port, that pid really does own the
|
|
14
|
+
* listener -- not a name-based guess, a kernel-backed fact.
|
|
15
|
+
*
|
|
16
|
+
* **Scope, disclosed rather than silently narrowed.** Linux only (`/proc`
|
|
17
|
+
* doesn't exist elsewhere) and only for a process in the same pid namespace
|
|
18
|
+
* as this one -- true for every spawn this repo produces today: a direct
|
|
19
|
+
* spawn, and a bwrap-wrapped one (bwrap execs into the target without
|
|
20
|
+
* `--unshare-pid`, so `child.pid` names the real process, not a wrapper).
|
|
21
|
+
* **Not true for `sandboxBackend: "container"`** -- a published Docker port
|
|
22
|
+
* is commonly owned by a host-side `docker-proxy` process, not the pid this
|
|
23
|
+
* module would be asked to verify, so a mismatch there would be a false
|
|
24
|
+
* positive. Callers must not invoke this for the container backend; See
|
|
25
|
+
* `controller.ts`'s call site, which guards on exactly that condition.
|
|
26
|
+
* Anything this function cannot determine returns `"unsupported"`, never a
|
|
27
|
+
* fabricated "verified" or a false "mismatch".
|
|
28
|
+
*
|
|
29
|
+
* **The spawned pid, or any of its descendants.** A launcher that forks the
|
|
30
|
+
* real server is the ordinary case: `sh -c "..."`, `npm run dev`, `dotnet run`
|
|
31
|
+
* (which builds and then runs the app as its child). The pid this run holds is
|
|
32
|
+
* the launcher's, and the listener belongs to a process under it — still this
|
|
33
|
+
* run's own process tree, which is exactly the property RT-024 asked for. The
|
|
34
|
+
* tree is walked from `/proc/<pid>/stat`'s parent-pid field, a kernel fact like
|
|
35
|
+
* the fd table, and a decoy outside that tree is refused exactly as before.
|
|
36
|
+
* Checking the spawned pid alone made every forking launcher time out at
|
|
37
|
+
* readiness (every .NET managed-lifecycle test, found at release integration).
|
|
38
|
+
*/
|
|
39
|
+
export type IdentityVerification = "verified" | "mismatch" | "unsupported";
|
|
40
|
+
/**
|
|
41
|
+
* Does `expectedPid` -- or a process descended from it -- own the socket
|
|
42
|
+
* listening on `port`, on this host, right now? A real kernel-state check, not
|
|
43
|
+
* a guess -- see module header for the mechanism and its disclosed scope.
|
|
44
|
+
*/
|
|
45
|
+
export declare function verifyListeningSocketOwner(port: number, expectedPid: number): IdentityVerification;
|
|
46
|
+
//# sourceMappingURL=process-identity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"process-identity.d.ts","sourceRoot":"","sources":["../src/process-identity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAIH,MAAM,MAAM,oBAAoB,GAAG,UAAU,GAAG,UAAU,GAAG,aAAa,CAAC;AAgH3E;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,oBAAoB,CAqBlG"}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RT-024's named hole, closed for the two mechanisms that can carry it:
|
|
3
|
+
* "none of readiness's five mechanisms confirm the listener is THIS run's
|
|
4
|
+
* process -- a stale listener from a dead run satisfies http/tcp-port
|
|
5
|
+
* identically." Ephemeral-by-default (`allocateEphemeralPort`) closes the
|
|
6
|
+
* common case by construction; this closes the fixed-port case by
|
|
7
|
+
* detection, for real, via `/proc` -- not a heuristic guess.
|
|
8
|
+
*
|
|
9
|
+
* **Mechanism.** A TCP listening socket on Linux is a row in `/proc/net/tcp`
|
|
10
|
+
* (or `/proc/net/tcp6`) carrying an inode; a process's open file descriptors
|
|
11
|
+
* are symlinks under `/proc/<pid>/fd/*` named `socket:[<inode>]` when that fd
|
|
12
|
+
* is a socket. If the expected pid's fd table holds a socket whose inode
|
|
13
|
+
* matches a LISTEN row for the target port, that pid really does own the
|
|
14
|
+
* listener -- not a name-based guess, a kernel-backed fact.
|
|
15
|
+
*
|
|
16
|
+
* **Scope, disclosed rather than silently narrowed.** Linux only (`/proc`
|
|
17
|
+
* doesn't exist elsewhere) and only for a process in the same pid namespace
|
|
18
|
+
* as this one -- true for every spawn this repo produces today: a direct
|
|
19
|
+
* spawn, and a bwrap-wrapped one (bwrap execs into the target without
|
|
20
|
+
* `--unshare-pid`, so `child.pid` names the real process, not a wrapper).
|
|
21
|
+
* **Not true for `sandboxBackend: "container"`** -- a published Docker port
|
|
22
|
+
* is commonly owned by a host-side `docker-proxy` process, not the pid this
|
|
23
|
+
* module would be asked to verify, so a mismatch there would be a false
|
|
24
|
+
* positive. Callers must not invoke this for the container backend; See
|
|
25
|
+
* `controller.ts`'s call site, which guards on exactly that condition.
|
|
26
|
+
* Anything this function cannot determine returns `"unsupported"`, never a
|
|
27
|
+
* fabricated "verified" or a false "mismatch".
|
|
28
|
+
*
|
|
29
|
+
* **The spawned pid, or any of its descendants.** A launcher that forks the
|
|
30
|
+
* real server is the ordinary case: `sh -c "..."`, `npm run dev`, `dotnet run`
|
|
31
|
+
* (which builds and then runs the app as its child). The pid this run holds is
|
|
32
|
+
* the launcher's, and the listener belongs to a process under it — still this
|
|
33
|
+
* run's own process tree, which is exactly the property RT-024 asked for. The
|
|
34
|
+
* tree is walked from `/proc/<pid>/stat`'s parent-pid field, a kernel fact like
|
|
35
|
+
* the fd table, and a decoy outside that tree is refused exactly as before.
|
|
36
|
+
* Checking the spawned pid alone made every forking launcher time out at
|
|
37
|
+
* readiness (every .NET managed-lifecycle test, found at release integration).
|
|
38
|
+
*/
|
|
39
|
+
import { readFileSync, readdirSync, readlinkSync } from "node:fs";
|
|
40
|
+
function hexPort(port) {
|
|
41
|
+
return port.toString(16).toUpperCase().padStart(4, "0");
|
|
42
|
+
}
|
|
43
|
+
/** Every inode `/proc/net/tcp{,6}` records as LISTENing on `port`, host-wide. Usually one; more than one only under `SO_REUSEPORT`. */
|
|
44
|
+
function listenInodesForPort(port) {
|
|
45
|
+
const inodes = new Set();
|
|
46
|
+
const targetPort = hexPort(port);
|
|
47
|
+
const TCP_LISTEN_STATE = "0A";
|
|
48
|
+
for (const path of ["/proc/net/tcp", "/proc/net/tcp6"]) {
|
|
49
|
+
let content;
|
|
50
|
+
try {
|
|
51
|
+
content = readFileSync(path, "utf8");
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Absent entirely (non-Linux) or unreadable -- the caller sees an empty
|
|
55
|
+
// set from both paths and reports "unsupported", never a false mismatch.
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
for (const line of content.split("\n").slice(1)) {
|
|
59
|
+
const fields = line.trim().split(/\s+/);
|
|
60
|
+
const localAddress = fields[1];
|
|
61
|
+
const state = fields[3];
|
|
62
|
+
const inode = fields[9];
|
|
63
|
+
if (localAddress === undefined || state === undefined || inode === undefined)
|
|
64
|
+
continue;
|
|
65
|
+
if (state !== TCP_LISTEN_STATE)
|
|
66
|
+
continue;
|
|
67
|
+
const portHex = localAddress.split(":")[1];
|
|
68
|
+
if (portHex === targetPort)
|
|
69
|
+
inodes.add(inode);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return inodes;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Does `expectedPid` own the socket listening on `port`, on this host, right
|
|
76
|
+
* now? A real kernel-state check, not a guess -- see module header for the
|
|
77
|
+
* mechanism and its disclosed scope.
|
|
78
|
+
*/
|
|
79
|
+
/** The parent pid recorded in `/proc/<pid>/stat`, or null if it cannot be read. The command name
|
|
80
|
+
* field is parenthesised and may itself contain spaces or parentheses, so the fields are read
|
|
81
|
+
* from after its LAST closing parenthesis. */
|
|
82
|
+
function parentPidOf(pid) {
|
|
83
|
+
let stat;
|
|
84
|
+
try {
|
|
85
|
+
stat = readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
const afterName = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
|
|
91
|
+
// afterName[0] is the state, afterName[1] the parent pid.
|
|
92
|
+
return afterName[1] ?? null;
|
|
93
|
+
}
|
|
94
|
+
/** `root` and every live process below it, from one pass over `/proc`'s parent-pid fields. */
|
|
95
|
+
function processTree(root) {
|
|
96
|
+
const childrenOf = new Map();
|
|
97
|
+
let entries;
|
|
98
|
+
try {
|
|
99
|
+
entries = readdirSync("/proc");
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return [String(root)];
|
|
103
|
+
}
|
|
104
|
+
for (const entry of entries) {
|
|
105
|
+
if (!/^\d+$/.test(entry))
|
|
106
|
+
continue;
|
|
107
|
+
const parent = parentPidOf(entry);
|
|
108
|
+
if (parent === null)
|
|
109
|
+
continue;
|
|
110
|
+
const siblings = childrenOf.get(parent);
|
|
111
|
+
if (siblings === undefined)
|
|
112
|
+
childrenOf.set(parent, [entry]);
|
|
113
|
+
else
|
|
114
|
+
siblings.push(entry);
|
|
115
|
+
}
|
|
116
|
+
const tree = [];
|
|
117
|
+
const pending = [String(root)];
|
|
118
|
+
const seen = new Set();
|
|
119
|
+
while (pending.length > 0) {
|
|
120
|
+
const pid = pending.pop();
|
|
121
|
+
if (seen.has(pid))
|
|
122
|
+
continue;
|
|
123
|
+
seen.add(pid);
|
|
124
|
+
tree.push(pid);
|
|
125
|
+
for (const child of childrenOf.get(pid) ?? [])
|
|
126
|
+
pending.push(child);
|
|
127
|
+
}
|
|
128
|
+
return tree;
|
|
129
|
+
}
|
|
130
|
+
/** Whether `pid`'s fd table holds one of `inodes`; `null` when that table cannot be read at all. */
|
|
131
|
+
function ownsOneOf(pid, inodes) {
|
|
132
|
+
let fds;
|
|
133
|
+
try {
|
|
134
|
+
fds = readdirSync(`/proc/${pid}/fd`);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
// Permission denied, or the pid is already gone -- cannot look inside its
|
|
138
|
+
// fd table, so no claim either way.
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
const SOCKET_FD_PATTERN = /^socket:\[(\d+)\]$/;
|
|
142
|
+
for (const fd of fds) {
|
|
143
|
+
let link;
|
|
144
|
+
try {
|
|
145
|
+
link = readlinkSync(`/proc/${pid}/fd/${fd}`);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// A fd can close between readdir and readlink -- not this pid's fault,
|
|
149
|
+
// just skip it and keep looking at the rest.
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const match = SOCKET_FD_PATTERN.exec(link);
|
|
153
|
+
if (match !== null && inodes.has(match[1]))
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Does `expectedPid` -- or a process descended from it -- own the socket
|
|
160
|
+
* listening on `port`, on this host, right now? A real kernel-state check, not
|
|
161
|
+
* a guess -- see module header for the mechanism and its disclosed scope.
|
|
162
|
+
*/
|
|
163
|
+
export function verifyListeningSocketOwner(port, expectedPid) {
|
|
164
|
+
const inodes = listenInodesForPort(port);
|
|
165
|
+
if (inodes.size === 0) {
|
|
166
|
+
// Either `/proc/net/tcp*` doesn't exist (non-Linux) or nothing is
|
|
167
|
+
// actually listening there yet -- either way this function cannot make
|
|
168
|
+
// a claim, so it declines rather than guessing.
|
|
169
|
+
return "unsupported";
|
|
170
|
+
}
|
|
171
|
+
// The spawned pid itself first: its fd table being unreadable is the one
|
|
172
|
+
// case that says nothing about ownership at all.
|
|
173
|
+
const own = ownsOneOf(String(expectedPid), inodes);
|
|
174
|
+
if (own === null)
|
|
175
|
+
return "unsupported";
|
|
176
|
+
if (own)
|
|
177
|
+
return "verified";
|
|
178
|
+
// Then its descendants. One that vanished or cannot be read is skipped --
|
|
179
|
+
// it cannot turn a real mismatch into a claim either way.
|
|
180
|
+
for (const pid of processTree(expectedPid).slice(1)) {
|
|
181
|
+
if (ownsOneOf(pid, inodes) === true)
|
|
182
|
+
return "verified";
|
|
183
|
+
}
|
|
184
|
+
return "mismatch";
|
|
185
|
+
}
|
|
186
|
+
//# sourceMappingURL=process-identity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"process-identity.js","sourceRoot":"","sources":["../src/process-identity.ts"],"names":[],"mappings":"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"}
|