mirror of
https://github.com/morluto/rea.git
synced 2026-11-06 14:56:32 +00:00
* perf(native): add opt-in Mach-O summary delivery and retained pages inspect_macho returns every import and export inline. For VS Code's Electron Framework one MCP call carries 2.5 MB of text (about 828k o200k tokens), and the same again as structuredContent; the MCP response budget admits it. Agents asking for slice identity, UUID or dependency counts paid that cost on every first call. Add the established opt-in detail selection: omitted or complete keeps the existing complete result and Evidence identity, while summary retains the complete Evidence and returns slice identity with per-collection returned/total/exhaustive counts (956 tokens). inspect_analysis_view kind macho pages the original rows of any collection or the command provenance, repeating the producer's own total and exhaustive flag so page exhaustion is never read as absence. Delivery detail never reaches the native tools or the source Evidence ID: registerEvidenceTools gains a parametersForEvidence hook, so the summary's parent is the same Evidence as a complete call. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(mcp): refuse unretainable summary delivery before analysis A server without Evidence retention (a fixed analysis source) ran the complete inspect_macho or inspect_artifact analysis for detail summary and only then refused it in summarizeRetainedAnalysis, paying the very cost summary delivery exists to avoid. registerEvidenceTools gains a preflight hook; both registrations check retention there, as the WASM, EVM, managed, pcap and capture handlers already do before dispatch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(native): keep whole rows in Mach-O architecture and segment pages The page item union listed plain-object row arrays, and zod returns the first union member that parses while dropping unknown keys. Entrypoint rows declare only file_offset, so every architecture and segment row (which also carry a numeric file_offset) matched it and lost its name, CPU fields, permissions and sections. Rows now use exact key sets, and imports/exports share one member. The MCP test pages every facet of a real /bin/ls and compares it with the unparsed complete rows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
247 lines
7.0 KiB
TypeScript
247 lines
7.0 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
projectAnalysisView,
|
|
type AnalysisViewParent,
|
|
} from "./analysisView.js";
|
|
|
|
const symbol = (name: string) => ({
|
|
name,
|
|
address: null,
|
|
weak: null,
|
|
reexport: null,
|
|
source: "libSystem",
|
|
});
|
|
const coverage = <T>(items: T[], exhaustive = true) => ({
|
|
items,
|
|
total: exhaustive ? items.length : null,
|
|
exhaustive,
|
|
limitations: exhaustive ? [] : ["nm output may omit stripped symbols."],
|
|
});
|
|
const report = {
|
|
format: "mach-o",
|
|
selected_architecture: { name: "arm64", container_file_offset: 0 },
|
|
endian: null,
|
|
word_size: 64,
|
|
file_type: "2",
|
|
flags: ["0x00200085"],
|
|
uuid: "4C4C44E9-5555-3144-A1E9-D858E24A1CCE",
|
|
entrypoints: coverage([{ file_offset: 16384 }]),
|
|
architectures: coverage([
|
|
{
|
|
name: "arm64",
|
|
cpu_type: "16777228",
|
|
cpu_subtype: "0",
|
|
file_offset: 0,
|
|
size: 4096,
|
|
alignment: 14,
|
|
},
|
|
]),
|
|
build_metadata: coverage([
|
|
{ platform: "1", minimum_os: "14.0", sdk: "15.0", tools: [] },
|
|
]),
|
|
load_commands: coverage([{ index: 0, kind: "LC_MAIN", fields: {} }]),
|
|
dependencies: coverage([
|
|
{
|
|
path: "/usr/lib/libSystem.B.dylib",
|
|
kind: "LC_LOAD_DYLIB",
|
|
current_version: "1351.0.0",
|
|
compatibility_version: "1.0.0",
|
|
},
|
|
]),
|
|
imports: coverage(
|
|
["_malloc", "_free", "_open"].map((name) => symbol(name)),
|
|
false,
|
|
),
|
|
exports: coverage([symbol("__mh_execute_header")]),
|
|
segments: coverage([
|
|
{
|
|
name: "__TEXT",
|
|
vm_address: "0x100000000",
|
|
vm_size: 16384,
|
|
file_offset: 0,
|
|
file_size: 16384,
|
|
maximum_permissions: {
|
|
read: true,
|
|
write: false,
|
|
execute: true,
|
|
raw: "5",
|
|
},
|
|
initial_permissions: {
|
|
read: true,
|
|
write: false,
|
|
execute: true,
|
|
raw: "5",
|
|
},
|
|
sections: coverage([]),
|
|
},
|
|
]),
|
|
provenance: [
|
|
{
|
|
tool: "nm",
|
|
command: ["/usr/bin/nm", "$ARTIFACT"],
|
|
tool_version: null,
|
|
version_reason: "fixture",
|
|
executable_sha256: "a".repeat(64),
|
|
exit: { code: 0, signal: null },
|
|
stdout_bytes: 10,
|
|
stderr_bytes: 0,
|
|
},
|
|
],
|
|
limitations: ["Imports and exports combine dyld_info and nm."],
|
|
};
|
|
const parent = (
|
|
normalizedResult: unknown = report,
|
|
subject: AnalysisViewParent["subject"] = {
|
|
name: "tool",
|
|
digest: { sha256: "b".repeat(64) },
|
|
format: "mach-o",
|
|
architecture: "arm64",
|
|
local_path: "/bin/tool",
|
|
},
|
|
): AnalysisViewParent => ({
|
|
evidenceId: `ev_${"c".repeat(64)}`,
|
|
operation: "inspect_macho",
|
|
normalizedResult,
|
|
subject,
|
|
limitations: ["Parent limitation."],
|
|
});
|
|
|
|
describe("Mach-O analysis views", () => {
|
|
it("summarizes slice identity and collection counts without rows", () => {
|
|
const projected = projectAnalysisView(parent(), { kind: "summary" });
|
|
if (!projected.ok) throw projected.error;
|
|
expect(projected.value).toMatchObject({
|
|
kind: "summary",
|
|
parent_operation: "inspect_macho",
|
|
artifact: { path: "/bin/tool", sha256: "b".repeat(64) },
|
|
summary: {
|
|
selected_architecture: { name: "arm64", container_file_offset: 0 },
|
|
uuid: report.uuid,
|
|
collections: {
|
|
imports: { returned: 3, total: null, exhaustive: false },
|
|
exports: { returned: 1, total: 1, exhaustive: true },
|
|
segments: { returned: 1, total: 1, exhaustive: true },
|
|
},
|
|
provenance_count: 1,
|
|
limitation_count: 3,
|
|
},
|
|
coverage: { status: "complete-within-view", examined: 10, total: 10 },
|
|
limitations: [
|
|
"Imports and exports combine dyld_info and nm.",
|
|
"Parent limitation.",
|
|
"nm output may omit stripped symbols.",
|
|
],
|
|
unknowns: [expect.stringContaining("exhaustive imports;")],
|
|
});
|
|
expect(JSON.stringify(projected.value)).not.toContain("_malloc");
|
|
});
|
|
|
|
it("pages original rows with the producer's own coverage", () => {
|
|
const first = projectAnalysisView(parent(), {
|
|
kind: "macho",
|
|
facet: "imports",
|
|
offset: 1,
|
|
limit: 1,
|
|
});
|
|
if (!first.ok) throw first.error;
|
|
expect(first.value).toMatchObject({
|
|
kind: "macho",
|
|
item: [symbol("_free")],
|
|
source_total: null,
|
|
source_exhaustive: false,
|
|
coverage: { status: "page", examined: 1, total: 3, next_offset: 2 },
|
|
});
|
|
expect(first.value.limitations).toContain(
|
|
"nm output may omit stripped symbols.",
|
|
);
|
|
// Rows whose keys include an earlier row shape's keys stay whole.
|
|
for (const facet of ["architectures", "segments"] as const) {
|
|
const page = projectAnalysisView(parent(), {
|
|
kind: "macho",
|
|
facet,
|
|
offset: 0,
|
|
limit: 64,
|
|
});
|
|
if (!page.ok) throw page.error;
|
|
expect(page.value.kind === "macho" && page.value.item).toEqual(
|
|
report[facet].items,
|
|
);
|
|
}
|
|
const provenance = projectAnalysisView(parent(), {
|
|
kind: "macho",
|
|
facet: "provenance",
|
|
offset: 0,
|
|
limit: 64,
|
|
});
|
|
if (!provenance.ok) throw provenance.error;
|
|
expect(provenance.value).toMatchObject({
|
|
item: report.provenance,
|
|
source_total: 1,
|
|
source_exhaustive: true,
|
|
coverage: { exhausted: true, next_offset: null },
|
|
});
|
|
const past = projectAnalysisView(parent(), {
|
|
kind: "macho",
|
|
facet: "exports",
|
|
offset: 5,
|
|
limit: 64,
|
|
});
|
|
if (!past.ok) throw past.error;
|
|
expect(past.value.coverage).toMatchObject({
|
|
status: "empty",
|
|
exhausted: true,
|
|
});
|
|
});
|
|
|
|
it("keeps a missing subject unknown instead of inventing identity", () => {
|
|
const projected = projectAnalysisView(parent(report, null), {
|
|
kind: "summary",
|
|
});
|
|
if (!projected.ok) throw projected.error;
|
|
expect(projected.value.artifact).toEqual({ path: "", sha256: null });
|
|
expect(projected.value.unknowns).toContain(
|
|
"The parent has no recorded subject, so the inspected artifact path and SHA-256 are unknown.",
|
|
);
|
|
});
|
|
|
|
it("rejects other view kinds and malformed retained results", () => {
|
|
const wrongKind = projectAnalysisView(parent(), {
|
|
kind: "wasm",
|
|
facet: "exports",
|
|
offset: 0,
|
|
limit: 64,
|
|
});
|
|
if (wrongKind.ok) throw new Error("expected a view-kind refusal");
|
|
expect(wrongKind.error).toMatchObject({
|
|
_tag: "AnalysisInputError",
|
|
issues: [
|
|
{
|
|
path: ["view"],
|
|
message: "Mach-O inspection supports summary and macho views.",
|
|
},
|
|
],
|
|
});
|
|
const contradictory = projectAnalysisView(
|
|
parent(report, {
|
|
name: "tool",
|
|
digest: { sha256: "b".repeat(64) },
|
|
format: "elf",
|
|
architecture: null,
|
|
local_path: "/bin/tool",
|
|
}),
|
|
{ kind: "summary" },
|
|
);
|
|
if (contradictory.ok) throw new Error("expected a subject refusal");
|
|
expect(contradictory.error).toMatchObject({
|
|
issues: [{ path: ["source", "subject"] }],
|
|
});
|
|
const malformed = projectAnalysisView(
|
|
parent({ ...report, imports: { items: "x" } }),
|
|
{ kind: "summary" },
|
|
);
|
|
expect(malformed.ok).toBe(false);
|
|
if (!malformed.ok) expect(malformed.error._tag).toBe("AnalysisInputError");
|
|
});
|
|
});
|