rea-agents 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/README.md +56 -9
  2. package/dist/application/ApplicationWorkflowEvidenceResolver.js +162 -0
  3. package/dist/application/BinarySession.js +9 -2
  4. package/dist/application/BinarySessionStatus.js +39 -1
  5. package/dist/application/CapabilityInventory.js +15 -0
  6. package/dist/application/CommandShimReplay.js +6 -3
  7. package/dist/application/CompositeProvider.js +5 -2
  8. package/dist/application/DirectAnalysis.js +1 -20
  9. package/dist/application/DirectAnalysisStatus.js +83 -0
  10. package/dist/application/Doctor.js +34 -25
  11. package/dist/application/DoctorDiagnostics.js +1 -4
  12. package/dist/application/DoctorProjection.js +43 -0
  13. package/dist/application/DoctorScope.js +102 -0
  14. package/dist/application/EvidenceReferenceResolver.js +26 -0
  15. package/dist/application/JavaScriptApplicationEvidence.js +1 -1
  16. package/dist/application/JavaScriptApplicationEvidenceGraph.js +10 -1
  17. package/dist/application/JavaScriptApplicationService.js +3 -2
  18. package/dist/application/JavaScriptApplicationWorkflowEvidence.js +26 -0
  19. package/dist/application/JavaScriptApplicationWorkflowService.js +49 -1
  20. package/dist/application/JavaScriptArtifactReconstruction.js +7 -0
  21. package/dist/application/JavaScriptModuleRelationships.js +25 -1
  22. package/dist/application/JavaScriptReturnShapeProjection.js +111 -0
  23. package/dist/application/JavaScriptSemanticGraphBuilder.js +294 -0
  24. package/dist/application/JavaScriptSemanticGraphConstruction.js +144 -0
  25. package/dist/application/JavaScriptSemanticGraphEvidence.js +86 -0
  26. package/dist/application/JavaScriptSemanticGraphFlowProjection.js +72 -0
  27. package/dist/application/JavaScriptSemanticGraphProjection.js +33 -0
  28. package/dist/application/JavaScriptSemanticTraceService.js +45 -0
  29. package/dist/application/LoopbackReplay.js +277 -133
  30. package/dist/application/LoopbackReplayRecorder.js +101 -0
  31. package/dist/application/PermissionAuthority.js +7 -3
  32. package/dist/application/ProcessCaptureJournal.js +95 -0
  33. package/dist/application/ProcessCaptureLifecycle.js +24 -13
  34. package/dist/application/ProcessCheckpoints.js +30 -4
  35. package/dist/application/ProcessCli.js +26 -4
  36. package/dist/application/ProcessHarness.js +20 -50
  37. package/dist/application/ProcessNormalization.js +3 -2
  38. package/dist/application/ProcessReactiveCoordinator.js +218 -0
  39. package/dist/application/ProcessReactiveEffects.js +133 -0
  40. package/dist/application/ProcessReactiveObservations.js +20 -0
  41. package/dist/application/ProcessSampling.js +2 -0
  42. package/dist/application/SessionProviderRouter.js +7 -6
  43. package/dist/application/Setup.js +11 -10
  44. package/dist/application/SetupPlan.js +1 -1
  45. package/dist/application/SetupRegistrationEnvironment.js +12 -0
  46. package/dist/application/TerminalRenderer.js +3 -1
  47. package/dist/browser/CdpEndpoint.js +37 -4
  48. package/dist/catalogIdentity.js +16 -7
  49. package/dist/cli/coreAnalysisCommands.js +34 -2
  50. package/dist/cli/javascriptApplicationAnalysis.js +19 -0
  51. package/dist/cli/setupCommands.js +81 -32
  52. package/dist/cli/utilityCommands.js +10 -2
  53. package/dist/cliApplicationCommands.js +79 -16
  54. package/dist/cliCommandNames.js +3 -0
  55. package/dist/cliElectronCommands.js +23 -33
  56. package/dist/cliObservationOptions.js +1 -1
  57. package/dist/cliOutput.js +56 -0
  58. package/dist/cliProcessCommands.js +32 -1
  59. package/dist/cliSetup.js +30 -21
  60. package/dist/contracts/applicationToolContracts.js +54 -7
  61. package/dist/contracts/applicationWorkflowInputContracts.js +119 -0
  62. package/dist/contracts/electronToolContracts.js +53 -32
  63. package/dist/contracts/javascriptApplicationWorkflowExamples.js +32 -9
  64. package/dist/contracts/processCaptureExample.js +1 -0
  65. package/dist/contracts/replayMachineExample.js +37 -0
  66. package/dist/contracts/toolContractExamples.js +2 -0
  67. package/dist/contracts/toolContracts.js +5 -1
  68. package/dist/contracts/toolEffects.js +3 -0
  69. package/dist/contracts/toolOutputSchemaGroups.js +2 -0
  70. package/dist/contracts/toolOutputSchemaPrimitives.js +40 -0
  71. package/dist/doctorRuntime.js +41 -0
  72. package/dist/domain/analysisErrorPresentation.js +5 -3
  73. package/dist/domain/analysisErrorProjection.js +7 -1
  74. package/dist/domain/changedBehavior.js +5 -4
  75. package/dist/domain/completionLedgerGeneration.js +190 -0
  76. package/dist/domain/errors.js +4 -2
  77. package/dist/domain/evidenceCompletionLedger.js +113 -0
  78. package/dist/domain/hopperStartupFailure.js +9 -0
  79. package/dist/domain/javascriptApplicationAnalysis.js +44 -2
  80. package/dist/domain/javascriptExportShapeComparison.js +145 -0
  81. package/dist/domain/javascriptExportShapeComparisonIdentity.js +11 -0
  82. package/dist/domain/javascriptExportShapeComparisonSchemas.js +277 -0
  83. package/dist/domain/javascriptExportShapeSelection.js +132 -0
  84. package/dist/domain/javascriptExportShapeVariants.js +272 -0
  85. package/dist/domain/javascriptRuntimeReconciliationParsing.js +2 -2
  86. package/dist/domain/javascriptSemanticAnalysis.js +17 -36
  87. package/dist/domain/javascriptSemanticCallResolution.js +129 -0
  88. package/dist/domain/javascriptSemanticCalls.js +269 -0
  89. package/dist/domain/javascriptSemanticGraph.js +356 -0
  90. package/dist/domain/javascriptSemanticGraphSchemas.js +329 -0
  91. package/dist/domain/javascriptSemanticGraphSerialization.js +23 -0
  92. package/dist/domain/javascriptSemanticIr.js +14 -1
  93. package/dist/domain/javascriptSemanticPrimitives.js +36 -0
  94. package/dist/domain/javascriptSemanticProjection.js +36 -4
  95. package/dist/domain/javascriptSemanticProvenance.js +29 -0
  96. package/dist/domain/javascriptSemanticQuery.js +294 -0
  97. package/dist/domain/javascriptSemanticQueryAssessment.js +74 -0
  98. package/dist/domain/javascriptSemanticQueryIdentity.js +86 -0
  99. package/dist/domain/javascriptSemanticQueryRelations.js +15 -0
  100. package/dist/domain/javascriptSemanticQuerySchemas.js +188 -0
  101. package/dist/domain/javascriptSemanticReturns.js +187 -0
  102. package/dist/domain/javascriptSemanticState.js +32 -0
  103. package/dist/domain/javascriptSemanticTraceSchemas.js +14 -0
  104. package/dist/domain/javascriptSemanticValues.js +54 -70
  105. package/dist/domain/processCapture.js +36 -0
  106. package/dist/domain/processCaptureValidation.js +101 -12
  107. package/dist/domain/processComparison.js +78 -25
  108. package/dist/domain/processObservation.js +153 -0
  109. package/dist/domain/processReactiveCheckpointDataflow.js +32 -0
  110. package/dist/domain/processReactiveMatching.js +210 -0
  111. package/dist/domain/processReactiveRuntime.js +109 -0
  112. package/dist/domain/processReactiveScenario.js +361 -0
  113. package/dist/domain/processReactiveScenarioPreflight.js +120 -0
  114. package/dist/domain/processReactiveTransition.js +159 -0
  115. package/dist/domain/processScenario.js +12 -0
  116. package/dist/domain/processTraceComparison.js +43 -0
  117. package/dist/domain/processTraceDimensionProjection.js +50 -0
  118. package/dist/domain/processTraceEvaluation.js +312 -0
  119. package/dist/domain/processTraceSpecification.js +373 -0
  120. package/dist/domain/reconstructionVerification.js +5 -4
  121. package/dist/domain/replayMachine.js +18 -2
  122. package/dist/domain/replayMachineRun.js +216 -0
  123. package/dist/domain/replayMachineRuntime.js +88 -9
  124. package/dist/domain/staticRuntimeCorrelation.js +4 -2
  125. package/dist/evaluation/CodexAgentEval.js +24 -0
  126. package/dist/generatedPackageMetadata.js +1 -1
  127. package/dist/ghidra/GhidraClient.js +13 -2
  128. package/dist/ghidra/GhidraDiagnostics.js +1 -0
  129. package/dist/ghidra/GhidraLauncher.js +7 -3
  130. package/dist/ghidra/GhidraProvider.js +8 -1
  131. package/dist/hopper/BridgeLauncher.js +56 -14
  132. package/dist/hopper/HopperClient.js +23 -10
  133. package/dist/hopper/HopperDiagnostics.js +14 -0
  134. package/dist/hopper/HopperProcessDiagnostic.js +14 -0
  135. package/dist/hopper/HopperProvider.js +8 -1
  136. package/dist/hopper/LinuxPrivateDisplayDiagnostic.js +72 -0
  137. package/dist/hopper/LinuxPrivateDisplayProbe.js +217 -0
  138. package/dist/main/transport.js +2 -0
  139. package/dist/mcpDoctor.js +310 -0
  140. package/dist/process/ProcessOwnership.js +200 -53
  141. package/dist/process/ProviderRunLineage.js +21 -0
  142. package/dist/server/createServer.js +4 -1
  143. package/dist/server/javascriptApplicationResult.js +22 -1
  144. package/dist/server/registerApplicationTools/characterization.js +3 -3
  145. package/dist/server/registerApplicationTools/compareExportShapes.js +37 -0
  146. package/dist/server/registerApplicationTools/compareVersions.js +12 -6
  147. package/dist/server/registerApplicationTools/controlledReplay.js +2 -2
  148. package/dist/server/registerApplicationTools/coverage.js +3 -3
  149. package/dist/server/registerApplicationTools/helpers.js +37 -25
  150. package/dist/server/registerApplicationTools/traceFeature.js +9 -5
  151. package/dist/server/registerApplicationTools/traceSemantics.js +30 -0
  152. package/dist/server/registerApplicationTools.js +4 -0
  153. package/dist/server/registerEvidenceResources.js +5 -1
  154. package/dist/server/registerProcessComparisonTool.js +17 -7
  155. package/dist/server/registerReplayMachineTool.js +20 -0
  156. package/dist/server/registerSessionTools.js +12 -7
  157. package/dist/server/sessionAvailabilityPolicy.js +2 -1
  158. package/dist/serverIdentity.js +19 -0
  159. package/package.json +62 -30
  160. package/scripts/hopper-demo-x11.py +459 -61
  161. package/scripts/prepack.mjs +55 -0
  162. package/scripts/prepare.mjs +28 -0
  163. package/scripts/rea.mjs +26 -5
  164. package/skills/reverse-engineer-anything/SKILL.md +2 -2
  165. package/skills/reverse-engineer-anything/references/javascript-applications.md +10 -0
@@ -0,0 +1,310 @@
1
+ import { Client } from "@modelcontextprotocol/client";
2
+ import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
3
+ import { Formatter } from "incur";
4
+ import { resolve } from "node:path";
5
+ import { CATALOG_IDENTITY, MCP_RESOURCE_CATALOG, MCP_RESOURCE_TEMPLATE_CATALOG, } from "./catalogIdentity.js";
6
+ import { PROMPT_CONTRACTS } from "./contracts/promptContracts.js";
7
+ import { PRODUCT_IDENTITY } from "./identity.js";
8
+ const DEFAULT_DEADLINE_MS = 20_000;
9
+ const STDERR_LIMIT_BYTES = 64 * 1_024;
10
+ const OUTPUT_FORMATS = ["toon", "json", "yaml", "md", "jsonl"];
11
+ /** Diagnose the actual production stdio adapter with one bounded SDK session. */
12
+ export const runProductionMcpDoctor = async (options) => {
13
+ const deadline = Date.now() + (options.deadlineMs ?? DEFAULT_DEADLINE_MS);
14
+ const controller = new AbortController();
15
+ const timer = setTimeout(() => controller.abort(new Error("Production MCP doctor deadline expired")), Math.max(1, deadline - Date.now()));
16
+ const stderr = boundedStderr();
17
+ const transport = new TrackedStdioClientTransport({
18
+ command: options.command,
19
+ args: [...options.args],
20
+ cwd: options.cwd,
21
+ env: targetFreeEnvironment(options.environment),
22
+ stderr: "pipe",
23
+ });
24
+ transport.stderr?.on("data", stderr.append);
25
+ const client = new Client({ name: "rea-mcp-doctor", version: "1" });
26
+ try {
27
+ await client.connect(transport, requestOptions(deadline, controller.signal));
28
+ return {
29
+ ...(await inspectProductionMcpSession(client, deadline, controller.signal)),
30
+ diagnostics: stderr.value(),
31
+ };
32
+ }
33
+ catch (cause) {
34
+ return {
35
+ healthy: false,
36
+ adapter: "production-stdio",
37
+ server: null,
38
+ inventory: null,
39
+ identity: null,
40
+ request_flow: { tool: "binary_session", ok: false },
41
+ checks: [
42
+ {
43
+ name: "transport",
44
+ ok: false,
45
+ detail: errorMessage(cause),
46
+ },
47
+ ],
48
+ diagnostics: stderr.value(),
49
+ };
50
+ }
51
+ finally {
52
+ clearTimeout(timer);
53
+ await closeMcpChild(client, transport);
54
+ }
55
+ };
56
+ const inspectProductionMcpSession = async (client, deadline, signal) => {
57
+ const request = requestOptions(deadline, signal);
58
+ const [tools, prompts, resources, templates, identity, requestFlow] = await Promise.all([
59
+ client.listTools(undefined, request),
60
+ client.listPrompts(undefined, request),
61
+ client.listResources(undefined, request),
62
+ client.listResourceTemplates(undefined, request),
63
+ client.readResource({ uri: "rea://server/identity" }, request),
64
+ client.callTool({ name: "binary_session", arguments: {} }, request),
65
+ ]);
66
+ const inventory = {
67
+ tools: compareInventory(CATALOG_IDENTITY.tools.map(({ name }) => name), tools.tools.map(({ name }) => name)),
68
+ prompts: compareInventory(PROMPT_CONTRACTS.map(({ name }) => name), prompts.prompts.map(({ name }) => name)),
69
+ resources: compareInventory(MCP_RESOURCE_CATALOG.map(({ name }) => name), resources.resources.map(({ name }) => name)),
70
+ resource_templates: compareInventory(MCP_RESOURCE_TEMPLATE_CATALOG.map(({ name }) => name), templates.resourceTemplates.map(({ name }) => name)),
71
+ };
72
+ const observedIdentity = parseIdentityResource(identity.contents);
73
+ const serverVersion = client.getServerVersion();
74
+ const protocolVersion = client.getNegotiatedProtocolVersion();
75
+ const checks = [
76
+ {
77
+ name: "initialize",
78
+ ok: serverVersion?.name === PRODUCT_IDENTITY.mcpServerKey &&
79
+ serverVersion.version === PRODUCT_IDENTITY.packageVersion &&
80
+ protocolVersion !== undefined,
81
+ detail: `${serverVersion?.name ?? "unknown"}@${serverVersion?.version ?? "unknown"}; protocol=${protocolVersion ?? "unknown"}`,
82
+ },
83
+ ...Object.entries(inventory).map(([name, comparison]) => inventoryCheck(name, comparison)),
84
+ {
85
+ name: "server-identity",
86
+ ok: observedIdentity.packageVersion === PRODUCT_IDENTITY.packageVersion &&
87
+ observedIdentity.catalogDigest ===
88
+ CATALOG_IDENTITY.digests.combined_sha256,
89
+ detail: `package=${observedIdentity.packageVersion ?? "missing"}; catalog=${observedIdentity.catalogDigest ?? "missing"}`,
90
+ },
91
+ {
92
+ name: "request-flow",
93
+ ok: requestFlow.isError !== true,
94
+ detail: "binary_session target-free request",
95
+ },
96
+ ];
97
+ return {
98
+ healthy: checks.every(({ ok }) => ok),
99
+ adapter: "production-stdio",
100
+ server: {
101
+ name: serverVersion?.name ?? null,
102
+ version: serverVersion?.version ?? null,
103
+ protocol_version: protocolVersion ?? null,
104
+ },
105
+ inventory,
106
+ identity: {
107
+ expected_package_version: PRODUCT_IDENTITY.packageVersion,
108
+ observed_package_version: observedIdentity.packageVersion,
109
+ expected_catalog_digest: CATALOG_IDENTITY.digests.combined_sha256,
110
+ observed_catalog_digest: observedIdentity.catalogDigest,
111
+ },
112
+ request_flow: {
113
+ tool: "binary_session",
114
+ ok: requestFlow.isError !== true,
115
+ },
116
+ checks,
117
+ };
118
+ };
119
+ /**
120
+ * Dispatcher entry for `rea mcp doctor`, kept separate from Incur MCP setup.
121
+ * @public
122
+ */
123
+ export const runProductionMcpDoctorCli = async (arguments_, input) => {
124
+ const parsed = parseOutputArguments(arguments_);
125
+ if (!parsed.ok)
126
+ return {
127
+ output: `${Formatter.format({ code: "VALIDATION_ERROR", message: parsed.message }, parsed.format)}\n`,
128
+ exitCode: 1,
129
+ };
130
+ const result = await runProductionMcpDoctor({
131
+ command: process.execPath,
132
+ args: [resolve(input.dispatcherPath), "mcp"],
133
+ cwd: input.packageRoot,
134
+ environment: process.env,
135
+ });
136
+ return {
137
+ output: `${Formatter.format(result, parsed.format)}\n`,
138
+ exitCode: result.healthy ? 0 : 1,
139
+ };
140
+ };
141
+ /** Compare exact names while retaining actionable mismatch locations. */
142
+ export const compareInventory = (expectedNames, observedNames) => {
143
+ const expected = new Set(expectedNames);
144
+ const observed = new Set(observedNames);
145
+ return {
146
+ expected: expectedNames.length,
147
+ observed: observedNames.length,
148
+ missing: [...expected].filter((name) => !observed.has(name)).sort(),
149
+ unexpected: [...observed].filter((name) => !expected.has(name)).sort(),
150
+ duplicates: [...observed]
151
+ .filter((name) => observedNames.filter((candidate) => candidate === name).length > 1)
152
+ .sort(),
153
+ };
154
+ };
155
+ const inventoryCheck = (name, comparison) => ({
156
+ name: `inventory:${name}`,
157
+ ok: comparison.expected === comparison.observed &&
158
+ comparison.missing.length === 0 &&
159
+ comparison.unexpected.length === 0 &&
160
+ comparison.duplicates.length === 0,
161
+ detail: `expected=${String(comparison.expected)} observed=${String(comparison.observed)} missing=${comparison.missing.join(",") || "none"} unexpected=${comparison.unexpected.join(",") || "none"} duplicates=${comparison.duplicates.join(",") || "none"}`,
162
+ });
163
+ const parseIdentityResource = (contents) => {
164
+ const first = contents.find((candidate) => isRecord(candidate) && typeof candidate.text === "string");
165
+ if (!isRecord(first) || typeof first.text !== "string")
166
+ return { packageVersion: null, catalogDigest: null };
167
+ try {
168
+ const identity = JSON.parse(first.text);
169
+ if (!isRecord(identity))
170
+ return { packageVersion: null, catalogDigest: null };
171
+ const packageIdentity = identity.package;
172
+ const catalog = identity.catalog;
173
+ const digests = isRecord(catalog) ? catalog.digests : undefined;
174
+ return {
175
+ packageVersion: isRecord(packageIdentity) && typeof packageIdentity.version === "string"
176
+ ? packageIdentity.version
177
+ : null,
178
+ catalogDigest: isRecord(digests) && typeof digests.combined_sha256 === "string"
179
+ ? digests.combined_sha256
180
+ : null,
181
+ };
182
+ }
183
+ catch {
184
+ return { packageVersion: null, catalogDigest: null };
185
+ }
186
+ };
187
+ const parseOutputArguments = (arguments_) => {
188
+ let format = "toon";
189
+ for (let index = 0; index < arguments_.length; index += 1) {
190
+ const argument = arguments_[index];
191
+ if (argument === "--json") {
192
+ format = "json";
193
+ continue;
194
+ }
195
+ const inline = argument?.startsWith("--format=")
196
+ ? argument.slice("--format=".length)
197
+ : undefined;
198
+ if (argument === "--format" || inline !== undefined) {
199
+ const candidate = inline ?? arguments_[index + 1];
200
+ if (inline === undefined)
201
+ index += 1;
202
+ if (!isOutputFormat(candidate))
203
+ return {
204
+ ok: false,
205
+ format,
206
+ message: `Invalid output format: ${candidate ?? "missing"}`,
207
+ };
208
+ format = candidate;
209
+ continue;
210
+ }
211
+ if (argument === "--full-output")
212
+ continue;
213
+ return {
214
+ ok: false,
215
+ format,
216
+ message: `Unknown mcp doctor option: ${argument ?? "missing"}`,
217
+ };
218
+ }
219
+ return { ok: true, format };
220
+ };
221
+ const isOutputFormat = (value) => OUTPUT_FORMATS.some((candidate) => candidate === value);
222
+ const requestOptions = (deadline, signal) => ({
223
+ signal,
224
+ timeout: Math.max(1, deadline - Date.now()),
225
+ });
226
+ const targetFreeEnvironment = (environment) => {
227
+ const result = Object.fromEntries(Object.entries(environment).filter((entry) => entry[1] !== undefined));
228
+ for (const name of [
229
+ "HOPPER_TARGET_PATH",
230
+ "HOPPER_TARGET_KIND",
231
+ "HOPPER_LOADER_ARGS_JSON",
232
+ ])
233
+ delete result[name];
234
+ return result;
235
+ };
236
+ const boundedStderr = () => {
237
+ let text = "";
238
+ let bytes = 0;
239
+ let truncated = false;
240
+ return {
241
+ append(chunk) {
242
+ const buffer = Buffer.isBuffer(chunk)
243
+ ? chunk
244
+ : Buffer.from(String(chunk), "utf8");
245
+ bytes += buffer.byteLength;
246
+ const retained = Buffer.byteLength(text, "utf8");
247
+ if (retained < STDERR_LIMIT_BYTES)
248
+ text += buffer
249
+ .subarray(0, STDERR_LIMIT_BYTES - retained)
250
+ .toString("utf8");
251
+ if (bytes > STDERR_LIMIT_BYTES)
252
+ truncated = true;
253
+ },
254
+ value: () => ({
255
+ stderr: text,
256
+ stderr_bytes: bytes,
257
+ stderr_truncated: truncated,
258
+ }),
259
+ };
260
+ };
261
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
262
+ const errorMessage = (cause) => cause instanceof Error ? cause.message : String(cause);
263
+ class TrackedStdioClientTransport extends StdioClientTransport {
264
+ spawnedPid = null;
265
+ async start() {
266
+ await super.start();
267
+ this.spawnedPid = this.pid;
268
+ }
269
+ }
270
+ const closeMcpChild = async (client, transport) => {
271
+ const pid = transport.spawnedPid ?? transport.pid;
272
+ await Promise.allSettled([client.close()]);
273
+ await Promise.allSettled([transport.close()]);
274
+ if (pid === null || !(await processExists(pid)))
275
+ return;
276
+ await terminateProcess(pid, "SIGTERM");
277
+ if (await waitForProcessExit(pid, 1_000))
278
+ return;
279
+ await terminateProcess(pid, "SIGKILL");
280
+ await waitForProcessExit(pid, 1_000);
281
+ };
282
+ const terminateProcess = async (pid, signal) => {
283
+ try {
284
+ process.kill(pid, signal);
285
+ }
286
+ catch (cause) {
287
+ if (!(cause instanceof Error && "code" in cause && cause.code === "ESRCH"))
288
+ throw cause;
289
+ }
290
+ };
291
+ const waitForProcessExit = async (pid, timeoutMs) => {
292
+ const deadline = Date.now() + timeoutMs;
293
+ do {
294
+ if (!(await processExists(pid)))
295
+ return true;
296
+ await new Promise((resolve) => setTimeout(resolve, 20));
297
+ } while (Date.now() < deadline);
298
+ return !(await processExists(pid));
299
+ };
300
+ const processExists = async (pid) => {
301
+ try {
302
+ process.kill(pid, 0);
303
+ return true;
304
+ }
305
+ catch (cause) {
306
+ if (cause instanceof Error && "code" in cause && cause.code === "ESRCH")
307
+ return false;
308
+ throw cause;
309
+ }
310
+ };
@@ -44,7 +44,7 @@ export const parseProcessEnvironment = (value) => Object.fromEntries(value
44
44
  return [entry.slice(0, separator), entry.slice(separator + 1)];
45
45
  }));
46
46
  const systemHost = {
47
- async listMembers(processGroupId) {
47
+ async listProcesses() {
48
48
  if (process.platform === "win32")
49
49
  return [];
50
50
  const { stdout } = await execFileAsync("ps", [
@@ -61,8 +61,7 @@ const systemHost = {
61
61
  processGroupId: Number(match[3]),
62
62
  state: match[4] ?? "",
63
63
  command: match[5] ?? "",
64
- }))
65
- .filter((member) => member.processGroupId === processGroupId);
64
+ }));
66
65
  },
67
66
  async environment(pid) {
68
67
  if (process.platform === "linux") {
@@ -95,73 +94,140 @@ const systemWindowsProcessTreeHost = {
95
94
  };
96
95
  /** Read the capture run token currently exposed by one live process. */
97
96
  export const readProcessRunId = async (pid, host = systemHost) => (await host.environment(pid)).REA_PROCESS_RUN_ID;
98
- /**
99
- * Verify run-token ownership before signaling a POSIX process group.
100
- *
101
- * Process IDs and group IDs can be reused. REA therefore re-reads every live
102
- * member's environment immediately before signaling and fails closed if any
103
- * member cannot be inspected or lacks the per-capture run token.
104
- */
97
+ /** Token-validate every rooted POSIX process group before signaling any. */
105
98
  export const cleanupOwnedProcessGroup = async (ownership, host = systemHost) => {
106
- let members;
99
+ let processes;
107
100
  try {
108
- members = await host.listMembers(ownership.processGroupId);
101
+ processes = liveProcesses(await host.listProcesses());
109
102
  }
110
103
  catch {
111
- return { cleaned: false, reason: "process group could not be inspected" };
104
+ return { cleaned: false, reason: "process table could not be inspected" };
112
105
  }
113
- const liveMembers = liveProcessGroupMembers(members);
114
- if (liveMembers.length === 0)
106
+ const plan = await createOwnedCleanupPlan(ownership, processes, host);
107
+ if ("cleaned" in plan)
108
+ return plan;
109
+ let signaled = false;
110
+ for (const processGroupId of plan.signalOrder) {
111
+ const revalidation = await revalidateOwnedProcessGroup(processGroupId, ownership, host);
112
+ if ("cleaned" in revalidation)
113
+ return revalidation;
114
+ if (revalidation.empty)
115
+ continue;
116
+ try {
117
+ host.signalGroup(processGroupId, "SIGKILL");
118
+ signaled = true;
119
+ }
120
+ catch (cause) {
121
+ const code = cause instanceof Error && "code" in cause ? cause.code : undefined;
122
+ if (code !== "ESRCH")
123
+ return { cleaned: false, reason: "owned process group signal failed" };
124
+ }
125
+ }
126
+ return { cleaned: true, signaled };
127
+ };
128
+ const createOwnedCleanupPlan = async (ownership, processes, host) => {
129
+ const launcher = processes.find(({ pid }) => pid === ownership.leaderPid);
130
+ const rootMembers = processes.filter(({ processGroupId }) => processGroupId === ownership.processGroupId);
131
+ if (launcher === undefined && rootMembers.length === 0)
115
132
  return { cleaned: true, signaled: false };
116
- const leader = liveMembers.find((member) => member.pid === ownership.leaderPid);
117
- if (leader !== undefined) {
118
- if (ownership.expectedParentPid !== undefined &&
119
- leader.parentPid !== ownership.expectedParentPid)
120
- return {
121
- cleaned: false,
122
- reason: "owned launcher parent identity did not match",
123
- };
124
- if (ownership.expectedCommand !== undefined &&
125
- !commandMatches(leader.command, ownership.expectedCommand))
133
+ let descendants = [];
134
+ if (launcher !== undefined) {
135
+ const identityFailure = launcherIdentityFailure(launcher, ownership);
136
+ if (identityFailure !== null)
137
+ return { cleaned: false, reason: identityFailure };
138
+ descendants = descendantsOf(launcher.pid, processes);
139
+ }
140
+ const descendantPids = new Set(descendants.map(({ pid }) => pid));
141
+ const processGroupIds = new Set([ownership.processGroupId]);
142
+ for (const descendant of descendants)
143
+ processGroupIds.add(descendant.processGroupId);
144
+ for (const processGroupId of processGroupIds) {
145
+ if (processGroupId === ownership.processGroupId)
146
+ continue;
147
+ const groupLeader = processes.find(({ pid, processGroupId: observedGroupId }) => pid === processGroupId && observedGroupId === processGroupId);
148
+ if (groupLeader === undefined || !descendantPids.has(groupLeader.pid))
126
149
  return {
127
150
  cleaned: false,
128
- reason: "owned launcher command identity did not match",
151
+ reason: "descendant process-group leader identity could not be verified",
129
152
  };
130
153
  }
154
+ const liveMembers = processes.filter(({ processGroupId }) => processGroupIds.has(processGroupId));
155
+ const failures = await processOwnershipFailures(liveMembers, ownership.runId, host);
156
+ if (failures.length > 0)
157
+ return cleanupValidationFailure(failures);
158
+ return {
159
+ signalOrder: [ownership.processGroupId].concat([...processGroupIds]
160
+ .filter((processGroupId) => processGroupId !== ownership.processGroupId)
161
+ .sort((left, right) => left - right)),
162
+ };
163
+ };
164
+ const launcherIdentityFailure = (launcher, ownership) => {
165
+ if (launcher.processGroupId !== ownership.processGroupId)
166
+ return "owned launcher process-group identity did not match";
167
+ if (ownership.expectedParentPid !== undefined &&
168
+ launcher.parentPid !== ownership.expectedParentPid)
169
+ return "owned launcher parent identity did not match";
170
+ if (ownership.expectedCommand !== undefined &&
171
+ !commandMatches(launcher.command, ownership.expectedCommand))
172
+ return "owned launcher command identity did not match";
173
+ return null;
174
+ };
175
+ const revalidateOwnedProcessGroup = async (processGroupId, ownership, host) => {
176
+ let members;
177
+ try {
178
+ members = liveProcesses(await host.listProcesses()).filter(({ processGroupId: observedGroupId }) => observedGroupId === processGroupId);
179
+ }
180
+ catch {
181
+ return {
182
+ cleaned: false,
183
+ reason: "process ownership could not be revalidated",
184
+ };
185
+ }
186
+ const failures = await processOwnershipFailures(members, ownership.runId, host);
187
+ if (failures.length > 0)
188
+ return cleanupValidationFailure(failures);
189
+ if (processGroupId === ownership.processGroupId) {
190
+ const launcher = members.find(({ pid }) => pid === ownership.leaderPid);
191
+ if (launcher !== undefined) {
192
+ const identityFailure = launcherIdentityFailure(launcher, ownership);
193
+ if (identityFailure !== null)
194
+ return { cleaned: false, reason: identityFailure };
195
+ }
196
+ }
197
+ if (processGroupId !== ownership.processGroupId &&
198
+ members.length > 0 &&
199
+ !members.some(({ pid }) => pid === processGroupId))
200
+ return {
201
+ cleaned: false,
202
+ reason: "descendant process-group leader identity could not be revalidated",
203
+ };
204
+ return { empty: members.length === 0 };
205
+ };
206
+ const cleanupValidationFailure = (failures) => ({
207
+ cleaned: false,
208
+ reason: failures.some(({ reason }) => reason === "run-token-mismatch")
209
+ ? "process tree contains an unowned or PID-reused process"
210
+ : "process ownership could not be revalidated",
211
+ failures,
212
+ });
213
+ const processOwnershipFailures = async (members, runId, host) => {
131
214
  const failures = [];
132
- for (const member of liveMembers) {
215
+ for (const member of members) {
133
216
  try {
134
- const environment = await host.environment(member.pid);
135
- if (environment.REA_PROCESS_RUN_ID !== ownership.runId)
217
+ if ((await host.environment(member.pid)).REA_PROCESS_RUN_ID !== runId)
136
218
  failures.push({ pid: member.pid, reason: "run-token-mismatch" });
137
219
  }
138
220
  catch {
139
221
  failures.push({ pid: member.pid, reason: "environment-unreadable" });
140
222
  }
141
223
  }
142
- if (failures.length > 0)
143
- return {
144
- cleaned: false,
145
- reason: failures.some(({ reason }) => reason === "run-token-mismatch")
146
- ? "process group contains an unowned or PID-reused process"
147
- : "process ownership could not be revalidated",
148
- failures,
149
- };
150
- try {
151
- host.signalGroup(ownership.processGroupId, "SIGKILL");
152
- }
153
- catch (cause) {
154
- const code = cause instanceof Error && "code" in cause ? cause.code : undefined;
155
- if (code !== "ESRCH")
156
- return { cleaned: false, reason: "owned process group signal failed" };
157
- }
158
- return { cleaned: true, signaled: true };
224
+ return failures;
159
225
  };
160
226
  /** Observe one group without signaling it, failing closed on identity doubt. */
161
227
  export const observeOwnedProcessGroup = async (ownership, host = systemHost) => {
162
228
  let members;
163
229
  try {
164
- members = await host.listMembers(ownership.processGroupId);
230
+ members = (await host.listProcesses()).filter(({ processGroupId }) => processGroupId === ownership.processGroupId);
165
231
  }
166
232
  catch {
167
233
  return {
@@ -169,7 +235,7 @@ export const observeOwnedProcessGroup = async (ownership, host = systemHost) =>
169
235
  reason: "process group could not be inspected",
170
236
  };
171
237
  }
172
- const liveMembers = liveProcessGroupMembers(members);
238
+ const liveMembers = liveProcesses(members);
173
239
  if (liveMembers.length === 0)
174
240
  return { state: "empty" };
175
241
  for (const member of liveMembers) {
@@ -190,10 +256,91 @@ export const observeOwnedProcessGroup = async (ownership, host = systemHost) =>
190
256
  }
191
257
  return { state: "alive" };
192
258
  };
193
- const liveProcessGroupMembers = (members) => members.filter(({ state }) => !state.startsWith("Z"));
259
+ /**
260
+ * Record the live launcher and descendant lineage after run-token validation.
261
+ *
262
+ * The observation is intentionally point-in-time. A verified empty descendant
263
+ * list means no descendants were live during this observation, not that the
264
+ * run never created a short-lived child.
265
+ */
266
+ export const observeOwnedProcessLineage = async (ownership, host = systemHost) => {
267
+ let processes;
268
+ try {
269
+ processes = liveProcesses(await host.listProcesses());
270
+ }
271
+ catch {
272
+ return unavailableLineage(ownership, "process table could not be inspected");
273
+ }
274
+ const launcher = processes.find(({ pid }) => pid === ownership.leaderPid);
275
+ if (launcher === undefined)
276
+ return unavailableLineage(ownership, "owned launcher is not live");
277
+ if (launcher.processGroupId !== ownership.processGroupId)
278
+ return unavailableLineage(ownership, "owned launcher process-group identity did not match");
279
+ if (ownership.expectedParentPid !== undefined &&
280
+ launcher.parentPid !== ownership.expectedParentPid)
281
+ return unavailableLineage(ownership, "owned launcher parent identity did not match");
282
+ if (ownership.expectedCommand !== undefined &&
283
+ !commandMatches(launcher.command, ownership.expectedCommand))
284
+ return unavailableLineage(ownership, "owned launcher command identity did not match");
285
+ const descendants = descendantsOf(launcher.pid, processes);
286
+ for (const member of [launcher, ...descendants]) {
287
+ try {
288
+ if ((await host.environment(member.pid)).REA_PROCESS_RUN_ID !==
289
+ ownership.runId)
290
+ return unavailableLineage(ownership, "process lineage contains an unowned or PID-reused process");
291
+ }
292
+ catch {
293
+ return unavailableLineage(ownership, "process ownership could not be revalidated");
294
+ }
295
+ }
296
+ return {
297
+ status: "verified",
298
+ observedAt: new Date().toISOString(),
299
+ lineage: {
300
+ schemaVersion: 1,
301
+ runId: ownership.runId,
302
+ launcherPid: launcher.pid,
303
+ launcherParentPid: launcher.parentPid,
304
+ processGroupId: launcher.processGroupId,
305
+ descendants: descendants
306
+ .sort((left, right) => left.pid - right.pid)
307
+ .map(({ pid, parentPid, processGroupId }) => ({
308
+ pid,
309
+ parentPid,
310
+ processGroupId,
311
+ })),
312
+ },
313
+ };
314
+ };
315
+ const descendantsOf = (launcherPid, processes) => {
316
+ const childrenByParent = new Map();
317
+ for (const process of processes) {
318
+ const siblings = childrenByParent.get(process.parentPid) ?? [];
319
+ siblings.push(process);
320
+ childrenByParent.set(process.parentPid, siblings);
321
+ }
322
+ const descendants = [];
323
+ const pending = [...(childrenByParent.get(launcherPid) ?? [])];
324
+ const visited = new Set([launcherPid]);
325
+ for (const process of pending) {
326
+ if (visited.has(process.pid))
327
+ continue;
328
+ visited.add(process.pid);
329
+ descendants.push(process);
330
+ pending.push(...(childrenByParent.get(process.pid) ?? []));
331
+ }
332
+ return descendants;
333
+ };
334
+ const unavailableLineage = (ownership, reason) => ({
335
+ status: "unavailable",
336
+ observedAt: new Date().toISOString(),
337
+ runId: ownership.runId,
338
+ launcherPid: ownership.leaderPid,
339
+ processGroupId: ownership.processGroupId,
340
+ reason,
341
+ });
342
+ const liveProcesses = (members) => members.filter(({ state }) => !state.startsWith("Z"));
194
343
  const commandMatches = (actual, expected) => {
195
- const normalizedActual = actual.trim();
196
- const normalizedExpected = expected.trim();
197
- return (normalizedActual === normalizedExpected ||
198
- normalizedActual.startsWith(`${normalizedExpected} `));
344
+ return (actual.trim() === expected.trim() ||
345
+ actual.trim().startsWith(`${expected.trim()} `));
199
346
  };
@@ -0,0 +1,21 @@
1
+ import { observeOwnedProcessLineage } from "./ProcessOwnership.js";
2
+ /** Retain the latest truthful process-lineage observation for one client. */
3
+ export class ProviderRunLineage {
4
+ #observation = null;
5
+ /** Clear stale lineage before a new launch attempt. */
6
+ reset() {
7
+ this.#observation = null;
8
+ }
9
+ /** Observe a token-owned launcher without signaling it. */
10
+ async observe(launch) {
11
+ if (launch?.ownership === undefined)
12
+ return;
13
+ this.#observation = await observeOwnedProcessLineage(launch.ownership);
14
+ }
15
+ /** Return a detached copy suitable for caller-visible status. */
16
+ snapshot() {
17
+ return this.#observation === null
18
+ ? null
19
+ : structuredClone(this.#observation);
20
+ }
21
+ }
@@ -15,7 +15,7 @@ import { createServerIdentity } from "../serverIdentity.js";
15
15
  import { registerBrowserTools } from "./registerBrowserTools.js";
16
16
  import { registerElectronTools } from "./registerElectronTools.js";
17
17
  import { registerApplicationTools } from "./registerApplicationTools.js";
18
- const TARGET_FREE_INSTRUCTIONS = "REA analyzes shipped artifacts and approved runtime evidence; use repository tools for source. Route: ASAR/JavaScript -> analyze_javascript_application; archive/package -> inventory_artifact; managed PE/CLI -> inspect_managed_artifact; browser/Electron runtime -> list_browser_targets/list_electron_targets; native binary/database -> open_binary, then binary_overview. Start with summaries and cite Evidence IDs. Never repeat identical analysis or read full Evidence unless a missing detail requires it.";
18
+ const TARGET_FREE_INSTRUCTIONS = "REA analyzes shipped artifacts. Route: ASAR/JavaScript -> analyze_javascript_application (configured root required); archive/package -> inventory_artifact; managed PE/CLI -> inspect_managed_artifact; browser/Electron runtime -> list_browser_targets/list_electron_targets; native binary/database -> open_binary, then binary_overview. Check binary_session for policy. Start with summaries and cite Evidence IDs. Never repeat identical analysis or read full Evidence without a specific need.";
19
19
  const ACTIVE_TARGET_INSTRUCTIONS = "REA analyzes the active reverse-engineering target. Start native analysis with binary_overview, then narrow with analyze_function, literal search, callers, callees, and xrefs. Prefer summary views, never repeat an identical call, and read full Evidence only when the task requires it.";
20
20
  import { LinuxJavaScriptReplayRunner } from "../replay/LinuxJavaScriptReplayRunner.js";
21
21
  import { SystemJavaScriptReplayHost } from "../replay/SystemJavaScriptReplayHost.js";
@@ -205,6 +205,9 @@ const registerObservationTools = ({ server, session, options, logger, permission
205
205
  logger,
206
206
  recordEvidence,
207
207
  recordEvidenceWithUnknown,
208
+ evidenceLookup: session === undefined
209
+ ? undefined
210
+ : (evidenceId) => session.evidenceById(evidenceId),
208
211
  evidenceFilePolicy: options.evidenceFilePolicy ?? {
209
212
  roots: [],
210
213
  maxBytes: 1,