mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:00:24 +08:00
1403 lines
42 KiB
JavaScript
1403 lines
42 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
'use strict';
|
|
|
|
const crypto = require('node:crypto');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const { DatabaseSync } = require('node:sqlite');
|
|
|
|
const {
|
|
canonicalDigest,
|
|
collectObservedPlatform,
|
|
mountForPath,
|
|
parseMountTable,
|
|
} = require('./ql3-physical-edge-evidence.cjs');
|
|
|
|
const MIB = 1024 * 1024;
|
|
const MAX_INPUT_BYTES = 256 * 1024;
|
|
const MAX_TREE_ENTRIES = 512;
|
|
const SECTOR_BYTES = 512;
|
|
const MANIFEST_KEYS = Object.freeze([
|
|
'databasePayloadBytes',
|
|
'deviceId',
|
|
'evidenceClass',
|
|
'expectedArchitecture',
|
|
'expectedFilesystem',
|
|
'maximumPrepareWriteAmplificationPermille',
|
|
'maximumResumeWriteAmplificationPermille',
|
|
'profile',
|
|
'schemaVersion',
|
|
'snapshotCount',
|
|
]);
|
|
const SESSION_KEYS = Object.freeze([
|
|
'barrier',
|
|
'collection',
|
|
'dataPath',
|
|
'deploymentRoot',
|
|
'evidenceClass',
|
|
'manifestDigest',
|
|
'prepareMeasurement',
|
|
'preparedAt',
|
|
'schemaVersion',
|
|
'sessionId',
|
|
'sha256',
|
|
'uid',
|
|
]);
|
|
const STORAGE_IDENTITY_KEYS = Object.freeze([
|
|
'architecture',
|
|
'blockDevice',
|
|
'bootId',
|
|
'filesystem',
|
|
'mountOptions',
|
|
'mountPath',
|
|
'platform',
|
|
'sectorsWritten',
|
|
]);
|
|
const TREE_KEYS = Object.freeze([
|
|
'allocatedBytes',
|
|
'directories',
|
|
'entries',
|
|
'logicalBytes',
|
|
'regularFiles',
|
|
]);
|
|
const SNAPSHOT_KEYS = Object.freeze([
|
|
'bytes',
|
|
'contractVersion',
|
|
'pageCount',
|
|
'pageSize',
|
|
'sha256',
|
|
]);
|
|
const REPORT_KEYS = Object.freeze([
|
|
'evidenceClass',
|
|
'generatedAt',
|
|
'manifest',
|
|
'measurements',
|
|
'observed',
|
|
'outcomes',
|
|
'qualification',
|
|
'schemaVersion',
|
|
'session',
|
|
'sha256',
|
|
'supported',
|
|
]);
|
|
const REPORT_SESSION_KEYS = Object.freeze([
|
|
'preparedAt',
|
|
'sessionDigest',
|
|
'sessionId',
|
|
'targetRolloutId',
|
|
'targetSnapshot',
|
|
]);
|
|
const REPORT_OBSERVED_KEYS = Object.freeze(['after', 'afterStart', 'before']);
|
|
const REPORT_MEASUREMENT_KEYS = Object.freeze([
|
|
'barrierAllocatedBytes',
|
|
'collectedSnapshotBytes',
|
|
'finalAllocatedBytes',
|
|
'logicalSnapshotBytes',
|
|
'prepareDeviceBytesWritten',
|
|
'prepareWriteAmplificationPermille',
|
|
'reclaimedAllocatedBytes',
|
|
'resumeDeviceBytesWritten',
|
|
'resumeWriteAmplificationPermille',
|
|
]);
|
|
const REPORT_OUTCOME_KEYS = Object.freeze([
|
|
'commitStatus',
|
|
'replayStatus',
|
|
'retainedSnapshots',
|
|
'sqliteIntegrity',
|
|
'stageRemoved',
|
|
'tombstonePresent',
|
|
]);
|
|
const REPORT_QUALIFICATION_KEYS = Object.freeze([
|
|
'doesNotProve',
|
|
'measures',
|
|
'passed',
|
|
'violations',
|
|
]);
|
|
const MEASURES = Object.freeze([
|
|
'production_collection_prepare',
|
|
'durable_collection_rename_stage',
|
|
'boot_identity_change',
|
|
'production_collection_commit_recovery',
|
|
'exact_collection_commit_replay',
|
|
'sqlite_integrity_after_reboot',
|
|
'per_boot_partition_write_sectors',
|
|
'allocated_snapshot_space_reclamation',
|
|
]);
|
|
const EXCLUSIONS = Object.freeze([
|
|
'abrupt_power_interruption_provenance',
|
|
'main_deployment_filesystem',
|
|
'whole_device_flash_translation_layer_write_amplification',
|
|
'mtd_ubi_device_write_amplification',
|
|
'long_term_flash_endurance',
|
|
'compose_restore_replacement_power_loss',
|
|
'standalone_or_cluster_profile',
|
|
]);
|
|
const UUID_V4_PATTERN =
|
|
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
const SHA256_PATTERN = /^[0-9a-f]{64}$/;
|
|
|
|
class QingLong3PhysicalComposeStorageEvidenceError extends Error {
|
|
constructor(message) {
|
|
super(
|
|
`QingLong 3.0 physical Edge Compose storage evidence failed: ${message}`,
|
|
);
|
|
this.name = 'QingLong3PhysicalComposeStorageEvidenceError';
|
|
}
|
|
}
|
|
|
|
function exactKeys(value, expected, label) {
|
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
throw new QingLong3PhysicalComposeStorageEvidenceError(
|
|
`${label} must be an object`,
|
|
);
|
|
}
|
|
if (JSON.stringify(Object.keys(value).sort()) !== JSON.stringify(expected)) {
|
|
throw new QingLong3PhysicalComposeStorageEvidenceError(
|
|
`${label} keys must be exactly ${expected.join(', ')}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
function hasExactKeys(value, expected) {
|
|
return (
|
|
value !== null &&
|
|
typeof value === 'object' &&
|
|
!Array.isArray(value) &&
|
|
JSON.stringify(Object.keys(value).sort()) === JSON.stringify(expected)
|
|
);
|
|
}
|
|
|
|
function normalizeComposeStorageManifest(value) {
|
|
exactKeys(value, MANIFEST_KEYS, 'manifest');
|
|
if (
|
|
value.schemaVersion !== 1 ||
|
|
value.evidenceClass !== 'physical_edge_compose_storage_candidate' ||
|
|
value.profile !== 'edge'
|
|
) {
|
|
throw new QingLong3PhysicalComposeStorageEvidenceError(
|
|
'manifest identity is invalid',
|
|
);
|
|
}
|
|
if (
|
|
typeof value.deviceId !== 'string' ||
|
|
!/^[a-z0-9][a-z0-9._-]{2,63}$/.test(value.deviceId)
|
|
) {
|
|
throw new QingLong3PhysicalComposeStorageEvidenceError(
|
|
'manifest deviceId is invalid',
|
|
);
|
|
}
|
|
if (!['x64', 'arm64', 'arm'].includes(value.expectedArchitecture)) {
|
|
throw new QingLong3PhysicalComposeStorageEvidenceError(
|
|
'manifest expectedArchitecture is invalid',
|
|
);
|
|
}
|
|
if (
|
|
typeof value.expectedFilesystem !== 'string' ||
|
|
!/^[a-z0-9][a-z0-9._-]{1,31}$/.test(value.expectedFilesystem)
|
|
) {
|
|
throw new QingLong3PhysicalComposeStorageEvidenceError(
|
|
'manifest expectedFilesystem is invalid',
|
|
);
|
|
}
|
|
if (value.snapshotCount !== 3) {
|
|
throw new QingLong3PhysicalComposeStorageEvidenceError(
|
|
'manifest snapshotCount must be 3',
|
|
);
|
|
}
|
|
if (
|
|
!Number.isSafeInteger(value.databasePayloadBytes) ||
|
|
value.databasePayloadBytes < MIB ||
|
|
value.databasePayloadBytes > 64 * MIB
|
|
) {
|
|
throw new QingLong3PhysicalComposeStorageEvidenceError(
|
|
'manifest databasePayloadBytes must be between 1 and 64 MiB',
|
|
);
|
|
}
|
|
for (const name of [
|
|
'maximumPrepareWriteAmplificationPermille',
|
|
'maximumResumeWriteAmplificationPermille',
|
|
]) {
|
|
if (
|
|
!Number.isSafeInteger(value[name]) ||
|
|
value[name] < 1000 ||
|
|
value[name] > 1_000_000
|
|
) {
|
|
throw new QingLong3PhysicalComposeStorageEvidenceError(
|
|
`manifest ${name} must be between 1000 and 1000000`,
|
|
);
|
|
}
|
|
}
|
|
return Object.freeze({
|
|
schemaVersion: 1,
|
|
evidenceClass: 'physical_edge_compose_storage_candidate',
|
|
profile: 'edge',
|
|
deviceId: value.deviceId,
|
|
expectedArchitecture: value.expectedArchitecture,
|
|
expectedFilesystem: value.expectedFilesystem,
|
|
snapshotCount: 3,
|
|
databasePayloadBytes: value.databasePayloadBytes,
|
|
maximumPrepareWriteAmplificationPermille:
|
|
value.maximumPrepareWriteAmplificationPermille,
|
|
maximumResumeWriteAmplificationPermille:
|
|
value.maximumResumeWriteAmplificationPermille,
|
|
});
|
|
}
|
|
|
|
function parseArguments(argv) {
|
|
const options = { json: false };
|
|
let separatorSeen = false;
|
|
let phase;
|
|
for (const argument of argv) {
|
|
if (argument === '--' && !separatorSeen) {
|
|
separatorSeen = true;
|
|
continue;
|
|
}
|
|
if (argument === '--json') {
|
|
options.json = true;
|
|
continue;
|
|
}
|
|
if (!phase && (argument === 'prepare' || argument === 'resume')) {
|
|
phase = argument;
|
|
continue;
|
|
}
|
|
const separator = argument.indexOf('=');
|
|
if (separator < 1) {
|
|
throw new QingLong3PhysicalComposeStorageEvidenceError(
|
|
`unsupported argument ${argument}`,
|
|
);
|
|
}
|
|
const name = argument.slice(0, separator);
|
|
const value = argument.slice(separator + 1);
|
|
if (name === '--manifest') options.manifestPath = value;
|
|
else if (name === '--data-path') options.dataPath = value;
|
|
else if (name === '--session') options.sessionPath = value;
|
|
else if (name === '--output') options.outputPath = value;
|
|
else {
|
|
throw new QingLong3PhysicalComposeStorageEvidenceError(
|
|
`unsupported argument ${argument}`,
|
|
);
|
|
}
|
|
}
|
|
if (!phase) {
|
|
throw new QingLong3PhysicalComposeStorageEvidenceError(
|
|
'phase must be prepare or resume',
|
|
);
|
|
}
|
|
for (const name of ['manifestPath', 'sessionPath']) {
|
|
if (!path.isAbsolute(options[name] ?? '')) {
|
|
throw new QingLong3PhysicalComposeStorageEvidenceError(
|
|
`${name} must be absolute`,
|
|
);
|
|
}
|
|
}
|
|
if (
|
|
(phase === 'prepare' && !path.isAbsolute(options.dataPath ?? '')) ||
|
|
(phase === 'resume' && options.dataPath !== undefined)
|
|
) {
|
|
throw new QingLong3PhysicalComposeStorageEvidenceError(
|
|
'dataPath is required only for prepare and must be absolute',
|
|
);
|
|
}
|
|
if (
|
|
(phase === 'resume' && !path.isAbsolute(options.outputPath ?? '')) ||
|
|
(phase === 'prepare' && options.outputPath !== undefined)
|
|
) {
|
|
throw new QingLong3PhysicalComposeStorageEvidenceError(
|
|
'outputPath is required only for resume and must be absolute',
|
|
);
|
|
}
|
|
return Object.freeze({ phase, ...options });
|
|
}
|
|
|
|
function readBoundedJson(filePath, label) {
|
|
let stat;
|
|
try {
|
|
stat = fs.lstatSync(filePath);
|
|
} catch (error) {
|
|
throw new QingLong3PhysicalComposeStorageEvidenceError(
|
|
`${label} is unavailable: ${error.message}`,
|
|
);
|
|
}
|
|
const uid = process.geteuid?.();
|
|
if (
|
|
!Number.isSafeInteger(uid) ||
|
|
!stat.isFile() ||
|
|
stat.isSymbolicLink() ||
|
|
stat.uid !== uid ||
|
|
(stat.mode & 0o777) !== 0o600 ||
|
|
stat.nlink !== 1 ||
|
|
stat.size < 2 ||
|
|
stat.size > MAX_INPUT_BYTES
|
|
) {
|
|
throw new QingLong3PhysicalComposeStorageEvidenceError(
|
|
`${label} must be a bounded current-user 0600 single-link file`,
|
|
);
|
|
}
|
|
try {
|
|
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
} catch (error) {
|
|
throw new QingLong3PhysicalComposeStorageEvidenceError(
|
|
`${label} is invalid: ${error.message}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
function readManifest(filePath) {
|
|
return normalizeComposeStorageManifest(readBoundedJson(filePath, 'manifest'));
|
|
}
|
|
|
|
function assertPrivateDirectory(directory, label) {
|
|
const stat = fs.lstatSync(directory);
|
|
const uid = process.geteuid?.();
|
|
if (
|
|
!Number.isSafeInteger(uid) ||
|
|
!stat.isDirectory() ||
|
|
stat.isSymbolicLink() ||
|
|
stat.uid !== uid ||
|
|
(stat.mode & 0o777) !== 0o700 ||
|
|
fs.realpathSync(directory) !== directory
|
|
) {
|
|
throw new QingLong3PhysicalComposeStorageEvidenceError(
|
|
`${label} must be a canonical current-user 0700 directory`,
|
|
);
|
|
}
|
|
}
|
|
|
|
function assertChildPath(parent, candidate, label) {
|
|
const normalized = path.normalize(candidate);
|
|
if (
|
|
normalized !== candidate ||
|
|
candidate === parent ||
|
|
!candidate.startsWith(`${parent}${path.sep}`)
|
|
) {
|
|
throw new QingLong3PhysicalComposeStorageEvidenceError(
|
|
`${label} must remain inside dataPath`,
|
|
);
|
|
}
|
|
}
|
|
|
|
function writePrivateNoReplace(filePath, contents) {
|
|
const parent = fs.realpathSync(path.dirname(filePath));
|
|
assertPrivateDirectory(parent, 'output parent');
|
|
const resolved = path.join(parent, path.basename(filePath));
|
|
if (resolved !== filePath) {
|
|
throw new QingLong3PhysicalComposeStorageEvidenceError(
|
|
'output path must be canonical',
|
|
);
|
|
}
|
|
const descriptor = fs.openSync(filePath, 'wx', 0o600);
|
|
try {
|
|
fs.writeFileSync(descriptor, contents, 'utf8');
|
|
fs.fsyncSync(descriptor);
|
|
} finally {
|
|
fs.closeSync(descriptor);
|
|
}
|
|
const directoryDescriptor = fs.openSync(parent, 'r');
|
|
try {
|
|
fs.fsyncSync(directoryDescriptor);
|
|
} finally {
|
|
fs.closeSync(directoryDescriptor);
|
|
}
|
|
}
|
|
|
|
function syncDirectory(directory) {
|
|
const descriptor = fs.openSync(directory, 'r');
|
|
try {
|
|
fs.fsyncSync(descriptor);
|
|
} finally {
|
|
fs.closeSync(descriptor);
|
|
}
|
|
}
|
|
|
|
function collectTreeStorage(root) {
|
|
let entries = 0;
|
|
let regularFiles = 0;
|
|
let directories = 0;
|
|
let logicalBytes = 0;
|
|
let allocatedBytes = 0;
|
|
const visit = (target) => {
|
|
entries += 1;
|
|
if (entries > MAX_TREE_ENTRIES) {
|
|
throw new QingLong3PhysicalComposeStorageEvidenceError(
|
|
'deployment tree exceeded 512 entries',
|
|
);
|
|
}
|
|
const stat = fs.lstatSync(target);
|
|
if (stat.isSymbolicLink()) {
|
|
throw new QingLong3PhysicalComposeStorageEvidenceError(
|
|
'deployment tree contains a symlink',
|
|
);
|
|
}
|
|
if (stat.isDirectory()) {
|
|
directories += 1;
|
|
for (const entry of fs.readdirSync(target).sort()) {
|
|
visit(path.join(target, entry));
|
|
}
|
|
return;
|
|
}
|
|
if (!stat.isFile()) {
|
|
throw new QingLong3PhysicalComposeStorageEvidenceError(
|
|
'deployment tree contains a non-regular entry',
|
|
);
|
|
}
|
|
regularFiles += 1;
|
|
logicalBytes += stat.size;
|
|
allocatedBytes += stat.blocks * SECTOR_BYTES;
|
|
};
|
|
visit(root);
|
|
return Object.freeze({
|
|
entries,
|
|
regularFiles,
|
|
directories,
|
|
logicalBytes,
|
|
allocatedBytes,
|
|
});
|
|
}
|
|
|
|
function parseSectorsWritten(raw) {
|
|
const fields = raw.trim().split(/\s+/);
|
|
const value = Number(fields[6]);
|
|
if (fields.length < 11 || !Number.isSafeInteger(value) || value < 0) {
|
|
throw new QingLong3PhysicalComposeStorageEvidenceError(
|
|
'block device statistics are invalid',
|
|
);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function collectStorageIdentity(dataPath) {
|
|
if (process.platform !== 'linux') {
|
|
throw new QingLong3PhysicalComposeStorageEvidenceError(
|
|
'collector requires Linux',
|
|
);
|
|
}
|
|
const observed = collectObservedPlatform(dataPath);
|
|
const mounts = parseMountTable(fs.readFileSync('/proc/mounts', 'utf8'));
|
|
const mount = mountForPath(mounts, dataPath);
|
|
if (!mount || mount.path !== dataPath) {
|
|
throw new QingLong3PhysicalComposeStorageEvidenceError(
|
|
'dataPath must be a dedicated mount point',
|
|
);
|
|
}
|
|
if (!mount.source.startsWith('/dev/')) {
|
|
throw new QingLong3PhysicalComposeStorageEvidenceError(
|
|
'dataPath must use a directly observable block device',
|
|
);
|
|
}
|
|
const blockDevice = fs.realpathSync(mount.source);
|
|
const blockName = path.basename(blockDevice);
|
|
const statPath = path.join('/sys/class/block', blockName, 'stat');
|
|
const sectorsWritten = parseSectorsWritten(fs.readFileSync(statPath, 'utf8'));
|
|
return Object.freeze({
|
|
platform: observed.platform,
|
|
architecture: observed.architecture,
|
|
bootId: observed.bootId,
|
|
mountPath: mount.path,
|
|
filesystem: mount.filesystem,
|
|
mountOptions: mount.options,
|
|
blockDevice,
|
|
sectorsWritten,
|
|
});
|
|
}
|
|
|
|
function validateStorageIdentity(manifest, identity, dataPath) {
|
|
const violations = [];
|
|
if (identity.platform !== 'linux') violations.push('platform must be Linux');
|
|
if (identity.architecture !== manifest.expectedArchitecture) {
|
|
violations.push('architecture did not match manifest');
|
|
}
|
|
if (identity.mountPath !== dataPath) {
|
|
violations.push('dataPath was not the dedicated mount point');
|
|
}
|
|
if (identity.filesystem !== manifest.expectedFilesystem) {
|
|
violations.push('filesystem did not match manifest');
|
|
}
|
|
if (!identity.mountOptions.includes('rw')) {
|
|
violations.push('dataPath was not mounted read-write');
|
|
}
|
|
if (!identity.blockDevice.startsWith('/dev/')) {
|
|
violations.push('block device identity was invalid');
|
|
}
|
|
return Object.freeze(violations);
|
|
}
|
|
|
|
function snapshotReceipt(evidence) {
|
|
return Object.freeze({
|
|
contractVersion: evidence.contractVersion,
|
|
sha256: evidence.sha256,
|
|
bytes: evidence.bytes,
|
|
pageCount: evidence.pageCount,
|
|
pageSize: evidence.pageSize,
|
|
});
|
|
}
|
|
|
|
function isSnapshot(value) {
|
|
return (
|
|
hasExactKeys(value, SNAPSHOT_KEYS) &&
|
|
value.contractVersion === 44 &&
|
|
typeof value.sha256 === 'string' &&
|
|
SHA256_PATTERN.test(value.sha256) &&
|
|
Number.isSafeInteger(value.bytes) &&
|
|
value.bytes > 0 &&
|
|
Number.isSafeInteger(value.pageCount) &&
|
|
value.pageCount > 0 &&
|
|
Number.isSafeInteger(value.pageSize) &&
|
|
value.pageSize >= 512 &&
|
|
value.pageSize <= 65_536
|
|
);
|
|
}
|
|
|
|
function rolloutReceipt(command, evidence, recordedAtMs) {
|
|
const result = Object.freeze({
|
|
schemaVersion: 1,
|
|
operation: 'local.deployment.compose.apply',
|
|
status: 'active',
|
|
attemptedGeneration: command.request.expectedGeneration,
|
|
activeGeneration: command.request.expectedGeneration,
|
|
profile: 'edge',
|
|
health: Object.freeze({ event: 'active' }),
|
|
service: Object.freeze({ kind: 'compose' }),
|
|
});
|
|
return Object.freeze({
|
|
schema: 'qinglong/local-compose-rollout-receipt@v2',
|
|
commandDigest: canonicalDigest(command),
|
|
rolloutId: command.request.rolloutId,
|
|
attemptedGeneration: command.request.expectedGeneration,
|
|
recordedAtMs,
|
|
healthEventDigest: canonicalDigest({
|
|
generation: command.request.expectedGeneration,
|
|
status: 'active',
|
|
}),
|
|
sqlite: Object.freeze({
|
|
contractVersion: evidence.contractVersion,
|
|
writeContractVersion: evidence.writeContractVersion,
|
|
writeObservation: 'unchanged',
|
|
backup: Object.freeze({
|
|
sha256: evidence.sha256,
|
|
bytes: evidence.bytes,
|
|
pageCount: evidence.pageCount,
|
|
pageSize: evidence.pageSize,
|
|
}),
|
|
}),
|
|
result,
|
|
});
|
|
}
|
|
|
|
function inflateDatabase(databasePath, payloadBytes) {
|
|
const database = new DatabaseSync(databasePath);
|
|
try {
|
|
database.exec(
|
|
'PRAGMA journal_mode = DELETE; CREATE TABLE ql3_physical_storage_payload (id INTEGER PRIMARY KEY, payload BLOB NOT NULL);',
|
|
);
|
|
const insert = database.prepare(
|
|
'INSERT INTO ql3_physical_storage_payload(payload) VALUES (zeroblob(?))',
|
|
);
|
|
const chunkBytes = Math.min(MIB, payloadBytes);
|
|
let remaining = payloadBytes;
|
|
while (remaining > 0) {
|
|
insert.run(Math.min(chunkBytes, remaining));
|
|
remaining -= Math.min(chunkBytes, remaining);
|
|
}
|
|
database.exec('DROP TABLE ql3_physical_storage_payload;');
|
|
} finally {
|
|
database.close();
|
|
}
|
|
}
|
|
|
|
function requireBuiltProducts() {
|
|
const root = path.resolve(__dirname, '..');
|
|
try {
|
|
return Object.freeze({
|
|
root,
|
|
deployment: require(path.join(
|
|
root,
|
|
'packages/ql3-local-owner-cli/dist/deployment/localDeployment.js',
|
|
)),
|
|
rolloutSafety: require(path.join(
|
|
root,
|
|
'packages/ql3-local-sqlite/dist/readiness/rolloutSafety.js',
|
|
)),
|
|
});
|
|
} catch (error) {
|
|
throw new QingLong3PhysicalComposeStorageEvidenceError(
|
|
`QingLong 3.0 packages must be built first: ${error.message}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
function sessionContents(session) {
|
|
return `${JSON.stringify(session, null, 2)}\n`;
|
|
}
|
|
|
|
function normalizeSession(value) {
|
|
exactKeys(value, SESSION_KEYS, 'session');
|
|
const { sha256, ...body } = value;
|
|
const prepareCommand = value.collection?.prepareCommand;
|
|
const commitCommand = value.collection?.commitCommand;
|
|
const target = value.collection?.target;
|
|
const expectedDeploymentRoot = path.join(
|
|
value.dataPath ?? '',
|
|
`.ql3-compose-storage-${value.sessionId ?? ''}`,
|
|
);
|
|
const expectedTargetPath = path.join(
|
|
expectedDeploymentRoot,
|
|
'service',
|
|
'rollout-backups',
|
|
`${target?.rolloutId ?? ''}.sqlite`,
|
|
);
|
|
const expectedStagePath = path.join(
|
|
path.dirname(expectedTargetPath),
|
|
`.${path.basename(expectedTargetPath)}.ql3-collection-stage`,
|
|
);
|
|
if (
|
|
value.schemaVersion !== 1 ||
|
|
value.evidenceClass !== 'physical_edge_compose_storage_session' ||
|
|
!UUID_V4_PATTERN.test(value.sessionId ?? '') ||
|
|
!SHA256_PATTERN.test(value.manifestDigest ?? '') ||
|
|
!Number.isSafeInteger(value.uid) ||
|
|
value.uid < 0 ||
|
|
typeof value.preparedAt !== 'string' ||
|
|
!Number.isFinite(Date.parse(value.preparedAt)) ||
|
|
!path.isAbsolute(value.dataPath ?? '') ||
|
|
!path.isAbsolute(value.deploymentRoot ?? '') ||
|
|
!hasExactKeys(value.barrier?.identity, STORAGE_IDENTITY_KEYS) ||
|
|
!hasExactKeys(value.barrier?.tree, TREE_KEYS) ||
|
|
!Number.isSafeInteger(value.barrier?.sectorsWritten) ||
|
|
!hasExactKeys(value.prepareMeasurement, [
|
|
'deviceBytesWritten',
|
|
'logicalSnapshotBytes',
|
|
'writeAmplificationPermille',
|
|
]) ||
|
|
!hasExactKeys(value.collection, [
|
|
'commitCommand',
|
|
'prepareCommand',
|
|
'snapshotCount',
|
|
'stagePath',
|
|
'target',
|
|
'targetPath',
|
|
]) ||
|
|
value.collection.snapshotCount !== 3 ||
|
|
!hasExactKeys(value.collection.target, ['rolloutId', 'snapshot']) ||
|
|
!UUID_V4_PATTERN.test(value.collection.target.rolloutId ?? '') ||
|
|
!isSnapshot(value.collection.target.snapshot) ||
|
|
value.dataPath !== path.normalize(value.dataPath) ||
|
|
value.deploymentRoot !== expectedDeploymentRoot ||
|
|
value.barrier.identity.mountPath !== value.dataPath ||
|
|
value.barrier.sectorsWritten !== value.barrier.identity.sectorsWritten ||
|
|
TREE_KEYS.some(
|
|
(key) =>
|
|
!Number.isSafeInteger(value.barrier.tree[key]) ||
|
|
value.barrier.tree[key] < 0,
|
|
) ||
|
|
!Number.isSafeInteger(value.prepareMeasurement.logicalSnapshotBytes) ||
|
|
value.prepareMeasurement.logicalSnapshotBytes < 1 ||
|
|
!Number.isSafeInteger(value.prepareMeasurement.deviceBytesWritten) ||
|
|
value.prepareMeasurement.deviceBytesWritten < 0 ||
|
|
amplificationPermille(
|
|
value.prepareMeasurement.deviceBytesWritten,
|
|
value.prepareMeasurement.logicalSnapshotBytes,
|
|
) !== value.prepareMeasurement.writeAmplificationPermille ||
|
|
!hasExactKeys(prepareCommand, [
|
|
'operation',
|
|
'options',
|
|
'request',
|
|
'schemaVersion',
|
|
]) ||
|
|
prepareCommand.schemaVersion !== 1 ||
|
|
prepareCommand.operation !==
|
|
'local.deployment.compose.evidence-collection.prepare' ||
|
|
!hasExactKeys(prepareCommand.options, [
|
|
'allowRootService',
|
|
'deploymentRoot',
|
|
]) ||
|
|
prepareCommand.options.deploymentRoot !== value.deploymentRoot ||
|
|
prepareCommand.options.allowRootService !== (value.uid === 0) ||
|
|
!hasExactKeys(prepareCommand.request, [
|
|
'collectionId',
|
|
'expectedGeneration',
|
|
'preparedAtMs',
|
|
'restoreIds',
|
|
'rolloutIds',
|
|
]) ||
|
|
!UUID_V4_PATTERN.test(prepareCommand.request.collectionId ?? '') ||
|
|
prepareCommand.request.expectedGeneration !== 4 ||
|
|
!Number.isSafeInteger(prepareCommand.request.preparedAtMs) ||
|
|
JSON.stringify(prepareCommand.request.rolloutIds) !==
|
|
JSON.stringify([target.rolloutId]) ||
|
|
JSON.stringify(prepareCommand.request.restoreIds) !== '[]' ||
|
|
!hasExactKeys(commitCommand, [
|
|
'operation',
|
|
'options',
|
|
'request',
|
|
'schemaVersion',
|
|
]) ||
|
|
commitCommand.schemaVersion !== 1 ||
|
|
commitCommand.operation !==
|
|
'local.deployment.compose.evidence-collection.commit' ||
|
|
JSON.stringify(commitCommand.options) !==
|
|
JSON.stringify(prepareCommand.options) ||
|
|
!hasExactKeys(commitCommand.request, [
|
|
'collectionId',
|
|
'committedAtMs',
|
|
'expectedGeneration',
|
|
]) ||
|
|
commitCommand.request.collectionId !==
|
|
prepareCommand.request.collectionId ||
|
|
commitCommand.request.expectedGeneration !==
|
|
prepareCommand.request.expectedGeneration ||
|
|
!Number.isSafeInteger(commitCommand.request.committedAtMs) ||
|
|
commitCommand.request.committedAtMs < prepareCommand.request.preparedAtMs ||
|
|
value.collection.targetPath !== expectedTargetPath ||
|
|
value.collection.stagePath !== expectedStagePath ||
|
|
value.sha256 !== canonicalDigest(body)
|
|
) {
|
|
throw new QingLong3PhysicalComposeStorageEvidenceError(
|
|
'session is invalid or drifted',
|
|
);
|
|
}
|
|
return Object.freeze(value);
|
|
}
|
|
|
|
async function preparePhase(options, manifest) {
|
|
const dataPath = fs.realpathSync(options.dataPath);
|
|
assertPrivateDirectory(dataPath, 'dataPath');
|
|
assertChildPath(dataPath, options.sessionPath, 'sessionPath');
|
|
if (fs.existsSync(options.sessionPath)) {
|
|
throw new QingLong3PhysicalComposeStorageEvidenceError(
|
|
'sessionPath already exists',
|
|
);
|
|
}
|
|
const initialIdentity = collectStorageIdentity(dataPath);
|
|
const preflightViolations = validateStorageIdentity(
|
|
manifest,
|
|
initialIdentity,
|
|
dataPath,
|
|
);
|
|
if (preflightViolations.length > 0) {
|
|
throw new QingLong3PhysicalComposeStorageEvidenceError(
|
|
`device preflight rejected: ${preflightViolations.join('; ')}`,
|
|
);
|
|
}
|
|
const products = requireBuiltProducts();
|
|
const sessionId = crypto.randomUUID();
|
|
const deploymentRoot = path.join(
|
|
dataPath,
|
|
`.ql3-compose-storage-${sessionId}`,
|
|
);
|
|
const uid = process.getuid?.();
|
|
if (!Number.isSafeInteger(uid)) {
|
|
throw new QingLong3PhysicalComposeStorageEvidenceError(
|
|
'a stable POSIX UID is required',
|
|
);
|
|
}
|
|
const timestamp = Date.now();
|
|
const image = `physical.invalid/qinglong3-local@sha256:${'a'.repeat(64)}`;
|
|
await products.deployment.prepareLocalDeployment({
|
|
schemaVersion: 1,
|
|
operation: 'local.deployment.prepare',
|
|
options: {
|
|
deploymentRoot,
|
|
profile: 'edge',
|
|
instanceId: `physical-compose-${sessionId.slice(0, 8)}`,
|
|
busyTimeoutMs: 100,
|
|
service: {
|
|
kind: 'compose',
|
|
image,
|
|
allowRootService: uid === 0,
|
|
},
|
|
},
|
|
request: {
|
|
ownerPepperKeyId: 'owner-v1',
|
|
registerMutationId: crypto.randomUUID(),
|
|
activateMutationId: crypto.randomUUID(),
|
|
registeredAtMs: timestamp,
|
|
activatedAtMs: timestamp + 1,
|
|
},
|
|
});
|
|
const databasePath = path.join(deploymentRoot, 'qinglong3.sqlite');
|
|
inflateDatabase(databasePath, manifest.databasePayloadBytes);
|
|
const rolloutIds = [];
|
|
const snapshots = [];
|
|
for (let generation = 2; generation <= 4; generation += 1) {
|
|
await products.deployment.switchLocalDeploymentComposeRevision({
|
|
schemaVersion: 1,
|
|
operation: 'local.deployment.compose.upgrade',
|
|
options: {
|
|
deploymentRoot,
|
|
allowRootService: uid === 0,
|
|
},
|
|
request: {
|
|
expectedGeneration: generation - 1,
|
|
image,
|
|
mutationId: crypto.randomUUID(),
|
|
changedAtMs: timestamp + generation,
|
|
},
|
|
});
|
|
const rolloutId = crypto.randomUUID();
|
|
const command = {
|
|
schemaVersion: 1,
|
|
operation: 'local.deployment.compose.apply',
|
|
options: {
|
|
deploymentRoot,
|
|
dockerExecutable: process.execPath,
|
|
dockerSocketPath: path.join(deploymentRoot, 'physical-docker.sock'),
|
|
allowRootService: uid === 0,
|
|
},
|
|
request: {
|
|
expectedGeneration: generation,
|
|
rolloutId,
|
|
startedAtMs: timestamp + generation * 10,
|
|
failureRollbackMutationId: crypto.randomUUID(),
|
|
failureRollbackChangedAtMs: timestamp + generation * 10 + 1,
|
|
},
|
|
};
|
|
const backupPath = path.join(
|
|
deploymentRoot,
|
|
'service',
|
|
'rollout-backups',
|
|
`${rolloutId}.sqlite`,
|
|
);
|
|
const evidence =
|
|
await products.rolloutSafety.createLocalSqliteRolloutBackup({
|
|
databasePath,
|
|
backupPath,
|
|
profile: 'edge',
|
|
});
|
|
const receiptPath = path.join(
|
|
deploymentRoot,
|
|
'service',
|
|
'rollouts',
|
|
`${rolloutId}.json`,
|
|
);
|
|
writePrivateNoReplace(
|
|
receiptPath,
|
|
`${JSON.stringify(
|
|
rolloutReceipt(
|
|
command,
|
|
evidence,
|
|
command.request.failureRollbackChangedAtMs,
|
|
),
|
|
null,
|
|
2,
|
|
)}\n`,
|
|
);
|
|
rolloutIds.push(rolloutId);
|
|
snapshots.push(evidence);
|
|
}
|
|
const collectionId = crypto.randomUUID();
|
|
const prepareCommand = {
|
|
schemaVersion: 1,
|
|
operation: 'local.deployment.compose.evidence-collection.prepare',
|
|
options: {
|
|
deploymentRoot,
|
|
allowRootService: uid === 0,
|
|
},
|
|
request: {
|
|
expectedGeneration: 4,
|
|
collectionId,
|
|
rolloutIds: [rolloutIds[0]],
|
|
restoreIds: [],
|
|
preparedAtMs: timestamp + 100,
|
|
},
|
|
};
|
|
const commitCommand = {
|
|
schemaVersion: 1,
|
|
operation: 'local.deployment.compose.evidence-collection.commit',
|
|
options: prepareCommand.options,
|
|
request: {
|
|
expectedGeneration: 4,
|
|
collectionId,
|
|
committedAtMs: timestamp + 101,
|
|
},
|
|
};
|
|
const prepared =
|
|
await products.deployment.collectLocalDeploymentComposeEvidence(
|
|
prepareCommand,
|
|
);
|
|
if (
|
|
prepared.status !== 'prepared' ||
|
|
prepared.collected.rolloutBackups !== 1
|
|
) {
|
|
throw new QingLong3PhysicalComposeStorageEvidenceError(
|
|
'production collection prepare did not publish',
|
|
);
|
|
}
|
|
const targetPath = path.join(
|
|
deploymentRoot,
|
|
'service',
|
|
'rollout-backups',
|
|
`${rolloutIds[0]}.sqlite`,
|
|
);
|
|
const stagePath = path.join(
|
|
path.dirname(targetPath),
|
|
`.${path.basename(targetPath)}.ql3-collection-stage`,
|
|
);
|
|
fs.renameSync(targetPath, stagePath);
|
|
syncDirectory(path.dirname(targetPath));
|
|
const barrierIdentity = collectStorageIdentity(dataPath);
|
|
if (
|
|
barrierIdentity.bootId !== initialIdentity.bootId ||
|
|
barrierIdentity.blockDevice !== initialIdentity.blockDevice ||
|
|
barrierIdentity.sectorsWritten < initialIdentity.sectorsWritten
|
|
) {
|
|
throw new QingLong3PhysicalComposeStorageEvidenceError(
|
|
'prepare block device identity changed',
|
|
);
|
|
}
|
|
const logicalSnapshotBytes = snapshots.reduce(
|
|
(total, snapshot) => total + snapshot.bytes,
|
|
0,
|
|
);
|
|
const deviceBytesWritten =
|
|
(barrierIdentity.sectorsWritten - initialIdentity.sectorsWritten) *
|
|
SECTOR_BYTES;
|
|
const writeAmplificationPermille = Math.ceil(
|
|
(deviceBytesWritten * 1000) / logicalSnapshotBytes,
|
|
);
|
|
const body = {
|
|
schemaVersion: 1,
|
|
evidenceClass: 'physical_edge_compose_storage_session',
|
|
sessionId,
|
|
manifestDigest: canonicalDigest(manifest),
|
|
uid,
|
|
preparedAt: new Date().toISOString(),
|
|
dataPath,
|
|
deploymentRoot,
|
|
barrier: {
|
|
identity: barrierIdentity,
|
|
sectorsWritten: barrierIdentity.sectorsWritten,
|
|
tree: collectTreeStorage(deploymentRoot),
|
|
},
|
|
prepareMeasurement: {
|
|
logicalSnapshotBytes,
|
|
deviceBytesWritten,
|
|
writeAmplificationPermille,
|
|
},
|
|
collection: {
|
|
snapshotCount: snapshots.length,
|
|
prepareCommand,
|
|
commitCommand,
|
|
target: {
|
|
rolloutId: rolloutIds[0],
|
|
snapshot: snapshotReceipt(snapshots[0]),
|
|
},
|
|
targetPath,
|
|
stagePath,
|
|
},
|
|
};
|
|
const session = Object.freeze({ ...body, sha256: canonicalDigest(body) });
|
|
writePrivateNoReplace(options.sessionPath, sessionContents(session));
|
|
process.stdout.write(
|
|
`${JSON.stringify({
|
|
schemaVersion: 1,
|
|
status: 'awaiting_external_reboot',
|
|
sessionId,
|
|
preparedGeneration: 4,
|
|
collectionStageDurable: true,
|
|
supported: false,
|
|
})}\n`,
|
|
);
|
|
}
|
|
|
|
function amplificationPermille(deviceBytes, logicalBytes) {
|
|
if (
|
|
!Number.isSafeInteger(deviceBytes) ||
|
|
deviceBytes < 0 ||
|
|
!Number.isSafeInteger(logicalBytes) ||
|
|
logicalBytes < 1
|
|
) {
|
|
return null;
|
|
}
|
|
return Math.ceil((deviceBytes * 1000) / logicalBytes);
|
|
}
|
|
|
|
function evaluateComposeStorageEvidence({
|
|
manifest,
|
|
dataPath,
|
|
targetSnapshot,
|
|
observed,
|
|
measurements,
|
|
outcomes,
|
|
}) {
|
|
const violations = [];
|
|
if (
|
|
!hasExactKeys(observed, REPORT_OBSERVED_KEYS) ||
|
|
!hasExactKeys(observed.before, STORAGE_IDENTITY_KEYS) ||
|
|
!hasExactKeys(observed.afterStart, STORAGE_IDENTITY_KEYS) ||
|
|
!hasExactKeys(observed.after, STORAGE_IDENTITY_KEYS)
|
|
) {
|
|
return Object.freeze(['observed storage identity shape is invalid']);
|
|
}
|
|
if (
|
|
observed.before.bootId === observed.after.bootId ||
|
|
observed.afterStart.bootId !== observed.after.bootId
|
|
) {
|
|
violations.push('boot identity did not prove a restart boundary');
|
|
}
|
|
for (const identity of [
|
|
observed.before,
|
|
observed.afterStart,
|
|
observed.after,
|
|
]) {
|
|
violations.push(...validateStorageIdentity(manifest, identity, dataPath));
|
|
if (
|
|
typeof identity.bootId !== 'string' ||
|
|
identity.bootId.length < 8 ||
|
|
!Number.isSafeInteger(identity.sectorsWritten) ||
|
|
identity.sectorsWritten < 0
|
|
) {
|
|
violations.push('storage identity counters are invalid');
|
|
}
|
|
}
|
|
if (
|
|
observed.before.blockDevice !== observed.afterStart.blockDevice ||
|
|
observed.afterStart.blockDevice !== observed.after.blockDevice ||
|
|
observed.after.sectorsWritten < observed.afterStart.sectorsWritten
|
|
) {
|
|
violations.push('block device identity or counter drifted');
|
|
}
|
|
if (
|
|
!hasExactKeys(measurements, REPORT_MEASUREMENT_KEYS) ||
|
|
REPORT_MEASUREMENT_KEYS.some(
|
|
(key) =>
|
|
!Number.isSafeInteger(measurements[key]) || measurements[key] < 0,
|
|
)
|
|
) {
|
|
violations.push('storage measurements are invalid');
|
|
}
|
|
if (
|
|
!hasExactKeys(outcomes, REPORT_OUTCOME_KEYS) ||
|
|
outcomes.commitStatus !== 'collected' ||
|
|
outcomes.replayStatus !== 'existing' ||
|
|
outcomes.sqliteIntegrity !== 'ok' ||
|
|
outcomes.stageRemoved !== true ||
|
|
outcomes.tombstonePresent !== true ||
|
|
outcomes.retainedSnapshots !== 2
|
|
) {
|
|
violations.push('production collection recovery outcome is invalid');
|
|
}
|
|
if (
|
|
Number.isSafeInteger(measurements.prepareWriteAmplificationPermille) &&
|
|
Number.isSafeInteger(measurements.resumeWriteAmplificationPermille) &&
|
|
(measurements.prepareWriteAmplificationPermille >
|
|
manifest.maximumPrepareWriteAmplificationPermille ||
|
|
measurements.resumeWriteAmplificationPermille >
|
|
manifest.maximumResumeWriteAmplificationPermille)
|
|
) {
|
|
violations.push('device write amplification exceeded manifest budget');
|
|
}
|
|
if (
|
|
!isSnapshot(targetSnapshot) ||
|
|
measurements.reclaimedAllocatedBytes < 1 ||
|
|
measurements.collectedSnapshotBytes !== targetSnapshot?.bytes ||
|
|
measurements.barrierAllocatedBytes - measurements.finalAllocatedBytes !==
|
|
measurements.reclaimedAllocatedBytes
|
|
) {
|
|
violations.push('snapshot allocation was not reclaimed');
|
|
}
|
|
if (
|
|
amplificationPermille(
|
|
measurements.prepareDeviceBytesWritten,
|
|
measurements.logicalSnapshotBytes,
|
|
) !== measurements.prepareWriteAmplificationPermille ||
|
|
amplificationPermille(
|
|
measurements.resumeDeviceBytesWritten,
|
|
measurements.collectedSnapshotBytes,
|
|
) !== measurements.resumeWriteAmplificationPermille
|
|
) {
|
|
violations.push('write amplification calculation drifted');
|
|
}
|
|
return Object.freeze(violations);
|
|
}
|
|
|
|
function buildComposeStorageReport({
|
|
manifest,
|
|
session,
|
|
observed,
|
|
measurements,
|
|
outcomes,
|
|
generatedAt,
|
|
}) {
|
|
const violations = evaluateComposeStorageEvidence({
|
|
manifest,
|
|
dataPath: session.dataPath,
|
|
targetSnapshot: session.collection.target.snapshot,
|
|
observed,
|
|
measurements,
|
|
outcomes,
|
|
});
|
|
const body = {
|
|
schemaVersion: 1,
|
|
evidenceClass: 'physical_edge_compose_storage_candidate',
|
|
supported: false,
|
|
generatedAt,
|
|
manifest,
|
|
session: {
|
|
sessionId: session.sessionId,
|
|
sessionDigest: session.sha256,
|
|
preparedAt: session.preparedAt,
|
|
targetRolloutId: session.collection.target.rolloutId,
|
|
targetSnapshot: session.collection.target.snapshot,
|
|
},
|
|
observed,
|
|
measurements,
|
|
outcomes,
|
|
qualification: {
|
|
passed: violations.length === 0,
|
|
violations,
|
|
measures: MEASURES,
|
|
doesNotProve: EXCLUSIONS,
|
|
},
|
|
};
|
|
return Object.freeze({ ...body, sha256: canonicalDigest(body) });
|
|
}
|
|
|
|
function validateComposeStorageReport(report, manifest, currentObserved) {
|
|
const violations = [];
|
|
if (!hasExactKeys(report, REPORT_KEYS)) {
|
|
return Object.freeze(['Compose storage report shape is invalid']);
|
|
}
|
|
const { sha256, ...body } = report;
|
|
if (!SHA256_PATTERN.test(sha256 ?? '') || canonicalDigest(body) !== sha256) {
|
|
violations.push('Compose storage report SHA-256 is invalid');
|
|
}
|
|
if (
|
|
report.schemaVersion !== 1 ||
|
|
report.evidenceClass !== 'physical_edge_compose_storage_candidate' ||
|
|
report.supported !== false ||
|
|
!Number.isFinite(Date.parse(report.generatedAt ?? ''))
|
|
) {
|
|
violations.push('Compose storage report identity is invalid');
|
|
}
|
|
if (
|
|
!hasExactKeys(report.session, REPORT_SESSION_KEYS) ||
|
|
!UUID_V4_PATTERN.test(report.session?.sessionId ?? '') ||
|
|
!SHA256_PATTERN.test(report.session?.sessionDigest ?? '') ||
|
|
!UUID_V4_PATTERN.test(report.session?.targetRolloutId ?? '') ||
|
|
!isSnapshot(report.session?.targetSnapshot) ||
|
|
!Number.isFinite(Date.parse(report.session?.preparedAt ?? '')) ||
|
|
!hasExactKeys(report.qualification, REPORT_QUALIFICATION_KEYS)
|
|
) {
|
|
violations.push('Compose storage report evidence shape is invalid');
|
|
}
|
|
if (
|
|
canonicalDigest(report.manifest) !== canonicalDigest(manifest) ||
|
|
report.manifest.deviceId !== manifest.deviceId
|
|
) {
|
|
violations.push('Compose storage report manifest did not match');
|
|
}
|
|
if (
|
|
report.observed?.after?.bootId !== currentObserved.bootId ||
|
|
report.observed?.after?.architecture !== currentObserved.architecture ||
|
|
report.observed?.after?.filesystem !== currentObserved.dataFilesystem ||
|
|
report.observed?.after?.mountPath !== currentObserved.dataPath
|
|
) {
|
|
violations.push('Compose storage report current device did not match');
|
|
}
|
|
const recomputed = evaluateComposeStorageEvidence({
|
|
manifest,
|
|
dataPath: currentObserved.dataPath,
|
|
targetSnapshot: report.session?.targetSnapshot,
|
|
observed: report.observed,
|
|
measurements: report.measurements,
|
|
outcomes: report.outcomes,
|
|
});
|
|
if (
|
|
report.qualification?.passed !== (recomputed.length === 0) ||
|
|
!Array.isArray(report.qualification?.violations) ||
|
|
JSON.stringify(report.qualification.violations) !==
|
|
JSON.stringify(recomputed) ||
|
|
JSON.stringify(report.qualification?.measures) !==
|
|
JSON.stringify(MEASURES) ||
|
|
JSON.stringify(report.qualification?.doesNotProve) !==
|
|
JSON.stringify(EXCLUSIONS) ||
|
|
recomputed.length > 0
|
|
) {
|
|
violations.push('Compose storage report qualification was widened');
|
|
}
|
|
return Object.freeze(violations);
|
|
}
|
|
|
|
async function resumePhase(options, manifest) {
|
|
const session = normalizeSession(
|
|
readBoundedJson(options.sessionPath, 'session'),
|
|
);
|
|
if (session.manifestDigest !== canonicalDigest(manifest)) {
|
|
throw new QingLong3PhysicalComposeStorageEvidenceError(
|
|
'session manifest digest did not match',
|
|
);
|
|
}
|
|
if (session.uid !== process.getuid?.()) {
|
|
throw new QingLong3PhysicalComposeStorageEvidenceError(
|
|
'session POSIX identity changed',
|
|
);
|
|
}
|
|
const dataPath = fs.realpathSync(session.dataPath);
|
|
if (dataPath !== session.dataPath) {
|
|
throw new QingLong3PhysicalComposeStorageEvidenceError(
|
|
'session dataPath is no longer canonical',
|
|
);
|
|
}
|
|
assertPrivateDirectory(dataPath, 'dataPath');
|
|
assertChildPath(session.dataPath, options.sessionPath, 'sessionPath');
|
|
assertChildPath(session.dataPath, options.outputPath, 'outputPath');
|
|
const deploymentRoot = fs.realpathSync(session.deploymentRoot);
|
|
if (deploymentRoot !== session.deploymentRoot) {
|
|
throw new QingLong3PhysicalComposeStorageEvidenceError(
|
|
'session deploymentRoot is no longer canonical',
|
|
);
|
|
}
|
|
assertPrivateDirectory(deploymentRoot, 'deploymentRoot');
|
|
const preparedIdentityViolations = validateStorageIdentity(
|
|
manifest,
|
|
session.barrier.identity,
|
|
session.dataPath,
|
|
);
|
|
if (preparedIdentityViolations.length > 0) {
|
|
throw new QingLong3PhysicalComposeStorageEvidenceError(
|
|
`prepared storage identity rejected: ${preparedIdentityViolations.join(
|
|
'; ',
|
|
)}`,
|
|
);
|
|
}
|
|
const products = requireBuiltProducts();
|
|
const afterStart = collectStorageIdentity(session.dataPath);
|
|
const resumeIdentityViolations = validateStorageIdentity(
|
|
manifest,
|
|
afterStart,
|
|
session.dataPath,
|
|
);
|
|
if (resumeIdentityViolations.length > 0) {
|
|
throw new QingLong3PhysicalComposeStorageEvidenceError(
|
|
`resume storage identity rejected: ${resumeIdentityViolations.join(
|
|
'; ',
|
|
)}`,
|
|
);
|
|
}
|
|
if (afterStart.bootId === session.barrier.identity.bootId) {
|
|
throw new QingLong3PhysicalComposeStorageEvidenceError(
|
|
'resume requires a different Linux boot identity',
|
|
);
|
|
}
|
|
if (
|
|
afterStart.blockDevice !== session.barrier.identity.blockDevice ||
|
|
afterStart.filesystem !== session.barrier.identity.filesystem
|
|
) {
|
|
throw new QingLong3PhysicalComposeStorageEvidenceError(
|
|
'storage identity changed across reboot',
|
|
);
|
|
}
|
|
const committed =
|
|
await products.deployment.collectLocalDeploymentComposeEvidence(
|
|
session.collection.commitCommand,
|
|
);
|
|
const replay =
|
|
await products.deployment.collectLocalDeploymentComposeEvidence(
|
|
session.collection.commitCommand,
|
|
);
|
|
const databaseEvidence =
|
|
await products.rolloutSafety.inspectLocalSqliteSnapshot({
|
|
databasePath: path.join(session.deploymentRoot, 'qinglong3.sqlite'),
|
|
profile: 'edge',
|
|
});
|
|
const after = collectStorageIdentity(session.dataPath);
|
|
const afterTree = collectTreeStorage(session.deploymentRoot);
|
|
const retainedSnapshots = fs
|
|
.readdirSync(
|
|
path.join(session.deploymentRoot, 'service', 'rollout-backups'),
|
|
)
|
|
.filter((entry) => entry.endsWith('.sqlite')).length;
|
|
const tombstonePath = path.join(
|
|
session.deploymentRoot,
|
|
'service',
|
|
'collected-evidence',
|
|
'rollout-backups',
|
|
`${session.collection.target.rolloutId}.json`,
|
|
);
|
|
const resumeDeviceBytesWritten =
|
|
(after.sectorsWritten - afterStart.sectorsWritten) * SECTOR_BYTES;
|
|
const reclaimedAllocatedBytes =
|
|
session.barrier.tree.allocatedBytes - afterTree.allocatedBytes;
|
|
const measurements = Object.freeze({
|
|
logicalSnapshotBytes: session.prepareMeasurement.logicalSnapshotBytes,
|
|
collectedSnapshotBytes: session.collection.target.snapshot.bytes,
|
|
prepareDeviceBytesWritten: session.prepareMeasurement.deviceBytesWritten,
|
|
resumeDeviceBytesWritten,
|
|
prepareWriteAmplificationPermille:
|
|
session.prepareMeasurement.writeAmplificationPermille,
|
|
resumeWriteAmplificationPermille: amplificationPermille(
|
|
resumeDeviceBytesWritten,
|
|
session.collection.target.snapshot.bytes,
|
|
),
|
|
barrierAllocatedBytes: session.barrier.tree.allocatedBytes,
|
|
finalAllocatedBytes: afterTree.allocatedBytes,
|
|
reclaimedAllocatedBytes,
|
|
});
|
|
const outcomes = Object.freeze({
|
|
commitStatus: committed.status,
|
|
replayStatus: replay.status,
|
|
sqliteIntegrity: databaseEvidence.contractVersion === 44 ? 'ok' : 'invalid',
|
|
stageRemoved: !fs.existsSync(session.collection.stagePath),
|
|
tombstonePresent: fs.existsSync(tombstonePath),
|
|
retainedSnapshots,
|
|
});
|
|
const report = buildComposeStorageReport({
|
|
manifest,
|
|
session,
|
|
observed: {
|
|
before: session.barrier.identity,
|
|
afterStart,
|
|
after,
|
|
},
|
|
measurements,
|
|
outcomes,
|
|
generatedAt: new Date().toISOString(),
|
|
});
|
|
writePrivateNoReplace(
|
|
options.outputPath,
|
|
`${JSON.stringify(report, null, options.json ? 0 : 2)}\n`,
|
|
);
|
|
process.stdout.write(
|
|
`${JSON.stringify(report, null, options.json ? 0 : 2)}\n`,
|
|
);
|
|
if (!report.qualification.passed) process.exitCode = 1;
|
|
}
|
|
|
|
async function main() {
|
|
if (Number(process.versions.node.split('.')[0]) < 24) {
|
|
throw new QingLong3PhysicalComposeStorageEvidenceError(
|
|
'Node.js 24 or newer is required',
|
|
);
|
|
}
|
|
const options = parseArguments(process.argv.slice(2));
|
|
const manifest = readManifest(options.manifestPath);
|
|
if (options.phase === 'prepare') {
|
|
await preparePhase(options, manifest);
|
|
} else {
|
|
await resumePhase(options, manifest);
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
QingLong3PhysicalComposeStorageEvidenceError,
|
|
amplificationPermille,
|
|
buildComposeStorageReport,
|
|
evaluateComposeStorageEvidence,
|
|
normalizeComposeStorageManifest,
|
|
normalizeSession,
|
|
parseArguments,
|
|
parseSectorsWritten,
|
|
validateComposeStorageReport,
|
|
validateStorageIdentity,
|
|
};
|
|
|
|
if (require.main === module) {
|
|
main().catch((error) => {
|
|
process.stderr.write(
|
|
`${error instanceof Error ? error.message : String(error)}\n`,
|
|
);
|
|
process.exitCode = 1;
|
|
});
|
|
}
|