rea-agents 2.4.0 → 3.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 (231) hide show
  1. package/README.md +91 -19
  2. package/bridge/ghidra/ReaGhidraBridge.java +524 -4
  3. package/bridge/hopper_bridge.py +313 -28
  4. package/dist/application/AnalysisClientCleanup.js +14 -0
  5. package/dist/application/AnalysisContextQueries.js +130 -0
  6. package/dist/application/AnalysisSnapshotCache.js +4 -2
  7. package/dist/application/ApplicationWorkflowEvidenceResolver.js +31 -13
  8. package/dist/application/ArtifactInventory/scanCanonical.js +1 -1
  9. package/dist/application/BinarySession.js +20 -8
  10. package/dist/application/BinarySessionComposition.js +21 -0
  11. package/dist/application/BinarySessionRecords.js +79 -9
  12. package/dist/application/BinarySessionStatus.js +31 -1
  13. package/dist/application/BrowserEvidence.js +14 -0
  14. package/dist/application/BrowserScenarioCapturePort.js +1 -0
  15. package/dist/application/BrowserScenarioCaptureService.js +53 -0
  16. package/dist/application/BrowserScenarioEvidence.js +40 -0
  17. package/dist/application/CallPathTracing.js +160 -0
  18. package/dist/application/CapabilityInventory.js +84 -14
  19. package/dist/application/ClientRegistrationStatus.js +4 -1
  20. package/dist/application/CompositeProvider.js +10 -1
  21. package/dist/application/DirectAnalysis.js +13 -4
  22. package/dist/application/EnhancedLiteralTracing.js +118 -0
  23. package/dist/application/EnhancedTools.js +71 -127
  24. package/dist/application/EvidenceLedger.js +45 -16
  25. package/dist/application/JavaScriptApplicationWorkflowEvidence.js +13 -0
  26. package/dist/application/JavaScriptApplicationWorkflowService.js +27 -1
  27. package/dist/application/JavaScriptArtifactAnalysis.js +13 -4
  28. package/dist/application/JavaScriptRuntimeObservationEvidence.js +34 -0
  29. package/dist/application/JavaScriptRuntimeObservationPort.js +1 -0
  30. package/dist/application/JavaScriptRuntimeObservationService.js +46 -0
  31. package/dist/application/JavaScriptSemanticGraphAsyncProjection.js +147 -0
  32. package/dist/application/JavaScriptSemanticGraphBuilder.js +34 -6
  33. package/dist/application/JavaScriptSemanticGraphChildProcessProjection.js +156 -0
  34. package/dist/application/JavaScriptSemanticGraphConstruction.js +2 -2
  35. package/dist/application/JavaScriptSemanticGraphDataProjection.js +203 -0
  36. package/dist/application/JavaScriptSemanticGraphFingerprintProjection.js +25 -0
  37. package/dist/application/JavaScriptSemanticGraphFlowProjection.js +129 -8
  38. package/dist/application/JavaScriptSemanticGraphObjectProjection.js +81 -0
  39. package/dist/application/JavaScriptSemanticGraphProjection.js +14 -7
  40. package/dist/application/JavaScriptSemanticGraphResourceProjection.js +59 -0
  41. package/dist/application/JavaScriptSemanticGraphValueProjection.js +77 -0
  42. package/dist/application/LazyAnalysisProvider.js +77 -0
  43. package/dist/application/LoopbackReplay.js +1 -1
  44. package/dist/application/NativeApiInspection.js +80 -0
  45. package/dist/application/ProcessReactiveEffects.js +6 -0
  46. package/dist/application/ProcessReactiveHarness.js +16 -1
  47. package/dist/application/ProjectPermissionStore.js +1 -0
  48. package/dist/application/ReconstructionObligationAuthority.js +72 -0
  49. package/dist/application/ReconstructionObligationCandidates.js +265 -0
  50. package/dist/application/ReconstructionObligationLedgerDiagnostics.js +47 -0
  51. package/dist/application/ReconstructionObligationLedgerEvaluation.js +263 -0
  52. package/dist/application/ReconstructionObligationLedgerProjection.js +86 -0
  53. package/dist/application/ReconstructionObligationLedgerService.js +72 -0
  54. package/dist/application/ReconstructionObligationOriginalCases.js +33 -0
  55. package/dist/application/ReconstructionObligationPolicies.js +132 -0
  56. package/dist/application/ReconstructionReadinessService.js +48 -0
  57. package/dist/application/SessionProviderRouter.js +6 -6
  58. package/dist/application/SetupClientConfiguration.js +4 -2
  59. package/dist/application/runtime.js +26 -9
  60. package/dist/artifacts/ArtifactProvider.js +87 -22
  61. package/dist/browser/BrowserScenarioSecrets.js +53 -0
  62. package/dist/browser/BrowserScenarioSessionPort.js +1 -0
  63. package/dist/browser/CdpBrowserProvider.js +2 -2
  64. package/dist/browser/CdpCaptureDocuments.js +57 -0
  65. package/dist/browser/CdpCaptureStorage.js +71 -29
  66. package/dist/browser/CdpCaptureStorageFingerprints.js +328 -0
  67. package/dist/browser/CdpEndpoint.js +8 -6
  68. package/dist/browser/CdpPageCapture.js +2 -0
  69. package/dist/browser/JavaScriptRuntimeScope.js +38 -0
  70. package/dist/browser/PlaywrightBrowserScenarioProvider.js +259 -0
  71. package/dist/browser/PlaywrightExecutionBoundary.js +42 -0
  72. package/dist/browser/PlaywrightScenarioActions.js +62 -0
  73. package/dist/browser/PlaywrightScenarioArtifacts.js +238 -0
  74. package/dist/browser/PlaywrightScenarioBrowser.js +144 -0
  75. package/dist/browser/PlaywrightScenarioEvents.js +301 -0
  76. package/dist/browser/PlaywrightScenarioSession.js +300 -0
  77. package/dist/browser/V8InspectorCaptureProjection.js +110 -0
  78. package/dist/browser/V8InspectorEndpoint.js +72 -0
  79. package/dist/browser/V8InspectorProvider.js +267 -0
  80. package/dist/catalogIdentity.js +11 -3
  81. package/dist/cli/artifactCommands.js +44 -3
  82. package/dist/cli/coreAnalysisCommands.js +78 -0
  83. package/dist/cli.js +4 -0
  84. package/dist/cliApplicationCommands.js +30 -2
  85. package/dist/cliBrowserAdvancedCommands.js +17 -5
  86. package/dist/cliBrowserCommands.js +1 -0
  87. package/dist/cliBrowserScenarioCommands.js +43 -0
  88. package/dist/cliCommandNames.js +9 -0
  89. package/dist/cliJavaScriptRuntimeCommands.js +164 -0
  90. package/dist/cliObservationOptions.js +4 -0
  91. package/dist/cliPolicyCommands.js +1 -0
  92. package/dist/config/environment.js +12 -0
  93. package/dist/config/parseConfig.js +88 -17
  94. package/dist/contracts/analysisSearchContract.js +14 -0
  95. package/dist/contracts/applicationToolContracts.js +86 -2
  96. package/dist/contracts/applicationWorkflowInputContracts.js +18 -0
  97. package/dist/contracts/artifactToolContracts.js +29 -2
  98. package/dist/contracts/browserProviderToolContracts.js +7 -0
  99. package/dist/contracts/browserScenarioToolContracts.js +81 -0
  100. package/dist/contracts/browserToolContracts.js +88 -98
  101. package/dist/contracts/enhancedInputs.js +35 -6
  102. package/dist/contracts/functionInstructionContract.js +8 -0
  103. package/dist/contracts/functionWorkflowToolContracts.js +30 -0
  104. package/dist/contracts/hopperMemoryContracts.js +22 -0
  105. package/dist/contracts/javascriptApplicationWorkflowExamples.js +42 -0
  106. package/dist/contracts/javascriptRuntimeObservationToolContracts.js +58 -0
  107. package/dist/contracts/promptContracts.js +14 -15
  108. package/dist/contracts/reconstructionReadinessExample.js +436 -0
  109. package/dist/contracts/toolContractExamples.js +8 -0
  110. package/dist/contracts/toolContractTypes.js +1 -0
  111. package/dist/contracts/toolContracts.js +34 -25
  112. package/dist/contracts/toolEffects.js +25 -0
  113. package/dist/contracts/toolOutputSchemaGroups.js +120 -30
  114. package/dist/contracts/toolOutputSchemaPrimitives.js +37 -0
  115. package/dist/domain/analysisErrorPresentation.js +8 -2
  116. package/dist/domain/analysisErrorProjection.js +9 -0
  117. package/dist/domain/artifactInspection.js +347 -0
  118. package/dist/domain/browserCaptureComparison.js +82 -0
  119. package/dist/domain/browserObservation.js +8 -1
  120. package/dist/domain/browserObservationErrors.js +1 -0
  121. package/dist/domain/browserObservationSchemas.js +26 -0
  122. package/dist/domain/browserScenario.js +165 -0
  123. package/dist/domain/browserScenarioCapture.js +57 -0
  124. package/dist/domain/browserScenarioCaptureValues.js +204 -0
  125. package/dist/domain/browserScenarioDiff.js +324 -0
  126. package/dist/domain/browserScenarioDiffValues.js +157 -0
  127. package/dist/domain/browserScenarioNormalization.js +45 -0
  128. package/dist/domain/browserScenarioValues.js +372 -0
  129. package/dist/domain/bytecodeProvider.js +179 -0
  130. package/dist/domain/conformancePackage.js +169 -0
  131. package/dist/domain/conformanceReplay.js +104 -0
  132. package/dist/domain/conformanceTrustGate.js +193 -0
  133. package/dist/domain/customProtocolCapture.js +137 -0
  134. package/dist/domain/eventProcessTree.js +214 -0
  135. package/dist/domain/evidenceBundle.js +47 -0
  136. package/dist/domain/hopperStartupFailure.js +1 -1
  137. package/dist/domain/hopperValues.js +14 -1
  138. package/dist/domain/inputIssueProjection.js +7 -1
  139. package/dist/domain/javascriptRuntimeObservation.js +170 -0
  140. package/dist/domain/javascriptRuntimeReconciliationParsing.js +120 -2
  141. package/dist/domain/javascriptRuntimeReconciliationResult.js +3 -3
  142. package/dist/domain/javascriptRuntimeReconciliationRuntime.js +2 -2
  143. package/dist/domain/javascriptRuntimeReconciliationSchemas.js +1 -1
  144. package/dist/domain/javascriptSemanticAnalysis.js +13 -16
  145. package/dist/domain/javascriptSemanticAsyncEffects.js +253 -0
  146. package/dist/domain/javascriptSemanticChildProcesses.js +237 -0
  147. package/dist/domain/javascriptSemanticDataEffectHelpers.js +125 -0
  148. package/dist/domain/javascriptSemanticDataEffects.js +284 -0
  149. package/dist/domain/javascriptSemanticDerivedAnalysis.js +41 -0
  150. package/dist/domain/javascriptSemanticFingerprints.js +174 -0
  151. package/dist/domain/javascriptSemanticIr.js +21 -1
  152. package/dist/domain/javascriptSemanticObjects.js +115 -0
  153. package/dist/domain/javascriptSemanticPromises.js +290 -0
  154. package/dist/domain/javascriptSemanticResources.js +136 -0
  155. package/dist/domain/javascriptSourceParser.js +15 -0
  156. package/dist/domain/javascriptStaticAnalysis.js +12 -15
  157. package/dist/domain/javascriptStaticAnalysisBundler.js +38 -0
  158. package/dist/domain/javascriptStaticAnalysisCalls.js +23 -1
  159. package/dist/domain/javascriptStaticAnalysisHelpers.js +8 -0
  160. package/dist/domain/mobileApplicationInvestigation.js +159 -0
  161. package/dist/domain/nativeApiBoundary.js +130 -0
  162. package/dist/domain/objcSwiftMetadata.js +150 -0
  163. package/dist/domain/packageAnalysis.js +123 -0
  164. package/dist/domain/peInspection.js +196 -0
  165. package/dist/domain/processCapture.js +8 -2
  166. package/dist/domain/processReactiveScenario.js +1 -0
  167. package/dist/domain/processReactiveTransition.js +9 -4
  168. package/dist/domain/protocolCapture.js +243 -0
  169. package/dist/domain/providerCleanupError.js +11 -0
  170. package/dist/domain/reconstructionObligationLedgerSchemas.js +278 -0
  171. package/dist/domain/reconstructionReadiness.js +121 -0
  172. package/dist/domain/reconstructionReadinessFindingHelpers.js +17 -0
  173. package/dist/domain/reconstructionReadinessIntegrityFindings.js +118 -0
  174. package/dist/domain/reconstructionReadinessJourneyFindings.js +193 -0
  175. package/dist/domain/reconstructionReadinessSchemas.js +240 -0
  176. package/dist/domain/sourceToBundleComparison.js +335 -0
  177. package/dist/domain/sourceToBundleComparisonSchemas.js +133 -0
  178. package/dist/domain/sourceToBundleSignals.js +202 -0
  179. package/dist/domain/webCaptureDiff.js +141 -27
  180. package/dist/domain/webScreenshot.js +1 -1
  181. package/dist/dotnet/ManagedStaticProvider.js +34 -5
  182. package/dist/generatedMcpToolCatalog.js +37 -0
  183. package/dist/generatedPackageMetadata.js +4 -4
  184. package/dist/ghidra/GhidraDefaults.js +1 -1
  185. package/dist/ghidra/GhidraFunctionValues.js +40 -2
  186. package/dist/ghidra/GhidraProviderCapabilities.js +47 -38
  187. package/dist/hopper/HopperCleanup.js +158 -0
  188. package/dist/hopper/HopperClient.js +93 -99
  189. package/dist/hopper/HopperProvider.js +61 -30
  190. package/dist/hopper/HopperRequestQueue.js +215 -0
  191. package/dist/hopper/HopperResponseStream.js +46 -0
  192. package/dist/hopper/protocol.js +63 -20
  193. package/dist/main/transport.js +24 -27
  194. package/dist/main.js +14 -6
  195. package/dist/mcpDoctor.js +27 -4
  196. package/dist/mcpStartupPolicy.js +14 -0
  197. package/dist/process/ProcessCommandIdentity.js +19 -0
  198. package/dist/process/ProcessOwnership.js +11 -8
  199. package/dist/server/DynamicToolAvailability.js +70 -0
  200. package/dist/server/LazyToolCatalog.js +134 -0
  201. package/dist/server/createServer.js +92 -154
  202. package/dist/server/hydrateServerTools.js +191 -0
  203. package/dist/server/lifecycleProgress.js +10 -0
  204. package/dist/server/permissionFailure.js +12 -0
  205. package/dist/server/registerApplicationTools/compareSourceToBundle.js +35 -0
  206. package/dist/server/registerApplicationTools/helpers.js +11 -7
  207. package/dist/server/registerApplicationTools/obligationLedger.js +27 -0
  208. package/dist/server/registerApplicationTools/readiness.js +55 -0
  209. package/dist/server/registerApplicationTools.js +6 -0
  210. package/dist/server/registerArtifactTools.js +9 -1
  211. package/dist/server/registerBrowserScenarioTool.js +25 -0
  212. package/dist/server/registerBrowserTools.js +2 -2
  213. package/dist/server/registerCloseLifecycleTool.js +57 -0
  214. package/dist/server/registerEnhancedTools.js +28 -7
  215. package/dist/server/registerEvidenceResources.js +54 -101
  216. package/dist/server/registerEvidenceTools.js +9 -2
  217. package/dist/server/registerInvestigationTools.js +1 -2
  218. package/dist/server/registerJavaScriptApplicationGraphResource.js +0 -4
  219. package/dist/server/registerJavaScriptRuntimeObservationTools.js +37 -0
  220. package/dist/server/registerReconstructionObligationLedgerResource.js +32 -0
  221. package/dist/server/registerReconstructionReadinessResource.js +32 -0
  222. package/dist/server/registerSessionRecordTools.js +82 -12
  223. package/dist/server/registerSessionStatusTool.js +11 -8
  224. package/dist/server/registerSessionTools.js +32 -58
  225. package/dist/server/sessionAvailabilityPolicy.js +10 -8
  226. package/dist/server/toolRegistrationOptions.js +21 -6
  227. package/dist/server/toolResult.js +12 -0
  228. package/package.json +28 -11
  229. package/scripts/package-runner-bootstrap.mjs +72 -0
  230. package/scripts/rea.mjs +11 -1
  231. package/skills/reverse-engineer-anything/SKILL.md +10 -3
@@ -2,21 +2,22 @@ import { randomBytes, randomUUID } from "node:crypto";
2
2
  import { access } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
- import { HopperCancelledError, HopperProcessError, HopperProtocolError, HopperStartError, hopperStartupFailure, HopperTimeoutError, } from "../domain/errors.js";
5
+ import { AnalysisCapabilityUnavailableError, HopperCancelledError, HopperProcessError, HopperProtocolError, HopperRemoteError, HopperStartError, hopperStartupFailure, HopperTimeoutError, } from "../domain/errors.js";
6
6
  import { err, ok } from "../domain/result.js";
7
7
  import { silentLogger } from "../logger.js";
8
- import { PendingOperations } from "../process/PendingOperations.js";
9
8
  import { PrivateRuntimeRoot } from "../process/PrivateRuntimeRoot.js";
10
9
  import { ProviderStartupDeadline } from "../process/ProviderDeadline.js";
11
10
  import { ProviderRunLineage } from "../process/ProviderRunLineage.js";
12
11
  import { ProviderProcessSupervisor, } from "../process/ProviderProcess.js";
13
- import { createOwnedHopperShutdownDiagnostic, providerCleanupFailure, } from "./HopperDiagnostics.js";
14
- import { isHopperCleanupRequired, isHopperShutdownAcknowledgement, parseHopperServerInfo, } from "./HopperSessionValues.js";
12
+ import { cleanupHopperSession } from "./HopperCleanup.js";
13
+ import { parseHopperServerInfo, } from "./HopperSessionValues.js";
15
14
  import { connectHopperSocketOnce } from "./HopperSocketConnection.js";
16
15
  import { hopperLauncherFailureDiagnostic } from "./HopperProcessDiagnostic.js";
17
- import { parseResponseLine, responseResult } from "./protocol.js";
18
- const MAX_LINE_BYTES = 10 * 1024 * 1024;
16
+ import { HopperRequestQueue, } from "./HopperRequestQueue.js";
17
+ import { HopperResponseStream } from "./HopperResponseStream.js";
18
+ import { responseResult, } from "./protocol.js";
19
19
  const SHUTDOWN_TIMEOUT_MS = 30_000;
20
+ const MAX_QUEUED_REQUESTS = 64;
20
21
  const SESSION_ROOT = process.platform === "darwin" ? "/tmp" : tmpdir();
21
22
  /** Safe launcher telemetry; stderr content is intentionally never exposed. */
22
23
  /**
@@ -29,7 +30,8 @@ const SESSION_ROOT = process.platform === "darwin" ? "/tmp" : tmpdir();
29
30
  */
30
31
  export class HopperClient {
31
32
  #options;
32
- #pending = new PendingOperations();
33
+ #requests;
34
+ #responses;
33
35
  #logger;
34
36
  #socket;
35
37
  #launch;
@@ -38,7 +40,6 @@ export class HopperClient {
38
40
  #token;
39
41
  #lineage = new ProviderRunLineage();
40
42
  #nextId = 1;
41
- #buffer = "";
42
43
  #closing = false;
43
44
  #launcherExitCode;
44
45
  #launcherFailureDiagnostic;
@@ -46,7 +47,7 @@ export class HopperClient {
46
47
  #startPromise;
47
48
  #closePromise;
48
49
  #onSocketData = (chunk) => {
49
- this.#onData(chunk);
50
+ this.#responses.push(chunk);
50
51
  };
51
52
  #onSocketError = () => {
52
53
  this.#failAll(new HopperProcessError(null));
@@ -62,11 +63,33 @@ export class HopperClient {
62
63
  requestTimeoutMs: options.requestTimeoutMs ?? 30_000,
63
64
  startupTimeoutMs: options.startupTimeoutMs ?? 120_000,
64
65
  };
66
+ this.#requests = new HopperRequestQueue(MAX_QUEUED_REQUESTS, ({ id, method, params }, failed) => {
67
+ const socket = this.#socket;
68
+ const token = this.#token;
69
+ if (socket === undefined || socket.destroyed || token === undefined) {
70
+ failed();
71
+ return;
72
+ }
73
+ socket.write(`${JSON.stringify({ id, token, method, params })}\n`, (cause) => {
74
+ if (cause !== undefined && cause !== null)
75
+ failed();
76
+ });
77
+ });
78
+ this.#responses = new HopperResponseStream({
79
+ accept: (message) => this.#acceptBridgeMessage(message),
80
+ hasQueued: (id) => this.#requests.hasQueued(id),
81
+ nextRequestId: () => this.#nextId,
82
+ abort: (message, cause) => this.#abortProtocol(message, cause),
83
+ });
65
84
  }
66
85
  /** Latest token-verified launcher lineage for the active run. */
67
86
  runtimeLineage() {
68
87
  return this.#lineage.snapshot();
69
88
  }
89
+ /** Observe a request that still occupies Hopper after caller timeout/cancel. */
90
+ requestActivity() {
91
+ return this.#requests.activity();
92
+ }
70
93
  /** Launch the bridge once and complete its authenticated health handshake. */
71
94
  start(signal) {
72
95
  if (this.#closePromise !== undefined) {
@@ -186,20 +209,29 @@ export class HopperClient {
186
209
  const started = await this.start(options.signal);
187
210
  if (!started.ok)
188
211
  return started;
189
- return this.#request(name, arguments_, options);
212
+ const result = await this.#request(name, arguments_, options);
213
+ return !result.ok &&
214
+ result.error instanceof HopperRemoteError &&
215
+ result.error.diagnosticType === "capability_unavailable"
216
+ ? err(new AnalysisCapabilityUnavailableError("hopper", name, result.error.safeMessage))
217
+ : result;
190
218
  }
191
219
  /** Stop the bridge, settle outstanding requests, and remove session artifacts. */
192
220
  close() {
221
+ return this.closeWithOutcome().then(() => undefined);
222
+ }
223
+ /** Stop the bridge and report whether every owned resource was verified clean. */
224
+ closeWithOutcome(options = {}) {
193
225
  const starting = this.#startPromise;
194
226
  const controller = this.#startupController;
195
227
  controller?.abort();
196
- this.#closePromise ??= Promise.resolve().then(() => this.#close(starting, controller));
228
+ this.#closePromise ??= Promise.resolve().then(() => this.#close(starting, controller, options));
197
229
  return this.#closePromise;
198
230
  }
199
- async #close(starting, controller) {
231
+ async #close(starting, controller, options) {
200
232
  try {
201
233
  await starting?.catch(() => undefined);
202
- await this.#cleanup();
234
+ return await this.#cleanup(options);
203
235
  }
204
236
  finally {
205
237
  if (this.#startPromise === starting)
@@ -210,63 +242,39 @@ export class HopperClient {
210
242
  this.#closePromise = undefined;
211
243
  }
212
244
  }
213
- async #cleanup() {
245
+ async #cleanup(options = {}) {
214
246
  this.#closing = true;
215
247
  try {
216
- const socket = this.#socket;
217
- let cleanupResult;
218
- if (socket !== undefined && !socket.destroyed) {
219
- const shutdown = await this.#request("shutdown", {}, { timeoutMs: SHUTDOWN_TIMEOUT_MS }).catch(() => err(new HopperProcessError(null)));
220
- if (shutdown.ok &&
221
- isHopperCleanupRequired(shutdown.value) &&
222
- this.#launch?.shutdownByCleanup === true &&
223
- this.#launch.cleanup !== undefined) {
224
- cleanupResult = await this.#launch
225
- .cleanup()
226
- .catch(providerCleanupFailure);
227
- if (!cleanupResult.cleaned) {
228
- await this.#fallbackDocumentShutdown();
229
- cleanupResult = await this.#launch
230
- .cleanup()
231
- .catch(providerCleanupFailure);
232
- }
233
- }
234
- else if (!shutdown.ok ||
235
- !isHopperShutdownAcknowledgement(shutdown.value))
236
- this.#logger.warn({ status: shutdown.ok ? "invalid-acknowledgement" : "failed" }, "Hopper document shutdown was not confirmed");
237
- }
238
- this.#failAll(new HopperProcessError(null));
239
- if (socket !== undefined)
240
- this.#detachSocket(socket);
241
- socket?.destroy();
248
+ return await cleanupHopperSession({
249
+ socket: this.#socket,
250
+ launch: this.#launch,
251
+ processSupervisor: this.#process,
252
+ runtimeRoot: this.#runtimeRoot,
253
+ activeRequest: this.#requests.activity(),
254
+ progress: options.progress,
255
+ logger: this.#logger,
256
+ onDiagnostic: this.#options.onDiagnostic,
257
+ request: (method) => this.#request(method, {}, { timeoutMs: SHUTDOWN_TIMEOUT_MS }),
258
+ releaseTransport: (socket) => {
259
+ this.#failAll(new HopperProcessError(null));
260
+ if (socket !== undefined)
261
+ this.#detachSocket(socket);
262
+ socket?.destroy();
263
+ },
264
+ });
265
+ }
266
+ finally {
242
267
  this.#socket = undefined;
243
- if (this.#process !== undefined) {
244
- const stopped = await this.#process.stop(cleanupResult === undefined ? {} : { cleanupResult });
245
- const diagnostic = createOwnedHopperShutdownDiagnostic(this.#process.launch, stopped, cleanupResult);
246
- this.#options.onDiagnostic?.(diagnostic);
247
- this.#logger.info(diagnostic, "Owned Hopper launcher shutdown completed");
248
- if (stopped.status === "incomplete")
249
- this.#logger.warn({ reason: stopped.reason }, "Owned launcher cleanup failed closed");
250
- }
251
268
  this.#process = undefined;
252
269
  this.#launch = undefined;
253
- const runtimeRoot = this.#runtimeRoot;
254
270
  this.#runtimeRoot = undefined;
255
- await runtimeRoot?.close();
256
271
  this.#token = undefined;
257
- this.#buffer = "";
272
+ this.#responses.reset();
258
273
  this.#launcherExitCode = undefined;
259
274
  this.#launcherFailureDiagnostic = undefined;
260
- }
261
- finally {
262
275
  this.#closing = false;
263
276
  }
264
277
  }
265
- async #fallbackDocumentShutdown() {
266
- const fallback = await this.#request("shutdown_document", {}, { timeoutMs: SHUTDOWN_TIMEOUT_MS }).catch(() => err(new HopperProcessError(null)));
267
- if (!fallback.ok || !isHopperShutdownAcknowledgement(fallback.value))
268
- this.#logger.warn({ status: fallback.ok ? "invalid-acknowledgement" : "failed" }, "Hopper document shutdown fallback was not confirmed");
269
- }
270
278
  async #connect(socketPath, deadline) {
271
279
  while (deadline.remainingMs() > 0) {
272
280
  if (deadline.signal.aborted)
@@ -306,17 +314,11 @@ export class HopperClient {
306
314
  const id = this.#nextId++;
307
315
  const startedAt = performance.now();
308
316
  const timeoutMs = options.timeoutMs ?? this.#options.requestTimeoutMs;
309
- const response = this.#pending.wait(id, {
317
+ const result = await this.#requests.run(id, method, params, {
310
318
  timeoutMs,
311
319
  ...(options.signal === undefined ? {} : { signal: options.signal }),
312
- timeoutValue: () => err(new HopperTimeoutError(timeoutMs)),
313
- cancelledValue: () => err(new HopperCancelledError()),
320
+ ...(options.progress !== undefined ? { progress: options.progress } : {}),
314
321
  });
315
- socket.write(`${JSON.stringify({ id, token, method, params })}\n`, (cause) => {
316
- if (cause !== undefined && cause !== null)
317
- this.#pending.settle(id, err(new HopperProcessError(null)));
318
- });
319
- const result = await response;
320
322
  this.#logger[result.ok ? "debug" : "warn"]({
321
323
  method,
322
324
  durationMs: Math.round((performance.now() - startedAt) * 100) / 100,
@@ -331,6 +333,30 @@ export class HopperClient {
331
333
  socket.on("error", this.#onSocketError);
332
334
  socket.on("close", this.#onSocketClose);
333
335
  }
336
+ #acceptBridgeMessage(message) {
337
+ if (!("event" in message))
338
+ return this.#requests.accept(message.id, responseResult(message));
339
+ if (!this.#requests.acceptEvent(message))
340
+ return false;
341
+ this.#emitBridgeDiagnostic(message);
342
+ return true;
343
+ }
344
+ #emitBridgeDiagnostic(message) {
345
+ if (message.event.type !== "diagnostic")
346
+ return;
347
+ try {
348
+ this.#options.onDiagnostic?.({
349
+ type: "bridge-diagnostic",
350
+ request_id: message.id,
351
+ code: message.event.error.code,
352
+ category: message.event.error.type,
353
+ message: message.event.error.message,
354
+ });
355
+ }
356
+ catch {
357
+ this.#logger.warn({ requestId: message.id }, "Hopper bridge diagnostic consumer rejected an event");
358
+ }
359
+ }
334
360
  #detachSocket(socket) {
335
361
  socket.off("data", this.#onSocketData);
336
362
  socket.off("error", this.#onSocketError);
@@ -364,44 +390,12 @@ export class HopperClient {
364
390
  if (launch.ownsProcessLifetime && !this.#closing)
365
391
  this.#failAll(new HopperProcessError(event.code, this.#launcherFailureDiagnostic));
366
392
  }
367
- #onData(chunk) {
368
- this.#buffer += chunk;
369
- let newline = this.#buffer.indexOf("\n");
370
- while (newline >= 0) {
371
- const line = this.#buffer.slice(0, newline).trim();
372
- this.#buffer = this.#buffer.slice(newline + 1);
373
- if (Buffer.byteLength(line) > MAX_LINE_BYTES) {
374
- this.#abortProtocol("Hopper response exceeded the maximum line size");
375
- return;
376
- }
377
- if (line.length > 0)
378
- this.#onLine(line);
379
- newline = this.#buffer.indexOf("\n");
380
- }
381
- if (Buffer.byteLength(this.#buffer) > MAX_LINE_BYTES) {
382
- this.#abortProtocol("Hopper response exceeded the maximum line size");
383
- }
384
- }
385
- #onLine(line) {
386
- const parsed = parseResponseLine(line);
387
- if (!parsed.ok) {
388
- this.#abortProtocol(parsed.error.message, parsed.error);
389
- return;
390
- }
391
- if (!this.#pending.has(parsed.value.id)) {
392
- if (parsed.value.id >= this.#nextId) {
393
- this.#abortProtocol("Hopper returned an unknown response id");
394
- }
395
- return;
396
- }
397
- this.#pending.settle(parsed.value.id, responseResult(parsed.value));
398
- }
399
393
  #abortProtocol(message, cause) {
400
394
  this.#failAll(new HopperProtocolError(message, { cause }));
401
395
  this.#socket?.destroy();
402
396
  }
403
397
  #failAll(error) {
404
- this.#pending.failAll(() => err(error));
398
+ this.#requests.failAll(error);
405
399
  }
406
400
  }
407
401
  const isAborted = (signal) => signal?.aborted === true;
@@ -3,7 +3,7 @@ import { accessSync, constants } from "node:fs";
3
3
  import { createAnalysisExecution } from "../application/AnalysisProvider.js";
4
4
  import { AnalysisCapabilityUnavailableError } from "../domain/errors.js";
5
5
  import { err } from "../domain/result.js";
6
- import { ENHANCED_TOOL_CONTRACTS, OFFICIAL_TOOL_CONTRACTS, } from "../contracts/toolContracts.js";
6
+ import { GENERATED_MCP_TOOL_CATALOG } from "../generatedMcpToolCatalog.js";
7
7
  import { HopperApplicationLauncher } from "./BridgeLauncher.js";
8
8
  import { hopperLoaderArgsForTarget, resolveHopperAnalysisProfile, } from "./HopperAnalysisProfile.js";
9
9
  import { HopperClient } from "./HopperClient.js";
@@ -24,36 +24,47 @@ const MUTATING_OPERATIONS = new Set([
24
24
  ]);
25
25
  /** Tool contracts implemented directly by the Hopper adapter. */
26
26
  export const HOPPER_PROVIDER_TOOL_CONTRACTS = Object.freeze([
27
- ...OFFICIAL_TOOL_CONTRACTS,
28
- ...ENHANCED_TOOL_CONTRACTS.filter((contract) => contract.name === "analyze_function"),
27
+ ...GENERATED_MCP_TOOL_CATALOG.filter(({ kind }) => kind === "official-proxy"),
28
+ ...GENERATED_MCP_TOOL_CATALOG.filter(({ name }) => name === "analyze_function"),
29
29
  ]);
30
- const CAPABILITIES = Object.freeze(HOPPER_PROVIDER_TOOL_CONTRACTS.map((contract) => Object.freeze({
31
- provider: IDENTITY,
32
- operation: contract.name,
33
- inputContractVersion: 1,
34
- outputContractVersion: 1,
35
- available: true,
36
- reason: null,
37
- pagination: "offset" in contract.inputSchema.shape ? "offset" : "none",
38
- exhaustive: false,
39
- effects: Object.freeze({
40
- mutatesArtifact: MUTATING_OPERATIONS.has(contract.name),
41
- launchesProcess: true,
42
- mayShowUi: true,
43
- mayAccessNetwork: false,
44
- mayWriteFilesystem: MUTATING_OPERATIONS.has(contract.name),
45
- changesPermissions: false,
46
- requiresRoot: false,
47
- }),
48
- limits: Object.freeze({
49
- maxResults: null,
50
- maxPayloadBytes: null,
51
- timeoutMs: null,
52
- }),
53
- limitations: Object.freeze([
54
- "Results depend on Hopper's completed static analysis.",
55
- ]),
56
- })));
30
+ const CAPABILITIES = Object.freeze(HOPPER_PROVIDER_TOOL_CONTRACTS.map((contract) => {
31
+ const operation = contract.analysisOperation;
32
+ if (operation === null)
33
+ throw new TypeError(`Missing analysis operation for ${contract.name}`);
34
+ return Object.freeze({
35
+ provider: IDENTITY,
36
+ operation,
37
+ inputContractVersion: 1,
38
+ outputContractVersion: 1,
39
+ available: true,
40
+ reason: null,
41
+ pagination: isRecord(contract.inputSchema.properties) &&
42
+ "offset" in contract.inputSchema.properties
43
+ ? "offset"
44
+ : "none",
45
+ exhaustive: false,
46
+ effects: Object.freeze({
47
+ mutatesArtifact: MUTATING_OPERATIONS.has(contract.name),
48
+ launchesProcess: true,
49
+ mayShowUi: true,
50
+ mayAccessNetwork: false,
51
+ mayWriteFilesystem: MUTATING_OPERATIONS.has(contract.name),
52
+ changesPermissions: false,
53
+ requiresRoot: false,
54
+ }),
55
+ limits: Object.freeze({
56
+ maxResults: null,
57
+ maxPayloadBytes: contract.name === "read_bytes" ? 4_096 : null,
58
+ timeoutMs: null,
59
+ }),
60
+ limitations: Object.freeze([
61
+ "Results depend on Hopper's completed static analysis.",
62
+ ]),
63
+ });
64
+ }));
65
+ function isRecord(value) {
66
+ return typeof value === "object" && value !== null && !Array.isArray(value);
67
+ }
57
68
  /** Concrete analysis provider backed by REA's private Hopper bridge. */
58
69
  export class HopperProvider {
59
70
  config;
@@ -178,6 +189,7 @@ export class HopperProvider {
178
189
  }),
179
190
  ...(context === undefined ? {} : { runId: context.runId }),
180
191
  logger: this.logger.child({ layer: "bridge" }),
192
+ onDiagnostic: (diagnostic) => this.logger.info(diagnostic, "Hopper bridge reported a diagnostic"),
181
193
  });
182
194
  return {
183
195
  execute: async (operation, parameters, options) => {
@@ -200,6 +212,25 @@ export class HopperProvider {
200
212
  ? []
201
213
  : [{ provider: executionProvider, observation }];
202
214
  },
215
+ requestActivitySnapshots: () => {
216
+ const activity = client.requestActivity();
217
+ return [
218
+ {
219
+ provider: executionProvider,
220
+ active: activity === null
221
+ ? null
222
+ : {
223
+ requestId: activity.requestId,
224
+ operation: activity.operation,
225
+ elapsedMs: activity.elapsedMs,
226
+ timeoutMs: activity.timeoutMs,
227
+ callerState: activity.callerState,
228
+ },
229
+ queuedRequests: activity?.queuedRequests ?? 0,
230
+ },
231
+ ];
232
+ },
233
+ closeWithOutcome: (options) => client.closeWithOutcome(options),
203
234
  close: () => client.close(),
204
235
  };
205
236
  }
@@ -0,0 +1,215 @@
1
+ import { performance } from "node:perf_hooks";
2
+ import { HopperCancelledError, HopperProcessError, HopperProtocolError, HopperTimeoutError, } from "../domain/errors.js";
3
+ import { err } from "../domain/result.js";
4
+ /** Bounded FIFO that keeps the wire serialized until Hopper actually replies. */
5
+ export class HopperRequestQueue {
6
+ maximum;
7
+ send;
8
+ #queue = [];
9
+ #active;
10
+ constructor(maximum, send) {
11
+ this.maximum = maximum;
12
+ this.send = send;
13
+ }
14
+ /** Queue one request, counting queue wait against its caller deadline. */
15
+ run(id, method, params, options) {
16
+ if (options.signal?.aborted === true)
17
+ return Promise.resolve(err(new HopperCancelledError()));
18
+ if (options.timeoutMs <= 0)
19
+ return Promise.resolve(err(new HopperTimeoutError(options.timeoutMs)));
20
+ if (this.#size() >= this.maximum)
21
+ return Promise.resolve(err(new HopperProtocolError(`Hopper serial request queue reached its ${String(this.maximum)}-request limit`)));
22
+ return new Promise((resolve) => {
23
+ let entry;
24
+ const onAbort = options.signal === undefined ? undefined : () => this.#cancel(entry);
25
+ const timer = setTimeout(() => this.#timeout(entry), options.timeoutMs);
26
+ entry = {
27
+ id,
28
+ method,
29
+ params,
30
+ signal: options.signal,
31
+ timeoutMs: options.timeoutMs,
32
+ deadline: performance.now() + options.timeoutMs,
33
+ progress: options.progress,
34
+ resolve,
35
+ timer,
36
+ onAbort,
37
+ callerState: "waiting",
38
+ callerSettled: false,
39
+ startedAt: undefined,
40
+ heartbeat: undefined,
41
+ };
42
+ this.#queue.push(entry);
43
+ if (onAbort !== undefined)
44
+ options.signal?.addEventListener("abort", onAbort, { once: true });
45
+ if (this.#active !== undefined || this.#queue.length > 1)
46
+ this.#report(entry, `${method} queued behind ${String(this.#size() - 1)} Hopper request(s)`);
47
+ this.#drain();
48
+ });
49
+ }
50
+ /** Settle the active wire request. False identifies an unexpected response id. */
51
+ accept(id, result) {
52
+ const entry = this.#active;
53
+ if (entry === undefined || entry.id !== id)
54
+ return false;
55
+ this.#releaseWire(entry);
56
+ if (!entry.callerSettled)
57
+ this.#settleCaller(entry, result, "waiting");
58
+ this.#active = undefined;
59
+ this.#drain();
60
+ return true;
61
+ }
62
+ /** Forward one correlated bridge event without releasing the wire request. */
63
+ acceptEvent(event) {
64
+ const entry = this.#active;
65
+ if (entry === undefined || entry.id !== event.id)
66
+ return false;
67
+ if (event.event.type === "progress" && !entry.callerSettled) {
68
+ if (event.event.terminal === true && entry.heartbeat !== undefined) {
69
+ clearInterval(entry.heartbeat);
70
+ entry.heartbeat = undefined;
71
+ }
72
+ this.#reportUpdate(entry, {
73
+ phase: event.event.phase,
74
+ completed: event.event.completed,
75
+ total: event.event.total,
76
+ message: event.event.message,
77
+ ...(event.event.terminal === undefined
78
+ ? {}
79
+ : { terminal: event.event.terminal }),
80
+ });
81
+ }
82
+ return true;
83
+ }
84
+ /** Fail the active wire request and every request still waiting in the FIFO. */
85
+ failAll(error) {
86
+ const active = this.#active;
87
+ this.#active = undefined;
88
+ if (active !== undefined) {
89
+ this.#releaseWire(active);
90
+ if (!active.callerSettled)
91
+ this.#settleCaller(active, err(error), "waiting");
92
+ }
93
+ for (const entry of this.#queue.splice(0))
94
+ this.#settleCaller(entry, err(error), "waiting");
95
+ }
96
+ /** Snapshot the one request still occupying Hopper's serial Python thread. */
97
+ activity() {
98
+ const active = this.#active;
99
+ if (active === undefined || active.startedAt === undefined)
100
+ return null;
101
+ return {
102
+ requestId: active.id,
103
+ operation: active.method,
104
+ elapsedMs: Math.max(0, Math.round(performance.now() - active.startedAt)),
105
+ timeoutMs: active.timeoutMs,
106
+ callerState: active.callerState,
107
+ queuedRequests: this.#queue.length,
108
+ };
109
+ }
110
+ /** Return whether an id belongs to a request that has not crossed the wire. */
111
+ hasQueued(id) {
112
+ return this.#queue.some((entry) => entry.id === id);
113
+ }
114
+ /** Number of caller requests waiting behind the active bridge operation. */
115
+ queuedCount() {
116
+ return this.#queue.length;
117
+ }
118
+ #size() {
119
+ return this.#queue.length + (this.#active === undefined ? 0 : 1);
120
+ }
121
+ #drain() {
122
+ if (this.#active !== undefined)
123
+ return;
124
+ const entry = this.#queue.shift();
125
+ if (entry === undefined)
126
+ return;
127
+ const remaining = entry.deadline - performance.now();
128
+ if (entry.signal?.aborted === true || remaining <= 0) {
129
+ this.#settleCaller(entry, entry.signal?.aborted === true
130
+ ? err(new HopperCancelledError())
131
+ : err(new HopperTimeoutError(entry.timeoutMs)), entry.signal?.aborted === true ? "cancelled" : "timed_out");
132
+ this.#drain();
133
+ return;
134
+ }
135
+ this.#active = entry;
136
+ entry.startedAt = performance.now();
137
+ this.#report(entry, `${entry.method} started on Hopper's serial bridge`);
138
+ entry.heartbeat = setInterval(() => {
139
+ if (entry.callerSettled || entry.startedAt === undefined)
140
+ return;
141
+ const elapsed = Math.round(performance.now() - entry.startedAt);
142
+ this.#report(entry, `${entry.method} is still running in Hopper (${String(elapsed)} ms elapsed)`);
143
+ }, 1_000);
144
+ try {
145
+ this.send({ id: entry.id, method: entry.method, params: entry.params }, () => this.accept(entry.id, err(new HopperProcessError(null))));
146
+ }
147
+ catch (cause) {
148
+ this.accept(entry.id, err(new HopperProtocolError("Hopper socket write failed", { cause })));
149
+ }
150
+ }
151
+ #timeout(entry) {
152
+ if (entry.callerSettled)
153
+ return;
154
+ if (this.#active === entry) {
155
+ this.#settleCaller(entry, err(new HopperTimeoutError(entry.timeoutMs)), "timed_out");
156
+ return;
157
+ }
158
+ const index = this.#queue.indexOf(entry);
159
+ if (index < 0)
160
+ return;
161
+ this.#queue.splice(index, 1);
162
+ this.#settleCaller(entry, err(new HopperTimeoutError(entry.timeoutMs)), "timed_out");
163
+ }
164
+ #cancel(entry) {
165
+ if (entry.callerSettled)
166
+ return;
167
+ if (this.#active === entry) {
168
+ this.#settleCaller(entry, err(new HopperCancelledError()), "cancelled");
169
+ return;
170
+ }
171
+ const index = this.#queue.indexOf(entry);
172
+ if (index < 0)
173
+ return;
174
+ this.#queue.splice(index, 1);
175
+ this.#settleCaller(entry, err(new HopperCancelledError()), "cancelled");
176
+ }
177
+ #settleCaller(entry, result, callerState) {
178
+ if (entry.callerSettled)
179
+ return;
180
+ entry.callerSettled = true;
181
+ entry.callerState = callerState;
182
+ if (this.#active === entry && entry.heartbeat !== undefined) {
183
+ clearInterval(entry.heartbeat);
184
+ entry.heartbeat = undefined;
185
+ }
186
+ clearTimeout(entry.timer);
187
+ if (entry.signal !== undefined && entry.onAbort !== undefined)
188
+ entry.signal.removeEventListener("abort", entry.onAbort);
189
+ entry.resolve(result);
190
+ }
191
+ #releaseWire(entry) {
192
+ if (entry.heartbeat !== undefined)
193
+ clearInterval(entry.heartbeat);
194
+ entry.heartbeat = undefined;
195
+ clearTimeout(entry.timer);
196
+ if (entry.signal !== undefined && entry.onAbort !== undefined)
197
+ entry.signal.removeEventListener("abort", entry.onAbort);
198
+ }
199
+ #report(entry, message) {
200
+ this.#reportUpdate(entry, {
201
+ phase: "hopper_request",
202
+ completed: 0,
203
+ total: 1,
204
+ message,
205
+ });
206
+ }
207
+ #reportUpdate(entry, update) {
208
+ try {
209
+ void Promise.resolve(entry.progress?.report(update)).catch(() => undefined);
210
+ }
211
+ catch {
212
+ // Progress observation cannot change the request outcome.
213
+ }
214
+ }
215
+ }