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.
- package/README.md +139 -21
- package/bridge/hopper_bridge.py +156 -14
- package/dist/application/AnalysisProvider.js +12 -1
- package/dist/application/ArtifactExtraction.js +166 -0
- package/dist/application/ArtifactGraphConstruction.js +257 -0
- package/dist/application/ArtifactInventory.js +253 -0
- package/dist/application/BinarySession.js +174 -14
- package/dist/application/CompositeProvider.js +73 -0
- package/dist/application/DirectAnalysis.js +44 -3
- package/dist/application/Doctor.js +35 -6
- package/dist/application/EnhancedTools.js +10 -7
- package/dist/application/EvidenceBundleCommands.js +51 -0
- package/dist/application/EvidenceBundleFiles.js +127 -0
- package/dist/application/EvidenceLedger.js +225 -18
- package/dist/application/FilesystemSnapshot.js +124 -0
- package/dist/application/LinuxHopper.js +186 -0
- package/dist/application/LoopbackReplay.js +195 -37
- package/dist/application/ProcessHarness.js +200 -191
- package/dist/application/ProcessNormalization.js +44 -0
- package/dist/application/ProcessOwnership.js +105 -0
- package/dist/application/ProcessSampling.js +284 -0
- package/dist/application/RealHopperAssertions.js +116 -0
- package/dist/application/ReferenceSourceImport.js +182 -0
- package/dist/application/ReferenceSourceImportEntries.js +122 -0
- package/dist/application/ReferenceSourceImportPolicy.js +73 -0
- package/dist/application/ReferenceSourceImportTypes.js +18 -0
- package/dist/application/ReferenceSourceVcsAdapter.js +34 -0
- package/dist/application/Setup.js +192 -41
- package/dist/application/Uninstall.js +130 -0
- package/dist/application/runtime.js +8 -1
- package/dist/artifacts/ArtifactPaths.js +51 -0
- package/dist/artifacts/ArtifactProvider.js +130 -0
- package/dist/artifacts/ArtifactReader.js +9 -0
- package/dist/artifacts/AsarArtifactReader.js +62 -0
- package/dist/artifacts/DirectoryArtifactReader.js +107 -0
- package/dist/artifacts/MachOSliceArtifactReader.js +66 -0
- package/dist/artifacts/SafeOutputTree.js +199 -0
- package/dist/artifacts/StreamBytes.js +10 -0
- package/dist/artifacts/ZipArtifactReader.js +109 -0
- package/dist/cli.js +229 -21
- package/dist/cliEvidenceCommands.js +68 -0
- package/dist/cliLogging.js +21 -0
- package/dist/config.js +35 -1
- package/dist/contracts/artifactComparisonExample.js +95 -0
- package/dist/contracts/artifactToolContracts.js +84 -0
- package/dist/contracts/enhancedInputs.js +4 -0
- package/dist/contracts/functionComparisonExample.js +57 -0
- package/dist/contracts/investigationExamples.js +118 -0
- package/dist/contracts/nativeToolContracts.js +52 -0
- package/dist/contracts/processCaptureExample.js +25 -0
- package/dist/contracts/toolContractExamples.js +69 -0
- package/dist/contracts/toolContracts.js +99 -36
- package/dist/contracts/toolOutputSchemas.js +156 -82
- package/dist/contracts/unknownContractExamples.js +33 -0
- package/dist/domain/artifactComparison.js +273 -0
- package/dist/domain/artifactGraph.js +194 -0
- package/dist/domain/artifactInventoryEvidence.js +150 -0
- package/dist/domain/binaryTarget.js +55 -0
- package/dist/domain/bundleComparison.js +266 -0
- package/dist/domain/callPath.js +346 -0
- package/dist/domain/changedBehavior.js +294 -0
- package/dist/domain/errors.js +199 -5
- package/dist/domain/evidence.js +41 -10
- package/dist/domain/evidenceBundle.js +187 -6
- package/dist/domain/functionComparison.js +201 -0
- package/dist/domain/functionComparisonNormalization.js +112 -0
- package/dist/domain/functionComparisonResults.js +54 -0
- package/dist/domain/functionComparisonSchemas.js +82 -0
- package/dist/domain/functionDossierEvidence.js +171 -0
- package/dist/domain/hopperValues.js +35 -8
- package/dist/domain/nativeInspection.js +142 -0
- package/dist/domain/processCapture.js +152 -54
- package/dist/domain/processComparison.js +106 -0
- package/dist/domain/reconstructionUnknowns.js +90 -0
- package/dist/domain/reconstructionVerification.js +285 -0
- package/dist/domain/reconstructionVerificationSchemas.js +126 -0
- package/dist/domain/referenceSourceClassification.js +496 -0
- package/dist/domain/referenceSourceGraph.js +376 -0
- package/dist/domain/referenceSourceImportParsing.js +235 -0
- package/dist/domain/referenceSourcePolicy.js +1 -0
- package/dist/domain/residualUnknown.js +239 -0
- package/dist/domain/staticRuntimeCorrelation.js +375 -0
- package/dist/hopper/BridgeLauncher.js +39 -3
- package/dist/hopper/HopperClient.js +14 -5
- package/dist/hopper/HopperProvider.js +57 -22
- package/dist/hopper/protocol.js +13 -2
- package/dist/identity.js +1 -0
- package/dist/main.js +5 -1
- package/dist/native/CommandRunner.js +156 -0
- package/dist/native/NativeMacOSProvider.js +306 -0
- package/dist/native/NativeMachoInspection.js +135 -0
- package/dist/native/parsers/codesign.js +55 -0
- package/dist/native/parsers/demangle.js +26 -0
- package/dist/native/parsers/dyldInfo.js +25 -0
- package/dist/native/parsers/lipo.js +67 -0
- package/dist/native/parsers/otool.js +193 -0
- package/dist/native/parsers/plist.js +23 -0
- package/dist/reference/ReferenceSourceReader.js +73 -0
- package/dist/reference/ReferenceSourceReaderEntries.js +206 -0
- package/dist/reference/ReferenceSourceReaderErrors.js +19 -0
- package/dist/reference/ReferenceSourceReaderFile.js +119 -0
- package/dist/reference/ReferenceSourceReaderPaths.js +23 -0
- package/dist/reference/ReferenceSourceReaderTypes.js +2 -0
- package/dist/reference/ReferenceSourceReaderValidate.js +71 -0
- package/dist/server/createServer.js +31 -5
- package/dist/server/recordDerivedEvidence.js +10 -0
- package/dist/server/registerArtifactComparisonTool.js +62 -0
- package/dist/server/registerArtifactTools.js +6 -0
- package/dist/server/registerBundleComparisonTool.js +47 -0
- package/dist/server/registerEnhancedTools.js +64 -12
- package/dist/server/registerEvidenceTools.js +36 -0
- package/dist/server/registerFunctionComparisonTool.js +68 -0
- package/dist/server/registerInvestigationTools.js +224 -0
- package/dist/server/registerNativeTools.js +6 -0
- package/dist/server/registerOfficialTools.js +47 -14
- package/dist/server/registerProcessComparisonTool.js +106 -0
- package/dist/server/registerSessionTools.js +179 -70
- package/dist/server/sessionEvidence.js +28 -0
- package/dist/server/sessionToolPolicies.js +64 -0
- package/dist/server/toolRegistrationOptions.js +7 -0
- package/dist/server/toolResult.js +8 -5
- package/install.sh +198 -0
- package/package.json +18 -1
- package/scripts/rea.mjs +5 -1
- package/skills/rea-analysis/SKILL.md +77 -2
package/README.md
CHANGED
|
@@ -10,15 +10,15 @@
|
|
|
10
10
|
|
|
11
11
|
[](https://www.npmjs.com/package/rea-agents)
|
|
12
12
|
[](https://github.com/morluto/rea/actions/workflows/ci.yml)
|
|
13
|
-
[](#68-tools-for-investigation)
|
|
14
14
|
[](https://nodejs.org/)
|
|
15
15
|
[](LICENSE)
|
|
16
16
|
|
|
17
|
-
[Quick start](#quick-start) · [Current status](#current-status) · [Investigation model](#the-investigation-model) · [
|
|
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>
|
|
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
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
114
|
+
### From Terminal — install the `rea` command
|
|
101
115
|
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
##
|
|
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
|
-
|
|
|
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
|
|
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["
|
|
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
|
|
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).
|
package/bridge/hopper_bridge.py
CHANGED
|
@@ -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":
|
|
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 = [{
|
|
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":
|
|
439
|
-
|
|
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
|
-
|
|
465
|
-
|
|
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
|
|
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
|
-
|
|
616
|
+
result = document.setNameAtAddress(address, params["name"])
|
|
617
|
+
_invalidate_search_inventory(document)
|
|
618
|
+
return result
|
|
487
619
|
if method == "set_addresses_names":
|
|
488
|
-
|
|
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
|
-
|
|
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
|
+
});
|