rea-agents 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/README.md +139 -21
  2. package/bridge/hopper_bridge.py +156 -14
  3. package/dist/application/AnalysisProvider.js +12 -1
  4. package/dist/application/ArtifactExtraction.js +166 -0
  5. package/dist/application/ArtifactGraphConstruction.js +257 -0
  6. package/dist/application/ArtifactInventory.js +253 -0
  7. package/dist/application/BinarySession.js +174 -14
  8. package/dist/application/CompositeProvider.js +73 -0
  9. package/dist/application/DirectAnalysis.js +44 -3
  10. package/dist/application/Doctor.js +35 -6
  11. package/dist/application/EnhancedTools.js +10 -7
  12. package/dist/application/EvidenceBundleCommands.js +51 -0
  13. package/dist/application/EvidenceBundleFiles.js +127 -0
  14. package/dist/application/EvidenceLedger.js +225 -18
  15. package/dist/application/FilesystemSnapshot.js +124 -0
  16. package/dist/application/LinuxHopper.js +186 -0
  17. package/dist/application/LoopbackReplay.js +195 -37
  18. package/dist/application/ProcessHarness.js +200 -191
  19. package/dist/application/ProcessNormalization.js +44 -0
  20. package/dist/application/ProcessOwnership.js +105 -0
  21. package/dist/application/ProcessSampling.js +284 -0
  22. package/dist/application/RealHopperAssertions.js +116 -0
  23. package/dist/application/ReferenceSourceImport.js +182 -0
  24. package/dist/application/ReferenceSourceImportEntries.js +122 -0
  25. package/dist/application/ReferenceSourceImportPolicy.js +73 -0
  26. package/dist/application/ReferenceSourceImportTypes.js +18 -0
  27. package/dist/application/ReferenceSourceVcsAdapter.js +34 -0
  28. package/dist/application/Setup.js +192 -41
  29. package/dist/application/Uninstall.js +130 -0
  30. package/dist/application/runtime.js +8 -1
  31. package/dist/artifacts/ArtifactPaths.js +51 -0
  32. package/dist/artifacts/ArtifactProvider.js +130 -0
  33. package/dist/artifacts/ArtifactReader.js +9 -0
  34. package/dist/artifacts/AsarArtifactReader.js +62 -0
  35. package/dist/artifacts/DirectoryArtifactReader.js +107 -0
  36. package/dist/artifacts/MachOSliceArtifactReader.js +66 -0
  37. package/dist/artifacts/SafeOutputTree.js +199 -0
  38. package/dist/artifacts/StreamBytes.js +10 -0
  39. package/dist/artifacts/ZipArtifactReader.js +109 -0
  40. package/dist/cli.js +229 -21
  41. package/dist/cliEvidenceCommands.js +68 -0
  42. package/dist/cliLogging.js +21 -0
  43. package/dist/config.js +35 -1
  44. package/dist/contracts/artifactComparisonExample.js +95 -0
  45. package/dist/contracts/artifactToolContracts.js +84 -0
  46. package/dist/contracts/enhancedInputs.js +4 -0
  47. package/dist/contracts/functionComparisonExample.js +57 -0
  48. package/dist/contracts/investigationExamples.js +118 -0
  49. package/dist/contracts/nativeToolContracts.js +52 -0
  50. package/dist/contracts/processCaptureExample.js +25 -0
  51. package/dist/contracts/toolContractExamples.js +69 -0
  52. package/dist/contracts/toolContracts.js +99 -36
  53. package/dist/contracts/toolOutputSchemas.js +156 -82
  54. package/dist/contracts/unknownContractExamples.js +33 -0
  55. package/dist/domain/artifactComparison.js +273 -0
  56. package/dist/domain/artifactGraph.js +194 -0
  57. package/dist/domain/artifactInventoryEvidence.js +150 -0
  58. package/dist/domain/binaryTarget.js +55 -0
  59. package/dist/domain/bundleComparison.js +266 -0
  60. package/dist/domain/callPath.js +346 -0
  61. package/dist/domain/changedBehavior.js +294 -0
  62. package/dist/domain/errors.js +199 -5
  63. package/dist/domain/evidence.js +41 -10
  64. package/dist/domain/evidenceBundle.js +187 -6
  65. package/dist/domain/functionComparison.js +201 -0
  66. package/dist/domain/functionComparisonNormalization.js +112 -0
  67. package/dist/domain/functionComparisonResults.js +54 -0
  68. package/dist/domain/functionComparisonSchemas.js +82 -0
  69. package/dist/domain/functionDossierEvidence.js +171 -0
  70. package/dist/domain/hopperValues.js +35 -8
  71. package/dist/domain/nativeInspection.js +142 -0
  72. package/dist/domain/processCapture.js +152 -54
  73. package/dist/domain/processComparison.js +106 -0
  74. package/dist/domain/reconstructionUnknowns.js +90 -0
  75. package/dist/domain/reconstructionVerification.js +285 -0
  76. package/dist/domain/reconstructionVerificationSchemas.js +126 -0
  77. package/dist/domain/referenceSourceClassification.js +496 -0
  78. package/dist/domain/referenceSourceGraph.js +376 -0
  79. package/dist/domain/referenceSourceImportParsing.js +235 -0
  80. package/dist/domain/referenceSourcePolicy.js +1 -0
  81. package/dist/domain/residualUnknown.js +239 -0
  82. package/dist/domain/staticRuntimeCorrelation.js +375 -0
  83. package/dist/hopper/BridgeLauncher.js +39 -3
  84. package/dist/hopper/HopperClient.js +14 -5
  85. package/dist/hopper/HopperProvider.js +57 -22
  86. package/dist/hopper/protocol.js +13 -2
  87. package/dist/identity.js +1 -0
  88. package/dist/main.js +5 -1
  89. package/dist/native/CommandRunner.js +156 -0
  90. package/dist/native/NativeMacOSProvider.js +306 -0
  91. package/dist/native/NativeMachoInspection.js +135 -0
  92. package/dist/native/parsers/codesign.js +55 -0
  93. package/dist/native/parsers/demangle.js +26 -0
  94. package/dist/native/parsers/dyldInfo.js +25 -0
  95. package/dist/native/parsers/lipo.js +67 -0
  96. package/dist/native/parsers/otool.js +193 -0
  97. package/dist/native/parsers/plist.js +23 -0
  98. package/dist/reference/ReferenceSourceReader.js +73 -0
  99. package/dist/reference/ReferenceSourceReaderEntries.js +206 -0
  100. package/dist/reference/ReferenceSourceReaderErrors.js +19 -0
  101. package/dist/reference/ReferenceSourceReaderFile.js +119 -0
  102. package/dist/reference/ReferenceSourceReaderPaths.js +23 -0
  103. package/dist/reference/ReferenceSourceReaderTypes.js +2 -0
  104. package/dist/reference/ReferenceSourceReaderValidate.js +71 -0
  105. package/dist/server/createServer.js +31 -5
  106. package/dist/server/recordDerivedEvidence.js +10 -0
  107. package/dist/server/registerArtifactComparisonTool.js +62 -0
  108. package/dist/server/registerArtifactTools.js +6 -0
  109. package/dist/server/registerBundleComparisonTool.js +47 -0
  110. package/dist/server/registerEnhancedTools.js +64 -12
  111. package/dist/server/registerEvidenceTools.js +36 -0
  112. package/dist/server/registerFunctionComparisonTool.js +68 -0
  113. package/dist/server/registerInvestigationTools.js +224 -0
  114. package/dist/server/registerNativeTools.js +6 -0
  115. package/dist/server/registerOfficialTools.js +47 -14
  116. package/dist/server/registerProcessComparisonTool.js +106 -0
  117. package/dist/server/registerSessionTools.js +179 -70
  118. package/dist/server/sessionEvidence.js +28 -0
  119. package/dist/server/sessionToolPolicies.js +64 -0
  120. package/dist/server/toolRegistrationOptions.js +7 -0
  121. package/dist/server/toolResult.js +8 -5
  122. package/install.sh +198 -0
  123. package/package.json +18 -1
  124. package/scripts/rea.mjs +5 -1
  125. package/skills/rea-analysis/SKILL.md +77 -2
@@ -1,7 +1,17 @@
1
1
  import { parseBinaryTarget } from "../domain/binaryTarget.js";
2
- import { HopperCancelledError, NoBinaryOpenError, } from "../domain/errors.js";
2
+ import { AnalysisCapabilityUnavailableError, EvidenceIntegrityError, EvidenceLimitError, AnalysisCancelledError, NoBinaryOpenError, } from "../domain/errors.js";
3
3
  import { err, ok } from "../domain/result.js";
4
+ import { createEvidence } from "../domain/evidence.js";
4
5
  import { EvidenceLedger } from "./EvidenceLedger.js";
6
+ import { UnknownRegistryError } from "../domain/errors.js";
7
+ import { enhancedToolNameSchema } from "../contracts/enhancedInputs.js";
8
+ import { OFFICIAL_TOOL_CONTRACTS } from "../contracts/toolContracts.js";
9
+ const REGISTRY_PROVIDER = {
10
+ id: "rea-unknown-registry",
11
+ name: "REA residual unknown registry",
12
+ version: "1",
13
+ };
14
+ const OFFICIAL_OPERATIONS = new Set(OFFICIAL_TOOL_CONTRACTS.map(({ name }) => name));
5
15
  /**
6
16
  * Owns the single active target shared by CLI and MCP adapters.
7
17
  *
@@ -17,7 +27,11 @@ export class BinarySession {
17
27
  #calls = new Set();
18
28
  #createClient;
19
29
  #providerIdentity;
20
- #evidence = new EvidenceLedger({ maxRecords: 10_000 });
30
+ #capabilities;
31
+ #evidence = new EvidenceLedger({
32
+ maxRecords: 10_000,
33
+ maxBytes: 64 * 1024 * 1024,
34
+ });
21
35
  constructor(provider) {
22
36
  this.provider = provider;
23
37
  this.#createClient =
@@ -28,14 +42,45 @@ export class BinarySession {
28
42
  typeof provider === "function"
29
43
  ? { id: "unidentified", name: "Unidentified provider", version: null }
30
44
  : provider.identity();
45
+ this.#capabilities =
46
+ typeof provider === "function"
47
+ ? undefined
48
+ : new Map(provider
49
+ .capabilities()
50
+ .map((descriptor) => [descriptor.operation, descriptor]));
31
51
  }
32
52
  /** Identify the provider producing evidence for this session. */
33
- providerIdentity() {
53
+ providerIdentity(operation) {
54
+ if (operation !== undefined) {
55
+ const exact = this.#capabilities?.get(operation)?.provider;
56
+ if (exact !== undefined)
57
+ return exact;
58
+ if (enhancedToolNameSchema.safeParse(operation).success) {
59
+ const providers = new Map();
60
+ for (const descriptor of this.#capabilities?.values() ?? [])
61
+ if (descriptor.available &&
62
+ OFFICIAL_OPERATIONS.has(descriptor.operation))
63
+ providers.set(descriptor.provider.id, descriptor.provider);
64
+ if (providers.size === 1) {
65
+ const provider = providers.values().next().value;
66
+ if (provider !== undefined)
67
+ return provider;
68
+ }
69
+ }
70
+ }
34
71
  return this.#providerIdentity;
35
72
  }
36
73
  /** Add one successful public observation to the session ledger. */
37
74
  recordEvidence(evidence) {
38
- this.#evidence.record(evidence);
75
+ return this.#evidence.record(evidence);
76
+ }
77
+ /** Check that comparison input Evidence is already session-owned. */
78
+ hasEvidence(evidenceId) {
79
+ return this.#evidence.has(evidenceId);
80
+ }
81
+ /** Read one detached session-owned Evidence record by semantic ID. */
82
+ evidenceById(evidenceId) {
83
+ return this.#evidence.get(evidenceId);
39
84
  }
40
85
  /** Return a deterministic snapshot without clearing session evidence. */
41
86
  exportEvidenceBundle() {
@@ -45,6 +90,42 @@ export class BinarySession {
45
90
  importEvidenceBundle(bundle) {
46
91
  return this.#evidence.import(bundle);
47
92
  }
93
+ /** Create an approved residual unknown and immutable mutation evidence. */
94
+ recordUnknown(input) {
95
+ const evidence = unknownMutationEvidence(this.#active?.target, input);
96
+ return this.#evidence.recordUnknown(input, evidence);
97
+ }
98
+ /** Atomically record derived Evidence and its approved residual unknown. */
99
+ recordEvidenceWithUnknown(evidence, input) {
100
+ return this.#evidence.recordWithUnknown(evidence, input, unknownMutationEvidence(undefined, input));
101
+ }
102
+ /** Update one unknown using compare-and-swap revision semantics. */
103
+ updateUnknown(input) {
104
+ const evidence = createEvidence(this.#active?.target, REGISTRY_PROVIDER, {
105
+ predicateType: "rea.residual-unknown-mutation/v1",
106
+ operation: "update_unknown",
107
+ parameters: {
108
+ unknown_id: input.unknown_id,
109
+ expected_revision: input.expected_revision,
110
+ },
111
+ result: { action: "update", status: input.status },
112
+ confidence: "derived",
113
+ authority: "analyst-inference",
114
+ evidenceLinks: unknownEvidenceLinks(input),
115
+ limitations: [
116
+ "Registry mutation evidence records analyst intent, not proof of the answer.",
117
+ ],
118
+ });
119
+ return this.#evidence.updateUnknown(input, evidence);
120
+ }
121
+ /** Query stable current residual-unknown heads. */
122
+ listUnknowns(filters = {}) {
123
+ return this.#evidence.listUnknowns(filters);
124
+ }
125
+ /** Check whether current head is a bundle-valid resolved state. */
126
+ verifyUnknownResolution(unknownId) {
127
+ return this.#evidence.verifyUnknownResolution(unknownId);
128
+ }
48
129
  /**
49
130
  * Open or switch targets after draining calls against the current target.
50
131
  * Returns the switch failure even if best-effort restoration also fails.
@@ -52,12 +133,12 @@ export class BinarySession {
52
133
  open(path, options = {}) {
53
134
  return this.#serialize(async () => {
54
135
  if (isAborted(options.signal))
55
- return err(new HopperCancelledError());
136
+ return err(new AnalysisCancelledError("open_binary"));
56
137
  const parsed = await parseBinaryTarget(path, process.cwd(), process.arch, options.targetKind);
57
138
  if (!parsed.ok)
58
139
  return parsed;
59
140
  if (isAborted(options.signal))
60
- return err(new HopperCancelledError());
141
+ return err(new AnalysisCancelledError("open_binary"));
61
142
  if (this.#active?.target.path === parsed.value.path)
62
143
  return ok(parsed.value);
63
144
  await this.#drainCalls();
@@ -74,7 +155,7 @@ export class BinarySession {
74
155
  if (isAborted(options.signal)) {
75
156
  await client.close();
76
157
  await this.#restore(previous);
77
- return err(new HopperCancelledError());
158
+ return err(new AnalysisCancelledError("open_binary"));
78
159
  }
79
160
  this.#active = { target: parsed.value, client };
80
161
  return ok(parsed.value);
@@ -94,10 +175,55 @@ export class BinarySession {
94
175
  /** Describe the active binary session. */
95
176
  status() {
96
177
  const target = this.#active?.target;
178
+ const provider = {
179
+ id: this.#providerIdentity.id,
180
+ name: this.#providerIdentity.name,
181
+ version: this.#providerIdentity.version,
182
+ };
183
+ const providers = new Map();
184
+ if (this.#capabilities === undefined)
185
+ providers.set(provider.id, provider);
186
+ else
187
+ for (const descriptor of this.#capabilities.values())
188
+ providers.set(descriptor.provider.id, descriptor.provider);
189
+ const providerList = [...providers.values()]
190
+ .sort((left, right) => left.id.localeCompare(right.id))
191
+ .map(({ id, name, version }) => ({ id, name, version }));
192
+ const capabilities = this.#capabilities === undefined
193
+ ? []
194
+ : [...this.#capabilities.values()]
195
+ .sort((left, right) => left.operation.localeCompare(right.operation))
196
+ .map((descriptor) => ({
197
+ operation: descriptor.operation,
198
+ available: descriptor.available,
199
+ reason: descriptor.reason,
200
+ input_contract_version: descriptor.inputContractVersion,
201
+ output_contract_version: descriptor.outputContractVersion,
202
+ pagination: descriptor.pagination,
203
+ exhaustive: descriptor.exhaustive,
204
+ effects: {
205
+ mutates_artifact: descriptor.effects.mutatesArtifact,
206
+ launches_process: descriptor.effects.launchesProcess,
207
+ may_show_ui: descriptor.effects.mayShowUi,
208
+ may_access_network: descriptor.effects.mayAccessNetwork,
209
+ may_write_filesystem: descriptor.effects.mayWriteFilesystem,
210
+ changes_permissions: descriptor.effects.changesPermissions,
211
+ requires_root: descriptor.effects.requiresRoot,
212
+ },
213
+ limits: {
214
+ max_results: descriptor.limits.maxResults,
215
+ max_payload_bytes: descriptor.limits.maxPayloadBytes,
216
+ timeout_ms: descriptor.limits.timeoutMs,
217
+ },
218
+ limitations: [...descriptor.limitations],
219
+ }));
97
220
  return target === undefined
98
- ? { open: false }
221
+ ? { open: false, provider, providers: providerList, capabilities }
99
222
  : {
100
223
  open: true,
224
+ provider,
225
+ providers: providerList,
226
+ capabilities,
101
227
  path: target.path,
102
228
  sha256: target.sha256,
103
229
  format: target.format,
@@ -115,9 +241,12 @@ export class BinarySession {
115
241
  * entering until the transition has settled.
116
242
  */
117
243
  async execute(name, arguments_, options) {
118
- const transitioned = await this.#waitForTransition(options?.signal);
244
+ const transitioned = await this.#waitForTransition(name, options?.signal);
119
245
  if (!transitioned.ok)
120
246
  return transitioned;
247
+ const capability = this.#capabilities?.get(name);
248
+ if (this.#capabilities !== undefined && capability?.available !== true)
249
+ return err(new AnalysisCapabilityUnavailableError(this.#providerIdentity.id, name, capability?.reason ?? "operation is not declared by this provider"));
121
250
  const active = this.#active;
122
251
  if (active === undefined)
123
252
  return err(new NoBinaryOpenError());
@@ -148,26 +277,57 @@ export class BinarySession {
148
277
  else
149
278
  await client.close();
150
279
  }
151
- async #waitForTransition(signal) {
280
+ async #waitForTransition(operation, signal) {
152
281
  if (signal?.aborted === true)
153
- return err(new HopperCancelledError());
282
+ return err(new AnalysisCancelledError(operation));
154
283
  if (signal === undefined) {
155
284
  await this.#transition;
156
285
  return ok(undefined);
157
286
  }
158
287
  return new Promise((resolve) => {
159
288
  const onAbort = () => {
160
- resolve(err(new HopperCancelledError()));
289
+ resolve(err(new AnalysisCancelledError(operation)));
161
290
  };
162
291
  signal.addEventListener("abort", onAbort, { once: true });
163
292
  this.#transition.then(() => {
164
293
  signal.removeEventListener("abort", onAbort);
165
- resolve(signal.aborted ? err(new HopperCancelledError()) : ok(undefined));
294
+ resolve(signal.aborted
295
+ ? err(new AnalysisCancelledError(operation))
296
+ : ok(undefined));
166
297
  }, () => {
167
298
  signal.removeEventListener("abort", onAbort);
168
- resolve(signal.aborted ? err(new HopperCancelledError()) : ok(undefined));
299
+ resolve(signal.aborted
300
+ ? err(new AnalysisCancelledError(operation))
301
+ : ok(undefined));
169
302
  });
170
303
  });
171
304
  }
172
305
  }
306
+ const unknownMutationEvidence = (target, input) => createEvidence(target, REGISTRY_PROVIDER, {
307
+ predicateType: "rea.residual-unknown-mutation/v1",
308
+ operation: "record_unknown",
309
+ parameters: {
310
+ domain: input.domain,
311
+ severity: input.severity,
312
+ },
313
+ result: {
314
+ action: "record",
315
+ question: input.question,
316
+ required_authority: input.required_authority,
317
+ required_confidence: input.required_confidence,
318
+ },
319
+ confidence: "derived",
320
+ authority: "analyst-inference",
321
+ evidenceLinks: unknownEvidenceLinks(input),
322
+ limitations: [
323
+ "Registry mutation evidence records analyst intent, not proof of the answer.",
324
+ ],
325
+ });
326
+ const unknownEvidenceLinks = (input) => [
327
+ ...input.supporting_evidence_ids,
328
+ ...input.contradicting_evidence_ids,
329
+ ...("resolution" in input && input.resolution !== null
330
+ ? input.resolution.evidence_ids
331
+ : []),
332
+ ].filter((id, index, values) => values.indexOf(id) === index);
173
333
  const isAborted = (signal) => signal?.aborted === true;
@@ -0,0 +1,73 @@
1
+ import { ProviderSelectionError } from "../domain/errors.js";
2
+ import { err, ok } from "../domain/result.js";
3
+ const compositeIdentity = (providers) => ({
4
+ id: `composite:${providers
5
+ .map((provider) => provider.identity().id)
6
+ .sort()
7
+ .join("+")}`,
8
+ name: "REA composite analysis provider",
9
+ version: null,
10
+ });
11
+ /** Deterministically route disjoint operations without eagerly starting children. */
12
+ export class CompositeProvider {
13
+ providers;
14
+ #identity;
15
+ #capabilities;
16
+ #providerByOperation;
17
+ constructor(providers) {
18
+ this.providers = providers;
19
+ if (providers.length === 0)
20
+ throw new RangeError("CompositeProvider requires at least one provider");
21
+ this.#identity = Object.freeze(compositeIdentity(providers));
22
+ const routes = new Map();
23
+ const capabilities = [];
24
+ for (const provider of providers) {
25
+ for (const descriptor of provider.capabilities()) {
26
+ if (routes.has(descriptor.operation))
27
+ throw new TypeError(`Multiple providers declare operation ${descriptor.operation}`);
28
+ routes.set(descriptor.operation, provider);
29
+ capabilities.push(descriptor);
30
+ }
31
+ }
32
+ this.#providerByOperation = routes;
33
+ this.#capabilities = Object.freeze(capabilities.sort((left, right) => left.operation.localeCompare(right.operation) ||
34
+ left.provider.id.localeCompare(right.provider.id)));
35
+ }
36
+ identity() {
37
+ return this.#identity;
38
+ }
39
+ capabilities() {
40
+ return this.#capabilities;
41
+ }
42
+ createClient(target) {
43
+ const clients = new Map();
44
+ const clientFor = (provider) => {
45
+ const existing = clients.get(provider);
46
+ if (existing !== undefined)
47
+ return existing;
48
+ const created = provider.createClient(target);
49
+ clients.set(provider, created);
50
+ return created;
51
+ };
52
+ return {
53
+ execute: (operation, parameters, options) => {
54
+ if (operation === "health")
55
+ return Promise.resolve(ok({
56
+ result: null,
57
+ rawResult: null,
58
+ provider: this.#identity,
59
+ limitations: [],
60
+ locations: [],
61
+ subject: null,
62
+ }));
63
+ const provider = this.#providerByOperation.get(operation);
64
+ return provider === undefined
65
+ ? Promise.resolve(err(new ProviderSelectionError(operation)))
66
+ : clientFor(provider).execute(operation, parameters, options);
67
+ },
68
+ close: async () => {
69
+ await Promise.allSettled([...clients.values()].map(async (client) => client.close()));
70
+ },
71
+ };
72
+ }
73
+ }
@@ -1,13 +1,35 @@
1
1
  import { parseConfig } from "../config.js";
2
+ import { EnhancedTools } from "./EnhancedTools.js";
2
3
  import { createBinarySession } from "./runtime.js";
3
4
  import { silentLogger } from "../logger.js";
4
5
  import { createEvidence } from "../domain/evidence.js";
6
+ const WORKFLOW_PROVIDER = {
7
+ id: "rea-workflow",
8
+ name: "REA composed investigation workflow",
9
+ version: "1",
10
+ };
5
11
  /**
6
12
  * Open one binary, execute one tool, and always release the bridge session.
7
13
  * Unlike MCP mode, every CLI invocation is intentionally isolated and does not
8
14
  * retain a target or bridge resources for a subsequent command.
9
15
  */
10
- export const runDirectAnalysis = async (path, tool, arguments_, logger = silentLogger) => {
16
+ export const runDirectAnalysis = async (path, tool, arguments_, logger = silentLogger) => runAnalysis(path, tool, arguments_, logger);
17
+ /** Execute one provider-native semantic operation with atomic provenance. */
18
+ export const runProviderAnalysis = async (path, tool, arguments_, logger = silentLogger) => runAnalysis(path, tool, arguments_, logger);
19
+ /** Describe configured providers without opening a target or launching Hopper. */
20
+ export const runSessionStatus = async (logger = silentLogger) => {
21
+ const config = parseConfig(process.env);
22
+ if (!config.ok)
23
+ return { error: config.error._tag, message: config.error.message };
24
+ const session = createBinarySession(config.value, logger);
25
+ try {
26
+ return session.status();
27
+ }
28
+ finally {
29
+ await session.close();
30
+ }
31
+ };
32
+ const runAnalysis = async (path, tool, arguments_, logger) => {
11
33
  const config = parseConfig(process.env);
12
34
  if (!config.ok)
13
35
  return { error: config.error._tag, message: config.error.message };
@@ -16,12 +38,31 @@ export const runDirectAnalysis = async (path, tool, arguments_, logger = silentL
16
38
  const opened = await session.open(path);
17
39
  if (!opened.ok)
18
40
  return { error: opened.error._tag, message: opened.error.message };
41
+ if (tool === "binary_overview" ||
42
+ tool === "analyze_function" ||
43
+ tool === "trace_feature") {
44
+ const result = await new EnhancedTools(session).execute(tool, arguments_);
45
+ return result.ok
46
+ ? createEvidence(opened.value, tool === "analyze_function"
47
+ ? session.providerIdentity(tool)
48
+ : WORKFLOW_PROVIDER, {
49
+ operation: tool,
50
+ parameters: arguments_,
51
+ result: result.value,
52
+ confidence: "derived",
53
+ limitations: ["Derived by an REA composed workflow."],
54
+ })
55
+ : { error: result.error._tag, message: result.error.message };
56
+ }
19
57
  const result = await session.execute(tool, arguments_);
20
58
  return result.ok
21
- ? createEvidence(opened.value, session.providerIdentity(), {
59
+ ? createEvidence(result.value.subject ?? opened.value, result.value.provider, {
22
60
  operation: tool,
23
61
  parameters: arguments_,
24
- result: result.value,
62
+ result: result.value.result,
63
+ rawResult: result.value.rawResult,
64
+ limitations: result.value.limitations,
65
+ locations: result.value.locations,
25
66
  })
26
67
  : { error: result.error._tag, message: result.error.message };
27
68
  }
@@ -7,8 +7,10 @@ import { execFile } from "node:child_process";
7
7
  import { promisify } from "node:util";
8
8
  import { parseBinaryTarget } from "../domain/binaryTarget.js";
9
9
  import { probeHomebrew } from "./homebrew.js";
10
+ import { linuxHopperLauncherPath, linuxSharedLibrariesAvailable, readLinuxDistribution, } from "./LinuxHopper.js";
10
11
  const execFileAsync = promisify(execFile);
11
12
  const DEFAULT_HOPPER = "/Applications/Hopper Disassembler.app/Contents/MacOS/hopper";
13
+ const SYSTEM_LINUX_HOPPER = "/opt/hopper/bin/Hopper";
12
14
  /**
13
15
  * Check requirements and an optional target without mutating the host.
14
16
  * Every failed check includes remediation suitable for either the one-shot CLI
@@ -17,13 +19,27 @@ const DEFAULT_HOPPER = "/Applications/Hopper Disassembler.app/Contents/MacOS/hop
17
19
  export const runDoctor = async (target, host = systemDoctorHost()) => {
18
20
  const checks = [];
19
21
  const nodeMajor = parseMajor(host.nodeVersion);
20
- checks.push(check("node", nodeMajor >= 22, host.nodeVersion, "Install Node.js 22 or newer."));
22
+ checks.push(check("node", nodeMajor >= 24, host.nodeVersion, {
23
+ remediation: "Install Node.js 24.18 or newer.",
24
+ classification: "missing_dependency",
25
+ }));
21
26
  const macosVersion = host.platform === "darwin" ? await host.macosVersion() : undefined;
22
27
  const macosMajor = macosVersion === undefined ? 0 : parseMajor(macosVersion);
23
- checks.push(check("macos", host.platform === "darwin" && macosMajor >= 12, macosVersion ?? host.platform, "Hopper requires macOS 12 or newer."));
28
+ const linuxDistribution = host.platform === "linux" ? await host.linuxDistribution() : undefined;
29
+ const supportedHost = (host.platform === "darwin" && macosMajor >= 12) ||
30
+ (host.platform === "linux" && linuxDistribution?.supported === true);
31
+ checks.push(check("host", supportedHost, macosVersion ??
32
+ (linuxDistribution === undefined
33
+ ? host.platform
34
+ : `${linuxDistribution.id} ${linuxDistribution.versionId ?? "unknown"}`), {
35
+ remediation: "REA supports macOS 12+, Ubuntu 24.04+, Fedora 41+, and 64-bit Arch Linux.",
36
+ classification: "unsupported_host",
37
+ }));
24
38
  const candidates = [
25
39
  host.configuredHopperPath,
26
40
  DEFAULT_HOPPER,
41
+ SYSTEM_LINUX_HOPPER,
42
+ linuxHopperLauncherPath(homedir()),
27
43
  ...(await host.manualHopperPaths()),
28
44
  await host.brewHopperPath(),
29
45
  ].filter((value) => value !== undefined);
@@ -33,9 +49,15 @@ export const runDoctor = async (target, host = systemDoctorHost()) => {
33
49
  hopperPath = candidate;
34
50
  break;
35
51
  }
36
- checks.push(check("hopper", hopperPath !== undefined, hopperPath, "Install Hopper with: brew install --cask hopper-disassembler, or set HOPPER_LAUNCHER_PATH."));
52
+ checks.push(check("hopper", hopperPath !== undefined, hopperPath, {
53
+ remediation: "Run rea setup --yes to install Hopper, or set HOPPER_LAUNCHER_PATH.",
54
+ classification: "missing_analysis_engine",
55
+ }));
37
56
  if (target !== undefined)
38
- checks.push(check("target", await host.validTarget(target), target, "Supply a readable local app or program path."));
57
+ checks.push(check("target", await host.validTarget(target), target, {
58
+ remediation: "Supply a readable local app or program path.",
59
+ classification: "config_drift",
60
+ }));
39
61
  return {
40
62
  healthy: checks.every(({ ok }) => ok),
41
63
  ...(hopperPath === undefined ? {} : { hopperPath }),
@@ -57,12 +79,18 @@ export const systemDoctorHost = () => ({
57
79
  return undefined;
58
80
  }
59
81
  },
82
+ linuxDistribution: readLinuxDistribution,
60
83
  async validTarget(path) {
61
84
  return (await parseBinaryTarget(path)).ok;
62
85
  },
63
86
  async executable(path) {
64
87
  try {
65
88
  await access(path, constants.X_OK);
89
+ if (process.platform === "linux") {
90
+ const linked = await execFileAsync("ldd", [path]);
91
+ if (!linuxSharedLibrariesAvailable(`${linked.stdout}\n${linked.stderr}`))
92
+ return false;
93
+ }
66
94
  return true;
67
95
  }
68
96
  catch {
@@ -102,9 +130,10 @@ export const systemDoctorHost = () => ({
102
130
  },
103
131
  });
104
132
  const parseMajor = (version) => Number.parseInt(version.split(".")[0] ?? "0", 10);
105
- const check = (name, ok, detail, remediation) => ({
133
+ const check = (name, ok, detail, failure) => ({
106
134
  name,
107
135
  ok,
136
+ classification: ok ? "healthy" : failure.classification,
108
137
  ...(detail === undefined ? {} : { detail }),
109
- ...(ok ? {} : { remediation }),
138
+ ...(ok ? {} : { remediation: failure.remediation }),
110
139
  });
@@ -1,5 +1,5 @@
1
1
  import { enhancedInputSchemas } from "../contracts/enhancedInputs.js";
2
- import { AnalysisProtocolError } from "../domain/errors.js";
2
+ import { AnalysisInputError, AnalysisCancelledError, AnalysisOutputError, } from "../domain/errors.js";
3
3
  import { parseDocuments, parseFunctionDossier, parseAddressedPage, parseListCount, parseRelatedAddresses, parseSegments, } from "../domain/hopperValues.js";
4
4
  import { err, ok } from "../domain/result.js";
5
5
  import { categorizeSwiftTypes, discoverObjcClasses, discoverObjcProtocols, discoverSwiftClasses, } from "../domain/symbolAnalysis.js";
@@ -282,10 +282,10 @@ export class EnhancedTools {
282
282
  if (!xrefs.ok)
283
283
  return xrefs;
284
284
  if (!Array.isArray(xrefs.value))
285
- return err(new AnalysisProtocolError("xrefs returned a non-array result"));
285
+ return err(new AnalysisOutputError("xrefs", "provider returned a non-array result"));
286
286
  for (const source of xrefs.value) {
287
287
  if (typeof source !== "string")
288
- return err(new AnalysisProtocolError("xrefs returned a non-address value"));
288
+ return err(new AnalysisOutputError("xrefs", "provider returned a non-address value"));
289
289
  if (operations >= operationBudget) {
290
290
  residual.add("Containing-procedure resolution stopped at the operation budget.");
291
291
  break;
@@ -331,16 +331,19 @@ export class EnhancedTools {
331
331
  return ok(entries);
332
332
  }
333
333
  if (page.value.nextOffset <= offset) {
334
- return err(new AnalysisProtocolError(`${tool} returned a non-advancing pagination offset`));
334
+ return err(new AnalysisOutputError(tool, "provider returned a non-advancing pagination offset"));
335
335
  }
336
336
  offset = page.value.nextOffset;
337
337
  }
338
338
  }
339
- #call(name, arguments_, signal) {
340
- return this.analysis.execute(name, arguments_, signal === undefined ? {} : { signal });
339
+ async #call(name, arguments_, signal) {
340
+ if (signal?.aborted === true)
341
+ return err(new AnalysisCancelledError(name));
342
+ const execution = await this.analysis.execute(name, arguments_, signal === undefined ? {} : { signal });
343
+ return execution.ok ? ok(execution.value.result) : execution;
341
344
  }
342
345
  }
343
- const invalidInput = (name, cause) => Promise.resolve(err(new AnalysisProtocolError(`Invalid ${name} input after MCP validation`, {
346
+ const invalidInput = (name, cause) => Promise.resolve(err(new AnalysisInputError(name, {
344
347
  cause,
345
348
  })));
346
349
  const resolveAddress = (value) => {
@@ -0,0 +1,51 @@
1
+ import { EvidenceIntegrityError, } from "../domain/errors.js";
2
+ import { jsonValueSchema } from "../domain/jsonValue.js";
3
+ import { err, ok } from "../domain/result.js";
4
+ import { compareBundles } from "../domain/bundleComparison.js";
5
+ import { EvidenceLedger } from "./EvidenceLedger.js";
6
+ import { readEvidenceBundle, writeEvidenceBundle, } from "./EvidenceBundleFiles.js";
7
+ /** Validate and merge one bundle using the same bounded ledger as MCP. */
8
+ export const importEvidenceBundleCommand = async (path, policy) => {
9
+ const loaded = await readEvidenceBundle(path, policy);
10
+ if (!loaded.ok)
11
+ return loaded;
12
+ const ledger = createLedger();
13
+ const imported = ledger.import(loaded.value);
14
+ return imported.ok
15
+ ? ok({ imported: imported.value, total: ledger.export().records.length })
16
+ : imported;
17
+ };
18
+ /** Validate a source bundle and atomically export canonical bytes. */
19
+ export const exportEvidenceBundleCommand = async (sourcePath, outputPath, overwrite, policy) => {
20
+ const loaded = await readEvidenceBundle(sourcePath, policy);
21
+ if (!loaded.ok)
22
+ return loaded;
23
+ return projectWrite(loaded.value, await writeEvidenceBundle(loaded.value, outputPath, overwrite, policy));
24
+ };
25
+ /** Compare two validated canonical Evidence v2 bundles without session state. */
26
+ export const compareEvidenceBundlesCommand = async (input) => {
27
+ const [left, right] = await Promise.all([
28
+ readEvidenceBundle(input.leftPath, input.policy),
29
+ readEvidenceBundle(input.rightPath, input.policy),
30
+ ]);
31
+ if (!left.ok)
32
+ return left;
33
+ if (!right.ok)
34
+ return right;
35
+ try {
36
+ return ok(jsonValueSchema.parse(compareBundles(left.value, right.value, [], input.offset, input.limit)));
37
+ }
38
+ catch (cause) {
39
+ return err(new EvidenceIntegrityError("Evidence bundle comparison failed", {
40
+ cause,
41
+ }));
42
+ }
43
+ };
44
+ const createLedger = () => new EvidenceLedger({ maxRecords: 10_000, maxBytes: 64 * 1024 * 1024 });
45
+ const projectWrite = (bundle, written) => written.ok
46
+ ? ok({
47
+ path: written.value.path,
48
+ bytes: written.value.bytes,
49
+ records: bundle.records.length,
50
+ })
51
+ : err(written.error);