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
package/README.md CHANGED
@@ -10,15 +10,15 @@
10
10
 
11
11
  [![npm version](https://img.shields.io/npm/v/rea-agents?style=flat-square&color=cb3837)](https://www.npmjs.com/package/rea-agents)
12
12
  [![CI](https://img.shields.io/github/actions/workflow/status/morluto/rea/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/morluto/rea/actions/workflows/ci.yml)
13
- [![50 MCP tools](https://img.shields.io/badge/MCP_tools-50-5c4ee5?style=flat-square)](#50-tools-for-investigation)
13
+ [![68 MCP tools](https://img.shields.io/badge/MCP_tools-68-5c4ee5?style=flat-square)](#68-tools-for-investigation)
14
14
  [![Node.js 24](https://img.shields.io/badge/Node.js-24.18.x-339933?style=flat-square&logo=nodedotjs&logoColor=white)](https://nodejs.org/)
15
15
  [![MIT license](https://img.shields.io/badge/license-MIT-f4c430?style=flat-square)](LICENSE)
16
16
 
17
- [Quick start](#quick-start) · [Current status](#current-status) · [Investigation model](#the-investigation-model) · [50 tools](#50-tools-for-investigation) · [Roadmap](#roadmap) · [How it works](#how-it-works)
17
+ [Quick start](#quick-start) · [Current status](#current-status) · [Investigation model](#the-investigation-model) · [68 tools](#68-tools-for-investigation) · [Roadmap](#roadmap) · [How it works](#how-it-works)
18
18
 
19
19
  <br />
20
20
 
21
- <code>npx skills add morluto/rea</code>
21
+ <code>curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash</code>
22
22
 
23
23
  </div>
24
24
 
@@ -41,8 +41,8 @@ npx skills add morluto/rea
41
41
  Then ask:
42
42
 
43
43
  ```text
44
- Set up REA and reverse engineer the Notes app. Explain how search works,
45
- show me how you know, and build a similar feature for my project.
44
+ Use REA to understand how search works in the Notes app, show me the
45
+ evidence, and build a similar feature for my project.
46
46
  ```
47
47
 
48
48
  Notes is only an example. Name any app you want to understand, or ask the agent to start with an overview.
@@ -81,6 +81,16 @@ REA shows how it reached its conclusions. It does not claim to recover original
81
81
 
82
82
  ## Quick start
83
83
 
84
+ ### One-command install — recommended
85
+
86
+ ```bash
87
+ curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
88
+ ```
89
+
90
+ The installer supports macOS 12+, Ubuntu 24.04+, Fedora 41+, and 64-bit Arch Linux. It installs compatible Node.js 24/npm 11 prerequisites, the exact latest `rea-agents-*` release, the official Hopper package, detected client registrations, and the bundled skill, then verifies the result. macOS uses Homebrew; Linux uses the native package manager and may show a system authorization prompt. Pin a reproducible release with `REA_VERSION=0.3.0`; `REA_VERSION` is the only supported installer environment override.
91
+
92
+ REA detects Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, and Devin. It configures detected clients with documented local MCP files; Devin is reported as skipped because its public documentation does not define a local configuration file. You can safely rerun the installer or `rea setup --yes`. If REA's local analysis engine needs one-time activation, installation remains intact and setup reports the exact remaining action rather than claiming full readiness.
93
+
84
94
  ### With a coding agent — recommended
85
95
 
86
96
  ```bash
@@ -89,22 +99,36 @@ npx skills add morluto/rea
89
99
 
90
100
  Ask your agent to set up REA. It will check your Mac, explain anything it needs to install, ask for approval, and guide you through system prompts. After setup, restart the agent if it asks you to load the full REA toolset.
91
101
 
92
- ### From Terminal
102
+ Approve installation if needed, complete the one-time activation when prompted, then describe the app or feature you want to understand. REA handles the binary-analysis tools behind the scenes.
103
+
104
+ ### From Terminal — no installation
93
105
 
94
106
  ```bash
95
107
  npx -y rea-agents setup --yes
108
+ npx -y rea-agents doctor
109
+ npx -y rea-agents analyze /Applications/Notes.app
96
110
  ```
97
111
 
98
112
  If macOS or an installer asks for confirmation, complete the prompt and run the same command again. Restart a configured coding agent so it loads REA.
99
113
 
100
- ### What setup handles
114
+ ### From Terminal — install the `rea` command
101
115
 
102
- - macOS 12 or newer
103
- - Node.js 24.18.x with npm 11.16.x (`nvm use` selects the pinned version)
116
+ ```bash
117
+ npm install --global rea-agents
118
+ rea setup --yes
119
+ rea doctor
120
+ rea analyze /Applications/Notes.app
121
+ ```
122
+
123
+ Choose either the no-install commands or the global installation. You do not need both.
124
+
125
+ ### Requirements
104
126
 
105
- If process capture reports that its native PTY backend is unavailable, install Xcode command-line tools and run `npm run rebuild:native`. Linux source builds require Python, `make`, and a C++ toolchain. Compatible packaged binaries do not require this rebuild.
127
+ - macOS 12 or newer
128
+ - Ubuntu 24.04+, Fedora 41+, or 64-bit Arch Linux
129
+ - Node.js 24.18.x with npm 11.16.x
106
130
 
107
- You do not need to install the reverse-engineering tools manually. Setup installs Homebrew and [Hopper](https://www.hopperapp.com/) when needed, configures detected Claude Desktop and Cursor installations, and installs the REA skill. Hopper is separate software and requires its own license; setup installs it but does not provide a license.
131
+ You do not need to choose or install binary-analysis tools yourself. REA setup handles them when needed. Deep binary analysis currently uses [Hopper](https://www.hopperapp.com/), a separate desktop application with its own license. REA can install the official macOS or Linux package, but you must approve installation and complete its one-time activation.
108
132
 
109
133
  If something is not working, run:
110
134
 
@@ -112,12 +136,64 @@ If something is not working, run:
112
136
  npx -y rea-agents doctor
113
137
  ```
114
138
 
139
+ `rea doctor --json` is read-only and distinguishes unsupported hosts, missing dependencies, a missing local analysis engine, configuration drift, and healthy checks. Fresh-install activation is reported by setup because Hopper does not expose a reliable noninteractive activation probe.
140
+
141
+ ### Linux installation and troubleshooting
142
+
143
+ On Ubuntu 24.04+, Fedora 41+, and 64-bit Arch Linux, setup downloads the matching official Hopper package, restricts downloads to Hopper's public origin, verifies the published size and checksum, and invokes `apt-get`, `dnf`, or `pacman`. When REA is not already running as root, `pkexec` presents the system authorization prompt. REA never invokes `sudo`.
144
+
145
+ The normal Linux launcher is `/opt/hopper/bin/Hopper`. If Hopper was installed elsewhere:
146
+
147
+ ```bash
148
+ export HOPPER_LAUNCHER_PATH=/absolute/path/to/Hopper
149
+ rea doctor --json
150
+ ```
151
+
152
+ If doctor reports a missing analysis engine even though the file exists, inspect shared-library resolution with:
153
+
154
+ ```bash
155
+ ldd /opt/hopper/bin/Hopper | grep 'not found'
156
+ ```
157
+
158
+ Install the missing distribution packages and rerun `rea setup --yes`. Hopper is a desktop application: real analysis requires an active `DISPLAY` or `WAYLAND_DISPLAY`, plus one-time license activation. The curl installer places the `rea` command in `~/.local/bin` on Linux; add that directory to future shell `PATH` values if it is not already present.
159
+
160
+ REA defaults `HOPPER_LAUNCHER_PATH` to `/Applications/Hopper Disassembler.app/Contents/MacOS/hopper` on macOS and `/opt/hopper/bin/Hopper` on Linux. Explicit configuration always takes precedence.
161
+
162
+ To remove only REA-owned MCP registrations and the managed skill:
163
+
164
+ ```bash
165
+ rea uninstall
166
+ rea uninstall --purge-data # also removes only ~/.rea/cache and ~/.rea/state
167
+ ```
168
+
169
+ Uninstall preserves Hopper, Homebrew, Node.js, evidence, captures, external evidence roots, unrelated skills, and other MCP servers. It refuses malformed client configuration and never follows purge-data symlinks.
170
+
115
171
  ### CLI or coding agent?
116
172
 
117
- | If you want to… | Use |
118
- | --------------------------------------------------------- | ------------------------------------------ |
119
- | Ask an agent to investigate an app and build a feature | Install the skill, then talk to your agent |
120
- | Inspect or decompile one part of an app from the Terminal | `rea analyze` or `rea decompile` |
173
+ | If you want to… | Use |
174
+ | --------------------------------------------------------- | -------------------------------------------------------------- |
175
+ | Ask an agent to investigate an app and build a feature | Install the skill, then talk to your agent |
176
+ | Inspect or decompile one part of an app from the Terminal | `rea analyze` or `rea decompile` |
177
+ | Validate, canonicalize, or compare Evidence v2 bundles | `rea evidence-import`, `rea evidence-export`, or `rea compare` |
178
+ | Import source as historical reference | `rea import-reference-source` |
179
+
180
+ Filesystem evidence commands and MCP file tools are disabled until the operator approves absolute roots:
181
+
182
+ ```bash
183
+ export REA_EVIDENCE_ROOTS_JSON='["/absolute/path/to/evidence"]'
184
+ rea evidence-import /absolute/path/to/evidence/bundle.json
185
+ rea evidence-export /absolute/path/to/evidence/bundle.json /absolute/path/to/evidence/canonical.json
186
+ rea compare /absolute/path/to/evidence/left.json /absolute/path/to/evidence/right.json
187
+ ```
188
+
189
+ Historical source import requires a separate allowlist and never treats source as current behavioral authority:
190
+
191
+ ```bash
192
+ export REA_REFERENCE_ROOTS_JSON='["/absolute/path/to/source"]'
193
+ rea import-reference-source /absolute/path/to/source
194
+ ```
195
+
196
+ Exports never replace an existing file unless `--overwrite` is explicit. Imports are size/depth bounded, validate every Evidence v2 ID and manifest, and never execute bundle content.
121
197
 
122
198
  ## One prompt, a full investigation
123
199
 
@@ -151,26 +227,38 @@ REA handles the app analysis in steps 1–5. The agent performs step 6 with its
151
227
  - Analyze Swift and Objective-C metadata without manually untangling every mangled symbol.
152
228
  - Leave names, comments, and bookmarks in Hopper so human and agent analysis reinforce each other.
153
229
 
154
- ## 50 tools for investigation
230
+ ## 68 tools for investigation
155
231
 
156
232
  | Tool family | Count | Examples |
157
233
  | ------------------------- | ----: | ----------------------------------------------------------------------------------------------------------------------- |
158
234
  | Native inspection | 33 | procedures, pseudocode, assembly, strings, names, segments, callers, callees, xrefs, annotations |
159
235
  | Investigation workflows | 10 | `binary_overview`, `analyze_function`, `batch_decompile`, `trace_feature`, call graphs, Swift and Objective-C discovery |
160
- | Workspace and observation | 7 | target lifecycle, Evidence v2 bundle import/export, deterministic process capture and comparison |
236
+ | Native macOS utilities | 5 | Mach-O metadata, code signatures, plists, architectures, Swift demangling; Hopper-free and provenance-bearing |
237
+ | Artifact graph | 2 | deterministic directory, ZIP/APK/IPA, and ASAR inventory; explicitly selected extraction into an absent owned tree |
238
+ | Workspace and observation | 18 | target lifecycle, Evidence v2 bundles, process/artifact/function comparison, evidence-linked residual-unknown lifecycle |
161
239
 
162
- The public interface describes what the agent is trying to learn. Providers decide how to answer. Hopper currently implements the native-analysis capabilities; the process harness implements controlled behavioral capture. Future providers can satisfy the same capability without changing the agent's investigation workflow.
240
+ The public interface describes what the agent is trying to learn. Providers decide how to answer. macOS utilities handle common semantic inspection without launching Hopper; Hopper handles deeper native analysis; the process harness implements controlled behavioral capture.
163
241
 
164
242
  ## Current status
165
243
 
166
244
  REA is already useful for native application investigation on macOS:
167
245
 
168
- - Open Mach-O, ELF, PE, `.app`, and Hopper database targets.
246
+ - Open Mach-O, ELF, PE, `.app`, ZIP, APK, IPA, ASAR, plist, JavaScript, source-map, and Hopper database targets.
247
+ - Traverse content-addressed artifact graphs without extraction; materialize only approved occurrences into absent output roots.
169
248
  - Build bounded function dossiers with pseudocode, assembly, CFG edges, comments, calls, references, strings, and names.
170
249
  - Search and trace features across symbols, strings, metadata, references, and call paths.
171
250
  - Record every successful result as deterministic Evidence v2 with artifact and provider identity, confidence, authority, limitations, and locations.
172
251
  - Export and import evidence bundles across sessions.
173
252
  - Capture approved PTY scenarios, child processes, filesystem changes, and loopback HTTP/WebSocket exchanges, then compare normalized captures.
253
+ - Compare complete artifact inventories by stable path, content, metadata, and relations; incomplete evidence never implies equivalence.
254
+ - Compare explicit function dossiers across text, calls, references, strings, and address-normalized CFG topology with per-facet unknowns.
255
+ - Compare canonical Evidence bundles by exact membership, explicit observation pairs, and residual-unknown histories without turning omissions into behavioral absence.
256
+ - Aggregate runtime comparisons into observed behavior changes while keeping static artifact/function differences labeled as candidates.
257
+ - Build bounded, Evidence-cited direct call paths by exact address without treating missing dossiers as graph leaves.
258
+ - Correlate exact static/runtime findings through explicit hypotheses without claiming causality from cochange.
259
+ - Verify finite behavioral and structural reconstruction specifications with pass, fail, and unknown kept distinct.
260
+ - Track residual unknowns through immutable CAS revisions, evidence-qualified resolution, contradictions, probes, and validated dependency relationships.
261
+ - With explicit `unknown_registry_approved: true`, record bounded trace/capture residuals, typed provider unavailability, and capture disagreements automatically.
174
262
 
175
263
  Hopper is the first provider, not the boundary of the project. Some current workflows still require Hopper and macOS; every evidence record identifies the provider and limitations behind its result.
176
264
 
@@ -189,6 +277,8 @@ REA is growing into a toolkit for understanding software across static artifacts
189
277
 
190
278
  Roadmap items describe direction, not shipped support. New providers must produce the same evidence and safety metadata as existing capabilities before they become part of the public workflow.
191
279
 
280
+ See the [static-analysis provider evaluation](docs/provider-evaluation.md) for the current research matrix and admission gate.
281
+
192
282
  ## Using REA with other coding agents
193
283
 
194
284
  Setup currently configures Claude Desktop and Cursor automatically. Any coding agent that supports local MCP servers can use REA with the configuration below.
@@ -215,10 +305,14 @@ flowchart LR
215
305
  REA --> Workspace["Investigation workspace<br/>evidence + artifacts + captures"]
216
306
  Workspace --> Router["Capability router"]
217
307
  Router --> Hopper["Hopper provider"]
308
+ Router --> Native["Native macOS provider"]
309
+ Router --> Artifact["Artifact graph provider"]
218
310
  Router --> Process["Process capture provider"]
219
- Router -. roadmap .-> More["Artifact, browser, dynamic,<br/>and additional static providers"]
311
+ Router -. roadmap .-> More["Browser, dynamic,<br/>and additional static providers"]
220
312
  Hopper --> Target["Target software"]
221
313
  Process --> Target
314
+ Native --> Target
315
+ Artifact --> Target
222
316
  ```
223
317
 
224
318
  The CLI and MCP server use the same application workflows and evidence contracts. A provider declares which capabilities it supports and the side effects those capabilities may have. Terminal commands are short-lived; an MCP session can retain an active target and evidence ledger across an investigation.
@@ -229,9 +323,20 @@ The agent workflow above is the easiest way to use REA. For a one-off overview f
229
323
 
230
324
  ```bash
231
325
  npx -y rea-agents analyze /Applications/Notes.app
326
+ npx -y rea-agents inspect /Applications/Notes.app
327
+ npx -y rea-agents inspect /Applications/Notes.app --detail detailed --limit 20
328
+ npx -y rea-agents search /Applications/Notes.app "offline"
329
+ npx -y rea-agents function /Applications/Notes.app 0x1000
330
+ npx -y rea-agents xrefs /Applications/Notes.app 0x1000
331
+ npx -y rea-agents trace /Applications/Notes.app "offline"
332
+ npx -y rea-agents compare /absolute/path/to/left-evidence.json /absolute/path/to/right-evidence.json
333
+ npx -y rea-agents capabilities
334
+ npx -y rea-agents providers
232
335
  ```
233
336
 
234
- Run `npx -y rea-agents --help` for direct decompilation and other options.
337
+ Run `npx -y rea-agents --help` for direct decompilation, bounded search and
338
+ other options. `analyze` and `inspect` share the same overview workflow;
339
+ `function`, `xrefs`, and `trace` return the same Evidence v2 envelopes as MCP.
235
340
 
236
341
  Or install the `rea` command globally:
237
342
 
@@ -251,6 +356,19 @@ REA derives explicit format and architecture arguments to prevent common FAT and
251
356
 
252
357
  Closing a REA session shuts down its bridge and removes its private socket directory. It does not quit a Hopper application the user may be using.
253
358
 
359
+ ## Advanced process-capture setup
360
+
361
+ Process capture is disabled by default. Enabling it requires
362
+ `REA_PROCESS_CAPTURE_ENABLED=true`, approved executable and working roots in
363
+ `REA_PROCESS_EXECUTABLE_ROOTS_JSON` and `REA_PROCESS_WORKING_ROOTS_JSON`, and an
364
+ environment allowlist in `REA_PROCESS_ALLOWED_ENV_JSON`. Because the current PTY
365
+ adapter uses host networking, it also requires
366
+ `REA_PROCESS_ALLOW_EXTERNAL_NETWORK=true`.
367
+
368
+ If the native PTY backend is unavailable, install Xcode command-line tools and
369
+ run `npm run rebuild:native`. Linux source builds require Python, `make`, and a
370
+ C++ toolchain. Compatible packaged binaries do not require this rebuild.
371
+
254
372
  ## Security model
255
373
 
256
374
  REA does not provide a hosted analysis service. Hopper communication uses an authenticated private local socket. Dynamic capabilities are disabled by default and require both operator policy and explicit per-call approval. REA is not a security sandbox: providers and launched targets run with the current user's permissions, and each capability reports its side effects and limitations. Report vulnerabilities through the private process in [SECURITY.md](SECURITY.md).
@@ -9,11 +9,15 @@ import json
9
9
  import hmac
10
10
  import os
11
11
  import re
12
+ import sre_parse
12
13
  import socket
13
14
 
14
15
  MAX_LINE_BYTES = 10 * 1024 * 1024
15
16
  BAD_ADDRESSES = (-1, 0xFFFFFFFFFFFFFFFF, None)
16
17
  _selected_document = None
18
+ _search_inventory_cache = {}
19
+ MAX_SEARCH_PATTERN_LENGTH = 256
20
+ MAX_SEARCH_VALUE_LENGTH = 4096
17
21
 
18
22
 
19
23
  def _hex(value):
@@ -89,6 +93,17 @@ def _procedure_identity(procedure):
89
93
  return {"address": _hex(procedure.getEntryPoint()), "name": _procedure_name(procedure)}
90
94
 
91
95
 
96
+ def _procedure_locals(procedure):
97
+ """Project opaque Hopper local-variable objects into an exact public shape."""
98
+ return [
99
+ {
100
+ "description": str(local),
101
+ "provenance": "hopper-public-python-api",
102
+ }
103
+ for local in procedure.getLocalVariableList()
104
+ ]
105
+
106
+
92
107
  def _containing_procedure(document, address):
93
108
  segment = document.getSegmentAtAddress(address)
94
109
  if segment is None:
@@ -182,6 +197,111 @@ def _strings(document):
182
197
  return result
183
198
 
184
199
 
200
+ def _invalidate_search_inventory(document):
201
+ """Discard derived names after analysis metadata changes."""
202
+ document_id = id(document)
203
+ for key in list(_search_inventory_cache):
204
+ if key[0] == document_id:
205
+ del _search_inventory_cache[key]
206
+
207
+
208
+ def _search_inventory(document, kind):
209
+ """Cache an immutable, address-sorted inventory for an unchanged document."""
210
+ key = (id(document), kind)
211
+ inventory = _search_inventory_cache.get(key)
212
+ if inventory is None:
213
+ values = _procedure_map(document) if kind == "procedure" else _strings(document)
214
+ inventory = tuple(sorted(values.items(), key=lambda item: int(item[0], 16)))
215
+ _search_inventory_cache[key] = inventory
216
+ return inventory
217
+
218
+
219
+ def _validate_regex_node(node, inside_repeat=False):
220
+ """Reject regex structures with disproportionate or non-local evaluation cost."""
221
+ forbidden = {
222
+ sre_parse.ASSERT,
223
+ sre_parse.ASSERT_NOT,
224
+ sre_parse.GROUPREF,
225
+ sre_parse.GROUPREF_EXISTS,
226
+ }
227
+ repeat_tokens = {sre_parse.MAX_REPEAT, sre_parse.MIN_REPEAT}
228
+ possessive = getattr(sre_parse, "POSSESSIVE_REPEAT", None)
229
+ if possessive is not None:
230
+ repeat_tokens.add(possessive)
231
+ for operation, argument in node:
232
+ if operation in forbidden:
233
+ raise ValueError("Regex lookarounds and backreferences are not supported")
234
+ if operation in repeat_tokens:
235
+ if inside_repeat:
236
+ raise ValueError("Nested regex repetitions are not supported")
237
+ minimum, maximum, child = argument
238
+ if maximum == sre_parse.MAXREPEAT or maximum > 1000:
239
+ raise ValueError("Unbounded or excessive regex repetitions are not supported")
240
+ _validate_regex_node(child, True)
241
+ elif operation == sre_parse.SUBPATTERN:
242
+ _validate_regex_node(argument[-1], inside_repeat)
243
+ elif operation == sre_parse.BRANCH:
244
+ for branch in argument[1]:
245
+ _validate_regex_node(branch, inside_repeat)
246
+
247
+
248
+ def _search_page(document, kind, params):
249
+ pattern = params.get("pattern")
250
+ if not isinstance(pattern, str) or not pattern or len(pattern) > MAX_SEARCH_PATTERN_LENGTH:
251
+ raise ValueError("pattern must contain between 1 and 256 characters")
252
+ mode = params.get("mode", "literal")
253
+ if mode not in ("literal", "regex"):
254
+ raise ValueError("mode must be literal or regex")
255
+ case_sensitive = params.get("case_sensitive", False)
256
+ if not isinstance(case_sensitive, bool):
257
+ raise ValueError("case_sensitive must be a boolean")
258
+ offset = params.get("offset", 0)
259
+ limit = params.get("limit", 100)
260
+ if not isinstance(offset, int) or isinstance(offset, bool) or offset < 0:
261
+ raise ValueError("offset must be a non-negative integer")
262
+ if not isinstance(limit, int) or isinstance(limit, bool) or limit < 1 or limit > 100:
263
+ raise ValueError("limit must be an integer between 1 and 100")
264
+
265
+ if mode == "literal":
266
+ needle = pattern if case_sensitive else pattern.casefold()
267
+ matches = lambda value: needle in (value if case_sensitive else value.casefold())
268
+ else:
269
+ try:
270
+ parsed = sre_parse.parse(pattern)
271
+ _validate_regex_node(parsed)
272
+ expression = re.compile(pattern, 0 if case_sensitive else re.IGNORECASE)
273
+ except re.error as error:
274
+ raise ValueError("Invalid regex pattern") from error
275
+ matches = lambda value: expression.search(value) is not None
276
+
277
+ selected = []
278
+ total = 0
279
+ page_end = offset + limit
280
+ for item in _search_inventory(document, kind):
281
+ if not matches(item[1]):
282
+ continue
283
+ if offset <= total < page_end:
284
+ selected.append(item)
285
+ total += 1
286
+ next_offset = offset + len(selected)
287
+ has_more = next_offset < total
288
+ return {
289
+ "items": [
290
+ {
291
+ "address": address,
292
+ "value": value[:MAX_SEARCH_VALUE_LENGTH],
293
+ "value_truncated": len(value) > MAX_SEARCH_VALUE_LENGTH,
294
+ }
295
+ for address, value in selected
296
+ ],
297
+ "offset": offset,
298
+ "limit": limit,
299
+ "total": total,
300
+ "next_offset": next_offset if has_more else None,
301
+ "has_more": has_more,
302
+ }
303
+
304
+
185
305
  def _page(values, offset, limit):
186
306
  """Return one deterministically address-sorted page without crossing an unbounded map."""
187
307
  if not isinstance(offset, int) or isinstance(offset, bool) or offset < 0:
@@ -354,7 +474,7 @@ def _analyze_function(document, params):
354
474
  def collection(name, items, scan_limited=False):
355
475
  return _bounded(items, _collection_offset(params, name), limit, None, scan_limited)
356
476
  return {
357
- "procedure": {"address": _hex(procedure.getEntryPoint()), "name": _procedure_name(procedure), "signature": procedure.signatureString(), "locals": _json_safe(procedure.getLocalVariableList())},
477
+ "procedure": {"address": _hex(procedure.getEntryPoint()), "name": _procedure_name(procedure), "signature": procedure.signatureString(), "locals": _procedure_locals(procedure)},
358
478
  "pseudocode": {"text": pseudo_text, "total_chars": len(pseudo), "returned_chars": len(pseudo_text), "truncated": pseudo_next < len(pseudo), "next_offset": pseudo_next if pseudo_next < len(pseudo) else None},
359
479
  "assembly": _bounded(assembly_lines, assembly_offset, max_instructions),
360
480
  "comments": collection("comments", comments, instruction_scan_truncated),
@@ -372,7 +492,7 @@ def _dispatch(method, params):
372
492
  """Dispatch only the closed operation set implemented by REA's public tools."""
373
493
  global _selected_document
374
494
  if method == "health":
375
- return {"name": "REA Hopper bridge", "version": "1.0.0"}
495
+ return {"name": "REA Hopper bridge", "version": "1.0.0", "run_id": REA_RUN_ID}
376
496
  if method == "shutdown":
377
497
  return {"shutdown": True}
378
498
  if method == "list_documents":
@@ -426,18 +546,30 @@ def _dispatch(method, params):
426
546
  return _hex(result)
427
547
  if method == "list_segments":
428
548
  result = []
549
+ permission_limitation = _unavailable(
550
+ "Hopper's public Python API does not expose segment or section permissions"
551
+ )
429
552
  for segment in document.getSegmentsList():
430
553
  start = segment.getStartingAddress()
431
- sections = [{"name": section.getName(), "start": _hex(section.getStartingAddress()), "end": _hex(section.getStartingAddress() + section.getLength())} for section in segment.getSectionsList()]
554
+ sections = [{
555
+ "name": section.getName(),
556
+ "start": _hex(section.getStartingAddress()),
557
+ "end": _hex(section.getStartingAddress() + section.getLength()),
558
+ "readable": None,
559
+ "writable": None,
560
+ "executable": None,
561
+ "permissions": permission_limitation,
562
+ "provenance": "hopper-public-python-api",
563
+ } for section in segment.getSectionsList()]
432
564
  result.append({
433
565
  "name": segment.getName(),
434
566
  "start": _hex(start),
435
567
  "end": _hex(start + segment.getLength()),
568
+ "readable": None,
436
569
  "writable": None,
437
570
  "executable": None,
438
- "permissions": _unavailable(
439
- "Hopper's public Python API does not expose segment permissions"
440
- ),
571
+ "permissions": permission_limitation,
572
+ "provenance": "hopper-public-python-api",
441
573
  "sections": sections,
442
574
  })
443
575
  return result
@@ -461,10 +593,8 @@ def _dispatch(method, params):
461
593
  result = {key: result[key]} if key in result else {}
462
594
  return _page(result, params.get("offset", 0), params.get("limit", 100))
463
595
  if method in ("search_procedures", "search_strings"):
464
- flags = 0 if params.get("case_sensitive", False) else re.IGNORECASE
465
- expression = re.compile(params["pattern"], flags)
466
- values = _procedure_map(document) if method == "search_procedures" else _strings(document)
467
- return {key: value for key, value in values.items() if expression.search(value)}
596
+ kind = "procedure" if method == "search_procedures" else "string"
597
+ return _search_page(document, kind, params)
468
598
  if method.startswith("procedure_"):
469
599
  procedure = _procedure(document, params.get("procedure"))
470
600
  if method == "procedure_address":
@@ -480,12 +610,16 @@ def _dispatch(method, params):
480
610
  if method == "procedure_info":
481
611
  blocks = list(procedure.basicBlockIterator())
482
612
  length = sum(max(0, block.getEndingAddress() - block.getStartingAddress()) for block in blocks)
483
- return {"name": _procedure_name(procedure), "entrypoint": _hex(procedure.getEntryPoint()), "basicblock_count": procedure.getBasicBlockCount(), "length": length, "signature": procedure.signatureString(), "locals": procedure.getLocalVariableList()}
613
+ return {"name": _procedure_name(procedure), "entrypoint": _hex(procedure.getEntryPoint()), "basicblock_count": procedure.getBasicBlockCount(), "length": length, "signature": procedure.signatureString(), "locals": _procedure_locals(procedure)}
484
614
  if method == "set_address_name":
485
615
  address = _address(document, params.get("address"))
486
- return document.setNameAtAddress(address, params["name"])
616
+ result = document.setNameAtAddress(address, params["name"])
617
+ _invalidate_search_inventory(document)
618
+ return result
487
619
  if method == "set_addresses_names":
488
- return {key: document.setNameAtAddress(_address(document, key), value) for key, value in params["names"].items()}
620
+ result = {key: document.setNameAtAddress(_address(document, key), value) for key, value in params["names"].items()}
621
+ _invalidate_search_inventory(document)
622
+ return result
489
623
  if method in ("set_comment", "set_inline_comment"):
490
624
  address = _address(document, params.get("address"))
491
625
  segment = _segment(document, address)
@@ -526,7 +660,7 @@ def _serve_connection(connection):
526
660
  should_stop = request["method"] == "shutdown"
527
661
  response = {"id": request_id, "result": _json_safe(result)}
528
662
  except Exception as error:
529
- response = {"id": request_id if isinstance(request_id, int) else 0, "error": {"code": -32000, "message": str(error)[:512]}}
663
+ response = {"id": request_id if isinstance(request_id, int) else 0, "error": {"code": -32000, "message": str(error)[:512], "type": _diagnostic_type(error)}}
530
664
  file.write((json.dumps(response, separators=(",", ":")) + "\n").encode("utf-8"))
531
665
  file.flush()
532
666
  if should_stop:
@@ -535,6 +669,14 @@ def _serve_connection(connection):
535
669
  connection.close()
536
670
 
537
671
 
672
+ def _diagnostic_type(error):
673
+ if isinstance(error, PermissionError):
674
+ return "authorization"
675
+ if isinstance(error, (ValueError, TypeError, KeyError)):
676
+ return "invalid_request"
677
+ return "bridge_exception"
678
+
679
+
538
680
  def _run():
539
681
  """Own a permission-restricted, single-client Unix socket for this bridge."""
540
682
  if os.path.exists(REA_SOCKET):
@@ -1 +1,12 @@
1
- export {};
1
+ import { jsonValueSchema } from "../domain/jsonValue.js";
2
+ /** Build a validated atomic provider observation at an adapter boundary. */
3
+ export const createAnalysisExecution = (result, provider, options = {}) => ({
4
+ result: jsonValueSchema.parse(result),
5
+ rawResult: options.rawResult === undefined
6
+ ? jsonValueSchema.parse(result)
7
+ : jsonValueSchema.parse(options.rawResult),
8
+ provider,
9
+ limitations: [...(options.limitations ?? [])],
10
+ locations: [...(options.locations ?? [])],
11
+ subject: options.subject ?? null,
12
+ });