feat(evidence): accept exact retained application references (#734)

This commit is contained in:
N0zoM1z0
2026-10-06 20:17:59 +08:00
committed by GitHub
parent 00eb03d0f2
commit d0f3480a3c
21 changed files with 569 additions and 36 deletions
+41
View File
@@ -136,6 +136,47 @@ 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.
## Retained application Evidence inputs
`trace_application_feature`, `trace_javascript_semantics`,
`compare_application_versions`, `compare_source_to_bundle`, and
`compare_javascript_export_shapes` accept complete inline application Evidence
or an exact reference to a record already retained by the current connection:
```json
{
"name": "trace_application_feature",
"arguments": {
"application": {
"kind": "retained-evidence",
"evidence_id": "ev_<64 lowercase hex characters>"
},
"seed": { "kind": "module", "value": "search.js", "match": "exact" }
}
}
```
Use the `evidence_id` returned by `analyze_javascript_application` (or another
compatible application-graph producer). Comparisons accept this form in `left`
and `right`; each side can independently be inline or retained. Native
observation arrays continue to take complete inline Evidence. Results and their
Evidence remain complete inline, and both input forms pass the same semantic,
identity, authority, and provenance checks without running the producer again.
References belong to the current connection's Evidence ledger. Opening another
target preserves retained records; `close_binary` clears them, even when no
binary is active. A fresh connection has its own ledger. A missing reference
reports its exact ID and `details.reason: "missing"`; the server cannot infer
whether it was never recorded, cleared, or retained by another connection.
Supply complete inline Evidence, repeat its producer, or import an exported
Evidence bundle before referencing that imported record. Export a bundle before
closing if the investigation needs it later.
CLI application workflows continue to read portable inline Evidence from files
and run the same analysis workflows; a standalone CLI invocation cannot resolve
another MCP connection's retained records. No additional lookup call, provider
selection, or approval step is required for a same-session follow-up.
## Aggregate native context
`get_navigation_context` composes the selected document, current address, and
+2 -2
View File
@@ -561,9 +561,9 @@
"mcp_prompts": 6
},
"digests": {
"tools_sha256": "bbd9aacf7bb0bc54a8f8743491583482e1176252513d99780a309d0fddeb3df0",
"tools_sha256": "270e49d736a98880133a74f356e4c568db7d7adc55fd576358c9da71017485ad",
"prompts_sha256": "ef2de8814390b340623c32001610aa2709d90bed51537b6e562ab3abf144ab4d",
"combined_sha256": "8778381e23b001485ddab201a9a41bdc3c2a0631ccadb1afa83371f6c2fcbe6c",
"combined_sha256": "88cedc97288ecd044f4257a670a6707ca474906de73cab3f5bf436bff24f0cc7",
"providers_sha256": "e094c361b60ec930a6062a1a90ee05536ce411541dba5b51ace7e2da2ba25af0"
}
}
@@ -1,5 +1,5 @@
{
"manifest_id": "ecm_c47a9e9f524d47b6eee9639d0cda8bba3c4d1a4dd0d6f38d15b5b9ef2a9c765e",
"manifest_id": "ecm_4d2054e0122c65b7527e18887808279916b93eb25a0fa4b22a368d390b419322",
"verifier": {
"id": "rea-managed-conformance",
"version": "1"
@@ -13,7 +13,7 @@
"skill_digests": [
{
"skill_id": "reverse-engineer-anything",
"sha256": "3912bba1d6a2269d514b913d8cb0f0c64d2621d5f225904a3194d0d4a7aecf80"
"sha256": "f921b1664b83f82849524d8b562b29746d82e64071c4d4689b098bfc7bd949de"
}
],
"claims": [
+1 -1
View File
@@ -4,7 +4,7 @@ description: Reverse engineer native, managed, Electron/JavaScript, packaged, an
metadata:
version: "24"
tool_count: 118
catalog_digest: "8778381e23b001485ddab201a9a41bdc3c2a0631ccadb1afa83371f6c2fcbe6c"
catalog_digest: "88cedc97288ecd044f4257a670a6707ca474906de73cab3f5bf436bff24f0cc7"
---
# REA
@@ -34,3 +34,14 @@ variants, and incomplete parent-property coverage stay unknown. This is static
inference, not runtime behavior. When runtime semantics are needed, run
behavioral probes against the relevant application versions and capture them
through the available browser, Electron, or process workflows.
## Reusing application Evidence
Application trace and compare tools accept complete inline Evidence or
`{"kind":"retained-evidence","evidence_id":"ev_<64 lowercase hex characters>"}`
for their application input (`application`, or `left`/`right`). Use the exact ID
returned by the producer on the same MCP connection. Resolution does not run
analysis or select a provider; findings remain inline. Native observation
arrays still take full Evidence. `close_binary` clears retained references;
export a bundle before closing, or supply portable inline Evidence in another
connection. A missing reference includes its ID and recovery guidance.
@@ -0,0 +1,80 @@
import { describe, expect, it } from "vitest";
import { evidenceInputSchema } from "../contracts/evidenceInputContracts.js";
import { JAVASCRIPT_FEATURE_TRACE_EXAMPLE } from "../contracts/javascriptApplicationWorkflowExamples.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { EvidenceLedger } from "./EvidenceLedger.js";
import { resolveEvidenceInput } from "./EvidenceInputResolver.js";
const evidence = JAVASCRIPT_FEATURE_TRACE_EXAMPLE.application;
const reference = {
kind: "retained-evidence" as const,
evidence_id: evidence.evidence_id,
};
describe("exact Evidence input resolution", () => {
it("uses the session's detached canonical record without modifying it", () => {
const ledger = new EvidenceLedger();
expect(ledger.record(evidence).ok).toBe(true);
const result = resolveEvidenceInput(reference, (id) => ledger.get(id));
if (!result.ok) throw result.error;
expect(result.value).toEqual(evidence);
result.value.limitations.push("caller-owned change");
expect(ledger.get(evidence.evidence_id)).toEqual(evidence);
});
it("leaves portable inline inputs independent of session lookup", () => {
expect(
resolveEvidenceInput(evidence, () => {
throw new Error("unexpected lookup");
}),
).toEqual({ ok: true, value: evidence });
});
it("reports a missing reference and an available recovery", () => {
for (const lookup of [undefined, () => undefined]) {
const result = resolveEvidenceInput(reference, lookup);
if (result.ok) throw new Error("unknown reference was resolved");
const projected = projectAnalysisError(result.error);
expect(projected).toMatchObject({
details: {
evidence_id: reference.evidence_id,
reason: "missing",
actual: null,
},
});
expect(projected.message).toContain("not retained in this session");
expect(projected.remediation.action).toContain(
"complete inline Evidence",
);
}
});
it("rejects a lookup returning another record rather than guessing identity", () => {
const otherId = `ev_${"f".repeat(64)}`;
const result = resolveEvidenceInput(
{ ...reference, evidence_id: otherId },
() => evidence,
);
if (result.ok) throw new Error("mismatched reference was resolved");
expect(projectAnalysisError(result.error)).toMatchObject({
details: {
reason: "identity_mismatch",
expected: otherId,
actual: evidence.evidence_id,
},
});
});
it("parses only complete inline records or strict exact references", () => {
expect(evidenceInputSchema.safeParse(reference).success).toBe(true);
expect(evidenceInputSchema.safeParse(evidence).success).toBe(true);
for (const invalid of [
{ evidence_id: evidence.evidence_id },
{ ...reference, kind: "another-session" },
{ ...reference, evidence_id: "ev_short" },
{ ...reference, session_id: "foreign" },
{ ...reference, normalized_result: {} },
])
expect(evidenceInputSchema.safeParse(invalid).success).toBe(false);
});
});
+70
View File
@@ -0,0 +1,70 @@
import type { EvidenceInput } from "../contracts/evidenceInputContracts.js";
import type { Evidence } from "../domain/evidence.js";
import { EvidenceReferenceError } from "../domain/evidenceErrors.js";
import { err, ok, type Result } from "../domain/result.js";
/** Read-only lookup supplied by the current investigation's Evidence ledger. */
export type EvidenceLookup = (evidenceId: string) => Evidence | undefined;
/** Resolve an exact reference without selecting a target or running analysis. */
export const resolveEvidenceInput = (
input: EvidenceInput,
lookup: EvidenceLookup | undefined,
): Result<Evidence, EvidenceReferenceError> => {
if (!("kind" in input)) return ok(input);
const record = lookup?.(input.evidence_id);
if (record === undefined)
return err(
new EvidenceReferenceError(
input.evidence_id,
"missing",
"Evidence retained in this session",
null,
),
);
if (record.evidence_id !== input.evidence_id)
return err(
new EvidenceReferenceError(
input.evidence_id,
"identity_mismatch",
input.evidence_id,
record.evidence_id,
),
);
return ok(record);
};
/** Resolve an application's Evidence before its existing semantic validation. */
export const resolveApplicationEvidenceRequest = <
T extends { readonly application: EvidenceInput },
>(
input: T,
lookup: EvidenceLookup | undefined,
): Result<
Omit<T, "application"> & { readonly application: Evidence },
EvidenceReferenceError
> => {
const application = resolveEvidenceInput(input.application, lookup);
if (!application.ok) return application;
return ok({ ...input, application: application.value });
};
/** Resolve explicitly paired Evidence without guessing another session's record. */
export const resolvePairedEvidenceRequest = <
T extends { readonly left: EvidenceInput; readonly right: EvidenceInput },
>(
input: T,
lookup: EvidenceLookup | undefined,
): Result<
Omit<T, "left" | "right"> & {
readonly left: Evidence;
readonly right: Evidence;
},
EvidenceReferenceError
> => {
const left = resolveEvidenceInput(input.left, lookup);
if (!left.ok) return left;
const right = resolveEvidenceInput(input.right, lookup);
if (!right.ok) return right;
return ok({ ...input, left: left.value, right: right.value });
};
+42 -5
View File
@@ -43,6 +43,10 @@ const reconstructionObligationLedgerOutputSchema = evidenceResultOf(
reconstructionObligationLedgerSchema,
);
const HASH = "0".repeat(64);
const retained = (evidence_id: string) => ({
kind: "retained-evidence" as const,
evidence_id,
});
/** Provider-neutral graph workflow contracts shared by MCP and CLI adapters. */
export const APPLICATION_TOOL_CONTRACTS = [
@@ -50,7 +54,7 @@ export const APPLICATION_TOOL_CONTRACTS = [
name: "trace_application_feature",
...toolContractMetadata("trace_application_feature"),
description:
"Trace a typed literal seed through every reachable part of an authenticated JavaScript Application Graph supplied as inline Evidence. Original static, native, passive-runtime, inferred, and unknown authorities remain distinct; native addon handoffs never open a provider or execute the application.",
"Trace a typed literal seed through every reachable part of an authenticated JavaScript Application Graph supplied as inline Evidence or exact same-session retained references. Original static, native, passive-runtime, inferred, and unknown authorities remain distinct; native addon handoffs never open a provider or execute the application.",
kind: "application",
inputSchema: traceApplicationFeatureRequestSchema,
outputSchema: traceOutputSchema,
@@ -59,13 +63,22 @@ export const APPLICATION_TOOL_CONTRACTS = [
title: "Trace one module seed through a retained application graph",
input: JAVASCRIPT_FEATURE_TRACE_EXAMPLE,
},
{
title: "Use exact Evidence already retained in this session",
input: {
...JAVASCRIPT_FEATURE_TRACE_EXAMPLE,
application: retained(
JAVASCRIPT_FEATURE_TRACE_EXAMPLE.application.evidence_id,
),
},
},
],
},
{
name: "trace_javascript_semantics",
...toolContractMetadata("trace_javascript_semantics"),
description:
"Trace static JavaScript data-flow, direct call/return, and closure relations from inline application Evidence. Dynamic or unsupported semantics remain explicit unknowns; static reachability never claims runtime execution.",
"Trace static JavaScript data-flow, direct call/return, and closure relations from inline application Evidence or an exact same-session retained reference. Dynamic or unsupported semantics remain explicit unknowns; static reachability never claims runtime execution.",
kind: "application",
inputSchema: traceJavaScriptSemanticsRequestSchema,
outputSchema: semanticTraceOutputSchema,
@@ -86,7 +99,7 @@ export const APPLICATION_TOOL_CONTRACTS = [
name: "compare_application_versions",
...toolContractMetadata("compare_application_versions"),
description:
"Compare two authenticated JavaScript Application Graph versions supplied as inline Evidence. Uses unique-only exact digest, module source digest, source-map identity, structural fingerprint, and non-module semantic-key tiers. Reports added, removed, changed, ambiguous, and unknown entities plus the complete matching changed_from graph without fuzzy or module-ordinal pairing.",
"Compare two authenticated JavaScript Application Graph versions supplied as inline Evidence or exact same-session retained references. Uses unique-only exact digest, module source digest, source-map identity, structural fingerprint, and non-module semantic-key tiers. Reports added, removed, changed, ambiguous, and unknown entities plus the complete matching changed_from graph without fuzzy or module-ordinal pairing.",
kind: "application",
inputSchema: compareApplicationVersionsRequestSchema,
outputSchema: comparisonOutputSchema,
@@ -95,13 +108,25 @@ export const APPLICATION_TOOL_CONTRACTS = [
title: "Compare authenticated static and reconciled application graphs",
input: JAVASCRIPT_APPLICATION_VERSION_COMPARISON_EXAMPLE,
},
{
title: "Use exact Evidence already retained in this session",
input: {
...JAVASCRIPT_APPLICATION_VERSION_COMPARISON_EXAMPLE,
left: retained(
JAVASCRIPT_APPLICATION_VERSION_COMPARISON_EXAMPLE.left.evidence_id,
),
right: retained(
JAVASCRIPT_APPLICATION_VERSION_COMPARISON_EXAMPLE.right.evidence_id,
),
},
},
],
},
{
name: "compare_source_to_bundle",
...toolContractMetadata("compare_source_to_bundle"),
description:
"Compare a cryptographically committed HistoricalSourceGraph with inline authenticated JavaScript Application Graph Evidence. Uses explicit exact-digest, source-map path, current-path, suffix, and basename signals with stable weights. Classifies unchanged, modified, removed, split, merged, duplicated, and unknown; incomplete coverage and ambiguous weak signals never become absence or forced matches.",
"Compare a cryptographically committed HistoricalSourceGraph with authenticated JavaScript Application Graph Evidence supplied inline or by exact same-session retained reference. Uses explicit exact-digest, source-map path, current-path, suffix, and basename signals with stable weights. Classifies unchanged, modified, removed, split, merged, duplicated, and unknown; incomplete coverage and ambiguous weak signals never become absence or forced matches.",
kind: "application",
inputSchema: compareSourceToBundleRequestSchema,
outputSchema: sourceToBundleOutputSchema,
@@ -117,7 +142,7 @@ export const APPLICATION_TOOL_CONTRACTS = [
name: "compare_javascript_export_shapes",
...toolContractMetadata("compare_javascript_export_shapes"),
description:
"Compare static return shapes for one exact module/export selector on each authenticated JavaScript Application Graph supplied as inline Evidence. Variants pair only by reciprocal unique literal discriminants; dynamic values, incomplete properties, and ambiguous variants remain unknown. Reports JSON Pointer changes without executing JavaScript; runtime behavior requires a separate agent-run probe.",
"Compare static return shapes for one exact module/export selector on each authenticated JavaScript Application Graph supplied as inline Evidence or exact same-session retained references. Variants pair only by reciprocal unique literal discriminants; dynamic values, incomplete properties, and ambiguous variants remain unknown. Reports JSON Pointer changes without executing JavaScript; runtime behavior requires a separate agent-run probe.",
kind: "application",
inputSchema: compareJavaScriptExportShapesRequestSchema,
outputSchema: exportShapeComparisonOutputSchema,
@@ -126,6 +151,18 @@ export const APPLICATION_TOOL_CONTRACTS = [
title: "Compare one exact parser export without execution",
input: JAVASCRIPT_EXPORT_SHAPE_COMPARISON_EXAMPLE,
},
{
title: "Use exact Evidence already retained in this session",
input: {
...JAVASCRIPT_EXPORT_SHAPE_COMPARISON_EXAMPLE,
left: retained(
JAVASCRIPT_EXPORT_SHAPE_COMPARISON_EXAMPLE.left.evidence_id,
),
right: retained(
JAVASCRIPT_EXPORT_SHAPE_COMPARISON_EXAMPLE.right.evidence_id,
),
},
},
],
},
{
@@ -1,6 +1,6 @@
import { z } from "zod";
import { evidenceSchema } from "../domain/evidence.js";
import { evidenceInputSchema } from "./evidenceInputContracts.js";
import { compareApplicationVersionsInputSchema } from "../domain/javascriptApplicationVersionComparisonSchemas.js";
import { compareJavaScriptExportShapesInputSchema } from "../domain/javascriptExportShapeComparisonSchemas.js";
import { traceApplicationFeatureInputSchema } from "../domain/javascriptFeatureTraceSchemas.js";
@@ -14,15 +14,15 @@ const traceApplicationFeatureFacts = {
direction: traceApplicationFeatureInputSchema.shape.direction,
} as const;
/** MCP/CLI trace request carrying all Evidence inline. */
/** Application trace request accepting inline or same-session application Evidence. */
export const traceApplicationFeatureRequestSchema = z.strictObject({
...traceApplicationFeatureFacts,
application: evidenceSchema,
application: evidenceInputSchema,
});
/** MCP/CLI semantic trace request carrying Evidence inline. */
/** Semantic trace request accepting inline or same-session application Evidence. */
export const traceJavaScriptSemanticsRequestSchema = z.strictObject({
application: evidenceSchema,
application: evidenceInputSchema,
query: javaScriptSemanticQueryInputSchema,
});
@@ -33,21 +33,21 @@ const compareApplicationVersionsFacts = {
compareApplicationVersionsInputSchema.shape.right_native_observations,
} as const;
/** MCP/CLI comparison request carrying all Evidence inline. */
/** Application comparison request accepting inline or same-session application Evidence. */
export const compareApplicationVersionsRequestSchema = z.strictObject({
...compareApplicationVersionsFacts,
left: evidenceSchema,
right: evidenceSchema,
left: evidenceInputSchema,
right: evidenceInputSchema,
});
const compareSourceToBundleFacts = {
reference: compareSourceToBundleInputSchema.shape.reference,
} as const;
/** Historical-source comparison carrying application Evidence inline. */
/** Historical-source comparison accepting inline or same-session application Evidence. */
export const compareSourceToBundleRequestSchema = z.strictObject({
...compareSourceToBundleFacts,
application: evidenceSchema,
application: evidenceInputSchema,
});
const compareJavaScriptExportShapesFacts = {
@@ -61,9 +61,9 @@ const compareJavaScriptExportShapesFacts = {
compareJavaScriptExportShapesInputSchema.shape.right_export_name,
} as const;
/** MCP/CLI export-shape request carrying application Evidence inline. */
/** Export-shape request accepting inline or same-session application Evidence. */
export const compareJavaScriptExportShapesRequestSchema = z.strictObject({
...compareJavaScriptExportShapesFacts,
left: evidenceSchema,
right: evidenceSchema,
left: evidenceInputSchema,
right: evidenceInputSchema,
});
+18
View File
@@ -0,0 +1,18 @@
import { z } from "zod";
import { evidenceSchema } from "../domain/evidence.js";
import { prefixedDigestSchema } from "../domain/digests.js";
/** Exact reference to Evidence already retained by the current session. */
export const retainedEvidenceReferenceSchema = z.strictObject({
kind: z.literal("retained-evidence"),
evidence_id: prefixedDigestSchema("ev"),
});
/** Portable inline Evidence or an exact, session-scoped reference. */
export const evidenceInputSchema = z.union([
evidenceSchema,
retainedEvidenceReferenceSchema,
]);
/** Evidence input after parsing the advertised boundary contract. */
export type EvidenceInput = z.output<typeof evidenceInputSchema>;
+11 -1
View File
@@ -6,7 +6,11 @@ import {
NoBinaryOpenError,
} from "./configurationErrors.js";
import { BrowserObservationError } from "./browserObservationError.js";
import { EvidenceFileError, EvidenceIntegrityError } from "./evidenceErrors.js";
import {
EvidenceFileError,
EvidenceIntegrityError,
EvidenceReferenceError,
} from "./evidenceErrors.js";
import {
HopperRemoteError,
HopperProcessError,
@@ -36,6 +40,8 @@ export const analysisErrorRemediationAction = (
: `Use the active REA session ${error.ownerRunId} or close it before opening this target again.`;
if (error instanceof HopperRemoteError)
return "Review the Hopper diagnostic details; correct the request or retry if the failure was transient.";
if (error instanceof EvidenceReferenceError)
return "Use an exact Evidence reference retained by this session, supply the complete inline Evidence, re-run its producer, or import its Evidence bundle. close_binary clears retained records.";
if (
error instanceof BinaryTargetError &&
error.constraint === "directory_requires_file"
@@ -122,6 +128,10 @@ export const analysisErrorUserMessage = (error: AnalysisError): string => {
if (standardMessage !== undefined) return standardMessage;
if (error instanceof ArtifactOperationError)
return artifactMessage(error.reason);
if (error instanceof EvidenceReferenceError)
return error.reason === "missing"
? `Evidence ${error.evidenceId} is not retained in this session. Supply complete inline Evidence, re-run its producer, or import its bundle before using this reference.`
: `Evidence ${error.evidenceId} does not match the requested reference (${error.reason}). Check the expected and actual identity in the diagnostic details.`;
if (error instanceof EvidenceIntegrityError)
return "Evidence is invalid or has changed. Recreate or re-import it, then try again.";
if (error instanceof EvidenceFileError)
+5 -1
View File
@@ -9,7 +9,11 @@ export class EvidenceIntegrityError extends AnalysisError {
export class EvidenceReferenceError extends EvidenceIntegrityError {
constructor(
readonly evidenceId: string,
readonly reason: "missing" | "wrong_operation" | "wrong_predicate",
readonly reason:
| "missing"
| "wrong_operation"
| "wrong_predicate"
| "identity_mismatch",
readonly expected: string,
readonly actual: string | null,
) {
File diff suppressed because one or more lines are too long
+13 -2
View File
@@ -93,14 +93,19 @@ export const createServer = (
const server = createMcpServer(session);
const availability = installSessionToolAvailability(server, session, options);
const toolLogger = logger.child({ layer: "server" });
const { activeTarget, recordEvidence, recordEvidenceWithUnknown } =
createSessionRecorders(server, session);
const {
evidenceById,
activeTarget,
recordEvidence,
recordEvidenceWithUnknown,
} = createSessionRecorders(server, session);
const toolContext: ServerToolContext = {
server,
analysis,
session,
options,
logger: toolLogger,
evidenceById,
activeTarget,
recordEvidence,
recordEvidenceWithUnknown,
@@ -124,6 +129,10 @@ const createSessionRecorders = (
server: McpServer,
session: BinarySessionPort | undefined,
) => ({
evidenceById:
session === undefined
? undefined
: (evidenceId: string) => session.evidenceById(evidenceId),
activeTarget:
session === undefined ? undefined : () => session.activeTarget(),
recordEvidence:
@@ -198,6 +207,7 @@ const registerBinaryAnalysisTools = ({
};
const registerObservationTools = ({
evidenceById,
server,
options,
logger,
@@ -223,6 +233,7 @@ const registerObservationTools = ({
runtime: options.javascriptRuntimeObservation,
});
registerApplicationTools(server, {
evidenceById,
logger,
recordEvidence,
recordEvidenceWithUnknown,
@@ -1,3 +1,4 @@
import { resolvePairedEvidenceRequest } from "../../application/EvidenceInputResolver.js";
import { recordSessionEvidenceSources } from "../sessionEvidence.js";
import type { McpServer } from "@modelcontextprotocol/server";
@@ -21,7 +22,12 @@ export const registerCompareJavaScriptExportShapesTool = (
contract.name,
toolRegistrationOptions(contract),
async (input) => {
const parsed = input;
const resolved = resolvePairedEvidenceRequest(
input,
options.evidenceById,
);
if (!resolved.ok) return toCallToolResult(resolved, contract);
const parsed = resolved.value;
const result = await logToolExecution(options.logger, contract.name, () =>
Promise.resolve(compareJavaScriptExportShapesEvidenceValidated(parsed)),
);
@@ -1,3 +1,4 @@
import { resolveApplicationEvidenceRequest } from "../../application/EvidenceInputResolver.js";
import { recordSessionEvidenceSources } from "../sessionEvidence.js";
import type { McpServer } from "@modelcontextprotocol/server";
@@ -21,12 +22,18 @@ export const registerCompareSourceToBundleTool = (
contract.name,
toolRegistrationOptions(contract),
async (input) => {
const resolved = resolveApplicationEvidenceRequest(
input,
options.evidenceById,
);
if (!resolved.ok) return toCallToolResult(resolved, contract);
const parsed = resolved.value;
const result = await logToolExecution(options.logger, contract.name, () =>
Promise.resolve(compareSourceToBundleEvidenceValidated(input)),
Promise.resolve(compareSourceToBundleEvidenceValidated(parsed)),
);
if (!result.ok) return toCallToolResult(result, contract);
const recorded = recordSessionEvidenceSources(options.recordEvidence, [
input.application,
parsed.application,
]);
if (!recorded.ok) return toCallToolResult(recorded, contract);
const comparison = sourceToBundleComparisonResultSchema.parse(
@@ -1,3 +1,4 @@
import { resolvePairedEvidenceRequest } from "../../application/EvidenceInputResolver.js";
import { recordSessionEvidenceSources } from "../sessionEvidence.js";
import type { McpServer } from "@modelcontextprotocol/server";
@@ -21,7 +22,12 @@ export const registerCompareApplicationVersionsTool = (
compareContract.name,
toolRegistrationOptions(compareContract),
async (input) => {
const parsed = input;
const resolved = resolvePairedEvidenceRequest(
input,
options.evidenceById,
);
if (!resolved.ok) return toCallToolResult(resolved, compareContract);
const parsed = resolved.value;
const result = await logToolExecution(
options.logger,
compareContract.name,
@@ -1,3 +1,4 @@
import { resolveApplicationEvidenceRequest } from "../../application/EvidenceInputResolver.js";
import { recordSessionEvidenceSources } from "../sessionEvidence.js";
import type { McpServer } from "@modelcontextprotocol/server";
@@ -20,13 +21,19 @@ export const registerTraceFeatureTool = (
traceContract.name,
toolRegistrationOptions(traceContract),
async (input) => {
const resolved = resolveApplicationEvidenceRequest(
input,
options.evidenceById,
);
if (!resolved.ok) return toCallToolResult(resolved, traceContract);
const parsed = resolved.value;
const result = await logToolExecution(
options.logger,
traceContract.name,
() => Promise.resolve(traceApplicationFeatureEvidenceValidated(input)),
() => Promise.resolve(traceApplicationFeatureEvidenceValidated(parsed)),
);
if (!result.ok) return toCallToolResult(result, traceContract);
const sources = [input.application, ...input.native_observations];
const sources = [parsed.application, ...parsed.native_observations];
const recorded = recordSessionEvidenceSources(
options.recordEvidence,
sources,
@@ -1,3 +1,4 @@
import { resolveApplicationEvidenceRequest } from "../../application/EvidenceInputResolver.js";
import { recordSessionEvidenceSources } from "../sessionEvidence.js";
import type { McpServer } from "@modelcontextprotocol/server";
@@ -20,12 +21,18 @@ export const registerTraceJavaScriptSemanticsTool = (
contract.name,
toolRegistrationOptions(contract),
async (input) => {
const resolved = resolveApplicationEvidenceRequest(
input,
options.evidenceById,
);
if (!resolved.ok) return toCallToolResult(resolved, contract);
const parsed = resolved.value;
const result = await logToolExecution(options.logger, contract.name, () =>
Promise.resolve(traceJavaScriptSemanticsEvidenceValidated(input)),
Promise.resolve(traceJavaScriptSemanticsEvidenceValidated(parsed)),
);
if (!result.ok) return toCallToolResult(result, contract);
const recorded = recordSessionEvidenceSources(options.recordEvidence, [
input.application,
parsed.application,
]);
if (!recorded.ok) return toCallToolResult(recorded, contract);
return recordResult(options, contract, result.value);
@@ -4,6 +4,7 @@ import type { Logger } from "../../logger.js";
/** Shared services for registering JavaScript application graph workflows. */
export interface ApplicationToolRegistration {
readonly logger: Logger;
readonly evidenceById: BinarySessionPort["evidenceById"] | undefined;
readonly recordEvidence: BinarySessionPort["recordEvidence"] | undefined;
readonly recordEvidenceWithUnknown:
| BinarySessionPort["recordEvidenceWithUnknown"]
@@ -0,0 +1,217 @@
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { afterEach, expect, it } from "vitest";
import { Ajv2020 } from "ajv/dist/2020.js";
import { createTestBinarySession } from "../../fixtures/binarySession.js";
import { createServer } from "../../../src/server/createServer.js";
import {
JAVASCRIPT_FEATURE_TRACE_EXAMPLE,
JAVASCRIPT_APPLICATION_VERSION_COMPARISON_EXAMPLE,
JAVASCRIPT_EXPORT_SHAPE_COMPARISON_EXAMPLE,
SOURCE_TO_BUNDLE_COMPARISON_EXAMPLE,
} from "../../../src/contracts/javascriptApplicationWorkflowExamples.js";
import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js";
const resources: Array<{ close(): Promise<unknown> }> = [];
afterEach(async () => {
for (const resource of resources.splice(0)) await resource.close();
});
const retained = (evidence_id: string) => ({
kind: "retained-evidence",
evidence_id,
});
const application = JAVASCRIPT_FEATURE_TRACE_EXAMPLE.application;
const pair = JAVASCRIPT_APPLICATION_VERSION_COMPARISON_EXAMPLE;
async function connect() {
const session = createTestBinarySession(() => {
throw new Error("references must not launch a provider");
});
const server = createServer(session, session);
const client = new Client({ name: "retained-evidence-test", version: "1" });
resources.push(client, server, session);
const [a, b] = InMemoryTransport.createLinkedPair();
await server.connect(b);
await client.connect(a);
return { client, session };
}
it("runs the same trace/compare workflows with inline and retained application Evidence", async () => {
const { client, session } = await connect();
expect(session.recordEvidence(pair.left).ok).toBe(true);
expect(session.recordEvidence(pair.right).ok).toBe(true);
const scenarios = [
{
name: "trace_application_feature",
inline: JAVASCRIPT_FEATURE_TRACE_EXAMPLE,
refs: {
...JAVASCRIPT_FEATURE_TRACE_EXAMPLE,
application: retained(application.evidence_id),
},
},
{
name: "compare_application_versions",
inline: pair,
refs: {
left: retained(pair.left.evidence_id),
right: retained(pair.right.evidence_id),
},
},
{
name: "compare_javascript_export_shapes",
inline: JAVASCRIPT_EXPORT_SHAPE_COMPARISON_EXAMPLE,
refs: {
...JAVASCRIPT_EXPORT_SHAPE_COMPARISON_EXAMPLE,
left: retained(pair.left.evidence_id),
right: retained(pair.right.evidence_id),
},
},
{
name: "compare_source_to_bundle",
inline: SOURCE_TO_BUNDLE_COMPARISON_EXAMPLE,
refs: {
...SOURCE_TO_BUNDLE_COMPARISON_EXAMPLE,
application: retained(application.evidence_id),
},
},
{
name: "trace_javascript_semantics",
inline: {
application,
query: {
seed: { kind: "literal", value: true },
direction: "backward-provenance",
},
},
refs: {
application: retained(application.evidence_id),
query: {
seed: { kind: "literal", value: true },
direction: "backward-provenance",
},
},
},
];
const ajv = new Ajv2020({ strict: false, validateFormats: false });
const { tools } = await client.listTools();
for (const scenario of scenarios) {
const tool = tools.find(({ name }) => name === scenario.name);
if (tool === undefined) throw new Error("missing advertised tool");
expect(ajv.validateSchema(tool.inputSchema)).toBe(true);
const validate = ajv.compile(tool.inputSchema);
expect(validate(scenario.inline), scenario.name).toBe(true);
expect(validate(scenario.refs), scenario.name).toBe(true);
const inline = await client.callTool({
name: scenario.name,
arguments: scenario.inline,
});
expect(inline.isError, scenario.name).not.toBe(true);
const recordsBefore = session.exportEvidenceBundle().records;
const referenced = await client.callTool({
name: scenario.name,
arguments: scenario.refs,
});
expect(referenced.isError, scenario.name).not.toBe(true);
expect(referenced.structuredContent).toEqual(inline.structuredContent);
expect(session.exportEvidenceBundle().records).toEqual(recordsBefore);
}
const mixed = await client.callTool({
name: "compare_application_versions",
arguments: { left: pair.left, right: retained(pair.right.evidence_id) },
});
expect(mixed.isError).not.toBe(true);
});
it("keeps references session-scoped and expires them when close_binary clears the ledger", async () => {
const first = await connect();
const second = await connect();
expect(first.session.recordEvidence(application).ok).toBe(true);
const request = {
name: "trace_application_feature",
arguments: {
...JAVASCRIPT_FEATURE_TRACE_EXAMPLE,
application: retained(application.evidence_id),
},
};
const foreign = await second.client.callTool(request);
expect(foreign).toMatchObject({
isError: true,
structuredContent: {
error: {
details: {
evidence_id: application.evidence_id,
reason: "missing",
actual: null,
},
},
},
});
const bundle = first.session.exportEvidenceBundle();
expect(second.session.importEvidenceBundle(bundle).ok).toBe(true);
expect((await second.client.callTool(request)).isError).not.toBe(true);
const closed = await first.client.callTool({
name: "close_binary",
arguments: {},
});
expect(closed.isError).not.toBe(true);
expect(await first.client.callTool(request)).toMatchObject({
isError: true,
structuredContent: { error: { details: { reason: "missing" } } },
});
for (const harness of [first, second]) {
const portable = await harness.client.callTool({
name: "trace_application_feature",
arguments: JAVASCRIPT_FEATURE_TRACE_EXAMPLE,
});
expect(portable.isError).not.toBe(true);
expect((await harness.client.callTool(request)).structuredContent).toEqual(
portable.structuredContent,
);
}
});
it("preserves validation for the referenced record's operation, predicate, and semantic identity", async () => {
const { client, session } = await connect();
for (const record of [
createEvidence(
undefined,
{ id: "fixture", name: "Fixture", version: "1" },
{ operation: "inventory_artifact", parameters: {}, result: {} },
),
createEvidence(
undefined,
{ id: "fixture", name: "Fixture", version: "1" },
{
operation: "analyze_javascript_application",
predicateType: "wrong-predicate",
parameters: {},
result: {},
},
),
]) {
expect(session.recordEvidence(record).ok).toBe(true);
const response = await client.callTool({
name: "trace_application_feature",
arguments: {
...JAVASCRIPT_FEATURE_TRACE_EXAMPLE,
application: retained(record.evidence_id),
},
});
expect(response).toMatchObject({
isError: true,
structuredContent: {
error: { code: "invalid_request", category: "invalid_input" },
},
});
}
const tampered = {
...application,
provider: { ...application.provider, id: "tampered" },
};
expect(() => parseEvidence(tampered)).toThrow();
expect(session.recordEvidence(tampered).ok).toBe(false);
const response = await client.callTool({
name: "trace_application_feature",
arguments: { ...JAVASCRIPT_FEATURE_TRACE_EXAMPLE, application: tampered },
});
expect(response.isError).toBe(true);
});