fix(snapshot): replay validated workflow evidence (#984)

* fix(snapshot): bind replay to validated cache entry

* fix(snapshot): persist composed workflow results

* fix(snapshot): enforce workflow replay identity

* fix(snapshot): honor cancellation after route resolution

* test(snapshot): make cancellation race deterministic

* fix(snapshot): replay all composed workflow evidence

* fix(snapshot): index workflow evidence once per snapshot

Validate workflow entries against a query and content index instead of scanning the evidence bundle for every entry.

* fix(snapshot): reuse previewed routes and bound workflow cache

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
morluto 2026-10-08 01:33:39 +08:00 committed by GitHub
parent 96a46ada0a
commit 1d62e54e1f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 1462 additions and 80 deletions

View File

@ -5,6 +5,8 @@ import { EnhancedTools } from "./EnhancedTools.js";
import { executeFunctionAnalysisEvidence } from "./FunctionAnalysisEvidence.js";
import type { DirectAnalysisDependencies } from "./DirectAnalysisDependencies.js";
import type { BinarySession } from "./binary/BinarySession.js";
import type { SessionProviderRoute } from "./binary/SessionProviderRouter.js";
import type { ResolvedSessionOpen } from "./binary/BinarySessionOpen.js";
import { silentLogger, type Logger } from "../logger.js";
import { createEvidence } from "../domain/evidence.js";
import type { Evidence } from "../domain/evidence.js";
@ -28,10 +30,17 @@ import {
snapshotEvidenceForQuery,
snapshotMatchesTarget,
} from "../domain/analysisSnapshot.js";
import type { AnalysisProfileCommitment } from "../domain/analysisProfile.js";
import {
analysisProfileSchema,
committedProviderSchema,
type AnalysisProfileCommitment,
} from "../domain/analysisProfile.js";
import { err, ok, type Result } from "../domain/result.js";
import type { AnalysisSnapshot } from "../domain/analysisSnapshot.js";
import type { AnalysisExecution } from "./AnalysisProvider.js";
import type {
AnalysisExecution,
ProviderIdentity,
} from "./AnalysisProvider.js";
import {
REA_WORKFLOW_PROVIDER,
workflowAnalysisProfile,
@ -194,7 +203,42 @@ const runAnalysis = async (
});
if (!prepared.ok) return cliError(prepared.error);
const { snapshot } = prepared.value;
const opened = await session.open(path, {
let resolvedTarget: ResolvedSessionOpen | undefined;
if (snapshot !== undefined && prepared.value.target !== undefined) {
const preview = await session.previewTarget(prepared.value.target, {
signal,
snapshot,
...(options.formatHint === undefined
? {}
: { formatHint: options.formatHint }),
...(options.providerId === undefined
? {}
: { providerId: options.providerId }),
});
if (!preview.ok) return cliError(preview.error);
resolvedTarget = preview.value;
const route = preview.value.route;
const bindingProfile = route.profile ?? undefined;
const evidenceProfile = analysisProfileForRoute(route, tool);
if (
bindingProfile !== undefined &&
evidenceProfile !== undefined &&
allowsSnapshotReplay(route, tool)
) {
const cached = snapshotEvidenceForQuery(snapshot, {
target: preview.value.target,
bindingProfile,
operation: tool,
parameters: arguments_,
provider: isWorkflowEvidenceTool(tool)
? REA_WORKFLOW_PROVIDER
: providerIdentityForRoute(route, tool),
evidenceProfile,
});
if (cached !== undefined) return cached;
}
}
const openOptions = {
signal,
...(options.formatHint === undefined
? {}
@ -203,26 +247,13 @@ const runAnalysis = async (
...(options.providerId === undefined
? {}
: { providerId: options.providerId }),
});
};
const opened =
resolvedTarget === undefined
? await session.open(path, openOptions)
: await session.openResolvedTarget(resolvedTarget, openOptions);
if (!opened.ok) return cliError(opened.error);
const evidenceProfile = analysisProfileForEvidence(session, tool);
const bindingProfile = session.analysisProfile();
if (
canReplayDirectTool(session, tool) &&
snapshot !== undefined &&
evidenceProfile !== undefined &&
bindingProfile !== undefined
) {
const cached = snapshotEvidenceForQuery(snapshot, {
target: opened.value,
bindingProfile,
operation: tool,
parameters: arguments_,
provider: replayProviderFor(session, tool),
evidenceProfile,
});
if (cached !== undefined) return cached;
}
const { output, evidence } = await executeAnalysisTool({
session,
openedTarget: opened.value,
@ -232,6 +263,41 @@ const runAnalysis = async (
evidenceProfile,
});
if (evidence !== undefined) session.recordEvidence(evidence);
if (
isWorkflowEvidenceTool(tool) &&
tool !== "trace_native_ui_action" &&
snapshotPath !== undefined &&
evidence !== undefined &&
"analysis_profile" in evidence &&
session.allowsSnapshotReplay(tool)
) {
const recorded = session.recordWorkflowSnapshot({
operation: tool,
parameters: arguments_,
execution: {
result: evidence.normalized_result,
rawResult: evidence.raw_result,
provider: committedProviderSchema.parse(evidence.provider),
analysisProfile: analysisProfileSchema.parse(
evidence.analysis_profile,
),
limitations: evidence.limitations,
locations: evidence.locations,
subject:
evidence.subject === null
? null
: {
path: evidence.subject.local_path,
sha256: evidence.subject.digest.sha256,
format: evidence.subject.format,
...(evidence.subject.architecture === null
? {}
: { architecture: evidence.subject.architecture }),
},
},
});
if (!recorded.ok) return cliError(recorded.error);
}
if (
tool !== "trace_native_ui_action" &&
snapshotPath !== undefined &&
@ -252,12 +318,6 @@ const runAnalysis = async (
}
};
const canReplayDirectTool = (
session: BinarySession,
tool: Parameters<typeof runAnalysis>[2],
): boolean =>
tool !== "trace_native_ui_action" && session.allowsSnapshotReplay(tool);
const executeAnalysisTool = async (input: {
readonly session: BinarySession;
readonly openedTarget: BinaryTarget;
@ -353,7 +413,10 @@ const prepareSnapshot = async (options: {
readonly formatHint?: ExecutableFormatHint;
readonly snapshotPath: string | undefined;
}): Promise<
Result<{ readonly snapshot?: AnalysisSnapshot }, AnalysisError>
Result<
{ readonly snapshot?: AnalysisSnapshot; readonly target?: BinaryTarget },
AnalysisError
>
> => {
const { path, snapshotPath } = options;
if (snapshotPath === undefined || !(await fileExists(snapshotPath)))
@ -374,20 +437,52 @@ const prepareSnapshot = async (options: {
"Analysis snapshot target does not match the requested binary",
),
);
return ok({ snapshot: loaded.value });
return ok({ snapshot: loaded.value, target: target.value });
};
const replayProviderFor = (
session: BinarySession,
const allowsSnapshotReplay = (
route: SessionProviderRoute,
tool:
| NativeToolName
| ArtifactAnalysisOperation
| ManagedToolName
| DirectAnalysisTool,
) =>
isWorkflowEvidenceTool(tool)
? REA_WORKFLOW_PROVIDER
: session.providerIdentity(tool);
): boolean => {
if (tool === "trace_native_ui_action") return false;
const descriptor = route.capabilities?.get(tool);
return descriptor === undefined
? ![...(route.capabilities?.values() ?? [])].some(
({ cachePolicy }) => cachePolicy === "live",
)
: descriptor.cachePolicy !== "live";
};
const analysisProfileForRoute = (
route: SessionProviderRoute,
tool:
| NativeToolName
| ArtifactAnalysisOperation
| ManagedToolName
| DirectAnalysisTool,
): AnalysisProfileCommitment | undefined => {
const profile = route.profile;
if (profile === null || profile === undefined) return undefined;
if (isWorkflowEvidenceTool(tool)) return workflowAnalysisProfile(profile);
const provider = providerIdentityForRoute(route, tool);
return provider.id === profile.provider.id ? profile : undefined;
};
/** Mirror BinarySession.providerIdentity for a route not opened yet. */
const providerIdentityForRoute = (
route: SessionProviderRoute,
operation: string,
): ProviderIdentity => {
const selected =
route.capabilities?.get(operation)?.provider ?? route.identity;
return route.profile?.provider.id === selected.id
? route.profile.provider
: selected;
};
const analysisProfileForEvidence = (
session: BinarySession,

View File

@ -10,15 +10,137 @@ import { createEvidence } from "../../domain/evidence.js";
import { createEvidenceBundle } from "../../domain/evidenceBundle.js";
import { ok } from "../../domain/result.js";
import { createAnalysisExecution } from "../AnalysisProvider.js";
import {
createAnalysisSnapshotWorkflowEntry,
parseAnalysisSnapshot,
snapshotBinding,
snapshotTarget,
} from "../../domain/analysisSnapshot.js";
import {
REA_WORKFLOW_PROVIDER,
workflowAnalysisProfile,
} from "../InvestigationProviders.js";
import type {
AnalysisOperation,
CapabilityDescriptor,
} from "../AnalysisProvider.js";
import {
AnalysisSnapshotCache,
SNAPSHOT_CACHE_ENTRY_CEILING,
isSnapshotCacheable,
} from "./AnalysisSnapshotCache.js";
describe("analysis snapshot cache capacity", () => {
it("shares capacity across query kinds while allowing replacements and fresh partitions", () => {
const cache = new AnalysisSnapshotCache();
const target = ANALYSIS_SNAPSHOT_TARGET;
const profile = ANALYSIS_SNAPSHOT_PROFILE;
const execution = {
...createAnalysisExecution("workflow", REA_WORKFLOW_PROVIDER, {
subject: target,
}),
analysisProfile: workflowAnalysisProfile(profile),
};
const workflowInput = {
target,
profile,
operation: "trace_feature",
parameters: { query: "existing" },
execution,
};
for (let index = 0; index < SNAPSHOT_CACHE_ENTRY_CEILING - 1; index += 1)
cache.recordWorkflow({
...workflowInput,
parameters: { query: String(index) },
});
// Staging and recording both spend the same budget; an existing query can
// still be refreshed when no space remains for a new binding.
const directInput = {
target,
profile,
operation: "address_name" as const,
parameters: { address: "0x1000", document: "fixture" },
execution: createAnalysisExecution(
"initial",
ANALYSIS_SNAPSHOT_PROVIDER,
{ subject: target, analysisProfile: profile },
),
};
cache.record(directInput);
cache.record({
...directInput,
parameters: { address: "0x2000", document: "fixture" },
});
cache.recordWorkflow(workflowInput);
expect(cache.entries()).toHaveLength(1);
expect(cache.workflowEntries()).toHaveLength(
SNAPSHOT_CACHE_ENTRY_CEILING - 1,
);
cache.record({
...directInput,
execution: { ...directInput.execution, result: "updated" },
});
cache.recordWorkflow({
...workflowInput,
parameters: { query: "0" },
execution: { ...execution, result: "updated" },
});
expect(
cache.lookup(target, profile, "address_name", directInput.parameters)
?.result,
).toBe("updated");
expect(
cache.workflowEntries().find(({ parameters }) => parameters.query === "0")
?.execution.result,
).toBe("updated");
const incoming = createAnalysisSnapshotWorkflowEntry({
...workflowInput,
target: snapshotTarget(target),
binding: snapshotBinding(profile),
});
const evidence = createEvidence(target, REA_WORKFLOW_PROVIDER, {
operation: workflowInput.operation,
parameters: workflowInput.parameters,
result: execution.result,
rawResult: execution.rawResult,
analysisProfile: execution.analysisProfile,
limitations: execution.limitations,
locations: execution.locations,
});
cache.stage(
parseAnalysisSnapshot({
target: snapshotTarget(target),
binding: snapshotBinding(profile),
entries: [],
workflow_entries: [incoming],
evidence_bundle: createEvidenceBundle([evidence]),
}),
);
expect(cache.workflowEntries()).toHaveLength(
SNAPSHOT_CACHE_ENTRY_CEILING - 1,
);
const otherTarget = { ...target, sha256: "b".repeat(64) };
cache.recordWorkflow({
...workflowInput,
target: otherTarget,
execution: { ...execution, subject: otherTarget },
});
expect(cache.entries()).toEqual([]);
expect(cache.workflowEntries()).toHaveLength(1);
cache.clear();
cache.record({
...directInput,
target: otherTarget,
execution: { ...directInput.execution, subject: otherTarget },
});
expect(cache.entries()).toHaveLength(1);
expect(cache.workflowEntries()).toEqual([]);
});
});
describe("analysis snapshot cache partitioning", () => {
it.each<{
operation: Exclude<AnalysisOperation, "health">;

View File

@ -9,6 +9,7 @@ import { err, ok, type Result } from "../../domain/result.js";
import {
analysisQueryId,
createAnalysisSnapshotEntry,
createAnalysisSnapshotWorkflowEntry,
snapshotBinding,
snapshotMatchesProfile,
snapshotMatchesTarget,
@ -16,6 +17,7 @@ import {
parseAnalysisSnapshot,
type AnalysisSnapshot,
type AnalysisSnapshotEntry,
type AnalysisSnapshotWorkflowEntry,
type AnalysisSnapshotBinding,
type AnalysisSnapshotTarget,
} from "../../domain/analysisSnapshot.js";
@ -54,6 +56,9 @@ const DOCUMENT_SCOPED_OPERATIONS: ReadonlySet<string> = new Set(
),
);
/** Maximum duplicated result bindings retained across direct and workflow queries. */
export const SNAPSHOT_CACHE_ENTRY_CEILING = 10_000;
/** Whether an operation is immutable and independent of provider UI state. */
export const isSnapshotCacheable = (
operation: AnalysisOperation,
@ -73,6 +78,7 @@ export const isSnapshotCacheable = (
/** Bounded in-memory cache for one immutable binary identity. */
export class AnalysisSnapshotCache {
readonly #entries = new Map<string, AnalysisSnapshotEntry>();
readonly #workflowEntries = new Map<string, AnalysisSnapshotWorkflowEntry>();
#target: AnalysisSnapshotTarget | undefined;
#binding: AnalysisSnapshotBinding | undefined;
@ -93,7 +99,7 @@ export class AnalysisSnapshotCache {
/** Replace cache state when the target or selected profile changes. */
select(target: BinaryTarget, profile: AnalysisProfileCommitment): void {
if (!this.matches(target, profile)) this.#entries.clear();
if (!this.matches(target, profile)) this.#clearEntries();
this.#target = snapshotTarget(target);
this.#binding = snapshotBinding(profile);
}
@ -109,14 +115,20 @@ export class AnalysisSnapshotCache {
snapshot.binding.analysis_profile,
))
)
this.#entries.clear();
this.#clearEntries();
this.#target = structuredClone(snapshot.target);
this.#binding = structuredClone(snapshot.binding);
let imported = 0;
for (const entry of snapshot.entries) {
if (!this.#entries.has(entry.query_id) && !this.#hasCapacity()) continue;
if (!this.#entries.has(entry.query_id)) imported += 1;
this.#entries.set(entry.query_id, structuredClone(entry));
}
for (const entry of snapshot.workflow_entries ?? []) {
if (!this.#workflowEntries.has(entry.query_id) && !this.#hasCapacity())
continue;
this.#workflowEntries.set(entry.query_id, structuredClone(entry));
}
return imported;
}
@ -137,6 +149,9 @@ export class AnalysisSnapshotCache {
target: snapshotTargetIdentity,
binding,
entries: this.entries(),
...(this.workflowEntries().length === 0
? {}
: { workflow_entries: this.workflowEntries() }),
evidence_bundle: retainedEvidence,
}),
);
@ -193,6 +208,47 @@ export class AnalysisSnapshotCache {
.map((entry) => structuredClone(entry));
}
/** Return canonical composed-workflow entries for persistence. */
workflowEntries(): AnalysisSnapshotWorkflowEntry[] {
return [...this.#workflowEntries.values()]
.sort((left, right) => left.query_id.localeCompare(right.query_id))
.map((entry) => structuredClone(entry));
}
/** Record one exact derived workflow result unless the shared cache is full. */
recordWorkflow(input: {
readonly target: BinaryTarget;
readonly profile: AnalysisProfileCommitment;
readonly operation: string;
readonly parameters: Readonly<Record<string, JsonValue>>;
readonly execution: {
readonly result: JsonValue;
readonly rawResult: JsonValue | null;
readonly provider: AnalysisExecution["provider"];
readonly analysisProfile: AnalysisProfileCommitment;
readonly limitations: readonly string[];
readonly locations: AnalysisExecution["locations"];
readonly subject: AnalysisExecution["subject"];
};
}): void {
this.select(input.target, input.profile);
const queryId = analysisQueryId(
snapshotTarget(input.target),
snapshotBinding(input.profile),
input.operation,
input.parameters,
);
if (!this.#workflowEntries.has(queryId) && !this.#hasCapacity()) return;
const entry = createAnalysisSnapshotWorkflowEntry({
target: snapshotTarget(input.target),
binding: snapshotBinding(input.profile),
operation: input.operation,
parameters: input.parameters,
execution: input.execution,
});
this.#workflowEntries.set(entry.query_id, entry);
}
/** Replay an exact provider-specific query, marking its cached provenance. */
lookup(
target: BinaryTarget,
@ -246,8 +302,14 @@ export class AnalysisSnapshotCache {
readonly execution: AnalysisExecution;
}): void {
const { target, profile, operation, parameters, execution } = input;
if (this.#entries.size >= 10_000) return;
this.select(target, profile);
const queryId = analysisQueryId(
snapshotTarget(target),
snapshotBinding(profile),
operation,
parameters,
);
if (!this.#entries.has(queryId) && !this.#hasCapacity()) return;
const entry = createAnalysisSnapshotEntry({
target: snapshotTarget(target),
binding: snapshotBinding(profile),
@ -260,8 +322,20 @@ export class AnalysisSnapshotCache {
/** Forget all target-bound entries. */
clear(): void {
this.#entries.clear();
this.#clearEntries();
this.#target = undefined;
this.#binding = undefined;
}
#clearEntries(): void {
this.#entries.clear();
this.#workflowEntries.clear();
}
#hasCapacity(): boolean {
return (
this.#entries.size + this.#workflowEntries.size <
SNAPSHOT_CACHE_ENTRY_CEILING
);
}
}

View File

@ -30,7 +30,10 @@ import { BinarySessionRecords } from "./BinarySessionRecords.js";
import { binarySessionStatus } from "./BinarySessionStatus.js";
import {
resolveSessionOpen,
resolveSessionTarget,
validateResolvedSessionTarget,
type BinarySessionOpenOptions,
type ResolvedSessionOpen,
} from "./BinarySessionOpen.js";
import {
bindExecutionTarget,
@ -150,6 +153,27 @@ export class BinarySession
return () => this.#availabilityListeners.delete(listener);
}
/** Resolve a target and provider profile without creating a provider client. */
previewTarget(
target: BinaryTarget,
options: BinarySessionOpenOptions = {},
): Promise<Result<ResolvedSessionOpen, AnalysisError>> {
if (isAborted(options.signal))
return Promise.resolve(err(new AnalysisCancelledError("open_binary")));
return resolveSessionTarget({
router: this.#providerRouter,
current: this.#active,
target,
options,
stagedSnapshotMatches: (target, profile) =>
this.matchesSnapshot(target, profile),
}).then((resolved) =>
isAborted(options.signal)
? err(new AnalysisCancelledError("open_binary"))
: resolved,
);
}
/**
* Open or switch targets after draining calls against the current target.
* Returns the switch failure even if best-effort restoration also fails.
@ -157,18 +181,49 @@ export class BinarySession
open(
path: string,
options: BinarySessionOpenOptions = {},
): Promise<Result<BinaryTarget, AnalysisError>> {
return this.#open(
() =>
resolveSessionOpen({
router: this.#providerRouter,
current: this.#active,
path,
options,
stagedSnapshotMatches: (target, profile) =>
this.matchesSnapshot(target, profile),
}),
options,
);
}
/** Open a previewed target without repeating parsing or provider profile discovery. */
openResolvedTarget(
resolved: Pick<ResolvedSessionOpen, "target" | "route">,
options: Pick<BinarySessionOpenOptions, "signal" | "snapshot"> = {},
): Promise<Result<BinaryTarget, AnalysisError>> {
return this.#open(
() =>
Promise.resolve(
validateResolvedSessionTarget({
...resolved,
current: this.#active,
options,
stagedSnapshotMatches: (target, profile) =>
this.matchesSnapshot(target, profile),
}),
),
options,
);
}
#open(
resolve: () => Promise<Result<ResolvedSessionOpen, AnalysisError>>,
options: Pick<BinarySessionOpenOptions, "signal" | "snapshot">,
): Promise<Result<BinaryTarget, AnalysisError>> {
return this.#serialize(async () => {
if (isAborted(options.signal))
return err(new AnalysisCancelledError("open_binary"));
const resolved = await resolveSessionOpen({
router: this.#providerRouter,
current: this.#active,
path,
options,
stagedSnapshotMatches: (target, profile) =>
this.matchesSnapshot(target, profile),
});
const resolved = await resolve();
if (!resolved.ok) return resolved;
const { target, route, sameTarget } = resolved.value;
const { profile, compatibility } = route;

View File

@ -46,11 +46,22 @@ interface ResolveSessionOpenInput {
) => boolean;
}
interface ResolveSessionTargetInput {
readonly router: SessionProviderRouter;
readonly current: CurrentOpenBinding | undefined;
readonly target: BinaryTarget;
readonly options: BinarySessionOpenOptions;
readonly stagedSnapshotMatches: (
target: BinaryTarget,
profile: AnalysisProfileCommitment | null,
) => boolean;
}
/** Parse a target, resolve its provider route, and validate snapshot binding. */
export const resolveSessionOpen = async (
input: ResolveSessionOpenInput,
): Promise<Result<ResolvedSessionOpen, AnalysisError>> => {
const { router, current, path, options, stagedSnapshotMatches } = input;
const { path, options } = input;
const parsed = await parseBinaryTarget(
path,
process.cwd(),
@ -59,7 +70,14 @@ export const resolveSessionOpen = async (
options.formatHint,
);
if (!parsed.ok) return parsed;
const target = parsed.value;
return resolveSessionTarget({ ...input, target: parsed.value });
};
/** Resolve a known target's provider route and validate snapshot binding. */
export const resolveSessionTarget = async (
input: ResolveSessionTargetInput,
): Promise<Result<ResolvedSessionOpen, AnalysisError>> => {
const { router, current, target, options } = input;
const sameTarget =
current?.target.path === target.path &&
snapshotMatchesTarget(snapshotTarget(current.target), target);
@ -68,7 +86,22 @@ export const resolveSessionOpen = async (
? ({ ok: true, value: current.route } as const)
: await router.resolve(target, options.providerId, options.signal);
if (!resolvedRoute.ok) return resolvedRoute;
const route = resolvedRoute.value;
return validateResolvedSessionTarget({
...input,
route: resolvedRoute.value,
});
};
/** Revalidate a previewed route against the current session and snapshot binding. */
export const validateResolvedSessionTarget = (
input: Omit<ResolveSessionTargetInput, "router"> & {
readonly route: SessionProviderRoute;
},
): Result<ResolvedSessionOpen, AnalysisError> => {
const { current, target, route, options, stagedSnapshotMatches } = input;
const sameTarget =
current?.target.path === target.path &&
snapshotMatchesTarget(snapshotTarget(current.target), target);
const snapshotError = validateSnapshot({
snapshot: options.snapshot,
target,

View File

@ -160,6 +160,43 @@ export abstract class BinarySessionRecords {
this.#emitSnapshotChanged();
}
/** Retain one derived workflow result alongside its provider cache entries. */
recordWorkflowSnapshot(input: {
readonly operation: string;
readonly parameters: Readonly<
Record<string, import("../../domain/jsonValue.js").JsonValue>
>;
readonly execution: Parameters<
AnalysisSnapshotCache["recordWorkflow"]
>[0]["execution"];
}): Result<null, EvidenceIntegrityError> {
const active = this.activeAnalysisBinding();
if (active === undefined || active.profile === null)
return err(
new EvidenceIntegrityError(
"Workflow snapshot entries require an active concrete provider profile",
),
);
try {
this.#snapshot.recordWorkflow({
target: active.target,
profile: active.profile,
...input,
});
this.#emitSnapshotChanged();
return ok(null);
} catch (cause: unknown) {
return err(
new EvidenceIntegrityError(
cause instanceof Error
? cause.message
: "Workflow snapshot entry validation failed",
{ cause },
),
);
}
}
protected invalidateSnapshot(): void {
this.#snapshot.clear();
this.#snapshotInvalidated = true;

View File

@ -17,6 +17,55 @@ import {
import { createEvidence } from "./evidence.js";
import { createEvidenceBundle } from "./evidenceBundle.js";
const snapshotWithHistoricalEvidence = () => {
const target = snapshotTarget(ANALYSIS_SNAPSHOT_TARGET);
const binding = snapshotBinding(ANALYSIS_SNAPSHOT_PROFILE);
const parameters = { procedure: "main" };
const makeEvidence = (result: string) =>
createEvidence(ANALYSIS_SNAPSHOT_TARGET, ANALYSIS_SNAPSHOT_PROVIDER, {
operation: "analyze_function",
parameters,
result: { summary: result },
analysisProfile: ANALYSIS_SNAPSHOT_PROFILE,
});
const current = makeEvidence("cached");
const entry = {
query_id: analysisQueryId(target, binding, "analyze_function", parameters),
operation: "analyze_function",
parameters,
execution: {
result: { summary: "cached" },
raw_result: null,
provider: binding.provider,
limitations: [],
locations: [],
subject: {
path: ANALYSIS_SNAPSHOT_TARGET.path,
sha256: ANALYSIS_SNAPSHOT_TARGET.sha256,
format: ANALYSIS_SNAPSHOT_TARGET.format,
architecture: ANALYSIS_SNAPSHOT_TARGET.architecture ?? null,
},
},
};
return {
current,
snapshot: {
target,
binding,
entries: [entry],
evidence_bundle: createEvidenceBundle([
makeEvidence("older"),
current,
createEvidence(ANALYSIS_SNAPSHOT_TARGET, ANALYSIS_SNAPSHOT_PROVIDER, {
operation: "legacy_query",
parameters: {},
result: { summary: "legacy" },
}),
]),
} satisfies AnalysisSnapshot,
};
};
describe("analysis snapshot contract", () => {
it("preserves DOS MZ target and subject formats with the x86 family", () => {
const target = {
@ -91,31 +140,7 @@ describe("analysis snapshot contract", () => {
describe("analysis snapshot Evidence binding", () => {
it("finds only Evidence committed to the exact binding and profile", () => {
const evidence = createEvidence(
ANALYSIS_SNAPSHOT_TARGET,
ANALYSIS_SNAPSHOT_PROVIDER,
{
operation: "analyze_function",
parameters: { procedure: "main" },
result: { summary: "cached" },
analysisProfile: ANALYSIS_SNAPSHOT_PROFILE,
},
);
const legacy = createEvidence(
ANALYSIS_SNAPSHOT_TARGET,
ANALYSIS_SNAPSHOT_PROVIDER,
{
operation: "legacy_query",
parameters: {},
result: { summary: "legacy" },
},
);
const snapshot: AnalysisSnapshot = {
target: snapshotTarget(ANALYSIS_SNAPSHOT_TARGET),
binding: snapshotBinding(ANALYSIS_SNAPSHOT_PROFILE),
entries: [],
evidence_bundle: createEvidenceBundle([evidence, legacy]),
};
const { current, snapshot } = snapshotWithHistoricalEvidence();
expect(
snapshotEvidenceForQuery(snapshot, {
target: ANALYSIS_SNAPSHOT_TARGET,
@ -125,7 +150,7 @@ describe("analysis snapshot Evidence binding", () => {
provider: ANALYSIS_SNAPSHOT_PROVIDER,
evidenceProfile: ANALYSIS_SNAPSHOT_PROFILE,
}),
).toEqual(evidence);
).toEqual(current);
expect(
snapshotEvidenceForQuery(snapshot, {
target: ANALYSIS_SNAPSHOT_TARGET,

View File

@ -101,17 +101,33 @@ const entrySchema = z.object({
subject: subjectSchema.nullable(),
}),
});
const workflowEntrySchema = z.object({
query_id: prefixedDigestSchema("query"),
operation: z.string().min(1),
parameters: jsonObjectSchema,
execution: z.object({
result: jsonValueSchema,
raw_result: jsonValueSchema.nullable(),
provider: committedProviderSchema,
analysis_profile: analysisProfileSchema,
limitations: z.array(z.string()),
locations: z.array(evidenceLocationSchema),
subject: subjectSchema.nullable(),
}),
});
/** Provider- and profile-exact cache of successful immutable analysis calls. */
export const analysisSnapshotSchema = z.object({
target: targetSchema,
binding: bindingSchema,
entries: z.array(entrySchema),
workflow_entries: z.array(workflowEntrySchema).optional(),
evidence_bundle: evidenceBundleSchema,
});
export type AnalysisSnapshot = z.infer<typeof analysisSnapshotSchema>;
export type AnalysisSnapshotEntry = z.infer<typeof entrySchema>;
export type AnalysisSnapshotWorkflowEntry = z.infer<typeof workflowEntrySchema>;
export type AnalysisSnapshotTarget = z.infer<typeof targetSchema>;
export type AnalysisSnapshotBinding = z.infer<typeof bindingSchema>;
@ -208,6 +224,19 @@ export const snapshotEvidenceForQuery = (
if (!snapshotMatchesBinding(snapshot, target, bindingProfile))
return undefined;
const encodedParameters = canonicalJson(parameters);
const queryId = analysisQueryId(
snapshot.target,
snapshot.binding,
operation,
parameters,
);
const entry = snapshot.entries.find(
(candidate) => candidate.query_id === queryId,
);
const workflowEntry = snapshot.workflow_entries?.find(
(candidate) => candidate.query_id === queryId,
);
if (entry === undefined && workflowEntry === undefined) return undefined;
return snapshot.evidence_bundle.records.find(
(record) =>
record.subject?.digest.sha256 === target.sha256 &&
@ -217,10 +246,77 @@ export const snapshotEvidenceForQuery = (
record.provider.version === provider.version &&
"analysis_profile" in record &&
analysisProfilesEqual(record.analysis_profile, evidenceProfile) &&
canonicalJson(record.parameters) === encodedParameters,
canonicalJson(record.parameters) === encodedParameters &&
((entry !== undefined && evidenceMatchesEntry(record, entry, snapshot)) ||
(workflowEntry !== undefined &&
evidenceMatchesWorkflowEntry(record, workflowEntry, snapshot))),
);
};
/** Create one exact replay record for a derived application workflow result. */
export const createAnalysisSnapshotWorkflowEntry = (input: {
readonly target: AnalysisSnapshotTarget;
readonly binding: AnalysisSnapshotBinding;
readonly operation: string;
readonly parameters: Readonly<Record<string, JsonValue>>;
readonly execution: SnapshotExecution & {
readonly analysisProfile: AnalysisProfileCommitment;
};
}): AnalysisSnapshotWorkflowEntry => {
const { target, binding, operation, parameters, execution } = input;
if (
execution.provider.id !== execution.analysisProfile.provider.id ||
execution.provider.name !== execution.analysisProfile.provider.name ||
execution.provider.version !== execution.analysisProfile.provider.version
)
throw new TypeError(
"Workflow snapshot execution provider does not match its profile",
);
return {
query_id: analysisQueryId(target, binding, operation, parameters),
operation,
parameters: jsonObjectSchema.parse(parameters),
execution: {
result: execution.result,
raw_result: execution.rawResult,
provider: execution.analysisProfile.provider,
analysis_profile: execution.analysisProfile,
limitations: [...execution.limitations],
locations: [...execution.locations],
subject:
execution.subject === null
? null
: {
...execution.subject,
architecture: execution.subject.architecture ?? null,
},
},
};
};
const evidenceMatchesEntry = (
evidence: Evidence,
entry: AnalysisSnapshotEntry,
snapshot: Pick<AnalysisSnapshot, "target" | "binding">,
): boolean =>
isCorrespondingEvidence(evidence, snapshot) &&
canonicalJson({
query: evidenceQueryKey(evidence),
normalized_result: evidence.normalized_result,
raw_result: evidence.raw_result,
limitations: evidence.limitations,
locations: evidence.locations,
subject:
evidence.subject === null
? null
: {
local_path: evidence.subject.local_path,
sha256: evidence.subject.digest.sha256,
format: evidence.subject.format,
architecture: evidence.subject.architecture,
},
}) === entryEvidenceKey(entry, snapshot.binding);
/** Compute the stable lookup key for one provider/profile-specific query. */
export const analysisQueryId = (
target: AnalysisSnapshotTarget,
@ -286,7 +382,9 @@ export const parseAnalysisSnapshot = (input: unknown): AnalysisSnapshot => {
);
const ids = new Set<string>();
const boundEntries: AnalysisSnapshotEntry[] = [];
const boundWorkflowEntries: AnalysisSnapshotWorkflowEntry[] = [];
const index = createEvidenceIndex(parsed);
const workflowIndex = createWorkflowEvidenceIndex(parsed);
for (const entry of parsed.entries) {
if (
entry.execution.provider.id !== parsed.binding.provider.id ||
@ -316,12 +414,60 @@ export const parseAnalysisSnapshot = (input: unknown): AnalysisSnapshot => {
}
boundEntries.push(entry);
}
for (const entry of parsed.workflow_entries ?? []) {
if (
entry.execution.provider.id !==
entry.execution.analysis_profile.provider.id ||
entry.execution.provider.name !==
entry.execution.analysis_profile.provider.name ||
entry.execution.provider.version !==
entry.execution.analysis_profile.provider.version
)
throw new TypeError(
"Analysis snapshot workflow entry provider does not match its profile",
);
const expected = analysisQueryId(
parsed.target,
parsed.binding,
entry.operation,
entry.parameters,
);
if (entry.query_id !== expected)
throw new TypeError(
"Analysis snapshot workflow query identifier does not match",
);
if (ids.has(entry.query_id))
throw new TypeError("Analysis snapshot contains duplicate queries");
ids.add(entry.query_id);
if (!workflowEntryHasMatchingEvidence(entry, workflowIndex)) {
if (workflowEntryHasCorrespondingEvidence(entry, workflowIndex))
throw new TypeError(
`Analysis snapshot workflow entry ${entry.operation} differs from its Evidence record`,
);
continue;
}
boundWorkflowEntries.push(entry);
}
const sorted = [...parsed.entries].sort((left, right) =>
left.query_id.localeCompare(right.query_id),
);
if (JSON.stringify(parsed.entries) !== JSON.stringify(sorted))
throw new TypeError("Analysis snapshot entries are not canonical");
return { ...parsed, entries: boundEntries };
const sortedWorkflows = [...(parsed.workflow_entries ?? [])].sort(
(left, right) => left.query_id.localeCompare(right.query_id),
);
if (
parsed.workflow_entries !== undefined &&
JSON.stringify(parsed.workflow_entries) !== JSON.stringify(sortedWorkflows)
)
throw new TypeError("Analysis snapshot workflow entries are not canonical");
return {
...parsed,
entries: boundEntries,
...(parsed.workflow_entries === undefined
? {}
: { workflow_entries: boundWorkflowEntries }),
};
};
/** Check that a cached provider execution is represented by bundled Evidence. */
@ -429,6 +575,108 @@ const entryEvidenceKey = (
},
});
const workflowEntryQueryKey = (entry: AnalysisSnapshotWorkflowEntry): string =>
canonicalJson({
operation: entry.operation,
parameters: entry.parameters,
provider: entry.execution.provider,
analysis_profile: entry.execution.analysis_profile,
});
const workflowEntryEvidenceKey = (
entry: AnalysisSnapshotWorkflowEntry,
): string =>
canonicalJson({
query: workflowEntryQueryKey(entry),
normalized_result: entry.execution.result,
raw_result: entry.execution.raw_result,
limitations: entry.execution.limitations,
locations: entry.execution.locations,
subject:
entry.execution.subject === null
? null
: {
local_path: entry.execution.subject.path,
sha256: entry.execution.subject.sha256,
format: entry.execution.subject.format,
architecture: entry.execution.subject.architecture,
},
});
const isWorkflowEvidence = (
evidence: Evidence,
snapshot: Pick<AnalysisSnapshot, "target">,
entry: AnalysisSnapshotWorkflowEntry,
): boolean =>
evidence.predicate_type === "rea.analysis" &&
evidence.confidence === "derived" &&
evidence.authority === "shipped-artifact" &&
evidence.subject?.digest.sha256 === snapshot.target.sha256 &&
"analysis_profile" in evidence &&
analysisProfilesEqual(
evidence.analysis_profile,
entry.execution.analysis_profile,
) &&
evidence.provider.id === entry.execution.provider.id &&
evidence.provider.name === entry.execution.provider.name &&
evidence.provider.version === entry.execution.provider.version;
const createWorkflowEvidenceIndex = (
snapshot: Pick<AnalysisSnapshot, "target" | "evidence_bundle">,
): EvidenceIndex => {
const matching = new Set<string>();
const corresponding = new Set<string>();
for (const evidence of snapshot.evidence_bundle.records) {
if (
evidence.predicate_type !== "rea.analysis" ||
evidence.confidence !== "derived" ||
evidence.authority !== "shipped-artifact" ||
evidence.subject?.digest.sha256 !== snapshot.target.sha256 ||
!("analysis_profile" in evidence)
)
continue;
corresponding.add(evidenceQueryKey(evidence));
matching.add(canonicalJson(evidenceContentProjection(evidence)));
}
return { matching, corresponding };
};
const workflowEntryHasMatchingEvidence = (
entry: AnalysisSnapshotWorkflowEntry,
index: EvidenceIndex,
): boolean => index.matching.has(workflowEntryEvidenceKey(entry));
const workflowEntryHasCorrespondingEvidence = (
entry: AnalysisSnapshotWorkflowEntry,
index: EvidenceIndex,
): boolean => index.corresponding.has(workflowEntryQueryKey(entry));
const evidenceMatchesWorkflowEntry = (
evidence: Evidence,
entry: AnalysisSnapshotWorkflowEntry,
snapshot: Pick<AnalysisSnapshot, "target">,
): boolean =>
isWorkflowEvidence(evidence, snapshot, entry) &&
canonicalJson(evidenceContentProjection(evidence)) ===
workflowEntryEvidenceKey(entry);
const evidenceContentProjection = (evidence: Evidence) => ({
query: evidenceQueryKey(evidence),
normalized_result: evidence.normalized_result,
raw_result: evidence.raw_result,
limitations: evidence.limitations,
locations: evidence.locations,
subject:
evidence.subject === null
? null
: {
local_path: evidence.subject.local_path,
sha256: evidence.subject.digest.sha256,
format: evidence.subject.format,
architecture: evidence.subject.architecture,
},
});
/** Serialize a validated snapshot with byte-stable canonical entry ordering. */
export const serializeAnalysisSnapshot = (snapshot: AnalysisSnapshot): string =>
`${canonicalJson(parseAnalysisSnapshot(snapshot))}\n`;

View File

@ -54,7 +54,7 @@ describe("direct analysis snapshot files", () => {
});
});
it("replays a cache hit from the supplied path without root configuration", async () => {
it("does not replay unbound historical Evidence from a supplied snapshot", async () => {
const directory = await createTestTempDirectory("rea-direct-snapshot-");
const snapshotPath = join(directory, "analysis.json");
const launcherPath = join(directory, "hopper-launcher");
@ -97,6 +97,6 @@ describe("direct analysis snapshot files", () => {
snapshotPath,
},
),
).resolves.toEqual(evidence);
).resolves.not.toEqual(evidence);
});
});

View File

@ -3,6 +3,7 @@ import type {
AnalysisProvider,
} from "../../../src/application/AnalysisProvider.js";
import type { BinarySession } from "../../../src/application/binary/BinarySession.js";
import { parseBinaryTarget } from "../../../src/application/BinaryTargetResolver.js";
import type { JsonValue } from "../../../src/domain/jsonValue.js";
import type { RecordUnknownInput } from "../../../src/domain/residualUnknown.js";
import { createAnalysisProfile } from "../../../src/domain/analysisProfile.js";
@ -159,6 +160,65 @@ describe("detached provider and target metadata", () => {
});
});
describe("opening previewed targets", () => {
it("rechecks the active target after another serialized transition", async () => {
const [first, second] = await createBinarySessionTargets();
const calls: string[] = [];
const session = createTestBinarySession(createCacheProvider(calls));
try {
expect((await session.open(first)).ok).toBe(true);
const target = session.activeTarget();
if (target === undefined) throw new Error("Expected an active target");
const preview = await session.previewTarget(target);
if (!preview.ok) throw preview.error;
expect(preview.value.sameTarget).toBe(true);
expect((await session.open(second)).ok).toBe(true);
expect((await session.openResolvedTarget(preview.value)).ok).toBe(true);
expect(session.activeTarget()?.path).toBe(first);
expect(calls).toEqual(["health", "health", "health"]);
} finally {
await session.close();
}
});
it("rejects cancellation and a staged snapshot changed after preview without startup", async () => {
const [first, second] = await createBinarySessionTargets();
const calls: string[] = [];
const session = createTestBinarySession(createCacheProvider(calls));
const other = createTestBinarySession(createCacheProvider([]));
try {
const parsed = await parseBinaryTarget(first);
if (!parsed.ok) throw parsed.error;
const preview = await session.previewTarget(parsed.value);
if (!preview.ok) throw preview.error;
const controller = new AbortController();
controller.abort();
const cancelled = await session.openResolvedTarget(preview.value, {
signal: controller.signal,
});
expect(cancelled).toMatchObject({
ok: false,
error: { _tag: "AnalysisCancelledError" },
});
expect((await other.open(second)).ok).toBe(true);
const snapshot = other.exportAnalysisSnapshot();
if (!snapshot.ok) throw snapshot.error;
expect(session.importAnalysisSnapshot(snapshot.value).ok).toBe(true);
const rejected = await session.openResolvedTarget(preview.value);
expect(rejected).toMatchObject({
ok: false,
error: { _tag: "EvidenceIntegrityError" },
});
expect(calls).toEqual([]);
expect(session.activeTarget()).toBeUndefined();
} finally {
await session.close();
await other.close();
}
});
});
describe("non-hopper provider dispatch", () => {
it("runs through a non-Hopper analysis provider", async () => {
const [first] = await createBinarySessionTargets();

View File

@ -0,0 +1,633 @@
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { runDirectAnalysis } from "../../src/application/DirectAnalysis.js";
import type {
AnalysisProvider,
CapabilityDescriptor,
ProviderIdentity,
} from "../../src/application/AnalysisProvider.js";
import type { DirectAnalysisDependencies } from "../../src/application/DirectAnalysisDependencies.js";
import { createAnalysisProfile } from "../../src/domain/analysisProfile.js";
import { createAnalysisExecution } from "../../src/application/AnalysisProvider.js";
import { ok } from "../../src/domain/result.js";
import {
readAnalysisSnapshot,
writeAnalysisSnapshot,
} from "../../src/application/binary/AnalysisSnapshotFiles.js";
import { createEvidence, type Evidence } from "../../src/domain/evidence.js";
import { createEvidenceBundle } from "../../src/domain/evidenceBundle.js";
import {
REA_WORKFLOW_PROVIDER,
workflowAnalysisProfile,
} from "../../src/application/InvestigationProviders.js";
import {
createAnalysisSnapshotWorkflowEntry,
type AnalysisSnapshot,
} from "../../src/domain/analysisSnapshot.js";
import { createTestBinarySession } from "../fixtures/binarySession.js";
import { BinarySession } from "../../src/application/binary/BinarySession.js";
import { SessionProviderRouter } from "../../src/application/binary/SessionProviderRouter.js";
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
const IDENTITY = {
id: "snapshot-fixture",
name: "Snapshot Fixture Provider",
version: "1",
} as const;
const operations = [
"list_segments",
"list_documents",
"list_procedures",
"list_strings",
] as const;
const workflowProviderOperations = [
...operations,
"analyze_function",
"list_names",
"search_strings",
"search_procedures",
"xrefs",
"resolve_containing_procedure",
] as const;
const functionDossier = {
procedure: {
address: "0x1000",
name: "fixture",
signature: null,
locals: [],
},
pseudocode: "return 0;",
assembly: ["ret"],
comments: [],
callers: [],
callees: [],
incoming_references: [],
outgoing_references: [],
referenced_strings: [],
referenced_names: [],
basic_blocks: [],
native_api: null,
native_value_flow: null,
limitations: [],
};
const makeProvider = (
starts: string[],
calls: string[],
profile = createAnalysisProfile(IDENTITY, { fixture: true }),
identity: ProviderIdentity = profile.provider,
supportedOperations: readonly CapabilityDescriptor["operation"][] = operations,
resolutions: string[] = [],
): AnalysisProvider => {
const capabilities: CapabilityDescriptor[] = supportedOperations.map(
(operation) => ({
operation,
provider: identity,
available: true,
reason: null,
cachePolicy: "snapshot",
effects: {
mutatesArtifact: false,
launchesProcess: true,
mayShowUi: false,
mayAccessNetwork: false,
mayWriteFilesystem: false,
changesPermissions: false,
requiresRoot: false,
},
limitations: [],
}),
);
return {
identity: () => identity,
capabilities: () => capabilities,
resolveAnalysisProfile: async () => {
resolutions.push(profile.digest);
return ok({ profile, compatibility: {} });
},
createClient: () => {
starts.push("start");
return {
execute: async (operation) => {
calls.push(operation);
const results: Readonly<Record<string, unknown>> = {
list_segments: [{ name: "__TEXT", start: "0x1000", end: "0x2000" }],
list_documents: ["fixture"],
list_procedures: ["0x1000"],
list_strings: { "0x1000": "fixture" },
analyze_function: functionDossier,
list_names: [{ address: "0x1000", name: "fixture" }],
search_strings: [{ address: "0x1000", value: "fixture" }],
search_procedures: [{ address: "0x1000", value: "fixture" }],
xrefs: [],
resolve_containing_procedure: null,
procedure_pseudo_code: "return 0;",
};
const result = results[operation] ?? null;
return ok(
createAnalysisExecution(result, profile.provider, {
analysisProfile: profile,
rawResult: result,
}),
);
},
close: async () => undefined,
};
},
};
};
const withAlternateWorkflowProfile = (
snapshot: AnalysisSnapshot,
path: string,
current: Evidence,
): AnalysisSnapshot => {
const alternateProfile = createAnalysisProfile(REA_WORKFLOW_PROVIDER, {
workflow: "binary_overview",
fixture: "alternate-profile",
});
const subject = {
path,
sha256: snapshot.target.sha256,
format: snapshot.target.format,
...(snapshot.target.architecture === null
? {}
: { architecture: snapshot.target.architecture }),
};
const alternateEvidence = createEvidence(subject, REA_WORKFLOW_PROVIDER, {
operation: current.operation,
parameters: current.parameters,
result: current.normalized_result,
rawResult: current.raw_result,
analysisProfile: alternateProfile,
confidence: "derived",
limitations: current.limitations,
locations: current.locations,
});
const alternateEntry = createAnalysisSnapshotWorkflowEntry({
target: snapshot.target,
binding: snapshot.binding,
operation: current.operation,
parameters: current.parameters,
execution: {
result: current.normalized_result,
rawResult: current.raw_result,
provider: REA_WORKFLOW_PROVIDER,
analysisProfile: alternateProfile,
limitations: current.limitations,
locations: current.locations,
subject,
},
});
return {
...snapshot,
workflow_entries: [alternateEntry],
evidence_bundle: createEvidenceBundle([
...snapshot.evidence_bundle.records,
alternateEvidence,
]),
};
};
const withEarlierHistoricalEvidence = (
snapshot: AnalysisSnapshot,
path: string,
current: Evidence,
): AnalysisSnapshot => {
const older = Array.from({ length: 64 }, (_, index) =>
createEvidence(
{
path,
sha256: snapshot.target.sha256,
format: "analysis-database",
},
REA_WORKFLOW_PROVIDER,
{
operation: "binary_overview",
parameters: {},
result: `historical-${index}`,
analysisProfile: workflowAnalysisProfile(
snapshot.binding.analysis_profile,
),
confidence: "derived",
limitations: ["Derived by an REA composed workflow."],
},
),
).find((record) => record.evidence_id < current.evidence_id);
if (older === undefined)
throw new Error("fixture could not construct earlier historical Evidence");
return {
...snapshot,
evidence_bundle: createEvidenceBundle([
...snapshot.evidence_bundle.records,
older,
]),
};
};
describe("direct analysis composed snapshot replay", () => {
it.each([
{
tool: "search_strings",
arguments: { pattern: "uncached", document: "fixture" },
},
{ tool: "inspect_native_api", arguments: { procedure: "0x2000" } },
] as const)(
"reuses the previewed profile on a $tool snapshot miss",
async (scenario) => {
const directory = await createTestTempDirectory(
"rea-snapshot-miss-route-",
);
const path = join(directory, "fixture.hop");
const snapshotPath = join(directory, "snapshot.json");
await writeFile(path, "fixture");
const starts: string[] = [];
const calls: string[] = [];
const resolutions: string[] = [];
const profile = createAnalysisProfile(IDENTITY, { fixture: true });
const provider = makeProvider(
starts,
calls,
profile,
IDENTITY,
workflowProviderOperations,
resolutions,
);
const dependencies: DirectAnalysisDependencies = {
createBinarySession: () => createTestBinarySession(provider),
createManagedBinarySession: () => createTestBinarySession(provider),
};
await runDirectAnalysis(
dependencies,
path,
"binary_overview",
{},
{ snapshotPath },
);
expect(resolutions).toHaveLength(1);
const first = await runDirectAnalysis(
dependencies,
path,
scenario.tool,
scenario.arguments,
{ snapshotPath },
);
expect(first).toMatchObject({ operation: scenario.tool });
expect(resolutions).toHaveLength(2);
expect(starts).toHaveLength(2);
const callsAfterMiss = [...calls];
const replay = await runDirectAnalysis(
dependencies,
path,
scenario.tool,
scenario.arguments,
{ snapshotPath },
);
expect(replay).toEqual(first);
expect(resolutions).toHaveLength(3);
expect(starts).toHaveLength(2);
expect(calls).toEqual(callsAfterMiss);
},
);
for (const scenario of [
{
tool: "inspect_native_api",
arguments: { procedure: "0x1000" },
},
{
tool: "inspect_native_dispatch_metadata",
arguments: {},
},
{
tool: "trace_feature",
arguments: { query: "fixture" },
},
{
tool: "trace_native_values",
arguments: { procedure: "0x1000" },
},
] as const) {
it(`persists and replays ${scenario.tool} without provider startup`, async () => {
const directory = await createTestTempDirectory("rea-workflow-command-");
const path = join(directory, "fixture.hop");
const snapshotPath = join(directory, "snapshot.json");
await writeFile(path, "fixture");
const starts: string[] = [];
const calls: string[] = [];
const provider = makeProvider(
starts,
calls,
createAnalysisProfile(IDENTITY, { fixture: true }),
IDENTITY,
workflowProviderOperations,
);
const dependencies: DirectAnalysisDependencies = {
createBinarySession: () => createTestBinarySession(provider),
createManagedBinarySession: () => createTestBinarySession(provider),
};
const first = await runDirectAnalysis(
dependencies,
path,
scenario.tool,
scenario.arguments,
{ snapshotPath },
);
const callsAfterFirst = [...calls];
const startsAfterFirst = [...starts];
const loaded = await readAnalysisSnapshot(snapshotPath);
if (!loaded.ok) throw loaded.error;
expect(
loaded.value.workflow_entries?.map(({ operation }) => operation),
).toContain(scenario.tool);
const second = await runDirectAnalysis(
dependencies,
path,
scenario.tool,
scenario.arguments,
{ snapshotPath },
);
expect(second).toEqual(first);
expect(calls).toEqual(callsAfterFirst);
expect(starts).toEqual(startsAfterFirst);
});
}
});
describe("binary overview snapshot replay", () => {
it("replays the identical binary overview without provider startup or calls", async () => {
const directory = await createTestTempDirectory("rea-workflow-snapshot-");
const path = join(directory, "fixture.hop");
const snapshotPath = join(directory, "snapshot.json");
await writeFile(path, "fixture");
const starts: string[] = [];
const calls: string[] = [];
const provider = makeProvider(starts, calls);
const dependencies: DirectAnalysisDependencies = {
createBinarySession: () => createTestBinarySession(provider),
createManagedBinarySession: () => createTestBinarySession(provider),
};
const first = await runDirectAnalysis(
dependencies,
path,
"binary_overview",
{},
{ snapshotPath },
);
expect(calls).toEqual(["health", ...operations]);
expect(starts).toHaveLength(1);
const loaded = await readAnalysisSnapshot(snapshotPath);
if (!loaded.ok) throw loaded.error;
expect(loaded.value.workflow_entries).toHaveLength(1);
const current = loaded.value.evidence_bundle.records.find(
(record) => record.operation === "binary_overview",
);
if (current === undefined)
throw new Error("composed Evidence was not saved");
expect(
(
await writeAnalysisSnapshot(
withEarlierHistoricalEvidence(loaded.value, path, current),
snapshotPath,
true,
)
).ok,
).toBe(true);
const second = await runDirectAnalysis(
dependencies,
path,
"binary_overview",
{},
{ snapshotPath },
);
expect(second).toEqual(first);
expect(calls).toEqual(["health", ...operations]);
expect(starts).toHaveLength(1);
});
});
describe("snapshot replay provider identity", () => {
it("uses the resolved provider version when capability metadata omits it", async () => {
const directory = await createTestTempDirectory("rea-profile-version-");
const path = join(directory, "fixture.hop");
const snapshotPath = join(directory, "snapshot.json");
await writeFile(path, "fixture");
const starts: string[] = [];
const calls: string[] = [];
const unresolvedIdentity = { ...IDENTITY, version: null };
const resolvedProfile = createAnalysisProfile(
{ ...IDENTITY, version: "launcher-sha256:fixture" },
{ fixture: true },
);
const provider = makeProvider(
starts,
calls,
resolvedProfile,
unresolvedIdentity,
["search_strings"],
);
const dependencies: DirectAnalysisDependencies = {
createBinarySession: () => createTestBinarySession(provider),
createManagedBinarySession: () => createTestBinarySession(provider),
};
const first = await runDirectAnalysis(
dependencies,
path,
"search_strings",
{ pattern: "fixture", document: "fixture" },
{ snapshotPath },
);
const callsAfterFirst = [...calls];
const startsAfterFirst = [...starts];
const firstSnapshot = await readAnalysisSnapshot(snapshotPath);
if (!firstSnapshot.ok) throw firstSnapshot.error;
expect(firstSnapshot.value.entries[0]?.execution.provider.version).toBe(
"launcher-sha256:fixture",
);
const second = await runDirectAnalysis(
dependencies,
path,
"search_strings",
{ pattern: "fixture", document: "fixture" },
{ snapshotPath },
);
expect(calls).toEqual(callsAfterFirst);
expect(starts).toEqual(startsAfterFirst);
expect(second).toEqual(first);
const changedProfileProvider = makeProvider(
starts,
calls,
createAnalysisProfile(
{ ...IDENTITY, version: "launcher-sha256:changed" },
{ fixture: true },
),
unresolvedIdentity,
["search_strings"],
);
const changedProfileResult = await runDirectAnalysis(
{
createBinarySession: () =>
createTestBinarySession(changedProfileProvider),
createManagedBinarySession: () =>
createTestBinarySession(changedProfileProvider),
},
path,
"search_strings",
{ pattern: "fixture", document: "fixture" },
{ snapshotPath },
);
expect(changedProfileResult).not.toEqual(first);
expect(changedProfileResult).toMatchObject({
code: "evidence_integrity_mismatch",
});
expect(starts).toEqual(startsAfterFirst);
expect(calls).toEqual(callsAfterFirst);
const changedProviderIdentity = {
id: "other-provider",
name: "Other Provider",
version: null,
};
const changedProvider = makeProvider(
starts,
calls,
createAnalysisProfile(
{ ...changedProviderIdentity, version: "other-version" },
{ fixture: true },
),
changedProviderIdentity,
["search_strings"],
);
const changedProviderResult = await runDirectAnalysis(
{
createBinarySession: () => createTestBinarySession(changedProvider),
createManagedBinarySession: () =>
createTestBinarySession(changedProvider),
},
path,
"search_strings",
{ pattern: "fixture", document: "fixture" },
{ snapshotPath },
);
expect(changedProviderResult).not.toEqual(first);
expect(changedProviderResult).toMatchObject({
code: "evidence_integrity_mismatch",
});
expect(starts).toEqual(startsAfterFirst);
expect(calls).toEqual(callsAfterFirst);
});
});
describe("workflow snapshot profile and cancellation binding", () => {
it("does not replay a valid entry from another workflow profile", async () => {
const directory = await createTestTempDirectory("rea-workflow-profile-");
const path = join(directory, "fixture.hop");
const snapshotPath = join(directory, "snapshot.json");
await writeFile(path, "fixture");
const starts: string[] = [];
const calls: string[] = [];
const provider = makeProvider(starts, calls);
const dependencies: DirectAnalysisDependencies = {
createBinarySession: () => createTestBinarySession(provider),
createManagedBinarySession: () => createTestBinarySession(provider),
};
await runDirectAnalysis(
dependencies,
path,
"binary_overview",
{},
{ snapshotPath },
);
const loaded = await readAnalysisSnapshot(snapshotPath);
if (!loaded.ok) throw loaded.error;
const current = loaded.value.evidence_bundle.records.find(
(record) => record.operation === "binary_overview",
);
if (current === undefined)
throw new Error("composed Evidence was not saved");
expect(
(
await writeAnalysisSnapshot(
withAlternateWorkflowProfile(loaded.value, path, current),
snapshotPath,
true,
)
).ok,
).toBe(true);
await runDirectAnalysis(
dependencies,
path,
"binary_overview",
{},
{ snapshotPath },
);
expect(calls).toEqual(["health", ...operations, "health", ...operations]);
expect(starts).toHaveLength(2);
});
it("does not return a cached result when cancelled during route resolution", async () => {
const directory = await createTestTempDirectory("rea-workflow-cancel-");
const path = join(directory, "fixture.hop");
const snapshotPath = join(directory, "snapshot.json");
await writeFile(path, "fixture");
const starts: string[] = [];
const calls: string[] = [];
const profile = createAnalysisProfile(IDENTITY, { fixture: true });
const provider = makeProvider(starts, calls, profile);
const initialDependencies: DirectAnalysisDependencies = {
createBinarySession: () => createTestBinarySession(provider),
createManagedBinarySession: () => createTestBinarySession(provider),
};
await runDirectAnalysis(
initialDependencies,
path,
"binary_overview",
{},
{ snapshotPath },
);
const controller = new AbortController();
const router = SessionProviderRouter.single(provider);
const resolve = router.resolve.bind(router);
router.resolve = async (...arguments_) => {
const resolved = await resolve(...arguments_);
controller.abort();
return resolved;
};
const session = new BinarySession(router);
const dependencies: DirectAnalysisDependencies = {
createBinarySession: () => session,
createManagedBinarySession: () => session,
};
const result = await runDirectAnalysis(
dependencies,
path,
"binary_overview",
{},
{ snapshotPath, signal: controller.signal },
);
expect(result).toMatchObject({
error: "Analysis failed",
code: "cancelled",
});
expect(calls).toEqual(["health", ...operations]);
expect(starts).toHaveLength(1);
});
});