feat(analysis-view): add native inventory summaries and pages (#5378)

Add opt-in summaries and stable selected-field pages for native inventories through the shared CLI/MCP Evidence layer. Preserve complete source access, row identity, provenance, string qualifications and unknown discovery coverage.

Record real true/jq workflows and portable old-Evidence checks, including tokenizer proxy reductions, additional call/schema costs and observed latency/memory regressions.

Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
This commit is contained in:
gensokyo-devandKaoru0822 authored and GitHub committed 2026-10-20 07:59:43 +00:00
1 parent b5d540f3a2
commit 2538214e2c
26 files changed
+2545 -217

No files matched your search

@@ -24,6 +24,17 @@ retain its complete source and receive that view immediately. Without that
advertised choice, keep the existing complete call. CLI inspection returns
portable complete Evidence; save it before projecting a selected inline view.
For native procedure/name/string inventories, select `detail: "summary"` when
the connected tool advertises it and the task needs counts or selected rows.
Use its parent Evidence ID with
`view: {kind: "native-inventory", offset: 0, limit: 32, fields: ["value"]}`.
Search tools retain the existing pattern/mode/case/document selection. Omit
`fields` for complete records; `record.address` and `source_index` remain on
selected rows. Selecting `value` keeps its string/decoding qualifications.
Parent Evidence ID plus source index preserves same-address
alternatives. Follow `coverage.next_offset` only as far as the question needs;
exhausting retained rows does not establish complete program discovery.
Use `record_unknown` to track unresolved questions in the user's investigation
and name the authority or environment still required. Supply supporting and
contradicting evidence IDs. It updates the session registry directly.
+42
View File
@@ -119,6 +119,46 @@ This marks completion of the MCP exchange; Evidence coverage, partial facts
and unknowns retain their separate meaning. Legacy replies keep their existing
framing.
`list_procedures`, `search_procedures`, `list_strings`, `search_strings` and
`list_names` accept `detail: "summary"`. Complete inline delivery remains the
default. Summary retains the original complete Evidence and returns its row
count, provider identity, analysis-profile digest, original query, parent Evidence
ID, limitations and explicit unknown discovery coverage. Delivery detail is excluded from provider arguments
and original Evidence identity.
Read selected rows with `inspect_analysis_view`:
```json
{
"source": {
"kind": "retained-evidence",
"evidence_id": "ev_<source digest>"
},
"view": {
"kind": "native-inventory",
"offset": 0,
"limit": 32,
"fields": ["value"]
}
}
```
Each row has `source_index` and `record`; parent Evidence ID plus source index
identifies same-address alternatives. Order is the original retained order.
Omit `fields` for complete records; a selected field list always retains
`record.address`. Selecting `value` also retains available `string` and
`decoding` qualifications, so native display text and unterminated fragments
remain distinguishable. Search tools apply their existing filters before retention.
Follow `coverage.next_offset`; exhaustion describes the retained list, not
complete discovery of all code or strings. An unbound source keeps
`artifact: null` and the derived Evidence keeps `subject: null`.
The CLI uses the same projector. For an original complete Evidence JSON file,
write `{"source":{"kind":"evidence-file","path":"/absolute/source.json"},"view":{"kind":"native-inventory","limit":32}}`
to `/absolute/view-request.json`, then run
`rea inspect-analysis-view /absolute/view-request.json --json`. This also reads
pre-summary source records without changing their identity; no live provider is started.
For oversized native function Evidence, `details.reported_limits.continuation`
contains a ready-to-call `inspect_analysis_view` request for the retained
pseudocode's first page. Use its `name` and `arguments` directly, then follow
@@ -127,6 +167,8 @@ limit is the view contract's default, not a guarantee that arbitrary source or
metadata fits; reduce the limit or export when needed. Continuations are
offered for completed `analyze_function` results only after session recording is
acknowledged; the complete result and its retention reference remain intact.
Oversized native inventories likewise include a ready-to-call first
`native-inventory` page. Reduce its limit or select fields when a row is large.
A broad follow-up or `get_evidence_bundle` can also exceed the response budget; exporting
preserves the complete session without sending it through a single MCP frame.
@@ -1,5 +1,10 @@
# Analysis efficiency measurements
The [native inventory view checkpoint](native-inventory-views.md) compares
complete delivery with summaries and selected rows on real `true` and jq
inventories, including oversized-response recovery, portable old Evidence,
CLI/MCP parity, tokenizer estimates and independent-engine variation.
The [retained-capture validation checkpoint](retained-capture-validation.md)
reconciles the earlier audit queue and measures immutable historical-capture
validation reuse on public compact and large qlogs, including first-view costs,
@@ -0,0 +1,115 @@
"""Count recorded native-inventory delivery, including errors and file recovery.
Requires tiktoken==0.12.0 in the experiment environment. These are serialization
proxies, not model usage or a claim about a host's schema injection/rendering.
"""
import argparse
import json
from pathlib import Path
import sys
import tiktoken
def compact(value):
return json.dumps(value, ensure_ascii=False, separators=(",", ":"), allow_nan=False)
def inspect_case(path, encoding):
report = json.loads((path / "report.json").read_text())
transcript = json.loads((path / "transcript.json").read_text())
catalog_text = (path / "catalog.json").read_text()
catalog = json.loads(catalog_text)
scope = next(
json.loads(line)
for line in reversed(path.with_suffix(".log").read_text().splitlines())
if line.startswith('{"resource_scope":')
)
observation = scope.get("payload_resource_observation") or {}
count = lambda value: len(encoding.encode(value, disallowed_special=()))
input_projection = [
{key: tool[key] for key in ["name", "description", "inputSchema"]}
for tool in catalog["tools"]
]
rpc = [event for event in transcript if "reply" in event or "failure" in event]
local = [event for event in transcript if event["name"] == "local-bundle-projection"]
arguments = sum(count(compact({"name": event["name"], "arguments": event["input"]})) for event in rpc)
text = sum(
count(item["text"])
for event in rpc
for item in event.get("reply", {}).get("content", [])
if item["type"] == "text"
)
full_replies = sum(count(compact(event["reply"])) for event in rpc if "reply" in event)
local_output = sum(count(compact(event["projection"])) for event in local)
schemas = count(compact(input_projection))
completed = (
report["status"] == "passed"
and len(report["tasks"]) == 2
and not report["cleanup_failures"]
and not report["receipt_failures"]
and scope["abort"] is None
and scope.get("payload_resource_observation_verified") is True
and observation.get("payload_exit_code") == 0
and observation.get("payload_cleanup_verified") is True
)
return {
"recording": path.name,
"status": report["status"],
"completed_with_verified_cleanup": completed,
"mode": report["mode"],
"catalog_tools": len(catalog["tools"]),
"catalog_bytes": len(catalog_text.encode()),
"rpc_calls": len(rpc),
"rpc_errors_or_rejections": sum(event.get("reply", {}).get("isError") is True or "failure" in event for event in rpc),
"rpc_argument_bytes": sum(len(compact({"name": event["name"], "arguments": event["input"]}).encode()) for event in rpc),
"rpc_reply_bytes": sum(call["reply_bytes"] or 0 for call in report["calls"]),
"local_recoveries": report.get("local_recoveries", []),
"token_estimates": {
"catalog_full_json": count(catalog_text),
"catalog_input_facing_projection": schemas,
"rpc_name_and_argument_json": arguments,
"rpc_text_content": text,
"rpc_complete_result_json": full_replies,
"local_projection_output_json": local_output,
"input_facing_catalog_plus_arguments_text_and_local_output": schemas + arguments + text + local_output,
},
"whole_scope_wall_seconds": scope["elapsed_seconds"],
"payload_family_cpu_seconds": observation["cpu_stat"]["usage_usec"] / 1_000_000 if observation.get("cpu_stat", {}).get("usage_usec") is not None else None,
"kernel_family_peak_bytes_including_file_cache": observation.get("memory_peak_bytes"),
"payload_exit_observation_verified": scope.get("payload_resource_observation_verified"),
"observed_resource_events": observation.get("resource_events"),
"final_whole_scope_events": scope.get("resource_events_final"),
"tasks": [{"phase": task["phase"], "elapsed_ms": task["elapsed_ms"]} for task in report["tasks"]],
"facts_sha256": report.get("facts_sha256"),
"sources_sha256": report.get("sources_sha256"),
}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--output", type=Path, required=True)
parser.add_argument("cases", type=Path, nargs="+")
args = parser.parse_args()
encoding = tiktoken.get_encoding("cl100k_base")
result = {
"python": sys.version,
"tiktoken": tiktoken.__version__,
"encoding": encoding.name,
"actual_model_usage": None,
"unmeasured": [
"model-host schema selection, wrapping and rendering",
"system prompts, local filesystem tool schema/program generation and follow-up reasoning",
"model input/cached/uncached/output usage",
"initialization and JSON-RPC wire framing",
"isolated correctness-oracle CPU and memory overhead",
],
"samples": [inspect_case(path, encoding) for path in args.cases],
}
args.output.write_text(json.dumps(result, indent=2) + "\n")
print(json.dumps({"samples": len(result["samples"]), "encoding": encoding.name, "actual_model_usage": None}))
if __name__ == "__main__":
main()
@@ -0,0 +1,231 @@
/** Verify real pre-summary source Evidence through retained MCP and provider-free CLI views. */
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
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 { finished } from "node:stream/promises";
import { parseArgs, promisify } from "node:util";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { STDIO_DEFAULT_MAX_BUFFER_SIZE } from "@modelcontextprotocol/server";
import { createRetainedQueryCaller } from "./retained-query-calls.mjs";
import { finalizeRetainedQueryReceipt } from "./retained-query-finalization.mjs";
const { values } = parseArgs({
options: Object.fromEntries(
["runtime", "source", "output"].map((name) => [name, { type: "string" }]),
),
});
for (const name of ["runtime", "source", "output"])
assert.ok(values[name] && values[name] === resolve(values[name]), name);
const hash = (value) => createHash("sha256").update(value).digest("hex");
const originalBytes = await readFile(join(values.source, "evidence.json"));
const original = JSON.parse(originalBytes);
const sources = JSON.parse(
await readFile(join(values.source, "sources.json"), "utf8"),
).filter((source) => source.phase === "first");
assert.equal(sources.length, 5);
await mkdir(values.output, { mode: 0o700 });
const home = await mkdtemp(join(tmpdir(), "rea-portable-native-inventory-"));
const environment = {
HOME: home,
PATH: process.env.PATH,
NODE_OPTIONS: "--max-old-space-size=2048",
REA_LOG_LEVEL: "silent",
};
const client = new Client({ name: "portable-native-inventory", version: "1" });
const transport = new StdioClientTransport({
command: process.execPath,
args: [join(values.runtime, "scripts/rea.mjs"), "mcp"],
env: environment,
stderr: "pipe",
});
const serverLog = createWriteStream(join(values.output, "server.log"));
const serverLogFinished = finished(serverLog);
serverLogFinished.catch(() => {});
transport.stderr?.pipe(serverLog);
const report = {
source_bundle_sha256: hash(originalBytes),
calls: [],
cli: [],
rows: {},
};
const transcript = [];
const call = createRetainedQueryCaller(client, (receipt, event) => {
report.calls.push(receipt);
transcript.push(event);
});
const result = async (name, input, phase = name) => {
const reply = await call(name, input, phase);
assert.notEqual(reply.isError, true, JSON.stringify(reply));
assert.ok(reply.structuredContent);
return reply.structuredContent;
};
const started = performance.now();
try {
await client.connect(transport);
const catalog = await client.listTools();
assert.equal(catalog.nextCursor, undefined);
report.catalog = {
tool_count: catalog.tools.length,
bytes: Buffer.byteLength(JSON.stringify(catalog)),
sha256: hash(JSON.stringify(catalog)),
};
const definitions = new Map(catalog.tools.map((tool) => [tool.name, tool]));
const originalCall = client.callTool.bind(client);
client.callTool = (input, options) =>
originalCall(input, {
...options,
toolDefinition: definitions.get(input.name),
});
await result("import_evidence_bundle", {
path: join(values.source, "evidence.json"),
});
for (const { operation, record } of sources) {
assert.deepEqual(
original.records.find(
(source) => source.evidence_id === record.evidence_id,
),
record,
);
const sourcePath = join(values.output, `${operation}.json`);
const sourceBytes = Buffer.from(JSON.stringify(record));
await writeFile(sourcePath, sourceBytes);
const source = {
kind: "retained-evidence",
evidence_id: record.evidence_id,
};
const pageView = {
kind: "native-inventory",
offset: 0,
limit: 4,
fields: ["value"],
};
const expected = await result(
"inspect_analysis_view",
{ source, view: pageView },
`${operation}:selected`,
);
for (const view of [{ kind: "summary" }, pageView]) {
const inputPath = join(
values.output,
`${operation}-${view.kind}.input.json`,
);
await writeFile(
inputPath,
JSON.stringify({
source: { kind: "evidence-file", path: sourcePath },
view,
}),
);
const cliStarted = performance.now();
const { stdout, stderr } = await promisify(execFile)(
process.execPath,
[
join(values.runtime, "scripts/rea.mjs"),
"inspect-analysis-view",
inputPath,
"--json",
],
{
env: environment,
encoding: "utf8",
timeout: 60000,
maxBuffer: STDIO_DEFAULT_MAX_BUFFER_SIZE,
},
);
const cliElapsed = performance.now() - cliStarted;
const actual = JSON.parse(stdout);
const mcp =
view.kind === "summary"
? await result(
"inspect_analysis_view",
{ source, view },
`${operation}:summary`,
)
: expected;
assert.deepEqual(actual, mcp);
report.cli.push({
operation,
view: view.kind,
elapsed_ms: cliElapsed,
stdout_bytes: Buffer.byteLength(stdout),
stderr_bytes: Buffer.byteLength(stderr),
evidence_id: actual.evidence_id,
});
}
const all = [];
let offset = 0;
do {
const evidence = await result(
"inspect_analysis_view",
{ source, view: { kind: "native-inventory", offset, limit: 128 } },
`${operation}:page:${offset}`,
);
const page = evidence.normalized_result;
assert.equal(page.kind, "native-inventory");
const expectedRows = Math.min(
128,
record.normalized_result.length - offset,
);
assert.equal(page.rows.length, expectedRows);
assert.equal(page.coverage.total, record.normalized_result.length);
for (const [index, row] of page.rows.entries())
assert.equal(row.source_index, offset + index);
all.push(...page.rows.map((row) => row.record));
assert.equal(
page.coverage.next_offset,
offset + expectedRows >= record.normalized_result.length
? null
: offset + expectedRows,
);
offset = page.coverage.next_offset;
} while (offset !== null);
assert.deepEqual(all, record.normalized_result);
report.rows[operation] = all.length;
assert.equal(hash(await readFile(sourcePath)), hash(sourceBytes));
}
const exportPath = join(values.output, "exported.json");
await result("export_evidence_bundle", { path: exportPath });
const exported = JSON.parse(await readFile(exportPath, "utf8"));
for (const { record } of sources)
assert.deepEqual(
exported.records.find(
(source) => source.evidence_id === record.evidence_id,
),
record,
);
assert.equal(
hash(await readFile(join(values.source, "evidence.json"))),
report.source_bundle_sha256,
);
report.status = "passed";
} catch (cause) {
report.status = "failed";
report.failure = String(cause?.stack ?? cause);
process.exitCode = 1;
} finally {
report.elapsed_before_cleanup_ms = performance.now() - started;
try {
await writeFile(
join(values.output, "transcript.json"),
JSON.stringify(transcript),
);
} catch (cause) {
report.status = "failed";
report.transcript_failure = String(cause);
}
const finalized = await finalizeRetainedQueryReceipt({
report,
client,
transport,
account: home,
serverLog,
serverLogFinished,
output: values.output,
});
if (report.status !== "passed" || !finalized) process.exitCode = 1;
}
@@ -0,0 +1,647 @@
{
"question": "Counts and first four address/value pairs for list_procedures, list_names, list_strings and literal main search_procedures/search_strings; first and warm repetition.",
"baseline_revision": "58d6043303e722115f1da18b9505c24abeeab423",
"runtime_pins": {
"baseline_manifest_sha256": "dc5e30fa1c75f999a8597608c5f0a3d8e1e16ccc14eb39d3884b5362076985dd",
"candidate_manifest_sha256": "14b4af9117051b722eb528dfdf79668fa188f874ce69853bd1ed609eb704a88d",
"candidate_tracked_patch_sha256": "e988c4f5142363dc2cbf6ab98e11d1ea1e4acccc66ce941d15eb51a945b4fb3a",
"candidate_untracked_sources": {
"experiments/analysis-efficiency/native-inventory-portable.mjs": "262cd4a076ba13936ff744c844892ec108b015f3adc908703120fddd251fe149",
"experiments/analysis-efficiency/native-inventory-views.mjs": "c5de78d1998e254a269e3d1694c96d7034162aac41965a7b842235ae09ac9db8",
"src/domain/analysisView/nativeInventoryView.test.ts": "b0b3a3d14a0d8be2a42bb9ae9e0e5a32eef94bf09ffcce01f89554300eeb97f2",
"src/domain/analysisView/nativeInventoryView.ts": "e1e06ead227c30c7eaa8231b48c934b5f0be57876847b022e75f6a89eadaddbd",
"src/domain/native/nativeInventory.ts": "431e6041cd2dec688ca541dbe343cea2228c9d616af1332338b9a36c73a32320",
"tests/boundary/mcp/nativeInventoryViewsMcp.test.ts": "f6991ba8e6d97922c430512d5bfec16b392e22a9eacab21a5853ea93f6d3a73d"
},
"note": "Frozen candidate before measurement; later documentation, bounded harness diagnostics and rebases are not relabeled as measured source revisions."
},
"environment": {
"platform": "Linux-6.1.0-53-cloud-amd64-x86_64-with-glibc2.36",
"node": "24.18.0",
"npm": "11.16.0",
"mcp_sdk": "2.3.1",
"ghidra": "12.1.4",
"java": "Eclipse Adoptium 21.0.12.1+1-LTS",
"java_heap": "768M",
"node_heaps_mib": 2048,
"active_java_processors": 1,
"cpu_affinity": [15],
"cpu_quota_percent": 100,
"family_memory_hard_bytes": 4294967296,
"family_memory_early_stop_bytes": 3221225472,
"swap_bytes": 0,
"task_limit": 192,
"cache_state": "Fresh sessions and Java analysis, then warm repeated calls; OS caches unflushed.",
"accounting": "cgroup v2 family counters at verified payload exit; kernel peak includes file cache and controller work; final whole-scope counters unavailable."
},
"artifacts": [
{
"path": "true",
"bytes": 35664,
"sha256": "c79bf44242829108e323378531f4ac839513ca1fba45efd6583643526e1e9fd2"
},
{
"path": "jq-linux-amd64",
"bytes": 2267912,
"sha256": "b1c22172dd303f3be49e935aa56aa48a8b7a46e0bc838b4997d3bb451495870f"
}
],
"driver_versions": [
{
"sha256": "c71957b673824a692efeaebb165a305799b6d7424a2dbb96d706a6f48e283aae",
"used_by": ["baseline-true-1", "baseline-jq-1"],
"path": "experiments/analysis-efficiency/native-inventory-views.mjs"
},
{
"sha256": "c5de78d1998e254a269e3d1694c96d7034162aac41965a7b842235ae09ac9db8",
"used_by": ["candidate-true-1", "baseline-jq-2", "candidate-jq-1"]
},
{
"sha256": "0ce2e2a2c02e366f5005f9b1e4563f7cd1a5a378d8461de6750154dccf41995b",
"runs": ["candidate-jq-2"],
"change": "Bounded full-source comparison diagnostic, same strict oracle; candidate-jq-2 has no cross-session full-source control. Compare task facts separately and preserve Ghidra primary alias variance.",
"candidate-jq-1": "Incomplete: scope_memory_early_stop while rendering full source mismatch; facts and source records retained; no completed report or cleanup proof."
}
],
"token_estimates": {
"python": "3.11.2 (main, Oct 7 2026, 12:35:07) [GCC 12.2.0]",
"tiktoken": "0.12.0",
"encoding": "cl100k_base",
"actual_model_usage": null,
"unmeasured": [
"model-host schema selection, wrapping and rendering",
"system prompts, local filesystem tool schema/program generation and follow-up reasoning",
"model input/cached/uncached/output usage",
"initialization and JSON-RPC wire framing",
"isolated correctness-oracle CPU and memory overhead"
],
"samples": [
{
"recording": "baseline-true-1",
"status": "passed",
"completed_with_verified_cleanup": true,
"mode": "complete",
"catalog_tools": 209,
"catalog_bytes": 3263492,
"rpc_calls": 13,
"rpc_errors_or_rejections": 0,
"rpc_argument_bytes": 854,
"rpc_reply_bytes": 921748,
"local_recoveries": [],
"token_estimates": {
"catalog_full_json": 831584,
"catalog_input_facing_projection": 185367,
"rpc_name_and_argument_json": 214,
"rpc_text_content": 120915,
"rpc_complete_result_json": 247753,
"local_projection_output_json": 0,
"input_facing_catalog_plus_arguments_text_and_local_output": 306496
},
"whole_scope_wall_seconds": 53.662,
"payload_family_cpu_seconds": 44.201557,
"kernel_family_peak_bytes_including_file_cache": 1157513216,
"payload_exit_observation_verified": true,
"observed_resource_events": {
"memory.events": {
"low": 0,
"high": 0,
"max": 0,
"oom": 0,
"oom_kill": 0,
"oom_group_kill": 0
},
"pids.events": {
"max": 0
}
},
"final_whole_scope_events": null,
"tasks": [
{
"phase": "first",
"elapsed_ms": 35533.921138000005
},
{
"phase": "warm",
"elapsed_ms": 236.46414299999742
}
],
"facts_sha256": "fb6cfbd6e3b9cdff8f649ffc41de54d7de38a003d3ec4f20f7cdad227e38afca",
"sources_sha256": "c87d8c5b12b5244e3777f61a396e2a19b333ce9a9c1cb246db4ab54fb075b13b"
},
{
"recording": "candidate-true-1",
"status": "passed",
"completed_with_verified_cleanup": true,
"mode": "summary",
"catalog_tools": 209,
"catalog_bytes": 3293844,
"rpc_calls": 23,
"rpc_errors_or_rejections": 0,
"rpc_argument_bytes": 3469,
"rpc_reply_bytes": 171130,
"local_recoveries": [],
"token_estimates": {
"catalog_full_json": 838818,
"catalog_input_facing_projection": 186009,
"rpc_name_and_argument_json": 1086,
"rpc_text_content": 20907,
"rpc_complete_result_json": 43673,
"local_projection_output_json": 0,
"input_facing_catalog_plus_arguments_text_and_local_output": 208002
},
"whole_scope_wall_seconds": 36.777,
"payload_family_cpu_seconds": 35.482574,
"kernel_family_peak_bytes_including_file_cache": 1244180480,
"payload_exit_observation_verified": true,
"observed_resource_events": {
"memory.events": {
"low": 0,
"high": 0,
"max": 0,
"oom": 0,
"oom_kill": 0,
"oom_group_kill": 0
},
"pids.events": {
"max": 0
}
},
"final_whole_scope_events": null,
"tasks": [
{
"phase": "first",
"elapsed_ms": 30791.001210000002
},
{
"phase": "warm",
"elapsed_ms": 223.86245000000054
}
],
"facts_sha256": "fb6cfbd6e3b9cdff8f649ffc41de54d7de38a003d3ec4f20f7cdad227e38afca",
"sources_sha256": "c87d8c5b12b5244e3777f61a396e2a19b333ce9a9c1cb246db4ab54fb075b13b"
},
{
"recording": "baseline-jq-2",
"status": "passed",
"completed_with_verified_cleanup": true,
"mode": "complete",
"catalog_tools": 209,
"catalog_bytes": 3263492,
"rpc_calls": 15,
"rpc_errors_or_rejections": 2,
"rpc_argument_bytes": 1139,
"rpc_reply_bytes": 3032150,
"local_recoveries": [
{
"phase": "first:list_names",
"status": "passed",
"bundle_bytes_read": 7318483,
"projection_bytes": 1186,
"elapsed_ms": 164.94348000001628
},
{
"phase": "warm:list_names",
"status": "passed",
"bundle_bytes_read": 7769768,
"projection_bytes": 1186,
"elapsed_ms": 116.13296299998183
}
],
"token_estimates": {
"catalog_full_json": 831584,
"catalog_input_facing_projection": 185367,
"rpc_name_and_argument_json": 307,
"rpc_text_content": 403794,
"rpc_complete_result_json": 820583,
"local_projection_output_json": 564,
"input_facing_catalog_plus_arguments_text_and_local_output": 590032
},
"whole_scope_wall_seconds": 355.586,
"payload_family_cpu_seconds": 318.976126,
"kernel_family_peak_bytes_including_file_cache": 1571012608,
"payload_exit_observation_verified": true,
"observed_resource_events": {
"memory.events": {
"low": 0,
"high": 0,
"max": 0,
"oom": 0,
"oom_kill": 0,
"oom_group_kill": 0
},
"pids.events": {
"max": 0
}
},
"final_whole_scope_events": null,
"tasks": [
{
"phase": "first",
"elapsed_ms": 334659.460051
},
{
"phase": "warm",
"elapsed_ms": 2520.551972999994
}
],
"facts_sha256": "34cf397309b0db4132f1fcfbcaf92daab75b8b823e4c328ac1592f46dff38094",
"sources_sha256": "e8f4f98c0cc9a141f07db695fcc5b2b3f22ae3697bb09d0d8fd03d6a47762260"
},
{
"recording": "candidate-jq-2",
"status": "passed",
"completed_with_verified_cleanup": true,
"mode": "summary",
"catalog_tools": 209,
"catalog_bytes": 3293844,
"rpc_calls": 21,
"rpc_errors_or_rejections": 0,
"rpc_argument_bytes": 3040,
"rpc_reply_bytes": 172180,
"local_recoveries": [],
"token_estimates": {
"catalog_full_json": 838818,
"catalog_input_facing_projection": 186009,
"rpc_name_and_argument_json": 921,
"rpc_text_content": 21372,
"rpc_complete_result_json": 44505,
"local_projection_output_json": 0,
"input_facing_catalog_plus_arguments_text_and_local_output": 208302
},
"whole_scope_wall_seconds": 333.104,
"payload_family_cpu_seconds": 311.318996,
"kernel_family_peak_bytes_including_file_cache": 1774460928,
"payload_exit_observation_verified": true,
"observed_resource_events": {
"memory.events": {
"low": 0,
"high": 0,
"max": 0,
"oom": 0,
"oom_kill": 0,
"oom_group_kill": 0
},
"pids.events": {
"max": 0
}
},
"final_whole_scope_events": null,
"tasks": [
{
"phase": "first",
"elapsed_ms": 318797.998245
},
{
"phase": "warm",
"elapsed_ms": 6916.484328999999
}
],
"facts_sha256": "34cf397309b0db4132f1fcfbcaf92daab75b8b823e4c328ac1592f46dff38094",
"sources_sha256": "237f727384546a5323bf39d08d3fe3061b89cb32da22d062493b008e474ba6a2"
}
]
},
"equivalence": {
"true_all_original_source_evidence_fields_and_ids_equal": true,
"jq_complete_selected_task_facts_equal": true,
"jq_all_independent_live_source_evidence_equal": false,
"earlier_independent_jq_source_differences": {
"attempt": "candidate-jq-1",
"completed": false,
"reason": "scope_memory_early_stop; no report or payload-exit observation",
"task_facts_exact_equal": true,
"full_source_equal": false,
"different_leaves": {
"symbol.primary": 156,
"[1, 'record', 'normalized_result', 45988, 'value']": 1,
"[1, 'record', 'normalized_result', 46001, 'value']": 1,
"evidence_id": 2,
"[6, 'record', 'normalized_result', 45988, 'value']": 1,
"[6, 'record', 'normalized_result', 46001, 'value']": 1
},
"examples": [
{
"path": [
1,
"record",
"normalized_result",
30995,
"symbol",
"primary"
],
"baseline": false,
"candidate": true
},
{
"path": [
1,
"record",
"normalized_result",
31013,
"symbol",
"primary"
],
"baseline": true,
"candidate": false
},
{
"path": [
1,
"record",
"normalized_result",
31120,
"symbol",
"primary"
],
"baseline": false,
"candidate": true
},
{
"path": [
1,
"record",
"normalized_result",
31121,
"symbol",
"primary"
],
"baseline": true,
"candidate": false
}
],
"interpretation": "Independent Ghidra analyses differ in symbol.primary for alias rows and two generated switch-label values (repeated in first/warm records). Root cause is unestablished; no byte-identical full-source or completed-performance claim."
},
"portable_checks": [
{
"recording": "portable-true-1",
"status": "passed",
"rows_reconstructed": {
"list_procedures": 231,
"list_names": 1027,
"list_strings": 143,
"search_procedures": 8,
"search_strings": 2
},
"actual_cli_mcp_equal_evidence_comparisons": 10,
"rpc_calls_including_import_and_export": 27,
"source_bundle_sha256": "d9f574ff9deb84cef58a714e563590c70184c15ba01c174f47c027b50b1d6bfa",
"whole_scope_wall_seconds": 25.194,
"family_cpu_seconds": 25.015127,
"kernel_family_peak_bytes_including_file_cache": 1025908736,
"resource_events": {
"memory.events": {
"low": 0,
"high": 0,
"max": 0,
"oom": 0,
"oom_kill": 0,
"oom_group_kill": 0
},
"pids.events": {
"max": 0
}
},
"payload_cleanup_verified": true,
"final_whole_scope_events": null
},
{
"recording": "portable-jq-1",
"status": "passed",
"rows_reconstructed": {
"list_procedures": 2279,
"list_names": 49684,
"list_strings": 2915,
"search_procedures": 0,
"search_strings": 24
},
"actual_cli_mcp_equal_evidence_comparisons": 10,
"rpc_calls_including_import_and_export": 444,
"source_bundle_sha256": "49ba421c1d7c2bbee5d6d4ae7acec437b076f760097eeae1aae8633d45f8e51c",
"whole_scope_wall_seconds": 77.537,
"family_cpu_seconds": 63.38141,
"kernel_family_peak_bytes_including_file_cache": 2034434048,
"resource_events": {
"memory.events": {
"low": 0,
"high": 0,
"max": 0,
"oom": 0,
"oom_kill": 0,
"oom_group_kill": 0
},
"pids.events": {
"max": 0
}
},
"payload_cleanup_verified": true,
"final_whole_scope_events": null
}
],
"limits": "This qualifies preservation/projection of recorded facts, not every Ghidra interpretation or complete program discovery."
},
"incomplete_attempts": [
{
"recording": "baseline-jq-1",
"completed": false,
"reason": "Default list_names exceeds the existing MCP response budget; first driver lacks export recovery.",
"abort": null,
"whole_scope_wall_seconds": 283.113,
"peak_sampled_bytes": 1308225536,
"last_observed_events": {
"memory.events": {
"low": 0,
"high": 0,
"max": 0,
"oom": 0,
"oom_kill": 0,
"oom_group_kill": 0
},
"pids.events": {
"max": 0
}
},
"payload_resource_observation": {
"phase": "payload-exited-observer-alive",
"ownership_token": "rea-efficiency-2ad56ea678404ef49da951c8f1761822.scope",
"payload_exit_code": 1,
"observer_pid": 3613358,
"remaining_processes": [3613358],
"payload_cleanup_verified": true,
"cpu_stat": {
"usage_usec": 281189241,
"user_usec": 274593631,
"system_usec": 6595610,
"nr_periods": 2829,
"nr_throttled": 585,
"throttled_usec": 218972,
"nr_bursts": 0,
"burst_usec": 0
},
"memory_peak_bytes": 1311993856,
"control_group": "user.slice/user-1004.slice/user@1004.service/app.slice/rea-efficiency-2ad56ea678404ef49da951c8f1761822.scope",
"resource_events": {
"memory.events": {
"low": 0,
"high": 0,
"max": 0,
"oom": 0,
"oom_kill": 0,
"oom_group_kill": 0
},
"pids.events": {
"max": 0
}
}
},
"final_whole_scope_events": null
},
{
"recording": "candidate-jq-1",
"completed": false,
"reason": "Independent full-source observations differ; guard stopped during comparison. Assertion diagnostic expansion is source-derived, not a recovered final stack trace.",
"abort": "scope_memory_early_stop",
"whole_scope_wall_seconds": 269.574,
"peak_sampled_bytes": 3222269952,
"last_observed_events": {
"memory.events": {
"low": 0,
"high": 26,
"max": 0,
"oom": 0,
"oom_kill": 0,
"oom_group_kill": 0
},
"pids.events": {
"max": 0
}
},
"payload_resource_observation": null,
"final_whole_scope_events": null
}
],
"validation": {
"focused_files": 24,
"focused_tests": 305,
"focused_command": "npm run test:focused -- src/domain/analysisView/EnvironmentManifestAccumulator.test.ts src/domain/analysisView/WebNetworkApiSurfaceAccumulator.test.ts src/domain/analysisView/analysisView.test.ts src/domain/analysisView/artifactInventoryView.test.ts src/domain/analysisView/environmentManifestRules.test.ts src/domain/analysisView/environmentManifestView.test.ts src/domain/analysisView/environmentStateView.test.ts src/domain/analysisView/environmentStateWriteCutoffs.test.ts src/domain/analysisView/il2cppMetadataView.test.ts src/domain/analysisView/interfaceBuilderView.test.ts src/domain/analysisView/machoView.test.ts src/domain/analysisView/nativeInventoryView.test.ts src/domain/analysisView/pcapCaptureView.test.ts src/domain/analysisView/scenarioCaptureRecords.test.ts src/domain/analysisView/webNetworkApiSurfaceView.test.ts src/domain/analysisView/webNetworkCaptureView.test.ts tests/boundary/mcp/nativeInventoryViewsMcp.test.ts tests/boundary/mcp/analysisViewMcp.test.ts tests/boundary/mcp/officialArguments.test.ts tests/boundary/mcp/officialTools.test.ts tests/boundary/mcp/largeToolResult.test.ts tests/boundary/mcp/ghidraEvidenceMcp.test.ts tests/boundary/mcp/nativeDispatchAvailability.test.ts tests/boundary/mcp/promptCompletion.test.ts",
"static_command": "npm run check",
"static_result": "passed, six tasks plus module/architecture guards",
"docs_result": "npm run docs:check passed",
"intermediate_failures": "Type errors and SDK fixture/error-envelope harness errors were corrected before passing checks. Two host-memory guard stops were retained; neither is a successful static check.",
"provider_scope": "Real Linux x86-64 Ghidra true/jq metadata; other engines/platforms and complete application reconstruction unqualified.",
"post_rebase": {
"base_revision": "0134732fc",
"implementation_revision": "8d1ddce86",
"locked_dependencies": "npm ci passed for updated package-lock.json; frozen measurement runtimes retain their original physical dependency tree",
"focused_files": 28,
"focused_tests": 325,
"focused_command": "npm run test:focused -- src/domain/analysisView/EnvironmentManifestAccumulator.test.ts src/domain/analysisView/WebNetworkApiSurfaceAccumulator.test.ts src/domain/analysisView/analysisView.test.ts src/domain/analysisView/artifactInventoryView.test.ts src/domain/analysisView/environmentManifestRules.test.ts src/domain/analysisView/environmentManifestView.test.ts src/domain/analysisView/environmentStateView.test.ts src/domain/analysisView/environmentStateWriteCutoffs.test.ts src/domain/analysisView/il2cppMetadataView.test.ts src/domain/analysisView/interfaceBuilderView.test.ts src/domain/analysisView/machoView.test.ts src/domain/analysisView/nativeInventoryView.test.ts src/domain/analysisView/pcapCaptureView.test.ts src/domain/analysisView/scenarioCaptureRecords.test.ts src/domain/analysisView/webNetworkApiSurfaceView.test.ts src/domain/analysisView/webNetworkCaptureView.test.ts tests/boundary/mcp/nativeInventoryViewsMcp.test.ts tests/boundary/mcp/analysisViewMcp.test.ts tests/boundary/mcp/officialArguments.test.ts tests/boundary/mcp/officialTools.test.ts tests/boundary/mcp/largeToolResult.test.ts tests/boundary/mcp/ghidraEvidenceMcp.test.ts tests/boundary/mcp/nativeDispatchAvailability.test.ts tests/boundary/mcp/promptCompletion.test.ts src/domain/analysisView/protobufCaptureView.test.ts src/browser/history/protobuf/ProtobufCapture.test.ts src/browser/history/protobuf/ProtobufWire.test.ts tests/acceptance/tools/protobufCapture.test.ts",
"static_check": "npm run check passed after final field-selection normalization",
"docs_check": "npm run docs:check passed, including five README consistency tests",
"final_change": "Normalize duplicate field selection once, preserving original view request; focused two-file/nine-test rerun passed",
"final_projector_sha256": "8795c18fe0a7c97b8a841aa3cc1fc6efe08fa1bf9711c21b0183b2cdd1f1b180",
"verification_runtime_manifest_sha256": "f6a30e4a56fa1f95926e2259fbefe837ed375fb1c608e2298fb88440e1898f2f",
"portable_real_source_checks": [
{
"recording": "portable-true-final-1",
"status": "passed",
"rows_reconstructed": {
"list_procedures": 231,
"list_names": 1027,
"list_strings": 143,
"search_procedures": 8,
"search_strings": 2
},
"cli_mcp_equal_comparisons": 10,
"rpc_calls": 27,
"catalog": {
"tool_count": 209,
"bytes": 3317414,
"sha256": "4c920219321a08102a29673a1cdd48f74d9990e766a313313903525a9f9efa87"
},
"whole_scope_wall_seconds": 23.489,
"family_cpu_seconds": 23.283072,
"kernel_family_peak_bytes": 1057517568,
"payload_cleanup_verified": true,
"resource_events": {
"memory.events": {
"low": 0,
"high": 0,
"max": 0,
"oom": 0,
"oom_kill": 0,
"oom_group_kill": 0
},
"pids.events": {
"max": 0
}
}
},
{
"recording": "portable-jq-final-1",
"status": "passed",
"rows_reconstructed": {
"list_procedures": 2279,
"list_names": 49684,
"list_strings": 2915,
"search_procedures": 0,
"search_strings": 24
},
"cli_mcp_equal_comparisons": 10,
"rpc_calls": 444,
"catalog": {
"tool_count": 209,
"bytes": 3317414,
"sha256": "4c920219321a08102a29673a1cdd48f74d9990e766a313313903525a9f9efa87"
},
"whole_scope_wall_seconds": 34.99,
"family_cpu_seconds": 34.589707,
"kernel_family_peak_bytes": 2153742336,
"payload_cleanup_verified": true,
"resource_events": {
"memory.events": {
"low": 0,
"high": 0,
"max": 0,
"oom": 0,
"oom_kill": 0,
"oom_group_kill": 0
},
"pids.events": {
"max": 0
}
}
}
],
"performance_limit": "Fixed runtime timing/token pairs remain pinned to the pre-rebase 58d-based runtime. Final source compatibility and full-row preservation were rerun; final main performance pairs and actual model usage were not rerun.",
"guard_stopped_static_retry": {
"recording": "full-check-rebase-1",
"abort": "host_available_memory_early_stop",
"completed": false,
"own_oom_events": 0
},
"final_static_and_docs_family": {
"wall_seconds": 93.069,
"cpu_seconds": 92.615364,
"kernel_peak_bytes": 5417869312,
"payload_cleanup_verified": true
}
},
"protocol_rebase": {
"base_revision": "4eaf3a117",
"implementation_revision": "7fbe79ef8",
"change": "Preserve latest main oversized-error protocol framing and both documentation insertions; native projector unchanged.",
"focused_command": "npm run test:focused -- src/server/EvidenceMcpServer.test.ts tests/boundary/mcp/nativeInventoryViewsMcp.test.ts tests/boundary/mcp/largeToolResult.test.ts",
"focused_files": 3,
"focused_tests": 11,
"result": "passed",
"real_old_source_cli_mcp_verifier": "true all rows and ten actual CLI/MCP comparisons passed again; no provider launched",
"catalog": {
"tool_count": 209,
"bytes": 3317414,
"sha256": "4c920219321a08102a29673a1cdd48f74d9990e766a313313903525a9f9efa87"
},
"docs_check": "npm run docs:check passed again",
"static_check_scope": "Earlier full static checks remain pinned above; final pre-push/CI checks track the latest branch head."
}
}
}
@@ -0,0 +1,201 @@
# Native inventory summaries and retained rows
Continues [#4249](https://github.com/morluto/rea/issues/4249). The analyst
question is to report observed row counts and the first four address/value
pairs from procedures, names, strings, and literal `main` procedure/string
searches. Complete delivery reads every row even when only those facts are
needed. A real jq name inventory exceeds the existing 10 MiB MCP result budget.
## Change and preserved semantics
The five existing tools accept opt-in `detail: "summary"`. Their original
complete Evidence is retained before a summary is returned. Delivery detail
does not enter provider parameters or the original Evidence identity. Default
complete requests keep their existing response. Oversized complete inventory
replies gain an actionable retained-page continuation after recording succeeds.
`inspect_analysis_view` projects `native-inventory` rows with offsets, limits
and selected fields. Parent Evidence ID and original `source_index` identify
duplicate-address alternatives; original order is preserved. Address is always
retained, and selecting a string's value also retains its string/decoding
qualifications. Counts describe retained provider observations. Exhausting a
page does not establish complete code/string discovery. Unknown source artifact
identity stays unknown rather than acquiring an invented path.
CLI and MCP use the shared application/domain projector. Its source-format
validation is reused only after successful representation-preserving validation
of an authenticated immutable JSON snapshot. Two weak sets distinguish names
and the stricter strings format. They retain no second parsed inventory or live
provider state; entries disappear with their source arrays. Mutable, foreign,
malformed or transformed values do not acquire this proof. A parent change
rebinds provenance rather than reusing an earlier view envelope.
This reduces delivery after the provider has produced its full inventory. It
does not reduce Ghidra discovery, eliminate the retained full inventory, or
establish a universal CPU/memory improvement. The catalog becomes larger and
summary-plus-page requires more RPCs than a small complete reply.
## Real inputs, controls and oracle
Read-only Linux x86-64 targets, without target execution:
| Target | Bytes | SHA-256 | Provenance |
| ------ | --------: | ------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------- |
| `true` | 35,664 | `c79bf44242829108e323378531f4ac839513ca1fba45efd6583643526e1e9fd2` | Installed Debian coreutils 9.1-1 |
| jq | 2,267,912 | `b1c22172dd303f3be49e935aa56aa48a8b7a46e0bc838b4997d3bb451495870f` | [Official jq 1.8.2 asset](https://github.com/jqlang/jq/releases/tag/jq-1.8.2), release API size/digest matched |
Baseline runtime is main `58d6043303e722115f1da18b9505c24abeeab423`.
The candidate runtime is frozen from that base plus this implementation before
measurement. Runtime file manifests, the exact patch, driver revisions, original
Evidence, transcripts, errors and resource observations are retained outside
Git. The curated [receipt](native-inventory-views.json) pins the recordings.
Later rebases do not relabel these timings as measurements of a different base.
Node 24.18.0/npm 11.16.0, MCP SDK 2.3.1, Ghidra 12.1.4 and Eclipse Adoptium
21.0.12.1+1-LTS. Each pair uses a fresh MCP server and Ghidra analysis, followed
by a warm repetition in that session. OS caches are unflushed. Java heap is
768 MiB and one active processor; Node heaps are 2 GiB. The family guard uses
one logical CPU, 4 GiB hard/3 GiB early-stop memory, no swap, and 192 tasks.
Heavy jobs are serial. Kernel peaks include file cache, controller verification
and receipt work, rather than isolated provider heap. Payload-exit observations
verify owned process cleanup; final whole-scope counters remain unavailable.
The live named SDK contracts validate replies. Both modes discover the full
catalog, open the same target, complete the question twice, export original
Evidence and close. Summary mode requests four rows only for nonempty results.
Complete mode includes acknowledged export recovery and local JSON projection
when `list_names` exceeds transport capacity. Its errors, export calls, bytes
read and local projection output remain in costs. Model generation of that
local program and filesystem-tool schemas are unmeasured.
The small pair preserves every field of every original source Evidence,
including IDs, and both phases' complete selected fact vectors. The large
completed pair compares the requested facts exactly; it must not claim identical
independent Ghidra analyses. An earlier candidate attempt found alias-primary
and two generated switch-label differences outside the requested selection.
Their underlying cause is unestablished. To qualify projection independently
of that variation, the portable verifier imports the actual baseline Evidence,
reads every canonical row through MCP, and compares it with the recorded source.
It also compares actual CLI file-source views with retained MCP views, without
Ghidra/JDK configuration, and reexports original source records unchanged.
## Measurements and limits
Each recording contains discovery, both first/warm questions, original Evidence
export and cleanup. The token proxy below adds input-facing catalog JSON,
name/argument JSON, text replies and local recovery output; it does not count
both text and structured copies as if every host supplied both to a model.
| Recording / metric | Before | After |
| ---------------------------------------- | ---------: | ---------: |
| `true`: RPC calls | 13 | 23 |
| `true`: RPC result bytes | 921,748 | 171,130 |
| `true`: combined token proxy | 306,496 | 208,002 |
| `true`: first question | 35.534 s | 30.791 s |
| `true`: warm question | 0.236 s | 0.224 s |
| `true`: whole scope wall | 53.662 s | 36.777 s |
| `true`: family CPU | 44.202 s | 35.483 s |
| `true`: kernel family peak | 1103.9 MiB | 1186.5 MiB |
| jq: RPC calls, including export recovery | 15 | 21 |
| jq: RPC result bytes | 3,032,150 | 172,180 |
| jq: combined token proxy | 590,032 | 208,302 |
| jq: first question | 334.659 s | 318.798 s |
| jq: warm question | 2.521 s | 6.916 s |
| jq: whole scope wall | 355.586 s | 333.104 s |
| jq: family CPU | 318.976 s | 311.319 s |
| jq: kernel family peak | 1498.2 MiB | 1692.3 MiB |
The catalog remains complete at 209 tools: 3,263,492 → 3,293,844 bytes.
Its input-facing projection grows by 642 proxy tokens, from 185,367 to 186,009.
Including that increase and every call, the combined serialization proxy falls
32.1% for `true` and 64.7% for jq. Actual model usage is unmeasured.
Preserve the negative results: the large warm question is slower and both
family memory peaks increase. The first native view also pays source-format
validation and SDK schema setup; these phases are not isolated by this driver.
Fresh analysis dominates total wall time and varies between sessions. These
observations do not establish a latency, CPU or memory improvement. The owning
benefit is selected delivery with complete source access and actionable
recovery, rather than eliminating full provider inventory production.
Individual completed samples, tokenizer estimates and portable checks are in
the receipt. A `cl100k_base` estimate over recorded serialization is not actual
model usage. Counts include discovery, call arguments, text replies, errors,
continuations and local recovery output; SDK result JSON is also reported
separately. Actual client schema selection/rendering, wire framing, model
reasoning and isolated correctness-oracle overhead are unmeasured. Single pairs
are observations rather than latency distributions or evidence of improved
model investigation outcomes.
Incomplete attempts remain separate. The first baseline jq driver stopped at
the real oversized reply because it lacked export recovery. The first candidate
jq attempt produced its selected facts and source export, then hit the 3 GiB
early-stop threshold while comparing differing full inventories; no final report
or payload-exit cleanup proof was produced. Expanding the enormous assertion
diagnostic is a source-derived explanation, not an observed final stack trace.
The harness now emits a bounded mismatch with source digests while retaining
strict equality and both full files. The subsequent recording explicitly does
not request cross-session full-source equality.
Two static-check attempts were stopped by the host-memory guard; later checks
passed after host memory recovered. No guard-stopped attempt is a successful
performance sample. Engine-version/platform equivalence, other providers,
complete application reconstruction and post-GC retained heap are unqualified.
Executed verification: 24 focused files/305 tests passed, including advertised
SDK-converted JSON Schema validation, all five tools, nonretaining refusal,
oversized recovery, same-address alternatives, decoding qualifiers, immutable
validation ownership and unknown-artifact sources. Full `npm run check` passed,
including module/architecture guards; `npm run docs:check` also passed. The portable runs passed with 27/444 MCP
calls, ten actual CLI comparisons each, every row reconstructed, original
records preserved and payload cleanup verified. Their scope times are
25.194/77.537 s and kernel family peaks are 978.4/1940.2 MiB; these additional
correctness runs are not part of the selected-question timing pairs.
After rebasing onto main `0134732fc` (including Protobuf/gRPC views), 28 focused
files/325 tests passed. Final field-selection normalization removes repeated
per-row scans of duplicate requested fields while retaining the exact request;
the affected two files/nine tests passed again. Final `npm run check` and
`npm run docs:check` passed. Both real old-source portable checks passed again
against the final build, including every canonical row and all 20 CLI/MCP
comparisons. The observed catalog is then 209 tools/3,317,414 bytes. Those final
source and dependency pins are separate from the earlier frozen timing/token
pairs: final-main performance pairs and actual model usage were not rerun.
## Reproduce
Build separate baseline/candidate runtimes, then run each mode with the same
absolute artifact, Ghidra and JDK paths:
```sh
python3 experiments/analysis-efficiency/resource-run.py --observe-payload-exit -- \
node experiments/analysis-efficiency/native-inventory-views.mjs \
--runtime /absolute/baseline --target /absolute/application \
--ghidra /absolute/ghidra --java /absolute/jdk \
--mode complete --output /absolute/before
python3 experiments/analysis-efficiency/resource-run.py --observe-payload-exit -- \
node experiments/analysis-efficiency/native-inventory-views.mjs \
--runtime /absolute/candidate --target /absolute/application \
--ghidra /absolute/ghidra --java /absolute/jdk \
--mode summary --output /absolute/after
python3 experiments/analysis-efficiency/resource-run.py --observe-payload-exit -- \
node experiments/analysis-efficiency/native-inventory-portable.mjs \
--runtime /absolute/candidate --source /absolute/before \
--output /absolute/portable
```
`--control /absolute/before` additionally requires every source Evidence field
to match across the two live analyses. Preserve failure if the engine produces
different observations. `--verify-pages` performs extra live full-page checks;
keep those correctness runs distinct from selected-question timing pairs.
With private `tiktoken==0.12.0`, `native-inventory-costs.py --output FILE CASE...`
counts completed recordings and their sibling `.log` resource observations.
The subsequent main `4eaf3a117` oversized-error protocol-framing fix was also
merged into the branch. Its producing-server, native inventory and large-result
checks passed (three files/11 tests), preserving both protocol revisions. The
real old `true` source verifier and `npm run docs:check` passed again. The native
projector and earlier performance pins are unchanged; latest-head pre-push/CI
checks provide the final branch status.
@@ -0,0 +1,374 @@
/** Compare complete delivery with selected native inventory views on real Ghidra. */
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
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 { finished } from "node:stream/promises";
import { isDeepStrictEqual, parseArgs } 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 } = parseArgs({
options: {
...Object.fromEntries(
["runtime", "target", "output", "ghidra", "java", "mode", "control"].map(
(name) => [name, { type: "string" }],
),
),
"verify-pages": { type: "boolean", default: false },
},
});
for (const name of ["runtime", "target", "output", "ghidra", "java"])
assert.ok(values[name] && values[name] === resolve(values[name]), name);
assert.ok(["complete", "summary"].includes(values.mode));
assert.ok(!values["verify-pages"] || values.mode === "summary");
const hash = (value) => createHash("sha256").update(value).digest("hex");
const output = values.output;
await mkdir(output, { mode: 0o700 });
const targetBytes = await readFile(values.target);
const home = await mkdtemp(join(tmpdir(), "rea-native-inventory-"));
const client = new Client({ name: "native-inventory-views", version: "1" });
const transport = new StdioClientTransport({
command: process.execPath,
args: [join(values.runtime, "scripts/rea.mjs"), "mcp"],
env: {
HOME: home,
PATH: process.env.PATH,
NODE_OPTIONS: "--max-old-space-size=2048",
GHIDRA_INSTALL_DIR: values.ghidra,
JAVA_HOME: values.java,
GHIDRA_HEADLESS_MAXMEM: "768M",
JAVA_TOOL_OPTIONS: "-XX:ActiveProcessorCount=1",
REA_GHIDRA_STARTUP_TIMEOUT_MS: "600000",
REA_LOG_LEVEL: "silent",
},
stderr: "pipe",
});
const serverLog = createWriteStream(join(output, "server.log"));
const serverLogFinished = finished(serverLog);
serverLogFinished.catch(() => {});
transport.stderr?.pipe(serverLog);
const report = {
artifact: {
path: values.target,
bytes: targetBytes.length,
sha256: hash(targetBytes),
},
mode: values.mode,
verify_pages: values["verify-pages"],
question:
"Report retained row counts and the first four address/value pairs for procedures, names, strings, and literal main procedure/string matches.",
calls: [],
tasks: [],
local_recoveries: [],
};
const transcript = [];
const call = createRetainedQueryCaller(client, (receipt, event) => {
report.calls.push(receipt);
transcript.push(event);
});
const result = async (name, input, phase) => {
const reply = await call(name, input, phase ?? name);
assert.notEqual(reply.isError, true, JSON.stringify(reply));
assert.ok(reply.structuredContent);
return reply.structuredContent;
};
const operations = [
"list_procedures",
"list_names",
"list_strings",
"search_procedures",
"search_strings",
];
const selectedRows = (rows) =>
rows.slice(0, 4).map(({ address, value }) => ({ address, value }));
const sourceReferences = [];
const started = performance.now();
let opened = false;
try {
await client.connect(transport);
const discovery = performance.now();
const catalog = await client.listTools();
assert.equal(catalog.nextCursor, undefined);
report.catalog = {
elapsed_ms: performance.now() - discovery,
tool_count: catalog.tools.length,
bytes: Buffer.byteLength(JSON.stringify(catalog)),
sha256: hash(JSON.stringify(catalog)),
};
await writeFile(join(output, "catalog.json"), JSON.stringify(catalog));
const definitions = new Map(catalog.tools.map((tool) => [tool.name, tool]));
const original = client.callTool.bind(client);
client.callTool = (input, options) =>
original(input, {
...options,
timeout: 600000,
toolDefinition: definitions.get(input.name),
});
await result("open_binary", { path: values.target, provider_id: "ghidra" });
opened = true;
for (const phase of ["first", "warm"]) {
const taskStarted = performance.now();
const facts = [];
for (const operation of operations) {
const parameters = operation.startsWith("search_")
? { pattern: "main", mode: "literal", case_sensitive: false }
: {};
const input =
values.mode === "summary"
? { ...parameters, detail: "summary" }
: parameters;
const reply = await call(operation, input, `${phase}:${operation}`);
let evidence;
if (reply.isError) {
const text = reply.content.find((item) => item.type === "text");
assert.ok(text);
const error = JSON.parse(text.text).error;
assert.equal(values.mode, "complete");
assert.equal(error.code, "resource_constraint");
const reference = error.details?.reported_limits?.evidence_reference;
assert.equal(error.details?.reported_limits?.boundary, "mcp-response");
assert.equal(reference?.kind, "retained-evidence");
const recovery = { phase: `${phase}:${operation}`, status: "started" };
report.local_recoveries.push(recovery);
const path = join(
output,
`recovery-${report.local_recoveries.length}.json`,
);
await result(
"export_evidence_bundle",
{ path },
`${phase}:${operation}:recovery-export`,
);
const recoveryStarted = performance.now();
try {
const bytes = await readFile(path);
const bundle = JSON.parse(bytes);
evidence = bundle.records.find(
(record) => record.evidence_id === reference.evidence_id,
);
assert.ok(evidence);
assert.equal(evidence.operation, operation);
assert.ok(Array.isArray(evidence.normalized_result));
// This is local JSON field selection after the documented export recovery,
// not another provider query or an MCP view available in the baseline.
const projection = {
evidence_id: evidence.evidence_id,
operation,
row_count: evidence.normalized_result.length,
rows: selectedRows(evidence.normalized_result),
provider: evidence.provider,
parameters: evidence.parameters,
limitations: evidence.limitations,
};
recovery.bundle_bytes_read = bytes.length;
recovery.projection_bytes = Buffer.byteLength(
JSON.stringify(projection),
);
recovery.status = "passed";
transcript.push({
name: "local-bundle-projection",
phase: recovery.phase,
input: {
path,
evidence_id: reference.evidence_id,
offset: 0,
limit: 4,
fields: ["address", "value"],
},
projection,
});
} finally {
recovery.elapsed_ms = performance.now() - recoveryStarted;
}
} else evidence = reply.structuredContent;
assert.ok(evidence);
assert.equal(evidence.subject.digest.sha256, report.artifact.sha256);
let count, rows, provider, query, parent;
if (values.mode === "complete") {
assert.equal(evidence.operation, operation);
assert.equal(evidence.provider.id, "ghidra");
assert.ok(Array.isArray(evidence.normalized_result));
count = evidence.normalized_result.length;
rows = selectedRows(evidence.normalized_result);
provider = evidence.provider;
query = evidence.parameters;
parent = evidence.evidence_id;
} else {
assert.equal(evidence.operation, "inspect_analysis_view");
const summary = evidence.normalized_result;
assert.equal(summary.kind, "summary");
assert.equal(summary.parent_operation, operation);
assert.equal(summary.summary.discovery_coverage, "unknown");
assert.equal(summary.source_provider.id, "ghidra");
count = summary.summary.row_count;
provider = summary.source_provider;
query = summary.source_parameters;
parent = summary.parent_evidence_id;
assert.deepEqual(evidence.evidence_links, [parent]);
rows = [];
if (count > 0) {
const pageEvidence = await result(
"inspect_analysis_view",
{
source: { kind: "retained-evidence", evidence_id: parent },
view: {
kind: "native-inventory",
offset: 0,
limit: 4,
fields: ["value"],
},
},
`${phase}:${operation}:page`,
);
const page = pageEvidence.normalized_result;
assert.equal(page.kind, "native-inventory");
assert.equal(page.parent_evidence_id, parent);
assert.equal(page.coverage.total, count);
assert.equal(page.rows.length, Math.min(4, count));
for (const [index, row] of page.rows.entries())
assert.equal(row.source_index, index);
rows = page.rows.map(({ record: { address, value } }) => ({
address,
value,
}));
}
}
assert.ok(Number.isSafeInteger(count) && count >= 0);
if (operation.startsWith("list_")) assert.ok(count > 0, operation);
assert.equal(provider.version, "12.1.4");
assert.equal(Object.hasOwn(query, "detail"), false);
assert.equal(rows.length, Math.min(4, count));
facts.push({
operation,
row_count: count,
rows,
provider,
parameters: query,
limitations: [...new Set(evidence.limitations)].sort(),
});
sourceReferences.push({ phase, operation, parent });
}
report.tasks.push({
phase,
elapsed_ms: performance.now() - taskStarted,
facts,
});
}
assert.deepEqual(report.tasks[0].facts, report.tasks[1].facts);
const taskFacts = report.tasks.map(({ phase, facts }) => ({ phase, facts }));
await writeFile(join(output, "facts.json"), JSON.stringify(taskFacts));
report.facts_sha256 = hash(JSON.stringify(taskFacts));
const bundlePath = join(output, "evidence.json");
await result("export_evidence_bundle", { path: bundlePath });
const bundle = JSON.parse(await readFile(bundlePath, "utf8"));
const sources = sourceReferences.map(({ phase, operation, parent }) => {
const record = bundle.records.find(
(record) => record.evidence_id === parent,
);
assert.ok(record);
assert.equal(record.operation, operation);
assert.equal(record.subject.digest.sha256, report.artifact.sha256);
const task = report.tasks
.find((task) => task.phase === phase)
.facts.find((fact) => fact.operation === operation);
assert.equal(record.normalized_result.length, task.row_count);
assert.deepEqual(selectedRows(record.normalized_result), task.rows);
assert.deepEqual(record.parameters, task.parameters);
assert.deepEqual(record.provider, task.provider);
return { phase, operation, record };
});
await writeFile(join(output, "sources.json"), JSON.stringify(sources));
report.sources_sha256 = hash(JSON.stringify(sources));
if (values.control) {
assert.deepEqual(
taskFacts,
JSON.parse(await readFile(join(values.control, "facts.json"), "utf8")),
);
const control = JSON.parse(
await readFile(join(values.control, "sources.json"), "utf8"),
);
// Assertion formatting of a large mismatch can itself exhaust the experiment
// scope. Retain both files and report a bounded failure without weakening it.
assert.ok(
isDeepStrictEqual(sources, control),
`Full source Evidence differs from control; inspect retained sources.json files (${report.sources_sha256} vs ${hash(JSON.stringify(control))}).`,
);
}
if (values["verify-pages"]) {
for (const { operation, parent } of sourceReferences.filter(
(source) => source.phase === "first",
)) {
const source = sources.find(
(source) => source.phase === "first" && source.operation === operation,
).record;
const all = [];
let offset = 0;
do {
const evidence = await result(
"inspect_analysis_view",
{
source: { kind: "retained-evidence", evidence_id: parent },
view: { kind: "native-inventory", offset, limit: 128 },
},
`verification:${operation}:${offset}`,
);
const page = evidence.normalized_result;
assert.equal(page.kind, "native-inventory");
assert.equal(
page.rows.length,
Math.min(128, source.normalized_result.length - offset),
);
for (const [index, row] of page.rows.entries())
assert.equal(row.source_index, offset + index);
all.push(...page.rows.map((row) => row.record));
assert.equal(
page.coverage.next_offset,
offset + page.rows.length >= source.normalized_result.length
? null
: offset + page.rows.length,
);
offset = page.coverage.next_offset;
} while (offset !== null);
assert.deepEqual(all, source.normalized_result);
}
}
assert.equal(hash(await readFile(values.target)), report.artifact.sha256);
report.status = "passed";
} catch (cause) {
report.status = "failed";
report.failure = String(cause?.stack ?? cause);
process.exitCode = 1;
} finally {
if (opened)
try {
await result("close_binary", {});
} catch (cause) {
report.status = "failed";
report.close_failure = String(cause?.stack ?? cause);
}
report.elapsed_before_cleanup_ms = performance.now() - started;
try {
await writeFile(
join(output, "transcript.json"),
JSON.stringify(transcript),
);
} catch (cause) {
report.status = "failed";
report.transcript_failure = String(cause);
}
const finalized = await finalizeRetainedQueryReceipt({
report,
client,
transport,
account: home,
serverLog,
serverLogFinished,
output,
});
if (report.status !== "passed" || !finalized) process.exitCode = 1;
}
+2 -2
View File
@@ -1,4 +1,4 @@
import { addressedValue } from "../contracts/toolOutputSchemaPrimitives.js";
import { addressedValueSchema } from "../domain/native/nativeInventory.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import {
AnalysisCapabilityUnavailableError,
@@ -54,7 +54,7 @@ export const resolveXrefsAddress = async (
const addresses = new Set<string>();
for (const [index, item] of inventory.value.result.entries()) {
const parsed = addressedValue.safeParse(item);
const parsed = addressedValueSchema.safeParse(item);
if (!parsed.success || parsed.data.address.length === 0)
return err(
new AnalysisOutputError(
@@ -149,6 +149,8 @@ export const inspectAnalysisViewValidated = (
{
evidenceId: parent.evidence_id,
operation: parent.operation,
provider: parent.provider,
analysisProfileDigest: parent.analysis_profile?.digest ?? null,
normalizedResult:
immutableJsonSnapshotMember(parent, "normalized_result") ??
parent.normalized_result,
@@ -175,11 +177,11 @@ export const inspectAnalysisViewValidated = (
if (!projected.ok) return projected;
if (
parent.subject !== null &&
parent.subject.digest.sha256 !== projected.value.artifact.sha256
parent.subject.digest.sha256 !== projected.value.artifact?.sha256
)
return err(
new EvidenceIntegrityError(
`Parent subject SHA-256 ${parent.subject.digest.sha256} differs from analysis artifact SHA-256 ${projected.value.artifact.sha256}.`,
`Parent subject SHA-256 ${parent.subject.digest.sha256} differs from analysis artifact SHA-256 ${projected.value.artifact?.sha256 ?? "unknown"}.`,
),
);
return ok(createAnalysisViewEvidence(input, parent, projected.value));
@@ -337,7 +339,7 @@ const createAnalysisViewEvidence = (
? { relationshipCoverage: parent.relationship_coverage }
: {}),
locations: [
...(result.artifact.path.length > 0
...(result.artifact != null && result.artifact.path.length > 0
? [{ kind: "artifact-path" as const, path: result.artifact.path }]
: []),
...(result.kind === "native" &&
@@ -1,5 +1,5 @@
import { z } from "zod";
import { addressedValue } from "../../contracts/toolOutputSchemaPrimitives.js";
import { addressedValueSchema } from "../../domain/native/nativeInventory.js";
import {
nativeDispatchMetadataResultSchema,
inspectNativeDispatchMetadata,
@@ -44,8 +44,8 @@ export const inspectNativeDispatch = async (
const names = z
.array(
z.union([
addressedValue.strict(),
addressedValue
addressedValueSchema.strict(),
addressedValueSchema
.omit({ value: true })
.extend({ name: z.string() })
.strict(),
+1 -1
View File
@@ -71,7 +71,7 @@ export const registerAnalysisViewCommands = (
});
cli.command(CLI_COMMANDS.inspectAnalysisView, {
description:
"Project a selected view of completed binary-layout, JavaScript application, native function, artifact-inventory, desktop-directory, WASM, Mach-O, Interface Builder, EVM, managed, Android package, Dart AOT, web network capture or pcap Evidence JSON",
"Project a selected view of completed binary-layout, JavaScript application, native function/inventory, artifact-inventory, desktop-directory, WASM, Mach-O, Interface Builder, EVM, managed, Android package, Dart AOT, web network capture or pcap Evidence JSON",
args: z.object({
inputJson: z.string().describe("Inline workflow JSON or JSON file path"),
}),
+17 -1
View File
@@ -17,10 +17,26 @@ export const ANALYSIS_VIEW_TOOL_CONTRACTS = [
...toolContractMetadata("inspect_analysis_view"),
kind: "application",
description:
'Inspect selected views of completed binary-layout, JavaScript application, native analyze_function, inspect_artifact, inventory_artifact, inspect_wasm_artifact, inspect_macho, decode_interface_builder, inspect_evm_interface, inspect_managed_artifact, inspect_managed_members, inspect_android_package, inspect_dart_aot, inspect_il2cpp_metadata or inspect_web_network_capture Evidence without repeating analysis. Historical capture summaries return identity and counts without bodies or archive inventories. Capture views page retained records: view {kind: "capture", detail: "preview" | "complete", offset: 0, limit: 20}; preview selects recorded headline fields and binary descriptors, complete returns exact records with all sidecars. Offsets follow the selected parent record order, preserving original producer ordinals. Paging never reopens the source or reruns the decoder. For inspect_pcap_capture parents, kind pcap pages endpoints, dns-queries, tls-sni, protocols, http-uris or unattributed-addresses. Select an exact query for dns-addresses, or a tshark/capinfos stream for UTF-16 diagnostics text. Page totals count retained facts; source coverage remains explicit. Qlog summaries keep profile/scope and counts; kind qlog pages traces, streams, loss-declarations, incomplete-records, contributions, ranges or final-sizes. Except traces, select trace_index; stream-specific facets also require exact stream_id and direction. Facts retain all-recorded-events-before-selection scope, and interval overlap or endpoint loss declarations do not prove retransmission or network loss. The api-surface view groups retained HAR requests by method, origin and format-rule path template; templated segments are inferences. It reads no headers or bodies and reports no query values. The environment-manifest view projects the same HAR requests on the primary origin (most JSON-like records) into a replayable tool surface: view {kind: "environment-manifest", template_rules: ["after-uid-literal", "opaque-token", "sibling-variance"], include_query_parameter_names: true}. Each tool has a deterministic name, a path template and parameters from the four format rules plus the selected opt-in rules, observed query parameter names, a value-free body shape for POST/PUT/PATCH, status and content-type sets and record ordinals; records are pointers into the parent, excluded groups carry a reason and three fixed unknowns are always stated. Templates, parameters and names are inferences; the view copies no header, cookie or body values and refuses parents inspected with sensitive values. A capture_browser_scenario parent that selected response bodies or header values projects the same way, one record per network transaction in request order; it relies on capture-time redaction of declared secrets instead of that refusal, and a transaction whose URL was redacted is left unrepresented. The environment-state view extracts, from the same parents, the initial state the recorded reads observed: view {kind: "environment-state", template_rules: [...]} with the rules of the manifest it accompanies. It reads response bodies recorded before the first related write and reports entity types with an identity field (proved by a path parameter or inferred from a field name), entities in collection order with per-view values where views differ, documents for non-parameterized non-entity paths written by POST/PUT/PATCH (item-shaped and delete-only paths stay recorded), and a render plan per read tool: item and list views reproduce their latest recordings from the state, and every other read tool is a recorded view with a reason. Types and identity are inferences; values are recorded data, so it refuses parents inspected with sensitive values, and a scenario parent must have selected response bodies; its values keep the capture redaction markers, and a write whose URL it redacted ends the state of every type. For a retained record use source: {kind: "retained-evidence", evidence_id: "<parent_evidence_id>"}. Use normalized_result.parent_evidence_id from a summary receipt. Source kind is required. Portable source uses {kind: "inline", evidence: <complete Evidence>}. A completed Evidence JSON file on this host uses {kind: "evidence-file", path: "<absolute path>"}: REA reads and authenticates it, then projects it like retained Evidence, so a large capture is bounded by the file reader rather than by inline-input accounting. For layout, JavaScript, inventory, WASM, Mach-O, Interface Builder, EVM, managed artifact, Android package and Dart AOT sources, request a summary with view: {kind: "summary"}; summary is a view kind and has no facet. The WASM summary gives validation, counts, WAT digest/length, tool profile and execution/network states without reading WAT text. Select a layout mitigations/linkage facet, one section/symbol/module, or a stable page. Module pages include assets, bundled and source modules; use node_id for ambiguous or unavailable paths. Typed module refeLine truncated
'Inspect selected views of completed binary-layout, JavaScript application, native analyze_function, inspect_artifact, inventory_artifact, inspect_wasm_artifact, inspect_macho, decode_interface_builder, inspect_evm_interface, inspect_managed_artifact, inspect_managed_members, inspect_android_package, inspect_dart_aot, inspect_il2cpp_metadata or inspect_web_network_capture Evidence without repeating analysis. Historical capture summaries return identity and counts without bodies or archive inventories. Capture views page retained records: view {kind: "capture", detail: "preview" | "complete", offset: 0, limit: 20}; preview selects recorded headline fields and binary descriptors, complete returns exact records with all sidecars. Offsets follow the selected parent record order, preserving original producer ordinals. Paging never reopens the source or reruns the decoder. For inspect_pcap_capture parents, kind pcap pages endpoints, dns-queries, tls-sni, protocols, http-uris or unattributed-addresses. Select an exact query for dns-addresses, or a tshark/capinfos stream for UTF-16 diagnostics text. Page totals count retained facts; source coverage remains explicit. Qlog summaries keep profile/scope and counts; kind qlog pages traces, streams, loss-declarations, incomplete-records, contributions, ranges or final-sizes. Except traces, select trace_index; stream-specific facets also require exact stream_id and direction. Facts retain all-recorded-events-before-selection scope, and interval overlap or endpoint loss declarations do not prove retransmission or network loss. The api-surface view groups retained HAR requests by method, origin and format-rule path template; templated segments are inferences. It reads no headers or bodies and reports no query values. The environment-manifest view projects the same HAR requests on the primary origin (most JSON-like records) into a replayable tool surface: view {kind: "environment-manifest", template_rules: ["after-uid-literal", "opaque-token", "sibling-variance"], include_query_parameter_names: true}. Each tool has a deterministic name, a path template and parameters from the four format rules plus the selected opt-in rules, observed query parameter names, a value-free body shape for POST/PUT/PATCH, status and content-type sets and record ordinals; records are pointers into the parent, excluded groups carry a reason and three fixed unknowns are always stated. Templates, parameters and names are inferences; the view copies no header, cookie or body values and refuses parents inspected with sensitive values. A capture_browser_scenario parent that selected response bodies or header values projects the same way, one record per network transaction in request order; it relies on capture-time redaction of declared secrets instead of that refusal, and a transaction whose URL was redacted is left unrepresented. The environment-state view extracts, from the same parents, the initial state the recorded reads observed: view {kind: "environment-state", template_rules: [...]} with the rules of the manifest it accompanies. It reads response bodies recorded before the first related write and reports entity types with an identity field (proved by a path parameter or inferred from a field name), entities in collection order with per-view values where views differ, documents for non-parameterized non-entity paths written by POST/PUT/PATCH (item-shaped and delete-only paths stay recorded), and a render plan per read tool: item and list views reproduce their latest recordings from the state, and every other read tool is a recorded view with a reason. Types and identity are inferences; values are recorded data, so it refuses parents inspected with sensitive values, and a scenario parent must have selected response bodies; its values keep the capture redaction markers, and a write whose URL it redacted ends the state of every type. For a retained record use source: {kind: "retained-evidence", evidence_id: "<parent_evidence_id>"}. Use normalized_result.parent_evidence_id from a summary receipt. Source kind is required. Portable source uses {kind: "inline", evidence: <complete Evidence>}. A completed Evidence JSON file on this host uses {kind: "evidence-file", path: "<absolute path>"}: REA reads and authenticates it, then projects it like retained Evidence, so a large capture is bounded by the file reader rather than by inline-input accounting. For layout, JavaScript, inventory, WASM, Mach-O, Interface Builder, EVM, managed artifact, Android package and Dart AOT sources, request a summary with view: {kind: "summary"}; summary is a view kind and has no facet. The WASM summary gives validation, counts, WAT digest/length, tool profile and execution/network states without reading WAT text. Select a layout mitigations/linkage facet, one section/symbol/module, or a stable page. Module pages include assets, bundled and source modules; use node_id for ambiguous or unavailable paths. Typed module refeLine truncated
inputSchema: inspectAnalysisViewInputSchema,
outputSchema: evidenceResultOf(analysisViewResultSchema),
examples: [
{
title:
"Page selected native inventory fields without repeating the complete list",
input: {
source: {
kind: "retained-evidence",
evidence_id: RETAINED_EXAMPLE_ID,
},
view: {
kind: "native-inventory",
offset: 0,
limit: 32,
fields: ["value"],
},
},
},
{
title: "Read exact inventory paths from retained inspection Evidence",
input: {
+19 -10
View File
@@ -1,3 +1,4 @@
import { analysisDetailSchema } from "./analysisDetail.js";
import { z } from "zod";
import { nativeFunctionAnnotationsInputSchema } from "../domain/native/nativeFunctionAnnotations.js";
@@ -109,13 +110,17 @@ export const OFFICIAL_TOOL_CONTRACTS = [
),
official(
"list_names",
"List every analyzed memory and external symbol as address/value pairs. Provider metadata distinguishes Ghidra primary, dynamic, external, type, and source facts when available.",
z.object({ document, address: optionalAddress }),
"List every analyzed memory and external symbol as address/value pairs. Provider metadata distinguishes Ghidra primary, dynamic, external, type, and source facts when available. Select detail summary for counts and retained native-inventory pages; complete remains the default.",
z.object({
document,
address: optionalAddress,
detail: analysisDetailSchema,
}),
),
official(
"list_procedures",
"List every analyzed procedure as address/value pairs after provider analysis. Ghidra metadata distinguishes thunks and external functions; use returned addresses in later function operations.",
z.object({ document }),
"List every analyzed procedure as address/value pairs after provider analysis. Ghidra metadata distinguishes thunks and external functions; use returned addresses in later function operations. Select detail summary for counts and retained native-inventory pages; complete remains the default.",
z.object({ document, detail: analysisDetailSchema }),
),
official(
"list_segments",
@@ -124,8 +129,12 @@ export const OFFICIAL_TOOL_CONTRACTS = [
),
official(
"list_strings",
"List every provider-reported string object or candidate, or filter to one address, as address/value pairs with encoding, terminator status, and byte length when available. Hopper reads complete typed objects and retains its original display text as provider_value; long literals may span adjacent unterminated objects. Kuna scans mapped printable sequences of at least five characters, including unterminated fragments; references are not computed.",
z.object({ document, address: optionalAddress }),
"List every provider-reported string object or candidate, or filter to one address, as address/value pairs with encoding, terminator status, and byte length when available. Hopper reads complete typed objects and retains its original display text as provider_value; long literals may span adjacent unterminated objects. Kuna scans mapped printable sequences of at least five characters, including unterminated fragments; references are not computed. Select detail summary for counts and retained native-inventory pages; complete remains the default.",
z.object({
document,
address: optionalAddress,
detail: analysisDetailSchema,
}),
),
official(
"next_address",
@@ -191,13 +200,13 @@ export const OFFICIAL_TOOL_CONTRACTS = [
),
official(
"search_procedures",
"Search every analyzed procedure name using literal matching by default or regex when requested. Results are deterministic and complete. Hopper regex mode uses ECMAScript Unicode syntax in a cancellable worker; a matching deadline leaves its API usable.",
z.object(analysisSearchInput),
"Search every analyzed procedure name using literal matching by default or regex when requested. Results are deterministic and complete. Hopper regex mode uses ECMAScript Unicode syntax in a cancellable worker; a matching deadline leaves its API usable. Select detail summary for counts and retained native-inventory pages; complete remains the default.",
z.object({ ...analysisSearchInput, detail: analysisDetailSchema }),
),
official(
"search_strings",
"Search every provider-reported string object or candidate using literal matching by default or regex when requested. Results are deterministic and complete over that provider inventory, not every text sequence in artifact bytes. Kuna scans mapped sequences of at least five characters, including unterminated fragments, without computing references. Ghidra may omit packed or undefined literals, including Rust strings; an empty result does not prove text is absent. If an address is known, read_bytes and xrefs inspect its bytes and references. Hopper retains typed byte decoding, native display, and explicit decoding unknowns. Adjacent long-literal fragments match independently. Hopper regex mode uses ECMAScript Unicode syntax in a cancellable worker; a matching deadline leaves its API usable.",
z.object(analysisSearchInput),
"Search every provider-reported string object or candidate using literal matching by default or regex when requested. Results are deterministic and complete over that provider inventory, not every text sequence in artifact bytes. Kuna scans mapped sequences of at least five characters, including unterminated fragments, without computing references. Ghidra may omit packed or undefined literals, including Rust strings; an empty result does not prove text is absent. If an address is known, read_bytes and xrefs inspect its bytes and references. Hopper retains typed byte decoding, native display, and explicit decoding unknowns. Adjacent long-literal fragments match independently. Hopper regex mode uses ECMAScript Unicode syntax in a cancellable worker; a matching deadline leaves its API usable. Select detail summary for counts and retained native-inventory pages; complete remains the default.",
z.object({ ...analysisSearchInput, detail: analysisDetailSchema }),
),
official(
"set_address_name",
+32 -7
View File
@@ -1,3 +1,6 @@
import { analysisDetailOutputSchemaOf } from "./analysisDetail.js";
import { nativeInventorySummaryViewResultSchema } from "../domain/analysisView/analysisView.js";
import { nativeInventoryRowsSchemas } from "../domain/native/nativeInventory.js";
import { z } from "zod";
import { byteSearchResultSchema } from "../domain/native/bytePattern.js";
import {
@@ -36,7 +39,6 @@ import {
functionInstructionWindowSchema,
referenceEdgeSchema,
unresolvedCallSchema,
analysisStringSchema,
procedureIdentitySchema,
} from "../domain/hopperValues.js";
import { nativeApiInspectionResultSchema } from "../domain/native/nativeApiBoundary.js";
@@ -75,7 +77,6 @@ import { nativeDispatchMetadataResultSchema } from "../domain/native/objcSwiftMe
import { nativeInvestigationTraceSchema } from "../domain/native/nativeInvestigationGraph.js";
import {
addressList,
addressedValue,
addressedEntry,
containingProcedureResolution,
functionDossierOutput,
@@ -130,10 +131,25 @@ export const officialOutputSchemas: Readonly<Record<string, z.ZodObject>> = {
inline_comment: evidenceResultOf(nullableText),
list_bookmarks: evidenceResultOf(z.array(analysisBookmarkSchema)),
list_documents: evidenceResultOf(z.array(z.string())),
list_names: evidenceResultOf(z.array(addressedValue)),
list_procedures: evidenceResultOf(z.array(addressedValue)),
list_names: analysisDetailOutputSchemaOf(
nativeInventoryRowsSchemas.names,
nativeInventorySummaryViewResultSchema.extend({
parent_operation: z.literal("list_names"),
}),
),
list_procedures: analysisDetailOutputSchemaOf(
nativeInventoryRowsSchemas.names,
nativeInventorySummaryViewResultSchema.extend({
parent_operation: z.literal("list_procedures"),
}),
),
list_segments: segmentOutput,
list_strings: evidenceResultOf(z.array(analysisStringSchema)),
list_strings: analysisDetailOutputSchemaOf(
nativeInventoryRowsSchemas.strings,
nativeInventorySummaryViewResultSchema.extend({
parent_operation: z.literal("list_strings"),
}),
),
next_address: evidenceResultOf(z.string()),
prev_address: evidenceResultOf(z.string()),
procedure_address: evidenceResultOf(z.string()),
@@ -192,10 +208,19 @@ export const officialOutputSchemas: Readonly<Record<string, z.ZodObject>> = {
),
procedure_pseudo_code: evidenceResultOf(nullableText),
resolve_containing_procedure: evidenceResultOf(containingProcedureResolution),
search_procedures: evidenceResultOf(
search_procedures: analysisDetailOutputSchemaOf(
z.array(z.object({ address: z.string(), value: z.string() })),
nativeInventorySummaryViewResultSchema.extend({
parent_operation: z.literal("search_procedures"),
}),
),
search_strings: analysisDetailOutputSchemaOf(
nativeInventoryRowsSchemas.strings,
nativeInventorySummaryViewResultSchema.extend({
parent_operation: z.literal("search_strings"),
}),
),
search_strings: evidenceResultOf(z.array(analysisStringSchema)),
set_address_name: evidenceResultOf(z.boolean()),
set_addresses_names: evidenceResultOf(z.record(z.string(), z.boolean())),
set_bookmark: evidenceResultOf(z.boolean()),
@@ -579,34 +579,6 @@ const availableMemoryPermissions = z.object({
source: z.literal("ghidra-memory-block"),
});
export const addressedValue = z.object({
address: z.string(),
value: z.string(),
symbol: z
.object({
primary: z.boolean(),
dynamic: z.boolean(),
external: z.boolean(),
type: z.string(),
source: z.enum(["default", "analysis", "ai", "imported", "user_defined"]),
})
.optional(),
procedure: z
.object({
external: z.boolean(),
thunk: z.boolean(),
thunk_target: z.string().nullable(),
})
.optional(),
string: z
.object({
encoding: z.string(),
termination: z.enum(["missing", "present_or_not_required"]),
byte_length: z.number().int().min(0),
})
.optional(),
});
const memoryRegionOutput = z.object({
name: z.string(),
start: z.string(),
+53 -19
View File
@@ -3,6 +3,17 @@ import {
protobufViewItemsSchema,
protobufViewContextSchema,
} from "./protobufCaptureView.js";
import {
nativeInventoryViewRequestSchema,
nativeInventoryViewRowSchema,
nativeInventoryViewContext,
nativeInventorySummarySchema,
projectNativeInventoryView,
} from "./nativeInventoryView.js";
import {
NATIVE_INVENTORY_OPERATIONS,
nativeInventoryOperationSchema,
} from "../native/nativeInventory.js";
import { isAbsolute } from "node:path";
import { localPathStringSchema } from "../localPath.js";
@@ -240,6 +251,7 @@ export const analysisViewRequestSchema = z.discriminatedUnion("kind", [
javascriptModuleReferencesRequestSchema,
javascriptFactsViewRequestSchema,
nativeViewSchema,
nativeInventoryViewRequestSchema,
artifactInventoryViewRequestSchema,
wasmViewRequestSchema,
machoViewRequestSchema,
@@ -364,6 +376,7 @@ const viewResultBase = {
"inspect_pcap_capture",
"inspect_dart_aot",
"inspect_il2cpp_metadata",
...NATIVE_INVENTORY_OPERATIONS,
]),
parent_digest: digestSchema,
view_digest: digestSchema,
@@ -502,6 +515,15 @@ export const il2cppMetadataSummaryViewResultSchema = z.strictObject({
summary: il2cppMetadataSummarySchema,
});
/** Exact inventory summary with the original observation's provider and query. */
export const nativeInventorySummaryViewResultSchema = z.strictObject({
...summaryViewResultBase,
...nativeInventoryViewContext,
parent_operation: nativeInventoryOperationSchema,
artifact: artifactSchema.nullable(),
summary: nativeInventorySummarySchema,
});
/** Projected facts for one selected view of retained analysis Evidence. */
export const analysisViewResultSchema = z.discriminatedUnion("kind", [
z.strictObject({
@@ -535,25 +557,32 @@ export const analysisViewResultSchema = z.discriminatedUnion("kind", [
]),
view: environmentStateViewRequestSchema,
}),
z.discriminatedUnion("parent_operation", [
binaryLayoutSummaryViewResultSchema,
desktopApplicationSummaryViewResultSchema,
javascriptApplicationSummaryViewResultSchema,
artifactInventorySummaryViewResultSchema,
wasmArtifactSummaryViewResultSchema,
machoSummaryViewResultSchema,
interfaceBuilderSummaryViewResultSchema,
evmInterfaceSummaryViewResultSchema,
managedArtifactSummaryViewResultSchema,
managedMemberSummaryViewResultSchema,
webNetworkCaptureSummaryViewResultSchema,
androidPackageSummaryViewResultSchema,
pcapCaptureSummaryViewResultSchema,
dartAotSummaryViewResultSchema,
il2cppMetadataSummaryViewResultSchema,
nativeInventorySummaryViewResultSchema,
]),
z.strictObject({
...summaryViewResultBase,
summary: z.union([
layoutSummarySchema,
desktopSummarySchema,
javascriptSummarySchema,
artifactInventorySummarySchema,
wasmSummarySchema,
machoSummarySchema,
interfaceBuilderSummarySchema,
evmSummarySchema,
managedSummarySchema,
managedMemberSummarySchema,
captureSummarySchema,
androidPackageSummarySchema,
pcapSummarySchema,
dartAotSummarySchema,
il2cppMetadataSummarySchema,
]),
...viewResultBase,
...nativeInventoryViewContext,
kind: z.literal("native-inventory"),
parent_operation: nativeInventoryOperationSchema,
artifact: artifactSchema.nullable(),
view: nativeInventoryViewRequestSchema,
rows: z.array(nativeInventoryViewRowSchema),
}),
z.strictObject({
kind: z.literal("protobuf"),
@@ -713,6 +742,8 @@ export interface AnalysisViewParent {
readonly evidenceId: string;
readonly operation: string;
readonly normalizedResult: unknown;
readonly provider?: Evidence["provider"];
readonly analysisProfileDigest?: string | null;
/** The owner proved normalizedResult belongs to a sealed, deep-frozen Evidence snapshot. */
readonly sealedResult?: boolean;
/** Preserve the authenticated source identity, including explicit absence. */
@@ -774,7 +805,7 @@ const unsupportedParent = (
parentArtifactPath(parent),
parent.operation === "capture_browser_scenario"
? `Parent Evidence operation "capture_browser_scenario" supports only the environment-manifest and environment-state views.`
: `Parent Evidence operation ${JSON.stringify(parent.operation)} does not support analysis views. Use completed binary layout, JavaScript application, native function, artifact inventory/inspection, WASM, Mach-O, Interface Builder, EVM, managed artifact or member, Android package, Dart AOT, IL2CPP metadata, or web network capture Evidence.`,
: `Parent Evidence operation ${JSON.stringify(parent.operation)} does not support analysis views. Use completed binary layout, JavaScript application, native function/inventory, artifact inventory/inspection, WASM, Mach-O, Interface Builder, EVM, managed artifact or member, Android package, Dart AOT, IL2CPP metadata, or web network capture Evidence.`,
);
const parentArtifactPath = (parent: AnalysisViewParent): string => {
@@ -800,6 +831,9 @@ const parentArtifactPath = (parent: AnalysisViewParent): string => {
/** Named source owners for views that need no extra decoder or accumulator. */
const retainedViewProjectors = new Map<string, typeof projectWasmArtifactView>([
...NATIVE_INVENTORY_OPERATIONS.map(
(operation) => [operation, projectNativeInventoryView] as const,
),
["inspect_desktop_application", projectDesktopApplicationView],
["inspect_wasm_artifact", projectWasmArtifactView],
["inspect_macho", projectMachoView],
@@ -0,0 +1,170 @@
import { afterEach, expect, it, vi } from "vitest";
import { freezeJsonSnapshot } from "../immutableJson.js";
import { nativeInventoryRowsSchemas } from "../native/nativeInventory.js";
import {
analysisViewRequestSchema,
projectAnalysisView,
type AnalysisViewParent,
} from "./analysisView.js";
afterEach(() => vi.restoreAllMocks());
const rows = () => [
{
address: "0x10",
value: "first",
procedure: { external: false, thunk: false, thunk_target: null },
},
{ address: "0x10", value: "alias" },
{ address: "0x20", value: "last" },
];
const parent = (normalizedResult: unknown): AnalysisViewParent => ({
evidenceId: `ev_${"a".repeat(64)}`,
operation: "search_procedures",
normalizedResult,
sealedResult: true,
provider: { id: "fixture", name: "Fixture", version: "1" },
artifact: null,
subject: null,
parameters: { pattern: "a", mode: "literal", case_sensitive: false },
limitations: ["Provider discovery is partial."],
});
const project = (source: AnalysisViewParent, view: unknown) => {
const result = projectAnalysisView(
source,
analysisViewRequestSchema.parse(view),
);
if (!result.ok) throw result.error;
return result.value;
};
it("pages exact source order and same-address alternatives, with selectable fields and stable row identity", () => {
const original = rows();
const source = parent(freezeJsonSnapshot(original));
const selected = project(source, {
kind: "native-inventory",
offset: 0,
limit: 2,
fields: ["value", "address", "value"],
});
if (selected.kind !== "native-inventory")
throw new Error("Expected row page");
expect(selected.rows).toEqual([
{ source_index: 0, record: { address: "0x10", value: "first" } },
{ source_index: 1, record: { address: "0x10", value: "alias" } },
]);
expect(selected.coverage).toMatchObject({
total: 3,
next_offset: 2,
exhausted: false,
});
expect(selected.view).toMatchObject({
fields: ["value", "address", "value"],
});
const complete = [0, 2].flatMap((offset) => {
const page = project(source, {
kind: "native-inventory",
offset,
limit: 2,
});
if (page.kind !== "native-inventory") throw new Error("Expected row page");
return page.rows.map((row) => row.record);
});
expect(complete).toEqual(original);
expect(source.normalizedResult).toEqual(original);
});
it("reuses only authenticated immutable validation while rebinding provenance to the current parent", () => {
const source = parent(freezeJsonSnapshot(rows()));
const parse = vi.spyOn(nativeInventoryRowsSchemas.names, "safeParse");
project(source, { kind: "summary" });
const next = project(
{
...source,
evidenceId: `ev_${"b".repeat(64)}`,
provider: { id: "other", name: "Other", version: null },
},
{ kind: "native-inventory", limit: 1 },
);
expect(parse).toHaveBeenCalledOnce();
expect(next).toMatchObject({
parent_evidence_id: `ev_${"b".repeat(64)}`,
source_provider: { id: "other", name: "Other", version: null },
source_parameters: source.parameters,
artifact: null,
limitations: source.limitations,
});
});
it("revalidates mutable and shallow-frozen arrays despite a forged sealing flag", () => {
for (const shallowFreeze of [false, true]) {
const values: unknown[] = rows();
if (shallowFreeze) Object.freeze(values);
const source = parent(values);
project(source, { kind: "native-inventory", limit: 1 });
Object.assign(values[0] as object, { value: 123 });
expect(() => project(source, { kind: "summary" })).toThrow();
}
});
it("does not use names validation as proof of the stricter string observation format", () => {
const source = parent(
freezeJsonSnapshot([
{
address: "0x10",
value: "x",
string: { encoding: "", termination: "missing", byte_length: 1 },
},
]),
);
project(source, { kind: "summary" });
expect(() =>
project({ ...source, operation: "list_strings" }, { kind: "summary" }),
).toThrow();
});
it("preserves string display and decoding facts, while empty results retain unknown discovery coverage", () => {
const original = [
{
address: "0x10",
value: "native display",
provider_value: "original display",
decoding: { available: false as const, reason: "not decoded" },
string: {
encoding: "native",
encoding_status: "inferred" as const,
termination: "missing" as const,
byte_length: 8,
},
},
];
const page = project(
{ ...parent(freezeJsonSnapshot(original)), operation: "list_strings" },
{ kind: "native-inventory" },
);
if (page.kind !== "native-inventory") throw new Error("Expected row page");
expect(page.rows.map((row) => row.record)).toEqual(original);
const selected = project(
{ ...parent(freezeJsonSnapshot(original)), operation: "list_strings" },
{ kind: "native-inventory", fields: ["value"] },
);
if (selected.kind !== "native-inventory")
throw new Error("Expected selected row page");
expect(selected.rows[0]?.record).toEqual({
address: original[0]!.address,
value: original[0]!.value,
string: original[0]!.string,
decoding: original[0]!.decoding,
});
const empty = parent(freezeJsonSnapshot([]));
const summary = project(empty, { kind: "summary" });
expect(summary).toMatchObject({
artifact: null,
summary: { row_count: 0, discovery_coverage: "unknown" },
});
expect(summary.unknowns.length).toBeGreaterThan(0);
expect(
project(empty, { kind: "native-inventory", offset: 2, fields: [] })
.coverage,
).toMatchObject({ status: "empty", total: 0, exhausted: true });
});
@@ -0,0 +1,192 @@
import { z } from "zod";
import { analysisStringSchema } from "../hopperValues.js";
import { canBorrowParsedJsonSnapshot } from "../immutableJson.js";
import { jsonValueSchema } from "../jsonValue.js";
import {
addressedValueSchema,
isNativeInventoryOperation,
nativeInventoryRowsSchemas,
} from "../native/nativeInventory.js";
import { digestSchema } from "../digests.js";
import { providerSchema } from "../evidence.js";
import { err, ok, type Result } from "../result.js";
import type { AnalysisError } from "../analysisErrorBase.js";
import type {
AnalysisViewParent,
AnalysisViewRequest,
UnsignedAnalysisView,
} from "./analysisView.js";
import { analysisViewInputError } from "./analysisViewErrors.js";
import { parseAnalysisViewResult } from "./analysisViewParent.js";
import {
boundedViewMessages,
completeWithinViewCoverage,
pageViewCoverage,
} from "./analysisViewCoverage.js";
/** Preserve all canonical native row metadata, including string decoding facts. */
const inventoryRecordSchema = addressedValueSchema.extend({
provider_value: analysisStringSchema.shape.provider_value,
decoding: analysisStringSchema.shape.decoding,
string: z.union([
analysisStringSchema.shape.string,
addressedValueSchema.shape.string,
]),
});
const fieldSchema = inventoryRecordSchema.keyof();
export const nativeInventoryViewRequestSchema = z.strictObject({
kind: z.literal("native-inventory"),
offset: z.number().int().nonnegative().default(0),
limit: z.number().int().positive().default(64),
fields: z
.array(fieldSchema)
.exactOptional()
.describe(
"Select record fields; address and source_index preserve identity. Selecting value also retains its string and decoding qualifications. Omit for every recorded field. Search tools filter before retention.",
),
});
/** Parent Evidence ID plus source_index identifies even same-address alternatives. */
export const nativeInventoryViewRowSchema = z.strictObject({
source_index: z.number().int().nonnegative(),
record: inventoryRecordSchema.partial().required({ address: true }),
});
export const nativeInventoryViewContext = {
source_provider: providerSchema.nullable(),
source_analysis_profile_digest: digestSchema.nullable(),
source_parameters: jsonValueSchema,
};
export const nativeInventorySummarySchema = z.strictObject({
collection: z.enum(["procedures", "strings", "names"]),
row_count: z.number().int().nonnegative(),
discovery_coverage: z.literal("unknown"),
selectable_fields: z.array(fieldSchema),
});
const checkedNames = new WeakSet<object>();
const checkedStrings = new WeakSet<object>();
type InventoryRecord = z.infer<typeof inventoryRecordSchema>;
/** Cache only a successful, representation-preserving validation of owned immutable facts. */
const readRows = (
parent: AnalysisViewParent,
strings: boolean,
): Result<readonly InventoryRecord[], AnalysisError> => {
const checked = strings ? checkedStrings : checkedNames;
const source = parent.normalizedResult;
if (Array.isArray(source) && checked.has(source)) return ok(source);
const parsed = strings
? parseAnalysisViewResult(nativeInventoryRowsSchemas.strings, parent)
: parseAnalysisViewResult(nativeInventoryRowsSchemas.names, parent);
if (!parsed.ok) return parsed;
if (
Array.isArray(source) &&
parent.sealedResult === true &&
canBorrowParsedJsonSnapshot(source, parsed.value)
) {
checked.add(source);
return ok(source as readonly InventoryRecord[]);
}
return parsed;
};
/** Project already completed provider observations without another provider read or sort. */
export const projectNativeInventoryView = (
parent: AnalysisViewParent,
view: AnalysisViewRequest,
): Result<UnsignedAnalysisView, AnalysisError> => {
if (
!isNativeInventoryOperation(parent.operation) ||
(view.kind !== "summary" && view.kind !== "native-inventory")
)
return err(
analysisViewInputError([
{
path: ["view"],
reason: "invalid_value",
message:
"Native inventory Evidence supports summary and native-inventory views.",
},
]),
);
const strings =
parent.operation === "list_strings" ||
parent.operation === "search_strings";
const parsed = readRows(parent, strings);
if (!parsed.ok) return parsed;
const rows = parsed.value;
const shared = {
parent_evidence_id: parent.evidenceId,
parent_operation: parent.operation,
parent_digest: parent.evidenceId.slice(3),
artifact: parent.artifact ?? null,
source_provider: parent.provider ?? null,
source_analysis_profile_digest: parent.analysisProfileDigest ?? null,
source_parameters: jsonValueSchema.parse(parent.parameters ?? null),
limitations: boundedViewMessages(
parent.limitations,
parent.evidenceId,
"limitations",
),
unknowns: [
"Counts and paging describe the retained provider-reported rows, not complete discovery of every program object. Missing matches do not establish absence outside that observation.",
...(parent.artifact == null
? ["No artifact identity was bound to the source Evidence."]
: []),
],
};
if (view.kind === "summary")
return ok({
...shared,
kind: "summary",
view,
summary: {
collection: strings
? "strings"
: parent.operation === "list_names"
? "names"
: "procedures",
row_count: rows.length,
discovery_coverage: "unknown",
selectable_fields: fieldSchema.options,
},
coverage: completeWithinViewCoverage(rows.length, rows.length),
});
// Normalize the selection once: repeated caller fields must not multiply
// work or temporary entries for every retained row. Keep the original view.
const fields =
view.fields === undefined
? undefined
: [...new Set(view.fields)].filter((field) => field !== "address");
const selectingValue = fields?.includes("value") === true;
const selected = rows.slice(view.offset, view.offset + view.limit);
return ok({
...shared,
kind: "native-inventory",
view,
rows: selected.map((record, index) => ({
source_index: view.offset + index,
record:
fields === undefined
? record
: {
address: record.address,
...(selectingValue && record.string !== undefined
? { string: record.string }
: {}),
...(selectingValue && record.decoding !== undefined
? { decoding: record.decoding }
: {}),
...Object.fromEntries(
fields
.filter((field) => Object.hasOwn(record, field))
.map((field) => [field, record[field]]),
),
},
})),
coverage: pageViewCoverage(view.offset, selected.length, rows.length),
});
};
+57
View File
@@ -0,0 +1,57 @@
import { z } from "zod";
import { analysisStringSchema } from "../hopperValues.js";
/** Canonical named native observations; same-address alternatives stay separate. */
export const addressedValueSchema = z.object({
address: z.string(),
value: z.string(),
symbol: z
.object({
primary: z.boolean(),
dynamic: z.boolean(),
external: z.boolean(),
type: z.string(),
source: z.enum(["default", "analysis", "ai", "imported", "user_defined"]),
})
.exactOptional(),
procedure: z
.object({
external: z.boolean(),
thunk: z.boolean(),
thunk_target: z.string().nullable(),
})
.exactOptional(),
string: z
.object({
encoding: z.string(),
termination: z.enum(["missing", "present_or_not_required"]),
byte_length: z.number().int().min(0),
})
.exactOptional(),
});
/** Canonical row arrays shared by complete delivery and retained source validation. */
export const nativeInventoryRowsSchemas = {
names: z.array(addressedValueSchema),
strings: z.array(analysisStringSchema),
};
export const NATIVE_INVENTORY_OPERATIONS = [
"list_procedures",
"search_procedures",
"list_strings",
"search_strings",
"list_names",
] as const;
export const nativeInventoryOperationSchema = z.enum(
NATIVE_INVENTORY_OPERATIONS,
);
export type NativeInventoryOperation = z.infer<
typeof nativeInventoryOperationSchema
>;
export const isNativeInventoryOperation = (
operation: string,
): operation is NativeInventoryOperation =>
NATIVE_INVENTORY_OPERATIONS.some((supported) => supported === operation);
+1
View File
@@ -885,6 +885,7 @@ const registerBinaryAnalysisTools = ({
activeTarget,
recordEvidence,
recordUnknown,
evidenceById,
withAdmittedAnalysis,
};
registerOfficialTools(server, analysisOptions);
+2 -2
View File
@@ -6,7 +6,7 @@ import type { JsonValue } from "../domain/jsonValue.js";
import type { AnalysisOperationPort } from "../application/AnalysisProvider.js";
import type { BinarySessionPort } from "../application/binary/BinarySessionPort.js";
import type { PromptCompletionKind } from "../contracts/promptContracts.js";
import { addressedValue } from "../contracts/toolOutputSchemaPrimitives.js";
import { addressedValueSchema } from "../domain/native/nativeInventory.js";
import { currentMcpRequestSignal } from "./EvidenceMcpServer.js";
import { parseArtifactInventoryEvidence } from "../domain/artifactInventoryEvidence.js";
import { EvidenceIntegrityError } from "../domain/evidenceErrors.js";
@@ -14,7 +14,7 @@ import { parseProcessCapture } from "../domain/process/processCaptureParsing.js"
const documentListSchema = z.array(z.string().min(1));
const procedureInventorySchema = z.array(
addressedValue.pick({ address: true, value: true }),
addressedValueSchema.pick({ address: true, value: true }),
);
const providerStatusSchema = z.object({
analysis_provider_candidates: z.array(
+6
View File
@@ -120,6 +120,12 @@ export const registerEvidenceTools = (
: { analysisProfile: execution.value.analysisProfile }),
rawResult: execution.value.rawResult,
limitations: execution.value.limitations,
...(execution.value.relationshipCoverage === undefined
? {}
: {
relationshipCoverage:
execution.value.relationshipCoverage,
}),
locations: execution.value.locations,
evidenceLinks: sourceEvidence.map(({ evidence_id: id }) => id),
},
+18 -136
View File
@@ -1,145 +1,27 @@
import type { EvidenceMcpServer } from "./EvidenceMcpServer.js";
import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js";
import type { ServerContext } from "@modelcontextprotocol/server";
import type {
AnalysisExecution,
AnalysisOperationPort,
} from "../application/AnalysisProvider.js";
import type { ProgressReporter } from "../application/ProgressReporter.js";
import { OFFICIAL_TOOL_CONTRACTS } from "../contracts/officialToolContracts.js";
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import { createEvidence } from "../domain/evidence.js";
import { jsonObjectSchema, type JsonValue } from "../domain/jsonValue.js";
import type { Result } from "../domain/result.js";
import type { Logger } from "pino";
import { mcpProgressReporter } from "./mcpProgress.js";
import { logToolExecution } from "./toolLogging.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
import type { WithAdmittedAnalysis } from "./analysisAdmission.js";
import { NATIVE_INVENTORY_OPERATIONS } from "../domain/native/nativeInventory.js";
import { registerEvidenceTools } from "./registerEvidenceTools.js";
import {
retainedSummaryDelivery,
withoutDeliveryDetail,
type EvidenceToolFamilyOptions,
} from "./retainedSummaryDelivery.js";
/** Optional session services used by direct tool registration. */
export interface OfficialToolRegistration {
readonly logger: Logger;
readonly activeTarget: (() => BinaryTarget | undefined) | undefined;
readonly recordEvidence: EvidenceWriter["recordEvidence"] | undefined;
readonly withAdmittedAnalysis: WithAdmittedAnalysis;
}
const SUMMARY_OPERATIONS: ReadonlySet<string> = new Set(
NATIVE_INVENTORY_OPERATIONS,
);
const withoutDetail = withoutDeliveryDetail(SUMMARY_OPERATIONS);
/** Register direct bridge proxies, preserving MCP cancellation and typed errors. */
/** Direct tools share Evidence recording/projection with the other provider-backed tools. */
export const registerOfficialTools = (
server: EvidenceMcpServer,
options: OfficialToolRegistration,
options: EvidenceToolFamilyOptions,
): void => {
for (const contract of OFFICIAL_TOOL_CONTRACTS) {
registerOfficialTool(server, contract, {
logger: options.logger,
activeTarget: options.activeTarget,
recordEvidence: options.recordEvidence,
withAdmittedAnalysis: options.withAdmittedAnalysis,
});
}
};
const registerOfficialTool = (
server: EvidenceMcpServer,
contract: (typeof OFFICIAL_TOOL_CONTRACTS)[number],
registration: {
readonly logger: Logger;
readonly activeTarget: (() => BinaryTarget | undefined) | undefined;
readonly recordEvidence: EvidenceWriter["recordEvidence"] | undefined;
readonly withAdmittedAnalysis: WithAdmittedAnalysis;
},
): void => {
server.registerTool(
contract.name,
toolRegistrationOptions(contract),
async (input: unknown, context: ServerContext) => {
const admitted = await registration.withAdmittedAnalysis(
contract.name,
context.mcpReq.signal,
async (admittedAnalysis) => {
const arguments_ = jsonObjectSchema.parse(
contract.inputSchema.parse(input),
);
const progress = mcpProgressReporter(context);
const result = await runOfficialOperation(
admittedAnalysis,
contract,
arguments_,
{
logger: registration.logger,
signal: context.mcpReq.signal,
progress,
},
);
if (!result.ok) {
return server.delivery.toCallToolResult(result, contract);
}
const evidence = createEvidence(
result.value.subject ?? registration.activeTarget?.(),
result.value.provider,
{
operation: contract.name,
parameters: arguments_,
result: result.value.result,
...(result.value.analysisProfile === undefined
? {}
: { analysisProfile: result.value.analysisProfile }),
rawResult: result.value.rawResult,
limitations: result.value.limitations,
...(result.value.relationshipCoverage === undefined
? {}
: {
relationshipCoverage: result.value.relationshipCoverage,
}),
locations: result.value.locations,
},
);
const recorded = registration.recordEvidence?.(evidence);
return server.delivery.toEvidenceToolResult(
evidence,
contract,
recorded,
);
},
);
return admitted.ok
? admitted.value
: server.delivery.toCallToolResult(admitted, contract);
},
);
};
const runOfficialOperation = async (
analysis: AnalysisOperationPort,
contract: (typeof OFFICIAL_TOOL_CONTRACTS)[number],
arguments_: Readonly<Record<string, JsonValue>>,
execution: {
readonly logger: Logger;
readonly signal: AbortSignal;
readonly progress: ProgressReporter;
},
): Promise<Result<AnalysisExecution, AnalysisError>> => {
await execution.progress.report({
phase: contract.name,
completed: 0,
total: 1,
message: "started",
registerEvidenceTools(server, OFFICIAL_TOOL_CONTRACTS, {
...options,
...retainedSummaryDelivery(SUMMARY_OPERATIONS, options),
parametersForExecution: withoutDetail,
parametersForEvidence: withoutDetail,
});
const result = await logToolExecution(execution.logger, contract.name, () =>
analysis.execute(contract.name, arguments_, {
signal: execution.signal,
progress: execution.progress,
}),
);
await execution.progress.report({
phase: contract.name,
completed: 1,
total: 1,
message: result.ok ? "completed" : "failed",
terminal: true,
});
return result;
};
+12 -5
View File
@@ -1,3 +1,4 @@
import { isNativeInventoryOperation } from "../domain/native/nativeInventory.js";
import { INSPECT_ANALYSIS_VIEW_OPERATION } from "../domain/analysisView/analysisViewErrors.js";
import type { CallToolResult } from "@modelcontextprotocol/server";
import { STDIO_DEFAULT_MAX_BUFFER_SIZE } from "@modelcontextprotocol/server";
@@ -26,13 +27,19 @@ export const createToolResultDelivery = (
new ToolResultDelivery(maximumResponseBytes ?? STDIO_DEFAULT_MAX_BUFFER_SIZE);
/** Offer a named, schema-validated source page only for retained native analysis. */
const nativeEvidenceContinuation = (
const retainedEvidenceContinuation = (
evidence: Evidence,
): Readonly<Record<string, JsonValue>> => {
if (evidence.operation !== "analyze_function") return {};
const view =
evidence.operation === "analyze_function"
? { kind: "native", facet: "pseudocode", offset: 0 }
: isNativeInventoryOperation(evidence.operation)
? { kind: "native-inventory", offset: 0, limit: 64 }
: undefined;
if (view === undefined) return {};
const input = inspectAnalysisViewInputSchema.safeParse({
source: { kind: "retained-evidence", evidence_id: evidence.evidence_id },
view: { kind: "native", facet: "pseudocode", offset: 0 },
view,
});
if (!input.success) return {};
return {
@@ -81,7 +88,7 @@ export class ToolResultDelivery {
kind: "retained-evidence",
evidence_id: evidence.evidence_id,
},
...nativeEvidenceContinuation(evidence),
...retainedEvidenceContinuation(evidence),
}),
});
}
@@ -171,7 +178,7 @@ export class ToolResultDelivery {
const retainedEvidence = recovery.evidence_reference !== undefined;
const recoveryAdvice =
recovery.continuation !== undefined
? "Call inspect_analysis_view with the supplied continuation arguments to read retained pseudocode. Follow coverage.next_offset for subsequent pages; reduce the page limit if needed, or export the complete retained Evidence."
? "Call inspect_analysis_view with the supplied continuation arguments to read retained analysis. Follow coverage.next_offset for subsequent pages; reduce the page limit if needed, or export the complete retained Evidence."
: retainedEvidence
? "Export retained evidence to consume the complete analysis, or use complete CLI JSON output."
: encoded.constraint === "string-length"
@@ -0,0 +1,329 @@
import { Ajv2020 } from "ajv/dist/2020.js";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { expect, it, onTestFinished } from "vitest";
import { z } from "zod";
import { createAnalysisExecution } from "../../../src/application/AnalysisProvider.js";
import { toolContract } from "../../../src/contracts/toolContracts.js";
import { inspectAnalysisViewInputSchema } from "../../../src/domain/analysisView/analysisView.js";
import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js";
import { NATIVE_INVENTORY_OPERATIONS } from "../../../src/domain/native/nativeInventory.js";
import { ok } from "../../../src/domain/result.js";
import { createServer } from "../../../src/server/createServer.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
import { createTestBinarySession } from "../../fixtures/binarySession.js";
import { connectGhidraMcp } from "./ghidraMcpHarness.js";
const procedureRows = [
{
address: "0x1000",
value: "first",
procedure: { external: false, thunk: false, thunk_target: null },
},
{
address: "0x2000",
value: "second",
procedure: { external: false, thunk: false, thunk_target: null },
},
];
const stringRows = [
{
address: "0x3000",
value: "hello",
string: {
encoding: "utf-8",
termination: "present_or_not_required",
byte_length: 6,
},
},
];
const nameRows = [
{
address: "0x1000",
value: "first",
symbol: {
primary: true,
dynamic: false,
external: false,
type: "function",
source: "analysis",
},
},
];
it("shares complete/summary source identity and advertised contracts across all five inventory tools", async () => {
const harness = await connectGhidraMcp(
"native-inventory-views",
(operation) => {
if (operation === "search_procedures")
return Promise.resolve(
ok(procedureRows.map(({ address, value }) => ({ address, value }))),
);
if (operation === "search_strings")
return Promise.resolve(
ok(stringRows.map(({ address, value }) => ({ address, value }))),
);
const result =
operation === "list_names"
? nameRows
: operation.endsWith("strings")
? stringRows
: procedureRows;
return Promise.resolve(ok(result));
},
);
onTestFinished(() => harness.close());
const tools = (await harness.mcp.listTools()).tools;
const ajv = new Ajv2020({ strict: false, validateFormats: false });
const viewTool = tools.find((tool) => tool.name === "inspect_analysis_view");
if (viewTool?.outputSchema === undefined)
throw new Error("Missing view schema");
const viewInputSchema: Record<string, unknown> = viewTool.inputSchema;
const viewOutputSchema: Record<string, unknown> = viewTool.outputSchema;
expect(ajv.validateSchema(viewInputSchema)).toBe(true);
expect(ajv.validateSchema(viewOutputSchema)).toBe(true);
for (const operation of NATIVE_INVENTORY_OPERATIONS) {
const contract = toolContract(operation);
const advertised = tools.find((tool) => tool.name === operation);
if (advertised?.outputSchema === undefined)
throw new Error("Missing operation schema");
const inputSchema: Record<string, unknown> = advertised.inputSchema;
const outputSchema: Record<string, unknown> = advertised.outputSchema;
expect(ajv.validateSchema(inputSchema)).toBe(true);
expect(ajv.validateSchema(outputSchema)).toBe(true);
const parameters = operation.startsWith("search_")
? { pattern: ".", mode: "regex" }
: {};
const completeReply = await harness.mcp.callTool({
name: operation,
arguments: parameters,
});
if (completeReply.isError) throw new Error(JSON.stringify(completeReply));
expect(
ajv.validate(outputSchema, completeReply.structuredContent),
JSON.stringify(ajv.errors),
).toBe(true);
const complete = parseEvidence(
contract.outputSchema.parse(completeReply.structuredContent),
);
if (!Array.isArray(complete.normalized_result))
throw new Error("Expected complete row array");
expect(complete.parameters).not.toHaveProperty("detail");
const summaryReply = await harness.mcp.callTool({
name: operation,
arguments: { ...parameters, detail: "summary" },
});
if (summaryReply.isError) throw new Error(JSON.stringify(summaryReply));
expect(
ajv.validate(outputSchema, summaryReply.structuredContent),
JSON.stringify(ajv.errors),
).toBe(true);
const summary = parseEvidence(
contract.outputSchema.parse(summaryReply.structuredContent),
);
expect(summary).toMatchObject({
operation: "inspect_analysis_view",
evidence_links: [complete.evidence_id],
normalized_result: {
parent_evidence_id: complete.evidence_id,
source_provider: complete.provider,
source_analysis_profile_digest:
complete.analysis_profile?.digest ?? null,
source_parameters: complete.parameters,
summary: {
row_count: (complete.normalized_result as unknown[]).length,
discovery_coverage: "unknown",
},
},
});
expect(summary.limitations).toEqual(
expect.arrayContaining(complete.limitations),
);
expect(harness.session.evidenceById(complete.evidence_id)).toEqual(
complete,
);
const allRows = [];
for (
let offset = 0;
offset < (complete.normalized_result as unknown[]).length;
offset++
) {
const pageReply = await harness.mcp.callTool({
name: "inspect_analysis_view",
arguments: {
source: {
kind: "retained-evidence",
evidence_id: complete.evidence_id,
},
view: { kind: "native-inventory", offset, limit: 1 },
},
});
if (pageReply.isError) throw new Error(JSON.stringify(pageReply));
expect(
ajv.validate(viewOutputSchema, pageReply.structuredContent),
JSON.stringify(ajv.errors),
).toBe(true);
const page = toolContract("inspect_analysis_view").outputSchema.parse(
pageReply.structuredContent,
).normalized_result;
if (page.kind !== "native-inventory")
throw new Error("Expected inventory page");
expect(page.rows[0]?.source_index).toBe(offset);
allRows.push(...page.rows.map((row) => row.record));
}
expect(allRows).toEqual(complete.normalized_result);
}
expect(
harness.calls.filter((name) =>
NATIVE_INVENTORY_OPERATIONS.some((operation) => operation === name),
),
).toHaveLength(10);
});
it("recovers an oversized complete inventory through a working retained page without another provider read", async () => {
const items = Array.from({ length: 1000 }, (_, index) => ({
address: `0x${(0x1000 + index * 0x2000).toString(16)}`,
value: "x".repeat(7000),
string: {
encoding: "utf-8",
termination: "present_or_not_required",
byte_length: 7001,
},
}));
const harness = await connectGhidraMcp("native-inventory-capacity", () =>
Promise.resolve(ok(items)),
);
onTestFinished(() => harness.close());
const complete = await harness.mcp.callTool({
name: "list_strings",
arguments: {},
});
expect(complete.isError).toBe(true);
const error = parseMcpToolError(complete).error;
expect(error.code).toBe("resource_constraint");
const recovery = z
.object({
reported_limits: z.object({
boundary: z.literal("mcp-response"),
continuation: z.unknown(),
}),
})
.parse(error.details).reported_limits;
const continuation = z
.object({
name: z.literal("inspect_analysis_view"),
arguments: inspectAnalysisViewInputSchema,
})
.parse(recovery.continuation);
const pageReply = await harness.mcp.callTool(continuation);
if (pageReply.isError) throw new Error(JSON.stringify(pageReply));
const page = toolContract("inspect_analysis_view").outputSchema.parse(
pageReply.structuredContent,
).normalized_result;
if (page.kind !== "native-inventory")
throw new Error("Expected inventory page");
expect(page.rows.map((row) => row.record)).toEqual(items.slice(0, 64));
expect(page.coverage).toMatchObject({ total: 1000, next_offset: 64 });
expect(harness.calls).toEqual(["list_strings"]);
const summaryReply = await harness.mcp.callTool({
name: "list_strings",
arguments: { detail: "summary" },
});
if (summaryReply.isError) throw new Error(JSON.stringify(summaryReply));
expect(
parseEvidence(summaryReply.structuredContent).normalized_result,
).toMatchObject({ summary: { row_count: 1000 } });
});
it("refuses summary before provider work when no retaining session is configured", async () => {
let calls = 0;
const server = createServer({
kind: "fixed",
analysis: {
execute: async () => {
calls++;
return ok(
createAnalysisExecution(procedureRows, {
id: "fixture",
name: "Fixture",
version: "1",
}),
);
},
},
});
const client = new Client({
name: "unretained-native-summary",
version: "1",
});
onTestFinished(async () => {
await client.close();
await server.close();
});
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
await server.connect(serverTransport);
await client.connect(clientTransport);
const reply = await client.callTool({
name: "list_procedures",
arguments: { detail: "summary" },
});
expect(reply.isError).toBe(true);
expect(parseMcpToolError(reply).error.code).toBe("capability_unavailable");
expect(calls).toBe(0);
});
it("projects imported inventories with an unknown artifact without inventing a path or opening a provider", async () => {
const session = createTestBinarySession(() => {
throw new Error("Retained views must not open a provider");
});
const source = createEvidence(
undefined,
{ id: "borrowed.native", name: "Borrowed engine", version: "1" },
{
operation: "list_procedures",
parameters: {},
result: procedureRows,
limitations: [
"The engine did not bind this observation to an artifact digest.",
],
},
);
const recorded = session.recordEvidence(source);
if (!recorded.ok) throw recorded.error;
const server = createServer({ kind: "session", session });
const client = new Client({ name: "unbound-native-views", version: "1" });
onTestFinished(async () => {
await client.close();
await server.close();
const closed = await session.close();
if (!closed.ok) throw closed.error;
});
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
await server.connect(serverTransport);
await client.connect(clientTransport);
for (const view of [
{ kind: "summary" },
{ kind: "native-inventory", fields: [] },
]) {
const reply = await client.callTool({
name: "inspect_analysis_view",
arguments: {
source: { kind: "retained-evidence", evidence_id: source.evidence_id },
view,
},
});
if (reply.isError) throw new Error(JSON.stringify(reply));
const evidence = parseEvidence(reply.structuredContent);
expect(evidence).toMatchObject({
subject: null,
locations: [],
evidence_links: [source.evidence_id],
normalized_result: {
artifact: null,
source_provider: source.provider,
source_parameters: {},
},
});
}
});