@trusty-squire/mcp 1.1.13-rc.9 → 1.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/README.md +136 -64
  2. package/assets/login/vnc-input.js +9 -0
  3. package/assets/login/vnc.html +214 -202
  4. package/assets/screencaps/connect-walkthrough.png +0 -0
  5. package/assets/screencaps/connect-walkthrough.svg +1 -1
  6. package/dist/api-client.d.ts +26 -3
  7. package/dist/api-client.d.ts.map +1 -1
  8. package/dist/api-client.js +19 -31
  9. package/dist/api-client.js.map +1 -1
  10. package/dist/bin.js +2 -3
  11. package/dist/bin.js.map +1 -1
  12. package/dist/bot/browser.d.ts +85 -80
  13. package/dist/bot/browser.d.ts.map +1 -1
  14. package/dist/bot/browser.js +1314 -1570
  15. package/dist/bot/browser.js.map +1 -1
  16. package/dist/bot/compact-observation-v2.d.ts +167 -0
  17. package/dist/bot/compact-observation-v2.d.ts.map +1 -0
  18. package/dist/bot/compact-observation-v2.js +1038 -0
  19. package/dist/bot/compact-observation-v2.js.map +1 -0
  20. package/dist/bot/credential-shape.d.ts +2 -0
  21. package/dist/bot/credential-shape.d.ts.map +1 -1
  22. package/dist/bot/credential-shape.js +21 -8
  23. package/dist/bot/credential-shape.js.map +1 -1
  24. package/dist/bot/element-fingerprint.d.ts +9 -0
  25. package/dist/bot/element-fingerprint.d.ts.map +1 -0
  26. package/dist/bot/element-fingerprint.js +147 -0
  27. package/dist/bot/element-fingerprint.js.map +1 -0
  28. package/dist/bot/email-verification.d.ts +7 -1
  29. package/dist/bot/email-verification.d.ts.map +1 -1
  30. package/dist/bot/email-verification.js +48 -14
  31. package/dist/bot/email-verification.js.map +1 -1
  32. package/dist/bot/google-login.d.ts +28 -52
  33. package/dist/bot/google-login.d.ts.map +1 -1
  34. package/dist/bot/google-login.js +246 -1119
  35. package/dist/bot/google-login.js.map +1 -1
  36. package/dist/bot/install-completion.d.ts +1 -2
  37. package/dist/bot/install-completion.d.ts.map +1 -1
  38. package/dist/bot/install-completion.js +1 -2
  39. package/dist/bot/install-completion.js.map +1 -1
  40. package/dist/bot/login-state.d.ts +13 -7
  41. package/dist/bot/login-state.d.ts.map +1 -1
  42. package/dist/bot/login-state.js +50 -114
  43. package/dist/bot/login-state.js.map +1 -1
  44. package/dist/bot/oauth-providers.d.ts +1 -1
  45. package/dist/bot/oauth-providers.d.ts.map +1 -1
  46. package/dist/bot/oauth-providers.js +3 -3
  47. package/dist/bot/oauth-providers.js.map +1 -1
  48. package/dist/bot/oauth-scope.d.ts +4 -0
  49. package/dist/bot/oauth-scope.d.ts.map +1 -0
  50. package/dist/bot/oauth-scope.js +58 -0
  51. package/dist/bot/oauth-scope.js.map +1 -0
  52. package/dist/bot/operator-browser-watchdog.d.ts +18 -6
  53. package/dist/bot/operator-browser-watchdog.d.ts.map +1 -1
  54. package/dist/bot/operator-browser-watchdog.js +91 -37
  55. package/dist/bot/operator-browser-watchdog.js.map +1 -1
  56. package/dist/bot/owner-process-reaper-worker.d.ts +2 -0
  57. package/dist/bot/owner-process-reaper-worker.d.ts.map +1 -0
  58. package/dist/bot/owner-process-reaper-worker.js +17 -0
  59. package/dist/bot/owner-process-reaper-worker.js.map +1 -0
  60. package/dist/bot/owner-process-reaper.d.ts +103 -0
  61. package/dist/bot/owner-process-reaper.d.ts.map +1 -0
  62. package/dist/bot/owner-process-reaper.js +1058 -0
  63. package/dist/bot/owner-process-reaper.js.map +1 -0
  64. package/dist/bot/pay-operator.d.ts +7 -1
  65. package/dist/bot/pay-operator.d.ts.map +1 -1
  66. package/dist/bot/pay-operator.js +269 -103
  67. package/dist/bot/pay-operator.js.map +1 -1
  68. package/dist/bot/profile.d.ts +10 -1
  69. package/dist/bot/profile.d.ts.map +1 -1
  70. package/dist/bot/profile.js +137 -11
  71. package/dist/bot/profile.js.map +1 -1
  72. package/dist/bot/promote-to-skill.js +1 -1
  73. package/dist/bot/promote-to-skill.js.map +1 -1
  74. package/dist/bot/provision-session.d.ts +57 -186
  75. package/dist/bot/provision-session.d.ts.map +1 -1
  76. package/dist/bot/provision-session.js +1976 -1812
  77. package/dist/bot/provision-session.js.map +1 -1
  78. package/dist/bot/remote-login-display.d.ts +61 -0
  79. package/dist/bot/remote-login-display.d.ts.map +1 -0
  80. package/dist/bot/remote-login-display.js +765 -0
  81. package/dist/bot/remote-login-display.js.map +1 -0
  82. package/dist/bot/session/hosts.d.ts +8 -0
  83. package/dist/bot/session/hosts.d.ts.map +1 -0
  84. package/dist/bot/session/hosts.js +36 -0
  85. package/dist/bot/session/hosts.js.map +1 -0
  86. package/dist/bot/session/lifecycle.d.ts +56 -0
  87. package/dist/bot/session/lifecycle.d.ts.map +1 -0
  88. package/dist/bot/session/lifecycle.js +962 -0
  89. package/dist/bot/session/lifecycle.js.map +1 -0
  90. package/dist/bot/session/model.d.ts +175 -0
  91. package/dist/bot/session/model.d.ts.map +1 -0
  92. package/dist/bot/session/model.js +77 -0
  93. package/dist/bot/session/model.js.map +1 -0
  94. package/dist/install/cli.d.ts +29 -9
  95. package/dist/install/cli.d.ts.map +1 -1
  96. package/dist/install/cli.js +243 -322
  97. package/dist/install/cli.js.map +1 -1
  98. package/dist/install/interactive.d.ts +1 -0
  99. package/dist/install/interactive.d.ts.map +1 -1
  100. package/dist/install/interactive.js +19 -5
  101. package/dist/install/interactive.js.map +1 -1
  102. package/dist/install/ui.d.ts.map +1 -1
  103. package/dist/install/ui.js +3 -15
  104. package/dist/install/ui.js.map +1 -1
  105. package/dist/operator-env.js +1 -1
  106. package/dist/operator-env.js.map +1 -1
  107. package/dist/server-instance-registry.d.ts +103 -0
  108. package/dist/server-instance-registry.d.ts.map +1 -0
  109. package/dist/server-instance-registry.js +392 -0
  110. package/dist/server-instance-registry.js.map +1 -0
  111. package/dist/server.d.ts +15 -1
  112. package/dist/server.d.ts.map +1 -1
  113. package/dist/server.js +164 -62
  114. package/dist/server.js.map +1 -1
  115. package/dist/session-guard.d.ts +28 -0
  116. package/dist/session-guard.d.ts.map +1 -0
  117. package/dist/session-guard.js +84 -0
  118. package/dist/session-guard.js.map +1 -0
  119. package/dist/session.d.ts +57 -13
  120. package/dist/session.d.ts.map +1 -1
  121. package/dist/session.js +246 -127
  122. package/dist/session.js.map +1 -1
  123. package/dist/tools/audit-log.d.ts +16 -2
  124. package/dist/tools/audit-log.d.ts.map +1 -1
  125. package/dist/tools/audit-log.js +223 -13
  126. package/dist/tools/audit-log.js.map +1 -1
  127. package/dist/tools/audit-rollup.d.ts +80 -0
  128. package/dist/tools/audit-rollup.d.ts.map +1 -0
  129. package/dist/tools/audit-rollup.js +0 -0
  130. package/dist/tools/audit-rollup.js.map +1 -0
  131. package/dist/tools/fetch-credential.d.ts +37 -0
  132. package/dist/tools/fetch-credential.d.ts.map +1 -0
  133. package/dist/tools/fetch-credential.js +153 -0
  134. package/dist/tools/fetch-credential.js.map +1 -0
  135. package/dist/tools/index.d.ts +3 -1
  136. package/dist/tools/index.d.ts.map +1 -1
  137. package/dist/tools/index.js +7 -3
  138. package/dist/tools/index.js.map +1 -1
  139. package/dist/tools/operate-pay.d.ts +4 -4
  140. package/dist/tools/operate-pay.d.ts.map +1 -1
  141. package/dist/tools/operate-pay.js +151 -96
  142. package/dist/tools/operate-pay.js.map +1 -1
  143. package/dist/tools/provision-drive.d.ts +47 -39
  144. package/dist/tools/provision-drive.d.ts.map +1 -1
  145. package/dist/tools/provision-drive.js +268 -164
  146. package/dist/tools/provision-drive.js.map +1 -1
  147. package/dist/tools/use-credential.d.ts.map +1 -1
  148. package/dist/tools/use-credential.js +3 -2
  149. package/dist/tools/use-credential.js.map +1 -1
  150. package/package.json +4 -6
  151. package/dist/bot/operator-direct-identity.d.ts +0 -26
  152. package/dist/bot/operator-direct-identity.d.ts.map +0 -1
  153. package/dist/bot/operator-direct-identity.js +0 -120
  154. package/dist/bot/operator-direct-identity.js.map +0 -1
  155. package/dist/bot/operator-profile-pool.d.ts +0 -97
  156. package/dist/bot/operator-profile-pool.d.ts.map +0 -1
  157. package/dist/bot/operator-profile-pool.js +0 -855
  158. package/dist/bot/operator-profile-pool.js.map +0 -1
  159. package/dist/bot/xvfb.d.ts +0 -13
  160. package/dist/bot/xvfb.d.ts.map +0 -1
  161. package/dist/bot/xvfb.js +0 -146
  162. package/dist/bot/xvfb.js.map +0 -1
@@ -0,0 +1,1058 @@
1
+ import { spawn } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { createRequire } from "node:module";
4
+ import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync, } from "node:fs";
5
+ import { homedir } from "node:os";
6
+ import { join, resolve } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ import { operatorBrowserProcessCommandState, operatorBrowserProcessMarkerState, } from "./operator-browser-watchdog.js";
9
+ import { processBirthIdentity, processBirthIdentityState, processProfileArgumentState, profilePathIdentity, profileProcessIdentityState, signalProfileProcess, } from "./profile.js";
10
+ export const OWNER_HELPER_MARKER_ENV = "TRUSTY_SQUIRE_OWNER_HELPER_MARKER";
11
+ export const OWNER_REAPER_WORKER_MARKER_ENV = "TRUSTY_SQUIRE_OWNER_REAPER_WORKER_MARKER";
12
+ const DEFAULT_POLL_MS = 1_000;
13
+ const DEFAULT_TERM_GRACE_MS = 2_000;
14
+ const DEFAULT_WORKER_READY_TIMEOUT_MS = 2_000;
15
+ const DEFAULT_WORKER_STABILITY_MS = 100;
16
+ const WORKER_RECOVERY_DELAY_MS = 100;
17
+ const workerReadyWait = new Int32Array(new SharedArrayBuffer(4));
18
+ const moduleRequire = createRequire(import.meta.url);
19
+ let activeReaper = null;
20
+ function defaultRootDir() {
21
+ const configured = (process.env.TRUSTY_SQUIRE_REAPER_DIR ?? "").trim();
22
+ return configured.length > 0
23
+ ? resolve(configured)
24
+ : join(homedir(), ".trusty-squire", "owner-reapers");
25
+ }
26
+ function envPositiveMs(name, fallback) {
27
+ const parsed = Number(process.env[name]);
28
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
29
+ }
30
+ function ensurePrivateDir(path) {
31
+ mkdirSync(path, { recursive: true, mode: 0o700 });
32
+ chmodSync(path, 0o700);
33
+ }
34
+ function isOwnerIdentity(value) {
35
+ if (value === null || typeof value !== "object")
36
+ return false;
37
+ const owner = value;
38
+ return (Number.isSafeInteger(owner.pid) && (owner.pid ?? 0) > 0 && typeof owner.start_time === "string");
39
+ }
40
+ function isLaunch(value) {
41
+ if (value === null || typeof value !== "object")
42
+ return false;
43
+ const launch = value;
44
+ const allowedKeys = new Set(["marker", "user_data_dir", "anchor"]);
45
+ return (Object.keys(launch).every((key) => allowedKeys.has(key)) &&
46
+ typeof launch.marker === "string" &&
47
+ typeof launch.user_data_dir === "string" &&
48
+ (launch.anchor === undefined || isOwnerIdentity(launch.anchor)));
49
+ }
50
+ function isProfileProcessIdentity(value) {
51
+ if (value === null || typeof value !== "object")
52
+ return false;
53
+ const identity = value;
54
+ return (typeof identity.host === "string" &&
55
+ Number.isSafeInteger(identity.pid) &&
56
+ (identity.pid ?? 0) > 0 &&
57
+ typeof identity.start_time === "string" &&
58
+ typeof identity.user_data_dir === "string" &&
59
+ (identity.process_group_id === undefined ||
60
+ identity.process_group_id === "unknown" ||
61
+ Number.isSafeInteger(identity.process_group_id)) &&
62
+ (identity.process_marker === undefined || typeof identity.process_marker === "string"));
63
+ }
64
+ function isHelper(value) {
65
+ if (value === null || typeof value !== "object")
66
+ return false;
67
+ const helper = value;
68
+ if (helper.state === "pending") {
69
+ return (typeof helper.marker === "string" &&
70
+ helper.pid === undefined &&
71
+ helper.start_time === undefined &&
72
+ helper.process_group_id === undefined);
73
+ }
74
+ return (Number.isSafeInteger(helper.pid) &&
75
+ (helper.pid ?? 0) > 0 &&
76
+ typeof helper.start_time === "string" &&
77
+ typeof helper.marker === "string" &&
78
+ (helper.process_group_id === undefined || Number.isSafeInteger(helper.process_group_id)));
79
+ }
80
+ function readManifest(path) {
81
+ try {
82
+ const value = JSON.parse(readFileSync(path, "utf8"));
83
+ const allowedKeys = new Set(["version", "token", "owner", "resources", "launches", "helpers"]);
84
+ if (Object.keys(value).some((key) => !allowedKeys.has(key)) ||
85
+ value.version !== 5 ||
86
+ typeof value.token !== "string" ||
87
+ !isOwnerIdentity(value.owner) ||
88
+ !Array.isArray(value.resources) ||
89
+ !value.resources.every(isProfileProcessIdentity) ||
90
+ !Array.isArray(value.launches) ||
91
+ !value.launches.every(isLaunch) ||
92
+ !Array.isArray(value.helpers) ||
93
+ !value.helpers.every(isHelper)) {
94
+ return { state: "unknown" };
95
+ }
96
+ return { state: "present", manifest: value };
97
+ }
98
+ catch (error) {
99
+ return error.code === "ENOENT"
100
+ ? { state: "missing" }
101
+ : { state: "unknown" };
102
+ }
103
+ }
104
+ function writeManifest(path, manifest) {
105
+ const temporary = `${path}.tmp-${randomUUID()}`;
106
+ try {
107
+ writeFileSync(temporary, `${JSON.stringify(manifest)}\n`, { mode: 0o600, flag: "wx" });
108
+ renameSync(temporary, path);
109
+ }
110
+ finally {
111
+ rmSync(temporary, { force: true });
112
+ }
113
+ }
114
+ export function shouldReapOwner(state) {
115
+ return state === "stale";
116
+ }
117
+ function ownerState(manifest) {
118
+ const running = linuxProcessRunningState(manifest.owner.pid);
119
+ return running === "matching" ? processBirthIdentityState(manifest.owner) : running;
120
+ }
121
+ function linuxProcessGroupId(pid) {
122
+ try {
123
+ const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
124
+ const closeParen = stat.lastIndexOf(")");
125
+ if (closeParen < 0)
126
+ return "unknown";
127
+ const processGroupId = Number(stat
128
+ .slice(closeParen + 2)
129
+ .trim()
130
+ .split(/\s+/)[2]);
131
+ return Number.isSafeInteger(processGroupId) && processGroupId > 0 ? processGroupId : "unknown";
132
+ }
133
+ catch (error) {
134
+ const code = error.code;
135
+ return code === "ENOENT" || code === "ESRCH" ? "stale" : "unknown";
136
+ }
137
+ }
138
+ function linuxProcessUidState(pid) {
139
+ const ownerUid = typeof process.getuid === "function" ? process.getuid() : null;
140
+ if (ownerUid === null)
141
+ return "unknown";
142
+ try {
143
+ const match = /^Uid:\s+(.+)$/m.exec(readFileSync(`/proc/${pid}/status`, "utf8"));
144
+ if (match === null)
145
+ return "unknown";
146
+ const uids = match[1].trim().split(/\s+/).map(Number).filter(Number.isSafeInteger);
147
+ return uids.includes(ownerUid) ? "matching" : "stale";
148
+ }
149
+ catch (error) {
150
+ const code = error.code;
151
+ return code === "ENOENT" || code === "ESRCH" ? "stale" : "unknown";
152
+ }
153
+ }
154
+ function linuxProcessRunningState(pid) {
155
+ try {
156
+ const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
157
+ const close = stat.lastIndexOf(")");
158
+ if (close < 0)
159
+ return "unknown";
160
+ const state = stat.slice(close + 2).split(" ")[0];
161
+ return state === "Z" || state === "X" ? "stale" : "matching";
162
+ }
163
+ catch (error) {
164
+ const code = error.code;
165
+ return code === "ENOENT" || code === "ESRCH" ? "stale" : "unknown";
166
+ }
167
+ }
168
+ function helperProcessMarkerState(pid, marker, readEnvironment = (processId) => readFileSync(`/proc/${processId}/environ`, "utf8")) {
169
+ try {
170
+ return readEnvironment(pid).split("\0").includes(`${OWNER_HELPER_MARKER_ENV}=${marker}`)
171
+ ? "matching"
172
+ : "stale";
173
+ }
174
+ catch (error) {
175
+ const code = error.code;
176
+ return code === "ENOENT" || code === "ESRCH" ? "stale" : "unknown";
177
+ }
178
+ }
179
+ function processIds(readProcessIds) {
180
+ return (readProcessIds?.() ??
181
+ readdirSync("/proc")
182
+ .filter((entry) => /^\d+$/.test(entry))
183
+ .map(Number));
184
+ }
185
+ function helperGroupMarkerState(processGroupId, marker, readers = {}) {
186
+ if (process.platform !== "linux" && readers.readProcessIds === undefined)
187
+ return "stale";
188
+ let unknown = false;
189
+ try {
190
+ for (const pid of processIds(readers.readProcessIds)) {
191
+ const groupId = (readers.readProcessGroupId ?? linuxProcessGroupId)(pid);
192
+ if (groupId === "unknown") {
193
+ const running = readers.readRunningState?.(pid) ??
194
+ (readers.readProcessIds === undefined ? linuxProcessRunningState(pid) : "matching");
195
+ if (running === "stale")
196
+ continue;
197
+ const markerState = (readers.readMarkerState ?? helperProcessMarkerState)(pid, marker);
198
+ if (markerState === "matching" ||
199
+ (markerState === "unknown" &&
200
+ (readers.readUidState ?? linuxProcessUidState)(pid) !== "stale")) {
201
+ unknown = true;
202
+ }
203
+ continue;
204
+ }
205
+ if (groupId === "stale" || groupId !== processGroupId)
206
+ continue;
207
+ const running = readers.readRunningState?.(pid) ??
208
+ (readers.readProcessIds === undefined ? linuxProcessRunningState(pid) : "matching");
209
+ if (running === "stale")
210
+ continue;
211
+ if (running === "unknown") {
212
+ unknown = true;
213
+ continue;
214
+ }
215
+ const markerState = (readers.readMarkerState ?? helperProcessMarkerState)(pid, marker);
216
+ if (markerState === "matching")
217
+ return "matching";
218
+ if (markerState === "unknown")
219
+ unknown = true;
220
+ }
221
+ }
222
+ catch {
223
+ return "unknown";
224
+ }
225
+ return unknown ? "unknown" : "stale";
226
+ }
227
+ function exactHelperMarkerProcessScan(marker, readers = {}) {
228
+ if (process.platform !== "linux" && readers.readProcessIds === undefined) {
229
+ return { matching: [], unknown: false };
230
+ }
231
+ const matching = [];
232
+ let unknown = false;
233
+ try {
234
+ for (const pid of processIds(readers.readProcessIds)) {
235
+ const running = readers.readRunningState?.(pid) ??
236
+ (readers.readProcessIds === undefined ? linuxProcessRunningState(pid) : "matching");
237
+ if (running === "stale")
238
+ continue;
239
+ if (running === "unknown") {
240
+ if ((readers.readUidState ?? linuxProcessUidState)(pid) !== "stale")
241
+ unknown = true;
242
+ continue;
243
+ }
244
+ const markerState = (readers.readMarkerState ?? helperProcessMarkerState)(pid, marker);
245
+ if (markerState === "matching")
246
+ matching.push(pid);
247
+ else if (markerState === "unknown" &&
248
+ (readers.readUidState ?? linuxProcessUidState)(pid) !== "stale") {
249
+ unknown = true;
250
+ }
251
+ }
252
+ }
253
+ catch {
254
+ unknown = true;
255
+ }
256
+ return { matching, unknown };
257
+ }
258
+ export function ownerHelperIdentityState(identity, readers = {}) {
259
+ if ("state" in identity) {
260
+ const scan = exactHelperMarkerProcessScan(identity.marker, readers);
261
+ if (scan.matching.length > 0)
262
+ return "matching";
263
+ return scan.unknown ? "unknown" : "stale";
264
+ }
265
+ if (identity.process_group_id !== undefined) {
266
+ const groupState = helperGroupMarkerState(identity.process_group_id, identity.marker, readers);
267
+ if (groupState !== "stale")
268
+ return groupState;
269
+ }
270
+ const running = readers.readRunningState?.(identity.pid) ??
271
+ (readers.readBirthState === undefined ? linuxProcessRunningState(identity.pid) : "matching");
272
+ if (running !== "matching")
273
+ return running;
274
+ const birth = readers.readBirthState?.(identity) ?? processBirthIdentityState(identity);
275
+ if (birth !== "matching")
276
+ return birth;
277
+ return (readers.readMarkerState ?? helperProcessMarkerState)(identity.pid, identity.marker);
278
+ }
279
+ function signalOwnerHelper(identity, signal) {
280
+ if (process.platform !== "linux")
281
+ return false;
282
+ if ("state" in identity) {
283
+ let signalled = false;
284
+ const groups = new Set();
285
+ for (const pid of exactHelperMarkerProcessScan(identity.marker).matching) {
286
+ const groupId = linuxProcessGroupId(pid);
287
+ if (typeof groupId === "number" &&
288
+ helperGroupMarkerState(groupId, identity.marker) === "matching") {
289
+ groups.add(groupId);
290
+ }
291
+ }
292
+ for (const groupId of groups) {
293
+ try {
294
+ process.kill(-groupId, signal);
295
+ signalled = true;
296
+ }
297
+ catch { }
298
+ }
299
+ return signalled;
300
+ }
301
+ const groupId = identity.process_group_id;
302
+ const target = groupId !== undefined && helperGroupMarkerState(groupId, identity.marker) === "matching"
303
+ ? -groupId
304
+ : ownerHelperIdentityState(identity) === "matching"
305
+ ? identity.pid
306
+ : null;
307
+ if (target === null)
308
+ return false;
309
+ try {
310
+ process.kill(target, signal);
311
+ return true;
312
+ }
313
+ catch {
314
+ return false;
315
+ }
316
+ }
317
+ function browserMarkerState(pid, readers) {
318
+ if (readers.readMarkerState !== undefined)
319
+ return readers.readMarkerState(pid);
320
+ if (readers.readMarker !== undefined) {
321
+ const marker = readers.readMarker(pid);
322
+ return marker === null ? { state: "missing" } : { state: "present", marker };
323
+ }
324
+ return operatorBrowserProcessMarkerState(pid);
325
+ }
326
+ function launchAnchorTrustState(launch, readers) {
327
+ if (launch.anchor === undefined)
328
+ return "pending";
329
+ const runningState = (readers.readRunningState ?? linuxProcessRunningState)(launch.anchor.pid);
330
+ if (runningState === "unknown")
331
+ return "unknown";
332
+ if (runningState === "stale")
333
+ return "trusted";
334
+ const birthState = readers.readBirthState?.(launch.anchor) ?? processBirthIdentityState(launch.anchor);
335
+ if (birthState === "unknown")
336
+ return "unknown";
337
+ if (birthState === "stale")
338
+ return "trusted";
339
+ const commandState = (readers.readCommandState ?? operatorBrowserProcessCommandState)(launch.anchor.pid);
340
+ const markerState = browserMarkerState(launch.anchor.pid, readers);
341
+ const profileState = (readers.readProfileState ?? processProfileArgumentState)(launch.anchor.pid, launch.user_data_dir);
342
+ // The anchor is a birth identity (pid + start_time) recorded at this launch,
343
+ // so we are already looking at THIS launch's process. Chrome overwrites its
344
+ // own /proc/<pid>/environ memory when it rewrites process titles, which erases
345
+ // the owner marker there — so a live chromium anchor is NEVER re-readable by
346
+ // marker even though it is genuinely ours. Its per-launch --user-data-dir is a
347
+ // unique ownership token that survives, so a matching chromium command plus a
348
+ // matching profile is definitive. A marker that IS still readable must not
349
+ // positively name a different launch; a missing/erased marker does not.
350
+ const markerContradicts = markerState.state === "present" && markerState.marker !== launch.marker;
351
+ return commandState === "matching" && profileState === "matching" && !markerContradicts
352
+ ? "trusted"
353
+ : "unknown";
354
+ }
355
+ function exactMarkerProcessScan(launch, readers = {}) {
356
+ if (process.platform !== "linux" && readers.readProcessIds === undefined) {
357
+ return { matching: [], unknown: false };
358
+ }
359
+ const matching = [];
360
+ const missingProfile = [];
361
+ const anchorTrust = launchAnchorTrustState(launch, readers);
362
+ let discoveredAnchor;
363
+ let unknown = anchorTrust === "unknown";
364
+ try {
365
+ for (const pid of processIds(readers.readProcessIds)) {
366
+ const commandState = (readers.readCommandState ?? operatorBrowserProcessCommandState)(pid);
367
+ if (commandState === "stale")
368
+ continue;
369
+ const markerState = browserMarkerState(pid, readers);
370
+ if (markerState.state === "unknown") {
371
+ // Only a CONFIRMED chromium process (commandState "matching") whose
372
+ // marker we transiently cannot read is conservatively treated as
373
+ // maybe-ours. A process we cannot even confirm is chromium — e.g. a
374
+ // hardened own-uid process such as sshd, whose /proc/<pid>/exe and
375
+ // environ are both EACCES, so commandState reads "unknown" — cannot be
376
+ // our marked browser: our own login browser launches unhardened under
377
+ // our uid with a readable environ. Poisoning the scan on such a process
378
+ // made every login-browser teardown report closure unproven.
379
+ if (commandState === "matching" &&
380
+ (readers.readUidState ?? linuxProcessUidState)(pid) !== "stale") {
381
+ unknown = true;
382
+ }
383
+ continue;
384
+ }
385
+ if (markerState.state !== "present" || markerState.marker !== launch.marker)
386
+ continue;
387
+ if (commandState !== "matching") {
388
+ unknown = true;
389
+ continue;
390
+ }
391
+ const profileState = (readers.readProfileState ?? processProfileArgumentState)(pid, launch.user_data_dir);
392
+ if (profileState !== "matching" && profileState !== "missing") {
393
+ unknown = true;
394
+ continue;
395
+ }
396
+ const identity = (readers.readBirthIdentity ?? processBirthIdentity)(pid);
397
+ if (identity === null) {
398
+ if ((readers.readRunningState ?? linuxProcessRunningState)(pid) !== "stale")
399
+ unknown = true;
400
+ continue;
401
+ }
402
+ const birthState = readers.readBirthState?.(identity) ?? processBirthIdentityState(identity);
403
+ if (birthState !== "matching") {
404
+ if (birthState === "unknown")
405
+ unknown = true;
406
+ continue;
407
+ }
408
+ if (profileState === "matching") {
409
+ matching.push(identity);
410
+ discoveredAnchor ??= identity;
411
+ }
412
+ else {
413
+ missingProfile.push(identity);
414
+ }
415
+ }
416
+ }
417
+ catch {
418
+ unknown = true;
419
+ }
420
+ const missingProfileTrusted = anchorTrust === "trusted" || (anchorTrust === "pending" && discoveredAnchor !== undefined);
421
+ if (missingProfileTrusted)
422
+ matching.push(...missingProfile);
423
+ else if (missingProfile.length > 0)
424
+ unknown = true;
425
+ return {
426
+ matching,
427
+ unknown,
428
+ ...(launch.anchor === undefined && discoveredAnchor !== undefined
429
+ ? { anchor: discoveredAnchor }
430
+ : {}),
431
+ };
432
+ }
433
+ function exactMarkerIdentityMatches(launch, identity, readers = {}) {
434
+ const scan = exactMarkerProcessScan(launch, {
435
+ ...readers,
436
+ readProcessIds: () => [identity.pid],
437
+ });
438
+ return scan.matching.some((current) => current.pid === identity.pid && current.start_time === identity.start_time);
439
+ }
440
+ export function ownerBrowserLaunchState(marker, profileDir, readers = {}) {
441
+ const launch = {
442
+ marker,
443
+ user_data_dir: profilePathIdentity(profileDir),
444
+ ...(readers.anchor === undefined ? {} : { anchor: readers.anchor }),
445
+ };
446
+ const scan = exactMarkerProcessScan(launch, readers);
447
+ if (scan.matching.length > 0)
448
+ return "matching";
449
+ return scan.unknown ? "unknown" : "stale";
450
+ }
451
+ export async function terminateOwnerBrowserLaunch(marker, profileDir, options = {}) {
452
+ const anchor = options.anchor ?? activeReaper?.launchAnchor(marker, profileDir) ?? undefined;
453
+ const readers = {
454
+ ...(anchor === undefined ? {} : { anchor }),
455
+ ...(options.readProcessIds === undefined ? {} : { readProcessIds: options.readProcessIds }),
456
+ ...(options.readBirthIdentity === undefined
457
+ ? {}
458
+ : { readBirthIdentity: options.readBirthIdentity }),
459
+ ...(options.readMarkerState === undefined ? {} : { readMarkerState: options.readMarkerState }),
460
+ ...(options.readCommandState === undefined
461
+ ? {}
462
+ : { readCommandState: options.readCommandState }),
463
+ ...(options.readProfileState === undefined
464
+ ? {}
465
+ : { readProfileState: options.readProfileState }),
466
+ ...(options.readBirthState === undefined ? {} : { readBirthState: options.readBirthState }),
467
+ ...(options.readRunningState === undefined
468
+ ? {}
469
+ : { readRunningState: options.readRunningState }),
470
+ ...(options.readUidState === undefined ? {} : { readUidState: options.readUidState }),
471
+ };
472
+ const kill = options.kill ?? ((pid, signal) => process.kill(pid, signal));
473
+ const wait = options.wait ??
474
+ (async (ms) => await new Promise((resolveWait) => setTimeout(resolveWait, ms)));
475
+ const graceMs = options.graceMs ?? envPositiveMs("TRUSTY_SQUIRE_REAPER_TERM_GRACE_MS", DEFAULT_TERM_GRACE_MS);
476
+ const launch = {
477
+ marker,
478
+ user_data_dir: profilePathIdentity(profileDir),
479
+ ...(anchor === undefined ? {} : { anchor }),
480
+ };
481
+ const scan = () => {
482
+ const result = exactMarkerProcessScan(launch, readers);
483
+ if (launch.anchor === undefined && result.anchor !== undefined)
484
+ launch.anchor = result.anchor;
485
+ return result;
486
+ };
487
+ const matching = () => scan().matching;
488
+ const signal = (identities, signalName) => {
489
+ for (const identity of identities) {
490
+ if (!exactMarkerIdentityMatches(launch, identity, readers))
491
+ continue;
492
+ try {
493
+ kill(identity.pid, signalName);
494
+ }
495
+ catch { }
496
+ }
497
+ };
498
+ const initial = matching();
499
+ if (initial.length === 0) {
500
+ return scan().unknown === false;
501
+ }
502
+ signal(initial, "SIGTERM");
503
+ await wait(graceMs);
504
+ const resistant = matching();
505
+ if (resistant.length === 0) {
506
+ return scan().unknown === false;
507
+ }
508
+ signal(resistant, "SIGKILL");
509
+ await wait(graceMs);
510
+ return matching().length === 0 && scan().unknown === false;
511
+ }
512
+ function signalTrackedResources(manifest, signal) {
513
+ let signalled = 0;
514
+ for (const identity of manifest.resources) {
515
+ if (signalProfileProcess(identity, identity.user_data_dir, signal))
516
+ signalled += 1;
517
+ }
518
+ for (const launch of manifest.launches) {
519
+ const scan = exactMarkerProcessScan(launch);
520
+ if (launch.anchor === undefined && scan.anchor !== undefined)
521
+ launch.anchor = scan.anchor;
522
+ for (const identity of scan.matching) {
523
+ if (!exactMarkerIdentityMatches(launch, identity))
524
+ continue;
525
+ try {
526
+ process.kill(identity.pid, signal);
527
+ signalled += 1;
528
+ }
529
+ catch { }
530
+ }
531
+ }
532
+ for (const helper of manifest.helpers) {
533
+ if (signalOwnerHelper(helper, signal))
534
+ signalled += 1;
535
+ }
536
+ return signalled;
537
+ }
538
+ function manifestCleanupComplete(manifest) {
539
+ return (manifest.resources.every((identity) => profileProcessIdentityState(identity, identity.user_data_dir) === "stale") &&
540
+ manifest.launches.every((launch) => {
541
+ const scan = exactMarkerProcessScan(launch);
542
+ return scan.matching.length === 0 && !scan.unknown;
543
+ }) &&
544
+ manifest.helpers.every((helper) => ownerHelperIdentityState(helper) === "stale"));
545
+ }
546
+ async function reapManifest(path, manifest) {
547
+ const graceMs = envPositiveMs("TRUSTY_SQUIRE_REAPER_TERM_GRACE_MS", DEFAULT_TERM_GRACE_MS);
548
+ const signalled = signalTrackedResources(manifest, "SIGTERM");
549
+ await new Promise((resolveWait) => setTimeout(resolveWait, graceMs));
550
+ const latestRead = readManifest(path);
551
+ const latest = latestRead.state === "present" ? latestRead.manifest : manifest;
552
+ for (const launch of latest.launches) {
553
+ if (launch.anchor !== undefined)
554
+ continue;
555
+ const prior = manifest.launches.find((candidate) => candidate.marker === launch.marker && candidate.user_data_dir === launch.user_data_dir);
556
+ if (prior?.anchor !== undefined)
557
+ launch.anchor = prior.anchor;
558
+ }
559
+ signalTrackedResources(latest, "SIGKILL");
560
+ await new Promise((resolveWait) => setTimeout(resolveWait, graceMs));
561
+ if (latestRead.state === "present" && manifestCleanupComplete(latest)) {
562
+ rmSync(path, { force: true });
563
+ }
564
+ return signalled;
565
+ }
566
+ export async function sweepOrphanedOwnerProcesses(rootDir = defaultRootDir()) {
567
+ await new Promise((resolveStart) => setImmediate(resolveStart));
568
+ if (process.platform !== "linux" || !existsSync(rootDir))
569
+ return 0;
570
+ let reaped = 0;
571
+ for (const entry of readdirSync(rootDir)) {
572
+ if (!entry.endsWith(".json"))
573
+ continue;
574
+ const path = join(rootDir, entry);
575
+ const read = readManifest(path);
576
+ if (read.state !== "present" || !shouldReapOwner(ownerState(read.manifest)))
577
+ continue;
578
+ reaped += await reapManifest(path, read.manifest);
579
+ }
580
+ return reaped;
581
+ }
582
+ export async function runOwnerProcessReaperWorker(manifestPath, onReady = () => undefined) {
583
+ const pollMs = envPositiveMs("TRUSTY_SQUIRE_REAPER_POLL_MS", DEFAULT_POLL_MS);
584
+ let ready = false;
585
+ for (;;) {
586
+ const read = readManifest(manifestPath);
587
+ if (read.state === "missing")
588
+ return;
589
+ if (read.state === "unknown") {
590
+ await new Promise((resolveWait) => setTimeout(resolveWait, pollMs));
591
+ continue;
592
+ }
593
+ if (!ready) {
594
+ onReady();
595
+ ready = true;
596
+ }
597
+ if (shouldReapOwner(ownerState(read.manifest))) {
598
+ await reapManifest(manifestPath, read.manifest);
599
+ if (readManifest(manifestPath).state === "missing")
600
+ return;
601
+ }
602
+ await new Promise((resolveWait) => setTimeout(resolveWait, pollMs));
603
+ }
604
+ }
605
+ function ownerReaperWorkerRunning(identity) {
606
+ if (processBirthIdentityState(identity) !== "matching")
607
+ return false;
608
+ return linuxProcessRunningState(identity.pid) === "matching";
609
+ }
610
+ function waitForOwnerReaperWorker(worker, readyPath, token) {
611
+ if (worker.pid === undefined)
612
+ return null;
613
+ const timeoutMs = envPositiveMs("TRUSTY_SQUIRE_REAPER_READY_TIMEOUT_MS", DEFAULT_WORKER_READY_TIMEOUT_MS);
614
+ const deadline = Date.now() + timeoutMs;
615
+ while (Date.now() < deadline) {
616
+ try {
617
+ const ready = JSON.parse(readFileSync(readyPath, "utf8"));
618
+ if (ready.version === 1 && ready.token === token && ready.pid === worker.pid) {
619
+ const identity = processBirthIdentity(worker.pid);
620
+ if (identity === null)
621
+ return null;
622
+ Atomics.wait(workerReadyWait, 0, 0, DEFAULT_WORKER_STABILITY_MS);
623
+ return ownerReaperWorkerRunning(identity) ? identity : null;
624
+ }
625
+ }
626
+ catch { }
627
+ Atomics.wait(workerReadyWait, 0, 0, 5);
628
+ }
629
+ return null;
630
+ }
631
+ function terminateOwnerHelperAfterRegistrationFailure(child, record) {
632
+ const graceMs = envPositiveMs("TRUSTY_SQUIRE_REAPER_TERM_GRACE_MS", DEFAULT_TERM_GRACE_MS);
633
+ signalOwnerHelper(record, "SIGTERM");
634
+ Atomics.wait(workerReadyWait, 0, 0, graceMs);
635
+ if (ownerHelperIdentityState(record) !== "stale") {
636
+ signalOwnerHelper(record, "SIGKILL");
637
+ Atomics.wait(workerReadyWait, 0, 0, graceMs);
638
+ }
639
+ return ownerTrackedHelperState(child) === "stale" || ownerHelperIdentityState(record) === "stale";
640
+ }
641
+ const trackedHelperProcesses = new WeakMap();
642
+ function launchOwnerReaperWorker(workerPath, manifestPath, rootDir, token, spawnWorker, onUnexpectedExit) {
643
+ const workerExecArgs = workerPath.endsWith(".ts")
644
+ ? ["--import", moduleRequire.resolve("tsx")]
645
+ : [];
646
+ const readyPath = join(rootDir, `${process.pid}-${token}-${randomUUID()}.ready`);
647
+ const workerMarker = `v1:reaper:${token}:${randomUUID()}`;
648
+ let worker;
649
+ try {
650
+ worker = spawnWorker(process.execPath, [...workerExecArgs, workerPath, manifestPath, readyPath, token], {
651
+ detached: true,
652
+ stdio: "ignore",
653
+ env: {
654
+ ...process.env,
655
+ [OWNER_HELPER_MARKER_ENV]: workerMarker,
656
+ [OWNER_REAPER_WORKER_MARKER_ENV]: workerMarker,
657
+ },
658
+ });
659
+ worker.unref();
660
+ }
661
+ catch {
662
+ rmSync(readyPath, { force: true });
663
+ return null;
664
+ }
665
+ let workerRecord = { marker: workerMarker, state: "pending" };
666
+ if (worker.pid !== undefined) {
667
+ const birth = processBirthIdentity(worker.pid);
668
+ const processGroupId = linuxProcessGroupId(worker.pid);
669
+ if (birth !== null && typeof processGroupId === "number") {
670
+ workerRecord = { ...birth, marker: workerMarker, process_group_id: processGroupId };
671
+ }
672
+ }
673
+ trackedHelperProcesses.set(worker, workerRecord);
674
+ let exited = false;
675
+ let ready = false;
676
+ let stopping = false;
677
+ const markExited = () => {
678
+ if (exited)
679
+ return;
680
+ exited = true;
681
+ if (ready && !stopping)
682
+ onUnexpectedExit();
683
+ };
684
+ worker.once("error", markExited);
685
+ worker.once("exit", markExited);
686
+ const identity = waitForOwnerReaperWorker(worker, readyPath, token);
687
+ rmSync(readyPath, { force: true });
688
+ if (identity === null || exited) {
689
+ stopping = true;
690
+ if (!terminateOwnerHelperAfterRegistrationFailure(worker, workerRecord)) {
691
+ throw new Error("owner process reaper worker group did not terminate");
692
+ }
693
+ trackedHelperProcesses.delete(worker);
694
+ return null;
695
+ }
696
+ ready = true;
697
+ return {
698
+ isAvailable: () => !exited && ownerReaperWorkerRunning(identity),
699
+ stop: () => {
700
+ stopping = true;
701
+ exited = true;
702
+ const terminated = terminateOwnerHelperAfterRegistrationFailure(worker, workerRecord);
703
+ if (terminated)
704
+ trackedHelperProcesses.delete(worker);
705
+ return terminated;
706
+ },
707
+ };
708
+ }
709
+ export function startOwnerProcessReaper(options = {}, runtime = {}) {
710
+ if (process.platform !== "linux")
711
+ return null;
712
+ if (activeReaper !== null)
713
+ return activeReaper.restart() ? activeReaper : null;
714
+ const rootDir = resolve(options.rootDir ?? defaultRootDir());
715
+ ensurePrivateDir(rootDir);
716
+ const startupSweep = setImmediate(() => {
717
+ void sweepOrphanedOwnerProcesses(rootDir).catch(() => undefined);
718
+ });
719
+ startupSweep.unref();
720
+ const owner = processBirthIdentity(process.pid);
721
+ if (owner === null)
722
+ return null;
723
+ const token = randomUUID();
724
+ const manifestPath = join(rootDir, `${owner.pid}-${token}.json`);
725
+ let manifest = {
726
+ version: 5,
727
+ token,
728
+ owner,
729
+ resources: [],
730
+ launches: [],
731
+ helpers: [],
732
+ };
733
+ writeManifest(manifestPath, manifest);
734
+ const compiledWorkerPath = fileURLToPath(new URL("./owner-process-reaper-worker.js", import.meta.url));
735
+ const sourceWorkerPath = fileURLToPath(new URL("./owner-process-reaper-worker.ts", import.meta.url));
736
+ const workerPath = options.workerPath ?? (existsSync(compiledWorkerPath) ? compiledWorkerPath : sourceWorkerPath);
737
+ if (!existsSync(workerPath)) {
738
+ rmSync(manifestPath, { force: true });
739
+ return null;
740
+ }
741
+ const spawnWorker = runtime.spawn ?? spawn;
742
+ let stopped = false;
743
+ let workerGeneration = 0;
744
+ let worker = null;
745
+ let recoveryTimer = null;
746
+ const clearRecoveryTimer = () => {
747
+ if (recoveryTimer === null)
748
+ return;
749
+ clearTimeout(recoveryTimer);
750
+ recoveryTimer = null;
751
+ };
752
+ const scheduleRecovery = () => {
753
+ if (stopped || recoveryTimer !== null)
754
+ return;
755
+ recoveryTimer = setTimeout(() => {
756
+ recoveryTimer = null;
757
+ if (stopped || worker?.isAvailable())
758
+ return;
759
+ worker = null;
760
+ try {
761
+ launchWorker();
762
+ }
763
+ catch { }
764
+ if (worker === null)
765
+ scheduleRecovery();
766
+ }, WORKER_RECOVERY_DELAY_MS);
767
+ recoveryTimer.unref();
768
+ };
769
+ const launchWorker = () => {
770
+ runtime.beforeWorkerLaunch?.();
771
+ const generation = ++workerGeneration;
772
+ const launched = launchOwnerReaperWorker(workerPath, manifestPath, rootDir, token, spawnWorker, () => {
773
+ if (generation !== workerGeneration)
774
+ return;
775
+ worker = null;
776
+ scheduleRecovery();
777
+ });
778
+ if (launched !== null)
779
+ worker = launched;
780
+ return launched;
781
+ };
782
+ const ensureWorker = () => {
783
+ if (stopped)
784
+ return false;
785
+ if (worker?.isAvailable())
786
+ return true;
787
+ clearRecoveryTimer();
788
+ worker?.stop();
789
+ worker = null;
790
+ try {
791
+ return launchWorker() !== null;
792
+ }
793
+ catch {
794
+ scheduleRecovery();
795
+ return false;
796
+ }
797
+ };
798
+ if (!ensureWorker()) {
799
+ stopped = true;
800
+ clearRecoveryTimer();
801
+ rmSync(manifestPath, { force: true });
802
+ return null;
803
+ }
804
+ const requireAvailable = () => {
805
+ if (!ensureWorker())
806
+ throw new Error("owner process reaper worker unavailable");
807
+ };
808
+ const update = (next) => {
809
+ manifest = next;
810
+ writeManifest(manifestPath, manifest);
811
+ };
812
+ const reaper = {
813
+ manifestPath,
814
+ token,
815
+ isAvailable: () => !stopped && worker?.isAvailable() === true,
816
+ restart: ensureWorker,
817
+ track: (identity) => {
818
+ requireAvailable();
819
+ update({
820
+ ...manifest,
821
+ resources: [...manifest.resources.filter((entry) => entry.pid !== identity.pid), identity],
822
+ });
823
+ },
824
+ untrack: (identity) => {
825
+ if (stopped)
826
+ return;
827
+ update({
828
+ ...manifest,
829
+ resources: manifest.resources.filter((entry) => entry.pid !== identity.pid || entry.start_time !== identity.start_time),
830
+ });
831
+ },
832
+ trackLaunch: (marker, profileDir) => {
833
+ requireAvailable();
834
+ update({
835
+ ...manifest,
836
+ launches: [
837
+ ...manifest.launches.filter((entry) => entry.marker !== marker),
838
+ { marker, user_data_dir: profilePathIdentity(profileDir) },
839
+ ],
840
+ });
841
+ },
842
+ bindLaunch: (marker, identity) => {
843
+ requireAvailable();
844
+ const launch = manifest.launches.find((entry) => entry.marker === marker);
845
+ // Ownership is proven by the birth-identity + unique per-launch profile
846
+ // (profileProcessIdentityState "matching"), a random per-launch
847
+ // --user-data-dir no other process shares. The environ marker is only a
848
+ // non-contradiction check here: Chrome erases its own marker from
849
+ // /proc/<pid>/environ when it rewrites process titles, so requiring the
850
+ // marker to still be readable back from the browser process left the
851
+ // anchor permanently unbound and every login-browser teardown reporting
852
+ // closure unproven. A marker that IS readable must not name a different
853
+ // launch; an erased marker does not disqualify.
854
+ const processMarker = operatorBrowserProcessMarkerState(identity.pid);
855
+ if (launch === undefined ||
856
+ profilePathIdentity(identity.user_data_dir) !== launch.user_data_dir ||
857
+ profileProcessIdentityState(identity, launch.user_data_dir) !== "matching" ||
858
+ (processMarker.state === "present" && processMarker.marker !== marker)) {
859
+ return false;
860
+ }
861
+ update({
862
+ ...manifest,
863
+ launches: manifest.launches.map((entry) => entry.marker === marker
864
+ ? { ...entry, anchor: { pid: identity.pid, start_time: identity.start_time } }
865
+ : entry),
866
+ });
867
+ return true;
868
+ },
869
+ launchAnchor: (marker, profileDir) => {
870
+ const normalizedProfile = profilePathIdentity(profileDir);
871
+ return (manifest.launches.find((entry) => entry.marker === marker && entry.user_data_dir === normalizedProfile)?.anchor ?? null);
872
+ },
873
+ untrackLaunch: (marker) => {
874
+ if (stopped)
875
+ return;
876
+ update({
877
+ ...manifest,
878
+ launches: manifest.launches.filter((entry) => entry.marker !== marker),
879
+ });
880
+ },
881
+ reserveHelper: (marker) => {
882
+ requireAvailable();
883
+ update({
884
+ ...manifest,
885
+ helpers: [
886
+ ...manifest.helpers.filter((entry) => entry.marker !== marker),
887
+ { marker, state: "pending" },
888
+ ],
889
+ });
890
+ },
891
+ bindHelper: (marker, identity) => {
892
+ requireAvailable();
893
+ if (!manifest.helpers.some((entry) => entry.marker === marker))
894
+ return;
895
+ update({
896
+ ...manifest,
897
+ helpers: [...manifest.helpers.filter((entry) => entry.marker !== marker), identity],
898
+ });
899
+ },
900
+ untrackHelper: (marker) => {
901
+ if (stopped)
902
+ return;
903
+ update({
904
+ ...manifest,
905
+ helpers: manifest.helpers.filter((entry) => entry.marker !== marker),
906
+ });
907
+ },
908
+ stop: () => {
909
+ if (stopped)
910
+ return;
911
+ stopped = true;
912
+ clearRecoveryTimer();
913
+ const stoppedWorker = worker?.stop() ?? true;
914
+ worker = null;
915
+ if (stoppedWorker)
916
+ rmSync(manifestPath, { force: true });
917
+ if (activeReaper === reaper)
918
+ activeReaper = null;
919
+ },
920
+ };
921
+ activeReaper = reaper;
922
+ return reaper;
923
+ }
924
+ export function ensureOwnerProcessReaper() {
925
+ if (activeReaper !== null)
926
+ return activeReaper.restart() ? activeReaper : null;
927
+ return startOwnerProcessReaper();
928
+ }
929
+ export function stopOwnerProcessReaper() {
930
+ activeReaper?.stop();
931
+ }
932
+ export function trackOwnerProcess(identity) {
933
+ ensureOwnerProcessReaper()?.track(identity);
934
+ }
935
+ export function untrackOwnerProcess(identity) {
936
+ activeReaper?.untrack(identity);
937
+ }
938
+ export function trackOwnerBrowserLaunch(marker, profileDir, runtime = {}) {
939
+ const reaper = (runtime.ensureReaper ?? ensureOwnerProcessReaper)();
940
+ if (process.platform === "linux" && reaper === null) {
941
+ throw new Error("owner process reaper unavailable for local browser launch");
942
+ }
943
+ reaper?.trackLaunch(marker, profileDir);
944
+ }
945
+ export function bindOwnerBrowserLaunch(marker, identity) {
946
+ if (process.platform !== "linux")
947
+ return true;
948
+ return activeReaper?.bindLaunch(marker, identity) === true;
949
+ }
950
+ // Launch markers remain tracked until exact-marker cleanup completes. Linux has
951
+ // one custodian: the owner reaper. This compatibility hook intentionally does
952
+ // not arm a second process watchdog.
953
+ export function markOwnerBrowserLaunchTerminal(_marker) { }
954
+ export function untrackOwnerBrowserLaunch(marker) {
955
+ activeReaper?.untrackLaunch(marker);
956
+ }
957
+ export function reconcileOwnerBrowserLaunchAfterLeaderExit(marker, profileDir, launchState = ownerBrowserLaunchState) {
958
+ if (launchState(marker, profileDir) === "stale")
959
+ untrackOwnerBrowserLaunch(marker);
960
+ }
961
+ export function spawnOwnerTrackedHelper(command, args, options = {}, runtime = {}) {
962
+ const reaper = ensureOwnerProcessReaper();
963
+ if (process.platform === "linux" && reaper === null) {
964
+ throw new Error("owner process reaper unavailable for session helper");
965
+ }
966
+ const marker = `v1:${randomUUID()}`;
967
+ reaper?.reserveHelper(marker);
968
+ let child;
969
+ try {
970
+ child = (runtime.spawn ?? spawn)(command, [...args], {
971
+ ...options,
972
+ detached: process.platform === "linux" ? true : options.detached,
973
+ env: {
974
+ ...(options.env ?? process.env),
975
+ [OWNER_HELPER_MARKER_ENV]: marker,
976
+ },
977
+ });
978
+ }
979
+ catch (error) {
980
+ reaper?.untrackHelper(marker);
981
+ throw error;
982
+ }
983
+ if (process.platform === "linux") {
984
+ let record = { marker, state: "pending" };
985
+ if (child.pid !== undefined) {
986
+ const birth = processBirthIdentity(child.pid);
987
+ const processGroupId = linuxProcessGroupId(child.pid);
988
+ if (birth !== null && typeof processGroupId === "number") {
989
+ record = { ...birth, marker, process_group_id: processGroupId };
990
+ }
991
+ }
992
+ trackedHelperProcesses.set(child, record);
993
+ const releaseIfStale = () => {
994
+ setTimeout(() => {
995
+ if (trackedHelperProcesses.get(child) !== record)
996
+ return;
997
+ if (ownerHelperIdentityState(record) !== "stale")
998
+ return;
999
+ activeReaper?.untrackHelper(marker);
1000
+ trackedHelperProcesses.delete(child);
1001
+ }, 0).unref();
1002
+ };
1003
+ child.once("error", releaseIfStale);
1004
+ child.once("exit", releaseIfStale);
1005
+ if (!("state" in record)) {
1006
+ try {
1007
+ reaper?.bindHelper(marker, record);
1008
+ }
1009
+ catch (error) {
1010
+ if (terminateOwnerHelperAfterRegistrationFailure(child, record)) {
1011
+ try {
1012
+ reaper?.untrackHelper(marker);
1013
+ trackedHelperProcesses.delete(child);
1014
+ }
1015
+ catch { }
1016
+ }
1017
+ throw error;
1018
+ }
1019
+ }
1020
+ }
1021
+ return child;
1022
+ }
1023
+ export function signalOwnerTrackedHelper(child, signal) {
1024
+ const identity = trackedHelperProcesses.get(child);
1025
+ if (identity !== undefined)
1026
+ return signalOwnerHelper(identity, signal);
1027
+ try {
1028
+ return child.kill(signal);
1029
+ }
1030
+ catch {
1031
+ return false;
1032
+ }
1033
+ }
1034
+ export function ownerTrackedHelperState(child) {
1035
+ const identity = trackedHelperProcesses.get(child);
1036
+ if (identity !== undefined)
1037
+ return ownerHelperIdentityState(identity);
1038
+ return child.exitCode !== null || child.signalCode !== null ? "stale" : "matching";
1039
+ }
1040
+ export async function waitForOwnerTrackedHelperExit(child, timeoutMs) {
1041
+ const deadline = Date.now() + timeoutMs;
1042
+ while (ownerTrackedHelperState(child) !== "stale" && Date.now() < deadline) {
1043
+ await new Promise((resolveWait) => {
1044
+ const timer = setTimeout(resolveWait, Math.min(25, Math.max(1, deadline - Date.now())));
1045
+ timer.unref();
1046
+ });
1047
+ }
1048
+ return ownerTrackedHelperState(child) === "stale";
1049
+ }
1050
+ export function releaseOwnerTrackedHelper(child) {
1051
+ const record = trackedHelperProcesses.get(child);
1052
+ if (record === undefined || ownerHelperIdentityState(record) !== "stale")
1053
+ return false;
1054
+ activeReaper?.untrackHelper(record.marker);
1055
+ trackedHelperProcesses.delete(child);
1056
+ return true;
1057
+ }
1058
+ //# sourceMappingURL=owner-process-reaper.js.map