mirror of
https://github.com/morluto/rea.git
synced 2026-10-31 11:56:12 +00:00
fix(evidence): include every subject format in bundles
This commit is contained in:
1 parent
59dc48db20
commit
50822e1eff
7 files changed
+144
-40
No files matched your search
@@ -3,7 +3,7 @@ import { createHash } from "node:crypto";
|
||||
import canonicalize from "canonicalize";
|
||||
import { z } from "zod";
|
||||
|
||||
import { evidenceSubjectDeclarationShapes } from "./evidenceSubject.js";
|
||||
import { withEvidenceSubjectDeclaration } from "./evidenceSubject.js";
|
||||
|
||||
import {
|
||||
compareUnicodeCodePoints,
|
||||
@@ -85,11 +85,7 @@ const subjectBaseSchema = z.object({
|
||||
path: localPathStringSchema,
|
||||
sha256: digestSchema,
|
||||
});
|
||||
const subjectSchema = z.discriminatedUnion("format", [
|
||||
subjectBaseSchema.extend(evidenceSubjectDeclarationShapes[0]),
|
||||
subjectBaseSchema.extend(evidenceSubjectDeclarationShapes[1]),
|
||||
subjectBaseSchema.extend(evidenceSubjectDeclarationShapes[2]),
|
||||
]);
|
||||
const subjectSchema = withEvidenceSubjectDeclaration(subjectBaseSchema);
|
||||
const targetIdentitySchema = z.object({
|
||||
sha256: digestSchema,
|
||||
kind: kindSchema,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { basename } from "node:path";
|
||||
import { z } from "zod";
|
||||
|
||||
import { evidenceSubjectDeclarationShapes } from "./evidenceSubject.js";
|
||||
import { withEvidenceSubjectDeclaration } from "./evidenceSubject.js";
|
||||
|
||||
import { digestCanonicalValue } from "./canonicalDigest.js";
|
||||
import { canonicalJsonDigestSteps } from "./canonicalJsonDigestSteps.js";
|
||||
@@ -43,11 +43,7 @@ const subjectBaseSchema = z.strictObject({
|
||||
digest: z.strictObject({ sha256: digestSchema }),
|
||||
local_path: nulFreeStringSchema,
|
||||
});
|
||||
const subjectSchema = z.discriminatedUnion("format", [
|
||||
subjectBaseSchema.extend(evidenceSubjectDeclarationShapes[0]),
|
||||
subjectBaseSchema.extend(evidenceSubjectDeclarationShapes[1]),
|
||||
subjectBaseSchema.extend(evidenceSubjectDeclarationShapes[2]),
|
||||
]);
|
||||
const subjectSchema = withEvidenceSubjectDeclaration(subjectBaseSchema);
|
||||
/** Source location attached to an evidence observation. */
|
||||
/** Source-location schema shared by evidence-bearing persistence formats. */
|
||||
export const evidenceLocationSchema = z.discriminatedUnion("kind", [
|
||||
|
||||
@@ -20,6 +20,36 @@ const provider = { id: "fixture", name: "Fixture", version: "1" };
|
||||
const targetDigest = "a".repeat(64);
|
||||
const foreignDigest = "b".repeat(64);
|
||||
|
||||
describe("artifact manifest projection", () => {
|
||||
it("round-trips GameCube DOL Evidence through retained bundles and target selection", () => {
|
||||
const record = createEvidence(
|
||||
{
|
||||
path: "/tmp/game.dol",
|
||||
sha256: targetDigest,
|
||||
format: "gamecube-dol",
|
||||
architecture: "powerpc",
|
||||
},
|
||||
provider,
|
||||
{
|
||||
operation: "inspect_artifact",
|
||||
parameters: {},
|
||||
result: { observed: true },
|
||||
},
|
||||
);
|
||||
const bundle = createImmutableEvidenceBundle([record]);
|
||||
expect(bundle.artifacts).toEqual([
|
||||
{
|
||||
digest: { sha256: targetDigest },
|
||||
format: "gamecube-dol",
|
||||
architecture: "powerpc",
|
||||
},
|
||||
]);
|
||||
const restored = parseEvidenceBundle(JSON.parse(JSON.stringify(bundle)));
|
||||
expect(restored.records[0]?.evidence_id).toBe(record.evidence_id);
|
||||
expect(evidenceBundleForTarget(restored, targetDigest)).toEqual(bundle);
|
||||
});
|
||||
});
|
||||
|
||||
describe("evidence bundle failure classification", () => {
|
||||
it("tags valid-shaped bundles that fail canonical ordering", () => {
|
||||
const records = ["first", "second"].map((label) =>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { withEvidenceSubjectDeclaration } from "./evidenceSubject.js";
|
||||
import { digestSchema } from "./digests.js";
|
||||
import { EvidenceIntegrityError } from "./evidenceErrors.js";
|
||||
import { err, ok, type Result } from "./result.js";
|
||||
import { describeValidationFailure } from "./validationFailure.js";
|
||||
@@ -20,17 +22,12 @@ import {
|
||||
} from "./residualUnknown.js";
|
||||
import { freezeJsonSnapshot } from "./immutableJson.js";
|
||||
|
||||
const [detectedSubjectSchema, rawSubjectSchema] =
|
||||
evidenceSchema.shape.subject.unwrap().options;
|
||||
const manifestFields = {
|
||||
digest: true,
|
||||
format: true,
|
||||
architecture: true,
|
||||
} as const;
|
||||
const artifactManifestSchema = z.discriminatedUnion("format", [
|
||||
detectedSubjectSchema.pick(manifestFields),
|
||||
rawSubjectSchema.pick(manifestFields),
|
||||
]);
|
||||
const artifactManifestBaseSchema = z.strictObject({
|
||||
digest: z.strictObject({ sha256: digestSchema }),
|
||||
});
|
||||
const artifactManifestSchema = withEvidenceSubjectDeclaration(
|
||||
artifactManifestBaseSchema,
|
||||
);
|
||||
const providerManifestSchema = providerSchema;
|
||||
const environmentManifestSchema = evidenceSchema.shape.environment.unwrap();
|
||||
const scenarioManifestSchema = z.strictObject({
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
import { analysisSnapshotSchema } from "./analysisSnapshot.js";
|
||||
import { BINARY_ARCHITECTURES } from "./binaryTargetTypes.js";
|
||||
import { evidenceBundleSchema } from "./evidenceBundle.js";
|
||||
import { evidenceSchema } from "./evidence.js";
|
||||
import { evidenceSubjectFormatSchema } from "./evidenceSubject.js";
|
||||
|
||||
@@ -38,6 +39,13 @@ describe("persisted observation subject declarations", () => {
|
||||
(architecture !== "6502" && architecture !== "m68k"));
|
||||
expect(evidence.success, JSON.stringify(declaration)).toBe(allowed);
|
||||
expect(snapshot.success, JSON.stringify(declaration)).toBe(allowed);
|
||||
const manifest = evidenceBundleSchema.shape.artifacts.element.safeParse(
|
||||
{
|
||||
...declaration,
|
||||
digest: { sha256: digest },
|
||||
},
|
||||
);
|
||||
expect(manifest.success, JSON.stringify(declaration)).toBe(allowed);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -41,20 +41,33 @@ export const evidenceSubjectFormatSchema = z.enum([
|
||||
]);
|
||||
|
||||
/** Format-owned architecture declarations shared by Evidence and persisted executions. */
|
||||
export const evidenceSubjectDeclarationShapes = [
|
||||
{
|
||||
format: evidenceSubjectFormatSchema.exclude(["raw-binary", "gamecube-dol"]),
|
||||
architecture: z
|
||||
.enum(BINARY_ARCHITECTURES)
|
||||
.exclude([...RAW_ONLY_ARCHITECTURES])
|
||||
.nullable(),
|
||||
},
|
||||
{
|
||||
format: z.literal("raw-binary"),
|
||||
architecture: z.enum(BINARY_ARCHITECTURES).nullable(),
|
||||
},
|
||||
{
|
||||
format: z.literal("gamecube-dol"),
|
||||
architecture: z.literal("powerpc"),
|
||||
},
|
||||
] as const;
|
||||
const detectedEvidenceSubjectDeclaration = {
|
||||
format: evidenceSubjectFormatSchema.exclude(["raw-binary", "gamecube-dol"]),
|
||||
architecture: z
|
||||
.enum(BINARY_ARCHITECTURES)
|
||||
.exclude([...RAW_ONLY_ARCHITECTURES])
|
||||
.nullable(),
|
||||
};
|
||||
|
||||
const rawEvidenceSubjectDeclaration = {
|
||||
format: z.literal("raw-binary"),
|
||||
architecture: z.enum(BINARY_ARCHITECTURES).nullable(),
|
||||
};
|
||||
|
||||
const gamecubeEvidenceSubjectDeclaration = {
|
||||
format: z.literal("gamecube-dol"),
|
||||
architecture: z.literal("powerpc"),
|
||||
};
|
||||
|
||||
/** Add the complete format-owned declaration to a persistence envelope. */
|
||||
export const withEvidenceSubjectDeclaration = <
|
||||
Shape extends z.ZodRawShape,
|
||||
Config extends z.core.$ZodObjectConfig,
|
||||
>(
|
||||
base: z.ZodObject<Shape, Config>,
|
||||
) =>
|
||||
z.discriminatedUnion("format", [
|
||||
base.extend(detectedEvidenceSubjectDeclaration),
|
||||
base.extend(rawEvidenceSubjectDeclaration),
|
||||
base.extend(gamecubeEvidenceSubjectDeclaration),
|
||||
]);
|
||||
@@ -18,6 +18,70 @@ import { PROCESS_PROVIDER } from "../../../src/domain/process/processEvidencePro
|
||||
import { createServer } from "../../../src/server/createServer.js";
|
||||
import { JSON_BYTE_ORDER_MARK_MESSAGE } from "../../../src/application/Utf8JsonInput.js";
|
||||
|
||||
it("exports and imports GameCube DOL Evidence without losing its subject identity", async () => {
|
||||
const root = await createTestTempDirectory("rea-dol-bundle-mcp-");
|
||||
const path = join(root, "bundle.json");
|
||||
const record = createEvidence(
|
||||
{
|
||||
path: join(root, "game.dol"),
|
||||
sha256: "d".repeat(64),
|
||||
format: "gamecube-dol",
|
||||
architecture: "powerpc",
|
||||
},
|
||||
{ id: "fixture", name: "Fixture", version: "1" },
|
||||
{
|
||||
operation: "inspect_artifact",
|
||||
parameters: {},
|
||||
result: { observed: true },
|
||||
},
|
||||
);
|
||||
const session = createTestBinarySession(() => {
|
||||
throw new Error("bundle round trip must not run a provider");
|
||||
});
|
||||
expect(session.recordEvidence(record).ok).toBe(true);
|
||||
const source = await connectEvidenceMcp("dol-source", session);
|
||||
const importedSession = createTestBinarySession(() => {
|
||||
throw new Error("bundle import must not run a provider");
|
||||
});
|
||||
const target = await connectEvidenceMcp("dol-target", importedSession);
|
||||
try {
|
||||
const inline = await source.client.callTool({
|
||||
name: "get_evidence_bundle",
|
||||
arguments: {},
|
||||
});
|
||||
expect(inline.isError).not.toBe(true);
|
||||
expect(inline.structuredContent).toMatchObject({
|
||||
result: {
|
||||
artifacts: [{ format: "gamecube-dol", architecture: "powerpc" }],
|
||||
},
|
||||
});
|
||||
const exported = await source.client.callTool({
|
||||
name: "export_evidence_bundle",
|
||||
arguments: { path },
|
||||
});
|
||||
expect(exported.isError).not.toBe(true);
|
||||
const imported = await target.client.callTool({
|
||||
name: "import_evidence_bundle",
|
||||
arguments: { path },
|
||||
});
|
||||
expect(imported.isError).not.toBe(true);
|
||||
const restored = await target.client.callTool({
|
||||
name: "get_evidence_bundle",
|
||||
arguments: {},
|
||||
});
|
||||
expect(restored.structuredContent).toEqual(inline.structuredContent);
|
||||
} finally {
|
||||
await Promise.allSettled([
|
||||
source.client.close(),
|
||||
source.server.close(),
|
||||
session.close(),
|
||||
target.client.close(),
|
||||
target.server.close(),
|
||||
importedSession.close(),
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves BOM recovery through public bundle import", async () => {
|
||||
const root = await createTestTempDirectory("rea-bundle-bom-mcp-");
|
||||
const path = join(root, "bundle.json");
|
||||
|
||||
Reference in new issue
Block a user