rea-agents 1.2.0 → 1.3.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 +74 -27
- package/bridge/hopper_bridge.py +161 -12
- package/dist/application/AnalysisSnapshotCache.js +131 -0
- package/dist/application/AnalysisSnapshotFiles.js +31 -0
- package/dist/application/ArtifactInventory.js +4 -0
- package/dist/application/AuthorizedArtifactInventory.js +16 -0
- package/dist/application/BinarySession.js +66 -36
- package/dist/application/BinarySessionPort.js +1 -0
- package/dist/application/BoundedJsonFiles.js +109 -0
- package/dist/application/CrossVersionInventory.js +98 -0
- package/dist/application/CrossVersionInvestigation.js +295 -0
- package/dist/application/DirectAnalysis.js +115 -22
- package/dist/application/Doctor.js +30 -1
- 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/ProcessCaptureAuthority.js +2 -2
- package/dist/application/ProcessCaptureError.js +12 -0
- package/dist/application/ProcessCaptureLifecycle.js +19 -1
- package/dist/application/ProcessCli.js +98 -31
- package/dist/application/ProcessHarness.js +2 -2
- package/dist/application/ReferenceSourceImportEntries.js +18 -3
- package/dist/application/ReferenceSourceImportTypes.js +32 -0
- package/dist/application/Setup.js +33 -60
- package/dist/application/SetupClients.js +19 -0
- package/dist/application/SetupInstallFailure.js +53 -0
- package/dist/application/SetupPlan.js +31 -0
- package/dist/application/Uninstall.js +33 -23
- package/dist/application/UnknownEvidence.js +33 -0
- package/dist/application/Upgrade.js +1 -1
- package/dist/application/runtime.js +1 -1
- package/dist/artifacts/ArtifactProvider.js +2 -5
- package/dist/cli.js +31 -11
- package/dist/cliEvidenceCommands.js +11 -12
- package/dist/cliInvestigationCommands.js +86 -0
- package/dist/cliOutput.js +41 -0
- package/dist/cliProcessCommands.js +14 -4
- package/dist/config.js +39 -24
- 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 +10 -6
- package/dist/contracts/toolOutputSchemas.js +33 -4
- package/dist/domain/analysisSnapshot.js +149 -0
- package/dist/domain/changedBehavior.js +30 -7
- package/dist/domain/errors.js +180 -41
- 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 +214 -0
- package/dist/domain/jsonValue.js +2 -0
- package/dist/domain/nativeInspection.js +3 -2
- package/dist/domain/processCapture.js +5 -4
- package/dist/domain/processComparison.js +5 -4
- package/dist/hopper/BridgeLauncher.js +47 -20
- package/dist/hopper/HopperClient.js +17 -2
- package/dist/hopper/HopperProvider.js +1 -0
- package/dist/main.js +47 -24
- package/dist/server/createServer.js +2 -0
- package/dist/server/promptCompletion.js +144 -0
- package/dist/server/registerEnhancedTools.js +2 -6
- package/dist/server/registerEvidenceTools.js +2 -8
- package/dist/server/registerFunctionComparisonTool.js +1 -1
- package/dist/server/registerInvestigationTools.js +31 -11
- package/dist/server/registerOfficialTools.js +6 -13
- package/dist/server/registerProcessComparisonTool.js +1 -7
- package/dist/server/registerPrompts.js +67 -0
- package/dist/server/registerSessionStatusTool.js +11 -0
- package/dist/server/registerSessionTools.js +52 -18
- package/dist/server/sessionEvidence.js +2 -8
- package/dist/server/sessionToolPolicies.js +1 -42
- package/dist/server/toolResult.js +7 -3
- package/install.sh +11 -11
- package/package.json +5 -3
- package/scripts/hopper-demo-x11.py +349 -0
- package/scripts/rea.mjs +6 -3
- package/skills/rea-analysis/SKILL.md +9 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { projectAnalysisError } from "../domain/errors.js";
|
|
1
|
+
import { AnalysisOutputError, projectAnalysisError, } from "../domain/errors.js";
|
|
2
2
|
/**
|
|
3
3
|
* Translate an application result into MCP text content.
|
|
4
4
|
* Error tags and safe messages remain visible while underlying causes, process
|
|
@@ -11,9 +11,10 @@ const errorResult = (error) => {
|
|
|
11
11
|
content: [
|
|
12
12
|
{
|
|
13
13
|
type: "text",
|
|
14
|
-
text:
|
|
14
|
+
text: projected.message,
|
|
15
15
|
},
|
|
16
16
|
],
|
|
17
|
+
structuredContent: { error: projected },
|
|
17
18
|
isError: true,
|
|
18
19
|
};
|
|
19
20
|
};
|
|
@@ -25,9 +26,12 @@ const successResult = (value, contract) => {
|
|
|
25
26
|
content: [
|
|
26
27
|
{
|
|
27
28
|
type: "text",
|
|
28
|
-
text: "
|
|
29
|
+
text: "Analysis returned an unreadable result. Retry once; if it continues, run `rea doctor`.",
|
|
29
30
|
},
|
|
30
31
|
],
|
|
32
|
+
structuredContent: {
|
|
33
|
+
error: projectAnalysisError(new AnalysisOutputError(contract.name, "output does not match the tool contract")),
|
|
34
|
+
},
|
|
31
35
|
isError: true,
|
|
32
36
|
};
|
|
33
37
|
}
|
package/install.sh
CHANGED
|
@@ -17,7 +17,7 @@ usage() {
|
|
|
17
17
|
cat <<'EOF'
|
|
18
18
|
Usage: install.sh [--version VERSION] [--dry-run] [--no-setup] [--no-prompt] [--verbose]
|
|
19
19
|
|
|
20
|
-
Installs only the REA CLI. Run `rea setup` separately to configure Hopper and
|
|
20
|
+
Installs only the REA CLI. Run `rea setup` separately to configure Hopper and agents.
|
|
21
21
|
EOF
|
|
22
22
|
}
|
|
23
23
|
|
|
@@ -54,13 +54,13 @@ fi
|
|
|
54
54
|
|
|
55
55
|
platform="$(uname -s)"
|
|
56
56
|
[[ "$platform" == "Darwin" || "$platform" == "Linux" ]] || fail "REA supports macOS and selected 64-bit Linux distributions."
|
|
57
|
-
command -v curl >/dev/null 2>&1 || fail "curl is required."
|
|
57
|
+
command -v curl >/dev/null 2>&1 || fail "curl is required. Install curl, then rerun this installer."
|
|
58
58
|
command -v node >/dev/null 2>&1 || fail "Node.js 22.19+ or 24.11+ is required; install it from https://nodejs.org and retry."
|
|
59
59
|
command -v npm >/dev/null 2>&1 || fail "npm is required; install it with Node.js and retry."
|
|
60
60
|
|
|
61
61
|
node_version="$(node -p 'process.versions.node' 2>/dev/null || true)"
|
|
62
62
|
IFS=. read -r node_major node_minor _node_patch <<<"$node_version"
|
|
63
|
-
[[ "$node_major" =~ ^[0-9]+$ && "$node_minor" =~ ^[0-9]+$ ]] || fail "
|
|
63
|
+
[[ "$node_major" =~ ^[0-9]+$ && "$node_minor" =~ ^[0-9]+$ ]] || fail "the active Node.js version could not be read. Check that node works and is on PATH, then retry."
|
|
64
64
|
if ! ((node_major == 22 && node_minor >= 19 || node_major >= 24)); then
|
|
65
65
|
fail "Node.js $node_version is unsupported; use Node.js 22.19+ or 24.11+."
|
|
66
66
|
fi
|
|
@@ -71,9 +71,9 @@ fi
|
|
|
71
71
|
if [[ -n "$version" ]]; then
|
|
72
72
|
[[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]] || fail "version must be an exact semantic version."
|
|
73
73
|
else
|
|
74
|
-
release_json="$(curl -fsSL --proto '=https' --tlsv1.2 --retry 3 -H 'Accept: application/vnd.github+json' "https://api.github.com/repos/$REPOSITORY/releases/latest")" || fail "could not
|
|
75
|
-
tag="$(printf '%s' "$release_json" | node -e 'let s="";process.stdin.on("data",c=>s+=c).on("end",()=>{const v=JSON.parse(s).tag_name;if(typeof v!=="string")process.exit(1);process.stdout.write(v)})')" || fail "
|
|
76
|
-
[[ "$tag" =~ ^rea-agents-([0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?)$ ]] || fail "latest release tag
|
|
74
|
+
release_json="$(curl -fsSL --proto '=https' --tlsv1.2 --retry 3 -H 'Accept: application/vnd.github+json' "https://api.github.com/repos/$REPOSITORY/releases/latest")" || fail "the latest REA release could not be resolved. Check network access or pass --version VERSION, then retry."
|
|
75
|
+
tag="$(printf '%s' "$release_json" | node -e 'let s="";process.stdin.on("data",c=>s+=c).on("end",()=>{const v=JSON.parse(s).tag_name;if(typeof v!=="string")process.exit(1);process.stdout.write(v)})' 2>/dev/null)" || fail "the release response was invalid. Retry later or pass --version VERSION."
|
|
76
|
+
[[ "$tag" =~ ^rea-agents-([0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?)$ ]] || fail "the latest release tag was invalid. Retry later or pass --version VERSION."
|
|
77
77
|
version="${BASH_REMATCH[1]}"
|
|
78
78
|
fi
|
|
79
79
|
|
|
@@ -82,7 +82,7 @@ if [[ "$platform" == "Linux" ]]; then
|
|
|
82
82
|
prefix_args=(--prefix "$HOME/.local")
|
|
83
83
|
install_bin="$HOME/.local/bin/rea"
|
|
84
84
|
else
|
|
85
|
-
npm_prefix="$(npm prefix --global)" || fail "could not
|
|
85
|
+
npm_prefix="$(npm prefix --global)" || fail "the npm global prefix could not be read. Repair the npm configuration, then retry."
|
|
86
86
|
install_bin="$npm_prefix/bin/rea"
|
|
87
87
|
fi
|
|
88
88
|
|
|
@@ -99,9 +99,9 @@ if [[ "$dry_run" == true ]]; then
|
|
|
99
99
|
fi
|
|
100
100
|
|
|
101
101
|
printf 'Installing %s@%s...\n' "$PACKAGE" "$version"
|
|
102
|
-
npm install --global "${prefix_args[@]}" "$PACKAGE@$version" || fail "npm
|
|
103
|
-
[[ -x "$install_bin" ]] || fail "the rea command
|
|
104
|
-
installed_version="$("$install_bin" --version 2>/dev/null | tr -d '[:space:]')" || fail "could not read the
|
|
102
|
+
npm install --global "${prefix_args[@]}" "$PACKAGE@$version" || fail "npm could not install REA. Check registry access and npm permissions, then retry."
|
|
103
|
+
[[ -x "$install_bin" ]] || fail "npm completed without installing the rea command. Check the npm global bin directory and PATH, then retry."
|
|
104
|
+
installed_version="$("$install_bin" --version 2>/dev/null | tr -d '[:space:]')" || fail "the installed REA version could not be read. Reinstall the requested version, then retry."
|
|
105
105
|
[[ "$installed_version" == "$version" ]] || fail "installed version $installed_version does not match $version."
|
|
106
106
|
|
|
107
107
|
printf 'REA %s is installed.\n' "$version"
|
|
@@ -109,5 +109,5 @@ if [[ "$start_setup" == true && -r /dev/tty && -w /dev/tty && -t 1 ]]; then
|
|
|
109
109
|
printf 'Starting REA setup...\n'
|
|
110
110
|
"$install_bin" setup </dev/tty >/dev/tty
|
|
111
111
|
else
|
|
112
|
-
printf 'Run %s setup to configure Hopper and
|
|
112
|
+
printf 'Run %s setup to configure Hopper and agents.\n' "$install_bin"
|
|
113
113
|
fi
|
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.3.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,6 +43,7 @@
|
|
|
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"
|
|
@@ -69,6 +70,7 @@
|
|
|
69
70
|
"typecheck": "tsc --noEmit",
|
|
70
71
|
"verify:hopper": "npm run build && node scripts/verify-real-hopper.mjs",
|
|
71
72
|
"verify:hopper:linux": "npm run build && node scripts/verify-real-hopper-linux.mjs",
|
|
73
|
+
"verify:setup:linux": "npm run build && node scripts/verify-linux-setup-e2e.mjs",
|
|
72
74
|
"build:fixtures": "node scripts/build-conformance-fixtures.mjs",
|
|
73
75
|
"verify:fixtures": "npm run build:fixtures && node scripts/verify-conformance-fixtures.mjs",
|
|
74
76
|
"verify:package": "npm run build && node scripts/verify-package.mjs",
|
|
@@ -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,
|