mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
feat(evidence): accept exact retained application references (#734)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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 });
|
||||
};
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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>;
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user