fix(mcp): return complete inline Evidence records (#585)

Return complete canonical Evidence records inline, including normalized_result, and preserve direct comparison and retained bundle identity.

Verified full deterministic gate, real Linux Ghidra CLI/MCP and independently installed artifact, generic package acceptance, private local samples, and targeted updates after syncing main.

Closes #551

Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)
This commit is contained in:
N0zoM1z0 2026-10-06 00:10:31 +08:00 committed by GitHub
parent 205a977777
commit ea9c3dab6b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 253 additions and 31 deletions

View File

@ -55,8 +55,13 @@ shutdown rules still apply; REA never kills a process it cannot prove it owns.
## Tool results
Evidence-producing tools return their result and Evidence context inline. Read
that result directly. Use `get_evidence_bundle` when the task needs broader
Evidence-producing tools return `{ result, evidence_id, evidence }` in both
text and structured content. `evidence` is the complete canonical Evidence
record, including `normalized_result`, which equals `result`. The same record
is retained in the session bundle. Read `result` directly, or pass `evidence`
to a compatible comparison tool: `analyze_function` Evidence can be passed
directly to `compare_functions`, and `inspect_artifact` Evidence to
`compare_artifacts`. Use `get_evidence_bundle` when the task needs broader
retained session history or an explicit bundle for transfer.
## Aggregate native context

View File

@ -545,9 +545,9 @@
"mcp_prompts": 6
},
"digests": {
"tools_sha256": "d898fbdf87a2684a0d7b7cffe0a5ae0b29c7f09651be912b152783738c997373",
"tools_sha256": "dc481fd931e612386c83cea7afe609ff4b1ed57b217f4ed877e5210d7ff3da16",
"prompts_sha256": "4762db348821fec787a5b8c6423f2c48a71f6f0ec2afee97b16ad69030ec504b",
"combined_sha256": "3efc2688f505dd5fee02c730efddb4fbeba79db0ab34c40494a5a1fc55be308b",
"combined_sha256": "9a38caecda4d0d5bae02db49909e2ae3b2560b8bcf0fac8885345097d856f931",
"providers_sha256": "2ae8b85b20be338739e500549d275e78e875aaa10235d2ca283f0ade7787da90"
}
}

View File

@ -1,5 +1,5 @@
{
"manifest_id": "ecm_9218fd2c70d82c981443b36551dfb24e045dcc9811db71a540b1cf6a37651767",
"manifest_id": "ecm_4d23d0a47c714a48078502042f6f7f902e8c591375e5f4c06dbc92d0263a65db",
"verifier": {
"id": "rea-managed-conformance",
"version": "1"
@ -13,7 +13,7 @@
"skill_digests": [
{
"skill_id": "reverse-engineer-anything",
"sha256": "316944ca527c869a3d1b1d1af58b79386d057d8cdd47eb163805b665492f6a55"
"sha256": "dcfaef23bdaf8bc6defab4a2f65d419f53f35c968b83f8abc665dce49453df23"
}
],
"claims": [

View File

@ -9,7 +9,7 @@ import { parseEvidence } from "../../dist/domain/evidence.js";
import { analysisProfileSchema } from "../../dist/domain/analysisProfile.js";
import { nativeValueTraceSchema } from "../../dist/domain/nativeValueTrace.js";
const entrypoint = fileURLToPath(new URL("../rea.mjs", import.meta.url));
const defaultEntrypoint = fileURLToPath(new URL("../rea.mjs", import.meta.url));
const environment = () => ({
PATH: process.env.PATH ?? "/usr/bin:/bin",
REA_LOG_LEVEL: "silent",
@ -67,7 +67,12 @@ const verifyGraph = (input, target, procedure, globalAddress) => {
};
/** Verify real Ghidra dependency composition through production CLI and stdio MCP. */
export async function verifyNativeValueE2e(target, procedure, globalAddress) {
export async function verifyNativeValueE2e(
target,
procedure,
globalAddress,
{ entrypoint = defaultEntrypoint } = {},
) {
const { stdout } = await promisify(execFile)(
process.execPath,
[
@ -142,10 +147,7 @@ export async function verifyNativeValueE2e(target, procedure, globalAddress) {
{ timeout: 180000 },
);
assert.notEqual(result.isError, true, JSON.stringify(result));
const mcpEvidence = parseEvidence({
...result.structuredContent?.evidence,
normalized_result: result.structuredContent?.result,
});
const mcpEvidence = parseEvidence(result.structuredContent?.evidence);
assert.equal(mcpEvidence.operation, "trace_native_values");
assert.equal(mcpEvidence.provider.id, "rea-workflow");
assert.deepEqual(mcpEvidence.analysis_profile, workflowProfile);
@ -157,6 +159,66 @@ export async function verifyNativeValueE2e(target, procedure, globalAddress) {
);
assert.deepEqual(mcpEvidence.normalized_result, mcpGraph);
assert.deepEqual(mcpGraph, cliGraph);
const analyzed = await client.callTool(
{ name: "analyze_function", arguments: { procedure } },
{ timeout: 180000 },
);
assert.notEqual(analyzed.isError, true, JSON.stringify(analyzed));
const functionEvidence = parseEvidence(
analyzed.structuredContent?.evidence,
);
assert.equal(functionEvidence.operation, "analyze_function");
assert.equal(functionEvidence.provider.id, "rea-workflow");
const functionProfile = analysisProfileSchema.parse(
functionEvidence.analysis_profile,
);
assert.deepEqual(
functionProfile.parameters.upstream_analysis_profile,
upstreamProfile,
);
assert.deepEqual(
functionEvidence.normalized_result,
analyzed.structuredContent?.result,
);
const comparison = await client.callTool(
{
name: "compare_functions",
arguments: {
left: analyzed.structuredContent.evidence,
right: analyzed.structuredContent.evidence,
},
},
{ timeout: 180000 },
);
assert.notEqual(comparison.isError, true, JSON.stringify(comparison));
const comparisonEvidence = parseEvidence(
comparison.structuredContent?.evidence,
);
assert.equal(comparisonEvidence.normalized_result.status, "unchanged");
assert.deepEqual(
comparisonEvidence.normalized_result,
comparison.structuredContent?.result,
);
assert.ok(
comparisonEvidence.evidence_links.includes(functionEvidence.evidence_id),
);
const bundle = await client.callTool({
name: "get_evidence_bundle",
arguments: {},
});
assert.notEqual(bundle.isError, true, JSON.stringify(bundle));
for (const evidence of [
mcpEvidence,
functionEvidence,
comparisonEvidence,
]) {
assert.deepEqual(
bundle.structuredContent?.result?.records.find(
(record) => record.evidence_id === evidence.evidence_id,
),
evidence,
);
}
const closed = await client.callTool({
name: "close_binary",
arguments: {},
@ -175,6 +237,8 @@ export async function verifyNativeValueE2e(target, procedure, globalAddress) {
mocked: false,
cli: true,
stdio_mcp: true,
inline_evidence: true,
direct_function_comparison: true,
nodes: cliGraph.total_nodes,
edges: cliGraph.total_edges,
decompilations: cliGraph.decompilations,

View File

@ -1,3 +1,4 @@
import assert from "node:assert/strict";
import { join } from "node:path";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
@ -5,6 +6,7 @@ import { promisify } from "node:util";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { MCP_STARTUP_POLICY } from "../dist/mcpStartupPolicy.js";
import { parseEvidence } from "../dist/domain/evidence.js";
import * as prompts from "./verify-package-prompts.mjs";
import { json, verifyCompleteToolCatalog } from "./lib/verify-package-core.mjs";
@ -155,8 +157,77 @@ const verifyMcpEvidenceBundle = async (client, mcpOptions, evidenceRoot) => {
throw new Error("packaged MCP evidence import failed");
};
/** Connect to the packaged MCP server and exercise the target-free catalog. */
export async function verifyPackageMcp({ cli, environment, evidenceRoot }) {
const verifyMcpInlineArtifactEvidence = async (
client,
mcpOptions,
artifactArchive,
) => {
const opened = await client.callTool(
{ name: "open_binary", arguments: { path: artifactArchive } },
mcpOptions,
);
assert.notEqual(opened.isError, true, JSON.stringify(opened));
try {
const inspected = await client.callTool(
{ name: "inspect_artifact", arguments: {} },
mcpOptions,
);
assert.notEqual(inspected.isError, true, JSON.stringify(inspected));
const source = parseEvidence(inspected.structuredContent?.evidence);
assert.deepEqual(
source.normalized_result,
inspected.structuredContent?.result,
);
assert.deepEqual(
json(prompts.mcpText(inspected)),
inspected.structuredContent,
);
const compared = await client.callTool(
{
name: "compare_artifacts",
arguments: {
left: inspected.structuredContent.evidence,
right: inspected.structuredContent.evidence,
},
},
mcpOptions,
);
assert.notEqual(compared.isError, true, JSON.stringify(compared));
const comparison = parseEvidence(compared.structuredContent?.evidence);
assert.deepEqual(
comparison.normalized_result,
compared.structuredContent?.result,
);
assert.ok(comparison.evidence_links.includes(source.evidence_id));
const bundle = await client.callTool(
{ name: "get_evidence_bundle", arguments: {} },
mcpOptions,
);
assert.notEqual(bundle.isError, true, JSON.stringify(bundle));
for (const evidence of [source, comparison]) {
assert.deepEqual(
bundle.structuredContent?.result?.records.find(
(record) => record.evidence_id === evidence.evidence_id,
),
evidence,
);
}
} finally {
const closed = await client.callTool(
{ name: "close_binary", arguments: {} },
mcpOptions,
);
assert.notEqual(closed.isError, true, JSON.stringify(closed));
}
};
/** Connect to the packaged MCP server and verify catalog and Evidence composition. */
export async function verifyPackageMcp({
cli,
environment,
evidenceRoot,
artifactArchive,
}) {
const diagnosed = json(
(
await execute(cli, ["mcp", "doctor", "--json"], {
@ -191,6 +262,7 @@ export async function verifyPackageMcp({ cli, environment, evidenceRoot }) {
await verifyMcpTargetFree(client, mcpOptions);
await verifyMcpUnknownProvider(client, mcpOptions);
await verifyMcpBinaryLifecycle(client, mcpOptions);
await verifyMcpInlineArtifactEvidence(client, mcpOptions, artifactArchive);
await verifyMcpEvidenceBundle(client, mcpOptions, evidenceRoot);
} catch (cause) {
throw new Error(`packaged MCP smoke failed: ${mcpStderr}`, { cause });

View File

@ -84,7 +84,7 @@ try {
cli,
environment: environmentData.environment,
});
await verifyPackageArtifactAndElectron({
const { artifactArchive } = await verifyPackageArtifactAndElectron({
cli,
workspace,
environment: environmentData.environment,
@ -131,6 +131,7 @@ try {
cli,
environment: environmentData.environment,
evidenceRoot,
artifactArchive,
});
process.stdout.write(
`${JSON.stringify({ verifier_run: await completeVerifierRun(verifierRun), cli: true, analysisCli: true, artifactCli: true, managedCli: true, managedReconstructionCli: true, managedNativeVerificationCli: true, managedApplicationGraphCli: true, evidenceCli: true, incurMcpCommand: PRODUCT_IDENTITY.mcpCommand, lifecycleScriptsRequired: false, doctor: "platform-appropriate", setup: supportedSetupHost ? "planned-then-idempotent" : "unsupported-host-rejected", setupPlanReadOnly: supportedSetupHost, existingHopperPreserved: supportedSetupHost, clients: supportedSetupHost ? 4 : 0, backupReadback: supportedSetupHost, failureRecovery: supportedSetupHost, configSymlinkLifecycle: supportedSetupHost, skill: supportedSetupHost, skillReferences: supportedSetupHost, mcpTools: TOOL_CONTRACTS.length, mcpPrompts: prompts.names.length, promptCompletion: true, promptCompletionLifecycle: true, evidenceMcp: true, targetFree: true, targetLifecycle: true, boundedRegexBridge: true, update, mcpStartup, mcpModuleLoading })}\n`,

View File

@ -4,7 +4,7 @@ description: Reverse engineer native, managed, Electron/JavaScript, packaged, an
metadata:
version: "23"
tool_count: 116
catalog_digest: "3efc2688f505dd5fee02c730efddb4fbeba79db0ab34c40494a5a1fc55be308b"
catalog_digest: "9a38caecda4d0d5bae02db49909e2ae3b2560b8bcf0fac8885345097d856f931"
---
# REA

View File

@ -18,9 +18,7 @@ import { analysisErrorProjectionSchema } from "./errorSchemas.js";
import { prefixedDigestSchema } from "./../domain/digests.js";
/** Inline result with its complete Evidence record. */
export const inlineEvidenceRecordSchema = evidenceEnvelopeSchema.omit({
normalized_result: true,
});
export const inlineEvidenceRecordSchema = evidenceEnvelopeSchema;
export const evidenceResultOf = (schema: z.ZodType) =>
z.strictObject({

File diff suppressed because one or more lines are too long

View File

@ -6,7 +6,7 @@ import type { JsonValue } from "../domain/jsonValue.js";
import type { Result } from "../domain/result.js";
/**
* Translate an application result into MCP text content.
* Serialize an application result as MCP text and structured content.
* Error tags and safe messages remain visible while underlying causes, process
* output, and other potentially sensitive details stay private.
*/
@ -36,7 +36,7 @@ const successResult = (
contract: ToolContract,
): CallToolResult => {
const candidate =
compactEvidence(value) ??
projectEvidence(value) ??
(contract.kind === "session" ? { result: value } : value);
return {
content: [
@ -49,7 +49,7 @@ const successResult = (
};
};
const compactEvidence = (value: JsonValue): JsonValue | undefined => {
const projectEvidence = (value: JsonValue): JsonValue | undefined => {
if (
typeof value !== "object" ||
value === null ||
@ -59,17 +59,13 @@ const compactEvidence = (value: JsonValue): JsonValue | undefined => {
!("normalized_result" in value)
)
return undefined;
const evidence = value as Record<string, JsonValue>;
const normalizedResult = evidence.normalized_result;
const evidenceId = evidence.evidence_id;
const normalizedResult = value.normalized_result;
const evidenceId = value.evidence_id;
if (normalizedResult === undefined || typeof evidenceId !== "string")
return undefined;
const inlineEvidence = Object.fromEntries(
Object.entries(evidence).filter(([key]) => key !== "normalized_result"),
);
return {
result: normalizedResult,
evidence_id: evidenceId,
evidence: inlineEvidence,
evidence: value,
};
};

View File

@ -0,0 +1,49 @@
import { describe, expect, it } from "vitest";
import { toolContract } from "../../../src/contracts/toolContracts.js";
import { parseEvidence } from "../../../src/domain/evidence.js";
import { parseEvidenceBundle } from "../../../src/domain/evidenceBundle.js";
import { connectGhidraMcp } from "./ghidraMcpHarness.js";
describe("inline MCP Evidence composition", () => {
it("feeds returned function Evidence directly to comparison and matches the bundle", async () => {
const harness = await connectGhidraMcp("inline-function-evidence");
const { mcp, session } = harness;
try {
const response = await mcp.callTool({
name: "analyze_function",
arguments: { procedure: "fixture_main" },
});
expect(response.isError).not.toBe(true);
const analyzed = toolContract("analyze_function").outputSchema.parse(
response.structuredContent,
);
const comparison = await mcp.callTool({
name: "compare_functions",
arguments: { left: analyzed.evidence, right: analyzed.evidence },
});
expect(comparison.isError).not.toBe(true);
const inline = parseEvidence(analyzed.evidence);
expect(inline.normalized_result).toEqual(analyzed.result);
expect(session.evidenceById(inline.evidence_id)).toEqual(inline);
const compared = toolContract("compare_functions").outputSchema.parse(
comparison.structuredContent,
);
const comparisonEvidence = parseEvidence(compared.evidence);
expect(comparisonEvidence.evidence_links).toContain(inline.evidence_id);
expect(comparisonEvidence.normalized_result).toEqual(compared.result);
const bundle = await mcp.callTool({
name: "get_evidence_bundle",
arguments: {},
});
const exported = toolContract("get_evidence_bundle").outputSchema.parse(
bundle.structuredContent,
);
const retained = parseEvidenceBundle(exported.result);
expect(retained.records).toContainEqual(inline);
expect(retained.records).toContainEqual(comparisonEvidence);
} finally {
await harness.close();
}
});
});

View File

@ -6,7 +6,8 @@ import { ok } from "../../../src/domain/result.js";
import { err } from "../../../src/domain/result.js";
import { HopperProcessError } from "../../../src/domain/errors.js";
import { toCallToolResult } from "../../../src/server/toolResult.js";
import { createEvidence } from "../../../src/domain/evidence.js";
import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js";
import type { JsonValue } from "../../../src/domain/jsonValue.js";
import { evidenceResultOf } from "../../../src/contracts/toolOutputSchemas.js";
const contract: ToolContract = {
@ -102,6 +103,7 @@ describe("tool result projection", () => {
result: { value: "observed" },
evidence_id: evidence.evidence_id,
evidence: {
normalized_result: { value: "observed" },
provider: { id: "fixture", name: "Fixture", version: "1" },
operation: "fixture",
predicate_type: "rea.analysis",
@ -119,5 +121,40 @@ describe("tool result projection", () => {
type: "text",
text: expect.stringContaining('"value":"observed"'),
});
const parsed = evidenceContract.outputSchema.parse(
result.structuredContent,
);
expect(parseEvidence(parsed.evidence)).toEqual(evidence);
});
it.each<JsonValue>([null, false, 0, "", [], { nested: [false, null, 7] }])(
"preserves a complete reusable Evidence record for JSON result %j",
(value) => {
const evidence = createEvidence(
undefined,
{ id: "fixture", name: "Fixture", version: "1" },
{ operation: "fixture", parameters: {}, result: value },
);
const evidenceContract = {
...contract,
outputSchema: evidenceResultOf(z.json()),
};
const result = toCallToolResult(ok(evidence), evidenceContract);
const parsed = evidenceContract.outputSchema.parse(
result.structuredContent,
);
expect(parsed.result).toEqual(value);
expect(parseEvidence(parsed.evidence)).toEqual(evidence);
expect(result.content).toEqual([
{ type: "text", text: JSON.stringify(parsed) },
]);
const { normalized_result: _missingResult, ...incomplete } = evidence;
expect(
evidenceContract.outputSchema.safeParse({
...parsed,
evidence: incomplete,
}).success,
).toBe(false);
},
);
});