mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
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:
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
|
advertised choice, keep the existing complete call. CLI inspection returns
|
||||||
portable complete Evidence; save it before projecting a selected inline view.
|
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
|
Use `record_unknown` to track unresolved questions in the user's investigation
|
||||||
and name the authority or environment still required. Supply supporting and
|
and name the authority or environment still required. Supply supporting and
|
||||||
contradicting evidence IDs. It updates the session registry directly.
|
contradicting evidence IDs. It updates the session registry directly.
|
||||||
|
|||||||
@@ -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
|
and unknowns retain their separate meaning. Legacy replies keep their existing
|
||||||
framing.
|
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`
|
For oversized native function Evidence, `details.reported_limits.continuation`
|
||||||
contains a ready-to-call `inspect_analysis_view` request for the retained
|
contains a ready-to-call `inspect_analysis_view` request for the retained
|
||||||
pseudocode's first page. Use its `name` and `arguments` directly, then follow
|
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
|
metadata fits; reduce the limit or export when needed. Continuations are
|
||||||
offered for completed `analyze_function` results only after session recording is
|
offered for completed `analyze_function` results only after session recording is
|
||||||
acknowledged; the complete result and its retention reference remain intact.
|
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
|
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.
|
preserves the complete session without sending it through a single MCP frame.
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
# Analysis efficiency measurements
|
# 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)
|
The [retained-capture validation checkpoint](retained-capture-validation.md)
|
||||||
reconciles the earlier audit queue and measures immutable historical-capture
|
reconciles the earlier audit queue and measures immutable historical-capture
|
||||||
validation reuse on public compact and large qlogs, including first-view costs,
|
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;
|
||||||
|
}
|
||||||
@@ -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 type { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||||
import {
|
import {
|
||||||
AnalysisCapabilityUnavailableError,
|
AnalysisCapabilityUnavailableError,
|
||||||
@@ -54,7 +54,7 @@ export const resolveXrefsAddress = async (
|
|||||||
|
|
||||||
const addresses = new Set<string>();
|
const addresses = new Set<string>();
|
||||||
for (const [index, item] of inventory.value.result.entries()) {
|
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)
|
if (!parsed.success || parsed.data.address.length === 0)
|
||||||
return err(
|
return err(
|
||||||
new AnalysisOutputError(
|
new AnalysisOutputError(
|
||||||
|
|||||||
@@ -149,6 +149,8 @@ export const inspectAnalysisViewValidated = (
|
|||||||
{
|
{
|
||||||
evidenceId: parent.evidence_id,
|
evidenceId: parent.evidence_id,
|
||||||
operation: parent.operation,
|
operation: parent.operation,
|
||||||
|
provider: parent.provider,
|
||||||
|
analysisProfileDigest: parent.analysis_profile?.digest ?? null,
|
||||||
normalizedResult:
|
normalizedResult:
|
||||||
immutableJsonSnapshotMember(parent, "normalized_result") ??
|
immutableJsonSnapshotMember(parent, "normalized_result") ??
|
||||||
parent.normalized_result,
|
parent.normalized_result,
|
||||||
@@ -175,11 +177,11 @@ export const inspectAnalysisViewValidated = (
|
|||||||
if (!projected.ok) return projected;
|
if (!projected.ok) return projected;
|
||||||
if (
|
if (
|
||||||
parent.subject !== null &&
|
parent.subject !== null &&
|
||||||
parent.subject.digest.sha256 !== projected.value.artifact.sha256
|
parent.subject.digest.sha256 !== projected.value.artifact?.sha256
|
||||||
)
|
)
|
||||||
return err(
|
return err(
|
||||||
new EvidenceIntegrityError(
|
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));
|
return ok(createAnalysisViewEvidence(input, parent, projected.value));
|
||||||
@@ -337,7 +339,7 @@ const createAnalysisViewEvidence = (
|
|||||||
? { relationshipCoverage: parent.relationship_coverage }
|
? { relationshipCoverage: parent.relationship_coverage }
|
||||||
: {}),
|
: {}),
|
||||||
locations: [
|
locations: [
|
||||||
...(result.artifact.path.length > 0
|
...(result.artifact != null && result.artifact.path.length > 0
|
||||||
? [{ kind: "artifact-path" as const, path: result.artifact.path }]
|
? [{ kind: "artifact-path" as const, path: result.artifact.path }]
|
||||||
: []),
|
: []),
|
||||||
...(result.kind === "native" &&
|
...(result.kind === "native" &&
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { addressedValue } from "../../contracts/toolOutputSchemaPrimitives.js";
|
import { addressedValueSchema } from "../../domain/native/nativeInventory.js";
|
||||||
import {
|
import {
|
||||||
nativeDispatchMetadataResultSchema,
|
nativeDispatchMetadataResultSchema,
|
||||||
inspectNativeDispatchMetadata,
|
inspectNativeDispatchMetadata,
|
||||||
@@ -44,8 +44,8 @@ export const inspectNativeDispatch = async (
|
|||||||
const names = z
|
const names = z
|
||||||
.array(
|
.array(
|
||||||
z.union([
|
z.union([
|
||||||
addressedValue.strict(),
|
addressedValueSchema.strict(),
|
||||||
addressedValue
|
addressedValueSchema
|
||||||
.omit({ value: true })
|
.omit({ value: true })
|
||||||
.extend({ name: z.string() })
|
.extend({ name: z.string() })
|
||||||
.strict(),
|
.strict(),
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export const registerAnalysisViewCommands = (
|
|||||||
});
|
});
|
||||||
cli.command(CLI_COMMANDS.inspectAnalysisView, {
|
cli.command(CLI_COMMANDS.inspectAnalysisView, {
|
||||||
description:
|
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({
|
args: z.object({
|
||||||
inputJson: z.string().describe("Inline workflow JSON or JSON file path"),
|
inputJson: z.string().describe("Inline workflow JSON or JSON file path"),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -17,10 +17,26 @@ export const ANALYSIS_VIEW_TOOL_CONTRACTS = [
|
|||||||
...toolContractMetadata("inspect_analysis_view"),
|
...toolContractMetadata("inspect_analysis_view"),
|
||||||
kind: "application",
|
kind: "application",
|
||||||
description:
|
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,
|
inputSchema: inspectAnalysisViewInputSchema,
|
||||||
outputSchema: evidenceResultOf(analysisViewResultSchema),
|
outputSchema: evidenceResultOf(analysisViewResultSchema),
|
||||||
examples: [
|
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",
|
title: "Read exact inventory paths from retained inspection Evidence",
|
||||||
input: {
|
input: {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { analysisDetailSchema } from "./analysisDetail.js";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { nativeFunctionAnnotationsInputSchema } from "../domain/native/nativeFunctionAnnotations.js";
|
import { nativeFunctionAnnotationsInputSchema } from "../domain/native/nativeFunctionAnnotations.js";
|
||||||
|
|
||||||
@@ -109,13 +110,17 @@ export const OFFICIAL_TOOL_CONTRACTS = [
|
|||||||
),
|
),
|
||||||
official(
|
official(
|
||||||
"list_names",
|
"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.",
|
"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 }),
|
z.object({
|
||||||
|
document,
|
||||||
|
address: optionalAddress,
|
||||||
|
detail: analysisDetailSchema,
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
official(
|
official(
|
||||||
"list_procedures",
|
"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.",
|
"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 }),
|
z.object({ document, detail: analysisDetailSchema }),
|
||||||
),
|
),
|
||||||
official(
|
official(
|
||||||
"list_segments",
|
"list_segments",
|
||||||
@@ -124,8 +129,12 @@ export const OFFICIAL_TOOL_CONTRACTS = [
|
|||||||
),
|
),
|
||||||
official(
|
official(
|
||||||
"list_strings",
|
"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.",
|
"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 }),
|
z.object({
|
||||||
|
document,
|
||||||
|
address: optionalAddress,
|
||||||
|
detail: analysisDetailSchema,
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
official(
|
official(
|
||||||
"next_address",
|
"next_address",
|
||||||
@@ -191,13 +200,13 @@ export const OFFICIAL_TOOL_CONTRACTS = [
|
|||||||
),
|
),
|
||||||
official(
|
official(
|
||||||
"search_procedures",
|
"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.",
|
"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),
|
z.object({ ...analysisSearchInput, detail: analysisDetailSchema }),
|
||||||
),
|
),
|
||||||
official(
|
official(
|
||||||
"search_strings",
|
"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.",
|
"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),
|
z.object({ ...analysisSearchInput, detail: analysisDetailSchema }),
|
||||||
),
|
),
|
||||||
official(
|
official(
|
||||||
"set_address_name",
|
"set_address_name",
|
||||||
|
|||||||
@@ -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 { z } from "zod";
|
||||||
import { byteSearchResultSchema } from "../domain/native/bytePattern.js";
|
import { byteSearchResultSchema } from "../domain/native/bytePattern.js";
|
||||||
import {
|
import {
|
||||||
@@ -36,7 +39,6 @@ import {
|
|||||||
functionInstructionWindowSchema,
|
functionInstructionWindowSchema,
|
||||||
referenceEdgeSchema,
|
referenceEdgeSchema,
|
||||||
unresolvedCallSchema,
|
unresolvedCallSchema,
|
||||||
analysisStringSchema,
|
|
||||||
procedureIdentitySchema,
|
procedureIdentitySchema,
|
||||||
} from "../domain/hopperValues.js";
|
} from "../domain/hopperValues.js";
|
||||||
import { nativeApiInspectionResultSchema } from "../domain/native/nativeApiBoundary.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 { nativeInvestigationTraceSchema } from "../domain/native/nativeInvestigationGraph.js";
|
||||||
import {
|
import {
|
||||||
addressList,
|
addressList,
|
||||||
addressedValue,
|
|
||||||
addressedEntry,
|
addressedEntry,
|
||||||
containingProcedureResolution,
|
containingProcedureResolution,
|
||||||
functionDossierOutput,
|
functionDossierOutput,
|
||||||
@@ -130,10 +131,25 @@ export const officialOutputSchemas: Readonly<Record<string, z.ZodObject>> = {
|
|||||||
inline_comment: evidenceResultOf(nullableText),
|
inline_comment: evidenceResultOf(nullableText),
|
||||||
list_bookmarks: evidenceResultOf(z.array(analysisBookmarkSchema)),
|
list_bookmarks: evidenceResultOf(z.array(analysisBookmarkSchema)),
|
||||||
list_documents: evidenceResultOf(z.array(z.string())),
|
list_documents: evidenceResultOf(z.array(z.string())),
|
||||||
list_names: evidenceResultOf(z.array(addressedValue)),
|
list_names: analysisDetailOutputSchemaOf(
|
||||||
list_procedures: evidenceResultOf(z.array(addressedValue)),
|
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_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()),
|
next_address: evidenceResultOf(z.string()),
|
||||||
prev_address: evidenceResultOf(z.string()),
|
prev_address: evidenceResultOf(z.string()),
|
||||||
procedure_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),
|
procedure_pseudo_code: evidenceResultOf(nullableText),
|
||||||
resolve_containing_procedure: evidenceResultOf(containingProcedureResolution),
|
resolve_containing_procedure: evidenceResultOf(containingProcedureResolution),
|
||||||
search_procedures: evidenceResultOf(
|
search_procedures: analysisDetailOutputSchemaOf(
|
||||||
z.array(z.object({ address: z.string(), value: z.string() })),
|
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_address_name: evidenceResultOf(z.boolean()),
|
||||||
set_addresses_names: evidenceResultOf(z.record(z.string(), z.boolean())),
|
set_addresses_names: evidenceResultOf(z.record(z.string(), z.boolean())),
|
||||||
set_bookmark: evidenceResultOf(z.boolean()),
|
set_bookmark: evidenceResultOf(z.boolean()),
|
||||||
|
|||||||
@@ -579,34 +579,6 @@ const availableMemoryPermissions = z.object({
|
|||||||
source: z.literal("ghidra-memory-block"),
|
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({
|
const memoryRegionOutput = z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
start: z.string(),
|
start: z.string(),
|
||||||
|
|||||||
@@ -3,6 +3,17 @@ import {
|
|||||||
protobufViewItemsSchema,
|
protobufViewItemsSchema,
|
||||||
protobufViewContextSchema,
|
protobufViewContextSchema,
|
||||||
} from "./protobufCaptureView.js";
|
} 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 { isAbsolute } from "node:path";
|
||||||
|
|
||||||
import { localPathStringSchema } from "../localPath.js";
|
import { localPathStringSchema } from "../localPath.js";
|
||||||
@@ -240,6 +251,7 @@ export const analysisViewRequestSchema = z.discriminatedUnion("kind", [
|
|||||||
javascriptModuleReferencesRequestSchema,
|
javascriptModuleReferencesRequestSchema,
|
||||||
javascriptFactsViewRequestSchema,
|
javascriptFactsViewRequestSchema,
|
||||||
nativeViewSchema,
|
nativeViewSchema,
|
||||||
|
nativeInventoryViewRequestSchema,
|
||||||
artifactInventoryViewRequestSchema,
|
artifactInventoryViewRequestSchema,
|
||||||
wasmViewRequestSchema,
|
wasmViewRequestSchema,
|
||||||
machoViewRequestSchema,
|
machoViewRequestSchema,
|
||||||
@@ -364,6 +376,7 @@ const viewResultBase = {
|
|||||||
"inspect_pcap_capture",
|
"inspect_pcap_capture",
|
||||||
"inspect_dart_aot",
|
"inspect_dart_aot",
|
||||||
"inspect_il2cpp_metadata",
|
"inspect_il2cpp_metadata",
|
||||||
|
...NATIVE_INVENTORY_OPERATIONS,
|
||||||
]),
|
]),
|
||||||
parent_digest: digestSchema,
|
parent_digest: digestSchema,
|
||||||
view_digest: digestSchema,
|
view_digest: digestSchema,
|
||||||
@@ -502,6 +515,15 @@ export const il2cppMetadataSummaryViewResultSchema = z.strictObject({
|
|||||||
summary: il2cppMetadataSummarySchema,
|
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. */
|
/** Projected facts for one selected view of retained analysis Evidence. */
|
||||||
export const analysisViewResultSchema = z.discriminatedUnion("kind", [
|
export const analysisViewResultSchema = z.discriminatedUnion("kind", [
|
||||||
z.strictObject({
|
z.strictObject({
|
||||||
@@ -535,25 +557,32 @@ export const analysisViewResultSchema = z.discriminatedUnion("kind", [
|
|||||||
]),
|
]),
|
||||||
view: environmentStateViewRequestSchema,
|
view: environmentStateViewRequestSchema,
|
||||||
}),
|
}),
|
||||||
|
z.discriminatedUnion("parent_operation", [
|
||||||
|
binaryLayoutSummaryViewResultSchema,
|
||||||
|
desktopApplicationSummaryViewResultSchema,
|
||||||
|
javascriptApplicationSummaryViewResultSchema,
|
||||||
|
artifactInventorySummaryViewResultSchema,
|
||||||
|
wasmArtifactSummaryViewResultSchema,
|
||||||
|
machoSummaryViewResultSchema,
|
||||||
|
interfaceBuilderSummaryViewResultSchema,
|
||||||
|
evmInterfaceSummaryViewResultSchema,
|
||||||
|
managedArtifactSummaryViewResultSchema,
|
||||||
|
managedMemberSummaryViewResultSchema,
|
||||||
|
webNetworkCaptureSummaryViewResultSchema,
|
||||||
|
androidPackageSummaryViewResultSchema,
|
||||||
|
pcapCaptureSummaryViewResultSchema,
|
||||||
|
dartAotSummaryViewResultSchema,
|
||||||
|
il2cppMetadataSummaryViewResultSchema,
|
||||||
|
nativeInventorySummaryViewResultSchema,
|
||||||
|
]),
|
||||||
z.strictObject({
|
z.strictObject({
|
||||||
...summaryViewResultBase,
|
...viewResultBase,
|
||||||
summary: z.union([
|
...nativeInventoryViewContext,
|
||||||
layoutSummarySchema,
|
kind: z.literal("native-inventory"),
|
||||||
desktopSummarySchema,
|
parent_operation: nativeInventoryOperationSchema,
|
||||||
javascriptSummarySchema,
|
artifact: artifactSchema.nullable(),
|
||||||
artifactInventorySummarySchema,
|
view: nativeInventoryViewRequestSchema,
|
||||||
wasmSummarySchema,
|
rows: z.array(nativeInventoryViewRowSchema),
|
||||||
machoSummarySchema,
|
|
||||||
interfaceBuilderSummarySchema,
|
|
||||||
evmSummarySchema,
|
|
||||||
managedSummarySchema,
|
|
||||||
managedMemberSummarySchema,
|
|
||||||
captureSummarySchema,
|
|
||||||
androidPackageSummarySchema,
|
|
||||||
pcapSummarySchema,
|
|
||||||
dartAotSummarySchema,
|
|
||||||
il2cppMetadataSummarySchema,
|
|
||||||
]),
|
|
||||||
}),
|
}),
|
||||||
z.strictObject({
|
z.strictObject({
|
||||||
kind: z.literal("protobuf"),
|
kind: z.literal("protobuf"),
|
||||||
@@ -713,6 +742,8 @@ export interface AnalysisViewParent {
|
|||||||
readonly evidenceId: string;
|
readonly evidenceId: string;
|
||||||
readonly operation: string;
|
readonly operation: string;
|
||||||
readonly normalizedResult: unknown;
|
readonly normalizedResult: unknown;
|
||||||
|
readonly provider?: Evidence["provider"];
|
||||||
|
readonly analysisProfileDigest?: string | null;
|
||||||
/** The owner proved normalizedResult belongs to a sealed, deep-frozen Evidence snapshot. */
|
/** The owner proved normalizedResult belongs to a sealed, deep-frozen Evidence snapshot. */
|
||||||
readonly sealedResult?: boolean;
|
readonly sealedResult?: boolean;
|
||||||
/** Preserve the authenticated source identity, including explicit absence. */
|
/** Preserve the authenticated source identity, including explicit absence. */
|
||||||
@@ -774,7 +805,7 @@ const unsupportedParent = (
|
|||||||
parentArtifactPath(parent),
|
parentArtifactPath(parent),
|
||||||
parent.operation === "capture_browser_scenario"
|
parent.operation === "capture_browser_scenario"
|
||||||
? `Parent Evidence operation "capture_browser_scenario" supports only the environment-manifest and environment-state views.`
|
? `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 => {
|
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. */
|
/** Named source owners for views that need no extra decoder or accumulator. */
|
||||||
const retainedViewProjectors = new Map<string, typeof projectWasmArtifactView>([
|
const retainedViewProjectors = new Map<string, typeof projectWasmArtifactView>([
|
||||||
|
...NATIVE_INVENTORY_OPERATIONS.map(
|
||||||
|
(operation) => [operation, projectNativeInventoryView] as const,
|
||||||
|
),
|
||||||
["inspect_desktop_application", projectDesktopApplicationView],
|
["inspect_desktop_application", projectDesktopApplicationView],
|
||||||
["inspect_wasm_artifact", projectWasmArtifactView],
|
["inspect_wasm_artifact", projectWasmArtifactView],
|
||||||
["inspect_macho", projectMachoView],
|
["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),
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
@@ -885,6 +885,7 @@ const registerBinaryAnalysisTools = ({
|
|||||||
activeTarget,
|
activeTarget,
|
||||||
recordEvidence,
|
recordEvidence,
|
||||||
recordUnknown,
|
recordUnknown,
|
||||||
|
evidenceById,
|
||||||
withAdmittedAnalysis,
|
withAdmittedAnalysis,
|
||||||
};
|
};
|
||||||
registerOfficialTools(server, analysisOptions);
|
registerOfficialTools(server, analysisOptions);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type { JsonValue } from "../domain/jsonValue.js";
|
|||||||
import type { AnalysisOperationPort } from "../application/AnalysisProvider.js";
|
import type { AnalysisOperationPort } from "../application/AnalysisProvider.js";
|
||||||
import type { BinarySessionPort } from "../application/binary/BinarySessionPort.js";
|
import type { BinarySessionPort } from "../application/binary/BinarySessionPort.js";
|
||||||
import type { PromptCompletionKind } from "../contracts/promptContracts.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 { currentMcpRequestSignal } from "./EvidenceMcpServer.js";
|
||||||
import { parseArtifactInventoryEvidence } from "../domain/artifactInventoryEvidence.js";
|
import { parseArtifactInventoryEvidence } from "../domain/artifactInventoryEvidence.js";
|
||||||
import { EvidenceIntegrityError } from "../domain/evidenceErrors.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 documentListSchema = z.array(z.string().min(1));
|
||||||
const procedureInventorySchema = z.array(
|
const procedureInventorySchema = z.array(
|
||||||
addressedValue.pick({ address: true, value: true }),
|
addressedValueSchema.pick({ address: true, value: true }),
|
||||||
);
|
);
|
||||||
const providerStatusSchema = z.object({
|
const providerStatusSchema = z.object({
|
||||||
analysis_provider_candidates: z.array(
|
analysis_provider_candidates: z.array(
|
||||||
|
|||||||
@@ -120,6 +120,12 @@ export const registerEvidenceTools = (
|
|||||||
: { analysisProfile: execution.value.analysisProfile }),
|
: { analysisProfile: execution.value.analysisProfile }),
|
||||||
rawResult: execution.value.rawResult,
|
rawResult: execution.value.rawResult,
|
||||||
limitations: execution.value.limitations,
|
limitations: execution.value.limitations,
|
||||||
|
...(execution.value.relationshipCoverage === undefined
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
|
relationshipCoverage:
|
||||||
|
execution.value.relationshipCoverage,
|
||||||
|
}),
|
||||||
locations: execution.value.locations,
|
locations: execution.value.locations,
|
||||||
evidenceLinks: sourceEvidence.map(({ evidence_id: id }) => id),
|
evidenceLinks: sourceEvidence.map(({ evidence_id: id }) => id),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,145 +1,27 @@
|
|||||||
import type { EvidenceMcpServer } from "./EvidenceMcpServer.js";
|
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 { OFFICIAL_TOOL_CONTRACTS } from "../contracts/officialToolContracts.js";
|
||||||
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
|
import { NATIVE_INVENTORY_OPERATIONS } from "../domain/native/nativeInventory.js";
|
||||||
import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
import { registerEvidenceTools } from "./registerEvidenceTools.js";
|
||||||
import { createEvidence } from "../domain/evidence.js";
|
import {
|
||||||
import { jsonObjectSchema, type JsonValue } from "../domain/jsonValue.js";
|
retainedSummaryDelivery,
|
||||||
import type { Result } from "../domain/result.js";
|
withoutDeliveryDetail,
|
||||||
import type { Logger } from "pino";
|
type EvidenceToolFamilyOptions,
|
||||||
import { mcpProgressReporter } from "./mcpProgress.js";
|
} from "./retainedSummaryDelivery.js";
|
||||||
import { logToolExecution } from "./toolLogging.js";
|
|
||||||
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
|
|
||||||
import type { WithAdmittedAnalysis } from "./analysisAdmission.js";
|
|
||||||
|
|
||||||
/** Optional session services used by direct tool registration. */
|
const SUMMARY_OPERATIONS: ReadonlySet<string> = new Set(
|
||||||
export interface OfficialToolRegistration {
|
NATIVE_INVENTORY_OPERATIONS,
|
||||||
readonly logger: Logger;
|
);
|
||||||
readonly activeTarget: (() => BinaryTarget | undefined) | undefined;
|
const withoutDetail = withoutDeliveryDetail(SUMMARY_OPERATIONS);
|
||||||
readonly recordEvidence: EvidenceWriter["recordEvidence"] | undefined;
|
|
||||||
readonly withAdmittedAnalysis: WithAdmittedAnalysis;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 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 = (
|
export const registerOfficialTools = (
|
||||||
server: EvidenceMcpServer,
|
server: EvidenceMcpServer,
|
||||||
options: OfficialToolRegistration,
|
options: EvidenceToolFamilyOptions,
|
||||||
): void => {
|
): void => {
|
||||||
for (const contract of OFFICIAL_TOOL_CONTRACTS) {
|
registerEvidenceTools(server, OFFICIAL_TOOL_CONTRACTS, {
|
||||||
registerOfficialTool(server, contract, {
|
...options,
|
||||||
logger: options.logger,
|
...retainedSummaryDelivery(SUMMARY_OPERATIONS, options),
|
||||||
activeTarget: options.activeTarget,
|
parametersForExecution: withoutDetail,
|
||||||
recordEvidence: options.recordEvidence,
|
parametersForEvidence: withoutDetail,
|
||||||
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",
|
|
||||||
});
|
});
|
||||||
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;
|
|
||||||
};
|
};
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { isNativeInventoryOperation } from "../domain/native/nativeInventory.js";
|
||||||
import { INSPECT_ANALYSIS_VIEW_OPERATION } from "../domain/analysisView/analysisViewErrors.js";
|
import { INSPECT_ANALYSIS_VIEW_OPERATION } from "../domain/analysisView/analysisViewErrors.js";
|
||||||
import type { CallToolResult } from "@modelcontextprotocol/server";
|
import type { CallToolResult } from "@modelcontextprotocol/server";
|
||||||
import { STDIO_DEFAULT_MAX_BUFFER_SIZE } 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);
|
new ToolResultDelivery(maximumResponseBytes ?? STDIO_DEFAULT_MAX_BUFFER_SIZE);
|
||||||
|
|
||||||
/** Offer a named, schema-validated source page only for retained native analysis. */
|
/** Offer a named, schema-validated source page only for retained native analysis. */
|
||||||
const nativeEvidenceContinuation = (
|
const retainedEvidenceContinuation = (
|
||||||
evidence: Evidence,
|
evidence: Evidence,
|
||||||
): Readonly<Record<string, JsonValue>> => {
|
): 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({
|
const input = inspectAnalysisViewInputSchema.safeParse({
|
||||||
source: { kind: "retained-evidence", evidence_id: evidence.evidence_id },
|
source: { kind: "retained-evidence", evidence_id: evidence.evidence_id },
|
||||||
view: { kind: "native", facet: "pseudocode", offset: 0 },
|
view,
|
||||||
});
|
});
|
||||||
if (!input.success) return {};
|
if (!input.success) return {};
|
||||||
return {
|
return {
|
||||||
@@ -81,7 +88,7 @@ export class ToolResultDelivery {
|
|||||||
kind: "retained-evidence",
|
kind: "retained-evidence",
|
||||||
evidence_id: evidence.evidence_id,
|
evidence_id: evidence.evidence_id,
|
||||||
},
|
},
|
||||||
...nativeEvidenceContinuation(evidence),
|
...retainedEvidenceContinuation(evidence),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -171,7 +178,7 @@ export class ToolResultDelivery {
|
|||||||
const retainedEvidence = recovery.evidence_reference !== undefined;
|
const retainedEvidence = recovery.evidence_reference !== undefined;
|
||||||
const recoveryAdvice =
|
const recoveryAdvice =
|
||||||
recovery.continuation !== undefined
|
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
|
: retainedEvidence
|
||||||
? "Export retained evidence to consume the complete analysis, or use complete CLI JSON output."
|
? "Export retained evidence to consume the complete analysis, or use complete CLI JSON output."
|
||||||
: encoded.constraint === "string-length"
|
: 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: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in new issue
Block a user