mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:00:24 +08:00
1585 lines
47 KiB
JavaScript
1585 lines
47 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
'use strict';
|
|
|
|
const crypto = require('node:crypto');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const { spawn } = require('node:child_process');
|
|
|
|
const {
|
|
canonicalDigest,
|
|
collectObservedPlatform,
|
|
} = require('./ql3-physical-edge-evidence.cjs');
|
|
const {
|
|
parseProcIo,
|
|
parseProcStat,
|
|
parseProcStatus,
|
|
} = require('./ql3-physical-edge-idle-sampler.cjs');
|
|
const { readReleaseIdentity } = require('./lib/ql3-release-identity.cjs');
|
|
|
|
const MIB = 1024 * 1024;
|
|
const QL3_VERSION = readReleaseIdentity(path.resolve(__dirname, '..')).version;
|
|
const MAX_INPUT_BYTES = 256 * 1024;
|
|
const MAX_OUTPUT_BYTES = 64 * 1024;
|
|
const MAX_ARTIFACT_FILES = 768;
|
|
const MAX_ARTIFACT_DIRECTORIES = 256;
|
|
const MAX_ARTIFACT_BYTES = 8 * MIB;
|
|
const MAX_EVENTS = 64;
|
|
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}$/;
|
|
const REQUIRED_PACKAGES = Object.freeze([
|
|
'@qinglong/local-admin',
|
|
'@qinglong/local-application',
|
|
'@qinglong/local-command-file',
|
|
'@qinglong/local-execution',
|
|
'@qinglong/local-process',
|
|
'@qinglong/local-secret',
|
|
'@qinglong/local-sqlite',
|
|
'@qinglong/runtime-core',
|
|
'croner',
|
|
'semver',
|
|
]);
|
|
const MANIFEST_KEYS = Object.freeze([
|
|
'deviceId',
|
|
'evidenceClass',
|
|
'expectedArchitecture',
|
|
'expectedArtifactBytes',
|
|
'expectedArtifactFiles',
|
|
'expectedArtifactSha256',
|
|
'expectedFilesystem',
|
|
'expectedNodeSha256',
|
|
'maximumBootAgeMs',
|
|
'maximumFirstActiveMs',
|
|
'maximumSampledRssBytes',
|
|
'profile',
|
|
'sampleIntervalMs',
|
|
'schemaVersion',
|
|
]);
|
|
const ARTIFACT_KEYS = Object.freeze([
|
|
'artifactBytes',
|
|
'artifactFiles',
|
|
'artifactMetadataSha256',
|
|
'artifactSha256',
|
|
'entrypointSha256',
|
|
'packages',
|
|
]);
|
|
const ENVIRONMENT_KEYS = Object.freeze([
|
|
'architecture',
|
|
'bootAgeMs',
|
|
'bootId',
|
|
'dataFilesystem',
|
|
'nodeExecutable',
|
|
'nodeSha256',
|
|
'nodeVersion',
|
|
'platform',
|
|
]);
|
|
const PATH_KEYS = Object.freeze([
|
|
'applicationConfig',
|
|
'applicationEntrypoint',
|
|
'artifactRoot',
|
|
'dataPath',
|
|
'deploymentRoot',
|
|
]);
|
|
const SESSION_KEYS = Object.freeze([
|
|
'artifact',
|
|
'environment',
|
|
'evidenceClass',
|
|
'manifestDigest',
|
|
'paths',
|
|
'preparedAt',
|
|
'schemaVersion',
|
|
'sessionId',
|
|
'sha256',
|
|
'uid',
|
|
]);
|
|
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',
|
|
]);
|
|
const REPORT_OBSERVED_KEYS = Object.freeze(['after', 'artifact', 'before']);
|
|
const OBSERVED_BOOT_KEYS = Object.freeze([
|
|
'architecture',
|
|
'bootAgeMs',
|
|
'bootId',
|
|
'dataFilesystem',
|
|
'nodeExecutable',
|
|
'nodeSha256',
|
|
'nodeVersion',
|
|
'platform',
|
|
]);
|
|
const MEASUREMENT_KEYS = Object.freeze([
|
|
'eventCount',
|
|
'firstActiveMs',
|
|
'maximumSampledRssBytes',
|
|
'processReadBytes',
|
|
'processWriteBytes',
|
|
'sampleCount',
|
|
]);
|
|
const OUTCOME_KEYS = Object.freeze([
|
|
'activeEventCount',
|
|
'aiStatus',
|
|
'exitCode',
|
|
'exitSignal',
|
|
'gracefulStop',
|
|
'sqliteContractVersion',
|
|
'stderrBytes',
|
|
]);
|
|
const QUALIFICATION_KEYS = Object.freeze([
|
|
'doesNotProve',
|
|
'measures',
|
|
'passed',
|
|
'violations',
|
|
]);
|
|
const MEASURES = Object.freeze([
|
|
'different_boot_identity',
|
|
'bounded_post_reboot_boot_age',
|
|
'exact_ai_excluded_native_release_closure',
|
|
'production_application_process_to_active_event',
|
|
'sampled_process_rss_until_active',
|
|
'sampled_process_io_after_first_proc_sample',
|
|
'graceful_sigterm_stop',
|
|
'sqlite_v35_after_stop',
|
|
]);
|
|
const EXCLUSIONS = Object.freeze([
|
|
'cold_node_runtime_or_dynamic_linker_cache',
|
|
'cold_artifact_directory_or_inode_cache',
|
|
'exclusive_application_page_cache_provenance',
|
|
'process_metrics_before_first_proc_sample',
|
|
'rss_peaks_between_samples',
|
|
'firmware_power_on_to_service_manager_start',
|
|
'systemd_or_openrc_supervisor_latency',
|
|
'compose_or_container_runtime_start',
|
|
'abrupt_power_loss_recovery',
|
|
'whole_device_cpu_wakeups',
|
|
'whole_device_flash_write_amplification',
|
|
'release_archive_signature_or_attestation',
|
|
'standalone_or_cluster_profile',
|
|
]);
|
|
|
|
class QingLong3PhysicalApplicationStartEvidenceError extends Error {
|
|
constructor(message) {
|
|
super(
|
|
`QingLong 3.0 physical Edge application start evidence failed: ${message}`,
|
|
);
|
|
this.name = 'QingLong3PhysicalApplicationStartEvidenceError';
|
|
}
|
|
}
|
|
|
|
function exactKeys(value, expected, label) {
|
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
`${label} must be an object`,
|
|
);
|
|
}
|
|
if (JSON.stringify(Object.keys(value).sort()) !== JSON.stringify(expected)) {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
`${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 normalizeApplicationStartManifest(value) {
|
|
exactKeys(value, MANIFEST_KEYS, 'manifest');
|
|
if (
|
|
value.schemaVersion !== 1 ||
|
|
value.evidenceClass !== 'physical_edge_application_start_candidate' ||
|
|
value.profile !== 'edge'
|
|
) {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
'manifest identity is invalid',
|
|
);
|
|
}
|
|
if (
|
|
typeof value.deviceId !== 'string' ||
|
|
!/^[a-z0-9][a-z0-9._-]{2,63}$/.test(value.deviceId)
|
|
) {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
'manifest deviceId is invalid',
|
|
);
|
|
}
|
|
if (!['x64', 'arm64', 'arm'].includes(value.expectedArchitecture)) {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
'manifest expectedArchitecture is invalid',
|
|
);
|
|
}
|
|
if (
|
|
typeof value.expectedFilesystem !== 'string' ||
|
|
!/^[a-z0-9][a-z0-9._-]{1,31}$/.test(value.expectedFilesystem)
|
|
) {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
'manifest expectedFilesystem is invalid',
|
|
);
|
|
}
|
|
if (
|
|
!SHA256_PATTERN.test(value.expectedArtifactSha256 ?? '') ||
|
|
!Number.isSafeInteger(value.expectedArtifactFiles) ||
|
|
value.expectedArtifactFiles < 1 ||
|
|
value.expectedArtifactFiles > MAX_ARTIFACT_FILES ||
|
|
!Number.isSafeInteger(value.expectedArtifactBytes) ||
|
|
value.expectedArtifactBytes < 1 ||
|
|
value.expectedArtifactBytes > MAX_ARTIFACT_BYTES ||
|
|
!SHA256_PATTERN.test(value.expectedNodeSha256 ?? '')
|
|
) {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
'manifest release artifact identity is invalid',
|
|
);
|
|
}
|
|
if (
|
|
!Number.isSafeInteger(value.maximumBootAgeMs) ||
|
|
value.maximumBootAgeMs < 10_000 ||
|
|
value.maximumBootAgeMs > 600_000 ||
|
|
!Number.isSafeInteger(value.maximumFirstActiveMs) ||
|
|
value.maximumFirstActiveMs < 100 ||
|
|
value.maximumFirstActiveMs > 120_000 ||
|
|
!Number.isSafeInteger(value.maximumSampledRssBytes) ||
|
|
value.maximumSampledRssBytes < 16 * MIB ||
|
|
value.maximumSampledRssBytes > 512 * MIB ||
|
|
!Number.isSafeInteger(value.sampleIntervalMs) ||
|
|
value.sampleIntervalMs < 10 ||
|
|
value.sampleIntervalMs > 1_000
|
|
) {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
'manifest measurement budget is invalid',
|
|
);
|
|
}
|
|
return Object.freeze({ ...value });
|
|
}
|
|
|
|
function parseArguments(argv) {
|
|
const options = { json: false };
|
|
let phase;
|
|
let separatorSeen = false;
|
|
for (const argument of argv) {
|
|
if (argument === '--' && !separatorSeen) {
|
|
separatorSeen = true;
|
|
continue;
|
|
}
|
|
if (argument === '--json') {
|
|
options.json = true;
|
|
continue;
|
|
}
|
|
if (
|
|
!phase &&
|
|
(argument === 'inspect' ||
|
|
argument === 'prepare' ||
|
|
argument === 'resume')
|
|
) {
|
|
phase = argument;
|
|
continue;
|
|
}
|
|
const separator = argument.indexOf('=');
|
|
if (separator < 1) {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
`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 === '--artifact-root') options.artifactRoot = value;
|
|
else if (name === '--session') options.sessionPath = value;
|
|
else if (name === '--output') options.outputPath = value;
|
|
else {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
`unsupported argument ${argument}`,
|
|
);
|
|
}
|
|
}
|
|
if (!phase) {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
'phase must be inspect, prepare or resume',
|
|
);
|
|
}
|
|
const required =
|
|
phase === 'inspect'
|
|
? ['artifactRoot']
|
|
: phase === 'prepare'
|
|
? ['manifestPath', 'dataPath', 'artifactRoot', 'sessionPath']
|
|
: ['manifestPath', 'sessionPath', 'outputPath'];
|
|
for (const name of required) {
|
|
if (!path.isAbsolute(options[name] ?? '')) {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
`${name} must be absolute`,
|
|
);
|
|
}
|
|
}
|
|
const allowed = new Set(['json', ...required]);
|
|
for (const name of Object.keys(options)) {
|
|
if (!allowed.has(name)) {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
`${name} is not accepted for ${phase}`,
|
|
);
|
|
}
|
|
}
|
|
return Object.freeze({ phase, ...options });
|
|
}
|
|
|
|
function readBoundedPrivateJson(filePath, label) {
|
|
let stat;
|
|
try {
|
|
stat = fs.lstatSync(filePath);
|
|
} catch (error) {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
`${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 QingLong3PhysicalApplicationStartEvidenceError(
|
|
`${label} must be a bounded current-user 0600 single-link file`,
|
|
);
|
|
}
|
|
try {
|
|
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
} catch (error) {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
`${label} is invalid: ${error.message}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
function readManifest(filePath) {
|
|
return normalizeApplicationStartManifest(
|
|
readBoundedPrivateJson(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 QingLong3PhysicalApplicationStartEvidenceError(
|
|
`${label} must be a canonical current-user 0700 directory`,
|
|
);
|
|
}
|
|
}
|
|
|
|
function assertChildPath(parent, candidate, label) {
|
|
if (
|
|
path.normalize(candidate) !== candidate ||
|
|
candidate === parent ||
|
|
!candidate.startsWith(`${parent}${path.sep}`)
|
|
) {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
`${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 QingLong3PhysicalApplicationStartEvidenceError(
|
|
'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 fileSha256(filePath) {
|
|
return crypto
|
|
.createHash('sha256')
|
|
.update(fs.readFileSync(filePath))
|
|
.digest('hex');
|
|
}
|
|
|
|
function installedPackages(nodeModulesRoot) {
|
|
const values = [];
|
|
for (const entry of fs.readdirSync(nodeModulesRoot, {
|
|
withFileTypes: true,
|
|
})) {
|
|
if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
|
|
if (!entry.name.startsWith('@')) {
|
|
values.push(entry.name);
|
|
continue;
|
|
}
|
|
for (const scoped of fs.readdirSync(
|
|
path.join(nodeModulesRoot, entry.name),
|
|
{ withFileTypes: true },
|
|
)) {
|
|
if (scoped.isDirectory()) values.push(`${entry.name}/${scoped.name}`);
|
|
}
|
|
}
|
|
return values.sort();
|
|
}
|
|
|
|
function artifactMetadataEntry(artifactRoot, target, kind, stat) {
|
|
return Object.freeze({
|
|
path: path.relative(artifactRoot, target) || '.',
|
|
kind,
|
|
device: stat.dev.toString(),
|
|
inode: stat.ino.toString(),
|
|
bytes: stat.size.toString(),
|
|
mode: Number(stat.mode & 0o777n),
|
|
uid: Number(stat.uid),
|
|
gid: Number(stat.gid),
|
|
links: stat.nlink.toString(),
|
|
modifiedNs: stat.mtimeNs.toString(),
|
|
changedNs: stat.ctimeNs.toString(),
|
|
});
|
|
}
|
|
|
|
function collectArtifactIdentity(artifactRootInput) {
|
|
const artifactRoot = fs.realpathSync(artifactRootInput);
|
|
if (
|
|
artifactRoot !== artifactRootInput ||
|
|
!path.isAbsolute(artifactRoot) ||
|
|
artifactRoot === path.parse(artifactRoot).root
|
|
) {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
'artifactRoot must be a canonical non-root directory',
|
|
);
|
|
}
|
|
const uid = process.geteuid?.();
|
|
if (!Number.isSafeInteger(uid)) {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
'a stable POSIX identity is required',
|
|
);
|
|
}
|
|
const files = [];
|
|
const metadata = [];
|
|
let directories = 0;
|
|
let artifactBytes = 0;
|
|
const visit = (target) => {
|
|
const stat = fs.lstatSync(target, { bigint: true });
|
|
if (
|
|
stat.isSymbolicLink() ||
|
|
(stat.uid !== 0n && stat.uid !== BigInt(uid)) ||
|
|
(stat.mode & 0o022n) !== 0n
|
|
) {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
'release artifact contains an untrusted entry',
|
|
);
|
|
}
|
|
if (stat.isDirectory()) {
|
|
metadata.push(
|
|
artifactMetadataEntry(artifactRoot, target, 'directory', stat),
|
|
);
|
|
directories += 1;
|
|
if (directories > MAX_ARTIFACT_DIRECTORIES) {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
'release artifact directory count exceeded',
|
|
);
|
|
}
|
|
for (const entry of fs.readdirSync(target).sort()) {
|
|
visit(path.join(target, entry));
|
|
}
|
|
return;
|
|
}
|
|
if (!stat.isFile() || stat.nlink !== 1n) {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
'release artifact contains a non-regular or linked entry',
|
|
);
|
|
}
|
|
const fileBytes = Number(stat.size);
|
|
if (!Number.isSafeInteger(fileBytes)) {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
'release artifact file size is invalid',
|
|
);
|
|
}
|
|
artifactBytes += fileBytes;
|
|
if (
|
|
files.length >= MAX_ARTIFACT_FILES ||
|
|
artifactBytes > MAX_ARTIFACT_BYTES
|
|
) {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
'release artifact budget exceeded',
|
|
);
|
|
}
|
|
files.push(
|
|
Object.freeze({
|
|
path: path.relative(artifactRoot, target),
|
|
mode: Number(stat.mode & 0o777n),
|
|
bytes: fileBytes,
|
|
sha256: fileSha256(target),
|
|
}),
|
|
);
|
|
metadata.push(artifactMetadataEntry(artifactRoot, target, 'file', stat));
|
|
};
|
|
visit(artifactRoot);
|
|
const nodeModulesRoot = path.join(artifactRoot, 'node_modules');
|
|
const packages = installedPackages(nodeModulesRoot);
|
|
if (JSON.stringify(packages) !== JSON.stringify(REQUIRED_PACKAGES)) {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
'release artifact package closure is invalid',
|
|
);
|
|
}
|
|
const packageRoot = path.join(
|
|
nodeModulesRoot,
|
|
'@qinglong',
|
|
'local-application',
|
|
);
|
|
const packageManifest = JSON.parse(
|
|
fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'),
|
|
);
|
|
const applicationEntrypoint = path.join(packageRoot, 'dist', 'cli.js');
|
|
if (
|
|
packageManifest.name !== '@qinglong/local-application' ||
|
|
packageManifest.version !== QL3_VERSION ||
|
|
packageManifest.bin?.['ql3-local-application'] !== 'dist/cli.js' ||
|
|
packageManifest.engines?.node !== '>=24.18.0 <25' ||
|
|
fs.realpathSync(applicationEntrypoint) !== applicationEntrypoint
|
|
) {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
'release application entrypoint contract is invalid',
|
|
);
|
|
}
|
|
const artifactSha256 = canonicalDigest({ files });
|
|
const artifactMetadataSha256 = canonicalDigest({ entries: metadata });
|
|
return Object.freeze({
|
|
artifactRoot,
|
|
applicationEntrypoint,
|
|
artifact: Object.freeze({
|
|
artifactSha256,
|
|
artifactMetadataSha256,
|
|
artifactFiles: files.length,
|
|
artifactBytes,
|
|
entrypointSha256: fileSha256(applicationEntrypoint),
|
|
packages: REQUIRED_PACKAGES,
|
|
}),
|
|
});
|
|
}
|
|
|
|
function preflightArtifactMetadata(artifactRootInput, expectedEntrypoint) {
|
|
const artifactRoot = fs.realpathSync(artifactRootInput);
|
|
const uid = process.geteuid?.();
|
|
let files = 0;
|
|
let directories = 0;
|
|
let entrypointFound = false;
|
|
const metadata = [];
|
|
const visit = (target) => {
|
|
const stat = fs.lstatSync(target, { bigint: true });
|
|
if (
|
|
stat.isSymbolicLink() ||
|
|
(stat.uid !== 0n && stat.uid !== BigInt(uid)) ||
|
|
(stat.mode & 0o022n) !== 0n
|
|
) {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
'release artifact metadata is untrusted',
|
|
);
|
|
}
|
|
if (stat.isDirectory()) {
|
|
metadata.push(
|
|
artifactMetadataEntry(artifactRoot, target, 'directory', stat),
|
|
);
|
|
directories += 1;
|
|
if (directories > MAX_ARTIFACT_DIRECTORIES) {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
'release artifact directory count exceeded',
|
|
);
|
|
}
|
|
for (const entry of fs.readdirSync(target).sort()) {
|
|
visit(path.join(target, entry));
|
|
}
|
|
return;
|
|
}
|
|
if (!stat.isFile() || stat.nlink !== 1n) {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
'release artifact metadata contains an unsupported entry',
|
|
);
|
|
}
|
|
files += 1;
|
|
metadata.push(artifactMetadataEntry(artifactRoot, target, 'file', stat));
|
|
if (files > MAX_ARTIFACT_FILES) {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
'release artifact file count exceeded',
|
|
);
|
|
}
|
|
if (target === expectedEntrypoint) entrypointFound = true;
|
|
};
|
|
visit(artifactRoot);
|
|
if (!entrypointFound) {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
'release application entrypoint is unavailable',
|
|
);
|
|
}
|
|
return Object.freeze({
|
|
files,
|
|
directories,
|
|
artifactMetadataSha256: canonicalDigest({ entries: metadata }),
|
|
});
|
|
}
|
|
|
|
function validArtifactSummary(value) {
|
|
return (
|
|
hasExactKeys(value, ARTIFACT_KEYS) &&
|
|
SHA256_PATTERN.test(value.artifactSha256 ?? '') &&
|
|
SHA256_PATTERN.test(value.artifactMetadataSha256 ?? '') &&
|
|
SHA256_PATTERN.test(value.entrypointSha256 ?? '') &&
|
|
Number.isSafeInteger(value.artifactFiles) &&
|
|
value.artifactFiles >= 1 &&
|
|
value.artifactFiles <= MAX_ARTIFACT_FILES &&
|
|
Number.isSafeInteger(value.artifactBytes) &&
|
|
value.artifactBytes >= 1 &&
|
|
value.artifactBytes <= MAX_ARTIFACT_BYTES &&
|
|
JSON.stringify(value.packages) === JSON.stringify(REQUIRED_PACKAGES)
|
|
);
|
|
}
|
|
|
|
function collectNodeIdentity() {
|
|
const nodeExecutable = fs.realpathSync(process.execPath);
|
|
const stat = fs.lstatSync(nodeExecutable);
|
|
const uid = process.geteuid?.();
|
|
if (
|
|
!Number.isSafeInteger(uid) ||
|
|
!stat.isFile() ||
|
|
stat.isSymbolicLink() ||
|
|
(stat.uid !== 0 && stat.uid !== uid) ||
|
|
(stat.mode & 0o022) !== 0 ||
|
|
(stat.mode & 0o111) === 0
|
|
) {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
'Node executable is not trusted',
|
|
);
|
|
}
|
|
return Object.freeze({
|
|
nodeExecutable,
|
|
nodeSha256: fileSha256(nodeExecutable),
|
|
nodeVersion: process.version,
|
|
});
|
|
}
|
|
|
|
function validateArtifactAgainstManifest(manifest, artifact, node) {
|
|
const violations = [];
|
|
if (
|
|
artifact.artifactSha256 !== manifest.expectedArtifactSha256 ||
|
|
artifact.artifactFiles !== manifest.expectedArtifactFiles ||
|
|
artifact.artifactBytes !== manifest.expectedArtifactBytes
|
|
) {
|
|
violations.push('release artifact did not match manifest');
|
|
}
|
|
if (node.nodeSha256 !== manifest.expectedNodeSha256) {
|
|
violations.push('Node executable did not match manifest');
|
|
}
|
|
return Object.freeze(violations);
|
|
}
|
|
|
|
function normalizeSession(value) {
|
|
exactKeys(value, SESSION_KEYS, 'session');
|
|
const { sha256, ...body } = value;
|
|
const expectedDeploymentRoot = path.join(
|
|
value.paths?.dataPath ?? '',
|
|
`.ql3-application-start-${value.sessionId ?? ''}`,
|
|
);
|
|
const expectedConfig = path.join(
|
|
expectedDeploymentRoot,
|
|
'local-application.json',
|
|
);
|
|
const expectedEntrypoint = path.join(
|
|
value.paths?.artifactRoot ?? '',
|
|
'node_modules',
|
|
'@qinglong',
|
|
'local-application',
|
|
'dist',
|
|
'cli.js',
|
|
);
|
|
if (
|
|
value.schemaVersion !== 1 ||
|
|
value.evidenceClass !== 'physical_edge_application_start_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)) ||
|
|
!validArtifactSummary(value.artifact) ||
|
|
!hasExactKeys(value.environment, ENVIRONMENT_KEYS) ||
|
|
value.environment.platform !== 'linux' ||
|
|
typeof value.environment.bootId !== 'string' ||
|
|
value.environment.bootId.length < 8 ||
|
|
!Number.isSafeInteger(value.environment.bootAgeMs) ||
|
|
value.environment.bootAgeMs < 0 ||
|
|
!path.isAbsolute(value.environment.nodeExecutable ?? '') ||
|
|
!SHA256_PATTERN.test(value.environment.nodeSha256 ?? '') ||
|
|
typeof value.environment.nodeVersion !== 'string' ||
|
|
!hasExactKeys(value.paths, PATH_KEYS) ||
|
|
!path.isAbsolute(value.paths.dataPath ?? '') ||
|
|
!path.isAbsolute(value.paths.artifactRoot ?? '') ||
|
|
value.paths.deploymentRoot !== expectedDeploymentRoot ||
|
|
value.paths.applicationConfig !== expectedConfig ||
|
|
value.paths.applicationEntrypoint !== expectedEntrypoint ||
|
|
value.sha256 !== canonicalDigest(body)
|
|
) {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
'session is invalid or drifted',
|
|
);
|
|
}
|
|
return Object.freeze(value);
|
|
}
|
|
|
|
function readBootAgeMs() {
|
|
const raw = fs.readFileSync('/proc/uptime', 'utf8').trim().split(/\s+/)[0];
|
|
const seconds = Number(raw);
|
|
const milliseconds = Math.floor(seconds * 1000);
|
|
if (
|
|
!Number.isFinite(seconds) ||
|
|
seconds < 0 ||
|
|
!Number.isSafeInteger(milliseconds)
|
|
) {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
'Linux boot age is invalid',
|
|
);
|
|
}
|
|
return milliseconds;
|
|
}
|
|
|
|
function bootObservation(dataPath, node, bootAgeMs) {
|
|
const observed = collectObservedPlatform(dataPath);
|
|
return Object.freeze({
|
|
platform: observed.platform,
|
|
architecture: observed.architecture,
|
|
bootId: observed.bootId,
|
|
dataFilesystem: observed.dataFilesystem,
|
|
nodeExecutable: node.nodeExecutable,
|
|
nodeSha256: node.nodeSha256,
|
|
nodeVersion: node.nodeVersion,
|
|
bootAgeMs,
|
|
});
|
|
}
|
|
|
|
function validateBootObservation(manifest, observation, dataPath) {
|
|
const violations = [];
|
|
if (observation.platform !== 'linux')
|
|
violations.push('platform is not Linux');
|
|
if (observation.architecture !== manifest.expectedArchitecture) {
|
|
violations.push('architecture did not match manifest');
|
|
}
|
|
if (observation.dataFilesystem !== manifest.expectedFilesystem) {
|
|
violations.push('filesystem did not match manifest');
|
|
}
|
|
if (
|
|
!path.isAbsolute(dataPath ?? '') ||
|
|
typeof observation.bootId !== 'string' ||
|
|
observation.bootId.length < 8 ||
|
|
!path.isAbsolute(observation.nodeExecutable ?? '') ||
|
|
!SHA256_PATTERN.test(observation.nodeSha256 ?? '') ||
|
|
typeof observation.nodeVersion !== 'string' ||
|
|
!/^v24\.\d+\.\d+$/.test(observation.nodeVersion) ||
|
|
!Number.isSafeInteger(observation.bootAgeMs) ||
|
|
observation.bootAgeMs < 0
|
|
) {
|
|
violations.push('boot observation is invalid');
|
|
}
|
|
return Object.freeze(violations);
|
|
}
|
|
|
|
function requireBuiltProducts() {
|
|
const root = path.resolve(__dirname, '..');
|
|
try {
|
|
return Object.freeze({
|
|
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 QingLong3PhysicalApplicationStartEvidenceError(
|
|
`QingLong 3.0 packages must be built first: ${error.message}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
async function preparePhase(options, manifest) {
|
|
if (process.platform !== 'linux') {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
'prepare requires Linux',
|
|
);
|
|
}
|
|
const dataPath = fs.realpathSync(options.dataPath);
|
|
assertPrivateDirectory(dataPath, 'dataPath');
|
|
assertChildPath(dataPath, options.sessionPath, 'sessionPath');
|
|
if (fs.existsSync(options.sessionPath)) {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
'sessionPath already exists',
|
|
);
|
|
}
|
|
const identity = collectArtifactIdentity(options.artifactRoot);
|
|
const node = collectNodeIdentity();
|
|
const artifactViolations = validateArtifactAgainstManifest(
|
|
manifest,
|
|
identity.artifact,
|
|
node,
|
|
);
|
|
if (artifactViolations.length > 0) {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
artifactViolations.join('; '),
|
|
);
|
|
}
|
|
const before = bootObservation(dataPath, node, readBootAgeMs());
|
|
const bootViolations = validateBootObservation(manifest, before, dataPath);
|
|
if (bootViolations.length > 0) {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
`device preflight rejected: ${bootViolations.join('; ')}`,
|
|
);
|
|
}
|
|
const uid = process.getuid?.();
|
|
if (!Number.isSafeInteger(uid)) {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
'a stable POSIX UID is required',
|
|
);
|
|
}
|
|
const sessionId = crypto.randomUUID();
|
|
const deploymentRoot = path.join(
|
|
dataPath,
|
|
`.ql3-application-start-${sessionId}`,
|
|
);
|
|
const timestamp = Date.now();
|
|
const products = requireBuiltProducts();
|
|
const prepared = await products.deployment.prepareLocalDeployment({
|
|
schemaVersion: 1,
|
|
operation: 'local.deployment.prepare',
|
|
options: {
|
|
deploymentRoot,
|
|
profile: 'edge',
|
|
instanceId: `physical-start-${sessionId.slice(0, 8)}`,
|
|
busyTimeoutMs: 100,
|
|
service: {
|
|
kind: 'openrc',
|
|
nodeExecutable: node.nodeExecutable,
|
|
applicationEntrypoint: identity.applicationEntrypoint,
|
|
allowRootService: uid === 0,
|
|
},
|
|
},
|
|
request: {
|
|
ownerPepperKeyId: 'owner-v1',
|
|
registerMutationId: crypto.randomUUID(),
|
|
activateMutationId: crypto.randomUUID(),
|
|
registeredAtMs: timestamp,
|
|
activatedAtMs: timestamp + 1,
|
|
},
|
|
});
|
|
if (prepared.status !== 'prepared' || prepared.profile !== 'edge') {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
'fresh native Edge deployment was not prepared',
|
|
);
|
|
}
|
|
const body = {
|
|
schemaVersion: 1,
|
|
evidenceClass: 'physical_edge_application_start_session',
|
|
sessionId,
|
|
manifestDigest: canonicalDigest(manifest),
|
|
uid,
|
|
preparedAt: new Date().toISOString(),
|
|
artifact: identity.artifact,
|
|
environment: {
|
|
platform: before.platform,
|
|
architecture: before.architecture,
|
|
bootId: before.bootId,
|
|
bootAgeMs: before.bootAgeMs,
|
|
dataFilesystem: before.dataFilesystem,
|
|
nodeExecutable: node.nodeExecutable,
|
|
nodeSha256: node.nodeSha256,
|
|
nodeVersion: node.nodeVersion,
|
|
},
|
|
paths: {
|
|
dataPath,
|
|
deploymentRoot,
|
|
artifactRoot: identity.artifactRoot,
|
|
applicationEntrypoint: identity.applicationEntrypoint,
|
|
applicationConfig: path.join(deploymentRoot, 'local-application.json'),
|
|
},
|
|
};
|
|
const session = Object.freeze({ ...body, sha256: canonicalDigest(body) });
|
|
writePrivateNoReplace(
|
|
options.sessionPath,
|
|
`${JSON.stringify(session, null, 2)}\n`,
|
|
);
|
|
process.stdout.write(
|
|
`${JSON.stringify({
|
|
schemaVersion: 1,
|
|
status: 'awaiting_external_reboot',
|
|
sessionId,
|
|
profile: 'edge',
|
|
applicationClosureVerified: true,
|
|
supported: false,
|
|
})}\n`,
|
|
);
|
|
}
|
|
|
|
function readProcessSample(processId, expectedExecutable) {
|
|
const root = `/proc/${processId}`;
|
|
const stat = parseProcStat(fs.readFileSync(`${root}/stat`, 'utf8'));
|
|
const status = parseProcStatus(fs.readFileSync(`${root}/status`, 'utf8'));
|
|
const io = parseProcIo(fs.readFileSync(`${root}/io`, 'utf8'));
|
|
const executable = fs.realpathSync(`${root}/exe`);
|
|
if (
|
|
executable !== expectedExecutable ||
|
|
status.uid !== process.getuid?.() ||
|
|
stat.processId !== processId
|
|
) {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
'application process identity drifted',
|
|
);
|
|
}
|
|
return Object.freeze({
|
|
startTicks: stat.startTicks,
|
|
rssBytes: status.rssBytes,
|
|
readBytes: io.readBytes,
|
|
writeBytes: io.writeBytes,
|
|
});
|
|
}
|
|
|
|
function parseEventLines(buffer, events) {
|
|
let remaining = buffer;
|
|
while (remaining.includes('\n')) {
|
|
const index = remaining.indexOf('\n');
|
|
const line = remaining.slice(0, index);
|
|
remaining = remaining.slice(index + 1);
|
|
if (!line) continue;
|
|
if (Buffer.byteLength(line, 'utf8') > 4096) {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
'application event line exceeded',
|
|
);
|
|
}
|
|
let value;
|
|
try {
|
|
value = JSON.parse(line);
|
|
} catch {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
'application emitted non-JSON stdout',
|
|
);
|
|
}
|
|
if (
|
|
!value ||
|
|
typeof value !== 'object' ||
|
|
Array.isArray(value) ||
|
|
value.schemaVersion !== 1 ||
|
|
value.component !== 'qinglong3-local-application' ||
|
|
typeof value.event !== 'string' ||
|
|
value.profile !== 'edge'
|
|
) {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
'application event shape is invalid',
|
|
);
|
|
}
|
|
events.push(Object.freeze(value));
|
|
if (events.length > MAX_EVENTS) {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
'application event count exceeded',
|
|
);
|
|
}
|
|
}
|
|
return remaining;
|
|
}
|
|
|
|
function timeoutPromise(milliseconds, message) {
|
|
return new Promise((_, reject) => {
|
|
const timer = setTimeout(() => {
|
|
reject(new QingLong3PhysicalApplicationStartEvidenceError(message));
|
|
}, milliseconds);
|
|
timer.unref();
|
|
});
|
|
}
|
|
|
|
async function measureApplicationStart(session, manifest) {
|
|
const startNs = process.hrtime.bigint();
|
|
const child = spawn(
|
|
session.environment.nodeExecutable,
|
|
[
|
|
session.paths.applicationEntrypoint,
|
|
'--config',
|
|
session.paths.applicationConfig,
|
|
],
|
|
{
|
|
cwd: session.paths.artifactRoot,
|
|
env: { NODE_ENV: 'production' },
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
},
|
|
);
|
|
const events = [];
|
|
let stdout = '';
|
|
let stderr = '';
|
|
let firstActiveNs;
|
|
let resolveActive;
|
|
let rejectActive;
|
|
const activePromise = new Promise((resolve, reject) => {
|
|
resolveActive = resolve;
|
|
rejectActive = reject;
|
|
});
|
|
const exitPromise = new Promise((resolve, reject) => {
|
|
child.once('error', reject);
|
|
child.once('exit', (code, signal) => resolve({ code, signal }));
|
|
});
|
|
child.stdout.setEncoding('utf8');
|
|
child.stderr.setEncoding('utf8');
|
|
child.stdout.on('data', (chunk) => {
|
|
try {
|
|
stdout += chunk;
|
|
if (Buffer.byteLength(stdout, 'utf8') > MAX_OUTPUT_BYTES) {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
'application stdout exceeded',
|
|
);
|
|
}
|
|
stdout = parseEventLines(stdout, events);
|
|
const activeEvents = events.filter(({ event }) => event === 'active');
|
|
if (activeEvents.length === 1 && firstActiveNs === undefined) {
|
|
firstActiveNs = process.hrtime.bigint();
|
|
resolveActive(activeEvents[0]);
|
|
} else if (activeEvents.length > 1) {
|
|
rejectActive(
|
|
new QingLong3PhysicalApplicationStartEvidenceError(
|
|
'application emitted multiple active events',
|
|
),
|
|
);
|
|
}
|
|
} catch (error) {
|
|
rejectActive(error);
|
|
}
|
|
});
|
|
child.stderr.on('data', (chunk) => {
|
|
stderr += chunk;
|
|
if (Buffer.byteLength(stderr, 'utf8') > MAX_OUTPUT_BYTES) {
|
|
rejectActive(
|
|
new QingLong3PhysicalApplicationStartEvidenceError(
|
|
'application stderr exceeded',
|
|
),
|
|
);
|
|
}
|
|
});
|
|
exitPromise.then(({ code, signal }) => {
|
|
if (firstActiveNs === undefined) {
|
|
rejectActive(
|
|
new QingLong3PhysicalApplicationStartEvidenceError(
|
|
`application exited before active: ${String(code)}/${String(signal)}`,
|
|
),
|
|
);
|
|
}
|
|
}, rejectActive);
|
|
|
|
let maximumSampledRssBytes = 0;
|
|
let sampleCount = 0;
|
|
let firstSample;
|
|
let lastSample;
|
|
let startTicks;
|
|
const sample = () => {
|
|
try {
|
|
const value = readProcessSample(
|
|
child.pid,
|
|
session.environment.nodeExecutable,
|
|
);
|
|
if (startTicks !== undefined && value.startTicks !== startTicks) {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
'application process generation changed',
|
|
);
|
|
}
|
|
startTicks ??= value.startTicks;
|
|
firstSample ??= value;
|
|
lastSample = value;
|
|
maximumSampledRssBytes = Math.max(maximumSampledRssBytes, value.rssBytes);
|
|
sampleCount += 1;
|
|
} catch (error) {
|
|
if (error?.code !== 'ENOENT' && firstActiveNs === undefined) {
|
|
rejectActive(error);
|
|
}
|
|
}
|
|
};
|
|
sample();
|
|
const sampler = setInterval(sample, manifest.sampleIntervalMs);
|
|
try {
|
|
const activeEvent = await Promise.race([
|
|
activePromise,
|
|
timeoutPromise(
|
|
manifest.maximumFirstActiveMs,
|
|
'application did not become active within the manifest budget',
|
|
),
|
|
]);
|
|
sample();
|
|
if (
|
|
activeEvent.level !== 'info' ||
|
|
activeEvent.aiStatus !== 'deployment_excluded' ||
|
|
activeEvent.instanceId !==
|
|
`physical-start-${session.sessionId.slice(0, 8)}` ||
|
|
!firstSample ||
|
|
!lastSample ||
|
|
sampleCount < 1
|
|
) {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
'application active evidence is invalid',
|
|
);
|
|
}
|
|
child.kill('SIGTERM');
|
|
const outcome = await Promise.race([
|
|
exitPromise,
|
|
timeoutPromise(35_000, 'application did not stop after SIGTERM'),
|
|
]);
|
|
if (stdout !== '' || stderr !== '') {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
'application left partial stdout or emitted stderr',
|
|
);
|
|
}
|
|
const activeEventCount = events.filter(
|
|
({ event }) => event === 'active',
|
|
).length;
|
|
const gracefulStop =
|
|
events.some(
|
|
({ event, signal }) =>
|
|
event === 'shutdown_requested' && signal === 'SIGTERM',
|
|
) &&
|
|
events.some(
|
|
({ event, stopResult }) =>
|
|
event === 'stopped' && stopResult === 'stopped',
|
|
);
|
|
return Object.freeze({
|
|
measurements: Object.freeze({
|
|
firstActiveMs: Math.ceil(Number(firstActiveNs - startNs) / 1_000_000),
|
|
maximumSampledRssBytes,
|
|
processReadBytes: Math.max(
|
|
0,
|
|
lastSample.readBytes - firstSample.readBytes,
|
|
),
|
|
processWriteBytes: Math.max(
|
|
0,
|
|
lastSample.writeBytes - firstSample.writeBytes,
|
|
),
|
|
sampleCount,
|
|
eventCount: events.length,
|
|
}),
|
|
outcomes: Object.freeze({
|
|
activeEventCount,
|
|
aiStatus: activeEvent.aiStatus,
|
|
gracefulStop,
|
|
exitCode: outcome.code,
|
|
exitSignal: outcome.signal,
|
|
stderrBytes: Buffer.byteLength(stderr, 'utf8'),
|
|
sqliteContractVersion: 0,
|
|
}),
|
|
});
|
|
} finally {
|
|
clearInterval(sampler);
|
|
if (child.exitCode === null && child.signalCode === null) {
|
|
child.kill('SIGKILL');
|
|
}
|
|
}
|
|
}
|
|
|
|
function evaluateApplicationStartEvidence({
|
|
manifest,
|
|
session,
|
|
observed,
|
|
measurements,
|
|
outcomes,
|
|
}) {
|
|
const violations = [];
|
|
if (
|
|
!hasExactKeys(observed, REPORT_OBSERVED_KEYS) ||
|
|
!hasExactKeys(observed.before, OBSERVED_BOOT_KEYS) ||
|
|
!hasExactKeys(observed.after, OBSERVED_BOOT_KEYS) ||
|
|
!validArtifactSummary(observed.artifact)
|
|
) {
|
|
return Object.freeze(['application start observation shape is invalid']);
|
|
}
|
|
if (
|
|
observed.before.bootId === observed.after.bootId ||
|
|
observed.after.bootAgeMs > manifest.maximumBootAgeMs
|
|
) {
|
|
violations.push('reboot boundary or boot age budget is invalid');
|
|
}
|
|
for (const candidate of [observed.before, observed.after]) {
|
|
violations.push(
|
|
...validateBootObservation(manifest, candidate, session.paths.dataPath),
|
|
);
|
|
}
|
|
if (
|
|
JSON.stringify(observed.artifact) !== JSON.stringify(session.artifact) ||
|
|
observed.artifact.artifactSha256 !== manifest.expectedArtifactSha256 ||
|
|
observed.before.nodeSha256 !== manifest.expectedNodeSha256 ||
|
|
observed.after.nodeSha256 !== manifest.expectedNodeSha256 ||
|
|
observed.before.nodeExecutable !== observed.after.nodeExecutable ||
|
|
observed.before.nodeVersion !== observed.after.nodeVersion
|
|
) {
|
|
violations.push('release artifact or Node identity drifted');
|
|
}
|
|
if (
|
|
!hasExactKeys(measurements, MEASUREMENT_KEYS) ||
|
|
MEASUREMENT_KEYS.some(
|
|
(key) =>
|
|
!Number.isSafeInteger(measurements[key]) || measurements[key] < 0,
|
|
) ||
|
|
measurements.firstActiveMs < 1 ||
|
|
measurements.firstActiveMs > manifest.maximumFirstActiveMs ||
|
|
measurements.maximumSampledRssBytes > manifest.maximumSampledRssBytes ||
|
|
measurements.sampleCount < 1 ||
|
|
measurements.eventCount < 1 ||
|
|
measurements.eventCount > MAX_EVENTS
|
|
) {
|
|
violations.push('application start measurement budget is invalid');
|
|
}
|
|
if (
|
|
!hasExactKeys(outcomes, OUTCOME_KEYS) ||
|
|
outcomes.activeEventCount !== 1 ||
|
|
outcomes.aiStatus !== 'deployment_excluded' ||
|
|
outcomes.gracefulStop !== true ||
|
|
outcomes.exitCode !== 0 ||
|
|
outcomes.exitSignal !== null ||
|
|
outcomes.stderrBytes !== 0 ||
|
|
outcomes.sqliteContractVersion !== 41
|
|
) {
|
|
violations.push('application lifecycle outcome is invalid');
|
|
}
|
|
return Object.freeze(violations);
|
|
}
|
|
|
|
function buildApplicationStartReport({
|
|
manifest,
|
|
session,
|
|
observed,
|
|
measurements,
|
|
outcomes,
|
|
generatedAt,
|
|
}) {
|
|
const violations = evaluateApplicationStartEvidence({
|
|
manifest,
|
|
session,
|
|
observed,
|
|
measurements,
|
|
outcomes,
|
|
});
|
|
const body = {
|
|
schemaVersion: 1,
|
|
evidenceClass: 'physical_edge_application_start_candidate',
|
|
supported: false,
|
|
generatedAt,
|
|
manifest,
|
|
session: {
|
|
sessionId: session.sessionId,
|
|
sessionDigest: session.sha256,
|
|
preparedAt: session.preparedAt,
|
|
},
|
|
observed,
|
|
measurements,
|
|
outcomes,
|
|
qualification: {
|
|
passed: violations.length === 0,
|
|
violations,
|
|
measures: MEASURES,
|
|
doesNotProve: EXCLUSIONS,
|
|
},
|
|
};
|
|
return Object.freeze({ ...body, sha256: canonicalDigest(body) });
|
|
}
|
|
|
|
function validateApplicationStartReport(report, manifest, currentObserved) {
|
|
const violations = [];
|
|
if (!hasExactKeys(report, REPORT_KEYS)) {
|
|
return Object.freeze(['application start report shape is invalid']);
|
|
}
|
|
const { sha256, ...body } = report;
|
|
if (!SHA256_PATTERN.test(sha256 ?? '') || canonicalDigest(body) !== sha256) {
|
|
violations.push('application start report SHA-256 is invalid');
|
|
}
|
|
if (
|
|
report.schemaVersion !== 1 ||
|
|
report.evidenceClass !== 'physical_edge_application_start_candidate' ||
|
|
report.supported !== false ||
|
|
!Number.isFinite(Date.parse(report.generatedAt ?? '')) ||
|
|
canonicalDigest(report.manifest) !== canonicalDigest(manifest) ||
|
|
!hasExactKeys(report.session, REPORT_SESSION_KEYS) ||
|
|
!UUID_V4_PATTERN.test(report.session?.sessionId ?? '') ||
|
|
!SHA256_PATTERN.test(report.session?.sessionDigest ?? '') ||
|
|
!Number.isFinite(Date.parse(report.session?.preparedAt ?? '')) ||
|
|
!hasExactKeys(report.qualification, QUALIFICATION_KEYS)
|
|
) {
|
|
violations.push('application start report identity is invalid');
|
|
}
|
|
if (
|
|
report.observed?.after?.bootId !== currentObserved.bootId ||
|
|
report.observed?.after?.architecture !== currentObserved.architecture ||
|
|
report.observed?.after?.dataFilesystem !== currentObserved.dataFilesystem
|
|
) {
|
|
violations.push('application start report current device did not match');
|
|
}
|
|
const syntheticSession = {
|
|
paths: { dataPath: currentObserved.dataPath },
|
|
artifact: report.observed?.artifact,
|
|
};
|
|
const recomputed = evaluateApplicationStartEvidence({
|
|
manifest,
|
|
session: syntheticSession,
|
|
observed: report.observed,
|
|
measurements: report.measurements,
|
|
outcomes: report.outcomes,
|
|
});
|
|
if (
|
|
report.qualification?.passed !== (recomputed.length === 0) ||
|
|
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('application start report qualification was widened');
|
|
}
|
|
return Object.freeze(violations);
|
|
}
|
|
|
|
async function resumePhase(options, manifest) {
|
|
if (process.platform !== 'linux') {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
'resume requires Linux',
|
|
);
|
|
}
|
|
const session = normalizeSession(
|
|
readBoundedPrivateJson(options.sessionPath, 'session'),
|
|
);
|
|
if (
|
|
session.manifestDigest !== canonicalDigest(manifest) ||
|
|
session.uid !== process.getuid?.()
|
|
) {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
'session manifest or POSIX identity changed',
|
|
);
|
|
}
|
|
const dataPath = fs.realpathSync(session.paths.dataPath);
|
|
assertPrivateDirectory(dataPath, 'dataPath');
|
|
assertChildPath(dataPath, options.sessionPath, 'sessionPath');
|
|
assertChildPath(dataPath, options.outputPath, 'outputPath');
|
|
if (
|
|
fs.realpathSync(session.paths.deploymentRoot) !==
|
|
session.paths.deploymentRoot ||
|
|
fs.realpathSync(session.paths.artifactRoot) !== session.paths.artifactRoot
|
|
) {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
'session path identity changed',
|
|
);
|
|
}
|
|
assertPrivateDirectory(session.paths.deploymentRoot, 'deploymentRoot');
|
|
const metadata = preflightArtifactMetadata(
|
|
session.paths.artifactRoot,
|
|
session.paths.applicationEntrypoint,
|
|
);
|
|
if (
|
|
metadata.files !== session.artifact.artifactFiles ||
|
|
metadata.artifactMetadataSha256 !== session.artifact.artifactMetadataSha256
|
|
) {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
'release artifact metadata count changed',
|
|
);
|
|
}
|
|
const nodeBefore = Object.freeze({
|
|
nodeExecutable: fs.realpathSync(process.execPath),
|
|
nodeSha256: session.environment.nodeSha256,
|
|
nodeVersion: process.version,
|
|
});
|
|
if (
|
|
nodeBefore.nodeExecutable !== session.environment.nodeExecutable ||
|
|
process.version !== session.environment.nodeVersion
|
|
) {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
'Node runtime path or version changed',
|
|
);
|
|
}
|
|
const bootAgeMs = readBootAgeMs();
|
|
const after = bootObservation(dataPath, nodeBefore, bootAgeMs);
|
|
const preflightViolations = validateBootObservation(
|
|
manifest,
|
|
after,
|
|
dataPath,
|
|
);
|
|
if (
|
|
after.bootId === session.environment.bootId ||
|
|
bootAgeMs > manifest.maximumBootAgeMs ||
|
|
preflightViolations.length > 0
|
|
) {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
`resume preflight rejected: ${[
|
|
...(after.bootId === session.environment.bootId
|
|
? ['boot identity did not change']
|
|
: []),
|
|
...(bootAgeMs > manifest.maximumBootAgeMs
|
|
? ['boot age exceeded manifest']
|
|
: []),
|
|
...preflightViolations,
|
|
].join('; ')}`,
|
|
);
|
|
}
|
|
const measurement = await measureApplicationStart(session, manifest);
|
|
const identityAfter = collectArtifactIdentity(session.paths.artifactRoot);
|
|
const nodeAfter = collectNodeIdentity();
|
|
const artifactViolations = validateArtifactAgainstManifest(
|
|
manifest,
|
|
identityAfter.artifact,
|
|
nodeAfter,
|
|
);
|
|
if (
|
|
artifactViolations.length > 0 ||
|
|
JSON.stringify(identityAfter.artifact) !==
|
|
JSON.stringify(session.artifact) ||
|
|
nodeAfter.nodeExecutable !== session.environment.nodeExecutable
|
|
) {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
`post-run release identity rejected: ${artifactViolations.join('; ')}`,
|
|
);
|
|
}
|
|
const products = requireBuiltProducts();
|
|
const sqlite = await products.rolloutSafety.inspectLocalSqliteSnapshot({
|
|
databasePath: path.join(session.paths.deploymentRoot, 'qinglong3.sqlite'),
|
|
profile: 'edge',
|
|
});
|
|
const outcomes = Object.freeze({
|
|
...measurement.outcomes,
|
|
sqliteContractVersion: sqlite.contractVersion,
|
|
});
|
|
const before = Object.freeze({
|
|
platform: session.environment.platform,
|
|
architecture: session.environment.architecture,
|
|
bootId: session.environment.bootId,
|
|
bootAgeMs: session.environment.bootAgeMs,
|
|
dataFilesystem: session.environment.dataFilesystem,
|
|
nodeExecutable: session.environment.nodeExecutable,
|
|
nodeSha256: session.environment.nodeSha256,
|
|
nodeVersion: session.environment.nodeVersion,
|
|
});
|
|
const report = buildApplicationStartReport({
|
|
manifest,
|
|
session,
|
|
observed: {
|
|
before,
|
|
after: Object.freeze({
|
|
...after,
|
|
nodeSha256: nodeAfter.nodeSha256,
|
|
}),
|
|
artifact: identityAfter.artifact,
|
|
},
|
|
measurements: measurement.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;
|
|
}
|
|
|
|
function inspectPhase(options) {
|
|
const identity = collectArtifactIdentity(options.artifactRoot);
|
|
const node = collectNodeIdentity();
|
|
process.stdout.write(
|
|
`${JSON.stringify(
|
|
{
|
|
schemaVersion: 1,
|
|
evidenceClass: 'physical_edge_application_start_inspection',
|
|
profile: 'edge',
|
|
artifact: identity.artifact,
|
|
node: {
|
|
executable: node.nodeExecutable,
|
|
sha256: node.nodeSha256,
|
|
version: node.nodeVersion,
|
|
},
|
|
supported: false,
|
|
},
|
|
null,
|
|
options.json ? 0 : 2,
|
|
)}\n`,
|
|
);
|
|
}
|
|
|
|
async function main() {
|
|
if (Number(process.versions.node.split('.')[0]) < 24) {
|
|
throw new QingLong3PhysicalApplicationStartEvidenceError(
|
|
'Node.js 24 or newer is required',
|
|
);
|
|
}
|
|
const options = parseArguments(process.argv.slice(2));
|
|
if (options.phase === 'inspect') {
|
|
inspectPhase(options);
|
|
return;
|
|
}
|
|
const manifest = readManifest(options.manifestPath);
|
|
if (options.phase === 'prepare') {
|
|
await preparePhase(options, manifest);
|
|
} else {
|
|
await resumePhase(options, manifest);
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
QingLong3PhysicalApplicationStartEvidenceError,
|
|
buildApplicationStartReport,
|
|
collectArtifactIdentity,
|
|
evaluateApplicationStartEvidence,
|
|
normalizeApplicationStartManifest,
|
|
normalizeSession,
|
|
parseArguments,
|
|
parseEventLines,
|
|
preflightArtifactMetadata,
|
|
readBootAgeMs,
|
|
validateApplicationStartReport,
|
|
validateArtifactAgainstManifest,
|
|
validateBootObservation,
|
|
};
|
|
|
|
if (require.main === module) {
|
|
main().catch((error) => {
|
|
process.stderr.write(
|
|
`${error instanceof Error ? error.message : String(error)}\n`,
|
|
);
|
|
process.exitCode = 1;
|
|
});
|
|
}
|