feat(evidence): aggregate exact retained comparison references (#8267)

This commit is contained in:
gensokyo-dev authored and GitHub committed 2026-10-31 13:59:13 +08:00
1 parent 6e2404b869
commit d9e01e5af2
12 files changed
+375 -29

No files matched your search

@@ -2,7 +2,7 @@
name: reverse-engineer-anything
description: Reverse engineer native, managed, Electron/JavaScript, packaged, firmware, and browser targets with REA, and build agent environments (AgentEnv bundles) of web applications from a live instance or a capture. Use shipped-artifact or requested runtime evidence to explain features, compare versions, decompile code, or guide a reconstruction. Skip REA for ordinary source-repository architecture analysis.
metadata:
version: "57"
version: "58"
---
# REA
@@ -13,6 +13,13 @@ Select columns, batch IDs and page individual facts; aliases remain unresolved.
Unpaired observations do not prove additions/removals. Existing matched dossier
pairs include inputs for `compare_functions`; otherwise analyze just chosen pairs.
Aggregate completed comparisons with `find_changed_behavior`, selecting each
comparison's exact `retained-evidence` ID instead of resending full records.
Inline comparison Evidence remains accepted. References belong to the current
server: after handoff, import the original bundle including its source closure.
Do not substitute dossier IDs or summary views. Static differences remain
behavior candidates, with runtime behavior unknown unless separately observed.
For a matching representation refusal, inspect `details.limit` and raise
`native.matching_representation_bytes` with `update_analysis_settings` before
retrying the unchanged MCP request. CLI launch seed:
+19
View File
@@ -1130,6 +1130,25 @@ and slice commitments establish the same canonical entry. The result reports
remain separate dimensions. Equal numeric addresses without applicable coordinate
commitments do not establish same-entry correspondence. Inspect the retained
source Evidence and coverage when correspondence remains ambiguous.
To aggregate already-retained comparison results, pass their exact IDs to
`find_changed_behavior`, for example:
```javascript
await call("find_changed_behavior", {
comparisons: comparisons.map(({ evidence_id }) => ({
kind: "retained-evidence",
evidence_id,
})),
});
```
These IDs select comparison Evidence, not function dossiers or summary views.
Complete inline comparisons remain accepted and can be mixed with references.
The current server must retain the comparisons and their original source closure;
import the complete source bundle after a session handoff. Missing or damaged
records are explicit failures. Aggregating static differences does not establish
observed runtime change.
`set_address_name` and `set_addresses_names` give Ghidra sessions the same
compact naming results as Hopper: a local function entry renames that function,
and any other mapped address, such as a global, receives a new or renamed primary
@@ -0,0 +1,42 @@
import { readChangedBehaviorSelection } from "../../domain/changedBehavior.js";
import { retainedEvidenceReferenceSchema } from "../../domain/evidenceReference.js";
import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import { err, ok, type Result } from "../../domain/result.js";
import {
resolveEvidenceInput,
type EvidenceLookup,
} from "../EvidenceInputResolver.js";
/** Resolve exact selected identities; the domain still owns full-record authentication. */
export const resolveChangedBehaviorEvidence = (
input: unknown,
lookup: EvidenceLookup | undefined,
): Result<{ readonly comparisons: readonly unknown[] }, AnalysisError> => {
const selection = readChangedBehaviorSelection(input);
if (!selection.ok) return selection;
const comparisons: unknown[] = [];
for (const [index, item] of selection.value.entries()) {
if (typeof item !== "object" || item === null || !("kind" in item)) {
comparisons.push(item);
continue;
}
const reference = retainedEvidenceReferenceSchema.safeParse(item);
if (!reference.success)
return err(
analysisInputErrorFromIssues(
"find_changed_behavior",
reference.error.issues.map((issue) => ({
...issue,
path: ["comparisons", index, ...issue.path],
})),
input,
{ cause: reference.error },
),
);
const resolved = resolveEvidenceInput(reference.data, lookup);
if (!resolved.ok) return resolved;
comparisons.push(resolved.value);
}
return ok({ comparisons });
};
+8 -1
View File
@@ -72,7 +72,14 @@ export const INVESTIGATION_EXAMPLES = {
right: PROCESS_CAPTURE_RECONSTRUCTION,
},
},
find_changed_behavior: { comparisons: [FUNCTION_COMPARISON_EVIDENCE] },
find_changed_behavior: {
comparisons: [
{
kind: "retained-evidence",
evidence_id: FUNCTION_COMPARISON_EVIDENCE.evidence_id,
},
],
},
build_call_path: {
functions: [FUNCTION_COMPARISON_EXAMPLE.left],
start: { address: "0x1000" },
+1 -1
View File
@@ -125,7 +125,7 @@ export const SESSION_TOOL_CONTRACTS = [
),
session(
"find_changed_behavior",
"Aggregate validated process, artifact, and function comparison Evidence. Runtime observations remain distinct from static behavior candidates; missing or incomplete comparisons produce unresolved findings, never causal claims. Returns every finding inline.",
"Aggregate validated process, artifact, and function comparison Evidence, supplied inline or by exact retained-evidence references from this session. Original records and their complete source closure remain authenticated; summaries do not substitute for comparisons. Runtime observations remain distinct from static behavior candidates; missing or incomplete comparisons produce unresolved findings, never causal claims. Returns every finding inline.",
changedBehaviorInputSchema,
),
session(
+26 -16
View File
@@ -10,7 +10,9 @@ import {
readComparisonResult,
type ParsedComparisonEvidence,
} from "./comparisonEvidenceResult.js";
import { evidenceSchema, type Evidence } from "./evidence.js";
import type { Evidence } from "./evidence.js";
import { evidenceInputSchema } from "./evidenceReference.js";
import { err, ok, type Result } from "./result.js";
import type { AnalysisInputError } from "./analysisErrorCore.js";
import { EvidenceIntegrityError } from "./evidenceErrors.js";
import { analysisInputErrorFromIssues } from "./inputIssueProjection.js";
@@ -29,9 +31,28 @@ const evidenceIdSchema = prefixedDigestSchema("ev");
/** Input for aggregating existing comparison Evidence. */
export const changedBehaviorInputSchema = z.strictObject({
comparisons: z.array(evidenceSchema).min(1),
comparisons: z.array(evidenceInputSchema).min(1),
});
const comparisonSelectionSchema = z.strictObject({
comparisons: z.array(z.unknown()).min(1),
});
/** Admit the selection envelope without copying or authenticating full records. */
export const readChangedBehaviorSelection = (
input: unknown,
): Result<readonly unknown[], AnalysisInputError> => {
const envelope = comparisonSelectionSchema.safeParse(input);
return envelope.success
? ok(envelope.data.comparisons)
: err(
analysisInputErrorFromIssues(OPERATION, envelope.error.issues, input, {
cause: envelope.error,
userMessage: "Provide at least one comparison Evidence item.",
}),
);
};
const findingSchema = z.object({
scope: z.enum(["runtime", "protocol", "resource", "static_candidate"]),
dimension: z.string().min(1),
@@ -107,20 +128,9 @@ export const findChangedBehavior = (input: unknown): FoundChangedBehavior => {
// The public input schema remains available to clients; this internal
// admission only checks the list shape because readEvidence below owns
// validation and authentication of each retained Evidence item.
const envelope = z
.strictObject({ comparisons: z.array(z.unknown()).min(1) })
.safeParse(input);
if (!envelope.success)
throw analysisInputErrorFromIssues(
OPERATION,
envelope.error.issues,
input,
{
cause: envelope.error,
userMessage: "Provide at least one comparison Evidence item.",
},
);
const { comparisons } = envelope.data;
const selection = readChangedBehaviorSelection(input);
if (!selection.ok) throw selection.error;
const comparisons = selection.value;
const evidence = comparisons.map((item, index) =>
parseComparisonEvidence(item, input, index),
);
+5 -5
View File
@@ -1,6 +1,9 @@
import { describe, expect, it } from "vitest";
import { ARTIFACT_COMPARISON_EXAMPLE } from "../contracts/artifactComparisonExample.js";
import { INVESTIGATION_EXAMPLES } from "../contracts/investigationExamples.js";
import {
INVESTIGATION_EXAMPLES,
FUNCTION_COMPARISON_EVIDENCE,
} from "../contracts/investigationExamples.js";
import {
artifactComparisonResultSchema,
compareArtifacts,
@@ -20,10 +23,7 @@ import {
staticRuntimeCorrelationResultSchema,
} from "./staticRuntimeCorrelation.js";
const staticComparison =
INVESTIGATION_EXAMPLES.find_changed_behavior.comparisons[0];
if (staticComparison === undefined)
throw new Error("Missing static comparison fixture");
const staticComparison = FUNCTION_COMPARISON_EVIDENCE;
const artifactComparison = createEvidence(
undefined,
+1 -1
View File
@@ -6,5 +6,5 @@ export const PACKAGE_METADATA = {
serverSdkVersion: "2.3.1",
clientSdkVersion: "2.3.1",
coreSdkVersion: "2.3.1",
skillVersion: "57",
skillVersion: "58",
} as const;
+8 -1
View File
@@ -1,4 +1,5 @@
import { resolveInvestigationEvidenceClosure } from "../application/investigation/InvestigationEvidence.js";
import { resolveChangedBehaviorEvidence } from "../application/investigation/ChangedBehaviorEvidence.js";
import type { EvidenceMcpServer } from "./EvidenceMcpServer.js";
import type { BinarySessionPort } from "../application/binary/BinarySessionPort.js";
@@ -68,8 +69,14 @@ const registerChangedBehavior = (
contract.name,
context.mcpReq.signal,
async () => {
const resolved = resolveChangedBehaviorEvidence(
input,
session.evidenceForAnalysis.bind(session),
);
if (!resolved.ok)
return server.delivery.toCallToolResult(resolved, contract);
const computed = await runDerivedOperation(context, contract.name, () =>
findChangedBehavior(input),
findChangedBehavior(resolved.value),
);
if (!computed.ok)
return server.delivery.toCallToolResult(computed, contract);
@@ -0,0 +1,78 @@
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect, it } from "vitest";
import { FUNCTION_COMPARISON_EXAMPLE } from "../../src/contracts/functionComparisonExample.js";
import { FUNCTION_COMPARISON_EVIDENCE } from "../../src/contracts/investigationExamples.js";
import { changedBehaviorResultSchema } from "../../src/domain/changedBehavior.js";
import { createEvidence, parseEvidence } from "../../src/domain/evidence.js";
import { createEvidenceBundle } from "../../src/domain/evidenceBundle.js";
import { connectLocalToolsMcp } from "../fixtures/localToolsMcp.js";
import { parseMcpToolError } from "../fixtures/mcpToolError.js";
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
it("aggregates selected retained observations within a caller's input budget", async () => {
// Distinct authenticated observations may have identical derived contents.
const comparisons = Array.from({ length: 12 }, (_, observation) =>
createEvidence(undefined, FUNCTION_COMPARISON_EVIDENCE.provider, {
operation: "compare_functions",
predicateType: "rea.function-comparison",
parameters: { ...FUNCTION_COMPARISON_EVIDENCE.parameters, observation },
result: FUNCTION_COMPARISON_EVIDENCE.normalized_result,
confidence: "derived",
authority: "analyst-inference",
limitations: FUNCTION_COMPARISON_EVIDENCE.limitations,
evidenceLinks: FUNCTION_COMPARISON_EVIDENCE.evidence_links,
}),
);
const directory = await createTestTempDirectory("rea-retained-comparisons-");
const path = join(directory, "comparisons.json");
await writeFile(
path,
JSON.stringify(
createEvidenceBundle([
FUNCTION_COMPARISON_EXAMPLE.left,
FUNCTION_COMPARISON_EXAMPLE.right,
...comparisons,
]),
),
);
const { call } = await connectLocalToolsMcp({
REA_MCP_MAX_INPUT_ELEMENTS: "1024",
});
const imported = await call("import_evidence_bundle", { path });
expect(imported.isError, JSON.stringify(imported)).not.toBe(true);
const inline = await call("find_changed_behavior", { comparisons });
expect(parseMcpToolError(inline).error).toMatchObject({
code: "resource_constraint",
details: { reported_limits: { max_input_elements: 1024 } },
});
const response = await call("find_changed_behavior", {
comparisons: comparisons.map(({ evidence_id }) => ({
kind: "retained-evidence",
evidence_id,
})),
});
expect(response.isError, JSON.stringify(response)).not.toBe(true);
const aggregate = parseEvidence(response.structuredContent);
const result = changedBehaviorResultSchema.parse(aggregate.normalized_result);
expect(result.behavior_status).toBe("unknown");
expect(result.summary.observed_changes).toBe(0);
expect(aggregate.parameters.comparison_evidence_ids).toEqual(
comparisons.map(({ evidence_id }) => evidence_id),
);
expect(
new Set(
result.findings.items.map(
({ source_comparison_id }) => source_comparison_id,
),
),
).toEqual(new Set(comparisons.map(({ evidence_id }) => evidence_id)));
expect(aggregate.evidence_links).toEqual(
expect.arrayContaining([
FUNCTION_COMPARISON_EXAMPLE.left.evidence_id,
FUNCTION_COMPARISON_EXAMPLE.right.evidence_id,
...comparisons.map(({ evidence_id }) => evidence_id),
]),
);
});
@@ -24,13 +24,12 @@ import {
import { jsonObjectSchema } from "../../../src/domain/jsonValue.js";
import { createServer } from "../../../src/server/createServer.js";
import { observed } from "../../fixtures/analysisExecution.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
import type { RelationshipCoverage } from "../../../src/domain/relationshipCoverage.js";
it("aggregates comparison Evidence and records an approved runtime gap", async () => {
const { session, server, client } = await connected();
const comparison =
INVESTIGATION_EXAMPLES.find_changed_behavior.comparisons[0];
if (comparison === undefined) throw new Error("missing comparison fixture");
const comparison = FUNCTION_COMPARISON_EVIDENCE;
for (const evidence of [
FUNCTION_COMPARISON_EXAMPLE.left,
FUNCTION_COMPARISON_EXAMPLE.right,
@@ -50,6 +49,19 @@ it("aggregates comparison Evidence and records an approved runtime gap", async (
provider: { id: "rea-changed-behavior" },
normalized_result: { behavior_status: "unknown" },
});
const referenced = await client.callTool({
name: "find_changed_behavior",
arguments: {
comparisons: [
{
kind: "retained-evidence",
evidence_id: comparison.evidence_id,
},
],
},
});
expect(referenced.isError, JSON.stringify(referenced)).not.toBe(true);
expect(inlineEvidence(referenced.structuredContent)).toEqual(evidence);
expect(session.listUnknowns({ domain: "changed-behavior" })).toMatchObject([
{
question: `Did both versions behave the same under a complete controlled replay? (Evidence ${evidence.evidence_id})`,
@@ -696,3 +708,167 @@ it("keeps an aggregate function reconstruction unknown while a constituent dimen
await close(session, server, client);
}
});
it.each([
{
name: "empty selection",
input: { comparisons: [] },
code: "invalid_request",
},
{
name: "missing record",
input: {
comparisons: [
{ kind: "retained-evidence", evidence_id: `ev_${"f".repeat(64)}` },
],
},
code: "evidence_integrity_mismatch",
},
{
name: "wrong operation",
input: {
comparisons: [
{
kind: "retained-evidence",
evidence_id: FUNCTION_COMPARISON_EXAMPLE.left.evidence_id,
},
],
},
code: "invalid_request",
},
{
name: "mixed duplicate identity",
input: {
comparisons: [
FUNCTION_COMPARISON_EVIDENCE,
{
kind: "retained-evidence",
evidence_id: FUNCTION_COMPARISON_EVIDENCE.evidence_id,
},
],
},
code: "invalid_request",
},
{
name: "malformed envelope",
input: { comparisons: "not-an-array" },
code: "invalid_request",
},
{
name: "malformed reference",
input: {
comparisons: [{ kind: "retained-evidence", evidence_id: "bad-id" }],
},
code: "invalid_request",
},
{
name: "damaged inline observation",
input: {
comparisons: [
{
kind: "retained-evidence",
evidence_id: FUNCTION_COMPARISON_EVIDENCE.evidence_id,
},
{
...FUNCTION_COMPARISON_EVIDENCE,
normalized_result: { tampered: true },
},
],
},
code: "evidence_integrity_mismatch",
},
])(
"preserves explicit failure for $name in reference aggregation",
async ({ input, code }) => {
let providerCalls = 0;
const { session, server, client } = await connected({
execute: async () => {
providerCalls++;
throw new Error("Retained aggregation must not query a provider");
},
});
try {
for (const source of [
FUNCTION_COMPARISON_EXAMPLE.left,
FUNCTION_COMPARISON_EXAMPLE.right,
FUNCTION_COMPARISON_EVIDENCE,
])
expect(session.recordEvidence(source).ok).toBe(true);
const response = await client.callTool({
name: "find_changed_behavior",
arguments: input,
});
expect(parseMcpToolError(response).error.code).toBe(code);
expect(providerCalls).toBe(0);
} finally {
await close(session, server, client);
}
},
);
it("requires the original source closure even when the comparison reference exists", async () => {
const { session, server, client } = await connected();
try {
expect(session.recordEvidence(FUNCTION_COMPARISON_EVIDENCE).ok).toBe(true);
const response = await client.callTool({
name: "find_changed_behavior",
arguments: {
comparisons: [
{
kind: "retained-evidence",
evidence_id: FUNCTION_COMPARISON_EVIDENCE.evidence_id,
},
],
},
});
expect(parseMcpToolError(response).error).toMatchObject({
code: "evidence_integrity_mismatch",
details: { reason: "missing" },
});
} finally {
await close(session, server, client);
}
});
it("preserves mixed inline and retained runtime/static comparison semantics", async () => {
const { session, server, client } = await connected();
try {
for (const source of [
FUNCTION_COMPARISON_EXAMPLE.left,
FUNCTION_COMPARISON_EXAMPLE.right,
FUNCTION_COMPARISON_EVIDENCE,
PROCESS_CAPTURE_REFERENCE,
PROCESS_CAPTURE_RECONSTRUCTION,
PROCESS_COMPARISON_EVIDENCE,
])
expect(session.recordEvidence(source).ok).toBe(true);
const complete = await client.callTool({
name: "find_changed_behavior",
arguments: {
comparisons: [
FUNCTION_COMPARISON_EVIDENCE,
PROCESS_COMPARISON_EVIDENCE,
],
},
});
expect(complete.isError, JSON.stringify(complete)).not.toBe(true);
const mixed = await client.callTool({
name: "find_changed_behavior",
arguments: {
comparisons: [
FUNCTION_COMPARISON_EVIDENCE,
{
kind: "retained-evidence",
evidence_id: PROCESS_COMPARISON_EVIDENCE.evidence_id,
},
],
},
});
expect(mixed.isError, JSON.stringify(mixed)).not.toBe(true);
expect(inlineEvidence(mixed.structuredContent)).toEqual(
inlineEvidence(complete.structuredContent),
);
} finally {
await close(session, server, client);
}
});