mirror of
https://github.com/morluto/rea
synced 2026-10-28 15:07:26 -04:00
7e8f7f6101
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 }));
|