feat(ql3): gate primary on shadow capture evidence

This commit is contained in:
whyour
2026-08-19 03:27:32 +08:00
parent c60a86d1bf
commit be98d6753b
28 changed files with 2227 additions and 30 deletions
+22 -1
View File
@@ -26,6 +26,9 @@ class Application {
private manualPrimaryRuntime?: {
stop(): Promise<'drained' | 'timed_out'>;
};
private legacyShadowCaptureEvidence?: {
close(): Promise<unknown>;
};
private isShuttingDown = false;
private workerMetadataMap = new Map<number, WorkerMetadata>();
private httpWorker?: Worker;
@@ -248,7 +251,16 @@ class Application {
const { bootstrapLegacyShadowStartupReconciliation } = await import(
'./runtime/adapters/legacy/bootstrapLegacyShadowStartupReconciliation'
);
await bootstrapLegacyShadowStartupReconciliation();
const legacyShadowStartup =
await bootstrapLegacyShadowStartupReconciliation();
const { bootstrapLegacyShadowCaptureEvidence } = await import(
'./runtime/adapters/legacy/bootstrapLegacyShadowCaptureEvidence'
);
this.legacyShadowCaptureEvidence =
await bootstrapLegacyShadowCaptureEvidence({
startup: legacyShadowStartup,
});
const { bootstrapDefaultManualPrimaryRuntime } = await import(
'./runtime/adapters/legacy/bootstrapDefaultManualPrimaryRuntime'
@@ -260,6 +272,7 @@ class Application {
server = await this.httpServerService.initialize(this.app, config.port);
} catch (error) {
await this.stopManualPrimaryRuntime();
await this.closeLegacyShadowCaptureEvidence();
throw error;
}
@@ -307,6 +320,7 @@ class Application {
if (serviceType === 'http') {
await this.stopManualPrimaryRuntime();
await this.httpServerService?.shutdown();
await this.closeLegacyShadowCaptureEvidence();
} else {
await this.grpcServerService?.shutdown();
}
@@ -330,6 +344,13 @@ class Application {
Logger.error('[runtime-activation] shutdown failed');
}
}
private async closeLegacyShadowCaptureEvidence(): Promise<void> {
const evidence = this.legacyShadowCaptureEvidence;
this.legacyShadowCaptureEvidence = undefined;
if (!evidence) return;
await evidence.close();
}
}
const app = new Application();
@@ -1,4 +1,5 @@
import { createHash } from 'crypto';
import { constants } from 'fs';
import fs from 'fs/promises';
import path from 'path';
import {
@@ -9,6 +10,10 @@ import type {
RuntimeRolloutLoadAudit,
RuntimeRolloutLoadResult,
} from '../../ports/runtimeRolloutLoader';
import {
parseLegacyShadowPrimaryGateReceipt,
type LegacyShadowPrimaryGateReceipt,
} from '../../domain/legacyShadowPrimaryGate';
export type {
RuntimeRolloutLoadAudit,
@@ -17,6 +22,7 @@ export type {
} from '../../ports/runtimeRolloutLoader';
export const MAX_RUNTIME_ROLLOUT_MANIFEST_BYTES = 64 * 1024;
export const MAX_RUNTIME_PRIMARY_GATE_RECEIPT_BYTES = 64 * 1024;
export interface RuntimeRolloutManifestLoaderOptions {
clock?: { now(): number };
@@ -34,6 +40,47 @@ function rejected(
};
}
async function loadPrimaryGateReceipt(
manifestPath: string,
receiptFile: string,
expectedSha256: string,
approvedAtMs: number,
): Promise<LegacyShadowPrimaryGateReceipt> {
const receiptPath = path.join(path.dirname(manifestPath), receiptFile);
let handle;
try {
handle = await fs.open(
receiptPath,
constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0),
);
const stat = await handle.stat();
if (
!stat.isFile() ||
(stat.mode & 0o077) !== 0 ||
stat.size < 2 ||
stat.size > MAX_RUNTIME_PRIMARY_GATE_RECEIPT_BYTES
) {
throw new TypeError('Primary gate receipt file shape is invalid');
}
const bytes = await handle.readFile();
if (createHash('sha256').update(bytes).digest('hex') !== expectedSha256) {
throw new TypeError('Primary gate receipt digest does not match');
}
const receipt = parseLegacyShadowPrimaryGateReceipt(
JSON.parse(bytes.toString('utf8')),
);
if (
receipt.assessment !== 'eligible' ||
receipt.generatedAtMs > approvedAtMs
) {
throw new TypeError('Primary gate receipt is not eligible for approval');
}
return receipt;
} finally {
await handle?.close();
}
}
export async function loadRuntimeRolloutManifest(
sourcePath: string,
options: RuntimeRolloutManifestLoaderOptions = {},
@@ -90,10 +137,29 @@ export async function loadRuntimeRolloutManifest(
try {
const decision = parseRuntimeRolloutManifest(value, evaluatedAtMs);
const status = decision.manifest.enabled ? 'accepted' : 'disabled';
let primaryGateReceipt: LegacyShadowPrimaryGateReceipt | undefined;
if (decision.manifest.enabled) {
try {
primaryGateReceipt = await loadPrimaryGateReceipt(
sourcePath,
decision.manifest.primaryGate.receiptFile,
decision.manifest.primaryGate.receiptSha256,
decision.manifest.approvedAtMs,
);
} catch (error) {
return rejected(
hashedAudit,
(error as NodeJS.ErrnoException).code === 'ENOENT'
? 'PRIMARY_GATE_READ_FAILED'
: 'PRIMARY_GATE_INVALID',
);
}
}
return {
status,
policy: decision.policy,
manifest: decision.manifest,
...(primaryGateReceipt === undefined ? {} : { primaryGateReceipt }),
audit: {
...hashedAudit,
status,
@@ -52,8 +52,25 @@ export async function bootstrapDefaultManualPrimaryRuntime(
Logger.info(`[runtime-activation] ${JSON.stringify(record)}`);
});
let stackModule: DefaultManualPrimaryStackModule | undefined;
let selectedProfile: 'edge' | 'standalone' | undefined;
if (selected) {
try {
const profile =
options.deploymentProfile ??
parseDeploymentProfile(process.env.QL_DEPLOYMENT_PROFILE);
if (profile === 'cluster-control' || profile === 'worker') {
throw new Error('Local Primary requires edge or standalone Profile');
}
if (
load.primaryGateReceipt?.assessment !== 'eligible' ||
load.primaryGateReceipt.origin !== 'manual' ||
load.primaryGateReceipt.profile !== profile
) {
throw new Error(
'Primary gate receipt does not authorize this deployment Profile',
);
}
selectedProfile = profile;
stackModule = await (
options.loadStack ?? (() => import('./defaultManualPrimaryActivation'))
)();
@@ -87,9 +104,7 @@ export async function bootstrapDefaultManualPrimaryRuntime(
}
return stackModule.createDefaultManualPrimaryActivationStack(rollout, {
...activationOptions,
deploymentProfile:
options.deploymentProfile ??
parseDeploymentProfile(process.env.QL_DEPLOYMENT_PROFILE),
deploymentProfile: selectedProfile!,
});
},
install: options.install ?? installManualPrimaryExecutionRouter,
@@ -0,0 +1,254 @@
import fs from 'fs/promises';
import path from 'path';
import config from '../../../config';
import Logger from '../../../loaders/logger';
import {
createLegacyShadowCaptureReport,
type LegacyShadowCaptureReport,
type LegacyShadowCaptureSnapshot,
} from '../../application/legacyShadowCaptureAuthority';
import {
configuredLegacyShadowOrigins,
legacyShadowCaptureSnapshot,
} from '../../compatibility/legacyExecutionBridge';
import {
parseDeploymentProfile,
type DeploymentProfile,
} from '../../domain/deploymentProfile';
import type { ExecutionOrigin } from '../../domain/run';
import type {
LegacyShadowStartupAudit,
LegacyShadowStartupDifferenceReport,
} from './bootstrapLegacyShadowStartupReconciliation';
export const LEGACY_SHADOW_CAPTURE_EVIDENCE_SCHEMA =
'qinglong/legacy-shadow-capture-evidence@v1';
export const LEGACY_SHADOW_CAPTURE_EVIDENCE_FILE_ENV =
'QL3_SHADOW_CAPTURE_EVIDENCE_FILE';
export interface LegacyShadowCaptureEvidence {
schema: typeof LEGACY_SHADOW_CAPTURE_EVIDENCE_SCHEMA;
profile: 'edge' | 'standalone';
startup: LegacyShadowStartupDifferenceReport;
capture: LegacyShadowCaptureReport;
qualification: {
passed: boolean;
startupConverged: boolean;
originCoverageExact: boolean;
captureComplete: boolean;
};
}
export type LegacyShadowCaptureEvidenceAudit =
| { state: 'disabled' }
| { state: 'profile_rejected'; profile: 'cluster-control' | 'worker' }
| { state: 'armed'; profile: 'edge' | 'standalone'; origins: number }
| {
state: 'exported';
profile: 'edge' | 'standalone';
origins: number;
qualified: boolean;
}
| { state: 'failed'; errorType: string };
export interface LegacyShadowCaptureEvidenceHandle {
active: boolean;
close(): Promise<LegacyShadowCaptureEvidenceAudit>;
}
export interface BootstrapLegacyShadowCaptureEvidenceOptions {
startup: LegacyShadowStartupAudit;
origins?: readonly ExecutionOrigin[];
profile?: DeploymentProfile;
outputPath?: string;
snapshot?: (
origins: readonly ExecutionOrigin[],
) => LegacyShadowCaptureSnapshot;
write?: (
outputPath: string,
evidence: LegacyShadowCaptureEvidence,
) => Promise<void>;
audit?: (record: LegacyShadowCaptureEvidenceAudit) => void | Promise<void>;
}
const FILE_NAME_PATTERN =
/^(?=.{1,128}$)(?!\.)(?!.*\.\.)(?:[A-Za-z0-9][A-Za-z0-9._-]*)\.json$/u;
function configuredOutputPath(): string | undefined {
const fileName = process.env[LEGACY_SHADOW_CAPTURE_EVIDENCE_FILE_ENV]?.trim();
if (!fileName) return undefined;
if (
!FILE_NAME_PATTERN.test(fileName) ||
path.basename(fileName) !== fileName
) {
throw new TypeError('Legacy Shadow capture evidence filename is invalid');
}
return path.join(config.configPath, fileName);
}
async function writeEvidence(
outputPath: string,
evidence: LegacyShadowCaptureEvidence,
): Promise<void> {
if (!path.isAbsolute(outputPath)) {
throw new TypeError('Legacy Shadow capture evidence path must be absolute');
}
const handle = await fs.open(outputPath, 'wx', 0o600);
try {
await handle.writeFile(`${JSON.stringify(evidence)}\n`, 'utf8');
await handle.sync();
} finally {
await handle.close();
}
}
function exactOriginCoverage(
origins: readonly ExecutionOrigin[],
startup: LegacyShadowStartupDifferenceReport,
): boolean {
return (
startup.configuredOriginCount === origins.length &&
startup.byOrigin.length === origins.length &&
origins.every((origin, index) => startup.byOrigin[index]?.origin === origin)
);
}
async function emitAudit(
audit: (record: LegacyShadowCaptureEvidenceAudit) => void | Promise<void>,
record: LegacyShadowCaptureEvidenceAudit,
): Promise<void> {
try {
await audit(record);
} catch {
// Evidence diagnostics must not change Legacy startup or shutdown.
}
}
/**
* Arms one process-lifetime capture window after startup reconciliation. It has
* no timer or watcher and writes exactly one owner-private report on a clean
* shutdown when an explicit filename is configured.
*/
export async function bootstrapLegacyShadowCaptureEvidence(
options: BootstrapLegacyShadowCaptureEvidenceOptions,
): Promise<LegacyShadowCaptureEvidenceHandle> {
const audit =
options.audit ??
((record: LegacyShadowCaptureEvidenceAudit) => {
Logger.info(`[ql3-shadow-capture] ${JSON.stringify(record)}`);
});
const origins = [
...new Set(options.origins ?? configuredLegacyShadowOrigins()),
];
let outputPath: string | undefined;
try {
outputPath = options.outputPath ?? configuredOutputPath();
} catch (error) {
const record: LegacyShadowCaptureEvidenceAudit = {
state: 'failed',
errorType: error instanceof Error ? error.name : 'unknown',
};
await emitAudit(audit, record);
return {
active: false,
async close() {
return record;
},
};
}
if (origins.length === 0 || outputPath === undefined) {
const record: LegacyShadowCaptureEvidenceAudit = { state: 'disabled' };
await emitAudit(audit, record);
return {
active: false,
async close() {
return record;
},
};
}
const profile =
options.profile ??
parseDeploymentProfile(process.env.QL_DEPLOYMENT_PROFILE);
if (profile === 'cluster-control' || profile === 'worker') {
const record: LegacyShadowCaptureEvidenceAudit = {
state: 'profile_rejected',
profile,
};
await emitAudit(audit, record);
return {
active: false,
async close() {
return record;
},
};
}
if (
options.startup.state !== 'reconciled' ||
options.startup.report.profile !== profile
) {
const record: LegacyShadowCaptureEvidenceAudit = {
state: 'failed',
errorType: 'LegacyShadowStartupEvidenceUnavailable',
};
await emitAudit(audit, record);
return {
active: false,
async close() {
return record;
},
};
}
const startupReport = options.startup.report;
const snapshot = options.snapshot ?? legacyShadowCaptureSnapshot;
const before = snapshot(origins);
const armed: LegacyShadowCaptureEvidenceAudit = {
state: 'armed',
profile,
origins: origins.length,
};
await emitAudit(audit, armed);
let closed: LegacyShadowCaptureEvidenceAudit | undefined;
return {
active: true,
async close() {
if (closed) return closed;
try {
const capture = createLegacyShadowCaptureReport(
profile,
origins,
before,
snapshot(origins),
);
const startupConverged = startupReport.assessment === 'converged';
const originCoverageExact = exactOriginCoverage(origins, startupReport);
const captureComplete = capture.assessment === 'captured';
const evidence: LegacyShadowCaptureEvidence = {
schema: LEGACY_SHADOW_CAPTURE_EVIDENCE_SCHEMA,
profile,
startup: startupReport,
capture,
qualification: {
passed: startupConverged && originCoverageExact && captureComplete,
startupConverged,
originCoverageExact,
captureComplete,
},
};
await (options.write ?? writeEvidence)(outputPath!, evidence);
closed = {
state: 'exported',
profile,
origins: origins.length,
qualified: evidence.qualification.passed,
};
} catch (error) {
closed = {
state: 'failed',
errorType: error instanceof Error ? error.name : 'unknown',
};
}
await emitAudit(audit, closed);
return closed;
},
};
}
@@ -0,0 +1,296 @@
import { randomUUID } from 'crypto';
import type { ExecutionOrigin } from '../domain/run';
export const LEGACY_SHADOW_CAPTURE_SNAPSHOT_SCHEMA =
'qinglong/legacy-shadow-capture-snapshot@v1';
export const LEGACY_SHADOW_CAPTURE_REPORT_SCHEMA =
'qinglong/legacy-shadow-capture-report@v1';
export type LegacyShadowCaptureFailureStage =
| 'fact'
| 'observer'
| 'initialization'
| 'accept';
export interface LegacyShadowCaptureCounts {
admitted: number;
captured: number;
failed: number;
pending: number;
failures: Record<LegacyShadowCaptureFailureStage, number>;
}
export interface LegacyShadowCaptureOriginSnapshot
extends LegacyShadowCaptureCounts {
origin: ExecutionOrigin;
}
export interface LegacyShadowCaptureSnapshot {
schema: typeof LEGACY_SHADOW_CAPTURE_SNAPSHOT_SCHEMA;
epoch: string;
observedAtMs: number;
byOrigin: readonly LegacyShadowCaptureOriginSnapshot[];
}
export interface LegacyShadowCaptureReport {
schema: typeof LEGACY_SHADOW_CAPTURE_REPORT_SCHEMA;
profile: 'edge' | 'standalone';
assessment: 'captured' | 'empty' | 'incomplete' | 'failures_observed';
epoch: string;
window: {
basis: 'process_local_legacy_admission';
startInclusiveMs: number;
endExclusiveMs: number;
};
configuredOriginCount: number;
totals: LegacyShadowCaptureCounts;
byOrigin: readonly LegacyShadowCaptureOriginSnapshot[];
capturePermille?: number;
}
export interface LegacyShadowCaptureAdmission {
captured(): void;
failed(stage: LegacyShadowCaptureFailureStage): void;
}
const FAILURE_STAGES: readonly LegacyShadowCaptureFailureStage[] = [
'fact',
'observer',
'initialization',
'accept',
];
function emptyCounts(): LegacyShadowCaptureCounts {
return {
admitted: 0,
captured: 0,
failed: 0,
pending: 0,
failures: { fact: 0, observer: 0, initialization: 0, accept: 0 },
};
}
function cloneCounts(
source: LegacyShadowCaptureCounts,
): LegacyShadowCaptureCounts {
return {
admitted: source.admitted,
captured: source.captured,
failed: source.failed,
pending: source.pending,
failures: { ...source.failures },
};
}
function assertCount(value: number, label: string): void {
if (!Number.isSafeInteger(value) || value < 0) {
throw new RangeError(`${label} must be a non-negative safe integer`);
}
}
function assertConserved(
counts: LegacyShadowCaptureCounts,
label: string,
): void {
for (const key of ['admitted', 'captured', 'failed', 'pending'] as const) {
assertCount(counts[key], `${label}.${key}`);
}
for (const stage of FAILURE_STAGES) {
assertCount(counts.failures[stage], `${label}.failures.${stage}`);
}
if (counts.captured + counts.failed + counts.pending !== counts.admitted) {
throw new RangeError(`${label} does not conserve admitted executions`);
}
if (
FAILURE_STAGES.reduce(
(total, stage) => total + counts.failures[stage],
0,
) !== counts.failed
) {
throw new RangeError(`${label} failure stages do not conserve failures`);
}
}
function originMap(
snapshot: LegacyShadowCaptureSnapshot,
): Map<ExecutionOrigin, LegacyShadowCaptureOriginSnapshot> {
if (snapshot.schema !== LEGACY_SHADOW_CAPTURE_SNAPSHOT_SCHEMA) {
throw new TypeError('Legacy Shadow capture snapshot schema is unsupported');
}
if (!/^[0-9a-f-]{36}$/u.test(snapshot.epoch)) {
throw new TypeError('Legacy Shadow capture epoch is invalid');
}
assertCount(snapshot.observedAtMs, 'snapshot.observedAtMs');
const result = new Map<ExecutionOrigin, LegacyShadowCaptureOriginSnapshot>();
for (const entry of snapshot.byOrigin) {
if (result.has(entry.origin)) {
throw new RangeError('Legacy Shadow capture snapshot repeats an origin');
}
assertConserved(entry, `snapshot.${entry.origin}`);
result.set(entry.origin, entry);
}
return result;
}
function subtractCounts(
before: LegacyShadowCaptureCounts,
after: LegacyShadowCaptureCounts,
label: string,
): LegacyShadowCaptureCounts {
const result = emptyCounts();
for (const key of ['admitted', 'captured', 'failed'] as const) {
result[key] = after[key] - before[key];
assertCount(result[key], `${label}.${key}`);
}
if (before.pending !== 0) {
throw new RangeError(`${label} starts with pending capture work`);
}
result.pending = after.pending;
for (const stage of FAILURE_STAGES) {
result.failures[stage] = after.failures[stage] - before.failures[stage];
assertCount(result.failures[stage], `${label}.failures.${stage}`);
}
assertConserved(result, label);
return result;
}
export class LegacyShadowCaptureAuthority {
private readonly epoch: string;
private readonly counts = new Map<
ExecutionOrigin,
LegacyShadowCaptureCounts
>();
constructor(
private readonly clock: { now(): number } = { now: Date.now },
epoch: string = randomUUID(),
) {
if (!/^[0-9a-f-]{36}$/u.test(epoch)) {
throw new TypeError('Legacy Shadow capture epoch is invalid');
}
this.epoch = epoch;
}
admit(origin: ExecutionOrigin): LegacyShadowCaptureAdmission {
const counts = this.counts.get(origin) ?? emptyCounts();
if (!this.counts.has(origin)) this.counts.set(origin, counts);
counts.admitted += 1;
counts.pending += 1;
let settled = false;
return Object.freeze({
captured: () => {
if (settled) return;
settled = true;
counts.pending -= 1;
counts.captured += 1;
},
failed: (stage: LegacyShadowCaptureFailureStage) => {
if (settled) return;
if (!FAILURE_STAGES.includes(stage)) {
throw new TypeError('Legacy Shadow capture failure stage is invalid');
}
settled = true;
counts.pending -= 1;
counts.failed += 1;
counts.failures[stage] += 1;
},
});
}
snapshot(origins: readonly ExecutionOrigin[]): LegacyShadowCaptureSnapshot {
const observedAtMs = this.clock.now();
assertCount(observedAtMs, 'snapshot.observedAtMs');
const configured = [...new Set(origins)];
return Object.freeze({
schema: LEGACY_SHADOW_CAPTURE_SNAPSHOT_SCHEMA,
epoch: this.epoch,
observedAtMs,
byOrigin: configured.map((origin) =>
Object.freeze({
origin,
...cloneCounts(this.counts.get(origin) ?? emptyCounts()),
}),
),
});
}
}
export function createLegacyShadowCaptureReport(
profile: 'edge' | 'standalone',
origins: readonly ExecutionOrigin[],
before: LegacyShadowCaptureSnapshot,
after: LegacyShadowCaptureSnapshot,
): LegacyShadowCaptureReport {
if (profile !== 'edge' && profile !== 'standalone') {
throw new TypeError('Legacy Shadow capture profile is invalid');
}
const configured = [...new Set(origins)];
if (configured.length < 1 || configured.length > 7) {
throw new RangeError('Legacy Shadow capture origin count is invalid');
}
if (before.epoch !== after.epoch) {
throw new RangeError(
'Legacy Shadow capture snapshots cross process epochs',
);
}
if (before.observedAtMs >= after.observedAtMs) {
throw new RangeError('Legacy Shadow capture window must be non-empty');
}
const beforeOrigins = originMap(before);
const afterOrigins = originMap(after);
if (
beforeOrigins.size !== configured.length ||
afterOrigins.size !== configured.length ||
configured.some(
(origin) => !beforeOrigins.has(origin) || !afterOrigins.has(origin),
)
) {
throw new RangeError('Legacy Shadow capture origin coverage is incomplete');
}
const totals = emptyCounts();
const byOrigin = configured.map((origin) => {
const counts = subtractCounts(
beforeOrigins.get(origin)!,
afterOrigins.get(origin)!,
`capture.${origin}`,
);
for (const key of ['admitted', 'captured', 'failed', 'pending'] as const) {
totals[key] += counts[key];
}
for (const stage of FAILURE_STAGES) {
totals.failures[stage] += counts.failures[stage];
}
return Object.freeze({ origin, ...counts });
});
assertConserved(totals, 'capture.totals');
const assessment =
totals.pending > 0
? 'incomplete'
: totals.failed > 0
? 'failures_observed'
: totals.admitted === 0
? 'empty'
: 'captured';
const report: LegacyShadowCaptureReport = {
schema: LEGACY_SHADOW_CAPTURE_REPORT_SCHEMA,
profile,
assessment,
epoch: before.epoch,
window: {
basis: 'process_local_legacy_admission' as const,
startInclusiveMs: before.observedAtMs,
endExclusiveMs: after.observedAtMs,
},
configuredOriginCount: configured.length,
totals,
byOrigin,
...(totals.admitted > 0
? {
capturePermille: Math.floor(
(totals.captured * 1_000) / totals.admitted,
),
}
: {}),
};
return Object.freeze(report);
}
@@ -64,6 +64,7 @@ function classifyError(error: unknown): string {
class SerialLegacyExecutionObservation
implements TrackedLegacyExecutionObservation
{
private readonly capture: Promise<LegacyShadowRunReference | null>;
private chain: Promise<LegacyShadowRunReference | null>;
constructor(
@@ -72,10 +73,15 @@ class SerialLegacyExecutionObservation
accepted: LegacyExecutionAcceptedFact,
private readonly reporter: ShadowObservationReporter,
) {
this.chain = writer.accept(accepted).catch((error) => {
this.capture = writer.accept(accepted).catch((error) => {
this.report('accept', error);
return null;
});
this.chain = this.capture;
}
async captureSettled(): Promise<'captured' | 'failed'> {
return (await this.capture) === null ? 'failed' : 'captured';
}
spawned(fact: LegacyExecutionSpawnedFact): void {
@@ -1,5 +1,10 @@
import Logger from '../../loaders/logger';
import type { LegacyShadowRunCorrelator } from '../application/legacyShadowRunCorrelator';
import {
LegacyShadowCaptureAuthority,
type LegacyShadowCaptureAdmission,
type LegacyShadowCaptureSnapshot,
} from '../application/legacyShadowCaptureAuthority';
import type { ExecutionOrigin } from '../domain/run';
import type {
LegacyExecutionCallbackFact,
@@ -51,6 +56,12 @@ let configuredOrigins: ReadonlySet<ExecutionOrigin> | undefined;
let defaultObserver: Promise<LegacyExecutionObserver> | undefined;
let defaultCorrelator: Promise<LegacyShadowRunCorrelator> | undefined;
const failureCounters = new Map<string, number>();
let captureAuthority: LegacyShadowCaptureAuthority | undefined;
function getCaptureAuthority(): LegacyShadowCaptureAuthority {
captureAuthority ??= new LegacyShadowCaptureAuthority();
return captureAuthority;
}
const localRegistry = new LegacyExecutionRegistry({
onOverflow() {
incrementFailure('registry:capacity_exceeded');
@@ -185,10 +196,27 @@ function getDefaultCorrelator(): Promise<LegacyShadowRunCorrelator> {
function beginFailOpen(
observer: LegacyExecutionObserver,
accepted: LegacyExecutionAcceptedFact,
capture?: LegacyShadowCaptureAdmission,
): LegacyExecutionObservation {
try {
return observer.begin(accepted);
const observation = observer.begin(accepted);
if (capture) {
const settled = observation.captureSettled?.();
if (!settled) {
capture.failed('observer');
} else {
void settled.then(
(outcome) =>
outcome === 'captured'
? capture.captured()
: capture.failed('accept'),
() => capture.failed('accept'),
);
}
}
return observation;
} catch {
capture?.failed('observer');
incrementFailure(`${accepted.origin}:begin:failed`);
try {
Logger.warn(
@@ -204,6 +232,7 @@ function beginFailOpen(
function createAcceptedFactFailOpen(
origin: ExecutionOrigin,
createFact: LegacyExecutionAcceptedFactFactory,
capture?: LegacyShadowCaptureAdmission,
): LegacyExecutionAcceptedFact | null {
try {
const fact = createFact();
@@ -212,6 +241,7 @@ function createAcceptedFactFailOpen(
}
return fact;
} catch {
capture?.failed('fact');
incrementFailure(`${origin}:fact:failed`);
try {
Logger.warn(`[ql3-shadow] fact creation failed origin=${origin}`);
@@ -225,10 +255,14 @@ function createAcceptedFactFailOpen(
function deferredObservation(
observer: Promise<LegacyExecutionObserver>,
accepted: LegacyExecutionAcceptedFact,
capture: LegacyShadowCaptureAdmission,
): LegacyExecutionObservation {
const delegate = observer
.then((value) => beginFailOpen(value, accepted))
.catch(() => NOOP_OBSERVATION);
.then((value) => beginFailOpen(value, accepted, capture))
.catch(() => {
capture.failed('initialization');
return NOOP_OBSERVATION;
});
const enqueue = <T>(
operation: (observation: LegacyExecutionObservation, fact: T) => void,
fact: T,
@@ -266,11 +300,12 @@ export function observeLegacyExecution(
: NOOP_OBSERVATION;
}
if (!readConfiguredOrigins().has(origin)) return undefined;
const fact = createAcceptedFactFailOpen(origin, createFact);
const capture = getCaptureAuthority().admit(origin);
const fact = createAcceptedFactFailOpen(origin, createFact, capture);
return fact
? localRegistry.register(
fact,
deferredObservation(getDefaultObserver(), fact),
deferredObservation(getDefaultObserver(), fact, capture),
)
: NOOP_OBSERVATION;
}
@@ -302,9 +337,10 @@ export function observeLegacyShellExecutionCallback(
: NOOP_OBSERVATION;
} else {
if (!readConfiguredOrigins().has(origin)) return undefined;
const fact = createAcceptedFactFailOpen(origin, createFact);
const capture = getCaptureAuthority().admit(origin);
const fact = createAcceptedFactFailOpen(origin, createFact, capture);
observation = fact
? deferredObservation(getDefaultObserver(), fact)
? deferredObservation(getDefaultObserver(), fact, capture)
: NOOP_OBSERVATION;
}
@@ -415,3 +451,9 @@ export function shadowBridgeFailureSnapshot(): Readonly<
> {
return Object.fromEntries(failureCounters);
}
export function legacyShadowCaptureSnapshot(
origins: readonly ExecutionOrigin[] = configuredLegacyShadowOrigins(),
): LegacyShadowCaptureSnapshot {
return getCaptureAuthority().snapshot(origins);
}
@@ -0,0 +1,418 @@
export const LEGACY_SHADOW_PRIMARY_GATE_SCHEMA =
'qinglong/legacy-shadow-primary-gate@v1';
export type LegacyShadowPrimaryGateViolation =
| 'capture_schema_invalid'
| 'capture_profile_mismatch'
| 'capture_not_qualified'
| 'capture_origin_coverage_invalid'
| 'capture_window_invalid'
| 'capture_sample_budget_invalid'
| 'capture_conservation_invalid'
| 'startup_not_converged'
| 'terminal_schema_invalid'
| 'terminal_profile_mismatch'
| 'terminal_window_mismatch'
| 'terminal_coverage_invalid'
| 'terminal_not_matched'
| 'resource_schema_invalid'
| 'resource_profile_mismatch'
| 'resource_not_qualified'
| 'resource_not_compiled_full_rollback';
export interface LegacyShadowPrimaryGateReceipt {
schema: typeof LEGACY_SHADOW_PRIMARY_GATE_SCHEMA;
schemaVersion: 1;
profile: 'edge' | 'standalone';
origin: 'manual';
generatedAtMs: number;
assessment: 'eligible' | 'ineligible';
window: {
startInclusiveMs: number;
endExclusiveMs: number;
};
counts: {
admitted: number;
captured: number;
terminalScanned: number;
terminalMatched: number;
};
evidence: {
captureSha256: string;
terminalSha256: string;
resourceSha256: string;
};
sources: {
capture: unknown;
terminal: unknown;
resource: unknown;
};
violations: readonly LegacyShadowPrimaryGateViolation[];
}
export interface LegacyShadowPrimaryGateInput {
profile: 'edge' | 'standalone';
generatedAtMs: number;
capture: unknown;
terminal: unknown;
resource: unknown;
}
const SAMPLE_BUDGETS = Object.freeze({
edge: { minimum: 8, maximum: 8 },
standalone: { minimum: 32, maximum: 128 },
});
const SHA256_PATTERN = /^[a-f0-9]{64}$/u;
function object(value: unknown): Record<string, any> | undefined {
return value !== null && typeof value === 'object' && !Array.isArray(value)
? (value as Record<string, any>)
: undefined;
}
function safeCount(value: unknown): value is number {
return Number.isSafeInteger(value) && Number(value) >= 0;
}
function add(
violations: LegacyShadowPrimaryGateViolation[],
violation: LegacyShadowPrimaryGateViolation,
): void {
if (!violations.includes(violation)) violations.push(violation);
}
function canonicalJson(value: unknown): string {
if (value === null) return 'null';
if (typeof value === 'string' || typeof value === 'boolean') {
return JSON.stringify(value);
}
if (typeof value === 'number') {
if (!Number.isFinite(value))
throw new TypeError('Evidence number is invalid');
return JSON.stringify(value);
}
if (Array.isArray(value)) {
return `[${value.map(canonicalJson).join(',')}]`;
}
const record = object(value);
if (!record) throw new TypeError('Evidence contains a non-JSON value');
return `{${Object.keys(record)
.sort()
.map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`)
.join(',')}}`;
}
export function legacyShadowPrimaryEvidenceSha256(value: unknown): string {
return createHash('sha256').update(canonicalJson(value)).digest('hex');
}
export function createLegacyShadowPrimaryGateReceipt(
input: LegacyShadowPrimaryGateInput,
): LegacyShadowPrimaryGateReceipt {
if (input.profile !== 'edge' && input.profile !== 'standalone') {
throw new TypeError('Primary gate profile is invalid');
}
if (!safeCount(input.generatedAtMs)) {
throw new TypeError('Primary gate timestamp is invalid');
}
const sources = {
capture: JSON.parse(canonicalJson(input.capture)),
terminal: JSON.parse(canonicalJson(input.terminal)),
resource: JSON.parse(canonicalJson(input.resource)),
};
const evidence = {
captureSha256: legacyShadowPrimaryEvidenceSha256(sources.capture),
terminalSha256: legacyShadowPrimaryEvidenceSha256(sources.terminal),
resourceSha256: legacyShadowPrimaryEvidenceSha256(sources.resource),
};
const violations: LegacyShadowPrimaryGateViolation[] = [];
const captureEvidence = object(sources.capture);
const capture = object(captureEvidence?.capture);
const startup = object(captureEvidence?.startup);
const captureWindow = object(capture?.window);
const captureTotals = object(capture?.totals);
const captureByOriginValue = capture?.byOrigin;
const captureByOrigin = Array.isArray(captureByOriginValue)
? captureByOriginValue
: [];
const admittedValue = captureTotals?.admitted;
const capturedValue = captureTotals?.captured;
const admitted = safeCount(admittedValue) ? admittedValue : 0;
const captured = safeCount(capturedValue) ? capturedValue : 0;
if (
captureEvidence?.schema !== 'qinglong/legacy-shadow-capture-evidence@v1' ||
capture?.schema !== 'qinglong/legacy-shadow-capture-report@v1'
) {
add(violations, 'capture_schema_invalid');
}
if (
captureEvidence?.profile !== input.profile ||
capture?.profile !== input.profile
) {
add(violations, 'capture_profile_mismatch');
}
if (
object(captureEvidence?.qualification)?.passed !== true ||
capture?.assessment !== 'captured' ||
capture?.capturePermille !== 1_000
) {
add(violations, 'capture_not_qualified');
}
if (
capture?.configuredOriginCount !== 1 ||
captureByOrigin.length !== 1 ||
object(captureByOrigin[0])?.origin !== 'manual' ||
startup?.configuredOriginCount !== 1 ||
!Array.isArray(startup?.byOrigin) ||
startup.byOrigin.length !== 1 ||
object(startup.byOrigin[0])?.origin !== 'manual'
) {
add(violations, 'capture_origin_coverage_invalid');
}
const startInclusiveMs = captureWindow?.startInclusiveMs;
const endExclusiveMs = captureWindow?.endExclusiveMs;
if (
captureWindow?.basis !== 'process_local_legacy_admission' ||
!safeCount(startInclusiveMs) ||
!safeCount(endExclusiveMs) ||
startInclusiveMs >= endExclusiveMs ||
endExclusiveMs > input.generatedAtMs
) {
add(violations, 'capture_window_invalid');
}
const budget = SAMPLE_BUDGETS[input.profile];
if (admitted < budget.minimum || admitted > budget.maximum) {
add(violations, 'capture_sample_budget_invalid');
}
if (
!safeCount(captureTotals?.failed) ||
!safeCount(captureTotals?.pending) ||
captured !== admitted ||
captureTotals?.failed !== 0 ||
captureTotals?.pending !== 0
) {
add(violations, 'capture_conservation_invalid');
}
if (
startup?.schema !== 'qinglong/legacy-shadow-startup-difference-report@v1' ||
startup?.profile !== input.profile ||
startup?.assessment !== 'converged' ||
object(startup?.coverage)?.remaining !== false
) {
add(violations, 'startup_not_converged');
}
const terminal = object(sources.terminal);
const terminalWindow = object(terminal?.window);
const terminalCoverage = object(terminal?.coverage);
const terminalCounts = object(terminal?.counts);
const terminalByOriginValue = terminal?.byOrigin;
const terminalByOrigin = Array.isArray(terminalByOriginValue)
? terminalByOriginValue
: [];
const terminalScannedValue = terminal?.scanned;
const terminalMatchedValue = terminalCounts?.matched;
const terminalScanned = safeCount(terminalScannedValue)
? terminalScannedValue
: 0;
const terminalMatched = safeCount(terminalMatchedValue)
? terminalMatchedValue
: 0;
const terminalObservedAtMs = terminal?.observedAtMs;
if (
terminal?.schema !== 'qinglong/legacy-shadow-terminal-difference-report@v1'
) {
add(violations, 'terminal_schema_invalid');
}
if (terminal?.profile !== input.profile) {
add(violations, 'terminal_profile_mismatch');
}
if (
terminalWindow?.startInclusiveMs !== startInclusiveMs ||
terminalWindow?.endExclusiveMs !== endExclusiveMs ||
terminalWindow?.closed !== true ||
!safeCount(terminalObservedAtMs) ||
terminalObservedAtMs > input.generatedAtMs
) {
add(violations, 'terminal_window_mismatch');
}
if (
terminalCoverage?.direction !== 'shadow_to_legacy' ||
terminalCoverage?.cohort !== 'legacy_owned_shadow_runs' ||
terminalCoverage?.legacyWithoutShadow !== 'not_measured' ||
terminalByOrigin.length !== 1 ||
object(terminalByOrigin[0])?.origin !== 'manual' ||
object(terminalByOrigin[0])?.scanned !== terminalScanned
) {
add(violations, 'terminal_coverage_invalid');
}
if (
terminal?.assessment !== 'matched' ||
terminal?.remaining !== false ||
terminal?.evidenceComplete !== true ||
terminal?.terminalAgreementPermille !== 1_000 ||
terminal?.fullyComparablePermille !== 1_000 ||
terminalScanned !== captured ||
terminalMatched !== captured
) {
add(violations, 'terminal_not_matched');
}
const resource = object(sources.resource);
const workload = object(resource?.workload);
const rollback = object(resource?.rollback);
const qualification = object(resource?.qualification);
if (
resource?.schemaVersion !== 1 ||
resource?.fixture !== 'qinglong/legacy-shadow-resource-rollback-evidence@v1'
) {
add(violations, 'resource_schema_invalid');
}
if (resource?.profile !== input.profile) {
add(violations, 'resource_profile_mismatch');
}
if (
qualification?.passed !== true ||
qualification?.violations?.length !== 0
) {
add(violations, 'resource_not_qualified');
}
if (
workload?.mode !== 'full' ||
workload?.runtime !== 'compiled_backend' ||
rollback?.performed !== true ||
rollback?.legacyContinued !== true ||
rollback?.shadowWritesStopped !== true ||
rollback?.databaseIntegrity !== 'ok'
) {
add(violations, 'resource_not_compiled_full_rollback');
}
return Object.freeze({
schema: LEGACY_SHADOW_PRIMARY_GATE_SCHEMA,
schemaVersion: 1,
profile: input.profile,
origin: 'manual',
generatedAtMs: input.generatedAtMs,
assessment: violations.length === 0 ? 'eligible' : 'ineligible',
window: {
startInclusiveMs: safeCount(startInclusiveMs) ? startInclusiveMs : 0,
endExclusiveMs: safeCount(endExclusiveMs) ? endExclusiveMs : 0,
},
counts: { admitted, captured, terminalScanned, terminalMatched },
evidence,
sources,
violations,
});
}
function assertExactKeys(
value: Record<string, unknown>,
expected: readonly string[],
label: string,
): void {
const keys = Object.keys(value).sort();
const wanted = [...expected].sort();
if (
keys.length !== wanted.length ||
keys.some((key, index) => key !== wanted[index])
) {
throw new TypeError(`${label} shape is invalid`);
}
}
export function parseLegacyShadowPrimaryGateReceipt(
value: unknown,
): LegacyShadowPrimaryGateReceipt {
const receipt = object(value);
if (!receipt) throw new TypeError('Primary gate receipt must be an object');
assertExactKeys(
receipt,
[
'schema',
'schemaVersion',
'profile',
'origin',
'generatedAtMs',
'assessment',
'window',
'counts',
'evidence',
'sources',
'violations',
],
'receipt',
);
const window = object(receipt.window);
const counts = object(receipt.counts);
const evidence = object(receipt.evidence);
const sources = object(receipt.sources);
if (!window || !counts || !evidence || !sources) {
throw new TypeError('Primary gate receipt nested shape is invalid');
}
assertExactKeys(
window,
['startInclusiveMs', 'endExclusiveMs'],
'receipt.window',
);
assertExactKeys(
sources,
['capture', 'terminal', 'resource'],
'receipt.sources',
);
assertExactKeys(
counts,
['admitted', 'captured', 'terminalScanned', 'terminalMatched'],
'receipt.counts',
);
assertExactKeys(
evidence,
['captureSha256', 'terminalSha256', 'resourceSha256'],
'receipt.evidence',
);
if (
receipt.schema !== LEGACY_SHADOW_PRIMARY_GATE_SCHEMA ||
receipt.schemaVersion !== 1 ||
!['edge', 'standalone'].includes(receipt.profile) ||
receipt.origin !== 'manual' ||
!safeCount(receipt.generatedAtMs) ||
!['eligible', 'ineligible'].includes(receipt.assessment) ||
!safeCount(window.startInclusiveMs) ||
!safeCount(window.endExclusiveMs) ||
window.startInclusiveMs >= window.endExclusiveMs ||
!Object.values(counts).every(safeCount) ||
!Object.values(evidence).every(
(digest) => typeof digest === 'string' && SHA256_PATTERN.test(digest),
) ||
!Array.isArray(receipt.violations) ||
receipt.violations.some(
(violation: unknown) =>
typeof violation !== 'string' || violation.length > 64,
) ||
(receipt.assessment === 'eligible' && receipt.violations.length !== 0) ||
(receipt.assessment === 'ineligible' && receipt.violations.length === 0)
) {
throw new TypeError('Primary gate receipt is invalid');
}
const recomputed = createLegacyShadowPrimaryGateReceipt({
profile: receipt.profile as 'edge' | 'standalone',
generatedAtMs: receipt.generatedAtMs,
capture: sources.capture,
terminal: sources.terminal,
resource: sources.resource,
});
for (const field of [
'assessment',
'window',
'counts',
'evidence',
'violations',
] as const) {
if (canonicalJson(receipt[field]) !== canonicalJson(recomputed[field])) {
throw new TypeError(`Primary gate receipt ${field} was not reproduced`);
}
}
return receipt as unknown as LegacyShadowPrimaryGateReceipt;
}
import { createHash } from 'crypto';
+51 -2
View File
@@ -5,7 +5,7 @@ import {
type RuntimeRolloutConfig,
} from './runtimeRollout';
export const RUNTIME_ROLLOUT_MANIFEST_VERSION = 1;
export const RUNTIME_ROLLOUT_MANIFEST_VERSION = 2;
export const MAX_RUNTIME_ROLLOUT_APPROVAL_MS = 30 * 24 * 60 * 60 * 1000;
export const REQUIRED_RUNTIME_ROLLOUT_GATES = [
@@ -33,6 +33,12 @@ export interface EnabledRuntimeRolloutManifest {
approvedAtMs: number;
expiresAtMs: number;
rollbackPlanRef: string;
primaryGate: {
schema: 'qinglong/legacy-shadow-primary-gate-reference@v1';
origin: 'manual';
receiptFile: string;
receiptSha256: string;
};
rollout: RuntimeRolloutConfig;
gates: Record<RuntimeRolloutGate, 'passed'>;
}
@@ -46,12 +52,15 @@ export interface RuntimeRolloutManifestDecision {
policy: RuntimeRolloutPolicy;
}
const MANUAL_ORIGIN: ExecutionOrigin = 'manual';
const MANUAL_ORIGIN = 'manual' as const satisfies ExecutionOrigin;
const ALLOWED_MANUAL_MODES = new Set<CompatibilityMode>([
'off',
'shadow',
'primary',
]);
const SHA256_PATTERN = /^[a-f0-9]{64}$/u;
const RECEIPT_FILE_PATTERN =
/^(?=.{1,128}$)(?!\.)(?!.*\.\.)(?:[A-Za-z0-9][A-Za-z0-9._-]*)\.json$/u;
function asObject(value: unknown, name: string): Record<string, unknown> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
@@ -139,6 +148,7 @@ export function parseRuntimeRolloutManifest(
'approvedAtMs',
'expiresAtMs',
'rollbackPlanRef',
'primaryGate',
'rollout',
'gates',
],
@@ -201,6 +211,39 @@ export function parseRuntimeRolloutManifest(
}
}
const primaryGateObject = asObject(
object.primaryGate,
'manifest.primaryGate',
);
assertExactKeys(
primaryGateObject,
['schema', 'origin', 'receiptFile', 'receiptSha256'],
'manifest.primaryGate',
);
if (
primaryGateObject.schema !==
'qinglong/legacy-shadow-primary-gate-reference@v1' ||
primaryGateObject.origin !== MANUAL_ORIGIN
) {
throw new TypeError('manifest.primaryGate authority is invalid');
}
const receiptFile = boundedString(
primaryGateObject.receiptFile,
'manifest.primaryGate.receiptFile',
128,
);
if (!RECEIPT_FILE_PATTERN.test(receiptFile)) {
throw new TypeError('manifest.primaryGate.receiptFile is invalid');
}
const receiptSha256 = boundedString(
primaryGateObject.receiptSha256,
'manifest.primaryGate.receiptSha256',
64,
);
if (!SHA256_PATTERN.test(receiptSha256)) {
throw new TypeError('manifest.primaryGate.receiptSha256 is invalid');
}
const rollout: RuntimeRolloutConfig = {
defaultMode: 'off',
origins: { manual: origins.manual as CompatibilityMode },
@@ -218,6 +261,12 @@ export function parseRuntimeRolloutManifest(
'manifest.rollbackPlanRef',
512,
),
primaryGate: {
schema: 'qinglong/legacy-shadow-primary-gate-reference@v1',
origin: MANUAL_ORIGIN,
receiptFile,
receiptSha256,
},
rollout,
gates: Object.fromEntries(
REQUIRED_RUNTIME_ROLLOUT_GATES.map((gate) => [gate, 'passed']),
@@ -42,6 +42,7 @@ export interface LegacyExecutionCancelledFact {
}
export interface LegacyExecutionObservation {
captureSettled?(): Promise<'captured' | 'failed'>;
spawned(fact: LegacyExecutionSpawnedFact): void;
running(fact: LegacyExecutionRunningFact): void;
startFailed(fact: LegacyExecutionStartFailedFact): void;
+5 -1
View File
@@ -1,5 +1,6 @@
import type { RuntimeRolloutPolicy } from '../domain/runtimeRollout';
import type { RuntimeRolloutManifest } from '../domain/runtimeRolloutManifest';
import type { LegacyShadowPrimaryGateReceipt } from '../domain/legacyShadowPrimaryGate';
export type RuntimeRolloutLoadStatus =
| 'missing'
@@ -19,7 +20,9 @@ export interface RuntimeRolloutLoadAudit {
| 'FILE_READ_FAILED'
| 'FILE_TOO_LARGE'
| 'INVALID_JSON'
| 'INVALID_MANIFEST';
| 'INVALID_MANIFEST'
| 'PRIMARY_GATE_READ_FAILED'
| 'PRIMARY_GATE_INVALID';
}
export interface RuntimeRolloutLoadResult {
@@ -27,6 +30,7 @@ export interface RuntimeRolloutLoadResult {
policy: RuntimeRolloutPolicy;
audit: RuntimeRolloutLoadAudit;
manifest?: RuntimeRolloutManifest;
primaryGateReceipt?: LegacyShadowPrimaryGateReceipt;
}
export interface RuntimeRolloutLoader {
+37 -4
View File
@@ -11,6 +11,24 @@
最新增量证据(2026-08-19):
- D-360/ADR-0453(已接受;首次真实目标实例 manual canary bundle 待执行):不再用 2.x RunningInstance 推测 Legacy→Shadow 分母,也不再让 Primary
manifest 只信任维护者填写的 `"passed"`。默认 Legacy bridge 为每个已启用 origin admission 分配 process-epoch token,固定守恒
`captured + failed + pending = admitted`,失败只分 `fact/observer/initialization/accept`;只有真实 Shadow writer accept 才结算 capturedLegacy
spawn/返回值与失败开放语义不变。HTTP worker 在 startup reconciliation 后装配无 timer/watcher 的一次性 exporter;仅显式配置
`QL3_SHADOW_CAPTURE_EVIDENCE_FILE` 时,干净 shutdown 才以 `0600`、no-replace 写一份不含 Run/Cron/Attempt/PID/task/path 的 capture+startup evidence。
`gate:legacy-shadow-primary:ql3` 当前只裁决 manualEdge 必须精确 8 条、Standalone 为 32128 条;capture window 与 closed terminal audit 必须完全一致,
captured/scanned/matched 相等、两种 agreement 均为 1000/1000,并绑定同 Profile 的 compiled-backend full rollback/resource report。Primary gate v1
bundle 嵌入三份低敏 source report 及 canonical SHA-256rollout manifest 升为 v2loader 通过同目录 basename+digest 读取 bundle 后重新计算 source digest 和
全部门禁,不信任 CLI 的 eligible 字段,且复验 receipt 先于审批、Profile 与实际部署一致。v1 enabled、缺失/篡改/ineligible bundle、样本不足、pending/failure、
terminal 漂移、audit-only rollback 或 symlink 均失败关闭;disabled/off 保持零重组件。真实 compiled backend `ScheduleService.runTask` 已让默认
system bridge 产生 admitted/captured `1/1`、failed/pending `0/0`,只证明真实结算链,不冒充正式 manual canary。该能力不自动写 manifest、不启用
Primary、不授权其他 origin,也不证明物理路由、flash、断电或跨主机签名。阶段门已重跑:聚焦 `48/48`、Legacy/Shadow 串行扩展
`117/117`、资源/回滚专项 `4/4``build:back`、完整 backend `1,469 pass / 0 fail / 2 conditional skip`、18-package clean build/test、四项可执行
架构审计与 `14/14` artifact audit 全部通过。14 档产物字节保持 D-358 基线。Linux arm64 router stress 维持 `128 MiB / 0.5 CPU / 0 swap / 64 PID`
cgroup peak `95,113,216` bytesEdge release 维持 `256 MiB / 1 CPU / 0 swap / 128 PID`13 个 workload 全绿、peak `144,740,352` bytes
两档 `memory.events max/oom/oom_kill` 增量均为 0。D-360 不改 PostgreSQL schema/migration、依赖树或 Kubernetes 拓扑,故不重跑 PostgreSQL HA
相邻 D-359 的 `142/142` 与 timeline `1→2` 只保留为既有证据,不冒充本阶段新结果。
- D-359/ADR-0451、ADR-0452(已接受):为 D-358 的闭合窗口审计增加真实 compiled-backend 资源与关闭回滚门。证据 schema 固定为
`qinglong/legacy-shadow-resource-rollback-evidence@v1`edge 为 8 candidates/1 pagestandalone 为 128 candidates/4 pages8 个样本分别精确执行
16/64 条查询,SQLite database/WAL/SHM/journal 的 logical/allocated bytes 和 file count 前后必须一致。full 模式经过独立进程重启:enabled 与 off
@@ -8290,7 +8308,10 @@ Primary 不使用普通环境变量或宽泛全局开关启用。孵化配置面
- 文件缺失、不可读、超过 64 KiB、JSON 损坏、未知字段、过期审批或 gate 不完整时 fail-closed 为 `off`
- `defaultMode` 必须保持 `off`;当前 manifest 只允许声明 `manual`,不得用一个配置隐式接管 boot、定时、gRPC 或其他来源。
- 启用记录必须包含有界 revision、审批人、审批起止时间和 rollback plan 引用;审批窗口最长 30 天。
- `durableCancellation``startupReconciliation``atomicLegacyProjection``rollbackDrill``edgeBudget` 必须全部为 `passed`
- `durableCancellation``startupReconciliation``atomicLegacyProjection``rollbackDrill``edgeBudget` 必须全部为 `passed`;从 schema v2 起这些声明不能
代替 Primary evidence bundle。
- enabled manifest 必须绑定同 config 目录的 `qinglong/legacy-shadow-primary-gate@v1` bundle basename 与 SHA-256。loader 必须 no-follow 读取、重算 embedded
capture/startup、terminal、resource source digest 和完整 eligibility,并复验 manual origin、实际 Profile 与审批时序;只验证 receipt 自称 eligible 不成立。
- 审计只记录路径、revision、判定、时间和源文件 SHA-256,不记录完整配置内容;接受判定必须在安装 owner router 前可观测,安装后审计失败必须撤销 router。
- edge 不启动文件 watcher。配置只在显式 bootstrap/reload 时读取,禁用时不创建 router、timer、连接或后台任务。
@@ -8298,13 +8319,19 @@ Primary 不使用普通环境变量或宽泛全局开关启用。孵化配置面
```json
{
"schemaVersion": 1,
"schemaVersion": 2,
"revision": "manual-primary-canary-1",
"enabled": true,
"approvedBy": "operator:admin",
"approvedAtMs": 1750000000000,
"expiresAtMs": 1750086400000,
"rollbackPlanRef": "docs/runbooks/disable-primary.md",
"primaryGate": {
"schema": "qinglong/legacy-shadow-primary-gate-reference@v1",
"origin": "manual",
"receiptFile": "manual-primary-gate.json",
"receiptSha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"rollout": {
"defaultMode": "off",
"origins": { "manual": "primary" },
@@ -8320,7 +8347,13 @@ Primary 不使用普通环境变量或宽泛全局开关启用。孵化配置面
}
```
当前 `next` 已在 HTTP worker 接入轻量 manifest bootstrap。文件缺失、disabled、rejectedmanual 非 primary 时不会加载完整 Runtime stack、创建 router 或启动 timer;只有 accepted 且所有 gate 为 passed 的 manual primary 配置才惰性加载真实组件。激活顺序固定为:记录 selected 审计、完整有界 startup reconciliation、记录 reconciled 审计、启动 timeout intent lifecycle、启动 cancel dispatch lifecycle、安装 router、记录 activated;任何一步失败都会撤销 router,并按 producer → consumer 顺序停止 lifecycle。HTTP shutdown 与监听失败也会执行有界清理。`QL_DEPLOYMENT_PROFILE` 未配置时为 standalone,非法值或在 cluster-control/worker 中误装本机 SQLite Primary 时 fail closed。完整 completion/log 重启恢复、固定物理 edge Gate、配置写入/用户可见状态和操作回滚演练完成后仍需单独评审。
当前 `next` 已在 HTTP worker 接入轻量 manifest bootstrap。文件缺失、disabled、rejectedmanual 非 primary、v1 enabled 或 Primary bundle
缺失/篡改/不可重放时不会加载完整 Runtime stack、创建 router 或启动 timer;只有 schema v2、五项 capability gate 完整,且 bundle 经 loader 独立重算为 eligible
的 manual primary 配置才惰性加载真实组件。激活顺序固定为:重放 evidence→复验实际 Profile→记录 selected 审计→完整有界 startup reconciliation→记录
reconciled 审计→启动 completion/timeout/cancel lifecycle→安装 router→记录 activated;任何一步失败都会撤销 router,并按 producer → consumer 顺序停止
lifecycle。HTTP shutdown 与监听失败也会执行有界清理。`QL_DEPLOYMENT_PROFILE` 未配置时为 standalone,非法值、receipt Profile 不一致或在
cluster-control/worker 中误装本机 SQLite Primary 时 fail closed。首次目标实例 manual canary、固定物理 edge Gate、配置写入/用户可见状态和操作回滚演练完成后
仍需单独评审。
## 26. 交付阶段
@@ -9143,7 +9176,7 @@ flowchart LR
| PR-1 Run Schema | Incubating | Run/RunAttempt/RunEvent schema、nullable cancel request 与 Attempt deadline 字段及恢复索引、CancellationDispatch 状态/version/lease/backoff schema、Repository port、临时 Sequelize adapter、统一事件大小/分页上限、跨 adapter RunRepository contract suite(原子事务、回滚、Run/Attempt/RetryPolicy CAS、唯一错误、分页与取消恢复);ADR-0041 的 `pg-0003-run-retry-policy`、capability v2、driver-neutral PostgreSQL Run Repository 与真实 `pg.Pool` 上的共享 Repository/rollback/SQLSTATE contractADR-0063/0069/0071/0073/0074/0076 的独立 Node 24 local-sqlite typed schema、十二条 reviewed migration、capability v6、共享 operation authority、readiness/RunRepository/API credential repository/receipt journal/dispatch plan/encrypted Secret envelope/Project Policy/security audit/authorized mutation/stable Identity catalog、Drizzle↔真实 catalog table/column/index/CHECK/FK lockstep、base/adopted/application edge/standalone 产物门禁;ADR-0064 的 legacy baseline/plan digest、Online Backup recovery、side-by-side target migration、staged manifest、双库栅栏 activation、source 生命周期写栅栏、target stable identity 和重启语义;ADR-0065 的独立 cutover authority、外部副作用停机 evidence、append-only journal、start/restart/stop barrier 与 unknown→manual_required 收敛;ADR-0066 的 adopted storage→Run reconciliation→receipt maintenance→domain recovery→lifecycle→admission application gate、严格有界 recovery summary 与 admission-first reverse stopADR-0067 的 SQLite 事实驱动 Run 候选源、256 条硬上限、截断失败关闭和唯一 Repository authorityADR-0068 的 receipt-first Reconciler、callback token/sequence fence、exact local-process identity、Attempt/Run/双 Event 原子终态推进和最终 verifierADR-0069 的 local-process 单向包边界、pre-spawn journal、受审 POSIX launcher、immutable receipt、exact identity 和 Profile-aware cleanup lifecycleADR-0070 的独立 local-execution、spawn 前后双 transaction CAS、callback digest、exact stop 补偿与 fail-closed starting 保留;ADR-0071 的独立 local-dispatch、不可变 revision/context、Secret-first materializer、Profile Artifact admission、4/64 MiB output hard quota 和窄 application facadeADR-0073/0074 的 Project-bound SecretRef、AES-256-GCM、外置 keyring 生命周期、双 SQLite authority CAS、application preflight、强 Principal/Policy 和 envelope+audit 原子提交;ADR-0086 的本机 Owner provisioning/challenge/claim/delivery acknowledgement/credential recovery CLIADR-0377 的 Local/Cluster 同构、Profile-aware、Project-scoped Artifact range read | fresh database/pepper setup、credential rotation/GC 运维编排与 Secret/Project/Role/Approval 管理 CLI/API/UI、备份/rekey、2.x/target process controller、人工 recovery、target 写后 reconciliation 与完整 cutover/rollback 演练;retry 产品策略、Artifact retention/tombstone stack、具体本机 lifecycle 和 target executableLinux x64/arm64、PID namespace、断电与固定路由设备门禁;PostgreSQL 16/18 双连接并发与 failover integrationTask revision/context 跨方言 contract/并发压力与引用感知 retention、Keyv 数据迁移 |
| PR-2 Run 状态机 | Incubating | 纯转换表、终态/时间/错误/执行器元数据规则、Run version 与 event sequence CAS、事务性 RunCommandService、回滚测试 | 重复 Worker callback/fencing、并发数据库压力测试、Primary 执行链接入 |
| PR-3 Executor 端口 | Incubating | ADR-0003、ExecutionSpec/Context/Handle/Result、Executor port、LocalProcessExecutor、进程组取消/超时升级、流式背压、Legacy Cron spec builder、真实进程 contract tests、可复现 edge 基准入口 | 固定 edge/多架构设备基线、Legacy builder 与 makeCommand 差异审计、Primary 生产流量接入 |
| PR-4 Shadow Run | Incubating | origin 三态策略;默认关闭的 `QL3_SHADOW_ORIGINS`manual、scheduled_node、boot、subscription、system 与 script 现有 ChildProcess 旁路观察;system crond 显式 origin marker、Shell execution ID、finish-only 准入、确定性 Run/Attempt 与 exact replay`@once` 保持 manual、gRPC transport 不冒充 origin 的准入裁决;每个 worker 懒加载;Run/Attempt/Event 影子生命周期;稳定且不复制 caller 原文的 task identity/revision 与有界日志引用;同 worker 有界注册表和跨 worker 持久化候选关联;stop all/stop instance、Shell callback、乱序/迟到/歧义处理;监听前一次性、Profile-aware 的 keyset Startup Reconciler,终态证据补齐、lost/abandoned/pending 分流与 terminal Attempt response-loss 修复;origin-bounded 且逐级守恒的版本化 startup difference report、固定字段 metric batch 与一次性 collector;显式、只读、闭合窗口且 Profile-bounded 的 Shadow→Legacy 终态差异审计;128/256 MiB Linux arm64 资源门、SQLite 零增长与 Shadow enabled→off 进程重启回滚;失败开放和契约测试 | Legacy→Shadow capture authority、具体 exporter 和 Primary 门禁 |
| PR-4 Shadow Run | Incubating | origin 三态策略;默认关闭的 `QL3_SHADOW_ORIGINS`manual、scheduled_node、boot、subscription、system 与 script 现有 ChildProcess 旁路观察;system crond 显式 origin marker、Shell execution ID、finish-only 准入、确定性 Run/Attempt 与 exact replay`@once` 保持 manual、gRPC transport 不冒充 origin 的准入裁决;每个 worker 懒加载;Run/Attempt/Event 影子生命周期;稳定且不复制 caller 原文的 task identity/revision 与有界日志引用;同 worker 有界注册表和跨 worker 持久化候选关联;stop all/stop instance、Shell callback、乱序/迟到/歧义处理;监听前一次性、Profile-aware 的 keyset Startup Reconciler,终态证据补齐、lost/abandoned/pending 分流与 terminal Attempt response-loss 修复;origin-bounded 且逐级守恒的版本化 startup difference report、固定字段 metric batch 与一次性 collector;显式、只读、闭合窗口且 Profile-bounded 的 Shadow→Legacy 终态差异审计;128/256 MiB Linux arm64 资源门、SQLite 零增长与 Shadow enabled→off 进程重启回滚;process-epoch Legacy admission/capture/failure/pending 守恒;clean-shutdown `0600` no-replace capture+startup exportermanual Edge 8/Standalone 32128 canarycapture/terminal/resource 自包含 Primary bundlerollout v2 loader 重算 source digest 与 eligibility;失败开放和契约测试 | 首次真实目标实例 manual canary bundle、其他 origin 独立 capture/Primary gate、固定物理 edge/flash/断电证据 |
| PR-5 Primary LocalExecutor | Incubating(默认不激活,仅 manifest-gated manual | runtime-owned Run 创建器;持久化先于 spawnRun/Attempt 完整成功、失败、取消、超时与 lost 闭环;Executor handle 身份校验;spawn 后激活写失败的 stop+lost 补偿;completion rejection 安全收敛;独立 Primary 幂等查询与唯一索引竞态裁决;durable `run.cancel_requested`、stop-before-signal、首次请求幂等、晚到完成裁决与待取消有界恢复查询;最多 64 条一页的 cross-worker cancellation source;独立 CancellationDispatch Repository 原子 claim/result、lease expiry 接管、owner/token/version fencing、指数退避与结果 RunEvent;最多 64 页的单周期 cancel supervisor;显式 start/stop、无重叠、错误隔离、停止等待有上限且 timer unref 的 lifecycle runnerLinux durable handle 的 PID/boot/start ticks/process-group 复验与 TERM/KILL controller;完整有界分页且 fail-closed 的 startup Reconcile supervisorRunningInstance nullable `run_id/attempt_id` 关联;Primary 专用组合 Repository 在同一 SQLite 事务提交前投影 Crontab/RunningInstance,失败整体回滚;有界且防穿越的 legacy log output refmanual owner seam、真实本机装配、单 spawn/fail-closed;严格 manual-only rollout manifest loader、短期审批/gate、配置哈希审计;HTTP worker 已接轻量 lazy bootstrapaccepted 后按 receipt-first reconcile→completion receipt lifecycle→timeout intent lifecycle→cancel dispatch lifecycle→router 顺序激活,失败撤销,监听失败和 shutdown 有界停止;Primary timeout 在 spawn 前持久化绝对 deadline,有界 source/requester/supervisor 只提交 timeout 意图并复用 CancellationDispatch;代码级 edge/standalone Profile 为各 lifecycle 提供不同 cadence 与页上限,cluster-control/worker 拒绝误装本机 SQLite Primary;统一 CompletionService 原子提交 Attempt/Run/双 Eventspawn 前保存 callback token hash、终态推进 sequence,实时回调与 receipt consumer 共享入口并覆盖两个清理 crash windowmanual Primary 已接入受限 POSIX launcher、`0600` direct-file stdout/stderr、父进程退出后续写、不可覆盖 receipt 生产、回执环境清除、TERM 转发等待及 live transaction 后清理;Startup Reconciler receipt-first 双检查并在确定 exited 后执行 profile 化的单次 50/100 ms publish grace`0007` 独立 CompletionReceiptJournal 在 spawn 前登记、为升级前 active Attempt 补登记并驱动周期扫描,使终态残留继续可发现;确定无效的已知 Attempt receipt 先持久化隔离状态,再进入确定性私有分片 quarantine;终态 missing 与 quarantine 按 edge/standalone retention 有界清理;非 Journal 文件具备只读优先、固定分片/条目上限、overflow fail-closed、显式同盘隔离的 Node 24 运维 CLI;扫描具备页上限、resume cursor、timer unref、无重叠、有界 stop 和低敏计数;ENOSPC 与 launcher receipt 存储失败有代码门禁 | 部署配置写入/审批入口与用户可见状态;PostgreSQL CancellationDispatch adaptercluster-control 生产启动拓扑;固定 edge/Linux 多架构与真实磁盘压力基线、完整 2.x API 契约和回滚演练 |
| PR-7 Worker Session、Run Lease 与启动协议基础 | Incubating(默认关闭,独立入口显式 opt-in | ADR-0012/0013/0014/0021/00570061/01080121/02310239/0377;有界 capability/Placement/DispatcherSQLite 协议孵化与 PostgreSQL v9 Session/Run Lease/credential/attestation authorityimmutable revision Placement、数据库时钟 keyset candidate、认证 Worker Pull、digest-only offer recoveryversioned capability-free ExecutionSpec response、stable claim 跨重启退避、单 owner 原子 inbox 准入与 TLS 1.3 mTLS/`ql3w` HTTPS client;同一 package journal 上 revision-fenced starting/spawn/started/running/completion 状态、callback digest、tagged no-spawn 与 ambiguous recoveryPostgreSQL starting/running/start-failure/completion 数据库权威事务、精确重放与 cancellation/timeout 优先终态;batch Secret delivery 在 Attempt advisory lock 下复验 Session/Lease/revision 完整围栏并复用单 AgentSecret-before-Artifact materializer 将同一 log ID 交给 Executor/journal/running ACKoffer-scoped `wlog-*` 私有文件 spool、Edge/Node 容量策略、append/quota/path 防护、barrier 后 output ownership、受审 POSIX Executor、truncation fact、固定内存流式 source、认证 Artifact stream、共享 immutable store port、S3-compatible SSE/checksum/条件 promotion adapter、upload-before-completion 协调,以及 Local/Cluster 同构、Profile-aware、ETag-fenced range read;用户取消 run.stop mutation 以数据库时间写 intent/Event 并在事务内复验 Project/RoleBinding fence;非执行取消 convergence lifecycle、运行期 expiry 与安全 lost retry 已接入 cluster-control 单一全局 cadence;完整 generation/version/token/Attempt fencing;独立最小权限 Worker ingress、CA/CRL 与连接 generation 热重载;offer journal、spawn barrier、receipt-first recovery;独立 `@qinglong/worker-runtime` 的本地 P-256 CSR、key/chain/trust 验证、generation + active pointer 安装和持久退避;默认关闭的 production process 已装配具体 execution graph、完整 Session heartbeat/drain/offline、direct-file bootstrap、单 Agent/单 cadence、startup reconciliation、证书 maintenance、transport fail-close/recovery 与 Edge/Node 有界预算;真实 PostgreSQL 18 + Linux Node 合约已覆盖 Run completion、credential 和 CA 双轮换且保持同一 Session;真实 K3s 合约已覆盖 TLS/credential Secret 分权、双对象 CAS、Recreate 顺序、identity generation 与单节点 PVC recovery;所有能力默认不可达且受 edge/cluster import audit 约束 | 具体 cert-manager/Vault/SPIFFE/离线 CA adapter 与模板、ingress reload controller、生产 RBAC、证书到期告警和 `ql3w` credential recovery 产品面;具体 KMS/Vault Secret provider、对象存储 credential/temporary lifecycle 与 retention/tombstoneWorker 管理 API;真实 Kubernetes 多节点 CSI/node-loss/production 360 秒 drain 与固定 edge 文件系统 suspend/时钟/断电、x64/arm64 资源门禁 |
| PR-8 Project/Policy/Approval Core | Incubating(默认拒绝、无生产业务执行入口) | ADR-0028;统一六类 ActorRef 与 exact-shape 校验;`0017` ownerless default Project 和 append-only versioned RoleBindingowner/admin/operator/viewer 固定矩阵;Project 内 mutation 幂等、expected-version CAS、双 SQLite 连接竞争门禁;archived read-only、revocation、存储损坏 fail-closedAgent 写/Secret/Tool `require_approval`ADR-0047 把六类 subject、role/permission matrix 与 fence 抽到 runtime-core`pg-0004-project-policy`/capability v3 建立 ownerless PostgreSQL baseline、严格 role/state CHECK、append-only runtime 权限、SERIALIZABLE Project lock、mutation replay、双连接单 winner 和 cluster admission authorizerADR-0049/`pg-0005` capability v4 建立 stable IdentitySubject、append-only digest-only API credential、真实 cluster bearer authenticator、write-only durable security audit 与最小权限 runtime role,且已验证 HTTP→credential→Policy→audit→handler 纵向链路;ADR-0051 建立 `/api/v3` 认证前 peer/global 双预算、transport-peer-only、无 timer 且有界内存的 overload shieldADR-0027 Artifact authorizer adapterADR-0029 `AuthenticatedPrincipal` contract、`0018` digest-only versioned challenge、CSPRNG/TTL、同事务消费 challenge + 写首 owner、精确重放与双连接竞争/崩溃回滚门禁;ADR-0030 `0019` stable identity/binding、legacy HS384 + current-session membership、logout/platform/revoke/disable、single-factor 与损坏 fail-closed 门禁;ADR-0031 `0020` digest-bound ApprovalRequest、User-only decision、Project/Role version fence、精确 expiry/重放/并发裁决及同事务 immutable dispatchADR-0032 `0021` execution backfill、三表原子 consume、稳定 due keyset、claim/renew/start/result fencing、pre-start takeover/post-start recovery-required、attempt budget、handler inspect/digest barrier 和 bounded dispatcherADR-0033/`0022` control/resolution backfill、start/renew/completion 原子联动、稳定 recovery keyset、双 resolver claim/takeover、finding/result 精确重放、自动/人工终结、迟到 completion 单 winner 和 evidence-only bounded reconcilerADR-0034/`0023` 首个 `run.create` canonical plan、Run/Attempt/Event/receipt 同事务、幂等 collision fail-closed、renew/终态 fence、真实 SQLite handler 与 automatic evidence providerADR-0035/`0024` 独立 `approval.recover` 矩阵、稳定 User + 五分钟强认证、Project/RoleBinding fence、human resolution + authorization fact 原子提交、撤权竞态与回滚门禁;ADR-0036 recovery-first 单 timer lifecycle、edge/standalone 独立 cadence/页预算、跨周期 cursor、非重叠与有界 stopADR-0074 以新的 Node 24 SQLite v5 ownerless Project/RoleBinding/audit authority 和独立 local-secret-admin 提供强 Principal、`secret.manage`、撤权 fence、envelope+allowed audit 原子提交及不回显语义;ADR-0086 以可信 POSIX console 和 staged delivery 完成本机首 Owner 产品 ceremony | fresh database/pepper setup 与安全迁移向导;`shareStore`/Express 到 authentication core 的 production migrationcredential rotation/revocation API、mTLS/Worker enrollment、恢复码;Project/Role/Approval/Secret 管理 CLI/API/UI、audit retention/query/export/alert、preview Artifact/digest/immutable plan builder、真实 MFA/hardware adapter、人工 recovery API/UI/独立 rate limit 与审计事件、handler/provider registry、lifecycle startup/shutdown/指标/admission gatePostgreSQL action/receipt/provider/recovery-authorization 与 OPA adapter、缓存 version 失效;Tool/Package/Secret/Shell 各自的 handler/evidence contractSecret/Run/Tool/Workflow waiting_approval 全入口装配;完整回滚演练 |
@@ -239,7 +239,7 @@ Shadow Adapter 不得:
Shadow 转换仍必须遵守 ADR-0001。无法合法映射时追加 compat.transition_mismatch,不能强行覆盖终态。
当前 Alpha 切片对已审 Node worker origin 直接观察同一 ChildProcess 的 spawn、error 和 exit 事件,因此不依赖 Shell callback 才能形成基本终态。下述两级关联已补充 Shell callback、stop/cancel 和乱序/迟到回调;ADR-0448 又补充了监听前一次性启动恢复,ADR-0449 将其投影为 origin-bounded、版本化的差异报告与固定字段 metric batch。ADR-0450 再提供显式、只读、Profile-bounded 的闭合窗口终态审计;ADR-0451 已在 128 MiB router stress 与 256 MiB Edge release cgroup 中证明有界查询、SQLite 零增长和进程重启后的 Shadow-off 回滚。 2.x RunningInstance 缺少可信 origin,因此这些证据只覆盖已写 Shadow Run 到 Legacy evidence 的一致性,不能单独证明 Legacy→Shadow 捕获率或替代正式 Primary gate
当前 Alpha 切片对已审 Node worker origin 直接观察同一 ChildProcess 的 spawn、error 和 exit 事件,因此不依赖 Shell callback 才能形成基本终态。下述两级关联已补充 Shell callback、stop/cancel 和乱序/迟到回调;ADR-0448 又补充了监听前一次性启动恢复,ADR-0449 将其投影为 origin-bounded、版本化的差异报告与固定字段 metric batch。ADR-0450 再提供显式、只读、Profile-bounded 的闭合窗口终态审计;ADR-0451 已在 128 MiB router stress 与 256 MiB Edge release cgroup 中证明有界查询、SQLite 零增长和进程重启后的 Shadow-off 回滚。ADR-0453 不再从 2.x RunningInstance 猜测反向分母,而是在默认 Legacy bridge admission 建立 process-epoch 守恒 token,并把 capture/startup、terminal 与 resource/rollback 三类低敏 source report 打包;rollout v2 loader 会独立重算 bundle 后才允许 manual Primary。
### 9.4 `next` Alpha callback 与 stop 关联
@@ -253,7 +253,7 @@ Shadow 转换仍必须遵守 ADR-0001。无法合法映射时追加 compat.trans
6. 取消事实在 Legacy kill 前投递;同 worker 的后续 exit 排在取消之后。跨 worker 使用持久化定位器尽力关联,任何查询或写入失败都不能阻断 kill 或改变 2.x API 响应。
7. 乱序 finished 可以从 queued/claimed 补齐 dispatching、starting、running 和终态;重复终态 callback、取消后的迟到成功 callback 不覆盖终态,也不追加重复完成事件。
这仍不是完整的 Shadow→Primary 门禁:ADR-0448 已提供启动后有界批量扫描、终态证据补齐、lost/abandoned 收敛和两事务 response-loss 修复;ADR-0449 已增加 startup 差异报表、固定低基数 metric batch 与可注入单次 collectorADR-0450 已完成不伪造反向捕获率的闭合窗口 Shadow→Legacy 终态差异查询;ADR-0451 已完成 Profile-aware 资源压力和 Shadow-off 进程重启回滚。具体 exporter、Legacy→Shadow capture authority 和正式 Primary gate 尚未完成。后续能力不得让 edge 增加常驻 watcher 或无界内存队列。
ADR-0453 已补齐 manual origin 的正式 Shadow→Primary 判定契约:process-local admission/capture/failure/pending 守恒、clean-shutdown 一次性 exporter、capture/startup + terminal + resource/rollback 自包含 bundle,以及 rollout v2 loader 的独立重算。它仍不会自动启用 Primary;目标实例必须产生真实 manual canary bundle,其他 origin 也必须独立评审。后续能力不得让 edge 增加常驻 watcher 或无界内存队列。
### 9.3 Shadow 写失败
@@ -73,5 +73,6 @@ ADR-0450 已冻结闭合窗口的 Shadow→Legacy 终态审计语义,但只证
## 后续
D-360 仍须建立可信的 origin-scoped Legacy→Shadow capture authority组合 observer failure/capture evidence 与本 ADR/ADR-0450 的结果,再定义正式 Primary gate。
D-360/ADR-0453 已建立 origin-scoped Legacy→Shadow capture authority并把 observer capture/failure、startup、ADR-0450 terminal 与本 ADR resource/rollback
组合为可由 rollout v2 loader 独立重放的 manual Primary gate bundle;首次目标实例 manual canary bundle 仍必须在实际启用前产生。
固定物理 edge、真实 flash 写放大和断电演练必须作为独立现场证据,不能由本 ADR 的 Docker arm64 结果替代。
@@ -0,0 +1,100 @@
# ADR-0453Origin-scoped Legacy Shadow 捕获权威与 Primary 门禁
- 状态:Accepted
- 日期:2026-08-19
- 关联 RFCQL-RFC-0001 D-02、D-360、PR-4、PR-5
- 关联 ADRADR-0002、ADR-0449、ADR-0450、ADR-0451
- AmendsADR-0002 的 Shadow→Primary 门禁、ADR-0451 的 Legacy→Shadow capture 缺口
## 上下文
ADR-0450 能证明已存在的 legacy-owned Shadow Run 与 Legacy 终态一致,但它明确不测量没有 Shadow Run 的 Legacy execution。ADR-0451 又证明
资源预算和 enabled→off 回滚,却仍不能把“看到的 Shadow 都正确”推导成“每个 Legacy admission 都被捕获”。旧
`shadowBridgeFailureSnapshot()` 只有进程累计失败 key,没有 admission 分母、pending 守恒、测量窗口或可供 rollout loader 重放的权威文件。
Primary manifest v1 还只要求维护者填写五个 `"passed"`。即使审批和文件摘要有效,运行时也无法重算这些字符串背后的 capture、terminal agreement、
startup convergence 与 rollback evidence;把字符串替换为一份只声明 `eligible` 的 receipt 同样不构成门禁。
## 决策
1. 已配置 origin 进入默认 Legacy Shadow bridge 后、构造 fact 前,必须取得一次 process-local admission token。token 只能结算为
`captured | failed | pending``captured + failed + pending = admitted`failure 进一步固定为 `fact | observer | initialization | accept` 四类并守恒。
Legacy spawn、返回值和失败开放语义不变;测试 override 不构成生产 capture authority。
2. `LegacyShadowRunObserver` 暴露只读异步 `captureSettled()`:只有真实 `LegacyShadowRunWriter.accept()` 成功才结算 captured。初始化失败、fact 构造失败、
observer begin 失败或 accept 失败分别结算到固定失败类;后续 spawned/running/terminal 写入仍由 ADR-0450 终态审计裁决,不能用 accept 成功替代终态一致。
3. snapshot schema 为 `qinglong/legacy-shadow-capture-snapshot@v1`,用随机 process epoch、时间和最多七行 origin 固定计数表达,不含 PID、Run、Attempt、Cron、
task、command、path、用户或错误消息。window report schema 为 `qinglong/legacy-shadow-capture-report@v1`;起始 snapshot 必须零 pending,同 epoch 前后差分、
origin exact coverage 和计数单调必须成立。
4. HTTP worker 在 Legacy normalization 和 ADR-0449 startup reconciliation 之后、Primary bootstrap/listen 之前装配一次性 exporter。只有显式设置
`QL3_SHADOW_CAPTURE_EVIDENCE_FILE=<basename>.json` 才开启;文件固定写入既有 config rootbasename 禁止路径、`.` 前缀和 `..`。exporter 无 timer、watcher、
listener、重试队列或数据库连接,只在干净 shutdown 写一次 `0600``wx` no-replace JSON;失败只形成低敏审计,不改变 Legacy shutdown。
5. capture evidence schema 为 `qinglong/legacy-shadow-capture-evidence@v1`,同时嵌入同进程启动时的 ADR-0449 report。只有 startup `converged`、Profile 与
origin 顺序精确一致、capture assessment 为 `captured` 才 qualifiedcrash、非干净退出、partial/no-replace 写失败、empty、pending 或任一失败都不能产生
Primary eligibility。
6. `gate:legacy-shadow-primary:ql3` 组合三份低敏输入:capture/startup evidence、ADR-0450 closed terminal report、ADR-0451 compiled-backend full rollback/resource
report。当前只允许 `manual`edge 必须精确 8 个 admissionstandalone 为 32128 个。terminal window 必须与 capture window 逐值相等,scanned/matched 必须
等于 capturedclosed、evidence complete、无 remainingterminal agreement 与 fully comparable 都必须为 1000/1000。资源报告必须同 Profile、full、
compiled backend、qualified,并证明 Legacy continued、Shadow stopped 与 SQLite integrity `ok`
7. Primary gate bundle schema 为 `qinglong/legacy-shadow-primary-gate@v1`。它嵌入三份 source report、各自 canonical JSON SHA-256、固定计数、window、结论与固定
violation code;不嵌入原文件路径。CLI 用 `O_NOFOLLOW`、1 MiB 上限读取输入,并以 `0600`、no-replace 写 bundleineligible 时不写输出。
8. Rollout manifest 升为 schema v2。enabled manifest 必须增加 `primaryGate` reference,只允许 `manual`、同 config 目录 basename 和 64-hex bundle digest。
loader 以 no-follow、64 KiB 上限读取 bundle,验证 manifest digest,重新计算 embedded source canonical digests,并从 source reports 重新执行完整 Primary gate
它不信任 CLI 写入的 `assessment`。bundle 必须 eligible、生成时间不晚于审批时间且 Profile 与实际 Local deployment 相同,之后才可惰性加载 Primary stack。
9. v1 enabled manifest 失败关闭,不自动补写或猜测 gate。disabled/missing/rejected 仍保持零 Primary stack、router、timer 和连接。`defaultMode=off`、manual-only、
最长 30 天审批、rollback plan 和既有 durable cancellation/atomic projection gates 保留。
## 被拒绝的替代方案
### 给 RunningInstances 增加 origin 后直接当分母
拒绝。2.x 多条 Node/Shell 路径只在 spawn/callback 后写 RunningInstances,缺失行本身不可见,无法证明 admission capture;为兼容观测改成数据库
fail-closed 还会改变 Legacy 可用性。
### 每次 Legacy execution 同步写一条新 admission ledger
拒绝作为本阶段方案。它会让 Shadow 数据库写参与 Legacy spawn 前置路径,并在低配 flash 上形成第二份逐执行持久化权威。若未来需要跨 crash 的在线连续窗口,必须以
独立 migration、retention、写放大和故障语义重新评审,不能暗中加入兼容桥。
### 只在日志里输出累计 counter
拒绝。日志片段没有同 epoch 起止、pending baseline、startup report binding、no-replace 文件或 loader 重放;丢日志时也不能 fail-closed。
### Loader 只验证 `eligible` receipt 的摘要
拒绝。摘要只能证明文件没变,不能证明内容真实执行 gate。v2 bundle 必须携带低敏 source reportsloader 必须独立重算 digest 和结论。
## 资源、安全与部署影响
- 每个已启用 Legacy admission 增加一个常数 token 和四个固定 counter 更新;最多七行 origin,没有按执行身份保存集合,不增加 timer、watcher、listener、线程、连接或
schema。未配置 Shadow origins 时仍在 fact factory、observer/Repository import 和 capture authority admission 前返回。
- 正常运行不写 capture 文件;只有显式证据 canary 的干净 shutdown 写一个有界文件。edge/standalone runtime 不需要 Prometheus、OTel、外部数据库、对象存储或
Cluster 组件。
- bundle 是本机 config-root trust domain 内的 rollout evidence,不是签名供应链 attestation。拥有 config root 写权限的 operator 仍是本机信任根;公开分发或跨主机
delegation 需另加签名 ceremony。
- 本 Gate 只开放 manual eligibility 的判定能力,不自动写 manifest、不启用 Primary、不接管 scheduled/system/boot/gRPC origin,也不证明物理 flash、断电或生产任务内容。
## 验证
- 纯 authority 覆盖成功、四类失败、pending、跨 epoch、非零 pending baseline、origin coverage 和脱敏守恒。
- exporter 覆盖 armed→exported、clean shutdown、`0600`、no-replace、重复 close、缺失 startup、未配置和失败开放。
- gate/CLI 覆盖 exact edge cohort、样本不足、terminal 漂移、audit-only rollback、symlink、no-replace、embedded source 篡改和 canonical digest 重算。
- rollout v2 覆盖 missing/tampered/ineligible bundle、审批时间、Profile mismatch、unknown field/path traversal 和 disabled lazy path。
- 真实 compiled backend 的 `ScheduleService.runTask` enabled child 已通过默认 observer/Repository 产生一个 `system` captureadmitted/captured 为 1/1failed/pending 为
0/0;它只证明 bridge 真实结算链,不冒充 manual Primary 的 8/32 条正式 canary。
- 阶段门已从 clean package artifacts 重跑:D-360 聚焦测试 `48/48`、Legacy/Shadow 串行扩展 `117/117`、资源/回滚专项 `4/4``build:back`
完整 backend `1,469 pass / 0 fail / 2 conditional skip`、18-package clean build/test、四项可执行架构审计与 `14/14` Local Profile artifact audit
全部通过。产物字节保持 D-358 基线:base `2,589,998 / 2,590,076`、adopted `2,809,293 / 2,809,416`、application
`3,632,877 / 3,632,997`、application-api `3,800,430 / 3,800,574`、AI `3,069,251 / 3,069,341`、application+AI
`4,493,151 / 4,493,283`、MCP `7,315,930 / 7,316,038`
- Linux arm64 Docker 资源门再次通过:router stress 保持 `128 MiB / 0.5 CPU / 0 swap / 64 PID`cgroup peak `95,113,216` bytesEdge release
保持 `256 MiB / 1 CPU / 0 swap / 128 PID`13 个 workload 全部通过,cgroup peak `144,740,352` bytes`memory.events`
`max/oom/oom_kill` 增量均为 0。Edge full rollback 中默认 `system` bridge 的真实 capture 为 `1/1`terminal audit p95 `5.236 ms`、RSS delta
`2,621,440` bytes、数据库存储前后稳定。Docker arm64 仍不是物理路由、flash wear 或断电证据。
- D-360 未修改 PostgreSQL schema、migration、依赖树或 Kubernetes 拓扑,因此不重跑 PostgreSQL HA;相邻 D-359 的 PostgreSQL HA `142/142`
timeline `1→2` 只作为未被本阶段触碰的既有证据,不冒充本阶段新结果。
## 后续
正式启用 manual Primary 前,维护者必须在目标 edge/standalone 实例完成一次真实、干净关闭的 manual canary,等待 settling 后运行 terminal audit,组合与同版本
resource report 生成 bundle,再由 v2 loader 重放。固定物理路由、flash 写放大、断电、非干净退出和 config-root 签名/备份仍是独立发布证据;其他 origin 必须分别建立
自己的 admission authority、样本预算和 rollback gate,不能复用 manual receipt。
+1
View File
@@ -456,6 +456,7 @@
| [ADR-0450](./ADR-0450-closed-window-legacy-shadow-terminal-difference-audit.md) | 闭合窗口的 Legacy Shadow 终态差异审计 | Accepted |
| [ADR-0451](./ADR-0451-profile-bounded-legacy-shadow-resource-and-off-rollback-evidence.md) | 按 Profile 有界的 Legacy Shadow 资源与关闭回滚证据 | Accepted |
| [ADR-0452](./ADR-0452-atomic-flattened-backend-build-publication.md) | 原子且扁平兼容的 Backend 构建发布 | Accepted |
| [ADR-0453](./ADR-0453-origin-scoped-legacy-shadow-capture-authority-and-primary-gate.md) | Origin-scoped Legacy Shadow 捕获权威与 Primary 门禁 | Accepted(首次真实目标实例 manual canary 待执行) |
## 规则
+1
View File
@@ -75,6 +75,7 @@
"audit:legacy-schema:ql3": "node scripts/ql3-schema-audit.cjs",
"audit:receipts:ql3": "node scripts/ql3-receipt-audit.cjs",
"audit:legacy-shadow-terminal:ql3": "node scripts/ql3-legacy-shadow-terminal-audit.cjs",
"gate:legacy-shadow-primary:ql3": "node scripts/ql3-legacy-shadow-primary-gate.cjs",
"audit:edge-imports:ql3": "node scripts/ql3-edge-import-audit.cjs",
"audit:cluster-dependencies:ql3": "node scripts/ql3-cluster-dependency-audit.cjs",
"audit:package-boundaries:ql3": "node scripts/ql3-package-boundary-audit.cjs",
+151
View File
@@ -0,0 +1,151 @@
#!/usr/bin/env node
require('ts-node/register/transpile-only');
const fs = require('node:fs');
const path = require('node:path');
const {
createLegacyShadowPrimaryGateReceipt,
} = require('../back/runtime/domain/legacyShadowPrimaryGate');
const MAX_EVIDENCE_BYTES = 1024 * 1024;
function parseArguments(argv) {
const options = { profile: 'edge' };
for (const argument of argv) {
if (argument === '--') continue;
if (argument.startsWith('--profile=')) {
options.profile = argument.slice('--profile='.length);
} else if (argument.startsWith('--capture=')) {
options.capturePath = path.resolve(argument.slice('--capture='.length));
} else if (argument.startsWith('--terminal=')) {
options.terminalPath = path.resolve(argument.slice('--terminal='.length));
} else if (argument.startsWith('--resource=')) {
options.resourcePath = path.resolve(argument.slice('--resource='.length));
} else if (argument.startsWith('--output=')) {
options.outputPath = path.resolve(argument.slice('--output='.length));
} else if (argument.startsWith('--generated-at-ms=')) {
const raw = argument.slice('--generated-at-ms='.length);
if (!/^\d+$/u.test(raw)) {
throw new TypeError('--generated-at-ms must be an integer');
}
options.generatedAtMs = Number(raw);
} else {
throw new TypeError(`Unsupported argument: ${argument}`);
}
}
if (options.profile !== 'edge' && options.profile !== 'standalone') {
throw new TypeError('--profile must be edge or standalone');
}
for (const name of [
'capturePath',
'terminalPath',
'resourcePath',
'outputPath',
]) {
if (!options[name] || !path.isAbsolute(options[name])) {
throw new TypeError(`--${name.replace('Path', '')} is required`);
}
}
if (
options.generatedAtMs !== undefined &&
(!Number.isSafeInteger(options.generatedAtMs) || options.generatedAtMs < 0)
) {
throw new TypeError('--generated-at-ms is invalid');
}
return options;
}
function readEvidence(sourcePath) {
const descriptor = fs.openSync(
sourcePath,
fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0),
);
try {
const stat = fs.fstatSync(descriptor);
if (
!stat.isFile() ||
(stat.mode & 0o077) !== 0 ||
stat.size < 2 ||
stat.size > MAX_EVIDENCE_BYTES
) {
throw new TypeError('Primary gate evidence file shape is invalid');
}
const bytes = Buffer.alloc(stat.size);
let offset = 0;
while (offset < bytes.length) {
const count = fs.readSync(
descriptor,
bytes,
offset,
bytes.length - offset,
offset,
);
if (count === 0) throw new Error('Primary gate evidence read stalled');
offset += count;
}
return {
value: JSON.parse(bytes.toString('utf8')),
};
} finally {
fs.closeSync(descriptor);
}
}
function writeReceipt(outputPath, receipt) {
const descriptor = fs.openSync(outputPath, 'wx', 0o600);
try {
fs.writeFileSync(descriptor, `${JSON.stringify(receipt)}\n`, 'utf8');
fs.fsyncSync(descriptor);
} finally {
fs.closeSync(descriptor);
}
}
function run(options) {
const capture = readEvidence(options.capturePath);
const terminal = readEvidence(options.terminalPath);
const resource = readEvidence(options.resourcePath);
const receipt = createLegacyShadowPrimaryGateReceipt({
profile: options.profile,
generatedAtMs: options.generatedAtMs ?? Date.now(),
capture: capture.value,
terminal: terminal.value,
resource: resource.value,
});
if (receipt.assessment !== 'eligible') {
const error = new Error(
`Legacy Shadow Primary gate is ineligible: ${receipt.violations.join(
',',
)}`,
);
error.code = 'QL3_PRIMARY_GATE_INELIGIBLE';
throw error;
}
writeReceipt(options.outputPath, receipt);
return receipt;
}
function main() {
const receipt = run(parseArguments(process.argv.slice(2)));
process.stdout.write(`${JSON.stringify(receipt)}\n`);
}
module.exports = {
MAX_EVIDENCE_BYTES,
parseArguments,
readEvidence,
run,
writeReceipt,
};
if (require.main === module) {
try {
main();
} catch (error) {
process.stderr.write(
`${error instanceof Error ? error.message : String(error)}\n`,
);
process.exitCode = 1;
}
}
@@ -369,7 +369,7 @@ async function runSummary(database) {
});
}
async function rollbackChild(mode, expectedBefore) {
async function rollbackChild(mode, expectedBefore, profile) {
const databasePath = process.env.QL3_SHADOW_DRILL_DATABASE;
if (!path.isAbsolute(databasePath ?? '')) {
throw new QingLong3LegacyShadowResourceRollbackError(
@@ -380,10 +380,17 @@ async function rollbackChild(mode, expectedBefore) {
await taskLimit.setCustomLimit();
const ScheduleService = fromRuntime('services/schedule').default;
const bridge = fromRuntime('runtime/compatibility/legacyExecutionBridge');
const { createLegacyShadowCaptureReport } = fromRuntime(
'runtime/application/legacyShadowCaptureAuthority',
);
const { sequelize } = fromRuntime('data');
let shortCircuitFactCalls = 0;
try {
const configuredOrigins = bridge.configuredLegacyShadowOrigins();
const captureBefore =
mode === 'enabled'
? bridge.legacyShadowCaptureSnapshot(['system'])
: undefined;
if (mode === 'off') {
const observation = bridge.observeLegacyExecution('system', () => {
shortCircuitFactCalls += 1;
@@ -434,6 +441,15 @@ async function rollbackChild(mode, expectedBefore) {
`${mode} restart did not preserve the expected Legacy result`,
);
}
const capture =
mode === 'enabled'
? createLegacyShadowCaptureReport(
profile,
['system'],
captureBefore,
bridge.legacyShadowCaptureSnapshot(['system']),
)
: undefined;
return Object.freeze({
mode,
configuredOrigins,
@@ -445,6 +461,7 @@ async function rollbackChild(mode, expectedBefore) {
shortCircuitFactCalls,
defaultObserverLoaded,
repositoryLoaded,
...(capture === undefined ? {} : { capture }),
peakProcessRssBytes: process.resourceUsage().maxRSS * 1024,
});
} finally {
@@ -558,6 +575,7 @@ async function runEvidence(options) {
[
'--internal-child=rollback-enabled',
`--expected-before=${fixtureRunCount}`,
`--profile=${options.profile}`,
],
{ ...childEnvironment, QL3_SHADOW_ORIGINS: 'system' },
)
@@ -568,6 +586,7 @@ async function runEvidence(options) {
[
'--internal-child=rollback-off',
`--expected-before=${fixtureRunCount + 1}`,
`--profile=${options.profile}`,
],
{ ...childEnvironment, QL3_SHADOW_ORIGINS: '' },
)
@@ -653,6 +672,7 @@ async function runEvidence(options) {
runDelta: enabled.runDelta,
defaultObserverLoaded: enabled.defaultObserverLoaded,
repositoryLoaded: enabled.repositoryLoaded,
capture: enabled.capture,
peakProcessRssBytes: enabled.peakProcessRssBytes,
}),
off: Object.freeze({
@@ -729,6 +749,7 @@ async function main() {
report = await rollbackChild(
mode === 'rollback-enabled' ? 'enabled' : 'off',
expectedBefore,
profile,
);
} else {
throw new QingLong3LegacyShadowResourceRollbackError(
@@ -25,6 +25,34 @@ function loadResult(status, mode = 'off') {
sourcePath: '/data/config/qinglong3-rollout.json',
status,
},
...(status === 'accepted'
? {
primaryGateReceipt: {
schema: 'qinglong/legacy-shadow-primary-gate@v1',
schemaVersion: 1,
profile: 'standalone',
origin: 'manual',
generatedAtMs: NOW - 2_000,
assessment: 'eligible',
window: {
startInclusiveMs: NOW - 10_000,
endExclusiveMs: NOW - 5_000,
},
counts: {
admitted: 32,
captured: 32,
terminalScanned: 32,
terminalMatched: 32,
},
evidence: {
captureSha256: 'a'.repeat(64),
terminalSha256: 'b'.repeat(64),
resourceSha256: 'c'.repeat(64),
},
violations: [],
},
}
: {}),
};
}
@@ -171,6 +199,27 @@ test('accepted bootstrap audits a lazy stack import failure without installing',
assert.deepEqual(calls, ['load-stack', 'audit:failed']);
});
test('accepted bootstrap rejects a Primary receipt for another Profile before loading', async () => {
const calls = [];
const load = loadResult('accepted', 'primary');
load.primaryGateReceipt.profile = 'edge';
await assert.rejects(
bootstrapDefaultManualPrimaryRuntime({
load: async () => load,
deploymentProfile: 'standalone',
async loadStack() {
calls.push('load-stack');
throw new Error('must remain lazy');
},
audit(record) {
calls.push(`audit:${record.activation}`);
},
}),
/does not authorize this deployment Profile/,
);
assert.deepEqual(calls, ['audit:failed']);
});
test('disabled bootstrap stays inert even with an invalid deployment profile', async () => {
const previous = process.env.QL_DEPLOYMENT_PROFILE;
process.env.QL_DEPLOYMENT_PROFILE = 'invalid-profile';
@@ -219,7 +268,7 @@ test('accepted bootstrap rejects and audits an invalid deployment profile', asyn
}),
/QL_DEPLOYMENT_PROFILE is invalid/,
);
assert.deepEqual(calls, ['load-stack', 'audit:selected', 'audit:failed']);
assert.deepEqual(calls, ['audit:failed']);
} finally {
if (previous === undefined) delete process.env.QL_DEPLOYMENT_PROFILE;
else process.env.QL_DEPLOYMENT_PROFILE = previous;
@@ -0,0 +1,162 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const fs = require('node:fs/promises');
const os = require('node:os');
const path = require('node:path');
const { afterEach, test } = require('node:test');
const {
LegacyShadowCaptureAuthority,
} = require('../../back/runtime/application/legacyShadowCaptureAuthority');
const {
bootstrapLegacyShadowCaptureEvidence,
} = require('../../back/runtime/adapters/legacy/bootstrapLegacyShadowCaptureEvidence');
const directories = [];
function startup(state = 'reconciled') {
if (state !== 'reconciled') return { state };
return {
state: 'reconciled',
profile: 'edge',
origins: 1,
summary: {},
metrics: {},
report: {
schema: 'qinglong/legacy-shadow-startup-difference-report@v1',
schemaVersion: 1,
profile: 'edge',
assessment: 'converged',
configuredOriginCount: 1,
budget: { pageSize: 8, maxPages: 1, maxCandidates: 8 },
coverage: {
pages: 1,
scanned: 0,
stopReason: 'complete',
remaining: false,
resumeAvailable: false,
},
outcomes: {
completed: 0,
cancelled: 0,
abandoned: 0,
markedLost: 0,
repaired: 0,
pending: 0,
ambiguous: 0,
skipped: 0,
failed: 0,
},
byOrigin: [
{
origin: 'manual',
scanned: 0,
completed: 0,
cancelled: 0,
abandoned: 0,
markedLost: 0,
repaired: 0,
pending: 0,
ambiguous: 0,
skipped: 0,
failed: 0,
},
],
},
};
}
afterEach(async () => {
await Promise.all(
directories
.splice(0)
.map((directory) => fs.rm(directory, { recursive: true, force: true })),
);
});
test('exports one qualified no-identity capture report on clean shutdown', async () => {
let now = 1_750_300_000_000;
const authority = new LegacyShadowCaptureAuthority(
{ now: () => now },
'019f75d2-3333-7333-8333-333333333333',
);
let evidence;
const audits = [];
const handle = await bootstrapLegacyShadowCaptureEvidence({
startup: startup(),
origins: ['manual'],
profile: 'edge',
outputPath: '/private/evidence.json',
snapshot: (origins) => authority.snapshot(origins),
async write(_outputPath, value) {
evidence = value;
},
audit: (record) => audits.push(record),
});
for (let index = 0; index < 8; index += 1) {
authority.admit('manual').captured();
}
now += 1_000;
assert.equal(handle.active, true);
assert.equal((await handle.close()).state, 'exported');
assert.deepEqual(await handle.close(), audits.at(-1));
assert.equal(evidence.qualification.passed, true);
assert.equal(evidence.capture.totals.admitted, 8);
assert.equal(evidence.capture.capturePermille, 1_000);
assert.doesNotMatch(JSON.stringify(evidence), /taskId|runId|attemptId|pid/);
assert.deepEqual(
audits.map((record) => record.state),
['armed', 'exported'],
);
});
test('writes owner-private evidence once and refuses overwrite', async () => {
const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'ql3-capture-'));
directories.push(directory);
const outputPath = path.join(directory, 'capture.json');
let now = 1_750_300_000_000;
const authority = new LegacyShadowCaptureAuthority(
{ now: () => now },
'019f75d2-4444-7444-8444-444444444444',
);
const options = {
startup: startup(),
origins: ['manual'],
profile: 'edge',
outputPath,
snapshot: (origins) => authority.snapshot(origins),
audit() {},
};
const first = await bootstrapLegacyShadowCaptureEvidence(options);
authority.admit('manual').captured();
now += 1_000;
assert.equal((await first.close()).state, 'exported');
assert.equal((await fs.stat(outputPath)).mode & 0o777, 0o600);
const second = await bootstrapLegacyShadowCaptureEvidence(options);
authority.admit('manual').captured();
now += 1_000;
assert.equal((await second.close()).state, 'failed');
});
test('stays inert without an explicit path and rejects missing startup authority', async () => {
const disabled = await bootstrapLegacyShadowCaptureEvidence({
startup: { state: 'disabled' },
origins: [],
profile: 'edge',
audit() {},
});
assert.equal(disabled.active, false);
assert.equal((await disabled.close()).state, 'disabled');
const failed = await bootstrapLegacyShadowCaptureEvidence({
startup: { state: 'incomplete' },
origins: ['manual'],
profile: 'edge',
outputPath: '/private/evidence.json',
audit() {},
});
assert.equal(failed.active, false);
assert.equal((await failed.close()).state, 'failed');
});
@@ -319,6 +319,9 @@ test('HTTP startup orders Shadow recovery after Legacy normalization and before
const shadowRecovery = source.indexOf(
'await bootstrapLegacyShadowStartupReconciliation()',
);
const captureEvidence = source.indexOf(
'await bootstrapLegacyShadowCaptureEvidence({',
);
const primaryActivation = source.indexOf(
'await bootstrapDefaultManualPrimaryRuntime()',
);
@@ -328,6 +331,7 @@ test('HTTP startup orders Shadow recovery after Legacy normalization and before
assert.equal(legacyNormalization >= 0, true);
assert.equal(legacyNormalization < shadowRecovery, true);
assert.equal(shadowRecovery < primaryActivation, true);
assert.equal(shadowRecovery < captureEvidence, true);
assert.equal(captureEvidence < primaryActivation, true);
assert.equal(primaryActivation < listen, true);
});
@@ -0,0 +1,113 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
LegacyShadowCaptureAuthority,
createLegacyShadowCaptureReport,
} = require('../../back/runtime/application/legacyShadowCaptureAuthority');
const EPOCH = '019f75d2-1111-7111-8111-111111111111';
function fixture() {
let now = 1_750_200_000_000;
const authority = new LegacyShadowCaptureAuthority({ now: () => now }, EPOCH);
return {
authority,
advance(milliseconds = 1_000) {
now += milliseconds;
},
};
}
test('creates a conserved origin-scoped capture window', () => {
const value = fixture();
const before = value.authority.snapshot(['manual']);
for (let index = 0; index < 8; index += 1) {
value.authority.admit('manual').captured();
}
value.advance();
const after = value.authority.snapshot(['manual']);
const report = createLegacyShadowCaptureReport(
'edge',
['manual'],
before,
after,
);
assert.equal(report.assessment, 'captured');
assert.equal(report.totals.admitted, 8);
assert.equal(report.totals.captured, 8);
assert.equal(report.totals.failed, 0);
assert.equal(report.totals.pending, 0);
assert.equal(report.capturePermille, 1_000);
assert.equal(JSON.stringify(report).includes('task'), false);
});
test('separates fixed failure stages and incomplete admissions', () => {
const value = fixture();
const before = value.authority.snapshot(['manual', 'scheduled_node']);
value.authority.admit('manual').failed('fact');
value.authority.admit('manual').failed('accept');
value.authority.admit('scheduled_node');
value.advance();
const report = createLegacyShadowCaptureReport(
'standalone',
['manual', 'scheduled_node'],
before,
value.authority.snapshot(['manual', 'scheduled_node']),
);
assert.equal(report.assessment, 'incomplete');
assert.deepEqual(report.totals, {
admitted: 3,
captured: 0,
failed: 2,
pending: 1,
failures: { fact: 1, observer: 0, initialization: 0, accept: 1 },
});
});
test('rejects cross-epoch, pending-baseline and incomplete-origin evidence', () => {
const left = fixture();
const right = new LegacyShadowCaptureAuthority(
{ now: () => 1_750_200_001_000 },
'019f75d2-2222-7222-8222-222222222222',
);
assert.throws(
() =>
createLegacyShadowCaptureReport(
'edge',
['manual'],
left.authority.snapshot(['manual']),
right.snapshot(['manual']),
),
/cross process epochs/,
);
left.authority.admit('manual');
const pending = left.authority.snapshot(['manual']);
left.advance();
assert.throws(
() =>
createLegacyShadowCaptureReport(
'edge',
['manual'],
pending,
left.authority.snapshot(['manual']),
),
/starts with pending/,
);
assert.throws(
() =>
createLegacyShadowCaptureReport(
'edge',
['manual', 'boot'],
left.authority.snapshot(['manual']),
left.authority.snapshot(['manual']),
),
/window must be non-empty|coverage is incomplete/,
);
});
+231
View File
@@ -0,0 +1,231 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { afterEach, test } = require('node:test');
const {
createLegacyShadowPrimaryGateReceipt,
parseLegacyShadowPrimaryGateReceipt,
} = require('../../back/runtime/domain/legacyShadowPrimaryGate');
const {
parseArguments,
readEvidence,
run,
} = require('../../scripts/ql3-legacy-shadow-primary-gate.cjs');
const START = 1_750_400_000_000;
const END = START + 60_000;
const GENERATED = END + 6 * 60_000;
const directories = [];
function captureEvidence(admitted = 8) {
const outcomes = {
completed: 0,
cancelled: 0,
abandoned: 0,
markedLost: 0,
repaired: 0,
pending: 0,
ambiguous: 0,
skipped: 0,
failed: 0,
};
return {
schema: 'qinglong/legacy-shadow-capture-evidence@v1',
profile: 'edge',
startup: {
schema: 'qinglong/legacy-shadow-startup-difference-report@v1',
profile: 'edge',
assessment: 'converged',
configuredOriginCount: 1,
coverage: { remaining: false },
outcomes,
byOrigin: [{ origin: 'manual', scanned: 0, ...outcomes }],
},
capture: {
schema: 'qinglong/legacy-shadow-capture-report@v1',
profile: 'edge',
assessment: 'captured',
epoch: '019f75d2-5555-7555-8555-555555555555',
window: {
basis: 'process_local_legacy_admission',
startInclusiveMs: START,
endExclusiveMs: END,
},
configuredOriginCount: 1,
totals: {
admitted,
captured: admitted,
failed: 0,
pending: 0,
failures: { fact: 0, observer: 0, initialization: 0, accept: 0 },
},
byOrigin: [
{
origin: 'manual',
admitted,
captured: admitted,
failed: 0,
pending: 0,
failures: {
fact: 0,
observer: 0,
initialization: 0,
accept: 0,
},
},
],
capturePermille: 1_000,
},
qualification: {
passed: true,
startupConverged: true,
originCoverageExact: true,
captureComplete: true,
},
};
}
function terminal(scanned = 8) {
return {
schema: 'qinglong/legacy-shadow-terminal-difference-report@v1',
profile: 'edge',
observedAtMs: GENERATED - 1,
window: {
basis: 'shadow_run_created_at',
startInclusiveMs: START,
endExclusiveMs: END,
minimumSettlingAgeMs: 300_000,
closed: true,
},
coverage: {
direction: 'shadow_to_legacy',
cohort: 'legacy_owned_shadow_runs',
legacyWithoutShadow: 'not_measured',
},
scanned,
remaining: false,
evidenceComplete: true,
assessment: 'matched',
counts: { matched: scanned },
byOrigin: [{ origin: 'manual', scanned, matched: scanned }],
terminalAgreementPermille: 1_000,
fullyComparablePermille: 1_000,
};
}
function resource() {
return {
schemaVersion: 1,
fixture: 'qinglong/legacy-shadow-resource-rollback-evidence@v1',
profile: 'edge',
workload: { mode: 'full', runtime: 'compiled_backend' },
rollback: {
performed: true,
legacyContinued: true,
shadowWritesStopped: true,
databaseIntegrity: 'ok',
},
qualification: { passed: true, violations: [] },
};
}
function input(overrides = {}) {
return {
profile: 'edge',
generatedAtMs: GENERATED,
capture: captureEvidence(),
terminal: terminal(),
resource: resource(),
...overrides,
};
}
afterEach(() => {
for (const directory of directories.splice(0)) {
fs.rmSync(directory, { recursive: true, force: true });
}
});
test('combines capture, startup, terminal and rollback evidence into eligibility', () => {
const receipt = createLegacyShadowPrimaryGateReceipt(input());
assert.equal(receipt.assessment, 'eligible');
assert.deepEqual(receipt.violations, []);
assert.deepEqual(receipt.counts, {
admitted: 8,
captured: 8,
terminalScanned: 8,
terminalMatched: 8,
});
assert.deepEqual(parseLegacyShadowPrimaryGateReceipt(receipt), receipt);
});
test('fails closed for an undersized cohort, terminal drift and audit-only rollback', () => {
const capture = captureEvidence(7);
const terminalReport = terminal(6);
const rollback = resource();
rollback.workload.mode = 'audit-only';
const receipt = createLegacyShadowPrimaryGateReceipt(
input({ capture, terminal: terminalReport, resource: rollback }),
);
assert.equal(receipt.assessment, 'ineligible');
assert.deepEqual(receipt.violations, [
'capture_sample_budget_invalid',
'terminal_not_matched',
'resource_not_compiled_full_rollback',
]);
assert.throws(
() =>
parseLegacyShadowPrimaryGateReceipt({
...receipt,
assessment: 'eligible',
}),
/invalid/,
);
});
test('CLI reads no-follow bounded inputs and publishes a no-replace receipt', () => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-primary-gate-'));
directories.push(directory);
const paths = Object.fromEntries(
['capture', 'terminal', 'resource', 'output'].map((name) => [
name,
path.join(directory, `${name}.json`),
]),
);
fs.writeFileSync(paths.capture, `${JSON.stringify(captureEvidence())}\n`, {
mode: 0o600,
});
fs.writeFileSync(paths.terminal, `${JSON.stringify(terminal())}\n`, {
mode: 0o600,
});
fs.writeFileSync(paths.resource, `${JSON.stringify(resource())}\n`, {
mode: 0o600,
});
const options = parseArguments([
'--profile=edge',
`--capture=${paths.capture}`,
`--terminal=${paths.terminal}`,
`--resource=${paths.resource}`,
`--output=${paths.output}`,
`--generated-at-ms=${GENERATED}`,
]);
const receipt = run(options);
assert.equal(receipt.assessment, 'eligible');
assert.equal(
parseLegacyShadowPrimaryGateReceipt(readEvidence(paths.output).value)
.assessment,
'eligible',
);
assert.equal(fs.statSync(paths.output).mode & 0o777, 0o600);
assert.throws(() => run(options), /EEXIST/);
const symlink = path.join(directory, 'capture-link.json');
fs.symlinkSync(paths.capture, symlink);
assert.throws(() => readEvidence(symlink), /ELOOP|symbolic/i);
});
+2
View File
@@ -125,6 +125,7 @@ test('serializes a successful observed process lifecycle without starting it', a
failure: (failure) => failures.push(failure),
});
const observation = observer.begin(acceptedFact());
assert.equal(await observation.captureSettled(), 'captured');
observation.spawned({
atMs: ACCEPTED_AT_MS + 1,
pid: 4242,
@@ -231,6 +232,7 @@ test('is default-off, fail-open, and rejects primary ownership', async () => {
failure: (failure) => failures.push(failure),
});
const failed = shadow.begin(acceptedFact());
assert.equal(await failed.captureSettled(), 'failed');
failed.spawned({ atMs: ACCEPTED_AT_MS + 1 });
failed.exited({ atMs: ACCEPTED_AT_MS + 2, exitCode: 0 });
await failed.settled();
@@ -146,6 +146,12 @@ test(
assert.equal(report.rollback.enabled.runDelta, 1);
assert.equal(report.rollback.enabled.defaultObserverLoaded, true);
assert.equal(report.rollback.enabled.repositoryLoaded, true);
assert.equal(report.rollback.enabled.capture.assessment, 'captured');
assert.equal(report.rollback.enabled.capture.totals.admitted, 1);
assert.equal(report.rollback.enabled.capture.totals.captured, 1);
assert.equal(report.rollback.enabled.capture.totals.failed, 0);
assert.equal(report.rollback.enabled.capture.totals.pending, 0);
assert.equal(report.rollback.enabled.capture.byOrigin[0].origin, 'system');
assert.deepEqual(report.rollback.off.configuredOrigins, []);
assert.equal(report.rollback.off.legacyExitCode, 0);
assert.equal(report.rollback.off.runDelta, 0);
+17 -2
View File
@@ -15,13 +15,19 @@ const NOW = 1_750_000_000_000;
function enabledManifest(overrides = {}) {
return {
schemaVersion: 1,
schemaVersion: 2,
revision: 'manual-primary-canary-1',
enabled: true,
approvedBy: 'operator:admin',
approvedAtMs: NOW - 1_000,
expiresAtMs: NOW + 60_000,
rollbackPlanRef: 'docs/runbooks/disable-primary.md',
primaryGate: {
schema: 'qinglong/legacy-shadow-primary-gate-reference@v1',
origin: 'manual',
receiptFile: 'manual-primary-gate.json',
receiptSha256: 'a'.repeat(64),
},
rollout: {
defaultMode: 'off',
origins: { manual: 'primary' },
@@ -120,7 +126,7 @@ test('parses a time-bounded, manual-only rollout manifest', () => {
);
const disabled = parseRuntimeRolloutManifest(
{ schemaVersion: 1, revision: 'disabled-1', enabled: false },
{ schemaVersion: 2, revision: 'disabled-1', enabled: false },
NOW,
);
assert.equal(disabled.policy.modeFor('manual'), 'off');
@@ -128,6 +134,7 @@ test('parses a time-bounded, manual-only rollout manifest', () => {
test('rejects broad, stale, incomplete, and extensible rollout manifests', () => {
const cases = [
enabledManifest({ schemaVersion: 1 }),
enabledManifest({
rollout: {
defaultMode: 'primary',
@@ -135,6 +142,14 @@ test('rejects broad, stale, incomplete, and extensible rollout manifests', () =>
allowLegacyFallbackBeforeStart: false,
},
}),
enabledManifest({
primaryGate: {
schema: 'qinglong/legacy-shadow-primary-gate-reference@v1',
origin: 'manual',
receiptFile: '../escaped.json',
receiptSha256: 'a'.repeat(64),
},
}),
enabledManifest({
rollout: {
defaultMode: 'off',
+138 -3
View File
@@ -1,6 +1,7 @@
require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const crypto = require('node:crypto');
const fs = require('node:fs/promises');
const os = require('node:os');
const path = require('node:path');
@@ -8,6 +9,9 @@ const { afterEach, test } = require('node:test');
const {
loadRuntimeRolloutManifest,
} = require('../../back/runtime/adapters/fs/runtimeRolloutManifestLoader');
const {
createLegacyShadowPrimaryGateReceipt,
} = require('../../back/runtime/domain/legacyShadowPrimaryGate');
const NOW = 1_750_000_000_000;
const directories = [];
@@ -18,15 +22,103 @@ async function fixturePath(name = 'qinglong3-rollout.json') {
return path.join(directory, name);
}
function enabledManifest() {
return {
function gateReceipt() {
const start = NOW - 20_000;
const end = NOW - 10_000;
const startup = {
schema: 'qinglong/legacy-shadow-startup-difference-report@v1',
profile: 'edge',
assessment: 'converged',
configuredOriginCount: 1,
coverage: { remaining: false },
byOrigin: [{ origin: 'manual' }],
};
const capture = {
schema: 'qinglong/legacy-shadow-capture-evidence@v1',
profile: 'edge',
startup,
capture: {
schema: 'qinglong/legacy-shadow-capture-report@v1',
profile: 'edge',
assessment: 'captured',
configuredOriginCount: 1,
window: {
basis: 'process_local_legacy_admission',
startInclusiveMs: start,
endExclusiveMs: end,
},
totals: { admitted: 8, captured: 8, failed: 0, pending: 0 },
byOrigin: [{ origin: 'manual' }],
capturePermille: 1_000,
},
qualification: { passed: true },
};
const terminal = {
schema: 'qinglong/legacy-shadow-terminal-difference-report@v1',
profile: 'edge',
observedAtMs: NOW - 3_000,
window: { startInclusiveMs: start, endExclusiveMs: end, closed: true },
coverage: {
direction: 'shadow_to_legacy',
cohort: 'legacy_owned_shadow_runs',
legacyWithoutShadow: 'not_measured',
},
assessment: 'matched',
scanned: 8,
remaining: false,
evidenceComplete: true,
counts: { matched: 8 },
byOrigin: [{ origin: 'manual', scanned: 8 }],
terminalAgreementPermille: 1_000,
fullyComparablePermille: 1_000,
};
const resource = {
schemaVersion: 1,
fixture: 'qinglong/legacy-shadow-resource-rollback-evidence@v1',
profile: 'edge',
workload: { mode: 'full', runtime: 'compiled_backend' },
rollback: {
performed: true,
legacyContinued: true,
shadowWritesStopped: true,
databaseIntegrity: 'ok',
},
qualification: { passed: true, violations: [] },
};
return createLegacyShadowPrimaryGateReceipt({
profile: 'edge',
generatedAtMs: NOW - 2_000,
capture,
terminal,
resource,
});
}
async function writeGateReceipt(sourcePath, value = gateReceipt()) {
const bytes = Buffer.from(`${JSON.stringify(value)}\n`);
await fs.writeFile(
path.join(path.dirname(sourcePath), 'primary-gate.json'),
bytes,
{ mode: 0o600 },
);
return crypto.createHash('sha256').update(bytes).digest('hex');
}
function enabledManifest(receiptSha256) {
return {
schemaVersion: 2,
revision: 'manual-primary-canary-1',
enabled: true,
approvedBy: 'operator:admin',
approvedAtMs: NOW - 1_000,
expiresAtMs: NOW + 60_000,
rollbackPlanRef: 'docs/runbooks/disable-primary.md',
primaryGate: {
schema: 'qinglong/legacy-shadow-primary-gate-reference@v1',
origin: 'manual',
receiptFile: 'primary-gate.json',
receiptSha256,
},
rollout: {
defaultMode: 'off',
origins: { manual: 'primary' },
@@ -69,7 +161,8 @@ test('fails closed when the rollout file is absent', async () => {
test('loads an approved manifest and audits only bounded metadata', async () => {
const sourcePath = await fixturePath();
const raw = JSON.stringify(enabledManifest());
const receiptSha256 = await writeGateReceipt(sourcePath);
const raw = JSON.stringify(enabledManifest(receiptSha256));
await fs.writeFile(sourcePath, raw);
const result = await loadRuntimeRolloutManifest(sourcePath, {
@@ -79,10 +172,52 @@ test('loads an approved manifest and audits only bounded metadata', async () =>
assert.equal(result.status, 'accepted');
assert.equal(result.policy.modeFor('manual'), 'primary');
assert.equal(result.audit.revision, 'manual-primary-canary-1');
assert.equal(result.primaryGateReceipt.assessment, 'eligible');
assert.match(result.audit.sourceSha256, /^[a-f0-9]{64}$/);
assert.doesNotMatch(JSON.stringify(result.audit), /operator:admin|rollback/);
});
test('rejects a missing, tampered or ineligible Primary gate receipt', async () => {
const missingPath = await fixturePath('missing-gate-manifest.json');
await fs.writeFile(
missingPath,
JSON.stringify(enabledManifest('a'.repeat(64))),
);
const missing = await loadRuntimeRolloutManifest(missingPath, {
clock: { now: () => NOW },
});
assert.equal(missing.status, 'rejected');
assert.equal(missing.audit.reasonCode, 'PRIMARY_GATE_READ_FAILED');
const tamperedPath = await fixturePath('tampered-gate-manifest.json');
const tampered = gateReceipt();
tampered.sources.terminal.counts.matched = 7;
const tamperedDigest = await writeGateReceipt(tamperedPath, tampered);
await fs.writeFile(
tamperedPath,
JSON.stringify(enabledManifest(tamperedDigest)),
);
const tamperedResult = await loadRuntimeRolloutManifest(tamperedPath, {
clock: { now: () => NOW },
});
assert.equal(tamperedResult.status, 'rejected');
assert.equal(tamperedResult.audit.reasonCode, 'PRIMARY_GATE_INVALID');
const invalidPath = await fixturePath('invalid-gate-manifest.json');
const ineligible = {
...gateReceipt(),
assessment: 'ineligible',
violations: ['terminal_not_matched'],
};
const digest = await writeGateReceipt(invalidPath, ineligible);
await fs.writeFile(invalidPath, JSON.stringify(enabledManifest(digest)));
const invalid = await loadRuntimeRolloutManifest(invalidPath, {
clock: { now: () => NOW },
});
assert.equal(invalid.status, 'rejected');
assert.equal(invalid.audit.reasonCode, 'PRIMARY_GATE_INVALID');
});
test('rejects malformed and oversized files without exposing their contents', async () => {
const malformedPath = await fixturePath('malformed.json');
await fs.writeFile(malformedPath, '{"approvedBy":"secret"');