rea-agents 2.3.0 → 2.5.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 (209) hide show
  1. package/README.md +80 -18
  2. package/bridge/ghidra/ReaGhidraBridge.java +48 -1
  3. package/bridge/hopper_bridge.py +259 -24
  4. package/dist/application/AnalysisClientCleanup.js +14 -0
  5. package/dist/application/ApplicationWorkflowEvidenceResolver.js +31 -13
  6. package/dist/application/ArtifactExtraction.js +5 -1
  7. package/dist/application/ArtifactGraphConstruction.js +3 -15
  8. package/dist/application/ArtifactInventory/classify.js +5 -1
  9. package/dist/application/ArtifactInventory/reader.js +2 -0
  10. package/dist/application/BinarySession.js +15 -4
  11. package/dist/application/BinarySessionStatus.js +32 -1
  12. package/dist/application/BrowserEvidence.js +14 -0
  13. package/dist/application/BrowserScenarioCapturePort.js +1 -0
  14. package/dist/application/BrowserScenarioCaptureService.js +53 -0
  15. package/dist/application/BrowserScenarioEvidence.js +40 -0
  16. package/dist/application/CallPathTracing.js +160 -0
  17. package/dist/application/CapabilityInventory.js +79 -15
  18. package/dist/application/CompositeProvider.js +10 -1
  19. package/dist/application/DirectAnalysis.js +7 -2
  20. package/dist/application/EnhancedLiteralTracing.js +118 -0
  21. package/dist/application/EnhancedTools.js +35 -121
  22. package/dist/application/JavaScriptApplicationWorkflowEvidence.js +13 -0
  23. package/dist/application/JavaScriptApplicationWorkflowService.js +27 -1
  24. package/dist/application/JavaScriptRuntimeObservationEvidence.js +34 -0
  25. package/dist/application/JavaScriptRuntimeObservationPort.js +1 -0
  26. package/dist/application/JavaScriptRuntimeObservationService.js +46 -0
  27. package/dist/application/JavaScriptSemanticGraphAsyncProjection.js +147 -0
  28. package/dist/application/JavaScriptSemanticGraphBuilder.js +34 -6
  29. package/dist/application/JavaScriptSemanticGraphChildProcessProjection.js +156 -0
  30. package/dist/application/JavaScriptSemanticGraphConstruction.js +2 -2
  31. package/dist/application/JavaScriptSemanticGraphDataProjection.js +203 -0
  32. package/dist/application/JavaScriptSemanticGraphFingerprintProjection.js +25 -0
  33. package/dist/application/JavaScriptSemanticGraphFlowProjection.js +129 -8
  34. package/dist/application/JavaScriptSemanticGraphObjectProjection.js +81 -0
  35. package/dist/application/JavaScriptSemanticGraphProjection.js +14 -7
  36. package/dist/application/JavaScriptSemanticGraphResourceProjection.js +59 -0
  37. package/dist/application/JavaScriptSemanticGraphValueProjection.js +77 -0
  38. package/dist/application/ProcessCaptureEnvironment.js +26 -0
  39. package/dist/application/ProcessCaptureLifecycle.js +1 -0
  40. package/dist/application/ProcessCheckpoints.js +5 -0
  41. package/dist/application/ProcessHarness.js +45 -34
  42. package/dist/application/ProcessNormalization.js +6 -0
  43. package/dist/application/ProcessReactiveCoordinator.js +10 -3
  44. package/dist/application/ProcessReactiveEffects.js +15 -1
  45. package/dist/application/ProcessReactiveHarness.js +100 -0
  46. package/dist/application/ProcessReactiveObservations.js +101 -16
  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/artifacts/ArtifactProvider.js +87 -22
  58. package/dist/browser/BrowserScenarioSecrets.js +53 -0
  59. package/dist/browser/BrowserScenarioSessionPort.js +1 -0
  60. package/dist/browser/CdpBrowserProvider.js +2 -2
  61. package/dist/browser/CdpCaptureDocuments.js +57 -0
  62. package/dist/browser/CdpCaptureStorage.js +71 -29
  63. package/dist/browser/CdpCaptureStorageFingerprints.js +328 -0
  64. package/dist/browser/CdpEndpoint.js +8 -6
  65. package/dist/browser/CdpPageCapture.js +2 -0
  66. package/dist/browser/JavaScriptRuntimeScope.js +38 -0
  67. package/dist/browser/PlaywrightBrowserScenarioProvider.js +253 -0
  68. package/dist/browser/PlaywrightExecutionBoundary.js +42 -0
  69. package/dist/browser/PlaywrightScenarioActions.js +62 -0
  70. package/dist/browser/PlaywrightScenarioArtifacts.js +238 -0
  71. package/dist/browser/PlaywrightScenarioBrowser.js +144 -0
  72. package/dist/browser/PlaywrightScenarioEvents.js +301 -0
  73. package/dist/browser/PlaywrightScenarioSession.js +300 -0
  74. package/dist/browser/V8InspectorCaptureProjection.js +110 -0
  75. package/dist/browser/V8InspectorEndpoint.js +72 -0
  76. package/dist/browser/V8InspectorProvider.js +267 -0
  77. package/dist/catalogIdentity.js +8 -0
  78. package/dist/cli/artifactCommands.js +39 -0
  79. package/dist/cli/coreAnalysisCommands.js +37 -0
  80. package/dist/cli.js +4 -0
  81. package/dist/cliApplicationCommands.js +30 -2
  82. package/dist/cliBrowserAdvancedCommands.js +17 -5
  83. package/dist/cliBrowserCommands.js +1 -0
  84. package/dist/cliBrowserScenarioCommands.js +43 -0
  85. package/dist/cliCommandNames.js +8 -0
  86. package/dist/cliJavaScriptRuntimeCommands.js +164 -0
  87. package/dist/cliObservationOptions.js +4 -0
  88. package/dist/cliPolicyCommands.js +1 -0
  89. package/dist/config/environment.js +12 -0
  90. package/dist/config/parseConfig.js +88 -17
  91. package/dist/contracts/analysisSearchContract.js +14 -0
  92. package/dist/contracts/applicationToolContracts.js +86 -2
  93. package/dist/contracts/applicationWorkflowInputContracts.js +18 -0
  94. package/dist/contracts/artifactToolContracts.js +22 -0
  95. package/dist/contracts/browserProviderToolContracts.js +7 -0
  96. package/dist/contracts/browserScenarioToolContracts.js +81 -0
  97. package/dist/contracts/browserToolContracts.js +88 -98
  98. package/dist/contracts/enhancedInputs.js +25 -6
  99. package/dist/contracts/functionInstructionContract.js +8 -0
  100. package/dist/contracts/hopperMemoryContracts.js +22 -0
  101. package/dist/contracts/javascriptApplicationWorkflowExamples.js +42 -0
  102. package/dist/contracts/javascriptRuntimeObservationToolContracts.js +58 -0
  103. package/dist/contracts/reconstructionReadinessExample.js +436 -0
  104. package/dist/contracts/toolContractExamples.js +5 -0
  105. package/dist/contracts/toolContractTypes.js +1 -0
  106. package/dist/contracts/toolContracts.js +13 -21
  107. package/dist/contracts/toolEffects.js +21 -0
  108. package/dist/contracts/toolOutputSchemaGroups.js +70 -20
  109. package/dist/contracts/toolOutputSchemaPrimitives.js +40 -0
  110. package/dist/domain/analysisErrorProjection.js +8 -0
  111. package/dist/domain/analysisSnapshot.js +2 -0
  112. package/dist/domain/artifactGraph.js +2 -0
  113. package/dist/domain/artifactInspection.js +347 -0
  114. package/dist/domain/binaryTarget.js +3 -6
  115. package/dist/domain/browserCaptureComparison.js +82 -0
  116. package/dist/domain/browserObservation.js +8 -1
  117. package/dist/domain/browserObservationErrors.js +1 -0
  118. package/dist/domain/browserObservationSchemas.js +26 -0
  119. package/dist/domain/browserScenario.js +165 -0
  120. package/dist/domain/browserScenarioCapture.js +57 -0
  121. package/dist/domain/browserScenarioCaptureValues.js +204 -0
  122. package/dist/domain/browserScenarioDiff.js +324 -0
  123. package/dist/domain/browserScenarioDiffValues.js +157 -0
  124. package/dist/domain/browserScenarioNormalization.js +45 -0
  125. package/dist/domain/browserScenarioValues.js +372 -0
  126. package/dist/domain/evidence.js +2 -0
  127. package/dist/domain/hopperValues.js +10 -0
  128. package/dist/domain/javascriptRuntimeObservation.js +170 -0
  129. package/dist/domain/javascriptRuntimeReconciliationParsing.js +120 -2
  130. package/dist/domain/javascriptRuntimeReconciliationResult.js +3 -3
  131. package/dist/domain/javascriptRuntimeReconciliationRuntime.js +2 -2
  132. package/dist/domain/javascriptRuntimeReconciliationSchemas.js +1 -1
  133. package/dist/domain/javascriptSemanticAnalysis.js +5 -4
  134. package/dist/domain/javascriptSemanticAsyncEffects.js +253 -0
  135. package/dist/domain/javascriptSemanticChildProcesses.js +237 -0
  136. package/dist/domain/javascriptSemanticDataEffectHelpers.js +125 -0
  137. package/dist/domain/javascriptSemanticDataEffects.js +284 -0
  138. package/dist/domain/javascriptSemanticDerivedAnalysis.js +41 -0
  139. package/dist/domain/javascriptSemanticFingerprints.js +174 -0
  140. package/dist/domain/javascriptSemanticIr.js +21 -1
  141. package/dist/domain/javascriptSemanticObjects.js +115 -0
  142. package/dist/domain/javascriptSemanticPromises.js +290 -0
  143. package/dist/domain/javascriptSemanticResources.js +136 -0
  144. package/dist/domain/javascriptStaticAnalysis.js +4 -3
  145. package/dist/domain/javascriptStaticAnalysisBundler.js +38 -0
  146. package/dist/domain/javascriptStaticAnalysisCalls.js +23 -1
  147. package/dist/domain/javascriptStaticAnalysisHelpers.js +8 -0
  148. package/dist/domain/processCapture.js +71 -1
  149. package/dist/domain/processCaptureReactiveValidation.js +175 -0
  150. package/dist/domain/processCaptureValidation.js +2 -0
  151. package/dist/domain/processObservation.js +15 -6
  152. package/dist/domain/processReactiveRuntime.js +21 -10
  153. package/dist/domain/processReactiveScenario.js +1 -0
  154. package/dist/domain/processReactiveTransition.js +17 -10
  155. package/dist/domain/processScenario.js +21 -0
  156. package/dist/domain/providerCleanupError.js +11 -0
  157. package/dist/domain/reconstructionObligationLedgerSchemas.js +278 -0
  158. package/dist/domain/reconstructionReadiness.js +121 -0
  159. package/dist/domain/reconstructionReadinessFindingHelpers.js +17 -0
  160. package/dist/domain/reconstructionReadinessIntegrityFindings.js +118 -0
  161. package/dist/domain/reconstructionReadinessJourneyFindings.js +193 -0
  162. package/dist/domain/reconstructionReadinessSchemas.js +240 -0
  163. package/dist/domain/sourceToBundleComparison.js +335 -0
  164. package/dist/domain/sourceToBundleComparisonSchemas.js +133 -0
  165. package/dist/domain/sourceToBundleSignals.js +202 -0
  166. package/dist/domain/webCaptureDiff.js +141 -27
  167. package/dist/domain/webScreenshot.js +1 -1
  168. package/dist/domain/zipPackageFormat.js +20 -0
  169. package/dist/generatedPackageMetadata.js +1 -1
  170. package/dist/ghidra/GhidraDefaults.js +1 -1
  171. package/dist/ghidra/GhidraFunctionValues.js +13 -1
  172. package/dist/ghidra/GhidraProviderCapabilities.js +7 -0
  173. package/dist/hopper/HopperCleanup.js +158 -0
  174. package/dist/hopper/HopperClient.js +69 -99
  175. package/dist/hopper/HopperProvider.js +20 -1
  176. package/dist/hopper/HopperRequestQueue.js +187 -0
  177. package/dist/hopper/HopperResponseStream.js +46 -0
  178. package/dist/hopper/protocol.js +1 -0
  179. package/dist/main/transport.js +24 -24
  180. package/dist/main.js +6 -0
  181. package/dist/mcpDoctor.js +25 -2
  182. package/dist/native/NativeMacOSProvider.js +1 -0
  183. package/dist/process/ProcessCommandIdentity.js +19 -0
  184. package/dist/process/ProcessOwnership.js +11 -8
  185. package/dist/server/DynamicToolAvailability.js +70 -0
  186. package/dist/server/createServer.js +46 -1
  187. package/dist/server/lifecycleProgress.js +10 -0
  188. package/dist/server/permissionFailure.js +12 -0
  189. package/dist/server/registerApplicationTools/compareSourceToBundle.js +35 -0
  190. package/dist/server/registerApplicationTools/helpers.js +11 -7
  191. package/dist/server/registerApplicationTools/obligationLedger.js +27 -0
  192. package/dist/server/registerApplicationTools/readiness.js +55 -0
  193. package/dist/server/registerApplicationTools.js +6 -0
  194. package/dist/server/registerArtifactTools.js +9 -1
  195. package/dist/server/registerBrowserScenarioTool.js +25 -0
  196. package/dist/server/registerBrowserTools.js +2 -2
  197. package/dist/server/registerCloseLifecycleTool.js +57 -0
  198. package/dist/server/registerEnhancedTools.js +16 -5
  199. package/dist/server/registerEvidenceResources.js +4 -0
  200. package/dist/server/registerEvidenceTools.js +9 -2
  201. package/dist/server/registerJavaScriptRuntimeObservationTools.js +37 -0
  202. package/dist/server/registerReconstructionObligationLedgerResource.js +39 -0
  203. package/dist/server/registerReconstructionReadinessResource.js +39 -0
  204. package/dist/server/registerSessionStatusTool.js +11 -8
  205. package/dist/server/registerSessionTools.js +11 -57
  206. package/dist/server/sessionAvailabilityPolicy.js +10 -8
  207. package/package.json +12 -6
  208. package/skills/reverse-engineer-anything/SKILL.md +4 -3
  209. package/skills/reverse-engineer-anything/references/native-and-artifacts.md +2 -2
@@ -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: (response) => this.#requests.accept(response.id, responseResult(response)),
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,
@@ -364,44 +366,12 @@ export class HopperClient {
364
366
  if (launch.ownsProcessLifetime && !this.#closing)
365
367
  this.#failAll(new HopperProcessError(event.code, this.#launcherFailureDiagnostic));
366
368
  }
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
369
  #abortProtocol(message, cause) {
400
370
  this.#failAll(new HopperProtocolError(message, { cause }));
401
371
  this.#socket?.destroy();
402
372
  }
403
373
  #failAll(error) {
404
- this.#pending.failAll(() => err(error));
374
+ this.#requests.failAll(error);
405
375
  }
406
376
  }
407
377
  const isAborted = (signal) => signal?.aborted === true;
@@ -47,7 +47,7 @@ const CAPABILITIES = Object.freeze(HOPPER_PROVIDER_TOOL_CONTRACTS.map((contract)
47
47
  }),
48
48
  limits: Object.freeze({
49
49
  maxResults: null,
50
- maxPayloadBytes: null,
50
+ maxPayloadBytes: contract.name === "read_bytes" ? 4_096 : null,
51
51
  timeoutMs: null,
52
52
  }),
53
53
  limitations: Object.freeze([
@@ -200,6 +200,25 @@ export class HopperProvider {
200
200
  ? []
201
201
  : [{ provider: executionProvider, observation }];
202
202
  },
203
+ requestActivitySnapshots: () => {
204
+ const activity = client.requestActivity();
205
+ return [
206
+ {
207
+ provider: executionProvider,
208
+ active: activity === null
209
+ ? null
210
+ : {
211
+ requestId: activity.requestId,
212
+ operation: activity.operation,
213
+ elapsedMs: activity.elapsedMs,
214
+ timeoutMs: activity.timeoutMs,
215
+ callerState: activity.callerState,
216
+ },
217
+ queuedRequests: activity?.queuedRequests ?? 0,
218
+ },
219
+ ];
220
+ },
221
+ closeWithOutcome: (options) => client.closeWithOutcome(options),
203
222
  close: () => client.close(),
204
223
  };
205
224
  }
@@ -0,0 +1,187 @@
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
+ /** Fail the active wire request and every request still waiting in the FIFO. */
63
+ failAll(error) {
64
+ const active = this.#active;
65
+ this.#active = undefined;
66
+ if (active !== undefined) {
67
+ this.#releaseWire(active);
68
+ if (!active.callerSettled)
69
+ this.#settleCaller(active, err(error), "waiting");
70
+ }
71
+ for (const entry of this.#queue.splice(0))
72
+ this.#settleCaller(entry, err(error), "waiting");
73
+ }
74
+ /** Snapshot the one request still occupying Hopper's serial Python thread. */
75
+ activity() {
76
+ const active = this.#active;
77
+ if (active === undefined || active.startedAt === undefined)
78
+ return null;
79
+ return {
80
+ requestId: active.id,
81
+ operation: active.method,
82
+ elapsedMs: Math.max(0, Math.round(performance.now() - active.startedAt)),
83
+ timeoutMs: active.timeoutMs,
84
+ callerState: active.callerState,
85
+ queuedRequests: this.#queue.length,
86
+ };
87
+ }
88
+ /** Return whether an id belongs to a request that has not crossed the wire. */
89
+ hasQueued(id) {
90
+ return this.#queue.some((entry) => entry.id === id);
91
+ }
92
+ /** Number of caller requests waiting behind the active bridge operation. */
93
+ queuedCount() {
94
+ return this.#queue.length;
95
+ }
96
+ #size() {
97
+ return this.#queue.length + (this.#active === undefined ? 0 : 1);
98
+ }
99
+ #drain() {
100
+ if (this.#active !== undefined)
101
+ return;
102
+ const entry = this.#queue.shift();
103
+ if (entry === undefined)
104
+ return;
105
+ const remaining = entry.deadline - performance.now();
106
+ if (entry.signal?.aborted === true || remaining <= 0) {
107
+ this.#settleCaller(entry, entry.signal?.aborted === true
108
+ ? err(new HopperCancelledError())
109
+ : err(new HopperTimeoutError(entry.timeoutMs)), entry.signal?.aborted === true ? "cancelled" : "timed_out");
110
+ this.#drain();
111
+ return;
112
+ }
113
+ this.#active = entry;
114
+ entry.startedAt = performance.now();
115
+ this.#report(entry, `${entry.method} started on Hopper's serial bridge`);
116
+ entry.heartbeat = setInterval(() => {
117
+ if (entry.callerSettled || entry.startedAt === undefined)
118
+ return;
119
+ const elapsed = Math.round(performance.now() - entry.startedAt);
120
+ this.#report(entry, `${entry.method} is still running in Hopper (${String(elapsed)} ms elapsed)`);
121
+ }, 1_000);
122
+ try {
123
+ this.send({ id: entry.id, method: entry.method, params: entry.params }, () => this.accept(entry.id, err(new HopperProcessError(null))));
124
+ }
125
+ catch (cause) {
126
+ this.accept(entry.id, err(new HopperProtocolError("Hopper socket write failed", { cause })));
127
+ }
128
+ }
129
+ #timeout(entry) {
130
+ if (entry.callerSettled)
131
+ return;
132
+ if (this.#active === entry) {
133
+ this.#settleCaller(entry, err(new HopperTimeoutError(entry.timeoutMs)), "timed_out");
134
+ return;
135
+ }
136
+ const index = this.#queue.indexOf(entry);
137
+ if (index < 0)
138
+ return;
139
+ this.#queue.splice(index, 1);
140
+ this.#settleCaller(entry, err(new HopperTimeoutError(entry.timeoutMs)), "timed_out");
141
+ }
142
+ #cancel(entry) {
143
+ if (entry.callerSettled)
144
+ return;
145
+ if (this.#active === entry) {
146
+ this.#settleCaller(entry, err(new HopperCancelledError()), "cancelled");
147
+ return;
148
+ }
149
+ const index = this.#queue.indexOf(entry);
150
+ if (index < 0)
151
+ return;
152
+ this.#queue.splice(index, 1);
153
+ this.#settleCaller(entry, err(new HopperCancelledError()), "cancelled");
154
+ }
155
+ #settleCaller(entry, result, callerState) {
156
+ if (entry.callerSettled)
157
+ return;
158
+ entry.callerSettled = true;
159
+ entry.callerState = callerState;
160
+ if (this.#active === entry && entry.heartbeat !== undefined) {
161
+ clearInterval(entry.heartbeat);
162
+ entry.heartbeat = undefined;
163
+ }
164
+ clearTimeout(entry.timer);
165
+ if (entry.signal !== undefined && entry.onAbort !== undefined)
166
+ entry.signal.removeEventListener("abort", entry.onAbort);
167
+ entry.resolve(result);
168
+ }
169
+ #releaseWire(entry) {
170
+ if (entry.heartbeat !== undefined)
171
+ clearInterval(entry.heartbeat);
172
+ entry.heartbeat = undefined;
173
+ clearTimeout(entry.timer);
174
+ if (entry.signal !== undefined && entry.onAbort !== undefined)
175
+ entry.signal.removeEventListener("abort", entry.onAbort);
176
+ }
177
+ #report(entry, message) {
178
+ void entry.progress
179
+ ?.report({
180
+ phase: "hopper_request",
181
+ completed: 0,
182
+ total: 1,
183
+ message,
184
+ })
185
+ .catch(() => undefined);
186
+ }
187
+ }
@@ -0,0 +1,46 @@
1
+ import { parseResponseLine } from "./protocol.js";
2
+ const MAX_LINE_BYTES = 10 * 1024 * 1024;
3
+ /** Incrementally validates bounded Hopper NDJSON response frames. */
4
+ export class HopperResponseStream {
5
+ options;
6
+ #buffer = "";
7
+ constructor(options) {
8
+ this.options = options;
9
+ }
10
+ push(chunk) {
11
+ this.#buffer += chunk;
12
+ let newline = this.#buffer.indexOf("\n");
13
+ while (newline >= 0) {
14
+ const line = this.#buffer.slice(0, newline).trim();
15
+ this.#buffer = this.#buffer.slice(newline + 1);
16
+ if (Buffer.byteLength(line) > MAX_LINE_BYTES) {
17
+ this.options.abort("Hopper response exceeded the maximum line size");
18
+ return;
19
+ }
20
+ if (line.length > 0 && !this.#acceptLine(line))
21
+ return;
22
+ newline = this.#buffer.indexOf("\n");
23
+ }
24
+ if (Buffer.byteLength(this.#buffer) > MAX_LINE_BYTES) {
25
+ this.options.abort("Hopper response exceeded the maximum line size");
26
+ }
27
+ }
28
+ reset() {
29
+ this.#buffer = "";
30
+ }
31
+ #acceptLine(line) {
32
+ const parsed = parseResponseLine(line);
33
+ if (!parsed.ok) {
34
+ this.options.abort(parsed.error.message, parsed.error);
35
+ return false;
36
+ }
37
+ if (this.options.accept(parsed.value))
38
+ return true;
39
+ if (parsed.value.id >= this.options.nextRequestId() ||
40
+ this.options.hasQueued(parsed.value.id)) {
41
+ this.options.abort("Hopper returned an unknown response id");
42
+ return false;
43
+ }
44
+ return true;
45
+ }
46
+ }
@@ -17,6 +17,7 @@ const responseSchema = z.union([
17
17
  "remote",
18
18
  "authorization",
19
19
  "invalid_request",
20
+ "capability_unavailable",
20
21
  "bridge_exception",
21
22
  ])
22
23
  .default("remote"),
@@ -14,35 +14,14 @@ export const startMcpTransport = async (dependencies, session, serverContext) =>
14
14
  analysisSnapshotFilePolicy: serverContext.runtimeState.snapshotPolicy,
15
15
  permissionAuthority: serverContext.permissionAuthority,
16
16
  browserObservation: serverContext.browserObservation,
17
+ browserScenarioCapture: serverContext.browserScenarioCapture,
17
18
  electronObservation: serverContext.electronObservation,
19
+ javascriptRuntimeObservation: serverContext.javascriptRuntimeObservation,
18
20
  artifactIntegrityContinueEnabled: () => serverContext.runtimeState.currentConfig
19
21
  .artifactIntegrityContinueEnabled,
20
22
  javascriptReplayPolicy: serverContext.runtimeState.javascriptReplayPolicy,
21
23
  managedRuntimePolicy: serverContext.runtimeState.managedRuntimePolicy,
22
- availabilityPolicy: () => ({
23
- processCaptureEnabled: serverContext.runtimeState.currentConfig.processExecutionPolicy
24
- .enabled,
25
- evidenceFileRoots: serverContext.runtimeState.currentConfig.evidenceFilePolicy
26
- .roots.length,
27
- investigationInputRoots: serverContext.runtimeState.currentConfig.investigationInputRoots
28
- .length,
29
- browserObservationEnabled: serverContext.runtimeState.currentConfig
30
- .browserObservationEnabled &&
31
- serverContext.runtimeState.currentConfig.browserCdpEndpoints
32
- .length > 0 &&
33
- serverContext.runtimeState.currentConfig.browserAllowedOrigins
34
- .length > 0,
35
- electronObservationEnabled: serverContext.runtimeState.currentConfig
36
- .electronObservationEnabled &&
37
- serverContext.runtimeState.currentConfig.electronCdpEndpoints
38
- .length > 0 &&
39
- serverContext.runtimeState.currentConfig.electronFileRoots
40
- .length > 0,
41
- javascriptReplayEnabled: serverContext.runtimeState.currentConfig.javascriptReplayPolicy
42
- .enabled,
43
- managedRuntimeEnabled: serverContext.runtimeState.currentConfig.managedRuntimePolicy
44
- .enabled,
45
- }),
24
+ availabilityPolicy: () => runtimeAvailability(serverContext.runtimeState),
46
25
  });
47
26
  liveServers.add(server);
48
27
  return server;
@@ -61,3 +40,24 @@ export const startMcpTransport = async (dependencies, session, serverContext) =>
61
40
  }
62
41
  return { ok: true, handle, liveServers };
63
42
  };
43
+ const runtimeAvailability = (state) => ({
44
+ processCaptureEnabled: state.currentConfig.processExecutionPolicy.enabled,
45
+ evidenceFileRoots: state.currentConfig.evidenceFilePolicy.roots.length,
46
+ investigationInputRoots: state.currentConfig.investigationInputRoots.length,
47
+ browserObservationEnabled: state.currentConfig.browserObservationEnabled &&
48
+ state.currentConfig.browserCdpEndpoints.length > 0 &&
49
+ state.currentConfig.browserAllowedOrigins.length > 0,
50
+ browserScenarioEnabled: state.currentConfig.browserScenarioPolicy.enabled &&
51
+ state.currentConfig.browserScenarioPolicy.allowedOrigins.length > 0 &&
52
+ (state.currentConfig.browserScenarioPolicy.executableRoots.length > 0 ||
53
+ state.currentConfig.browserScenarioPolicy.cdpEndpoints.length > 0),
54
+ electronObservationEnabled: state.currentConfig.electronObservationEnabled &&
55
+ state.currentConfig.electronCdpEndpoints.length > 0 &&
56
+ state.currentConfig.electronFileRoots.length > 0,
57
+ v8InspectorObservationEnabled: state.currentConfig.v8InspectorObservationEnabled &&
58
+ state.currentConfig.v8InspectorEndpoints.length > 0 &&
59
+ (state.currentConfig.v8InspectorFileRoots.length > 0 ||
60
+ state.currentConfig.v8InspectorAllowedOrigins.length > 0),
61
+ javascriptReplayEnabled: state.currentConfig.javascriptReplayPolicy.enabled,
62
+ managedRuntimeEnabled: state.currentConfig.managedRuntimePolicy.enabled,
63
+ });
package/dist/main.js CHANGED
@@ -9,6 +9,8 @@ import { projectAnalysisError } from "./domain/errors.js";
9
9
  import { loadConfiguredPermissionAuthority } from "./application/PermissionConfiguration.js";
10
10
  import { CdpBrowserProvider } from "./browser/CdpBrowserProvider.js";
11
11
  import { CdpElectronProvider } from "./browser/CdpElectronProvider.js";
12
+ import { V8InspectorProvider } from "./browser/V8InspectorProvider.js";
13
+ import { PlaywrightBrowserScenarioProvider } from "./browser/PlaywrightBrowserScenarioProvider.js";
12
14
  import { SERVER_START_FAILED } from "./main/messages.js";
13
15
  import { createRuntimeState } from "./main/state.js";
14
16
  import { openInitialTarget } from "./main/startup.js";
@@ -59,7 +61,9 @@ export const run = async (dependencies = runtimeDependencies()) => {
59
61
  }
60
62
  const session = createBinarySession(config.value, logger);
61
63
  const browserObservation = new CdpBrowserProvider();
64
+ const browserScenarioCapture = new PlaywrightBrowserScenarioProvider();
62
65
  const electronObservation = new CdpElectronProvider();
66
+ const javascriptRuntimeObservation = new V8InspectorProvider();
63
67
  const opened = await openInitialTarget(session, config.value, serverLogger, dependencies.writeStderr);
64
68
  if (!opened.ok)
65
69
  return opened.exitCode;
@@ -68,7 +72,9 @@ export const run = async (dependencies = runtimeDependencies()) => {
68
72
  logger,
69
73
  serverLogger,
70
74
  browserObservation,
75
+ browserScenarioCapture,
71
76
  electronObservation,
77
+ javascriptRuntimeObservation,
72
78
  permissionAuthority: permissionAuthority.value,
73
79
  runtimeState,
74
80
  });