mirror of
https://github.com/morluto/rea.git
synced 2026-10-28 10:25:42 +00:00
Extend static manifest declarations and retain precise source locations and historical unknowns. Share bounded selected component views across CLI and MCP with complete parent Evidence. Verified real ApiDemos, an AAPT2-built fixture, legacy Evidence, default-package metadata and relevant CLI/MCP contracts. Recorded whole-task token estimates, timing/resource limits and independent main-failure controls in the PR. Co-authored-by: Harsh <johiyaharsh@gmail.com>
295 lines
9.2 KiB
JavaScript
295 lines
9.2 KiB
JavaScript
/** Real APK task through public MCP, with complete retained Evidence and an independent XML oracle. */
|
|
import assert from "node:assert/strict";
|
|
import { createHash } from "node:crypto";
|
|
import { createReadStream } from "node:fs";
|
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
import { createRequire } from "node:module";
|
|
import { dirname, join, resolve } from "node:path";
|
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
import { parseArgs } from "node:util";
|
|
import { performance } from "node:perf_hooks";
|
|
import { createVerifierCommands } from "../../scripts/lib/verify-commands.mjs";
|
|
|
|
const { values } = parseArgs({
|
|
options: {
|
|
repository: { type: "string" },
|
|
target: { type: "string" },
|
|
output: { type: "string" },
|
|
mode: { type: "string" },
|
|
"java-home": { type: "string" },
|
|
jar: { type: "string" },
|
|
action: {
|
|
type: "string",
|
|
default: "android.provider.Telephony.SMS_RECEIVED",
|
|
},
|
|
},
|
|
});
|
|
for (const name of [
|
|
"repository",
|
|
"target",
|
|
"output",
|
|
"mode",
|
|
"java-home",
|
|
"jar",
|
|
])
|
|
assert.ok(values[name], `Missing --${name}`);
|
|
assert.ok(["before", "after"].includes(values.mode));
|
|
const repo = resolve(values.repository),
|
|
target = resolve(values.target),
|
|
output = resolve(values.output);
|
|
await mkdir(output, { mode: 0o700 });
|
|
const digest = createHash("sha256");
|
|
for await (const chunk of createReadStream(target)) digest.update(chunk);
|
|
const sha256 = digest.digest("hex");
|
|
const require = createRequire(join(repo, "package.json"));
|
|
const { Client } = await import(
|
|
pathToFileURL(require.resolve("@modelcontextprotocol/client"))
|
|
);
|
|
const { StdioClientTransport } = await import(
|
|
pathToFileURL(require.resolve("@modelcontextprotocol/client/stdio"))
|
|
);
|
|
const commands = await createVerifierCommands();
|
|
const env = {
|
|
PATH: process.env.PATH,
|
|
JAVA_HOME: resolve(values["java-home"]),
|
|
REA_JADX_MCP_JAR: resolve(values.jar),
|
|
REA_JADX_HEAP_MIB: "1024",
|
|
REA_JADX_ACTIVE_PROCESSOR_COUNT: "2",
|
|
NODE_OPTIONS: "--max-old-space-size=3072",
|
|
UV_THREADPOOL_SIZE: "2",
|
|
REA_LOG_LEVEL: "silent",
|
|
REA_MCP_TOOL_FAMILIES: "android,application,session",
|
|
REA_MCP_INPUT_SCHEMA_PROFILE: "compact",
|
|
};
|
|
const transport = new StdioClientTransport({
|
|
command: process.execPath,
|
|
args: [join(repo, "scripts/rea.mjs"), "mcp"],
|
|
cwd: repo,
|
|
env,
|
|
stderr: "pipe",
|
|
maxBufferSize: 16 * 1024 * 1024,
|
|
});
|
|
const diagnostics = [];
|
|
let diagnosticBytes = 0;
|
|
let omittedDiagnosticBytes = 0;
|
|
transport.stderr?.on("data", (chunk) => {
|
|
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
const retained = bytes.subarray(0, Math.max(0, 65536 - diagnosticBytes));
|
|
if (retained.length > 0) diagnostics.push(retained);
|
|
diagnosticBytes += retained.length;
|
|
omittedDiagnosticBytes += bytes.length - retained.length;
|
|
});
|
|
const client = new Client({
|
|
name: "rea-real-android-components",
|
|
version: "1",
|
|
});
|
|
let catalog;
|
|
const calls = [],
|
|
runs = [];
|
|
const invoke = async (name, arguments_) => {
|
|
const definitions = catalog.tools.filter((tool) => tool.name === name);
|
|
assert.equal(definitions.length, 1);
|
|
const start = performance.now();
|
|
const result = await client.callTool(
|
|
{ name, arguments: arguments_ },
|
|
{ toolDefinition: definitions[0], timeout: 120000 },
|
|
);
|
|
calls.push({
|
|
name,
|
|
arguments: arguments_,
|
|
result,
|
|
elapsed_ms: performance.now() - start,
|
|
});
|
|
assert.notEqual(
|
|
result.isError,
|
|
true,
|
|
`${name}: ${JSON.stringify(result.content)}`,
|
|
);
|
|
return result.structuredContent;
|
|
};
|
|
const selectedFields = [
|
|
"kind",
|
|
"declared_name",
|
|
"name",
|
|
"exported",
|
|
"exported_default",
|
|
"permission",
|
|
"permission_source",
|
|
"enabled",
|
|
"enabled_declaration",
|
|
"process",
|
|
"intent_filters",
|
|
"meta_data",
|
|
"location",
|
|
];
|
|
const stripLocations = (row) => {
|
|
const { location: _location, ...fields } = row;
|
|
return {
|
|
...fields,
|
|
intent_filters: row.intent_filters?.map(
|
|
({
|
|
location: _filterLocation,
|
|
source_index: _filterIndex,
|
|
coverage: _coverage,
|
|
...filter
|
|
}) => ({
|
|
...filter,
|
|
data: filter.data.map(({ location: _dataLocation, ...data }) => data),
|
|
}),
|
|
),
|
|
meta_data: row.meta_data?.map(
|
|
({ location: _metadataLocation, ...metadata }) => metadata,
|
|
),
|
|
};
|
|
};
|
|
const assertOracleFacts = (row, expected, source) => {
|
|
assert.equal(row.declaration_coverage, "complete");
|
|
const cleaned = stripLocations(row);
|
|
for (const [key, value] of Object.entries(expected))
|
|
assert.deepEqual(
|
|
cleaned[key],
|
|
value,
|
|
`Independent oracle: ${expected.source_index}.${key}`,
|
|
);
|
|
assert.equal(
|
|
source.slice(
|
|
row.location.offset,
|
|
row.location.offset + row.kind.length + 1,
|
|
),
|
|
"<" + row.kind,
|
|
);
|
|
};
|
|
try {
|
|
await client.connect(transport);
|
|
catalog = await client.listTools();
|
|
await writeFile(join(output, "catalog.json"), JSON.stringify(catalog));
|
|
for (const temperature of ["first-capture", "warm-session"]) {
|
|
const firstCall = calls.length;
|
|
const start = performance.now();
|
|
const result = await invoke(
|
|
"inspect_android_package",
|
|
values.mode === "before"
|
|
? { path: target }
|
|
: {
|
|
path: target,
|
|
detail: "summary",
|
|
view: {
|
|
kind: "android",
|
|
facet: "components",
|
|
actions: [values.action],
|
|
fields: selectedFields,
|
|
limit: 64,
|
|
},
|
|
},
|
|
);
|
|
const rpcMs = performance.now() - start;
|
|
const normalized = result.normalized_result;
|
|
const parentId =
|
|
values.mode === "before"
|
|
? result.evidence_id
|
|
: normalized.parent_evidence_id;
|
|
const path = join(output, temperature + "-evidence.json");
|
|
await invoke("export_evidence_bundle", { path });
|
|
const bundle = JSON.parse(await readFile(path, "utf8"));
|
|
const parent = bundle.records.find(
|
|
(record) => record.evidence_id === parentId,
|
|
);
|
|
assert.ok(parent);
|
|
assert.equal(parent.subject.digest.sha256, sha256);
|
|
const oraclePath = join(
|
|
dirname(fileURLToPath(import.meta.url)),
|
|
"fixtures/android-components/manifest-oracle.py",
|
|
);
|
|
const oracle = JSON.parse(
|
|
(
|
|
await commands.exec(
|
|
"python3",
|
|
[oraclePath, path, parentId, values.action],
|
|
{ timeout: 15000, maxBuffer: 4 * 1024 * 1024 },
|
|
)
|
|
).stdout,
|
|
);
|
|
assert.ok(
|
|
oracle.components.length > 0,
|
|
"The completed task must exercise a real positive action match",
|
|
);
|
|
const complete = parent.normalized_result;
|
|
assert.equal(complete.components.items.length, oracle.inventory.length);
|
|
assert.equal(oracle.inventory.length, oracle.source_components);
|
|
for (const [index, expected] of oracle.inventory.entries()) {
|
|
const row = complete.components.items[index];
|
|
if (values.mode === "after")
|
|
assertOracleFacts(
|
|
{ ...row, source_index: index },
|
|
expected,
|
|
complete.manifest.text,
|
|
);
|
|
else {
|
|
for (const key of ["kind", "declared_name", "name", "permission"])
|
|
assert.deepEqual(
|
|
row[key],
|
|
expected[key],
|
|
`Legacy inventory: ${index}.${key}`,
|
|
);
|
|
}
|
|
}
|
|
if (values.mode === "after") {
|
|
assert.equal(normalized.facet, "components");
|
|
assert.equal(normalized.source_status, "complete");
|
|
assert.equal(normalized.source_components, oracle.source_components);
|
|
assert.equal(normalized.coverage.exhausted, true);
|
|
assert.equal(normalized.coverage.total, oracle.components.length);
|
|
assert.equal(normalized.items.length, oracle.components.length);
|
|
for (let i = 0; i < oracle.components.length; i++) {
|
|
const row = normalized.items[i],
|
|
expected = oracle.components[i];
|
|
assertOracleFacts(row, expected, complete.manifest.text);
|
|
}
|
|
} else {
|
|
for (const expected of oracle.components) {
|
|
const row = normalized.components.items[expected.source_index];
|
|
assert.equal(row.name, expected.name);
|
|
assert.ok(row.actions.includes(values.action));
|
|
}
|
|
}
|
|
runs.push({
|
|
temperature,
|
|
rpc_ms: rpcMs,
|
|
calls: calls.length - firstCall,
|
|
matched_components: oracle.components.length,
|
|
source_components: oracle.source_components,
|
|
parent_evidence_id: parentId,
|
|
answer_verified: true,
|
|
inventory_verified: oracle.inventory.length,
|
|
inventory_scope:
|
|
values.mode === "after"
|
|
? "declared-structured-facets"
|
|
: "legacy-identity-permission",
|
|
});
|
|
}
|
|
} finally {
|
|
// exec drains already adopted owners; adopt the live MCP client only at its final cleanup boundary.
|
|
commands.adopt(client, "real Android component MCP client");
|
|
await commands.close();
|
|
await writeFile(join(output, "stderr.log"), Buffer.concat(diagnostics));
|
|
await writeFile(join(output, "calls.json"), JSON.stringify(calls), {
|
|
mode: 0o600,
|
|
});
|
|
await writeFile(
|
|
join(output, "report.json"),
|
|
JSON.stringify(
|
|
{
|
|
mode: values.mode,
|
|
target,
|
|
sha256,
|
|
runs,
|
|
tool_count: catalog?.tools.length,
|
|
omitted_diagnostic_bytes: omittedDiagnosticBytes,
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
);
|
|
}
|
|
console.log(JSON.stringify({ mode: values.mode, runs, verified: true }));
|