diff --git a/packages/cli/src/recipe-evidence.ts b/packages/cli/src/recipe-evidence.ts index 729b18484..f9a8d1520 100644 --- a/packages/cli/src/recipe-evidence.ts +++ b/packages/cli/src/recipe-evidence.ts @@ -4,7 +4,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises" import { dirname, join, relative, resolve } from "node:path" import { fileURLToPath } from "node:url" import { promisify } from "node:util" -import { checkWorkspacePolicy, isPlainObject as isRecord, refreshArtifactManifestFileSha256s, sha256StableJson, upsertArtifactManifestFiles, verifyArtifactBundle, type ArtifactBundle, type ArtifactBundleVerificationResult, type ArtifactManifest, type ArtifactManifestFile, type ExecutionResult, type Runtime, type RuntimeInfo, type RuntimePolicy, type WorkspacePolicyResult, type WorkspaceRecipe } from "@chubes4/wp-codebox-core" +import { artifactFileDigest, artifactManifestFileWithSha256, checkWorkspacePolicy, isPlainObject as isRecord, refreshArtifactManifestFileSha256s, sha256StableJson, upsertArtifactManifestFiles, verifyArtifactBundle, type ArtifactBundle, type ArtifactBundleVerificationResult, type ArtifactManifest, type ArtifactManifestFile, type ExecutionResult, type Runtime, type RuntimeInfo, type RuntimePolicy, type WorkspacePolicyResult, type WorkspaceRecipe } from "@chubes4/wp-codebox-core" export interface RecipeArtifactEvidenceFile { path: string @@ -415,11 +415,11 @@ async function readPatchSummary(path: string): Promise { const hash = createHash("sha256").update("wp-codebox/artifact-content/v1\n") for (const [index, input] of inputs.entries()) { @@ -69,7 +87,7 @@ export async function calculateArtifactManifestFileSha256(directory: string, man return calculateArtifactManifestSelfSha256(manifest, manifestFileName) } - return createHash("sha256").update(await readFile(join(directory, file.path))).digest("hex") + return artifactFileDigest(await readFile(join(directory, file.path))).value } export function calculateArtifactManifestSelfSha256(manifest: ArtifactManifest, manifestFileName = "manifest.json"): string { @@ -108,7 +126,7 @@ function manifestWithPlaceholderSelfHash(manifest: ArtifactManifest, manifestFil return { ...manifest, files: manifest.files.map((file) => file.path === manifestFileName - ? { ...file, sha256: { algorithm: "sha256", value: "0".repeat(64) } } + ? { ...file, sha256: placeholderArtifactFileDigest() } : file), } } diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index 8fa930659..96f216bbb 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -1,7 +1,7 @@ import { createHash } from "node:crypto" import { lstat, readdir, readFile, realpath } from "node:fs/promises" import { isAbsolute, join, normalize, relative, sep } from "node:path" -import { calculateArtifactContentDigest, calculateArtifactManifestFileSha256 } from "./artifact-manifest.js" +import { artifactFileDigest, calculateArtifactContentDigest, calculateArtifactManifestFileSha256 } from "./artifact-manifest.js" import type { ArtifactFileDigest, ArtifactManifest, ArtifactManifestFile, ArtifactSpec } from "./artifact-manifest.js" import { RUNTIME_EPISODE_ACTION_SCHEMA, RUNTIME_EPISODE_OBSERVATION_SCHEMA, RUNTIME_EPISODE_SNAPSHOT_SCHEMA, RUNTIME_EPISODE_TRACE_SCHEMA, validateRuntimeEpisodeTrace } from "./runtime-episode.js" import { RUNTIME_REFERENCE_MANIFEST_SCHEMA, RUNTIME_REPLAY_REFERENCE_INDEX_SCHEMA, runtimeReferenceManifestDigest, runtimeReplayReferenceIndexDigest } from "./runtime-reference.js" @@ -1035,7 +1035,7 @@ async function verifyReviewEvidence(directory: string, manifest: ArtifactManifes validateArtifactReference(evidence.patch, "files/review.json:evidence.patch", manifestFiles, violations) if (typeof evidence.patchSha256 === "string") { try { - const patchSha256 = createHash("sha256").update(await readFile(join(directory, evidence.patch))).digest("hex") + const patchSha256 = artifactFileDigest(await readFile(join(directory, evidence.patch))).value if (patchSha256 !== evidence.patchSha256) { violations.push({ code: "review-evidence-mismatch", path: "files/review.json:evidence.patchSha256", file: "files/review.json", message: "Review patchSha256 does not match the referenced patch file." }) } @@ -1235,7 +1235,7 @@ async function verifyRuntimeEpisodeTraceRefFileDigest(directory: string, ref: Ru } try { - const value = createHash("sha256").update(await readFile(join(directory, ref.path))).digest("hex") + const value = artifactFileDigest(await readFile(join(directory, ref.path))).value if (value !== ref.digest.value) { violations.push({ code: "file-hash-mismatch", path, file: ref.path, message: `Runtime reference artifact ref hash does not match ${ref.path}: expected ${value}, got ${ref.digest.value}` }) } @@ -1251,7 +1251,7 @@ async function verifyReferencedFileDigest(directory: string, file: RuntimeRefere } try { - const value = createHash("sha256").update(await readFile(join(directory, file.path))).digest("hex") + const value = artifactFileDigest(await readFile(join(directory, file.path))).value if (value !== file.sha256.value) { violations.push({ code: "file-hash-mismatch", path, file: file.path, message: `Runtime reference manifest file ref hash does not match ${file.path}: expected ${value}, got ${file.sha256.value}` }) } diff --git a/packages/runtime-core/src/runtime-episode.ts b/packages/runtime-core/src/runtime-episode.ts index 194d9cc60..a908fe714 100644 --- a/packages/runtime-core/src/runtime-episode.ts +++ b/packages/runtime-core/src/runtime-episode.ts @@ -2,8 +2,8 @@ import { createHash } from "node:crypto" import { mkdir, readFile, writeFile } from "node:fs/promises" import { join } from "node:path" -import { refreshArtifactManifestFileSha256s, upsertArtifactManifestFiles } from "./artifact-manifest.js" -import type { ArtifactManifest, ArtifactManifestFile } from "./artifact-manifest.js" +import { artifactFileDigest, artifactManifestFile, refreshArtifactManifestFileSha256s, upsertArtifactManifestFiles } from "./artifact-manifest.js" +import type { ArtifactManifest } from "./artifact-manifest.js" import { isPlainObject as isRecord } from "./object-utils.js" import { RUNTIME_EPISODE_ACTION_SCHEMA, @@ -140,10 +140,6 @@ function runtimeEpisodeJsonLines(trace: RuntimeEpisodeTrace): string { return `${records.map((record) => JSON.stringify(record)).join("\n")}\n` } -function artifactManifestFile(path: string, kind: string, contentType: string): ArtifactManifestFile { - return { path, kind, contentType, sha256: { algorithm: "sha256", value: "0".repeat(64) } } -} - export async function createRuntimeEpisode(spec: RuntimeEpisodeSpec, backend: RuntimeBackend): Promise { return RuntimeEpisodeRunner.create(spec, backend) } @@ -303,7 +299,7 @@ class RuntimeEpisodeRunner implements RuntimeEpisode { refs: baseRefs, } await writeFile(join(this.artifacts.directory, relativePath), `${JSON.stringify(bundle, null, 2)}\n`) - const digest = { algorithm: "sha256" as const, value: createHash("sha256").update(await readFile(join(this.artifacts.directory, relativePath))).digest("hex") } + const digest = artifactFileDigest(await readFile(join(this.artifacts.directory, relativePath))) const artifactRef: RuntimeEpisodeTraceRef = { kind: "runtime-snapshot-bundle", id: bundleId, diff --git a/packages/runtime-core/src/runtime-reference.ts b/packages/runtime-core/src/runtime-reference.ts index 0c1e9f916..975fa6975 100644 --- a/packages/runtime-core/src/runtime-reference.ts +++ b/packages/runtime-core/src/runtime-reference.ts @@ -1,6 +1,7 @@ import { createHash } from "node:crypto" import type { ArtifactFileDigest } from "./artifact-manifest.js" +import { stableJson } from "./object-utils.js" import type { ObservationResult, RuntimeEpisodeContentDigest, @@ -376,21 +377,6 @@ function runtimeEpisodeSnapshotDigestPayload(snapshot: Snapshot): Record stableJson(item)).join(",")}]` - } - - return `{${Object.keys(value) - .sort() - .map((key) => `${JSON.stringify(key)}:${stableJson((value as Record)[key])}`) - .join(",")}}` -} - function compactUndefined(value: T): T { return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined)) as T } diff --git a/packages/runtime-playground/src/artifact-bundle-builder.ts b/packages/runtime-playground/src/artifact-bundle-builder.ts index f86bf1afd..d75c5f863 100644 --- a/packages/runtime-playground/src/artifact-bundle-builder.ts +++ b/packages/runtime-playground/src/artifact-bundle-builder.ts @@ -3,6 +3,8 @@ import { join, relative } from "node:path" import { buildRuntimeReferenceManifest, buildRuntimeReplayReferenceIndex, + artifactManifestFile, + artifactManifestFileWithSha256, calculateArtifactManifestFileSha256, refreshArtifactManifestFileSha256s, type ArtifactBundle, @@ -28,7 +30,6 @@ import { buildBlueprintAfter, buildBlueprintAfterNotes, buildTestResults, - fileEntry, serializeCapturedMountFiles, type CapturedMountFiles, type MountDiffsResult, @@ -109,7 +110,7 @@ export class ArtifactBundleBuilder { const runtimeSnapshotFiles = runtimeSnapshots.flatMap((snapshot) => (snapshot.artifactRefs ?? []) .filter((ref): ref is typeof ref & { path: string } => typeof ref.path === "string" && ref.path.length > 0) - .map((ref) => fileEntry(join(source.artifactRoot, ref.path), "runtime-snapshot", "application/json")), + .map((ref) => artifactManifestFile(join(source.artifactRoot, ref.path), "runtime-snapshot", "application/json")), ) const capturedMounts = await source.captureMountedFiles(filesDirectory, redactor) const { mountDiffs, changedFiles, patch } = await source.captureMountDiffs(filesDirectory, redactor) @@ -180,33 +181,33 @@ export class ArtifactBundleBuilder { }) const manifestFiles: ArtifactManifestFile[] = [ - fileEntry(manifestPath, "manifest", "application/json"), - fileEntry(metadataPath, "metadata", "application/json"), - fileEntry(blueprintAfterPath, "blueprint-after", "application/json"), - fileEntry(blueprintAfterNotesPath, "blueprint-after-notes", "application/json"), - fileEntry(eventsPath, "events", "application/x-ndjson"), - fileEntry(commandsPath, "commands", "application/x-ndjson"), - fileEntry(observationsPath, "observations", "application/x-ndjson"), - fileEntry(runtimeLogPath, "log", "text/plain"), - fileEntry(commandsLogPath, "log", "text/plain"), - fileEntry(mountsPath, "mounts", "application/json"), - fileEntry(capturedMountsPath, "mounted-files", "application/json"), - fileEntry(diffsPath, "mount-diffs", "application/json"), - fileEntry(changedFilesPath, "changed-files", "application/json"), - fileEntry(patchPath, "patch", "text/x-diff"), - fileEntry(testResultsPath, "test-results", "application/json"), - fileEntry(reviewPath, "review", "application/json"), - fileEntry(runtimeReferenceManifestPath, "runtime-reference-manifest", "application/json"), - fileEntry(runtimeReferenceIndexPath, "runtime-reference-index", "application/json"), - fileEntry(runtimeReplayReferenceIndexPath, "runtime-replay-index", "application/json"), + artifactManifestFile(manifestPath, "manifest", "application/json"), + artifactManifestFile(metadataPath, "metadata", "application/json"), + artifactManifestFile(blueprintAfterPath, "blueprint-after", "application/json"), + artifactManifestFile(blueprintAfterNotesPath, "blueprint-after-notes", "application/json"), + artifactManifestFile(eventsPath, "events", "application/x-ndjson"), + artifactManifestFile(commandsPath, "commands", "application/x-ndjson"), + artifactManifestFile(observationsPath, "observations", "application/x-ndjson"), + artifactManifestFile(runtimeLogPath, "log", "text/plain"), + artifactManifestFile(commandsLogPath, "log", "text/plain"), + artifactManifestFile(mountsPath, "mounts", "application/json"), + artifactManifestFile(capturedMountsPath, "mounted-files", "application/json"), + artifactManifestFile(diffsPath, "mount-diffs", "application/json"), + artifactManifestFile(changedFilesPath, "changed-files", "application/json"), + artifactManifestFile(patchPath, "patch", "text/x-diff"), + artifactManifestFile(testResultsPath, "test-results", "application/json"), + artifactManifestFile(reviewPath, "review", "application/json"), + artifactManifestFile(runtimeReferenceManifestPath, "runtime-reference-manifest", "application/json"), + artifactManifestFile(runtimeReferenceIndexPath, "runtime-reference-index", "application/json"), + artifactManifestFile(runtimeReplayReferenceIndexPath, "runtime-replay-index", "application/json"), ...source.browserManifestFiles(), ...source.observationManifestFiles(), ...source.pluginCheckManifestFiles(), ...source.themeCheckManifestFiles(), ...runtimeSnapshotFiles, - ...mountDiffs.map((diff) => fileEntry(join(source.artifactRoot, diff.artifactPath), "diff", "text/x-diff")), + ...mountDiffs.map((diff) => artifactManifestFile(join(source.artifactRoot, diff.artifactPath), "diff", "text/x-diff")), ...capturedMounts.files.map((file) => - fileEntry(join(source.artifactRoot, file.artifactPath), "file", file.contentType), + artifactManifestFile(join(source.artifactRoot, file.artifactPath), "file", file.contentType), ), ] @@ -275,15 +276,12 @@ export class ArtifactBundleBuilder { snapshots: runtimeSnapshots, }) await writeFile(runtimeReferenceManifestPath, `${JSON.stringify(runtimeReferenceManifest, null, 2)}\n`) - const runtimeReferenceManifestRef = { - path: "files/runtime-reference-manifest.json", - kind: "runtime-reference-manifest", - contentType: "application/json", - sha256: { - algorithm: "sha256" as const, - value: await calculateArtifactManifestFileSha256(source.artifactRoot, manifest, { path: "files/runtime-reference-manifest.json", kind: "runtime-reference-manifest", contentType: "application/json", sha256: { algorithm: "sha256", value: "0".repeat(64) } }), - }, - } + const runtimeReferenceManifestRef = artifactManifestFileWithSha256( + "files/runtime-reference-manifest.json", + "runtime-reference-manifest", + "application/json", + await calculateArtifactManifestFileSha256(source.artifactRoot, manifest, artifactManifestFile("files/runtime-reference-manifest.json", "runtime-reference-manifest", "application/json")), + ) const runtimeReplayReferenceIndex = buildRuntimeReplayReferenceIndex({ createdAt, runtime, diff --git a/packages/runtime-playground/src/artifacts.ts b/packages/runtime-playground/src/artifacts.ts index 6838b669d..f67e10476 100644 --- a/packages/runtime-playground/src/artifacts.ts +++ b/packages/runtime-playground/src/artifacts.ts @@ -2,8 +2,8 @@ import { createHash } from "node:crypto" import { readdir, readFile } from "node:fs/promises" import { basename, join } from "node:path" import { normalizeBlueprint, preferredVersionsForEnvironment } from "./blueprint.js" +import { artifactFileDigest } from "@chubes4/wp-codebox-core" import type { - ArtifactManifestFile, ArtifactPreview, ArtifactProvenance, ArtifactRedactionSummary, @@ -278,7 +278,7 @@ export function buildArtifactReview({ ], evidence: { patch: "files/patch.diff", - patchSha256: createHash("sha256").update(patch).digest("hex"), + patchSha256: artifactFileDigest(patch).value, artifactContentDigest: contentDigest, changedFiles: "files/changed-files.json", testResults: "files/test-results.json", @@ -410,10 +410,6 @@ export function buildBlueprintAfterNotes({ } } -export function fileEntry(path: string, kind: ArtifactManifestFile["kind"], contentType: string): ArtifactManifestFile { - return { path, kind, contentType, sha256: { algorithm: "sha256", value: "0".repeat(64) } } -} - export function mountTargetPath(mount: MountSpec, relativePath: string): string { return `${mount.target.replace(/\/+$/, "")}/${relativePath}` } diff --git a/packages/runtime-playground/src/browser-artifacts.ts b/packages/runtime-playground/src/browser-artifacts.ts index 28d62b52d..559b889fa 100644 --- a/packages/runtime-playground/src/browser-artifacts.ts +++ b/packages/runtime-playground/src/browser-artifacts.ts @@ -1,7 +1,6 @@ import { join } from "node:path" -import type { ArtifactManifestFile, ArtifactReviewBrowserSummary } from "@chubes4/wp-codebox-core" +import { artifactManifestFile, type ArtifactManifestFile, type ArtifactReviewBrowserSummary } from "@chubes4/wp-codebox-core" import type { Request } from "playwright" -import { fileEntry } from "./artifacts.js" export interface BrowserProbeArtifact { requestedUrl: string @@ -288,7 +287,7 @@ export function browserManifestFiles(artifactRoot: string, probes: BrowserProbeA files.set(probe.files.summary, { kind: "browser-summary", contentType: "application/json" }) } - return [...files.entries()].map(([path, entry]) => fileEntry(join(artifactRoot, path), entry.kind, entry.contentType)) + return [...files.entries()].map(([path, entry]) => artifactManifestFile(join(artifactRoot, path), entry.kind, entry.contentType)) } export function browserRedactionPaths(probe: BrowserProbeArtifact): string[] { diff --git a/packages/runtime-playground/src/check-artifacts.ts b/packages/runtime-playground/src/check-artifacts.ts index c4df60194..b10af561e 100644 --- a/packages/runtime-playground/src/check-artifacts.ts +++ b/packages/runtime-playground/src/check-artifacts.ts @@ -1,7 +1,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises" import { join, relative } from "node:path" -import type { ArtifactManifestFile } from "@chubes4/wp-codebox-core" -import { fileEntry, type ArtifactRedactor } from "./artifacts.js" +import { artifactManifestFile, type ArtifactManifestFile } from "@chubes4/wp-codebox-core" +import type { ArtifactRedactor } from "./artifacts.js" import type { normalizePluginCheckOutput, normalizeThemeCheckOutput } from "./commands.js" export interface PluginCheckArtifact { @@ -76,8 +76,8 @@ export async function writeThemeCheckArtifacts( export function pluginCheckManifestFiles(artifactRoot: string, pluginChecks: PluginCheckArtifact[]): ArtifactManifestFile[] { return pluginChecks.flatMap((check) => [ - fileEntry(join(artifactRoot, check.files.raw), "plugin-check-raw", "application/json"), - fileEntry(join(artifactRoot, check.files.normalized), "plugin-check", "application/json"), + artifactManifestFile(join(artifactRoot, check.files.raw), "plugin-check-raw", "application/json"), + artifactManifestFile(join(artifactRoot, check.files.normalized), "plugin-check", "application/json"), ]) } @@ -92,7 +92,7 @@ export function themeCheckManifestFiles(artifactRoot: string, themeChecks: Theme files.set(check.files.normalized, { kind: "theme-check-normalized", contentType: "application/json" }) } - return [...files.entries()].map(([path, entry]) => fileEntry(join(artifactRoot, path), entry.kind, entry.contentType)) + return [...files.entries()].map(([path, entry]) => artifactManifestFile(join(artifactRoot, path), entry.kind, entry.contentType)) } export async function redactPluginCheckArtifacts(artifactRoot: string, pluginChecks: PluginCheckArtifact[], redactor: ArtifactRedactor): Promise { diff --git a/packages/runtime-playground/src/runtime-artifact-helpers.ts b/packages/runtime-playground/src/runtime-artifact-helpers.ts index 26197a1fc..f8bbc986c 100644 --- a/packages/runtime-playground/src/runtime-artifact-helpers.ts +++ b/packages/runtime-playground/src/runtime-artifact-helpers.ts @@ -1,5 +1,6 @@ import { readFile, writeFile } from "node:fs/promises" import { join } from "node:path" +import { artifactManifestFile } from "@chubes4/wp-codebox-core" import type { ArtifactBundle, ArtifactManifestFile, @@ -15,7 +16,7 @@ import type { Snapshot, } from "@chubes4/wp-codebox-core" import { ArtifactBundleBuilder } from "./artifact-bundle-builder.js" -import { fileEntry, type ArtifactRedactor } from "./artifacts.js" +import type { ArtifactRedactor } from "./artifacts.js" import { browserManifestFiles as browserArtifactManifestFiles, browserRedactionPaths, browserReviewSummary as browserArtifactReviewSummary, type BrowserProbeArtifact } from "./browser-artifacts.js" import { pluginCheckManifestFiles, redactPluginCheckArtifacts, redactThemeCheckArtifacts, themeCheckManifestFiles, type PluginCheckArtifact, type ThemeCheckArtifact } from "./check-artifacts.js" import { captureMountDiffs, captureMountedFiles } from "./mounted-artifact-capture.js" @@ -104,7 +105,7 @@ function observationManifestFiles(artifactRoot: string, observations: Observatio return observations.flatMap((observation) => (observation.artifactRefs ?? []) .filter((ref): ref is RuntimeEpisodeTraceRef & { path: string } => typeof ref.path === "string" && ref.path.length > 0) - .map((ref) => fileEntry(join(artifactRoot, ref.path), ref.kind, ref.path.endsWith(".json") ? "application/json" : "text/plain")), + .map((ref) => artifactManifestFile(join(artifactRoot, ref.path), ref.kind, ref.path.endsWith(".json") ? "application/json" : "text/plain")), ) }