Compare commits

...

8 Commits

Author SHA1 Message Date
N0zoM1z0 1941d343b8 fix(ghidra): preserve omitted PDB identity uncertainty 2026-10-14 08:24:25 +08:00
N0zoM1z0 c8ba745c92 test(ghidra): update PDB snapshot profile expectation
Align the public MCP inventory and snapshot-replay regression with program-info-v4. Six socket-backed MCP tests pass. Other v1/v2/v3 literals deliberately exercise rejection of historical projections.
2026-10-14 08:24:25 +08:00
N0zoM1z0 c121db7a47 fix(ghidra): retain conflicting PDB identity observations
Compare declared CodeView identity with recognized Program GUID and hexadecimal age formats. Preserve both sources when fields conflict or equivalence cannot be established. Keep known Program identity alongside loaded-state attestation caveats at the projection owner, and revise PE snapshot coverage to program-info-v4.

Validation: 246 associated tests across 15 suites, runtime build, typecheck and changed-file lint pass; two Windows-only cases skip on Linux. Five conflict/unknown-format cases reproduce before the comparison fix; their loaded-state variants also expose the original Program projection dropping identity fields.
2026-10-14 08:24:25 +08:00
N0zoM1z0 2e86757063 fix(ghidra): measure escaped PDB locators before omission
Preserve declared locators that fit the actual 1024-byte JSON UTF-8 budget. Use constant-time bounds for small and oversized inputs, and a bounded scan for escaping and Unicode before allocation. Advance PE coverage to program-info-v3 so earlier projections remain historical.

Validation: 237 focused tests across 15 suites pass, with two Windows-only cases skipped on Linux. Six locator cases and the intermediate-profile replay regression fail before this fix. Typecheck and changed-file lint pass.
2026-10-14 08:24:24 +08:00
N0zoM1z0 6629c5c945 fix(snapshot): revise workflow and PDB coverage profiles
Invalidate workflow Evidence written before upstream limitation propagation, including workflows that previously had no revision. Advance the PE Program PDB observation contract to prevent replaying legacy nullable-state projections while retaining historical Evidence.

Validation: 219 focused tests across 15 suites pass, with two Windows-only cases skipped on Linux. Three historical-profile regressions fail before the fix and pass afterward. Current workflow snapshots replay without extra provider startup or calls; standalone typecheck and changed-file lint pass.
2026-10-14 08:24:24 +08:00
N0zoM1z0 cf7efda456 fix(evidence): stabilize caveats and retain partial PDB identity
Canonicalize composed upstream limitations independently of concurrent provider timing. Preserve declared CodeView identity when Program PDB identity fields are omitted, alongside loaded-state and resource-coverage observations.

Validation: 171 focused tests across 12 suites and runtime build pass. Add controlled dependent-branch ordering and partial PDB attribute regressions; use the declared 4096 UTF-16 wire budget.
2026-10-14 08:24:24 +08:00
N0zoM1z0 7570215ad9 fix(ghidra): preserve nullable PDB coverage observations
Use reported Program observations once and preserve declared PE references only when needed. Keep missing load state unknown, retain omission coverage, and bound escaped inline locators without claiming every procedure name is generated.
2026-10-14 08:24:24 +08:00
N0zoM1z0 7b327544f3 fix(workflows): retain upstream symbol coverage caveats
Keep limitations from exactly the provider observations used by a composed
CLI/MCP call. Reuse the authenticated Ghidra PDB handshake owner, preserve
caller options and failure identity, and deduplicate caveats without extra
engine queries.

Verified on actual Windows with Ghidra 12.1.4 PUBLIC, JDK21.0.12.1 and
Node24.18.0, using the host where.exe and its matching Microsoft public PDB.
Public CLI and MCP composed Evidence retain the referenced GUID/age and
unknown PDB load state. Engine processes used two CPU affinity slots and
512MiB heaps; the CLI test needed a900s startup budget. The initial600s
startup timeout is retained as performance evidence. No paired speedup or
complete-symbol-coverage claim is made.

Refs #2259
2026-10-14 08:24:24 +08:00
16 changed files with 911 additions and 173 deletions

View File

@ -42,6 +42,7 @@ import {
workflowAnalysisProfile,
} from "./InvestigationProviders.js";
import {
executeComposedWorkflow,
createWorkflowEvidence,
workflowSnapshotRecord,
recordWorkflowUnknowns,
@ -401,18 +402,17 @@ const executeAnalysisTool = async (input: {
tool === "trace_native_ui_action"
) {
const { EnhancedTools } = await import("./EnhancedTools.js");
const result = await new EnhancedTools(session).execute(
tool,
input.arguments,
signal,
const result = await executeComposedWorkflow(session, (observed) =>
new EnhancedTools(observed).execute(tool, input.arguments, signal),
);
if (!result.ok) return { output: cliError(result.error) };
const evidence = createWorkflowEvidence({
target: input.openedTarget,
operation: tool,
parameters: input.arguments,
result: result.value,
result: result.value.result,
upstreamProfile: evidenceProfile,
upstreamLimitations: result.value.limitations,
});
return { output: evidence, evidence };
}

View File

@ -19,12 +19,12 @@ export const workflowAnalysisProfile = (
createAnalysisProfile(REA_WORKFLOW_PROVIDER, {
upstream_analysis_profile: upstream,
...(operation === "binary_overview"
? { workflow_revision: 3 }
? { workflow_revision: 4 }
: operation === "batch_decompile" ||
operation === "get_call_graph" ||
operation === "trace_feature"
? { workflow_revision: 2 }
: {}),
? { workflow_revision: 3 }
: { workflow_revision: 1 }),
});
/** Provider identity for deterministic artifact inventories. */

View File

@ -3,15 +3,145 @@ import { describe, expect, it } from "vitest";
import {
recordWorkflowUnknowns,
createWorkflowEvidence,
executeComposedWorkflow,
} from "./WorkflowEvidence.js";
import { InvestigationRecords } from "./investigation/InvestigationRecords.js";
import { UnknownRegistryError } from "../domain/unknownRegistryError.js";
import { err } from "../domain/result.js";
import { err, ok } from "../domain/result.js";
import {
createAnalysisExecution,
type AnalysisOperationPort,
} from "./AnalysisProvider.js";
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
import { nativeApiBoundarySchema } from "../domain/native/nativeApiBoundary.js";
import { projectNativeApiInspection } from "./native/NativeApiInspection.js";
import { functionDossierSchema } from "../domain/hopperValues.js";
import { ghidraFunctionDossier } from "../domain/ghidraValues.fixture.js";
const provider = { id: "fixture", name: "Fixture", version: "1" };
it("keeps Evidence identity stable when dependent branches finish in opposite orders", async () => {
const run = async (order: readonly number[]) => {
const gate = () => {
let resolve: () => void = () => {
throw new Error("gate not initialized");
};
const promise = new Promise<void>((release) => {
resolve = release;
});
return { promise, resolve };
};
const parents = [gate(), gate()];
const children = [gate(), gate()];
const analysis: AnalysisOperationPort = {
execute: async (operation, parameters) => {
const index = Number(parameters.index);
if (operation === "procedure_address") {
await parents[index]!.promise;
return ok(createAnalysisExecution(String(index), provider));
}
children[index]!.resolve();
return ok(
createAnalysisExecution(String(index), provider, {
limitations: [
index === 0 ? "first child caveat" : "second child caveat",
],
}),
);
},
};
const composed = await executeComposedWorkflow(
analysis,
async (observed) => {
const branches = Promise.all(
[0, 1].map(async (index) => {
await observed.execute("procedure_address", { index });
const child = await observed.execute("address_name", { index });
if (!child.ok) throw child.error;
return child.value.result;
}),
);
for (const index of order) {
parents[index]!.resolve();
await children[index]!.promise;
}
return ok(await branches);
},
);
if (!composed.ok) throw composed.error;
return createWorkflowEvidence({
target: undefined,
operation: "batch_decompile",
parameters: {},
result: composed.value.result,
upstreamProfile: undefined,
upstreamLimitations: composed.value.limitations,
});
};
const left = await run([0, 1]);
const right = await run([1, 0]);
expect(left.normalized_result).toEqual(right.normalized_result);
expect(left.limitations).toEqual(right.limitations);
expect(left.evidence_id).toBe(right.evidence_id);
});
it("retains unique upstream limits in canonical order across concurrent observations", async () => {
let release: () => void = () => {
throw new Error("gate not initialized");
};
const gate = new Promise<void>((resolve) => {
release = resolve;
});
let calls = 0;
const options = { signal: new AbortController().signal };
const analysis: AnalysisOperationPort = {
execute: async (_, parameters, forwarded) => {
expect(parameters).toEqual({ marker: true });
expect(forwarded).toBe(options);
const call = calls++;
if (call === 0) await gate;
else release();
return ok(
createAnalysisExecution([], provider, {
limitations: call === 0 ? ["first", "shared"] : ["second", "shared"],
}),
);
},
};
const result = await executeComposedWorkflow(analysis, async (observed) => {
await Promise.all([
observed.execute("list_strings", { marker: true }, options),
observed.execute("list_procedures", { marker: true }, options),
]);
return ok({ count: 0 });
});
expect(calls).toBe(2);
expect(result).toEqual(
ok({ result: { count: 0 }, limitations: ["first", "second", "shared"] }),
);
});
it("preserves a failed composed operation without an extra provider read", async () => {
const failure = err(new AnalysisCancelledError("list_strings"));
let calls = 0;
const result = await executeComposedWorkflow(
{
execute: async () => {
calls++;
return failure;
},
},
async (observed) => {
const observedResult = await observed.execute("list_strings", {});
return observedResult.ok
? ok(observedResult.value.result)
: observedResult;
},
);
expect(result).toBe(failure);
expect(calls).toBe(1);
});
describe("workflow residual question retention", () => {
it("keeps questions linked to workflow Evidence and records repeats idempotently", () => {
const records = new InvestigationRecords();
@ -24,6 +154,7 @@ describe("workflow residual question retention", () => {
parameters: { procedure: "0x1000" },
result,
upstreamProfile: undefined,
upstreamLimitations: [],
});
expect(records.recordEvidence(evidence).ok).toBe(true);
const input = {
@ -61,6 +192,7 @@ describe("workflow residual question retention", () => {
parameters: {},
result,
upstreamProfile: undefined,
upstreamLimitations: [],
});
expect(records.recordEvidence(evidence).ok).toBe(true);
@ -88,6 +220,7 @@ describe("workflow residual question retention", () => {
parameters: {},
result,
upstreamProfile: undefined,
upstreamLimitations: [],
});
expect(records.recordEvidence(evidence).ok).toBe(true);
@ -139,6 +272,7 @@ it.each([
parameters: { procedure: "0x1000" },
result,
upstreamProfile: undefined,
upstreamLimitations: [],
});
const records = new InvestigationRecords();
expect(records.recordEvidence(evidence).ok).toBe(true);

View File

@ -2,7 +2,12 @@ import {
REA_WORKFLOW_PROVIDER,
workflowAnalysisProfile,
} from "./InvestigationProviders.js";
import type { AnalysisOperation } from "./AnalysisProvider.js";
import type {
AnalysisOperation,
AnalysisOperationPort,
} from "./AnalysisProvider.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import { ok, type Result } from "../domain/result.js";
import type { AnalysisProfileCommitment } from "../domain/analysisProfile.js";
import type { WorkflowSnapshotRecordInput } from "./binary/BinarySessionRecords.js";
import {
@ -18,6 +23,37 @@ import {
import type { JsonValue } from "../domain/jsonValue.js";
import type { UnknownRegistryPort } from "./investigation/InvestigationRecordPort.js";
import { UnknownRegistryError } from "../domain/unknownRegistryError.js";
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
/** Retain the limits of exactly the observations used by one composed call. */
export const executeComposedWorkflow = async (
analysis: AnalysisOperationPort,
execute: (
observed: AnalysisOperationPort,
) => Promise<Result<JsonValue, AnalysisError>>,
): Promise<
Result<
{ readonly result: JsonValue; readonly limitations: readonly string[] },
AnalysisError
>
> => {
const limitations = new Set<string>();
const result = await execute({
execute: async (operation, parameters, options) => {
const execution = await analysis.execute(operation, parameters, options);
if (execution.ok)
execution.value.limitations.forEach((limitation) =>
limitations.add(limitation),
);
return execution;
},
});
if (!result.ok) return result;
return ok({
result: result.value,
limitations: [...limitations].sort(compareUnicodeCodePoints),
});
};
/** Build the shared Evidence record for a successful composed binary workflow. */
export const createWorkflowEvidence = (input: {
@ -26,6 +62,7 @@ export const createWorkflowEvidence = (input: {
readonly parameters: Readonly<Record<string, JsonValue>>;
readonly result: JsonValue;
readonly upstreamProfile: AnalysisProfileCommitment | undefined;
readonly upstreamLimitations: readonly string[];
}): Evidence =>
createEvidence(input.target, REA_WORKFLOW_PROVIDER, {
operation: input.operation,
@ -40,7 +77,10 @@ export const createWorkflowEvidence = (input: {
),
}),
confidence: "derived",
limitations: ["Derived by an REA composed workflow."],
limitations: [
"Derived by an REA composed workflow.",
...input.upstreamLimitations,
],
...(input.operation === "batch_decompile"
? { locations: batchProcedureLocations(input.result) }
: {}),

View File

@ -218,7 +218,7 @@ export const resolveGhidraAnalysisProfile = async (
].sort(),
import_mode: "ephemeral-source-immutable",
...(target.format === "pe"
? { pdb_observations: "program-info-v1" }
? { pdb_observations: "program-info-v5" }
: {}),
annotation_policy: "atomic-function-entry-metadata-v1",
load_image_observations: "source-mappings-entry-context-v2",

View File

@ -1,95 +1,297 @@
import { describe, expect, it } from "vitest";
import { expect, it } from "vitest";
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
import { ghidraPeCodeViewLimitations } from "./GhidraPeProfile.js";
import { ghidraPePdbLimitations } from "./GhidraPeProfile.js";
import {
ghidraPdbLimitations,
type GhidraSessionInfo,
} from "./GhidraSessionValues.js";
describe("ghidraPeCodeViewLimitations", () => {
it("returns empty limitations for non-PE targets", () => {
const elfTarget: BinaryTarget = {
path: "/test/file",
sha256: "abc",
kind: "executable",
format: "elf",
architecture: "x86_64",
availableArchitectures: ["x86_64"],
const target: BinaryTarget = {
path: "/fixture/public.exe",
sha256: "a".repeat(64),
kind: "executable",
format: "pe",
architecture: "x86_64",
availableArchitectures: ["x86_64"],
executableRole: "application",
managed: false,
codeView: {
pdbPath: "public.pdb",
pdbFileName: "public.pdb",
guid: "01234567-89ab-cdef-0123-456789abcdef",
age: 10,
},
};
const absentAttributes = {
file: null,
guid: null,
age: null,
signature: null,
version: null,
loaded: null,
};
it.each(["file", "guid", "age"] as const)(
"retains declared identity when only reported %s is omitted",
(attribute) => {
const pdb: GhidraSessionInfo["pdb"] = {
file: "program.pdb",
guid: "reported-guid",
age: "a",
signature: null,
version: "RSDS",
loaded: null,
resource_limit: { omitted_attributes: [attribute], utf16_budget: 4096 },
[attribute]: null,
};
expect(ghidraPeCodeViewLimitations(elfTarget)).toEqual([]);
});
const caveats = ghidraPePdbLimitations(target, pdb).join(" ");
expect(caveats).toContain("omitted");
expect(caveats).toContain('PDB "public.pdb"');
expect(caveats).toContain("01234567-89ab-cdef-0123-456789abcdef");
expect(caveats).toContain("load state unknown");
},
);
it("does not claim CodeView symbols were unapplied when the session loaded a PDB", () => {
const peTarget: BinaryTarget = {
path: "/test/file.exe",
sha256: "abc",
kind: "executable",
architecture: "x86_64",
availableArchitectures: ["x86_64"],
executableRole: "application",
managed: false,
format: "pe",
it.each([undefined, absentAttributes])(
"retains declared identity and unknown load state when reported attributes are %j",
(pdb) => {
const caveats = ghidraPePdbLimitations(target, pdb);
expect(caveats).toHaveLength(1);
expect(caveats[0]).toContain(
'PDB "public.pdb" (GUID 01234567-89ab-cdef-0123-456789abcdef, age (decimal) 10)',
);
expect(caveats[0]).toContain("load state unknown");
expect(caveats[0]).not.toContain("were not applied");
expect(caveats[0]).not.toContain("names are analysis-generated");
},
);
it.each([true, false, null])(
"uses reported PDB observations once with loaded = %s",
(loaded) => {
const pdb = {
file: "public.pdb",
guid: "{01234567-89AB-CDEF-0123-456789ABCDEF}",
age: "0X000A",
signature: null,
version: "RSDS",
loaded,
};
const caveats = ghidraPePdbLimitations(target, pdb);
expect(caveats).toEqual(ghidraPdbLimitations(pdb));
expect(caveats).toHaveLength(1);
expect(caveats.join(" ")).not.toContain('PDB "public.pdb"');
},
);
it.each([
{ file: "different.pdb", guid: "0123456789abcdef0123456789abcdef", age: "a" },
{ file: "public.pdb", guid: "1123456789abcdef0123456789abcdef", age: "a" },
{ file: "public.pdb", guid: "0123456789abcdef0123456789abcdef", age: "10" },
{ file: "public.pdb", guid: "unrecognized-guid", age: "a" },
{ file: "public.pdb", guid: "0123456789abcdef0123456789abcdef", age: "ten" },
])("retains non-equivalent declared PDB identity for %j", (identity) => {
for (const loaded of [false, null, true]) {
const pdb = { ...absentAttributes, ...identity, version: "RSDS", loaded };
const caveats = ghidraPePdbLimitations(target, pdb);
expect(caveats[0]).toBe(ghidraPdbLimitations(pdb)[0]);
expect(caveats[0]).toContain(identity.file);
expect(caveats[0]).toContain(identity.guid);
expect(caveats[0]).toContain(`age (hex) ${identity.age}`);
expect(caveats).toHaveLength(2);
expect(caveats[1]).toContain('PDB "public.pdb"');
expect(caveats[1]).toContain("01234567-89ab-cdef-0123-456789abcdef");
expect(caveats[1]).toContain("age (decimal) 10");
expect(caveats[1]).toContain("equivalence is not established");
expect(pdb).toMatchObject({ ...identity, loaded });
}
});
it.each([
"0123456789ABCDEF0123456789ABCDEF",
"01234567-89ab-cdef-0123-456789abcdef",
])("avoids duplicate identity for normalized matching GUID %s", (guid) => {
const pdb = { ...absentAttributes, file: "public.pdb", guid, age: "a" };
expect(ghidraPePdbLimitations(target, pdb)).toEqual(
ghidraPdbLimitations(pdb),
);
});
it("retains explicit failure to load without asserting all names are generated", () => {
const caveats = ghidraPePdbLimitations(target, {
...absentAttributes,
loaded: false,
});
expect(caveats[0]).toContain("reports not loaded");
expect(caveats[0]).not.toContain("names are analysis-generated");
});
it("retains a loaded-state attestation caveat even when reference attributes are absent", () => {
const caveats = ghidraPePdbLimitations(target, {
...absentAttributes,
loaded: true,
});
expect(caveats[0]).toEqual(
ghidraPdbLimitations({ ...absentAttributes, loaded: true })[0],
);
expect(caveats).toHaveLength(2);
expect(caveats[1]).toContain('PDB "public.pdb"');
expect(caveats[0]).toContain("not an independent PDB identity");
expect(caveats[0]).not.toContain("reports not loaded");
});
it("preserves omitted-attribute resource coverage with declared fallback identity", () => {
const pdb: GhidraSessionInfo["pdb"] = {
...absentAttributes,
resource_limit: { omitted_attributes: ["file"], utf16_budget: 4096 },
};
const caveats = ghidraPePdbLimitations(target, pdb);
expect(caveats).toHaveLength(2);
expect(caveats[0]).toContain("omitted");
expect(caveats[1]).toContain("public.pdb");
});
it("reports NB10 declared age without inventing a GUID", () => {
const caveats = ghidraPePdbLimitations(
{
...target,
codeView: {
pdbPath: "C:\\symbols.pdb",
pdbFileName: "symbols.pdb",
guid: "12345678-1234-1234-1234-123456789abc",
age: 1,
pdbPath: "legacy.pdb",
pdbFileName: "legacy.pdb",
guid: null,
age: 2,
},
};
expect(ghidraPeCodeViewLimitations(peTarget, true)).toEqual([]);
});
},
undefined,
);
expect(caveats[0]).toContain('PDB "legacy.pdb" (age (decimal) 2)');
expect(caveats[0]).not.toContain("GUID");
});
it("returns empty limitations for PE target without codeView", () => {
const peTarget: BinaryTarget = {
path: "/test/file.exe",
sha256: "def",
kind: "executable",
format: "pe",
architecture: "x86_64",
availableArchitectures: ["x86_64"],
executableRole: "application",
managed: false,
};
expect(ghidraPeCodeViewLimitations(peTarget)).toEqual([]);
});
it("returns limitation with GUID and age for RSDS CodeView records", () => {
const peTarget: BinaryTarget = {
path: "/test/file.exe",
sha256: "def",
kind: "executable",
format: "pe",
architecture: "x86_64",
availableArchitectures: ["x86_64"],
executableRole: "application",
managed: false,
it.each([false, null, true])(
"keeps an omitted NB10 GUID unknown with loaded = %s",
(loaded) => {
const pdbPath = "a".repeat(4070) + "\\legacy.pdb";
const legacy = {
...target,
codeView: {
pdbPath: "C:\\build\\app.pdb",
pdbFileName: "app.pdb",
guid: "01234567-89ab-cdef-0123-456789abcdef",
age: 1,
},
};
expect(ghidraPeCodeViewLimitations(peTarget)).toEqual([
'The target references debug symbols "app.pdb" (GUID 01234567-89ab-cdef-0123-456789abcdef, age 1) that were not applied; procedure names are analysis-generated.',
]);
});
it("returns limitation without GUID for NB10 CodeView records", () => {
const peTarget: BinaryTarget = {
path: "/test/file.exe",
sha256: "def",
kind: "executable",
format: "pe",
architecture: "x86",
availableArchitectures: ["x86"],
executableRole: "application",
managed: false,
codeView: {
pdbPath: "D:\\test\\legacy.pdb",
pdbPath,
pdbFileName: "legacy.pdb",
guid: null,
age: 2,
},
};
expect(ghidraPeCodeViewLimitations(peTarget)).toEqual([
'The target references debug symbols "legacy.pdb" (age 2) that were not applied; procedure names are analysis-generated.',
const pdb = {
...absentAttributes,
file: pdbPath,
age: "2",
signature: "01234567",
version: "NB10",
loaded,
};
expect(ghidraPePdbLimitations(legacy, pdb)).toEqual(
ghidraPdbLimitations(pdb),
);
const omitted: GhidraSessionInfo["pdb"] = {
...pdb,
resource_limit: { omitted_attributes: ["guid"], utf16_budget: 4096 },
};
const caveats = ghidraPePdbLimitations(legacy, omitted);
expect(caveats).toEqual([
...ghidraPdbLimitations(omitted),
expect.stringContaining('PDB "legacy.pdb" (age (decimal) 2)'),
]);
});
expect(caveats.at(-1)).toContain("equivalence is not established");
},
);
it.each(["signature", "version"] as const)(
"keeps matching NB10 identity when only %s is omitted",
(attribute) => {
const legacy = {
...target,
codeView: {
pdbPath: "legacy.pdb",
pdbFileName: "legacy.pdb",
guid: null,
age: 2,
},
};
const pdb: GhidraSessionInfo["pdb"] = {
...absentAttributes,
file: "legacy.pdb",
age: "2",
resource_limit: { omitted_attributes: [attribute], utf16_budget: 4096 },
};
expect(ghidraPePdbLimitations(legacy, pdb)).toEqual(
ghidraPdbLimitations(pdb),
);
},
);
it("retains missing-report uncertainty without a declared CodeView record", () => {
const { codeView: _, ...withoutReference } = target;
expect(ghidraPePdbLimitations(withoutReference, undefined)).toEqual(
ghidraPdbLimitations(undefined),
);
});
it("adds no PDB warning for a non-PE target", () => {
const elf: BinaryTarget = {
path: "/fixture/program",
sha256: "a".repeat(64),
kind: "executable",
format: "elf",
architecture: "x86_64",
availableArchitectures: ["x86_64"],
};
expect(ghidraPePdbLimitations(elf, undefined)).toEqual([]);
});
it("bounds an escaped inline locator while preserving its declared source value", () => {
const name = '"'.repeat(5000) + ".pdb";
const selected = {
...target,
codeView: { ...target.codeView!, pdbFileName: name },
};
const caveats = ghidraPePdbLimitations(selected, undefined).join(" ");
expect(Buffer.byteLength(caveats)).toBeLessThan(1024);
expect(caveats).toContain("locator omitted");
expect(caveats).toContain("01234567-89ab-cdef-0123-456789abcdef");
expect(selected.codeView.pdbFileName).toBe(name);
});
it.each([
["long ASCII", "a".repeat(171)],
["ASCII at the byte budget", "a".repeat(1022)],
["ASCII beyond the byte budget", "a".repeat(1023)],
["quotes at the byte budget", '"'.repeat(511)],
["quotes beyond the byte budget", '"'.repeat(512)],
["short control escapes", "\n".repeat(511)],
["short control escapes beyond the byte budget", "\n".repeat(512)],
["six-byte control escapes", "\u0001".repeat(170)],
["six-byte control escapes beyond the byte budget", "\u0001".repeat(171)],
["UTF-8 BMP text", "中".repeat(340)],
["UTF-8 BMP text beyond the byte budget", "中".repeat(341)],
["surrogate pairs", "😀".repeat(255)],
["surrogate pairs beyond the byte budget", "😀".repeat(256)],
["unpaired high surrogates", "\ud800".repeat(170)],
["unpaired high surrogates beyond the byte budget", "\ud800".repeat(171)],
["unpaired low surrogates", "\udfff".repeat(170)],
["unpaired low surrogates beyond the byte budget", "\udfff".repeat(171)],
])("uses actual JSON UTF-8 size for %s", (_, name) => {
const selected = {
...target,
codeView: { ...target.codeView!, pdbFileName: name },
};
const caveat = ghidraPePdbLimitations(selected, undefined).join(" ");
const encoded = JSON.stringify(name);
if (Buffer.byteLength(encoded) <= 1024) {
expect(caveat).toContain(`PDB ${encoded} (`);
expect(caveat).not.toContain("locator omitted");
} else {
expect(caveat).toContain("locator omitted");
expect(caveat).not.toContain(encoded);
}
expect(selected.codeView.pdbFileName).toBe(name);
});

View File

@ -1,24 +1,113 @@
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
import type {
BinaryTarget,
PeCodeViewRecord,
} from "../domain/binaryTargetTypes.js";
import {
ghidraPdbLimitations,
type GhidraSessionInfo,
} from "./GhidraSessionValues.js";
/**
* Report unapplied PE companion PDB symbols when CodeView debug information is present.
*
* Ghidra does not automatically locate or apply sidecar PDBs in REA's isolated
* runtime directories without companion admission. When the PE target header
* references debug symbols, surface this limitation so callers understand that
* procedure names remain analysis-generated.
*/
export const ghidraPeCodeViewLimitations = (
target: BinaryTarget,
pdbLoaded = false,
): readonly string[] => {
if (target.format !== "pe" || target.codeView === undefined || pdbLoaded) {
return [];
}
const { pdbFileName, guid, age } = target.codeView;
return [
guid !== null
? `The target references debug symbols "${pdbFileName}" (GUID ${guid}, age ${age}) that were not applied; procedure names are analysis-generated.`
: `The target references debug symbols "${pdbFileName}" (age ${age}) that were not applied; procedure names are analysis-generated.`,
];
const PDB_LOCATOR_INLINE_BYTE_BUDGET = 1024;
/** Establish equivalence only for the declared fields and recognized formats. */
const reportedPdbMatchesDeclaration = (
declared: PeCodeViewRecord,
reported: NonNullable<GhidraSessionInfo["pdb"]>,
): boolean => {
if (
reported.resource_limit?.omitted_attributes.some(
(attribute) =>
attribute === "file" || attribute === "guid" || attribute === "age",
) ||
(reported.file !== declared.pdbPath &&
reported.file !== declared.pdbFileName) ||
reported.age === null ||
!/^(?:0x)?[0-9a-f]+$/iu.test(reported.age) ||
Number.parseInt(reported.age, 16) !== declared.age
)
return false;
if (declared.guid === null) return reported.guid === null;
if (reported.guid === null) return false;
let guid = reported.guid;
if (guid.startsWith("{") && guid.endsWith("}")) guid = guid.slice(1, -1);
if (
!/^(?:[0-9a-f]{32}|[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12})$/iu.test(
guid,
)
)
return false;
return (
guid.replaceAll("-", "").toLowerCase() ===
declared.guid.replaceAll("-", "").toLowerCase()
);
};
/** Measure JSON quotes, escapes and UTF-8 without allocating the encoded string. */
const pdbLocatorFitsInlineBudget = (value: string): boolean => {
if (value.length * 6 + 2 <= PDB_LOCATOR_INLINE_BYTE_BUDGET) return true;
if (value.length + 2 > PDB_LOCATOR_INLINE_BYTE_BUDGET) return false;
let bytes = 2;
for (let index = 0; index < value.length; index += 1) {
const point = value.codePointAt(index) ?? 0;
if (point > 0xffff) {
bytes += 4;
index += 1;
} else if (point >= 0xd800 && point <= 0xdfff) {
// JSON.stringify escapes unpaired UTF-16 surrogates as six ASCII bytes.
bytes += 6;
} else if (point < 0x20) {
bytes +=
point === 8 ||
point === 9 ||
point === 10 ||
point === 12 ||
point === 13
? 2
: 6;
} else if (point === 0x22 || point === 0x5c) {
bytes += 2;
} else {
bytes += point < 0x80 ? 1 : point < 0x800 ? 2 : 3;
}
if (bytes > PDB_LOCATOR_INLINE_BYTE_BUDGET) return false;
}
return true;
};
/** Use reported Program facts first, retaining declared PE facts as a fallback. */
export const ghidraPePdbLimitations = (
target: BinaryTarget,
pdb: GhidraSessionInfo["pdb"],
): readonly string[] => {
if (target.format !== "pe") return [];
const reported = ghidraPdbLimitations(pdb);
if (
target.codeView === undefined ||
(pdb !== undefined && reportedPdbMatchesDeclaration(target.codeView, pdb))
)
return reported;
const { pdbFileName, guid, age } = target.codeView;
// Include JSON escaping in the inline budget before allocating that representation.
const locator = pdbLocatorFitsInlineBudget(pdbFileName)
? JSON.stringify(pdbFileName)
: `(declared locator omitted from this inline caveat: ${PDB_LOCATOR_INLINE_BYTE_BUDGET}-byte resource budget)`;
const identity =
guid === null
? `age (decimal) ${age}`
: `GUID ${guid}, age (decimal) ${age}`;
const state =
pdb === undefined
? "Ghidra did not report PDB attributes; load state unknown"
: pdb.loaded === false
? "Ghidra reports not loaded"
: pdb.loaded === true
? "Ghidra reports a loaded PDB; this declared identity is not an independent matching-PDB attestation"
: "Ghidra reports load state unknown";
const comparison =
pdb !== undefined &&
[pdb.file, pdb.guid, pdb.age].some((value) => value !== null)
? " Reported and declared PDB observations are retained separately; equivalence is not established."
: "";
const declared = `The PE CodeView record references PDB ${locator} (${identity}); ${state}.${comparison} Original PDB names and types may be missing. Use list_procedures and address-based analysis to inspect recovered functions. REA does not admit a caller-selected PDB companion.`;
return pdb === undefined ? [declared] : [...reported, declared];
};

View File

@ -35,6 +35,11 @@ import { WINDOWS_NATIVE_AUTHORITY_UNAVAILABLE_REASON } from "../process/WindowsA
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { ProviderStartupDeadline } from "../process/ProviderDeadline.js";
import { createAnalysisProfile } from "../domain/analysisProfile.js";
import { createAnalysisExecution } from "../application/AnalysisProvider.js";
import { AnalysisSnapshotCache } from "../application/binary/AnalysisSnapshotCache.js";
import { createEvidence } from "../domain/evidence.js";
import { createEvidenceBundle } from "../domain/evidenceBundle.js";
const INSTALL = "/opt/ghidra_12.1.4_PUBLIC";
const installationHost = (
@ -80,6 +85,69 @@ const createElfClient = async (ghidra: GhidraProvider) => {
return ghidra.createClient(target, resolved.value.profile);
};
it.each([
"program-info-v1",
"program-info-v2",
"program-info-v3",
"program-info-v4",
])(
"retains historical PE PDB Evidence for %s without replaying its old projection",
async (pdbObservations) => {
const target = peTarget("x86_64");
const resolved = await provider().resolveAnalysisProfile(target);
if (!resolved.ok || resolved.value.profile === null)
throw new Error("Expected admitted PE profile");
const current = resolved.value.profile;
const prior = createAnalysisProfile(current.provider, {
...current.parameters,
pdb_observations: pdbObservations,
});
const operation = "search_procedures";
const parameters = { pattern: "wmain" };
const limitations = [
pdbObservations === "program-info-v4"
? "Legacy projection discarded NB10 declaration when the reported GUID was omitted."
: "Legacy projection collapsed unknown PDB load state.",
];
const cache = new AnalysisSnapshotCache();
cache.record({
target,
profile: prior,
operation,
parameters,
execution: createAnalysisExecution([], prior.provider, {
subject: target,
analysisProfile: prior,
limitations,
}),
});
const evidence = createEvidence(target, prior.provider, {
operation,
parameters,
result: [],
analysisProfile: prior,
limitations,
});
const exported = cache.export(
target,
prior,
createEvidenceBundle([evidence]),
);
if (!exported.ok) throw exported.error;
const imported = new AnalysisSnapshotCache();
expect(imported.import(exported.value, undefined, () => ok(0)).ok).toBe(
true,
);
expect(
imported.lookup(target, current, operation, parameters),
).toBeUndefined();
expect(
imported.lookup(target, prior, operation, parameters)?.limitations,
).toEqual(expect.arrayContaining(limitations));
expect(exported.value.evidence_bundle.records).toContainEqual(evidence);
},
);
describe("Ghidra provider", () => {
it("commits DOS loader and real-mode language while retaining the CPU family", async () => {
const ghidra = provider();
@ -830,36 +898,60 @@ describe("Ghidra result projection", () => {
});
describe("Ghidra PE CodeView debug symbol limitations", () => {
it("projects unapplied companion PDB limitation for PE target with CodeView record", async () => {
const ghidra = provider(installationHost(), () => ({
start: () => Promise.resolve(ok(sessionInfo())),
callTool: () => Promise.resolve(ok(null)),
close: () => Promise.resolve(ok(null)),
}));
const target = {
...peTarget("x86_64"),
codeView: {
pdbPath: "C:\\projects\\app\\build\\test.pdb",
pdbFileName: "test.pdb",
guid: "01234567-89ab-cdef-0123-456789abcdef",
age: 1,
},
};
const profile = await ghidra.resolveAnalysisProfile(target);
if (!profile.ok || profile.value.profile === null)
throw new Error("expected PE profile");
const client = ghidra.createClient(target, profile.value.profile);
const health = await client.execute("health", {});
expect(health).toMatchObject({
ok: true,
value: {
limitations: expect.arrayContaining([
'The target references debug symbols "test.pdb" (GUID 01234567-89ab-cdef-0123-456789abcdef, age 1) that were not applied; procedure names are analysis-generated.',
]),
},
});
await client.close();
});
it.each([
["absent report", undefined, "load state unknown"],
["unknown load", null, "load state unknown"],
["not loaded", false, "reports not loaded"],
["loaded", true, "reports a loaded PDB"],
] as const)(
"preserves %s with one PDB caveat for a declared CodeView record",
async (_, loaded, expected) => {
const ghidra = provider(installationHost(), () => ({
start: () =>
Promise.resolve(
ok({
...sessionInfo(),
...(loaded === undefined
? {}
: {
pdb: {
file: "test.pdb",
guid: "01234567-89ab-cdef-0123-456789abcdef",
age: "1",
signature: null,
version: "RSDS",
loaded,
},
}),
}),
),
callTool: () => Promise.resolve(ok(null)),
close: () => Promise.resolve(ok(null)),
}));
const target = {
...peTarget("x86_64"),
codeView: {
pdbPath: "C:\\projects\\app\\build\\test.pdb",
pdbFileName: "test.pdb",
guid: "01234567-89ab-cdef-0123-456789abcdef",
age: 1,
},
};
const profile = await ghidra.resolveAnalysisProfile(target);
if (!profile.ok || profile.value.profile === null)
throw new Error("expected PE profile");
const client = ghidra.createClient(target, profile.value.profile);
const health = await client.execute("health", {});
if (!health.ok) throw health.error;
const pdbCaveats = health.value.limitations.filter((limit) =>
limit.includes("PDB"),
);
expect(pdbCaveats).toHaveLength(1);
expect(pdbCaveats[0]).toContain(expected);
expect(pdbCaveats[0]).not.toContain("names are analysis-generated");
await client.close();
},
);
});
describe("Ghidra relationship coverage", () => {

View File

@ -69,7 +69,7 @@ import {
isGhidraMipsSoftFloatTarget,
} from "./GhidraMipsProfile.js";
import { ghidraRiscvLimitations } from "./GhidraRiscvProfile.js";
import { ghidraPeCodeViewLimitations } from "./GhidraPeProfile.js";
import { ghidraPePdbLimitations } from "./GhidraPeProfile.js";
import { unverifiedGhidraBuildLimitation } from "./GhidraInstallationPolicy.js";
import { GhidraHeadlessLauncher } from "./GhidraLauncher.js";
import { attestGhidraNativeLoadImage } from "./GhidraLoadImageAttest.js";
@ -89,10 +89,7 @@ import {
limitationsFor,
} from "./GhidraProviderCapabilities.js";
import type { GhidraSessionError } from "./GhidraSessionError.js";
import {
ghidraPdbLimitations,
type GhidraSessionInfo,
} from "./GhidraSessionValues.js";
import type { GhidraSessionInfo } from "./GhidraSessionValues.js";
import { ghidraExtensionFailure } from "./extensions/GhidraExtensionFailures.js";
import {
ghidraExtensionSchema,
@ -531,7 +528,6 @@ export const createGhidraProviderClient = (input: {
// Replaced by the reported outcome once the session handshake is checked.
const seedLimitations: string[] = [...ghidraSeedLimitations(seeds)];
const pdbLimitations: string[] = [];
let pdbLoaded = false;
// Replaced by the reported storage outcome once the handshake is checked.
const softFloatLimitations: string[] = [
...ghidraMipsSoftFloatLimitations(softFloat),
@ -657,11 +653,10 @@ export const createGhidraProviderClient = (input: {
info: GhidraSessionInfo,
): Promise<AnalysisError | undefined> => {
if (target.format === "pe") {
pdbLoaded = info.pdb?.loaded === true;
pdbLimitations.splice(
0,
pdbLimitations.length,
...ghidraPdbLimitations(info.pdb),
...ghidraPePdbLimitations(target, info.pdb),
);
}
const softFloatReason = ghidraMipsSoftFloatFailure(
@ -716,7 +711,6 @@ export const createGhidraProviderClient = (input: {
...ghidraExtensionLimitations(extensions),
...seedLimitations,
...pdbLimitations,
...ghidraPeCodeViewLimitations(target, pdbLoaded),
...(releaseLimitation === undefined ? [] : [releaseLimitation]),
];
const nativeAotOwner =

View File

@ -121,21 +121,26 @@ export const ghidraPdbLimitations = (
limitations.push(
`PDB attributes ${pdb.resource_limit.omitted_attributes.join(", ")} were omitted because the handshake's ${pdb.resource_limit.utf16_budget}-UTF-16-unit PDB attribute budget was exhausted. Omitted values remain unknown; they were not truncated or reported as absent.`,
);
const hasReference = [
pdb.file,
pdb.guid,
pdb.age,
pdb.signature,
pdb.version,
].some((value) => value !== null);
const reference = hasReference
? `The Ghidra Program references PDB ${pdb.file ?? "(file unknown)"} (GUID ${pdb.guid ?? "unknown"}, age (hex) ${pdb.age ?? "unknown"}, signature ${pdb.signature ?? "unknown"}, version ${pdb.version ?? "unknown"}); `
: "";
if (pdb.loaded === true)
return [
...limitations,
"Ghidra reports a loaded PDB; this is not an independent PDB identity or complete symbol/type-coverage attestation. Original names and types may still be missing; an empty name search does not prove a procedure is absent. Use list_procedures and address-based analysis to inspect recovered functions.",
`${reference}Ghidra reports a loaded PDB; this is not an independent PDB identity or complete symbol/type-coverage attestation. Original names and types may still be missing; an empty name search does not prove a procedure is absent. Use list_procedures and address-based analysis to inspect recovered functions.`,
];
if (
[pdb.file, pdb.guid, pdb.age, pdb.signature, pdb.version].every(
(value) => value === null,
)
)
return limitations;
if (!hasReference) return limitations;
const state = pdb.loaded === false ? "not loaded" : "load state unknown";
return [
...limitations,
`The target references PDB ${pdb.file ?? "(file unknown)"} (GUID ${pdb.guid ?? "unknown"}, age (hex) ${pdb.age ?? "unknown"}, signature ${pdb.signature ?? "unknown"}, version ${pdb.version ?? "unknown"}); Ghidra reports ${state}. Original names and types may be missing; an empty name search does not prove a procedure is absent. Use list_procedures and address-based analysis to inspect recovered functions. REA does not admit a caller-selected PDB companion.`,
`${reference}Ghidra reports ${state}. Original names and types may be missing; an empty name search does not prove a procedure is absent. Use list_procedures and address-based analysis to inspect recovered functions. REA does not admit a caller-selected PDB companion.`,
];
};

View File

@ -14,6 +14,7 @@ import type { BinarySessionPort } from "../application/binary/BinarySessionPort.
import { EnhancedTools } from "../application/EnhancedTools.js";
import type { ValidatedEnhancedCall } from "../application/EnhancedToolTypes.js";
import {
executeComposedWorkflow,
createWorkflowEvidence,
workflowSnapshotRecord,
recordWorkflowUnknowns,
@ -231,15 +232,20 @@ const executeEnhancedTool = async (
context,
parameters,
});
const services = new EnhancedTools({
const analysis: AnalysisOperationPort = {
execute: (operation, operationParameters, executionOptions) =>
admittedAnalysis.execute(operation, operationParameters, {
...executionOptions,
progress,
}),
});
};
const result = await logToolExecution(registration.logger, name, () =>
services.executeValidated(validatedCall, context.mcpReq.signal),
executeComposedWorkflow(analysis, (observed) =>
new EnhancedTools(observed).executeValidated(
validatedCall,
context.mcpReq.signal,
),
),
);
await progress.report({
phase: name,
@ -253,8 +259,9 @@ const executeEnhancedTool = async (
target: registration.activeTarget?.(),
operation: name,
parameters,
result: result.value,
result: result.value.result,
upstreamProfile: registration.analysisProfile?.(),
upstreamLimitations: result.value.limitations,
});
const recorded = registration.recordEvidence?.(evidence);
if (recorded !== undefined && !recorded.ok)
@ -273,7 +280,7 @@ const executeEnhancedTool = async (
}
const unknowns = recordWorkflowUnknowns({
name,
result: result.value,
result: result.value.result,
evidenceId: evidence.evidence_id,
recordUnknown: registration.recordUnknown,
});

View File

@ -136,6 +136,7 @@ interface SnapshotProviderOptions {
readonly profile?: ReturnType<typeof createAnalysisProfile>;
readonly identity?: ProviderIdentity;
readonly supportedOperations?: readonly CapabilityDescriptor["operation"][];
readonly limitations?: readonly string[];
}
const makeProvider = (
@ -209,6 +210,7 @@ const makeProvider = (
createAnalysisExecution(result, profile.provider, {
analysisProfile: profile,
rawResult: result,
limitations: options.limitations ?? [],
}),
);
},
@ -253,11 +255,11 @@ const withAlternateWorkflowProfile = (
snapshot: AnalysisSnapshot,
path: string,
current: Evidence,
): AnalysisSnapshot => {
const alternateProfile = createAnalysisProfile(REA_WORKFLOW_PROVIDER, {
alternateProfile = createAnalysisProfile(REA_WORKFLOW_PROVIDER, {
workflow: "binary_overview",
fixture: "alternate-profile",
});
}),
): AnalysisSnapshot => {
if (
typeof current.normalized_result !== "object" ||
current.normalized_result === null ||
@ -466,6 +468,78 @@ const verifyWindowsSnapshotAdmission = async (
).toHaveLength(1);
};
it.each([
{ tool: "binary_overview", arguments: {}, priorRevision: 3 },
{
tool: "inspect_native_api",
arguments: { procedure: "0x1000" },
priorRevision: undefined,
},
] as const)(
"refreshes pre-coverage $tool Evidence and replays the current revision",
async (scenario) => {
const fixture = await createSnapshotReplayFixture("rea-prior-coverage-");
const limitation = "Original PDB names may be missing.";
const provider = fixture.createProvider({
supportedOperations: workflowProviderOperations,
limitations: [limitation],
});
const dependencies = fixture.dependencies(provider);
const options = { snapshotPath: fixture.snapshotPath };
const first = parseEvidence(
await runDirectAnalysis(
dependencies,
fixture.path,
scenario.tool,
scenario.arguments,
options,
),
);
const saved = await readAnalysisSnapshot(fixture.snapshotPath);
if (!saved.ok) throw saved.error;
const priorProfile = createAnalysisProfile(REA_WORKFLOW_PROVIDER, {
upstream_analysis_profile: saved.value.binding.analysis_profile,
...(scenario.priorRevision === undefined
? {}
: { workflow_revision: scenario.priorRevision }),
});
const prior = withAlternateWorkflowProfile(
saved.value,
fixture.path,
{ ...first, limitations: ["Derived by an REA composed workflow."] },
priorProfile,
);
const written = await withArtifactResourceScope((resources) =>
writeAnalysisSnapshot(prior, fixture.snapshotPath, true, resources),
);
if (!written.ok) throw written.error;
const refreshed = parseEvidence(
await runDirectAnalysis(
dependencies,
fixture.path,
scenario.tool,
scenario.arguments,
options,
),
);
expect(refreshed.limitations).toContain(limitation);
expect(refreshed.analysis_profile?.digest).not.toBe(priorProfile.digest);
expect(refreshed.normalized_result).toEqual(first.normalized_result);
const starts = [...fixture.starts];
const calls = [...fixture.calls];
const replayed = await runDirectAnalysis(
dependencies,
fixture.path,
scenario.tool,
scenario.arguments,
options,
);
expect(replayed).toEqual(refreshed);
expect(fixture.starts).toEqual(starts);
expect(fixture.calls).toEqual(calls);
},
);
describe("direct analysis composed snapshot replay", () => {
it.skipIf(process.platform !== "win32")(
"does not replay a CLI snapshot through a Windows junction",

View File

@ -9,6 +9,7 @@ import {
AnalysisOutputError,
} from "../../../src/domain/analysisErrorCore.js";
import type { RelationshipCoverage } from "../../../src/domain/relationshipCoverage.js";
import { parseEvidence } from "../../../src/domain/evidence.js";
import { err, ok as resultOk } from "../../../src/domain/result.js";
import { closeEnhancedToolResources, connect } from "./enhancedToolsHarness.js";
@ -125,9 +126,13 @@ describe("enhanced MCP tools", () => {
limitations: [caveat],
},
});
expect(JSON.stringify(graph.structuredContent)?.split(caveat)).toHaveLength(
2,
);
const evidence = parseEvidence(graph.structuredContent);
expect(
JSON.stringify(evidence.normalized_result).split(caveat),
).toHaveLength(2);
expect(
evidence.limitations.filter((limit) => limit === caveat),
).toHaveLength(1);
const trace = await client.callTool({
name: "trace_call_path",

View File

@ -0,0 +1,42 @@
import { afterEach, expect, it } from "vitest";
import { createAnalysisExecution } from "../../../src/application/AnalysisProvider.js";
import { ok } from "../../../src/domain/result.js";
import { parseEvidence } from "../../../src/domain/evidence.js";
import { connect, closeEnhancedToolResources } from "./enhancedToolsHarness.js";
afterEach(closeEnhancedToolResources);
it("retains provider coverage caveats in a composed MCP overview without repeated reads", async () => {
const calls: string[] = [];
const limitation = "Companion PDB symbol coverage is unverified.";
const client = await connect({
execute: async (operation) => {
calls.push(operation);
const value = operation === "current_document" ? "fixture" : [];
return ok(
createAnalysisExecution(
value,
{ id: "fixture", name: "Fixture", version: "1" },
{ limitations: [limitation] },
),
);
},
});
const response = await client.callTool({
name: "binary_overview",
arguments: {},
});
expect(response.isError).not.toBe(true);
const evidence = parseEvidence(response.structuredContent);
expect(evidence.limitations).toEqual([
"Artifact identity is unavailable for this observation.",
"Derived by an REA composed workflow.",
limitation,
]);
expect(evidence.normalized_result).toMatchObject({
document: "fixture",
procedure_count: 0,
string_count: 0,
});
expect(calls.length).toBe(new Set(calls).size);
});

View File

@ -315,7 +315,7 @@ it("retains missing PDB coverage in empty PE name-search Evidence and snapshot r
const evidence = sessionEvidence(harness.session, reply.structuredContent);
expect(evidence.normalized_result).toEqual([]);
expect(evidence.analysis_profile?.parameters.pdb_observations).toBe(
"program-info-v1",
"program-info-v5",
);
const limitation = evidence.limitations.find((value) =>
value.includes(pdb.file),

View File

@ -0,0 +1,54 @@
import { expect, it } from "vitest";
import { parseConfig } from "../../src/config/parseConfig.js";
import { runDirectAnalysis } from "../../src/application/DirectAnalysis.js";
import { createAnalysisExecution } from "../../src/application/AnalysisProvider.js";
import { ok } from "../../src/domain/result.js";
import {
createBinarySessionTargets,
createTestBinarySession,
} from "../fixtures/binarySession.js";
it("retains provider coverage caveats in a composed CLI result and closes the session", async () => {
const [path] = await createBinarySessionTargets();
const limitation = "Companion PDB symbol coverage is unverified.";
let reads = 0;
let closed = 0;
const createSession = () =>
createTestBinarySession(() => ({
execute: async (operation) => {
reads++;
return ok(
createAnalysisExecution(
operation === "current_document" ? "fixture" : [],
{ id: "fixture", name: "Fixture", version: "1" },
{ limitations: [limitation] },
),
);
},
close: async () => {
closed++;
return ok(null);
},
}));
const output = await runDirectAnalysis(
{
readConfiguration: () => parseConfig({}),
createBinarySession: createSession,
createManagedBinarySession: createSession,
},
path,
"binary_overview",
{},
);
expect(output).toMatchObject({
limitations: ["Derived by an REA composed workflow.", limitation],
normalized_result: {
document: "fixture",
segment_count: 0,
procedure_count: 0,
string_count: 0,
},
});
expect(reads).toBe(5);
expect(closed).toBe(1);
});