fix: unify release runtime coherence verification

This commit is contained in:
DongHyeonka
2026-08-02 03:38:49 +09:00
parent 184bd98d92
commit 990603e24a
5 changed files with 406 additions and 98 deletions
+110 -70
View File
@@ -1,4 +1,5 @@
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
import { pathToFileURL } from "node:url";
import { shouldRetry } from "../src/adapters/http/retry-policy.ts";
import { createTelemetryAdapter } from "../src/adapters/telemetry/best-effort-telemetry.ts";
@@ -6,8 +7,17 @@ import { verifyCompatibilityTuple } from "../src/application/policies/compatibil
import type { StoragePort } from "../src/application/ports/storage-port.ts";
import { decideChunkRecovery } from "../src/application/use-cases/decide-chunk-recovery.ts";
import { validateRuntimeConfig } from "../src/bootstrap/runtime-config-schema.ts";
import { compareReleaseToRuntime } from "../src/contracts/release-tokens.ts";
import {
parseReleaseArtifact,
parseRuntimeConfigArtifact,
type ReleaseArtifact,
} from "../src/contracts/release-artifacts.ts";
import { projectTelemetryEvent } from "../src/contracts/telemetry.ts";
import { EXPECTED_CONTRACT_SET_PACKAGES } from "../src/features/installed-contract-contributions.ts";
import {
verifyReleaseRuntimeCoherence,
type ReleaseRuntimeCoherenceInput,
} from "./lib/release-runtime-coherence.ts";
type RecoveryAssertion = Readonly<{
assertion: string;
@@ -38,35 +48,15 @@ type RunbookDocument = Readonly<{
runbooks: Record<string, RunbookSpecification>;
}>;
type ReleaseManifest = Record<string, unknown> & {
buildId: string;
configSchemaVersion: string;
apiContractVersion: string;
assetManifestHash: string;
releaseId: string;
};
const runbookId = process.argv
.slice(2)
.find((argument) => /^FE-RB-00[1-5]$/.test(argument));
const document = JSON.parse(
await readFile("config/runbooks/runbooks.json", "utf8"),
) as RunbookDocument;
const specification = runbookId ? document.runbooks[runbookId] : undefined;
if (!runbookId || !specification) {
process.stderr.write("Usage: drill:runbook -- FE-RB-001..FE-RB-005\n");
process.exit(2);
}
async function releaseManifest(): Promise<ReleaseManifest> {
async function releaseManifest(): Promise<ReleaseArtifact> {
for (const candidate of [
"dist/release-manifest.json",
"public/release-manifest.json",
]) {
try {
return JSON.parse(
await readFile(candidate, "utf8"),
) as ReleaseManifest;
return parseReleaseArtifact(
JSON.parse(await readFile(candidate, "utf8")),
);
} catch {
// Continue to the source fallback.
}
@@ -238,15 +228,29 @@ async function drillTelemetry(): Promise<DrillResult> {
async function drillRollback(): Promise<DrillResult> {
const release = await releaseManifest();
const runtime = JSON.parse(
await readFile(
(await access("dist/config.json").then(() => true).catch(() => false))
? "dist/config.json"
: "public/config.json",
"utf8",
const runtimeArtifact = parseRuntimeConfigArtifact(
JSON.parse(
await readFile(
(await access("dist/config.json").then(() => true).catch(() => false))
? "dist/config.json"
: "public/config.json",
"utf8",
),
),
);
const coherent = compareReleaseToRuntime(release, runtime);
const runtime = {
...runtimeArtifact,
BUILD_ID: requireIdentity(runtimeArtifact.BUILD_ID, "runtime BUILD_ID"),
RELEASE_ID: requireIdentity(
runtimeArtifact.RELEASE_ID,
"runtime RELEASE_ID",
),
};
const coherent = await verifyRollbackReleaseCoherence({
release,
runtime,
contractPackages: EXPECTED_CONTRACT_SET_PACKAGES,
});
const mixed = verifyCompatibilityTuple({
frontend: {
buildId: "build-a",
@@ -284,42 +288,78 @@ const drillById: Record<string, () => Promise<DrillResult>> = {
"FE-RB-004": drillTelemetry,
"FE-RB-005": drillRollback,
};
const drill = await drillById[runbookId]();
const escalationPathAsserted = specification.escalation.length >= 2;
const passed =
drill.triggerAsserted &&
drill.containmentAsserted &&
escalationPathAsserted &&
drill.recoveryAssertions.every((item) => item.passed) &&
drill.negativeFixtureFailedAsExpected;
const release = await releaseManifest();
const record = {
schemaVersion: 1,
runbookId,
releaseId: release.releaseId,
drillTimestamp: new Date().toISOString(),
triggerInjected: specification.triggerKinds[0],
triggerAsserted: drill.triggerAsserted,
containmentAsserted: drill.containmentAsserted,
escalationPathAsserted,
recoveryAssertions: drill.recoveryAssertions,
negativeFixtureFailedAsExpected: drill.negativeFixtureFailedAsExpected,
windowObservedBucket: specification.window,
providerVerificationRequired: drill.providerVerificationRequired,
passed,
};
const artifactDirectory = `artifacts/runbooks/${runbookId}`;
await mkdir(artifactDirectory, { recursive: true });
await writeFile(
`${artifactDirectory}/record.json`,
`${JSON.stringify(record, null, 2)}\n`,
);
if (!passed) {
process.stderr.write(`${runbookId} drill failed.\n`);
process.exit(1);
export async function verifyRollbackReleaseCoherence(
input: ReleaseRuntimeCoherenceInput,
) {
return verifyReleaseRuntimeCoherence(input);
}
async function main(): Promise<void> {
const runbookId = process.argv
.slice(2)
.find((argument) => /^FE-RB-00[1-5]$/.test(argument));
const document = JSON.parse(
await readFile("config/runbooks/runbooks.json", "utf8"),
) as RunbookDocument;
const specification = runbookId ? document.runbooks[runbookId] : undefined;
if (!runbookId || !specification) {
process.stderr.write("Usage: drill:runbook -- FE-RB-001..FE-RB-005\n");
process.exit(2);
}
const drill = await drillById[runbookId]();
const escalationPathAsserted = specification.escalation.length >= 2;
const passed =
drill.triggerAsserted &&
drill.containmentAsserted &&
escalationPathAsserted &&
drill.recoveryAssertions.every((item) => item.passed) &&
drill.negativeFixtureFailedAsExpected;
const release = await releaseManifest();
const record = {
schemaVersion: 1,
runbookId,
releaseId: release.releaseId,
drillTimestamp: new Date().toISOString(),
triggerInjected: specification.triggerKinds[0],
triggerAsserted: drill.triggerAsserted,
containmentAsserted: drill.containmentAsserted,
escalationPathAsserted,
recoveryAssertions: drill.recoveryAssertions,
negativeFixtureFailedAsExpected: drill.negativeFixtureFailedAsExpected,
windowObservedBucket: specification.window,
providerVerificationRequired: drill.providerVerificationRequired,
passed,
};
const artifactDirectory = `artifacts/runbooks/${runbookId}`;
await mkdir(artifactDirectory, { recursive: true });
await writeFile(
`${artifactDirectory}/record.json`,
`${JSON.stringify(record, null, 2)}\n`,
);
if (!passed) {
process.stderr.write(`${runbookId} drill failed.\n`);
process.exit(1);
}
process.stdout.write(
`${runbookId} drill: PASS (${specification.gateId}; provider verification ${
drill.providerVerificationRequired ? "still required" : "not required"
})\n`,
);
}
function requireIdentity(value: string | undefined, label: string): string {
if (value === undefined || value.length === 0) {
throw new TypeError(`${label} must be a non-empty string`);
}
return value;
}
const invokedPath = process.argv[1];
if (
invokedPath !== undefined &&
import.meta.url === pathToFileURL(invokedPath).href
) {
await main();
}
process.stdout.write(
`${runbookId} drill: PASS (${specification.gateId}; provider verification ${
drill.providerVerificationRequired ? "still required" : "not required"
})\n`,
);
+85
View File
@@ -0,0 +1,85 @@
import { isVersionCompatible } from "../../src/application/policies/compatibility.ts";
import { verifyContractSet } from "../../src/contracts/contract-set.ts";
import type { InstalledContractPackageIdentity } from "../../src/contracts/external-contract-runtime.ts";
import type { ReleaseArtifact } from "../../src/contracts/release-artifacts.ts";
import { compareReleaseToRuntime } from "../../src/contracts/release-tokens.ts";
export type ReleaseRuntimeCoherenceInput = Readonly<{
release: ReleaseArtifact;
runtime: Readonly<{
BUILD_ID: string;
CONFIG_SCHEMA_VERSION: string;
API_CONTRACT_VERSION?: string;
RELEASE_ID: string;
}>;
contractPackages: readonly InstalledContractPackageIdentity[];
}>;
export type ReleaseRuntimeCoherence = Readonly<{
compatible: boolean;
mismatches: readonly string[];
warnings: readonly string[];
}>;
/**
* Verifies the runtime identity using the release schema's own contract model.
* V1 retains the scalar compatibility policy. V2 has no scalar projection: its
* identity is the exact compiled package tuple set and canonical set digest.
*/
export async function verifyReleaseRuntimeCoherence(
input: ReleaseRuntimeCoherenceInput,
): Promise<ReleaseRuntimeCoherence> {
if (input.release.schemaVersion === 1) {
if (input.runtime.API_CONTRACT_VERSION === undefined) {
return compareWithoutContractScalar(input, ["apiContractVersion"]);
}
return compareReleaseToRuntime(input.release, {
...input.runtime,
API_CONTRACT_VERSION: input.runtime.API_CONTRACT_VERSION,
});
}
const comparison = compareWithoutContractScalar(input);
const contractSet = await verifyContractSet({
expected: input.contractPackages,
manifest: input.release.contractSet,
});
if (contractSet.ok) return comparison;
const mismatches = Object.freeze([
...comparison.mismatches,
contractSet.code,
]);
return Object.freeze({
compatible: false,
mismatches,
warnings: comparison.warnings,
});
}
function compareWithoutContractScalar(
input: Pick<ReleaseRuntimeCoherenceInput, "release" | "runtime">,
initialMismatches: readonly string[] = [],
): ReleaseRuntimeCoherence {
const mismatches = [...initialMismatches];
if (input.release.buildId !== input.runtime.BUILD_ID) {
mismatches.push("buildId");
}
if (
!isVersionCompatible(
input.release.configSchemaVersion,
input.runtime.CONFIG_SCHEMA_VERSION,
)
) {
mismatches.push("configSchemaVersion");
}
const warnings =
input.release.releaseId === input.runtime.RELEASE_ID
? []
: ["releaseId"];
return Object.freeze({
compatible: mismatches.length === 0,
mismatches: Object.freeze(mismatches),
warnings: Object.freeze(warnings),
});
}
+6 -14
View File
@@ -5,9 +5,6 @@ import {
verifyCompatibilityTuple,
type CompatibilityTuple,
} from "../src/application/policies/compatibility.ts";
import {
compareReleaseToRuntime,
} from "../src/contracts/release-tokens.ts";
import {
parseBuildManifestArtifact,
parseReleaseArtifact,
@@ -17,13 +14,13 @@ import {
type ReleaseArtifact,
type RuntimeConfigArtifact,
} from "../src/contracts/release-artifacts.ts";
import { verifyContractSet } from "../src/contracts/contract-set.ts";
import { EXPECTED_CONTRACT_SET_PACKAGES } from "../src/features/installed-contract-contributions.ts";
import {
ROUTE_REGISTRY,
ROUTE_RUNTIME_CONTRACT,
} from "../src/features/installed-feature-contracts.ts";
import { assertMatchesJsonSchema } from "./lib/json-schema.ts";
import { verifyReleaseRuntimeCoherence } from "./lib/release-runtime-coherence.ts";
type CoherenceFixture = Readonly<{
name: string;
@@ -71,22 +68,17 @@ const actualAssetManifestHash = createHash("sha256")
.update(viteManifest)
.digest("hex");
const artifactComparison = compareReleaseToRuntime(release, runtimeConfig);
const artifactComparison = await verifyReleaseRuntimeCoherence({
release,
runtime: runtimeConfig,
contractPackages: EXPECTED_CONTRACT_SET_PACKAGES,
});
const artifactMismatches: string[] = [...artifactComparison.mismatches];
for (const [token, value] of Object.entries(projectReleaseTokens(release))) {
if (token !== "schemaVersion" && (typeof value !== "string" || value.length === 0)) {
artifactMismatches.push(`releaseToken:${token}`);
}
}
if (release.schemaVersion === 2) {
const contractSetVerification = await verifyContractSet({
expected: EXPECTED_CONTRACT_SET_PACKAGES,
manifest: release.contractSet,
});
if (!contractSetVerification.ok) {
artifactMismatches.push(contractSetVerification.code);
}
}
if (
typeof release.builtAt !== "string" ||
!Number.isFinite(Date.parse(release.builtAt))
+4 -14
View File
@@ -42,33 +42,23 @@ export function compareReleaseToRuntime(
release: Readonly<{
buildId: string;
configSchemaVersion: string;
apiContractVersion?: string;
apiContractVersion: string;
assetManifestHash: string;
releaseId: string;
}>,
runtimeConfig: Readonly<{
BUILD_ID: string;
CONFIG_SCHEMA_VERSION: string;
/**
* §5.1. Removed from Runtime Config V2. When neither side declares it there
* is nothing to disagree about: contract identity is verified by the
* Release Manifest V2 `contractSet` check instead.
*/
API_CONTRACT_VERSION?: string;
API_CONTRACT_VERSION: string;
RELEASE_ID: string;
}>,
) {
const declaredContractVersion =
runtimeConfig.API_CONTRACT_VERSION ?? release.apiContractVersion ?? "0";
return verifyCompatibilityTuple({
frontend: {
...release,
apiContractVersion: release.apiContractVersion ?? declaredContractVersion,
},
frontend: release,
runtime: {
buildId: runtimeConfig.BUILD_ID,
configSchemaVersion: runtimeConfig.CONFIG_SCHEMA_VERSION,
apiContractVersion: declaredContractVersion,
apiContractVersion: runtimeConfig.API_CONTRACT_VERSION,
assetManifestHash: release.assetManifestHash,
releaseId: runtimeConfig.RELEASE_ID,
},
+201
View File
@@ -1,10 +1,103 @@
import { describe, expect, it } from "vitest";
import { verifyRollbackReleaseCoherence } from "../../scripts/drill-runbook.ts";
import { verifyReleaseRuntimeCoherence } from "../../scripts/lib/release-runtime-coherence.ts";
import type { InstalledContractPackageIdentity } from "../../src/contracts/external-contract-runtime.ts";
import { computeContractSetDigest } from "../../src/contracts/contract-set-canonical.ts";
import type {
ReleaseArtifact,
RuntimeConfigArtifact,
} from "../../src/contracts/release-artifacts.ts";
import {
compareReleaseToRuntime,
RELEASE_TOKEN_REGISTRY,
} from "../../src/contracts/release-tokens.ts";
const contractPackages = [
{
packageId: "@example/accounts",
version: "1.2.3",
digest: `sha256:${"1".repeat(64)}`,
runtimeProtocolVersion: 1,
sourceRevision: "a".repeat(40),
},
{
packageId: "@example/billing",
version: "2.3.4",
digest: `sha256:${"2".repeat(64)}`,
runtimeProtocolVersion: 1,
sourceRevision: "b".repeat(40),
},
] as const satisfies readonly InstalledContractPackageIdentity[];
const releaseV1 = {
schemaVersion: 1,
appVersion: "1.0.0",
buildId: "build-a",
commitSha: "abc1234",
configSchemaVersion: "1",
apiContractVersion: "1.4.0",
assetManifestHash: "assets-a",
releaseId: "release-a",
builtAt: "2026-08-01T00:00:00.000Z",
routeChunks: {},
} as const satisfies ReleaseArtifact;
const runtimeV1 = {
APP_ENV: "local",
API_BASE_URL: "http://localhost:8080/",
REQUEST_TIMEOUT_MS: 10_000,
MAX_RETRY_ATTEMPTS: 2,
TELEMETRY_ENABLED: false,
AUTH_MODE: "external",
CONFIG_SCHEMA_VERSION: "1",
API_CONTRACT_VERSION: "1.6.0",
RELEASE_MANIFEST_URL: "/release-manifest.json",
BUILD_ID: "build-a",
RELEASE_ID: "release-a",
} as const satisfies RuntimeConfigArtifact;
const runtimeV2 = {
APP_ENV: "local",
API_BASE_URL: "http://localhost:8080/",
REQUEST_TIMEOUT_MS: 10_000,
MAX_RETRY_ATTEMPTS: 2,
TELEMETRY_ENABLED: false,
AUTH_MODE: "external",
CONFIG_SCHEMA_VERSION: "2.0",
RELEASE_MANIFEST_URL: "/release-manifest.json",
BUILD_ID: "build-a",
RELEASE_ID: "release-a",
CAPABILITY_OVERRIDES: {
REALTIME: "DEFAULT",
WEB_WORKER: "DEFAULT",
SERVICE_WORKER: "DEFAULT",
OFFLINE_COMMANDS: "DEFAULT",
},
} as const satisfies RuntimeConfigArtifact;
async function releaseV2With(
packages: readonly InstalledContractPackageIdentity[],
setDigest?: `sha256:${string}`,
): Promise<ReleaseArtifact> {
return {
schemaVersion: 2,
appVersion: "1.0.0",
buildId: "build-a",
commitSha: "abc1234",
configSchemaVersion: "2.0",
assetManifestHash: "assets-a",
releaseId: "release-a",
builtAt: "2026-08-01T00:00:00.000Z",
routeChunks: {},
contractSet: {
setAlgorithm: "CA_CONTRACT_SET_V1",
setDigest: setDigest ?? (await computeContractSetDigest(packages)),
packages: [...packages],
},
};
}
describe("release coherence", () => {
it("owns all nine release tokens and keeps builtAt diagnostic-only", () => {
// §5.2 adds contractSetDigest beside the legacy apiContractVersion scalar.
@@ -55,4 +148,112 @@ describe("release coherence", () => {
mismatches: ["buildId", "configSchemaVersion", "apiContractVersion"],
});
});
it("gives release verification and rollback drills the same V1/V2 tamper verdicts", async () => {
const packageAdded = [
...contractPackages,
{
packageId: "@example/notifications",
version: "3.0.0",
digest: `sha256:${"3".repeat(64)}`,
runtimeProtocolVersion: 1,
sourceRevision: "c".repeat(40),
},
] as const satisfies readonly InstalledContractPackageIdentity[];
const packageRemoved = contractPackages.slice(0, 1);
const versionChanged = [
{ ...contractPackages[0], version: "1.2.4" },
contractPackages[1],
] as const satisfies readonly InstalledContractPackageIdentity[];
const packageDigestChanged = [
{
...contractPackages[0],
digest: `sha256:${"f".repeat(64)}`,
},
contractPackages[1],
] as const satisfies readonly InstalledContractPackageIdentity[];
const exactV2 = await releaseV2With(contractPackages);
const matrix = [
{
name: "V1 scalar success",
release: releaseV1,
runtime: runtimeV1,
expectedCompatible: true,
},
{
name: "V1 scalar mismatch",
release: releaseV1,
runtime: { ...runtimeV1, API_CONTRACT_VERSION: "2.0.0" },
expectedCompatible: false,
},
{
name: "V2 exact package set",
release: exactV2,
runtime: runtimeV2,
expectedCompatible: true,
},
{
name: "V2 exact package set in non-canonical manifest order",
release: await releaseV2With([...contractPackages].reverse()),
runtime: runtimeV2,
expectedCompatible: true,
},
{
name: "V2 package added",
release: await releaseV2With(packageAdded),
runtime: runtimeV2,
expectedCompatible: false,
},
{
name: "V2 package removed",
release: await releaseV2With(packageRemoved),
runtime: runtimeV2,
expectedCompatible: false,
},
{
name: "V2 package version tampered",
release: await releaseV2With(versionChanged),
runtime: runtimeV2,
expectedCompatible: false,
},
{
name: "V2 package digest tampered",
release: await releaseV2With(packageDigestChanged),
runtime: runtimeV2,
expectedCompatible: false,
},
{
name: "V2 set digest tampered",
release: await releaseV2With(
contractPackages,
`sha256:${"0".repeat(64)}`,
),
runtime: runtimeV2,
expectedCompatible: false,
},
];
for (const fixture of matrix) {
const input = {
release: fixture.release,
runtime: fixture.runtime,
contractPackages,
};
const verifierVerdict = (
await verifyReleaseRuntimeCoherence(input)
).compatible;
const rollbackDrillVerdict = (
await verifyRollbackReleaseCoherence(input)
).compatible;
expect(verifierVerdict, `${fixture.name}: verifier`).toBe(
fixture.expectedCompatible,
);
expect(rollbackDrillVerdict, `${fixture.name}: rollback drill`).toBe(
fixture.expectedCompatible,
);
expect(rollbackDrillVerdict, `${fixture.name}: identical verdict`).toBe(
verifierVerdict,
);
}
});
});