mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +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 <contributor@rea.tools>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
275 lines
8.5 KiB
TypeScript
275 lines
8.5 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import { JebProvider } from "./JebProvider.js";
|
|
import type { JebMcpConnection } from "./JebMcpConnection.js";
|
|
import {
|
|
AnalysisCapabilityUnavailableError,
|
|
AnalysisInputError,
|
|
AnalysisProtocolError,
|
|
AnalysisCancelledError,
|
|
} from "../domain/analysisErrorCore.js";
|
|
import type { JsonValue } from "../domain/jsonValue.js";
|
|
|
|
const scriptedConnection = (
|
|
responses: Readonly<Record<string, JsonValue | Error>>,
|
|
): JebMcpConnection & { readonly calls: unknown[][] } => {
|
|
const calls: unknown[][] = [];
|
|
return {
|
|
calls,
|
|
async connect() {
|
|
return [];
|
|
},
|
|
async call(name, args) {
|
|
calls.push([name, args]);
|
|
const response = responses[name];
|
|
if (response === undefined)
|
|
throw new Error(`Unexpected JEB MCP call: ${name}`);
|
|
if (response instanceof Error) throw response;
|
|
return response;
|
|
},
|
|
async close() {},
|
|
};
|
|
};
|
|
|
|
const providerWith = (connection: JebMcpConnection) =>
|
|
new JebProvider({
|
|
environment: { REA_JEB_MCP_URL: "http://127.0.0.1:8425/mcp" },
|
|
connectionFactory: () => connection,
|
|
});
|
|
|
|
describe("JebProvider boundary", () => {
|
|
it("normalizes client information and caches engine identity", async () => {
|
|
const connection = scriptedConnection({
|
|
get_client_information: {
|
|
success: true,
|
|
version: "5.48.0.202610071539",
|
|
gui_client: false,
|
|
startup_ts: 1791623439,
|
|
},
|
|
});
|
|
const provider = providerWith(connection);
|
|
const result = await provider.execute({
|
|
operation: "inspect_jeb_client",
|
|
input: {},
|
|
});
|
|
expect(result.ok).toBe(true);
|
|
if (!result.ok) return;
|
|
const normalized = result.value.result as {
|
|
readonly engine: { readonly version: string | null };
|
|
readonly startup_ts: number | null;
|
|
};
|
|
expect(normalized.engine.version).toBe("5.48.0.202610071539");
|
|
expect(normalized.startup_ts).toBe(1791623439);
|
|
expect(result.value.provider.version).toBe("5.48.0.202610071539");
|
|
});
|
|
|
|
it("reports unavailable when the engine endpoint is unreachable", async () => {
|
|
const provider = providerWith(
|
|
scriptedConnection({
|
|
get_client_information: new Error(
|
|
"connect ECONNREFUSED 127.0.0.1:8425",
|
|
),
|
|
list_units: new Error("connect ECONNREFUSED 127.0.0.1:8425"),
|
|
}),
|
|
);
|
|
const availability = await provider.inspectAvailability();
|
|
expect(availability.status).toBe("unavailable");
|
|
const result = await provider.execute({
|
|
operation: "list_jeb_units",
|
|
input: { count: 10, index: 0 },
|
|
});
|
|
expect(result.ok).toBe(false);
|
|
if (result.ok) return;
|
|
expect(result.error).toBeInstanceOf(AnalysisCapabilityUnavailableError);
|
|
expect(result.error.message).toContain("ECONNREFUSED");
|
|
});
|
|
|
|
it("maps transport failures during an operation to provider unavailability", async () => {
|
|
const provider = providerWith(
|
|
scriptedConnection({
|
|
get_client_information: {
|
|
success: true,
|
|
version: "5.48.0",
|
|
gui_client: false,
|
|
},
|
|
list_units: new Error("connect ECONNREFUSED 127.0.0.1:8425"),
|
|
}),
|
|
);
|
|
const result = await provider.execute({
|
|
operation: "list_jeb_units",
|
|
input: { count: 10, index: 0 },
|
|
});
|
|
expect(result.ok).toBe(false);
|
|
if (result.ok) return;
|
|
expect(result.error).toBeInstanceOf(AnalysisCapabilityUnavailableError);
|
|
expect(result.error.message).toContain("ECONNREFUSED");
|
|
});
|
|
|
|
it("preserves engine refusal reasons as unsupported targets", async () => {
|
|
const provider = providerWith(
|
|
scriptedConnection({
|
|
get_client_information: {
|
|
success: true,
|
|
version: "5.48.0",
|
|
gui_client: false,
|
|
},
|
|
decompile_code_item: {
|
|
success: false,
|
|
message:
|
|
"The decompilation failed because the item address does not reference an existing code item",
|
|
},
|
|
}),
|
|
);
|
|
const result = await provider.execute({
|
|
operation: "decompile_jeb_item",
|
|
input: { item_address: "LNope;->x()V", item_kind: "method" },
|
|
});
|
|
expect(result.ok).toBe(false);
|
|
if (result.ok) return;
|
|
expect(result.error.message).toContain("does not reference an existing");
|
|
});
|
|
|
|
it("derives partial coverage only from a full engine page", async () => {
|
|
const provider = providerWith(
|
|
scriptedConnection({
|
|
get_client_information: {
|
|
success: true,
|
|
version: "5.48.0",
|
|
gui_client: false,
|
|
},
|
|
list_units: {
|
|
success: true,
|
|
units: Array.from({ length: 3 }, (_, index) => ({
|
|
unit_path: `a/class${index}.dex`,
|
|
unit_type: "dex",
|
|
})),
|
|
},
|
|
}),
|
|
);
|
|
const full = await provider.execute({
|
|
operation: "list_jeb_units",
|
|
input: { count: 3, index: 0 },
|
|
});
|
|
expect(
|
|
full.ok &&
|
|
(full.value.result as { coverage: string }).coverage === "partial",
|
|
).toBe(true);
|
|
const last = await provider.execute({
|
|
operation: "list_jeb_units",
|
|
input: { count: 4, index: 0 },
|
|
});
|
|
expect(
|
|
last.ok &&
|
|
(last.value.result as { coverage: string }).coverage === "complete",
|
|
).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("JebProvider target and endpoint selection", () => {
|
|
it("keeps the implicitly selected unit unknown when unit_path is omitted", async () => {
|
|
const provider = providerWith(
|
|
scriptedConnection({
|
|
get_client_information: {
|
|
success: true,
|
|
version: "5.48.0",
|
|
gui_client: false,
|
|
},
|
|
decompile_code_item: { success: true, text: "int x() { return 1; }" },
|
|
}),
|
|
);
|
|
const result = await provider.execute({
|
|
operation: "decompile_jeb_item",
|
|
input: { item_address: "LFoo;->x()I", item_kind: "method" },
|
|
});
|
|
expect(result.ok).toBe(true);
|
|
if (!result.ok) return;
|
|
const normalized = result.value.result as {
|
|
readonly unit_path: string | null;
|
|
};
|
|
expect(normalized.unit_path).toBeNull();
|
|
expect(result.value.limitations.join(" ")).toContain("implicitly");
|
|
});
|
|
|
|
it("rejects unusable endpoints at construction", () => {
|
|
const rejection = (value: string): AnalysisInputError => {
|
|
try {
|
|
new JebProvider({ environment: { REA_JEB_MCP_URL: value } });
|
|
} catch (cause) {
|
|
if (cause instanceof AnalysisInputError) return cause;
|
|
throw cause;
|
|
}
|
|
throw new Error("endpoint should have been rejected");
|
|
};
|
|
expect(rejection("ftp://example.invalid/mcp").cause).toMatch(
|
|
/http and https/,
|
|
);
|
|
expect(rejection("http://user:pass@127.0.0.1:8425/mcp").cause).toMatch(
|
|
/credentials/,
|
|
);
|
|
});
|
|
});
|
|
|
|
it("refuses remote endpoints before constructing a connection", () => {
|
|
expect(
|
|
() =>
|
|
new JebProvider({
|
|
environment: { REA_JEB_MCP_URL: "http://example.com/mcp" },
|
|
}),
|
|
).toThrow(AnalysisInputError);
|
|
});
|
|
|
|
it.each(["list_jeb_units", "decompile_jeb_item"] as const)(
|
|
"rejects an omitted successful payload for %s",
|
|
async (operation) => {
|
|
const provider = providerWith(
|
|
scriptedConnection({
|
|
get_client_information: { success: true, version: "5.48.0" },
|
|
list_units: { success: true },
|
|
decompile_code_item: { success: true },
|
|
}),
|
|
);
|
|
const result = await provider.execute(
|
|
operation === "list_jeb_units"
|
|
? { operation, input: { count: 10, index: 0 } }
|
|
: {
|
|
operation,
|
|
input: { item_address: "LTest;->x()V", item_kind: "method" },
|
|
},
|
|
);
|
|
expect(result.ok).toBe(false);
|
|
if (!result.ok) expect(result.error).toBeInstanceOf(AnalysisProtocolError);
|
|
},
|
|
);
|
|
|
|
it("forwards in-flight cancellation to the MCP call", async () => {
|
|
const controller = new AbortController();
|
|
let entered!: () => void;
|
|
const started = new Promise<void>((resolve) => {
|
|
entered = resolve;
|
|
});
|
|
const provider = providerWith({
|
|
async connect() {
|
|
return [];
|
|
},
|
|
async close() {},
|
|
async call(_name, _args, signal) {
|
|
entered();
|
|
await new Promise<void>((_resolve, reject) =>
|
|
signal!.addEventListener("abort", () => reject(signal!.reason), {
|
|
once: true,
|
|
}),
|
|
);
|
|
return {};
|
|
},
|
|
});
|
|
const pending = provider.execute(
|
|
{ operation: "inspect_jeb_client", input: {} },
|
|
{ signal: controller.signal },
|
|
);
|
|
await started;
|
|
controller.abort();
|
|
const result = await pending;
|
|
expect(result.ok).toBe(false);
|
|
if (!result.ok) expect(result.error).toBeInstanceOf(AnalysisCancelledError);
|
|
});
|