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.
- package/README.md +91 -19
- package/bridge/ghidra/ReaGhidraBridge.java +524 -4
- package/bridge/hopper_bridge.py +313 -28
- package/dist/application/AnalysisClientCleanup.js +14 -0
- package/dist/application/AnalysisContextQueries.js +130 -0
- package/dist/application/AnalysisSnapshotCache.js +4 -2
- package/dist/application/ApplicationWorkflowEvidenceResolver.js +31 -13
- package/dist/application/ArtifactInventory/scanCanonical.js +1 -1
- package/dist/application/BinarySession.js +20 -8
- package/dist/application/BinarySessionComposition.js +21 -0
- package/dist/application/BinarySessionRecords.js +79 -9
- package/dist/application/BinarySessionStatus.js +31 -1
- package/dist/application/BrowserEvidence.js +14 -0
- package/dist/application/BrowserScenarioCapturePort.js +1 -0
- package/dist/application/BrowserScenarioCaptureService.js +53 -0
- package/dist/application/BrowserScenarioEvidence.js +40 -0
- package/dist/application/CallPathTracing.js +160 -0
- package/dist/application/CapabilityInventory.js +84 -14
- package/dist/application/ClientRegistrationStatus.js +4 -1
- package/dist/application/CompositeProvider.js +10 -1
- package/dist/application/DirectAnalysis.js +13 -4
- package/dist/application/EnhancedLiteralTracing.js +118 -0
- package/dist/application/EnhancedTools.js +71 -127
- package/dist/application/EvidenceLedger.js +45 -16
- package/dist/application/JavaScriptApplicationWorkflowEvidence.js +13 -0
- package/dist/application/JavaScriptApplicationWorkflowService.js +27 -1
- package/dist/application/JavaScriptArtifactAnalysis.js +13 -4
- package/dist/application/JavaScriptRuntimeObservationEvidence.js +34 -0
- package/dist/application/JavaScriptRuntimeObservationPort.js +1 -0
- package/dist/application/JavaScriptRuntimeObservationService.js +46 -0
- package/dist/application/JavaScriptSemanticGraphAsyncProjection.js +147 -0
- package/dist/application/JavaScriptSemanticGraphBuilder.js +34 -6
- package/dist/application/JavaScriptSemanticGraphChildProcessProjection.js +156 -0
- package/dist/application/JavaScriptSemanticGraphConstruction.js +2 -2
- package/dist/application/JavaScriptSemanticGraphDataProjection.js +203 -0
- package/dist/application/JavaScriptSemanticGraphFingerprintProjection.js +25 -0
- package/dist/application/JavaScriptSemanticGraphFlowProjection.js +129 -8
- package/dist/application/JavaScriptSemanticGraphObjectProjection.js +81 -0
- package/dist/application/JavaScriptSemanticGraphProjection.js +14 -7
- package/dist/application/JavaScriptSemanticGraphResourceProjection.js +59 -0
- package/dist/application/JavaScriptSemanticGraphValueProjection.js +77 -0
- package/dist/application/LazyAnalysisProvider.js +77 -0
- package/dist/application/LoopbackReplay.js +1 -1
- package/dist/application/NativeApiInspection.js +80 -0
- package/dist/application/ProcessReactiveEffects.js +6 -0
- package/dist/application/ProcessReactiveHarness.js +16 -1
- package/dist/application/ProjectPermissionStore.js +1 -0
- package/dist/application/ReconstructionObligationAuthority.js +72 -0
- package/dist/application/ReconstructionObligationCandidates.js +265 -0
- package/dist/application/ReconstructionObligationLedgerDiagnostics.js +47 -0
- package/dist/application/ReconstructionObligationLedgerEvaluation.js +263 -0
- package/dist/application/ReconstructionObligationLedgerProjection.js +86 -0
- package/dist/application/ReconstructionObligationLedgerService.js +72 -0
- package/dist/application/ReconstructionObligationOriginalCases.js +33 -0
- package/dist/application/ReconstructionObligationPolicies.js +132 -0
- package/dist/application/ReconstructionReadinessService.js +48 -0
- package/dist/application/SessionProviderRouter.js +6 -6
- package/dist/application/SetupClientConfiguration.js +4 -2
- package/dist/application/runtime.js +26 -9
- package/dist/artifacts/ArtifactProvider.js +87 -22
- package/dist/browser/BrowserScenarioSecrets.js +53 -0
- package/dist/browser/BrowserScenarioSessionPort.js +1 -0
- package/dist/browser/CdpBrowserProvider.js +2 -2
- package/dist/browser/CdpCaptureDocuments.js +57 -0
- package/dist/browser/CdpCaptureStorage.js +71 -29
- package/dist/browser/CdpCaptureStorageFingerprints.js +328 -0
- package/dist/browser/CdpEndpoint.js +8 -6
- package/dist/browser/CdpPageCapture.js +2 -0
- package/dist/browser/JavaScriptRuntimeScope.js +38 -0
- package/dist/browser/PlaywrightBrowserScenarioProvider.js +259 -0
- package/dist/browser/PlaywrightExecutionBoundary.js +42 -0
- package/dist/browser/PlaywrightScenarioActions.js +62 -0
- package/dist/browser/PlaywrightScenarioArtifacts.js +238 -0
- package/dist/browser/PlaywrightScenarioBrowser.js +144 -0
- package/dist/browser/PlaywrightScenarioEvents.js +301 -0
- package/dist/browser/PlaywrightScenarioSession.js +300 -0
- package/dist/browser/V8InspectorCaptureProjection.js +110 -0
- package/dist/browser/V8InspectorEndpoint.js +72 -0
- package/dist/browser/V8InspectorProvider.js +267 -0
- package/dist/catalogIdentity.js +11 -3
- package/dist/cli/artifactCommands.js +44 -3
- package/dist/cli/coreAnalysisCommands.js +78 -0
- package/dist/cli.js +4 -0
- package/dist/cliApplicationCommands.js +30 -2
- package/dist/cliBrowserAdvancedCommands.js +17 -5
- package/dist/cliBrowserCommands.js +1 -0
- package/dist/cliBrowserScenarioCommands.js +43 -0
- package/dist/cliCommandNames.js +9 -0
- package/dist/cliJavaScriptRuntimeCommands.js +164 -0
- package/dist/cliObservationOptions.js +4 -0
- package/dist/cliPolicyCommands.js +1 -0
- package/dist/config/environment.js +12 -0
- package/dist/config/parseConfig.js +88 -17
- package/dist/contracts/analysisSearchContract.js +14 -0
- package/dist/contracts/applicationToolContracts.js +86 -2
- package/dist/contracts/applicationWorkflowInputContracts.js +18 -0
- package/dist/contracts/artifactToolContracts.js +29 -2
- package/dist/contracts/browserProviderToolContracts.js +7 -0
- package/dist/contracts/browserScenarioToolContracts.js +81 -0
- package/dist/contracts/browserToolContracts.js +88 -98
- package/dist/contracts/enhancedInputs.js +35 -6
- package/dist/contracts/functionInstructionContract.js +8 -0
- package/dist/contracts/functionWorkflowToolContracts.js +30 -0
- package/dist/contracts/hopperMemoryContracts.js +22 -0
- package/dist/contracts/javascriptApplicationWorkflowExamples.js +42 -0
- package/dist/contracts/javascriptRuntimeObservationToolContracts.js +58 -0
- package/dist/contracts/promptContracts.js +14 -15
- package/dist/contracts/reconstructionReadinessExample.js +436 -0
- package/dist/contracts/toolContractExamples.js +8 -0
- package/dist/contracts/toolContractTypes.js +1 -0
- package/dist/contracts/toolContracts.js +34 -25
- package/dist/contracts/toolEffects.js +25 -0
- package/dist/contracts/toolOutputSchemaGroups.js +120 -30
- package/dist/contracts/toolOutputSchemaPrimitives.js +37 -0
- package/dist/domain/analysisErrorPresentation.js +8 -2
- package/dist/domain/analysisErrorProjection.js +9 -0
- package/dist/domain/artifactInspection.js +347 -0
- package/dist/domain/browserCaptureComparison.js +82 -0
- package/dist/domain/browserObservation.js +8 -1
- package/dist/domain/browserObservationErrors.js +1 -0
- package/dist/domain/browserObservationSchemas.js +26 -0
- package/dist/domain/browserScenario.js +165 -0
- package/dist/domain/browserScenarioCapture.js +57 -0
- package/dist/domain/browserScenarioCaptureValues.js +204 -0
- package/dist/domain/browserScenarioDiff.js +324 -0
- package/dist/domain/browserScenarioDiffValues.js +157 -0
- package/dist/domain/browserScenarioNormalization.js +45 -0
- package/dist/domain/browserScenarioValues.js +372 -0
- package/dist/domain/bytecodeProvider.js +179 -0
- package/dist/domain/conformancePackage.js +169 -0
- package/dist/domain/conformanceReplay.js +104 -0
- package/dist/domain/conformanceTrustGate.js +193 -0
- package/dist/domain/customProtocolCapture.js +137 -0
- package/dist/domain/eventProcessTree.js +214 -0
- package/dist/domain/evidenceBundle.js +47 -0
- package/dist/domain/hopperStartupFailure.js +1 -1
- package/dist/domain/hopperValues.js +14 -1
- package/dist/domain/inputIssueProjection.js +7 -1
- package/dist/domain/javascriptRuntimeObservation.js +170 -0
- package/dist/domain/javascriptRuntimeReconciliationParsing.js +120 -2
- package/dist/domain/javascriptRuntimeReconciliationResult.js +3 -3
- package/dist/domain/javascriptRuntimeReconciliationRuntime.js +2 -2
- package/dist/domain/javascriptRuntimeReconciliationSchemas.js +1 -1
- package/dist/domain/javascriptSemanticAnalysis.js +13 -16
- package/dist/domain/javascriptSemanticAsyncEffects.js +253 -0
- package/dist/domain/javascriptSemanticChildProcesses.js +237 -0
- package/dist/domain/javascriptSemanticDataEffectHelpers.js +125 -0
- package/dist/domain/javascriptSemanticDataEffects.js +284 -0
- package/dist/domain/javascriptSemanticDerivedAnalysis.js +41 -0
- package/dist/domain/javascriptSemanticFingerprints.js +174 -0
- package/dist/domain/javascriptSemanticIr.js +21 -1
- package/dist/domain/javascriptSemanticObjects.js +115 -0
- package/dist/domain/javascriptSemanticPromises.js +290 -0
- package/dist/domain/javascriptSemanticResources.js +136 -0
- package/dist/domain/javascriptSourceParser.js +15 -0
- package/dist/domain/javascriptStaticAnalysis.js +12 -15
- package/dist/domain/javascriptStaticAnalysisBundler.js +38 -0
- package/dist/domain/javascriptStaticAnalysisCalls.js +23 -1
- package/dist/domain/javascriptStaticAnalysisHelpers.js +8 -0
- package/dist/domain/mobileApplicationInvestigation.js +159 -0
- package/dist/domain/nativeApiBoundary.js +130 -0
- package/dist/domain/objcSwiftMetadata.js +150 -0
- package/dist/domain/packageAnalysis.js +123 -0
- package/dist/domain/peInspection.js +196 -0
- package/dist/domain/processCapture.js +8 -2
- package/dist/domain/processReactiveScenario.js +1 -0
- package/dist/domain/processReactiveTransition.js +9 -4
- package/dist/domain/protocolCapture.js +243 -0
- package/dist/domain/providerCleanupError.js +11 -0
- package/dist/domain/reconstructionObligationLedgerSchemas.js +278 -0
- package/dist/domain/reconstructionReadiness.js +121 -0
- package/dist/domain/reconstructionReadinessFindingHelpers.js +17 -0
- package/dist/domain/reconstructionReadinessIntegrityFindings.js +118 -0
- package/dist/domain/reconstructionReadinessJourneyFindings.js +193 -0
- package/dist/domain/reconstructionReadinessSchemas.js +240 -0
- package/dist/domain/sourceToBundleComparison.js +335 -0
- package/dist/domain/sourceToBundleComparisonSchemas.js +133 -0
- package/dist/domain/sourceToBundleSignals.js +202 -0
- package/dist/domain/webCaptureDiff.js +141 -27
- package/dist/domain/webScreenshot.js +1 -1
- package/dist/dotnet/ManagedStaticProvider.js +34 -5
- package/dist/generatedMcpToolCatalog.js +37 -0
- package/dist/generatedPackageMetadata.js +4 -4
- package/dist/ghidra/GhidraDefaults.js +1 -1
- package/dist/ghidra/GhidraFunctionValues.js +40 -2
- package/dist/ghidra/GhidraProviderCapabilities.js +47 -38
- package/dist/hopper/HopperCleanup.js +158 -0
- package/dist/hopper/HopperClient.js +93 -99
- package/dist/hopper/HopperProvider.js +61 -30
- package/dist/hopper/HopperRequestQueue.js +215 -0
- package/dist/hopper/HopperResponseStream.js +46 -0
- package/dist/hopper/protocol.js +63 -20
- package/dist/main/transport.js +24 -27
- package/dist/main.js +14 -6
- package/dist/mcpDoctor.js +27 -4
- package/dist/mcpStartupPolicy.js +14 -0
- package/dist/process/ProcessCommandIdentity.js +19 -0
- package/dist/process/ProcessOwnership.js +11 -8
- package/dist/server/DynamicToolAvailability.js +70 -0
- package/dist/server/LazyToolCatalog.js +134 -0
- package/dist/server/createServer.js +92 -154
- package/dist/server/hydrateServerTools.js +191 -0
- package/dist/server/lifecycleProgress.js +10 -0
- package/dist/server/permissionFailure.js +12 -0
- package/dist/server/registerApplicationTools/compareSourceToBundle.js +35 -0
- package/dist/server/registerApplicationTools/helpers.js +11 -7
- package/dist/server/registerApplicationTools/obligationLedger.js +27 -0
- package/dist/server/registerApplicationTools/readiness.js +55 -0
- package/dist/server/registerApplicationTools.js +6 -0
- package/dist/server/registerArtifactTools.js +9 -1
- package/dist/server/registerBrowserScenarioTool.js +25 -0
- package/dist/server/registerBrowserTools.js +2 -2
- package/dist/server/registerCloseLifecycleTool.js +57 -0
- package/dist/server/registerEnhancedTools.js +28 -7
- package/dist/server/registerEvidenceResources.js +54 -101
- package/dist/server/registerEvidenceTools.js +9 -2
- package/dist/server/registerInvestigationTools.js +1 -2
- package/dist/server/registerJavaScriptApplicationGraphResource.js +0 -4
- package/dist/server/registerJavaScriptRuntimeObservationTools.js +37 -0
- package/dist/server/registerReconstructionObligationLedgerResource.js +32 -0
- package/dist/server/registerReconstructionReadinessResource.js +32 -0
- package/dist/server/registerSessionRecordTools.js +82 -12
- package/dist/server/registerSessionStatusTool.js +11 -8
- package/dist/server/registerSessionTools.js +32 -58
- package/dist/server/sessionAvailabilityPolicy.js +10 -8
- package/dist/server/toolRegistrationOptions.js +21 -6
- package/dist/server/toolResult.js +12 -0
- package/package.json +28 -11
- package/scripts/package-runner-bootstrap.mjs +72 -0
- package/scripts/rea.mjs +11 -1
- package/skills/reverse-engineer-anything/SKILL.md +10 -3
package/bridge/hopper_bridge.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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
|
|
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 = {
|
|
599
|
-
|
|
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(
|
|
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(
|
|
971
|
+
return _page(dict(_search_inventory(document, "procedure")), params.get("offset", 0), params.get("limit", 100))
|
|
744
972
|
if method == "list_strings":
|
|
745
|
-
values =
|
|
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
|
|
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
|
-
|
|
783
|
-
|
|
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
|
-
|
|
787
|
-
|
|
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()]
|
|
@@ -814,30 +1046,68 @@ def _serve_connection(connection):
|
|
|
814
1046
|
if not line or len(line) > MAX_LINE_BYTES:
|
|
815
1047
|
break
|
|
816
1048
|
request_id = None
|
|
1049
|
+
authenticated = False
|
|
817
1050
|
should_stop = False
|
|
818
1051
|
try:
|
|
819
1052
|
request = json.loads(line.decode("utf-8"))
|
|
820
1053
|
if set(request) != {"id", "token", "method", "params"}:
|
|
821
1054
|
raise ValueError("Invalid bridge request shape")
|
|
822
1055
|
request_id = request["id"]
|
|
1056
|
+
if isinstance(request_id, bool) or not isinstance(request_id, int) or request_id < 0:
|
|
1057
|
+
raise ValueError("Invalid bridge request id")
|
|
823
1058
|
if not isinstance(request["token"], str) or not hmac.compare_digest(request["token"], REA_TOKEN):
|
|
824
1059
|
raise PermissionError("Invalid bridge capability")
|
|
1060
|
+
authenticated = True
|
|
1061
|
+
_write_message(file, {"id": request_id, "event": {
|
|
1062
|
+
"type": "progress",
|
|
1063
|
+
"phase": "hopper_bridge",
|
|
1064
|
+
"completed": 0,
|
|
1065
|
+
"total": 1,
|
|
1066
|
+
"message": "Hopper bridge started request",
|
|
1067
|
+
}})
|
|
825
1068
|
result = _dispatch(request["method"], request["params"])
|
|
826
1069
|
should_stop = request["method"] == "shutdown_document" or (
|
|
827
1070
|
request["method"] == "shutdown" and not result.get("cleanup_required", False)
|
|
828
1071
|
)
|
|
829
|
-
|
|
1072
|
+
safe_result = _json_safe(result)
|
|
1073
|
+
_write_message(file, {"id": request_id, "event": {
|
|
1074
|
+
"type": "progress",
|
|
1075
|
+
"phase": "hopper_bridge",
|
|
1076
|
+
"completed": 1,
|
|
1077
|
+
"total": 1,
|
|
1078
|
+
"message": "Hopper bridge completed request",
|
|
1079
|
+
"terminal": True,
|
|
1080
|
+
}})
|
|
1081
|
+
response = {"id": request_id, "result": safe_result}
|
|
830
1082
|
except Exception as error:
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
1083
|
+
diagnostic = _safe_diagnostic(error)
|
|
1084
|
+
if authenticated:
|
|
1085
|
+
_write_message(file, {"id": request_id, "event": {
|
|
1086
|
+
"type": "diagnostic",
|
|
1087
|
+
"error": diagnostic,
|
|
1088
|
+
}})
|
|
1089
|
+
response_id = request_id if (
|
|
1090
|
+
isinstance(request_id, int)
|
|
1091
|
+
and not isinstance(request_id, bool)
|
|
1092
|
+
and request_id >= 0
|
|
1093
|
+
) else 0
|
|
1094
|
+
response = {"id": response_id, "error": diagnostic}
|
|
1095
|
+
_write_message(file, response)
|
|
834
1096
|
if should_stop:
|
|
835
1097
|
break
|
|
836
1098
|
file.close()
|
|
837
1099
|
connection.close()
|
|
838
1100
|
|
|
839
1101
|
|
|
1102
|
+
def _write_message(file, message):
|
|
1103
|
+
"""Write and flush one compact bridge message."""
|
|
1104
|
+
file.write((json.dumps(message, separators=(",", ":")) + "\n").encode("utf-8"))
|
|
1105
|
+
file.flush()
|
|
1106
|
+
|
|
1107
|
+
|
|
840
1108
|
def _diagnostic_type(error):
|
|
1109
|
+
if isinstance(error, CapabilityUnavailableError):
|
|
1110
|
+
return "capability_unavailable"
|
|
841
1111
|
if isinstance(error, PermissionError):
|
|
842
1112
|
return "authorization"
|
|
843
1113
|
if isinstance(error, (ValueError, TypeError, KeyError)):
|
|
@@ -845,6 +1115,20 @@ def _diagnostic_type(error):
|
|
|
845
1115
|
return "bridge_exception"
|
|
846
1116
|
|
|
847
1117
|
|
|
1118
|
+
def _safe_diagnostic(error):
|
|
1119
|
+
"""Project one exception without retaining provider or credential text."""
|
|
1120
|
+
diagnostic_type = _diagnostic_type(error)
|
|
1121
|
+
if diagnostic_type == "capability_unavailable":
|
|
1122
|
+
message = str(error)[:512]
|
|
1123
|
+
elif diagnostic_type == "authorization":
|
|
1124
|
+
message = "Invalid bridge capability"
|
|
1125
|
+
elif diagnostic_type == "invalid_request":
|
|
1126
|
+
message = "Invalid Hopper bridge request"
|
|
1127
|
+
else:
|
|
1128
|
+
message = "%s: Hopper bridge operation failed" % type(error).__name__[:128]
|
|
1129
|
+
return {"code": -32000, "message": message, "type": diagnostic_type}
|
|
1130
|
+
|
|
1131
|
+
|
|
848
1132
|
def _run():
|
|
849
1133
|
"""Own a permission-restricted, single-client Unix socket for this bridge."""
|
|
850
1134
|
if os.path.exists(REA_SOCKET):
|
|
@@ -864,4 +1148,5 @@ def _run():
|
|
|
864
1148
|
|
|
865
1149
|
# Hopper's public objects are bound to its dedicated Python execution thread.
|
|
866
1150
|
# Keep dispatch on that thread; moving calls to an arbitrary worker can deadlock.
|
|
867
|
-
|
|
1151
|
+
if __name__ == "__main__":
|
|
1152
|
+
_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
|
+
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { AnalysisOutputError } from "../domain/errors.js";
|
|
2
|
+
import { err, ok } from "../domain/result.js";
|
|
3
|
+
const parameters = (document, address) => ({
|
|
4
|
+
...(document === undefined ? {} : { document }),
|
|
5
|
+
...(address === undefined ? {} : { address }),
|
|
6
|
+
});
|
|
7
|
+
const executionOptions = (signal) => signal === undefined ? {} : { signal };
|
|
8
|
+
/** Compose the provider's volatile selection state into one coherent query. */
|
|
9
|
+
export const getNavigationContext = async (analysis, input, signal) => {
|
|
10
|
+
const document = input.document ??
|
|
11
|
+
(await analysis.execute("current_document", {}, executionOptions(signal)));
|
|
12
|
+
if (typeof document !== "string" && !document.ok)
|
|
13
|
+
return document;
|
|
14
|
+
const documentName = typeof document === "string" ? document : document.value.result;
|
|
15
|
+
const documentArgs = typeof documentName === "string" ? parameters(documentName) : {};
|
|
16
|
+
const address = await analysis.execute("current_address", documentArgs, executionOptions(signal));
|
|
17
|
+
if (!address.ok)
|
|
18
|
+
return address;
|
|
19
|
+
if (typeof address.value.result !== "string")
|
|
20
|
+
return ok({
|
|
21
|
+
document: documentName,
|
|
22
|
+
address: address.value.result,
|
|
23
|
+
procedure: null,
|
|
24
|
+
});
|
|
25
|
+
const procedure = await analysis.execute("resolve_containing_procedure", parameters(typeof documentName === "string" ? documentName : undefined, address.value.result), executionOptions(signal));
|
|
26
|
+
if (!procedure.ok) {
|
|
27
|
+
if (!isMissingCurrentProcedure(procedure.error))
|
|
28
|
+
return procedure;
|
|
29
|
+
return ok({
|
|
30
|
+
document: documentName,
|
|
31
|
+
address: address.value.result,
|
|
32
|
+
procedure: null,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return ok({
|
|
36
|
+
document: documentName,
|
|
37
|
+
address: address.value.result,
|
|
38
|
+
procedure: containingProcedureName(procedure.value.result),
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
/** Inspect bounded provider-neutral facets at one explicit reproducible address. */
|
|
42
|
+
export const inspectAddressContext = async (analysis, input, signal) => {
|
|
43
|
+
const document = await resolveDocument(analysis, input.document, signal);
|
|
44
|
+
if (!document.ok)
|
|
45
|
+
return document;
|
|
46
|
+
const args = parameters(document.value, input.address);
|
|
47
|
+
const operations = [
|
|
48
|
+
"address_name",
|
|
49
|
+
"resolve_containing_procedure",
|
|
50
|
+
"comment",
|
|
51
|
+
"inline_comment",
|
|
52
|
+
"list_bookmarks",
|
|
53
|
+
];
|
|
54
|
+
const results = await Promise.all(operations.map((operation) => analysis.execute(operation, operation === "list_bookmarks" ? parameters(document.value) : args, executionOptions(signal))));
|
|
55
|
+
for (const result of results) {
|
|
56
|
+
if (!result.ok && isRequestLevelFailure(result.error))
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
const name = facet(results[0], operations[0]);
|
|
60
|
+
const procedure = facet(results[1], operations[1]);
|
|
61
|
+
const comment = facet(results[2], operations[2]);
|
|
62
|
+
const inlineComment = facet(results[3], operations[3]);
|
|
63
|
+
const bookmarks = facet(results[4], operations[4]);
|
|
64
|
+
return ok({
|
|
65
|
+
address: input.address,
|
|
66
|
+
document: document.value,
|
|
67
|
+
name,
|
|
68
|
+
procedure,
|
|
69
|
+
comment,
|
|
70
|
+
inline_comment: inlineComment,
|
|
71
|
+
bookmarks: bookmarks.state === "available"
|
|
72
|
+
? {
|
|
73
|
+
state: "available",
|
|
74
|
+
value: matchingBookmarks(bookmarks.value, resolvedAddress(results[1], input.address)),
|
|
75
|
+
}
|
|
76
|
+
: bookmarks,
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
const resolveDocument = async (analysis, document, signal) => {
|
|
80
|
+
if (document !== undefined)
|
|
81
|
+
return ok(document);
|
|
82
|
+
const current = await analysis.execute("current_document", {}, executionOptions(signal));
|
|
83
|
+
if (!current.ok)
|
|
84
|
+
return current;
|
|
85
|
+
return typeof current.value.result === "string"
|
|
86
|
+
? ok(current.value.result)
|
|
87
|
+
: err(new AnalysisOutputError("current_document", "expected a document identity string"));
|
|
88
|
+
};
|
|
89
|
+
const facet = (result, operation) => result?.ok === true
|
|
90
|
+
? { state: "available", value: result.value.result }
|
|
91
|
+
: {
|
|
92
|
+
state: "unavailable",
|
|
93
|
+
reason: result?.error.message ?? `${operation} returned no result`,
|
|
94
|
+
remediation: `Call ${operation} directly to inspect provider availability and diagnostics.`,
|
|
95
|
+
};
|
|
96
|
+
const isMissingCurrentProcedure = (error) => /(?:no procedure exists|not in a procedure)/iu.test(error.message);
|
|
97
|
+
const isRequestLevelFailure = (error) => error._tag === "AnalysisCancelledError" ||
|
|
98
|
+
error._tag === "AnalysisTimeoutError" ||
|
|
99
|
+
error._tag === "NoBinaryOpenError" ||
|
|
100
|
+
error._tag === "PermissionRequiredError";
|
|
101
|
+
const containingProcedureName = (value) => {
|
|
102
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
103
|
+
return null;
|
|
104
|
+
if (value.found !== true)
|
|
105
|
+
return null;
|
|
106
|
+
const procedure = value.procedure;
|
|
107
|
+
if (typeof procedure !== "object" ||
|
|
108
|
+
procedure === null ||
|
|
109
|
+
Array.isArray(procedure))
|
|
110
|
+
return null;
|
|
111
|
+
return typeof procedure.name === "string" ? procedure.name : null;
|
|
112
|
+
};
|
|
113
|
+
const resolvedAddress = (result, fallback) => {
|
|
114
|
+
if (result?.ok !== true)
|
|
115
|
+
return fallback;
|
|
116
|
+
const value = result.value.result;
|
|
117
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
118
|
+
return fallback;
|
|
119
|
+
return typeof value.query_address === "string"
|
|
120
|
+
? value.query_address
|
|
121
|
+
: fallback;
|
|
122
|
+
};
|
|
123
|
+
const matchingBookmarks = (value, address) => {
|
|
124
|
+
if (!Array.isArray(value))
|
|
125
|
+
return [];
|
|
126
|
+
return value.filter((bookmark) => typeof bookmark === "object" &&
|
|
127
|
+
bookmark !== null &&
|
|
128
|
+
!Array.isArray(bookmark) &&
|
|
129
|
+
bookmark.address === address);
|
|
130
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { EvidenceIntegrityError, NoBinaryOpenError, } from "../domain/errors.js";
|
|
2
2
|
import { err, ok } from "../domain/result.js";
|
|
3
3
|
import { analysisQueryId, createAnalysisSnapshotEntry, snapshotBinding, snapshotMatchesProfile, snapshotMatchesTarget, snapshotTarget, } from "../domain/analysisSnapshot.js";
|
|
4
|
-
import {
|
|
4
|
+
import { GENERATED_MCP_TOOL_CATALOG } from "../generatedMcpToolCatalog.js";
|
|
5
5
|
const STATEFUL_OPERATIONS = new Set([
|
|
6
6
|
"health",
|
|
7
7
|
"current_address",
|
|
@@ -19,7 +19,9 @@ const CURSOR_DEFAULT_OPERATIONS = new Set([
|
|
|
19
19
|
"prev_address",
|
|
20
20
|
"xrefs",
|
|
21
21
|
]);
|
|
22
|
-
const DOCUMENT_SCOPED_OPERATIONS = new Set(
|
|
22
|
+
const DOCUMENT_SCOPED_OPERATIONS = new Set(GENERATED_MCP_TOOL_CATALOG.filter(({ kind }) => kind === "official-proxy")
|
|
23
|
+
.map(({ name }) => name)
|
|
24
|
+
.filter((name) => name !== "current_document" &&
|
|
23
25
|
name !== "list_documents" &&
|
|
24
26
|
name !== "set_current_document"));
|
|
25
27
|
/** Whether an operation is immutable and independent of provider UI state. */
|