mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
feat(native): make function matching allowance configurable (#7159)
Allow native matching representation budgets to be selected at runtime or launch while retaining the 96 MiB default and main shared-provenance accounting. Preserve actionable configuration recovery through the setting owner and verify complete CLI/MCP matches for 20k/50k synthetic inventories and real stripped LLVM 14/15 inventories. Fixes #6406. Based on #7112 by Kaoru0822; original authorship and co-author credit retained. Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
27 files changed
+1511
-27
No files matched your search
@@ -0,0 +1,276 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { createHash } from "node:crypto";
|
||||
import { createWriteStream } from "node:fs";
|
||||
import { mkdir, mkdtemp, open, readFile, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { finished } from "node:stream/promises";
|
||||
import { parseArgs } from "node:util";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
let parseEvidence, createEvidenceBundle;
|
||||
import { createRetainedQueryCaller } from "./retained-query-calls.mjs";
|
||||
import { finalizeRetainedQueryReceipt } from "./retained-query-finalization.mjs";
|
||||
const { values } = parseArgs({
|
||||
options: Object.fromEntries(
|
||||
["runtime", "target", "output", "ghidra", "java", "profile"].map((key) => [
|
||||
key,
|
||||
{ type: "string" },
|
||||
]),
|
||||
),
|
||||
});
|
||||
for (const key of [
|
||||
"runtime",
|
||||
"target",
|
||||
"output",
|
||||
"ghidra",
|
||||
"java",
|
||||
"profile",
|
||||
]) {
|
||||
assert.ok(values[key], `Missing --${key}`);
|
||||
values[key] = resolve(values[key]);
|
||||
}
|
||||
const target = values.target,
|
||||
output = values.output,
|
||||
repo = values.runtime;
|
||||
await mkdir(output);
|
||||
const report = {
|
||||
target,
|
||||
profile_scope:
|
||||
"Ghidra-reported function identities and defined strings under explicit disabled-analyzer profile; no claim of default-analysis or function-body completeness",
|
||||
calls: [],
|
||||
},
|
||||
transcript = [];
|
||||
let account,
|
||||
transport,
|
||||
serverLog,
|
||||
serverLogFinished,
|
||||
client,
|
||||
call,
|
||||
opened = false;
|
||||
const start = performance.now();
|
||||
try {
|
||||
({ parseEvidence } = await import(
|
||||
pathToFileURL(join(repo, "dist/domain/evidence.js")).href
|
||||
));
|
||||
({ createEvidenceBundle } = await import(
|
||||
pathToFileURL(join(repo, "dist/domain/evidenceBundle.js")).href
|
||||
));
|
||||
report.sha256 = createHash("sha256")
|
||||
.update(await readFile(target))
|
||||
.digest("hex");
|
||||
const disabled = JSON.parse(await readFile(values.profile, "utf8")).names;
|
||||
report.disabled_analyzers = disabled;
|
||||
account = await mkdtemp(join(tmpdir(), "rea-large-native-inventory-"));
|
||||
transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [join(repo, "scripts/rea.mjs"), "mcp"],
|
||||
cwd: repo,
|
||||
env: {
|
||||
HOME: account,
|
||||
PATH: process.env.PATH,
|
||||
NODE_OPTIONS: "--max-old-space-size=3072",
|
||||
GHIDRA_INSTALL_DIR: values.ghidra,
|
||||
JAVA_HOME: values.java,
|
||||
GHIDRA_HEADLESS_MAXMEM: "3072M",
|
||||
REA_GHIDRA_STARTUP_TIMEOUT_MS: "600000",
|
||||
REA_GHIDRA_DISABLED_ANALYZERS: disabled.join(","),
|
||||
REA_MCP_INPUT_SCHEMA_PROFILE: "compact",
|
||||
REA_LOG_LEVEL: "silent",
|
||||
},
|
||||
stderr: "pipe",
|
||||
});
|
||||
serverLog = createWriteStream(join(output, "server.log"));
|
||||
serverLogFinished = finished(serverLog);
|
||||
serverLogFinished.catch(() => {});
|
||||
transport.stderr?.pipe(serverLog);
|
||||
client = new Client({
|
||||
name: "large-native-inventory-verifier",
|
||||
version: "1",
|
||||
});
|
||||
call = createRetainedQueryCaller(client, (receipt, event) => {
|
||||
report.calls.push(receipt);
|
||||
transcript.push(event);
|
||||
});
|
||||
await client.connect(transport);
|
||||
const catalog = await client.listTools();
|
||||
await writeFile(join(output, "catalog.json"), JSON.stringify(catalog));
|
||||
const defs = new Map(catalog.tools.map((t) => [t.name, t]));
|
||||
const original = client.callTool.bind(client);
|
||||
client.callTool = (input, options) =>
|
||||
original(input, {
|
||||
...options,
|
||||
timeout: 900000,
|
||||
toolDefinition: defs.get(input.name),
|
||||
});
|
||||
const raw = async (name, args, phase) => {
|
||||
const r = await call(name, args, phase);
|
||||
if (r.isError) {
|
||||
const e = JSON.parse(r.content.find((c) => c.type === "text").text).error;
|
||||
throw new Error(
|
||||
name +
|
||||
": " +
|
||||
e.code +
|
||||
": " +
|
||||
(e.details?.diagnostics?.failure_message ?? e.message),
|
||||
);
|
||||
}
|
||||
return r;
|
||||
};
|
||||
const good = async (name, args, phase) =>
|
||||
parseEvidence((await raw(name, args, phase)).structuredContent);
|
||||
await raw("open_binary", { path: target, provider_id: "ghidra" }, "open");
|
||||
opened = true;
|
||||
const procedures = await good(
|
||||
"list_procedures",
|
||||
{ detail: "summary" },
|
||||
"functions",
|
||||
);
|
||||
report.procedures = procedures.normalized_result;
|
||||
const full = await call("list_strings", {}, "complete-strings");
|
||||
report.complete_strings_refused = full.isError === true;
|
||||
if (full.isError) {
|
||||
const error = JSON.parse(
|
||||
full.content.find((c) => c.type === "text").text,
|
||||
).error;
|
||||
assert.equal(error.code, "resource_constraint");
|
||||
assert.equal(error.details.reported_limits.boundary, "mcp-response");
|
||||
report.overflow_evidence_id =
|
||||
error.details.reported_limits.evidence_reference.evidence_id;
|
||||
}
|
||||
const parentId =
|
||||
report.overflow_evidence_id ??
|
||||
parseEvidence(full.structuredContent).evidence_id;
|
||||
const strings = await good(
|
||||
"inspect_analysis_view",
|
||||
{
|
||||
source: { kind: "retained-evidence", evidence_id: parentId },
|
||||
view: { kind: "summary" },
|
||||
},
|
||||
"summary-strings",
|
||||
);
|
||||
const summary = strings.normalized_result;
|
||||
report.strings = summary;
|
||||
if (report.overflow_evidence_id)
|
||||
assert.equal(summary.parent_evidence_id, report.overflow_evidence_id);
|
||||
const selected = [];
|
||||
const total = summary.summary.row_count;
|
||||
for (const offset of [...new Set([0, Math.max(0, total - 3)])]) {
|
||||
const e = await good(
|
||||
"inspect_analysis_view",
|
||||
{
|
||||
source: {
|
||||
kind: "retained-evidence",
|
||||
evidence_id: summary.parent_evidence_id,
|
||||
},
|
||||
view: { kind: "native-inventory", offset, limit: 3, fields: ["value"] },
|
||||
},
|
||||
"selected-strings",
|
||||
);
|
||||
assert.equal(e.normalized_result.coverage.total, total);
|
||||
selected.push(...e.normalized_result.rows);
|
||||
}
|
||||
report.byte_oracles = [];
|
||||
const file = await open(target, "r");
|
||||
try {
|
||||
for (const row of selected) {
|
||||
const mapping = await good(
|
||||
"address_to_file_offset",
|
||||
{ address: row.record.address },
|
||||
"string-file-coordinate",
|
||||
);
|
||||
const offset = mapping.normalized_result.file_offset;
|
||||
assert.ok(Number.isSafeInteger(offset));
|
||||
const expected = Buffer.from(row.record.value, "utf8"),
|
||||
bytes = Buffer.alloc(expected.length);
|
||||
const read = await file.read(bytes, 0, bytes.length, offset);
|
||||
assert.equal(read.bytesRead, bytes.length);
|
||||
const encoding =
|
||||
row.record.string?.encoding ?? row.record.decoding?.encoding;
|
||||
if (["utf-8", "UTF-8", "ascii", "ASCII", "US-ASCII"].includes(encoding))
|
||||
assert.deepEqual(bytes, expected);
|
||||
report.byte_oracles.push({
|
||||
source_index: row.source_index,
|
||||
address: row.record.address,
|
||||
file_offset: offset,
|
||||
encoding,
|
||||
utf8_bytes_match: bytes.equals(expected),
|
||||
bytes_sha256: createHash("sha256").update(bytes).digest("hex"),
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
await file.close();
|
||||
}
|
||||
await raw(
|
||||
"export_evidence_bundle",
|
||||
{ path: join(output, "complete-evidence.json") },
|
||||
"export",
|
||||
);
|
||||
await raw("close_binary", {}, "close");
|
||||
opened = false;
|
||||
const bundle = JSON.parse(
|
||||
await readFile(join(output, "complete-evidence.json"), "utf8"),
|
||||
);
|
||||
const source = parseEvidence(
|
||||
bundle.records.find((e) => e.evidence_id === summary.parent_evidence_id),
|
||||
);
|
||||
assert.equal(source.subject.digest.sha256, report.sha256);
|
||||
assert.equal(source.normalized_result.length, total);
|
||||
for (const row of selected)
|
||||
assert.equal(
|
||||
row.record.value,
|
||||
source.normalized_result[row.source_index].value,
|
||||
);
|
||||
const fn = parseEvidence(
|
||||
bundle.records.find(
|
||||
(e) => e.evidence_id === report.procedures.parent_evidence_id,
|
||||
),
|
||||
);
|
||||
assert.equal(fn.subject.digest.sha256, report.sha256);
|
||||
report.procedure_count = fn.normalized_result.length;
|
||||
assert.deepEqual(
|
||||
fn.analysis_profile.parameters.analysis_options.disabled_analyzers,
|
||||
disabled,
|
||||
);
|
||||
assert.equal(
|
||||
fn.evidence_links.length,
|
||||
0,
|
||||
"This capture expects a self-contained inventory; retain its full source closure otherwise.",
|
||||
);
|
||||
await writeFile(
|
||||
join(output, "functions.evidence.json"),
|
||||
JSON.stringify(createEvidenceBundle([fn])),
|
||||
{ flag: "wx" },
|
||||
);
|
||||
report.profile_digest = fn.analysis_profile.digest;
|
||||
report.source_string_count = source.normalized_result.length;
|
||||
report.status = "passed";
|
||||
} catch (cause) {
|
||||
report.status = "failed";
|
||||
report.failure = String(cause?.stack ?? cause);
|
||||
process.exitCode = 1;
|
||||
} finally {
|
||||
if (opened)
|
||||
try {
|
||||
const r = await call("close_binary", {}, "cleanup");
|
||||
assert.notEqual(r.isError, true);
|
||||
} catch (cause) {
|
||||
report.close_failure = String(cause);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
report.elapsed_ms = performance.now() - start;
|
||||
if (
|
||||
!(await finalizeRetainedQueryReceipt({
|
||||
report,
|
||||
client,
|
||||
transport,
|
||||
account,
|
||||
serverLog,
|
||||
serverLogFinished,
|
||||
output,
|
||||
transcript,
|
||||
}))
|
||||
)
|
||||
process.exitCode = 1;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"description": "Ghidra 12.1.4 Linux x86-64 read-only inventory verification profile; ASCII Strings remains enabled. Default-analysis and complete function-body coverage are not claimed.",
|
||||
"names": [
|
||||
"Condense Filler Bytes",
|
||||
"Apply Data Archives",
|
||||
"DWARF",
|
||||
"Data Reference",
|
||||
"ELF Scalar Operand References",
|
||||
"Embedded Media",
|
||||
"External Symbol Resolver",
|
||||
"Non-Returning Functions - Known",
|
||||
"Create Address Tables",
|
||||
"Call-Fixup Installer",
|
||||
"Disassemble Entry Points",
|
||||
"External Entry References",
|
||||
"Subroutine References",
|
||||
"Shared Return Calls",
|
||||
"Stack",
|
||||
"X86 Function Callee Purge",
|
||||
"GCC Exception Handlers",
|
||||
"Aggressive Instruction Finder",
|
||||
"Call Convention ID",
|
||||
"Decompiler Parameter ID",
|
||||
"Decompiler Switch Analysis",
|
||||
"Function ID",
|
||||
"Demangler GNU",
|
||||
"x86 Constant Reference Analyzer",
|
||||
"Reference",
|
||||
"Non-Returning Functions - Discovered",
|
||||
"Function Start Search",
|
||||
"Function Start Search After Code",
|
||||
"Function Start Pre Search"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
# Native matching capacity verification
|
||||
|
||||
The runtime setting changes admission headroom, not matching semantics. The
|
||||
96 MiB default is preserved. This experiment checks the real CLI and stdio MCP
|
||||
with synthetic Ghidra-shaped rows and a real pair of stripped LLVM inventories.
|
||||
It is not evidence of faster matching or behavioral equivalence.
|
||||
|
||||
The control is main `14631f698`; candidate runtime is `115df89c9`, based on
|
||||
Kaoru0822's #7112 (`87d3d6022`, `0600c7d11`) with main's #7108 provenance
|
||||
accounting retained. Imported commits retain Kaoru's authorship and their
|
||||
Claude Opus 5.5 co-author trailers.
|
||||
|
||||
## Synthetic capacity
|
||||
|
||||
Each side has 20,000 or 50,000 unique imported names, original source indices,
|
||||
explicit symbol/procedure metadata and different addresses. Provider identity
|
||||
and limitations explicitly label these as synthetic; no native provider ran.
|
||||
The known answer is every row paired by its unique exact name, with no
|
||||
left-only, right-only or unresolved groups. Verification authenticates complete
|
||||
exports and checks every member against its original inventory identity,
|
||||
index, address and name. MCP also reads first/last selected pages.
|
||||
|
||||
CLI uses two source bundles and writes the complete matching/source closure.
|
||||
The control refuses both sizes at 96 MiB. The candidate succeeds with the
|
||||
536870912-byte launch seed. MCP imports the same records, tries the default,
|
||||
raises the runtime setting to 512 MiB and repeats matching twice. The 20k
|
||||
retained case already fits the default; the 50k case is refused then recovers
|
||||
without restarting or replacing inputs. Matching Evidence IDs remain stable
|
||||
across allowance changes and repeated successful requests.
|
||||
|
||||
| Lane / rows per side | Outcome | Whole elapsed s | CPU s | Family peak GiB | Text token estimate |
|
||||
| -------------------- | ------- | --------------: | ----: | --------------: | ------------------: |
|
||||
| mcp-20000 | matched | 25.89 | 30.77 | 1.77 | 938,961 |
|
||||
| mcp-50000 | matched | 71.74 | 81.87 | 2.31 | 938,209 |
|
||||
| cli-20000 | matched | 27.55 | 31.04 | 1.02 | 1,166 |
|
||||
| cli-50000 | matched | 63.61 | 70.37 | 1.53 | 1,138 |
|
||||
| main-cli-20000 | refused | 10.45 | 12.28 | 0.72 | 364 |
|
||||
| main-cli-50000 | refused | 17.85 | 19.17 | 0.90 | 357 |
|
||||
|
||||
These whole-harness costs include input preparation/authentication, client and
|
||||
catalog work, matching, output publication and offline correctness checks.
|
||||
MCP rows include the initial default attempt, settings change, two raised-budget
|
||||
matches, two selected pages and export. CLI rows include one fresh invocation
|
||||
and its complete export. The lanes do different amounts of validation/work;
|
||||
these numbers are not an adapter speed comparison. A refused control does not
|
||||
complete the matching task, so no completed-task speedup can be inferred.
|
||||
|
||||
The first successful MCP match is 3.535 s at 20k (under the default, before
|
||||
raising it), and 12.393 s at 50k (first raised-budget call). Last repeated calls
|
||||
are 3.216 s and 13.640 s. Original receipt phase labels `cold`/`warm` denoted
|
||||
raised-budget call order; the published driver now uses `first-raised` and
|
||||
`repeat-raised` to avoid mislabelling the already-warm 20k case. Every final
|
||||
whole harness starts a fresh process; OS file caches are warm. CLI matching
|
||||
invocations, including complete publication, are 19.546 s / 47.712 s; the extra
|
||||
whole-harness cost is visible in the table.
|
||||
|
||||
Token estimates use cl100k_base over one raw catalog discovery, every argument
|
||||
and every text reply, including refusal, settings, paging and export. CLI counts
|
||||
its invocation arguments, request JSON and actual stdout. Local Evidence file
|
||||
contents are not sent to the agent; their loading and checking are included in
|
||||
resource costs. Catalog discovery dominates MCP. Initialization, JSON-RPC
|
||||
framing, prompts, host rendering and replay are excluded; actual model usage
|
||||
was not measured. The matching summary keeps the full report out of the MCP
|
||||
frame; the complete 50k export is about 85 MiB and remains accessible locally.
|
||||
|
||||
## Real stripped inventories
|
||||
|
||||
The unmodified, stripped Linux x86-64 LLVM 14 and 15 shared libraries produce
|
||||
43,643 and 45,230 Ghidra procedure observations. Target digests:
|
||||
|
||||
- LLVM 14: `436887791de0478d72c8323be99df69d6d0cf82745e5abec79d5e0374f4df560`.
|
||||
- LLVM 15: `e45650cba881293ba3b6a0e7241920fc48fa4a522ca6dfda72dc94f5c54e44b0`.
|
||||
|
||||
Both MCP and CLI match the same complete inputs at 512 MiB: 34,008 exact-symbol
|
||||
correspondences, 9,635 left-only observations and 11,222 right-only observations.
|
||||
Every source row appears exactly once, with its original inventory ID, source
|
||||
index, address and name. Matching Evidence IDs are identical between adapters
|
||||
for both synthetic sizes and this real pair. The default retained request is
|
||||
refused. Left/right-only observations do not establish actual product additions
|
||||
or removals; inventory discovery completeness remains unknown.
|
||||
|
||||
An independent GNU `nm --without-symbol-versions --dynamic --no-demangle
|
||||
--format=posix` comparison corroborates 32,935 matched raw dynamic symbol names.
|
||||
The other 1,073 are not corroborated by that raw-name comparison (examples include
|
||||
`<EXTERNAL>::lstat64`); no raw-name equivalence is claimed for that remainder.
|
||||
The independent check costs 1.86 s elapsed/CPU and 0.34 GiB sampled RSS, in
|
||||
addition to the matching harness. No byte fingerprints or decompilation were
|
||||
supplied, and no behavioral-equivalence claim is made.
|
||||
|
||||
| Real matching lane | Whole elapsed s | CPU s | Family peak GiB | Text token estimate |
|
||||
| ------------------ | --------------: | ----: | --------------: | ------------------: |
|
||||
| real-mcp | 56.04 | 62.63 | 2.11 | 943,676 |
|
||||
| real-cli | 74.04 | 79.28 | 1.36 | 2,482 |
|
||||
|
||||
The capture phase is measured separately: LLVM 14 takes 280.14 s elapsed,
|
||||
366.63 s CPU and 2.88 GiB family RSS; LLVM 15 takes 278.39 s, 358.03 s and
|
||||
3.11 GiB. These are actual REA/Ghidra MCP captures with Ghidra 12.1.4/JDK
|
||||
21.0.12.1 and the explicitly committed 29-disabled-analyzer profile in
|
||||
`native-inventory-profile.json`, with ASCII Strings enabled. This deliberately
|
||||
narrow profile does not establish default-analysis or full body coverage.
|
||||
The captures also validate 177,830/193,493 defined strings, retained summaries
|
||||
and first/last pages after complete delivery exceeds 10 MiB. Six selected UTF-8
|
||||
observations per target match independently read original-file bytes, including
|
||||
non-default `.shstrtab` address-space observations. Exports are authenticated
|
||||
before selecting unchanged, self-contained procedure Evidence into the two
|
||||
function-only bundles; no Ghidra fact is reconstructed or relabelled.
|
||||
|
||||
End to end, sum both capture phases, the chosen matching lane and the independent
|
||||
symbol check. Sequential peak RSS is the maximum of the phases. The actual
|
||||
workflow used separate capture and matching connections, so its token accounting
|
||||
must include three catalog discoveries and every capture RPC, not merely the
|
||||
matching row above. Capture/discovery totals are reported below. Failed early
|
||||
capture attempts (600 s SDK timeout, unregistered delayed-analyzer name, and a
|
||||
driver exporting after session close) remain failed verification attempts;
|
||||
all cleaned up without OOM. The final driver exports before closing, then
|
||||
performs memory-intensive offline checks after the Java process has exited.
|
||||
|
||||
Capture reproduction (provide an appropriate process-family supervisor):
|
||||
|
||||
```sh
|
||||
node experiments/analysis-efficiency/native-inventory-capture.mjs \
|
||||
--runtime /absolute/candidate --target /absolute/libLLVM.so \
|
||||
--output /tmp/llvm-capture --ghidra /absolute/ghidra --java /absolute/jdk \
|
||||
--profile experiments/analysis-efficiency/native-inventory-profile.json
|
||||
```
|
||||
|
||||
Use each capture's `functions.evidence.json` as the matching driver's input.
|
||||
|
||||
| Capture | Catalog tokens | Arguments + replies | Total token estimate |
|
||||
| ---------------------------- | -------------: | ------------------: | -------------------: |
|
||||
| rea-6426-llvm14-inventory-v4 | 932,545 | 14,685 | 947,230 |
|
||||
| rea-6406-llvm15-inventory | 932,629 | 14,663 | 947,292 |
|
||||
|
||||
Both captures + real-mcp + independent symbol check: 616.42 s elapsed, 789.15 s CPU, 2,838,198 estimated text tokens; sequential sampled family peak 3.11 GiB.
|
||||
|
||||
Both captures + real-cli + independent symbol check: 634.43 s elapsed, 805.81 s CPU, 1,897,004 estimated text tokens; sequential sampled family peak 3.11 GiB.
|
||||
|
||||
Early failed capture attempts, excluded from those successful-run totals:
|
||||
|
||||
| Attempt | Elapsed s | CPU s | Peak family GiB | Outcome |
|
||||
| ---------------------------- | --------: | -----: | --------------: | ---------------------- |
|
||||
| rea-6426-llvm14-inventory | 610.34 | 920.41 | 2.86 | SDK timeout |
|
||||
| rea-6426-llvm14-inventory-v2 | 39.26 | 66.81 | 2.16 | unregistered analyzer |
|
||||
| rea-6426-llvm14-inventory-v3 | 217.65 | 285.06 | 3.12 | driver export ordering |
|
||||
|
||||
## Reproduce
|
||||
|
||||
Use Node 24.22.1, the pinned npm install, and built runtime worktrees. Supply
|
||||
new output directories. The driver keeps the catalog, actual RPC transcript,
|
||||
phase receipts, complete matching bundle and verification results.
|
||||
|
||||
```sh
|
||||
node experiments/analysis-efficiency/native-matching-capacity.mjs \
|
||||
--runtime /absolute/candidate --output /tmp/matching-50k \
|
||||
--adapter mcp --rows 50000 --allowance 536870912
|
||||
node experiments/analysis-efficiency/native-matching-capacity.mjs \
|
||||
--runtime /absolute/candidate --output /tmp/matching-cli \
|
||||
--adapter cli --left /tmp/matching-50k/left.evidence.json \
|
||||
--right /tmp/matching-50k/right.evidence.json --allowance 536870912
|
||||
```
|
||||
|
||||
For real data, replace the two paths with authenticated one-inventory bundles
|
||||
captured from the desired targets; no provider is started by the matcher.
|
||||
Omit allowance and use `--refusal` when a baseline refusal is the expected oracle.
|
||||
Do not split inventories: that changes global uniqueness.
|
||||
|
||||
All measured runs used two CPU affinity slots and a supervisor with an 8 GiB
|
||||
summed process-family RSS ceiling and a 4 GiB available-host-memory floor.
|
||||
Node old space was 3 GiB. No resource stop or OOM occurred. RSS is summed every
|
||||
100 ms, can count shared pages repeatedly and can miss short peaks. CPU includes
|
||||
all waited descendants. The driver does not itself enforce a family limit;
|
||||
apply a supervisor appropriate to the host. The 1 GiB setting maximum is a
|
||||
representation policy, not a process-memory or host-capacity guarantee.
|
||||
|
||||
The small CLI harness pilot failed because it treated the documented
|
||||
`{summary, evidence_bundle}` CLI envelope as bare Evidence; its failed receipt
|
||||
was retained. The named envelope was corrected before all capacity measurements.
|
||||
@@ -0,0 +1,413 @@
|
||||
/** Real CLI/SDK capacity investigations; synthetic inputs are explicitly labelled. */
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { createWriteStream } from "node:fs";
|
||||
import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { finished } from "node:stream/promises";
|
||||
import { parseArgs, promisify } from "node:util";
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
import { createRetainedQueryCaller } from "./retained-query-calls.mjs";
|
||||
import { finalizeRetainedQueryReceipt } from "./retained-query-finalization.mjs";
|
||||
const { values: v } = parseArgs({
|
||||
options: {
|
||||
...Object.fromEntries(
|
||||
[
|
||||
"runtime",
|
||||
"left",
|
||||
"right",
|
||||
"output",
|
||||
"adapter",
|
||||
"allowance",
|
||||
"rows",
|
||||
].map((k) => [k, { type: "string" }]),
|
||||
),
|
||||
refusal: { type: "boolean", default: false },
|
||||
},
|
||||
});
|
||||
for (const k of ["runtime", "output"]) {
|
||||
assert.ok(v[k]);
|
||||
v[k] = resolve(v[k]);
|
||||
}
|
||||
assert.ok(["cli", "mcp"].includes(v.adapter));
|
||||
await mkdir(v.output);
|
||||
let account, client, transport, serverLog, serverLogFinished, call;
|
||||
const report = {
|
||||
adapter: v.adapter,
|
||||
runtime: v.runtime,
|
||||
synthetic_rows: v.rows ? Number(v.rows) : null,
|
||||
allowance: v.allowance ? Number(v.allowance) : null,
|
||||
calls: [],
|
||||
runs: [],
|
||||
},
|
||||
transcript = [];
|
||||
const start = performance.now();
|
||||
try {
|
||||
const load = (p) => import(pathToFileURL(join(v.runtime, p)).href);
|
||||
const { createEvidence, parseEvidence } = await load(
|
||||
"dist/domain/evidence.js",
|
||||
);
|
||||
const { createEvidenceBundle, parseEvidenceBundle } = await load(
|
||||
"dist/domain/evidenceBundle.js",
|
||||
);
|
||||
const { nativeFunctionMatchingSchema } = await load(
|
||||
"dist/domain/native/functionMatching.js",
|
||||
);
|
||||
report.revision = (
|
||||
await promisify(execFile)("git", ["-C", v.runtime, "rev-parse", "HEAD"])
|
||||
).stdout.trim();
|
||||
const sources = [];
|
||||
for (const [index, key] of ["left", "right"].entries()) {
|
||||
if (v.rows) {
|
||||
const count = Number(v.rows);
|
||||
assert.ok(Number.isSafeInteger(count) && count > 0);
|
||||
const source = createEvidence(
|
||||
{
|
||||
path: `/synthetic/${key}.elf`,
|
||||
sha256: String(index + 1).repeat(64),
|
||||
format: "elf",
|
||||
architecture: "x86_64",
|
||||
},
|
||||
{
|
||||
id: "fixture.capacity",
|
||||
name: "Synthetic capacity fixture",
|
||||
version: "1",
|
||||
},
|
||||
{
|
||||
operation: "list_procedures",
|
||||
parameters: {},
|
||||
result: Array.from({ length: count }, (_, i) => ({
|
||||
address: `0x${(0x400000 + index * 0x400000 + i * 16).toString(16)}`,
|
||||
value: `subsystem_function_${i}`,
|
||||
symbol: {
|
||||
primary: true,
|
||||
dynamic: false,
|
||||
external: false,
|
||||
type: "Function",
|
||||
source: "imported",
|
||||
},
|
||||
procedure: { external: false, thunk: false, thunk_target: null },
|
||||
})),
|
||||
limitations: [
|
||||
"Synthetic Ghidra-shaped inventory; no native provider ran.",
|
||||
],
|
||||
},
|
||||
);
|
||||
v[key] = join(v.output, `${key}.evidence.json`);
|
||||
await writeFile(v[key], JSON.stringify(createEvidenceBundle([source])), {
|
||||
flag: "wx",
|
||||
});
|
||||
sources.push(source);
|
||||
} else {
|
||||
assert.ok(v[key], `Missing --${key}`);
|
||||
v[key] = resolve(v[key]);
|
||||
const bundle = parseEvidenceBundle(
|
||||
JSON.parse(await readFile(v[key], "utf8")),
|
||||
);
|
||||
const inventories = bundle.records.filter(
|
||||
(e) => e.operation === "list_procedures",
|
||||
);
|
||||
assert.equal(inventories.length, 1);
|
||||
sources.push(inventories[0]);
|
||||
}
|
||||
}
|
||||
const request = {
|
||||
left: { kind: "retained-evidence", evidence_id: sources[0].evidence_id },
|
||||
right: { kind: "retained-evidence", evidence_id: sources[1].evidence_id },
|
||||
detail: "summary",
|
||||
};
|
||||
report.input_kind = sources.every((e) => e.provider.id === "fixture.capacity")
|
||||
? "synthetic"
|
||||
: "captured-provider";
|
||||
if (report.input_kind === "synthetic")
|
||||
report.synthetic_rows = sources[0].normalized_result.length;
|
||||
report.sources = sources.map((e, i) => ({
|
||||
path: v[["left", "right"][i]],
|
||||
evidence_id: e.evidence_id,
|
||||
subject: e.subject,
|
||||
provider: e.provider,
|
||||
profile_digest: e.analysis_profile?.digest ?? null,
|
||||
rows: e.normalized_result.length,
|
||||
}));
|
||||
account = await mkdtemp(join(tmpdir(), "rea-matching-capacity-"));
|
||||
const environment = {
|
||||
HOME: account,
|
||||
PATH: process.env.PATH,
|
||||
NODE_OPTIONS: "--max-old-space-size=3072",
|
||||
REA_LOG_LEVEL: "silent",
|
||||
REA_MCP_INPUT_SCHEMA_PROFILE: "compact",
|
||||
};
|
||||
const errorOf = (reply) =>
|
||||
JSON.parse(reply.content.find((c) => c.type === "text").text).error;
|
||||
let parentId,
|
||||
bundlePath = join(v.output, "matching.evidence.json");
|
||||
if (v.adapter === "cli") {
|
||||
if (v.allowance)
|
||||
environment.REA_NATIVE_MATCHING_REPRESENTATION_BYTES = v.allowance;
|
||||
const path = join(v.output, "request.json");
|
||||
await writeFile(path, JSON.stringify(request));
|
||||
const args = [
|
||||
join(v.runtime, "scripts/rea.mjs"),
|
||||
"match-native-functions",
|
||||
path,
|
||||
"--source_bundle",
|
||||
v.left,
|
||||
"--source_bundle",
|
||||
v.right,
|
||||
"--evidence_output",
|
||||
bundlePath,
|
||||
"--json",
|
||||
];
|
||||
let result,
|
||||
exit = 0;
|
||||
const at = performance.now();
|
||||
try {
|
||||
result = await promisify(execFile)(process.execPath, args, {
|
||||
cwd: v.runtime,
|
||||
env: environment,
|
||||
maxBuffer: 16 * 1024 ** 2,
|
||||
timeout: 300000,
|
||||
});
|
||||
} catch (cause) {
|
||||
if (cause.code !== 1) throw cause;
|
||||
result = cause;
|
||||
exit = 1;
|
||||
}
|
||||
await writeFile(join(v.output, "cli.stdout.json"), result.stdout);
|
||||
await writeFile(join(v.output, "cli.stderr.log"), result.stderr ?? "");
|
||||
const reply = JSON.parse(result.stdout);
|
||||
report.runs.push({
|
||||
phase: "cold",
|
||||
elapsed_ms: performance.now() - at,
|
||||
exit_code: exit,
|
||||
});
|
||||
transcript.push({
|
||||
name: "match-native-functions",
|
||||
input: request,
|
||||
args,
|
||||
reply,
|
||||
});
|
||||
if (exit) {
|
||||
assert.equal(v.refusal, true);
|
||||
assert.equal(reply.code, "resource_constraint");
|
||||
assert.equal(reply.retryable, false);
|
||||
report.refusal = reply;
|
||||
report.outcome = "refused";
|
||||
} else {
|
||||
assert.equal(v.refusal, false);
|
||||
const e = parseEvidence(reply.summary);
|
||||
assert.equal(reply.evidence_bundle.path, bundlePath);
|
||||
parentId = e.normalized_result.parent_evidence_id;
|
||||
report.runs[0].summary = e.normalized_result.summary;
|
||||
report.outcome = "matched";
|
||||
}
|
||||
} else {
|
||||
transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [join(v.runtime, "scripts/rea.mjs"), "mcp"],
|
||||
cwd: v.runtime,
|
||||
env: environment,
|
||||
stderr: "pipe",
|
||||
});
|
||||
serverLog = createWriteStream(join(v.output, "server.log"));
|
||||
serverLogFinished = finished(serverLog);
|
||||
serverLogFinished.catch(() => {});
|
||||
transport.stderr?.pipe(serverLog);
|
||||
client = new Client({ name: "native-matching-capacity", version: "1" });
|
||||
call = createRetainedQueryCaller(client, (receipt, event) => {
|
||||
report.calls.push(receipt);
|
||||
transcript.push(event);
|
||||
});
|
||||
await client.connect(transport);
|
||||
const catalog = await client.listTools();
|
||||
await writeFile(join(v.output, "catalog.json"), JSON.stringify(catalog));
|
||||
const definitions = new Map(catalog.tools.map((t) => [t.name, t]));
|
||||
const original = client.callTool.bind(client);
|
||||
client.callTool = (input, options) =>
|
||||
original(input, {
|
||||
...options,
|
||||
timeout: 300000,
|
||||
toolDefinition: definitions.get(input.name),
|
||||
});
|
||||
const good = async (name, input, phase) => {
|
||||
const r = await call(name, input, phase);
|
||||
if (r.isError) {
|
||||
const e = errorOf(r);
|
||||
throw new Error(`${name}: ${e.code}: ${e.message}`);
|
||||
}
|
||||
return r;
|
||||
};
|
||||
for (const side of ["left", "right"])
|
||||
await good("import_evidence_bundle", { path: v[side] }, `import:${side}`);
|
||||
const initial = await call("match_native_functions", request, "default");
|
||||
if (initial.isError) {
|
||||
const e = errorOf(initial);
|
||||
assert.equal(e.code, "resource_constraint");
|
||||
assert.equal(e.retryable, false);
|
||||
report.refusal = e;
|
||||
report.default_admitted = false;
|
||||
} else {
|
||||
report.default_admitted = true;
|
||||
parentId = parseEvidence(initial.structuredContent).normalized_result
|
||||
.parent_evidence_id;
|
||||
}
|
||||
if (v.allowance) {
|
||||
await good(
|
||||
"update_analysis_settings",
|
||||
{
|
||||
changes: [
|
||||
{
|
||||
setting: "native.matching_representation_bytes",
|
||||
action: "set",
|
||||
value: Number(v.allowance),
|
||||
},
|
||||
],
|
||||
},
|
||||
"raise-budget",
|
||||
);
|
||||
for (const phase of ["first-raised", "repeat-raised"]) {
|
||||
const at = performance.now();
|
||||
const e = parseEvidence(
|
||||
(await good("match_native_functions", request, phase))
|
||||
.structuredContent,
|
||||
);
|
||||
const id = e.normalized_result.parent_evidence_id;
|
||||
if (parentId) assert.equal(id, parentId);
|
||||
parentId = id;
|
||||
report.runs.push({
|
||||
phase,
|
||||
elapsed_ms: performance.now() - at,
|
||||
summary: e.normalized_result.summary,
|
||||
});
|
||||
}
|
||||
report.outcome = "matched";
|
||||
} else if (initial.isError) {
|
||||
assert.equal(v.refusal, true);
|
||||
report.outcome = "refused";
|
||||
} else {
|
||||
assert.equal(v.refusal, false);
|
||||
report.outcome = "matched";
|
||||
}
|
||||
if (report.outcome === "matched") {
|
||||
report.selected = [];
|
||||
for (const offset of [
|
||||
...new Set([0, Math.max(0, sources[0].normalized_result.length - 2)]),
|
||||
]) {
|
||||
const e = parseEvidence(
|
||||
(
|
||||
await good(
|
||||
"inspect_analysis_view",
|
||||
{
|
||||
source: { kind: "retained-evidence", evidence_id: parentId },
|
||||
view: {
|
||||
kind: "function-matches",
|
||||
collection: "functions",
|
||||
sides: ["left"],
|
||||
fields: ["name"],
|
||||
offset,
|
||||
limit: 2,
|
||||
},
|
||||
},
|
||||
"selected-correspondences",
|
||||
)
|
||||
).structuredContent,
|
||||
);
|
||||
assert.equal(
|
||||
e.normalized_result.coverage.total,
|
||||
sources[0].normalized_result.length,
|
||||
);
|
||||
for (const row of e.normalized_result.items) {
|
||||
const original = sources[0].normalized_result[row.source_index];
|
||||
assert.equal(row.inventory_evidence_id, sources[0].evidence_id);
|
||||
assert.equal(row.address, original.address);
|
||||
assert.equal(row.name, original.value);
|
||||
}
|
||||
report.selected.push(...e.normalized_result.items);
|
||||
}
|
||||
await good("export_evidence_bundle", { path: bundlePath }, "export");
|
||||
// Release the server's retained representation before offline verification.
|
||||
await client.close();
|
||||
await transport.close();
|
||||
}
|
||||
}
|
||||
if (report.outcome === "matched") {
|
||||
const bundle = parseEvidenceBundle(
|
||||
JSON.parse(await readFile(bundlePath, "utf8")),
|
||||
);
|
||||
const evidence = bundle.records.find((e) => e.evidence_id === parentId);
|
||||
assert.ok(evidence);
|
||||
const result = nativeFunctionMatchingSchema.parse(
|
||||
evidence.normalized_result,
|
||||
);
|
||||
assert.deepEqual(
|
||||
[result.left.evidence_id, result.right.evidence_id],
|
||||
sources.map((e) => e.evidence_id),
|
||||
);
|
||||
const seen = [new Set(), new Set()];
|
||||
for (const group of result.matches)
|
||||
for (const [side, key] of ["left", "right"].entries())
|
||||
for (const member of group[key]) {
|
||||
const source = sources[side];
|
||||
assert.equal(member.inventory_evidence_id, source.evidence_id);
|
||||
assert.ok(!seen[side].has(member.source_index));
|
||||
seen[side].add(member.source_index);
|
||||
const row = source.normalized_result[member.source_index];
|
||||
assert.ok(row);
|
||||
assert.equal(member.address, row.address);
|
||||
assert.equal(member.name, row.value);
|
||||
assert.ok(member.evidence_links.includes(source.evidence_id));
|
||||
if (report.synthetic_rows !== null) {
|
||||
assert.equal(group.state, "matched");
|
||||
assert.equal(group.basis, "exact-symbol");
|
||||
assert.equal(group.left.length, 1);
|
||||
assert.equal(group.right.length, 1);
|
||||
assert.equal(
|
||||
group.left[0].source_index,
|
||||
group.right[0].source_index,
|
||||
);
|
||||
}
|
||||
}
|
||||
assert.deepEqual(
|
||||
seen.map((s) => s.size),
|
||||
sources.map((e) => e.normalized_result.length),
|
||||
);
|
||||
if (report.synthetic_rows !== null)
|
||||
assert.deepEqual(result.summary, {
|
||||
matched: report.synthetic_rows,
|
||||
only_left: 0,
|
||||
only_right: 0,
|
||||
unresolved: 0,
|
||||
});
|
||||
else assert.ok(result.summary.matched > 0);
|
||||
for (const source of sources)
|
||||
assert.ok(
|
||||
bundle.records.some((e) => e.evidence_id === source.evidence_id),
|
||||
);
|
||||
report.parent_evidence_id = parentId;
|
||||
report.summary = result.summary;
|
||||
report.verified_members = seen.map((s) => s.size);
|
||||
}
|
||||
report.status = "passed";
|
||||
} catch (cause) {
|
||||
report.status = "failed";
|
||||
report.failure = String(cause?.stack ?? cause);
|
||||
process.exitCode = 1;
|
||||
} finally {
|
||||
report.elapsed_ms = performance.now() - start;
|
||||
if (
|
||||
!(await finalizeRetainedQueryReceipt({
|
||||
report,
|
||||
client,
|
||||
transport,
|
||||
account,
|
||||
serverLog,
|
||||
serverLogFinished,
|
||||
output: v.output,
|
||||
transcript,
|
||||
}))
|
||||
)
|
||||
process.exitCode = 1;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import json,subprocess,argparse,hashlib
|
||||
p=argparse.ArgumentParser();p.add_argument("--left-target",required=True);p.add_argument("--right-target",required=True);p.add_argument("--matching-report",required=True);p.add_argument("--matching-bundle",required=True);args=p.parse_args()
|
||||
paths=[args.left_target,args.right_target]
|
||||
receipt=json.load(open(args.matching_report))
|
||||
for i,path in enumerate(paths):
|
||||
h=hashlib.sha256()
|
||||
with open(path,'rb') as f:
|
||||
for chunk in iter(lambda:f.read(1024*1024),b''):h.update(chunk)
|
||||
assert h.hexdigest()==receipt['sources'][i]['subject']['digest']['sha256']
|
||||
names=[]
|
||||
for p in paths:
|
||||
r=subprocess.run(['nm','--without-symbol-versions','--dynamic','--no-demangle','--format=posix',p],capture_output=True,text=True,check=True,timeout=20)
|
||||
rows=[line.split() for line in r.stdout.splitlines() if line]
|
||||
assert all(2<=len(row)<=4 and len(row[1])==1 for row in rows)
|
||||
names.append({row[0] for row in rows})
|
||||
report=receipt;bundle=json.load(open(args.matching_bundle));source=next(e for e in bundle['records'] if e['evidence_id']==report['parent_evidence_id']);matches=[m for m in source['normalized_result']['matches'] if m['state']=='matched'];common=names[0]&names[1]
|
||||
verified=[];other=[]
|
||||
for m in matches:
|
||||
assert m['basis']=='exact-symbol' and len(m['left'])==len(m['right'])==1
|
||||
a=m['left'][0];b=m['right'][0];assert a['name']==b['name']
|
||||
(verified if a['name'] in common else other).append(a['name'])
|
||||
r={'command':['nm','--without-symbol-versions','--dynamic','--no-demangle','--format=posix'],'source_files':paths,'common_raw_dynamic_names':len(common),'matched_correspondences':len(matches),'raw_name_corroborated':len(verified),'provider_only_names':len(other),'provider_only_examples':other[:8]}
|
||||
print(json.dumps(r,indent=2))
|
||||
Reference in new issue
Block a user