Files
qinglong/back/runtime/domain/manualPrimaryCanaryCeremony.ts

477 lines
15 KiB
TypeScript

import { createHash } from 'crypto';
import {
parseLegacyShadowPrimaryGateReceipt,
type LegacyShadowPrimaryGateReceipt,
} from './legacyShadowPrimaryGate';
import {
parseRuntimeRolloutManifest,
REQUIRED_RUNTIME_ROLLOUT_GATES,
type DisabledRuntimeRolloutManifest,
type EnabledRuntimeRolloutManifest,
} from './runtimeRolloutManifest';
export const MANUAL_PRIMARY_CANARY_PLAN_SCHEMA =
'qinglong/manual-primary-canary-plan@v1';
export const MANUAL_PRIMARY_CANARY_QUALIFICATION_SCHEMA =
'qinglong/manual-primary-canary-qualification@v1';
export const MANUAL_PRIMARY_CANARY_MINIMUM_SETTLING_AGE_MS = 5 * 60_000;
export const MANUAL_PRIMARY_CANARY_MAX_APPROVAL_MS = 24 * 60 * 60 * 1_000;
const SESSION_PATTERN = /^[a-z0-9](?:[a-z0-9-]{6,62}[a-z0-9])?$/u;
const SHA256_PATTERN = /^[a-f0-9]{64}$/u;
export type ManualPrimaryCanaryProfile = 'edge' | 'standalone';
export type ManualPrimaryCanaryCurrentRollout =
| { state: 'absent' }
| { state: 'disabled'; sha256: string };
export interface ManualPrimaryCanaryFileSet {
plan: string;
capture: string;
terminal: string;
resource: string;
primaryGate: string;
qualification: string;
selection: string;
previousRollout: string;
rollbackIntent: string;
rollbackComplete: string;
rollout: 'qinglong3-rollout.json';
}
export interface ManualPrimaryCanaryPlan {
schema: typeof MANUAL_PRIMARY_CANARY_PLAN_SCHEMA;
schemaVersion: 1;
sessionId: string;
profile: ManualPrimaryCanaryProfile;
origin: 'manual';
createdAtMs: number;
admissionTarget: number;
minimumSettlingAgeMs: typeof MANUAL_PRIMARY_CANARY_MINIMUM_SETTLING_AGE_MS;
currentRollout: ManualPrimaryCanaryCurrentRollout;
files: ManualPrimaryCanaryFileSet;
activation: {
maxApprovalMs: typeof MANUAL_PRIMARY_CANARY_MAX_APPROVAL_MS;
defaultMode: 'off';
allowLegacyFallbackBeforeStart: false;
};
}
export interface ManualPrimaryCanaryQualification {
schema: typeof MANUAL_PRIMARY_CANARY_QUALIFICATION_SCHEMA;
schemaVersion: 1;
sessionId: string;
profile: ManualPrimaryCanaryProfile;
origin: 'manual';
qualifiedAtMs: number;
assessment: 'eligible';
planSha256: string;
primaryGateFileSha256: string;
sourceFileSha256: {
capture: string;
terminal: string;
resource: string;
};
sourceCanonicalSha256: {
capture: string;
terminal: string;
resource: string;
};
window: {
startInclusiveMs: number;
endExclusiveMs: number;
};
counts: {
admitted: number;
captured: number;
terminalScanned: number;
terminalMatched: number;
};
}
function record(value: unknown, name: string): Record<string, unknown> {
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
throw new TypeError(`${name} must be an object`);
}
return value as Record<string, unknown>;
}
function exactKeys(
value: Record<string, unknown>,
expected: readonly string[],
name: string,
): void {
const actual = Object.keys(value).sort();
const wanted = [...expected].sort();
if (
actual.length !== wanted.length ||
actual.some((key, index) => key !== wanted[index])
) {
throw new TypeError(`${name} shape is invalid`);
}
}
function safeInteger(value: unknown, name: string): number {
if (!Number.isSafeInteger(value) || Number(value) < 0) {
throw new TypeError(`${name} must be a non-negative safe integer`);
}
return Number(value);
}
function sha256(value: unknown, name: string): string {
if (typeof value !== 'string' || !SHA256_PATTERN.test(value)) {
throw new TypeError(`${name} must be a SHA-256 digest`);
}
return value;
}
function sessionId(value: unknown): string {
if (typeof value !== 'string' || !SESSION_PATTERN.test(value)) {
throw new TypeError('Manual Primary canary session id is invalid');
}
return value;
}
function profile(value: unknown): ManualPrimaryCanaryProfile {
if (value !== 'edge' && value !== 'standalone') {
throw new TypeError('Manual Primary canary profile is invalid');
}
return value;
}
function admissionTarget(
selectedProfile: ManualPrimaryCanaryProfile,
value: unknown,
): number {
const target = safeInteger(value, 'admissionTarget');
if (
(selectedProfile === 'edge' && target !== 8) ||
(selectedProfile === 'standalone' && (target < 32 || target > 128))
) {
throw new TypeError('Manual Primary canary admission target is invalid');
}
return target;
}
export function manualPrimaryCanaryFileSet(
rawSessionId: string,
): ManualPrimaryCanaryFileSet {
const id = sessionId(rawSessionId);
const prefix = `ql3-primary-canary-${id}`;
return Object.freeze({
plan: `${prefix}.plan.json`,
capture: `${prefix}.capture.json`,
terminal: `${prefix}.terminal.json`,
resource: `${prefix}.resource.json`,
primaryGate: `${prefix}.primary-gate.json`,
qualification: `${prefix}.qualification.json`,
selection: `${prefix}.selection.json`,
previousRollout: `${prefix}.previous-rollout.json`,
rollbackIntent: `${prefix}.rollback-intent.json`,
rollbackComplete: `${prefix}.rollback-complete.json`,
rollout: 'qinglong3-rollout.json',
});
}
export function manualPrimaryCanarySha256(bytes: string | Buffer): string {
return createHash('sha256').update(bytes).digest('hex');
}
export function createManualPrimaryCanaryPlan(input: {
sessionId: string;
profile: ManualPrimaryCanaryProfile;
createdAtMs: number;
admissionTarget: number;
currentRollout: ManualPrimaryCanaryCurrentRollout;
}): ManualPrimaryCanaryPlan {
const id = sessionId(input.sessionId);
const selectedProfile = profile(input.profile);
const createdAtMs = safeInteger(input.createdAtMs, 'createdAtMs');
const target = admissionTarget(selectedProfile, input.admissionTarget);
const currentRollout = input.currentRollout;
if (currentRollout.state === 'disabled') {
sha256(currentRollout.sha256, 'currentRollout.sha256');
} else if (currentRollout.state !== 'absent') {
throw new TypeError('Current rollout state is invalid');
}
return Object.freeze({
schema: MANUAL_PRIMARY_CANARY_PLAN_SCHEMA,
schemaVersion: 1,
sessionId: id,
profile: selectedProfile,
origin: 'manual',
createdAtMs,
admissionTarget: target,
minimumSettlingAgeMs: MANUAL_PRIMARY_CANARY_MINIMUM_SETTLING_AGE_MS,
currentRollout: { ...currentRollout },
files: manualPrimaryCanaryFileSet(id),
activation: {
maxApprovalMs: MANUAL_PRIMARY_CANARY_MAX_APPROVAL_MS,
defaultMode: 'off' as const,
allowLegacyFallbackBeforeStart: false as const,
},
});
}
export function parseManualPrimaryCanaryPlan(
value: unknown,
): ManualPrimaryCanaryPlan {
const plan = record(value, 'plan');
exactKeys(
plan,
[
'schema',
'schemaVersion',
'sessionId',
'profile',
'origin',
'createdAtMs',
'admissionTarget',
'minimumSettlingAgeMs',
'currentRollout',
'files',
'activation',
],
'plan',
);
if (
plan.schema !== MANUAL_PRIMARY_CANARY_PLAN_SCHEMA ||
plan.schemaVersion !== 1 ||
plan.origin !== 'manual' ||
plan.minimumSettlingAgeMs !== MANUAL_PRIMARY_CANARY_MINIMUM_SETTLING_AGE_MS
) {
throw new TypeError('Manual Primary canary plan header is invalid');
}
const selectedProfile = profile(plan.profile);
const id = sessionId(plan.sessionId);
const current = record(plan.currentRollout, 'plan.currentRollout');
if (current.state === 'absent') {
exactKeys(current, ['state'], 'plan.currentRollout');
} else if (current.state === 'disabled') {
exactKeys(current, ['state', 'sha256'], 'plan.currentRollout');
sha256(current.sha256, 'plan.currentRollout.sha256');
} else {
throw new TypeError('Manual Primary canary current rollout is invalid');
}
const files = record(plan.files, 'plan.files');
const expectedFiles = manualPrimaryCanaryFileSet(id);
exactKeys(files, Object.keys(expectedFiles), 'plan.files');
for (const [key, expected] of Object.entries(expectedFiles)) {
if (files[key] !== expected) {
throw new TypeError(`plan.files.${key} is invalid`);
}
}
const activation = record(plan.activation, 'plan.activation');
exactKeys(
activation,
['maxApprovalMs', 'defaultMode', 'allowLegacyFallbackBeforeStart'],
'plan.activation',
);
if (
activation.maxApprovalMs !== MANUAL_PRIMARY_CANARY_MAX_APPROVAL_MS ||
activation.defaultMode !== 'off' ||
activation.allowLegacyFallbackBeforeStart !== false
) {
throw new TypeError('Manual Primary canary activation policy is invalid');
}
return createManualPrimaryCanaryPlan({
sessionId: id,
profile: selectedProfile,
createdAtMs: safeInteger(plan.createdAtMs, 'plan.createdAtMs'),
admissionTarget: admissionTarget(selectedProfile, plan.admissionTarget),
currentRollout: current as ManualPrimaryCanaryCurrentRollout,
});
}
export function createManualPrimaryCanaryQualification(input: {
plan: ManualPrimaryCanaryPlan;
planSha256: string;
primaryGate: LegacyShadowPrimaryGateReceipt;
primaryGateFileSha256: string;
sourceFileSha256: {
capture: string;
terminal: string;
resource: string;
};
qualifiedAtMs: number;
}): ManualPrimaryCanaryQualification {
const plan = parseManualPrimaryCanaryPlan(input.plan);
const gate = parseLegacyShadowPrimaryGateReceipt(input.primaryGate);
const qualifiedAtMs = safeInteger(input.qualifiedAtMs, 'qualifiedAtMs');
if (
gate.assessment !== 'eligible' ||
gate.profile !== plan.profile ||
gate.origin !== plan.origin ||
gate.counts.admitted !== plan.admissionTarget ||
gate.counts.captured !== plan.admissionTarget ||
gate.counts.terminalScanned !== plan.admissionTarget ||
gate.counts.terminalMatched !== plan.admissionTarget ||
gate.window.startInclusiveMs < plan.createdAtMs ||
gate.window.endExclusiveMs > gate.generatedAtMs ||
gate.generatedAtMs > qualifiedAtMs
) {
throw new TypeError('Manual Primary canary gate does not match the plan');
}
return Object.freeze({
schema: MANUAL_PRIMARY_CANARY_QUALIFICATION_SCHEMA,
schemaVersion: 1,
sessionId: plan.sessionId,
profile: plan.profile,
origin: 'manual',
qualifiedAtMs,
assessment: 'eligible',
planSha256: sha256(input.planSha256, 'planSha256'),
primaryGateFileSha256: sha256(
input.primaryGateFileSha256,
'primaryGateFileSha256',
),
sourceFileSha256: {
capture: sha256(input.sourceFileSha256.capture, 'capture file digest'),
terminal: sha256(input.sourceFileSha256.terminal, 'terminal file digest'),
resource: sha256(input.sourceFileSha256.resource, 'resource file digest'),
},
sourceCanonicalSha256: {
capture: gate.evidence.captureSha256,
terminal: gate.evidence.terminalSha256,
resource: gate.evidence.resourceSha256,
},
window: { ...gate.window },
counts: { ...gate.counts },
});
}
export function parseManualPrimaryCanaryQualification(
value: unknown,
): ManualPrimaryCanaryQualification {
const qualification = record(value, 'qualification');
exactKeys(
qualification,
[
'schema',
'schemaVersion',
'sessionId',
'profile',
'origin',
'qualifiedAtMs',
'assessment',
'planSha256',
'primaryGateFileSha256',
'sourceFileSha256',
'sourceCanonicalSha256',
'window',
'counts',
],
'qualification',
);
if (
qualification.schema !== MANUAL_PRIMARY_CANARY_QUALIFICATION_SCHEMA ||
qualification.schemaVersion !== 1 ||
qualification.origin !== 'manual' ||
qualification.assessment !== 'eligible'
) {
throw new TypeError('Manual Primary canary qualification is invalid');
}
sessionId(qualification.sessionId);
profile(qualification.profile);
safeInteger(qualification.qualifiedAtMs, 'qualification.qualifiedAtMs');
sha256(qualification.planSha256, 'qualification.planSha256');
sha256(
qualification.primaryGateFileSha256,
'qualification.primaryGateFileSha256',
);
for (const name of ['sourceFileSha256', 'sourceCanonicalSha256'] as const) {
const digests = record(qualification[name], `qualification.${name}`);
exactKeys(digests, ['capture', 'terminal', 'resource'], name);
for (const key of ['capture', 'terminal', 'resource']) {
sha256(digests[key], `qualification.${name}.${key}`);
}
}
const window = record(qualification.window, 'qualification.window');
exactKeys(window, ['startInclusiveMs', 'endExclusiveMs'], 'window');
const start = safeInteger(window.startInclusiveMs, 'window.startInclusiveMs');
const end = safeInteger(window.endExclusiveMs, 'window.endExclusiveMs');
if (start >= end) throw new TypeError('Qualification window is invalid');
const counts = record(qualification.counts, 'qualification.counts');
exactKeys(
counts,
['admitted', 'captured', 'terminalScanned', 'terminalMatched'],
'counts',
);
const normalizedCounts = Object.fromEntries(
Object.entries(counts).map(([key, count]) => [
key,
safeInteger(count, `counts.${key}`),
]),
) as ManualPrimaryCanaryQualification['counts'];
if (new Set(Object.values(normalizedCounts)).size !== 1) {
throw new TypeError('Qualification counts do not agree');
}
return qualification as unknown as ManualPrimaryCanaryQualification;
}
export function createManualPrimaryCanaryEnabledManifest(input: {
plan: ManualPrimaryCanaryPlan;
qualification: ManualPrimaryCanaryQualification;
approvedBy: string;
approvedAtMs: number;
approvalMs: number;
}): EnabledRuntimeRolloutManifest {
const plan = parseManualPrimaryCanaryPlan(input.plan);
const qualification = parseManualPrimaryCanaryQualification(
input.qualification,
);
const approvedAtMs = safeInteger(input.approvedAtMs, 'approvedAtMs');
const approvalMs = safeInteger(input.approvalMs, 'approvalMs');
if (
qualification.sessionId !== plan.sessionId ||
qualification.profile !== plan.profile ||
qualification.qualifiedAtMs > approvedAtMs ||
approvalMs < 60_000 ||
approvalMs > MANUAL_PRIMARY_CANARY_MAX_APPROVAL_MS ||
typeof input.approvedBy !== 'string' ||
input.approvedBy.trim() !== input.approvedBy ||
input.approvedBy.length < 3 ||
input.approvedBy.length > 128 ||
/[\u0000-\u001f\u007f]/u.test(input.approvedBy)
) {
throw new TypeError('Manual Primary canary approval is invalid');
}
const manifest: EnabledRuntimeRolloutManifest = {
schemaVersion: 2,
revision: `manual-primary-${plan.sessionId}`,
enabled: true,
approvedBy: input.approvedBy,
approvedAtMs,
expiresAtMs: approvedAtMs + approvalMs,
rollbackPlanRef: plan.files.plan,
primaryGate: {
schema: 'qinglong/legacy-shadow-primary-gate-reference@v1',
origin: 'manual',
receiptFile: plan.files.primaryGate,
receiptSha256: qualification.primaryGateFileSha256,
},
rollout: {
defaultMode: 'off',
origins: { manual: 'primary' },
allowLegacyFallbackBeforeStart: false,
},
gates: Object.fromEntries(
REQUIRED_RUNTIME_ROLLOUT_GATES.map((gate) => [gate, 'passed']),
) as EnabledRuntimeRolloutManifest['gates'],
};
parseRuntimeRolloutManifest(manifest, approvedAtMs);
return Object.freeze(manifest);
}
export function createManualPrimaryCanaryDisabledManifest(
rawSessionId: string,
): DisabledRuntimeRolloutManifest {
const manifest: DisabledRuntimeRolloutManifest = {
schemaVersion: 2,
revision: `manual-primary-${sessionId(rawSessionId)}-rollback`,
enabled: false,
};
parseRuntimeRolloutManifest(manifest, 0);
return Object.freeze(manifest);
}