mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 05:56:19 -10:00
fix!: guide snapshot inputs to the restore workflow (#8867)
BREAKING CHANGE: Snapshot-shaped bundle import failures use invalid_request rather than evidence_integrity_mismatch.
This commit is contained in:
1 parent
6df2ac1d7e
commit
30228c9a5f
3 files changed
+111
No files matched your search
@@ -4,6 +4,7 @@ import { jsonValueValidationIssue } from "../domain/jsonValue.js";
|
||||
import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js";
|
||||
import { requireAuthenticationHeadroom } from "./JsonAuthenticationHeadroom.js";
|
||||
import { absoluteCallerPath } from "../filesystem/callerPath.js";
|
||||
import { resemblesAnalysisSnapshot } from "../domain/analysisSnapshot.js";
|
||||
|
||||
import { describeValidationFailure } from "../domain/validationFailure.js";
|
||||
import {
|
||||
@@ -157,6 +158,21 @@ export const authenticateEvidenceBundleValue = (
|
||||
userMessage: failure.message,
|
||||
}),
|
||||
);
|
||||
if (
|
||||
operation === "import_evidence_bundle" &&
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
!Object.hasOwn(value, "records") &&
|
||||
resemblesAnalysisSnapshot(value)
|
||||
)
|
||||
return err(
|
||||
new EvidenceFileError("read", "wrong-kind", {
|
||||
path: absoluteCallerPath(path),
|
||||
cause: failure.cause,
|
||||
userMessage:
|
||||
"This JSON appears to be an analysis snapshot rather than an Evidence bundle. Its snapshot content has not been validated. To restore it, use a matching target, provider and analysis profile: call open_binary with snapshot_path set to this file, or pass --snapshot to a compatible CLI analysis command (which loads and updates the snapshot). Alternatively, supply a bundle from export_evidence_bundle.",
|
||||
}),
|
||||
);
|
||||
return err(
|
||||
new EvidenceIntegrityError("Evidence bundle validation failed", {
|
||||
cause: failure.cause,
|
||||
|
||||
@@ -190,6 +190,27 @@ export type AnalysisSnapshotWorkflowEntry = z.infer<typeof workflowEntrySchema>;
|
||||
export type AnalysisSnapshotTarget = z.infer<typeof targetSchema>;
|
||||
export type AnalysisSnapshotBinding = z.infer<typeof bindingSchema>;
|
||||
|
||||
/** Recognize an already-parsed JSON file envelope; this does not validate or authenticate a snapshot. */
|
||||
export const resemblesAnalysisSnapshot = (value: unknown): boolean => {
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value))
|
||||
return false;
|
||||
const record = value as Record<string, unknown>;
|
||||
const objectMember = (key: string) =>
|
||||
Object.hasOwn(record, key) &&
|
||||
typeof record[key] === "object" &&
|
||||
record[key] !== null &&
|
||||
!Array.isArray(record[key]);
|
||||
return (
|
||||
objectMember("target") &&
|
||||
objectMember("binding") &&
|
||||
objectMember("evidence_bundle") &&
|
||||
Object.hasOwn(record, "entries") &&
|
||||
Array.isArray(record.entries) &&
|
||||
Object.hasOwn(record, "workflow_entries") &&
|
||||
Array.isArray(record.workflow_entries)
|
||||
);
|
||||
};
|
||||
|
||||
// These handles only represent entries parsed in this realm. File readers can
|
||||
// release their payloads while retaining an authenticated comparison operand.
|
||||
const preparedEntryBrand = Symbol("prepared-snapshot-entry");
|
||||
|
||||
@@ -553,6 +553,7 @@ describe("MCP investigation history snapshot replay", () => {
|
||||
replay,
|
||||
"investigation-history-replay",
|
||||
);
|
||||
await expectSnapshotImportGuidance(replayMcp, snapshotPath);
|
||||
const reopened = await replayMcp.callTool({
|
||||
name: "open_binary",
|
||||
arguments: { path: firstPath, snapshot_path: snapshotPath },
|
||||
@@ -577,6 +578,79 @@ describe("MCP investigation history snapshot replay", () => {
|
||||
);
|
||||
});
|
||||
|
||||
const expectSnapshotImportGuidance = async (mcp: Client, path: string) => {
|
||||
const before = await readFile(path);
|
||||
const imported = await mcp.callTool({
|
||||
name: "import_evidence_bundle",
|
||||
arguments: { path },
|
||||
});
|
||||
const error = parseMcpToolError(imported).error;
|
||||
expect(error.code).toBe("invalid_request");
|
||||
expect(error.remediation.action).toContain("open_binary");
|
||||
expect(error.remediation.action).toContain("snapshot_path");
|
||||
expect(error.remediation.action).toContain("--snapshot");
|
||||
expect(error.remediation.action).toContain("loads and updates");
|
||||
expect(error.remediation.action).toContain("not been validated");
|
||||
expect((await readFile(path)).equals(before)).toBe(true);
|
||||
};
|
||||
|
||||
it("does not authenticate snapshot-shaped or unrelated malformed bundle input", async () => {
|
||||
const { snapshotPath } = await createWorkflowFiles(
|
||||
"rea-mcp-snapshot-kind-",
|
||||
[],
|
||||
);
|
||||
const { mcp } = await connectWorkflowMcp(
|
||||
createWorkflowSession(makeProvider([], [])),
|
||||
"snapshot-kind",
|
||||
);
|
||||
await writeFile(
|
||||
snapshotPath,
|
||||
JSON.stringify({
|
||||
target: {},
|
||||
binding: {},
|
||||
evidence_bundle: {},
|
||||
entries: [],
|
||||
workflow_entries: [],
|
||||
}),
|
||||
);
|
||||
const snapshotLike = parseMcpToolError(
|
||||
await mcp.callTool({
|
||||
name: "import_evidence_bundle",
|
||||
arguments: { path: snapshotPath },
|
||||
}),
|
||||
).error;
|
||||
expect(snapshotLike.code).toBe("invalid_request");
|
||||
expect(snapshotLike.remediation.action).toContain("not been validated");
|
||||
|
||||
await writeFile(
|
||||
snapshotPath,
|
||||
JSON.stringify({
|
||||
target: {},
|
||||
binding: {},
|
||||
evidence_bundle: {},
|
||||
entries: [],
|
||||
workflow_entries: [],
|
||||
records: [],
|
||||
}),
|
||||
);
|
||||
const ambiguousBundle = parseMcpToolError(
|
||||
await mcp.callTool({
|
||||
name: "import_evidence_bundle",
|
||||
arguments: { path: snapshotPath },
|
||||
}),
|
||||
).error;
|
||||
expect(ambiguousBundle.code).toBe("evidence_integrity_mismatch");
|
||||
|
||||
await writeFile(snapshotPath, JSON.stringify({ records: [] }));
|
||||
const malformedBundle = parseMcpToolError(
|
||||
await mcp.callTool({
|
||||
name: "import_evidence_bundle",
|
||||
arguments: { path: snapshotPath },
|
||||
}),
|
||||
).error;
|
||||
expect(malformedBundle.code).toBe("evidence_integrity_mismatch");
|
||||
});
|
||||
|
||||
const createWorkflowFiles = async (
|
||||
prefix: string,
|
||||
filenames: readonly string[],
|
||||
|
||||
Reference in new issue
Block a user