mirror of
https://github.com/morluto/rea
synced 2026-10-27 14:42:18 -04:00
feat(javascript): return a retained summary view on request (#1568)
analyze_javascript_application returns the complete application graph inline, which repeats tens of kilobytes even for a few source lines and exceeds the default 10 MiB MCP frame for a 300-function source file. The new optional detail input keeps complete delivery by default. With detail "summary" the server records the complete analysis Evidence in the session and returns its inspect_analysis_view summary, whose parent_evidence_id lets callers page modules, select items, or pass the retained reference to application workflows. A server without Evidence retention refuses summary detail with capability_unavailable rather than returning a summary whose analysis cannot be reached. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { JAVASCRIPT_APPLICATION_EVIDENCE_EXAMPLE } from "../../contracts/javascript/javascriptRuntimeReconciliationExample.js";
|
||||
import { createEvidence, parseEvidence } from "../../domain/evidence.js";
|
||||
@@ -9,7 +9,13 @@ import {
|
||||
analysisViewJavaScriptEvidence,
|
||||
analysisViewLayoutEvidence,
|
||||
} from "../../../tests/fixtures/analysisView.js";
|
||||
import { inspectAnalysisView } from "./AnalysisViewService.js";
|
||||
import {
|
||||
inspectAnalysisView,
|
||||
summarizeRetainedAnalysis,
|
||||
} from "./AnalysisViewService.js";
|
||||
import type { Evidence } from "../../domain/evidence.js";
|
||||
import { EvidenceIntegrityError } from "../../domain/evidenceErrors.js";
|
||||
import { err, ok } from "../../domain/result.js";
|
||||
|
||||
it.each(["/fixtures/unknown.exe", ""])(
|
||||
"preserves recorded empty native identity fields: path=%j",
|
||||
@@ -266,3 +272,47 @@ it("rejects contradictory subject and analysis artifact digests", () => {
|
||||
error: { _tag: "EvidenceIntegrityError" },
|
||||
});
|
||||
});
|
||||
|
||||
describe("summarizeRetainedAnalysis", () => {
|
||||
const parent = analysisViewJavaScriptEvidence(
|
||||
analysisViewJavaScriptAnalysisWithSource(),
|
||||
);
|
||||
|
||||
it("retains the complete Evidence before projecting its summary", () => {
|
||||
const retained = new Map<string, Evidence>();
|
||||
const summary = summarizeRetainedAnalysis(parent, {
|
||||
recordEvidence: (evidence) => {
|
||||
retained.set(evidence.evidence_id, evidence);
|
||||
return ok("added");
|
||||
},
|
||||
evidenceById: (evidenceId) => retained.get(evidenceId),
|
||||
});
|
||||
if (!summary.ok) throw summary.error;
|
||||
expect(retained.get(parent.evidence_id)).toBe(parent);
|
||||
expect(summary.value.evidence_links).toEqual([parent.evidence_id]);
|
||||
expect(summary.value.normalized_result).toMatchObject({
|
||||
kind: "summary",
|
||||
parent_evidence_id: parent.evidence_id,
|
||||
parent_operation: "analyze_javascript_application",
|
||||
});
|
||||
});
|
||||
|
||||
it("refuses a summary whose complete Evidence cannot be retained", () => {
|
||||
const unavailable = summarizeRetainedAnalysis(parent, {
|
||||
recordEvidence: undefined,
|
||||
evidenceById: undefined,
|
||||
});
|
||||
expect(unavailable.ok).toBe(false);
|
||||
if (unavailable.ok) return;
|
||||
expect(unavailable.error._tag).toBe("AnalysisCapabilityUnavailableError");
|
||||
expect(unavailable.error.userMessage).toContain("detail complete");
|
||||
|
||||
const conflict = new EvidenceIntegrityError("ledger conflict");
|
||||
expect(
|
||||
summarizeRetainedAnalysis(parent, {
|
||||
recordEvidence: () => err(conflict),
|
||||
evidenceById: () => parent,
|
||||
}),
|
||||
).toEqual(err(conflict));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,9 @@ import { z } from "zod";
|
||||
import { ANALYSIS_VIEW_PROVIDER } from "../InvestigationProviders.js";
|
||||
import type { EvidenceLookup } from "../EvidenceInputResolver.js";
|
||||
import { resolveEvidenceInput } from "../EvidenceInputResolver.js";
|
||||
import type { EvidenceWriter } from "../investigation/InvestigationRecordPort.js";
|
||||
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import { AnalysisCapabilityUnavailableError } from "../../domain/analysisErrorCore.js";
|
||||
import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js";
|
||||
import { EvidenceIntegrityError } from "../../domain/evidenceErrors.js";
|
||||
import {
|
||||
@@ -76,6 +78,47 @@ export const inspectAnalysisViewValidated = (
|
||||
return ok(createAnalysisViewEvidence(input, parent, projected.value));
|
||||
};
|
||||
|
||||
/** Session Evidence ownership required to deliver a view instead of a complete record. */
|
||||
export interface AnalysisViewRetention {
|
||||
readonly evidenceById: EvidenceLookup | undefined;
|
||||
readonly recordEvidence: EvidenceWriter["recordEvidence"] | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retain complete analysis Evidence in the session, then project its summary
|
||||
* view from the retained record. Callers page or select the rest through
|
||||
* inspect_analysis_view with the view's parent Evidence ID.
|
||||
*/
|
||||
export const summarizeRetainedAnalysis = (
|
||||
evidence: Evidence,
|
||||
retention: AnalysisViewRetention,
|
||||
): Result<Evidence, AnalysisError> => {
|
||||
if (
|
||||
retention.recordEvidence === undefined ||
|
||||
retention.evidenceById === undefined
|
||||
)
|
||||
return err(
|
||||
new AnalysisCapabilityUnavailableError(
|
||||
ANALYSIS_VIEW_PROVIDER.id,
|
||||
evidence.operation,
|
||||
"summary detail requires a server session that retains Evidence for later views",
|
||||
{
|
||||
userMessage:
|
||||
"This server does not retain Evidence. Select detail complete to receive the complete analysis inline.",
|
||||
},
|
||||
),
|
||||
);
|
||||
const recorded = retention.recordEvidence(evidence);
|
||||
if (!recorded.ok) return recorded;
|
||||
return inspectAnalysisViewValidated(
|
||||
{
|
||||
source: { kind: "retained-evidence", evidence_id: evidence.evidence_id },
|
||||
view: { kind: "summary" },
|
||||
},
|
||||
retention.evidenceById,
|
||||
);
|
||||
};
|
||||
|
||||
const resolveAnalysisViewSource = (
|
||||
source: InspectAnalysisViewInput["source"],
|
||||
lookup: EvidenceLookup | undefined,
|
||||
|
||||
@@ -11,6 +11,9 @@ import {
|
||||
electronActiveObservationInputSchema,
|
||||
electronActiveObservationResultSchema,
|
||||
} from "../../domain/javascript/electronActiveObservation.js";
|
||||
import { z } from "zod";
|
||||
|
||||
import { analysisViewResultSchema } from "../../domain/analysisView/analysisView.js";
|
||||
import {
|
||||
analyzeJavaScriptApplicationInputSchema,
|
||||
javascriptApplicationAnalysisResultSchema,
|
||||
@@ -23,8 +26,21 @@ import { JAVASCRIPT_RUNTIME_RECONCILIATION_EXAMPLE } from "./javascriptRuntimeRe
|
||||
|
||||
const listOutputSchema = evidenceResultOf(electronTargetListSchema);
|
||||
const inspectionOutputSchema = evidenceResultOf(electronPageInspectionSchema);
|
||||
/** Caller-selected delivery of a completed JavaScript application analysis. */
|
||||
export const analyzeJavaScriptApplicationRequestSchema =
|
||||
analyzeJavaScriptApplicationInputSchema.extend({
|
||||
detail: z
|
||||
.enum(["complete", "summary"])
|
||||
.default("complete")
|
||||
.describe(
|
||||
"complete returns the complete application analysis Evidence inline. summary retains that complete Evidence in this session and returns its inspect_analysis_view summary; page modules or select items from normalized_result.parent_evidence_id.",
|
||||
),
|
||||
});
|
||||
const applicationOutputSchema = evidenceResultOf(
|
||||
javascriptApplicationAnalysisResultSchema,
|
||||
z.union([
|
||||
javascriptApplicationAnalysisResultSchema,
|
||||
analysisViewResultSchema,
|
||||
]),
|
||||
);
|
||||
const reconciliationOutputSchema = evidenceResultOf(
|
||||
javascriptRuntimeReconciliationResultSchema,
|
||||
@@ -79,9 +95,9 @@ export const ELECTRON_TOOL_CONTRACTS = [
|
||||
name: "analyze_javascript_application",
|
||||
...toolContractMetadata("analyze_javascript_application"),
|
||||
description:
|
||||
"Reconstruct one local ASAR or extracted JavaScript application as an inline application graph without executing it. Returns recovered graph nodes, edges, semantic relations, integrity contradictions, limitations, and coverage. Integrity mismatches fail by default; record-and-continue retains observed bytes as untrusted and marks graph coverage partial.",
|
||||
"Reconstruct one local ASAR or extracted JavaScript application as an inline application graph without executing it. Returns recovered graph nodes, edges, semantic relations, integrity contradictions, limitations, and coverage. Select detail summary to retain the complete analysis Evidence in this session and receive only its summary view, then inspect selected modules or pages with inspect_analysis_view. Integrity mismatches fail by default; record-and-continue retains observed bytes as untrusted and marks graph coverage partial.",
|
||||
kind: "electron-provider",
|
||||
inputSchema: analyzeJavaScriptApplicationInputSchema,
|
||||
inputSchema: analyzeJavaScriptApplicationRequestSchema,
|
||||
outputSchema: applicationOutputSchema,
|
||||
examples: [
|
||||
{
|
||||
@@ -92,6 +108,13 @@ export const ELECTRON_TOOL_CONTRACTS = [
|
||||
integrity_policy: "fail",
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Retain the complete analysis and return its summary view",
|
||||
input: {
|
||||
input_path: "/Applications/Example.app/Contents/Resources/app.asar",
|
||||
detail: "summary",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -468,6 +468,7 @@ const registerObservationTools = ({
|
||||
});
|
||||
registerElectronTools(server, {
|
||||
...common,
|
||||
evidenceById,
|
||||
electron: options.electronObservation,
|
||||
electronActive: options.electronActiveObservation,
|
||||
observationLoadFailure:
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { ToolResultDelivery } from "./toolResult.js";
|
||||
import type { EvidenceMcpServer } from "./EvidenceMcpServer.js";
|
||||
import { summarizeRetainedAnalysis } from "../application/analysisView/AnalysisViewService.js";
|
||||
import type { EvidenceLookup } from "../application/EvidenceInputResolver.js";
|
||||
import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js";
|
||||
import {
|
||||
optionalProviderUnavailable,
|
||||
@@ -36,6 +38,7 @@ interface ElectronToolRegistration {
|
||||
readonly electron: ElectronObservationPort | undefined;
|
||||
readonly electronActive: ElectronActiveObservationPort | undefined;
|
||||
readonly recordEvidence: EvidenceWriter["recordEvidence"] | undefined;
|
||||
readonly evidenceById: EvidenceLookup | undefined;
|
||||
}
|
||||
|
||||
interface ElectronToolContext {
|
||||
@@ -95,8 +98,15 @@ export const registerElectronTools = (
|
||||
registration,
|
||||
analyzeContract,
|
||||
{ input, context },
|
||||
(parsed, { signal, progress }) =>
|
||||
analyzeJavaScriptApplicationValidated(parsed, { signal, progress }),
|
||||
async ({ detail, ...request }, { signal, progress }) => {
|
||||
const analyzed = await analyzeJavaScriptApplicationValidated(
|
||||
request,
|
||||
{ signal, progress },
|
||||
);
|
||||
return detail === "summary" && analyzed.ok
|
||||
? summarizeRetainedAnalysis(analyzed.value, options)
|
||||
: analyzed;
|
||||
},
|
||||
),
|
||||
);
|
||||
server.registerTool(
|
||||
|
||||
Reference in New Issue
Block a user