rea-agents 1.2.0 → 1.4.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 +95 -27
- package/bridge/hopper_bridge.py +172 -14
- package/dist/application/AnalysisSnapshotCache.js +131 -0
- package/dist/application/AnalysisSnapshotFiles.js +31 -0
- package/dist/application/ArtifactGraphConstruction.js +1 -1
- package/dist/application/ArtifactInventory.js +100 -24
- package/dist/application/AuthorizedArtifactInventory.js +16 -0
- package/dist/application/BinarySession.js +149 -36
- package/dist/application/BinarySessionPort.js +1 -0
- package/dist/application/BoundedJsonFiles.js +109 -0
- package/dist/application/CapabilityInventory.js +127 -0
- package/dist/application/ClientRegistrationStatus.js +69 -0
- package/dist/application/CrossVersionInventory.js +109 -0
- package/dist/application/CrossVersionInvestigation.js +358 -0
- package/dist/application/DirectAnalysis.js +205 -24
- package/dist/application/Doctor.js +104 -3
- package/dist/application/EvidenceBundleFiles.js +15 -108
- package/dist/application/EvidenceLedger.js +3 -2
- package/dist/application/InvestigationProviders.js +48 -0
- package/dist/application/InvestigationWorkspaceStore.js +214 -0
- package/dist/application/LinuxHopper.js +72 -48
- package/dist/application/PermissionAuthority.js +183 -0
- package/dist/application/PermissionConfiguration.js +26 -0
- package/dist/application/ProcessCaptureAuthority.js +2 -2
- package/dist/application/ProcessCaptureError.js +25 -0
- package/dist/application/ProcessCaptureLifecycle.js +22 -2
- package/dist/application/ProcessCli.js +111 -31
- package/dist/application/ProcessHarness.js +6 -3
- package/dist/application/ProgressReporter.js +30 -0
- package/dist/application/ProjectPermissionStore.js +110 -0
- package/dist/application/ReferenceSourceImportEntries.js +18 -3
- package/dist/application/ReferenceSourceImportTypes.js +32 -0
- package/dist/application/Setup.js +35 -101
- package/dist/application/SetupClients.js +19 -0
- package/dist/application/SetupInstallFailure.js +53 -0
- package/dist/application/SetupPlan.js +31 -0
- package/dist/application/SupportedClients.js +41 -0
- package/dist/application/Uninstall.js +34 -24
- package/dist/application/UnknownEvidence.js +33 -0
- package/dist/application/Upgrade.js +3 -1
- package/dist/application/runtime.js +2 -2
- package/dist/artifacts/ArtifactProvider.js +15 -8
- package/dist/catalogIdentity.js +121 -0
- package/dist/cli.js +76 -13
- package/dist/cliEvidenceCommands.js +52 -12
- package/dist/cliInvestigationCommands.js +137 -0
- package/dist/cliLogging.js +20 -3
- package/dist/cliOutput.js +41 -0
- package/dist/cliPolicyCommands.js +115 -0
- package/dist/config.js +102 -27
- package/dist/contracts/artifactToolContracts.js +14 -1
- package/dist/contracts/errorSchemas.js +98 -0
- package/dist/contracts/promptContracts.js +256 -0
- package/dist/contracts/sessionLifecycleInputs.js +11 -0
- package/dist/contracts/toolContractTypes.js +1 -0
- package/dist/contracts/toolContracts.js +20 -7
- package/dist/contracts/toolOutputSchemas.js +70 -9
- package/dist/domain/analysisSnapshot.js +149 -0
- package/dist/domain/artifactComparison.js +38 -11
- package/dist/domain/artifactGraph.js +25 -1
- package/dist/domain/artifactInventoryEvidence.js +18 -0
- package/dist/domain/changedBehavior.js +35 -10
- package/dist/domain/errors.js +384 -32
- package/dist/domain/evidence.js +4 -2
- package/dist/domain/evidenceBundle.js +28 -0
- package/dist/domain/hopperStartupFailure.js +55 -0
- package/dist/domain/investigationWorkspace.js +341 -0
- package/dist/domain/jsonValue.js +2 -0
- package/dist/domain/nativeInspection.js +3 -2
- package/dist/domain/permissionPolicy.js +157 -0
- package/dist/domain/processCapture.js +5 -4
- package/dist/domain/processComparison.js +6 -4
- package/dist/domain/reconstructionVerification.js +1 -1
- package/dist/domain/reconstructionVerificationSchemas.js +1 -0
- package/dist/domain/staticRuntimeCorrelation.js +5 -1
- package/dist/generatedPackageMetadata.js +9 -0
- package/dist/hopper/BridgeLauncher.js +51 -20
- package/dist/hopper/HopperClient.js +45 -3
- package/dist/hopper/HopperProvider.js +1 -0
- package/dist/identity.js +10 -3
- package/dist/logger.js +2 -2
- package/dist/main.js +139 -28
- package/dist/server/createServer.js +54 -5
- package/dist/server/mcpProgress.js +23 -0
- package/dist/server/promptCompletion.js +144 -0
- package/dist/server/registerArtifactComparisonTool.js +6 -2
- package/dist/server/registerBundleComparisonTool.js +6 -2
- package/dist/server/registerEnhancedTools.js +20 -7
- package/dist/server/registerEvidenceResources.js +302 -0
- package/dist/server/registerEvidenceTools.js +59 -8
- package/dist/server/registerFunctionComparisonTool.js +7 -3
- package/dist/server/registerInvestigationTools.js +111 -18
- package/dist/server/registerOfficialTools.js +25 -14
- package/dist/server/registerProcessComparisonTool.js +8 -10
- package/dist/server/registerPrompts.js +67 -0
- package/dist/server/registerSessionStatusTool.js +52 -0
- package/dist/server/registerSessionTools.js +178 -22
- package/dist/server/runDerivedOperation.js +35 -0
- package/dist/server/sessionEvidence.js +2 -8
- package/dist/server/sessionToolPolicies.js +1 -42
- package/dist/server/toolResult.js +34 -6
- package/dist/serverIdentity.js +70 -0
- package/install.sh +11 -11
- package/package.json +10 -6
- package/scripts/hopper-demo-x11.py +349 -0
- package/scripts/rea.mjs +6 -3
- package/skills/rea-analysis/SKILL.md +9 -2
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rea-agents",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Reverse engineer anything from your terminal or
|
|
3
|
+
"version": "1.4.0",
|
|
4
|
+
"description": "Reverse engineer anything from your terminal or agent with one CLI and MCP server.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"keywords": [
|
|
15
15
|
"reverse-engineering",
|
|
16
16
|
"binary-analysis",
|
|
17
|
-
"
|
|
17
|
+
"agents",
|
|
18
18
|
"agent-skills",
|
|
19
19
|
"cli",
|
|
20
20
|
"mcp-server",
|
|
@@ -43,11 +43,13 @@
|
|
|
43
43
|
"scripts/rea.mjs",
|
|
44
44
|
"scripts/prepare-node-pty.mjs",
|
|
45
45
|
"scripts/rebuild-native.mjs",
|
|
46
|
+
"scripts/hopper-demo-x11.py",
|
|
46
47
|
"skills",
|
|
47
48
|
"README.md",
|
|
48
49
|
"LICENSE"
|
|
49
50
|
],
|
|
50
51
|
"scripts": {
|
|
52
|
+
"prebuild": "node scripts/generate-package-metadata.mjs",
|
|
51
53
|
"build": "tsc -p tsconfig.build.json",
|
|
52
54
|
"check": "npm run typecheck && npm run lint && npm run format:check && npm test",
|
|
53
55
|
"format": "prettier --write .",
|
|
@@ -69,12 +71,14 @@
|
|
|
69
71
|
"typecheck": "tsc --noEmit",
|
|
70
72
|
"verify:hopper": "npm run build && node scripts/verify-real-hopper.mjs",
|
|
71
73
|
"verify:hopper:linux": "npm run build && node scripts/verify-real-hopper-linux.mjs",
|
|
74
|
+
"verify:setup:linux": "npm run build && node scripts/verify-linux-setup-e2e.mjs",
|
|
72
75
|
"build:fixtures": "node scripts/build-conformance-fixtures.mjs",
|
|
73
76
|
"verify:fixtures": "npm run build:fixtures && node scripts/verify-conformance-fixtures.mjs",
|
|
74
77
|
"verify:package": "npm run build && node scripts/verify-package.mjs",
|
|
75
78
|
"verify:published": "node scripts/verify-published-package.mjs",
|
|
76
79
|
"config:print": "node scripts/print-mcp-config.mjs",
|
|
77
|
-
"docs:generate": "typedoc",
|
|
80
|
+
"docs:generate": "npm run build && typedoc && node scripts/generate-error-schema.mjs",
|
|
81
|
+
"docs:error-schema": "npm run build && node scripts/generate-error-schema.mjs",
|
|
78
82
|
"prepack": "npm run build",
|
|
79
83
|
"prepare": "husky",
|
|
80
84
|
"rebuild:native": "node scripts/rebuild-native.mjs"
|
|
@@ -87,7 +91,7 @@
|
|
|
87
91
|
"@babel/parser": "8.0.4",
|
|
88
92
|
"@babel/types": "8.0.4",
|
|
89
93
|
"@electron/asar": "4.2.0",
|
|
90
|
-
"@modelcontextprotocol/server": "2.0.0-beta.
|
|
94
|
+
"@modelcontextprotocol/server": "2.0.0-beta.4",
|
|
91
95
|
"@xterm/addon-serialize": "0.14.0",
|
|
92
96
|
"@xterm/headless": "6.0.0",
|
|
93
97
|
"@zip.js/zip.js": "2.8.26",
|
|
@@ -108,7 +112,7 @@
|
|
|
108
112
|
},
|
|
109
113
|
"devDependencies": {
|
|
110
114
|
"@fast-check/vitest": "0.4.1",
|
|
111
|
-
"@modelcontextprotocol/client": "2.0.0-beta.
|
|
115
|
+
"@modelcontextprotocol/client": "2.0.0-beta.4",
|
|
112
116
|
"@types/node": "24.10.1",
|
|
113
117
|
"@types/write-file-atomic": "4.0.3",
|
|
114
118
|
"@types/ws": "8.18.1",
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Inspect or activate Hopper's version-pinned demo dialog on a private X display."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import ctypes
|
|
8
|
+
import hashlib
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import secrets
|
|
12
|
+
import subprocess
|
|
13
|
+
import time
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Keep this digest aligned with LinuxHopper.ts and rerun real-Hopper verification
|
|
17
|
+
# before accepting another vendor build.
|
|
18
|
+
SUPPORTED_HOPPER_SHA256 = {
|
|
19
|
+
"0294ced141cc373468ee22d8343e7dac41980cb05a937994ca81c9f09afe7ded"
|
|
20
|
+
}
|
|
21
|
+
EXPECTED_SCREEN = (1280, 1024)
|
|
22
|
+
EXPECTED_DIALOG = (189, 370, 901, 284)
|
|
23
|
+
DEMO_CLICK = (305, 632)
|
|
24
|
+
PRIVATE_DISPLAY_UNAVAILABLE = 70
|
|
25
|
+
X11_AUTHORIZATION_FAILED = 71
|
|
26
|
+
UNSUPPORTED_HOPPER_BUILD = 72
|
|
27
|
+
INVALID_LAUNCH_COMMAND = 73
|
|
28
|
+
PROCESS_OWNERSHIP_MISMATCH = 74
|
|
29
|
+
HOPPER_EXITED_DURING_STARTUP = 75
|
|
30
|
+
UNSUPPORTED_DEMO_DIALOG = 76
|
|
31
|
+
UNEXPECTED_DISPLAY_GEOMETRY = 77
|
|
32
|
+
X11_INPUT_FAILED = 78
|
|
33
|
+
RUNTIME_DEPENDENCY_UNAVAILABLE = 79
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AdapterFailure(Exception):
|
|
37
|
+
def __init__(self, code: int, message: str) -> None:
|
|
38
|
+
super().__init__(message)
|
|
39
|
+
self.code = code
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class XWindowAttributes(ctypes.Structure):
|
|
43
|
+
_fields_ = [
|
|
44
|
+
("x", ctypes.c_int),
|
|
45
|
+
("y", ctypes.c_int),
|
|
46
|
+
("width", ctypes.c_int),
|
|
47
|
+
("height", ctypes.c_int),
|
|
48
|
+
("border_width", ctypes.c_int),
|
|
49
|
+
("depth", ctypes.c_int),
|
|
50
|
+
("visual", ctypes.c_void_p),
|
|
51
|
+
("root", ctypes.c_ulong),
|
|
52
|
+
("class_", ctypes.c_int),
|
|
53
|
+
("bit_gravity", ctypes.c_int),
|
|
54
|
+
("win_gravity", ctypes.c_int),
|
|
55
|
+
("backing_store", ctypes.c_int),
|
|
56
|
+
("backing_planes", ctypes.c_ulong),
|
|
57
|
+
("backing_pixel", ctypes.c_ulong),
|
|
58
|
+
("save_under", ctypes.c_int),
|
|
59
|
+
("colormap", ctypes.c_ulong),
|
|
60
|
+
("map_installed", ctypes.c_int),
|
|
61
|
+
("map_state", ctypes.c_int),
|
|
62
|
+
("all_event_masks", ctypes.c_long),
|
|
63
|
+
("your_event_mask", ctypes.c_long),
|
|
64
|
+
("do_not_propagate_mask", ctypes.c_long),
|
|
65
|
+
("override_redirect", ctypes.c_int),
|
|
66
|
+
("screen", ctypes.c_void_p),
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def parse_args() -> argparse.Namespace:
|
|
71
|
+
parser = argparse.ArgumentParser()
|
|
72
|
+
parser.add_argument("--hopper", required=True)
|
|
73
|
+
parser.add_argument("--socket", required=True)
|
|
74
|
+
parser.add_argument("command", nargs=argparse.REMAINDER)
|
|
75
|
+
return parser.parse_args()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def digest(path: Path) -> str:
|
|
79
|
+
value = hashlib.sha256()
|
|
80
|
+
with path.open("rb") as file:
|
|
81
|
+
for chunk in iter(lambda: file.read(1024 * 1024), b""):
|
|
82
|
+
value.update(chunk)
|
|
83
|
+
return value.hexdigest()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def descendants(pid: int) -> set[int]:
|
|
87
|
+
found = {pid}
|
|
88
|
+
changed = True
|
|
89
|
+
while changed:
|
|
90
|
+
changed = False
|
|
91
|
+
for stat in Path("/proc").glob("[0-9]*/stat"):
|
|
92
|
+
try:
|
|
93
|
+
fields = stat.read_text().split()
|
|
94
|
+
child = int(fields[0])
|
|
95
|
+
parent = int(fields[3])
|
|
96
|
+
except (OSError, ValueError, IndexError):
|
|
97
|
+
continue
|
|
98
|
+
if parent in found and child not in found:
|
|
99
|
+
found.add(child)
|
|
100
|
+
changed = True
|
|
101
|
+
return found
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def hopper_is_owned(leader: int, hopper: Path) -> bool:
|
|
105
|
+
expected = hopper.resolve()
|
|
106
|
+
for pid in descendants(leader):
|
|
107
|
+
try:
|
|
108
|
+
if Path(f"/proc/{pid}/exe").resolve() == expected:
|
|
109
|
+
return True
|
|
110
|
+
except OSError:
|
|
111
|
+
continue
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def windows(x11: ctypes.CDLL, display: int, root: int) -> list[tuple[int, int, int, int, int]]:
|
|
116
|
+
found: list[tuple[int, int, int, int, int]] = []
|
|
117
|
+
pending = [root]
|
|
118
|
+
while pending:
|
|
119
|
+
window = pending.pop()
|
|
120
|
+
attributes = XWindowAttributes()
|
|
121
|
+
if x11.XGetWindowAttributes(display, window, ctypes.byref(attributes)):
|
|
122
|
+
if attributes.map_state == 2:
|
|
123
|
+
found.append(
|
|
124
|
+
(window, attributes.x, attributes.y, attributes.width, attributes.height)
|
|
125
|
+
)
|
|
126
|
+
root_return = ctypes.c_ulong()
|
|
127
|
+
parent_return = ctypes.c_ulong()
|
|
128
|
+
children = ctypes.POINTER(ctypes.c_ulong)()
|
|
129
|
+
count = ctypes.c_uint()
|
|
130
|
+
if x11.XQueryTree(
|
|
131
|
+
display,
|
|
132
|
+
window,
|
|
133
|
+
ctypes.byref(root_return),
|
|
134
|
+
ctypes.byref(parent_return),
|
|
135
|
+
ctypes.byref(children),
|
|
136
|
+
ctypes.byref(count),
|
|
137
|
+
):
|
|
138
|
+
pending.extend(children[index] for index in range(count.value))
|
|
139
|
+
if children:
|
|
140
|
+
x11.XFree(children)
|
|
141
|
+
return found
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def configure_x11(x11: ctypes.CDLL, xtst: ctypes.CDLL) -> None:
|
|
145
|
+
window_pointer = ctypes.POINTER(ctypes.c_ulong)
|
|
146
|
+
x11.XOpenDisplay.argtypes = [ctypes.c_char_p]
|
|
147
|
+
x11.XOpenDisplay.restype = ctypes.c_void_p
|
|
148
|
+
x11.XDefaultRootWindow.argtypes = [ctypes.c_void_p]
|
|
149
|
+
x11.XDefaultRootWindow.restype = ctypes.c_ulong
|
|
150
|
+
x11.XDisplayWidth.argtypes = [ctypes.c_void_p, ctypes.c_int]
|
|
151
|
+
x11.XDisplayWidth.restype = ctypes.c_int
|
|
152
|
+
x11.XDisplayHeight.argtypes = [ctypes.c_void_p, ctypes.c_int]
|
|
153
|
+
x11.XDisplayHeight.restype = ctypes.c_int
|
|
154
|
+
x11.XGetWindowAttributes.argtypes = [
|
|
155
|
+
ctypes.c_void_p,
|
|
156
|
+
ctypes.c_ulong,
|
|
157
|
+
ctypes.POINTER(XWindowAttributes),
|
|
158
|
+
]
|
|
159
|
+
x11.XGetWindowAttributes.restype = ctypes.c_int
|
|
160
|
+
x11.XQueryTree.argtypes = [
|
|
161
|
+
ctypes.c_void_p,
|
|
162
|
+
ctypes.c_ulong,
|
|
163
|
+
window_pointer,
|
|
164
|
+
window_pointer,
|
|
165
|
+
ctypes.POINTER(window_pointer),
|
|
166
|
+
ctypes.POINTER(ctypes.c_uint),
|
|
167
|
+
]
|
|
168
|
+
x11.XQueryTree.restype = ctypes.c_int
|
|
169
|
+
x11.XFree.argtypes = [ctypes.c_void_p]
|
|
170
|
+
x11.XFree.restype = ctypes.c_int
|
|
171
|
+
x11.XFlush.argtypes = [ctypes.c_void_p]
|
|
172
|
+
x11.XFlush.restype = ctypes.c_int
|
|
173
|
+
x11.XCloseDisplay.argtypes = [ctypes.c_void_p]
|
|
174
|
+
x11.XCloseDisplay.restype = ctypes.c_int
|
|
175
|
+
xtst.XTestFakeMotionEvent.argtypes = [
|
|
176
|
+
ctypes.c_void_p,
|
|
177
|
+
ctypes.c_int,
|
|
178
|
+
ctypes.c_int,
|
|
179
|
+
ctypes.c_int,
|
|
180
|
+
ctypes.c_ulong,
|
|
181
|
+
]
|
|
182
|
+
xtst.XTestFakeMotionEvent.restype = ctypes.c_int
|
|
183
|
+
xtst.XTestFakeButtonEvent.argtypes = [
|
|
184
|
+
ctypes.c_void_p,
|
|
185
|
+
ctypes.c_uint,
|
|
186
|
+
ctypes.c_int,
|
|
187
|
+
ctypes.c_ulong,
|
|
188
|
+
]
|
|
189
|
+
xtst.XTestFakeButtonEvent.restype = ctypes.c_int
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def start_xvfb(session: Path) -> tuple[subprocess.Popen[bytes], str, Path]:
|
|
193
|
+
authority = session / "Xauthority"
|
|
194
|
+
for _ in range(20):
|
|
195
|
+
display_number = 100 + secrets.randbelow(900)
|
|
196
|
+
display_name = f":{display_number}"
|
|
197
|
+
if Path(f"/tmp/.X11-unix/X{display_number}").exists():
|
|
198
|
+
continue
|
|
199
|
+
cookie = secrets.token_hex(16)
|
|
200
|
+
try:
|
|
201
|
+
subprocess.run(
|
|
202
|
+
[
|
|
203
|
+
"/usr/bin/xauth", "-f", str(authority), "add",
|
|
204
|
+
display_name, ".", cookie,
|
|
205
|
+
],
|
|
206
|
+
check=True,
|
|
207
|
+
stdout=subprocess.DEVNULL,
|
|
208
|
+
stderr=subprocess.DEVNULL,
|
|
209
|
+
)
|
|
210
|
+
except (OSError, subprocess.CalledProcessError) as error:
|
|
211
|
+
raise AdapterFailure(
|
|
212
|
+
X11_AUTHORIZATION_FAILED, "private X authorization failed"
|
|
213
|
+
) from error
|
|
214
|
+
authority.chmod(0o600)
|
|
215
|
+
process = subprocess.Popen(
|
|
216
|
+
[
|
|
217
|
+
"/usr/bin/Xvfb",
|
|
218
|
+
display_name,
|
|
219
|
+
"-screen",
|
|
220
|
+
"0",
|
|
221
|
+
"1280x1024x24",
|
|
222
|
+
"-nolisten",
|
|
223
|
+
"tcp",
|
|
224
|
+
"-auth",
|
|
225
|
+
str(authority),
|
|
226
|
+
],
|
|
227
|
+
stdout=subprocess.DEVNULL,
|
|
228
|
+
stderr=subprocess.DEVNULL,
|
|
229
|
+
)
|
|
230
|
+
deadline = time.monotonic() + 2
|
|
231
|
+
socket = Path(f"/tmp/.X11-unix/X{display_number}")
|
|
232
|
+
while time.monotonic() < deadline and process.poll() is None:
|
|
233
|
+
if socket.exists():
|
|
234
|
+
return process, display_name, authority
|
|
235
|
+
time.sleep(0.05)
|
|
236
|
+
if process.poll() is None:
|
|
237
|
+
process.terminate()
|
|
238
|
+
process.wait()
|
|
239
|
+
raise AdapterFailure(PRIVATE_DISPLAY_UNAVAILABLE, "private X display unavailable")
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def main() -> int:
|
|
243
|
+
args = parse_args()
|
|
244
|
+
hopper = Path(args.hopper)
|
|
245
|
+
socket = Path(args.socket)
|
|
246
|
+
if digest(hopper) not in SUPPORTED_HOPPER_SHA256:
|
|
247
|
+
raise AdapterFailure(UNSUPPORTED_HOPPER_BUILD, "unsupported Hopper binary")
|
|
248
|
+
command = args.command[1:] if args.command[:1] == ["--"] else args.command
|
|
249
|
+
if not command or Path(command[0]).resolve() != hopper.resolve():
|
|
250
|
+
raise AdapterFailure(INVALID_LAUNCH_COMMAND, "invalid Hopper launch command")
|
|
251
|
+
xvfb, display_name, authority = start_xvfb(socket.parent)
|
|
252
|
+
child: subprocess.Popen[bytes] | None = None
|
|
253
|
+
display: int | None = None
|
|
254
|
+
try:
|
|
255
|
+
hopper_environment = dict(os.environ)
|
|
256
|
+
for name in ("WAYLAND_DISPLAY", "WAYLAND_SOCKET"):
|
|
257
|
+
hopper_environment.pop(name, None)
|
|
258
|
+
hopper_environment.update(
|
|
259
|
+
{
|
|
260
|
+
"DISPLAY": display_name,
|
|
261
|
+
"XAUTHORITY": str(authority),
|
|
262
|
+
"GDK_BACKEND": "x11",
|
|
263
|
+
"QT_QPA_PLATFORM": "xcb",
|
|
264
|
+
"XDG_SESSION_TYPE": "x11",
|
|
265
|
+
}
|
|
266
|
+
)
|
|
267
|
+
child = subprocess.Popen(command, env=hopper_environment)
|
|
268
|
+
leader = os.getpid()
|
|
269
|
+
os.environ["XAUTHORITY"] = str(authority)
|
|
270
|
+
|
|
271
|
+
try:
|
|
272
|
+
x11 = ctypes.CDLL("libX11.so.6")
|
|
273
|
+
xtst = ctypes.CDLL("libXtst.so.6")
|
|
274
|
+
except OSError as error:
|
|
275
|
+
raise AdapterFailure(
|
|
276
|
+
RUNTIME_DEPENDENCY_UNAVAILABLE, "X11 runtime unavailable"
|
|
277
|
+
) from error
|
|
278
|
+
configure_x11(x11, xtst)
|
|
279
|
+
display = x11.XOpenDisplay(display_name.encode())
|
|
280
|
+
if not display:
|
|
281
|
+
raise AdapterFailure(PRIVATE_DISPLAY_UNAVAILABLE, "private X display unavailable")
|
|
282
|
+
root = x11.XDefaultRootWindow(display)
|
|
283
|
+
if (
|
|
284
|
+
x11.XDisplayWidth(display, 0),
|
|
285
|
+
x11.XDisplayHeight(display, 0),
|
|
286
|
+
) != EXPECTED_SCREEN:
|
|
287
|
+
raise AdapterFailure(
|
|
288
|
+
UNEXPECTED_DISPLAY_GEOMETRY, "unexpected private display geometry"
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
deadline = time.monotonic() + 10
|
|
292
|
+
observed: list[tuple[int, int, int, int, int]] = []
|
|
293
|
+
while time.monotonic() < deadline:
|
|
294
|
+
if socket.exists():
|
|
295
|
+
x11.XCloseDisplay(display)
|
|
296
|
+
display = None
|
|
297
|
+
return child.wait()
|
|
298
|
+
if not hopper_is_owned(leader, hopper):
|
|
299
|
+
if child.poll() is None:
|
|
300
|
+
time.sleep(0.1)
|
|
301
|
+
continue
|
|
302
|
+
raise AdapterFailure(
|
|
303
|
+
HOPPER_EXITED_DURING_STARTUP, "owned Hopper process exited"
|
|
304
|
+
)
|
|
305
|
+
observed = windows(x11, display, root)
|
|
306
|
+
if any(window[1:] == EXPECTED_DIALOG for window in observed):
|
|
307
|
+
break
|
|
308
|
+
time.sleep(0.1)
|
|
309
|
+
else:
|
|
310
|
+
if not hopper_is_owned(leader, hopper):
|
|
311
|
+
raise AdapterFailure(
|
|
312
|
+
PROCESS_OWNERSHIP_MISMATCH, "Hopper process ownership mismatch"
|
|
313
|
+
)
|
|
314
|
+
raise AdapterFailure(
|
|
315
|
+
UNSUPPORTED_DEMO_DIALOG, "expected Hopper demo dialog not found"
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
input_results = (
|
|
319
|
+
xtst.XTestFakeMotionEvent(display, -1, DEMO_CLICK[0], DEMO_CLICK[1], 0),
|
|
320
|
+
xtst.XTestFakeButtonEvent(display, 1, True, 0),
|
|
321
|
+
xtst.XTestFakeButtonEvent(display, 1, False, 0),
|
|
322
|
+
)
|
|
323
|
+
if not all(input_results):
|
|
324
|
+
raise AdapterFailure(X11_INPUT_FAILED, "private X input failed")
|
|
325
|
+
x11.XFlush(display)
|
|
326
|
+
x11.XCloseDisplay(display)
|
|
327
|
+
display = None
|
|
328
|
+
return child.wait()
|
|
329
|
+
finally:
|
|
330
|
+
if display is not None:
|
|
331
|
+
x11.XCloseDisplay(display)
|
|
332
|
+
if child is not None and child.poll() is None:
|
|
333
|
+
child.terminate()
|
|
334
|
+
child.wait()
|
|
335
|
+
if xvfb.poll() is None:
|
|
336
|
+
xvfb.terminate()
|
|
337
|
+
try:
|
|
338
|
+
xvfb.wait(timeout=2)
|
|
339
|
+
except subprocess.TimeoutExpired:
|
|
340
|
+
xvfb.kill()
|
|
341
|
+
xvfb.wait()
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
if __name__ == "__main__":
|
|
345
|
+
try:
|
|
346
|
+
raise SystemExit(main())
|
|
347
|
+
except AdapterFailure as error:
|
|
348
|
+
print(str(error), file=os.sys.stderr)
|
|
349
|
+
raise SystemExit(error.code) from None
|
package/scripts/rea.mjs
CHANGED
|
@@ -11,9 +11,12 @@ const isMcpMode =
|
|
|
11
11
|
args[0] === "--mcp" || (args.length === 1 && args[0] === "mcp");
|
|
12
12
|
|
|
13
13
|
if (isMcpMode) {
|
|
14
|
-
const {
|
|
15
|
-
|
|
14
|
+
const { runEntrypoint } = await import("../dist/main.js");
|
|
15
|
+
await runEntrypoint();
|
|
16
16
|
} else {
|
|
17
17
|
const { createCli } = await import("../dist/cli.js");
|
|
18
|
-
await
|
|
18
|
+
const { sanitizeCliOutput } = await import("../dist/cliOutput.js");
|
|
19
|
+
await createCli().serve(args, {
|
|
20
|
+
stdout: (output) => process.stdout.write(sanitizeCliOutput(output)),
|
|
21
|
+
});
|
|
19
22
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: rea-analysis
|
|
3
3
|
description: Reverse engineer apps with REA. Explore how features work, then build a version tailored to your project.
|
|
4
4
|
metadata:
|
|
5
|
-
version: "
|
|
5
|
+
version: "12"
|
|
6
6
|
tool_count: 68
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -24,7 +24,7 @@ Notes is only a documentation example. Never select an app unless the user names
|
|
|
24
24
|
2. If setup is needed, tell the user REA needs to install its local binary-analysis tools. Do not lead with implementation details or assume the user knows reverse-engineering products.
|
|
25
25
|
3. Before installing external software, obtain approval and identify what will be installed. If deeper analysis needs Hopper, describe it as REA's local analysis engine and note that it is a separate Mac app with its own license. Then run `npx -y rea-agents setup --yes`.
|
|
26
26
|
4. If macOS or an installer requests human input, tell the user exactly what needs attention. After they finish, rerun setup and doctor.
|
|
27
|
-
5. If setup registers a new MCP server, tell the user to restart their
|
|
27
|
+
5. If setup registers a new MCP server, tell the user to restart their agent to load all REA tools. Direct CLI commands remain available before restart.
|
|
28
28
|
|
|
29
29
|
## Locate the app
|
|
30
30
|
|
|
@@ -109,6 +109,13 @@ bundle digests to anchor paginated reports.
|
|
|
109
109
|
Use `find_changed_behavior` to combine existing comparison Evidence. Runtime
|
|
110
110
|
process differences are observed changes; artifact and function differences
|
|
111
111
|
remain static candidates, not causal proof. Supply complete comparison pages.
|
|
112
|
+
For an automatic two-version artifact investigation, use its
|
|
113
|
+
`investigation_run` mode with explicit write approval and a workspace beneath
|
|
114
|
+
`REA_EVIDENCE_ROOTS_JSON`. It checkpoints inventory, comparison, and report
|
|
115
|
+
Evidence in monotonic CAS-linked revisions. Repeating the same content and
|
|
116
|
+
budgets resumes or reuses the run. This automatic mode does not execute either
|
|
117
|
+
version or perform fuzzy function matching, so its behavior status remains
|
|
118
|
+
unknown without separate controlled runtime Evidence.
|
|
112
119
|
|
|
113
120
|
Use `build_call_path` with explicit `analyze_function` Evidence groups from one
|
|
114
121
|
artifact and provider. Select endpoints by exact address. Missing dossiers,
|