mirror of
https://github.com/morluto/rea.git
synced 2026-10-12 02:19:24 +00:00
* feat(jeb): add JEB engine provider over its MCP interface
Add a bring-your-own JEB provider that serves project, unit, and
decompilation inspection from a caller-started JEB client speaking MCP
(streamable HTTP, selected with REA_JEB_MCP_URL). Expose four provider-neutral
operations through shared CLI/MCP workflows: inspect_jeb_client,
open_jeb_project, list_jeb_units, and decompile_jeb_item, each returning
Evidence with engine identity, endpoint, and preserved refusal reasons.
The adapter keeps JEB's envelope semantics: success fields are parsed
format-aware at the boundary, message-only refusals become unsupported-target
errors with the engine's own reason, transport failures become availability
failures with recovery guidance, and unit pagination derives coverage from the
engine's 100-item page cap. Explicit unknowns (implicitly selected unit,
omitted filters) are reported instead of guessed.
Verified against a real JEB 5.48.0 licensed build serving jeb-mcp-server 1.3.0
headless: client inspection, unit listing, and method decompilation were
exercised through the CLI, MCP boundary, and scripts/verify-jeb.mjs.
JEB 5.48.0 headless instances do not advertise open_project; the refusal is
preserved and documented.
* fix(jeb): verify against a real signed APK and fix CLI option surface
Verify the provider against a locally built, v1/v2/v3-signed probe APK
(binary manifest, classes.dex with a launcher activity) on JEB 5.48.0: the
full unit tree (apk, manifest, three certificate units, dex bytecode,
decompiler), wildcard-filtered and paginated unit listing with derived
coverage, parent-restricted listing, and launcher-activity method
decompilation through both CLI and MCP, including dexdec's inline
constant folding of the probe's obfuscated tag.
Move list-jeb-units filter/pagination and decompile-jeb-item unit selection
from positional arguments to options; the MCP schemas are unchanged.
* fix(jeb): preserve engine refusals through the real MCP boundary
Testing against the published Signal 8.30.3 universal APK exposed a boundary
bug the scripted provider tests missed: decodeJebToolResult raised a protocol
error for isError results before the provider could map JEB's
{success:false, message} envelope, so real engine refusals surfaced as
unreadable output instead of the engine's own reason. Decode refusal
envelopes like any structured result and let the provider preserve the
reason; cover the decoder directly, including contentless and unparseable
results.
Signal verification: full 109 MB R8/Kotlin/Compose APK with four signature
schemes and native arm64 ELF units parsed; MainActivity.onCreate and R8
synthetic lambdas decompiled through CLI and MCP; paginated, filtered, and
parent-restricted unit listing; refusal path confirmed on the live engine.
* docs(jeb): record Flutter Gallery verification on the real-engine lane
Verify the provider against the official Flutter Gallery 2.9.2 release APK
(112 MB): per-ABI libapp.so Dart AOT snapshot images analyzed as native code
with 75k methods on arm64, snapshot internals and wrapper methods decompiled
through both CLI and MCP with explicit unit selection, decoded manifest, and
filtered native-library unit discovery. Note that JEB 5.48.0 headless does
not expose the dedicated Dart snapshot processor or run_script; both remain
GUI-only engine surfaces.
* docs(jeb): translate the README JEB row for every supported language
The JEB row was inserted in English across all translated READMEs; each now
carries its own label, capability summary, requirements, and guide link
following that language's Android-row conventions.
* fix(jeb): enforce local endpoints and preserve cancellation
* fix(jeb): refuse redirects across the local MCP boundary
---------
Co-authored-by: REA Contributor <[email protected]>
Co-authored-by: morluto <[email protected]>
154 lines
5.2 KiB
JavaScript
154 lines
5.2 KiB
JavaScript
import { execFile } from "node:child_process";
|
|
import { mkdtemp, writeFile, rm } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { promisify } from "node:util";
|
|
|
|
import { JebAnalysisService } from "../dist/application/jeb/JebAnalysisService.js";
|
|
import { createJebAnalysisProvider } from "../dist/composition/jeb.js";
|
|
|
|
const run = promisify(execFile);
|
|
|
|
const options = parseArguments(process.argv.slice(2));
|
|
if (options.help) {
|
|
process.stdout.write(
|
|
"Usage: npm run verify:jeb [-- --item-address ADDRESS --marker TEXT]\n" +
|
|
"Requires a running JEB client serving MCP at REA_JEB_MCP_URL\n" +
|
|
"(default http://127.0.0.1:8425/mcp) with a project already open.\n" +
|
|
"JEB 5.48 headless instances open their target at start (--infile or\n" +
|
|
"the documented programmatic launcher); open_jeb_project is verified\n" +
|
|
"only when the engine advertises it. Without --item-address, a tiny\n" +
|
|
"Java fixture is compiled and its known method is decompiled.\n",
|
|
);
|
|
process.exit(0);
|
|
}
|
|
|
|
const provider = createJebAnalysisProvider(process.env);
|
|
const service = new JebAnalysisService(provider);
|
|
const fixtureDirectory = await mkdtemp(join(tmpdir(), "rea-jeb-"));
|
|
try {
|
|
const client = await service.execute("inspect_jeb_client", {});
|
|
if (!client.ok) throw new Error(client.error.message);
|
|
const engine = client.value.normalized_result.engine;
|
|
if (engine.version === null)
|
|
throw new Error("The JEB client did not report its version");
|
|
|
|
// JEB 5.48 headless clients do not advertise open_project; a project opened
|
|
// at engine start is the supported target-selection route there. Attempt the
|
|
// tool and record the engine's exact refusal when it is unavailable.
|
|
let openRefusal = null;
|
|
const target = await resolveTarget(options, fixtureDirectory);
|
|
const opened = await service.execute("open_jeb_project", {
|
|
path: target.path,
|
|
});
|
|
if (opened.ok) {
|
|
const artifact = opened.value.normalized_result.input_files.find(
|
|
(file) => typeof file === "object" && file !== null,
|
|
);
|
|
if (
|
|
artifact === undefined ||
|
|
!/^[0-9a-f]{64}$/u.test(artifact.contents_sha256_hash)
|
|
)
|
|
throw new Error("The engine did not report an input artifact digest");
|
|
} else {
|
|
openRefusal = opened.error.message;
|
|
}
|
|
|
|
const units = await service.execute("list_jeb_units", { count: 100 });
|
|
if (!units.ok) throw new Error(units.error.message);
|
|
if (units.value.normalized_result.units.length === 0)
|
|
throw new Error("The engine reported no project units");
|
|
|
|
const decompiled = await service.execute("decompile_jeb_item", {
|
|
item_address: target.itemAddress,
|
|
item_kind: "method",
|
|
});
|
|
if (!decompiled.ok) throw new Error(decompiled.error.message);
|
|
const text = decompiled.value.normalized_result.text;
|
|
if (!text.includes(target.marker))
|
|
throw new Error(
|
|
"Decompiled pseudo-code did not contain the fixture marker",
|
|
);
|
|
|
|
const report = {
|
|
ok: true,
|
|
provider: "jeb",
|
|
host_platform: process.platform,
|
|
engine_version: engine.version,
|
|
engine_gui: engine.gui_client,
|
|
endpoint: engine.endpoint,
|
|
open_project_observed: openRefusal === null,
|
|
...(openRefusal === null ? {} : { open_project_refusal: openRefusal }),
|
|
unit_count: units.value.normalized_result.units.length,
|
|
decompiled_marker_observed: true,
|
|
};
|
|
process.stdout.write(`${JSON.stringify(report)}\n`);
|
|
} catch (cause) {
|
|
process.stderr.write(
|
|
`JEB real-provider verification failed: ${cause instanceof Error ? cause.message : String(cause)}\n`,
|
|
);
|
|
process.exitCode = 1;
|
|
} finally {
|
|
await provider.close().catch(() => undefined);
|
|
await rm(fixtureDirectory, { recursive: true, force: true }).catch(
|
|
() => undefined,
|
|
);
|
|
}
|
|
|
|
async function resolveTarget(selected, directory) {
|
|
if (selected.itemAddress !== undefined)
|
|
return {
|
|
path: "/engine-side/target",
|
|
itemAddress: selected.itemAddress,
|
|
marker: selected.marker ?? "",
|
|
};
|
|
await writeFile(
|
|
join(directory, "Hello.java"),
|
|
[
|
|
"public class Hello {",
|
|
" public static String greet(String name) {",
|
|
' return "Hello, " + name + "!";',
|
|
" }",
|
|
"}",
|
|
"",
|
|
].join("\n"),
|
|
);
|
|
try {
|
|
await run("javac", [join(directory, "Hello.java")]);
|
|
} catch {
|
|
throw new Error(
|
|
"javac is required to build the default verification fixture",
|
|
);
|
|
}
|
|
return {
|
|
path: join(directory, "Hello.class"),
|
|
itemAddress: "LHello;->greet(Ljava/lang/String;)Ljava/lang/String;",
|
|
marker: "Hello, ",
|
|
};
|
|
}
|
|
|
|
function parseArguments(args) {
|
|
if (args.length === 1 && args[0] === "--help") return { help: true };
|
|
const values = new Map();
|
|
for (let index = 0; index < args.length; index += 1) {
|
|
const flag = args[index];
|
|
const value = args[index + 1];
|
|
if (
|
|
(flag !== "--item-address" && flag !== "--marker") ||
|
|
value === undefined
|
|
)
|
|
throw new Error(
|
|
"Expected no arguments, or --item-address ADDRESS [--marker TEXT]",
|
|
);
|
|
values.set(flag, value);
|
|
index += 1;
|
|
}
|
|
return {
|
|
help: false,
|
|
...(values.has("--item-address")
|
|
? { itemAddress: values.get("--item-address") }
|
|
: {}),
|
|
...(values.has("--marker") ? { marker: values.get("--marker") } : {}),
|
|
};
|
|
}
|