rea-agents 1.6.0 → 2.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 (238) hide show
  1. package/README.md +157 -66
  2. package/bridge/ghidra/ReaGhidraBridge.java +2075 -0
  3. package/bridge/hopper_bridge.py +13 -3
  4. package/dist/application/AbortablePromise.js +25 -0
  5. package/dist/application/AnalysisProvider.js +3 -0
  6. package/dist/application/AnalysisProviderEvaluation.js +60 -0
  7. package/dist/application/AnalysisProviderRegistry.js +228 -0
  8. package/dist/application/AnalysisSnapshotCache.js +41 -21
  9. package/dist/application/AnalysisSnapshotFiles.js +5 -6
  10. package/dist/application/BinarySession.js +218 -101
  11. package/dist/application/CapabilityInventory.js +18 -0
  12. package/dist/application/CompositeProvider.js +20 -8
  13. package/dist/application/CrossVersionInvestigation.js +10 -6
  14. package/dist/application/DirectAnalysis.js +49 -35
  15. package/dist/application/Doctor.js +110 -8
  16. package/dist/application/ElectronBoundaryAnalysis.js +96 -0
  17. package/dist/application/ElectronBoundaryGraph.js +9 -0
  18. package/dist/application/ElectronBoundaryGraphContext.js +62 -0
  19. package/dist/application/ElectronBoundaryGraphIpc.js +242 -0
  20. package/dist/application/ElectronBoundaryGraphNative.js +86 -0
  21. package/dist/application/ElectronBoundaryGraphWindows.js +199 -0
  22. package/dist/application/EnhancedTools.js +39 -14
  23. package/dist/application/InvestigationProviders.js +41 -0
  24. package/dist/application/JavaScriptApplicationEvidence.js +26 -0
  25. package/dist/application/JavaScriptApplicationEvidenceGraph.js +89 -0
  26. package/dist/application/JavaScriptApplicationService.js +74 -0
  27. package/dist/application/JavaScriptApplicationWorkflowEvidence.js +29 -0
  28. package/dist/application/JavaScriptApplicationWorkflowService.js +90 -0
  29. package/dist/application/JavaScriptArtifactAnalysis.js +305 -0
  30. package/dist/application/JavaScriptArtifactAnalysisTypes.js +1 -0
  31. package/dist/application/JavaScriptArtifactFiles.js +231 -0
  32. package/dist/application/JavaScriptArtifactGraphAccumulator.js +54 -0
  33. package/dist/application/JavaScriptArtifactGraphBuilder.js +144 -0
  34. package/dist/application/JavaScriptArtifactGraphContext.js +205 -0
  35. package/dist/application/JavaScriptArtifactGraphDocuments.js +108 -0
  36. package/dist/application/JavaScriptArtifactGraphEvidence.js +99 -0
  37. package/dist/application/JavaScriptArtifactGraphFindings.js +271 -0
  38. package/dist/application/JavaScriptArtifactGraphStructure.js +409 -0
  39. package/dist/application/JavaScriptArtifactReconstruction.js +77 -0
  40. package/dist/application/JavaScriptArtifactReconstructionInput.js +18 -0
  41. package/dist/application/JavaScriptReplayPlanning.js +201 -0
  42. package/dist/application/JavaScriptReplayService.js +266 -0
  43. package/dist/application/JavaScriptRuntimeReconciliationEvidence.js +25 -0
  44. package/dist/application/JavaScriptRuntimeReconciliationService.js +27 -0
  45. package/dist/application/MacHopper.js +20 -4
  46. package/dist/application/ManagedApplicationGraphService.js +55 -0
  47. package/dist/application/ManagedMemberComparisonService.js +122 -0
  48. package/dist/application/ManagedNativeVerificationService.js +39 -0
  49. package/dist/application/ManagedReconstructionService.js +58 -0
  50. package/dist/application/ManagedRuntimeCorrelationService.js +103 -0
  51. package/dist/application/PermissionAuthority.js +1 -1
  52. package/dist/application/ProcessCaptureLifecycle.js +1 -1
  53. package/dist/application/ProcessHarness.js +1 -1
  54. package/dist/application/ProcessSampling.js +16 -12
  55. package/dist/application/ProjectPermissionStore.js +1 -0
  56. package/dist/application/SessionProviderRouter.js +219 -0
  57. package/dist/application/Setup.js +84 -60
  58. package/dist/application/SetupClients.js +1 -1
  59. package/dist/application/SetupPlan.js +9 -2
  60. package/dist/application/SupportedClients.js +40 -16
  61. package/dist/application/Upgrade.js +1 -1
  62. package/dist/application/runtime.js +11 -6
  63. package/dist/artifacts/AsarArtifactReader.js +23 -3
  64. package/dist/browser/CdpBrowserProvider.js +4 -2
  65. package/dist/browser/CdpCaptureEvents.js +49 -0
  66. package/dist/browser/CdpElectronInspection.js +106 -162
  67. package/dist/browser/CdpElectronProvider.js +9 -3
  68. package/dist/browser/CdpElectronScriptEvents.js +69 -0
  69. package/dist/browser/CdpElectronScripts.js +94 -0
  70. package/dist/browser/CdpElectronWorkers.js +56 -0
  71. package/dist/browser/CdpEndpoint.js +14 -1
  72. package/dist/browser/CdpPageCapture.js +8 -4
  73. package/dist/catalogIdentity.js +6 -42
  74. package/dist/cli.js +446 -34
  75. package/dist/cliApplicationCommands.js +62 -0
  76. package/dist/cliBrowserAdvancedCommands.js +5 -4
  77. package/dist/cliBrowserCommands.js +5 -4
  78. package/dist/cliCommandNames.js +56 -0
  79. package/dist/cliElectronCommands.js +106 -3
  80. package/dist/cliEvidenceCommands.js +4 -3
  81. package/dist/cliInvestigationCommands.js +2 -1
  82. package/dist/cliJsonInput.js +54 -0
  83. package/dist/cliPolicyCommands.js +2 -1
  84. package/dist/cliProcessCommands.js +3 -2
  85. package/dist/config.js +101 -2
  86. package/dist/contracts/applicationToolContracts.js +91 -0
  87. package/dist/contracts/artifactToolContracts.js +5 -9
  88. package/dist/contracts/browserToolContracts.js +14 -62
  89. package/dist/contracts/electronToolContracts.js +45 -19
  90. package/dist/contracts/enhancedInputs.js +2 -2
  91. package/dist/contracts/errorSchemas.js +1 -0
  92. package/dist/contracts/investigationExamples.js +0 -6
  93. package/dist/contracts/javascriptApplicationWorkflowExamples.js +46 -0
  94. package/dist/contracts/javascriptRuntimeReconciliationExample.js +208 -0
  95. package/dist/contracts/managedToolContracts.js +153 -0
  96. package/dist/contracts/managedWorkflowExamples.js +346 -0
  97. package/dist/contracts/managedWorkflowToolContracts.js +203 -0
  98. package/dist/contracts/nativeToolContracts.js +2 -6
  99. package/dist/contracts/promptContracts.js +18 -2
  100. package/dist/contracts/providerSelection.js +28 -0
  101. package/dist/contracts/sessionLifecycleInputs.js +2 -0
  102. package/dist/contracts/toolContractExamples.js +10 -25
  103. package/dist/contracts/toolContracts.js +69 -65
  104. package/dist/contracts/toolEffects.js +160 -0
  105. package/dist/contracts/toolOutputSchemas.js +152 -42
  106. package/dist/doctorRuntime.js +47 -0
  107. package/dist/domain/analysisProfile.js +78 -0
  108. package/dist/domain/analysisSnapshot.js +92 -46
  109. package/dist/domain/artifactComparison.js +3 -4
  110. package/dist/domain/artifactGraph.js +1 -0
  111. package/dist/domain/binaryTarget.js +4 -29
  112. package/dist/domain/browserObservation.js +3 -0
  113. package/dist/domain/bundleComparison.js +4 -4
  114. package/dist/domain/electronObservation.js +13 -0
  115. package/dist/domain/electronStaticAnalysis.js +9 -0
  116. package/dist/domain/electronStaticAnalysisBrowser.js +194 -0
  117. package/dist/domain/electronStaticAnalysisIpc.js +171 -0
  118. package/dist/domain/electronStaticAnalysisNative.js +164 -0
  119. package/dist/domain/electronStaticAnalysisTypes.js +1 -0
  120. package/dist/domain/electronStaticAnalysisValues.js +103 -0
  121. package/dist/domain/errors.js +148 -7
  122. package/dist/domain/evidence.js +44 -5
  123. package/dist/domain/evidenceBundle.js +9 -7
  124. package/dist/domain/functionComparison.js +9 -3
  125. package/dist/domain/functionComparisonNormalization.js +37 -0
  126. package/dist/domain/functionComparisonSchemas.js +3 -8
  127. package/dist/domain/functionDossierEvidence.js +4 -1
  128. package/dist/domain/hopperValues.js +66 -9
  129. package/dist/domain/inputIssueProjection.js +52 -0
  130. package/dist/domain/investigationWorkspace.js +2 -1
  131. package/dist/domain/javascriptApplicationAnalysis.js +155 -0
  132. package/dist/domain/javascriptApplicationChangeGraph.js +231 -0
  133. package/dist/domain/javascriptApplicationEvidenceSchemas.js +379 -0
  134. package/dist/domain/javascriptApplicationGraph.js +282 -0
  135. package/dist/domain/javascriptApplicationGraphSchemas.js +139 -0
  136. package/dist/domain/javascriptApplicationVersionComparison.js +116 -0
  137. package/dist/domain/javascriptApplicationVersionComparisonSchemas.js +139 -0
  138. package/dist/domain/javascriptApplicationVersionItems.js +221 -0
  139. package/dist/domain/javascriptApplicationVersionKeys.js +170 -0
  140. package/dist/domain/javascriptAstFingerprint.js +143 -0
  141. package/dist/domain/javascriptFeatureSeed.js +140 -0
  142. package/dist/domain/javascriptFeatureTrace.js +232 -0
  143. package/dist/domain/javascriptFeatureTraceSchemas.js +136 -0
  144. package/dist/domain/javascriptFeatureTraversal.js +88 -0
  145. package/dist/domain/javascriptNativeHandoff.js +85 -0
  146. package/dist/domain/javascriptReplay.js +361 -0
  147. package/dist/domain/javascriptRuntimeLoadState.js +106 -0
  148. package/dist/domain/javascriptRuntimeReconciliation.js +42 -0
  149. package/dist/domain/javascriptRuntimeReconciliationGraph.js +169 -0
  150. package/dist/domain/javascriptRuntimeReconciliationMatching.js +247 -0
  151. package/dist/domain/javascriptRuntimeReconciliationParsing.js +160 -0
  152. package/dist/domain/javascriptRuntimeReconciliationResult.js +119 -0
  153. package/dist/domain/javascriptRuntimeReconciliationRuntime.js +316 -0
  154. package/dist/domain/javascriptRuntimeReconciliationSchemas.js +245 -0
  155. package/dist/domain/javascriptRuntimeStaticCandidates.js +191 -0
  156. package/dist/domain/javascriptStaticAnalysis.js +346 -0
  157. package/dist/domain/javascriptStaticAnalysisFindings.js +39 -0
  158. package/dist/domain/javascriptStaticAnalysisHelpers.js +274 -0
  159. package/dist/domain/javascriptStaticAnalysisState.js +23 -0
  160. package/dist/domain/javascriptStaticAnalysisTypes.js +1 -0
  161. package/dist/domain/managedApplicationGraph.js +556 -0
  162. package/dist/domain/managedArtifact.js +444 -0
  163. package/dist/domain/managedMemberComparison.js +565 -0
  164. package/dist/domain/managedNativeVerification.js +408 -0
  165. package/dist/domain/managedReconstruction.js +183 -0
  166. package/dist/domain/managedRuntimeCorrelation.js +175 -0
  167. package/dist/domain/reconstructionVerificationSchemas.js +1 -3
  168. package/dist/dotnet/ManagedArtifactInspector.js +318 -0
  169. package/dist/dotnet/ManagedMemberInspector.js +887 -0
  170. package/dist/dotnet/ManagedMetadataHeaps.js +131 -0
  171. package/dist/dotnet/ManagedMetadataInventory.js +368 -0
  172. package/dist/dotnet/ManagedMetadataLayout.js +322 -0
  173. package/dist/dotnet/ManagedNativeBoundaryInspector.js +391 -0
  174. package/dist/dotnet/ManagedPeReader.js +157 -0
  175. package/dist/dotnet/ManagedReaderFailure.js +10 -0
  176. package/dist/dotnet/ManagedStaticProvider.js +203 -0
  177. package/dist/generatedPackageMetadata.js +2 -2
  178. package/dist/ghidra/GhidraAnalysisProfile.js +37 -0
  179. package/dist/ghidra/GhidraClient.js +400 -0
  180. package/dist/ghidra/GhidraClientTypes.js +1 -0
  181. package/dist/ghidra/GhidraDefaults.js +20 -0
  182. package/dist/ghidra/GhidraDiagnostics.js +43 -0
  183. package/dist/ghidra/GhidraDoctor.js +59 -0
  184. package/dist/ghidra/GhidraFunctionValues.js +264 -0
  185. package/dist/ghidra/GhidraInstallation.js +239 -0
  186. package/dist/ghidra/GhidraInventoryValues.js +217 -0
  187. package/dist/ghidra/GhidraLauncher.js +139 -0
  188. package/dist/ghidra/GhidraProvider.js +366 -0
  189. package/dist/ghidra/GhidraRequestQueue.js +98 -0
  190. package/dist/ghidra/GhidraResponseBuffer.js +31 -0
  191. package/dist/ghidra/GhidraResponseRouter.js +24 -0
  192. package/dist/ghidra/GhidraSessionError.js +39 -0
  193. package/dist/ghidra/GhidraSessionValues.js +62 -0
  194. package/dist/ghidra/GhidraSocketConnection.js +46 -0
  195. package/dist/ghidra/protocol.js +69 -0
  196. package/dist/hopper/BridgeLauncher.js +19 -54
  197. package/dist/hopper/HopperAnalysisProfile.js +106 -0
  198. package/dist/hopper/HopperClient.js +154 -162
  199. package/dist/hopper/HopperProvider.js +99 -7
  200. package/dist/hopper/HopperSessionValues.js +27 -0
  201. package/dist/hopper/HopperSocketConnection.js +31 -0
  202. package/dist/main.js +14 -0
  203. package/dist/native/NativeMacOSProvider.js +3 -1
  204. package/dist/process/PendingOperations.js +59 -0
  205. package/dist/process/PrivateRuntimeRoot.js +33 -0
  206. package/dist/process/ProviderDeadline.js +95 -0
  207. package/dist/process/ProviderProcess.js +275 -0
  208. package/dist/replay/JavaScriptReplayWorker.js +286 -0
  209. package/dist/replay/LinuxJavaScriptReplayRunner.js +434 -0
  210. package/dist/replay/LinuxRuntimeClosure.js +46 -0
  211. package/dist/replay/LinuxSeccompPolicy.js +40 -0
  212. package/dist/replay/ReplayWorkerProtocol.js +55 -0
  213. package/dist/replay/SystemJavaScriptReplayHost.js +182 -0
  214. package/dist/server/createServer.js +53 -0
  215. package/dist/server/promptCompletion.js +9 -3
  216. package/dist/server/registerApplicationTools.js +115 -0
  217. package/dist/server/registerArtifactComparisonTool.js +16 -9
  218. package/dist/server/registerBrowserTools.js +33 -8
  219. package/dist/server/registerBundleComparisonTool.js +17 -20
  220. package/dist/server/registerElectronTools.js +45 -3
  221. package/dist/server/registerEnhancedTools.js +67 -19
  222. package/dist/server/registerEvidenceTools.js +5 -1
  223. package/dist/server/registerFunctionComparisonTool.js +14 -7
  224. package/dist/server/registerInvestigationTools.js +21 -17
  225. package/dist/server/registerManagedTools.js +6 -0
  226. package/dist/server/registerManagedWorkflowTools.js +277 -0
  227. package/dist/server/registerOfficialTools.js +11 -8
  228. package/dist/server/registerProcessComparisonTool.js +25 -47
  229. package/dist/server/registerSessionStatusTool.js +7 -7
  230. package/dist/server/registerSessionTools.js +57 -87
  231. package/dist/server/sessionEvidence.js +14 -19
  232. package/dist/server/toolInputValidation.js +77 -0
  233. package/dist/server/toolRegistrationOptions.js +79 -2
  234. package/dist/server/toolResult.js +25 -17
  235. package/package.json +17 -7
  236. package/scripts/rea.mjs +36 -2
  237. package/skills/rea-analysis/SKILL.md +59 -3
  238. /package/dist/{application → process}/ProcessOwnership.js +0 -0
@@ -13,20 +13,19 @@ import { snapshotEvidenceForQuery, snapshotMatchesTarget, } from "../domain/anal
13
13
  import { err, ok } from "../domain/result.js";
14
14
  import { loadConfiguredPermissionAuthority } from "./PermissionConfiguration.js";
15
15
  import { authorizeFileReadWithDeferredWrite, } from "./DeferredFileAuthorization.js";
16
- const WORKFLOW_PROVIDER = {
17
- id: "rea-workflow",
18
- name: "REA composed investigation workflow",
19
- version: "1",
20
- };
16
+ import { REA_WORKFLOW_PROVIDER, workflowAnalysisProfile, } from "./InvestigationProviders.js";
21
17
  /**
22
- * Open one binary, execute one tool, and always release the bridge session.
18
+ * Open one binary, execute one tool, and always release provider resources.
23
19
  * Unlike MCP mode, every CLI invocation is intentionally isolated and does not
24
- * retain a target or bridge resources for a subsequent command.
20
+ * retain a target or provider client for a subsequent command.
25
21
  */
26
22
  export const runDirectAnalysis = async (path, tool, arguments_, options = {}) => withProcessCancellation(options.signal, (signal) => runAnalysis(path, tool, arguments_, {
27
23
  logger: options.logger ?? silentLogger,
28
24
  snapshotPath: options.snapshotPath,
29
25
  signal,
26
+ ...(options.providerId === undefined
27
+ ? {}
28
+ : { providerId: options.providerId }),
30
29
  ...(options.permissionAuthority === undefined
31
30
  ? {}
32
31
  : { permissionAuthority: options.permissionAuthority }),
@@ -37,7 +36,7 @@ export const runProviderAnalysis = async (path, tool, arguments_, logger = silen
37
36
  snapshotPath: undefined,
38
37
  signal: operationSignal,
39
38
  }));
40
- /** Describe configured providers without opening a target or launching Hopper. */
39
+ /** Describe configured providers without opening a target or starting one. */
41
40
  export const runSessionStatus = async (logger = silentLogger) => {
42
41
  const config = parseConfig(process.env);
43
42
  if (!config.ok)
@@ -135,30 +134,44 @@ const runAnalysis = async (path, tool, arguments_, options) => {
135
134
  });
136
135
  if (!authorization.ok)
137
136
  return cliError(authorization.error);
138
- const unavailable = snapshotUnavailable(snapshotPath, config.value.hopperLoaderArgs);
139
- if (unavailable !== undefined)
140
- return cliError(unavailable);
141
137
  const session = createBinarySession(config.value, logger);
142
138
  try {
143
139
  const prepared = await prepareSnapshot({
144
140
  path,
145
- tool,
146
- arguments: arguments_,
147
141
  snapshotPath,
148
142
  policy: config.value.analysisSnapshotFilePolicy,
149
- provider: replayProviderFor(session, tool),
150
143
  });
151
144
  if (!prepared.ok)
152
145
  return cliError(prepared.error);
153
- if (prepared.value.evidence !== undefined)
154
- return prepared.value.evidence;
155
- const writable = await authorizeDeferredWrite(authorization.value);
156
- if (!writable.ok)
157
- return cliError(writable.error);
158
146
  const { snapshot } = prepared.value;
159
- const opened = await session.open(path, snapshot === undefined ? { signal } : { signal, snapshot });
147
+ const opened = await session.open(path, {
148
+ signal,
149
+ ...(snapshot === undefined ? {} : { snapshot }),
150
+ ...(options.providerId === undefined
151
+ ? {}
152
+ : { providerId: options.providerId }),
153
+ });
160
154
  if (!opened.ok)
161
155
  return cliError(opened.error);
156
+ const evidenceProfile = analysisProfileForEvidence(session, tool);
157
+ const bindingProfile = session.analysisProfile();
158
+ if (snapshot !== undefined &&
159
+ evidenceProfile !== undefined &&
160
+ bindingProfile !== undefined) {
161
+ const cached = snapshotEvidenceForQuery(snapshot, {
162
+ target: opened.value,
163
+ bindingProfile,
164
+ operation: tool,
165
+ parameters: arguments_,
166
+ provider: replayProviderFor(session, tool),
167
+ evidenceProfile,
168
+ });
169
+ if (cached !== undefined)
170
+ return cached;
171
+ }
172
+ const writable = await authorizeDeferredWrite(authorization.value);
173
+ if (!writable.ok)
174
+ return cliError(writable.error);
162
175
  let output;
163
176
  let evidence;
164
177
  if (tool === "binary_overview" ||
@@ -170,10 +183,13 @@ const runAnalysis = async (path, tool, arguments_, options) => {
170
183
  else {
171
184
  evidence = createEvidence(opened.value, tool === "analyze_function"
172
185
  ? session.providerIdentity(tool)
173
- : WORKFLOW_PROVIDER, {
186
+ : REA_WORKFLOW_PROVIDER, {
174
187
  operation: tool,
175
188
  parameters: arguments_,
176
189
  result: result.value,
190
+ ...(evidenceProfile === undefined
191
+ ? {}
192
+ : { analysisProfile: evidenceProfile }),
177
193
  confidence: "derived",
178
194
  limitations: ["Derived by an REA composed workflow."],
179
195
  });
@@ -189,6 +205,9 @@ const runAnalysis = async (path, tool, arguments_, options) => {
189
205
  operation: tool,
190
206
  parameters: arguments_,
191
207
  result: result.value.result,
208
+ ...(result.value.analysisProfile === undefined
209
+ ? {}
210
+ : { analysisProfile: result.value.analysisProfile }),
192
211
  rawResult: result.value.rawResult,
193
212
  limitations: result.value.limitations,
194
213
  locations: result.value.locations,
@@ -226,7 +245,7 @@ const withProcessCancellation = async (suppliedSignal, operation) => {
226
245
  }
227
246
  };
228
247
  const prepareSnapshot = async (options) => {
229
- const { path, tool, arguments: arguments_, snapshotPath, policy, provider, } = options;
248
+ const { path, snapshotPath, policy } = options;
230
249
  if (snapshotPath === undefined || !(await fileExists(snapshotPath)))
231
250
  return ok({});
232
251
  const loaded = await readAnalysisSnapshot(snapshotPath, policy);
@@ -237,22 +256,17 @@ const prepareSnapshot = async (options) => {
237
256
  return target;
238
257
  if (!snapshotMatchesTarget(loaded.value.target, target.value))
239
258
  return err(new EvidenceIntegrityError("Analysis snapshot target does not match the requested binary"));
240
- const evidence = snapshotEvidenceForQuery(loaded.value, {
241
- target: target.value,
242
- operation: tool,
243
- parameters: arguments_,
244
- provider,
245
- });
246
- return ok(evidence === undefined
247
- ? { snapshot: loaded.value }
248
- : { snapshot: loaded.value, evidence });
259
+ return ok({ snapshot: loaded.value });
249
260
  };
250
- const snapshotUnavailable = (snapshotPath, loaderArgs) => snapshotPath !== undefined && loaderArgs.length > 0
251
- ? new EvidenceIntegrityError("Analysis snapshots are unavailable with custom Hopper loader arguments")
252
- : undefined;
253
261
  const replayProviderFor = (session, tool) => tool === "binary_overview" || tool === "trace_feature"
254
- ? WORKFLOW_PROVIDER
262
+ ? REA_WORKFLOW_PROVIDER
255
263
  : session.providerIdentity(tool);
264
+ const analysisProfileForEvidence = (session, tool) => {
265
+ if (tool !== "binary_overview" && tool !== "trace_feature")
266
+ return session.analysisProfile(tool);
267
+ const upstream = session.analysisProfile();
268
+ return upstream === undefined ? undefined : workflowAnalysisProfile(upstream);
269
+ };
256
270
  const fileExists = async (path) => {
257
271
  try {
258
272
  await access(path);
@@ -65,6 +65,10 @@ export const runDoctor = async (target, host = systemDoctorHost()) => {
65
65
  remediation: unsupportedHopperRemediation(host.configuredHopperPath === hopperPath),
66
66
  classification: "config_drift",
67
67
  }));
68
+ const providerInspections = await inspectDoctorProviders(host, checks);
69
+ const javascriptReplayCheck = await host.javascriptReplayCheck?.();
70
+ if (javascriptReplayCheck !== undefined)
71
+ checks.push(javascriptReplayCheck);
68
72
  if (host.platform === "linux" && hopperPath !== undefined)
69
73
  checks.push(check("hopper-demo-runtime", await host.linuxDemoRuntimeReady(), undefined, {
70
74
  remediation: "Rerun rea setup to install the Xvfb, xauth, Python, X11, and XTEST packages required for Linux demo sessions.",
@@ -76,7 +80,33 @@ export const runDoctor = async (target, host = systemDoctorHost()) => {
76
80
  classification: "config_drift",
77
81
  }));
78
82
  const installationPaths = (await host.installationPaths?.()) ?? [];
79
- const installedSkillVersion = await host.installedSkillVersion?.();
83
+ const observedSkillIdentity = await host.installedSkillIdentity?.();
84
+ const legacySkillVersion = observedSkillIdentity === undefined
85
+ ? await host.installedSkillVersion?.()
86
+ : undefined;
87
+ const installedSkillIdentity = observedSkillIdentity ??
88
+ (legacySkillVersion === undefined
89
+ ? undefined
90
+ : {
91
+ version: legacySkillVersion,
92
+ toolCount: null,
93
+ catalogDigest: null,
94
+ });
95
+ const installedSkillVersion = installedSkillIdentity?.version ?? undefined;
96
+ const skillAligned = installedSkillIdentity?.version === PRODUCT_IDENTITY.skillVersion &&
97
+ installedSkillIdentity.toolCount === CATALOG_IDENTITY.counts.mcp_tools &&
98
+ installedSkillIdentity.catalogDigest ===
99
+ CATALOG_IDENTITY.digests.combined_sha256;
100
+ if (!skillAligned)
101
+ checks.push({
102
+ name: "skill:identity",
103
+ ok: false,
104
+ classification: "config_drift",
105
+ detail: installedSkillIdentity === undefined
106
+ ? "Installed REA skill identity is missing."
107
+ : "Installed REA skill identity is stale.",
108
+ remediation: "Run rea setup to update the installed REA skill.",
109
+ });
80
110
  const registrations = (await host.clientRegistrations?.()) ?? [];
81
111
  for (const registration of registrations)
82
112
  if (registration.state !== "aligned")
@@ -89,8 +119,16 @@ export const runDoctor = async (target, host = systemDoctorHost()) => {
89
119
  "Run rea setup, then restart the affected client.",
90
120
  });
91
121
  return {
92
- healthy: checks.every(({ ok }) => ok),
122
+ healthy: doctorHealthy(checks, {
123
+ hopperAvailable: hopperPath !== undefined &&
124
+ checks
125
+ .filter(({ name }) => name.startsWith("hopper"))
126
+ .every(({ ok }) => ok),
127
+ hopperConfigured: host.configuredHopperPath !== undefined,
128
+ providerInspections,
129
+ }),
93
130
  ...(hopperPath === undefined ? {} : { hopperPath }),
131
+ ...(providerInspections === undefined ? {} : { providerInspections }),
94
132
  checks,
95
133
  identity: {
96
134
  cli_package_version: PRODUCT_IDENTITY.packageVersion,
@@ -111,13 +149,14 @@ export const runDoctor = async (target, host = systemDoctorHost()) => {
111
149
  },
112
150
  skill: {
113
151
  installed_version: installedSkillVersion ?? null,
114
- state: installedSkillVersion === undefined
115
- ? "unknown"
116
- : installedSkillVersion === PRODUCT_IDENTITY.skillVersion
152
+ installed_tool_count: installedSkillIdentity?.toolCount ?? null,
153
+ installed_catalog_digest: installedSkillIdentity?.catalogDigest ?? null,
154
+ state: installedSkillIdentity === undefined
155
+ ? "missing"
156
+ : skillAligned
117
157
  ? "aligned"
118
158
  : "stale",
119
- remediation: installedSkillVersion === undefined ||
120
- installedSkillVersion === PRODUCT_IDENTITY.skillVersion
159
+ remediation: skillAligned
121
160
  ? null
122
161
  : "Run rea setup to update the installed REA skill.",
123
162
  },
@@ -129,7 +168,7 @@ const unsupportedHopperRemediation = (configured) => configured
129
168
  ? "Unset or update HOPPER_LAUNCHER_PATH, install the Hopper build supported by this REA release, or update REA."
130
169
  : "Install the Hopper build supported by this REA release, or update REA for a newer Hopper build.";
131
170
  /** Create diagnostics backed by the current process and host commands. */
132
- export const systemDoctorHost = () => ({
171
+ export const systemDoctorHost = (options = {}) => ({
133
172
  platform: process.platform,
134
173
  nodeVersion: process.versions.node,
135
174
  ...(process.env.HOPPER_LAUNCHER_PATH === undefined
@@ -208,6 +247,12 @@ export const systemDoctorHost = () => ({
208
247
  }
209
248
  return paths;
210
249
  },
250
+ ...(options.providerInspections === undefined
251
+ ? {}
252
+ : { providerInspections: options.providerInspections }),
253
+ ...(options.javascriptReplayCheck === undefined
254
+ ? {}
255
+ : { javascriptReplayCheck: options.javascriptReplayCheck }),
211
256
  async installationPaths() {
212
257
  try {
213
258
  const command = process.platform === "win32" ? "where" : "which";
@@ -227,6 +272,23 @@ export const systemDoctorHost = () => ({
227
272
  return undefined;
228
273
  }
229
274
  },
275
+ async installedSkillIdentity() {
276
+ try {
277
+ const content = await readFile(join(homedir(), ".agents/skills", PRODUCT_IDENTITY.skillName, "SKILL.md"), "utf8");
278
+ const version = /^\s{2}version:\s*"([^"]+)"\s*$/mu.exec(content)?.[1] ?? null;
279
+ const countText = /^\s{2}tool_count:\s*(\d+)\s*$/mu.exec(content)?.[1];
280
+ const catalogDigest = /^\s{2}catalog_digest:\s*"([a-f0-9]{64})"\s*$/mu.exec(content)?.[1] ??
281
+ null;
282
+ return {
283
+ version,
284
+ toolCount: countText === undefined ? null : Number.parseInt(countText, 10),
285
+ catalogDigest,
286
+ };
287
+ }
288
+ catch {
289
+ return undefined;
290
+ }
291
+ },
230
292
  clientRegistrations: () => readClientRegistrationStatuses(homedir()),
231
293
  });
232
294
  const parseMajor = (version) => Number.parseInt(version.split(".")[0] ?? "0", 10);
@@ -243,3 +305,43 @@ const check = (name, ok, detail, failure) => ({
243
305
  ...(detail === undefined ? {} : { detail }),
244
306
  ...(ok ? {} : { remediation: failure.remediation }),
245
307
  });
308
+ const providerDoctorChecks = (inspection) => inspection.checks.map((candidate) => ({
309
+ name: candidate.name === "configuration"
310
+ ? inspection.id
311
+ : `${inspection.id}-${candidate.name}`,
312
+ ok: candidate.ok,
313
+ classification: candidate.ok ? "healthy" : candidate.classification,
314
+ detail: candidate.detail,
315
+ ...(candidate.remediation === null
316
+ ? {}
317
+ : { remediation: candidate.remediation }),
318
+ }));
319
+ const inspectDoctorProviders = async (host, checks) => {
320
+ const inspections = await host.providerInspections?.();
321
+ if (inspections === undefined)
322
+ return undefined;
323
+ for (const inspection of inspections)
324
+ checks.push(...providerDoctorChecks(inspection));
325
+ return inspections;
326
+ };
327
+ const doctorHealthy = (checks, providers) => {
328
+ if (providers.providerInspections === undefined)
329
+ return checks.every(({ ok }) => ok);
330
+ const providerChecks = checks.filter(({ name }) => name.startsWith("hopper") ||
331
+ providers.providerInspections?.some((inspection) => providerCheckName(inspection.id, name)) === true);
332
+ const coreHealthy = checks
333
+ .filter((candidate) => !providerChecks.includes(candidate))
334
+ .every(({ ok }) => ok);
335
+ const configuredProvidersHealthy = providerChecks
336
+ .filter(({ name }) => {
337
+ if (name.startsWith("hopper"))
338
+ return providers.hopperConfigured;
339
+ return providers.providerInspections?.some((inspection) => inspection.configured && providerCheckName(inspection.id, name));
340
+ })
341
+ .every(({ ok }) => ok);
342
+ return (coreHealthy &&
343
+ configuredProvidersHealthy &&
344
+ (providers.hopperAvailable ||
345
+ providers.providerInspections.some(({ available }) => available)));
346
+ };
347
+ const providerCheckName = (providerId, name) => name === providerId || name.startsWith(`${providerId}-`);
@@ -0,0 +1,96 @@
1
+ import { resolveArtifactPath } from "./JavaScriptArtifactGraphContext.js";
2
+ /** Flatten all per-file IPC facts into deterministic application records. */
3
+ export const collectElectronIpcRecords = (analysis) => analysis.files
4
+ .flatMap(({ file, javascript }) => (javascript?.electron.ipc ?? []).map((finding) => ({
5
+ key: ipcRecordKey(file.path, finding),
6
+ file,
7
+ finding,
8
+ })))
9
+ .sort((left, right) => compareCodePoints(left.key, right.key));
10
+ /** Match only exact literal channels and retain ambiguous matches separately. */
11
+ export const classifyElectronIpcPairings = (records) => {
12
+ const handlers = records.filter(isMainHandler);
13
+ return records.filter(isPairableRendererTransmission).map((transmission) => {
14
+ const candidates = transmission.finding.channel === null
15
+ ? []
16
+ : handlers.filter((handler) => handler.finding.channel === transmission.finding.channel &&
17
+ compatibleIpcModes(transmission.finding, handler.finding));
18
+ return {
19
+ transmission,
20
+ candidates,
21
+ status: candidates.length === 1
22
+ ? "paired"
23
+ : candidates.length > 1
24
+ ? "ambiguous"
25
+ : "unpaired",
26
+ };
27
+ });
28
+ };
29
+ /** Return only pairings supported by one unique literal main handler. */
30
+ export const unambiguousElectronIpcPairings = (records) => classifyElectronIpcPairings(records).flatMap((state) => {
31
+ const handler = state.candidates[0];
32
+ return state.status === "paired" && handler !== undefined
33
+ ? [{ transmission: state.transmission, handler }]
34
+ : [];
35
+ });
36
+ /** Summarize static Electron findings without treating gaps as absence. */
37
+ export const summarizeElectronBoundaries = (analysis) => {
38
+ const files = new Map(analysis.files.map(({ file }) => [file.path, file]));
39
+ const javascript = analysis.files.flatMap(({ javascript: value }) => value === null ? [] : [value]);
40
+ const windows = javascript.flatMap(({ electron }) => electron.browser_windows);
41
+ const bridges = javascript.flatMap(({ electron }) => electron.context_bridge_apis);
42
+ const validations = javascript.flatMap(({ electron }) => electron.sender_validations);
43
+ const utilities = analysis.files.flatMap(({ file, javascript: value }) => (value?.electron.utility_processes ?? []).map((finding) => ({
44
+ file,
45
+ finding,
46
+ })));
47
+ const nativeBindings = analysis.files.flatMap(({ file, javascript: value }) => (value?.electron.native_addon_bindings ?? []).map((finding) => ({
48
+ file,
49
+ finding,
50
+ })));
51
+ const ipc = collectElectronIpcRecords(analysis);
52
+ const pairing = classifyElectronIpcPairings(ipc);
53
+ const literalChannels = new Set(ipc.flatMap(({ finding }) => finding.channel === null ? [] : [finding.channel]));
54
+ return {
55
+ browser_windows: windows.length,
56
+ explicit_web_preferences: windows.reduce((count, window) => count + window.web_preferences.length, 0),
57
+ preload_entrypoints: windows.filter(({ preload_path: path }) => path !== null).length,
58
+ context_bridge_apis: bridges.length,
59
+ exposed_api_members: bridges.reduce((count, bridge) => count + bridge.members.length, 0),
60
+ ipc: {
61
+ operations: ipc.length,
62
+ literal_channels: literalChannels.size,
63
+ dynamic_channel_operations: ipc.filter(({ finding }) => finding.channel === null).length,
64
+ renderer_transmissions: ipc.filter(isRendererTransmission).length,
65
+ renderer_listeners: ipc.filter(({ finding }) => finding.side === "renderer" && finding.mode === "listen").length,
66
+ main_handlers: ipc.filter(isMainHandler).length,
67
+ paired_renderer_transmissions: pairing.filter(({ status }) => status === "paired").length,
68
+ ambiguous_renderer_transmissions: pairing.filter(({ status }) => status === "ambiguous").length,
69
+ unpaired_literal_renderer_transmissions: pairing.filter(({ status, transmission }) => status === "unpaired" && transmission.finding.channel !== null).length,
70
+ },
71
+ sender_validation_observations: validations.length,
72
+ utility_processes: utilities.length,
73
+ resolved_utility_entrypoints: utilities.filter(({ file, finding }) => resolvesToFile(file, finding.module_path, files)).length,
74
+ native_addon_bindings: nativeBindings.length,
75
+ resolved_native_addon_bindings: nativeBindings.filter(({ file, finding }) => resolvesToFile(file, finding.specifier, files, "native-addon")).length,
76
+ };
77
+ };
78
+ const compatibleIpcModes = (transmission, handler) => transmission.mode === "invoke"
79
+ ? handler.mode === "handle"
80
+ : transmission.operation === "send-to-host"
81
+ ? false
82
+ : transmission.mode === "send" && handler.mode === "listen";
83
+ const isRendererTransmission = ({ finding }) => finding.side === "renderer" &&
84
+ (finding.mode === "send" || finding.mode === "invoke");
85
+ const isPairableRendererTransmission = (record) => isRendererTransmission(record) && record.finding.operation !== "send-to-host";
86
+ const isMainHandler = ({ finding }) => finding.side === "main" &&
87
+ (finding.mode === "listen" || finding.mode === "handle");
88
+ const resolvesToFile = (file, specifier, files, kind) => {
89
+ if (specifier === null)
90
+ return false;
91
+ const resolved = resolveArtifactPath(specifier, file.path, files);
92
+ return (resolved !== null &&
93
+ (kind === undefined || files.get(resolved)?.kind === kind));
94
+ };
95
+ const ipcRecordKey = (path, finding) => `${path}\0${String(finding.location.start.line)}:${String(finding.location.start.column)}\0${finding.side}\0${finding.operation}`;
96
+ const compareCodePoints = (left, right) => left < right ? -1 : left > right ? 1 : 0;
@@ -0,0 +1,9 @@
1
+ import { addElectronIpcBoundaries } from "./ElectronBoundaryGraphIpc.js";
2
+ import { addElectronNativeBoundaries } from "./ElectronBoundaryGraphNative.js";
3
+ import { addElectronWindowBoundaries } from "./ElectronBoundaryGraphWindows.js";
4
+ /** Add every static Electron process and security-boundary fact to the JAG. */
5
+ export const addElectronBoundaries = (context) => {
6
+ addElectronWindowBoundaries(context);
7
+ addElectronIpcBoundaries(context);
8
+ addElectronNativeBoundaries(context);
9
+ };
@@ -0,0 +1,62 @@
1
+ import { sha256Text } from "../domain/javascriptStaticAnalysisHelpers.js";
2
+ import { astObservationEvidence, staticInferenceEvidence, } from "./JavaScriptArtifactGraphEvidence.js";
3
+ /** Resolve the file or recovered bundle module that owns one finding. */
4
+ export const electronFindingSourceNode = (context, file, moduleKey) => moduleKey === null
5
+ ? (context.assetNodes.get(file.path) ?? context.fileNodes.get(file.path))
6
+ : context.moduleNodes.get(`${file.path}\0${moduleKey}`);
7
+ /** Observation-scoped identity shared safely across different artifact files. */
8
+ export const electronObservationIdentity = (context, scope, key) => ({
9
+ strategy: "observation-fingerprint",
10
+ stability: "observation-only",
11
+ observation_sha256: sha256Text(`${context.snapshot.manifest.root_sha256}\0${scope}\0${key}`),
12
+ scope: scope.slice(0, 4_096),
13
+ });
14
+ /** Add a direct AST syntax relationship. */
15
+ export const addElectronAstEdge = (context, input) => {
16
+ context.accumulator.addEdge({
17
+ source_node_id: input.source.node_id,
18
+ target_node_id: input.target.node_id,
19
+ relation: input.relation,
20
+ properties: input.properties,
21
+ evidence: astObservationEvidence({
22
+ sha256: input.file.sha256,
23
+ path: input.file.path,
24
+ range: input.range,
25
+ operation: input.operation,
26
+ coverage: input.coverage,
27
+ ...(input.limitations === undefined
28
+ ? {}
29
+ : { limitations: input.limitations }),
30
+ }),
31
+ });
32
+ };
33
+ /** Add an Electron relationship that static syntax suggests but cannot prove. */
34
+ export const addElectronInferenceEdge = (context, input) => {
35
+ context.accumulator.addEdge({
36
+ source_node_id: input.source.node_id,
37
+ target_node_id: input.target.node_id,
38
+ relation: input.relation,
39
+ properties: input.properties,
40
+ evidence: staticInferenceEvidence({
41
+ sha256: input.file.sha256,
42
+ path: input.file.path,
43
+ range: input.range,
44
+ operation: input.operation,
45
+ coverage: input.coverage,
46
+ ...(input.confidence === undefined
47
+ ? {}
48
+ : { confidence: input.confidence }),
49
+ ...(input.limitations === undefined
50
+ ? {}
51
+ : { limitations: input.limitations }),
52
+ }),
53
+ });
54
+ };
55
+ /** Deterministic source-range key for artifact-local graph identities. */
56
+ export const electronRangeKey = (range) => `${String(range.start.line)}:${String(range.start.column)}-${String(range.end.line)}:${String(range.end.column)}`;
57
+ /** Test exact source-range containment without assuming runtime reachability. */
58
+ export const electronRangeContains = (outer, inner) => comparePoint(outer.start, inner.start) <= 0 &&
59
+ comparePoint(outer.end, inner.end) >= 0;
60
+ const comparePoint = (left, right) => left.line === right.line
61
+ ? left.column - right.column
62
+ : left.line - right.line;