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 canonicalize from "canonicalize";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { evidenceSubjectDeclarationShapes } from "./evidenceSubject.js";
|
import { withEvidenceSubjectDeclaration } from "./evidenceSubject.js";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
compareUnicodeCodePoints,
|
compareUnicodeCodePoints,
|
||||||
@@ -85,11 +85,7 @@ const subjectBaseSchema = z.object({
|
|||||||
path: localPathStringSchema,
|
path: localPathStringSchema,
|
||||||
sha256: digestSchema,
|
sha256: digestSchema,
|
||||||
});
|
});
|
||||||
const subjectSchema = z.discriminatedUnion("format", [
|
const subjectSchema = withEvidenceSubjectDeclaration(subjectBaseSchema);
|
||||||
subjectBaseSchema.extend(evidenceSubjectDeclarationShapes[0]),
|
|
||||||
subjectBaseSchema.extend(evidenceSubjectDeclarationShapes[1]),
|
|
||||||
subjectBaseSchema.extend(evidenceSubjectDeclarationShapes[2]),
|
|
||||||
]);
|
|
||||||
const targetIdentitySchema = z.object({
|
const targetIdentitySchema = z.object({
|
||||||
sha256: digestSchema,
|
sha256: digestSchema,
|
||||||
kind: kindSchema,
|
kind: kindSchema,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { basename } from "node:path";
|
import { basename } from "node:path";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { evidenceSubjectDeclarationShapes } from "./evidenceSubject.js";
|
import { withEvidenceSubjectDeclaration } from "./evidenceSubject.js";
|
||||||
|
|
||||||
import { digestCanonicalValue } from "./canonicalDigest.js";
|
import { digestCanonicalValue } from "./canonicalDigest.js";
|
||||||
import { canonicalJsonDigestSteps } from "./canonicalJsonDigestSteps.js";
|
import { canonicalJsonDigestSteps } from "./canonicalJsonDigestSteps.js";
|
||||||
@@ -43,11 +43,7 @@ const subjectBaseSchema = z.strictObject({
|
|||||||
digest: z.strictObject({ sha256: digestSchema }),
|
digest: z.strictObject({ sha256: digestSchema }),
|
||||||
local_path: nulFreeStringSchema,
|
local_path: nulFreeStringSchema,
|
||||||
});
|
});
|
||||||
const subjectSchema = z.discriminatedUnion("format", [
|
const subjectSchema = withEvidenceSubjectDeclaration(subjectBaseSchema);
|
||||||
subjectBaseSchema.extend(evidenceSubjectDeclarationShapes[0]),
|
|
||||||
subjectBaseSchema.extend(evidenceSubjectDeclarationShapes[1]),
|
|
||||||
subjectBaseSchema.extend(evidenceSubjectDeclarationShapes[2]),
|
|
||||||
]);
|
|
||||||
/** Source location attached to an evidence observation. */
|
/** Source location attached to an evidence observation. */
|
||||||
/** Source-location schema shared by evidence-bearing persistence formats. */
|
/** Source-location schema shared by evidence-bearing persistence formats. */
|
||||||
export const evidenceLocationSchema = z.discriminatedUnion("kind", [
|
export const evidenceLocationSchema = z.discriminatedUnion("kind", [
|
||||||
|
|||||||
@@ -20,6 +20,36 @@ const provider = { id: "fixture", name: "Fixture", version: "1" };
|
|||||||
const targetDigest = "a".repeat(64);
|
const targetDigest = "a".repeat(64);
|
||||||
const foreignDigest = "b".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", () => {
|
describe("evidence bundle failure classification", () => {
|
||||||
it("tags valid-shaped bundles that fail canonical ordering", () => {
|
it("tags valid-shaped bundles that fail canonical ordering", () => {
|
||||||
const records = ["first", "second"].map((label) =>
|
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 { EvidenceIntegrityError } from "./evidenceErrors.js";
|
||||||
import { err, ok, type Result } from "./result.js";
|
import { err, ok, type Result } from "./result.js";
|
||||||
import { describeValidationFailure } from "./validationFailure.js";
|
import { describeValidationFailure } from "./validationFailure.js";
|
||||||
@@ -20,17 +22,12 @@ import {
|
|||||||
} from "./residualUnknown.js";
|
} from "./residualUnknown.js";
|
||||||
import { freezeJsonSnapshot } from "./immutableJson.js";
|
import { freezeJsonSnapshot } from "./immutableJson.js";
|
||||||
|
|
||||||
const [detectedSubjectSchema, rawSubjectSchema] =
|
const artifactManifestBaseSchema = z.strictObject({
|
||||||
evidenceSchema.shape.subject.unwrap().options;
|
digest: z.strictObject({ sha256: digestSchema }),
|
||||||
const manifestFields = {
|
});
|
||||||
digest: true,
|
const artifactManifestSchema = withEvidenceSubjectDeclaration(
|
||||||
format: true,
|
artifactManifestBaseSchema,
|
||||||
architecture: true,
|
);
|
||||||
} as const;
|
|
||||||
const artifactManifestSchema = z.discriminatedUnion("format", [
|
|
||||||
detectedSubjectSchema.pick(manifestFields),
|
|
||||||
rawSubjectSchema.pick(manifestFields),
|
|
||||||
]);
|
|
||||||
const providerManifestSchema = providerSchema;
|
const providerManifestSchema = providerSchema;
|
||||||
const environmentManifestSchema = evidenceSchema.shape.environment.unwrap();
|
const environmentManifestSchema = evidenceSchema.shape.environment.unwrap();
|
||||||
const scenarioManifestSchema = z.strictObject({
|
const scenarioManifestSchema = z.strictObject({
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
|||||||
|
|
||||||
import { analysisSnapshotSchema } from "./analysisSnapshot.js";
|
import { analysisSnapshotSchema } from "./analysisSnapshot.js";
|
||||||
import { BINARY_ARCHITECTURES } from "./binaryTargetTypes.js";
|
import { BINARY_ARCHITECTURES } from "./binaryTargetTypes.js";
|
||||||
|
import { evidenceBundleSchema } from "./evidenceBundle.js";
|
||||||
import { evidenceSchema } from "./evidence.js";
|
import { evidenceSchema } from "./evidence.js";
|
||||||
import { evidenceSubjectFormatSchema } from "./evidenceSubject.js";
|
import { evidenceSubjectFormatSchema } from "./evidenceSubject.js";
|
||||||
|
|
||||||
@@ -38,6 +39,13 @@ describe("persisted observation subject declarations", () => {
|
|||||||
(architecture !== "6502" && architecture !== "m68k"));
|
(architecture !== "6502" && architecture !== "m68k"));
|
||||||
expect(evidence.success, JSON.stringify(declaration)).toBe(allowed);
|
expect(evidence.success, JSON.stringify(declaration)).toBe(allowed);
|
||||||
expect(snapshot.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. */
|
/** Format-owned architecture declarations shared by Evidence and persisted executions. */
|
||||||
export const evidenceSubjectDeclarationShapes = [
|
const detectedEvidenceSubjectDeclaration = {
|
||||||
{
|
format: evidenceSubjectFormatSchema.exclude(["raw-binary", "gamecube-dol"]),
|
||||||
format: evidenceSubjectFormatSchema.exclude(["raw-binary", "gamecube-dol"]),
|
architecture: z
|
||||||
architecture: z
|
.enum(BINARY_ARCHITECTURES)
|
||||||
.enum(BINARY_ARCHITECTURES)
|
.exclude([...RAW_ONLY_ARCHITECTURES])
|
||||||
.exclude([...RAW_ONLY_ARCHITECTURES])
|
.nullable(),
|
||||||
.nullable(),
|
};
|
||||||
},
|
|
||||||
{
|
const rawEvidenceSubjectDeclaration = {
|
||||||
format: z.literal("raw-binary"),
|
format: z.literal("raw-binary"),
|
||||||
architecture: z.enum(BINARY_ARCHITECTURES).nullable(),
|
architecture: z.enum(BINARY_ARCHITECTURES).nullable(),
|
||||||
},
|
};
|
||||||
{
|
|
||||||
format: z.literal("gamecube-dol"),
|
const gamecubeEvidenceSubjectDeclaration = {
|
||||||
architecture: z.literal("powerpc"),
|
format: z.literal("gamecube-dol"),
|
||||||
},
|
architecture: z.literal("powerpc"),
|
||||||
] as const;
|
};
|
||||||
|
|
||||||
|
/** 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 { createServer } from "../../../src/server/createServer.js";
|
||||||
import { JSON_BYTE_ORDER_MARK_MESSAGE } from "../../../src/application/Utf8JsonInput.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 () => {
|
it("preserves BOM recovery through public bundle import", async () => {
|
||||||
const root = await createTestTempDirectory("rea-bundle-bom-mcp-");
|
const root = await createTestTempDirectory("rea-bundle-bom-mcp-");
|
||||||
const path = join(root, "bundle.json");
|
const path = join(root, "bundle.json");
|
||||||
|
|||||||
Reference in new issue
Block a user