mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
416 lines
14 KiB
JavaScript
416 lines
14 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
require('ts-node/register/transpile-only');
|
|
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const {
|
|
legacyShadowPrimaryEvidenceSha256,
|
|
parseLegacyShadowPrimaryGateReceipt,
|
|
} = require('../back/runtime/domain/legacyShadowPrimaryGate');
|
|
const {
|
|
createManualPrimaryCanaryDisabledManifest,
|
|
manualPrimaryCanaryFileSet,
|
|
manualPrimaryCanarySha256,
|
|
parseManualPrimaryCanaryPlan,
|
|
parseManualPrimaryCanaryQualification,
|
|
} = require('../back/runtime/domain/manualPrimaryCanaryCeremony');
|
|
const {
|
|
parseRuntimeRolloutManifest,
|
|
} = require('../back/runtime/domain/runtimeRolloutManifest');
|
|
const {
|
|
MANUAL_PRIMARY_RUNTIME_RECEIPT_FILE,
|
|
MAX_MANUAL_PRIMARY_RUNTIME_RECEIPT_BYTES,
|
|
parseManualPrimaryRuntimeReceipt,
|
|
} = require('../back/runtime/domain/manualPrimaryRuntimeReceipt');
|
|
const {
|
|
readPrivateJson,
|
|
serialized,
|
|
} = require('./ql3-manual-primary-canary.cjs');
|
|
|
|
const REQUIREMENTS = new Set([
|
|
'prepared',
|
|
'qualified',
|
|
'selected',
|
|
'active',
|
|
'off',
|
|
'rolled-back',
|
|
]);
|
|
const ROLLBACK_REASONS = new Set([
|
|
'operator_request',
|
|
'runtime_failure',
|
|
'gate_rejected',
|
|
'approval_expired',
|
|
]);
|
|
|
|
function hasExactKeys(value, expected) {
|
|
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
return false;
|
|
}
|
|
const actual = Object.keys(value).sort();
|
|
const wanted = [...expected].sort();
|
|
return (
|
|
actual.length === wanted.length &&
|
|
actual.every((key, index) => key === wanted[index])
|
|
);
|
|
}
|
|
|
|
function parseArguments(argv) {
|
|
const options = { require: 'prepared' };
|
|
for (const argument of argv) {
|
|
if (argument === '--') continue;
|
|
if (argument.startsWith('--root=')) {
|
|
const value = argument.slice('--root='.length);
|
|
if (!value) throw new TypeError('--root must not be empty');
|
|
options.root = path.resolve(value);
|
|
} else if (argument.startsWith('--session=')) {
|
|
options.sessionId = argument.slice('--session='.length);
|
|
} else if (argument.startsWith('--require=')) {
|
|
options.require = argument.slice('--require='.length);
|
|
} else {
|
|
throw new TypeError(`Unsupported argument: ${argument}`);
|
|
}
|
|
}
|
|
if (!options.root || !path.isAbsolute(options.root)) {
|
|
throw new TypeError('--root must be absolute');
|
|
}
|
|
if (!options.sessionId) throw new TypeError('--session is required');
|
|
if (!REQUIREMENTS.has(options.require)) {
|
|
throw new TypeError('--require is invalid');
|
|
}
|
|
return options;
|
|
}
|
|
|
|
function regularFile(target) {
|
|
try {
|
|
const stat = fs.lstatSync(target);
|
|
return stat.isFile() && !stat.isSymbolicLink();
|
|
} catch (error) {
|
|
if (error?.code === 'ENOENT') return false;
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
function inspectRuntimeProcess(identity) {
|
|
if (identity.kind !== 'linux-proc' || process.platform !== 'linux') {
|
|
return 'unsupported';
|
|
}
|
|
let bootId;
|
|
try {
|
|
bootId = fs.readFileSync('/proc/sys/kernel/random/boot_id', 'utf8').trim();
|
|
} catch (error) {
|
|
if (error?.code === 'ENOENT') return 'unsupported';
|
|
throw error;
|
|
}
|
|
if (bootId !== identity.bootId) return 'identity_mismatch';
|
|
let stat;
|
|
try {
|
|
stat = fs.readFileSync(`/proc/${identity.pid}/stat`, 'utf8');
|
|
} catch (error) {
|
|
if (error?.code === 'ENOENT' || error?.code === 'ESRCH') return 'exited';
|
|
throw error;
|
|
}
|
|
const close = stat.lastIndexOf(')');
|
|
const fields =
|
|
close < 1
|
|
? []
|
|
: stat
|
|
.slice(close + 1)
|
|
.trim()
|
|
.split(/\s+/u);
|
|
if (
|
|
fields.length < 20 ||
|
|
Number(fields[2]) !== identity.processGroupId ||
|
|
fields[19] !== identity.startTimeTicks
|
|
) {
|
|
return 'identity_mismatch';
|
|
}
|
|
return ['Z', 'X', 'x'].includes(fields[0]) ? 'exited' : 'running';
|
|
}
|
|
|
|
function run(options, dependencies = {}) {
|
|
const rootStat = fs.lstatSync(options.root);
|
|
if (
|
|
!rootStat.isDirectory() ||
|
|
rootStat.isSymbolicLink() ||
|
|
(rootStat.mode & 0o022) !== 0 ||
|
|
(typeof process.getuid === 'function' && rootStat.uid !== process.getuid())
|
|
) {
|
|
throw new TypeError('Canary root is unsafe');
|
|
}
|
|
const files = manualPrimaryCanaryFileSet(options.sessionId);
|
|
const resolve = (name) => path.join(options.root, files[name]);
|
|
const planRead = readPrivateJson(resolve('plan'), 64 * 1024);
|
|
const plan = parseManualPrimaryCanaryPlan(planRead.value);
|
|
if (plan.sessionId !== options.sessionId) {
|
|
throw new TypeError('Canary plan session mismatch');
|
|
}
|
|
const gatePresent = regularFile(resolve('primaryGate'));
|
|
const qualificationPresent = regularFile(resolve('qualification'));
|
|
if (gatePresent !== qualificationPresent) {
|
|
throw new TypeError('Canary qualification is partial');
|
|
}
|
|
let eligible = false;
|
|
if (gatePresent) {
|
|
const gateRead = readPrivateJson(resolve('primaryGate'), 64 * 1024);
|
|
const gate = parseLegacyShadowPrimaryGateReceipt(gateRead.value);
|
|
const qualification = parseManualPrimaryCanaryQualification(
|
|
readPrivateJson(resolve('qualification'), 64 * 1024).value,
|
|
);
|
|
const sources = {
|
|
capture: readPrivateJson(resolve('capture')),
|
|
terminal: readPrivateJson(resolve('terminal')),
|
|
resource: readPrivateJson(resolve('resource')),
|
|
};
|
|
if (
|
|
gate.assessment !== 'eligible' ||
|
|
gate.profile !== plan.profile ||
|
|
gate.origin !== 'manual' ||
|
|
gate.counts.admitted !== plan.admissionTarget ||
|
|
qualification.sessionId !== plan.sessionId ||
|
|
qualification.profile !== plan.profile ||
|
|
qualification.planSha256 !== planRead.sha256 ||
|
|
qualification.primaryGateFileSha256 !== gateRead.sha256 ||
|
|
qualification.sourceFileSha256.capture !== sources.capture.sha256 ||
|
|
qualification.sourceFileSha256.terminal !== sources.terminal.sha256 ||
|
|
qualification.sourceFileSha256.resource !== sources.resource.sha256 ||
|
|
qualification.sourceCanonicalSha256.capture !==
|
|
gate.evidence.captureSha256 ||
|
|
qualification.sourceCanonicalSha256.terminal !==
|
|
gate.evidence.terminalSha256 ||
|
|
qualification.sourceCanonicalSha256.resource !==
|
|
gate.evidence.resourceSha256 ||
|
|
qualification.window.startInclusiveMs !== gate.window.startInclusiveMs ||
|
|
qualification.window.endExclusiveMs !== gate.window.endExclusiveMs ||
|
|
qualification.counts.admitted !== gate.counts.admitted ||
|
|
qualification.counts.captured !== gate.counts.captured ||
|
|
qualification.counts.terminalScanned !== gate.counts.terminalScanned ||
|
|
qualification.counts.terminalMatched !== gate.counts.terminalMatched ||
|
|
legacyShadowPrimaryEvidenceSha256(sources.capture.value) !==
|
|
qualification.sourceCanonicalSha256.capture ||
|
|
legacyShadowPrimaryEvidenceSha256(sources.terminal.value) !==
|
|
qualification.sourceCanonicalSha256.terminal ||
|
|
legacyShadowPrimaryEvidenceSha256(sources.resource.value) !==
|
|
qualification.sourceCanonicalSha256.resource
|
|
) {
|
|
throw new TypeError('Canary qualification drifted');
|
|
}
|
|
eligible = true;
|
|
}
|
|
const rolloutPath = resolve('rollout');
|
|
let rolloutMode = 'off';
|
|
let approvalExpired = false;
|
|
let rolloutSha256;
|
|
if (regularFile(rolloutPath)) {
|
|
const rolloutRead = readPrivateJson(rolloutPath, 64 * 1024);
|
|
const raw = rolloutRead.value;
|
|
const now = Date.now();
|
|
approvalExpired =
|
|
raw?.enabled === true &&
|
|
Number.isSafeInteger(raw.expiresAtMs) &&
|
|
raw.expiresAtMs <= now;
|
|
const evaluatedAt = approvalExpired ? raw.approvedAtMs : now;
|
|
const decision = parseRuntimeRolloutManifest(raw, evaluatedAt);
|
|
rolloutSha256 = rolloutRead.sha256;
|
|
if (decision.manifest.enabled && !approvalExpired) {
|
|
if (
|
|
decision.manifest.revision !== `manual-primary-${plan.sessionId}` ||
|
|
!eligible ||
|
|
decision.manifest.primaryGate.receiptSha256 !==
|
|
readPrivateJson(resolve('primaryGate'), 64 * 1024).sha256
|
|
) {
|
|
throw new TypeError('Active rollout is not bound to the canary');
|
|
}
|
|
if (!regularFile(resolve('selection'))) {
|
|
throw new TypeError('Selected rollout has no selection receipt');
|
|
}
|
|
const selection = readPrivateJson(resolve('selection'), 64 * 1024).value;
|
|
if (
|
|
!hasExactKeys(selection, [
|
|
'schema',
|
|
'schemaVersion',
|
|
'sessionId',
|
|
'profile',
|
|
'selectedAtMs',
|
|
'expiresAtMs',
|
|
'manifestSha256',
|
|
'priorRollout',
|
|
]) ||
|
|
selection?.schema !== 'qinglong/manual-primary-canary-selection@v1' ||
|
|
selection.schemaVersion !== 1 ||
|
|
selection.sessionId !== plan.sessionId ||
|
|
selection.profile !== plan.profile ||
|
|
selection.selectedAtMs !== decision.manifest.approvedAtMs ||
|
|
selection.manifestSha256 !== rolloutRead.sha256 ||
|
|
selection.expiresAtMs !== decision.manifest.expiresAtMs ||
|
|
JSON.stringify(selection.priorRollout) !==
|
|
JSON.stringify(plan.currentRollout)
|
|
) {
|
|
throw new TypeError('Selection receipt drifted');
|
|
}
|
|
rolloutMode = 'primary_selected';
|
|
}
|
|
}
|
|
const rolledBack = regularFile(resolve('rollbackComplete'));
|
|
if (rolledBack) {
|
|
const expectedDisabled = serialized(
|
|
createManualPrimaryCanaryDisabledManifest(plan.sessionId),
|
|
);
|
|
if (
|
|
!regularFile(rolloutPath) ||
|
|
readPrivateJson(rolloutPath, 64 * 1024).sha256 !==
|
|
manualPrimaryCanarySha256(expectedDisabled)
|
|
) {
|
|
throw new TypeError('Rollback completion is not effective');
|
|
}
|
|
const intentRead = readPrivateJson(resolve('rollbackIntent'), 64 * 1024);
|
|
const intent = intentRead.value;
|
|
const completed = readPrivateJson(
|
|
resolve('rollbackComplete'),
|
|
64 * 1024,
|
|
).value;
|
|
const disabledSha256 = manualPrimaryCanarySha256(expectedDisabled);
|
|
if (
|
|
!hasExactKeys(intent, [
|
|
'schema',
|
|
'schemaVersion',
|
|
'sessionId',
|
|
'profile',
|
|
'createdAtMs',
|
|
'operator',
|
|
'reason',
|
|
'enabledManifestSha256',
|
|
'disabledManifestSha256',
|
|
]) ||
|
|
intent?.schema !== 'qinglong/manual-primary-canary-rollback-intent@v1' ||
|
|
intent.schemaVersion !== 1 ||
|
|
intent.sessionId !== plan.sessionId ||
|
|
intent.profile !== plan.profile ||
|
|
typeof intent.operator !== 'string' ||
|
|
intent.operator.length < 3 ||
|
|
intent.operator.length > 128 ||
|
|
/[\u0000-\u001f\u007f]/u.test(intent.operator) ||
|
|
!ROLLBACK_REASONS.has(intent.reason) ||
|
|
!Number.isSafeInteger(intent.createdAtMs) ||
|
|
!/^[a-f0-9]{64}$/u.test(intent.enabledManifestSha256) ||
|
|
intent.disabledManifestSha256 !== disabledSha256 ||
|
|
!hasExactKeys(completed, [
|
|
'schema',
|
|
'schemaVersion',
|
|
'sessionId',
|
|
'profile',
|
|
'completedAtMs',
|
|
'intentSha256',
|
|
'disabledManifestSha256',
|
|
]) ||
|
|
completed?.schema !==
|
|
'qinglong/manual-primary-canary-rollback-complete@v1' ||
|
|
completed.schemaVersion !== 1 ||
|
|
completed.sessionId !== plan.sessionId ||
|
|
completed.profile !== plan.profile ||
|
|
!Number.isSafeInteger(completed.completedAtMs) ||
|
|
completed.completedAtMs < intent.createdAtMs ||
|
|
completed.intentSha256 !== intentRead.sha256 ||
|
|
completed.disabledManifestSha256 !== disabledSha256
|
|
) {
|
|
throw new TypeError('Rollback receipt chain drifted');
|
|
}
|
|
rolloutMode = 'off';
|
|
}
|
|
let runtimeActivationObserved = false;
|
|
let runtimeActivationCurrent = false;
|
|
let runtimeReceiptState = 'missing';
|
|
let runtimeProcessState = 'missing';
|
|
const runtimeReceiptPath = path.join(
|
|
options.root,
|
|
MANUAL_PRIMARY_RUNTIME_RECEIPT_FILE,
|
|
);
|
|
if (regularFile(runtimeReceiptPath)) {
|
|
const receipt = parseManualPrimaryRuntimeReceipt(
|
|
readPrivateJson(
|
|
runtimeReceiptPath,
|
|
MAX_MANUAL_PRIMARY_RUNTIME_RECEIPT_BYTES,
|
|
).value,
|
|
);
|
|
runtimeReceiptState = receipt.state;
|
|
const selectionPath = resolve('selection');
|
|
if (regularFile(selectionPath)) {
|
|
const selection = readPrivateJson(selectionPath, 64 * 1024).value;
|
|
runtimeActivationObserved =
|
|
receipt.revision === `manual-primary-${plan.sessionId}` &&
|
|
receipt.profile === plan.profile &&
|
|
selection?.schema === 'qinglong/manual-primary-canary-selection@v1' &&
|
|
selection.sessionId === plan.sessionId &&
|
|
selection.profile === plan.profile &&
|
|
selection.manifestSha256 === receipt.rolloutSourceSha256;
|
|
}
|
|
if (receipt.state === 'active') {
|
|
runtimeProcessState = (
|
|
dependencies.inspectRuntimeProcess ?? inspectRuntimeProcess
|
|
)(receipt.process);
|
|
runtimeActivationCurrent = runtimeProcessState === 'running';
|
|
}
|
|
}
|
|
const compatible =
|
|
options.require === 'prepared' ||
|
|
(options.require === 'qualified' && eligible) ||
|
|
(options.require === 'selected' && rolloutMode === 'primary_selected') ||
|
|
(options.require === 'active' &&
|
|
rolloutMode === 'primary_selected' &&
|
|
runtimeActivationObserved &&
|
|
runtimeActivationCurrent) ||
|
|
(options.require === 'off' &&
|
|
rolloutMode === 'off' &&
|
|
!runtimeActivationCurrent) ||
|
|
(options.require === 'rolled-back' &&
|
|
rolloutMode === 'off' &&
|
|
rolledBack &&
|
|
!runtimeActivationCurrent);
|
|
const report = {
|
|
schema: 'qinglong/manual-primary-canary-audit@v1',
|
|
schemaVersion: 1,
|
|
sessionId: plan.sessionId,
|
|
profile: plan.profile,
|
|
admissionTarget: plan.admissionTarget,
|
|
eligible,
|
|
rolloutMode,
|
|
approvalExpired,
|
|
runtimeActivationObserved,
|
|
runtimeActivationCurrent,
|
|
runtimeReceiptState,
|
|
runtimeProcessState,
|
|
rolledBack,
|
|
planSha256: planRead.sha256,
|
|
...(rolloutSha256 === undefined ? {} : { rolloutSha256 }),
|
|
requirement: options.require,
|
|
compatible,
|
|
};
|
|
if (!compatible) {
|
|
const error = new Error(
|
|
'Manual Primary canary requirement is not satisfied',
|
|
);
|
|
error.report = report;
|
|
throw error;
|
|
}
|
|
return report;
|
|
}
|
|
|
|
function main() {
|
|
process.stdout.write(
|
|
`${JSON.stringify(run(parseArguments(process.argv.slice(2))))}\n`,
|
|
);
|
|
}
|
|
|
|
module.exports = { parseArguments, run };
|
|
|
|
if (require.main === module) {
|
|
try {
|
|
main();
|
|
} catch (error) {
|
|
process.stderr.write(
|
|
`${error instanceof Error ? error.message : String(error)}\n`,
|
|
);
|
|
process.exitCode = 1;
|
|
}
|
|
}
|