mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
fix(snapshot): preserve authenticated replay provenance
This commit is contained in:
1 parent
f4987281fa
commit
0c147a4a70
4 files changed
+467
-17
No files matched your search
@@ -9,7 +9,10 @@ import {
|
||||
import { NoBinaryOpenError } from "../../domain/configurationErrors.js";
|
||||
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import type { AnalysisInputError } from "../../domain/analysisErrorCore.js";
|
||||
import { rebindEvidenceSubjectTargetPath } from "../../domain/evidence.js";
|
||||
import {
|
||||
rebindEvidenceSubjectTargetPath,
|
||||
type Evidence,
|
||||
} from "../../domain/evidence.js";
|
||||
import { err, ok, type Result } from "../../domain/result.js";
|
||||
import {
|
||||
analysisQueryId,
|
||||
@@ -38,6 +41,10 @@ import { OFFICIAL_TOOL_CONTRACTS } from "../../contracts/officialToolContracts.j
|
||||
import { compareUnicodeCodePoints } from "../../domain/unicodeCodePointOrder.js";
|
||||
|
||||
import type { EvidenceMetadataReplacement } from "../investigation/EvidenceLedger.js";
|
||||
import {
|
||||
snapshotReplayBinding,
|
||||
type SnapshotReplayBinding,
|
||||
} from "./AnalysisSnapshotReplayBinding.js";
|
||||
|
||||
const RELATIONSHIP_OPERATIONS: ReadonlySet<AnalysisOperation> = new Set([
|
||||
"procedure_callers",
|
||||
@@ -96,7 +103,9 @@ export const requiresNativeSourceAdmissionBeforeReplay = (
|
||||
/** Bounded in-memory cache for one immutable binary identity. */
|
||||
export class AnalysisSnapshotCache {
|
||||
readonly #entries = new Map<string, AnalysisSnapshotEntry>();
|
||||
readonly #evidenceIds = new Map<string, string>();
|
||||
// Null means imported provenance cannot be faithfully replayed. It is distinct
|
||||
// from an entry recorded before an Evidence ID was assigned.
|
||||
readonly #replayBindings = new Map<string, SnapshotReplayBinding | null>();
|
||||
readonly #workflowEntries = new Map<string, AnalysisSnapshotWorkflowEntry>();
|
||||
#target: AnalysisSnapshotTarget | undefined;
|
||||
#binding: AnalysisSnapshotBinding | undefined;
|
||||
@@ -145,11 +154,17 @@ export class AnalysisSnapshotCache {
|
||||
if (!this.#entries.has(entry.query_id)) imported += 1;
|
||||
this.#entries.set(entry.query_id, entry);
|
||||
stagedQueryIds.add(entry.query_id);
|
||||
this.#evidenceIds.delete(entry.query_id);
|
||||
this.#replayBindings.delete(entry.query_id);
|
||||
}
|
||||
// Snapshot validation authenticated these records. Recover the original
|
||||
// identity before lookup adds replay caveats; do not mint a new record.
|
||||
const evidenceIds = new Map<string, string>();
|
||||
const evidenceByQuery = new Map<string, Evidence>();
|
||||
const records = new Map(
|
||||
snapshot.evidence_bundle.records.map((record) => [
|
||||
record.evidence_id,
|
||||
record,
|
||||
]),
|
||||
);
|
||||
const ambiguous = new Set<string>();
|
||||
for (const evidence of snapshot.evidence_bundle.records) {
|
||||
const queryId = analysisQueryId(
|
||||
@@ -165,14 +180,21 @@ export class AnalysisSnapshotCache {
|
||||
!evidenceMatchesEntry(evidence, entry, snapshot)
|
||||
)
|
||||
continue;
|
||||
const previous = evidenceIds.get(queryId);
|
||||
if (previous !== undefined && previous !== evidence.evidence_id)
|
||||
const previous = evidenceByQuery.get(queryId);
|
||||
if (
|
||||
previous !== undefined &&
|
||||
previous.evidence_id !== evidence.evidence_id
|
||||
)
|
||||
ambiguous.add(queryId);
|
||||
else evidenceIds.set(queryId, evidence.evidence_id);
|
||||
else evidenceByQuery.set(queryId, evidence);
|
||||
}
|
||||
for (const [queryId, evidenceId] of evidenceIds) {
|
||||
if (ambiguous.has(queryId)) this.#evidenceIds.delete(queryId);
|
||||
else this.#evidenceIds.set(queryId, evidenceId);
|
||||
for (const [queryId, evidence] of evidenceByQuery) {
|
||||
this.#replayBindings.set(
|
||||
queryId,
|
||||
ambiguous.has(queryId)
|
||||
? null
|
||||
: snapshotReplayBinding(evidence, records),
|
||||
);
|
||||
}
|
||||
for (const entry of snapshot.workflow_entries) {
|
||||
if (!this.#workflowEntries.has(entry.query_id) && !this.#hasCapacity())
|
||||
@@ -187,7 +209,29 @@ export class AnalysisSnapshotCache {
|
||||
refreshEvidenceMetadata(
|
||||
replacements: readonly EvidenceMetadataReplacement[],
|
||||
): void {
|
||||
if (this.#target === undefined || this.#binding === undefined) return;
|
||||
if (
|
||||
replacements.length === 0 ||
|
||||
this.#target === undefined ||
|
||||
this.#binding === undefined
|
||||
)
|
||||
return;
|
||||
const currentRecords = new Map(
|
||||
replacements.map(({ current }) => [current.evidence_id, current]),
|
||||
);
|
||||
for (const [queryId, binding] of this.#replayBindings) {
|
||||
if (binding === null || binding.sourceEvidence === undefined) continue;
|
||||
const sources = binding.sourceEvidence;
|
||||
if (!sources.some((source) => currentRecords.has(source.evidence_id)))
|
||||
continue;
|
||||
this.#replayBindings.set(queryId, {
|
||||
evidenceId: binding.evidenceId,
|
||||
sourceEvidence: Object.freeze(
|
||||
sources.map(
|
||||
(source) => currentRecords.get(source.evidence_id) ?? source,
|
||||
),
|
||||
),
|
||||
});
|
||||
}
|
||||
const header = { target: this.#target, binding: this.#binding };
|
||||
for (const { previous, current } of replacements) {
|
||||
if (
|
||||
@@ -379,6 +423,10 @@ export class AnalysisSnapshotCache {
|
||||
);
|
||||
const cached = this.#entries.get(queryId);
|
||||
if (cached === undefined) return undefined;
|
||||
const binding = this.#replayBindings.get(queryId);
|
||||
// Do not turn a refused provenance selection into a fresh, unlinked record.
|
||||
// The provider must produce a new observation before this query can replay.
|
||||
if (binding === null) return undefined;
|
||||
const cachedSubject = cached.execution.subject;
|
||||
const subject =
|
||||
cachedSubject == null
|
||||
@@ -394,9 +442,8 @@ export class AnalysisSnapshotCache {
|
||||
},
|
||||
target,
|
||||
);
|
||||
const evidenceId = this.#evidenceIds.get(queryId);
|
||||
return structuredClone({
|
||||
...(evidenceId === undefined ? {} : { evidenceId }),
|
||||
const execution: AnalysisExecution = structuredClone({
|
||||
...(binding === undefined ? {} : { evidenceId: binding.evidenceId }),
|
||||
result: cached.execution.result,
|
||||
rawResult: cached.execution.raw_result,
|
||||
provider: cached.execution.provider,
|
||||
@@ -421,6 +468,9 @@ export class AnalysisSnapshotCache {
|
||||
locations: cached.execution.locations,
|
||||
subject: subject ?? null,
|
||||
});
|
||||
return binding?.sourceEvidence === undefined
|
||||
? execution
|
||||
: { ...execution, sourceEvidence: binding.sourceEvidence };
|
||||
}
|
||||
|
||||
/** Record one successful immutable call unless the cache is full. */
|
||||
@@ -468,8 +518,11 @@ export class AnalysisSnapshotCache {
|
||||
});
|
||||
this.#entries.set(entry.query_id, entry);
|
||||
if (execution.evidenceId === undefined)
|
||||
this.#evidenceIds.delete(entry.query_id);
|
||||
else this.#evidenceIds.set(entry.query_id, execution.evidenceId);
|
||||
this.#replayBindings.delete(entry.query_id);
|
||||
else
|
||||
this.#replayBindings.set(entry.query_id, {
|
||||
evidenceId: execution.evidenceId,
|
||||
});
|
||||
}
|
||||
|
||||
/** Forget all target-bound entries. */
|
||||
@@ -481,7 +534,7 @@ export class AnalysisSnapshotCache {
|
||||
|
||||
#clearEntries(): void {
|
||||
this.#entries.clear();
|
||||
this.#evidenceIds.clear();
|
||||
this.#replayBindings.clear();
|
||||
this.#workflowEntries.clear();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { Evidence } from "../../domain/evidence.js";
|
||||
|
||||
/** Authenticated observation identity and its immediate source records. */
|
||||
export interface SnapshotReplayBinding {
|
||||
readonly evidenceId: string;
|
||||
readonly sourceEvidence?: readonly Evidence[];
|
||||
}
|
||||
|
||||
/** Preserve direct links using the snapshot's authenticated immutable records. */
|
||||
export const snapshotReplayBinding = (
|
||||
evidence: Evidence,
|
||||
records: ReadonlyMap<string, Evidence>,
|
||||
): SnapshotReplayBinding | null => {
|
||||
if (evidence.evidence_links.length === 0)
|
||||
return { evidenceId: evidence.evidence_id };
|
||||
const sources: Evidence[] = [];
|
||||
for (const id of evidence.evidence_links) {
|
||||
const source = records.get(id);
|
||||
// An external link without its source cannot be represented by sourceEvidence.
|
||||
if (source === undefined) return null;
|
||||
sources.push(source);
|
||||
}
|
||||
return {
|
||||
evidenceId: evidence.evidence_id,
|
||||
sourceEvidence: Object.freeze(sources),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,134 @@
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { expect, it, onTestFinished } from "vitest";
|
||||
|
||||
import { readAnalysisSnapshot } from "../../../src/application/binary/AnalysisSnapshotFiles.js";
|
||||
import { createAnalysisProfile } from "../../../src/domain/analysisProfile.js";
|
||||
import { createEvidence } from "../../../src/domain/evidence.js";
|
||||
import { createEvidenceBundle } from "../../../src/domain/evidenceBundle.js";
|
||||
import { MANAGED_STATIC_PROVIDER } from "../../../src/domain/investigationProviders.js";
|
||||
import { ok } from "../../../src/domain/result.js";
|
||||
import { ManagedStaticProvider } from "../../../src/dotnet/ManagedStaticProvider.js";
|
||||
import { buildManagedPeFixture } from "../../../src/dotnet/ManagedPe.fixture.js";
|
||||
import { createTestBinarySession } from "../../fixtures/binarySession.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
it("reruns ambiguous imported observations without changing their historical Evidence", async () => {
|
||||
const directory = await createTestTempDirectory("rea-snapshot-provenance-");
|
||||
const path = join(directory, "application.exe");
|
||||
const originalPath = join(directory, "original.json");
|
||||
const ambiguousPath = join(directory, "ambiguous.json");
|
||||
const bytes = buildManagedPeFixture();
|
||||
const changedBytes = buildManagedPeFixture({ methodName: "Changed" });
|
||||
await writeFile(path, bytes);
|
||||
|
||||
const provider = new ManagedStaticProvider();
|
||||
// Select a concrete profile for the real static reader at the session boundary.
|
||||
// Production managed composition normally uses it as a profileless auxiliary.
|
||||
const profile = createAnalysisProfile(MANAGED_STATIC_PROVIDER, {
|
||||
fixture: "snapshot-provenance",
|
||||
});
|
||||
const session = createTestBinarySession(provider, {
|
||||
resolveAnalysisProfile: () => Promise.resolve(ok({ profile })),
|
||||
});
|
||||
onTestFinished(async () => {
|
||||
const closed = await session.close();
|
||||
if (!closed.ok) throw closed.error;
|
||||
});
|
||||
const opened = await session.open(path);
|
||||
if (!opened.ok) throw opened.error;
|
||||
const observed = await session.execute("inspect_managed_artifact", {});
|
||||
if (!observed.ok) throw observed.error;
|
||||
const members = await session.execute("inspect_managed_members", {});
|
||||
if (!members.ok) throw members.error;
|
||||
if (
|
||||
observed.value.evidenceId === undefined ||
|
||||
members.value.evidenceId === undefined
|
||||
)
|
||||
throw new Error("Real managed observations have no Evidence identity");
|
||||
const original = session.evidenceById(observed.value.evidenceId);
|
||||
const source = session.evidenceById(members.value.evidenceId);
|
||||
if (original === undefined || source === undefined)
|
||||
throw new Error("Real managed observations were not retained");
|
||||
const saved = session.exportAnalysisSnapshot();
|
||||
if (!saved.ok) throw saved.error;
|
||||
const written = await session.writeAnalysisSnapshot(
|
||||
saved.value,
|
||||
originalPath,
|
||||
false,
|
||||
);
|
||||
if (!written.ok) throw written.error;
|
||||
|
||||
// Evidence import admits a second authenticated observation of the same facts
|
||||
// with different upstream provenance. Neither ID identifies the cache entry alone.
|
||||
const alternative = createEvidence(opened.value, provider.identity(), {
|
||||
operation: original.operation,
|
||||
parameters: original.parameters,
|
||||
result: original.normalized_result,
|
||||
rawResult: original.raw_result,
|
||||
analysisProfile: profile,
|
||||
limitations: original.limitations,
|
||||
locations: original.locations,
|
||||
evidenceLinks: [source.evidence_id],
|
||||
});
|
||||
expect(alternative.evidence_id).not.toBe(original.evidence_id);
|
||||
const imported = session.importEvidenceBundle(
|
||||
createEvidenceBundle([source, alternative]),
|
||||
);
|
||||
if (!imported.ok) throw imported.error;
|
||||
const closed = await session.closeWithSnapshot(ambiguousPath, false);
|
||||
if (!closed.ok) throw closed.error;
|
||||
const originalSnapshot = await readAnalysisSnapshot(originalPath);
|
||||
const ambiguousSnapshot = await readAnalysisSnapshot(ambiguousPath);
|
||||
if (!originalSnapshot.ok) throw originalSnapshot.error;
|
||||
if (!ambiguousSnapshot.ok) throw ambiguousSnapshot.error;
|
||||
const historicalBytes = await readFile(ambiguousPath);
|
||||
const historicalEvidence = ambiguousSnapshot.value.evidence_bundle;
|
||||
|
||||
// An unambiguous recorded snapshot remains immutable: replay does not promise
|
||||
// to re-read bytes after open admitted the selected target.
|
||||
const control = await session.open(path, {
|
||||
snapshot: originalSnapshot.value,
|
||||
});
|
||||
if (!control.ok) throw control.error;
|
||||
await writeFile(path, changedBytes);
|
||||
const replayed = await session.execute("inspect_managed_artifact", {});
|
||||
if (!replayed.ok) throw replayed.error;
|
||||
expect(replayed.value.result).toEqual(observed.value.result);
|
||||
expect(replayed.value.limitations).toContain(
|
||||
"Loaded from a local REA analysis snapshot; this call did not re-run the provider.",
|
||||
);
|
||||
const controlClosed = await session.close();
|
||||
if (!controlClosed.ok) throw controlClosed.error;
|
||||
|
||||
await writeFile(path, bytes);
|
||||
const reopened = await session.open(path, {
|
||||
snapshot: ambiguousSnapshot.value,
|
||||
});
|
||||
if (!reopened.ok) throw reopened.error;
|
||||
await writeFile(path, changedBytes);
|
||||
const ambiguous = await session.execute("inspect_managed_artifact", {});
|
||||
expect(ambiguous).toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
_tag: "AnalysisArtifactChangedError",
|
||||
operation: "inspect_managed_artifact",
|
||||
},
|
||||
});
|
||||
expect(session.exportEvidenceBundle()).toEqual(historicalEvidence);
|
||||
expect((await readFile(ambiguousPath)).equals(historicalBytes)).toBe(true);
|
||||
|
||||
// Once fresh source bytes are available again, the real reader can observe
|
||||
// them while the imported records and their upstream links remain retained.
|
||||
await writeFile(path, bytes);
|
||||
const fresh = await session.execute("inspect_managed_artifact", {});
|
||||
if (!fresh.ok) throw fresh.error;
|
||||
expect(fresh.value.result).toEqual(observed.value.result);
|
||||
expect(fresh.value.limitations).not.toContain(
|
||||
"Loaded from a local REA analysis snapshot; this call did not re-run the provider.",
|
||||
);
|
||||
expect(session.evidenceById(original.evidence_id)).toEqual(original);
|
||||
expect(session.evidenceById(alternative.evidence_id)).toEqual(alternative);
|
||||
expect((await readFile(ambiguousPath)).equals(historicalBytes)).toBe(true);
|
||||
});
|
||||
@@ -0,0 +1,236 @@
|
||||
import { readFile, unlink, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { expect, onTestFinished } from "vitest";
|
||||
|
||||
import { readAnalysisSnapshot } from "../../../src/application/binary/AnalysisSnapshotFiles.js";
|
||||
import { toolContract } from "../../../src/contracts/toolContracts.js";
|
||||
import { createAnalysisProfile } from "../../../src/domain/analysisProfile.js";
|
||||
import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js";
|
||||
import { createEvidenceBundle } from "../../../src/domain/evidenceBundle.js";
|
||||
import { MANAGED_STATIC_PROVIDER } from "../../../src/domain/investigationProviders.js";
|
||||
import { ok } from "../../../src/domain/result.js";
|
||||
import { ManagedStaticProvider } from "../../../src/dotnet/ManagedStaticProvider.js";
|
||||
import { buildManagedPeFixture } from "../../../src/dotnet/ManagedPe.fixture.js";
|
||||
import { silentLogger } from "../../../src/logger.js";
|
||||
import { EvidenceMcpServer } from "../../../src/server/EvidenceMcpServer.js";
|
||||
import { withAdmittedAnalysis } from "../../../src/server/analysisAdmission.js";
|
||||
import { registerEvidenceTools } from "../../../src/server/registerEvidenceTools.js";
|
||||
import { withoutDeliveryDetail } from "../../../src/server/retainedSummaryDelivery.js";
|
||||
import { createToolResultDelivery } from "../../../src/server/toolResult.js";
|
||||
import { createTestBinarySession } from "../../fixtures/binarySession.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
|
||||
import { mcpTest } from "../../support/mcp/mcpFixture.js";
|
||||
|
||||
const createSnapshotReplayFixture = async (missingSource: boolean) => {
|
||||
const directory = await createTestTempDirectory("rea-snapshot-source-mcp-");
|
||||
const path = join(directory, "application.exe");
|
||||
const snapshotPath = join(directory, "historical.json");
|
||||
await writeFile(path, buildManagedPeFixture());
|
||||
const provider = new ManagedStaticProvider();
|
||||
const profile = createAnalysisProfile(MANAGED_STATIC_PROVIDER, {
|
||||
fixture: "snapshot-source-delivery",
|
||||
});
|
||||
// A concrete profile selects the real static reader for this session-boundary
|
||||
// integration. Normal managed composition uses a profileless auxiliary route.
|
||||
const session = createTestBinarySession(provider, {
|
||||
resolveAnalysisProfile: () => Promise.resolve(ok({ profile })),
|
||||
});
|
||||
onTestFinished(async () => {
|
||||
const closed = await session.close();
|
||||
if (!closed.ok) throw closed.error;
|
||||
});
|
||||
const opened = await session.open(path);
|
||||
if (!opened.ok) throw opened.error;
|
||||
const facts = [];
|
||||
for (const operation of [
|
||||
"inspect_managed_members",
|
||||
"inspect_managed_native_boundaries",
|
||||
"inspect_managed_artifact",
|
||||
] as const) {
|
||||
const execution = await session.execute(operation, {});
|
||||
if (!execution.ok) throw execution.error;
|
||||
const evidenceId = execution.value.evidenceId;
|
||||
const evidence =
|
||||
evidenceId === undefined ? undefined : session.evidenceById(evidenceId);
|
||||
if (evidence === undefined)
|
||||
throw new Error("Real managed observation was not retained");
|
||||
facts.push(evidence);
|
||||
}
|
||||
const [ancestor, sourceFacts, parentFacts] = facts;
|
||||
if (
|
||||
ancestor === undefined ||
|
||||
sourceFacts === undefined ||
|
||||
parentFacts === undefined
|
||||
)
|
||||
throw new Error("Expected three managed observations");
|
||||
const linked = (evidence: typeof parentFacts, sourceId: string) =>
|
||||
createEvidence(opened.value, provider.identity(), {
|
||||
operation: evidence.operation,
|
||||
parameters: evidence.parameters,
|
||||
result: evidence.normalized_result,
|
||||
rawResult: evidence.raw_result,
|
||||
analysisProfile: profile,
|
||||
limitations: evidence.limitations,
|
||||
locations: evidence.locations,
|
||||
evidenceLinks: [sourceId],
|
||||
});
|
||||
const source = linked(sourceFacts, ancestor.evidence_id);
|
||||
const parent = linked(parentFacts, source.evidence_id);
|
||||
const exported = session.exportAnalysisSnapshot();
|
||||
if (!exported.ok) throw exported.error;
|
||||
// Persist a valid historical snapshot whose primitive entries have source
|
||||
// links. Current record() excludes these because entries alone omit sources.
|
||||
const written = await session.writeAnalysisSnapshot(
|
||||
{
|
||||
...exported.value,
|
||||
entries: missingSource
|
||||
? exported.value.entries.filter(
|
||||
(entry) => entry.operation !== source.operation,
|
||||
)
|
||||
: exported.value.entries,
|
||||
evidence_bundle: createEvidenceBundle(
|
||||
missingSource ? [ancestor, parent] : [ancestor, source, parent],
|
||||
),
|
||||
},
|
||||
snapshotPath,
|
||||
false,
|
||||
);
|
||||
if (!written.ok) throw written.error;
|
||||
const closed = await session.close();
|
||||
if (!closed.ok) throw closed.error;
|
||||
const loaded = await readAnalysisSnapshot(snapshotPath);
|
||||
if (!loaded.ok) throw loaded.error;
|
||||
const historicalBytes = await readFile(snapshotPath);
|
||||
const reopened = await session.open(path, { snapshot: loaded.value });
|
||||
if (!reopened.ok) throw reopened.error;
|
||||
await unlink(path);
|
||||
return {
|
||||
directory,
|
||||
snapshotPath,
|
||||
session,
|
||||
ancestor,
|
||||
source,
|
||||
parent,
|
||||
historicalBytes,
|
||||
};
|
||||
};
|
||||
|
||||
for (const missingSource of [false, true])
|
||||
mcpTest(
|
||||
missingSource
|
||||
? "declines snapshot replay when its immediate source record is unavailable"
|
||||
: "preserves immediate and transitive imported snapshot provenance through MCP Evidence delivery",
|
||||
async ({ mcp }) => {
|
||||
const {
|
||||
directory,
|
||||
snapshotPath,
|
||||
session,
|
||||
ancestor,
|
||||
source,
|
||||
parent,
|
||||
historicalBytes,
|
||||
} = await createSnapshotReplayFixture(missingSource);
|
||||
|
||||
// Exercise the same named-contract production adapter as the official tools,
|
||||
// directly on the session so managed's independent-service route cannot bypass replay.
|
||||
const server = new EvidenceMcpServer(
|
||||
{ name: "snapshot-source-delivery", version: "1" },
|
||||
{ capabilities: {} },
|
||||
session.recordEvidence.bind(session),
|
||||
createToolResultDelivery(undefined),
|
||||
);
|
||||
registerEvidenceTools(
|
||||
server,
|
||||
[toolContract("inspect_managed_artifact")],
|
||||
{
|
||||
logger: silentLogger,
|
||||
activeTarget: () => session.activeTarget(),
|
||||
recordEvidence: session.recordEvidence.bind(session),
|
||||
withAdmittedAnalysis: withAdmittedAnalysis({
|
||||
kind: "session",
|
||||
session,
|
||||
}),
|
||||
parametersForExecution: withoutDeliveryDetail(
|
||||
new Set(["inspect_managed_artifact"]),
|
||||
),
|
||||
parametersForEvidence: withoutDeliveryDetail(
|
||||
new Set(["inspect_managed_artifact"]),
|
||||
),
|
||||
},
|
||||
);
|
||||
const client = await mcp.connect(server);
|
||||
const response = await client.callTool({
|
||||
name: "inspect_managed_artifact",
|
||||
arguments: {},
|
||||
});
|
||||
if (missingSource) {
|
||||
expect(parseMcpToolError(response)).toMatchObject({
|
||||
error: { code: "target_unavailable" },
|
||||
});
|
||||
expect(session.evidenceById(parent.evidence_id)).toEqual(parent);
|
||||
expect(session.evidenceById(source.evidence_id)).toBeUndefined();
|
||||
expect(session.evidenceById(ancestor.evidence_id)).toEqual(ancestor);
|
||||
expect((await readFile(snapshotPath)).equals(historicalBytes)).toBe(
|
||||
true,
|
||||
);
|
||||
return;
|
||||
}
|
||||
expect(response.isError, JSON.stringify(response.content)).not.toBe(true);
|
||||
const replayed = parseEvidence(response.structuredContent);
|
||||
expect(replayed.normalized_result).toEqual(parent.normalized_result);
|
||||
expect(replayed.evidence_links).toEqual([source.evidence_id]);
|
||||
expect(replayed.evidence_links).not.toContain(ancestor.evidence_id);
|
||||
expect(session.evidenceById(replayed.evidence_id)).toEqual(replayed);
|
||||
expect(session.evidenceById(parent.evidence_id)).toEqual(parent);
|
||||
expect(session.evidenceById(source.evidence_id)).toEqual(source);
|
||||
expect(session.evidenceById(ancestor.evidence_id)).toEqual(ancestor);
|
||||
expect(session.evidenceById(source.evidence_id)?.evidence_links).toEqual([
|
||||
ancestor.evidence_id,
|
||||
]);
|
||||
const firstReplay = await session.execute("inspect_managed_artifact", {});
|
||||
if (!firstReplay.ok) throw firstReplay.error;
|
||||
expect(firstReplay.value.sourceEvidence?.[0]).toBe(
|
||||
session.evidenceForAnalysis(source.evidence_id),
|
||||
);
|
||||
expect(Object.isFrozen(firstReplay.value.sourceEvidence?.[0])).toBe(true);
|
||||
|
||||
if (source.subject === null)
|
||||
throw new Error("Expected a file-backed source");
|
||||
const relocated = {
|
||||
...source,
|
||||
subject: {
|
||||
...source.subject,
|
||||
local_path: join(directory, "relocated.exe"),
|
||||
},
|
||||
};
|
||||
const imported = session.importEvidenceBundle(
|
||||
createEvidenceBundle([relocated]),
|
||||
);
|
||||
if (!imported.ok) throw imported.error;
|
||||
const refreshed = await session.execute("inspect_managed_artifact", {});
|
||||
if (!refreshed.ok) throw refreshed.error;
|
||||
expect(refreshed.value.sourceEvidence?.[0]).toBe(
|
||||
session.evidenceForAnalysis(source.evidence_id),
|
||||
);
|
||||
expect(refreshed.value.sourceEvidence?.[0]).toEqual(relocated);
|
||||
expect(refreshed.value.sourceEvidence?.[0]?.evidence_links).toEqual([
|
||||
ancestor.evidence_id,
|
||||
]);
|
||||
expect(Object.isFrozen(refreshed.value.sourceEvidence?.[0])).toBe(true);
|
||||
const refreshedResponse = await client.callTool({
|
||||
name: "inspect_managed_artifact",
|
||||
arguments: {},
|
||||
});
|
||||
expect(
|
||||
refreshedResponse.isError,
|
||||
JSON.stringify(refreshedResponse.content),
|
||||
).not.toBe(true);
|
||||
expect(
|
||||
parseEvidence(refreshedResponse.structuredContent).evidence_links,
|
||||
).toEqual([source.evidence_id]);
|
||||
expect(session.evidenceById(source.evidence_id)).toEqual(relocated);
|
||||
expect((await readFile(snapshotPath)).equals(historicalBytes)).toBe(true);
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user