Add opt-in retained GDB and objdump summaries using the shared analysis-view layer. Preserve default complete Evidence, ordered records, provenance and completion facts. Validate real Bash disassembly, whole-client context costs, CLI publication and cleanup; retain the original measured cohorts and explicit coverage limitations. Closes #5584.
341 lines
10 KiB
TypeScript
341 lines
10 KiB
TypeScript
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
|
import pino from "pino";
|
|
import { expect, it, onTestFinished, vi } from "vitest";
|
|
import { EvidenceMcpServer } from "../../../src/server/EvidenceMcpServer.js";
|
|
import {
|
|
registerGdbTools,
|
|
type GdbToolManager,
|
|
} from "../../../src/server/registerGdbTools.js";
|
|
import { registerReverseEngineeringTools } from "../../../src/server/registerReverseEngineeringTools.js";
|
|
import { registerAnalysisViewTool } from "../../../src/server/registerAnalysisViewTool.js";
|
|
import { createToolResultDelivery } from "../../../src/server/toolResult.js";
|
|
import {
|
|
createEvidence,
|
|
parseEvidence,
|
|
type Evidence,
|
|
} from "../../../src/domain/evidence.js";
|
|
import { ok } from "../../../src/domain/result.js";
|
|
import { inspectAnalysisView } from "../../../src/application/analysisView/AnalysisViewService.js";
|
|
import { TestReverseEngineeringService } from "../../support/reverseEngineeringService.js";
|
|
import { advertisedMcpToolSchemas } from "../../fixtures/advertisedMcpTool.js";
|
|
|
|
const sessionId = "00000000-0000-4000-8000-000000000001";
|
|
const capture = {
|
|
command: "show architecture",
|
|
mi: "7^done",
|
|
records: [
|
|
'~"architecture\\n"',
|
|
'=thread-created,id="1"',
|
|
"unrecognized 🧪 record",
|
|
"7^done",
|
|
],
|
|
console: '~"architecture\\n"',
|
|
target: "",
|
|
log: "",
|
|
process_exit_observed: false,
|
|
process_exit_code: null,
|
|
process_exit_signal: null,
|
|
output_truncated: true,
|
|
output_invalid_utf8: true as const,
|
|
completion_status: "unknown" as const,
|
|
};
|
|
const gdbEvidence = (
|
|
parameters = { session_id: sessionId, command: capture.command },
|
|
) =>
|
|
createEvidence(
|
|
undefined,
|
|
{ id: "gdb", name: "GDB", version: null },
|
|
{
|
|
operation: "gdb_console",
|
|
parameters,
|
|
result: capture,
|
|
rawResult: capture,
|
|
limitations: ["Capture is partial."],
|
|
},
|
|
);
|
|
|
|
const connect = async (retain = true) => {
|
|
const ledger = new Map<string, Evidence>();
|
|
const record = (evidence: Evidence) => {
|
|
ledger.set(evidence.evidence_id, evidence);
|
|
return ok("added" as const);
|
|
};
|
|
const lookup = (id: string) => ledger.get(id);
|
|
const delivery = createToolResultDelivery(2 * 1024 * 1024);
|
|
const server = new EvidenceMcpServer(
|
|
{ name: "command-views", version: "1" },
|
|
{},
|
|
retain ? record : undefined,
|
|
delivery,
|
|
);
|
|
const execute = vi.fn(async () =>
|
|
ok({ result: capture, evidence: gdbEvidence() }),
|
|
);
|
|
const manager: GdbToolManager = {
|
|
start: async () =>
|
|
ok({ session_id: sessionId, mi_version: "mi3" as const }),
|
|
execute,
|
|
status: () => undefined,
|
|
close: async () => ok({ target_state: "none_observed" as const }),
|
|
retryCleanupFailures: async () => ok(null),
|
|
};
|
|
const run = vi.fn(async () => ({
|
|
stdout: "BFD advertised targets\n",
|
|
stderr: "diagnostic\n",
|
|
}));
|
|
const service = new TestReverseEngineeringService({ run });
|
|
const logger = pino({ enabled: false });
|
|
registerGdbTools(
|
|
server,
|
|
manager,
|
|
logger,
|
|
delivery,
|
|
retain ? record : undefined,
|
|
undefined,
|
|
retain ? lookup : undefined,
|
|
);
|
|
registerReverseEngineeringTools(
|
|
server,
|
|
service,
|
|
logger,
|
|
delivery,
|
|
retain ? record : undefined,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
retain ? lookup : undefined,
|
|
);
|
|
registerAnalysisViewTool(
|
|
server,
|
|
logger,
|
|
retain ? lookup : undefined,
|
|
retain ? record : undefined,
|
|
);
|
|
const client = new Client({ name: "command-view-client", version: "1" });
|
|
onTestFinished(async () => {
|
|
await client.close();
|
|
await server.close();
|
|
await service.close();
|
|
});
|
|
const [ct, st] = InMemoryTransport.createLinkedPair();
|
|
await server.connect(st);
|
|
await client.connect(ct);
|
|
return { client, ledger, execute, run };
|
|
};
|
|
|
|
it("delivers ordered GDB records once and queries their retained parent after close through advertised SDK schemas", async () => {
|
|
const { client, ledger } = await connect();
|
|
const schemas = await advertisedMcpToolSchemas(client, "gdb_console");
|
|
const viewSchemas = await advertisedMcpToolSchemas(
|
|
client,
|
|
"inspect_analysis_view",
|
|
);
|
|
const input = { session_id: sessionId, command: capture.command };
|
|
const full = await client.callTool({ name: "gdb_console", arguments: input });
|
|
const summary = await client.callTool({
|
|
name: "gdb_console",
|
|
arguments: { ...input, detail: "summary" },
|
|
});
|
|
for (const reply of [full, summary]) {
|
|
expect(reply.isError).not.toBe(true);
|
|
expect(
|
|
schemas.ajv.validate(schemas.outputSchema, reply.structuredContent),
|
|
JSON.stringify(schemas.ajv.errors),
|
|
).toBe(true);
|
|
}
|
|
expect(parseEvidence(full.structuredContent)).toEqual(gdbEvidence());
|
|
const selected = parseEvidence(summary.structuredContent);
|
|
expect(selected.raw_result).toBeNull();
|
|
expect(selected.subject).toBeNull();
|
|
expect(selected.normalized_result).toMatchObject({
|
|
parent_evidence_id: gdbEvidence().evidence_id,
|
|
artifact: null,
|
|
source_provider: gdbEvidence().provider,
|
|
source_parameters: gdbEvidence().parameters,
|
|
summary: {
|
|
records: capture.records,
|
|
mi: capture.mi,
|
|
completion_status: "unknown",
|
|
output_truncated: true,
|
|
output_invalid_utf8: true,
|
|
process_exit_code: null,
|
|
available_counts: {
|
|
records: 4,
|
|
console_utf16_units: capture.console.length,
|
|
target_utf16_units: 0,
|
|
log_utf16_units: 0,
|
|
},
|
|
},
|
|
});
|
|
expect(selected.normalized_result).not.toHaveProperty("summary.console");
|
|
expect(selected.normalized_result).not.toHaveProperty("summary.target");
|
|
expect(selected.normalized_result).not.toHaveProperty("summary.log");
|
|
await client.callTool({
|
|
name: "close_gdb_session",
|
|
arguments: { session_id: sessionId },
|
|
});
|
|
const queried = await client.callTool({
|
|
name: "inspect_analysis_view",
|
|
arguments: {
|
|
source: {
|
|
kind: "retained-evidence",
|
|
evidence_id: gdbEvidence().evidence_id,
|
|
},
|
|
view: { kind: "summary" },
|
|
},
|
|
});
|
|
expect(
|
|
viewSchemas.ajv.validate(
|
|
viewSchemas.outputSchema,
|
|
queried.structuredContent,
|
|
),
|
|
JSON.stringify(viewSchemas.ajv.errors),
|
|
).toBe(true);
|
|
expect(parseEvidence(queried.structuredContent)).toEqual(selected);
|
|
expect(ledger.get(gdbEvidence().evidence_id)).toEqual(gdbEvidence());
|
|
});
|
|
|
|
it("keeps pathless objdump observations subjectless and preserves the default complete capture", async () => {
|
|
const { client, ledger } = await connect();
|
|
const schemas = await advertisedMcpToolSchemas(
|
|
client,
|
|
"inspect_with_objdump",
|
|
);
|
|
expect(
|
|
schemas.ajv.validate(schemas.inputSchema, {
|
|
operation: "architectures",
|
|
detail: "summary",
|
|
}),
|
|
).toBe(true);
|
|
const full = await client.callTool({
|
|
name: "inspect_with_objdump",
|
|
arguments: { operation: "architectures" },
|
|
});
|
|
const selected = await client.callTool({
|
|
name: "inspect_with_objdump",
|
|
arguments: { operation: "architectures", detail: "summary" },
|
|
});
|
|
for (const reply of [full, selected])
|
|
expect(
|
|
schemas.ajv.validate(schemas.outputSchema, reply.structuredContent),
|
|
JSON.stringify(schemas.ajv.errors),
|
|
).toBe(true);
|
|
const parent = parseEvidence(full.structuredContent);
|
|
const summary = parseEvidence(selected.structuredContent);
|
|
expect(summary.normalized_result).toMatchObject({
|
|
parent_evidence_id: parent.evidence_id,
|
|
artifact: null,
|
|
source_parameters: { operation: "architectures" },
|
|
summary: parent.normalized_result,
|
|
});
|
|
expect(summary.raw_result).toBeNull();
|
|
expect(summary.subject).toBeNull();
|
|
expect(ledger.get(parent.evidence_id)).toEqual(parent);
|
|
});
|
|
|
|
it("rejects summary without retention before executing either native command", async () => {
|
|
const { client, execute, run } = await connect(false);
|
|
for (const input of [
|
|
{
|
|
name: "gdb_console",
|
|
arguments: {
|
|
session_id: sessionId,
|
|
command: capture.command,
|
|
detail: "summary",
|
|
},
|
|
},
|
|
{
|
|
name: "inspect_with_objdump",
|
|
arguments: { operation: "architectures", detail: "summary" },
|
|
},
|
|
])
|
|
expect((await client.callTool(input)).isError).toBe(true);
|
|
expect(execute).not.toHaveBeenCalled();
|
|
expect(run).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("rejects malformed historical GDB facts and conflicting source commands", () => {
|
|
const source = gdbEvidence({
|
|
session_id: sessionId,
|
|
command: "a different command",
|
|
});
|
|
expect(
|
|
inspectAnalysisView({
|
|
source: { kind: "inline", evidence: source },
|
|
view: { kind: "summary" },
|
|
}).ok,
|
|
).toBe(false);
|
|
const malformed = createEvidence(undefined, source.provider, {
|
|
operation: "gdb_console",
|
|
parameters: {},
|
|
result: { ...capture, records: [123] },
|
|
});
|
|
expect(
|
|
inspectAnalysisView({
|
|
source: { kind: "inline", evidence: malformed },
|
|
view: { kind: "summary" },
|
|
}).ok,
|
|
).toBe(false);
|
|
});
|
|
|
|
it.each([true, false, null])(
|
|
"preserves opaque objdump capture and unknown/change facts (%s)",
|
|
(changed) => {
|
|
const report = {
|
|
stdout: "opaque output 🧪\n",
|
|
stderr: "diagnostic",
|
|
exit_code: "ETIMEDOUT",
|
|
signal: "SIGTERM",
|
|
output_truncated: true,
|
|
completion_status: "unknown",
|
|
artifact_sha256_after:
|
|
changed === null ? null : (changed ? "b" : "a").repeat(64),
|
|
artifact_changed: changed,
|
|
};
|
|
const parent = createEvidence(
|
|
{
|
|
path: "/artifacts/input",
|
|
sha256: "a".repeat(64),
|
|
format: "elf",
|
|
architecture: "x86_64",
|
|
},
|
|
{ id: "gnu.objdump", name: "GNU objdump", version: null },
|
|
{
|
|
operation: "inspect_with_objdump",
|
|
parameters: { path: "/artifacts/input", operation: "symbols" },
|
|
result: report,
|
|
rawResult: report,
|
|
},
|
|
);
|
|
const selected = inspectAnalysisView({
|
|
source: { kind: "inline", evidence: parent },
|
|
view: { kind: "summary" },
|
|
});
|
|
expect(selected.ok).toBe(true);
|
|
if (!selected.ok) throw selected.error;
|
|
expect(selected.value.normalized_result).toMatchObject({ summary: report });
|
|
if (changed !== null) {
|
|
const conflicting = createEvidence(
|
|
{
|
|
path: "/artifacts/input",
|
|
sha256: "a".repeat(64),
|
|
format: "elf",
|
|
architecture: "x86_64",
|
|
},
|
|
parent.provider,
|
|
{
|
|
operation: parent.operation,
|
|
parameters: parent.parameters,
|
|
result: { ...report, artifact_changed: !changed },
|
|
},
|
|
);
|
|
expect(
|
|
inspectAnalysisView({
|
|
source: { kind: "inline", evidence: conflicting },
|
|
view: { kind: "summary" },
|
|
}).ok,
|
|
).toBe(false);
|
|
}
|
|
},
|
|
);
|