rea-agents 1.6.0 → 2.0.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 (238) hide show
  1. package/README.md +157 -66
  2. package/bridge/ghidra/ReaGhidraBridge.java +2075 -0
  3. package/bridge/hopper_bridge.py +13 -3
  4. package/dist/application/AbortablePromise.js +25 -0
  5. package/dist/application/AnalysisProvider.js +3 -0
  6. package/dist/application/AnalysisProviderEvaluation.js +60 -0
  7. package/dist/application/AnalysisProviderRegistry.js +228 -0
  8. package/dist/application/AnalysisSnapshotCache.js +41 -21
  9. package/dist/application/AnalysisSnapshotFiles.js +5 -6
  10. package/dist/application/BinarySession.js +218 -101
  11. package/dist/application/CapabilityInventory.js +18 -0
  12. package/dist/application/CompositeProvider.js +20 -8
  13. package/dist/application/CrossVersionInvestigation.js +10 -6
  14. package/dist/application/DirectAnalysis.js +49 -35
  15. package/dist/application/Doctor.js +110 -8
  16. package/dist/application/ElectronBoundaryAnalysis.js +96 -0
  17. package/dist/application/ElectronBoundaryGraph.js +9 -0
  18. package/dist/application/ElectronBoundaryGraphContext.js +62 -0
  19. package/dist/application/ElectronBoundaryGraphIpc.js +242 -0
  20. package/dist/application/ElectronBoundaryGraphNative.js +86 -0
  21. package/dist/application/ElectronBoundaryGraphWindows.js +199 -0
  22. package/dist/application/EnhancedTools.js +39 -14
  23. package/dist/application/InvestigationProviders.js +41 -0
  24. package/dist/application/JavaScriptApplicationEvidence.js +26 -0
  25. package/dist/application/JavaScriptApplicationEvidenceGraph.js +89 -0
  26. package/dist/application/JavaScriptApplicationService.js +74 -0
  27. package/dist/application/JavaScriptApplicationWorkflowEvidence.js +29 -0
  28. package/dist/application/JavaScriptApplicationWorkflowService.js +90 -0
  29. package/dist/application/JavaScriptArtifactAnalysis.js +305 -0
  30. package/dist/application/JavaScriptArtifactAnalysisTypes.js +1 -0
  31. package/dist/application/JavaScriptArtifactFiles.js +231 -0
  32. package/dist/application/JavaScriptArtifactGraphAccumulator.js +54 -0
  33. package/dist/application/JavaScriptArtifactGraphBuilder.js +144 -0
  34. package/dist/application/JavaScriptArtifactGraphContext.js +205 -0
  35. package/dist/application/JavaScriptArtifactGraphDocuments.js +108 -0
  36. package/dist/application/JavaScriptArtifactGraphEvidence.js +99 -0
  37. package/dist/application/JavaScriptArtifactGraphFindings.js +271 -0
  38. package/dist/application/JavaScriptArtifactGraphStructure.js +409 -0
  39. package/dist/application/JavaScriptArtifactReconstruction.js +77 -0
  40. package/dist/application/JavaScriptArtifactReconstructionInput.js +18 -0
  41. package/dist/application/JavaScriptReplayPlanning.js +201 -0
  42. package/dist/application/JavaScriptReplayService.js +266 -0
  43. package/dist/application/JavaScriptRuntimeReconciliationEvidence.js +25 -0
  44. package/dist/application/JavaScriptRuntimeReconciliationService.js +27 -0
  45. package/dist/application/MacHopper.js +20 -4
  46. package/dist/application/ManagedApplicationGraphService.js +55 -0
  47. package/dist/application/ManagedMemberComparisonService.js +122 -0
  48. package/dist/application/ManagedNativeVerificationService.js +39 -0
  49. package/dist/application/ManagedReconstructionService.js +58 -0
  50. package/dist/application/ManagedRuntimeCorrelationService.js +103 -0
  51. package/dist/application/PermissionAuthority.js +1 -1
  52. package/dist/application/ProcessCaptureLifecycle.js +1 -1
  53. package/dist/application/ProcessHarness.js +1 -1
  54. package/dist/application/ProcessSampling.js +16 -12
  55. package/dist/application/ProjectPermissionStore.js +1 -0
  56. package/dist/application/SessionProviderRouter.js +219 -0
  57. package/dist/application/Setup.js +84 -60
  58. package/dist/application/SetupClients.js +1 -1
  59. package/dist/application/SetupPlan.js +9 -2
  60. package/dist/application/SupportedClients.js +40 -16
  61. package/dist/application/Upgrade.js +1 -1
  62. package/dist/application/runtime.js +11 -6
  63. package/dist/artifacts/AsarArtifactReader.js +23 -3
  64. package/dist/browser/CdpBrowserProvider.js +4 -2
  65. package/dist/browser/CdpCaptureEvents.js +49 -0
  66. package/dist/browser/CdpElectronInspection.js +106 -162
  67. package/dist/browser/CdpElectronProvider.js +9 -3
  68. package/dist/browser/CdpElectronScriptEvents.js +69 -0
  69. package/dist/browser/CdpElectronScripts.js +94 -0
  70. package/dist/browser/CdpElectronWorkers.js +56 -0
  71. package/dist/browser/CdpEndpoint.js +14 -1
  72. package/dist/browser/CdpPageCapture.js +8 -4
  73. package/dist/catalogIdentity.js +6 -42
  74. package/dist/cli.js +446 -34
  75. package/dist/cliApplicationCommands.js +62 -0
  76. package/dist/cliBrowserAdvancedCommands.js +5 -4
  77. package/dist/cliBrowserCommands.js +5 -4
  78. package/dist/cliCommandNames.js +56 -0
  79. package/dist/cliElectronCommands.js +106 -3
  80. package/dist/cliEvidenceCommands.js +4 -3
  81. package/dist/cliInvestigationCommands.js +2 -1
  82. package/dist/cliJsonInput.js +54 -0
  83. package/dist/cliPolicyCommands.js +2 -1
  84. package/dist/cliProcessCommands.js +3 -2
  85. package/dist/config.js +101 -2
  86. package/dist/contracts/applicationToolContracts.js +91 -0
  87. package/dist/contracts/artifactToolContracts.js +5 -9
  88. package/dist/contracts/browserToolContracts.js +14 -62
  89. package/dist/contracts/electronToolContracts.js +45 -19
  90. package/dist/contracts/enhancedInputs.js +2 -2
  91. package/dist/contracts/errorSchemas.js +1 -0
  92. package/dist/contracts/investigationExamples.js +0 -6
  93. package/dist/contracts/javascriptApplicationWorkflowExamples.js +46 -0
  94. package/dist/contracts/javascriptRuntimeReconciliationExample.js +208 -0
  95. package/dist/contracts/managedToolContracts.js +153 -0
  96. package/dist/contracts/managedWorkflowExamples.js +346 -0
  97. package/dist/contracts/managedWorkflowToolContracts.js +203 -0
  98. package/dist/contracts/nativeToolContracts.js +2 -6
  99. package/dist/contracts/promptContracts.js +18 -2
  100. package/dist/contracts/providerSelection.js +28 -0
  101. package/dist/contracts/sessionLifecycleInputs.js +2 -0
  102. package/dist/contracts/toolContractExamples.js +10 -25
  103. package/dist/contracts/toolContracts.js +69 -65
  104. package/dist/contracts/toolEffects.js +160 -0
  105. package/dist/contracts/toolOutputSchemas.js +152 -42
  106. package/dist/doctorRuntime.js +47 -0
  107. package/dist/domain/analysisProfile.js +78 -0
  108. package/dist/domain/analysisSnapshot.js +92 -46
  109. package/dist/domain/artifactComparison.js +3 -4
  110. package/dist/domain/artifactGraph.js +1 -0
  111. package/dist/domain/binaryTarget.js +4 -29
  112. package/dist/domain/browserObservation.js +3 -0
  113. package/dist/domain/bundleComparison.js +4 -4
  114. package/dist/domain/electronObservation.js +13 -0
  115. package/dist/domain/electronStaticAnalysis.js +9 -0
  116. package/dist/domain/electronStaticAnalysisBrowser.js +194 -0
  117. package/dist/domain/electronStaticAnalysisIpc.js +171 -0
  118. package/dist/domain/electronStaticAnalysisNative.js +164 -0
  119. package/dist/domain/electronStaticAnalysisTypes.js +1 -0
  120. package/dist/domain/electronStaticAnalysisValues.js +103 -0
  121. package/dist/domain/errors.js +148 -7
  122. package/dist/domain/evidence.js +44 -5
  123. package/dist/domain/evidenceBundle.js +9 -7
  124. package/dist/domain/functionComparison.js +9 -3
  125. package/dist/domain/functionComparisonNormalization.js +37 -0
  126. package/dist/domain/functionComparisonSchemas.js +3 -8
  127. package/dist/domain/functionDossierEvidence.js +4 -1
  128. package/dist/domain/hopperValues.js +66 -9
  129. package/dist/domain/inputIssueProjection.js +52 -0
  130. package/dist/domain/investigationWorkspace.js +2 -1
  131. package/dist/domain/javascriptApplicationAnalysis.js +155 -0
  132. package/dist/domain/javascriptApplicationChangeGraph.js +231 -0
  133. package/dist/domain/javascriptApplicationEvidenceSchemas.js +379 -0
  134. package/dist/domain/javascriptApplicationGraph.js +282 -0
  135. package/dist/domain/javascriptApplicationGraphSchemas.js +139 -0
  136. package/dist/domain/javascriptApplicationVersionComparison.js +116 -0
  137. package/dist/domain/javascriptApplicationVersionComparisonSchemas.js +139 -0
  138. package/dist/domain/javascriptApplicationVersionItems.js +221 -0
  139. package/dist/domain/javascriptApplicationVersionKeys.js +170 -0
  140. package/dist/domain/javascriptAstFingerprint.js +143 -0
  141. package/dist/domain/javascriptFeatureSeed.js +140 -0
  142. package/dist/domain/javascriptFeatureTrace.js +232 -0
  143. package/dist/domain/javascriptFeatureTraceSchemas.js +136 -0
  144. package/dist/domain/javascriptFeatureTraversal.js +88 -0
  145. package/dist/domain/javascriptNativeHandoff.js +85 -0
  146. package/dist/domain/javascriptReplay.js +361 -0
  147. package/dist/domain/javascriptRuntimeLoadState.js +106 -0
  148. package/dist/domain/javascriptRuntimeReconciliation.js +42 -0
  149. package/dist/domain/javascriptRuntimeReconciliationGraph.js +169 -0
  150. package/dist/domain/javascriptRuntimeReconciliationMatching.js +247 -0
  151. package/dist/domain/javascriptRuntimeReconciliationParsing.js +160 -0
  152. package/dist/domain/javascriptRuntimeReconciliationResult.js +119 -0
  153. package/dist/domain/javascriptRuntimeReconciliationRuntime.js +316 -0
  154. package/dist/domain/javascriptRuntimeReconciliationSchemas.js +245 -0
  155. package/dist/domain/javascriptRuntimeStaticCandidates.js +191 -0
  156. package/dist/domain/javascriptStaticAnalysis.js +346 -0
  157. package/dist/domain/javascriptStaticAnalysisFindings.js +39 -0
  158. package/dist/domain/javascriptStaticAnalysisHelpers.js +274 -0
  159. package/dist/domain/javascriptStaticAnalysisState.js +23 -0
  160. package/dist/domain/javascriptStaticAnalysisTypes.js +1 -0
  161. package/dist/domain/managedApplicationGraph.js +556 -0
  162. package/dist/domain/managedArtifact.js +444 -0
  163. package/dist/domain/managedMemberComparison.js +565 -0
  164. package/dist/domain/managedNativeVerification.js +408 -0
  165. package/dist/domain/managedReconstruction.js +183 -0
  166. package/dist/domain/managedRuntimeCorrelation.js +175 -0
  167. package/dist/domain/reconstructionVerificationSchemas.js +1 -3
  168. package/dist/dotnet/ManagedArtifactInspector.js +318 -0
  169. package/dist/dotnet/ManagedMemberInspector.js +887 -0
  170. package/dist/dotnet/ManagedMetadataHeaps.js +131 -0
  171. package/dist/dotnet/ManagedMetadataInventory.js +368 -0
  172. package/dist/dotnet/ManagedMetadataLayout.js +322 -0
  173. package/dist/dotnet/ManagedNativeBoundaryInspector.js +391 -0
  174. package/dist/dotnet/ManagedPeReader.js +157 -0
  175. package/dist/dotnet/ManagedReaderFailure.js +10 -0
  176. package/dist/dotnet/ManagedStaticProvider.js +203 -0
  177. package/dist/generatedPackageMetadata.js +2 -2
  178. package/dist/ghidra/GhidraAnalysisProfile.js +37 -0
  179. package/dist/ghidra/GhidraClient.js +400 -0
  180. package/dist/ghidra/GhidraClientTypes.js +1 -0
  181. package/dist/ghidra/GhidraDefaults.js +20 -0
  182. package/dist/ghidra/GhidraDiagnostics.js +43 -0
  183. package/dist/ghidra/GhidraDoctor.js +59 -0
  184. package/dist/ghidra/GhidraFunctionValues.js +264 -0
  185. package/dist/ghidra/GhidraInstallation.js +239 -0
  186. package/dist/ghidra/GhidraInventoryValues.js +217 -0
  187. package/dist/ghidra/GhidraLauncher.js +139 -0
  188. package/dist/ghidra/GhidraProvider.js +366 -0
  189. package/dist/ghidra/GhidraRequestQueue.js +98 -0
  190. package/dist/ghidra/GhidraResponseBuffer.js +31 -0
  191. package/dist/ghidra/GhidraResponseRouter.js +24 -0
  192. package/dist/ghidra/GhidraSessionError.js +39 -0
  193. package/dist/ghidra/GhidraSessionValues.js +62 -0
  194. package/dist/ghidra/GhidraSocketConnection.js +46 -0
  195. package/dist/ghidra/protocol.js +69 -0
  196. package/dist/hopper/BridgeLauncher.js +19 -54
  197. package/dist/hopper/HopperAnalysisProfile.js +106 -0
  198. package/dist/hopper/HopperClient.js +154 -162
  199. package/dist/hopper/HopperProvider.js +99 -7
  200. package/dist/hopper/HopperSessionValues.js +27 -0
  201. package/dist/hopper/HopperSocketConnection.js +31 -0
  202. package/dist/main.js +14 -0
  203. package/dist/native/NativeMacOSProvider.js +3 -1
  204. package/dist/process/PendingOperations.js +59 -0
  205. package/dist/process/PrivateRuntimeRoot.js +33 -0
  206. package/dist/process/ProviderDeadline.js +95 -0
  207. package/dist/process/ProviderProcess.js +275 -0
  208. package/dist/replay/JavaScriptReplayWorker.js +286 -0
  209. package/dist/replay/LinuxJavaScriptReplayRunner.js +434 -0
  210. package/dist/replay/LinuxRuntimeClosure.js +46 -0
  211. package/dist/replay/LinuxSeccompPolicy.js +40 -0
  212. package/dist/replay/ReplayWorkerProtocol.js +55 -0
  213. package/dist/replay/SystemJavaScriptReplayHost.js +182 -0
  214. package/dist/server/createServer.js +53 -0
  215. package/dist/server/promptCompletion.js +9 -3
  216. package/dist/server/registerApplicationTools.js +115 -0
  217. package/dist/server/registerArtifactComparisonTool.js +16 -9
  218. package/dist/server/registerBrowserTools.js +33 -8
  219. package/dist/server/registerBundleComparisonTool.js +17 -20
  220. package/dist/server/registerElectronTools.js +45 -3
  221. package/dist/server/registerEnhancedTools.js +67 -19
  222. package/dist/server/registerEvidenceTools.js +5 -1
  223. package/dist/server/registerFunctionComparisonTool.js +14 -7
  224. package/dist/server/registerInvestigationTools.js +21 -17
  225. package/dist/server/registerManagedTools.js +6 -0
  226. package/dist/server/registerManagedWorkflowTools.js +277 -0
  227. package/dist/server/registerOfficialTools.js +11 -8
  228. package/dist/server/registerProcessComparisonTool.js +25 -47
  229. package/dist/server/registerSessionStatusTool.js +7 -7
  230. package/dist/server/registerSessionTools.js +57 -87
  231. package/dist/server/sessionEvidence.js +14 -19
  232. package/dist/server/toolInputValidation.js +77 -0
  233. package/dist/server/toolRegistrationOptions.js +79 -2
  234. package/dist/server/toolResult.js +25 -17
  235. package/package.json +17 -7
  236. package/scripts/rea.mjs +36 -2
  237. package/skills/rea-analysis/SKILL.md +59 -3
  238. /package/dist/{application → process}/ProcessOwnership.js +0 -0
@@ -1,11 +1,16 @@
1
1
  import { randomBytes, randomUUID } from "node:crypto";
2
- import { access, mkdtemp, rm } from "node:fs/promises";
3
- import { createConnection } from "node:net";
2
+ import { access } from "node:fs/promises";
4
3
  import { tmpdir } from "node:os";
5
4
  import { join } from "node:path";
6
5
  import { HopperCancelledError, HopperProcessError, HopperProtocolError, HopperStartError, hopperStartupFailure, HopperTimeoutError, } from "../domain/errors.js";
7
6
  import { err, ok } from "../domain/result.js";
8
7
  import { silentLogger } from "../logger.js";
8
+ import { PendingOperations } from "../process/PendingOperations.js";
9
+ import { PrivateRuntimeRoot } from "../process/PrivateRuntimeRoot.js";
10
+ import { ProviderStartupDeadline } from "../process/ProviderDeadline.js";
11
+ import { ProviderProcessSupervisor, } from "../process/ProviderProcess.js";
12
+ import { isHopperCleanupRequired, isHopperShutdownAcknowledgement, parseHopperServerInfo, } from "./HopperSessionValues.js";
13
+ import { connectHopperSocketOnce } from "./HopperSocketConnection.js";
9
14
  import { parseResponseLine, responseResult } from "./protocol.js";
10
15
  const MAX_LINE_BYTES = 10 * 1024 * 1024;
11
16
  const SHUTDOWN_TIMEOUT_MS = 30_000;
@@ -20,11 +25,12 @@ const SESSION_ROOT = process.platform === "darwin" ? "/tmp" : tmpdir();
20
25
  */
21
26
  export class HopperClient {
22
27
  #options;
23
- #pending = new Map();
28
+ #pending = new PendingOperations();
24
29
  #logger;
25
30
  #socket;
26
31
  #launch;
27
- #directory;
32
+ #process;
33
+ #runtimeRoot;
28
34
  #token;
29
35
  #nextId = 1;
30
36
  #buffer = "";
@@ -33,6 +39,16 @@ export class HopperClient {
33
39
  #startupController;
34
40
  #startPromise;
35
41
  #closePromise;
42
+ #onSocketData = (chunk) => {
43
+ this.#onData(chunk);
44
+ };
45
+ #onSocketError = () => {
46
+ this.#failAll(new HopperProcessError(null));
47
+ };
48
+ #onSocketClose = () => {
49
+ if (!this.#closing)
50
+ this.#failAll(new HopperProcessError(null));
51
+ };
36
52
  constructor(options) {
37
53
  this.#logger = options.logger ?? silentLogger;
38
54
  this.#options = {
@@ -49,68 +65,104 @@ export class HopperClient {
49
65
  if (this.#startPromise !== undefined)
50
66
  return this.#startPromise;
51
67
  const controller = new AbortController();
52
- const startupSignal = signal === undefined
53
- ? controller.signal
54
- : AbortSignal.any([signal, controller.signal]);
55
- const started = this.#start(startupSignal);
68
+ const onAbort = () => controller.abort(signal?.reason);
69
+ if (signal?.aborted === true)
70
+ onAbort();
71
+ else
72
+ signal?.addEventListener("abort", onAbort, { once: true });
73
+ const started = this.#start(controller.signal);
56
74
  this.#startupController = controller;
57
75
  this.#startPromise = started;
58
- void started.then((result) => {
59
- if (!result.ok && this.#startPromise === started) {
76
+ const release = () => {
77
+ signal?.removeEventListener("abort", onAbort);
78
+ };
79
+ const reset = () => {
80
+ if (this.#startPromise === started) {
60
81
  this.#startPromise = undefined;
61
82
  if (this.#startupController === controller) {
62
83
  this.#startupController = undefined;
63
84
  }
64
85
  }
86
+ };
87
+ void started.then((result) => {
88
+ release();
89
+ if (!result.ok)
90
+ reset();
91
+ }, () => {
92
+ release();
93
+ reset();
65
94
  });
66
95
  return started;
67
96
  }
68
97
  async #start(signal) {
69
98
  if (isAborted(signal))
70
99
  return err(new HopperCancelledError());
71
- if (this.#socket !== undefined || this.#directory !== undefined) {
100
+ if (this.#socket !== undefined || this.#runtimeRoot !== undefined) {
72
101
  return err(new HopperProtocolError("Hopper client is already started"));
73
102
  }
103
+ const deadline = new ProviderStartupDeadline(this.#options.startupTimeoutMs, signal);
74
104
  try {
75
- this.#directory = await mkdtemp(join(SESSION_ROOT, "rea-"));
105
+ return await this.#startWithin(deadline);
106
+ }
107
+ finally {
108
+ deadline.dispose();
109
+ }
110
+ }
111
+ async #startWithin(deadline) {
112
+ try {
113
+ this.#runtimeRoot = await PrivateRuntimeRoot.create({
114
+ parent: SESSION_ROOT,
115
+ prefix: "rea-",
116
+ });
76
117
  }
77
118
  catch (cause) {
78
119
  return err(new HopperStartError({ cause }));
79
120
  }
80
- if (isAborted(signal)) {
121
+ if (deadline.signal.aborted) {
81
122
  await this.#cleanup();
82
- return err(new HopperCancelledError());
123
+ return err(startupInterruption(deadline));
83
124
  }
84
- const socketPath = join(this.#directory, "bridge.sock");
125
+ const socketPath = join(this.#runtimeRoot.path, "bridge.sock");
85
126
  this.#token = randomBytes(32).toString("hex");
86
127
  this.#launcherExitCode = undefined;
87
128
  const runId = randomUUID();
88
- const launched = await this.#options.launcher.launch({
89
- directory: this.#directory,
129
+ const launched = await this.#options.launcher
130
+ .launch({
131
+ directory: this.#runtimeRoot.path,
90
132
  socketPath,
91
133
  token: this.#token,
92
134
  runId,
93
- }, signal === undefined ? {} : { signal });
135
+ }, { signal: deadline.signal })
136
+ .catch((cause) => err(new HopperStartError({ cause })));
94
137
  if (!launched.ok) {
95
138
  await this.#cleanup();
96
- return launched;
139
+ return deadline.signal.aborted
140
+ ? err(startupInterruption(deadline))
141
+ : launched;
97
142
  }
98
143
  this.#launch = launched.value;
99
144
  this.#attachLauncher(launched.value);
100
- const connected = await this.#connect(socketPath, signal);
145
+ const connected = await this.#connect(socketPath, deadline);
101
146
  if (!connected.ok) {
102
147
  await this.#cleanup();
103
148
  return connected;
104
149
  }
150
+ const remainingMs = deadline.remainingMs();
151
+ if (remainingMs <= 0) {
152
+ await this.#cleanup();
153
+ return err(new HopperTimeoutError(this.#options.startupTimeoutMs));
154
+ }
105
155
  const health = await this.#request("health", {}, {
106
- timeoutMs: this.#options.startupTimeoutMs,
107
- ...(signal === undefined ? {} : { signal }),
156
+ timeoutMs: remainingMs,
157
+ signal: deadline.signal,
108
158
  });
109
159
  if (!health.ok) {
110
160
  await this.#cleanup();
111
- return health;
161
+ return deadline.signal.aborted
162
+ ? err(startupInterruption(deadline))
163
+ : health;
112
164
  }
113
- const parsed = parseServerInfo(health.value, runId);
165
+ const parsed = parseHopperServerInfo(health.value, runId);
114
166
  if (!parsed.ok)
115
167
  await this.#cleanup();
116
168
  return parsed;
@@ -148,43 +200,45 @@ export class HopperClient {
148
200
  this.#closing = true;
149
201
  try {
150
202
  const socket = this.#socket;
151
- let cleanupAttempted = false;
203
+ let cleanupResult;
152
204
  if (socket !== undefined && !socket.destroyed) {
153
205
  const shutdown = await this.#request("shutdown", {}, { timeoutMs: SHUTDOWN_TIMEOUT_MS }).catch(() => err(new HopperProcessError(null)));
154
206
  if (shutdown.ok &&
155
- isCleanupRequired(shutdown.value) &&
207
+ isHopperCleanupRequired(shutdown.value) &&
156
208
  this.#launch?.shutdownByCleanup === true &&
157
209
  this.#launch.cleanup !== undefined) {
158
- cleanupAttempted = true;
159
- const cleanup = await this.#launch.cleanup();
160
- if (!cleanup.cleaned) {
210
+ cleanupResult = await this.#launch
211
+ .cleanup()
212
+ .catch(providerCleanupFailure);
213
+ if (!cleanupResult.cleaned) {
161
214
  await this.#fallbackDocumentShutdown();
162
- const retried = await this.#launch.cleanup();
163
- if (!retried.cleaned)
164
- this.#logger.warn({ reason: retried.reason }, "Owned launcher cleanup failed closed");
215
+ cleanupResult = await this.#launch
216
+ .cleanup()
217
+ .catch(providerCleanupFailure);
165
218
  }
166
219
  }
167
- else if (!shutdown.ok || !isShutdownAcknowledgement(shutdown.value))
220
+ else if (!shutdown.ok ||
221
+ !isHopperShutdownAcknowledgement(shutdown.value))
168
222
  this.#logger.warn({ status: shutdown.ok ? "invalid-acknowledgement" : "failed" }, "Hopper document shutdown was not confirmed");
169
223
  }
170
224
  this.#failAll(new HopperProcessError(null));
225
+ if (socket !== undefined)
226
+ this.#detachSocket(socket);
171
227
  socket?.destroy();
172
228
  this.#socket = undefined;
173
- if (this.#launch?.ownsProcessLifetime === true) {
174
- if (this.#launch.cleanup !== undefined && !cleanupAttempted) {
175
- const cleanup = await this.#launch.cleanup();
176
- if (!cleanup.cleaned)
177
- this.#logger.warn({ reason: cleanup.reason }, "Owned launcher cleanup failed closed");
178
- }
179
- else
180
- this.#launch.process.kill("SIGTERM");
229
+ if (this.#process !== undefined) {
230
+ const stopped = await this.#process.stop(cleanupResult === undefined ? {} : { cleanupResult });
231
+ if (stopped.status === "incomplete")
232
+ this.#logger.warn({ reason: stopped.reason }, "Owned launcher cleanup failed closed");
181
233
  }
234
+ this.#process = undefined;
182
235
  this.#launch = undefined;
183
- if (this.#directory !== undefined) {
184
- await rm(this.#directory, { recursive: true, force: true });
185
- this.#directory = undefined;
186
- }
236
+ const runtimeRoot = this.#runtimeRoot;
237
+ this.#runtimeRoot = undefined;
238
+ await runtimeRoot?.close();
187
239
  this.#token = undefined;
240
+ this.#buffer = "";
241
+ this.#launcherExitCode = undefined;
188
242
  }
189
243
  finally {
190
244
  this.#closing = false;
@@ -192,14 +246,13 @@ export class HopperClient {
192
246
  }
193
247
  async #fallbackDocumentShutdown() {
194
248
  const fallback = await this.#request("shutdown_document", {}, { timeoutMs: SHUTDOWN_TIMEOUT_MS }).catch(() => err(new HopperProcessError(null)));
195
- if (!fallback.ok || !isShutdownAcknowledgement(fallback.value))
249
+ if (!fallback.ok || !isHopperShutdownAcknowledgement(fallback.value))
196
250
  this.#logger.warn({ status: fallback.ok ? "invalid-acknowledgement" : "failed" }, "Hopper document shutdown fallback was not confirmed");
197
251
  }
198
- async #connect(socketPath, signal) {
199
- const deadline = Date.now() + this.#options.startupTimeoutMs;
200
- while (Date.now() < deadline) {
201
- if (signal?.aborted === true)
202
- return err(new HopperCancelledError());
252
+ async #connect(socketPath, deadline) {
253
+ while (deadline.remainingMs() > 0) {
254
+ if (deadline.signal.aborted)
255
+ return err(startupInterruption(deadline));
203
256
  if (this.#closing)
204
257
  return err(new HopperProcessError(null));
205
258
  if (this.#launcherExitCode !== undefined &&
@@ -209,18 +262,18 @@ export class HopperClient {
209
262
  await access(socketPath);
210
263
  }
211
264
  catch {
212
- if (!(await waitForDelay(50, signal)))
213
- return err(new HopperCancelledError());
265
+ if ((await deadline.wait(50)) === "aborted")
266
+ return err(startupInterruption(deadline));
214
267
  continue;
215
268
  }
216
- const attempt = await connectOnce(socketPath);
269
+ const attempt = await connectHopperSocketOnce(socketPath, deadline.signal);
217
270
  if (attempt.ok) {
218
271
  this.#socket = attempt.value;
219
272
  this.#attachSocket(attempt.value);
220
273
  return ok(undefined);
221
274
  }
222
- if (!(await waitForDelay(50, signal)))
223
- return err(new HopperCancelledError());
275
+ if ((await deadline.wait(50)) === "aborted")
276
+ return err(startupInterruption(deadline));
224
277
  }
225
278
  return err(new HopperTimeoutError(this.#options.startupTimeoutMs));
226
279
  }
@@ -235,28 +288,15 @@ export class HopperClient {
235
288
  const id = this.#nextId++;
236
289
  const startedAt = performance.now();
237
290
  const timeoutMs = options.timeoutMs ?? this.#options.requestTimeoutMs;
238
- const response = new Promise((resolve) => {
239
- const onAbort = options.signal === undefined
240
- ? undefined
241
- : () => {
242
- this.#abandon(id, err(new HopperCancelledError()));
243
- };
244
- const timer = setTimeout(() => {
245
- this.#abandon(id, err(new HopperTimeoutError(timeoutMs)));
246
- }, timeoutMs);
247
- this.#pending.set(id, {
248
- resolve,
249
- timer,
250
- signal: options.signal,
251
- onAbort,
252
- });
253
- options.signal?.addEventListener("abort", onAbort ?? (() => undefined), {
254
- once: true,
255
- });
291
+ const response = this.#pending.wait(id, {
292
+ timeoutMs,
293
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
294
+ timeoutValue: () => err(new HopperTimeoutError(timeoutMs)),
295
+ cancelledValue: () => err(new HopperCancelledError()),
256
296
  });
257
297
  socket.write(`${JSON.stringify({ id, token, method, params })}\n`, (cause) => {
258
298
  if (cause !== undefined && cause !== null)
259
- this.#settle(id, err(new HopperProcessError(null)));
299
+ this.#pending.settle(id, err(new HopperProcessError(null)));
260
300
  });
261
301
  const result = await response;
262
302
  this.#logger[result.ok ? "debug" : "warn"]({
@@ -269,31 +309,41 @@ export class HopperClient {
269
309
  }
270
310
  #attachSocket(socket) {
271
311
  socket.setEncoding("utf8");
272
- socket.on("data", (chunk) => {
273
- this.#onData(chunk);
274
- });
275
- socket.on("error", () => {
276
- this.#failAll(new HopperProcessError(null));
277
- });
278
- socket.on("close", () => {
279
- if (!this.#closing)
280
- this.#failAll(new HopperProcessError(null));
281
- });
312
+ socket.on("data", this.#onSocketData);
313
+ socket.on("error", this.#onSocketError);
314
+ socket.on("close", this.#onSocketClose);
315
+ }
316
+ #detachSocket(socket) {
317
+ socket.off("data", this.#onSocketData);
318
+ socket.off("error", this.#onSocketError);
319
+ socket.off("close", this.#onSocketClose);
282
320
  }
283
321
  #attachLauncher(launch) {
284
- launch.process.stderr?.on("data", (chunk) => {
322
+ this.#process = new ProviderProcessSupervisor(launch, {
323
+ onDiagnostic: (event) => this.#onLauncherDiagnostic(launch, event),
324
+ });
325
+ }
326
+ #onLauncherDiagnostic(launch, event) {
327
+ if (event.type === "output" && event.stream === "stderr") {
285
328
  this.#options.onDiagnostic?.({
286
329
  type: "launcher-stderr",
287
- bytes: chunk.byteLength,
330
+ bytes: event.bytes,
288
331
  });
332
+ return;
333
+ }
334
+ if (event.type === "error") {
335
+ this.#logger.warn({ message: event.message }, "Hopper launcher process emitted an error");
336
+ return;
337
+ }
338
+ if (event.type !== "exit")
339
+ return;
340
+ this.#launcherExitCode = event.code;
341
+ this.#options.onDiagnostic?.({
342
+ type: "launcher-exit",
343
+ code: event.code,
289
344
  });
290
- launch.process.on("exit", (code) => {
291
- this.#launcherExitCode = code;
292
- this.#options.onDiagnostic?.({ type: "launcher-exit", code });
293
- if (launch.ownsProcessLifetime && !this.#closing) {
294
- this.#failAll(new HopperProcessError(code));
295
- }
296
- });
345
+ if (launch.ownsProcessLifetime && !this.#closing)
346
+ this.#failAll(new HopperProcessError(event.code));
297
347
  }
298
348
  #onData(chunk) {
299
349
  this.#buffer += chunk;
@@ -325,81 +375,23 @@ export class HopperClient {
325
375
  }
326
376
  return;
327
377
  }
328
- this.#settle(parsed.value.id, responseResult(parsed.value));
378
+ this.#pending.settle(parsed.value.id, responseResult(parsed.value));
329
379
  }
330
380
  #abortProtocol(message, cause) {
331
381
  this.#failAll(new HopperProtocolError(message, { cause }));
332
382
  this.#socket?.destroy();
333
383
  }
334
- #settle(id, result) {
335
- const pending = this.#pending.get(id);
336
- if (pending === undefined)
337
- return;
338
- this.#pending.delete(id);
339
- clearTimeout(pending.timer);
340
- if (pending.signal !== undefined && pending.onAbort !== undefined) {
341
- pending.signal.removeEventListener("abort", pending.onAbort);
342
- }
343
- pending.resolve(result);
344
- }
345
- #abandon(id, result) {
346
- if (!this.#pending.has(id))
347
- return;
348
- this.#settle(id, result);
349
- }
350
384
  #failAll(error) {
351
- for (const id of this.#pending.keys())
352
- this.#settle(id, err(error));
385
+ this.#pending.failAll(() => err(error));
353
386
  }
354
387
  }
355
388
  const isAborted = (signal) => signal?.aborted === true;
356
- const waitForDelay = (milliseconds, signal) => {
357
- if (signal?.aborted === true)
358
- return Promise.resolve(false);
359
- return new Promise((resolve) => {
360
- const timer = setTimeout(() => {
361
- signal?.removeEventListener("abort", onAbort);
362
- resolve(true);
363
- }, milliseconds);
364
- const onAbort = () => {
365
- clearTimeout(timer);
366
- resolve(false);
367
- };
368
- signal?.addEventListener("abort", onAbort, { once: true });
369
- });
370
- };
371
- const connectOnce = async (socketPath) => new Promise((resolve) => {
372
- const socket = createConnection(socketPath);
373
- socket.once("connect", () => {
374
- resolve(ok(socket));
375
- });
376
- socket.once("error", (cause) => {
377
- socket.destroy();
378
- resolve(err(new HopperStartError({ cause })));
379
- });
389
+ const startupInterruption = (deadline) => deadline.cancelled
390
+ ? new HopperCancelledError()
391
+ : new HopperTimeoutError(deadline.timeoutMs);
392
+ const providerCleanupFailure = (cause) => ({
393
+ cleaned: false,
394
+ reason: cause instanceof Error
395
+ ? `owned provider cleanup failed: ${cause.message}`
396
+ : "owned provider cleanup failed",
380
397
  });
381
- const isShutdownAcknowledgement = (value) => {
382
- if (typeof value !== "object" || value === null || Array.isArray(value))
383
- return false;
384
- if (value.shutdown !== true)
385
- return false;
386
- if (value.analysis_stopped === true && value.document_closed === true)
387
- return true;
388
- return false;
389
- };
390
- const isCleanupRequired = (value) => typeof value === "object" &&
391
- value !== null &&
392
- !Array.isArray(value) &&
393
- value.shutdown === true &&
394
- value.cleanup_required === true;
395
- const parseServerInfo = (value, expectedRunId) => {
396
- if (typeof value === "object" &&
397
- value !== null &&
398
- !Array.isArray(value) &&
399
- value.name === "REA Hopper bridge" &&
400
- typeof value.version === "string" &&
401
- value.run_id === expectedRunId) {
402
- return ok({ name: value.name, version: value.version });
403
- }
404
- return err(new HopperProtocolError("Hopper bridge health result is invalid"));
405
- };
@@ -1,15 +1,19 @@
1
1
  import { fileURLToPath } from "node:url";
2
+ import { accessSync, constants } from "node:fs";
2
3
  import { createAnalysisExecution } from "../application/AnalysisProvider.js";
3
4
  import { AnalysisCapabilityUnavailableError } from "../domain/errors.js";
4
5
  import { err } from "../domain/result.js";
5
6
  import { ENHANCED_TOOL_CONTRACTS, OFFICIAL_TOOL_CONTRACTS, } from "../contracts/toolContracts.js";
6
7
  import { HopperApplicationLauncher } from "./BridgeLauncher.js";
8
+ import { hopperLoaderArgsForTarget, resolveHopperAnalysisProfile, } from "./HopperAnalysisProfile.js";
7
9
  import { HopperClient } from "./HopperClient.js";
8
- const IDENTITY = Object.freeze({
10
+ /** Public identity committed by every Hopper-backed observation. */
11
+ export const HOPPER_PROVIDER_IDENTITY = Object.freeze({
9
12
  id: "hopper",
10
13
  name: "Hopper Disassembler",
11
14
  version: null,
12
15
  });
16
+ const IDENTITY = HOPPER_PROVIDER_IDENTITY;
13
17
  const MUTATING_OPERATIONS = new Set([
14
18
  "set_address_name",
15
19
  "set_addresses_names",
@@ -18,11 +22,12 @@ const MUTATING_OPERATIONS = new Set([
18
22
  "set_inline_comment",
19
23
  "unset_bookmark",
20
24
  ]);
21
- const PROVIDER_TOOL_CONTRACTS = [
25
+ /** Tool contracts implemented directly by the Hopper adapter. */
26
+ export const HOPPER_PROVIDER_TOOL_CONTRACTS = Object.freeze([
22
27
  ...OFFICIAL_TOOL_CONTRACTS,
23
28
  ...ENHANCED_TOOL_CONTRACTS.filter((contract) => contract.name === "analyze_function"),
24
- ];
25
- const CAPABILITIES = Object.freeze(PROVIDER_TOOL_CONTRACTS.map((contract) => Object.freeze({
29
+ ]);
30
+ const CAPABILITIES = Object.freeze(HOPPER_PROVIDER_TOOL_CONTRACTS.map((contract) => Object.freeze({
26
31
  provider: IDENTITY,
27
32
  operation: contract.name,
28
33
  inputContractVersion: 1,
@@ -63,12 +68,98 @@ export class HopperProvider {
63
68
  capabilities() {
64
69
  return CAPABILITIES;
65
70
  }
66
- createClient(target) {
71
+ inspectAvailability() {
72
+ const diagnostics = {
73
+ launcher_path: this.config.hopperLauncherPath,
74
+ platform: process.platform,
75
+ };
76
+ if (process.platform !== "darwin" && process.platform !== "linux")
77
+ return {
78
+ status: "unavailable",
79
+ code: "unsupported_host",
80
+ reason: `Hopper integration is not supported on ${process.platform}.`,
81
+ diagnostics,
82
+ };
83
+ try {
84
+ accessSync(this.config.hopperLauncherPath, constants.X_OK);
85
+ return {
86
+ status: "available",
87
+ code: null,
88
+ reason: null,
89
+ diagnostics,
90
+ };
91
+ }
92
+ catch {
93
+ return {
94
+ status: "unavailable",
95
+ code: "executable_missing",
96
+ reason: `Hopper launcher is missing or not executable: ${this.config.hopperLauncherPath}`,
97
+ diagnostics,
98
+ };
99
+ }
100
+ }
101
+ inspectTargetSupport(target) {
102
+ const diagnostics = {
103
+ target_kind: target.kind,
104
+ target_format: target.format,
105
+ architecture: target.architecture ?? null,
106
+ };
107
+ if (target.kind !== "executable" && target.kind !== "database")
108
+ return {
109
+ status: "unsupported",
110
+ code: "target_kind_unsupported",
111
+ reason: `Hopper cannot directly analyze ${target.kind} targets.`,
112
+ diagnostics,
113
+ };
114
+ if (target.kind === "database")
115
+ return {
116
+ status: "supported",
117
+ code: null,
118
+ reason: null,
119
+ diagnostics,
120
+ };
121
+ if (!["mach-o", "elf", "pe"].includes(target.format))
122
+ return {
123
+ status: "unsupported",
124
+ code: "target_format_unsupported",
125
+ reason: `Hopper cannot open ${target.format} through this adapter.`,
126
+ diagnostics,
127
+ };
128
+ if (target.architecture === undefined)
129
+ return {
130
+ status: "unsupported",
131
+ code: "architecture_unsupported",
132
+ reason: "Hopper requires a supported concrete target architecture.",
133
+ diagnostics,
134
+ };
135
+ return {
136
+ status: "supported",
137
+ code: null,
138
+ reason: null,
139
+ diagnostics,
140
+ };
141
+ }
142
+ resolveAnalysisProfile(target, options) {
143
+ return resolveHopperAnalysisProfile(target, {
144
+ launcherPath: this.config.hopperLauncherPath,
145
+ loaderArgsOverride: this.config.hopperLoaderArgs,
146
+ provider: IDENTITY,
147
+ ...(options?.signal === undefined ? {} : { signal: options.signal }),
148
+ });
149
+ }
150
+ createClient(target, profile) {
67
151
  if (target.kind !== "executable" && target.kind !== "database")
68
152
  return {
69
153
  execute: (operation) => Promise.resolve(err(new AnalysisCapabilityUnavailableError(IDENTITY.id, operation, `Hopper cannot open ${target.kind} targets directly. Inventory or extract the artifact first.`))),
70
154
  close: () => Promise.resolve(),
71
155
  };
156
+ const derivedLoaderArgs = hopperLoaderArgsForTarget(target);
157
+ if (!derivedLoaderArgs.ok)
158
+ return {
159
+ execute: () => Promise.resolve(err(derivedLoaderArgs.error)),
160
+ close: () => Promise.resolve(),
161
+ };
162
+ const executionProvider = profile?.provider ?? IDENTITY;
72
163
  const client = new HopperClient({
73
164
  launcher: new HopperApplicationLauncher({
74
165
  launcherPath: this.config.hopperLauncherPath,
@@ -76,7 +167,7 @@ export class HopperProvider {
76
167
  targetKind: target.kind,
77
168
  loaderArgs: this.config.hopperLoaderArgs.length > 0
78
169
  ? this.config.hopperLoaderArgs
79
- : target.loaderArgs,
170
+ : derivedLoaderArgs.value,
80
171
  bridgeScriptPath: fileURLToPath(new URL("../../bridge/hopper_bridge.py", import.meta.url)),
81
172
  ...(process.platform === "linux"
82
173
  ? {
@@ -93,7 +184,8 @@ export class HopperProvider {
93
184
  return result.ok
94
185
  ? {
95
186
  ok: true,
96
- value: createAnalysisExecution(result.value, IDENTITY, {
187
+ value: createAnalysisExecution(result.value, executionProvider, {
188
+ ...(profile === undefined ? {} : { analysisProfile: profile }),
97
189
  limitations: [
98
190
  "Results depend on Hopper's completed static analysis.",
99
191
  ],
@@ -0,0 +1,27 @@
1
+ import { HopperProtocolError } from "../domain/errors.js";
2
+ import { err, ok } from "../domain/result.js";
3
+ /** Validate the bridge identity and exact per-launch run identifier. */
4
+ export const parseHopperServerInfo = (value, expectedRunId) => {
5
+ if (typeof value === "object" &&
6
+ value !== null &&
7
+ !Array.isArray(value) &&
8
+ value.name === "REA Hopper bridge" &&
9
+ typeof value.version === "string" &&
10
+ value.run_id === expectedRunId)
11
+ return ok({ name: value.name, version: value.version });
12
+ return err(new HopperProtocolError("Hopper bridge health result is invalid"));
13
+ };
14
+ /** Require Hopper to confirm both analysis and document shutdown. */
15
+ export const isHopperShutdownAcknowledgement = (value) => {
16
+ if (typeof value !== "object" || value === null || Array.isArray(value))
17
+ return false;
18
+ if (value.shutdown !== true)
19
+ return false;
20
+ return value.analysis_stopped === true && value.document_closed === true;
21
+ };
22
+ /** Detect the verified Linux bridge response that requires group cleanup. */
23
+ export const isHopperCleanupRequired = (value) => typeof value === "object" &&
24
+ value !== null &&
25
+ !Array.isArray(value) &&
26
+ value.shutdown === true &&
27
+ value.cleanup_required === true;