fix(evidence): include every subject format in bundles

This commit is contained in:
morluto committed 2026-10-27 13:59:32 +08:00
1 parent 59dc48db20
commit 50822e1eff
7 files changed
+144 -40

No files matched your search

+2 -6
View File
@@ -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,
+2 -6
View File
@@ -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", [
+30
View File
@@ -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) =>
+8 -11
View File
@@ -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({
+8
View File
@@ -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);
}
}
});
+30 -17
View File
@@ -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");