rea-agents 1.5.0 → 1.7.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 (172) hide show
  1. package/README.md +144 -58
  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 +229 -102
  11. package/dist/application/BrowserEvidence.js +87 -15
  12. package/dist/application/BrowserObservationService.js +65 -1
  13. package/dist/application/CapabilityInventory.js +7 -0
  14. package/dist/application/ClientConfigPath.js +33 -0
  15. package/dist/application/CompositeProvider.js +20 -8
  16. package/dist/application/CrossVersionInventory.js +16 -3
  17. package/dist/application/CrossVersionInvestigation.js +17 -62
  18. package/dist/application/CrossVersionInvestigationEvidence.js +64 -0
  19. package/dist/application/CrossVersionInvestigationReplay.js +170 -0
  20. package/dist/application/DirectAnalysis.js +49 -35
  21. package/dist/application/Doctor.js +70 -13
  22. package/dist/application/ElectronEvidence.js +35 -0
  23. package/dist/application/ElectronObservationPort.js +1 -0
  24. package/dist/application/ElectronObservationService.js +46 -0
  25. package/dist/application/EnhancedTools.js +1 -1
  26. package/dist/application/EvidenceLedger.js +5 -2
  27. package/dist/application/InvestigationProviders.js +11 -0
  28. package/dist/application/JavaScriptArtifactAnalysis.js +299 -0
  29. package/dist/application/JavaScriptArtifactAnalysisTypes.js +1 -0
  30. package/dist/application/JavaScriptArtifactFiles.js +231 -0
  31. package/dist/application/JavaScriptArtifactGraphAccumulator.js +54 -0
  32. package/dist/application/JavaScriptArtifactGraphBuilder.js +101 -0
  33. package/dist/application/JavaScriptArtifactGraphContext.js +205 -0
  34. package/dist/application/JavaScriptArtifactGraphDocuments.js +108 -0
  35. package/dist/application/JavaScriptArtifactGraphEvidence.js +89 -0
  36. package/dist/application/JavaScriptArtifactGraphFindings.js +271 -0
  37. package/dist/application/JavaScriptArtifactGraphStructure.js +409 -0
  38. package/dist/application/JavaScriptArtifactReconstruction.js +74 -0
  39. package/dist/application/JavaScriptArtifactReconstructionInput.js +99 -0
  40. package/dist/application/MacHopper.js +20 -4
  41. package/dist/application/PermissionAuthority.js +30 -0
  42. package/dist/application/ProcessCaptureLifecycle.js +2 -6
  43. package/dist/application/ProcessHarness.js +13 -7
  44. package/dist/application/ProcessSampling.js +59 -8
  45. package/dist/application/ProjectPermissionStore.js +1 -0
  46. package/dist/application/ReferenceSourceImport.js +13 -16
  47. package/dist/application/SessionProviderRouter.js +219 -0
  48. package/dist/application/Setup.js +153 -70
  49. package/dist/application/SetupClients.js +8 -5
  50. package/dist/application/SetupInstallFailure.js +24 -0
  51. package/dist/application/SetupPlan.js +19 -8
  52. package/dist/application/SetupSkill.js +20 -3
  53. package/dist/application/SupportedClients.js +40 -16
  54. package/dist/application/Uninstall.js +20 -7
  55. package/dist/application/Upgrade.js +15 -4
  56. package/dist/application/runtime.js +9 -6
  57. package/dist/artifacts/AsarArtifactReader.js +23 -3
  58. package/dist/browser/CdpBrowserProvider.js +158 -53
  59. package/dist/browser/CdpCaptureCompleteness.js +90 -0
  60. package/dist/browser/CdpCaptureDocuments.js +180 -21
  61. package/dist/browser/CdpCaptureEvents.js +342 -43
  62. package/dist/browser/CdpCaptureValues.js +5 -7
  63. package/dist/browser/CdpConnection.js +47 -24
  64. package/dist/browser/CdpElectronInspection.js +330 -0
  65. package/dist/browser/CdpElectronProvider.js +136 -0
  66. package/dist/browser/CdpEndpoint.js +77 -5
  67. package/dist/browser/CdpObservationSession.js +301 -0
  68. package/dist/browser/CdpPageCapture.js +159 -56
  69. package/dist/browser/CdpSafeMetadata.js +203 -0
  70. package/dist/browser/CdpScreenshot.js +101 -0
  71. package/dist/browser/CdpTargetSession.js +47 -0
  72. package/dist/browser/CdpWebMcpDiscovery.js +242 -0
  73. package/dist/browser/ElectronFileScope.js +50 -0
  74. package/dist/browser/PngVisualDiff.js +177 -0
  75. package/dist/browser/SensitiveTextCapture.js +25 -0
  76. package/dist/browser/WebSourceMapFetcher.js +357 -0
  77. package/dist/catalogIdentity.js +2 -33
  78. package/dist/cli.js +62 -34
  79. package/dist/cliBrowserAdvancedCommands.js +170 -0
  80. package/dist/cliBrowserCommands.js +180 -3
  81. package/dist/cliCommandNames.js +43 -0
  82. package/dist/cliElectronCommands.js +115 -0
  83. package/dist/cliEvidenceCommands.js +4 -3
  84. package/dist/cliInvestigationCommands.js +17 -9
  85. package/dist/cliPolicyCommands.js +49 -3
  86. package/dist/cliProcessCommands.js +3 -2
  87. package/dist/config.js +58 -7
  88. package/dist/contracts/browserToolContracts.js +304 -3
  89. package/dist/contracts/electronToolContracts.js +73 -0
  90. package/dist/contracts/enhancedInputs.js +2 -2
  91. package/dist/contracts/providerSelection.js +28 -0
  92. package/dist/contracts/sessionLifecycleInputs.js +2 -0
  93. package/dist/contracts/toolContracts.js +24 -20
  94. package/dist/contracts/toolOutputSchemas.js +117 -22
  95. package/dist/doctorRuntime.js +6 -0
  96. package/dist/domain/analysisProfile.js +78 -0
  97. package/dist/domain/analysisSnapshot.js +92 -46
  98. package/dist/domain/artifactGraph.js +1 -0
  99. package/dist/domain/binaryTarget.js +4 -29
  100. package/dist/domain/browserCompleteness.js +101 -0
  101. package/dist/domain/browserObservation.js +240 -14
  102. package/dist/domain/browserSession.js +63 -0
  103. package/dist/domain/electronObservation.js +162 -0
  104. package/dist/domain/errors.js +81 -6
  105. package/dist/domain/evidence.js +44 -5
  106. package/dist/domain/evidenceBundle.js +9 -7
  107. package/dist/domain/functionComparison.js +9 -3
  108. package/dist/domain/functionComparisonNormalization.js +37 -0
  109. package/dist/domain/functionDossierEvidence.js +4 -1
  110. package/dist/domain/hopperValues.js +66 -9
  111. package/dist/domain/investigationWorkspace.js +6 -1
  112. package/dist/domain/javascriptApplicationEvidenceSchemas.js +361 -0
  113. package/dist/domain/javascriptApplicationGraph.js +281 -0
  114. package/dist/domain/javascriptApplicationGraphSchemas.js +132 -0
  115. package/dist/domain/javascriptAstFingerprint.js +143 -0
  116. package/dist/domain/javascriptStaticAnalysis.js +358 -0
  117. package/dist/domain/javascriptStaticAnalysisHelpers.js +264 -0
  118. package/dist/domain/javascriptStaticAnalysisState.js +17 -0
  119. package/dist/domain/javascriptStaticAnalysisTypes.js +1 -0
  120. package/dist/domain/jsonShape.js +125 -0
  121. package/dist/domain/referenceSourceGraph.js +3 -1
  122. package/dist/domain/webBundleAnalysis.js +195 -0
  123. package/dist/domain/webBundleAnalyzer.js +423 -0
  124. package/dist/domain/webCaptureDiff.js +184 -0
  125. package/dist/domain/webContentArtifact.js +47 -0
  126. package/dist/domain/webInventory.js +61 -0
  127. package/dist/domain/webMcpDiscovery.js +68 -0
  128. package/dist/domain/webScreenshot.js +114 -0
  129. package/dist/generatedPackageMetadata.js +2 -2
  130. package/dist/ghidra/GhidraAnalysisProfile.js +37 -0
  131. package/dist/ghidra/GhidraClient.js +400 -0
  132. package/dist/ghidra/GhidraClientTypes.js +1 -0
  133. package/dist/ghidra/GhidraDefaults.js +20 -0
  134. package/dist/ghidra/GhidraDiagnostics.js +43 -0
  135. package/dist/ghidra/GhidraDoctor.js +59 -0
  136. package/dist/ghidra/GhidraFunctionValues.js +264 -0
  137. package/dist/ghidra/GhidraInstallation.js +239 -0
  138. package/dist/ghidra/GhidraInventoryValues.js +217 -0
  139. package/dist/ghidra/GhidraLauncher.js +139 -0
  140. package/dist/ghidra/GhidraProvider.js +360 -0
  141. package/dist/ghidra/GhidraRequestQueue.js +98 -0
  142. package/dist/ghidra/GhidraResponseBuffer.js +31 -0
  143. package/dist/ghidra/GhidraResponseRouter.js +24 -0
  144. package/dist/ghidra/GhidraSessionError.js +39 -0
  145. package/dist/ghidra/GhidraSessionValues.js +62 -0
  146. package/dist/ghidra/GhidraSocketConnection.js +46 -0
  147. package/dist/ghidra/protocol.js +69 -0
  148. package/dist/hopper/BridgeLauncher.js +21 -57
  149. package/dist/hopper/HopperAnalysisProfile.js +106 -0
  150. package/dist/hopper/HopperClient.js +197 -165
  151. package/dist/hopper/HopperProvider.js +105 -8
  152. package/dist/hopper/HopperSessionValues.js +27 -0
  153. package/dist/hopper/HopperSocketConnection.js +31 -0
  154. package/dist/main.js +36 -31
  155. package/dist/native/NativeMacOSProvider.js +6 -3
  156. package/dist/native/NativeMachoInspection.js +21 -6
  157. package/dist/process/PendingOperations.js +59 -0
  158. package/dist/process/PrivateRuntimeRoot.js +33 -0
  159. package/dist/{application → process}/ProcessOwnership.js +40 -20
  160. package/dist/process/ProviderDeadline.js +95 -0
  161. package/dist/process/ProviderProcess.js +275 -0
  162. package/dist/server/createServer.js +8 -0
  163. package/dist/server/promptCompletion.js +9 -3
  164. package/dist/server/registerBrowserTools.js +61 -2
  165. package/dist/server/registerElectronTools.js +36 -0
  166. package/dist/server/registerEnhancedTools.js +9 -6
  167. package/dist/server/registerInvestigationTools.js +11 -12
  168. package/dist/server/registerOfficialTools.js +3 -0
  169. package/dist/server/registerSessionTools.js +7 -1
  170. package/package.json +12 -6
  171. package/scripts/rea.mjs +37 -3
  172. package/skills/rea-analysis/SKILL.md +19 -9
@@ -1,5 +1,6 @@
1
1
  import { parseBinaryTarget } from "../domain/binaryTarget.js";
2
- import { AnalysisCapabilityUnavailableError, EvidenceIntegrityError, EvidenceLimitError, AnalysisCancelledError, NoBinaryOpenError, } from "../domain/errors.js";
2
+ import { analysisProfilesEqual, } from "../domain/analysisProfile.js";
3
+ import { AnalysisCapabilityUnavailableError, EvidenceIntegrityError, EvidenceLimitError, AnalysisCancelledError, NoBinaryOpenError, ProviderAdapterError, } from "../domain/errors.js";
3
4
  import { err, ok } from "../domain/result.js";
4
5
  import { createEvidence } from "../domain/evidence.js";
5
6
  import { evidenceBundleForTarget } from "../domain/evidenceBundle.js";
@@ -7,27 +8,25 @@ import { EvidenceLedger } from "./EvidenceLedger.js";
7
8
  import { UnknownRegistryError } from "../domain/errors.js";
8
9
  import { enhancedToolNameSchema } from "../contracts/enhancedInputs.js";
9
10
  import { OFFICIAL_TOOL_CONTRACTS } from "../contracts/toolContracts.js";
10
- import { snapshotMatchesTarget } from "../domain/analysisSnapshot.js";
11
+ import { snapshotMatchesBinding, snapshotMatchesTarget, snapshotTarget, } from "../domain/analysisSnapshot.js";
11
12
  import { AnalysisSnapshotCache, isSnapshotCacheable, } from "./AnalysisSnapshotCache.js";
12
13
  import { UNKNOWN_REGISTRY_PROVIDER, unknownEvidenceLinks, unknownMutationEvidence, } from "./UnknownEvidence.js";
13
14
  import { parseInvestigationWorkspace, } from "../domain/investigationWorkspace.js";
15
+ import { SessionProviderRouter, } from "./SessionProviderRouter.js";
14
16
  const OFFICIAL_OPERATIONS = new Set(OFFICIAL_TOOL_CONTRACTS.map(({ name }) => name));
15
17
  /**
16
18
  * Owns the single active target shared by CLI and MCP adapters.
17
19
  *
18
- * Target transitions are serialized because each client dispatches Hopper API
19
- * work on its dedicated Python thread, and switching targets tears that bridge
20
- * down. A failed switch recreates the previous target instead of retaining a
21
- * client whose bridge was already shut down.
20
+ * Target transitions are serialized because switching targets tears down the
21
+ * active provider client. A failed switch recreates the previous target instead
22
+ * of retaining a client whose resources were already shut down.
22
23
  */
23
24
  export class BinarySession {
24
25
  provider;
25
26
  #active;
26
27
  #transition = Promise.resolve();
27
28
  #calls = new Set();
28
- #createClient;
29
- #providerIdentity;
30
- #capabilities;
29
+ #providerRouter;
31
30
  #evidence = new EvidenceLedger({
32
31
  maxRecords: 10_000,
33
32
  maxBytes: 64 * 1024 * 1024,
@@ -36,46 +35,55 @@ export class BinarySession {
36
35
  #investigationWorkspaces = new Map();
37
36
  #runtimeAvailability = new Map();
38
37
  #availabilityListeners = new Set();
39
- #snapshotsEnabled;
40
38
  #snapshotInvalidated = false;
41
39
  constructor(provider, options = {}) {
42
40
  this.provider = provider;
43
- this.#snapshotsEnabled = options.snapshotsEnabled ?? true;
44
- this.#createClient =
45
- typeof provider === "function"
41
+ this.#providerRouter =
42
+ provider instanceof SessionProviderRouter
46
43
  ? provider
47
- : (target) => provider.createClient(target);
48
- this.#providerIdentity =
49
- typeof provider === "function"
50
- ? { id: "unidentified", name: "Unidentified provider", version: null }
51
- : provider.identity();
52
- this.#capabilities =
53
- typeof provider === "function"
54
- ? undefined
55
- : new Map(provider
56
- .capabilities()
57
- .map((descriptor) => [descriptor.operation, descriptor]));
44
+ : SessionProviderRouter.legacy(provider, options);
58
45
  }
59
46
  /** Identify the provider producing evidence for this session. */
60
47
  providerIdentity(operation) {
48
+ const route = this.#currentRoute();
49
+ let selected = route.identity;
61
50
  if (operation !== undefined) {
62
- const exact = this.#capabilities?.get(operation)?.provider;
51
+ const exact = route.capabilities?.get(operation)?.provider;
63
52
  if (exact !== undefined)
64
- return structuredClone(exact);
65
- if (enhancedToolNameSchema.safeParse(operation).success) {
53
+ selected = exact;
54
+ else if (enhancedToolNameSchema.safeParse(operation).success) {
66
55
  const providers = new Map();
67
- for (const descriptor of this.#capabilities?.values() ?? [])
56
+ for (const descriptor of route.capabilities?.values() ?? [])
68
57
  if (descriptor.available &&
69
58
  OFFICIAL_OPERATIONS.has(descriptor.operation))
70
59
  providers.set(descriptor.provider.id, descriptor.provider);
71
60
  if (providers.size === 1) {
72
61
  const provider = providers.values().next().value;
73
62
  if (provider !== undefined)
74
- return structuredClone(provider);
63
+ selected = provider;
75
64
  }
76
65
  }
77
66
  }
78
- return structuredClone(this.#providerIdentity);
67
+ const profile = this.#active?.profile;
68
+ return structuredClone(profile !== null &&
69
+ profile !== undefined &&
70
+ profile.provider.id === selected.id
71
+ ? profile.provider
72
+ : selected);
73
+ }
74
+ /** Return the selected immutable profile, optionally scoped to an operation. */
75
+ analysisProfile(operation) {
76
+ const profile = this.#active?.profile;
77
+ if (profile === null || profile === undefined)
78
+ return undefined;
79
+ if (operation !== undefined &&
80
+ this.providerIdentity(operation).id !== profile.provider.id)
81
+ return undefined;
82
+ return structuredClone(profile);
83
+ }
84
+ /** Return opaque adapter metadata retained for legacy open_binary output. */
85
+ openCompatibility() {
86
+ return structuredClone(this.#active?.compatibility ?? {});
79
87
  }
80
88
  /** Observe runtime provider-health changes that affect discovery metadata. */
81
89
  onAvailabilityChanged(listener) {
@@ -104,20 +112,24 @@ export class BinarySession {
104
112
  }
105
113
  /** Export immutable cached calls and session evidence for the active target. */
106
114
  exportAnalysisSnapshot() {
107
- if (!this.#snapshotsEnabled)
108
- return err(new EvidenceIntegrityError("Analysis snapshots are unavailable with custom Hopper loader arguments"));
109
115
  if (this.#snapshotInvalidated)
110
116
  return err(new EvidenceIntegrityError("Analysis snapshots are unavailable after analysis metadata mutations"));
111
117
  const target = this.#active?.target;
112
- return this.#snapshot.export(target, target === undefined
118
+ const profile = this.#active?.profile ?? undefined;
119
+ if (target !== undefined && profile === undefined)
120
+ return err(new EvidenceIntegrityError("Analysis snapshots require a concrete provider analysis profile"));
121
+ return this.#snapshot.export(target, profile, target === undefined
113
122
  ? this.#evidence.export()
114
123
  : evidenceBundleForTarget(this.#evidence.export(), target.sha256));
115
124
  }
116
125
  /** Stage validated cached calls for an identical binary and merge its evidence. */
117
126
  importAnalysisSnapshot(snapshot) {
118
- if (!this.#snapshotsEnabled)
119
- return err(new EvidenceIntegrityError("Analysis snapshots are unavailable with custom Hopper loader arguments"));
120
- return this.#snapshot.import(snapshot, this.#active?.target, (bundle) => this.#evidence.import(bundle));
127
+ const active = this.#active;
128
+ if (active?.profile === null)
129
+ return err(new EvidenceIntegrityError("Analysis snapshot profile_mismatch: the active target has no concrete analysis profile"));
130
+ return this.#snapshot.import(snapshot, active === undefined
131
+ ? undefined
132
+ : { target: active.target, profile: active.profile }, (bundle) => this.#evidence.import(bundle));
121
133
  }
122
134
  /** Retain one validated immutable workspace revision for session resources. */
123
135
  retainInvestigationWorkspace(workspace) {
@@ -187,14 +199,31 @@ export class BinarySession {
187
199
  const parsed = await parseBinaryTarget(path, process.cwd(), process.arch, options.targetKind);
188
200
  if (!parsed.ok)
189
201
  return parsed;
202
+ const sameTarget = this.#active?.target.path === parsed.value.path &&
203
+ snapshotMatchesTarget(snapshotTarget(this.#active.target), parsed.value);
204
+ const resolvedRoute = sameTarget &&
205
+ options.providerId === undefined &&
206
+ this.#active !== undefined
207
+ ? ok(this.#active.route)
208
+ : await this.#providerRouter.resolve(parsed.value, options.providerId, options.signal);
209
+ if (!resolvedRoute.ok)
210
+ return resolvedRoute;
211
+ const route = resolvedRoute.value;
212
+ const { profile, compatibility } = route;
190
213
  if (options.snapshot !== undefined &&
191
- !snapshotMatchesTarget(options.snapshot.target, parsed.value))
192
- return err(new EvidenceIntegrityError("Analysis snapshot target does not match the requested binary"));
193
- if (this.#active === undefined && !this.#snapshot.matches(parsed.value))
194
- return err(new EvidenceIntegrityError("Analysis snapshot target does not match the requested binary"));
214
+ (profile === null ||
215
+ !snapshotMatchesBinding(options.snapshot, parsed.value, profile)))
216
+ return err(new EvidenceIntegrityError("Analysis snapshot profile_mismatch: target, provider, or analysis profile does not match the requested binary"));
217
+ if (this.#active === undefined &&
218
+ !this.#snapshot.matches(parsed.value, profile ?? undefined))
219
+ return err(new EvidenceIntegrityError("Analysis snapshot profile_mismatch: staged target, provider, or analysis profile does not match"));
195
220
  if (isAborted(options.signal))
196
221
  return err(new AnalysisCancelledError("open_binary"));
197
- if (this.#active?.target.path === parsed.value.path) {
222
+ const activeProfile = this.#active?.profile;
223
+ const sameProfile = activeProfile === null || activeProfile === undefined
224
+ ? profile === null
225
+ : profile !== null && analysisProfilesEqual(activeProfile, profile);
226
+ if (sameTarget && sameProfile) {
198
227
  if (options.snapshot !== undefined) {
199
228
  const imported = this.importAnalysisSnapshot(options.snapshot);
200
229
  if (!imported.ok)
@@ -206,7 +235,7 @@ export class BinarySession {
206
235
  const previous = this.#active;
207
236
  this.#active = undefined;
208
237
  await previous?.client.close();
209
- const client = this.#createClient(parsed.value);
238
+ const client = route.createClient(parsed.value);
210
239
  const started = await client.execute("health", {}, options);
211
240
  if (!started.ok) {
212
241
  await client.close();
@@ -218,9 +247,18 @@ export class BinarySession {
218
247
  await this.#restore(previous);
219
248
  return err(new AnalysisCancelledError("open_binary"));
220
249
  }
221
- this.#active = { target: parsed.value, client };
250
+ this.#active = {
251
+ target: parsed.value,
252
+ client,
253
+ profile,
254
+ compatibility: structuredClone(compatibility),
255
+ route,
256
+ };
222
257
  this.#clearRuntimeAvailability();
223
- this.#snapshot.select(parsed.value);
258
+ if (profile === null)
259
+ this.#snapshot.clear();
260
+ else
261
+ this.#snapshot.select(parsed.value, profile);
224
262
  if (options.snapshot !== undefined) {
225
263
  const imported = this.importAnalysisSnapshot(options.snapshot);
226
264
  if (!imported.ok) {
@@ -251,59 +289,74 @@ export class BinarySession {
251
289
  /** Describe the active binary session. */
252
290
  status() {
253
291
  const target = this.#active?.target;
292
+ const route = this.#currentRoute();
293
+ const configuredProvider = this.#providerRouter.configuredIdentity();
254
294
  const provider = {
255
- id: this.#providerIdentity.id,
256
- name: this.#providerIdentity.name,
257
- version: this.#providerIdentity.version,
295
+ id: configuredProvider.id,
296
+ name: configuredProvider.name,
297
+ version: configuredProvider.version,
258
298
  };
259
- const providers = new Map();
260
- if (this.#capabilities === undefined)
261
- providers.set(provider.id, provider);
262
- else
263
- for (const descriptor of this.#capabilities.values())
264
- providers.set(descriptor.provider.id, descriptor.provider);
265
- const providerList = [...providers.values()]
266
- .sort((left, right) => left.id.localeCompare(right.id))
299
+ const providerList = this.#providerRouter
300
+ .providerIdentities(route)
267
301
  .map(({ id, name, version }) => ({ id, name, version }));
268
- const capabilities = this.#capabilities === undefined
302
+ const capabilities = route.capabilities === undefined
269
303
  ? []
270
- : [...this.#capabilities.values()]
304
+ : [...route.capabilities.values()]
271
305
  .sort((left, right) => left.operation.localeCompare(right.operation))
272
306
  .map((descriptor) => ({
273
307
  ...descriptor,
274
308
  ...this.#runtimeAvailability.get(descriptor.operation),
275
309
  }))
276
- .map((descriptor) => ({
277
- operation: descriptor.operation,
278
- available: descriptor.available,
279
- reason: descriptor.reason,
280
- input_contract_version: descriptor.inputContractVersion,
281
- output_contract_version: descriptor.outputContractVersion,
282
- pagination: descriptor.pagination,
283
- exhaustive: descriptor.exhaustive,
284
- effects: {
285
- mutates_artifact: descriptor.effects.mutatesArtifact,
286
- launches_process: descriptor.effects.launchesProcess,
287
- may_show_ui: descriptor.effects.mayShowUi,
288
- may_access_network: descriptor.effects.mayAccessNetwork,
289
- may_write_filesystem: descriptor.effects.mayWriteFilesystem,
290
- changes_permissions: descriptor.effects.changesPermissions,
291
- requires_root: descriptor.effects.requiresRoot,
292
- },
293
- limits: {
294
- max_results: descriptor.limits.maxResults,
295
- max_payload_bytes: descriptor.limits.maxPayloadBytes,
296
- timeout_ms: descriptor.limits.timeoutMs,
310
+ .map((descriptor) => capabilityStatus(descriptor));
311
+ const analysisProviderBinding = route.binding === null
312
+ ? null
313
+ : {
314
+ provider: {
315
+ id: route.binding.identity.id,
316
+ name: route.binding.identity.name,
317
+ version: route.binding.identity.version,
297
318
  },
298
- limitations: [...descriptor.limitations],
299
- }));
319
+ selection_source: route.binding.selectionSource,
320
+ analysis_profile: structuredClone(route.binding.profile),
321
+ };
322
+ const analysisProviderCandidates = this.#providerRouter
323
+ .candidateStatuses(route)
324
+ .map((candidate) => ({
325
+ provider: {
326
+ id: candidate.provider.id,
327
+ name: candidate.provider.name,
328
+ version: candidate.provider.version,
329
+ },
330
+ availability: {
331
+ status: candidate.availability.status,
332
+ code: candidate.availability.code,
333
+ reason: candidate.availability.reason,
334
+ diagnostics: structuredClone(candidate.availability.diagnostics),
335
+ },
336
+ target_support: {
337
+ status: candidate.targetSupport.status,
338
+ code: candidate.targetSupport.code,
339
+ reason: candidate.targetSupport.reason,
340
+ diagnostics: structuredClone(candidate.targetSupport.diagnostics),
341
+ },
342
+ selected: candidate.selected,
343
+ capabilities: candidate.capabilities
344
+ .slice()
345
+ .sort((left, right) => left.operation.localeCompare(right.operation))
346
+ .map((descriptor) => capabilityStatus(descriptor)),
347
+ }));
348
+ const common = {
349
+ provider,
350
+ providers: providerList,
351
+ capabilities,
352
+ analysis_provider_binding: analysisProviderBinding,
353
+ analysis_provider_candidates: analysisProviderCandidates,
354
+ };
300
355
  return target === undefined
301
- ? { open: false, provider, providers: providerList, capabilities }
356
+ ? { open: false, ...common }
302
357
  : {
303
358
  open: true,
304
- provider,
305
- providers: providerList,
306
- capabilities,
359
+ ...common,
307
360
  path: target.path,
308
361
  sha256: target.sha256,
309
362
  format: target.format,
@@ -311,14 +364,14 @@ export class BinarySession {
311
364
  architecture: target.architecture ?? null,
312
365
  };
313
366
  }
314
- /** Return the immutable artifact identity captured before Hopper launched. */
367
+ /** Return the immutable artifact identity captured before its provider started. */
315
368
  activeTarget() {
316
369
  return this.#active === undefined
317
370
  ? undefined
318
371
  : structuredClone(this.#active.target);
319
372
  }
320
373
  /**
321
- * Invoke a Hopper tool against the active target.
374
+ * Invoke a provider operation against the active target.
322
375
  * Calls may overlap, but a pending target transition prevents new calls from
323
376
  * entering until the transition has settled.
324
377
  */
@@ -326,15 +379,26 @@ export class BinarySession {
326
379
  const transitioned = await this.#waitForTransition(name, options?.signal);
327
380
  if (!transitioned.ok)
328
381
  return transitioned;
329
- const capability = this.#capabilities?.get(name);
330
- if (this.#capabilities !== undefined && capability?.available !== true)
331
- return err(new AnalysisCapabilityUnavailableError(this.#providerIdentity.id, name, capability?.reason ?? "operation is not declared by this provider"));
332
382
  const active = this.#active;
333
383
  if (active === undefined)
334
384
  return err(new NoBinaryOpenError());
335
- const cacheable = isSnapshotCacheable(name, capability, arguments_);
385
+ const capability = active.route.capabilities?.get(name);
386
+ if (active.route.capabilities !== undefined &&
387
+ capability?.available !== true) {
388
+ const selectionError = this.#providerRouter.unboundOperationError(name, active.route);
389
+ if (selectionError !== undefined)
390
+ return err(selectionError);
391
+ return err(new AnalysisCapabilityUnavailableError(active.route.binding?.identity.id ?? active.route.identity.id, name, capability?.reason ?? "operation is not declared by this provider"));
392
+ }
393
+ const profile = active.profile !== null &&
394
+ (capability === undefined ||
395
+ capability.provider.id === active.profile.provider.id)
396
+ ? active.profile
397
+ : undefined;
398
+ const cacheable = profile !== undefined &&
399
+ isSnapshotCacheable(name, capability, arguments_);
336
400
  const cached = cacheable
337
- ? this.#snapshot.lookup(active.target, name, arguments_, capability.provider)
401
+ ? this.#snapshot.lookup(active.target, profile, name, arguments_)
338
402
  : undefined;
339
403
  if (cached !== undefined)
340
404
  return ok(cached);
@@ -342,14 +406,21 @@ export class BinarySession {
342
406
  this.#calls.add(call);
343
407
  try {
344
408
  const result = await call;
345
- this.#observeRuntimeAvailability(name, result);
346
- if (result.ok && cacheable)
347
- this.#snapshot.record(active.target, name, arguments_, result.value);
348
- else if (result.ok && capability?.effects.mutatesArtifact === true) {
409
+ const profiled = commitExecutionProfile(name, result, profile);
410
+ this.#observeRuntimeAvailability(name, profiled);
411
+ if (profiled.ok && cacheable)
412
+ this.#snapshot.record({
413
+ target: active.target,
414
+ profile,
415
+ operation: name,
416
+ parameters: arguments_,
417
+ execution: profiled.value,
418
+ });
419
+ else if (profiled.ok && capability?.effects.mutatesArtifact === true) {
349
420
  this.#snapshot.clear();
350
421
  this.#snapshotInvalidated = true;
351
422
  }
352
- return result;
423
+ return profiled;
353
424
  }
354
425
  finally {
355
426
  this.#calls.delete(call);
@@ -372,9 +443,10 @@ export class BinarySession {
372
443
  "HopperStartError",
373
444
  "HopperRemoteError",
374
445
  ].includes(result.error._tag)) {
375
- const providerId = this.#capabilities?.get(operation)?.provider.id;
446
+ const capabilities = this.#active?.route.capabilities;
447
+ const providerId = capabilities?.get(operation)?.provider.id;
376
448
  let changed = false;
377
- for (const descriptor of this.#capabilities?.values() ?? [])
449
+ for (const descriptor of capabilities?.values() ?? [])
378
450
  if (providerId !== undefined && descriptor.provider.id === providerId)
379
451
  changed =
380
452
  this.#setRuntimeAvailability(descriptor.operation, false, "Provider became unavailable during this session.") || changed;
@@ -401,8 +473,20 @@ export class BinarySession {
401
473
  this.#emitAvailabilityChanged();
402
474
  }
403
475
  #emitAvailabilityChanged() {
404
- for (const listener of this.#availabilityListeners)
405
- listener();
476
+ for (const listener of this.#availabilityListeners) {
477
+ try {
478
+ const notification = listener();
479
+ if (notification !== undefined)
480
+ void notification.catch(() => undefined);
481
+ }
482
+ catch {
483
+ // External observers are best-effort notifications. Contain only the
484
+ // callback failure so state transitions and other listeners continue.
485
+ }
486
+ }
487
+ }
488
+ #currentRoute() {
489
+ return this.#active?.route ?? this.#providerRouter.initialRoute();
406
490
  }
407
491
  #serialize(operation) {
408
492
  const result = this.#transition.then(operation, operation);
@@ -415,10 +499,16 @@ export class BinarySession {
415
499
  async #restore(previous) {
416
500
  if (previous === undefined)
417
501
  return;
418
- const client = this.#createClient(previous.target);
502
+ const client = previous.route.createClient(previous.target);
419
503
  const started = await client.execute("health", {});
420
504
  if (started.ok)
421
- this.#active = { target: previous.target, client };
505
+ this.#active = {
506
+ target: previous.target,
507
+ client,
508
+ profile: previous.profile,
509
+ compatibility: previous.compatibility,
510
+ route: previous.route,
511
+ };
422
512
  else
423
513
  await client.close();
424
514
  }
@@ -449,3 +539,40 @@ export class BinarySession {
449
539
  }
450
540
  }
451
541
  const isAborted = (signal) => signal?.aborted === true;
542
+ const commitExecutionProfile = (operation, result, profile) => {
543
+ if (!result.ok || profile === undefined)
544
+ return result;
545
+ const provider = result.value.provider;
546
+ if (provider.id !== profile.provider.id ||
547
+ provider.name !== profile.provider.name ||
548
+ provider.version !== profile.provider.version)
549
+ return err(new ProviderAdapterError(profile.provider.id, `${operation}:profile`));
550
+ return ok({
551
+ ...result.value,
552
+ analysisProfile: structuredClone(profile),
553
+ });
554
+ };
555
+ const capabilityStatus = (descriptor) => ({
556
+ operation: descriptor.operation,
557
+ available: descriptor.available,
558
+ reason: descriptor.reason,
559
+ input_contract_version: descriptor.inputContractVersion,
560
+ output_contract_version: descriptor.outputContractVersion,
561
+ pagination: descriptor.pagination,
562
+ exhaustive: descriptor.exhaustive,
563
+ effects: {
564
+ mutates_artifact: descriptor.effects.mutatesArtifact,
565
+ launches_process: descriptor.effects.launchesProcess,
566
+ may_show_ui: descriptor.effects.mayShowUi,
567
+ may_access_network: descriptor.effects.mayAccessNetwork,
568
+ may_write_filesystem: descriptor.effects.mayWriteFilesystem,
569
+ changes_permissions: descriptor.effects.changesPermissions,
570
+ requires_root: descriptor.effects.requiresRoot,
571
+ },
572
+ limits: {
573
+ max_results: descriptor.limits.maxResults,
574
+ max_payload_bytes: descriptor.limits.maxPayloadBytes,
575
+ timeout_ms: descriptor.limits.timeoutMs,
576
+ },
577
+ limitations: [...descriptor.limitations],
578
+ });
@@ -2,15 +2,13 @@ import { createEvidence, } from "../domain/evidence.js";
2
2
  import { jsonValueSchema } from "../domain/jsonValue.js";
3
3
  /** Create Evidence v2 for a policy-scoped external browser observation. */
4
4
  export const createBrowserEvidence = (operation, input, result, provider) => createEvidence(undefined, provider, {
5
- predicateType: operation === "list_browser_targets"
6
- ? "rea.browser-target-list/v1"
7
- : "rea.web-page-inspection/v1",
5
+ predicateType: browserPredicate(operation),
8
6
  operation,
9
7
  parameters: browserParameters(input),
10
8
  result: jsonValueSchema.parse(result),
11
9
  confidence: "observed",
12
10
  authority: "external-service",
13
- environment: "target" in result
11
+ environment: "browser" in result
14
12
  ? {
15
13
  id: `${result.browser.product}@${result.browser.revision}`,
16
14
  platform: process.platform,
@@ -20,16 +18,90 @@ export const createBrowserEvidence = (operation, input, result, provider) => cre
20
18
  : null,
21
19
  limitations: result.limitations,
22
20
  });
23
- const browserParameters = (input) => ({
24
- cdp_endpoint: input.cdp_endpoint,
25
- allowed_origins: input.allowed_origins,
26
- ...("target_id" in input
27
- ? {
21
+ const browserParameters = (input) => {
22
+ if (!("cdp_endpoint" in input)) {
23
+ if ("max_changes" in input)
24
+ return {
25
+ before_target_id: input.before.inspection.target.target_id,
26
+ before_capture_ended_at: input.before.inspection.capture_window.ended_at,
27
+ after_target_id: input.after.inspection.target.target_id,
28
+ after_capture_ended_at: input.after.inspection.capture_window.ended_at,
29
+ max_changes: input.max_changes,
30
+ };
31
+ return {
32
+ before_artifact_uri: input.before.uri,
33
+ after_artifact_uri: input.after.uri,
34
+ channel_threshold: input.channel_threshold,
35
+ maximum_pixels: input.maximum_pixels,
36
+ };
37
+ }
38
+ const scope = {
39
+ cdp_endpoint: input.cdp_endpoint,
40
+ allowed_origins: input.allowed_origins,
41
+ };
42
+ if (!("target_id" in input))
43
+ return { ...scope, offset: input.offset, limit: input.limit };
44
+ if ("screenshot_approved" in input)
45
+ return {
46
+ ...scope,
47
+ target_id: input.target_id,
48
+ maximum_image_bytes: input.maximum_image_bytes,
49
+ };
50
+ if ("max_tools" in input)
51
+ return {
52
+ ...scope,
28
53
  target_id: input.target_id,
29
54
  observation_ms: input.observation_ms,
30
- include_script_sources: input.include_script_sources,
31
- include_storage_keys: input.include_storage_keys,
32
- limits: input.limits,
33
- }
34
- : { offset: input.offset, limit: input.limit }),
35
- });
55
+ max_tools: input.max_tools,
56
+ max_schema_bytes: input.max_schema_bytes,
57
+ max_schema_nodes: input.max_schema_nodes,
58
+ max_schema_depth: input.max_schema_depth,
59
+ };
60
+ if (!("include_accessibility_text" in input))
61
+ return {
62
+ ...scope,
63
+ target_id: input.target_id,
64
+ observation_ms: input.observation_ms,
65
+ max_timeline_events: input.max_timeline_events,
66
+ };
67
+ return {
68
+ ...scope,
69
+ target_id: input.target_id,
70
+ observation_ms: input.observation_ms,
71
+ include_accessibility_text: input.include_accessibility_text,
72
+ include_console_text: input.include_console_text,
73
+ include_json_body_shapes: input.include_json_body_shapes,
74
+ include_websocket_shapes: input.include_websocket_shapes,
75
+ include_script_sources: input.include_script_sources,
76
+ include_storage_keys: input.include_storage_keys,
77
+ limits: input.limits,
78
+ ...(input.include_script_sources && "source_capture_approved" in input
79
+ ? {
80
+ source_capture_approved: input.source_capture_approved,
81
+ fetch_source_maps: input.fetch_source_maps,
82
+ source_map_fetch_approved: input.source_map_fetch_approved,
83
+ analysis_limits: input.analysis_limits,
84
+ }
85
+ : {}),
86
+ };
87
+ };
88
+ const browserPredicate = (operation) => {
89
+ switch (operation) {
90
+ case "list_browser_targets":
91
+ return "rea.browser-target-list/v1";
92
+ case "inspect_web_page":
93
+ return "rea.web-page-inspection/v2";
94
+ case "analyze_web_bundle":
95
+ return "rea.web-bundle-analysis/v1";
96
+ case "observe_web_session":
97
+ return "rea.web-observation-session/v1";
98
+ case "discover_webmcp_tools":
99
+ return "rea.webmcp-discovery/v1";
100
+ case "compare_web_captures":
101
+ return "rea.web-capture-diff/v1";
102
+ case "capture_web_screenshot":
103
+ return "rea.web-screenshot/v1";
104
+ case "compare_web_screenshots":
105
+ return "rea.web-screenshot-diff/v1";
106
+ }
107
+ };