rea-agents 2.4.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 (186) 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/BinarySession.js +15 -4
  7. package/dist/application/BinarySessionStatus.js +31 -1
  8. package/dist/application/BrowserEvidence.js +14 -0
  9. package/dist/application/BrowserScenarioCapturePort.js +1 -0
  10. package/dist/application/BrowserScenarioCaptureService.js +53 -0
  11. package/dist/application/BrowserScenarioEvidence.js +40 -0
  12. package/dist/application/CallPathTracing.js +160 -0
  13. package/dist/application/CapabilityInventory.js +68 -12
  14. package/dist/application/CompositeProvider.js +10 -1
  15. package/dist/application/DirectAnalysis.js +7 -2
  16. package/dist/application/EnhancedLiteralTracing.js +118 -0
  17. package/dist/application/EnhancedTools.js +35 -121
  18. package/dist/application/JavaScriptApplicationWorkflowEvidence.js +13 -0
  19. package/dist/application/JavaScriptApplicationWorkflowService.js +27 -1
  20. package/dist/application/JavaScriptRuntimeObservationEvidence.js +34 -0
  21. package/dist/application/JavaScriptRuntimeObservationPort.js +1 -0
  22. package/dist/application/JavaScriptRuntimeObservationService.js +46 -0
  23. package/dist/application/JavaScriptSemanticGraphAsyncProjection.js +147 -0
  24. package/dist/application/JavaScriptSemanticGraphBuilder.js +34 -6
  25. package/dist/application/JavaScriptSemanticGraphChildProcessProjection.js +156 -0
  26. package/dist/application/JavaScriptSemanticGraphConstruction.js +2 -2
  27. package/dist/application/JavaScriptSemanticGraphDataProjection.js +203 -0
  28. package/dist/application/JavaScriptSemanticGraphFingerprintProjection.js +25 -0
  29. package/dist/application/JavaScriptSemanticGraphFlowProjection.js +129 -8
  30. package/dist/application/JavaScriptSemanticGraphObjectProjection.js +81 -0
  31. package/dist/application/JavaScriptSemanticGraphProjection.js +14 -7
  32. package/dist/application/JavaScriptSemanticGraphResourceProjection.js +59 -0
  33. package/dist/application/JavaScriptSemanticGraphValueProjection.js +77 -0
  34. package/dist/application/ProcessReactiveEffects.js +6 -0
  35. package/dist/application/ProcessReactiveHarness.js +16 -1
  36. package/dist/application/ProjectPermissionStore.js +1 -0
  37. package/dist/application/ReconstructionObligationAuthority.js +72 -0
  38. package/dist/application/ReconstructionObligationCandidates.js +265 -0
  39. package/dist/application/ReconstructionObligationLedgerDiagnostics.js +47 -0
  40. package/dist/application/ReconstructionObligationLedgerEvaluation.js +263 -0
  41. package/dist/application/ReconstructionObligationLedgerProjection.js +86 -0
  42. package/dist/application/ReconstructionObligationLedgerService.js +72 -0
  43. package/dist/application/ReconstructionObligationOriginalCases.js +33 -0
  44. package/dist/application/ReconstructionObligationPolicies.js +132 -0
  45. package/dist/application/ReconstructionReadinessService.js +48 -0
  46. package/dist/artifacts/ArtifactProvider.js +87 -22
  47. package/dist/browser/BrowserScenarioSecrets.js +53 -0
  48. package/dist/browser/BrowserScenarioSessionPort.js +1 -0
  49. package/dist/browser/CdpBrowserProvider.js +2 -2
  50. package/dist/browser/CdpCaptureDocuments.js +57 -0
  51. package/dist/browser/CdpCaptureStorage.js +71 -29
  52. package/dist/browser/CdpCaptureStorageFingerprints.js +328 -0
  53. package/dist/browser/CdpEndpoint.js +8 -6
  54. package/dist/browser/CdpPageCapture.js +2 -0
  55. package/dist/browser/JavaScriptRuntimeScope.js +38 -0
  56. package/dist/browser/PlaywrightBrowserScenarioProvider.js +253 -0
  57. package/dist/browser/PlaywrightExecutionBoundary.js +42 -0
  58. package/dist/browser/PlaywrightScenarioActions.js +62 -0
  59. package/dist/browser/PlaywrightScenarioArtifacts.js +238 -0
  60. package/dist/browser/PlaywrightScenarioBrowser.js +144 -0
  61. package/dist/browser/PlaywrightScenarioEvents.js +301 -0
  62. package/dist/browser/PlaywrightScenarioSession.js +300 -0
  63. package/dist/browser/V8InspectorCaptureProjection.js +110 -0
  64. package/dist/browser/V8InspectorEndpoint.js +72 -0
  65. package/dist/browser/V8InspectorProvider.js +267 -0
  66. package/dist/catalogIdentity.js +8 -0
  67. package/dist/cli/artifactCommands.js +39 -0
  68. package/dist/cli/coreAnalysisCommands.js +37 -0
  69. package/dist/cli.js +4 -0
  70. package/dist/cliApplicationCommands.js +30 -2
  71. package/dist/cliBrowserAdvancedCommands.js +17 -5
  72. package/dist/cliBrowserCommands.js +1 -0
  73. package/dist/cliBrowserScenarioCommands.js +43 -0
  74. package/dist/cliCommandNames.js +8 -0
  75. package/dist/cliJavaScriptRuntimeCommands.js +164 -0
  76. package/dist/cliObservationOptions.js +4 -0
  77. package/dist/cliPolicyCommands.js +1 -0
  78. package/dist/config/environment.js +12 -0
  79. package/dist/config/parseConfig.js +88 -17
  80. package/dist/contracts/analysisSearchContract.js +14 -0
  81. package/dist/contracts/applicationToolContracts.js +86 -2
  82. package/dist/contracts/applicationWorkflowInputContracts.js +18 -0
  83. package/dist/contracts/artifactToolContracts.js +22 -0
  84. package/dist/contracts/browserProviderToolContracts.js +7 -0
  85. package/dist/contracts/browserScenarioToolContracts.js +81 -0
  86. package/dist/contracts/browserToolContracts.js +88 -98
  87. package/dist/contracts/enhancedInputs.js +25 -6
  88. package/dist/contracts/functionInstructionContract.js +8 -0
  89. package/dist/contracts/hopperMemoryContracts.js +22 -0
  90. package/dist/contracts/javascriptApplicationWorkflowExamples.js +42 -0
  91. package/dist/contracts/javascriptRuntimeObservationToolContracts.js +58 -0
  92. package/dist/contracts/reconstructionReadinessExample.js +436 -0
  93. package/dist/contracts/toolContractExamples.js +5 -0
  94. package/dist/contracts/toolContractTypes.js +1 -0
  95. package/dist/contracts/toolContracts.js +13 -21
  96. package/dist/contracts/toolEffects.js +21 -0
  97. package/dist/contracts/toolOutputSchemaGroups.js +70 -20
  98. package/dist/contracts/toolOutputSchemaPrimitives.js +37 -0
  99. package/dist/domain/analysisErrorProjection.js +8 -0
  100. package/dist/domain/artifactInspection.js +347 -0
  101. package/dist/domain/browserCaptureComparison.js +82 -0
  102. package/dist/domain/browserObservation.js +8 -1
  103. package/dist/domain/browserObservationErrors.js +1 -0
  104. package/dist/domain/browserObservationSchemas.js +26 -0
  105. package/dist/domain/browserScenario.js +165 -0
  106. package/dist/domain/browserScenarioCapture.js +57 -0
  107. package/dist/domain/browserScenarioCaptureValues.js +204 -0
  108. package/dist/domain/browserScenarioDiff.js +324 -0
  109. package/dist/domain/browserScenarioDiffValues.js +157 -0
  110. package/dist/domain/browserScenarioNormalization.js +45 -0
  111. package/dist/domain/browserScenarioValues.js +372 -0
  112. package/dist/domain/hopperValues.js +10 -0
  113. package/dist/domain/javascriptRuntimeObservation.js +170 -0
  114. package/dist/domain/javascriptRuntimeReconciliationParsing.js +120 -2
  115. package/dist/domain/javascriptRuntimeReconciliationResult.js +3 -3
  116. package/dist/domain/javascriptRuntimeReconciliationRuntime.js +2 -2
  117. package/dist/domain/javascriptRuntimeReconciliationSchemas.js +1 -1
  118. package/dist/domain/javascriptSemanticAnalysis.js +5 -4
  119. package/dist/domain/javascriptSemanticAsyncEffects.js +253 -0
  120. package/dist/domain/javascriptSemanticChildProcesses.js +237 -0
  121. package/dist/domain/javascriptSemanticDataEffectHelpers.js +125 -0
  122. package/dist/domain/javascriptSemanticDataEffects.js +284 -0
  123. package/dist/domain/javascriptSemanticDerivedAnalysis.js +41 -0
  124. package/dist/domain/javascriptSemanticFingerprints.js +174 -0
  125. package/dist/domain/javascriptSemanticIr.js +21 -1
  126. package/dist/domain/javascriptSemanticObjects.js +115 -0
  127. package/dist/domain/javascriptSemanticPromises.js +290 -0
  128. package/dist/domain/javascriptSemanticResources.js +136 -0
  129. package/dist/domain/javascriptStaticAnalysis.js +4 -3
  130. package/dist/domain/javascriptStaticAnalysisBundler.js +38 -0
  131. package/dist/domain/javascriptStaticAnalysisCalls.js +23 -1
  132. package/dist/domain/javascriptStaticAnalysisHelpers.js +8 -0
  133. package/dist/domain/processCapture.js +8 -2
  134. package/dist/domain/processReactiveScenario.js +1 -0
  135. package/dist/domain/processReactiveTransition.js +9 -4
  136. package/dist/domain/providerCleanupError.js +11 -0
  137. package/dist/domain/reconstructionObligationLedgerSchemas.js +278 -0
  138. package/dist/domain/reconstructionReadiness.js +121 -0
  139. package/dist/domain/reconstructionReadinessFindingHelpers.js +17 -0
  140. package/dist/domain/reconstructionReadinessIntegrityFindings.js +118 -0
  141. package/dist/domain/reconstructionReadinessJourneyFindings.js +193 -0
  142. package/dist/domain/reconstructionReadinessSchemas.js +240 -0
  143. package/dist/domain/sourceToBundleComparison.js +335 -0
  144. package/dist/domain/sourceToBundleComparisonSchemas.js +133 -0
  145. package/dist/domain/sourceToBundleSignals.js +202 -0
  146. package/dist/domain/webCaptureDiff.js +141 -27
  147. package/dist/domain/webScreenshot.js +1 -1
  148. package/dist/generatedPackageMetadata.js +1 -1
  149. package/dist/ghidra/GhidraDefaults.js +1 -1
  150. package/dist/ghidra/GhidraFunctionValues.js +13 -1
  151. package/dist/ghidra/GhidraProviderCapabilities.js +7 -0
  152. package/dist/hopper/HopperCleanup.js +158 -0
  153. package/dist/hopper/HopperClient.js +69 -99
  154. package/dist/hopper/HopperProvider.js +20 -1
  155. package/dist/hopper/HopperRequestQueue.js +187 -0
  156. package/dist/hopper/HopperResponseStream.js +46 -0
  157. package/dist/hopper/protocol.js +1 -0
  158. package/dist/main/transport.js +24 -24
  159. package/dist/main.js +6 -0
  160. package/dist/mcpDoctor.js +25 -2
  161. package/dist/process/ProcessCommandIdentity.js +19 -0
  162. package/dist/process/ProcessOwnership.js +11 -8
  163. package/dist/server/DynamicToolAvailability.js +70 -0
  164. package/dist/server/createServer.js +46 -1
  165. package/dist/server/lifecycleProgress.js +10 -0
  166. package/dist/server/permissionFailure.js +12 -0
  167. package/dist/server/registerApplicationTools/compareSourceToBundle.js +35 -0
  168. package/dist/server/registerApplicationTools/helpers.js +11 -7
  169. package/dist/server/registerApplicationTools/obligationLedger.js +27 -0
  170. package/dist/server/registerApplicationTools/readiness.js +55 -0
  171. package/dist/server/registerApplicationTools.js +6 -0
  172. package/dist/server/registerArtifactTools.js +9 -1
  173. package/dist/server/registerBrowserScenarioTool.js +25 -0
  174. package/dist/server/registerBrowserTools.js +2 -2
  175. package/dist/server/registerCloseLifecycleTool.js +57 -0
  176. package/dist/server/registerEnhancedTools.js +16 -5
  177. package/dist/server/registerEvidenceResources.js +4 -0
  178. package/dist/server/registerEvidenceTools.js +9 -2
  179. package/dist/server/registerJavaScriptRuntimeObservationTools.js +37 -0
  180. package/dist/server/registerReconstructionObligationLedgerResource.js +39 -0
  181. package/dist/server/registerReconstructionReadinessResource.js +39 -0
  182. package/dist/server/registerSessionStatusTool.js +11 -8
  183. package/dist/server/registerSessionTools.js +11 -57
  184. package/dist/server/sessionAvailabilityPolicy.js +10 -8
  185. package/package.json +12 -6
  186. package/skills/reverse-engineer-anything/SKILL.md +2 -2
@@ -11,19 +11,86 @@ import os
11
11
  import re
12
12
  import sre_parse
13
13
  import socket
14
+ from typing import Any, Optional, Protocol, Sequence
14
15
 
15
16
  MAX_LINE_BYTES = 10 * 1024 * 1024
17
+ MAX_READ_BYTES = 4096
16
18
  BAD_ADDRESSES = (-1, 0xFFFFFFFFFFFFFFFF, None)
17
19
  _selected_document = None
18
20
  _search_inventory_cache = {}
21
+ _pseudocode_cache = {}
22
+ _hopper_api = None
19
23
  MAX_SEARCH_PATTERN_LENGTH = 256
20
24
  MAX_SEARCH_VALUE_LENGTH = 4096
21
25
 
22
26
 
27
+ class HopperDocument(Protocol):
28
+ """Typed minimum used by the provider-neutral Hopper API facade."""
29
+
30
+ def getDocumentName(self) -> str: ...
31
+
32
+ def getExecutableFilePath(self) -> Optional[str]: ...
33
+
34
+ def getDatabaseFilePath(self) -> Optional[str]: ...
35
+
36
+ def backgroundProcessActive(self) -> bool: ...
37
+
38
+
39
+ class HopperDocumentProvider(Protocol):
40
+ """Hopper's injected global document provider."""
41
+
42
+ @classmethod
43
+ def getAllDocuments(cls) -> Sequence[HopperDocument]: ...
44
+
45
+ @classmethod
46
+ def getCurrentDocument(cls) -> Optional[HopperDocument]: ...
47
+
48
+
49
+ class HopperApiFacade:
50
+ """Small injected boundary around Hopper globals for standalone tests."""
51
+
52
+ def __init__(self, document_provider: HopperDocumentProvider):
53
+ self._document_provider = document_provider
54
+
55
+ def documents(self) -> Sequence[HopperDocument]:
56
+ return self._document_provider.getAllDocuments()
57
+
58
+ def current_document(self) -> Optional[HopperDocument]:
59
+ return self._document_provider.getCurrentDocument()
60
+
61
+ def require_analysis_complete(
62
+ self, document: HopperDocument, operation: str
63
+ ) -> None:
64
+ if document.backgroundProcessActive():
65
+ raise CapabilityUnavailableError(
66
+ "%s requires completed Hopper background analysis" % operation
67
+ )
68
+
69
+
70
+ def _configure_hopper_api(document_provider):
71
+ """Inject Hopper's document provider or a standalone test facade."""
72
+ global _hopper_api
73
+ _hopper_api = HopperApiFacade(document_provider)
74
+
75
+
76
+ def _api():
77
+ """Resolve Hopper globals lazily so importing this module needs no Hopper."""
78
+ global _hopper_api
79
+ if _hopper_api is not None:
80
+ return _hopper_api
81
+ document_provider = globals().get("Document")
82
+ if document_provider is None:
83
+ raise CapabilityUnavailableError(
84
+ "Hopper Document API is unavailable outside the Hopper runtime"
85
+ )
86
+ _hopper_api = HopperApiFacade(document_provider)
87
+ return _hopper_api
88
+
89
+
23
90
  def _session_document():
24
91
  """Find only the document opened for this authenticated REA session."""
25
92
  target = os.path.realpath(REA_TARGET_PATH)
26
- for document in Document.getAllDocuments():
93
+ for document in _api().documents():
27
94
  paths = (document.getExecutableFilePath(), document.getDatabaseFilePath())
28
95
  for path in paths:
29
96
  if path and os.path.realpath(path) == target:
@@ -36,6 +103,31 @@ MAX_REGEX_CANDIDATE_LENGTH = 4096
36
103
  MAX_REGEX_SEARCH_WORK_UNITS = 1000000
37
104
 
38
105
 
106
+ class CapabilityUnavailableError(Exception):
107
+ """The active Hopper build lacks one admitted public API operation."""
108
+
109
+
110
+ EXHAUSTIVE_ANALYSIS_METHODS = frozenset(
111
+ (
112
+ "analyze_function",
113
+ "list_names",
114
+ "list_procedures",
115
+ "list_strings",
116
+ "procedure_address",
117
+ "procedure_assembly",
118
+ "procedure_callees",
119
+ "procedure_callers",
120
+ "procedure_info",
121
+ "procedure_pseudo_code",
122
+ "procedure_references",
123
+ "read_function_instructions",
124
+ "resolve_containing_procedure",
125
+ "search_procedures",
126
+ "search_strings",
127
+ )
128
+ )
129
+
130
+
39
131
  def _hex(value):
40
132
  return "0x%x" % value
41
133
 
@@ -54,7 +146,8 @@ def _json_safe(value):
54
146
  def _document(name=None):
55
147
  """Resolve an explicit or session-selected document without changing Hopper UI."""
56
148
  global _selected_document
57
- documents = Document.getAllDocuments()
149
+ api = _api()
150
+ documents = api.documents()
58
151
  if name is not None:
59
152
  for candidate in documents:
60
153
  if candidate.getDocumentName() == name:
@@ -64,7 +157,7 @@ def _document(name=None):
64
157
  for candidate in documents:
65
158
  if candidate.getDocumentName() == _selected_document:
66
159
  return candidate
67
- current = Document.getCurrentDocument()
160
+ current = api.current_document()
68
161
  if current is None:
69
162
  raise ValueError("No Hopper document is loaded")
70
163
  return current
@@ -225,12 +318,35 @@ def _invalidate_search_inventory(document):
225
318
  del _search_inventory_cache[key]
226
319
 
227
320
 
321
+ def _invalidate_pseudocode(document):
322
+ """Discard cached decompilation after an analysis annotation changes."""
323
+ document_id = id(document)
324
+ for key in list(_pseudocode_cache):
325
+ if key[0] == document_id:
326
+ del _pseudocode_cache[key]
327
+
328
+
329
+ def _pseudocode(document, procedure):
330
+ """Reuse one provider decompilation for repeated bounded projections."""
331
+ key = (id(document), procedure.getEntryPoint())
332
+ if key not in _pseudocode_cache:
333
+ _pseudocode_cache[key] = procedure.decompile()
334
+ return _pseudocode_cache[key]
335
+
336
+
228
337
  def _search_inventory(document, kind):
229
338
  """Cache an immutable, address-sorted inventory for an unchanged document."""
230
339
  key = (id(document), kind)
231
340
  inventory = _search_inventory_cache.get(key)
232
341
  if inventory is None:
233
- values = _procedure_map(document) if kind == "procedure" else _strings(document)
342
+ if kind == "procedure":
343
+ values = _procedure_map(document)
344
+ elif kind == "string":
345
+ values = _strings(document)
346
+ elif kind == "name":
347
+ values = {_hex(address): value for address, value in _name_map(document).items()}
348
+ else:
349
+ raise ValueError("Unknown inventory kind")
234
350
  inventory = tuple(sorted(values.items(), key=lambda item: int(item[0], 16)))
235
351
  _search_inventory_cache[key] = inventory
236
352
  return inventory
@@ -507,6 +623,22 @@ def _name_map(document):
507
623
  return result
508
624
 
509
625
 
626
+ def _render_instruction(segment, address):
627
+ instruction = segment.getInstructionAtAddress(address)
628
+ if instruction is None:
629
+ return None
630
+ arguments = [
631
+ instruction.getFormattedArgument(index)
632
+ for index in range(instruction.getArgumentCount())
633
+ ]
634
+ suffix = ", ".join(value for value in arguments if value is not None)
635
+ return "%s: %s%s" % (
636
+ _hex(address),
637
+ instruction.getInstructionString(),
638
+ (" " + suffix) if suffix else "",
639
+ )
640
+
641
+
510
642
  def _assembly(procedure, limit=None):
511
643
  """Render bounded assembly while guarding against malformed instruction cycles."""
512
644
  lines = []
@@ -520,9 +652,9 @@ def _assembly(procedure, limit=None):
520
652
  instruction = segment.getInstructionAtAddress(address)
521
653
  if instruction is None:
522
654
  break
523
- arguments = [instruction.getFormattedArgument(index) for index in range(instruction.getArgumentCount())]
524
- suffix = ", ".join(value for value in arguments if value is not None)
525
- lines.append("%s: %s%s" % (_hex(address), instruction.getInstructionString(), (" " + suffix) if suffix else ""))
655
+ line = _render_instruction(segment, address)
656
+ if line is not None:
657
+ lines.append(line)
526
658
  if limit is not None and len(lines) >= limit:
527
659
  return "\n".join(lines)
528
660
  length = instruction.getInstructionLength()
@@ -532,6 +664,45 @@ def _assembly(procedure, limit=None):
532
664
  return "\n".join(lines)
533
665
 
534
666
 
667
+ def _read_function_instructions(document, params):
668
+ """Read one bounded raw-instruction window without invoking decompilation."""
669
+ procedure = _procedure(document, params.get("procedure"))
670
+ offset = _offset(params, "offset")
671
+ limit = params.get("limit", 64)
672
+ if offset > 100000:
673
+ raise ValueError("offset must not exceed 100000")
674
+ if not isinstance(limit, int) or isinstance(limit, bool) or limit < 1 or limit > 500:
675
+ raise ValueError("limit must be an integer between 1 and 500")
676
+ addresses, scan_truncated = _instruction_addresses(
677
+ procedure, offset + limit + 1
678
+ )
679
+ start = min(offset, len(addresses))
680
+ end = min(len(addresses), start + limit)
681
+ segment = procedure.getSegment()
682
+ items = []
683
+ for address in addresses[start:end]:
684
+ line = _render_instruction(segment, address)
685
+ if line is not None:
686
+ items.append(line)
687
+ has_more = scan_truncated or end < len(addresses)
688
+ return {
689
+ "procedure": _procedure_identity(procedure),
690
+ "instructions": {
691
+ "items": items,
692
+ "total": None if scan_truncated else len(addresses),
693
+ "returned": len(items),
694
+ "truncated": has_more,
695
+ "next_offset": end if has_more else None,
696
+ },
697
+ "instructions_scanned": len(addresses),
698
+ "instruction_scan_truncated": scan_truncated,
699
+ "limitations": [
700
+ "Instruction text and ordering are Hopper-specific representations.",
701
+ "The fast path does not decompile the procedure or scan whole-program names and strings.",
702
+ ],
703
+ }
704
+
705
+
535
706
  def _analyze_function(document, params):
536
707
  """Collect a bounded, single-pass function dossier for agent callers."""
537
708
  procedure = _procedure(document, params.get("procedure"))
@@ -560,8 +731,8 @@ def _analyze_function(document, params):
560
731
  "end": _hex(block.getEndingAddress()),
561
732
  "successors": sorted(set(successors), key=lambda value: int(value, 16)),
562
733
  })
563
- pseudo = procedure.decompile() or ""
564
- assembly_lines = _assembly(procedure).splitlines() if params.get("include_assembly", False) else []
734
+ pseudo = _pseudocode(document, procedure) or ""
735
+ assembly_lines = _assembly(procedure, max_instructions).splitlines() if params.get("include_assembly", False) else []
565
736
  callers = sorted((_procedure_identity(item) for item in procedure.getAllCallerProcedures()), key=lambda item: int(item["address"], 16))
566
737
  callees = sorted((_procedure_identity(item) for item in procedure.getAllCalleeProcedures()), key=lambda item: int(item["address"], 16))
567
738
  comments = []
@@ -595,8 +766,14 @@ def _analyze_function(document, params):
595
766
  incoming.append(item)
596
767
  if source in procedure_addresses:
597
768
  outgoing.append(item)
598
- string_map = {int(address, 16): value for address, value in _strings(document).items()}
599
- name_map = _name_map(document)
769
+ string_map = {
770
+ int(address, 16): value
771
+ for address, value in _search_inventory(document, "string")
772
+ }
773
+ name_map = {
774
+ int(address, 16): value
775
+ for address, value in _search_inventory(document, "name")
776
+ }
600
777
  referenced_strings = []
601
778
  referenced_names = []
602
779
  for edge in outgoing:
@@ -615,7 +792,13 @@ def _analyze_function(document, params):
615
792
  return {
616
793
  "procedure": {"address": _hex(procedure.getEntryPoint()), "name": _procedure_name(procedure), "classification": None, "signature": procedure.signatureString(), "locals": _procedure_locals(procedure)},
617
794
  "pseudocode": {"text": pseudo_text, "total_chars": len(pseudo), "returned_chars": len(pseudo_text), "truncated": pseudo_next < len(pseudo), "next_offset": pseudo_next if pseudo_next < len(pseudo) else None},
618
- "assembly": _bounded(assembly_lines, assembly_offset, max_instructions),
795
+ "assembly": _bounded(
796
+ assembly_lines,
797
+ assembly_offset,
798
+ max_instructions,
799
+ None,
800
+ instruction_scan_truncated if params.get("include_assembly", False) else False,
801
+ ),
619
802
  "comments": collection("comments", comments, instruction_scan_truncated),
620
803
  "callers": collection("callers", callers), "callees": collection("callees", callees),
621
804
  "incoming_references": collection("incoming_references", incoming, instruction_scan_truncated),
@@ -671,9 +854,54 @@ def _dispatch(method, params):
671
854
  return _selected_document
672
855
 
673
856
  document = _document(params.get("document"))
857
+ if method in EXHAUSTIVE_ANALYSIS_METHODS:
858
+ _api().require_analysis_complete(document, method)
674
859
 
675
860
  if method == "analyze_function":
676
861
  return _analyze_function(document, params)
862
+ if method == "read_function_instructions":
863
+ return _read_function_instructions(document, params)
864
+ if method == "read_bytes":
865
+ length = params.get("length", 256)
866
+ if isinstance(length, bool) or not isinstance(length, int):
867
+ raise ValueError("Byte-read length must be an integer")
868
+ if length < 1 or length > MAX_READ_BYTES:
869
+ raise ValueError("Byte-read length must be between 1 and 4096")
870
+ reader = getattr(document, "readBytes", None)
871
+ if not callable(reader):
872
+ raise CapabilityUnavailableError(
873
+ "Hopper's public Python API does not expose readBytes"
874
+ )
875
+ address = _address(document, params.get("address"))
876
+ value = reader(address, length)
877
+ if not isinstance(value, (bytes, bytearray)):
878
+ raise CapabilityUnavailableError(
879
+ "Hopper's public Python API returned an unsupported byte representation"
880
+ )
881
+ data = bytes(value)
882
+ return {
883
+ "address": _hex(address),
884
+ "requested_bytes": length,
885
+ "returned_bytes": len(data),
886
+ "bytes_hex": data.hex(),
887
+ "complete": len(data) == length,
888
+ }
889
+ if method == "address_to_file_offset":
890
+ mapper = getattr(document, "getFileOffsetFromAddress", None)
891
+ if not callable(mapper):
892
+ raise CapabilityUnavailableError(
893
+ "Hopper's public Python API does not expose getFileOffsetFromAddress"
894
+ )
895
+ address = _address(document, params.get("address"))
896
+ offset = mapper(address)
897
+ if (
898
+ isinstance(offset, bool)
899
+ or not isinstance(offset, int)
900
+ or offset in BAD_ADDRESSES
901
+ or offset < 0
902
+ ):
903
+ raise ValueError("Address has no authoritative file-offset mapping")
904
+ return {"address": _hex(address), "file_offset": offset}
677
905
  if method == "resolve_containing_procedure":
678
906
  address = _address(document, params.get("address"))
679
907
  procedure, reason = _containing_procedure(document, address)
@@ -740,19 +968,16 @@ def _dispatch(method, params):
740
968
  })
741
969
  return result
742
970
  if method == "list_procedures":
743
- return _page(_procedure_map(document), params.get("offset", 0), params.get("limit", 100))
971
+ return _page(dict(_search_inventory(document, "procedure")), params.get("offset", 0), params.get("limit", 100))
744
972
  if method == "list_strings":
745
- values = _strings(document)
973
+ values = dict(_search_inventory(document, "string"))
746
974
  requested = params.get("address")
747
975
  if requested is not None:
748
976
  key = _hex(_address(document, requested))
749
977
  values = {key: values[key]} if key in values else {}
750
978
  return _page(values, params.get("offset", 0), params.get("limit", 100))
751
979
  if method == "list_names":
752
- result = {}
753
- for segment in document.getSegmentsList():
754
- for item in segment.getNamedAddresses():
755
- result[_hex(item)] = segment.getNameAtAddress(item)
980
+ result = dict(_search_inventory(document, "name"))
756
981
  requested = params.get("address")
757
982
  if requested is not None:
758
983
  key = _hex(_address(document, requested))
@@ -768,7 +993,7 @@ def _dispatch(method, params):
768
993
  if method == "procedure_assembly":
769
994
  return _assembly(procedure)
770
995
  if method == "procedure_pseudo_code":
771
- return procedure.decompile()
996
+ return _pseudocode(document, procedure)
772
997
  if method == "procedure_callers":
773
998
  return sorted((_hex(item.getEntryPoint()) for item in procedure.getAllCallerProcedures()), key=lambda value: int(value, 16))
774
999
  if method == "procedure_callees":
@@ -779,12 +1004,18 @@ def _dispatch(method, params):
779
1004
  return {"name": _procedure_name(procedure), "entrypoint": _hex(procedure.getEntryPoint()), "basicblock_count": procedure.getBasicBlockCount(), "length": length, "signature": procedure.signatureString(), "locals": _procedure_locals(procedure), "classification": None}
780
1005
  if method == "set_address_name":
781
1006
  address = _address(document, params.get("address"))
782
- result = document.setNameAtAddress(address, params["name"])
783
- _invalidate_search_inventory(document)
1007
+ try:
1008
+ result = document.setNameAtAddress(address, params["name"])
1009
+ finally:
1010
+ _invalidate_search_inventory(document)
1011
+ _invalidate_pseudocode(document)
784
1012
  return result
785
1013
  if method == "set_addresses_names":
786
- result = {key: document.setNameAtAddress(_address(document, key), value) for key, value in params["names"].items()}
787
- _invalidate_search_inventory(document)
1014
+ try:
1015
+ result = {key: document.setNameAtAddress(_address(document, key), value) for key, value in params["names"].items()}
1016
+ finally:
1017
+ _invalidate_search_inventory(document)
1018
+ _invalidate_pseudocode(document)
788
1019
  return result
789
1020
  if method in ("set_comment", "set_inline_comment"):
790
1021
  address = _address(document, params.get("address"))
@@ -792,6 +1023,7 @@ def _dispatch(method, params):
792
1023
  setter = segment.setCommentAtAddress if method == "set_comment" else segment.setInlineCommentAtAddress
793
1024
  getter = segment.getCommentAtAddress if method == "set_comment" else segment.getInlineCommentAtAddress
794
1025
  setter(address, params["comment"])
1026
+ _invalidate_pseudocode(document)
795
1027
  return getter(address) == params["comment"]
796
1028
  if method == "list_bookmarks":
797
1029
  return [{"address": _hex(item), "name": document.getBookmarkName(item)} for item in document.getBookmarks()]
@@ -838,6 +1070,8 @@ def _serve_connection(connection):
838
1070
 
839
1071
 
840
1072
  def _diagnostic_type(error):
1073
+ if isinstance(error, CapabilityUnavailableError):
1074
+ return "capability_unavailable"
841
1075
  if isinstance(error, PermissionError):
842
1076
  return "authorization"
843
1077
  if isinstance(error, (ValueError, TypeError, KeyError)):
@@ -864,4 +1098,5 @@ def _run():
864
1098
 
865
1099
  # Hopper's public objects are bound to its dedicated Python execution thread.
866
1100
  # Keep dispatch on that thread; moving calls to an arbitrary worker can deadlock.
867
- _run()
1101
+ if __name__ == "__main__":
1102
+ _run()
@@ -0,0 +1,14 @@
1
+ import { ProviderCleanupError } from "../domain/providerCleanupError.js";
2
+ import { err, ok } from "../domain/result.js";
3
+ /** Close one provider client and normalize unexpected adapter rejection. */
4
+ export const closeAnalysisClient = async (client, providerId, options = {}) => {
5
+ try {
6
+ if (client.closeWithOutcome !== undefined)
7
+ return await client.closeWithOutcome(options);
8
+ await client.close();
9
+ return ok(null);
10
+ }
11
+ catch (cause) {
12
+ return err(new ProviderCleanupError(providerId, ["provider-client"], { reason: "provider client close rejected unexpectedly" }, { cause }));
13
+ }
14
+ };
@@ -1,11 +1,12 @@
1
1
  import { z } from "zod";
2
- import { compareApplicationVersionsRequestSchema, compareJavaScriptExportShapesRequestSchema, traceApplicationFeatureRequestSchema, traceJavaScriptSemanticsRequestSchema, } from "../contracts/applicationWorkflowInputContracts.js";
2
+ import { compareApplicationVersionsRequestSchema, compareJavaScriptExportShapesRequestSchema, compareSourceToBundleRequestSchema, traceApplicationFeatureRequestSchema, traceJavaScriptSemanticsRequestSchema, } from "../contracts/applicationWorkflowInputContracts.js";
3
3
  import { AnalysisInputError } from "../domain/errors.js";
4
4
  import { projectInputIssues } from "../domain/inputIssueProjection.js";
5
5
  import { compareApplicationVersionsInputSchema } from "../domain/javascriptApplicationVersionComparisonSchemas.js";
6
6
  import { compareJavaScriptExportShapesInputSchema } from "../domain/javascriptExportShapeComparisonSchemas.js";
7
7
  import { traceApplicationFeatureInputSchema } from "../domain/javascriptFeatureTraceSchemas.js";
8
8
  import { traceJavaScriptSemanticsInputSchema } from "../domain/javascriptSemanticTraceSchemas.js";
9
+ import { compareSourceToBundleInputSchema } from "../domain/sourceToBundleComparisonSchemas.js";
9
10
  import { err, ok } from "../domain/result.js";
10
11
  import { resolveEvidenceReferences, } from "./EvidenceReferenceResolver.js";
11
12
  const APPLICATION_GRAPH_IDENTITIES = [
@@ -35,18 +36,14 @@ export const resolveTraceApplicationFeatureRequest = (input, lookup) => {
35
36
  };
36
37
  /** Resolve a trace request already parsed by the caller-facing contract. */
37
38
  export const resolveTraceApplicationFeatureRequestValidated = (input, lookup) => {
38
- const application = input.application === undefined
39
- ? resolveEvidenceReferences(lookup, input.application_evidence_id === undefined
40
- ? []
41
- : [input.application_evidence_id], APPLICATION_GRAPH_IDENTITIES)
42
- : ok([input.application]);
39
+ const application = graphEvidence(input.application, input.application_evidence_id, lookup);
43
40
  if (!application.ok)
44
41
  return application;
45
42
  const native = resolveEvidenceReferences(lookup, input.native_observation_evidence_ids);
46
43
  if (!native.ok)
47
44
  return native;
48
45
  const raw = {
49
- application: application.value[0],
46
+ application: application.value,
50
47
  native_observations: [...input.native_observations, ...native.value],
51
48
  seed: input.seed,
52
49
  direction: input.direction,
@@ -66,14 +63,10 @@ export const resolveTraceJavaScriptSemanticsRequest = (input, lookup) => {
66
63
  };
67
64
  /** Resolve a semantic trace request already parsed by its public contract. */
68
65
  export const resolveTraceJavaScriptSemanticsRequestValidated = (input, lookup) => {
69
- const application = input.application === undefined
70
- ? resolveEvidenceReferences(lookup, input.application_evidence_id === undefined
71
- ? []
72
- : [input.application_evidence_id], APPLICATION_GRAPH_IDENTITIES)
73
- : ok([input.application]);
66
+ const application = graphEvidence(input.application, input.application_evidence_id, lookup);
74
67
  if (!application.ok)
75
68
  return application;
76
- const raw = { application: application.value[0], query: input.query };
69
+ const raw = { application: application.value, query: input.query };
77
70
  const parsed = traceJavaScriptSemanticsInputSchema.safeParse(raw);
78
71
  return parsed.success
79
72
  ? ok(parsed.data)
@@ -121,6 +114,31 @@ export const resolveCompareApplicationVersionsRequestValidated = (input, lookup)
121
114
  ? ok(parsed.data)
122
115
  : invalid("compare_application_versions", parsed.error, raw);
123
116
  };
117
+ /** Parse and resolve one historical-source comparison adapter request. */
118
+ export const resolveCompareSourceToBundleRequest = (input, lookup) => {
119
+ const parsed = compareSourceToBundleRequestSchema.safeParse(input);
120
+ return parsed.success
121
+ ? resolveCompareSourceToBundleRequestValidated(parsed.data, lookup)
122
+ : invalid("compare_source_to_bundle", parsed.error, input);
123
+ };
124
+ /** Resolve a historical-source comparison request parsed by its public contract. */
125
+ export const resolveCompareSourceToBundleRequestValidated = (input, lookup) => {
126
+ const application = graphEvidence(input.application, input.application_evidence_id, lookup);
127
+ if (!application.ok)
128
+ return application;
129
+ const raw = {
130
+ reference: input.reference,
131
+ application: application.value,
132
+ limits: input.limits,
133
+ ...(input.unknown_registry_approved === undefined
134
+ ? {}
135
+ : { unknown_registry_approved: input.unknown_registry_approved }),
136
+ };
137
+ const parsed = compareSourceToBundleInputSchema.safeParse(raw);
138
+ return parsed.success
139
+ ? ok(parsed.data)
140
+ : invalid("compare_source_to_bundle", parsed.error, raw);
141
+ };
124
142
  /** Parse and resolve an export-shape adapter request into canonical input. */
125
143
  export const resolveCompareJavaScriptExportShapesRequest = (input, lookup) => {
126
144
  const parsed = compareJavaScriptExportShapesRequestSchema.safeParse(input);
@@ -9,6 +9,7 @@ import { BinarySessionRecords } from "./BinarySessionRecords.js";
9
9
  import { binarySessionStatus } from "./BinarySessionStatus.js";
10
10
  import { resolveSessionOpen, } from "./BinarySessionOpen.js";
11
11
  import { prepareSessionExecution } from "./BinarySessionExecution.js";
12
+ import { closeAnalysisClient } from "./AnalysisClientCleanup.js";
12
13
  const OFFICIAL_OPERATIONS = new Set(OFFICIAL_TOOL_CONTRACTS.map(({ name }) => name));
13
14
  /**
14
15
  * Owns the single active target shared by CLI and MCP adapters.
@@ -116,7 +117,14 @@ export class BinarySession extends BinarySessionRecords {
116
117
  await this.#drainCalls();
117
118
  const previous = this.#active;
118
119
  this.#active = undefined;
119
- await previous?.client.close();
120
+ if (previous !== undefined) {
121
+ const closed = await closeAnalysisClient(previous.client, previous.route.identity.id);
122
+ if (!closed.ok) {
123
+ this.clearSessionRecords();
124
+ this.#clearRuntimeAvailability();
125
+ return closed;
126
+ }
127
+ }
120
128
  const runId = randomUUID();
121
129
  const client = route.createClient(target, { runId });
122
130
  const started = await client.execute("health", {}, options);
@@ -157,15 +165,17 @@ export class BinarySession extends BinarySessionRecords {
157
165
  });
158
166
  }
159
167
  /** Close the active target, if any. */
160
- close() {
168
+ close(options = {}) {
161
169
  return this.#serialize(async () => {
162
170
  const previous = this.#active;
163
171
  this.#active = undefined;
164
172
  await this.#drainCalls();
165
- await previous?.client.close();
173
+ const closed = previous === undefined
174
+ ? ok(null)
175
+ : await closeAnalysisClient(previous.client, previous.route.identity.id, options);
166
176
  this.clearSessionRecords();
167
177
  this.#clearRuntimeAvailability();
168
- return ok(null);
178
+ return closed;
169
179
  });
170
180
  }
171
181
  /** Describe the active binary session. */
@@ -177,6 +187,7 @@ export class BinarySession extends BinarySessionRecords {
177
187
  runtimeAvailability: this.#runtimeAvailability,
178
188
  runId: this.#active?.runId,
179
189
  runtimeLineageSnapshots: this.#active?.client.runtimeLineageSnapshots?.() ?? [],
190
+ requestActivitySnapshots: this.#active?.client.requestActivitySnapshots?.() ?? [],
180
191
  });
181
192
  }
182
193
  /** Return the immutable artifact identity captured before its provider started. */
@@ -1,5 +1,5 @@
1
1
  /** Project internal provider routing state into the caller-visible session status. */
2
- export const binarySessionStatus = ({ target, route, router, runtimeAvailability, runId, runtimeLineageSnapshots, }) => {
2
+ export const binarySessionStatus = ({ target, route, router, runtimeAvailability, runId, runtimeLineageSnapshots, requestActivitySnapshots, }) => {
3
3
  const configuredProvider = router.configuredIdentity();
4
4
  const provider = providerSummary(configuredProvider);
5
5
  const providers = router.providerIdentities(route).map(providerSummary);
@@ -41,6 +41,7 @@ export const binarySessionStatus = ({ target, route, router, runtimeAvailability
41
41
  run_id: runId,
42
42
  process_lineage: processLineageStatus(runtimeLineageSnapshots),
43
43
  },
44
+ analysis_activity: requestActivityStatus(requestActivitySnapshots),
44
45
  };
45
46
  return target === undefined
46
47
  ? { open: false, ...common }
@@ -54,6 +55,35 @@ export const binarySessionStatus = ({ target, route, router, runtimeAvailability
54
55
  architecture: target.architecture ?? null,
55
56
  };
56
57
  };
58
+ const requestActivityStatus = (snapshots) => {
59
+ const providers = snapshots
60
+ .slice()
61
+ .sort((left, right) => left.provider.id.localeCompare(right.provider.id))
62
+ .map(({ provider, active, queuedRequests }) => ({
63
+ provider: providerSummary(provider),
64
+ active: active === null
65
+ ? null
66
+ : {
67
+ request_id: active.requestId,
68
+ operation: active.operation,
69
+ elapsed_ms: active.elapsedMs,
70
+ timeout_ms: active.timeoutMs,
71
+ caller_state: active.callerState,
72
+ },
73
+ queued_requests: queuedRequests,
74
+ }));
75
+ const active = snapshots.flatMap((snapshot) => snapshot.active === null ? [] : [snapshot.active]);
76
+ return {
77
+ status: snapshots.length === 0
78
+ ? "not_observed"
79
+ : active.some(({ callerState }) => callerState === "timed_out")
80
+ ? "timed_out_busy"
81
+ : active.length > 0
82
+ ? "busy"
83
+ : "idle",
84
+ providers,
85
+ };
86
+ };
57
87
  const providerSummary = ({ id, name, version, }) => ({ id, name, version });
58
88
  const processLineageStatus = (snapshots) => snapshots.length === 0
59
89
  ? { status: "not_observed" }
@@ -1,5 +1,6 @@
1
1
  import { createEvidence, } from "../domain/evidence.js";
2
2
  import { jsonValueSchema } from "../domain/jsonValue.js";
3
+ import { commitBrowserScenarioNormalization } from "../domain/browserScenarioNormalization.js";
3
4
  /** Create Evidence v2 for a policy-scoped external browser observation. */
4
5
  export const createBrowserEvidence = (operation, input, result, provider) => createEvidence(undefined, provider, {
5
6
  predicateType: browserPredicate(operation),
@@ -20,6 +21,18 @@ export const createBrowserEvidence = (operation, input, result, provider) => cre
20
21
  });
21
22
  const browserParameters = (input) => {
22
23
  if (!("cdp_endpoint" in input)) {
24
+ if ("before_scenario" in input &&
25
+ input.before_scenario !== undefined &&
26
+ input.after_scenario !== undefined)
27
+ return {
28
+ comparison_kind: "browser_scenario",
29
+ before_browser: input.before_scenario.browser,
30
+ after_browser: input.after_scenario.browser,
31
+ before_start_origin: input.before_scenario.scenario.start_origin,
32
+ after_start_origin: input.after_scenario.scenario.start_origin,
33
+ max_changes: input.max_changes,
34
+ normalization_sha256: commitBrowserScenarioNormalization(input.normalization).sha256,
35
+ };
23
36
  if ("max_changes" in input)
24
37
  return {
25
38
  before_target_id: input.before.inspection.target.target_id,
@@ -74,6 +87,7 @@ const browserParameters = (input) => {
74
87
  include_websocket_shapes: input.include_websocket_shapes,
75
88
  include_script_sources: input.include_script_sources,
76
89
  include_storage_keys: input.include_storage_keys,
90
+ include_storage_fingerprints: input.include_storage_fingerprints,
77
91
  limits: input.limits,
78
92
  ...(input.include_script_sources && "source_capture_approved" in input
79
93
  ? {
@@ -0,0 +1 @@
1
+ export {};