feat(ql3): authorize reconciliation reviews

This commit is contained in:
whyour
2026-08-21 19:34:29 +08:00
parent 827ba5294f
commit 22f58b1eff
19 changed files with 4293 additions and 107 deletions
+21 -17
View File
@@ -11,21 +11,25 @@
最新增量证据(2026-08-21):
- D-391/ADR-0484第一切片已实现,commit/verify 待实现):`reconciliation_planned` 仍是内容无关 summary,不能为人工裁决泄漏明细或
获得 import authority。既有 Local Owner 已在 `deployment/reconciliation/review/` 增加
`reconciliation.review.prepare|diagnostics`,并为后续 commit/verify 冻结
`reconciliation_planned → reconciliation_review_prepared → reconciliation_reviewed` CAS 建立唯一 review fence。diagnostics 每次只把
一个 database/domain/fact-kind 的最多 64 条私有记录 no-replace 写入 caller 指定的 owner-only 文件;stdout 只返回 page digest、计数和
offset,不含路径、名称或 fact digest。commit 不信任 page,而是从 exact sealed bundle 重新流式派生 canonical facts,与 Edge ≤8 MiB、
Standalone ≤32 MiB 的 private NDJSON decision file 逐条匹配。unknown、Secret/credential/pepper、active Run 与 custody 缺口固定 blocked
人工选择也不能提升为自动可导入。authorization 必须来自 target Owner credential 建立的近期强认证 User,并以独立 issuer keyring、
exact review file/plan/bundle/head fence 签名;terminal review 只保存签名 authorization、compact counts/digest 和 content-free receipt。
verify 不打开 SQLite。实现不新增 package/dependency/binary/daemon,不把文件平铺回 `src/` 根,也不调用 DML、Secret 解密、Docker/init/
network。后续领域 adapter 必须消费 exact `reviewDigest` 后重新认证和授权,分别定义 backup、prepare/commit、幂等与 rollback,不能把
review completion 当作 reconciliation completion。当前聚焦套件 `28 total / 26 pass / 2 conditional Docker skip / 0 fail`Local Owner
`250 total / 243 pass / 7 conditional skip / 0 fail`tracked backend `1540 total / 1538 pass / 2 conditional skip / 0 fail`
18-package clean build/逐包测试、八项架构/发布审计、十四档 artifact 和真实 Docker readonly `2/2` 全通过。workspace 仍为 18
packagesLocal Owner `149 source / 148 nested / 1 root binary entry`;新增 3 个源码全部位于 review 子目录,基础常驻 closure 不增长。
- D-391/ADR-0484已接受):既有 Local Owner 已完整实现
`reconciliation.review.prepare|diagnostics|commit|verify`,以
`reconciliation_planned → reconciliation_review_prepared → reconciliation_reviewed` CAS 建立唯一 review fence。diagnostics 每次只把一个
database/domain/fact-kind 的最多 64 条私有记录 no-replace 写入 caller 指定的 owner-only 文件;stdout 不含路径、名称或 fact digest。
commit 不信任 page,而是从 exact sealed bundle 重新流式派生 canonical facts,与 Edge ≤8 MiB、Standalone ≤32 MiB 的稳定 descriptor
NDJSON decision file 逐条匹配。unknown、Secret/credential/pepper、active Run 与 custody 缺口固定 blocked,人工选择不能提升为自动可导入。
生产 composition 只用新的 Local SQLite authentication-read projection 加载 credential/pepper,通过既有 Owner authentication 建立最多
5 分钟的强 User principal;独立最多八代 issuer keyring 签发最长 30 分钟的 domain-separated authorization,并在签名前后重验 decision
file、credential、keyring、planbundle fingerprint 与 prepared head。authorization/review/receipt/seal/head 的 crash/response-loss 窗口均
exact replayterminal evidence 为 `0400/0500`verify 只验证签名和 exact binding不打开 SQLite、不写文件。实现不新增 packagedependency
binary 或 daemon,不平铺 `src/`,也不调用 DML、Secret 解密、Docker/init/network。后续领域 adapter 必须以独立 ADR 消费 exact
`reviewDigest` 后重新认证授权,不能把 review completion 当作 reconciliation completion。聚焦套件
`32 total / 30 pass / 2 conditional Docker skip / 0 fail`Local Owner `254 total / 247 pass / 7 conditional skip / 0 fail`tracked
backend `1541 total / 1539 pass / 2 conditional skip / 0 fail`18-package clean build/逐包测试、八项架构/部署/发布审计、十四档 artifact
真实 Docker readonly `2/2` 全通过。workspace 仍为 18 packages、`singleSourcePackages=[]``shallowSourcePackages=[]`Local Owner
`155 source / 154 nested / 1 root binary entry`Local SQLite `203 source / 202 nested / 1 root public export`,新增 7 个生产源码全部位于既有
领域目录。基础 Edge/Standalone closure 精确保持 `2,611,978 / 2,612,056` bytes、319 files、58 modules。PostgreSQL 18.6 arm64 physical HA
以 146 gates、timeline `1 → 2` 通过,private evidence SHA-256 为
`3d6623465913d43e6f1a8838896d6deb6664dafd0c26970bddb4d6165fb60c00`,离线审计无 finding。
- D-390/ADR-0483(已接受):既有 Local Owner 已实现密封 capture 的严格只读消费与独立
`reconciliation.plan.prepare|commit|verify`。capture v2 使用 SQLite 可识别的固定 `target.sqlite* / legacy.sqlite* /
recovery.sqlite` 物理名和 `0400/0500` terminal sealmain-only 走 immutable readonlyWAL+SHM 完整配对走普通 readonlyhot
@@ -38,8 +42,8 @@
`246 total / 239 pass / 7 conditional skip / 0 fail`tracked backend `1540 total / 1538 pass / 2 conditional skip / 0 fail`
18-package clean build/逐包测试、八项架构/发布审计和十四档 artifact audit 全通过。workspace 仍为 18 packagesLocal Owner
`146 source / 145 nested / 1 root binary entry`;基础 Edge/Standalone closure 仍为 319 files、58 modules,不含一次性 plan authority。
D-391/ADR-0484 已冻结消费 exact plan digest 的私有逐对象诊断、人工裁决与审批协议;它仍不能借 plan 获得自动 import 或 rollback
authority,完成实现与完整门禁前保持 Proposed
D-391/ADR-0484 已完成私有逐对象诊断、受认证人工裁决、签名 terminal review 与只读 verify;它仍不能借 plan 或 review 获得自动 import
rollback 或 restart authority,下一阶段领域 adapter 继续使用独立 ADR 和门禁
- D-389/ADR-0482(已接受):target stopped 后的 `reconciliation_required` 不能直接逆迁移或覆盖 2.x source;既有 Local Owner
已实现独立 `reconciliation.capture.prepare|commit|verify`,只允许 exact stopped reconciliation head,以 instance CAS 建立唯一
capture fence,并把 target main/sidecars、Legacy source main/sidecars、activation recovery 与内容无关 lineage 以固定 64 KiB
@@ -1,6 +1,6 @@
# ADR-0484:私有 Reconciliation 诊断与受认证人工裁决
- 状态:ProposedD-391 契约冻结
- 状态:AcceptedD-391 已实现并完成门禁
- 日期:2026-08-21
- 关联 RFCQL-RFC-0001 D-05、D-06、D-17、D-64、D-87、D-184、D-259、D-383、D-389、D-390、D-391
- 关联 ADRADR-0064、ADR-0094、ADR-0095、ADR-0194、ADR-0201、ADR-0314、ADR-0315、ADR-0482、ADR-0483
@@ -121,20 +121,25 @@ output 不进入基础 Edge/Standalone artifactCluster/PostgreSQL/Kubernetes
## 当前实现进度
D-391 第一切片已实现 `review.prepare``review.diagnostics`。prepare 读取并重验 exact terminal plan/bundle,以 instance CAS 建立
`reconciliation_review_prepared` 唯一 fence,覆盖 head response loss、第二 review、rollback 和 restart 拒绝。diagnostics 在每次
SQLite open 前后重验密封资产,只为一个 database/domain/fact-kind 发布最多 64 条 owner-only pageSecret、identity、history 和 unknown
facts 固定 blocked,未知表不读取 rowterminal result 不返回路径、名称或 fact digest。page 使用 256 KiB 固定上限、deterministic
stage、hard-link no-replace 与 fsync,重复请求只能得到 byte-exact existing page。
D-391 已完整实现 `review.prepare``review.diagnostics``review.commit``review.verify`。prepare/diagnostics 保持第一切片的 exact
plan/bundle/head fence、64 条私有分页、blocked fact 与 byte-exact replaycommit 新增 Edge 8 MiB、Standalone 32 MiB 的稳定 descriptor
NDJSON 流,逐条重新派生密封 bundle facts,不读取 diagnostics page。生产 composition 只通过新的 Local SQLite authentication-read
projection 读取 credential/pepper,并使用既有 `establishAuthenticatedLocalCommand` 建立最多 5 分钟的强 User principal。独立 issuer
keyring 最多八代 keyauthorization 生命周期最多 30 分钟,签名前后重验 decision file、credential、keyring、plan、bundle fingerprint 和
prepared head。
验证结果:聚焦套件 `28 total / 26 pass / 2 conditional Docker skip / 0 fail`;完整 Local Owner
`250 total / 243 pass / 7 conditional skip / 0 fail`tracked backend `1540 total / 1538 pass / 2 conditional skip / 0 fail`
18-package clean build/逐包测试、八项架构/发布审计、十四档 artifact audit 与真实 Docker readonly `2/2` 均通过。workspace 仍为 18
packages`singleSourcePackages=[]``shallowSourcePackages=[]`Local Owner 为 `149 source / 148 nested / 1 root binary entry`,新增
3 个源码全部位于 `deployment/reconciliation/review/`,没有新增 dependency 或常驻对象,基础 Edge/Standalone closure 未增长。
authorization、review、receipt、seal 与 head 的每个 crash/response-loss 窗口均已覆盖;terminal evidence 收敛为 `0400/0500`verify
复验签名与全部 exact binding,不打开 SQLite、不写文件。CLI 只返回 content-free digest/count/replay facts。`reconciliation_reviewed`
仍不授予 import、rollback、restart、SQL、Secret 解密或外部副作用 authority;任何领域 adapter 必须由下一份独立 ADR 定义。
`review.commit`、强认证 User/issuer keyring authorization、terminal seal、`reconciliation_reviewed` 推进和 `review.verify` 尚未实现,因此本
ADR 继续保持 Proposed;当前 prepared review 不授予任何 import、rollback 或 restart authority。
验证结果:聚焦套件 `32 total / 30 pass / 2 conditional Docker skip / 0 fail`;完整 Local Owner
`254 total / 247 pass / 7 conditional skip / 0 fail`tracked backend `1541 total / 1539 pass / 2 conditional skip / 0 fail`
18-package clean build/逐包测试、八项架构/部署/发布审计、十四档 artifact audit 与真实 Docker readonly `2/2` 均通过。PostgreSQL 18.6
arm64 physical HA 以 146 gates、timeline `1 → 2` 通过,private report SHA-256 为
`3d6623465913d43e6f1a8838896d6deb6664dafd0c26970bddb4d6165fb60c00`,独立 evidence audit 无 finding。workspace 仍为 18 packages
`singleSourcePackages=[]``shallowSourcePackages=[]`Local Owner 为 `155 source / 154 nested / 1 root binary entry`Local SQLite 为
`203 source / 202 nested / 1 root public export`。新增 7 个生产源码全部进入既有领域目录,没有新增 package、dependency、binary 或常驻
对象;基础 Edge/Standalone closure 精确保持 `2,611,978 / 2,612,056` bytes、319 files、58 modules。
## 被拒绝的替代方案
@@ -117,6 +117,12 @@ import {
writeLocalReconciliationReviewDiagnostics,
writeLocalReconciliationReviewDiagnosticsCommandFile,
} from './reconciliation/review/preparation';
import {
commitLocalReconciliationReview,
commitLocalReconciliationReviewCommandFile,
verifyLocalReconciliationReview,
verifyLocalReconciliationReviewCommandFile,
} from './reconciliation/review/completion';
export {
commitLocalReconciliationPlan,
@@ -129,6 +135,10 @@ export {
prepareLocalReconciliationReviewCommandFile,
writeLocalReconciliationReviewDiagnostics,
writeLocalReconciliationReviewDiagnosticsCommandFile,
commitLocalReconciliationReview,
commitLocalReconciliationReviewCommandFile,
verifyLocalReconciliationReview,
verifyLocalReconciliationReviewCommandFile,
};
export {
@@ -181,6 +191,38 @@ export {
type LocalReconciliationReviewPrepareCommand,
type LocalReconciliationReviewPrepareResult,
} from './reconciliation/review/contract';
export {
MAX_LOCAL_RECONCILIATION_REVIEW_AUTHORIZATION_LIFETIME_MS,
normalizeLocalReconciliationReviewCommitCommand,
normalizeLocalReconciliationReviewVerifyCommand,
type LocalReconciliationReviewCommitCommand,
type LocalReconciliationReviewCommitOptions,
type LocalReconciliationReviewTerminalResult,
type LocalReconciliationReviewVerifyCommand,
} from './reconciliation/review/completionContract';
export { type LocalReconciliationReviewCompletionDependencies } from './reconciliation/review/completion';
export {
LOCAL_RECONCILIATION_REVIEW_DISPOSITIONS,
LOCAL_RECONCILIATION_REVIEW_REASONS,
MAX_EDGE_LOCAL_RECONCILIATION_REVIEW_DECISION_BYTES,
MAX_STANDALONE_LOCAL_RECONCILIATION_REVIEW_DECISION_BYTES,
type LocalReconciliationReviewDecision,
type LocalReconciliationReviewDecisionHeader,
type LocalReconciliationReviewDisposition,
type LocalReconciliationReviewReason,
} from './reconciliation/review/decisionFile';
export {
MAX_LOCAL_RECONCILIATION_REVIEW_ISSUER_KEYS,
LocalReconciliationReviewIssuerKeyringFileProvider,
ensureLocalReconciliationReviewIssuerKeyring,
type LocalReconciliationReviewIssuerKeyringSummary,
} from './reconciliation/review/issuerKeyring';
export {
normalizeLocalReconciliationReview,
normalizeLocalReconciliationReviewReceipt,
type LocalReconciliationReview,
type LocalReconciliationReviewReceipt,
} from './reconciliation/review/terminalEvidence';
export {
localReconciliationReviewDirectory,
normalizeLocalReconciliationReviewIntent,
@@ -18,8 +18,10 @@ import {
prepareLocalReconciliationReviewCommandFile,
commitLocalReconciliationCaptureCommandFile,
commitLocalReconciliationPlanCommandFile,
commitLocalReconciliationReviewCommandFile,
verifyLocalReconciliationCaptureCommandFile,
verifyLocalReconciliationPlanCommandFile,
verifyLocalReconciliationReviewCommandFile,
writeLocalReconciliationReviewDiagnosticsCommandFile,
prepareLocalDeploymentCommandFile,
proveLocalDeploymentLegacyReadinessCommandFile,
@@ -35,7 +37,7 @@ import {
} from './localDeployment';
const USAGE =
'Usage: ql3-local-deploy <prepare|adopted-prepare|adopted-verify|status|service-intent-prepare|service-outcome-consume|service-cutover-consume|service-legacy-rollback-prepare|service-legacy-rollback-authorize|service-legacy-rollback-consume|cutover-legacy-stop|cutover-target-start|cutover-target-restart|cutover-target-stop|cutover-legacy-rollback-prepare|cutover-legacy-rollback-commit|cutover-legacy-readiness-probe|cutover-manual-diagnose|cutover-manual-resolution-prepare|cutover-manual-resolution-commit|reconciliation-capture-prepare|reconciliation-capture-commit|reconciliation-capture-verify|reconciliation-plan-prepare|reconciliation-plan-commit|reconciliation-plan-verify|reconciliation-review-prepare|reconciliation-review-diagnostics|compose-revision|compose-preflight|compose-apply|compose-restore-prepare|compose-restore-commit|compose-evidence-collect-prepare|compose-evidence-collect-commit> --command-file /absolute/private-command.json';
'Usage: ql3-local-deploy <prepare|adopted-prepare|adopted-verify|status|service-intent-prepare|service-outcome-consume|service-cutover-consume|service-legacy-rollback-prepare|service-legacy-rollback-authorize|service-legacy-rollback-consume|cutover-legacy-stop|cutover-target-start|cutover-target-restart|cutover-target-stop|cutover-legacy-rollback-prepare|cutover-legacy-rollback-commit|cutover-legacy-readiness-probe|cutover-manual-diagnose|cutover-manual-resolution-prepare|cutover-manual-resolution-commit|reconciliation-capture-prepare|reconciliation-capture-commit|reconciliation-capture-verify|reconciliation-plan-prepare|reconciliation-plan-commit|reconciliation-plan-verify|reconciliation-review-prepare|reconciliation-review-diagnostics|reconciliation-review-commit|reconciliation-review-verify|compose-revision|compose-preflight|compose-apply|compose-restore-prepare|compose-restore-commit|compose-evidence-collect-prepare|compose-evidence-collect-commit> --command-file /absolute/private-command.json';
async function main(argv: readonly string[]): Promise<void> {
if (argv.length === 1 && (argv[0] === '--help' || argv[0] === '-h')) {
@@ -72,6 +74,8 @@ async function main(argv: readonly string[]): Promise<void> {
argv[0] !== 'reconciliation-plan-verify' &&
argv[0] !== 'reconciliation-review-prepare' &&
argv[0] !== 'reconciliation-review-diagnostics' &&
argv[0] !== 'reconciliation-review-commit' &&
argv[0] !== 'reconciliation-review-verify' &&
argv[0] !== 'compose-revision' &&
argv[0] !== 'compose-preflight' &&
argv[0] !== 'compose-apply' &&
@@ -158,6 +162,10 @@ async function main(argv: readonly string[]): Promise<void> {
? prepareLocalReconciliationReviewCommandFile(argv[2]!)
: argv[0] === 'reconciliation-review-diagnostics'
? writeLocalReconciliationReviewDiagnosticsCommandFile(argv[2]!)
: argv[0] === 'reconciliation-review-commit'
? commitLocalReconciliationReviewCommandFile(argv[2]!)
: argv[0] === 'reconciliation-review-verify'
? verifyLocalReconciliationReviewCommandFile(argv[2]!)
: argv[0] === 'compose-revision'
? switchLocalDeploymentComposeRevisionCommandFile(argv[2]!)
: argv[0] === 'compose-preflight'
@@ -0,0 +1,752 @@
import { createHash, createHmac, timingSafeEqual } from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import type { LocalSecretKeyMaterial } from '@qinglong/runtime-core/local-secret';
import {
normalizeSecurityPrincipal,
type SecurityPrincipal,
} from '@qinglong/runtime-core/security';
import { LocalDeploymentConfigurationError } from '../../foundation/error';
import type {
LocalReconciliationReviewDecision,
LocalReconciliationReviewDisposition,
LocalReconciliationReviewReason,
} from './decisionFile';
import { LocalReconciliationReviewIssuerKeyringFileProvider } from './issuerKeyring';
const HEADER_KIND =
'qinglong3-local-reconciliation-review-authorization-header';
const DECISION_KIND =
'qinglong3-local-reconciliation-review-authorization-decision';
const SIGNATURE_KIND =
'qinglong3-local-reconciliation-review-authorization-signature';
const MAX_LINE_BYTES = 64 * 1024;
const READ_CHUNK_BYTES = 64 * 1024;
const DIGEST_PATTERN = /^[0-9a-f]{64}$/;
export interface LocalReconciliationReviewAuthorizationHeader {
readonly schemaVersion: 1;
readonly kind: typeof HEADER_KIND;
readonly reviewId: string;
readonly profile: 'edge' | 'standalone';
readonly planDigest: string;
readonly preparationDigest: string;
readonly bundleDigest: string;
readonly bundleFingerprintDigest: string;
readonly preparedHeadDigest: string;
readonly reviewer: Readonly<SecurityPrincipal>;
readonly issuedAtMs: number;
readonly expiresAtMs: number;
}
export interface LocalReconciliationReviewAuthorizationEvidence {
readonly fileBytes: number;
readonly authorizationDigest: string;
readonly decisionCount: number;
readonly decisionSetDigest: string;
readonly decisionFileDigest: string;
readonly keyId: string;
readonly dispositionCounts: Readonly<
Record<LocalReconciliationReviewDisposition, number>
>;
readonly reasonCounts: Readonly<
Record<LocalReconciliationReviewReason, number>
>;
readonly header: Readonly<LocalReconciliationReviewAuthorizationHeader>;
}
interface SignatureRecord {
readonly schemaVersion: 1;
readonly kind: typeof SIGNATURE_KIND;
readonly algorithm: 'hmac-sha256';
readonly keyId: string;
readonly contentBytes: number;
readonly contentDigest: string;
readonly decisionCount: number;
readonly decisionSetDigest: string;
readonly decisionFileDigest: string;
readonly dispositionCounts: Readonly<
Record<LocalReconciliationReviewDisposition, number>
>;
readonly reasonCounts: Readonly<
Record<LocalReconciliationReviewReason, number>
>;
readonly authenticationTag: string;
}
const DISPOSITIONS = [
'retain_target',
'adopt_legacy',
'retain_both',
'exclude_legacy',
'defer',
'manual_external',
] as const;
const REASONS = [
'preserve_target',
'prefer_legacy',
'preserve_both',
'legacy_excluded',
'deferred_review',
'external_recovery_required',
] as const;
function zeroCounts<T extends string>(keys: readonly T[]): Record<T, number> {
return Object.fromEntries(keys.map((key) => [key, 0])) as Record<T, number>;
}
function configurationError(message: string, cause?: unknown): never {
throw new LocalDeploymentConfigurationError(
`reconciliation review authorization ${message}`,
{ cause },
);
}
function exact(
value: unknown,
keys: readonly string[],
label: string,
): Record<string, unknown> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
configurationError(`${label} must be an object`);
}
const record = value as Record<string, unknown>;
const actual = Object.keys(record).sort();
const expected = [...keys].sort();
if (
actual.length !== expected.length ||
actual.some((key, index) => key !== expected[index])
) {
configurationError(`${label} shape is invalid`);
}
return record;
}
function canonicalLine(value: unknown): Buffer {
const bytes = Buffer.from(`${JSON.stringify(value)}\n`, 'utf8');
if (bytes.byteLength < 3 || bytes.byteLength > MAX_LINE_BYTES + 1) {
configurationError('record exceeds its line bound');
}
return bytes;
}
function writeAll(descriptor: number, bytes: Buffer): void {
let offset = 0;
while (offset < bytes.byteLength) {
const written = fs.writeSync(
descriptor,
bytes,
offset,
bytes.byteLength - offset,
);
if (written < 1) configurationError('write stalled');
offset += written;
}
}
function signingMessage(
contentDigest: string,
contentBytes: number,
decisionFileDigest: string,
): Buffer {
return Buffer.from(
`qinglong3.local-reconciliation-review-authorization.v1\0${contentDigest}\0${contentBytes}\0${decisionFileDigest}`,
'utf8',
);
}
function wipe(material: LocalSecretKeyMaterial | null | undefined): void {
material?.key.fill(0);
}
function syncDirectory(directory: string): void {
const descriptor = fs.openSync(directory, fs.constants.O_RDONLY);
try {
fs.fsyncSync(descriptor);
} finally {
fs.closeSync(descriptor);
}
}
function normalizeHeader(
value: unknown,
): Readonly<LocalReconciliationReviewAuthorizationHeader> {
const record = exact(
value,
[
'bundleDigest',
'bundleFingerprintDigest',
'expiresAtMs',
'issuedAtMs',
'kind',
'planDigest',
'preparationDigest',
'preparedHeadDigest',
'profile',
'reviewId',
'reviewer',
'schemaVersion',
],
'header',
);
if (
record.schemaVersion !== 1 ||
record.kind !== HEADER_KIND ||
(record.profile !== 'edge' && record.profile !== 'standalone') ||
!Number.isSafeInteger(record.issuedAtMs) ||
!Number.isSafeInteger(record.expiresAtMs) ||
(record.expiresAtMs as number) <= (record.issuedAtMs as number) ||
typeof record.reviewId !== 'string' ||
[
record.planDigest,
record.preparationDigest,
record.bundleDigest,
record.bundleFingerprintDigest,
record.preparedHeadDigest,
].some((value) => typeof value !== 'string' || !DIGEST_PATTERN.test(value))
) {
configurationError('header is invalid');
}
const principal = normalizeSecurityPrincipal(
record.reviewer as SecurityPrincipal,
record.issuedAtMs as number,
);
if (
principal.subject.type !== 'user' ||
!['hardware', 'local_console', 'multi_factor'].includes(
principal.assurance,
) ||
(record.issuedAtMs as number) - principal.authenticatedAtMs >
5 * 60 * 1_000 ||
principal.authenticatedAtMs > (record.issuedAtMs as number) ||
(record.expiresAtMs as number) - (record.issuedAtMs as number) >
30 * 60 * 1_000
) {
configurationError('reviewer authority is invalid');
}
return Object.freeze({
...(record as unknown as LocalReconciliationReviewAuthorizationHeader),
reviewer: principal,
});
}
function authorizationDecision(
value: unknown,
): Readonly<LocalReconciliationReviewDecision> {
const record = exact(
value,
[
'database',
'disposition',
'domain',
'factDigest',
'factKind',
'kind',
'ordinal',
'reason',
'schemaVersion',
],
'decision',
);
if (
record.schemaVersion !== 1 ||
record.kind !== DECISION_KIND ||
(record.database !== 'legacy' && record.database !== 'target') ||
![
'schema_lineage',
'automation',
'secret_and_config',
'run_history',
'plugin_package',
'ai_and_tool',
'identity_policy_audit',
'unknown',
].includes(record.domain as string) ||
(record.factKind !== 'schema_object' && record.factKind !== 'table') ||
!Number.isSafeInteger(record.ordinal) ||
(record.ordinal as number) < 1 ||
typeof record.factDigest !== 'string' ||
!DIGEST_PATTERN.test(record.factDigest) ||
!DISPOSITIONS.includes(
record.disposition as LocalReconciliationReviewDisposition,
) ||
!REASONS.includes(record.reason as LocalReconciliationReviewReason)
) {
configurationError('decision record is invalid');
}
const reasonByDisposition: Record<
LocalReconciliationReviewDisposition,
LocalReconciliationReviewReason
> = {
retain_target: 'preserve_target',
adopt_legacy: 'prefer_legacy',
retain_both: 'preserve_both',
exclude_legacy: 'legacy_excluded',
defer: 'deferred_review',
manual_external: 'external_recovery_required',
};
if (
reasonByDisposition[
record.disposition as LocalReconciliationReviewDisposition
] !== record.reason
) {
configurationError('decision reason does not match disposition');
}
return Object.freeze({
...(record as unknown as LocalReconciliationReviewDecision),
kind: 'qinglong3-local-reconciliation-review-decision',
});
}
function normalizeCounts<T extends string>(
value: unknown,
keys: readonly T[],
label: string,
): Readonly<Record<T, number>> {
const record = exact(value, keys, label);
if (
keys.some(
(key) =>
!Number.isSafeInteger(record[key]) || (record[key] as number) < 0,
)
) {
configurationError(`${label} is invalid`);
}
return Object.freeze(record) as Readonly<Record<T, number>>;
}
function parseSignature(value: unknown): Readonly<SignatureRecord> {
const record = exact(
value,
[
'algorithm',
'authenticationTag',
'contentBytes',
'contentDigest',
'decisionCount',
'decisionFileDigest',
'decisionSetDigest',
'dispositionCounts',
'keyId',
'kind',
'reasonCounts',
'schemaVersion',
],
'signature',
);
let tag: Buffer | undefined;
try {
tag =
typeof record.authenticationTag === 'string'
? Buffer.from(record.authenticationTag, 'base64url')
: Buffer.alloc(0);
if (
record.schemaVersion !== 1 ||
record.kind !== SIGNATURE_KIND ||
record.algorithm !== 'hmac-sha256' ||
typeof record.keyId !== 'string' ||
!Number.isSafeInteger(record.contentBytes) ||
(record.contentBytes as number) < 1 ||
typeof record.contentDigest !== 'string' ||
!DIGEST_PATTERN.test(record.contentDigest) ||
!Number.isSafeInteger(record.decisionCount) ||
(record.decisionCount as number) < 0 ||
typeof record.decisionSetDigest !== 'string' ||
!DIGEST_PATTERN.test(record.decisionSetDigest) ||
typeof record.decisionFileDigest !== 'string' ||
!DIGEST_PATTERN.test(record.decisionFileDigest) ||
tag.byteLength !== 32 ||
tag.toString('base64url') !== record.authenticationTag
) {
configurationError('signature record is invalid');
}
return Object.freeze({
...(record as unknown as SignatureRecord),
dispositionCounts: normalizeCounts(
record.dispositionCounts,
DISPOSITIONS,
'disposition counts',
),
reasonCounts: normalizeCounts(
record.reasonCounts,
REASONS,
'reason counts',
),
});
} finally {
tag?.fill(0);
}
}
export async function publishLocalReconciliationReviewAuthorization(
options: Readonly<{
targetPath: string;
stagePath: string;
maxBytes: number;
header: Readonly<LocalReconciliationReviewAuthorizationHeader>;
keyProvider: LocalReconciliationReviewIssuerKeyringFileProvider;
writeDecisions: (
append: (decision: Readonly<LocalReconciliationReviewDecision>) => void,
) => Readonly<{
readonly decisionFileDigest: string;
readonly confirmDecisionFileAuthority: () => void;
}>;
confirmAuthority: () => void | Promise<void>;
}>,
): Promise<Readonly<LocalReconciliationReviewAuthorizationEvidence>> {
await options.confirmAuthority();
const keyringBefore = options.keyProvider.inspect();
let material: LocalSecretKeyMaterial | undefined;
let descriptor: number | undefined;
let created = false;
try {
material = await options.keyProvider.active();
if (
material.keyId !== keyringBefore.activeKeyId ||
material.key.byteLength !== 32
) {
configurationError('active issuer key drifted');
}
descriptor = fs.openSync(
options.stagePath,
fs.constants.O_CREAT |
fs.constants.O_EXCL |
fs.constants.O_WRONLY |
(fs.constants.O_NOFOLLOW ?? 0),
0o600,
);
created = true;
fs.fchmodSync(descriptor, 0o600);
const fileHash = createHash('sha256');
const contentHash = createHash('sha256');
const decisionHash = createHash('sha256').update(
'qinglong3.local-reconciliation-review-decision-set.v1\0',
);
let fileBytes = 0;
let contentBytes = 0;
let decisionCount = 0;
const dispositionCounts = zeroCounts(DISPOSITIONS);
const reasonCounts = zeroCounts(REASONS);
const writeContent = (value: unknown, isDecision: boolean): void => {
const line = canonicalLine(value);
try {
if (fileBytes + line.byteLength > options.maxBytes) {
configurationError('exceeds profile byte budget');
}
writeAll(descriptor!, line);
fileHash.update(line);
contentHash.update(line);
contentBytes += line.byteLength;
fileBytes += line.byteLength;
if (isDecision) decisionHash.update(line);
} finally {
line.fill(0);
}
};
writeContent(options.header, false);
const decisionFile = options.writeDecisions((selected) => {
const record = Object.freeze({ ...selected, kind: DECISION_KIND });
writeContent(record, true);
decisionCount += 1;
dispositionCounts[selected.disposition] += 1;
reasonCounts[selected.reason] += 1;
});
if (!DIGEST_PATTERN.test(decisionFile.decisionFileDigest)) {
configurationError('decision file evidence is invalid');
}
await options.confirmAuthority();
decisionFile.confirmDecisionFileAuthority();
const keyringAfter = options.keyProvider.inspect();
if (
keyringAfter.keyringDigest !== keyringBefore.keyringDigest ||
keyringAfter.activeKeyId !== material.keyId
) {
configurationError('issuer authority changed while signing');
}
const contentDigest = contentHash.digest('hex');
const decisionSetDigest = decisionHash.digest('hex');
const message = signingMessage(
contentDigest,
contentBytes,
decisionFile.decisionFileDigest,
);
const authenticationTag = createHmac('sha256', material.key)
.update(message)
.digest('base64url');
message.fill(0);
await options.confirmAuthority();
decisionFile.confirmDecisionFileAuthority();
const keyringSigned = options.keyProvider.inspect();
if (
keyringSigned.keyringDigest !== keyringBefore.keyringDigest ||
keyringSigned.activeKeyId !== material.keyId
) {
configurationError('issuer authority changed after signing');
}
const signature: SignatureRecord = Object.freeze({
schemaVersion: 1,
kind: SIGNATURE_KIND,
algorithm: 'hmac-sha256',
keyId: material.keyId,
contentBytes,
contentDigest,
decisionCount,
decisionFileDigest: decisionFile.decisionFileDigest,
decisionSetDigest,
dispositionCounts: Object.freeze(dispositionCounts),
reasonCounts: Object.freeze(reasonCounts),
authenticationTag,
});
const signatureLine = canonicalLine(signature);
try {
if (fileBytes + signatureLine.byteLength > options.maxBytes) {
configurationError('exceeds profile byte budget');
}
writeAll(descriptor, signatureLine);
fileHash.update(signatureLine);
fileBytes += signatureLine.byteLength;
} finally {
signatureLine.fill(0);
}
fs.fsyncSync(descriptor);
fs.closeSync(descriptor);
descriptor = undefined;
try {
fs.linkSync(options.stagePath, options.targetPath);
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
configurationError('target already exists');
}
syncDirectory(path.dirname(options.targetPath));
fs.unlinkSync(options.stagePath);
created = false;
syncDirectory(path.dirname(options.stagePath));
return Object.freeze({
fileBytes,
authorizationDigest: fileHash.digest('hex'),
decisionCount,
decisionFileDigest: decisionFile.decisionFileDigest,
decisionSetDigest,
keyId: material.keyId,
dispositionCounts: Object.freeze(dispositionCounts),
reasonCounts: Object.freeze(reasonCounts),
header: options.header,
});
} catch (error) {
if (error instanceof LocalDeploymentConfigurationError) throw error;
return configurationError('cannot be published', error);
} finally {
wipe(material);
if (descriptor !== undefined) fs.closeSync(descriptor);
if (created) {
try {
fs.unlinkSync(options.stagePath);
} catch {
// A complete signed stage may be recovered; partial stages fail closed.
}
}
}
}
function parseLine(line: Buffer): unknown {
if (line.byteLength < 2 || line.byteLength > MAX_LINE_BYTES) {
configurationError('line bound is invalid');
}
try {
return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(line));
} catch (error) {
configurationError('line is not valid UTF-8 JSON', error);
}
}
export async function verifyLocalReconciliationReviewAuthorization(
filePath: string,
options: Readonly<{
maxBytes: number;
allowedModes: readonly number[];
keyProvider: LocalReconciliationReviewIssuerKeyringFileProvider;
expected: Readonly<{
reviewId: string;
profile: 'edge' | 'standalone';
planDigest: string;
preparationDigest: string;
bundleDigest: string;
bundleFingerprintDigest: string;
preparedHeadDigest: string;
}>;
}>,
): Promise<Readonly<LocalReconciliationReviewAuthorizationEvidence>> {
const uid = process.getuid?.();
if (!Number.isSafeInteger(uid) || uid !== process.geteuid?.()) {
configurationError('requires stable POSIX identity');
}
const before = fs.lstatSync(filePath, { bigint: true });
if (
!before.isFile() ||
before.isSymbolicLink() ||
Number(before.uid) !== uid ||
!options.allowedModes.includes(Number(before.mode) & 0o777) ||
before.nlink !== 1n ||
before.size < 2n ||
before.size > BigInt(options.maxBytes)
) {
configurationError('file identity or size is invalid');
}
let descriptor: number | undefined;
let material: LocalSecretKeyMaterial | null = null;
try {
descriptor = fs.openSync(
filePath,
fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0),
);
const opened = fs.fstatSync(descriptor, { bigint: true });
if (
opened.dev !== before.dev ||
opened.ino !== before.ino ||
opened.size !== before.size
) {
configurationError('file changed while opening');
}
const chunk = Buffer.allocUnsafe(READ_CHUNK_BYTES);
const fileHash = createHash('sha256');
const contentHash = createHash('sha256');
const decisionHash = createHash('sha256').update(
'qinglong3.local-reconciliation-review-decision-set.v1\0',
);
let offset = 0;
let length = 0;
let fileBytes = 0;
let contentBytes = 0;
const lineBuffer = Buffer.allocUnsafe(MAX_LINE_BYTES);
let lineLength = 0;
const nextLine = (): Buffer | null => {
while (true) {
if (offset >= length) {
length = fs.readSync(descriptor!, chunk, 0, chunk.byteLength, null);
offset = 0;
if (length === 0) {
if (lineLength !== 0) configurationError('must end with newline');
return null;
}
fileBytes += length;
fileHash.update(chunk.subarray(0, length));
}
const byte = chunk[offset++]!;
if (byte === 0x0a) {
const line = Buffer.from(lineBuffer.subarray(0, lineLength));
lineLength = 0;
return line;
}
if (lineLength >= MAX_LINE_BYTES)
configurationError('line is too large');
lineBuffer[lineLength] = byte;
lineLength += 1;
}
};
const updateContent = (
line: Buffer,
includeInDecisionSet: boolean,
): void => {
contentHash.update(line).update('\n');
contentBytes += line.byteLength + 1;
if (includeInDecisionSet) decisionHash.update(line).update('\n');
};
const first = nextLine();
if (first === null) configurationError('header is absent');
const normalizedHeader = normalizeHeader(parseLine(first));
updateContent(first, false);
for (const [key, expected] of Object.entries(options.expected)) {
if (
normalizedHeader[
key as keyof LocalReconciliationReviewAuthorizationHeader
] !== expected
) {
configurationError('header binding drifted');
}
}
const dispositionCounts = zeroCounts(DISPOSITIONS);
const reasonCounts = zeroCounts(REASONS);
let decisionCount = 0;
let pending = nextLine();
if (pending === null) configurationError('signature is absent');
while (true) {
const following = nextLine();
if (following === null) break;
const selected = authorizationDecision(parseLine(pending));
updateContent(pending, true);
decisionCount += 1;
dispositionCounts[selected.disposition] += 1;
reasonCounts[selected.reason] += 1;
pending.fill(0);
pending = following;
}
const signature = parseSignature(parseLine(pending));
pending.fill(0);
const contentDigest = contentHash.digest('hex');
const decisionSetDigest = decisionHash.digest('hex');
if (
signature.contentBytes !== contentBytes ||
signature.contentDigest !== contentDigest ||
signature.decisionCount !== decisionCount ||
signature.decisionSetDigest !== decisionSetDigest ||
JSON.stringify(signature.dispositionCounts) !==
JSON.stringify(dispositionCounts) ||
JSON.stringify(signature.reasonCounts) !== JSON.stringify(reasonCounts)
) {
configurationError('signed summary drifted');
}
material = await options.keyProvider.resolve(signature.keyId);
if (!material || material.key.byteLength !== 32)
configurationError('issuer key is unavailable');
const message = signingMessage(
contentDigest,
contentBytes,
signature.decisionFileDigest,
);
const expectedTag = createHmac('sha256', material.key)
.update(message)
.digest();
message.fill(0);
const actualTag = Buffer.from(signature.authenticationTag, 'base64url');
const valid = timingSafeEqual(expectedTag, actualTag);
expectedTag.fill(0);
actualTag.fill(0);
if (!valid) configurationError('signature is invalid');
const after = fs.fstatSync(descriptor, { bigint: true });
const pathAfter = fs.lstatSync(filePath, { bigint: true });
if (
fileBytes !== Number(before.size) ||
after.dev !== before.dev ||
after.ino !== before.ino ||
after.size !== before.size ||
pathAfter.dev !== before.dev ||
pathAfter.ino !== before.ino ||
pathAfter.size !== before.size ||
pathAfter.mtimeNs !== before.mtimeNs ||
pathAfter.ctimeNs !== before.ctimeNs ||
pathAfter.mode !== before.mode ||
pathAfter.nlink !== before.nlink
) {
configurationError('file changed while reading');
}
return Object.freeze({
fileBytes,
authorizationDigest: fileHash.digest('hex'),
decisionCount,
decisionFileDigest: signature.decisionFileDigest,
decisionSetDigest,
keyId: signature.keyId,
dispositionCounts: Object.freeze(dispositionCounts),
reasonCounts: Object.freeze(reasonCounts),
header: normalizedHeader,
});
} catch (error) {
if (error instanceof LocalDeploymentConfigurationError) throw error;
return configurationError('cannot be verified', error);
} finally {
wipe(material);
if (descriptor !== undefined) fs.closeSync(descriptor);
}
}
@@ -0,0 +1,976 @@
import fs from 'node:fs';
import path from 'node:path';
import { readPrivateLocalCommandFile } from '@qinglong/local-command-file';
import {
establishAuthenticatedLocalCommand,
type AuthenticatedLocalCommand,
} from '@qinglong/local-owner-console/authenticated-command';
import { openLocalSqliteAuthenticationReadDatabase } from '@qinglong/local-sqlite/authentication-read';
import { currentIdentity } from '../../foundation/contract';
import { LocalDeploymentConfigurationError } from '../../foundation/error';
import {
publishExactFile,
validatePrivateDirectory,
} from '../../foundation/files';
import { cutoverDigest } from '../../cutover/targetEvidence';
import {
advanceLocalCutoverInstanceHead,
readLocalCutoverInstanceHead,
type LocalCutoverInstanceHead,
} from '../../cutover/instanceLineage';
import { readLocalReconciliationCaptureIntent } from '../preparation';
import { readLocalReconciliationPlanTerminal } from '../planning/preparation';
import { withLocalReconciliationSealedDatabase } from '../sealed-bundle/reader';
import {
publishLocalReconciliationReviewAuthorization,
verifyLocalReconciliationReviewAuthorization,
type LocalReconciliationReviewAuthorizationEvidence,
type LocalReconciliationReviewAuthorizationHeader,
} from './authorization';
import {
normalizeLocalReconciliationReviewCommitCommand,
normalizeLocalReconciliationReviewVerifyCommand,
type LocalReconciliationReviewCommitCommand,
type LocalReconciliationReviewTerminalResult,
type LocalReconciliationReviewVerifyCommand,
} from './completionContract';
import {
assertLocalReconciliationReviewDecisionMatchesFact,
MAX_EDGE_LOCAL_RECONCILIATION_REVIEW_DECISION_BYTES,
MAX_STANDALONE_LOCAL_RECONCILIATION_REVIEW_DECISION_BYTES,
withLocalReconciliationReviewDecisionFile,
} from './decisionFile';
import { visitLocalReconciliationDiagnosticFacts } from './diagnostics';
import {
ensureLocalReconciliationReviewIssuerKeyring,
LocalReconciliationReviewIssuerKeyringFileProvider,
} from './issuerKeyring';
import {
normalizeLocalReconciliationReviewIntent,
type LocalReconciliationReviewDependencies,
type LocalReconciliationReviewIntent,
} from './preparation';
import {
buildLocalReconciliationReview,
buildLocalReconciliationReviewReceipt,
normalizeLocalReconciliationReview,
normalizeLocalReconciliationReviewReceipt,
readLocalReconciliationReviewTerminalJson,
terminalEvidenceContents,
type LocalReconciliationReview,
type LocalReconciliationReviewReceipt,
} from './terminalEvidence';
const MAX_AUTHENTICATION_AGE_MS = 5 * 60 * 1_000;
const COMMIT_CLOCK_SKEW_MS = 60_000;
interface ReviewPaths {
readonly root: string;
readonly staging: string;
readonly intent: string;
readonly authorization: string;
readonly authorizationStage: string;
readonly review: string;
readonly receipt: string;
}
type AuthenticationDatabase = Awaited<
ReturnType<typeof openLocalSqliteAuthenticationReadDatabase>
>;
export interface LocalReconciliationReviewCompletionDependencies
extends LocalReconciliationReviewDependencies {
readonly openAuthenticationDatabase?: typeof openLocalSqliteAuthenticationReadDatabase;
readonly authenticate?: typeof establishAuthenticatedLocalCommand;
readonly now?: () => number;
readonly afterAuthorizationPublished?: () => void;
readonly afterReviewPublished?: () => void;
readonly afterReceiptPublished?: () => void;
readonly afterTerminalSealed?: () => void;
readonly afterHeadAdvanced?: () => void;
}
interface TerminalReview {
readonly intent: Readonly<LocalReconciliationReviewIntent>;
readonly authorization: Readonly<LocalReconciliationReviewAuthorizationEvidence>;
readonly review: Readonly<LocalReconciliationReview>;
readonly receipt: Readonly<LocalReconciliationReviewReceipt>;
readonly head: Readonly<LocalCutoverInstanceHead>;
}
function configurationError(message: string, cause?: unknown): never {
throw new LocalDeploymentConfigurationError(message, { cause });
}
function paths(reviewRoot: string, reviewId: string): Readonly<ReviewPaths> {
const root = path.join(reviewRoot, reviewId);
const staging = path.join(root, 'staging');
return Object.freeze({
root,
staging,
intent: path.join(root, 'intent.json'),
authorization: path.join(root, 'authorization.ndjson'),
authorizationStage: path.join(staging, 'authorization.ndjson.stage'),
review: path.join(root, 'review.json'),
receipt: path.join(root, 'receipt.json'),
});
}
function validateDirectory(
directory: string,
uid: number,
modes: readonly number[],
label: string,
): number {
let stat: fs.Stats;
try {
stat = fs.lstatSync(directory);
} catch (error) {
configurationError(`${label} is unavailable`, error);
}
const mode = stat.mode & 0o777;
if (
!stat.isDirectory() ||
stat.isSymbolicLink() ||
stat.uid !== uid ||
!modes.includes(mode) ||
fs.realpathSync(directory) !== directory
) {
configurationError(`${label} identity is invalid`);
}
return mode;
}
function validateCatalog(
selected: Readonly<ReviewPaths>,
terminal: boolean,
): void {
const allowed = new Set([
'authorization.ndjson',
'intent.json',
'receipt.json',
'review.json',
'staging',
...(!terminal
? [
'.intent.json.ql3-deploy-stage',
'.receipt.json.ql3-deploy-stage',
'.review.json.ql3-deploy-stage',
]
: []),
]);
for (const entry of fs.readdirSync(selected.root, { withFileTypes: true })) {
if (!allowed.has(entry.name) || entry.isSymbolicLink()) {
configurationError(
'reconciliation review root contains unknown material',
);
}
}
const stagingEntries = fs.readdirSync(selected.staging);
if (
terminal
? stagingEntries.length !== 0
: stagingEntries.some((entry) => entry !== 'authorization.ndjson.stage')
) {
configurationError(
'reconciliation review staging contains unknown material',
);
}
}
function readIntent(
selected: Readonly<ReviewPaths>,
uid: number,
): Readonly<LocalReconciliationReviewIntent> {
return normalizeLocalReconciliationReviewIntent(
readLocalReconciliationReviewTerminalJson(
selected.intent,
uid,
[0o600, 0o400],
),
);
}
function validateIntentBinding(
command: Readonly<LocalReconciliationReviewCommitCommand>,
intent: Readonly<LocalReconciliationReviewIntent>,
): void {
if (
intent.command.options.deploymentRoot !== command.options.deploymentRoot ||
intent.command.options.captureRoot !== command.options.captureRoot ||
intent.command.options.planRoot !== command.options.planRoot ||
intent.command.options.reviewRoot !== command.options.reviewRoot ||
intent.command.options.allowRootService !==
command.options.allowRootService ||
intent.command.request.reviewId !== command.request.reviewId ||
intent.preparationDigest !== command.request.expectedPreparationDigest
) {
configurationError('review commit is detached from its preparation');
}
}
function validateHeadIdentity(
head: Readonly<LocalCutoverInstanceHead>,
intent: Readonly<LocalReconciliationReviewIntent>,
): void {
if (
head.profile !== intent.profile ||
head.cutoverId !== intent.cutoverId ||
head.activationDigest !== intent.activationDigest ||
head.generation !== intent.generation
) {
configurationError('reconciliation review instance head identity drifted');
}
}
function expectedAuthorization(
intent: Readonly<LocalReconciliationReviewIntent>,
preparedHeadDigest: string,
): Readonly<{
reviewId: string;
profile: 'edge' | 'standalone';
planDigest: string;
preparationDigest: string;
bundleDigest: string;
bundleFingerprintDigest: string;
preparedHeadDigest: string;
}> {
return Object.freeze({
reviewId: intent.command.request.reviewId,
profile: intent.profile,
planDigest: intent.command.request.expectedPlanDigest,
preparationDigest: intent.preparationDigest,
bundleDigest: intent.bundleDigest,
bundleFingerprintDigest: intent.bundleFingerprintDigest,
preparedHeadDigest,
});
}
function maxAuthorizationBytes(profile: 'edge' | 'standalone'): number {
return profile === 'edge'
? MAX_EDGE_LOCAL_RECONCILIATION_REVIEW_DECISION_BYTES
: MAX_STANDALONE_LOCAL_RECONCILIATION_REVIEW_DECISION_BYTES;
}
function strongPrincipal(
authenticated: Readonly<AuthenticatedLocalCommand>,
committedAtMs: number,
): Readonly<LocalReconciliationReviewAuthorizationHeader['reviewer']> {
const principal = authenticated.principal;
if (
principal.subject.type !== 'user' ||
!['hardware', 'local_console', 'multi_factor'].includes(
principal.assurance,
) ||
principal.authenticatedAtMs > committedAtMs ||
committedAtMs - principal.authenticatedAtMs > MAX_AUTHENTICATION_AGE_MS ||
principal.expiresAtMs <= committedAtMs
) {
configurationError(
'review commit requires a recent strongly authenticated User',
);
}
return principal;
}
function advanceReviewedHead(
intent: Readonly<LocalReconciliationReviewIntent>,
committedAtMs: number,
reviewDigest: string,
uid: number,
): Readonly<LocalCutoverInstanceHead> {
return advanceLocalCutoverInstanceHead(
{
options: { deploymentRoot: intent.command.options.deploymentRoot },
request: {
cutoverId: intent.cutoverId,
profile: intent.profile,
instanceId: intent.instanceId,
expectedActivationDigest: intent.activationDigest,
requestedAtMs: committedAtMs,
},
},
uid,
'reconciliation_reviewed',
intent.generation,
reviewDigest,
);
}
function sealFile(filePath: string, uid: number): void {
let descriptor: number | undefined;
try {
const before = fs.lstatSync(filePath, { bigint: true });
if (
!before.isFile() ||
before.isSymbolicLink() ||
Number(before.uid) !== uid ||
![0o600, 0o400].includes(Number(before.mode) & 0o777) ||
before.nlink !== 1n
) {
configurationError('review terminal file cannot be sealed');
}
descriptor = fs.openSync(
filePath,
fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0),
);
const opened = fs.fstatSync(descriptor, { bigint: true });
if (
opened.dev !== before.dev ||
opened.ino !== before.ino ||
opened.size !== before.size
) {
configurationError('review terminal file changed while sealing');
}
if ((Number(opened.mode) & 0o777) !== 0o400)
fs.fchmodSync(descriptor, 0o400);
fs.fsyncSync(descriptor);
} finally {
if (descriptor !== undefined) fs.closeSync(descriptor);
}
}
function sealDirectory(directory: string, uid: number): void {
const mode = validateDirectory(
directory,
uid,
[0o700, 0o500],
'review terminal directory',
);
const descriptor = fs.openSync(directory, fs.constants.O_RDONLY);
try {
if (mode !== 0o500) fs.fchmodSync(descriptor, 0o500);
fs.fsyncSync(descriptor);
} finally {
fs.closeSync(descriptor);
}
}
function sealTerminal(selected: Readonly<ReviewPaths>, uid: number): void {
if (fs.readdirSync(selected.staging).length !== 0) {
configurationError('review staging must be empty before terminal seal');
}
for (const filePath of [
selected.intent,
selected.authorization,
selected.review,
selected.receipt,
]) {
sealFile(filePath, uid);
}
sealDirectory(selected.staging, uid);
sealDirectory(selected.root, uid);
validateCatalog(selected, true);
}
function syncDirectory(directory: string): void {
const descriptor = fs.openSync(directory, fs.constants.O_RDONLY);
try {
fs.fsyncSync(descriptor);
} finally {
fs.closeSync(descriptor);
}
}
async function recoverAuthorizationStage(
selected: Readonly<ReviewPaths>,
intent: Readonly<LocalReconciliationReviewIntent>,
issuerKeyringPath: string,
preparedHeadDigest: string,
): Promise<void> {
if (!fs.existsSync(selected.authorizationStage)) return;
if (fs.existsSync(selected.authorization)) {
const stage = fs.statSync(selected.authorizationStage, { bigint: true });
const target = fs.statSync(selected.authorization, { bigint: true });
if (stage.dev !== target.dev || stage.ino !== target.ino) {
configurationError('authorization stage conflicts with published target');
}
fs.unlinkSync(selected.authorizationStage);
syncDirectory(selected.staging);
await verifyLocalReconciliationReviewAuthorization(selected.authorization, {
maxBytes: maxAuthorizationBytes(intent.profile),
allowedModes: [0o600],
keyProvider: new LocalReconciliationReviewIssuerKeyringFileProvider(
issuerKeyringPath,
),
expected: expectedAuthorization(intent, preparedHeadDigest),
});
return;
}
await verifyLocalReconciliationReviewAuthorization(
selected.authorizationStage,
{
maxBytes: maxAuthorizationBytes(intent.profile),
allowedModes: [0o600],
keyProvider: new LocalReconciliationReviewIssuerKeyringFileProvider(
issuerKeyringPath,
),
expected: expectedAuthorization(intent, preparedHeadDigest),
},
);
fs.linkSync(selected.authorizationStage, selected.authorization);
syncDirectory(selected.root);
fs.unlinkSync(selected.authorizationStage);
syncDirectory(selected.staging);
}
function validateReviewBinding(
intent: Readonly<LocalReconciliationReviewIntent>,
authorization: Readonly<LocalReconciliationReviewAuthorizationEvidence>,
review: Readonly<LocalReconciliationReview>,
receipt: Readonly<LocalReconciliationReviewReceipt>,
): void {
const reviewerDigest = cutoverDigest({
subject: authorization.header.reviewer.subject,
authenticationId: authorization.header.reviewer.authenticationId,
authenticatedAtMs: authorization.header.reviewer.authenticatedAtMs,
assurance: authorization.header.reviewer.assurance,
});
if (
review.reviewId !== intent.command.request.reviewId ||
review.profile !== intent.profile ||
review.planDigest !== intent.command.request.expectedPlanDigest ||
review.preparationDigest !== intent.preparationDigest ||
review.bundleDigest !== intent.bundleDigest ||
review.bundleFingerprintDigest !== intent.bundleFingerprintDigest ||
review.authorizationDigest !== authorization.authorizationDigest ||
review.decisionFileDigest !== authorization.decisionFileDigest ||
review.decisionSetDigest !== authorization.decisionSetDigest ||
review.decisionCount !== authorization.decisionCount ||
review.preparedHeadDigest !== authorization.header.preparedHeadDigest ||
review.issuedAtMs !== authorization.header.issuedAtMs ||
review.expiresAtMs !== authorization.header.expiresAtMs ||
review.committedAtMs !== authorization.header.issuedAtMs ||
review.reviewerDigest !== reviewerDigest ||
JSON.stringify(review.dispositionCounts) !==
JSON.stringify(authorization.dispositionCounts) ||
JSON.stringify(review.reasonCounts) !==
JSON.stringify(authorization.reasonCounts) ||
receipt.reviewId !== review.reviewId ||
receipt.planDigest !== review.planDigest ||
receipt.preparationDigest !== review.preparationDigest ||
receipt.authorizationDigest !== review.authorizationDigest ||
receipt.decisionSetDigest !== review.decisionSetDigest ||
receipt.decisionCount !== review.decisionCount ||
receipt.keyId !== authorization.keyId ||
receipt.reviewDigest !== review.reviewDigest ||
receipt.committedAtMs !== review.committedAtMs
) {
configurationError('terminal reconciliation review binding drifted');
}
}
async function readTerminal(
selected: Readonly<ReviewPaths>,
intent: Readonly<LocalReconciliationReviewIntent>,
issuerKeyringPath: string,
uid: number,
allowedModes: readonly number[],
): Promise<
Readonly<{
authorization: Readonly<LocalReconciliationReviewAuthorizationEvidence>;
review: Readonly<LocalReconciliationReview>;
receipt: Readonly<LocalReconciliationReviewReceipt>;
}>
> {
const review = normalizeLocalReconciliationReview(
readLocalReconciliationReviewTerminalJson(
selected.review,
uid,
allowedModes,
),
);
const receipt = normalizeLocalReconciliationReviewReceipt(
readLocalReconciliationReviewTerminalJson(
selected.receipt,
uid,
allowedModes,
),
);
const authorization = await verifyLocalReconciliationReviewAuthorization(
selected.authorization,
{
maxBytes: maxAuthorizationBytes(intent.profile),
allowedModes,
keyProvider: new LocalReconciliationReviewIssuerKeyringFileProvider(
issuerKeyringPath,
),
expected: expectedAuthorization(intent, review.preparedHeadDigest),
},
);
validateReviewBinding(intent, authorization, review, receipt);
return Object.freeze({ authorization, review, receipt });
}
function result(
operation: LocalReconciliationReviewTerminalResult['operation'],
status: LocalReconciliationReviewTerminalResult['status'],
terminal: Readonly<{
authorization: Readonly<LocalReconciliationReviewAuthorizationEvidence>;
review: Readonly<LocalReconciliationReview>;
}>,
head: Readonly<LocalCutoverInstanceHead>,
): Readonly<LocalReconciliationReviewTerminalResult> {
return Object.freeze({
schemaVersion: 1,
operation,
status,
state: 'reconciliation_reviewed',
reviewId: terminal.review.reviewId,
reviewDigest: terminal.review.reviewDigest,
authorizationDigest: terminal.authorization.authorizationDigest,
decisionSetDigest: terminal.authorization.decisionSetDigest,
decisionCount: terminal.authorization.decisionCount,
instanceHeadDigest: head.headDigest,
});
}
async function confirmCurrentAuthority(
authenticated: Readonly<AuthenticatedLocalCommand>,
command: Readonly<LocalReconciliationReviewCommitCommand>,
intent: Readonly<LocalReconciliationReviewIntent>,
uid: number,
): Promise<void> {
await authenticated.confirm();
const terminal = readLocalReconciliationPlanTerminal(
command.options.planRoot,
intent.command.request.planId,
uid,
);
if (
terminal.plan.planDigest !== intent.command.request.expectedPlanDigest ||
terminal.plan.bundleDigest !== intent.bundleDigest ||
terminal.bundle.fingerprintDigest !== intent.bundleFingerprintDigest
) {
configurationError('review authority lost exact plan or bundle binding');
}
const currentHead = readLocalCutoverInstanceHead(
command.options.deploymentRoot,
intent.instanceId,
uid,
);
validateHeadIdentity(currentHead, intent);
if (
currentHead.state !== 'reconciliation_review_prepared' ||
currentHead.headDigest !== command.request.expectedHeadDigest ||
currentHead.sourceRecordDigest !== intent.preparationDigest
) {
configurationError('review authority lost prepared head binding');
}
}
async function publishAuthorization(
command: Readonly<LocalReconciliationReviewCommitCommand>,
intent: Readonly<LocalReconciliationReviewIntent>,
selected: Readonly<ReviewPaths>,
head: Readonly<LocalCutoverInstanceHead>,
terminal: ReturnType<typeof readLocalReconciliationPlanTerminal>,
authenticated: Readonly<AuthenticatedLocalCommand>,
dependencies: LocalReconciliationReviewCompletionDependencies,
uid: number,
): Promise<Readonly<LocalReconciliationReviewAuthorizationEvidence>> {
const reviewer = strongPrincipal(
authenticated,
command.request.committedAtMs,
);
ensureLocalReconciliationReviewIssuerKeyring(
command.options.issuerKeyringPath,
);
const keyProvider = new LocalReconciliationReviewIssuerKeyringFileProvider(
command.options.issuerKeyringPath,
);
const header: Readonly<LocalReconciliationReviewAuthorizationHeader> =
Object.freeze({
schemaVersion: 1,
kind: 'qinglong3-local-reconciliation-review-authorization-header',
reviewId: command.request.reviewId,
profile: intent.profile,
planDigest: terminal.plan.planDigest,
preparationDigest: intent.preparationDigest,
bundleDigest: terminal.bundle.receipt.bundleDigest,
bundleFingerprintDigest: terminal.bundle.fingerprintDigest,
preparedHeadDigest: head.headDigest,
reviewer,
issuedAtMs: command.request.committedAtMs,
expiresAtMs:
command.request.committedAtMs + command.request.authorizationLifetimeMs,
});
return publishLocalReconciliationReviewAuthorization({
targetPath: selected.authorization,
stagePath: selected.authorizationStage,
maxBytes: maxAuthorizationBytes(intent.profile),
header,
keyProvider,
confirmAuthority: () =>
confirmCurrentAuthority(authenticated, command, intent, uid),
writeDecisions: (append) => {
const reviewed = withLocalReconciliationReviewDecisionFile(
command.request.decisionFilePath,
{
reviewId: command.request.reviewId,
profile: intent.profile,
planDigest: terminal.plan.planDigest,
preparationDigest: intent.preparationDigest,
},
(cursor) => {
for (const database of ['legacy', 'target'] as const) {
const opened = withLocalReconciliationSealedDatabase(
terminal.bundle,
database,
uid,
dependencies,
(client) =>
visitLocalReconciliationDiagnosticFacts(
client,
database,
(fact) => {
if (fact.decisionRequirement === 'informational') return;
const decision = cursor.next();
if (decision === null) {
configurationError(
'review decision file omitted a canonical fact',
);
}
assertLocalReconciliationReviewDecisionMatchesFact(
decision,
fact,
);
append(decision);
},
),
);
if (opened === null) {
configurationError(
'manual-required SQLite topology cannot be reviewed',
);
}
}
},
);
return Object.freeze({
decisionFileDigest: reviewed.evidence.fileDigest,
confirmDecisionFileAuthority: reviewed.confirmIdentity,
});
},
});
}
export async function commitLocalReconciliationReview(
input: unknown,
dependencies: LocalReconciliationReviewCompletionDependencies = {},
): Promise<Readonly<LocalReconciliationReviewTerminalResult>> {
const command = normalizeLocalReconciliationReviewCommitCommand(input);
const uid = currentIdentity().uid;
validatePrivateDirectory(
command.options.deploymentRoot,
uid,
'deploymentRoot',
);
validatePrivateDirectory(command.options.captureRoot, uid, 'captureRoot');
validatePrivateDirectory(command.options.planRoot, uid, 'planRoot');
validatePrivateDirectory(command.options.reviewRoot, uid, 'reviewRoot');
const selected = paths(command.options.reviewRoot, command.request.reviewId);
validateDirectory(
selected.root,
uid,
[0o700, 0o500],
'reconciliationReviewDirectory',
);
validateDirectory(
selected.staging,
uid,
[0o700, 0o500],
'reconciliationReviewStaging',
);
const intent = readIntent(selected, uid);
validateIntentBinding(command, intent);
const terminalPlan = readLocalReconciliationPlanTerminal(
command.options.planRoot,
intent.command.request.planId,
uid,
);
if (
terminalPlan.plan.planDigest !==
intent.command.request.expectedPlanDigest ||
terminalPlan.plan.bundleDigest !== intent.bundleDigest ||
terminalPlan.bundle.fingerprintDigest !== intent.bundleFingerprintDigest
) {
configurationError('review commit lost terminal plan binding');
}
const captureIntent = readLocalReconciliationCaptureIntent(
command.options.captureRoot,
terminalPlan.plan.captureId,
);
if (
captureIntent.command.request.targetDatabasePath !==
command.options.targetDatabasePath
) {
configurationError(
'authentication database is detached from sealed target',
);
}
let head = readLocalCutoverInstanceHead(
command.options.deploymentRoot,
intent.instanceId,
uid,
);
validateHeadIdentity(head, intent);
if (fs.existsSync(selected.receipt)) {
const terminal = await readTerminal(
selected,
intent,
command.options.issuerKeyringPath,
uid,
[0o600, 0o400],
);
if (
terminal.review.committedAtMs !== command.request.committedAtMs ||
terminal.authorization.header.expiresAtMs !==
command.request.committedAtMs +
command.request.authorizationLifetimeMs ||
terminal.review.preparedHeadDigest !==
command.request.expectedHeadDigest ||
(head.state !== 'reconciliation_review_prepared' &&
head.state !== 'reconciliation_reviewed') ||
(head.state === 'reconciliation_review_prepared' &&
head.sourceRecordDigest !== intent.preparationDigest) ||
(head.state === 'reconciliation_reviewed' &&
head.sourceRecordDigest !== terminal.review.reviewDigest)
) {
configurationError('terminal review lost instance head binding');
}
sealTerminal(selected, uid);
const wasReviewed = head.state === 'reconciliation_reviewed';
head =
head.state === 'reconciliation_reviewed'
? head
: advanceReviewedHead(
intent,
terminal.review.committedAtMs,
terminal.review.reviewDigest,
uid,
);
dependencies.afterHeadAdvanced?.();
return result(
command.operation,
wasReviewed ? 'existing' : 'prepared',
terminal,
head,
);
}
if (
head.state !== 'reconciliation_review_prepared' ||
head.headDigest !== command.request.expectedHeadDigest ||
head.sourceRecordDigest !== intent.preparationDigest
) {
configurationError(
'review commit lost prepared instance head compare-and-swap',
);
}
validateCatalog(selected, false);
await recoverAuthorizationStage(
selected,
intent,
command.options.issuerKeyringPath,
command.request.expectedHeadDigest,
);
let authorization: Readonly<LocalReconciliationReviewAuthorizationEvidence>;
if (fs.existsSync(selected.authorization)) {
authorization = await verifyLocalReconciliationReviewAuthorization(
selected.authorization,
{
maxBytes: maxAuthorizationBytes(intent.profile),
allowedModes: [0o600, 0o400],
keyProvider: new LocalReconciliationReviewIssuerKeyringFileProvider(
command.options.issuerKeyringPath,
),
expected: expectedAuthorization(
intent,
command.request.expectedHeadDigest,
),
},
);
} else {
const now = (dependencies.now ?? Date.now)();
if (
!Number.isSafeInteger(now) ||
Math.abs(now - command.request.committedAtMs) > COMMIT_CLOCK_SKEW_MS
) {
configurationError(
'review commit timestamp is outside its bounded clock window',
);
}
const openDatabase =
dependencies.openAuthenticationDatabase ??
openLocalSqliteAuthenticationReadDatabase;
const database: AuthenticationDatabase = await openDatabase({
databasePath: command.options.targetDatabasePath,
profile: intent.profile,
...(command.options.busyTimeoutMs === undefined
? {}
: { busyTimeoutMs: command.options.busyTimeoutMs }),
});
try {
const authenticate =
dependencies.authenticate ?? establishAuthenticatedLocalCommand;
const authenticated = await authenticate(database, {
deploymentRoot: command.options.deploymentRoot,
databasePath: command.options.targetDatabasePath,
ownerPepperKeyringDirectory:
command.options.ownerPepperKeyringDirectory,
credentialFilePath: command.options.credentialFilePath,
authenticationNamespace: 'local_reconciliation_review',
now: () => command.request.committedAtMs,
});
authorization = await publishAuthorization(
command,
intent,
selected,
head,
terminalPlan,
authenticated,
dependencies,
uid,
);
dependencies.afterAuthorizationPublished?.();
} finally {
await database.close();
}
}
if (
authorization.header.issuedAtMs !== command.request.committedAtMs ||
authorization.header.expiresAtMs !==
command.request.committedAtMs + command.request.authorizationLifetimeMs
) {
configurationError(
'review authorization lifetime is detached from command',
);
}
const review = buildLocalReconciliationReview({
authorization,
decisionFileDigest: authorization.decisionFileDigest,
committedAtMs: command.request.committedAtMs,
});
publishExactFile(
selected.review,
terminalEvidenceContents(review),
0o600,
uid,
'reconciliation review',
);
dependencies.afterReviewPublished?.();
const receipt = buildLocalReconciliationReviewReceipt(
review,
authorization.keyId,
);
publishExactFile(
selected.receipt,
terminalEvidenceContents(receipt),
0o600,
uid,
'reconciliation review receipt',
);
dependencies.afterReceiptPublished?.();
validateReviewBinding(intent, authorization, review, receipt);
sealTerminal(selected, uid);
dependencies.afterTerminalSealed?.();
head = advanceReviewedHead(
intent,
review.committedAtMs,
review.reviewDigest,
uid,
);
dependencies.afterHeadAdvanced?.();
return result(command.operation, 'prepared', { authorization, review }, head);
}
export async function verifyLocalReconciliationReview(
input: unknown,
): Promise<Readonly<LocalReconciliationReviewTerminalResult>> {
const command = normalizeLocalReconciliationReviewVerifyCommand(input);
const uid = currentIdentity().uid;
validatePrivateDirectory(
command.options.deploymentRoot,
uid,
'deploymentRoot',
);
validatePrivateDirectory(command.options.captureRoot, uid, 'captureRoot');
validatePrivateDirectory(command.options.planRoot, uid, 'planRoot');
validatePrivateDirectory(command.options.reviewRoot, uid, 'reviewRoot');
const selected = paths(command.options.reviewRoot, command.request.reviewId);
validateDirectory(
selected.root,
uid,
[0o500],
'reconciliationReviewDirectory',
);
validateDirectory(
selected.staging,
uid,
[0o500],
'reconciliationReviewStaging',
);
validateCatalog(selected, true);
const intent = readIntent(selected, uid);
if (
intent.command.options.deploymentRoot !== command.options.deploymentRoot ||
intent.command.options.captureRoot !== command.options.captureRoot ||
intent.command.options.planRoot !== command.options.planRoot ||
intent.command.options.reviewRoot !== command.options.reviewRoot ||
intent.command.options.allowRootService !==
command.options.allowRootService ||
intent.command.request.reviewId !== command.request.reviewId
) {
configurationError('review verify is detached from preparation');
}
const terminalPlan = readLocalReconciliationPlanTerminal(
command.options.planRoot,
intent.command.request.planId,
uid,
);
if (
terminalPlan.plan.planDigest !==
intent.command.request.expectedPlanDigest ||
terminalPlan.plan.bundleDigest !== intent.bundleDigest ||
terminalPlan.bundle.fingerprintDigest !== intent.bundleFingerprintDigest
) {
configurationError('review verify lost terminal plan binding');
}
const terminal = await readTerminal(
selected,
intent,
command.options.issuerKeyringPath,
uid,
[0o400],
);
if (terminal.review.reviewDigest !== command.request.expectedReviewDigest) {
configurationError('review verify digest mismatch');
}
const head = readLocalCutoverInstanceHead(
command.options.deploymentRoot,
intent.instanceId,
uid,
);
validateHeadIdentity(head, intent);
if (
head.state !== 'reconciliation_reviewed' ||
head.sourceRecordDigest !== terminal.review.reviewDigest
) {
configurationError('review verify lost reviewed instance head');
}
return result(command.operation, 'verified', terminal, head);
}
export async function commitLocalReconciliationReviewCommandFile(
filePath: string,
): Promise<Readonly<LocalReconciliationReviewTerminalResult>> {
return commitLocalReconciliationReview(readPrivateLocalCommandFile(filePath));
}
export async function verifyLocalReconciliationReviewCommandFile(
filePath: string,
): Promise<Readonly<LocalReconciliationReviewTerminalResult>> {
return verifyLocalReconciliationReview(readPrivateLocalCommandFile(filePath));
}
@@ -0,0 +1,361 @@
import path from 'node:path';
import { currentIdentity } from '../../foundation/contract';
import { LocalDeploymentConfigurationError } from '../../foundation/error';
import type { LocalReconciliationReviewOptions } from './contract';
const DIGEST_PATTERN = /^[0-9a-f]{64}$/;
const SAFE_PATH_PATTERN = /^\/[A-Za-z0-9._/@-]+$/;
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 MAX_PATH_BYTES = 4_096;
export const MAX_LOCAL_RECONCILIATION_REVIEW_AUTHORIZATION_LIFETIME_MS =
30 * 60 * 1_000;
export interface LocalReconciliationReviewCommitOptions
extends LocalReconciliationReviewOptions {
readonly targetDatabasePath: string;
readonly ownerPepperKeyringDirectory: string;
readonly credentialFilePath: string;
readonly issuerKeyringPath: string;
readonly busyTimeoutMs?: number;
}
export interface LocalReconciliationReviewCommitCommand {
readonly schemaVersion: 1;
readonly operation: 'local.deployment.reconciliation.review.commit';
readonly options: Readonly<LocalReconciliationReviewCommitOptions>;
readonly request: Readonly<{
reviewId: string;
expectedPreparationDigest: string;
expectedHeadDigest: string;
decisionFilePath: string;
committedAtMs: number;
authorizationLifetimeMs: number;
}>;
}
export interface LocalReconciliationReviewVerifyCommand {
readonly schemaVersion: 1;
readonly operation: 'local.deployment.reconciliation.review.verify';
readonly options: Readonly<
LocalReconciliationReviewOptions & { readonly issuerKeyringPath: string }
>;
readonly request: Readonly<{
reviewId: string;
expectedReviewDigest: string;
}>;
}
export interface LocalReconciliationReviewTerminalResult {
readonly schemaVersion: 1;
readonly operation:
| 'local.deployment.reconciliation.review.commit'
| 'local.deployment.reconciliation.review.verify';
readonly status: 'prepared' | 'existing' | 'verified';
readonly state: 'reconciliation_reviewed';
readonly reviewId: string;
readonly reviewDigest: string;
readonly authorizationDigest: string;
readonly decisionSetDigest: string;
readonly decisionCount: number;
readonly instanceHeadDigest: string;
}
function configurationError(message: string): never {
throw new LocalDeploymentConfigurationError(message);
}
function object(value: unknown, label: string): Record<string, unknown> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
configurationError(`${label} must be an object`);
}
return value as Record<string, unknown>;
}
function exact(
value: Record<string, unknown>,
keys: readonly string[],
label: string,
): void {
const actual = Object.keys(value).sort();
const expected = [...keys].sort();
if (
actual.length !== expected.length ||
actual.some((key, index) => key !== expected[index])
) {
configurationError(`${label} shape is invalid`);
}
}
function safePath(value: unknown, label: string): string {
if (
typeof value !== 'string' ||
!path.isAbsolute(value) ||
path.parse(value).root === value ||
path.normalize(value) !== value ||
value.includes('\0') ||
value.includes('//') ||
!SAFE_PATH_PATTERN.test(value) ||
Buffer.byteLength(value, 'utf8') > MAX_PATH_BYTES
) {
configurationError(`${label} must be a safe non-root absolute path`);
}
return value;
}
function digest(value: unknown, label: string): string {
if (typeof value !== 'string' || !DIGEST_PATTERN.test(value)) {
configurationError(`${label} must be a SHA-256 digest`);
}
return value;
}
function reviewId(value: unknown): string {
if (typeof value !== 'string' || !UUID_V4_PATTERN.test(value)) {
configurationError('reviewId must be a UUID v4');
}
return value;
}
function descendant(root: string, candidate: string, label: string): void {
const relative = path.relative(root, candidate);
if (
relative.length === 0 ||
relative === '..' ||
relative.startsWith(`..${path.sep}`) ||
path.isAbsolute(relative)
) {
configurationError(`${label} must be a descendant of deploymentRoot`);
}
}
function overlaps(left: string, right: string): boolean {
const relative = path.relative(left, right);
return (
relative === '' ||
(!relative.startsWith('..') && !path.isAbsolute(relative))
);
}
function roots(
value: Record<string, unknown>,
): Readonly<LocalReconciliationReviewOptions> {
const identity = currentIdentity();
if (
typeof value.allowRootService !== 'boolean' ||
(identity.uid === 0) !== value.allowRootService
) {
configurationError('reconciliation review command identity is invalid');
}
const selected = [
safePath(value.deploymentRoot, 'deploymentRoot'),
safePath(value.captureRoot, 'captureRoot'),
safePath(value.planRoot, 'planRoot'),
safePath(value.reviewRoot, 'reviewRoot'),
];
for (let left = 0; left < selected.length; left += 1) {
for (let right = left + 1; right < selected.length; right += 1) {
if (
overlaps(selected[left]!, selected[right]!) ||
overlaps(selected[right]!, selected[left]!)
) {
configurationError(
'deployment, capture, plan and review roots must not overlap',
);
}
}
}
return Object.freeze({
deploymentRoot: selected[0]!,
captureRoot: selected[1]!,
planRoot: selected[2]!,
reviewRoot: selected[3]!,
allowRootService: value.allowRootService,
});
}
export function normalizeLocalReconciliationReviewCommitCommand(
value: unknown,
): Readonly<LocalReconciliationReviewCommitCommand> {
const command = object(value, 'reconciliation review commit command');
exact(
command,
['operation', 'options', 'request', 'schemaVersion'],
'command',
);
if (
command.schemaVersion !== 1 ||
command.operation !== 'local.deployment.reconciliation.review.commit'
) {
configurationError('reconciliation review commit command is invalid');
}
const options = object(command.options, 'options');
const hasBusyTimeout = Object.hasOwn(options, 'busyTimeoutMs');
exact(
options,
[
'allowRootService',
'captureRoot',
'credentialFilePath',
'deploymentRoot',
'issuerKeyringPath',
'ownerPepperKeyringDirectory',
'planRoot',
'reviewRoot',
'targetDatabasePath',
...(hasBusyTimeout ? ['busyTimeoutMs'] : []),
],
'options',
);
const normalizedRoots = roots(options);
const targetDatabasePath = safePath(
options.targetDatabasePath,
'targetDatabasePath',
);
const ownerPepperKeyringDirectory = safePath(
options.ownerPepperKeyringDirectory,
'ownerPepperKeyringDirectory',
);
const credentialFilePath = safePath(
options.credentialFilePath,
'credentialFilePath',
);
const issuerKeyringPath = safePath(
options.issuerKeyringPath,
'issuerKeyringPath',
);
for (const [label, selected] of [
['ownerPepperKeyringDirectory', ownerPepperKeyringDirectory],
['credentialFilePath', credentialFilePath],
['issuerKeyringPath', issuerKeyringPath],
] as const) {
descendant(normalizedRoots.deploymentRoot, selected, label);
}
if (
options.busyTimeoutMs !== undefined &&
(!Number.isSafeInteger(options.busyTimeoutMs) ||
(options.busyTimeoutMs as number) < 100 ||
(options.busyTimeoutMs as number) > 30_000)
) {
configurationError('busyTimeoutMs is invalid');
}
const request = object(command.request, 'request');
exact(
request,
[
'authorizationLifetimeMs',
'committedAtMs',
'decisionFilePath',
'expectedHeadDigest',
'expectedPreparationDigest',
'reviewId',
],
'request',
);
const decisionFilePath = safePath(
request.decisionFilePath,
'decisionFilePath',
);
if (
[
normalizedRoots.deploymentRoot,
normalizedRoots.captureRoot,
normalizedRoots.planRoot,
normalizedRoots.reviewRoot,
].some(
(root) =>
overlaps(root, decisionFilePath) || overlaps(decisionFilePath, root),
) ||
!Number.isSafeInteger(request.committedAtMs) ||
(request.committedAtMs as number) < 0 ||
!Number.isSafeInteger(request.authorizationLifetimeMs) ||
(request.authorizationLifetimeMs as number) < 1 ||
(request.authorizationLifetimeMs as number) >
MAX_LOCAL_RECONCILIATION_REVIEW_AUTHORIZATION_LIFETIME_MS
) {
configurationError('reconciliation review commit request is invalid');
}
return Object.freeze({
schemaVersion: 1 as const,
operation: 'local.deployment.reconciliation.review.commit' as const,
options: Object.freeze({
...normalizedRoots,
targetDatabasePath,
ownerPepperKeyringDirectory,
credentialFilePath,
issuerKeyringPath,
...(options.busyTimeoutMs === undefined
? {}
: { busyTimeoutMs: options.busyTimeoutMs as number }),
}),
request: Object.freeze({
reviewId: reviewId(request.reviewId),
expectedPreparationDigest: digest(
request.expectedPreparationDigest,
'expectedPreparationDigest',
),
expectedHeadDigest: digest(
request.expectedHeadDigest,
'expectedHeadDigest',
),
decisionFilePath,
committedAtMs: request.committedAtMs as number,
authorizationLifetimeMs: request.authorizationLifetimeMs as number,
}),
});
}
export function normalizeLocalReconciliationReviewVerifyCommand(
value: unknown,
): Readonly<LocalReconciliationReviewVerifyCommand> {
const command = object(value, 'reconciliation review verify command');
exact(
command,
['operation', 'options', 'request', 'schemaVersion'],
'command',
);
if (
command.schemaVersion !== 1 ||
command.operation !== 'local.deployment.reconciliation.review.verify'
) {
configurationError('reconciliation review verify command is invalid');
}
const options = object(command.options, 'options');
exact(
options,
[
'allowRootService',
'captureRoot',
'deploymentRoot',
'issuerKeyringPath',
'planRoot',
'reviewRoot',
],
'options',
);
const normalizedRoots = roots(options);
const issuerKeyringPath = safePath(
options.issuerKeyringPath,
'issuerKeyringPath',
);
descendant(
normalizedRoots.deploymentRoot,
issuerKeyringPath,
'issuerKeyringPath',
);
const request = object(command.request, 'request');
exact(request, ['expectedReviewDigest', 'reviewId'], 'request');
return Object.freeze({
schemaVersion: 1 as const,
operation: 'local.deployment.reconciliation.review.verify' as const,
options: Object.freeze({ ...normalizedRoots, issuerKeyringPath }),
request: Object.freeze({
reviewId: reviewId(request.reviewId),
expectedReviewDigest: digest(
request.expectedReviewDigest,
'expectedReviewDigest',
),
}),
});
}
@@ -0,0 +1,439 @@
import { createHash } from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import { LocalDeploymentConfigurationError } from '../../foundation/error';
import type { LocalReconciliationDiagnosticFact } from './diagnostics';
const HEADER_KIND = 'qinglong3-local-reconciliation-review-decision-header';
const DECISION_KIND = 'qinglong3-local-reconciliation-review-decision';
const DIGEST_PATTERN = /^[0-9a-f]{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 MAX_LINE_BYTES = 64 * 1024;
const READ_CHUNK_BYTES = 64 * 1024;
export const MAX_EDGE_LOCAL_RECONCILIATION_REVIEW_DECISION_BYTES =
8 * 1024 * 1024;
export const MAX_STANDALONE_LOCAL_RECONCILIATION_REVIEW_DECISION_BYTES =
32 * 1024 * 1024;
export const LOCAL_RECONCILIATION_REVIEW_DISPOSITIONS = Object.freeze([
'retain_target',
'adopt_legacy',
'retain_both',
'exclude_legacy',
'defer',
'manual_external',
] as const);
export type LocalReconciliationReviewDisposition =
(typeof LOCAL_RECONCILIATION_REVIEW_DISPOSITIONS)[number];
export const LOCAL_RECONCILIATION_REVIEW_REASONS = Object.freeze([
'preserve_target',
'prefer_legacy',
'preserve_both',
'legacy_excluded',
'deferred_review',
'external_recovery_required',
] as const);
export type LocalReconciliationReviewReason =
(typeof LOCAL_RECONCILIATION_REVIEW_REASONS)[number];
export interface LocalReconciliationReviewDecisionHeader {
readonly schemaVersion: 1;
readonly kind: typeof HEADER_KIND;
readonly diagnosticsContractVersion: 1;
readonly reviewId: string;
readonly profile: 'edge' | 'standalone';
readonly planDigest: string;
readonly preparationDigest: string;
}
export interface LocalReconciliationReviewDecision {
readonly schemaVersion: 1;
readonly kind: typeof DECISION_KIND;
readonly database: 'legacy' | 'target';
readonly domain: LocalReconciliationDiagnosticFact['domain'];
readonly factKind: LocalReconciliationDiagnosticFact['factKind'];
readonly ordinal: number;
readonly factDigest: string;
readonly disposition: LocalReconciliationReviewDisposition;
readonly reason: LocalReconciliationReviewReason;
}
export interface LocalReconciliationReviewDecisionFileEvidence {
readonly fileBytes: number;
readonly fileDigest: string;
readonly decisionCount: number;
}
export interface LocalReconciliationReviewDecisionCursor {
readonly header: Readonly<LocalReconciliationReviewDecisionHeader>;
next(): Readonly<LocalReconciliationReviewDecision> | null;
}
interface FileIdentity {
readonly device: bigint;
readonly inode: bigint;
readonly size: bigint;
readonly modifiedAtNs: bigint;
readonly changedAtNs: bigint;
readonly uid: number;
readonly mode: number;
readonly links: bigint;
}
function configurationError(message: string, cause?: unknown): never {
throw new LocalDeploymentConfigurationError(
`reconciliation review decision file ${message}`,
{ cause },
);
}
function exact(
value: unknown,
keys: readonly string[],
label: string,
): Record<string, unknown> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
configurationError(`${label} must be an object`);
}
const record = value as Record<string, unknown>;
const actual = Object.keys(record).sort();
const expected = [...keys].sort();
if (
actual.length !== expected.length ||
actual.some((key, index) => key !== expected[index])
) {
configurationError(`${label} shape is invalid`);
}
return record;
}
function parse(line: Buffer, label: string): unknown {
if (line.byteLength < 2 || line.byteLength > MAX_LINE_BYTES) {
configurationError(`${label} line bound is invalid`);
}
try {
return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(line));
} catch (error) {
configurationError(`${label} is not valid UTF-8 JSON`, error);
}
}
function header(
value: unknown,
expected: Readonly<{
reviewId: string;
profile: 'edge' | 'standalone';
planDigest: string;
preparationDigest: string;
}>,
): Readonly<LocalReconciliationReviewDecisionHeader> {
const record = exact(
value,
[
'diagnosticsContractVersion',
'kind',
'planDigest',
'preparationDigest',
'profile',
'reviewId',
'schemaVersion',
],
'header',
);
if (
record.schemaVersion !== 1 ||
record.kind !== HEADER_KIND ||
record.diagnosticsContractVersion !== 1 ||
typeof record.reviewId !== 'string' ||
!UUID_V4_PATTERN.test(record.reviewId) ||
(record.profile !== 'edge' && record.profile !== 'standalone') ||
typeof record.planDigest !== 'string' ||
!DIGEST_PATTERN.test(record.planDigest) ||
typeof record.preparationDigest !== 'string' ||
!DIGEST_PATTERN.test(record.preparationDigest) ||
record.reviewId !== expected.reviewId ||
record.profile !== expected.profile ||
record.planDigest !== expected.planDigest ||
record.preparationDigest !== expected.preparationDigest
) {
configurationError('header binding is invalid');
}
return Object.freeze(
record,
) as unknown as Readonly<LocalReconciliationReviewDecisionHeader>;
}
function decision(value: unknown): Readonly<LocalReconciliationReviewDecision> {
const record = exact(
value,
[
'database',
'disposition',
'domain',
'factDigest',
'factKind',
'kind',
'ordinal',
'reason',
'schemaVersion',
],
'decision',
);
if (
record.schemaVersion !== 1 ||
record.kind !== DECISION_KIND ||
(record.database !== 'legacy' && record.database !== 'target') ||
![
'schema_lineage',
'automation',
'secret_and_config',
'run_history',
'plugin_package',
'ai_and_tool',
'identity_policy_audit',
'unknown',
].includes(record.domain as string) ||
(record.factKind !== 'schema_object' && record.factKind !== 'table') ||
!Number.isSafeInteger(record.ordinal) ||
(record.ordinal as number) < 1 ||
(record.ordinal as number) > 4_096 ||
typeof record.factDigest !== 'string' ||
!DIGEST_PATTERN.test(record.factDigest) ||
!LOCAL_RECONCILIATION_REVIEW_DISPOSITIONS.includes(
record.disposition as LocalReconciliationReviewDisposition,
) ||
!LOCAL_RECONCILIATION_REVIEW_REASONS.includes(
record.reason as LocalReconciliationReviewReason,
)
) {
configurationError('decision is invalid');
}
const reasonByDisposition: Record<
LocalReconciliationReviewDisposition,
LocalReconciliationReviewReason
> = {
retain_target: 'preserve_target',
adopt_legacy: 'prefer_legacy',
retain_both: 'preserve_both',
exclude_legacy: 'legacy_excluded',
defer: 'deferred_review',
manual_external: 'external_recovery_required',
};
if (
reasonByDisposition[
record.disposition as LocalReconciliationReviewDisposition
] !== record.reason
) {
configurationError('decision reason does not match disposition');
}
return Object.freeze(
record,
) as unknown as Readonly<LocalReconciliationReviewDecision>;
}
function identity(stat: fs.BigIntStats): FileIdentity {
return Object.freeze({
device: stat.dev,
inode: stat.ino,
size: stat.size,
modifiedAtNs: stat.mtimeNs,
changedAtNs: stat.ctimeNs,
uid: Number(stat.uid),
mode: Number(stat.mode) & 0o777,
links: stat.nlink,
});
}
function same(left: FileIdentity, right: FileIdentity): boolean {
return Object.keys(left).every(
(key) =>
left[key as keyof FileIdentity] === right[key as keyof FileIdentity],
);
}
function assertParent(filePath: string, uid: number): FileIdentity {
const stat = fs.lstatSync(path.dirname(filePath), { bigint: true });
const selected = identity(stat);
if (
!stat.isDirectory() ||
stat.isSymbolicLink() ||
selected.uid !== uid ||
selected.mode !== 0o700 ||
fs.realpathSync(path.dirname(filePath)) !== path.dirname(filePath)
) {
configurationError('parent must be a canonical current-UID 0700 directory');
}
return selected;
}
export function assertLocalReconciliationReviewDecisionMatchesFact(
selected: Readonly<LocalReconciliationReviewDecision>,
fact: Readonly<LocalReconciliationDiagnosticFact>,
): void {
if (
selected.database !== fact.database ||
selected.domain !== fact.domain ||
selected.factKind !== fact.factKind ||
selected.ordinal !== fact.ordinal ||
selected.factDigest !== fact.factDigest
) {
configurationError('decision sequence does not match canonical facts');
}
if (
fact.decisionRequirement === 'informational' ||
(fact.decisionRequirement === 'blocked' &&
selected.disposition !== 'defer' &&
selected.disposition !== 'manual_external') ||
(fact.domain === 'run_history' &&
!['retain_target', 'retain_both', 'defer', 'manual_external'].includes(
selected.disposition,
)) ||
((fact.domain === 'secret_and_config' ||
fact.domain === 'identity_policy_audit' ||
fact.domain === 'unknown') &&
!['defer', 'manual_external'].includes(selected.disposition))
) {
configurationError(
'decision disposition is not allowed for canonical fact',
);
}
}
export function withLocalReconciliationReviewDecisionFile<T>(
filePath: string,
expected: Readonly<{
reviewId: string;
profile: 'edge' | 'standalone';
planDigest: string;
preparationDigest: string;
}>,
consume: (cursor: LocalReconciliationReviewDecisionCursor) => T,
): Readonly<{
result: T;
evidence: LocalReconciliationReviewDecisionFileEvidence;
confirmIdentity(): void;
}> {
const uid = process.getuid?.();
if (!Number.isSafeInteger(uid) || uid !== process.geteuid?.()) {
configurationError('requires stable POSIX identity');
}
const parent = assertParent(filePath, uid!);
const beforeStat = fs.lstatSync(filePath, { bigint: true });
const before = identity(beforeStat);
const maxBytes =
expected.profile === 'edge'
? MAX_EDGE_LOCAL_RECONCILIATION_REVIEW_DECISION_BYTES
: MAX_STANDALONE_LOCAL_RECONCILIATION_REVIEW_DECISION_BYTES;
if (
!beforeStat.isFile() ||
beforeStat.isSymbolicLink() ||
before.uid !== uid ||
before.mode !== 0o600 ||
before.links !== 1n ||
before.size < 2n ||
before.size > BigInt(maxBytes)
) {
configurationError('identity or size is invalid');
}
let descriptor: number | undefined;
try {
descriptor = fs.openSync(
filePath,
fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0),
);
const opened = identity(fs.fstatSync(descriptor, { bigint: true }));
if (!same(before, opened)) configurationError('changed while opening');
const chunk = Buffer.allocUnsafe(READ_CHUNK_BYTES);
let chunkOffset = 0;
let chunkLength = 0;
let bytesRead = 0;
const lineBuffer = Buffer.allocUnsafe(MAX_LINE_BYTES);
let lineLength = 0;
const fileHash = createHash('sha256');
const nextLine = (): Buffer | null => {
while (true) {
if (chunkOffset >= chunkLength) {
chunkLength = fs.readSync(
descriptor!,
chunk,
0,
chunk.byteLength,
null,
);
chunkOffset = 0;
if (chunkLength === 0) {
if (lineLength !== 0) configurationError('must end with newline');
return null;
}
bytesRead += chunkLength;
fileHash.update(chunk.subarray(0, chunkLength));
}
const byte = chunk[chunkOffset++]!;
if (byte === 0x0a) {
const line = Buffer.from(lineBuffer.subarray(0, lineLength));
lineLength = 0;
return line;
}
if (lineLength >= MAX_LINE_BYTES)
configurationError('line is too large');
lineBuffer[lineLength] = byte;
lineLength += 1;
}
};
const first = nextLine();
if (!first) configurationError('header is absent');
const normalizedHeader = header(parse(first, 'header'), expected);
let decisionCount = 0;
const cursor: LocalReconciliationReviewDecisionCursor = Object.freeze({
header: normalizedHeader,
next() {
const line = nextLine();
if (line === null) return null;
decisionCount += 1;
return decision(parse(line, 'decision'));
},
});
const result = consume(cursor);
if (cursor.next() !== null) configurationError('contains extra decisions');
const after = identity(fs.fstatSync(descriptor, { bigint: true }));
const currentParent = assertParent(filePath, uid!);
if (
bytesRead !== Number(before.size) ||
!same(before, after) ||
parent.device !== currentParent.device ||
parent.inode !== currentParent.inode ||
!same(before, identity(fs.lstatSync(filePath, { bigint: true })))
) {
configurationError('identity changed while reading');
}
const confirmIdentity = (): void => {
const latestParent = assertParent(filePath, uid!);
if (
parent.device !== latestParent.device ||
parent.inode !== latestParent.inode ||
!same(before, identity(fs.lstatSync(filePath, { bigint: true })))
) {
configurationError('identity changed after reading');
}
};
return Object.freeze({
result,
evidence: Object.freeze({
fileBytes: bytesRead,
fileDigest: fileHash.digest('hex'),
decisionCount,
}),
confirmIdentity,
});
} catch (error) {
if (error instanceof LocalDeploymentConfigurationError) throw error;
return configurationError('cannot be read', error);
} finally {
if (descriptor !== undefined) fs.closeSync(descriptor);
}
}
@@ -89,9 +89,7 @@ function quotedIdentifier(name: string): string {
return `"${name.replaceAll('"', '""')}"`;
}
function requirement(
domain: LocalReconciliationPlanDomain,
): Readonly<{
function requirement(domain: LocalReconciliationPlanDomain): Readonly<{
decisionRequirement: LocalReconciliationDiagnosticDecisionRequirement;
reason: LocalReconciliationDiagnosticReason;
}> {
@@ -188,7 +186,10 @@ function schemaFacts(
domain: LocalReconciliationPlanDomain,
offset: number,
limit: number,
): Readonly<{ records: readonly Readonly<LocalReconciliationDiagnosticFact>[]; complete: boolean }> {
): Readonly<{
records: readonly Readonly<LocalReconciliationDiagnosticFact>[];
complete: boolean;
}> {
const rows = client
.prepare(
`SELECT type, name, tbl_name AS tableName
@@ -208,7 +209,8 @@ function schemaFacts(
let complete = true;
for (const row of rows) {
catalog += 1;
if (catalog > 4_096) configurationError('diagnostic schema budget is exceeded');
if (catalog > 4_096)
configurationError('diagnostic schema budget is exceeded');
if (
typeof row.type !== 'string' ||
typeof row.name !== 'string' ||
@@ -250,7 +252,10 @@ function tableFacts(
domain: LocalReconciliationPlanDomain,
offset: number,
limit: number,
): Readonly<{ records: readonly Readonly<LocalReconciliationDiagnosticFact>[]; complete: boolean }> {
): Readonly<{
records: readonly Readonly<LocalReconciliationDiagnosticFact>[];
complete: boolean;
}> {
const rows = client
.prepare(
`SELECT name, type
@@ -269,7 +274,8 @@ function tableFacts(
let complete = true;
for (const row of rows) {
catalog += 1;
if (catalog > 512) configurationError('diagnostic table budget is exceeded');
if (catalog > 512)
configurationError('diagnostic table budget is exceeded');
if (
typeof row.name !== 'string' ||
typeof row.type !== 'string' ||
@@ -277,7 +283,8 @@ function tableFacts(
) {
configurationError('diagnostic table catalog drifted');
}
if (classifyLocalReconciliationFact(database, row.name) !== domain) continue;
if (classifyLocalReconciliationFact(database, row.name) !== domain)
continue;
const ordinal = seen + 1;
seen += 1;
if (seen <= offset) continue;
@@ -345,13 +352,57 @@ export function buildLocalReconciliationDiagnosticPage(
nextOffset,
records: selected.records,
});
const page = Object.freeze({ ...payload, pageDigest: cutoverDigest(payload) });
if (Buffer.byteLength(`${JSON.stringify(page, null, 2)}\n`, 'utf8') > MAX_PAGE_BYTES) {
const page = Object.freeze({
...payload,
pageDigest: cutoverDigest(payload),
});
if (
Buffer.byteLength(`${JSON.stringify(page, null, 2)}\n`, 'utf8') >
MAX_PAGE_BYTES
) {
configurationError('diagnostic page exceeds its 256 KiB budget');
}
return page;
}
/**
* Re-derives the complete bounded fact sequence without trusting diagnostic
* page files. One database descriptor is held by the caller and each query is
* bounded by the same catalog ceilings as diagnostics.
*/
export function visitLocalReconciliationDiagnosticFacts(
client: DatabaseSync,
database: LocalReconciliationSealedDatabaseKind,
visitor: (fact: Readonly<LocalReconciliationDiagnosticFact>) => void,
): void {
for (const domain of [
'schema_lineage',
'automation',
'secret_and_config',
'run_history',
'plugin_package',
'ai_and_tool',
'identity_policy_audit',
'unknown',
] as const satisfies readonly LocalReconciliationPlanDomain[]) {
for (const factKind of ['schema_object', 'table'] as const) {
let offset = 0;
while (true) {
const selected =
factKind === 'schema_object'
? schemaFacts(client, database, domain, offset, 64)
: tableFacts(client, database, domain, offset, 64);
for (const record of selected.records) visitor(record);
if (selected.complete) break;
if (selected.records.length < 1) {
configurationError('diagnostic canonical sequence did not advance');
}
offset += selected.records.length;
}
}
}
}
function pageBytes(page: Readonly<LocalReconciliationDiagnosticPage>): Buffer {
const bytes = Buffer.from(`${JSON.stringify(page, null, 2)}\n`, 'utf8');
if (bytes.byteLength < 2 || bytes.byteLength > MAX_PAGE_BYTES) {
@@ -396,11 +447,7 @@ function syncDirectory(directory: string): void {
}
}
function writeStage(
stagePath: string,
bytes: Buffer,
uid: number,
): void {
function writeStage(stagePath: string, bytes: Buffer, uid: number): void {
let descriptor: number | undefined;
let created = false;
try {
@@ -0,0 +1,404 @@
import { createHash, randomBytes } from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import {
assertLocalSecretKeyId,
type LocalSecretKeyMaterial,
type LocalSecretKeyProvider,
} from '@qinglong/runtime-core/local-secret';
import { LocalDeploymentConfigurationError } from '../../foundation/error';
const KEYRING_KIND = 'qinglong3-local-reconciliation-review-issuer-keyring';
const KEY_ID_PREFIX = 'qlrrk-';
const KEY_BYTES = 32;
const MAX_KEYRING_BYTES = 16 * 1024;
const MAX_PATH_BYTES = 4_096;
export const MAX_LOCAL_RECONCILIATION_REVIEW_ISSUER_KEYS = 8;
interface IssuerKeyRecord {
readonly generation: number;
readonly keyId: string;
readonly material: string;
}
interface IssuerKeyringManifest {
readonly schemaVersion: 1;
readonly kind: typeof KEYRING_KIND;
readonly activeGeneration: number;
readonly keys: readonly Readonly<IssuerKeyRecord>[];
}
export interface LocalReconciliationReviewIssuerKeyringSummary {
readonly schemaVersion: 1;
readonly kind: 'qinglong3-local-reconciliation-review-issuer-keyring-summary';
readonly activeGeneration: number;
readonly activeKeyId: string;
readonly keyCount: number;
readonly keyringDigest: string;
}
interface FileIdentity {
readonly device: bigint;
readonly inode: bigint;
readonly size: bigint;
readonly modifiedAtNs: bigint;
readonly changedAtNs: bigint;
}
function configurationError(message: string, cause?: unknown): never {
throw new LocalDeploymentConfigurationError(
`reconciliation review issuer keyring ${message}`,
{ cause },
);
}
function currentUid(): number {
if (
typeof process.getuid !== 'function' ||
typeof process.geteuid !== 'function'
) {
configurationError('requires POSIX identity');
}
const uid = process.getuid();
if (uid !== process.geteuid() || !Number.isSafeInteger(uid) || uid < 0) {
configurationError('requires stable POSIX identity');
}
return uid;
}
function boundedPath(value: string): string {
if (
typeof value !== 'string' ||
!path.isAbsolute(value) ||
path.parse(value).root === value ||
path.normalize(value) !== value ||
value.includes('\0') ||
Buffer.byteLength(value, 'utf8') > MAX_PATH_BYTES
) {
configurationError('path is invalid');
}
return value;
}
function assertParent(filePath: string, uid: number): fs.BigIntStats {
let stat: fs.BigIntStats;
try {
stat = fs.lstatSync(path.dirname(filePath), { bigint: true });
} catch (error) {
configurationError('parent is unavailable', error);
}
if (
!stat.isDirectory() ||
stat.isSymbolicLink() ||
Number(stat.uid) !== uid ||
(Number(stat.mode) & 0o777) !== 0o700 ||
fs.realpathSync(path.dirname(filePath)) !== path.dirname(filePath)
) {
configurationError('parent must be a canonical current-UID 0700 directory');
}
return stat;
}
function exact(value: object, keys: readonly string[]): boolean {
const actual = Object.keys(value).sort();
const expected = [...keys].sort();
return (
actual.length === expected.length &&
actual.every((key, index) => key === expected[index])
);
}
function parseManifest(bytes: Buffer): Readonly<IssuerKeyringManifest> {
let value: unknown;
try {
value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
} catch (error) {
configurationError('content is invalid', error);
}
if (
!value ||
typeof value !== 'object' ||
Array.isArray(value) ||
!exact(value, ['activeGeneration', 'keys', 'kind', 'schemaVersion'])
) {
configurationError('shape is invalid');
}
const candidate = value as Record<string, unknown>;
if (
candidate.schemaVersion !== 1 ||
candidate.kind !== KEYRING_KIND ||
!Number.isSafeInteger(candidate.activeGeneration) ||
(candidate.activeGeneration as number) < 1 ||
!Array.isArray(candidate.keys) ||
candidate.keys.length < 1 ||
candidate.keys.length > MAX_LOCAL_RECONCILIATION_REVIEW_ISSUER_KEYS
) {
configurationError('manifest is invalid');
}
const keys: IssuerKeyRecord[] = [];
let previousGeneration = 0;
for (const raw of candidate.keys) {
if (
!raw ||
typeof raw !== 'object' ||
Array.isArray(raw) ||
!exact(raw, ['generation', 'keyId', 'material'])
) {
configurationError('key record shape is invalid');
}
const record = raw as Record<string, unknown>;
let decoded: Buffer | undefined;
try {
assertLocalSecretKeyId(record.keyId as string);
decoded =
typeof record.material === 'string'
? Buffer.from(record.material, 'base64url')
: Buffer.alloc(0);
if (
!Number.isSafeInteger(record.generation) ||
(record.generation as number) !== previousGeneration + 1 ||
typeof record.keyId !== 'string' ||
!record.keyId.startsWith(KEY_ID_PREFIX) ||
typeof record.material !== 'string' ||
decoded.byteLength !== KEY_BYTES ||
decoded.toString('base64url') !== record.material
) {
configurationError('key record is invalid');
}
previousGeneration = record.generation as number;
keys.push(
Object.freeze({
generation: previousGeneration,
keyId: record.keyId,
material: record.material,
}),
);
} finally {
decoded?.fill(0);
}
}
if (candidate.activeGeneration !== previousGeneration) {
configurationError('active generation is not the immutable tail');
}
return Object.freeze({
schemaVersion: 1,
kind: KEYRING_KIND,
activeGeneration: previousGeneration,
keys: Object.freeze(keys),
});
}
function canonical(manifest: Readonly<IssuerKeyringManifest>): Buffer {
return Buffer.from(`${JSON.stringify(manifest)}\n`, 'utf8');
}
function identity(stat: fs.BigIntStats): FileIdentity {
return Object.freeze({
device: stat.dev,
inode: stat.ino,
size: stat.size,
modifiedAtNs: stat.mtimeNs,
changedAtNs: stat.ctimeNs,
});
}
function sameIdentity(left: FileIdentity, right: FileIdentity): boolean {
return (
left.device === right.device &&
left.inode === right.inode &&
left.size === right.size &&
left.modifiedAtNs === right.modifiedAtNs &&
left.changedAtNs === right.changedAtNs
);
}
function load(filePath: string): Readonly<{
manifest: Readonly<IssuerKeyringManifest>;
identity: FileIdentity;
}> {
const uid = currentUid();
const parent = assertParent(filePath, uid);
let descriptor: number | undefined;
let bytes: Buffer | undefined;
try {
const before = fs.lstatSync(filePath, { bigint: true });
if (
!before.isFile() ||
before.isSymbolicLink() ||
Number(before.uid) !== uid ||
(Number(before.mode) & 0o777) !== 0o600 ||
before.nlink !== 1n ||
before.size < 2n ||
before.size > BigInt(MAX_KEYRING_BYTES)
) {
configurationError('file identity is invalid');
}
const expected = identity(before);
descriptor = fs.openSync(
filePath,
fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0),
);
const opened = fs.fstatSync(descriptor, { bigint: true });
if (!opened.isFile() || !sameIdentity(expected, identity(opened))) {
configurationError('file changed while opening');
}
bytes = fs.readFileSync(descriptor);
const after = fs.fstatSync(descriptor, { bigint: true });
const currentParent = assertParent(filePath, uid);
if (
!sameIdentity(expected, identity(after)) ||
parent.dev !== currentParent.dev ||
parent.ino !== currentParent.ino
) {
configurationError('authority changed while reading');
}
return Object.freeze({
manifest: parseManifest(bytes),
identity: expected,
});
} catch (error) {
if (error instanceof LocalDeploymentConfigurationError) throw error;
return configurationError('cannot be read', error);
} finally {
bytes?.fill(0);
if (descriptor !== undefined) fs.closeSync(descriptor);
}
}
function summary(
manifest: Readonly<IssuerKeyringManifest>,
): Readonly<LocalReconciliationReviewIssuerKeyringSummary> {
const bytes = canonical(manifest);
try {
const active = manifest.keys[manifest.keys.length - 1]!;
return Object.freeze({
schemaVersion: 1,
kind: 'qinglong3-local-reconciliation-review-issuer-keyring-summary',
activeGeneration: manifest.activeGeneration,
activeKeyId: active.keyId,
keyCount: manifest.keys.length,
keyringDigest: createHash('sha256')
.update('qinglong3.local-reconciliation-review-issuer-keyring.v1\0')
.update(bytes)
.digest('hex'),
});
} finally {
bytes.fill(0);
}
}
function material(record: Readonly<IssuerKeyRecord>): LocalSecretKeyMaterial {
return Object.freeze({
keyId: record.keyId,
key: Uint8Array.from(Buffer.from(record.material, 'base64url')),
});
}
export class LocalReconciliationReviewIssuerKeyringFileProvider
implements LocalSecretKeyProvider
{
private readonly filePath: string;
constructor(candidatePath: string) {
this.filePath = boundedPath(candidatePath);
}
async active(): Promise<LocalSecretKeyMaterial> {
const manifest = load(this.filePath).manifest;
return material(manifest.keys[manifest.keys.length - 1]!);
}
async resolve(keyId: string): Promise<LocalSecretKeyMaterial | null> {
try {
assertLocalSecretKeyId(keyId);
} catch (error) {
configurationError('key id is invalid', error);
}
const record = load(this.filePath).manifest.keys.find(
(candidate) => candidate.keyId === keyId,
);
return record ? material(record) : null;
}
inspect(): Readonly<LocalReconciliationReviewIssuerKeyringSummary> {
return summary(load(this.filePath).manifest);
}
}
function syncDirectory(directory: string): void {
const descriptor = fs.openSync(directory, fs.constants.O_RDONLY);
try {
fs.fsyncSync(descriptor);
} finally {
fs.closeSync(descriptor);
}
}
export function ensureLocalReconciliationReviewIssuerKeyring(
candidatePath: string,
): Readonly<LocalReconciliationReviewIssuerKeyringSummary> {
const filePath = boundedPath(candidatePath);
if (fs.existsSync(filePath)) return summary(load(filePath).manifest);
const uid = currentUid();
assertParent(filePath, uid);
const key = randomBytes(KEY_BYTES);
const keyId = `${KEY_ID_PREFIX}${randomBytes(12).toString('base64url')}`;
const manifest: Readonly<IssuerKeyringManifest> = Object.freeze({
schemaVersion: 1,
kind: KEYRING_KIND,
activeGeneration: 1,
keys: Object.freeze([
Object.freeze({
generation: 1,
keyId,
material: key.toString('base64url'),
}),
]),
});
const bytes = canonical(manifest);
const stage = `${filePath}.ql3-reconciliation-review-stage`;
let descriptor: number | undefined;
let created = false;
try {
descriptor = fs.openSync(
stage,
fs.constants.O_CREAT |
fs.constants.O_EXCL |
fs.constants.O_WRONLY |
(fs.constants.O_NOFOLLOW ?? 0),
0o600,
);
created = true;
fs.fchmodSync(descriptor, 0o600);
fs.writeFileSync(descriptor, bytes);
fs.fsyncSync(descriptor);
fs.closeSync(descriptor);
descriptor = undefined;
try {
fs.linkSync(stage, filePath);
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
}
syncDirectory(path.dirname(filePath));
fs.unlinkSync(stage);
created = false;
syncDirectory(path.dirname(filePath));
return summary(load(filePath).manifest);
} catch (error) {
if (error instanceof LocalDeploymentConfigurationError) throw error;
return configurationError('cannot be provisioned', error);
} finally {
key.fill(0);
bytes.fill(0);
if (descriptor !== undefined) fs.closeSync(descriptor);
if (created) {
try {
fs.unlinkSync(stage);
} catch {
// An unpublished owner-only stage is fail-closed and recoverable.
}
}
}
}
@@ -107,7 +107,10 @@ function contents(value: unknown): string {
function overlaps(left: string, right: string): boolean {
const relative = path.relative(left, right);
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
return (
relative === '' ||
(!relative.startsWith('..') && !path.isAbsolute(relative))
);
}
export function localReconciliationReviewDirectory(
@@ -149,19 +152,34 @@ function ensureReviewDirectory(
return paths;
}
function validateCatalog(paths: Readonly<LocalReconciliationReviewPaths>): void {
function validateCatalog(
paths: Readonly<LocalReconciliationReviewPaths>,
): void {
const allowed = new Set([
'authorization.ndjson',
'intent.json',
'receipt.json',
'review.json',
'staging',
'.receipt.json.ql3-deploy-stage',
'.review.json.ql3-deploy-stage',
'.intent.json.ql3-deploy-stage',
]);
for (const entry of fs.readdirSync(paths.root, { withFileTypes: true })) {
if (!allowed.has(entry.name) || entry.isSymbolicLink()) {
configurationError('reconciliation review root contains unknown material');
configurationError(
'reconciliation review root contains unknown material',
);
}
}
if (fs.readdirSync(paths.staging).length !== 0) {
configurationError('reconciliation review staging contains unknown material');
if (
fs
.readdirSync(paths.staging)
.some((entry) => entry !== 'authorization.ndjson.stage')
) {
configurationError(
'reconciliation review staging contains unknown material',
);
}
}
@@ -172,7 +190,8 @@ function buildIntent(
if (
terminal.intent.command.options.deploymentRoot !==
command.options.deploymentRoot ||
terminal.intent.command.options.captureRoot !== command.options.captureRoot ||
terminal.intent.command.options.captureRoot !==
command.options.captureRoot ||
terminal.intent.command.options.planRoot !== command.options.planRoot ||
terminal.intent.command.options.allowRootService !==
command.options.allowRootService ||
@@ -197,7 +216,10 @@ function buildIntent(
planReceiptDigest: terminal.receipt.receiptDigest,
plannedHeadDigest: command.request.expectedHeadDigest,
});
return Object.freeze({ ...payload, preparationDigest: cutoverDigest(payload) });
return Object.freeze({
...payload,
preparationDigest: cutoverDigest(payload),
});
}
export function normalizeLocalReconciliationReviewIntent(
@@ -224,7 +246,9 @@ export function normalizeLocalReconciliationReviewIntent(
],
'reconciliation review intent',
);
const command = normalizeLocalReconciliationReviewPrepareCommand(intent.command);
const command = normalizeLocalReconciliationReviewPrepareCommand(
intent.command,
);
const { preparationDigest, ...payload } = intent;
if (
intent.schema !== INTENT_SCHEMA ||
@@ -311,10 +335,22 @@ export function prepareLocalReconciliationReview(
): Readonly<LocalReconciliationReviewPrepareResult> {
const command = normalizeLocalReconciliationReviewPrepareCommand(input);
const identity = currentIdentity();
validatePrivateDirectory(command.options.deploymentRoot, identity.uid, 'deploymentRoot');
validatePrivateDirectory(command.options.captureRoot, identity.uid, 'captureRoot');
validatePrivateDirectory(
command.options.deploymentRoot,
identity.uid,
'deploymentRoot',
);
validatePrivateDirectory(
command.options.captureRoot,
identity.uid,
'captureRoot',
);
validatePrivateDirectory(command.options.planRoot, identity.uid, 'planRoot');
validatePrivateDirectory(command.options.reviewRoot, identity.uid, 'reviewRoot');
validatePrivateDirectory(
command.options.reviewRoot,
identity.uid,
'reviewRoot',
);
const terminal = readLocalReconciliationPlanTerminal(
command.options.planRoot,
command.request.planId,
@@ -336,7 +372,9 @@ export function prepareLocalReconciliationReview(
(head.state !== 'reconciliation_planned' &&
head.state !== 'reconciliation_review_prepared')
) {
configurationError('review prepare lost the planned instance head compare-and-swap');
configurationError(
'review prepare lost the planned instance head compare-and-swap',
);
}
const paths = ensureReviewDirectory(
command.options.reviewRoot,
@@ -384,7 +422,8 @@ function validateDiagnosticsBinding(
intent.command.options.captureRoot !== command.options.captureRoot ||
intent.command.options.planRoot !== command.options.planRoot ||
intent.command.options.reviewRoot !== command.options.reviewRoot ||
intent.command.options.allowRootService !== command.options.allowRootService ||
intent.command.options.allowRootService !==
command.options.allowRootService ||
intent.command.request.reviewId !== command.request.reviewId ||
intent.preparationDigest !== command.request.expectedPreparationDigest
) {
@@ -413,13 +452,36 @@ export function writeLocalReconciliationReviewDiagnostics(
): Readonly<LocalReconciliationReviewDiagnosticsResult> {
const command = normalizeLocalReconciliationReviewDiagnosticsCommand(input);
const identity = currentIdentity();
validatePrivateDirectory(command.options.deploymentRoot, identity.uid, 'deploymentRoot');
validatePrivateDirectory(command.options.captureRoot, identity.uid, 'captureRoot');
validatePrivateDirectory(
command.options.deploymentRoot,
identity.uid,
'deploymentRoot',
);
validatePrivateDirectory(
command.options.captureRoot,
identity.uid,
'captureRoot',
);
validatePrivateDirectory(command.options.planRoot, identity.uid, 'planRoot');
validatePrivateDirectory(command.options.reviewRoot, identity.uid, 'reviewRoot');
const paths = reviewPaths(command.options.reviewRoot, command.request.reviewId);
validatePrivateDirectory(paths.root, identity.uid, 'reconciliationReviewDirectory');
validatePrivateDirectory(paths.staging, identity.uid, 'reconciliationReviewStaging');
validatePrivateDirectory(
command.options.reviewRoot,
identity.uid,
'reviewRoot',
);
const paths = reviewPaths(
command.options.reviewRoot,
command.request.reviewId,
);
validatePrivateDirectory(
paths.root,
identity.uid,
'reconciliationReviewDirectory',
);
validatePrivateDirectory(
paths.staging,
identity.uid,
'reconciliationReviewStaging',
);
const intent = readLocalReconciliationReviewIntent(
command.options.reviewRoot,
command.request.reviewId,
@@ -466,7 +528,9 @@ export function writeLocalReconciliationReviewDiagnostics(
}),
);
if (page === null) {
configurationError('manual-required SQLite topology has no diagnostic page');
configurationError(
'manual-required SQLite topology has no diagnostic page',
);
}
const currentHead = readLocalCutoverInstanceHead(
command.options.deploymentRoot,
@@ -478,7 +542,9 @@ export function writeLocalReconciliationReviewDiagnostics(
currentHead.state !== 'reconciliation_review_prepared' ||
currentHead.sourceRecordDigest !== intent.preparationDigest
) {
configurationError('review diagnostics instance head changed while reading');
configurationError(
'review diagnostics instance head changed while reading',
);
}
dependencies.beforeDiagnosticPublish?.();
const status = publishLocalReconciliationDiagnosticPage(
@@ -505,7 +571,9 @@ export function writeLocalReconciliationReviewDiagnostics(
export function prepareLocalReconciliationReviewCommandFile(
filePath: string,
): Readonly<LocalReconciliationReviewPrepareResult> {
return prepareLocalReconciliationReview(readPrivateLocalCommandFile(filePath));
return prepareLocalReconciliationReview(
readPrivateLocalCommandFile(filePath),
);
}
export function writeLocalReconciliationReviewDiagnosticsCommandFile(
@@ -0,0 +1,396 @@
import fs from 'node:fs';
import { LocalDeploymentConfigurationError } from '../../foundation/error';
import { cutoverDigest } from '../../cutover/targetEvidence';
import type { LocalReconciliationReviewAuthorizationEvidence } from './authorization';
import type {
LocalReconciliationReviewDisposition,
LocalReconciliationReviewReason,
} from './decisionFile';
const REVIEW_SCHEMA = 'qinglong3-local-reconciliation-review';
const RECEIPT_SCHEMA = 'qinglong3-local-reconciliation-review-receipt';
const DIGEST_PATTERN = /^[0-9a-f]{64}$/;
const MAX_TERMINAL_BYTES = 64 * 1024;
export interface LocalReconciliationReview {
readonly schema: typeof REVIEW_SCHEMA;
readonly schemaVersion: 1;
readonly state: 'reconciliation_reviewed';
readonly reviewId: string;
readonly profile: 'edge' | 'standalone';
readonly planDigest: string;
readonly preparationDigest: string;
readonly bundleDigest: string;
readonly bundleFingerprintDigest: string;
readonly preparedHeadDigest: string;
readonly authorizationDigest: string;
readonly decisionFileDigest: string;
readonly decisionSetDigest: string;
readonly decisionCount: number;
readonly dispositionCounts: Readonly<
Record<LocalReconciliationReviewDisposition, number>
>;
readonly reasonCounts: Readonly<
Record<LocalReconciliationReviewReason, number>
>;
readonly reviewerDigest: string;
readonly issuedAtMs: number;
readonly expiresAtMs: number;
readonly committedAtMs: number;
readonly reviewDigest: string;
}
export interface LocalReconciliationReviewReceipt {
readonly schema: typeof RECEIPT_SCHEMA;
readonly schemaVersion: 1;
readonly state: 'reconciliation_reviewed';
readonly reviewId: string;
readonly planDigest: string;
readonly preparationDigest: string;
readonly authorizationDigest: string;
readonly decisionSetDigest: string;
readonly decisionCount: number;
readonly keyId: string;
readonly reviewDigest: string;
readonly committedAtMs: number;
readonly receiptDigest: string;
}
function configurationError(message: string, cause?: unknown): never {
throw new LocalDeploymentConfigurationError(
`terminal reconciliation review ${message}`,
{ cause },
);
}
function object(value: unknown, label: string): Record<string, unknown> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
configurationError(`${label} must be an object`);
}
return value as Record<string, unknown>;
}
function exact(
value: Record<string, unknown>,
keys: readonly string[],
label: string,
): void {
const actual = Object.keys(value).sort();
const expected = [...keys].sort();
if (
actual.length !== expected.length ||
actual.some((key, index) => key !== expected[index])
) {
configurationError(`${label} shape is invalid`);
}
}
function digest(value: unknown, label: string): string {
if (typeof value !== 'string' || !DIGEST_PATTERN.test(value)) {
configurationError(`${label} is invalid`);
}
return value;
}
function counts(
value: unknown,
keys: readonly string[],
label: string,
): Readonly<Record<string, number>> {
const record = object(value, label);
exact(record, keys, label);
if (
keys.some(
(key) =>
!Number.isSafeInteger(record[key]) || (record[key] as number) < 0,
)
) {
configurationError(`${label} is invalid`);
}
return Object.freeze(record as Record<string, number>);
}
export function buildLocalReconciliationReview(
input: Readonly<{
authorization: Readonly<LocalReconciliationReviewAuthorizationEvidence>;
decisionFileDigest: string;
committedAtMs: number;
}>,
): Readonly<LocalReconciliationReview> {
const header = input.authorization.header;
const reviewerDigest = cutoverDigest({
subject: header.reviewer.subject,
authenticationId: header.reviewer.authenticationId,
authenticatedAtMs: header.reviewer.authenticatedAtMs,
assurance: header.reviewer.assurance,
});
const payload = Object.freeze({
schema: REVIEW_SCHEMA,
schemaVersion: 1 as const,
state: 'reconciliation_reviewed' as const,
reviewId: header.reviewId,
profile: header.profile,
planDigest: header.planDigest,
preparationDigest: header.preparationDigest,
bundleDigest: header.bundleDigest,
bundleFingerprintDigest: header.bundleFingerprintDigest,
preparedHeadDigest: header.preparedHeadDigest,
authorizationDigest: input.authorization.authorizationDigest,
decisionFileDigest: digest(
input.decisionFileDigest,
'decision file digest',
),
decisionSetDigest: input.authorization.decisionSetDigest,
decisionCount: input.authorization.decisionCount,
dispositionCounts: input.authorization.dispositionCounts,
reasonCounts: input.authorization.reasonCounts,
reviewerDigest,
issuedAtMs: header.issuedAtMs,
expiresAtMs: header.expiresAtMs,
committedAtMs: input.committedAtMs,
});
return Object.freeze({ ...payload, reviewDigest: cutoverDigest(payload) });
}
export function buildLocalReconciliationReviewReceipt(
review: Readonly<LocalReconciliationReview>,
keyId: string,
): Readonly<LocalReconciliationReviewReceipt> {
const payload = Object.freeze({
schema: RECEIPT_SCHEMA,
schemaVersion: 1 as const,
state: 'reconciliation_reviewed' as const,
reviewId: review.reviewId,
planDigest: review.planDigest,
preparationDigest: review.preparationDigest,
authorizationDigest: review.authorizationDigest,
decisionSetDigest: review.decisionSetDigest,
decisionCount: review.decisionCount,
keyId,
reviewDigest: review.reviewDigest,
committedAtMs: review.committedAtMs,
});
return Object.freeze({ ...payload, receiptDigest: cutoverDigest(payload) });
}
export function normalizeLocalReconciliationReview(
value: unknown,
): Readonly<LocalReconciliationReview> {
const review = object(value, 'review');
exact(
review,
[
'authorizationDigest',
'bundleDigest',
'bundleFingerprintDigest',
'committedAtMs',
'decisionCount',
'decisionFileDigest',
'decisionSetDigest',
'dispositionCounts',
'expiresAtMs',
'issuedAtMs',
'planDigest',
'preparationDigest',
'preparedHeadDigest',
'profile',
'reasonCounts',
'reviewDigest',
'reviewId',
'reviewerDigest',
'schema',
'schemaVersion',
'state',
],
'review',
);
const dispositionCounts = counts(
review.dispositionCounts,
[
'adopt_legacy',
'defer',
'exclude_legacy',
'manual_external',
'retain_both',
'retain_target',
],
'disposition counts',
);
const reasonCounts = counts(
review.reasonCounts,
[
'deferred_review',
'external_recovery_required',
'legacy_excluded',
'prefer_legacy',
'preserve_both',
'preserve_target',
],
'reason counts',
);
const { reviewDigest, ...rawPayload } = review;
const payload = Object.freeze({
...rawPayload,
dispositionCounts,
reasonCounts,
});
if (
review.schema !== REVIEW_SCHEMA ||
review.schemaVersion !== 1 ||
review.state !== 'reconciliation_reviewed' ||
(review.profile !== 'edge' && review.profile !== 'standalone') ||
!Number.isSafeInteger(review.decisionCount) ||
(review.decisionCount as number) < 0 ||
!Number.isSafeInteger(review.issuedAtMs) ||
!Number.isSafeInteger(review.expiresAtMs) ||
!Number.isSafeInteger(review.committedAtMs) ||
(review.expiresAtMs as number) <= (review.issuedAtMs as number) ||
[
review.planDigest,
review.preparationDigest,
review.bundleDigest,
review.bundleFingerprintDigest,
review.preparedHeadDigest,
review.authorizationDigest,
review.decisionFileDigest,
review.decisionSetDigest,
review.reviewerDigest,
reviewDigest,
].some(
(selected) =>
typeof selected !== 'string' || !DIGEST_PATTERN.test(selected),
) ||
cutoverDigest(payload) !== reviewDigest ||
Object.values(dispositionCounts).reduce(
(total, count) => total + count,
0,
) !== review.decisionCount ||
Object.values(reasonCounts).reduce((total, count) => total + count, 0) !==
review.decisionCount
) {
configurationError('review drifted');
}
return Object.freeze({
...(review as unknown as LocalReconciliationReview),
dispositionCounts:
dispositionCounts as LocalReconciliationReview['dispositionCounts'],
reasonCounts: reasonCounts as LocalReconciliationReview['reasonCounts'],
});
}
export function normalizeLocalReconciliationReviewReceipt(
value: unknown,
): Readonly<LocalReconciliationReviewReceipt> {
const receipt = object(value, 'receipt');
exact(
receipt,
[
'authorizationDigest',
'committedAtMs',
'decisionCount',
'decisionSetDigest',
'keyId',
'planDigest',
'preparationDigest',
'receiptDigest',
'reviewDigest',
'reviewId',
'schema',
'schemaVersion',
'state',
],
'receipt',
);
const { receiptDigest, ...payload } = receipt;
if (
receipt.schema !== RECEIPT_SCHEMA ||
receipt.schemaVersion !== 1 ||
receipt.state !== 'reconciliation_reviewed' ||
!Number.isSafeInteger(receipt.decisionCount) ||
(receipt.decisionCount as number) < 0 ||
!Number.isSafeInteger(receipt.committedAtMs) ||
typeof receipt.keyId !== 'string' ||
receipt.keyId.length < 1 ||
receipt.keyId.length > 128 ||
[
receipt.planDigest,
receipt.preparationDigest,
receipt.authorizationDigest,
receipt.decisionSetDigest,
receipt.reviewDigest,
receiptDigest,
].some(
(selected) =>
typeof selected !== 'string' || !DIGEST_PATTERN.test(selected),
) ||
cutoverDigest(payload) !== receiptDigest
) {
configurationError('receipt drifted');
}
return Object.freeze(
receipt,
) as unknown as Readonly<LocalReconciliationReviewReceipt>;
}
export function terminalEvidenceContents(value: unknown): string {
const contents = `${JSON.stringify(value, null, 2)}\n`;
if (
Buffer.byteLength(contents, 'utf8') < 2 ||
Buffer.byteLength(contents, 'utf8') > MAX_TERMINAL_BYTES
) {
configurationError('evidence exceeds its byte bound');
}
return contents;
}
export function readLocalReconciliationReviewTerminalJson(
filePath: string,
uid: number,
allowedModes: readonly number[],
): unknown {
let descriptor: number | undefined;
let bytes: Buffer | undefined;
try {
const before = fs.lstatSync(filePath, { bigint: true });
if (
!before.isFile() ||
before.isSymbolicLink() ||
Number(before.uid) !== uid ||
!allowedModes.includes(Number(before.mode) & 0o777) ||
before.nlink !== 1n ||
before.size < 2n ||
before.size > BigInt(MAX_TERMINAL_BYTES)
) {
configurationError('evidence identity is invalid');
}
descriptor = fs.openSync(
filePath,
fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0),
);
const opened = fs.fstatSync(descriptor, { bigint: true });
if (
opened.dev !== before.dev ||
opened.ino !== before.ino ||
opened.size !== before.size
) {
configurationError('evidence changed while opening');
}
bytes = fs.readFileSync(descriptor);
const after = fs.fstatSync(descriptor, { bigint: true });
if (
after.dev !== before.dev ||
after.ino !== before.ino ||
after.size !== before.size
) {
configurationError('evidence changed while reading');
}
return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
} catch (error) {
if (error instanceof LocalDeploymentConfigurationError) throw error;
return configurationError('evidence cannot be read', error);
} finally {
bytes?.fill(0);
if (descriptor !== undefined) fs.closeSync(descriptor);
}
}
@@ -10,11 +10,13 @@ const { test } = require('node:test');
const {
commitLocalReconciliationCapture,
commitLocalReconciliationPlan,
commitLocalReconciliationReview,
prepareLocalReconciliationCapture,
prepareLocalReconciliationPlan,
prepareLocalReconciliationReview,
verifyLocalReconciliationCapture,
verifyLocalReconciliationPlan,
verifyLocalReconciliationReview,
writeLocalReconciliationReviewDiagnostics,
} = require('../dist/deployment/localDeployment.js');
const {
@@ -193,9 +195,13 @@ function fixture(
fs.writeFileSync(`${targetDatabasePath}-wal`, 'target-wal-facts\n', {
mode: 0o600,
});
fs.writeFileSync(`${legacySourcePath}-journal`, 'legacy-journal-state\n', {
mode: 0o600,
});
fs.writeFileSync(
`${legacySourcePath}-journal`,
'legacy-journal-state\n',
{
mode: 0o600,
},
);
}
}
const commitmentPayload = {
@@ -682,6 +688,7 @@ function preparedPlan(t, options = {}) {
};
return {
...state,
captureCommand: state.command,
captured,
planRoot,
prepareCommand,
@@ -694,7 +701,10 @@ function preparedReview(t, options = {}) {
const state = preparedPlan(t, options);
const planned = commitLocalReconciliationPlan(state.planCommitCommand);
const root = path.dirname(state.captureRoot);
const reviewRoot = path.join(root, `review-root-${options.reviewSuffix ?? '1'}`);
const reviewRoot = path.join(
root,
`review-root-${options.reviewSuffix ?? '1'}`,
);
const diagnosticRoot = path.join(
root,
`diagnostic-root-${options.reviewSuffix ?? '1'}`,
@@ -712,8 +722,7 @@ function preparedReview(t, options = {}) {
allowRootService: rootAcknowledgement(),
},
request: {
reviewId:
options.reviewId ?? '00000000-0000-4000-8000-000000000301',
reviewId: options.reviewId ?? '00000000-0000-4000-8000-000000000301',
planId: state.prepareCommand.request.planId,
expectedPlanDigest: planned.planDigest,
expectedHeadDigest: planned.instanceHeadDigest,
@@ -758,6 +767,163 @@ function diagnosticCommand(
};
}
function writeReviewDecisionFile(state, prepared, fileName = 'review.ndjson') {
const decisions = [];
const domains = [
'schema_lineage',
'automation',
'secret_and_config',
'run_history',
'plugin_package',
'ai_and_tool',
'identity_policy_audit',
'unknown',
];
for (const database of ['legacy', 'target']) {
for (const domain of domains) {
for (const factKind of ['schema_object', 'table']) {
let offset = 0;
let pageNumber = 0;
while (true) {
const command = diagnosticCommand(state, prepared, {
database,
domain,
factKind,
offset,
outputName: `decision-${database}-${domain}-${factKind}-${pageNumber}.json`,
});
const result = writeLocalReconciliationReviewDiagnostics(command);
const page = JSON.parse(
fs.readFileSync(command.request.outputPath, 'utf8'),
);
for (const fact of page.records) {
if (fact.decisionRequirement === 'informational') continue;
const blocked = fact.decisionRequirement === 'blocked';
decisions.push({
schemaVersion: 1,
kind: 'qinglong3-local-reconciliation-review-decision',
database: fact.database,
domain: fact.domain,
factKind: fact.factKind,
ordinal: fact.ordinal,
factDigest: fact.factDigest,
disposition: blocked ? 'manual_external' : 'retain_target',
reason: blocked
? 'external_recovery_required'
: 'preserve_target',
});
}
if (result.complete) break;
offset = result.nextOffset;
pageNumber += 1;
}
}
}
}
const records = [
{
schemaVersion: 1,
kind: 'qinglong3-local-reconciliation-review-decision-header',
diagnosticsContractVersion: 1,
reviewId: state.reviewCommand.request.reviewId,
profile: state.command.request.profile,
planDigest: state.planned.planDigest,
preparationDigest: prepared.preparationDigest,
},
...decisions,
];
const filePath = path.join(state.diagnosticRoot, fileName);
fs.writeFileSync(
filePath,
`${records.map((record) => JSON.stringify(record)).join('\n')}\n`,
{ mode: 0o600 },
);
return { filePath, records, decisions };
}
function reviewCommitFixture(t, options = {}) {
const state = preparedReview(t, options);
const prepared = prepareLocalReconciliationReview(state.reviewCommand);
const reviewFile = writeReviewDecisionFile(state, prepared);
const ownerPepperKeyringDirectory = path.join(
state.deploymentRoot,
'review-owner-peppers',
);
fs.mkdirSync(ownerPepperKeyringDirectory, { mode: 0o700 });
const credentialFilePath = path.join(
state.deploymentRoot,
'review-credential.json',
);
fs.writeFileSync(credentialFilePath, '{}\n', { mode: 0o600 });
const issuerKeyringPath = path.join(
state.deploymentRoot,
'review-issuer.keyring',
);
const committedAtMs = Date.now();
const command = {
schemaVersion: 1,
operation: 'local.deployment.reconciliation.review.commit',
options: {
...state.reviewCommand.options,
targetDatabasePath: state.targetDatabasePath,
ownerPepperKeyringDirectory,
credentialFilePath,
issuerKeyringPath,
},
request: {
reviewId: state.reviewCommand.request.reviewId,
expectedPreparationDigest: prepared.preparationDigest,
expectedHeadDigest: prepared.instanceHeadDigest,
decisionFilePath: reviewFile.filePath,
committedAtMs,
authorizationLifetimeMs: 30 * 60 * 1_000,
},
};
let authentications = 0;
let confirmations = 0;
const dependencies = {
now: () => committedAtMs,
async openAuthenticationDatabase() {
return { async close() {} };
},
async authenticate(_database, authenticateOptions) {
authentications += 1;
assert.equal(
authenticateOptions.authenticationNamespace,
'local_reconciliation_review',
);
return {
principal: {
subject: { type: 'user', id: 'review-owner' },
authenticationId: 'local_reconciliation_review:test',
authenticatedAtMs: committedAtMs,
expiresAtMs: committedAtMs + 60_000,
assurance: 'local_console',
},
databaseFence: {
credentialId: 'review-owner',
credentialVersion: 1,
pepperKeyId: 'review-owner-v1',
pepperVersion: 1,
},
async confirm() {
confirmations += 1;
},
};
},
};
return {
...state,
prepared,
reviewFile,
command,
dependencies,
issuerKeyringPath,
authenticationCount: () => authentications,
confirmationCount: () => confirmations,
};
}
function dockerReadSealedSqlite(assetsDirectory, mode) {
const source =
mode === 'main_only_immutable'
@@ -770,7 +936,9 @@ function dockerReadSealedSqlite(assetsDirectory, mode) {
const files = fs.readdirSync('/bundle').sort();
const snapshot = () => Object.fromEntries(files.map((name) => [name, crypto.createHash('sha256').update(fs.readFileSync('/bundle/' + name)).digest('hex')]));
const before = snapshot();
const client = new DatabaseSync(${JSON.stringify(source)}, { allowExtension: false, defensive: true, readOnly: true, timeout: 0 });
const client = new DatabaseSync(${JSON.stringify(
source,
)}, { allowExtension: false, defensive: true, readOnly: true, timeout: 0 });
client.enableDefensive(true);
client.exec('PRAGMA trusted_schema = OFF; PRAGMA query_only = ON; PRAGMA temp_store = MEMORY; PRAGMA mmap_size = 0; PRAGMA cache_size = -2048');
const row = client.prepare('SELECT COUNT(*) AS count FROM "QingLong3TaskDefinitions"').get();
@@ -851,10 +1019,7 @@ test('commit captures main, sidecars and recovery then verifies without sources'
fs.readFileSync(captureAssetPath(state, 'target-wal'), 'utf8'),
'target-wal-facts\n',
);
assert.equal(
fs.statSync(capturePath(state, 'assets')).mode & 0o777,
0o500,
);
assert.equal(fs.statSync(capturePath(state, 'assets')).mode & 0o777, 0o500);
for (const asset of manifest.assets) {
assert.equal(
fs.statSync(captureAssetPath(state, asset.logicalName)).mode & 0o777,
@@ -1002,19 +1167,13 @@ test('commit converges a partially sealed terminal bundle without sources', (t)
fs.statSync(captureAssetPath(state, 'target-wal')).mode & 0o777,
0o600,
);
assert.equal(
fs.statSync(capturePath(state, 'assets')).mode & 0o777,
0o700,
);
assert.equal(fs.statSync(capturePath(state, 'assets')).mode & 0o777, 0o700);
fs.unlinkSync(state.targetDatabasePath);
fs.unlinkSync(state.legacySourcePath);
fs.unlinkSync(state.recoveryPath);
const resumed = commitLocalReconciliationCapture(state.commitCommand);
assert.equal(resumed.state, 'reconciliation_captured');
assert.equal(
fs.statSync(capturePath(state, 'assets')).mode & 0o777,
0o500,
);
assert.equal(fs.statSync(capturePath(state, 'assets')).mode & 0o777, 0o500);
const manifest = JSON.parse(
fs.readFileSync(capturePath(state, 'manifest.json'), 'utf8'),
);
@@ -1206,13 +1365,13 @@ test('capture manifest schema v1 is rejected instead of silently upgraded', (t)
test('plan reads sealed main-only SQLite with fixed budgets and verifies without opening', (t) => {
const state = preparedPlan(t);
const beforeAssets = fs.readdirSync(
capturePath(state, 'assets'),
).map((name) => ({
name,
bytes: fs.readFileSync(capturePath(state, `assets/${name}`)),
stat: fs.statSync(capturePath(state, `assets/${name}`), { bigint: true }),
}));
const beforeAssets = fs
.readdirSync(capturePath(state, 'assets'))
.map((name) => ({
name,
bytes: fs.readFileSync(capturePath(state, `assets/${name}`)),
stat: fs.statSync(capturePath(state, `assets/${name}`), { bigint: true }),
}));
const opens = [];
const committed = commitLocalReconciliationPlan(state.planCommitCommand, {
beforeDatabaseOpen(kind, mode, cacheKiB) {
@@ -1467,9 +1626,7 @@ test('hot journal and unpaired sidecars become manual without SQLite open', (t)
],
);
assert.equal(
plan.domains.every(
(domain) => domain.disposition === 'manual_required',
),
plan.domains.every((domain) => domain.disposition === 'manual_required'),
true,
);
});
@@ -1481,11 +1638,7 @@ test('unknown target schema is summarized only by digest and requires manual rev
});
commitLocalReconciliationPlan(state.planCommitCommand);
const planText = fs.readFileSync(
path.join(
state.planRoot,
state.prepareCommand.request.planId,
'plan.json',
),
path.join(state.planRoot, state.prepareCommand.request.planId, 'plan.json'),
'utf8',
);
const plan = JSON.parse(planText);
@@ -1706,7 +1859,10 @@ test('review diagnostics publish one private exact page without changing assets'
assert.equal(result.recordCount, 1);
assert.equal(result.complete, true);
assert.equal(result.nextOffset, null);
assert.equal(JSON.stringify(result).includes(command.request.outputPath), false);
assert.equal(
JSON.stringify(result).includes(command.request.outputPath),
false,
);
assert.equal(JSON.stringify(result).includes('Crontabs'), false);
const pageText = fs.readFileSync(command.request.outputPath, 'utf8');
const page = JSON.parse(pageText);
@@ -1845,6 +2001,334 @@ test('review diagnostics page at sixty-four and CLI output stays content-free',
assert.equal(cli.stderr, '');
});
test('review commit signs the exact decision stream, seals terminal evidence and verifies read-only', async (t) => {
const state = reviewCommitFixture(t, {
planId: '00000000-0000-4000-8000-000000000351',
reviewId: '00000000-0000-4000-8000-000000000352',
});
const targetBefore = fs.statSync(state.targetDatabasePath, { bigint: true });
const targetBytes = fs.readFileSync(state.targetDatabasePath);
const committed = await commitLocalReconciliationReview(
state.command,
state.dependencies,
);
assert.equal(committed.status, 'prepared');
assert.equal(committed.state, 'reconciliation_reviewed');
assert.equal(committed.decisionCount, state.reviewFile.decisions.length);
assert.equal(state.authenticationCount(), 1);
assert.equal(state.confirmationCount(), 3);
const reviewDirectory = path.join(
state.reviewRoot,
state.reviewCommand.request.reviewId,
);
assert.deepEqual(fs.readdirSync(reviewDirectory).sort(), [
'authorization.ndjson',
'intent.json',
'receipt.json',
'review.json',
'staging',
]);
assert.equal(fs.statSync(reviewDirectory).mode & 0o777, 0o500);
assert.equal(
fs.statSync(path.join(reviewDirectory, 'staging')).mode & 0o777,
0o500,
);
for (const fileName of [
'authorization.ndjson',
'intent.json',
'receipt.json',
'review.json',
]) {
assert.equal(
fs.statSync(path.join(reviewDirectory, fileName)).mode & 0o777,
0o400,
);
}
const authorizationText = fs.readFileSync(
path.join(reviewDirectory, 'authorization.ndjson'),
'utf8',
);
assert.equal(authorizationText.includes('Crontabs'), false);
assert.equal(authorizationText.includes('private-value'), false);
assert.equal(authorizationText.includes('0 0 * * *'), false);
const head = readLocalCutoverInstanceHead(
state.deploymentRoot,
state.captureCommand.request.instanceId,
state.uid,
);
assert.equal(head.state, 'reconciliation_reviewed');
assert.equal(head.sourceRecordDigest, committed.reviewDigest);
const verifyCommand = {
schemaVersion: 1,
operation: 'local.deployment.reconciliation.review.verify',
options: {
...state.reviewCommand.options,
issuerKeyringPath: state.issuerKeyringPath,
},
request: {
reviewId: state.reviewCommand.request.reviewId,
expectedReviewDigest: committed.reviewDigest,
},
};
const verified = await verifyLocalReconciliationReview(verifyCommand);
assert.equal(verified.status, 'verified');
assert.equal(verified.reviewDigest, committed.reviewDigest);
const targetAfter = fs.statSync(state.targetDatabasePath, { bigint: true });
assert.equal(
fs.readFileSync(state.targetDatabasePath).equals(targetBytes),
true,
);
assert.equal(targetAfter.mtimeNs, targetBefore.mtimeNs);
assert.equal(targetAfter.ctimeNs, targetBefore.ctimeNs);
const commandPath = path.join(state.deploymentRoot, 'review-verify.json');
fs.writeFileSync(commandPath, `${JSON.stringify(verifyCommand)}\n`, {
mode: 0o600,
});
const cli = spawnSync(
process.execPath,
[
path.join(__dirname, '../dist/deployment/localDeploymentCli.js'),
'reconciliation-review-verify',
'--command-file',
commandPath,
],
{ encoding: 'utf8' },
);
assert.equal(cli.status, 0, cli.stderr);
assert.equal(JSON.parse(cli.stdout).status, 'verified');
assert.equal(cli.stdout.includes(state.reviewRoot), false);
assert.equal(cli.stdout.includes('review-owner'), false);
assert.equal(cli.stdout.includes('Crontabs'), false);
});
test('review commit rejects missing and policy-invalid decisions before terminal publication', async (t) => {
const missing = reviewCommitFixture(t, {
planId: '00000000-0000-4000-8000-000000000361',
reviewId: '00000000-0000-4000-8000-000000000362',
reviewSuffix: 'missing',
});
const missingRecords = missing.reviewFile.records.slice(0, -1);
fs.writeFileSync(
missing.reviewFile.filePath,
`${missingRecords.map((record) => JSON.stringify(record)).join('\n')}\n`,
{ mode: 0o600 },
);
await assert.rejects(
commitLocalReconciliationReview(missing.command, missing.dependencies),
/omitted a canonical fact/,
);
assert.equal(
fs.existsSync(
path.join(
missing.reviewRoot,
missing.reviewCommand.request.reviewId,
'authorization.ndjson',
),
),
false,
);
const blocked = reviewCommitFixture(t, {
planId: '00000000-0000-4000-8000-000000000363',
reviewId: '00000000-0000-4000-8000-000000000364',
reviewSuffix: 'blocked',
});
const blockedRecord = blocked.reviewFile.records.find(
(record) =>
record.kind === 'qinglong3-local-reconciliation-review-decision' &&
[
'secret_and_config',
'run_history',
'identity_policy_audit',
'unknown',
].includes(record.domain),
);
blockedRecord.disposition = 'adopt_legacy';
blockedRecord.reason = 'prefer_legacy';
fs.writeFileSync(
blocked.reviewFile.filePath,
`${blocked.reviewFile.records
.map((record) => JSON.stringify(record))
.join('\n')}\n`,
{ mode: 0o600 },
);
await assert.rejects(
commitLocalReconciliationReview(blocked.command, blocked.dependencies),
/not allowed for canonical fact/,
);
});
test('review commit rejects weak principals, oversized Edge streams and decision-file drift around signing', async (t) => {
const weak = reviewCommitFixture(t, {
planId: '00000000-0000-4000-8000-000000000365',
reviewId: '00000000-0000-4000-8000-000000000366',
reviewSuffix: 'weak',
});
await assert.rejects(
commitLocalReconciliationReview(weak.command, {
...weak.dependencies,
async authenticate() {
return {
principal: {
subject: { type: 'user', id: 'weak-user' },
authenticationId: 'local_reconciliation_review:weak',
authenticatedAtMs: weak.command.request.committedAtMs,
expiresAtMs: weak.command.request.committedAtMs + 60_000,
assurance: 'single_factor',
},
databaseFence: {},
async confirm() {},
};
},
}),
/recent strongly authenticated User/,
);
const oversized = reviewCommitFixture(t, {
planId: '00000000-0000-4000-8000-000000000367',
reviewId: '00000000-0000-4000-8000-000000000368',
reviewSuffix: 'oversized',
});
fs.truncateSync(oversized.reviewFile.filePath, 8 * 1024 * 1024 + 1);
await assert.rejects(
commitLocalReconciliationReview(oversized.command, oversized.dependencies),
/identity or size is invalid/,
);
const drift = reviewCommitFixture(t, {
planId: '00000000-0000-4000-8000-000000000369',
reviewId: '00000000-0000-4000-8000-00000000036a',
reviewSuffix: 'drift',
});
let confirmations = 0;
await assert.rejects(
commitLocalReconciliationReview(drift.command, {
...drift.dependencies,
async authenticate() {
return {
principal: {
subject: { type: 'user', id: 'review-owner' },
authenticationId: 'local_reconciliation_review:drift',
authenticatedAtMs: drift.command.request.committedAtMs,
expiresAtMs: drift.command.request.committedAtMs + 60_000,
assurance: 'hardware',
},
databaseFence: {},
async confirm() {
confirmations += 1;
if (confirmations === 3) {
fs.appendFileSync(drift.reviewFile.filePath, '{}\n');
}
},
};
},
}),
/identity changed after reading/,
);
});
test('review commit resumes authorization, receipt, seal and head response-loss windows without re-authentication', async (t) => {
const authorizationState = reviewCommitFixture(t, {
planId: '00000000-0000-4000-8000-000000000371',
reviewId: '00000000-0000-4000-8000-000000000372',
reviewSuffix: 'authorization-crash',
});
await assert.rejects(
commitLocalReconciliationReview(authorizationState.command, {
...authorizationState.dependencies,
afterAuthorizationPublished() {
throw new Error('authorization crash');
},
}),
/authorization crash/,
);
const authorizationDirectory = path.join(
authorizationState.reviewRoot,
authorizationState.reviewCommand.request.reviewId,
);
fs.linkSync(
path.join(authorizationDirectory, 'authorization.ndjson'),
path.join(authorizationDirectory, 'staging', 'authorization.ndjson.stage'),
);
const authorizationReplay = await commitLocalReconciliationReview(
authorizationState.command,
{
...authorizationState.dependencies,
async authenticate() {
throw new Error('must not re-authenticate signed response-loss replay');
},
},
);
assert.equal(authorizationReplay.state, 'reconciliation_reviewed');
const receiptState = reviewCommitFixture(t, {
planId: '00000000-0000-4000-8000-000000000373',
reviewId: '00000000-0000-4000-8000-000000000374',
reviewSuffix: 'receipt-crash',
});
await assert.rejects(
commitLocalReconciliationReview(receiptState.command, {
...receiptState.dependencies,
afterReceiptPublished() {
throw new Error('receipt crash');
},
}),
/receipt crash/,
);
const receiptReplay = await commitLocalReconciliationReview(
receiptState.command,
receiptState.dependencies,
);
assert.equal(receiptReplay.state, 'reconciliation_reviewed');
const sealState = reviewCommitFixture(t, {
planId: '00000000-0000-4000-8000-000000000375',
reviewId: '00000000-0000-4000-8000-000000000376',
reviewSuffix: 'seal-crash',
});
await assert.rejects(
commitLocalReconciliationReview(sealState.command, {
...sealState.dependencies,
afterTerminalSealed() {
throw new Error('seal crash');
},
}),
/seal crash/,
);
assert.equal(
fs.statSync(
path.join(sealState.reviewRoot, sealState.reviewCommand.request.reviewId),
).mode & 0o777,
0o500,
);
const sealReplay = await commitLocalReconciliationReview(
sealState.command,
sealState.dependencies,
);
assert.equal(sealReplay.state, 'reconciliation_reviewed');
const headState = reviewCommitFixture(t, {
planId: '00000000-0000-4000-8000-000000000377',
reviewId: '00000000-0000-4000-8000-000000000378',
reviewSuffix: 'head-crash',
});
await assert.rejects(
commitLocalReconciliationReview(headState.command, {
...headState.dependencies,
afterHeadAdvanced() {
throw new Error('head response loss');
},
}),
/head response loss/,
);
const headReplay = await commitLocalReconciliationReview(
headState.command,
headState.dependencies,
);
assert.equal(headReplay.status, 'existing');
});
test(
'real stopped Docker target produces an independently verified bundle',
{ skip: process.env.QL3_RECONCILIATION_DOCKER_GATE !== '1' },
+5
View File
@@ -315,6 +315,11 @@
"require": "./dist/security/securityAuditQuery.js",
"default": "./dist/security/securityAuditQuery.js"
},
"./authentication-read": {
"types": "./dist/security/authenticationRead.d.ts",
"require": "./dist/security/authenticationRead.js",
"default": "./dist/security/authenticationRead.js"
},
"./security-audit-retention": {
"types": "./dist/security/securityAuditRetention.d.ts",
"require": "./dist/security/securityAuditRetention.js",
@@ -0,0 +1,58 @@
// Security owns the read-only credential projection used by stopped-state commands.
import type { ApiCredentialRepository } from '@qinglong/runtime-core/api-credential';
import type { LocalOwnerPepperRepository } from '@qinglong/runtime-core/local-owner-pepper';
import { LocalSqliteOperationAuthority } from '../authority/operationAuthority';
import { LocalSqliteOwnerPepperRepository } from '../local-owner/ownerPepperRepository';
import {
assertLocalSqliteOptions,
assertLocalSqlitePathBoundary,
openLocalSqliteClient,
type LocalSqliteDatabaseOptions,
type LocalSqliteProfile,
} from '../storage/config';
import { LocalSqliteApiCredentialRepository } from './apiCredentialRepository';
import {
auditLocalSqliteReadiness,
type LocalSqliteReadinessEvidence,
} from '../readiness/readiness';
export interface LocalSqliteAuthenticationReadDatabase {
readonly profile: LocalSqliteProfile;
readonly readiness: LocalSqliteReadinessEvidence;
readonly apiCredentials: ApiCredentialRepository;
readonly ownerPepper: Pick<LocalOwnerPepperRepository, 'resolveKey'>;
close(): Promise<void>;
}
/**
* Opens only the repositories needed to authenticate a local User. The SQLite
* descriptor is read-only, so stopped-state review cannot change journals,
* schema, credential rows, or any other target state.
*/
export async function openLocalSqliteAuthenticationReadDatabase(
options: LocalSqliteDatabaseOptions,
): Promise<LocalSqliteAuthenticationReadDatabase> {
assertLocalSqliteOptions(options);
assertLocalSqlitePathBoundary(options.databasePath, false);
const client = openLocalSqliteClient(options, true);
try {
const readiness = await auditLocalSqliteReadiness(client);
const authority = new LocalSqliteOperationAuthority(client);
let closePromise: Promise<void> | undefined;
return Object.freeze({
profile: options.profile,
readiness,
apiCredentials: new LocalSqliteApiCredentialRepository(authority),
ownerPepper: new LocalSqliteOwnerPepperRepository(authority),
close() {
if (closePromise) return closePromise;
closePromise = authority.close();
return closePromise;
},
});
} catch (error) {
if (client.isOpen) client.close();
throw error;
}
}
@@ -0,0 +1,40 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { test } = require('node:test');
const {
openLocalSqliteAuthenticationReadDatabase,
} = require('../dist/security/authenticationRead.js');
const { migrateLocalSqlitePath } = require('../dist/migration/migration.js');
test('authentication projection opens the target read-only without journal or file drift', async (t) => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-auth-read-'));
fs.chmodSync(root, 0o700);
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const databasePath = path.join(root, 'qinglong3.sqlite');
await migrateLocalSqlitePath({ databasePath, profile: 'edge' });
fs.chmodSync(databasePath, 0o600);
const bytes = fs.readFileSync(databasePath);
const before = fs.statSync(databasePath, { bigint: true });
const database = await openLocalSqliteAuthenticationReadDatabase({
databasePath,
profile: 'edge',
});
try {
assert.equal(database.profile, 'edge');
assert.equal(database.readiness.contractName, 'local-control-core');
assert.equal(database.readiness.contractVersion, 50);
assert.equal(await database.apiCredentials.resolve('absent'), null);
assert.equal(await database.ownerPepper.resolveKey('absent'), null);
} finally {
await database.close();
}
const after = fs.statSync(databasePath, { bigint: true });
assert.equal(fs.readFileSync(databasePath).equals(bytes), true);
assert.equal(after.size, before.size);
assert.equal(after.mtimeNs, before.mtimeNs);
assert.equal(after.ctimeNs, before.ctimeNs);
assert.deepEqual(fs.readdirSync(root).sort(), ['qinglong3.sqlite']);
});
+30
View File
@@ -2010,6 +2010,14 @@ function auditSourceImports(root, packagePath, findings) {
'src/lifecycle/localReadiness.ts' &&
specifier === '@qinglong/local-sqlite/readiness-inspection'
) &&
!(
path.relative(packageDirectory, filePath) ===
'src/deployment/reconciliation/review/completion.ts' &&
[
'@qinglong/local-owner-console/authenticated-command',
'@qinglong/local-sqlite/authentication-read',
].includes(specifier)
) &&
!(
[
'src/deployment/compose/composeApply.ts',
@@ -2643,6 +2651,28 @@ function auditSourceImports(root, packagePath, findings) {
) {
continue;
}
if (
packagePath === 'packages/ql3-local-owner-cli' &&
[
{
file: 'src/deployment/reconciliation/review/authorization.ts',
specifiers: [
'@qinglong/runtime-core/local-secret',
'@qinglong/runtime-core/security',
],
},
{
file: 'src/deployment/reconciliation/review/issuerKeyring.ts',
specifiers: ['@qinglong/runtime-core/local-secret'],
},
].some(
({ file, specifiers }) =>
path.relative(packageDirectory, filePath) === file &&
specifiers.includes(specifier),
)
) {
continue;
}
if (
packagePath === 'packages/ql3-local-owner-cli' &&
path.relative(packageDirectory, filePath) ===
@@ -2083,6 +2083,73 @@ test('confines fresh setup authority to the reviewed owner CLI subpath', (t) =>
);
});
test('confines reconciliation review authentication to exact read-only owners', (t) => {
const root = fs.mkdtempSync(
path.join(os.tmpdir(), 'ql3-reconciliation-review-boundary-'),
);
const reviewDirectory = path.join(
root,
'packages/ql3-local-owner-cli/src/deployment/reconciliation/review',
);
fs.mkdirSync(reviewDirectory, { recursive: true });
fs.writeFileSync(
path.join(reviewDirectory, 'authorization.ts'),
[
"import type { Key } from '@qinglong/runtime-core/local-secret';",
"import type { Principal } from '@qinglong/runtime-core/security';",
].join('\n'),
);
fs.writeFileSync(
path.join(reviewDirectory, 'completion.ts'),
[
"import { authenticate } from '@qinglong/local-owner-console/authenticated-command';",
"import { database } from '@qinglong/local-sqlite/authentication-read';",
].join('\n'),
);
fs.writeFileSync(
path.join(reviewDirectory, 'issuerKeyring.ts'),
"import type { Key } from '@qinglong/runtime-core/local-secret';",
);
fs.writeFileSync(
path.join(reviewDirectory, 'neighbor.ts'),
[
"import { authenticate } from '@qinglong/local-owner-console/authenticated-command';",
"import { database } from '@qinglong/local-sqlite/authentication-read';",
"import type { Key } from '@qinglong/runtime-core/local-secret';",
"import type { Principal } from '@qinglong/runtime-core/security';",
].join('\n'),
);
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const findings = [];
auditSourceImports(root, 'packages/ql3-local-owner-cli', findings);
assert.deepEqual(
findings.map(({ code, file, specifier }) => ({ code, file, specifier })),
[
{
code: 'FORBIDDEN_LOCAL_ADOPTION_CLI_AUTHORITY_IMPORT',
file: 'packages/ql3-local-owner-cli/src/deployment/reconciliation/review/neighbor.ts',
specifier: '@qinglong/local-owner-console/authenticated-command',
},
{
code: 'FORBIDDEN_LOCAL_ADOPTION_CLI_AUTHORITY_IMPORT',
file: 'packages/ql3-local-owner-cli/src/deployment/reconciliation/review/neighbor.ts',
specifier: '@qinglong/local-sqlite/authentication-read',
},
{
code: 'FORBIDDEN_PACKAGE_SOURCE_IMPORT',
file: 'packages/ql3-local-owner-cli/src/deployment/reconciliation/review/neighbor.ts',
specifier: '@qinglong/runtime-core/local-secret',
},
{
code: 'FORBIDDEN_PACKAGE_SOURCE_IMPORT',
file: 'packages/ql3-local-owner-cli/src/deployment/reconciliation/review/neighbor.ts',
specifier: '@qinglong/runtime-core/security',
},
],
);
});
test('deleted Owner ceremony package names remain dependency tombstones', (t) => {
const root = fixture(
t,
+4 -4
View File
@@ -207,10 +207,10 @@ test('current QL3 workspace has exactly eighteen reviewed package boundaries', (
rootSourceFileRoles: localOwnerCli.rootSourceFileRoles,
},
{
sourceFiles: 149,
sourceFiles: 155,
rootSourceFiles: 1,
rootSourceLines: 50,
nestedSourceFiles: 148,
nestedSourceFiles: 154,
rootSourceFileRoles: { 'cli.ts': 'binary_entry' },
},
);
@@ -540,10 +540,10 @@ test('current QL3 workspace has exactly eighteen reviewed package boundaries', (
rootSourceFileRoles: localSqlite.rootSourceFileRoles,
},
{
sourceFiles: 202,
sourceFiles: 203,
rootSourceFiles: 1,
rootSourceLines: 31,
nestedSourceFiles: 201,
nestedSourceFiles: 202,
rootSourceFileRoles: { 'index.ts': 'public_export' },
},
);