feat(local): publish secret config reconciliation plans

This commit is contained in:
whyour
2026-08-23 17:27:01 +08:00
parent fbb67ff5c8
commit 5dd8678d7e
13 changed files with 1811 additions and 20 deletions
+9 -5
View File
@@ -22,11 +22,15 @@
diagnostics 不含 Env name/value/row body。absent、unsupported schema、Edge over-budget、旧顺序、同名连接、disabled、保留 `QL3_`、异常状态与
overflow 均已覆盖。第二切片在既有 Local Owner reconciliation application 子目录增加私有 NDJSON row planEdge/Standalone 文件上限为 8/32 MiB
单行上限 64 KiBactive/disabled candidate 使用不同 `legacy-db-env-*` 命名空间,目标 Secret 占用只记录 envelope 元数据组合摘要并强制
`review_skip_conflict`plan/receipt 绑定 application、独立 review authorization、sealed bundle、prepared head、row/candidate set 与文件摘要,且不含原
Env name/value、目标 ciphertext/key ID 或 row body。Local Admin 完整测试 `96/96`Local Owner 完整测试 `277/270/7/0`;受限沙箱中的 3 个
loopback `EPERM` 用例已在沙箱外对应测试文件 `15/15` 通过。后端完整门 `1563/1561/2/0`18-package clean build/test
`2924/2902/22/0`package boundary、Cluster dependency、Edge import 与十四档 Local artifact audit 全部 compatible。远程第一切片 x64/arm64
backend 失败的共因是新增 Local Admin 嵌套文件后结构快照仍为 47/46,现已同步为 48/47Local Owner 同步为 176/175workspace 仍为 18 packages、
`review_skip_conflict`第三切片将该 plan 绑定 D-391 `manual_external` review、sealed bundle、application、target projection、Automation adoption ledger
的有界 content-free 投影与当前 head`defer`、active Env 无已采纳 Legacy Task、历史 `Configs` 或 target 冲突都继续 manual。publisher 以 no-replace、
`0400/0500`、文件/目录 `fsync` 发布 `plan.ndjson``receipt.json`,覆盖 plan、receipt、terminal seal、head CAS 四个 response-loss 窗口,并只允许
`reconciliation_application_planned|reconciliation_automation_applied → reconciliation_secret_config_planned` 的合法单向推进;独立 verify 只读复算,不修复漂移。
全部 evidence 不含原 Env name/value、目标 ciphertext/key ID 或 row body。Local Admin 完整测试 `96/96`Local Owner 完整测试 `287/280/7/0`
后端完整门 `1563/1561/2/0`18-package clean build/test `2934/2912/22/0`package boundary、Cluster dependency、Edge import 与十四档
Local artifact audit 全部 compatible,基础 Edge/Standalone 仍为 `2,611,978 / 2,612,056 bytes`、319 files、58 loaded modulesOwner-only authority
没有进入低资源常驻制品。
Local Admin 保持 48/47Local Owner 因两个职责明确的嵌套文件增至 178/177,根目录仍只有一个 50 行 binary entryworkspace 仍为 18 packages、
`singleSourcePackages=[]``shallowSourcePackages=[]`,且只允许 exact Secret/Config row planner 导入 inspection subpath。
D-385D-388 的 `config.sh`/Keyv/SSH data-directory lineage 与 SQLite `Envs` 保持分离;当前无稳定生产 schema 的历史 `Configs` 表继续 sealed+manual
@@ -1,6 +1,6 @@
# ADR-0491:有界 Secret/Config Reconciliation 与任务环境绑定
- 状态:ProposedD-397 已实现 Legacy Env inspection私有有界 row plan原子 application 尚未完成)
- 状态:ProposedD-397 已实现 Legacy Env inspection私有有界 row plan 与 durable plan publication;独立 signed decision 和原子 application 尚未完成)
- 日期:2026-08-23
- 决策:D-397
- 关联:ADR-0073、ADR-0074、ADR-0092、ADR-0094、ADR-0480、ADR-0482、ADR-0483、ADR-0484、ADR-0485、ADR-0486、ADR-0487、ADR-0488、ADR-0490
@@ -27,7 +27,7 @@ application plan
→ cross-domain completion
```
Secret/Config 不消费 Automation decision 作为自身授权。它必须重新绑定 D-391 `secret_and_config` facts同一 sealed bundle、D-392 application plan当前 target snapshot独立的逐候选 signed decision强认证 User、Project Policy、Secret custody 与 Task mutation authority 都要在写事务前及事务内重新验证。
Secret/Config 不消费 Automation decision 作为自身授权。D-391 `secret_and_config` facts 标为 `blocked`,其 review action 只能是 `manual_external|defer`D-397 专用 adapter 只接收每条 fact 都精确选择 `manual_external` 的决策流,把它重新绑定到同一 sealed bundle、D-392 application plan当前 target snapshot,任何 `defer` 都继续失败关闭。后续 application 还必须消费独立的逐候选 signed decision强认证 User、Project Policy、Secret custody 与 Task mutation authority 都要在写事务前及事务内重新验证。
存在 active Env 时,Automation 必须已经完成,且至少一个经 `QingLong3LegacyAdoptions` 证明的 Legacy Task 可绑定;否则不得用“Secret 已保存”冒充行为迁移。只有停用 Env 的场景可以在 Automation `no_effect` 后做纯保全。
@@ -64,7 +64,9 @@ id ASC
实现逐行读取,不把整张 `Envs` 或全部停用值加载到内存;active value 的在途内存由 64 KiB 合同封顶,停用值通过第二次有界扫描逐个交付。它位于既有 `@qinglong/local-admin/src/legacy-adoption/secret-and-config/`,不新增 workspace package、production dependency、daemon、timer、watcher、listener、socket、数据库连接池或 `src` 根平铺文件。
Local Owner 使用私有 NDJSON row plan 记录 header、逐行 content-free disposition、逐 candidate 目标冲突投影与 footer。Edge/Standalone plan 文件分别限制为 8 MiB/32 MiB,单行不超过 64 KiB;超过预算立即失败关闭。公开 plan/receipt 不保存原 Env name/value、目标 ciphertext、key ID 或原始 row body。active 与 disabled candidate 分别使用 `legacy-db-env-*``legacy-db-env-disabled-*` 命名空间;目标已经存在时只记录 envelope 元数据的组合摘要并进入 `review_skip_conflict`,不得读取明文、覆盖或自动改名。plan 绑定 application、独立 review authorization、sealed bundle、target projection 与 prepared head,并产生可重新计算的 row-set、candidate-set、plan-file 和 receipt digest。
Local Owner 使用私有 NDJSON row plan 记录 header、逐行 content-free disposition、逐 candidate 目标冲突投影与 footer。Edge/Standalone plan 文件分别限制为 8 MiB/32 MiB,单行不超过 64 KiB;超过预算立即失败关闭。公开 plan/receipt 不保存原 Env name/value、目标 ciphertext、key ID 或原始 row body。active 与 disabled candidate 分别使用 `legacy-db-env-*``legacy-db-env-disabled-*` 命名空间;目标已经存在时只记录 envelope 元数据的组合摘要并进入 `review_skip_conflict`,不得读取明文、覆盖或自动改名。plan 绑定 application、D-391 review authorization、sealed bundle、target projection、Automation adoption ledger 的有界 content-free 投影与 prepared head,并产生可重新计算的 row-set、candidate-set、adoption-set、plan-file 和 receipt digest。
durable publisher 固定写入 `<secretConfigRoot>/<secretConfigId>/{plan.ndjson,receipt.json,staging/}`,使用 no-replace publication、`0400/0500` 权限、文件与目录 `fsync`,并覆盖 plan、receipt、terminal seal、head CAS 四个 response-loss 窗口。只有 Automation 无需 adapter 时的 `reconciliation_application_planned`,或 Automation 已完成时的 `reconciliation_automation_applied`,可以单向推进到 `reconciliation_secret_config_planned`verify 只读复算 plan/receipt/seal/head 绑定,不修复漂移。active Env 若没有至少一条已采纳 Legacy Task ledger 记录仍为 manual;历史 `Configs` 计入 `unadaptedLegacyConfigCount` 并保持 manual。
### 4. 原子 application 必须同时完成 custody 与行为绑定
@@ -131,6 +133,6 @@ Cluster 不得把 Legacy Env 明文写入 PostgreSQL、ConfigMap、Job command
## 当前验证与后续门禁
D-397 当前切片已经实现并测试:absent、unsupported、Edge over-budget、2.x 顺序、同名连接、disabled preservation、保留前缀、异常状态、effective overflow、candidate digest、content-free diagnostics、私有有界 row plan、目标 Secret 冲突、no-effect/manual outcome、plan/receipt drift 与 plan 字节预算。调用方 visitor 的预算异常保持原始类型,不再被误报为 SQLite 读取失败。Local Admin 完整测试为 96/96Local Owner 完整测试为 277/270/7/0;后端完整门为 1563/1561/2/018-package clean build/test 为 2924/2902/22/0。package boundary、Cluster dependency、Edge import 与十四档 Local artifact audit 全部 compatibleworkspace 保持 18 packages、`singleSourcePackages=[]``shallowSourcePackages=[]`。第一切片远程 x64/arm64 backend 失败已定位为新增嵌套 Local Admin 文件后审阅计数仍停留在 47/46,本切片已同步 Local Admin 48/47Local Owner 176/175,并以精确文件 + subpath 规则允许 Secret/Config planner 读取 inspection相邻文件继续被依赖隔离门拒绝。
D-397 当前切片已经实现并测试:absent、unsupported、Edge over-budget、2.x 顺序、同名连接、disabled preservation、保留前缀、异常状态、effective overflow、candidate digest、content-free diagnostics、私有有界 row plan、目标 Secret 冲突、Automation adoption projection、no-effect/manual outcome、durable no-replace publication、terminal seal、head CAS、四个 response-loss 窗口、只读 verify 与 plan/receipt drift。调用方 visitor 的预算异常保持原始类型,不再被误报为 SQLite 读取失败。Local Admin 完整测试为 96/96Local Owner 完整测试为 287/280/7/0;后端完整门为 1563/1561/2/018-package clean build/test 为 2934/2912/22/0。package boundary、Cluster dependency、Edge import 与十四档 Local artifact audit 全部 compatible基础 Edge/Standalone 仍为 2,611,978 / 2,612,056 bytes、319 files、58 loaded modulesOwner-only authority 没有进入低资源常驻制品。workspace 保持 18 packages、`singleSourcePackages=[]``shallowSourcePackages=[]`。Local Admin 保持 48/47Local Owner 随两个职责明确的嵌套文件增至 178/177;根目录仍只有一个 50 行 binary entry,没有新增平铺源文件。依赖隔离门仍只允许 exact Secret/Config row planner 导入 inspection subpath相邻文件继续被拒绝。
转为 Accepted 前仍必须完成:独立 signed decision、原子 Secret/Task/Trigger/dispatch publisher、prepared/apply/rollback response-loss、completion schema 演进、18-package/boundary/artifact gates、真实 Edge 空间预算、PostgreSQL HA 与 Cluster Secret provider live gate。
+1 -1
View File
@@ -494,7 +494,7 @@
| [ADR-0488](./ADR-0488-cross-domain-reconciliation-completion-fence.md) | 跨领域 Reconciliation 完成围栏与目标重启授权 | Accepted |
| [ADR-0489](./ADR-0489-service-manager-completion-restart-lineage.md) | Service Manager 完成围栏重启谱系 | Accepted |
| [ADR-0490](./ADR-0490-run-history-terminal-preservation.md) | Run History 终态保全与跨领域完成证明 | Accepted |
| [ADR-0491](./ADR-0491-bounded-secret-config-reconciliation-and-task-binding.md) | 有界 Secret/Config Reconciliation 与任务环境绑定 | ProposedD-397 inspection + row plan |
| [ADR-0491](./ADR-0491-bounded-secret-config-reconciliation-and-task-binding.md) | 有界 Secret/Config Reconciliation 与任务环境绑定 | ProposedD-397 inspection + durable plan |
## 规则
@@ -37,6 +37,7 @@ export type LocalCutoverInstanceHeadState =
| 'reconciliation_automation_apply_prepared'
| 'reconciliation_automation_applied'
| 'reconciliation_automation_rolled_back'
| 'reconciliation_secret_config_planned'
| 'reconciliation_completed'
| 'rollback_prepared'
| 'legacy_restart_requested'
@@ -183,6 +184,7 @@ function parseHead(value: unknown): Readonly<LocalCutoverInstanceHead> {
head.state !== 'reconciliation_automation_apply_prepared' &&
head.state !== 'reconciliation_automation_applied' &&
head.state !== 'reconciliation_automation_rolled_back' &&
head.state !== 'reconciliation_secret_config_planned' &&
head.state !== 'reconciliation_completed' &&
head.state !== 'rollback_prepared' &&
head.state !== 'legacy_restart_requested' &&
@@ -364,6 +366,7 @@ export function advanceLocalCutoverInstanceHead(
| 'reconciliation_automation_apply_prepared'
| 'reconciliation_automation_applied'
| 'reconciliation_automation_rolled_back'
| 'reconciliation_secret_config_planned'
| 'reconciliation_completed'
| 'rollback_prepared'
| 'legacy_restart_requested'
@@ -417,6 +420,7 @@ export function advanceLocalCutoverInstanceHead(
current.state === 'reconciliation_automation_apply_prepared' ||
current.state === 'reconciliation_automation_applied' ||
current.state === 'reconciliation_automation_rolled_back' ||
current.state === 'reconciliation_secret_config_planned' ||
current.state === 'reconciliation_completed' ||
current.state === 'legacy_restart_requested' ||
current.state === 'legacy_running' ||
@@ -462,6 +466,9 @@ export function advanceLocalCutoverInstanceHead(
current.state === 'reconciliation_automation_apply_prepared') ||
(state === 'reconciliation_automation_rolled_back' &&
current.state === 'reconciliation_automation_applied') ||
(state === 'reconciliation_secret_config_planned' &&
(current.state === 'reconciliation_application_planned' ||
current.state === 'reconciliation_automation_applied')) ||
(state === 'reconciliation_completed' &&
(current.state === 'reconciliation_application_planned' ||
current.state === 'reconciliation_automation_applied')) ||
@@ -154,6 +154,13 @@ import {
verifyLocalReconciliationAutomationApply,
verifyLocalReconciliationAutomationApplyCommandFile,
} from './reconciliation/application/automation/applyCoordinator';
import {
planLocalReconciliationSecretConfig,
planLocalReconciliationSecretConfigCommandFile,
readLocalReconciliationSecretConfigTerminal,
verifyLocalReconciliationSecretConfigPlan,
verifyLocalReconciliationSecretConfigPlanCommandFile,
} from './reconciliation/application/secret-and-config/coordinator';
import {
preserveLocalReconciliationRunHistory,
preserveLocalReconciliationRunHistoryCommandFile,
@@ -206,6 +213,11 @@ export {
verifyLocalReconciliationAutomationApplyCommandFile,
rollbackLocalReconciliationAutomationApply,
rollbackLocalReconciliationAutomationApplyCommandFile,
planLocalReconciliationSecretConfig,
planLocalReconciliationSecretConfigCommandFile,
readLocalReconciliationSecretConfigTerminal,
verifyLocalReconciliationSecretConfigPlan,
verifyLocalReconciliationSecretConfigPlanCommandFile,
preserveLocalReconciliationRunHistory,
preserveLocalReconciliationRunHistoryCommandFile,
readLocalReconciliationRunHistoryTerminal,
@@ -325,6 +337,31 @@ export {
type LocalReconciliationAutomationPlanSummary,
type LocalReconciliationAutomationRowRequirement,
} from './reconciliation/application/automation/rowPlan';
export {
normalizeLocalReconciliationSecretConfigPlanCommand,
normalizeLocalReconciliationSecretConfigVerifyCommand,
type LocalReconciliationSecretConfigOptions,
type LocalReconciliationSecretConfigPlanCommand,
type LocalReconciliationSecretConfigPlanResult,
type LocalReconciliationSecretConfigVerifyCommand,
} from './reconciliation/application/secret-and-config/contract';
export {
type LocalReconciliationSecretConfigPlanDependencies,
type LocalReconciliationSecretConfigTerminal,
} from './reconciliation/application/secret-and-config/coordinator';
export {
MAX_EDGE_LOCAL_RECONCILIATION_SECRET_CONFIG_PLAN_BYTES,
MAX_STANDALONE_LOCAL_RECONCILIATION_SECRET_CONFIG_PLAN_BYTES,
normalizeLocalReconciliationSecretConfigPlanReceipt,
type LocalReconciliationSecretConfigCandidateRequirement,
type LocalReconciliationSecretConfigPlanCandidate,
type LocalReconciliationSecretConfigPlanFooter,
type LocalReconciliationSecretConfigPlanHeader,
type LocalReconciliationSecretConfigPlanReceipt,
type LocalReconciliationSecretConfigPlanRow,
type LocalReconciliationSecretConfigPlanSummary,
type LocalReconciliationSecretConfigTarget,
} from './reconciliation/application/secret-and-config/rowPlan';
export {
LocalDeploymentConfigurationError,
@@ -33,6 +33,8 @@ import {
applyLocalReconciliationAutomationCommandFile,
verifyLocalReconciliationAutomationApplyCommandFile,
rollbackLocalReconciliationAutomationApplyCommandFile,
planLocalReconciliationSecretConfigCommandFile,
verifyLocalReconciliationSecretConfigPlanCommandFile,
preserveLocalReconciliationRunHistoryCommandFile,
verifyLocalReconciliationRunHistoryCommandFile,
completeLocalReconciliationCommandFile,
@@ -52,7 +54,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|reconciliation-review-commit|reconciliation-review-verify|reconciliation-application-prepare|reconciliation-application-commit|reconciliation-application-verify|reconciliation-automation-plan|reconciliation-automation-verify|reconciliation-automation-decision-prepare|reconciliation-automation-decision-commit|reconciliation-automation-decision-verify|reconciliation-automation-apply|reconciliation-automation-apply-verify|reconciliation-automation-apply-rollback|reconciliation-run-history-preserve|reconciliation-run-history-verify|reconciliation-complete|reconciliation-complete-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';
'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|reconciliation-application-prepare|reconciliation-application-commit|reconciliation-application-verify|reconciliation-automation-plan|reconciliation-automation-verify|reconciliation-automation-decision-prepare|reconciliation-automation-decision-commit|reconciliation-automation-decision-verify|reconciliation-automation-apply|reconciliation-automation-apply-verify|reconciliation-automation-apply-rollback|reconciliation-secret-config-plan|reconciliation-secret-config-verify|reconciliation-run-history-preserve|reconciliation-run-history-verify|reconciliation-complete|reconciliation-complete-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')) {
@@ -102,6 +104,8 @@ async function main(argv: readonly string[]): Promise<void> {
argv[0] !== 'reconciliation-automation-apply' &&
argv[0] !== 'reconciliation-automation-apply-verify' &&
argv[0] !== 'reconciliation-automation-apply-rollback' &&
argv[0] !== 'reconciliation-secret-config-plan' &&
argv[0] !== 'reconciliation-secret-config-verify' &&
argv[0] !== 'reconciliation-run-history-preserve' &&
argv[0] !== 'reconciliation-run-history-verify' &&
argv[0] !== 'reconciliation-complete' &&
@@ -218,6 +222,10 @@ async function main(argv: readonly string[]): Promise<void> {
? verifyLocalReconciliationAutomationApplyCommandFile(argv[2]!)
: argv[0] === 'reconciliation-automation-apply-rollback'
? rollbackLocalReconciliationAutomationApplyCommandFile(argv[2]!)
: argv[0] === 'reconciliation-secret-config-plan'
? planLocalReconciliationSecretConfigCommandFile(argv[2]!)
: argv[0] === 'reconciliation-secret-config-verify'
? verifyLocalReconciliationSecretConfigPlanCommandFile(argv[2]!)
: argv[0] === 'reconciliation-run-history-preserve'
? preserveLocalReconciliationRunHistoryCommandFile(argv[2]!)
: argv[0] === 'reconciliation-run-history-verify'
@@ -0,0 +1,312 @@
import path from 'node:path';
import { currentIdentity } from '../../../foundation/contract';
import { LocalDeploymentConfigurationError } from '../../../foundation/error';
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 SAFE_PATH_PATTERN = /^\/[A-Za-z0-9._/@-]+$/;
const MAX_PATH_BYTES = 4_096;
export interface LocalReconciliationSecretConfigOptions {
readonly deploymentRoot: string;
readonly applicationRoot: string;
readonly secretConfigRoot: string;
readonly allowRootService: boolean;
}
export interface LocalReconciliationSecretConfigPlanCommand {
readonly schemaVersion: 1;
readonly operation: 'local.deployment.reconciliation.secret-config.plan';
readonly options: Readonly<LocalReconciliationSecretConfigOptions>;
readonly request: Readonly<{
secretConfigId: string;
applicationId: string;
expectedApplicationPlanDigest: string;
expectedHeadDigest: string;
decisionFilePath: string;
projectId: string;
preparedAtMs: number;
}>;
}
export interface LocalReconciliationSecretConfigVerifyCommand {
readonly schemaVersion: 1;
readonly operation: 'local.deployment.reconciliation.secret-config.verify';
readonly options: Readonly<LocalReconciliationSecretConfigOptions>;
readonly request: Readonly<{
secretConfigId: string;
expectedSecretConfigPlanDigest: string;
}>;
}
export interface LocalReconciliationSecretConfigPlanResult {
readonly schemaVersion: 1;
readonly operation:
| LocalReconciliationSecretConfigPlanCommand['operation']
| LocalReconciliationSecretConfigVerifyCommand['operation'];
readonly status: 'prepared' | 'existing' | 'verified';
readonly state: 'reconciliation_secret_config_planned';
readonly secretConfigId: string;
readonly secretConfigPlanDigest: string;
readonly outcome: 'ready' | 'manual_required' | 'no_effect';
readonly rowCount: number;
readonly eligibleBindingCount: number;
readonly eligiblePreservationCount: number;
readonly targetConflictCount: number;
readonly adoptedLegacyTaskCount: number;
readonly unadaptedLegacyConfigCount: number;
readonly instanceHeadDigest: string;
}
function configurationError(message: string): never {
throw new LocalDeploymentConfigurationError(
`reconciliation secret config ${message}`,
);
}
function object(value: unknown, label: string): Record<string, unknown> {
if (
!value ||
typeof value !== 'object' ||
Array.isArray(value) ||
(Object.getPrototypeOf(value) !== Object.prototype &&
Object.getPrototypeOf(value) !== null)
) {
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 overlaps(left: string, right: string): boolean {
const relative = path.relative(left, right);
return (
relative === '' ||
(!relative.startsWith('..') && !path.isAbsolute(relative))
);
}
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 identifier(value: unknown, label: string): string {
if (typeof value !== 'string' || !UUID_V4_PATTERN.test(value)) {
configurationError(`${label} must be a lowercase UUID v4`);
}
return value;
}
function projectId(value: unknown): string {
if (
typeof value !== 'string' ||
value.length < 1 ||
value.length > 128 ||
/[\u0000-\u001f\u007f]/u.test(value)
) {
configurationError('projectId is invalid');
}
return value;
}
function normalizeOptions(
value: unknown,
): Readonly<LocalReconciliationSecretConfigOptions> {
const options = object(value, 'options');
exact(
options,
[
'allowRootService',
'applicationRoot',
'deploymentRoot',
'secretConfigRoot',
],
'options',
);
const identity = currentIdentity();
if (
typeof options.allowRootService !== 'boolean' ||
(identity.uid === 0) !== options.allowRootService
) {
configurationError('command identity is invalid');
}
const roots = [
safePath(options.deploymentRoot, 'deploymentRoot'),
safePath(options.applicationRoot, 'applicationRoot'),
safePath(options.secretConfigRoot, 'secretConfigRoot'),
];
for (let left = 0; left < roots.length; left += 1) {
for (let right = left + 1; right < roots.length; right += 1) {
if (
overlaps(roots[left]!, roots[right]!) ||
overlaps(roots[right]!, roots[left]!)
) {
configurationError('authority roots overlap');
}
}
}
return Object.freeze({
deploymentRoot: roots[0]!,
applicationRoot: roots[1]!,
secretConfigRoot: roots[2]!,
allowRootService: options.allowRootService,
});
}
function command(
value: unknown,
operation:
| LocalReconciliationSecretConfigPlanCommand['operation']
| LocalReconciliationSecretConfigVerifyCommand['operation'],
): Readonly<{
options: Readonly<LocalReconciliationSecretConfigOptions>;
request: Record<string, unknown>;
}> {
const selected = object(value, 'command');
exact(
selected,
['operation', 'options', 'request', 'schemaVersion'],
'command',
);
if (selected.schemaVersion !== 1 || selected.operation !== operation) {
configurationError('command version or operation is invalid');
}
return Object.freeze({
options: normalizeOptions(selected.options),
request: object(selected.request, 'request'),
});
}
export function normalizeLocalReconciliationSecretConfigPlanCommand(
value: unknown,
): Readonly<LocalReconciliationSecretConfigPlanCommand> {
const selected = command(
value,
'local.deployment.reconciliation.secret-config.plan',
);
exact(
selected.request,
[
'applicationId',
'decisionFilePath',
'expectedApplicationPlanDigest',
'expectedHeadDigest',
'preparedAtMs',
'projectId',
'secretConfigId',
],
'request',
);
const decisionFilePath = safePath(
selected.request.decisionFilePath,
'decisionFilePath',
);
if (
[
selected.options.deploymentRoot,
selected.options.applicationRoot,
selected.options.secretConfigRoot,
].some(
(root) =>
overlaps(root, decisionFilePath) || overlaps(decisionFilePath, root),
)
) {
configurationError('decisionFilePath overlaps an authority root');
}
if (
!Number.isSafeInteger(selected.request.preparedAtMs) ||
(selected.request.preparedAtMs as number) < 0
) {
configurationError('preparedAtMs is invalid');
}
return Object.freeze({
schemaVersion: 1,
operation: 'local.deployment.reconciliation.secret-config.plan',
options: selected.options,
request: Object.freeze({
secretConfigId: identifier(
selected.request.secretConfigId,
'secretConfigId',
),
applicationId: identifier(
selected.request.applicationId,
'applicationId',
),
expectedApplicationPlanDigest: digest(
selected.request.expectedApplicationPlanDigest,
'expectedApplicationPlanDigest',
),
expectedHeadDigest: digest(
selected.request.expectedHeadDigest,
'expectedHeadDigest',
),
decisionFilePath,
projectId: projectId(selected.request.projectId),
preparedAtMs: selected.request.preparedAtMs as number,
}),
});
}
export function normalizeLocalReconciliationSecretConfigVerifyCommand(
value: unknown,
): Readonly<LocalReconciliationSecretConfigVerifyCommand> {
const selected = command(
value,
'local.deployment.reconciliation.secret-config.verify',
);
exact(
selected.request,
['expectedSecretConfigPlanDigest', 'secretConfigId'],
'request',
);
return Object.freeze({
schemaVersion: 1,
operation: 'local.deployment.reconciliation.secret-config.verify',
options: selected.options,
request: Object.freeze({
secretConfigId: identifier(
selected.request.secretConfigId,
'secretConfigId',
),
expectedSecretConfigPlanDigest: digest(
selected.request.expectedSecretConfigPlanDigest,
'expectedSecretConfigPlanDigest',
),
}),
});
}
@@ -0,0 +1,863 @@
import fs from 'node:fs';
import path from 'node:path';
import { readPrivateLocalCommandFile } from '@qinglong/local-command-file';
import { currentIdentity } from '../../../foundation/contract';
import { LocalDeploymentConfigurationError } from '../../../foundation/error';
import {
ensurePrivateDirectory,
publishExactFile,
syncPublishedDirectory,
validatePrivateDirectory,
} from '../../../foundation/files';
import {
advanceLocalCutoverInstanceHead,
readLocalCutoverInstanceHead,
type LocalCutoverInstanceHead,
} from '../../../cutover/instanceLineage';
import { readLocalReconciliationPlanTerminal } from '../../planning/preparation';
import {
assertLocalReconciliationReviewDecisionMatchesFact,
withLocalReconciliationReviewDecisionFile,
} from '../../review/decisionFile';
import { visitLocalReconciliationDiagnosticFacts } from '../../review/diagnostics';
import { withLocalReconciliationSealedDatabase } from '../../sealed-bundle/reader';
import {
readLocalReconciliationApplicationTerminal,
type LocalReconciliationApplicationTerminal,
} from '../coordinator';
import {
normalizeLocalReconciliationSecretConfigPlanCommand,
normalizeLocalReconciliationSecretConfigVerifyCommand,
type LocalReconciliationSecretConfigPlanCommand,
type LocalReconciliationSecretConfigPlanResult,
} from './contract';
import {
buildLocalReconciliationSecretConfigPlanReceipt,
hashLocalReconciliationSecretConfigPlanFile,
MAX_EDGE_LOCAL_RECONCILIATION_SECRET_CONFIG_PLAN_BYTES,
MAX_STANDALONE_LOCAL_RECONCILIATION_SECRET_CONFIG_PLAN_BYTES,
normalizeLocalReconciliationSecretConfigPlanReceipt,
writeLocalReconciliationSecretConfigPlan,
type LocalReconciliationSecretConfigPlanHeader,
type LocalReconciliationSecretConfigPlanReceipt,
} from './rowPlan';
const MAX_RECEIPT_BYTES = 64 * 1024;
export interface LocalReconciliationSecretConfigPlanDependencies {
readonly beforeDatabaseOpen?: (
kind: 'legacy' | 'target',
mode: 'main_only_immutable' | 'wal_shm_readonly',
cacheKiB: 2_048 | 8_192,
) => void;
readonly afterDatabaseClose?: (kind: 'legacy' | 'target') => void;
readonly afterPlanPublished?: () => void;
readonly afterReceiptPublished?: () => void;
readonly afterTerminalSealed?: () => void;
readonly afterHeadAdvanced?: () => void;
}
interface SecretConfigPaths {
readonly root: string;
readonly staging: string;
readonly plan: string;
readonly planStage: string;
readonly receipt: string;
}
interface SecretConfigReviewAuthority {
readonly tableDisposition: 'absent' | 'manual_external';
readonly unadaptedLegacyConfigCount: number;
readonly decisionFileDigest: string;
readonly confirmDecisionFileIdentity: () => void;
readonly planTerminal: ReturnType<typeof readLocalReconciliationPlanTerminal>;
}
function configurationError(message: string, cause?: unknown): never {
throw new LocalDeploymentConfigurationError(
`reconciliation secret config ${message}`,
{ cause },
);
}
function secretConfigPaths(
secretConfigRoot: string,
secretConfigId: string,
): Readonly<SecretConfigPaths> {
const root = path.join(secretConfigRoot, secretConfigId);
const staging = path.join(root, 'staging');
return Object.freeze({
root,
staging,
plan: path.join(root, 'plan.ndjson'),
planStage: path.join(staging, 'plan.ndjson.stage'),
receipt: path.join(root, 'receipt.json'),
});
}
function validateCatalog(
selected: Readonly<SecretConfigPaths>,
terminal: boolean,
): void {
const allowed = new Set([
'plan.ndjson',
'receipt.json',
'staging',
...(!terminal ? ['.receipt.json.ql3-deploy-stage'] : []),
]);
for (const entry of fs.readdirSync(selected.root, { withFileTypes: true })) {
if (!allowed.has(entry.name) || entry.isSymbolicLink()) {
configurationError('plan root contains unknown material');
}
}
const stagingEntries = fs.readdirSync(selected.staging);
if (
terminal
? stagingEntries.length !== 0
: stagingEntries.some((entry) => entry !== 'plan.ndjson.stage')
) {
configurationError('plan staging contains unknown material');
}
}
function validateDirectory(
directory: string,
uid: number,
modes: readonly number[],
label: string,
): number {
let stat: fs.Stats;
try {
stat = fs.lstatSync(directory);
} catch (error) {
return 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 readReceipt(
filePath: string,
uid: number,
allowedModes: readonly number[],
): Readonly<LocalReconciliationSecretConfigPlanReceipt> {
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_RECEIPT_BYTES)
) {
configurationError('receipt 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('receipt changed while opening');
}
bytes = fs.readFileSync(descriptor);
const after = fs.fstatSync(descriptor, { bigint: true });
const current = fs.lstatSync(filePath, { bigint: true });
if (
after.dev !== before.dev ||
after.ino !== before.ino ||
after.size !== before.size ||
current.dev !== before.dev ||
current.ino !== before.ino ||
current.mtimeNs !== before.mtimeNs ||
current.ctimeNs !== before.ctimeNs ||
current.mode !== before.mode ||
current.nlink !== before.nlink
) {
configurationError('receipt changed while reading');
}
return normalizeLocalReconciliationSecretConfigPlanReceipt(
JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes)),
);
} catch (error) {
if (error instanceof LocalDeploymentConfigurationError) throw error;
return configurationError('receipt cannot be read', error);
} finally {
bytes?.fill(0);
if (descriptor !== undefined) fs.closeSync(descriptor);
}
}
function validatePlanFile(
filePath: string,
receipt: Readonly<LocalReconciliationSecretConfigPlanReceipt>,
uid: number,
allowedModes: readonly number[],
): void {
let descriptor: number | 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 !== BigInt(receipt.planFileBytes)
) {
configurationError('plan file 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 ||
hashLocalReconciliationSecretConfigPlanFile(
descriptor,
receipt.planFileBytes,
) !== receipt.planFileDigest
) {
configurationError('plan file content drifted');
}
const after = fs.fstatSync(descriptor, { bigint: true });
const current = fs.lstatSync(filePath, { bigint: true });
if (
after.dev !== before.dev ||
after.ino !== before.ino ||
after.mtimeNs !== before.mtimeNs ||
after.ctimeNs !== before.ctimeNs ||
current.dev !== before.dev ||
current.ino !== before.ino ||
current.mtimeNs !== before.mtimeNs ||
current.ctimeNs !== before.ctimeNs
) {
configurationError('plan file changed while verifying');
}
} catch (error) {
if (error instanceof LocalDeploymentConfigurationError) throw error;
configurationError('plan file cannot be verified', error);
} finally {
if (descriptor !== undefined) fs.closeSync(descriptor);
}
}
function receiptContents(
receipt: Readonly<LocalReconciliationSecretConfigPlanReceipt>,
): string {
const contents = `${JSON.stringify(receipt, null, 2)}\n`;
if (Buffer.byteLength(contents, 'utf8') > MAX_RECEIPT_BYTES) {
configurationError('receipt exceeds 64 KiB');
}
return contents;
}
function expectedPriorState(
terminal: Readonly<LocalReconciliationApplicationTerminal>,
): 'reconciliation_application_planned' | 'reconciliation_automation_applied' {
const automation = terminal.plan.domains.find(
(selected) => selected.domain === 'automation',
);
if (!automation) configurationError('Automation domain summary is missing');
return automation.action === 'adapter_required' ||
automation.action === 'adapter_and_manual'
? 'reconciliation_automation_applied'
: 'reconciliation_application_planned';
}
function validateApplicationBinding(
command: Readonly<LocalReconciliationSecretConfigPlanCommand>,
terminal: Readonly<LocalReconciliationApplicationTerminal>,
head: Readonly<LocalCutoverInstanceHead>,
): void {
const secretConfig = terminal.plan.domains.find(
(selected) => selected.domain === 'secret_and_config',
);
const priorState = expectedPriorState(terminal);
if (
terminal.intent.command.options.deploymentRoot !==
command.options.deploymentRoot ||
terminal.intent.command.options.applicationRoot !==
command.options.applicationRoot ||
terminal.plan.applicationId !== command.request.applicationId ||
terminal.plan.applicationPlanDigest !==
command.request.expectedApplicationPlanDigest ||
terminal.head.state !== 'reconciliation_application_planned' ||
terminal.head.sourceRecordDigest !== terminal.plan.applicationPlanDigest ||
!secretConfig ||
secretConfig.action !== 'manual_external' ||
head.state !== priorState ||
head.headDigest !== command.request.expectedHeadDigest ||
(priorState === 'reconciliation_application_planned' &&
head.sourceRecordDigest !== terminal.plan.applicationPlanDigest) ||
command.request.preparedAtMs < terminal.plan.committedAtMs ||
command.request.preparedAtMs < head.updatedAtMs
) {
configurationError('plan is detached from its ordered application head');
}
}
function reviewAuthority(
command: Readonly<LocalReconciliationSecretConfigPlanCommand>,
terminal: Readonly<LocalReconciliationApplicationTerminal>,
dependencies: LocalReconciliationSecretConfigPlanDependencies,
uid: number,
): Readonly<SecretConfigReviewAuthority> {
const planTerminal = readLocalReconciliationPlanTerminal(
terminal.review.intent.command.options.planRoot,
terminal.review.intent.command.request.planId,
uid,
);
let envDisposition: 'absent' | 'manual_external' = 'absent';
let envSeen = false;
let unadaptedLegacyConfigCount = 0;
const reviewed = withLocalReconciliationReviewDecisionFile(
command.request.decisionFilePath,
{
reviewId: terminal.review.review.reviewId,
profile: terminal.plan.profile,
planDigest: planTerminal.plan.planDigest,
preparationDigest: terminal.review.intent.preparationDigest,
},
(cursor) => {
for (const database of ['legacy', 'target'] as const) {
const opened = withLocalReconciliationSealedDatabase(
planTerminal.bundle,
database,
uid,
dependencies,
(client) =>
visitLocalReconciliationDiagnosticFacts(
client,
database,
(fact) => {
if (fact.decisionRequirement === 'informational') return;
const decision = cursor.next();
if (decision === null) {
configurationError('decision file omitted a canonical fact');
}
assertLocalReconciliationReviewDecisionMatchesFact(
decision,
fact,
);
if (fact.domain !== 'secret_and_config') return;
if (decision.disposition !== 'manual_external') {
configurationError(
'Secret/Config facts require explicit external custody review',
);
}
if (
fact.database === 'legacy' &&
fact.factKind === 'table' &&
fact.tableName === 'Envs'
) {
if (envSeen) {
configurationError('legacy Envs authority is ambiguous');
}
envSeen = true;
envDisposition = 'manual_external';
}
if (
fact.database === 'legacy' &&
fact.factKind === 'table' &&
fact.tableName === 'Configs'
) {
unadaptedLegacyConfigCount += 1;
}
},
),
);
if (opened === null) {
configurationError('manual-required SQLite topology cannot be adapted');
}
}
},
);
if (
reviewed.evidence.fileDigest !==
terminal.review.authorization.decisionFileDigest ||
reviewed.evidence.decisionCount !==
terminal.review.authorization.decisionCount
) {
configurationError('signed review authority drifted');
}
return Object.freeze({
tableDisposition: envDisposition,
unadaptedLegacyConfigCount,
decisionFileDigest: reviewed.evidence.fileDigest,
confirmDecisionFileIdentity: reviewed.confirmIdentity,
planTerminal,
});
}
function maxPlanBytes(profile: 'edge' | 'standalone'): number {
return profile === 'edge'
? MAX_EDGE_LOCAL_RECONCILIATION_SECRET_CONFIG_PLAN_BYTES
: MAX_STANDALONE_LOCAL_RECONCILIATION_SECRET_CONFIG_PLAN_BYTES;
}
function publishPlan(
selected: Readonly<SecretConfigPaths>,
command: Readonly<LocalReconciliationSecretConfigPlanCommand>,
terminal: Readonly<LocalReconciliationApplicationTerminal>,
head: Readonly<LocalCutoverInstanceHead>,
authority: Readonly<SecretConfigReviewAuthority>,
dependencies: LocalReconciliationSecretConfigPlanDependencies,
uid: number,
): Readonly<LocalReconciliationSecretConfigPlanReceipt> {
let descriptor: number | undefined;
let createdStage = false;
try {
descriptor = fs.openSync(
selected.planStage,
fs.constants.O_CREAT |
fs.constants.O_EXCL |
fs.constants.O_WRONLY |
(fs.constants.O_NOFOLLOW ?? 0),
0o600,
);
createdStage = true;
fs.fchmodSync(descriptor, 0o600);
const header: Omit<
LocalReconciliationSecretConfigPlanHeader,
'headerDigest'
> = Object.freeze({
schemaVersion: 1,
kind: 'qinglong3-local-reconciliation-secret-config-plan-header',
secretConfigId: command.request.secretConfigId,
applicationId: command.request.applicationId,
applicationPlanDigest: terminal.plan.applicationPlanDigest,
reviewDigest: terminal.review.review.reviewDigest,
reviewAuthorizationDigest:
terminal.review.authorization.authorizationDigest,
reviewDecisionSetDigest: terminal.review.authorization.decisionSetDigest,
reviewDecisionFileDigest: authority.decisionFileDigest,
bundleDigest: authority.planTerminal.bundle.receipt.bundleDigest,
bundleFingerprintDigest: authority.planTerminal.bundle.fingerprintDigest,
profile: terminal.plan.profile,
projectId: command.request.projectId,
tableDisposition: authority.tableDisposition,
unadaptedLegacyConfigCount: authority.unadaptedLegacyConfigCount,
preparedHeadDigest: head.headDigest,
preparedAtMs: command.request.preparedAtMs,
});
const generated = withLocalReconciliationSealedDatabase(
authority.planTerminal.bundle,
'target',
uid,
dependencies,
(target) =>
withLocalReconciliationSealedDatabase(
authority.planTerminal.bundle,
'legacy',
uid,
dependencies,
(legacy) =>
writeLocalReconciliationSecretConfigPlan({
descriptor: descriptor!,
maxBytes: maxPlanBytes(terminal.plan.profile),
header,
legacy,
target,
}),
),
);
if (generated === null || generated === undefined) {
configurationError('manual-required SQLite topology cannot be planned');
}
fs.fsyncSync(descriptor);
fs.closeSync(descriptor);
descriptor = undefined;
authority.confirmDecisionFileIdentity();
const receipt = buildLocalReconciliationSecretConfigPlanReceipt(
generated.header,
generated.footer,
generated.fileBytes,
generated.fileDigest,
);
if (fs.existsSync(selected.plan)) {
validatePlanFile(selected.plan, receipt, uid, [0o600]);
} else {
try {
fs.linkSync(selected.planStage, selected.plan);
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
validatePlanFile(selected.plan, receipt, uid, [0o600]);
}
syncPublishedDirectory(selected.root);
}
fs.unlinkSync(selected.planStage);
createdStage = false;
syncPublishedDirectory(selected.staging);
return receipt;
} catch (error) {
if (error instanceof LocalDeploymentConfigurationError) throw error;
return configurationError('plan cannot be published', error);
} finally {
if (descriptor !== undefined) fs.closeSync(descriptor);
if (createdStage) {
try {
fs.unlinkSync(selected.planStage);
} catch {
// A complete stage remains recoverable; a partial stage fails closed.
}
}
}
}
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('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('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],
'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<SecretConfigPaths>, uid: number): void {
if (fs.readdirSync(selected.staging).length !== 0) {
configurationError('staging must be empty before terminal seal');
}
sealFile(selected.plan, uid);
sealFile(selected.receipt, uid);
sealDirectory(selected.staging, uid);
sealDirectory(selected.root, uid);
validateCatalog(selected, true);
}
function advanceHead(
terminal: Readonly<LocalReconciliationApplicationTerminal>,
receipt: Readonly<LocalReconciliationSecretConfigPlanReceipt>,
uid: number,
): Readonly<LocalCutoverInstanceHead> {
return advanceLocalCutoverInstanceHead(
{
options: {
deploymentRoot: terminal.intent.command.options.deploymentRoot,
},
request: {
cutoverId: terminal.intent.cutoverId,
profile: terminal.intent.profile,
instanceId: terminal.intent.instanceId,
expectedActivationDigest: terminal.intent.activationDigest,
requestedAtMs: receipt.preparedAtMs,
},
},
uid,
'reconciliation_secret_config_planned',
terminal.intent.generation,
receipt.secretConfigPlanDigest,
);
}
function result(
operation: LocalReconciliationSecretConfigPlanResult['operation'],
status: LocalReconciliationSecretConfigPlanResult['status'],
receipt: Readonly<LocalReconciliationSecretConfigPlanReceipt>,
head: Readonly<LocalCutoverInstanceHead>,
): Readonly<LocalReconciliationSecretConfigPlanResult> {
return Object.freeze({
schemaVersion: 1,
operation,
status,
state: 'reconciliation_secret_config_planned',
secretConfigId: receipt.secretConfigId,
secretConfigPlanDigest: receipt.secretConfigPlanDigest,
outcome: receipt.outcome,
rowCount: receipt.rowCount,
eligibleBindingCount: receipt.eligibleBindingCount,
eligiblePreservationCount: receipt.eligiblePreservationCount,
targetConflictCount: receipt.targetConflictCount,
adoptedLegacyTaskCount: receipt.adoptedLegacyTaskCount,
unadaptedLegacyConfigCount: receipt.unadaptedLegacyConfigCount,
instanceHeadDigest: head.headDigest,
});
}
function validateTerminalBinding(
receipt: Readonly<LocalReconciliationSecretConfigPlanReceipt>,
terminal: Readonly<LocalReconciliationApplicationTerminal>,
secretConfigId: string,
): void {
if (
receipt.secretConfigId !== secretConfigId ||
receipt.applicationId !== terminal.plan.applicationId ||
receipt.applicationPlanDigest !== terminal.plan.applicationPlanDigest
) {
configurationError('terminal plan binding drifted');
}
}
export interface LocalReconciliationSecretConfigTerminal {
readonly receipt: Readonly<LocalReconciliationSecretConfigPlanReceipt>;
readonly planPath: string;
}
export function readLocalReconciliationSecretConfigTerminal(
secretConfigRoot: string,
secretConfigId: string,
uid: number,
): Readonly<LocalReconciliationSecretConfigTerminal> {
const selected = secretConfigPaths(secretConfigRoot, secretConfigId);
validateDirectory(selected.root, uid, [0o500], 'Secret/Config plan root');
validateDirectory(selected.staging, uid, [0o500], 'Secret/Config staging');
validateCatalog(selected, true);
const receipt = readReceipt(selected.receipt, uid, [0o400]);
if (receipt.secretConfigId !== secretConfigId) {
configurationError('Secret/Config terminal identity drifted');
}
validatePlanFile(selected.plan, receipt, uid, [0o400]);
return Object.freeze({ receipt, planPath: selected.plan });
}
export async function planLocalReconciliationSecretConfig(
value: unknown,
dependencies: LocalReconciliationSecretConfigPlanDependencies = {},
): Promise<Readonly<LocalReconciliationSecretConfigPlanResult>> {
const command = normalizeLocalReconciliationSecretConfigPlanCommand(value);
const identity = currentIdentity();
for (const [directory, label] of [
[command.options.deploymentRoot, 'deploymentRoot'],
[command.options.applicationRoot, 'applicationRoot'],
[command.options.secretConfigRoot, 'secretConfigRoot'],
] as const) {
validatePrivateDirectory(directory, identity.uid, label);
}
const terminal = await readLocalReconciliationApplicationTerminal(
command.options.applicationRoot,
command.request.applicationId,
identity.uid,
);
const selected = secretConfigPaths(
command.options.secretConfigRoot,
command.request.secretConfigId,
);
if (fs.existsSync(selected.receipt)) {
validateDirectory(
selected.root,
identity.uid,
[0o700, 0o500],
'Secret/Config plan root',
);
validateDirectory(
selected.staging,
identity.uid,
[0o700, 0o500],
'Secret/Config staging',
);
validateCatalog(selected, false);
const receipt = readReceipt(selected.receipt, identity.uid, [0o600, 0o400]);
validateTerminalBinding(
receipt,
terminal,
command.request.secretConfigId,
);
if (
receipt.applicationPlanDigest !==
command.request.expectedApplicationPlanDigest ||
receipt.preparedHeadDigest !== command.request.expectedHeadDigest ||
receipt.preparedAtMs !== command.request.preparedAtMs
) {
configurationError('terminal plan is not an exact command replay');
}
validatePlanFile(selected.plan, receipt, identity.uid, [0o600, 0o400]);
let head = readLocalCutoverInstanceHead(
command.options.deploymentRoot,
terminal.intent.instanceId,
identity.uid,
);
const existing = head.state === 'reconciliation_secret_config_planned';
if (!existing) validateApplicationBinding(command, terminal, head);
if (
existing &&
head.sourceRecordDigest !== receipt.secretConfigPlanDigest
) {
configurationError('terminal plan head digest drifted');
}
sealTerminal(selected, identity.uid);
dependencies.afterTerminalSealed?.();
head = existing ? head : advanceHead(terminal, receipt, identity.uid);
dependencies.afterHeadAdvanced?.();
return result(
command.operation,
existing ? 'existing' : 'prepared',
receipt,
head,
);
}
const head = readLocalCutoverInstanceHead(
command.options.deploymentRoot,
terminal.intent.instanceId,
identity.uid,
);
validateApplicationBinding(command, terminal, head);
const authority = reviewAuthority(
command,
terminal,
dependencies,
identity.uid,
);
ensurePrivateDirectory(
selected.root,
identity.uid,
'secretConfigPlanDirectory',
);
ensurePrivateDirectory(
selected.staging,
identity.uid,
'secretConfigPlanStaging',
);
validateCatalog(selected, false);
const receipt = publishPlan(
selected,
command,
terminal,
head,
authority,
dependencies,
identity.uid,
);
dependencies.afterPlanPublished?.();
authority.confirmDecisionFileIdentity();
publishExactFile(
selected.receipt,
receiptContents(receipt),
0o600,
identity.uid,
'reconciliation secret config receipt',
);
dependencies.afterReceiptPublished?.();
sealTerminal(selected, identity.uid);
dependencies.afterTerminalSealed?.();
const advanced = advanceHead(terminal, receipt, identity.uid);
dependencies.afterHeadAdvanced?.();
return result(command.operation, 'prepared', receipt, advanced);
}
export async function verifyLocalReconciliationSecretConfigPlan(
value: unknown,
): Promise<Readonly<LocalReconciliationSecretConfigPlanResult>> {
const command = normalizeLocalReconciliationSecretConfigVerifyCommand(value);
const identity = currentIdentity();
for (const [directory, label] of [
[command.options.deploymentRoot, 'deploymentRoot'],
[command.options.applicationRoot, 'applicationRoot'],
[command.options.secretConfigRoot, 'secretConfigRoot'],
] as const) {
validatePrivateDirectory(directory, identity.uid, label);
}
const selected = secretConfigPaths(
command.options.secretConfigRoot,
command.request.secretConfigId,
);
validateDirectory(selected.root, identity.uid, [0o500], 'Secret/Config plan root');
validateDirectory(selected.staging, identity.uid, [0o500], 'Secret/Config staging');
validateCatalog(selected, true);
const receipt = readReceipt(selected.receipt, identity.uid, [0o400]);
if (
receipt.secretConfigPlanDigest !==
command.request.expectedSecretConfigPlanDigest
) {
configurationError('expected Secret/Config plan digest drifted');
}
const terminal = await readLocalReconciliationApplicationTerminal(
command.options.applicationRoot,
receipt.applicationId,
identity.uid,
);
validateTerminalBinding(receipt, terminal, command.request.secretConfigId);
validatePlanFile(selected.plan, receipt, identity.uid, [0o400]);
const head = readLocalCutoverInstanceHead(
command.options.deploymentRoot,
terminal.intent.instanceId,
identity.uid,
);
if (
head.state !== 'reconciliation_secret_config_planned' ||
head.sourceRecordDigest !== receipt.secretConfigPlanDigest
) {
configurationError('Secret/Config plan is detached from the instance head');
}
return result(command.operation, 'verified', receipt, head);
}
export function planLocalReconciliationSecretConfigCommandFile(
filePath: string,
dependencies: LocalReconciliationSecretConfigPlanDependencies = {},
): Promise<Readonly<LocalReconciliationSecretConfigPlanResult>> {
return planLocalReconciliationSecretConfig(
readPrivateLocalCommandFile(filePath),
dependencies,
);
}
export function verifyLocalReconciliationSecretConfigPlanCommandFile(
filePath: string,
): Promise<Readonly<LocalReconciliationSecretConfigPlanResult>> {
return verifyLocalReconciliationSecretConfigPlan(
readPrivateLocalCommandFile(filePath),
);
}
@@ -24,6 +24,8 @@ 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 HASH_BUFFER_BYTES = 64 * 1024;
const MAX_EDGE_AUTOMATION_ADOPTION_RECORDS = 128;
const MAX_STANDALONE_AUTOMATION_ADOPTION_RECORDS = 512;
export const MAX_EDGE_LOCAL_RECONCILIATION_SECRET_CONFIG_PLAN_BYTES =
8 * 1024 * 1024;
export const MAX_STANDALONE_LOCAL_RECONCILIATION_SECRET_CONFIG_PLAN_BYTES =
@@ -43,7 +45,8 @@ export interface LocalReconciliationSecretConfigPlanHeader {
readonly bundleFingerprintDigest: string;
readonly profile: 'edge' | 'standalone';
readonly projectId: string;
readonly tableDisposition: 'adopt_legacy' | 'retain_both';
readonly tableDisposition: 'absent' | 'manual_external';
readonly unadaptedLegacyConfigCount: number;
readonly preparedHeadDigest: string;
readonly preparedAtMs: number;
readonly headerDigest: string;
@@ -99,6 +102,9 @@ export interface LocalReconciliationSecretConfigPlanSummary {
readonly eligibleBindingCount: number;
readonly eligiblePreservationCount: number;
readonly targetConflictCount: number;
readonly automationAdoptionRecordCount: number;
readonly adoptedLegacyTaskCount: number;
readonly unadaptedLegacyConfigCount: number;
readonly outcome: 'ready' | 'manual_required' | 'no_effect';
}
@@ -110,6 +116,7 @@ export interface LocalReconciliationSecretConfigPlanFooter
readonly legacyInventoryDigest: string;
readonly rowSetDigest: string;
readonly candidateSetDigest: string;
readonly automationAdoptionSetDigest: string;
readonly secretConfigPlanDigest: string;
}
@@ -125,6 +132,7 @@ export interface LocalReconciliationSecretConfigPlanReceipt
readonly legacyInventoryDigest: string;
readonly rowSetDigest: string;
readonly candidateSetDigest: string;
readonly automationAdoptionSetDigest: string;
readonly secretConfigPlanDigest: string;
readonly planFileBytes: number;
readonly planFileDigest: string;
@@ -200,6 +208,154 @@ function bytesDigest(value: unknown, length: number, label: string): string {
return createHash('sha256').update(value).digest('hex');
}
function adoptionText(
row: Readonly<Record<string, unknown>>,
key: string,
pattern?: RegExp,
): string {
const value = row[key];
if (
typeof value !== 'string' ||
value.length < 1 ||
value.length > 128 ||
(pattern !== undefined && !pattern.test(value))
) {
fail(`target Automation adoption ${key} drifted`);
}
return value;
}
function adoptionCount(
row: Readonly<Record<string, unknown>>,
key: string,
maximum: number,
): number {
const value = row[key];
if (
!Number.isSafeInteger(value) ||
(value as number) < 0 ||
(value as number) > maximum
) {
fail(`target Automation adoption ${key} drifted`);
}
return value as number;
}
function targetAutomationAdoptionProjection(
target: DatabaseSync,
projectId: string,
profile: 'edge' | 'standalone',
): Readonly<{
recordCount: number;
adoptedTaskCount: number;
setDigest: string;
}> {
const maximumRecords =
profile === 'edge'
? MAX_EDGE_AUTOMATION_ADOPTION_RECORDS
: MAX_STANDALONE_AUTOMATION_ADOPTION_RECORDS;
const hash = createHash('sha256').update(
'qinglong3.local-reconciliation-secret-config-automation-adoption-set.v1\0',
);
let recordCount = 0;
let adoptedTaskCount = 0;
try {
const rows = target
.prepare(
`SELECT "mutation_id" AS "mutationId",
"decision_id" AS "decisionId",
"plan_digest" AS "planDigest",
"inventory_digest" AS "inventoryDigest",
"decision_digest" AS "decisionDigest",
"receipt_digest" AS "receiptDigest",
"authorization_file_digest" AS "authorizationFileDigest",
"publication_digest" AS "publicationDigest",
"row_count" AS "rowCount",
"adopted_task_count" AS "adoptedTaskCount",
"adopted_trigger_count" AS "adoptedTriggerCount",
"skipped_count" AS "skippedCount",
"audit_event_id" AS "auditEventId",
"created_at_ms" AS "createdAtMs"
FROM "QingLong3LegacyAdoptions"
WHERE "project_id" = ?
ORDER BY "created_at_ms" ASC, "mutation_id" ASC`,
)
.iterate(projectId) as Iterable<Readonly<Record<string, unknown>>>;
for (const row of rows) {
recordCount += 1;
if (recordCount > maximumRecords) {
fail('target Automation adoption projection exceeds profile budget');
}
const rowCount = adoptionCount(row, 'rowCount', 100_000);
const selectedAdoptedTaskCount = adoptionCount(
row,
'adoptedTaskCount',
rowCount,
);
const skippedCount = adoptionCount(row, 'skippedCount', rowCount);
if (selectedAdoptedTaskCount + skippedCount !== rowCount) {
fail('target Automation adoption row accounting drifted');
}
adoptedTaskCount += selectedAdoptedTaskCount;
if (!Number.isSafeInteger(adoptedTaskCount)) {
fail('target Automation adoption task count overflowed');
}
const payload = Object.freeze({
mutationId: adoptionText(row, 'mutationId', UUID_V4_PATTERN),
decisionId: adoptionText(
row,
'decisionId',
/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/,
),
planDigest: adoptionText(row, 'planDigest', DIGEST_PATTERN),
inventoryDigest: adoptionText(
row,
'inventoryDigest',
DIGEST_PATTERN,
),
decisionDigest: adoptionText(row, 'decisionDigest', DIGEST_PATTERN),
receiptDigest: adoptionText(row, 'receiptDigest', DIGEST_PATTERN),
authorizationFileDigest: adoptionText(
row,
'authorizationFileDigest',
DIGEST_PATTERN,
),
publicationDigest: adoptionText(
row,
'publicationDigest',
DIGEST_PATTERN,
),
rowCount,
adoptedTaskCount: selectedAdoptedTaskCount,
adoptedTriggerCount: adoptionCount(
row,
'adoptedTriggerCount',
500_000,
),
skippedCount,
auditEventId: adoptionText(row, 'auditEventId', UUID_V4_PATTERN),
createdAtMs: adoptionCount(
row,
'createdAtMs',
Number.MAX_SAFE_INTEGER,
),
});
if (payload.auditEventId !== payload.mutationId) {
fail('target Automation adoption audit binding drifted');
}
hash.update('\0').update(JSON.stringify(payload));
}
} catch (error) {
if (error instanceof LocalDeploymentConfigurationError) throw error;
return fail('target Automation adoption projection is unavailable', error);
}
return Object.freeze({
recordCount,
adoptedTaskCount,
setDigest: hash.digest('hex'),
});
}
function targetSecret(
target: DatabaseSync,
projectId: string,
@@ -402,6 +558,11 @@ export function writeLocalReconciliationSecretConfigPlan(
append(candidate, 'candidate');
},
});
const automationAdoption = targetAutomationAdoptionProjection(
options.target,
header.projectId,
header.profile,
);
const summary: LocalReconciliationSecretConfigPlanSummary = Object.freeze({
tableState: inventory.tableState,
rowCount: inventory.rowCount,
@@ -415,10 +576,17 @@ export function writeLocalReconciliationSecretConfigPlan(
eligibleBindingCount,
eligiblePreservationCount,
targetConflictCount,
automationAdoptionRecordCount: automationAdoption.recordCount,
adoptedLegacyTaskCount: automationAdoption.adoptedTaskCount,
unadaptedLegacyConfigCount: header.unadaptedLegacyConfigCount,
outcome:
inventory.tableState === 'absent' || inventory.rowCount === 0
(inventory.tableState === 'absent' || inventory.rowCount === 0) &&
header.unadaptedLegacyConfigCount === 0
? ('no_effect' as const)
: !inventory.mutationReady || targetConflictCount > 0
: !inventory.mutationReady ||
targetConflictCount > 0 ||
header.unadaptedLegacyConfigCount > 0 ||
(eligibleBindingCount > 0 && automationAdoption.adoptedTaskCount < 1)
? ('manual_required' as const)
: ('ready' as const),
});
@@ -430,6 +598,7 @@ export function writeLocalReconciliationSecretConfigPlan(
legacyInventoryDigest: inventory.inventoryDigest,
rowSetDigest: rowHash.digest('hex'),
candidateSetDigest: candidateHash.digest('hex'),
automationAdoptionSetDigest: automationAdoption.setDigest,
});
const footer = Object.freeze({
...footerPayload,
@@ -464,6 +633,7 @@ export function buildLocalReconciliationSecretConfigPlanReceipt(
legacyInventoryDigest: footer.legacyInventoryDigest,
rowSetDigest: footer.rowSetDigest,
candidateSetDigest: footer.candidateSetDigest,
automationAdoptionSetDigest: footer.automationAdoptionSetDigest,
secretConfigPlanDigest: footer.secretConfigPlanDigest,
planFileBytes,
planFileDigest,
@@ -479,6 +649,9 @@ export function buildLocalReconciliationSecretConfigPlanReceipt(
eligibleBindingCount: footer.eligibleBindingCount,
eligiblePreservationCount: footer.eligiblePreservationCount,
targetConflictCount: footer.targetConflictCount,
automationAdoptionRecordCount: footer.automationAdoptionRecordCount,
adoptedLegacyTaskCount: footer.adoptedLegacyTaskCount,
unadaptedLegacyConfigCount: footer.unadaptedLegacyConfigCount,
outcome: footer.outcome,
preparedAtMs: header.preparedAtMs,
});
@@ -493,8 +666,11 @@ export function normalizeLocalReconciliationSecretConfigPlanReceipt(
[
'activeGroupCount',
'activeRowCount',
'adoptedLegacyTaskCount',
'applicationId',
'applicationPlanDigest',
'automationAdoptionRecordCount',
'automationAdoptionSetDigest',
'bindingReadyCount',
'candidateSetDigest',
'disabledRowCount',
@@ -519,6 +695,7 @@ export function normalizeLocalReconciliationSecretConfigPlanReceipt(
'state',
'tableState',
'targetConflictCount',
'unadaptedLegacyConfigCount',
],
'receipt',
);
@@ -537,6 +714,7 @@ export function normalizeLocalReconciliationSecretConfigPlanReceipt(
receipt.legacyInventoryDigest,
receipt.rowSetDigest,
receipt.candidateSetDigest,
receipt.automationAdoptionSetDigest,
receipt.secretConfigPlanDigest,
receipt.planFileDigest,
receiptDigest,
@@ -556,6 +734,9 @@ export function normalizeLocalReconciliationSecretConfigPlanReceipt(
receipt.eligibleBindingCount,
receipt.eligiblePreservationCount,
receipt.targetConflictCount,
receipt.automationAdoptionRecordCount,
receipt.adoptedLegacyTaskCount,
receipt.unadaptedLegacyConfigCount,
receipt.planFileBytes,
receipt.preparedAtMs,
].every((count) => Number.isSafeInteger(count) && (count as number) >= 0) ||
@@ -771,7 +771,11 @@ function replayResult(
head.state === 'reconciliation_application_planned' ||
head.state === 'reconciliation_automation_planned' ||
head.state === 'reconciliation_automation_decision_prepared' ||
head.state === 'reconciliation_automation_reviewed');
head.state === 'reconciliation_automation_reviewed' ||
head.state === 'reconciliation_automation_apply_prepared' ||
head.state === 'reconciliation_automation_applied' ||
head.state === 'reconciliation_automation_rolled_back' ||
head.state === 'reconciliation_secret_config_planned');
const completionRestartPendingHead =
(record.state === 'target_active' || record.state === 'manual_required') &&
completionFence !== undefined &&
@@ -22,6 +22,7 @@ const {
readLocalReconciliationAutomationDecisionTerminal,
rollbackLocalReconciliationAutomationApply,
planLocalReconciliationAutomation,
planLocalReconciliationSecretConfig,
prepareLocalReconciliationPlan,
prepareLocalReconciliationReview,
verifyLocalReconciliationCapture,
@@ -29,6 +30,7 @@ const {
verifyLocalReconciliationAutomationDecision,
verifyLocalReconciliationAutomationApply,
verifyLocalReconciliationAutomationPlan,
verifyLocalReconciliationSecretConfigPlan,
verifyLocalReconciliationCompletion,
verifyLocalReconciliationPlan,
verifyLocalReconciliationReview,
@@ -791,6 +793,58 @@ function automationReadyDatabaseInitializer() {
};
}
function secretConfigDatabaseInitializer({
active = false,
configs = false,
} = {}) {
return ({ legacySourcePath, recoveryPath, targetDatabasePath }) => {
const legacy = new DatabaseSync(legacySourcePath);
legacy.exec(`
CREATE TABLE "Envs" (
id INTEGER PRIMARY KEY,
name TEXT,
value TEXT,
status INTEGER,
position REAL,
"isPinned" INTEGER,
"createdAt" TEXT
);
INSERT INTO "Envs" VALUES (
1,
'${active ? 'ACTIVE_TOKEN' : 'DISABLED_TOKEN'}',
'private-secret-value',
${active ? 0 : 1},
1,
0,
'2026-01-01'
);
${
configs
? 'CREATE TABLE "Configs" (id INTEGER PRIMARY KEY, value TEXT); INSERT INTO "Configs" VALUES (1, \'private-config-value\');'
: ''
}
`);
legacy.close();
fs.chmodSync(legacySourcePath, 0o600);
fs.copyFileSync(legacySourcePath, recoveryPath);
fs.chmodSync(recoveryPath, 0o600);
const migration = spawnSync(
process.execPath,
[
'-e',
`require('@qinglong/local-sqlite/migration')
.migrateLocalSqlitePath({ databasePath: process.argv[1], profile: 'edge' })
.catch((error) => { console.error(error); process.exitCode = 1; });`,
targetDatabasePath,
],
{ encoding: 'utf8', cwd: path.join(__dirname, '..') },
);
assert.equal(migration.status, 0, migration.stderr);
fs.chmodSync(targetDatabasePath, 0o600);
};
}
function mutateAutomationTarget({ targetDatabasePath }, occupied = false) {
const target = new DatabaseSync(targetDatabasePath);
if (occupied) {
@@ -1167,6 +1221,69 @@ function applicationCommitCommand(state, prepared) {
};
}
async function secretConfigPlanFixture(t, options = {}) {
const suffix = options.suffix ?? 'plan';
const state = await reviewedApplicationFixture(t, {
planId:
options.planId ?? '00000000-0000-4000-8000-000000000421',
reviewId:
options.reviewId ?? '00000000-0000-4000-8000-000000000422',
applicationId:
options.applicationId ?? '00000000-0000-4000-8000-000000000423',
reviewSuffix: `secret-config-${suffix}`,
createDefaultSidecars: false,
initializeDatabases: secretConfigDatabaseInitializer({
active: options.active === true,
configs: options.configs === true,
}),
mutateTarget({ targetDatabasePath }) {
const target = new DatabaseSync(targetDatabasePath);
target.exec('PRAGMA user_version=1');
target.close();
return Object.freeze({});
},
});
const preparedApplication = await prepareLocalReconciliationApplication(
state.prepareApplicationCommand,
);
const application = await commitLocalReconciliationApplication(
applicationCommitCommand(state, preparedApplication),
);
const secretConfigRoot = path.join(
path.dirname(state.captureRoot),
`secret-config-plan-${suffix}`,
);
fs.mkdirSync(secretConfigRoot, { mode: 0o700 });
const secretConfigId =
options.secretConfigId ?? '00000000-0000-4000-8000-000000000424';
const command = {
schemaVersion: 1,
operation: 'local.deployment.reconciliation.secret-config.plan',
options: {
deploymentRoot: state.deploymentRoot,
applicationRoot: state.applicationRoot,
secretConfigRoot,
allowRootService: rootAcknowledgement(),
},
request: {
secretConfigId,
applicationId: application.applicationId,
expectedApplicationPlanDigest: application.applicationPlanDigest,
expectedHeadDigest: application.instanceHeadDigest,
decisionFilePath: state.reviewFile.filePath,
projectId: 'default',
preparedAtMs: state.prepareApplicationCommand.request.preparedAtMs + 2,
},
};
return {
...state,
application,
secretConfigRoot,
secretConfigId,
secretConfigCommand: command,
};
}
async function plannedAutomationFixture(t, options = {}) {
const suffix = options.suffix ?? 'decision';
const state = await reviewedApplicationFixture(t, {
@@ -3197,6 +3314,186 @@ test('completion fence seals all eight no-effect domains before target restart',
assert.equal(activeHead.generation, 2);
});
test('Secret/Config plan publishes, seals, verifies and stays content-free', async (t) => {
const state = await secretConfigPlanFixture(t, { suffix: 'terminal' });
const opened = [];
const closed = [];
const planned = await planLocalReconciliationSecretConfig(
state.secretConfigCommand,
{
beforeDatabaseOpen(kind, mode, cacheKiB) {
opened.push({ kind, mode, cacheKiB });
},
afterDatabaseClose(kind) {
closed.push(kind);
},
},
);
assert.equal(planned.status, 'prepared');
assert.equal(planned.state, 'reconciliation_secret_config_planned');
assert.equal(planned.outcome, 'ready');
assert.equal(planned.rowCount, 1);
assert.equal(planned.eligibleBindingCount, 0);
assert.equal(planned.eligiblePreservationCount, 1);
assert.equal(planned.adoptedLegacyTaskCount, 0);
assert.deepEqual(opened, [
{ kind: 'legacy', mode: 'main_only_immutable', cacheKiB: 2048 },
{ kind: 'target', mode: 'main_only_immutable', cacheKiB: 2048 },
{ kind: 'target', mode: 'main_only_immutable', cacheKiB: 2048 },
{ kind: 'legacy', mode: 'main_only_immutable', cacheKiB: 2048 },
]);
assert.deepEqual(closed, ['legacy', 'target', 'legacy', 'target']);
const root = path.join(state.secretConfigRoot, state.secretConfigId);
assert.deepEqual(fs.readdirSync(root).sort(), [
'plan.ndjson',
'receipt.json',
'staging',
]);
assert.equal(fs.statSync(root).mode & 0o777, 0o500);
assert.equal(fs.statSync(path.join(root, 'staging')).mode & 0o777, 0o500);
assert.equal(fs.statSync(path.join(root, 'plan.ndjson')).mode & 0o777, 0o400);
assert.equal(fs.statSync(path.join(root, 'receipt.json')).mode & 0o777, 0o400);
const serialized = fs.readFileSync(path.join(root, 'plan.ndjson'), 'utf8');
for (const privateValue of [
'DISABLED_TOKEN',
'private-secret-value',
state.targetDatabasePath,
state.reviewFile.filePath,
]) {
assert.equal(serialized.includes(privateValue), false);
}
const receipt = JSON.parse(
fs.readFileSync(path.join(root, 'receipt.json'), 'utf8'),
);
assert.equal(receipt.unadaptedLegacyConfigCount, 0);
assert.match(receipt.automationAdoptionSetDigest, /^[0-9a-f]{64}$/);
const head = readLocalCutoverInstanceHead(
state.deploymentRoot,
state.captureCommand.request.instanceId,
state.uid,
);
assert.equal(head.state, 'reconciliation_secret_config_planned');
assert.equal(head.sourceRecordDigest, planned.secretConfigPlanDigest);
const verifyCommand = {
schemaVersion: 1,
operation: 'local.deployment.reconciliation.secret-config.verify',
options: state.secretConfigCommand.options,
request: {
secretConfigId: state.secretConfigId,
expectedSecretConfigPlanDigest: planned.secretConfigPlanDigest,
},
};
const verified = await verifyLocalReconciliationSecretConfigPlan(
verifyCommand,
);
assert.equal(verified.status, 'verified');
assert.equal(
(await planLocalReconciliationSecretConfig(state.secretConfigCommand))
.status,
'existing',
);
const commandPath = path.join(
state.deploymentRoot,
'secret-config-verify-command.json',
);
fs.writeFileSync(commandPath, `${JSON.stringify(verifyCommand)}\n`, {
mode: 0o600,
});
const cli = spawnSync(
process.execPath,
[
path.join(__dirname, '../dist/deployment/localDeploymentCli.js'),
'reconciliation-secret-config-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.secretConfigRoot), false);
assert.equal(cli.stdout.includes('DISABLED_TOKEN'), false);
assert.equal(cli.stderr, '');
});
test('Secret/Config plan recovers exact publication response loss windows', async (t) => {
for (const [hook, finalStatus] of [
['afterPlanPublished', 'prepared'],
['afterReceiptPublished', 'prepared'],
['afterTerminalSealed', 'prepared'],
['afterHeadAdvanced', 'existing'],
]) {
await t.test(hook, async (subtest) => {
const state = await secretConfigPlanFixture(subtest, { suffix: hook });
let fault = true;
await assert.rejects(
planLocalReconciliationSecretConfig(state.secretConfigCommand, {
[hook]() {
if (fault) {
fault = false;
throw new Error(`secret-config-${hook}-fault`);
}
},
}),
new RegExp(`secret-config-${hook}-fault`),
);
const recovered = await planLocalReconciliationSecretConfig(
state.secretConfigCommand,
);
assert.equal(recovered.status, finalStatus);
assert.equal(recovered.outcome, 'ready');
const verified = await verifyLocalReconciliationSecretConfigPlan({
schemaVersion: 1,
operation: 'local.deployment.reconciliation.secret-config.verify',
options: state.secretConfigCommand.options,
request: {
secretConfigId: state.secretConfigId,
expectedSecretConfigPlanDigest: recovered.secretConfigPlanDigest,
},
});
assert.equal(verified.status, 'verified');
});
}
});
test('Secret/Config plan keeps active Env and unknown Configs manual', async (t) => {
await t.test('active without adopted task', async (subtest) => {
const state = await secretConfigPlanFixture(subtest, {
suffix: 'active-manual',
active: true,
});
const planned = await planLocalReconciliationSecretConfig(
state.secretConfigCommand,
);
assert.equal(planned.outcome, 'manual_required');
assert.equal(planned.eligibleBindingCount, 1);
assert.equal(planned.adoptedLegacyTaskCount, 0);
});
await t.test('historical Configs', async (subtest) => {
const state = await secretConfigPlanFixture(subtest, {
suffix: 'configs-manual',
configs: true,
});
const planned = await planLocalReconciliationSecretConfig(
state.secretConfigCommand,
);
assert.equal(planned.outcome, 'manual_required');
assert.equal(planned.unadaptedLegacyConfigCount, 1);
const serialized = fs.readFileSync(
path.join(
state.secretConfigRoot,
state.secretConfigId,
'plan.ndjson',
),
'utf8',
);
assert.equal(serialized.includes('private-config-value'), false);
});
});
test('completion fence retains automation rollback backup while other domains remain manual', async (t) => {
const state = await appliedAutomationFixture(t, {
suffix: 'completion-fence',
@@ -27,7 +27,8 @@ const HEADER = Object.freeze({
bundleFingerprintDigest: '1'.repeat(64),
profile: 'edge',
projectId: 'project-1',
tableDisposition: 'adopt_legacy',
tableDisposition: 'manual_external',
unadaptedLegacyConfigCount: 0,
preparedHeadDigest: '2'.repeat(64),
preparedAtMs: 1_780_000_000_000,
});
@@ -60,11 +61,60 @@ function databases() {
created_at_ms INTEGER NOT NULL,
PRIMARY KEY (project_id, secret_name, version)
);
CREATE TABLE "QingLong3LegacyAdoptions" (
mutation_id TEXT PRIMARY KEY,
decision_id TEXT NOT NULL,
project_id TEXT NOT NULL,
plan_digest TEXT NOT NULL,
inventory_digest TEXT NOT NULL,
decision_digest TEXT NOT NULL,
receipt_digest TEXT NOT NULL,
authorization_file_digest TEXT NOT NULL,
publication_digest TEXT NOT NULL,
row_count INTEGER NOT NULL,
adopted_task_count INTEGER NOT NULL,
adopted_trigger_count INTEGER NOT NULL,
skipped_count INTEGER NOT NULL,
audit_event_id TEXT NOT NULL,
created_at_ms INTEGER NOT NULL
);
`);
return { legacy, target };
}
function writePlan(t, legacy, target, maxBytes = 8 * 1024 * 1024) {
function insertAutomationAdoption(target, adoptedTaskCount = 1) {
const mutationId = '30000000-0000-4000-8000-000000000003';
target
.prepare(
`INSERT INTO "QingLong3LegacyAdoptions" VALUES
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
)
.run(
mutationId,
'019b0000-0000-7000-8000-000000000001',
HEADER.projectId,
'3'.repeat(64),
'4'.repeat(64),
'5'.repeat(64),
'6'.repeat(64),
'7'.repeat(64),
'8'.repeat(64),
adoptedTaskCount,
adoptedTaskCount,
0,
0,
mutationId,
HEADER.preparedAtMs,
);
}
function writePlan(
t,
legacy,
target,
maxBytes = 8 * 1024 * 1024,
header = HEADER,
) {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-secret-plan-'));
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const filePath = path.join(directory, 'plan.ndjson');
@@ -74,7 +124,7 @@ function writePlan(t, legacy, target, maxBytes = 8 * 1024 * 1024) {
result = writeLocalReconciliationSecretConfigPlan({
descriptor,
maxBytes,
header: HEADER,
header,
legacy,
target,
});
@@ -107,6 +157,7 @@ test('writes a content-free Env plan with separate active and disabled candidate
(2, 'TOKEN', 'pinned-secret', 0, 1, 1, '2026-01-02'),
(3, 'DISABLED_TOKEN', 'disabled-secret', 1, 0, 0, '2026-01-03');
`);
insertAutomationAdoption(target);
const { result, records, serialized } = writePlan(t, legacy, target);
assert.equal(result.footer.outcome, 'ready');
@@ -114,6 +165,9 @@ test('writes a content-free Env plan with separate active and disabled candidate
assert.equal(result.footer.eligibleBindingCount, 1);
assert.equal(result.footer.eligiblePreservationCount, 1);
assert.equal(result.footer.targetConflictCount, 0);
assert.equal(result.footer.automationAdoptionRecordCount, 1);
assert.equal(result.footer.adoptedLegacyTaskCount, 1);
assert.match(result.footer.automationAdoptionSetDigest, /^[0-9a-f]{64}$/);
const candidates = records.filter((record) =>
record.kind.endsWith('-candidate'),
);
@@ -234,6 +288,28 @@ test('makes absent Envs no-effect and malformed Env manual', (t) => {
assert.equal(manual.serialized.includes('private-value'), false);
});
test('keeps active Env and historical Configs manual without adoption authority', (t) => {
const { legacy, target } = databases();
t.after(() => legacy.close());
t.after(() => target.close());
legacy.exec(
`INSERT INTO "Envs" VALUES
(1, 'TOKEN', 'private-value', 0, 1, 0, '2026-01-01')`,
);
const withoutAdoption = writePlan(t, legacy, target);
assert.equal(withoutAdoption.result.footer.outcome, 'manual_required');
assert.equal(withoutAdoption.result.footer.adoptedLegacyTaskCount, 0);
assert.equal(withoutAdoption.serialized.includes('private-value'), false);
insertAutomationAdoption(target);
const withConfigs = writePlan(t, legacy, target, 8 * 1024 * 1024, {
...HEADER,
unadaptedLegacyConfigCount: 1,
});
assert.equal(withConfigs.result.footer.outcome, 'manual_required');
assert.equal(withConfigs.result.footer.unadaptedLegacyConfigCount, 1);
});
test('fails closed before exceeding the plan byte budget', (t) => {
const { legacy, target } = databases();
t.after(() => legacy.close());
+2 -2
View File
@@ -207,10 +207,10 @@ test('current QL3 workspace has exactly eighteen reviewed package boundaries', (
rootSourceFileRoles: localOwnerCli.rootSourceFileRoles,
},
{
sourceFiles: 176,
sourceFiles: 178,
rootSourceFiles: 1,
rootSourceLines: 50,
nestedSourceFiles: 175,
nestedSourceFiles: 177,
rootSourceFileRoles: { 'cli.ts': 'binary_entry' },
},
);