fix: preserve selected paths when reusing evidence

This commit is contained in:
morluto committed 2026-11-04 12:27:59 +08:00
1 parent 675d3367d1
commit 6fd318a99a
2 files changed
+86 -3

No files matched your search

+15 -2
View File
@@ -1,3 +1,4 @@
import { basename } from "node:path";
import type { AnalysisExecution } from "../AnalysisProvider.js";
import type { Evidence } from "../../domain/evidence.js";
import type { JsonValue } from "../../domain/jsonValue.js";
@@ -12,8 +13,20 @@ export const bindSessionEvidenceExecution = (
evidence: Evidence,
): AnalysisExecution => {
if (
execution.sourceEvidence !== undefined &&
execution.sourceEvidence.length > 0
(execution.sourceEvidence !== undefined &&
execution.sourceEvidence.length > 0) ||
evidence.caller_input_path !== undefined
)
return execution;
// Semantic IDs intentionally omit display paths and names. A duplicate in
// the ledger can therefore refer to the same bytes at a different path.
const selected = execution.subject;
if (
selected == null
? evidence.subject !== null
: evidence.subject === null ||
evidence.subject.local_path !== selected.path ||
evidence.subject.name !== (basename(selected.path) || "artifact")
)
return execution;
const subject =
@@ -5,8 +5,14 @@ import { expect, it } from "vitest";
import { createAnalysisExecution } from "../../../src/application/AnalysisProvider.js";
import { BinarySession } from "../../../src/application/binary/BinarySession.js";
import { createAnalysisProfile } from "../../../src/domain/analysisProfile.js";
import { createEvidence, type Evidence } from "../../../src/domain/evidence.js";
import {
createEvidence,
parseEvidence,
withEvidenceCallerInputPath,
type Evidence,
} from "../../../src/domain/evidence.js";
import { ok } from "../../../src/domain/result.js";
import { createImmutableEvidenceBundle } from "../../../src/domain/evidenceBundle.js";
import { toolContract } from "../../../src/contracts/toolContracts.js";
import { silentLogger } from "../../../src/logger.js";
import { EvidenceMcpServer } from "../../../src/server/EvidenceMcpServer.js";
@@ -14,6 +20,7 @@ import {
withAdmittedAnalysis,
type WithAdmittedAnalysis,
} from "../../../src/server/analysisAdmission.js";
import { createServer } from "../../../src/server/createServer.js";
import { registerEvidenceTools } from "../../../src/server/registerEvidenceTools.js";
import { createToolResultDelivery } from "../../../src/server/toolResult.js";
import { createTestProviderRouter } from "../../fixtures/binarySession.js";
@@ -140,3 +147,66 @@ it.each(["normal", "copied", "parameters", "forged", "source"] as const)(
}
},
);
it("preserves the selected path when reusing an identical observation for a different file", async () => {
const directory = await createTestTempDirectory("rea-reused-subject-");
const first = join(directory, "first.com");
const second = join(directory, "second.com");
const bytes = Buffer.from([0xb8, 0, 0x4c, 0xcd, 0x21]);
await writeFile(first, bytes);
await writeFile(second, bytes);
const provider = {
id: "fixture",
name: "Fixture analysis provider",
version: "1",
};
const router = createTestProviderRouter(
() => ({
execute: async () => ok(createAnalysisExecution(["0x200"], provider)),
close: async () => ok(null),
}),
{
resolveAnalysisProfile: async () =>
ok({ profile: createAnalysisProfile(provider, {}) }),
},
);
const session = new BinarySession(router);
const server = createServer({ kind: "session", session });
const client = new Client({ name: "reused-subject", version: "1" });
const [ct, st] = InMemoryTransport.createLinkedPair();
try {
await server.connect(st);
await client.connect(ct);
const call = async (name: string, args: Record<string, unknown>) => {
const r = await client.callTool({ name, arguments: args });
expect(r.isError).not.toBe(true);
return r.structuredContent;
};
await call("open_binary", { path: first, format: "dos-com" });
const before = await call("procedure_callees", { procedure: "0x100" });
await call("open_binary", { path: second, format: "dos-com" });
const after = await call("procedure_callees", { procedure: "0x100" });
expect(before).toMatchObject({
subject: { name: "first.com", local_path: first },
});
expect(after).toMatchObject({
subject: { name: "second.com", local_path: second },
});
expect(parseEvidence(after).evidence_id).toEqual(
parseEvidence(before).evidence_id,
);
expect(
session.importEvidenceBundle(
createImmutableEvidenceBundle([
withEvidenceCallerInputPath(parseEvidence(after), "earlier-input"),
]),
).ok,
).toBe(true);
const sameTarget = await call("procedure_callees", { procedure: "0x100" });
expect(sameTarget).not.toHaveProperty("caller_input_path");
} finally {
await client.close();
await server.close();
await session.close();
}
});