rea-agents 2.5.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +14 -4
  2. package/bridge/ghidra/ReaGhidraBridge.java +477 -4
  3. package/bridge/hopper_bridge.py +54 -4
  4. package/dist/application/AnalysisContextQueries.js +130 -0
  5. package/dist/application/AnalysisSnapshotCache.js +4 -2
  6. package/dist/application/ArtifactInventory/scanCanonical.js +1 -1
  7. package/dist/application/BinarySession.js +5 -4
  8. package/dist/application/BinarySessionComposition.js +21 -0
  9. package/dist/application/BinarySessionRecords.js +79 -9
  10. package/dist/application/CapabilityInventory.js +16 -2
  11. package/dist/application/ClientRegistrationStatus.js +4 -1
  12. package/dist/application/DirectAnalysis.js +6 -2
  13. package/dist/application/EnhancedTools.js +37 -7
  14. package/dist/application/EvidenceLedger.js +45 -16
  15. package/dist/application/JavaScriptArtifactAnalysis.js +13 -4
  16. package/dist/application/LazyAnalysisProvider.js +77 -0
  17. package/dist/application/LoopbackReplay.js +1 -1
  18. package/dist/application/NativeApiInspection.js +80 -0
  19. package/dist/application/SessionProviderRouter.js +6 -6
  20. package/dist/application/SetupClientConfiguration.js +4 -2
  21. package/dist/application/runtime.js +26 -9
  22. package/dist/browser/PlaywrightBrowserScenarioProvider.js +8 -2
  23. package/dist/catalogIdentity.js +3 -3
  24. package/dist/cli/artifactCommands.js +5 -3
  25. package/dist/cli/coreAnalysisCommands.js +41 -0
  26. package/dist/cliCommandNames.js +1 -0
  27. package/dist/contracts/artifactToolContracts.js +8 -3
  28. package/dist/contracts/enhancedInputs.js +10 -0
  29. package/dist/contracts/functionWorkflowToolContracts.js +30 -0
  30. package/dist/contracts/promptContracts.js +14 -15
  31. package/dist/contracts/toolContractExamples.js +3 -0
  32. package/dist/contracts/toolContracts.js +21 -4
  33. package/dist/contracts/toolEffects.js +4 -0
  34. package/dist/contracts/toolOutputSchemaGroups.js +50 -10
  35. package/dist/domain/analysisErrorPresentation.js +8 -2
  36. package/dist/domain/analysisErrorProjection.js +1 -0
  37. package/dist/domain/bytecodeProvider.js +179 -0
  38. package/dist/domain/conformancePackage.js +169 -0
  39. package/dist/domain/conformanceReplay.js +104 -0
  40. package/dist/domain/conformanceTrustGate.js +193 -0
  41. package/dist/domain/customProtocolCapture.js +137 -0
  42. package/dist/domain/eventProcessTree.js +214 -0
  43. package/dist/domain/evidenceBundle.js +47 -0
  44. package/dist/domain/hopperStartupFailure.js +1 -1
  45. package/dist/domain/hopperValues.js +4 -1
  46. package/dist/domain/inputIssueProjection.js +7 -1
  47. package/dist/domain/javascriptSemanticAnalysis.js +8 -12
  48. package/dist/domain/javascriptSourceParser.js +15 -0
  49. package/dist/domain/javascriptStaticAnalysis.js +8 -12
  50. package/dist/domain/mobileApplicationInvestigation.js +159 -0
  51. package/dist/domain/nativeApiBoundary.js +130 -0
  52. package/dist/domain/objcSwiftMetadata.js +150 -0
  53. package/dist/domain/packageAnalysis.js +123 -0
  54. package/dist/domain/peInspection.js +196 -0
  55. package/dist/domain/protocolCapture.js +243 -0
  56. package/dist/dotnet/ManagedStaticProvider.js +34 -5
  57. package/dist/generatedMcpToolCatalog.js +37 -0
  58. package/dist/generatedPackageMetadata.js +4 -4
  59. package/dist/ghidra/GhidraDefaults.js +1 -1
  60. package/dist/ghidra/GhidraFunctionValues.js +27 -1
  61. package/dist/ghidra/GhidraProviderCapabilities.js +40 -38
  62. package/dist/hopper/HopperClient.js +26 -2
  63. package/dist/hopper/HopperProvider.js +42 -30
  64. package/dist/hopper/HopperRequestQueue.js +32 -4
  65. package/dist/hopper/HopperResponseStream.js +2 -2
  66. package/dist/hopper/protocol.js +63 -21
  67. package/dist/main/transport.js +2 -5
  68. package/dist/main.js +14 -12
  69. package/dist/mcpDoctor.js +2 -2
  70. package/dist/mcpStartupPolicy.js +14 -0
  71. package/dist/server/LazyToolCatalog.js +134 -0
  72. package/dist/server/createServer.js +64 -171
  73. package/dist/server/hydrateServerTools.js +191 -0
  74. package/dist/server/registerEnhancedTools.js +13 -3
  75. package/dist/server/registerEvidenceResources.js +50 -101
  76. package/dist/server/registerInvestigationTools.js +1 -2
  77. package/dist/server/registerJavaScriptApplicationGraphResource.js +0 -4
  78. package/dist/server/registerReconstructionObligationLedgerResource.js +0 -7
  79. package/dist/server/registerReconstructionReadinessResource.js +0 -7
  80. package/dist/server/registerSessionRecordTools.js +82 -12
  81. package/dist/server/registerSessionStatusTool.js +1 -1
  82. package/dist/server/registerSessionTools.js +21 -1
  83. package/dist/server/toolRegistrationOptions.js +21 -6
  84. package/dist/server/toolResult.js +12 -0
  85. package/package.json +21 -10
  86. package/scripts/package-runner-bootstrap.mjs +72 -0
  87. package/scripts/rea.mjs +11 -1
  88. package/skills/reverse-engineer-anything/SKILL.md +10 -3
package/README.md CHANGED
@@ -103,6 +103,13 @@ detected agents, then asks which capabilities to set up: agent integration
103
103
  Nothing is preselected. Choosing agent integration opens a second empty
104
104
  checklist for the specific detected agents that should receive a registration.
105
105
 
106
+ If bare `npx` finds a project-local REA package, its dispatcher restarts setup
107
+ through `rea-agents@latest` before planning any changes. This prevents an older
108
+ local dependency from silently writing stale integration state. An already
109
+ published older release cannot contain that dispatcher protection; run
110
+ `npx -y rea-agents@latest setup` once to repair registrations created by one.
111
+ Intentional rollbacks remain available through an exact package request.
112
+
106
113
  REA keeps the journey inline so its history remains in the terminal. Selecting
107
114
  a capability does not select every detected target or authorize a change.
108
115
  Before anything changes, REA validates existing configuration, prints exact
@@ -374,7 +381,7 @@ REA handles the app analysis in steps 1–5. The agent performs step 6 with its
374
381
  | Tool family | Count | Examples |
375
382
  | ------------------------- | ----: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
376
383
  | Native inspection | 36 | procedures, pseudocode, assembly, strings, names, segments, callers, callees, xrefs, annotations, bounded byte reads, file-offset translation |
377
- | Investigation workflows | 12 | `binary_overview`, `analyze_function`, `batch_decompile`, `trace_feature`, exact string-to-code lookup, bounded call paths, call graphs, Swift and Objective-C discovery |
384
+ | Investigation workflows | 13 | `binary_overview`, `analyze_function`, `inspect_native_api`, `batch_decompile`, `trace_feature`, exact string-to-code lookup, bounded call paths, call graphs, Swift and Objective-C discovery |
378
385
  | Native macOS utilities | 5 | Mach-O metadata, code signatures, plists, architectures, Swift demangling; Hopper-free and provenance-bearing |
379
386
  | Artifact graph | 3 | bounded provider-neutral inspection, deterministic directory/ZIP/APK/IPA/MSIX/AppX/ASAR inventory, and explicitly selected extraction into an absent owned tree |
380
387
  | Managed PE/CLI | 8 | PE/CLI identity, metadata members, CIL hashes, P/Invoke/native-boundary declarations and verification, application-graph projection, decompiler reconstruction import, token remapping, runtime-correlation plans, and version comparison |
@@ -382,7 +389,7 @@ REA handles the app analysis in steps 1–5. The agent performs step 6 with its
382
389
  | Electron analysis | 4 | passive root-confined observation, bounded static application mapping, and evidence-backed static/runtime reconciliation |
383
390
  | JavaScript runtime | 2 | approved attach-only Node/Electron Inspector target discovery plus bounded script and execution-context observation without evaluation or instrumentation |
384
391
  | Application workflows | 12 | bounded cross-layer traces, unique-only version matching, historical-source to bundle mapping, static export return-shape comparison, approved Linux-isolated extracted-module replay, managed-runtime characterization, reconstruction coverage closure, deterministic obligation ledgers, and end-to-end readiness conformance |
385
- | Workspace and observation | 19 | target lifecycle, Evidence v2 bundles, direct finite replay-machine evaluation, process/artifact/function comparison, evidence-linked residual-unknown lifecycle |
392
+ | Workspace and observation | 22 | target lifecycle, Evidence v2 bundle snapshots, aggregate navigation/address context, direct finite replay-machine evaluation, process/artifact/function comparison, evidence-linked residual-unknown lifecycle |
386
393
 
387
394
  The public interface describes what the agent is trying to learn. Providers decide how to answer. macOS utilities handle common semantic inspection without launching Hopper; Hopper handles deeper native analysis; the process harness implements controlled behavioral capture.
388
395
 
@@ -530,7 +537,7 @@ Setup detects Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf,
530
537
  "mcpServers": {
531
538
  "rea": {
532
539
  "command": "npx",
533
- "args": ["-y", "rea-agents@2.5.0", "mcp"]
540
+ "args": ["-y", "rea-agents@3.0.0", "mcp"]
534
541
  }
535
542
  }
536
543
  }
@@ -787,7 +794,10 @@ Any agent that can run a local MCP server can use the manual configuration. Setu
787
794
 
788
795
  ## Development
789
796
 
790
- See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, architecture, tests, and release instructions. PR CI publishes generated API documentation as the `api-docs` workflow artifact.
797
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, architecture, and release
798
+ instructions, and [docs/testing.md](docs/testing.md) for the behavioral test
799
+ depths, focused developer commands, coverage floors, and CI evidence. PR CI
800
+ publishes generated API documentation as the `api-docs` workflow artifact.
791
801
 
792
802
  `npm run verify:agent` runs brandless native, JavaScript-application, managed,
793
803
  and browser prompts through a real local Codex CLI. Its JSON report measures
@@ -55,6 +55,7 @@ import ghidra.program.model.block.CodeBlockIterator;
55
55
  import ghidra.program.model.block.CodeBlockReference;
56
56
  import ghidra.program.model.block.CodeBlockReferenceIterator;
57
57
  import ghidra.program.model.data.StringDataInstance;
58
+ import ghidra.program.model.data.DataType;
58
59
  import ghidra.program.model.listing.CommentType;
59
60
  import ghidra.program.model.listing.Data;
60
61
  import ghidra.program.model.listing.DataIterator;
@@ -70,15 +71,25 @@ import ghidra.program.model.symbol.ReferenceIterator;
70
71
  import ghidra.program.model.symbol.SourceType;
71
72
  import ghidra.program.model.symbol.Symbol;
72
73
  import ghidra.program.model.symbol.SymbolIterator;
74
+ import ghidra.program.model.pcode.FunctionPrototype;
75
+ import ghidra.program.model.pcode.HighFunction;
76
+ import ghidra.program.model.pcode.HighSymbol;
77
+ import ghidra.program.model.pcode.JumpTable;
73
78
 
74
79
  public final class ReaGhidraBridge extends HeadlessScript {
75
- private static final int BRIDGE_VERSION = 5;
80
+ private static final int BRIDGE_VERSION = 6;
76
81
  private static final int MAX_DESCRIPTOR_BYTES = 16 * 1024;
77
82
  private static final int MAX_REQUEST_CHARACTERS = 256 * 1024;
78
83
  private static final int MAX_RESPONSE_BYTES = 1024 * 1024;
84
+ private static final int RESPONSE_ENVELOPE_RESERVE_BYTES = 32 * 1024;
85
+ private static final int MAX_NATIVE_API_RESPONSE_BYTES = 192 * 1024;
79
86
  private static final int MAX_INVENTORY_ITEMS = 1_000_000;
80
87
  private static final int MAX_FUNCTION_ITEMS = 100_000;
81
88
  private static final int MAX_FUNCTION_INSTRUCTIONS = 100_000;
89
+ private static final int MAX_NATIVE_API_PARAMETERS = 64;
90
+ private static final int MAX_NATIVE_API_JUMP_TABLES = 4;
91
+ private static final int MAX_NATIVE_API_LOAD_TABLES = 4;
92
+ private static final int MAX_NATIVE_API_MAPPINGS = 32;
82
93
  private static final int DECOMPILE_TIMEOUT_SECONDS = 30;
83
94
  private static final int DECOMPILE_PAYLOAD_MBYTES = 8;
84
95
  private static final int MAX_LIST_VALUE_CODE_POINTS = 1_024;
@@ -377,7 +388,9 @@ public final class ReaGhidraBridge extends HeadlessScript {
377
388
  private JsonElement procedurePseudocode(JsonObject params) throws Exception {
378
389
  requireKeys(params, Set.of("document", "procedure"));
379
390
  requireDocument(params);
380
- String value = decompile(resolveProcedure(requireString(params, "procedure")));
391
+ String value = decompiledText(
392
+ decompile(resolveProcedure(requireString(params, "procedure")))
393
+ );
381
394
  return value == null ? JsonNull.INSTANCE : GSON.toJsonTree(value);
382
395
  }
383
396
 
@@ -576,7 +589,8 @@ public final class ReaGhidraBridge extends HeadlessScript {
576
589
  );
577
590
  JsonObject offsets = requireCollectionOffsets(params);
578
591
  InstructionScan scan = scanInstructions(function, maximumInstructions);
579
- String pseudocode = decompile(function);
592
+ DecompileResults decompilation = decompile(function);
593
+ String pseudocode = decompiledText(decompilation);
580
594
  if (pseudocode == null) {
581
595
  pseudocode = "";
582
596
  }
@@ -662,6 +676,19 @@ public final class ReaGhidraBridge extends HeadlessScript {
662
676
  instructionScan.addProperty("scanned", scan.instructions.size());
663
677
  instructionScan.addProperty("truncated", scan.truncated);
664
678
  result.add("instruction_scan", instructionScan);
679
+ int nativeApiBudget = Math.min(
680
+ MAX_NATIVE_API_RESPONSE_BYTES,
681
+ Math.max(
682
+ 0,
683
+ MAX_RESPONSE_BYTES -
684
+ encodedUtf8Length(result) -
685
+ RESPONSE_ENVELOPE_RESERVE_BYTES
686
+ )
687
+ );
688
+ result.add(
689
+ "native_api",
690
+ nativeApiBoundary(function, decompilation, nativeApiBudget)
691
+ );
665
692
  JsonArray limitations = new JsonArray();
666
693
  limitations.add(
667
694
  "Unresolved computed or indirect flows without target addresses are not represented as reference edges."
@@ -971,7 +998,446 @@ public final class ReaGhidraBridge extends HeadlessScript {
971
998
  return matches.get(0);
972
999
  }
973
1000
 
974
- private String decompile(Function function) {
1001
+ private JsonObject nativeApiBoundary(
1002
+ Function function,
1003
+ DecompileResults decompilation,
1004
+ int maximumBytes
1005
+ ) throws Exception {
1006
+ if (decompilation == null || decompilation.getHighFunction() == null) {
1007
+ return unavailableNativeApiBoundary(
1008
+ "Ghidra has no HighFunction model for an external or bodyless procedure"
1009
+ );
1010
+ }
1011
+
1012
+ HighFunction highFunction = decompilation.getHighFunction();
1013
+ FunctionPrototype prototype = highFunction.getFunctionPrototype();
1014
+ String signatureSource = function
1015
+ .getSignatureSource()
1016
+ .name()
1017
+ .toLowerCase(Locale.ROOT);
1018
+ JsonObject result = new JsonObject();
1019
+ result.addProperty("available", true);
1020
+ result.addProperty("provenance", "ghidra-high-function");
1021
+ result.addProperty("signature_source", signatureSource);
1022
+ String callingConvention = prototype.getModelName();
1023
+ result.addProperty(
1024
+ "calling_convention",
1025
+ callingConvention == null || callingConvention.isBlank()
1026
+ ? "unknown"
1027
+ : callingConvention
1028
+ );
1029
+ result.add(
1030
+ "return_type",
1031
+ inferredBoundaryType(
1032
+ "return",
1033
+ null,
1034
+ null,
1035
+ prototype.getReturnType(),
1036
+ prototype.getReturnStorage().toString(),
1037
+ false,
1038
+ signatureSource
1039
+ )
1040
+ );
1041
+
1042
+ JsonArray parameters = new JsonArray();
1043
+ result.add("parameters", parameters);
1044
+ result.addProperty("parameters_truncated", false);
1045
+
1046
+ JsonArray jumpTables = new JsonArray();
1047
+ result.add("jump_tables", jumpTables);
1048
+ result.addProperty("jump_tables_truncated", false);
1049
+
1050
+ JsonObject pseudocode = new JsonObject();
1051
+ pseudocode.addProperty("classification", "decompiler-generated-non-source");
1052
+ pseudocode.addProperty("compilable", false);
1053
+ result.add("pseudocode", pseudocode);
1054
+
1055
+ JsonArray artifacts = new JsonArray();
1056
+ artifacts.add("recovered-signature");
1057
+ artifacts.add("register-and-stack-variables");
1058
+ artifacts.add("compiler-generated-control-flow");
1059
+ artifacts.add("pointer-arithmetic");
1060
+ artifacts.add("pseudocode");
1061
+ result.add("decompiler_artifacts", artifacts);
1062
+
1063
+ JsonArray limitations = new JsonArray();
1064
+ limitations.add(
1065
+ "Types and calling conventions are Ghidra decompiler observations, not source declarations."
1066
+ );
1067
+ limitations.add(
1068
+ "Register variables, compiler-generated blocks, and pointer arithmetic remain decompiler artifacts."
1069
+ );
1070
+ limitations.add(
1071
+ "Pseudocode is neither original source nor guaranteed to compile."
1072
+ );
1073
+ result.add("limitations", limitations);
1074
+ if (encodedUtf8Length(result) > maximumBytes) {
1075
+ return unavailableNativeApiBoundary(
1076
+ "The remaining Ghidra response budget cannot contain a structured native API boundary"
1077
+ );
1078
+ }
1079
+
1080
+ int parameterCount = Math.min(
1081
+ prototype.getNumParams(),
1082
+ MAX_NATIVE_API_PARAMETERS
1083
+ );
1084
+ for (int index = 0; index < parameterCount; index += 1) {
1085
+ HighSymbol parameter = prototype.getParam(index);
1086
+ if (parameter == null) {
1087
+ continue;
1088
+ }
1089
+ parameters.add(
1090
+ inferredBoundaryType(
1091
+ "parameter",
1092
+ index,
1093
+ parameter.getName(),
1094
+ parameter.getDataType(),
1095
+ parameter.getStorage().toString(),
1096
+ parameter.isTypeLocked(),
1097
+ signatureSource
1098
+ )
1099
+ );
1100
+ if (encodedUtf8Length(result) > maximumBytes) {
1101
+ parameters.remove(parameters.size() - 1);
1102
+ break;
1103
+ }
1104
+ }
1105
+ result.addProperty(
1106
+ "parameters_truncated",
1107
+ prototype.getNumParams() > parameters.size()
1108
+ );
1109
+
1110
+ JumpTable[] recoveredJumpTables = highFunction.getJumpTables();
1111
+ int jumpTableCount = Math.min(
1112
+ recoveredJumpTables.length,
1113
+ MAX_NATIVE_API_JUMP_TABLES
1114
+ );
1115
+ for (int index = 0; index < jumpTableCount; index += 1) {
1116
+ jumpTables.add(inferredJumpTable(recoveredJumpTables[index]));
1117
+ if (encodedUtf8Length(result) > maximumBytes) {
1118
+ jumpTables.remove(jumpTables.size() - 1);
1119
+ break;
1120
+ }
1121
+ }
1122
+ result.addProperty(
1123
+ "jump_tables_truncated",
1124
+ recoveredJumpTables.length > jumpTables.size()
1125
+ );
1126
+ return result;
1127
+ }
1128
+
1129
+ private static JsonObject unavailableNativeApiBoundary(String reason) {
1130
+ JsonObject unavailable = new JsonObject();
1131
+ unavailable.addProperty("available", false);
1132
+ unavailable.addProperty("reason", reason);
1133
+ JsonArray unknowns = new JsonArray();
1134
+ unknowns.add("Return and parameter boundary types are unavailable.");
1135
+ unknowns.add("Jump-table mappings are unavailable.");
1136
+ unavailable.add("residual_unknowns", unknowns);
1137
+ return unavailable;
1138
+ }
1139
+
1140
+ private static int encodedUtf8Length(JsonElement value) {
1141
+ return GSON.toJson(value).getBytes(StandardCharsets.UTF_8).length;
1142
+ }
1143
+
1144
+ private static JsonObject inferredBoundaryType(
1145
+ String role,
1146
+ Integer ordinal,
1147
+ String name,
1148
+ DataType dataType,
1149
+ String storage,
1150
+ boolean typeLocked,
1151
+ String signatureSource
1152
+ ) {
1153
+ JsonObject result = new JsonObject();
1154
+ result.addProperty("role", role);
1155
+ if (ordinal == null) {
1156
+ result.add("ordinal", JsonNull.INSTANCE);
1157
+ }
1158
+ else {
1159
+ result.addProperty("ordinal", ordinal);
1160
+ }
1161
+ if (name == null || name.isBlank()) {
1162
+ result.add("name", JsonNull.INSTANCE);
1163
+ }
1164
+ else {
1165
+ result.addProperty("name", name);
1166
+ }
1167
+ result.addProperty("data_type", dataType.getDisplayName());
1168
+ int dataTypeLength = dataType.getLength();
1169
+ if (dataTypeLength < 0) {
1170
+ result.add("size_bytes", JsonNull.INSTANCE);
1171
+ }
1172
+ else {
1173
+ result.addProperty("size_bytes", dataTypeLength);
1174
+ }
1175
+ if (storage.isBlank()) {
1176
+ result.add("storage", JsonNull.INSTANCE);
1177
+ }
1178
+ else {
1179
+ result.addProperty("storage", storage);
1180
+ }
1181
+ result.addProperty(
1182
+ "confidence",
1183
+ inferenceConfidence(signatureSource, typeLocked)
1184
+ );
1185
+
1186
+ JsonArray evidence = new JsonArray();
1187
+ evidence.add(
1188
+ inferenceEvidence(
1189
+ "signature-source",
1190
+ "ghidra-function-manager",
1191
+ "Function signature source is " + signatureSource + "."
1192
+ )
1193
+ );
1194
+ evidence.add(
1195
+ inferenceEvidence(
1196
+ "decompiler-type",
1197
+ "ghidra-high-function",
1198
+ "HighFunction recovered data type " + dataType.getDisplayName() + "."
1199
+ )
1200
+ );
1201
+ if (typeLocked) {
1202
+ evidence.add(
1203
+ inferenceEvidence(
1204
+ "type-lock",
1205
+ "ghidra-high-symbol",
1206
+ "The decompiler marks this parameter type as locked."
1207
+ )
1208
+ );
1209
+ }
1210
+ result.add("evidence", evidence);
1211
+
1212
+ JsonArray artifacts = new JsonArray();
1213
+ artifacts.add("recovered-signature");
1214
+ if (!storage.isBlank()) {
1215
+ artifacts.add("register-or-stack-storage");
1216
+ }
1217
+ if (name != null && (name.startsWith("param_") || name.startsWith("unaff_"))) {
1218
+ artifacts.add("compiler-generated-variable");
1219
+ }
1220
+ result.add("decompiler_artifacts", artifacts);
1221
+ return result;
1222
+ }
1223
+
1224
+ private JsonObject inferredJumpTable(JumpTable jumpTable) throws Exception {
1225
+ JsonObject result = new JsonObject();
1226
+ result.addProperty(
1227
+ "dispatch_address",
1228
+ canonicalAddress(jumpTable.getSwitchAddress())
1229
+ );
1230
+ JumpTable.LoadTable[] loadTables = jumpTable.getLoadTables();
1231
+ List<Address> referencedDataAddresses = loadTables.length == 0
1232
+ ? dispatchPathDataReferences(jumpTable)
1233
+ : List.of();
1234
+ JsonArray dataSources = new JsonArray();
1235
+ int loadTableCount = Math.min(
1236
+ loadTables.length,
1237
+ MAX_NATIVE_API_LOAD_TABLES
1238
+ );
1239
+ for (int index = 0; index < loadTableCount; index += 1) {
1240
+ JumpTable.LoadTable loadTable = loadTables[index];
1241
+ JsonObject source = new JsonObject();
1242
+ source.addProperty("address", canonicalAddress(loadTable.getAddress()));
1243
+ source.addProperty("provenance", "ghidra-decompiler-load-table");
1244
+ source.addProperty("entry_size_bytes", loadTable.getSize());
1245
+ source.addProperty("entry_count", loadTable.getNum());
1246
+ source.addProperty("confidence", "high");
1247
+ JsonArray evidence = new JsonArray();
1248
+ evidence.add(
1249
+ inferenceEvidence(
1250
+ "jump-table",
1251
+ "ghidra-high-function",
1252
+ "Decompiler load table starts at " +
1253
+ canonicalAddress(loadTable.getAddress()) +
1254
+ "."
1255
+ )
1256
+ );
1257
+ source.add("evidence", evidence);
1258
+ dataSources.add(source);
1259
+ }
1260
+ int referencedDataCount = Math.min(
1261
+ referencedDataAddresses.size(),
1262
+ MAX_NATIVE_API_LOAD_TABLES - loadTableCount
1263
+ );
1264
+ for (int index = 0; index < referencedDataCount; index += 1) {
1265
+ Address address = referencedDataAddresses.get(index);
1266
+ JsonObject source = new JsonObject();
1267
+ source.addProperty("address", canonicalAddress(address));
1268
+ source.addProperty(
1269
+ "provenance",
1270
+ "ghidra-dispatch-block-data-reference"
1271
+ );
1272
+ source.add("entry_size_bytes", JsonNull.INSTANCE);
1273
+ source.add("entry_count", JsonNull.INSTANCE);
1274
+ source.addProperty("confidence", "medium");
1275
+ JsonArray evidence = new JsonArray();
1276
+ evidence.add(
1277
+ inferenceEvidence(
1278
+ "jump-table",
1279
+ "ghidra-reference-manager",
1280
+ "The dispatch block or an immediate predecessor contains a typed data reference to " +
1281
+ canonicalAddress(address) +
1282
+ "."
1283
+ )
1284
+ );
1285
+ source.add("evidence", evidence);
1286
+ dataSources.add(source);
1287
+ }
1288
+ result.add("data_sources", dataSources);
1289
+ result.addProperty(
1290
+ "data_sources_truncated",
1291
+ loadTables.length > loadTableCount ||
1292
+ referencedDataAddresses.size() > referencedDataCount
1293
+ );
1294
+
1295
+ Address[] targets = jumpTable.getCases();
1296
+ Integer[] labels = jumpTable.getLabelValues();
1297
+ JsonArray mappings = new JsonArray();
1298
+ int mappingCount = Math.min(targets.length, MAX_NATIVE_API_MAPPINGS);
1299
+ for (int index = 0; index < mappingCount; index += 1) {
1300
+ JsonObject mapping = new JsonObject();
1301
+ if (labels == null || index >= labels.length || labels[index] == null) {
1302
+ mapping.add("case_value", JsonNull.INSTANCE);
1303
+ }
1304
+ else {
1305
+ mapping.addProperty("case_value", labels[index]);
1306
+ }
1307
+ String targetAddress = canonicalAddress(targets[index]);
1308
+ mapping.addProperty("target_address", targetAddress);
1309
+ JsonArray dataAddresses = new JsonArray();
1310
+ for (int sourceIndex = 0; sourceIndex < loadTableCount; sourceIndex += 1) {
1311
+ dataAddresses.add(
1312
+ canonicalAddress(loadTables[sourceIndex].getAddress())
1313
+ );
1314
+ }
1315
+ mapping.add("data_addresses", dataAddresses);
1316
+ mapping.addProperty("confidence", "medium");
1317
+ JsonArray evidence = new JsonArray();
1318
+ evidence.add(
1319
+ inferenceEvidence(
1320
+ "jump-table",
1321
+ "ghidra-high-function",
1322
+ "Recovered target " +
1323
+ targetAddress +
1324
+ " from dispatch " +
1325
+ canonicalAddress(jumpTable.getSwitchAddress()) +
1326
+ "."
1327
+ )
1328
+ );
1329
+ mapping.add("evidence", evidence);
1330
+ mappings.add(mapping);
1331
+ }
1332
+ result.add("mappings", mappings);
1333
+ result.addProperty("mappings_truncated", targets.length > mappingCount);
1334
+
1335
+ JsonArray limitations = new JsonArray();
1336
+ if (loadTables.length == 0 && referencedDataAddresses.isEmpty()) {
1337
+ limitations.add(
1338
+ "Ghidra recovered targets but exposed no backing load-table or dispatch-block data address."
1339
+ );
1340
+ }
1341
+ if (loadTables.length == 0 && !referencedDataAddresses.isEmpty()) {
1342
+ limitations.add(
1343
+ "Load-table metadata was absent; table-level candidate ownership is inferred from typed data references in the dispatch block and its immediate control-flow predecessors, but candidates are not assigned to individual mappings."
1344
+ );
1345
+ }
1346
+ if (labels == null || labels.length != targets.length) {
1347
+ limitations.add(
1348
+ "Case labels were unavailable or incomplete; null labels are preserved."
1349
+ );
1350
+ }
1351
+ if (
1352
+ loadTables.length > loadTableCount ||
1353
+ referencedDataAddresses.size() > referencedDataCount ||
1354
+ targets.length > mappingCount
1355
+ ) {
1356
+ limitations.add(
1357
+ "Native API jump-table observations reached REA's bounded output limits."
1358
+ );
1359
+ }
1360
+ result.add("limitations", limitations);
1361
+ return result;
1362
+ }
1363
+
1364
+ private List<Address> dispatchPathDataReferences(JumpTable jumpTable)
1365
+ throws Exception {
1366
+ BasicBlockModel model = new BasicBlockModel(currentProgram, false);
1367
+ CodeBlock dispatchBlock = model.getFirstCodeBlockContaining(
1368
+ jumpTable.getSwitchAddress(),
1369
+ monitor
1370
+ );
1371
+ if (dispatchBlock == null) {
1372
+ return List.of();
1373
+ }
1374
+ List<CodeBlock> blocks = new ArrayList<>();
1375
+ blocks.add(dispatchBlock);
1376
+ CodeBlockReferenceIterator sources = dispatchBlock.getSources(monitor);
1377
+ while (sources.hasNext()) {
1378
+ monitor.checkCancelled();
1379
+ CodeBlock sourceBlock = sources.next().getSourceBlock();
1380
+ if (sourceBlock != null) {
1381
+ blocks.add(sourceBlock);
1382
+ }
1383
+ }
1384
+ Set<Address> addresses = new HashSet<>();
1385
+ for (CodeBlock block : blocks) {
1386
+ InstructionIterator instructions = currentProgram
1387
+ .getListing()
1388
+ .getInstructions(block, true);
1389
+ while (instructions.hasNext()) {
1390
+ monitor.checkCancelled();
1391
+ Instruction instruction = instructions.next();
1392
+ for (
1393
+ Reference reference : currentProgram
1394
+ .getReferenceManager()
1395
+ .getReferencesFrom(instruction.getAddress())
1396
+ ) {
1397
+ if (
1398
+ reference.getReferenceType().isData() &&
1399
+ currentProgram
1400
+ .getMemory()
1401
+ .getLoadedAndInitializedAddressSet()
1402
+ .contains(reference.getToAddress())
1403
+ ) {
1404
+ addresses.add(reference.getToAddress());
1405
+ }
1406
+ }
1407
+ }
1408
+ }
1409
+ List<Address> ordered = new ArrayList<>(addresses);
1410
+ ordered.sort(Address::compareTo);
1411
+ return List.copyOf(ordered);
1412
+ }
1413
+
1414
+ private static JsonObject inferenceEvidence(
1415
+ String kind,
1416
+ String source,
1417
+ String detail
1418
+ ) {
1419
+ JsonObject evidence = new JsonObject();
1420
+ evidence.addProperty("kind", kind);
1421
+ evidence.addProperty("source", source);
1422
+ evidence.addProperty("detail", detail);
1423
+ return evidence;
1424
+ }
1425
+
1426
+ private static String inferenceConfidence(
1427
+ String signatureSource,
1428
+ boolean typeLocked
1429
+ ) {
1430
+ if (
1431
+ typeLocked ||
1432
+ signatureSource.equals("user_defined") ||
1433
+ signatureSource.equals("imported")
1434
+ ) {
1435
+ return "high";
1436
+ }
1437
+ return signatureSource.equals("analysis") ? "medium" : "low";
1438
+ }
1439
+
1440
+ private DecompileResults decompile(Function function) {
975
1441
  if (function.isExternal() || function.getBody().isEmpty()) {
976
1442
  return null;
977
1443
  }
@@ -995,6 +1461,13 @@ public final class ReaGhidraBridge extends HeadlessScript {
995
1461
  "Ghidra decompilation failed: " + boundedMessage(results.getErrorMessage())
996
1462
  );
997
1463
  }
1464
+ return results;
1465
+ }
1466
+
1467
+ private static String decompiledText(DecompileResults results) {
1468
+ if (results == null) {
1469
+ return null;
1470
+ }
998
1471
  DecompiledFunction value = results.getDecompiledFunction();
999
1472
  return value == null ? null : value.getC();
1000
1473
  }
@@ -1046,29 +1046,65 @@ def _serve_connection(connection):
1046
1046
  if not line or len(line) > MAX_LINE_BYTES:
1047
1047
  break
1048
1048
  request_id = None
1049
+ authenticated = False
1049
1050
  should_stop = False
1050
1051
  try:
1051
1052
  request = json.loads(line.decode("utf-8"))
1052
1053
  if set(request) != {"id", "token", "method", "params"}:
1053
1054
  raise ValueError("Invalid bridge request shape")
1054
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")
1055
1058
  if not isinstance(request["token"], str) or not hmac.compare_digest(request["token"], REA_TOKEN):
1056
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
+ }})
1057
1068
  result = _dispatch(request["method"], request["params"])
1058
1069
  should_stop = request["method"] == "shutdown_document" or (
1059
1070
  request["method"] == "shutdown" and not result.get("cleanup_required", False)
1060
1071
  )
1061
- response = {"id": request_id, "result": _json_safe(result)}
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}
1062
1082
  except Exception as error:
1063
- response = {"id": request_id if isinstance(request_id, int) else 0, "error": {"code": -32000, "message": str(error)[:512], "type": _diagnostic_type(error)}}
1064
- file.write((json.dumps(response, separators=(",", ":")) + "\n").encode("utf-8"))
1065
- file.flush()
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)
1066
1096
  if should_stop:
1067
1097
  break
1068
1098
  file.close()
1069
1099
  connection.close()
1070
1100
 
1071
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
+
1072
1108
  def _diagnostic_type(error):
1073
1109
  if isinstance(error, CapabilityUnavailableError):
1074
1110
  return "capability_unavailable"
@@ -1079,6 +1115,20 @@ def _diagnostic_type(error):
1079
1115
  return "bridge_exception"
1080
1116
 
1081
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
+
1082
1132
  def _run():
1083
1133
  """Own a permission-restricted, single-client Unix socket for this bridge."""
1084
1134
  if os.path.exists(REA_SOCKET):