rea-agents 0.5.0 → 1.1.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 +40 -16
- package/dist/application/Doctor.js +4 -4
- package/dist/application/EnhancedTools.js +44 -36
- package/dist/application/EvidenceBundleFiles.js +4 -3
- package/dist/application/MacHopper.js +175 -0
- package/dist/application/Setup.js +83 -54
- package/dist/application/Upgrade.js +106 -0
- package/dist/cli.js +37 -3
- package/dist/contracts/toolContracts.js +3 -3
- package/dist/contracts/toolOutputSchemas.js +48 -10
- package/dist/domain/errors.js +26 -0
- package/dist/domain/hopperValues.js +2 -0
- package/dist/domain/runtimeVersion.js +8 -0
- package/dist/native/NativeMacOSProvider.js +2 -0
- package/install.sh +79 -164
- package/package.json +3 -4
|
@@ -1,30 +1,30 @@
|
|
|
1
1
|
import { access, copyFile, mkdir, readFile, rename, rm, writeFile, } from "node:fs/promises";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
|
-
import { execFile } from "node:child_process";
|
|
5
|
-
import { promisify } from "node:util";
|
|
6
4
|
import { z } from "zod";
|
|
7
5
|
import writeFileAtomic from "write-file-atomic";
|
|
8
6
|
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
9
7
|
import { PRODUCT_IDENTITY } from "../identity.js";
|
|
8
|
+
import { supportsNodeVersion } from "../domain/runtimeVersion.js";
|
|
10
9
|
import { runDoctor, systemDoctorHost } from "./Doctor.js";
|
|
11
|
-
import { probeHomebrew } from "./homebrew.js";
|
|
12
10
|
import { installLinuxHopper, readLinuxDistribution, } from "./LinuxHopper.js";
|
|
13
|
-
|
|
11
|
+
import { installMacHopper } from "./MacHopper.js";
|
|
14
12
|
const registrationCommand = () => process.env.npm_command === "exec"
|
|
15
13
|
? ["npx", "-y", PRODUCT_IDENTITY.packageName, "mcp"]
|
|
16
14
|
: [resolve(process.argv[1] ?? PRODUCT_IDENTITY.cliBinary), "mcp"];
|
|
17
15
|
/**
|
|
18
16
|
* Install prerequisites and configure detected clients idempotently.
|
|
19
|
-
*
|
|
20
|
-
*
|
|
17
|
+
* Discovery always precedes mutation. Interactive confirmation or explicit
|
|
18
|
+
* unattended options authorize only the actions represented in the result.
|
|
21
19
|
*/
|
|
22
|
-
export const runSetup = async (
|
|
23
|
-
const
|
|
20
|
+
export const runSetup = async (options, host = systemSetupHost(), confirm) => {
|
|
21
|
+
const appliedActions = [];
|
|
22
|
+
let plannedActions = [];
|
|
24
23
|
const clients = {};
|
|
25
24
|
const fail = async (remediation) => ({
|
|
26
25
|
status: "needs_human",
|
|
27
|
-
|
|
26
|
+
plannedActions,
|
|
27
|
+
appliedActions,
|
|
28
28
|
clients,
|
|
29
29
|
doctor: await host.doctor(),
|
|
30
30
|
remediation,
|
|
@@ -32,43 +32,85 @@ export const runSetup = async (yes, host = systemSetupHost()) => {
|
|
|
32
32
|
const unsupported = await hostRemediation(host);
|
|
33
33
|
if (unsupported !== undefined)
|
|
34
34
|
return fail(unsupported);
|
|
35
|
-
if (host.platform === "darwin" && !(await host.hasHomebrew())) {
|
|
36
|
-
if (!yes)
|
|
37
|
-
return fail("Re-run with --yes to install Homebrew.");
|
|
38
|
-
if (!(await host.installHomebrew()))
|
|
39
|
-
return fail("Homebrew installation was interrupted; resolve the installer prompt and re-run setup.");
|
|
40
|
-
actions.push("installed_homebrew");
|
|
41
|
-
}
|
|
42
35
|
let hopperPath = await host.hopperPath();
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
36
|
+
const detectedClients = await host.detectedClients();
|
|
37
|
+
plannedActions = [
|
|
38
|
+
...(hopperPath === undefined
|
|
39
|
+
? [
|
|
40
|
+
{
|
|
41
|
+
kind: "install_hopper",
|
|
42
|
+
target: host.platform === "darwin"
|
|
43
|
+
? "~/Applications/Hopper Disassembler.app"
|
|
44
|
+
: "system package manager",
|
|
45
|
+
detail: "Download the official Hopper package, verify it, install it, and open Hopper for activation.",
|
|
46
|
+
external: true,
|
|
47
|
+
},
|
|
48
|
+
]
|
|
49
|
+
: []),
|
|
50
|
+
...detectedClients
|
|
51
|
+
.filter(({ format }) => format !== "unsupported")
|
|
52
|
+
.map((client) => ({
|
|
53
|
+
kind: "configure_client",
|
|
54
|
+
target: client.configPath,
|
|
55
|
+
detail: `Add the REA MCP registration for ${client.name}; preserve unrelated configuration.`,
|
|
56
|
+
external: false,
|
|
57
|
+
})),
|
|
58
|
+
{
|
|
59
|
+
kind: "install_skill",
|
|
60
|
+
target: "~/.agents/skills/rea-analysis/SKILL.md",
|
|
61
|
+
detail: "Install or update the bundled REA analysis skill.",
|
|
62
|
+
external: false,
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
let approved = options.approved;
|
|
66
|
+
let interactiveApproval = false;
|
|
67
|
+
if (!approved && confirm !== undefined && !options.structured) {
|
|
68
|
+
interactiveApproval = await confirm(plannedActions);
|
|
69
|
+
approved = interactiveApproval;
|
|
70
|
+
}
|
|
71
|
+
if (!approved)
|
|
72
|
+
return {
|
|
73
|
+
status: confirm === undefined ? "needs_confirmation" : "planned",
|
|
74
|
+
plannedActions,
|
|
75
|
+
appliedActions,
|
|
76
|
+
clients,
|
|
77
|
+
doctor: await host.doctor(),
|
|
78
|
+
remediation: "Review the setup plan, then rerun interactively or with --yes.",
|
|
79
|
+
};
|
|
80
|
+
if (hopperPath === undefined &&
|
|
81
|
+
(interactiveApproval || options.installHopper)) {
|
|
82
|
+
hopperPath = await host.installHopper();
|
|
50
83
|
if (hopperPath === undefined)
|
|
51
|
-
return
|
|
84
|
+
return {
|
|
85
|
+
status: "needs_human",
|
|
86
|
+
plannedActions,
|
|
87
|
+
appliedActions,
|
|
88
|
+
clients,
|
|
89
|
+
doctor: await host.doctor(),
|
|
90
|
+
remediation: "Hopper installation failed; install Hopper manually or rerun setup after resolving the reported system error.",
|
|
91
|
+
};
|
|
92
|
+
appliedActions.push("installed_hopper");
|
|
52
93
|
}
|
|
53
|
-
for (const client of
|
|
94
|
+
for (const client of detectedClients) {
|
|
54
95
|
const result = await host.configureClient(client, hopperPath, registrationCommand());
|
|
55
96
|
clients[client.name] = result;
|
|
56
97
|
if (result.status === "failed")
|
|
57
98
|
return fail(`${client.name} configuration ${result.reason} verification failed; no successful configuration was reported.`);
|
|
58
99
|
if (result.status === "configured")
|
|
59
|
-
|
|
100
|
+
appliedActions.push(`configured_${client.name}`);
|
|
60
101
|
}
|
|
61
102
|
const skill = await host.installSkill();
|
|
62
103
|
if (skill === "failed")
|
|
63
104
|
return fail("Agent skill installation or readback failed.");
|
|
64
105
|
if (skill === "installed")
|
|
65
|
-
|
|
106
|
+
appliedActions.push("installed_skill");
|
|
66
107
|
const doctor = await host.doctor();
|
|
67
|
-
const activationRequired =
|
|
108
|
+
const activationRequired = appliedActions.includes("installed_hopper");
|
|
68
109
|
const ready = doctor.healthy && !activationRequired;
|
|
69
110
|
return {
|
|
70
111
|
status: ready ? "ready" : "needs_human",
|
|
71
|
-
|
|
112
|
+
plannedActions,
|
|
113
|
+
appliedActions,
|
|
72
114
|
clients,
|
|
73
115
|
doctor,
|
|
74
116
|
...(ready
|
|
@@ -76,15 +118,17 @@ export const runSetup = async (yes, host = systemSetupHost()) => {
|
|
|
76
118
|
: {
|
|
77
119
|
remediation: activationRequired
|
|
78
120
|
? "Open Hopper, complete its one-time activation, then rerun rea doctor --json."
|
|
79
|
-
:
|
|
121
|
+
: hopperPath === undefined
|
|
122
|
+
? "Hopper is optional for non-Hopper providers. Rerun with --yes --install-hopper for deep native analysis."
|
|
123
|
+
: "Run rea doctor and apply each reported remediation.",
|
|
80
124
|
}),
|
|
81
125
|
};
|
|
82
126
|
};
|
|
83
127
|
const hostRemediation = async (host) => {
|
|
84
128
|
if (host.platform !== "darwin" && host.platform !== "linux")
|
|
85
129
|
return "REA supports Hopper on macOS and selected 64-bit Linux distributions.";
|
|
86
|
-
if (
|
|
87
|
-
return "Install Node.js
|
|
130
|
+
if (!supportsNodeVersion(host.nodeVersion))
|
|
131
|
+
return "Install Node.js 22.19+ or 24.11+ and rerun setup.";
|
|
88
132
|
if (host.platform === "darwin") {
|
|
89
133
|
const version = await host.macosVersion();
|
|
90
134
|
return version === undefined || major(version) < 12
|
|
@@ -95,7 +139,7 @@ const hostRemediation = async (host) => {
|
|
|
95
139
|
? undefined
|
|
96
140
|
: "REA supports Hopper on Ubuntu 24.04+, Fedora 41+, and 64-bit Arch Linux.";
|
|
97
141
|
};
|
|
98
|
-
/** Production setup effects for
|
|
142
|
+
/** Production setup effects for Hopper, agent configuration, and the canonical skill directory. */
|
|
99
143
|
const systemSetupHost = () => {
|
|
100
144
|
const doctorHost = systemDoctorHost();
|
|
101
145
|
return {
|
|
@@ -103,15 +147,13 @@ const systemSetupHost = () => {
|
|
|
103
147
|
nodeVersion: process.versions.node,
|
|
104
148
|
macosVersion: () => doctorHost.macosVersion(),
|
|
105
149
|
linuxDistribution: readLinuxDistribution,
|
|
106
|
-
hasHomebrew: () => brewSucceeds(["--version"]),
|
|
107
|
-
installHomebrew: () => commandSucceeds("/bin/bash", [
|
|
108
|
-
"-c",
|
|
109
|
-
"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)",
|
|
110
|
-
], { ...process.env, NONINTERACTIVE: "1" }),
|
|
111
150
|
hopperPath: async () => (await runDoctor(undefined, doctorHost)).hopperPath,
|
|
112
|
-
installHopper: () =>
|
|
113
|
-
|
|
114
|
-
|
|
151
|
+
installHopper: async () => {
|
|
152
|
+
const result = process.platform === "linux"
|
|
153
|
+
? await installLinuxHopper()
|
|
154
|
+
: await installMacHopper();
|
|
155
|
+
return result.status === "installed" ? result.launcherPath : undefined;
|
|
156
|
+
},
|
|
115
157
|
detectedClients: () => detectClients(homedir()),
|
|
116
158
|
configureClient: (client, hopperPath, command) => client.format === "unsupported"
|
|
117
159
|
? Promise.resolve({ status: "skipped" })
|
|
@@ -351,19 +393,6 @@ export const installCanonicalSkill = async (home) => {
|
|
|
351
393
|
return "failed";
|
|
352
394
|
}
|
|
353
395
|
};
|
|
354
|
-
const commandSucceeds = async (command, args, env) => {
|
|
355
|
-
try {
|
|
356
|
-
await execFileAsync(command, args, env === undefined ? {} : { env });
|
|
357
|
-
return true;
|
|
358
|
-
}
|
|
359
|
-
catch {
|
|
360
|
-
return false;
|
|
361
|
-
}
|
|
362
|
-
};
|
|
363
|
-
const brewSucceeds = async (args) => {
|
|
364
|
-
const result = await probeHomebrew(async (command) => (await commandSucceeds(command, args)) ? true : undefined);
|
|
365
|
-
return result === true;
|
|
366
|
-
};
|
|
367
396
|
const major = (version) => Number.parseInt(version.split(".")[0] ?? "0", 10);
|
|
368
397
|
const exists = async (path) => {
|
|
369
398
|
try {
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { execFile, spawn } from "node:child_process";
|
|
2
|
+
import { realpath } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { PRODUCT_IDENTITY } from "../identity.js";
|
|
8
|
+
const execFileAsync = promisify(execFile);
|
|
9
|
+
const registryResponseSchema = z.object({ version: z.string().min(1) });
|
|
10
|
+
const UPGRADE_COMMAND = "npm install --global rea-agents@latest";
|
|
11
|
+
/** Check the npm registry and update the same global REA installation. */
|
|
12
|
+
export const runUpgrade = async (currentVersion, host = systemUpgradeHost(), output = "human") => {
|
|
13
|
+
const latestVersion = await host.latestVersion();
|
|
14
|
+
if (latestVersion === currentVersion)
|
|
15
|
+
return {
|
|
16
|
+
status: "current",
|
|
17
|
+
currentVersion,
|
|
18
|
+
latestVersion,
|
|
19
|
+
installMethod: "npm",
|
|
20
|
+
};
|
|
21
|
+
const installation = await host.installation();
|
|
22
|
+
if (installation === undefined)
|
|
23
|
+
return {
|
|
24
|
+
status: "failed",
|
|
25
|
+
currentVersion,
|
|
26
|
+
latestVersion: latestVersion ?? null,
|
|
27
|
+
reason: "unknown-install-method",
|
|
28
|
+
remediation: `Update manually with: ${UPGRADE_COMMAND}`,
|
|
29
|
+
};
|
|
30
|
+
if (!(await host.installLatest(installation, output)))
|
|
31
|
+
return {
|
|
32
|
+
status: "failed",
|
|
33
|
+
currentVersion,
|
|
34
|
+
latestVersion: latestVersion ?? null,
|
|
35
|
+
reason: "install",
|
|
36
|
+
remediation: `${UPGRADE_COMMAND} failed.`,
|
|
37
|
+
};
|
|
38
|
+
return {
|
|
39
|
+
status: "upgraded",
|
|
40
|
+
previousVersion: currentVersion,
|
|
41
|
+
latestVersion: latestVersion ?? null,
|
|
42
|
+
versionCheck: latestVersion === undefined ? "unavailable" : "available",
|
|
43
|
+
installMethod: "npm",
|
|
44
|
+
command: UPGRADE_COMMAND,
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
/** Create the npm registry and subprocess effects for a production upgrade. */
|
|
48
|
+
export const systemUpgradeHost = () => ({
|
|
49
|
+
latestVersion: async () => {
|
|
50
|
+
try {
|
|
51
|
+
const response = await fetch(`https://registry.npmjs.org/${PRODUCT_IDENTITY.packageName}/latest`, { signal: AbortSignal.timeout(10_000) });
|
|
52
|
+
if (!response.ok)
|
|
53
|
+
return undefined;
|
|
54
|
+
const parsed = registryResponseSchema.safeParse(await response.json());
|
|
55
|
+
return parsed.success ? parsed.data.version : undefined;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
installation: () => detectNpmInstallation(fileURLToPath(new URL("../..", import.meta.url)), systemNpmInstallationHost),
|
|
62
|
+
installLatest: ({ prefix }, output) => runCommand("npm", [
|
|
63
|
+
"install",
|
|
64
|
+
"--global",
|
|
65
|
+
"--prefix",
|
|
66
|
+
prefix,
|
|
67
|
+
`${PRODUCT_IDENTITY.packageName}@latest`,
|
|
68
|
+
], output),
|
|
69
|
+
});
|
|
70
|
+
const systemNpmInstallationHost = {
|
|
71
|
+
canonicalPath: realpath,
|
|
72
|
+
globalRoot: async () => (await execFileAsync("npm", ["root", "--global"])).stdout.trim(),
|
|
73
|
+
globalPrefix: async () => (await execFileAsync("npm", ["prefix", "--global"])).stdout.trim(),
|
|
74
|
+
};
|
|
75
|
+
/** Identify the npm prefix only when it owns the running package. */
|
|
76
|
+
export const detectNpmInstallation = async (packageRoot, host) => {
|
|
77
|
+
try {
|
|
78
|
+
const canonicalPackageRoot = await host.canonicalPath(packageRoot);
|
|
79
|
+
const npmRoot = await host.globalRoot();
|
|
80
|
+
const globalPackageRoot = await host.canonicalPath(resolve(npmRoot, PRODUCT_IDENTITY.packageName));
|
|
81
|
+
if (canonicalPackageRoot === globalPackageRoot) {
|
|
82
|
+
const prefix = await host.globalPrefix();
|
|
83
|
+
return prefix.length === 0 ? undefined : { prefix };
|
|
84
|
+
}
|
|
85
|
+
return inferUnixGlobalPrefix(canonicalPackageRoot);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return inferUnixGlobalPrefix(packageRoot);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
const inferUnixGlobalPrefix = (packageRoot) => {
|
|
92
|
+
const nodeModules = dirname(packageRoot);
|
|
93
|
+
const library = dirname(nodeModules);
|
|
94
|
+
if (basename(packageRoot) !== PRODUCT_IDENTITY.packageName ||
|
|
95
|
+
basename(nodeModules) !== "node_modules" ||
|
|
96
|
+
basename(library) !== "lib")
|
|
97
|
+
return undefined;
|
|
98
|
+
return { prefix: dirname(library) };
|
|
99
|
+
};
|
|
100
|
+
const runCommand = (command, arguments_, output) => new Promise((resolveResult) => {
|
|
101
|
+
const child = spawn(command, [...arguments_], {
|
|
102
|
+
stdio: output === "human" ? "inherit" : ["inherit", process.stderr, "inherit"],
|
|
103
|
+
});
|
|
104
|
+
child.once("error", () => resolveResult(false));
|
|
105
|
+
child.once("exit", (code) => resolveResult(code === 0));
|
|
106
|
+
});
|
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { Cli, z } from "incur";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { createInterface } from "node:readline/promises";
|
|
3
4
|
import { runDoctor } from "./application/Doctor.js";
|
|
4
5
|
import { runDirectAnalysis, runProviderAnalysis, runSessionStatus, } from "./application/DirectAnalysis.js";
|
|
5
6
|
import { runSetup } from "./application/Setup.js";
|
|
6
7
|
import { runUninstall } from "./application/Uninstall.js";
|
|
8
|
+
import { runUpgrade, systemUpgradeHost } from "./application/Upgrade.js";
|
|
7
9
|
import { PRODUCT_IDENTITY } from "./identity.js";
|
|
8
10
|
import { createLogger, parseLogLevel } from "./logger.js";
|
|
9
11
|
import { logCliCommand } from "./cliLogging.js";
|
|
@@ -59,10 +61,20 @@ const registerCoreCommands = (cli, logger) => {
|
|
|
59
61
|
yes: z
|
|
60
62
|
.boolean()
|
|
61
63
|
.default(false)
|
|
62
|
-
.describe("Approve
|
|
64
|
+
.describe("Approve user-owned setup actions without prompting"),
|
|
65
|
+
installHopper: z
|
|
66
|
+
.boolean()
|
|
67
|
+
.default(false)
|
|
68
|
+
.describe("Also approve Hopper installation with --yes"),
|
|
63
69
|
}),
|
|
64
|
-
alias: { yes: "y" },
|
|
65
|
-
run: ({ options }) => logCliCommand(logger, "setup", () => runSetup(
|
|
70
|
+
alias: { yes: "y", installHopper: "install-hopper" },
|
|
71
|
+
run: ({ options, formatExplicit }) => logCliCommand(logger, "setup", () => runSetup({
|
|
72
|
+
approved: options.yes,
|
|
73
|
+
installHopper: options.installHopper,
|
|
74
|
+
structured: formatExplicit,
|
|
75
|
+
}, undefined, options.yes || formatExplicit || process.stdin.isTTY !== true
|
|
76
|
+
? undefined
|
|
77
|
+
: confirmSetup)),
|
|
66
78
|
});
|
|
67
79
|
cli.command("doctor", {
|
|
68
80
|
description: "Check whether REA is ready",
|
|
@@ -82,6 +94,10 @@ const registerCoreCommands = (cli, logger) => {
|
|
|
82
94
|
alias: { purgeData: "purge-data" },
|
|
83
95
|
run: ({ options }) => logCliCommand(logger, "uninstall", () => runUninstall(options.purgeData)),
|
|
84
96
|
});
|
|
97
|
+
cli.command("upgrade", {
|
|
98
|
+
description: "Upgrade a global npm installation to the latest REA release",
|
|
99
|
+
run: ({ formatExplicit }) => logCliCommand(logger, "upgrade", () => runUpgrade(process.env.REA_PACKAGE_VERSION ?? "0.0.0-development", systemUpgradeHost(), formatExplicit ? "structured" : "human")),
|
|
100
|
+
});
|
|
85
101
|
cli.command("analyze", {
|
|
86
102
|
description: "Get an overview of an app",
|
|
87
103
|
args: z.object({
|
|
@@ -107,6 +123,24 @@ const registerCoreCommands = (cli, logger) => {
|
|
|
107
123
|
run: ({ args }) => logCliCommand(logger, "decompile", () => runDirectAnalysis(args.path, "procedure_pseudo_code", { procedure: args.address }, logger)),
|
|
108
124
|
});
|
|
109
125
|
};
|
|
126
|
+
const confirmSetup = async (actions) => {
|
|
127
|
+
process.stdout.write("\nREA setup plan\n");
|
|
128
|
+
for (const action of actions)
|
|
129
|
+
process.stdout.write(` - ${action.detail}\n ${action.target}${action.external ? " (external software)" : ""}\n`);
|
|
130
|
+
const prompt = createInterface({
|
|
131
|
+
input: process.stdin,
|
|
132
|
+
output: process.stdout,
|
|
133
|
+
});
|
|
134
|
+
try {
|
|
135
|
+
const answer = (await prompt.question("Continue? [Y/n] "))
|
|
136
|
+
.trim()
|
|
137
|
+
.toLowerCase();
|
|
138
|
+
return answer === "" || answer === "y" || answer === "yes";
|
|
139
|
+
}
|
|
140
|
+
finally {
|
|
141
|
+
prompt.close();
|
|
142
|
+
}
|
|
143
|
+
};
|
|
110
144
|
const registerXrefsCommand = (cli, logger) => {
|
|
111
145
|
cli.command("xrefs", {
|
|
112
146
|
description: "List bounded references to an analyzed address",
|
|
@@ -146,10 +146,10 @@ export const ENHANCED_TOOL_CONTRACTS = [
|
|
|
146
146
|
enhanced("swift_classes", "Discover legacy-mangled Swift class procedures after exhaustively paging analyzed procedures. Returns at most 100 entries and scans at most 5,000 symbols; use analyze_swift_types for other Swift kinds.", enhancedInputSchemas.swift_classes),
|
|
147
147
|
enhanced("get_objc_classes", "Discover and deduplicate Objective-C class labels after exhaustively paging names, optionally filtering by literal substring. Returns at most 100 classes; inspect matching metadata and references next.", enhancedInputSchemas.get_objc_classes),
|
|
148
148
|
enhanced("get_objc_protocols", "Discover and deduplicate Objective-C and Swift protocol labels after exhaustively paging names. Returns at most 100 entries; use xrefs or analyze_function to connect a protocol to implementations.", enhancedInputSchemas.get_objc_protocols),
|
|
149
|
-
enhanced("batch_decompile", "Decompile up to 20 explicit procedure symbols or addresses concurrently.
|
|
150
|
-
enhanced("get_call_graph", "Traverse Hopper's caller or callee relationships from one symbol or address for at most five levels.
|
|
149
|
+
enhanced("batch_decompile", "Decompile up to 20 explicit procedure symbols or addresses concurrently. Returns ordered per-item ok/error variants and aggregate counts; use analyze_function for a richer single-function dossier.", enhancedInputSchemas.batch_decompile),
|
|
150
|
+
enhanced("get_call_graph", "Traverse Hopper's caller or callee relationships from one symbol or address for at most five levels. Every node has an ok/error status and failures use safe typed projections; indirect calls may be missing and results are not a whole-program CFG.", enhancedInputSchemas.get_call_graph),
|
|
151
151
|
enhanced("analyze_swift_types", "Categorize exhaustively paged procedure names into Swift classes, structs, enums, protocols, extensions, and other symbols. Scans at most 5,000 names and returns at most 50 entries per category.", enhancedInputSchemas.analyze_swift_types),
|
|
152
|
-
enhanced("find_xrefs_to_name", "Resolve
|
|
152
|
+
enhanced("find_xrefs_to_name", "Resolve an exact name through Hopper's exhaustively paged name inventory and return a resolved or unresolved result. Unresolved names use the stable name_not_found reason; xrefs remain untyped.", enhancedInputSchemas.find_xrefs_to_name),
|
|
153
153
|
enhanced("binary_overview", "Use immediately after opening a target to summarize document, exhaustive procedure/string counts, and a bounded segment sample. detail controls segment fields and limit controls only the returned segment sample.", enhancedInputSchemas.binary_overview),
|
|
154
154
|
enhanced("analyze_function", "Preferred bounded analysis for one procedure symbol or address. Returns identity, pseudocode, optional assembly, comments, calls, incoming references, and blocks; unsupported outgoing references and CFG edges carry explicit unavailable metadata.", enhancedInputSchemas.analyze_function),
|
|
155
155
|
enhanced("trace_feature", "Trace a bounded literal feature query through matching strings and procedures, xrefs, and truthful containing-procedure resolution. Returns the operation budget, truncation, and residual unknowns; unknown_registry_approved: true records them durably without inferring reference kinds.", enhancedInputSchemas.trace_feature),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { ANALYSIS_ERROR_TAGS } from "../domain/errors.js";
|
|
2
3
|
import { evidenceSchema } from "../domain/evidence.js";
|
|
3
4
|
import { processCaptureComparisonSchema, processCaptureSchema, } from "../domain/processCapture.js";
|
|
4
5
|
import { evidenceBundleSchema } from "../domain/evidenceBundle.js";
|
|
@@ -149,9 +150,24 @@ const symbolDiscoveryOutput = (property) => resultOf(z.object({
|
|
|
149
150
|
count: z.number().int().min(0),
|
|
150
151
|
[property]: z.array(addressedEntry),
|
|
151
152
|
}));
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
153
|
+
const analysisErrorProjectionSchema = z
|
|
154
|
+
.object({
|
|
155
|
+
tag: z.enum(ANALYSIS_ERROR_TAGS),
|
|
156
|
+
message: z.string(),
|
|
157
|
+
details: z.record(z.string(), z.union([z.string(), z.number(), z.null()])),
|
|
158
|
+
})
|
|
159
|
+
.strict();
|
|
160
|
+
const graphNode = z.discriminatedUnion("status", [
|
|
161
|
+
z.object({
|
|
162
|
+
address: z.string(),
|
|
163
|
+
status: z.literal("ok"),
|
|
164
|
+
calls: z.array(z.string()),
|
|
165
|
+
}),
|
|
166
|
+
z.object({
|
|
167
|
+
address: z.string(),
|
|
168
|
+
status: z.literal("error"),
|
|
169
|
+
error: analysisErrorProjectionSchema,
|
|
170
|
+
}),
|
|
155
171
|
]);
|
|
156
172
|
const functionDossierOutput = resultOf(functionDossierSchema);
|
|
157
173
|
/** Exact structured-content schemas for the direct Hopper operations. */
|
|
@@ -207,10 +223,23 @@ export const enhancedOutputSchemas = {
|
|
|
207
223
|
swift_classes: symbolDiscoveryOutput("classes"),
|
|
208
224
|
get_objc_classes: symbolDiscoveryOutput("classes"),
|
|
209
225
|
get_objc_protocols: symbolDiscoveryOutput("protocols"),
|
|
210
|
-
batch_decompile: resultOf(z.
|
|
211
|
-
z.
|
|
212
|
-
|
|
213
|
-
|
|
226
|
+
batch_decompile: resultOf(z.object({
|
|
227
|
+
items: z.array(z.discriminatedUnion("status", [
|
|
228
|
+
z.object({
|
|
229
|
+
address: z.string(),
|
|
230
|
+
status: z.literal("ok"),
|
|
231
|
+
pseudocode: z.string().min(1),
|
|
232
|
+
}),
|
|
233
|
+
z.object({
|
|
234
|
+
address: z.string(),
|
|
235
|
+
status: z.literal("error"),
|
|
236
|
+
error: analysisErrorProjectionSchema,
|
|
237
|
+
}),
|
|
238
|
+
])),
|
|
239
|
+
total: z.number().int().min(0),
|
|
240
|
+
succeeded: z.number().int().min(0),
|
|
241
|
+
failed: z.number().int().min(0),
|
|
242
|
+
})),
|
|
214
243
|
get_call_graph: resultOf(z.record(z.string(), z.array(graphNode))),
|
|
215
244
|
analyze_swift_types: resultOf(z.object({
|
|
216
245
|
total: z.number().int().min(0),
|
|
@@ -219,9 +248,18 @@ export const enhancedOutputSchemas = {
|
|
|
219
248
|
items: z.array(addressedEntry),
|
|
220
249
|
})),
|
|
221
250
|
})),
|
|
222
|
-
find_xrefs_to_name: resultOf(z.
|
|
223
|
-
z.object({
|
|
224
|
-
|
|
251
|
+
find_xrefs_to_name: resultOf(z.discriminatedUnion("status", [
|
|
252
|
+
z.object({
|
|
253
|
+
status: z.literal("resolved"),
|
|
254
|
+
name: z.string(),
|
|
255
|
+
address: z.string(),
|
|
256
|
+
xrefs: addressList,
|
|
257
|
+
}),
|
|
258
|
+
z.object({
|
|
259
|
+
status: z.literal("unresolved"),
|
|
260
|
+
name: z.string(),
|
|
261
|
+
reason: z.literal("name_not_found"),
|
|
262
|
+
}),
|
|
225
263
|
])),
|
|
226
264
|
binary_overview: resultOf(z.object({
|
|
227
265
|
document: z.string(),
|
package/dist/domain/errors.js
CHANGED
|
@@ -1,3 +1,29 @@
|
|
|
1
|
+
/** Stable tags exposed by safe analysis-error projections. */
|
|
2
|
+
export const ANALYSIS_ERROR_TAGS = [
|
|
3
|
+
"AnalysisProtocolError",
|
|
4
|
+
"AnalysisInputError",
|
|
5
|
+
"AnalysisOutputError",
|
|
6
|
+
"AnalysisCapabilityUnavailableError",
|
|
7
|
+
"AnalysisCancelledError",
|
|
8
|
+
"AnalysisTimeoutError",
|
|
9
|
+
"ProviderSelectionError",
|
|
10
|
+
"ProviderAdapterError",
|
|
11
|
+
"ArtifactOperationError",
|
|
12
|
+
"ProcessCaptureError",
|
|
13
|
+
"EvidenceIntegrityError",
|
|
14
|
+
"EvidenceLimitError",
|
|
15
|
+
"EvidenceFileError",
|
|
16
|
+
"UnknownRegistryError",
|
|
17
|
+
"HopperTimeoutError",
|
|
18
|
+
"HopperCancelledError",
|
|
19
|
+
"HopperProtocolError",
|
|
20
|
+
"HopperRemoteError",
|
|
21
|
+
"HopperProcessError",
|
|
22
|
+
"HopperStartError",
|
|
23
|
+
"ConfigurationError",
|
|
24
|
+
"NoBinaryOpenError",
|
|
25
|
+
"BinaryTargetError",
|
|
26
|
+
];
|
|
1
27
|
/** Base class for expected analysis, provider, and session failures. */
|
|
2
28
|
export class AnalysisError extends Error {
|
|
3
29
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { AnalysisOutputError, HopperProtocolError } from "./errors.js";
|
|
3
3
|
import { err, ok } from "./result.js";
|
|
4
|
+
/** Return the non-negative byte distance between hexadecimal addresses. */
|
|
5
|
+
export const addressDistance = (start, end) => Math.max(0, Number.parseInt(end, 16) - Number.parseInt(start, 16));
|
|
4
6
|
const procedureMapSchema = z.record(z.string(), z.string());
|
|
5
7
|
const addressedNamesSchema = z.array(z.object({ address: z.string(), name: z.string() }));
|
|
6
8
|
const addressedNameMapSchema = z.record(z.string(), z.string());
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Return whether a Node.js version satisfies REA's tested runtime families. */
|
|
2
|
+
export const supportsNodeVersion = (version) => {
|
|
3
|
+
const [majorText = "", minorText = ""] = version.split(".");
|
|
4
|
+
const major = Number.parseInt(majorText, 10);
|
|
5
|
+
const minor = Number.parseInt(minorText, 10);
|
|
6
|
+
return ((major === 22 && minor >= 19) ||
|
|
7
|
+
(major >= 24 && (major !== 24 || minor >= 11)));
|
|
8
|
+
};
|
|
@@ -81,6 +81,8 @@ class NativeMacOSClient {
|
|
|
81
81
|
this.runner = runner;
|
|
82
82
|
}
|
|
83
83
|
async execute(operation, parameters, options) {
|
|
84
|
+
if (options?.signal?.aborted === true)
|
|
85
|
+
return err(new AnalysisCancelledError(operation));
|
|
84
86
|
if (operation === "health")
|
|
85
87
|
return ok(createAnalysisExecution(null, IDENTITY));
|
|
86
88
|
if (!isNativeOperation(operation))
|