mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
fix(ql3): preserve target-native reconciliation domains
This commit is contained in:
@@ -58,10 +58,19 @@
|
|||||||
`Secret/Config backup.sha256 == Automation targetAfter.sha256`,再证明 current target 等于 Secret/Config `targetAfter`。lineage 只新增
|
`Secret/Config backup.sha256 == Automation targetAfter.sha256`,再证明 current target 等于 Secret/Config `targetAfter`。lineage 只新增
|
||||||
`reconciliation_secret_config_applied → reconciliation_completed` 一条合法边;receipt seal 与 completed head durable 后,才幂等删除两类数据库等量
|
`reconciliation_secret_config_applied → reconciliation_completed` 一条合法边;receipt seal 与 completed head durable 后,才幂等删除两类数据库等量
|
||||||
backup。Secret/Config 的 ciphertext material、intent 与 receipt 继续 `0400` 保留,backup/rollback root 封为 `0500`,不增加后台 GC、timer 或常驻内存。
|
backup。Secret/Config 的 ciphertext material、intent 与 receipt 继续 `0400` 保留,backup/rollback root 封为 `0500`,不增加后台 GC、timer 或常驻内存。
|
||||||
rolled-back、target drift、提前丢失 backup 与其余 manual 域继续失败关闭;完整迁移库当前仍可能因 Identity/Unknown 保持 manual,这不是 v3 可绕过的限制。
|
rolled-back、target drift、提前丢失 backup 与其余 manual 域继续失败关闭。
|
||||||
|
第九切片以 D-398/ADR-0493 修正 planner 对目标原生数据的两个架构误判,而没有新增 adapter 或放宽未知 schema。fresh v52 中
|
||||||
|
`QingLong3LegacyAdoptions`、`QingLong3LegacyAdoptionTasks`、`QingLong3LocalExecutionContextRecipes`、`QingLong3LocalTaskExecutionRevisions`
|
||||||
|
精确归入 Automation,`QingLong3RunAttemptLogArtifactTombstones`、`QingLong3RunAttemptLogRetentionState` 精确归入 Run History;不使用
|
||||||
|
`QingLong3*` 宽泛通配,未来未登记表仍是 row-free、blocked 的 `unknown`。`identity_policy_audit` 按来源分离:没有 Legacy `Auths/Users`
|
||||||
|
时,Target 原生 Project/RoleBinding/Identity/Credential/Pepper/Approval/SecurityAudit/LocalOwner 事实为 `target_only`,必须逐事实 signed
|
||||||
|
`retain_target`,随后才以 `no_effect/application_summary` 进入 completion;只要存在 Legacy 身份事实,仍为
|
||||||
|
`manual_required/identity_custody_required`,本切片不声称旧 credential、session、token、Policy 或 Audit 已迁移。Secret/Config v52 fixture
|
||||||
|
现已用真实 `complete → replay → verify` 完成 v3 闭环,不再由测试伪造 receipt、直接推进 head 或手工回收 storage;真正未知表和 Legacy 身份回归
|
||||||
|
继续失败关闭。
|
||||||
全部 evidence 不含原 Env name/value、目标 ciphertext/key ID 或 row body。v52 Local SQLite 完整测试为 `247/247`,publisher 定向回归 `6/6`;fresh Edge
|
全部 evidence 不含原 Env name/value、目标 ciphertext/key ID 或 row body。v52 Local SQLite 完整测试为 `247/247`,publisher 定向回归 `6/6`;fresh Edge
|
||||||
readiness 为 contract v52、104 migrations、89 required tables、SQLite 3.53.3、`DELETE` journal。Local Admin 为 `96/96`,Local Owner 为
|
readiness 为 contract v52、104 migrations、89 required tables、SQLite 3.53.3、`DELETE` journal。Local Admin 为 `96/96`,ADR-0493 后 Local Owner 有效结果为
|
||||||
`300 total / 293 pass / 7 conditional skip / 0 fail`;18-package clean build 与逐包顺序测试单次退出 0,完整 backend 为
|
`301 total / 294 pass / 7 conditional skip / 0 fail`;完整 backend 为
|
||||||
`1567 total / 1565 pass / 2 conditional skip / 0 fail`。package boundary、精确 Cluster dependency/legacy boundary、122-module Edge import、本地镜像与
|
`1567 total / 1565 pass / 2 conditional skip / 0 fail`。package boundary、精确 Cluster dependency/legacy boundary、122-module Edge import、本地镜像与
|
||||||
`14/14` Local artifact audit 全部 compatible;基础 Edge/Standalone 为 `2,635,529 / 2,635,607 bytes`、323 files、58 loaded modules,距 4 MiB
|
`14/14` Local artifact audit 全部 compatible;基础 Edge/Standalone 为 `2,635,529 / 2,635,607 bytes`、323 files、58 loaded modules,距 4 MiB
|
||||||
上限仍分别保留 `1,558,775 / 1,558,697 bytes`,且闭包只有 Local SQLite、runtime-core 与 SemVer,没有 Cluster/PostgreSQL 依赖。
|
上限仍分别保留 `1,558,775 / 1,558,697 bytes`,且闭包只有 Local SQLite、runtime-core 与 SemVer,没有 Cluster/PostgreSQL 依赖。
|
||||||
|
|||||||
@@ -143,6 +143,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 冲突、Automation adoption projection、no-effect/manual outcome、durable no-replace publication、terminal seal、head CAS、逐候选独立 signed decision、同一强认证 reviewer、decision/authorization byte bound、`skip → manual_required`、prepare/commit response-loss exact replay、只读 terminal verify、v51 逐 Task/Trigger adoption provenance、v52 Local SQLite 原子 application publisher、Owner prepared/apply/rollback orchestration,以及 ADR-0492 completion v3。v52 在一个 `BEGIN IMMEDIATE` 内复验 Project/RoleBinding、外部 authority、逐 Task/Trigger provenance、当前 head、Plugin ownership 与 Trigger 数量,流式写入加密 Secret、content-free audit、Task rev2、dispatch、Trigger rev2、schedule 和四类 application ledger;deferred parent FK 允许最多 100,000 Task/500,000 Trigger 逐项发布而不在 JS 堆保留全集。Owner 在写前固定 backup 与 stopped proof,以有界 ciphertext-only material 连接 reviewed decision 和 publisher,并覆盖 apply/rollback 全部 response-loss 窗口。completion v3 保留 v1/v2 exact shape,验证 signed decision/apply/current target/head,只有 completed head durable 后才幂等回收 Secret/Config backup;rolled-back、target drift 与其余 manual 域继续失败关闭。commit response-loss 通过 durable receipt exact replay,并重新验证 Secret envelope、Task/Trigger head 与 schedule;目标占用、provenance 缺项、提交前 authority 漂移均回滚全部 DML,rollback 则恢复写前 snapshot。
|
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、逐候选独立 signed decision、同一强认证 reviewer、decision/authorization byte bound、`skip → manual_required`、prepare/commit response-loss exact replay、只读 terminal verify、v51 逐 Task/Trigger adoption provenance、v52 Local SQLite 原子 application publisher、Owner prepared/apply/rollback orchestration,以及 ADR-0492 completion v3。v52 在一个 `BEGIN IMMEDIATE` 内复验 Project/RoleBinding、外部 authority、逐 Task/Trigger provenance、当前 head、Plugin ownership 与 Trigger 数量,流式写入加密 Secret、content-free audit、Task rev2、dispatch、Trigger rev2、schedule 和四类 application ledger;deferred parent FK 允许最多 100,000 Task/500,000 Trigger 逐项发布而不在 JS 堆保留全集。Owner 在写前固定 backup 与 stopped proof,以有界 ciphertext-only material 连接 reviewed decision 和 publisher,并覆盖 apply/rollback 全部 response-loss 窗口。completion v3 保留 v1/v2 exact shape,验证 signed decision/apply/current target/head,只有 completed head durable 后才幂等回收 Secret/Config backup;rolled-back、target drift 与其余 manual 域继续失败关闭。commit response-loss 通过 durable receipt exact replay,并重新验证 Secret envelope、Task/Trigger head 与 schedule;目标占用、provenance 缺项、提交前 authority 漂移均回滚全部 DML,rollback 则恢复写前 snapshot。
|
||||||
|
|
||||||
本切片当前验证:Local SQLite `247/247`,其中 Secret/Config application publisher 定向回归 `6/6`;fresh Edge readiness 为 contract v52、104 migrations、89 required tables、SQLite 3.53.3、`DELETE` journal。Local Admin 为 `96/96`,Local Owner 为 `300 total / 293 pass / 7 conditional skip / 0 fail`;18-package clean build 与逐包顺序测试单次退出 0,完整 backend 为 `1567 total / 1565 pass / 2 conditional skip / 0 fail`。package boundary、精确 Cluster dependency/legacy boundary、122-module Edge import、service-manager bridge import、本地镜像与 `14/14` Local artifact audit 全部 compatible;Local Admin 为 49 source / 48 nested / 1 root export,Local Owner 为 188/187/1,workspace 仍为 18 packages 且没有单文件或浅层 package。基础 Edge/Standalone 为 `2,635,529 / 2,635,607 bytes`、323 files、58 loaded modules,且没有 Cluster/PostgreSQL 闭包。本切片不改 PostgreSQL schema、连接、role、Pool、容器或 Kubernetes 拓扑,因此不重跑且不重新占有 PostgreSQL HA 证明。
|
本切片当前验证:Local SQLite `247/247`,其中 Secret/Config application publisher 定向回归 `6/6`;fresh Edge readiness 为 contract v52、104 migrations、89 required tables、SQLite 3.53.3、`DELETE` journal。Local Admin 为 `96/96`;ADR-0493 后 Local Owner 有效结果为 `301 total / 294 pass / 7 conditional skip / 0 fail`。完整 backend 为 `1567 total / 1565 pass / 2 conditional skip / 0 fail`。package boundary、精确 Cluster dependency/legacy boundary、122-module Edge import、service-manager bridge import、本地镜像与 `14/14` Local artifact audit 全部 compatible;Local Admin 为 49 source / 48 nested / 1 root export,Local Owner 为 188/187/1,workspace 仍为 18 packages 且没有单文件或浅层 package。基础 Edge/Standalone 为 `2,635,529 / 2,635,607 bytes`、323 files、58 loaded modules,且没有 Cluster/PostgreSQL 闭包。本切片不改 PostgreSQL schema、连接、role、Pool、容器或 Kubernetes 拓扑,因此不重跑且不重新占有 PostgreSQL HA 证明。
|
||||||
|
|
||||||
转为 Accepted 前仍必须完成:真实 Edge 空间预算证据与 Cluster Secret provider live gate;ADR-0492 已完成本机 completion schema 演进和 completed-head 后 rollback material 回收,但本切片的 Local Owner 编排或 PostgreSQL HA 证据不得冒充外部密钥托管,也不得把仍含 Identity/Unknown manual 域的迁移冒充跨领域 completion 已完成。
|
转为 Accepted 前仍必须完成:真实 Edge 空间预算证据与 Cluster Secret provider live gate;ADR-0492 已完成本机 completion schema 演进和 completed-head 后 rollback material 回收,ADR-0493 又让没有 Legacy 身份输入的 fresh v52 目标身份经 signed `retain_target` 正确形成 no-effect,并精确消除六张已知目标表的 `unknown` 误判。Legacy `Auths/Users` 或真正未知表仍保持 manual;本切片的 Local Owner 编排或 PostgreSQL HA 证据不得冒充外部密钥托管。
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ Cluster 不复用本机 receipt、POSIX storage 或 instance head。Cluster comp
|
|||||||
|
|
||||||
- completion receipt 的消费者必须按 `schemaVersion` 解析,未知版本继续失败关闭。
|
- completion receipt 的消费者必须按 `schemaVersion` 解析,未知版本继续失败关闭。
|
||||||
- `advanceLocalCutoverInstanceHead` 是 CRITICAL 共享状态机;本 ADR 只增加一条 source-state 边,不改任何旧 transition。
|
- `advanceLocalCutoverInstanceHead` 是 CRITICAL 共享状态机;本 ADR 只增加一条 source-state 边,不改任何旧 transition。
|
||||||
- 完整迁移库当前仍可能因 `identity_policy_audit` 或 `unknown` 保持 manual;这是正确的全局围栏,不应为了演示 v3 成功而放宽。
|
- 本 ADR 验收时完整迁移库仍会因目标原生 `identity_policy_audit` 和六张未登记 v52 表落入 `unknown` 而保持 manual;ADR-0493 已通过精确分类与 signed `retain_target` 修正这两个本地 planner 缺口。Legacy `Auths/Users` 和真正未知表仍保持全局围栏。
|
||||||
- v3 verify 要求 completed storage,不能在 head durable 之前充当修复命令。
|
- v3 verify 要求 completed storage,不能在 head durable 之前充当修复命令。
|
||||||
|
|
||||||
## 被拒绝的替代方案
|
## 被拒绝的替代方案
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# ADR-0493:目标原生 Reconciliation 域分类与身份保留
|
||||||
|
|
||||||
|
- 状态:Accepted
|
||||||
|
- 日期:2026-08-24
|
||||||
|
- 决策:D-398
|
||||||
|
- 关联:ADR-0483、ADR-0484、ADR-0485、ADR-0488、ADR-0492
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
Reconciliation 必须同时处理两类不同事实:从 2.x 迁入的数据,以及已经属于 QingLong 3.0 目标库的原生数据。此前实现把 `identity_policy_audit` 整域无条件标为 `manual_required/identity_custody_required`,因此即使 Legacy 完全没有 `Auths/Users`,fresh v52 目标库自带的 Project、RoleBinding、Credential、Pepper、SecurityAudit 与 LocalOwner 表也会阻止全局 completion。
|
||||||
|
|
||||||
|
同一盘点还发现 fresh v52 的六张已知表没有进入稳定领域,被误归为 `unknown`:
|
||||||
|
|
||||||
|
- Automation:`QingLong3LegacyAdoptions`、`QingLong3LegacyAdoptionTasks`、`QingLong3LocalExecutionContextRecipes`、`QingLong3LocalTaskExecutionRevisions`;
|
||||||
|
- Run History:`QingLong3RunAttemptLogArtifactTombstones`、`QingLong3RunAttemptLogRetentionState`。
|
||||||
|
|
||||||
|
这不是缺少新 adapter,而是现有 planner 对目标原生 schema 的知识不完整。为绕过门禁而伪造 completion receipt 或把整个 `QingLong3*` 前缀视为可信都会破坏 fail-closed 边界。
|
||||||
|
|
||||||
|
## 决策
|
||||||
|
|
||||||
|
### 1. 已知目标表使用精确领域目录
|
||||||
|
|
||||||
|
上述六张表按精确表名分别归入 Automation 与 Run History。Schema object 继续按其 `tableName` 继承同一领域,因此相关 index/trigger 不需要单独的宽泛名称规则。
|
||||||
|
|
||||||
|
不引入 `QingLong3*`、`LegacyAdoption*` 或 `Local*` 的整体通配。未来新增但尚未登记的目标表仍进入 `unknown`,不读取行数,并要求人工外部处理。
|
||||||
|
|
||||||
|
### 2. 目标原生身份是保留事实,不是迁移事实
|
||||||
|
|
||||||
|
`identity_policy_audit` 的 disposition 改为按来源判定:
|
||||||
|
|
||||||
|
- Legacy 存在 `Auths/Users` 时继续 `manual_required`;
|
||||||
|
- Legacy 没有身份事实、Target 有已知身份事实时为 `target_only`;
|
||||||
|
- 两端都没有时为 `aligned`。
|
||||||
|
|
||||||
|
Target 身份诊断是 `required/reviewable_fact`,必须在 signed review 中逐事实选择 `retain_target` 才能形成 `no_effect` application action。它不能选择 `adopt_legacy` 或 `exclude_legacy`。Legacy 身份诊断仍是 `blocked/identity_custody_required`,只允许 `defer|manual_external`,本 ADR 不声明 2.x credential、session、password hash、token、Policy 或 Audit 已迁移。
|
||||||
|
|
||||||
|
### 3. Completion 只消费既有闭合证据
|
||||||
|
|
||||||
|
本 ADR 不增加 completion schema、adapter、instance state 或绕过规则。目标原生身份经过 canonical diagnostics、signed decision 和 application summary 后,以既有 `no_effect/application_summary` 进入 completion;真正未知表和任何 Legacy 身份事实仍阻止 completion。
|
||||||
|
|
||||||
|
完整 v52 + Legacy Secret/Config fixture 必须通过真实 v3 completion,而不是测试直接构造 receipt、推进 head 或调用 storage collector。只有 completed head durable 后,Secret/Config backup 才能由既有流程回收。
|
||||||
|
|
||||||
|
### 4. 部署与资源边界不变
|
||||||
|
|
||||||
|
实现只修改既有 Local Owner 一次性 reconciliation planner/reviewer,没有新增 workspace package、production dependency、SQL migration、daemon、listener、timer、Pool、容器或 Kubernetes workload。Edge/Standalone 常驻闭包、SQLite cache 上限和 artifact 预算不变;Cluster 仍使用自己的 PostgreSQL、Secret provider 与 HA authority。
|
||||||
|
|
||||||
|
## 被拒绝的替代方案
|
||||||
|
|
||||||
|
### 为目标原生身份增加 migration adapter
|
||||||
|
|
||||||
|
拒绝。没有 Legacy 身份输入时不存在要迁移的数据;额外 adapter 只会复制已经由目标数据库拥有的事实并扩大权限面。
|
||||||
|
|
||||||
|
### 信任全部 QingLong3 前缀
|
||||||
|
|
||||||
|
拒绝。名称前缀不是 schema ownership 证明,会让未来未知表自动越过人工审查。
|
||||||
|
|
||||||
|
### 自动跳过目标身份评审
|
||||||
|
|
||||||
|
拒绝。`target_only` 仍必须形成逐事实 signed `retain_target` 决策,不能仅凭分类器直接授权 completion。
|
||||||
|
|
||||||
|
### 放行 Legacy Auths/Users
|
||||||
|
|
||||||
|
拒绝。旧身份材料的 custody、hash/token 兼容、撤销、Policy 与 Audit 语义尚未建立独立 adapter;本 ADR 只消除目标原生数据被误当成迁移输入的问题。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
- 定向回归 `4/4`:fresh v52 `unknown=0`、目标身份 `retain_target`、Legacy 身份继续 blocked、真正未知表继续 row-free/manual;
|
||||||
|
- Secret/Config completion v3 使用真实 `complete → replay → verify` 链路,首调 `completed`、重放 `existing`,receipt 为 v3 且 Identity/Unknown 均为 `no_effect`;
|
||||||
|
- completed head durable 后 Secret/Config backup 被回收,加密 material 与 receipt 保持只读;
|
||||||
|
- Local Owner 受限沙箱为 `301 total / 291 pass / 7 conditional skip / 3 loopback EPERM`,两个 loopback 文件在沙箱外 `15/15`,有效结果 `301/294/7/0`;
|
||||||
|
- 完整 backend 为 `1567 total / 1565 pass / 2 conditional skip / 0 fail`;18-package clean build 与逐包顺序测试除 Worker 三条 sandbox loopback 外全部通过,对应 Worker 文件在沙箱外 `8/8`;
|
||||||
|
- package boundary、精确 Cluster dependency、122-module Edge import、service-manager bridge import、本地镜像与 `14/14` Local artifact audit 全部 compatible;workspace 保持 18 packages,`singleSourcePackages=[]`、`shallowSourcePackages=[]`;
|
||||||
|
- 基础 Edge/Standalone 保持 `2,635,529 / 2,635,607 bytes`、323 files、58 loaded modules,没有 Cluster/PostgreSQL 闭包。
|
||||||
@@ -496,6 +496,7 @@
|
|||||||
| [ADR-0490](./ADR-0490-run-history-terminal-preservation.md) | Run History 终态保全与跨领域完成证明 | 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 与任务环境绑定 | Proposed(D-397 local apply/rollback/completion complete;Edge/Cluster gate pending) |
|
| [ADR-0491](./ADR-0491-bounded-secret-config-reconciliation-and-task-binding.md) | 有界 Secret/Config Reconciliation 与任务环境绑定 | Proposed(D-397 local apply/rollback/completion complete;Edge/Cluster gate pending) |
|
||||||
| [ADR-0492](./ADR-0492-compatible-secret-config-completion-v3.md) | 兼容 Secret/Config 的 Reconciliation Completion v3 | Accepted |
|
| [ADR-0492](./ADR-0492-compatible-secret-config-completion-v3.md) | 兼容 Secret/Config 的 Reconciliation Completion v3 | Accepted |
|
||||||
|
| [ADR-0493](./ADR-0493-target-native-reconciliation-domain-preservation.md) | 目标原生 Reconciliation 域分类与身份保留 | Accepted |
|
||||||
|
|
||||||
## 规则
|
## 规则
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,16 @@ import {
|
|||||||
|
|
||||||
const MAX_SCHEMA_OBJECTS = 4_096;
|
const MAX_SCHEMA_OBJECTS = 4_096;
|
||||||
const MAX_TABLES = 512;
|
const MAX_TABLES = 512;
|
||||||
|
const TARGET_AUTOMATION_TABLES = new Set([
|
||||||
|
'QingLong3LegacyAdoptions',
|
||||||
|
'QingLong3LegacyAdoptionTasks',
|
||||||
|
'QingLong3LocalExecutionContextRecipes',
|
||||||
|
'QingLong3LocalTaskExecutionRevisions',
|
||||||
|
]);
|
||||||
|
const TARGET_RUN_HISTORY_TABLES = new Set([
|
||||||
|
'QingLong3RunAttemptLogArtifactTombstones',
|
||||||
|
'QingLong3RunAttemptLogRetentionState',
|
||||||
|
]);
|
||||||
|
|
||||||
export interface LocalReconciliationDomainInventory {
|
export interface LocalReconciliationDomainInventory {
|
||||||
readonly domain: LocalReconciliationPlanDomain;
|
readonly domain: LocalReconciliationPlanDomain;
|
||||||
@@ -85,6 +95,7 @@ function targetDomain(name: string): LocalReconciliationPlanDomain {
|
|||||||
return 'schema_lineage';
|
return 'schema_lineage';
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
|
TARGET_AUTOMATION_TABLES.has(name) ||
|
||||||
name.includes('TaskDefinition') ||
|
name.includes('TaskDefinition') ||
|
||||||
name.includes('Trigger') ||
|
name.includes('Trigger') ||
|
||||||
name.includes('Automation')
|
name.includes('Automation')
|
||||||
@@ -95,6 +106,7 @@ function targetDomain(name: string): LocalReconciliationPlanDomain {
|
|||||||
return 'secret_and_config';
|
return 'secret_and_config';
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
|
TARGET_RUN_HISTORY_TABLES.has(name) ||
|
||||||
name === 'Runs' ||
|
name === 'Runs' ||
|
||||||
name.startsWith('Run') ||
|
name.startsWith('Run') ||
|
||||||
name.startsWith('StepRun') ||
|
name.startsWith('StepRun') ||
|
||||||
|
|||||||
@@ -175,10 +175,11 @@ function domainDisposition(
|
|||||||
) {
|
) {
|
||||||
return 'unsupported';
|
return 'unsupported';
|
||||||
}
|
}
|
||||||
|
if (domain === 'identity_policy_audit' && legacyFacts > 0) {
|
||||||
|
return 'manual_required';
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
(domain === 'secret_and_config' ||
|
(domain === 'secret_and_config' || domain === 'run_history') &&
|
||||||
domain === 'run_history' ||
|
|
||||||
domain === 'identity_policy_audit') &&
|
|
||||||
legacyFacts + targetFacts > 0
|
legacyFacts + targetFacts > 0
|
||||||
) {
|
) {
|
||||||
return 'manual_required';
|
return 'manual_required';
|
||||||
|
|||||||
@@ -297,10 +297,14 @@ export function assertLocalReconciliationReviewDecisionMatchesFact(
|
|||||||
!['retain_target', 'retain_both', 'defer', 'manual_external'].includes(
|
!['retain_target', 'retain_both', 'defer', 'manual_external'].includes(
|
||||||
selected.disposition,
|
selected.disposition,
|
||||||
)) ||
|
)) ||
|
||||||
((fact.domain === 'secret_and_config' ||
|
((fact.domain === 'secret_and_config' || fact.domain === 'unknown') &&
|
||||||
fact.domain === 'identity_policy_audit' ||
|
!['defer', 'manual_external'].includes(selected.disposition)) ||
|
||||||
fact.domain === 'unknown') &&
|
(fact.domain === 'identity_policy_audit' &&
|
||||||
!['defer', 'manual_external'].includes(selected.disposition))
|
!(
|
||||||
|
fact.database === 'target'
|
||||||
|
? ['retain_target', 'defer', 'manual_external']
|
||||||
|
: ['defer', 'manual_external']
|
||||||
|
).includes(selected.disposition))
|
||||||
) {
|
) {
|
||||||
configurationError(
|
configurationError(
|
||||||
'decision disposition is not allowed for canonical fact',
|
'decision disposition is not allowed for canonical fact',
|
||||||
|
|||||||
@@ -195,6 +195,12 @@ function requirement(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (domain === 'identity_policy_audit') {
|
if (domain === 'identity_policy_audit') {
|
||||||
|
if (database === 'target') {
|
||||||
|
return Object.freeze({
|
||||||
|
decisionRequirement: 'required' as const,
|
||||||
|
reason: 'reviewable_fact' as const,
|
||||||
|
});
|
||||||
|
}
|
||||||
return Object.freeze({
|
return Object.freeze({
|
||||||
decisionRequirement: 'blocked' as const,
|
decisionRequirement: 'blocked' as const,
|
||||||
reason: 'identity_custody_required' as const,
|
reason: 'identity_custody_required' as const,
|
||||||
|
|||||||
@@ -74,17 +74,6 @@ const {
|
|||||||
const {
|
const {
|
||||||
targetStoppedEvidence,
|
targetStoppedEvidence,
|
||||||
} = require('../dist/deployment/cutover/targetStopRecordEvidence.js');
|
} = require('../dist/deployment/cutover/targetStopRecordEvidence.js');
|
||||||
const {
|
|
||||||
collectLocalReconciliationSecretConfigCompletedStorage,
|
|
||||||
localReconciliationSecretConfigApplyPaths,
|
|
||||||
readLocalReconciliationSecretConfigApplyIntent,
|
|
||||||
validateLocalReconciliationSecretConfigCompletedStorage,
|
|
||||||
} = require('../dist/deployment/reconciliation/application/secret-and-config/application/storage.js');
|
|
||||||
const {
|
|
||||||
buildLocalReconciliationCompletionReceipt,
|
|
||||||
normalizeLocalReconciliationCompletionReceipt,
|
|
||||||
} = require('../dist/deployment/reconciliation/completion/evidence.js');
|
|
||||||
|
|
||||||
function digest(value) {
|
function digest(value) {
|
||||||
return crypto
|
return crypto
|
||||||
.createHash('sha256')
|
.createHash('sha256')
|
||||||
@@ -2974,6 +2963,55 @@ test('review diagnostics keep secret and unknown facts blocked and row-free', (t
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('review diagnostics preserve target-native identity but keep legacy identity fail-closed', (t) => {
|
||||||
|
const initializeDatabases = (paths) => {
|
||||||
|
planningDatabaseInitializer()(paths);
|
||||||
|
const legacy = new DatabaseSync(paths.legacySourcePath);
|
||||||
|
legacy.exec('CREATE TABLE "Auths" (id INTEGER PRIMARY KEY)');
|
||||||
|
legacy.close();
|
||||||
|
fs.copyFileSync(paths.legacySourcePath, paths.recoveryPath);
|
||||||
|
fs.chmodSync(paths.recoveryPath, 0o600);
|
||||||
|
const target = new DatabaseSync(paths.targetDatabasePath);
|
||||||
|
target.exec(
|
||||||
|
'CREATE TABLE "QingLong3IdentityRecords" (id INTEGER PRIMARY KEY)',
|
||||||
|
);
|
||||||
|
target.close();
|
||||||
|
};
|
||||||
|
const state = preparedReview(t, {
|
||||||
|
initializeDatabases,
|
||||||
|
planId: '00000000-0000-4000-8000-000000000333',
|
||||||
|
reviewId: '00000000-0000-4000-8000-000000000334',
|
||||||
|
reviewSuffix: 'identity-custody',
|
||||||
|
});
|
||||||
|
assert.equal(state.planned.outcome, 'manual_required');
|
||||||
|
const prepared = prepareLocalReconciliationReview(state.reviewCommand);
|
||||||
|
const legacyCommand = diagnosticCommand(state, prepared, {
|
||||||
|
database: 'legacy',
|
||||||
|
domain: 'identity_policy_audit',
|
||||||
|
outputName: 'legacy-identity.json',
|
||||||
|
});
|
||||||
|
writeLocalReconciliationReviewDiagnostics(legacyCommand);
|
||||||
|
const legacy = JSON.parse(
|
||||||
|
fs.readFileSync(legacyCommand.request.outputPath, 'utf8'),
|
||||||
|
);
|
||||||
|
assert.equal(legacy.records[0].name, 'Auths');
|
||||||
|
assert.equal(legacy.records[0].decisionRequirement, 'blocked');
|
||||||
|
assert.equal(legacy.records[0].reason, 'identity_custody_required');
|
||||||
|
|
||||||
|
const targetCommand = diagnosticCommand(state, prepared, {
|
||||||
|
database: 'target',
|
||||||
|
domain: 'identity_policy_audit',
|
||||||
|
outputName: 'target-identity.json',
|
||||||
|
});
|
||||||
|
writeLocalReconciliationReviewDiagnostics(targetCommand);
|
||||||
|
const target = JSON.parse(
|
||||||
|
fs.readFileSync(targetCommand.request.outputPath, 'utf8'),
|
||||||
|
);
|
||||||
|
assert.equal(target.records[0].name, 'QingLong3IdentityRecords');
|
||||||
|
assert.equal(target.records[0].decisionRequirement, 'required');
|
||||||
|
assert.equal(target.records[0].reason, 'reviewable_fact');
|
||||||
|
});
|
||||||
|
|
||||||
test('review diagnostics page at sixty-four and CLI output stays content-free', (t) => {
|
test('review diagnostics page at sixty-four and CLI output stays content-free', (t) => {
|
||||||
const initializeDatabases = (paths) => {
|
const initializeDatabases = (paths) => {
|
||||||
planningDatabaseInitializer()(paths);
|
planningDatabaseInitializer()(paths);
|
||||||
@@ -4352,7 +4390,7 @@ test('Secret/Config apply publishes encrypted material atomically and recovers e
|
|||||||
assert.equal(databaseCloses, authentications);
|
assert.equal(databaseCloses, authentications);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('completion v3 proves Secret/Config apply but preserves rollback authority while other domains remain manual', async (t) => {
|
test('completion v3 completes a classified v52 target and collects Secret/Config rollback authority', async (t) => {
|
||||||
const state = await appliedSecretConfigFixture(t, {
|
const state = await appliedSecretConfigFixture(t, {
|
||||||
suffix: 'completion-v3',
|
suffix: 'completion-v3',
|
||||||
});
|
});
|
||||||
@@ -4402,116 +4440,71 @@ test('completion v3 proves Secret/Config apply but preserves rollback authority
|
|||||||
const backupPath = path.join(backupRoot, 'before.sqlite');
|
const backupPath = path.join(backupRoot, 'before.sqlite');
|
||||||
const materialPath = path.join(applyRoot, 'materials.ndjson');
|
const materialPath = path.join(applyRoot, 'materials.ndjson');
|
||||||
assert.equal(fs.existsSync(backupPath), true);
|
assert.equal(fs.existsSync(backupPath), true);
|
||||||
|
const plan = JSON.parse(
|
||||||
await assert.rejects(
|
fs.readFileSync(
|
||||||
completeLocalReconciliation(command),
|
path.join(
|
||||||
/identity_policy_audit is not terminally reconciled/,
|
state.planRoot,
|
||||||
|
state.prepareCommand.request.planId,
|
||||||
|
'plan.json',
|
||||||
|
),
|
||||||
|
'utf8',
|
||||||
|
),
|
||||||
);
|
);
|
||||||
assert.equal(fs.existsSync(backupPath), true);
|
assert.deepEqual(
|
||||||
|
plan.domains.find((domain) => domain.domain === 'unknown'),
|
||||||
|
{
|
||||||
|
domain: 'unknown',
|
||||||
|
legacySchemaObjects: 0,
|
||||||
|
targetSchemaObjects: 0,
|
||||||
|
legacyTables: 0,
|
||||||
|
targetTables: 0,
|
||||||
|
legacyRows: 0,
|
||||||
|
targetRows: 0,
|
||||||
|
rowCountsComplete: true,
|
||||||
|
inventoryDigest: plan.domains.find(
|
||||||
|
(domain) => domain.domain === 'unknown',
|
||||||
|
).inventoryDigest,
|
||||||
|
disposition: 'aligned',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
plan.domains.find((domain) => domain.domain === 'identity_policy_audit')
|
||||||
|
.disposition,
|
||||||
|
'target_only',
|
||||||
|
);
|
||||||
|
const targetIdentityDecisions = state.reviewFile.decisions.filter(
|
||||||
|
(decision) =>
|
||||||
|
decision.database === 'target' &&
|
||||||
|
decision.domain === 'identity_policy_audit',
|
||||||
|
);
|
||||||
|
assert.ok(targetIdentityDecisions.length > 0);
|
||||||
|
assert.equal(
|
||||||
|
targetIdentityDecisions.every(
|
||||||
|
(decision) => decision.disposition === 'retain_target',
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
const completed = await completeLocalReconciliation(command);
|
||||||
|
assert.equal(completed.status, 'completed');
|
||||||
|
assert.equal(completed.state, 'reconciliation_completed');
|
||||||
|
assert.equal(completed.adapterCount, 1);
|
||||||
|
assert.equal((await completeLocalReconciliation(command)).status, 'existing');
|
||||||
assert.equal(
|
assert.equal(
|
||||||
readLocalCutoverInstanceHead(
|
readLocalCutoverInstanceHead(
|
||||||
state.deploymentRoot,
|
state.deploymentRoot,
|
||||||
state.captureCommand.request.instanceId,
|
state.captureCommand.request.instanceId,
|
||||||
state.uid,
|
state.uid,
|
||||||
).state,
|
).state,
|
||||||
'reconciliation_secret_config_applied',
|
|
||||||
);
|
|
||||||
assert.equal(
|
|
||||||
fs.existsSync(path.join(completionRoot, command.request.completionId)),
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
const domainNames = [
|
|
||||||
'schema_lineage',
|
|
||||||
'automation',
|
|
||||||
'secret_and_config',
|
|
||||||
'run_history',
|
|
||||||
'plugin_package',
|
|
||||||
'ai_and_tool',
|
|
||||||
'identity_policy_audit',
|
|
||||||
'unknown',
|
|
||||||
];
|
|
||||||
const domains = domainNames.map((domain) =>
|
|
||||||
domain === 'secret_and_config'
|
|
||||||
? {
|
|
||||||
domain,
|
|
||||||
action: 'adapter_required',
|
|
||||||
evidenceKind: 'secret_config_application',
|
|
||||||
evidenceDigest: state.applied.applyDigest,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
domain,
|
|
||||||
action: 'no_effect',
|
|
||||||
evidenceKind: 'application_summary',
|
|
||||||
evidenceDigest: 'a'.repeat(64),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const receipt = buildLocalReconciliationCompletionReceipt({
|
|
||||||
completionId: command.request.completionId,
|
|
||||||
applicationId: command.request.applicationId,
|
|
||||||
profile: state.captureCommand.request.profile,
|
|
||||||
instanceId: state.captureCommand.request.instanceId,
|
|
||||||
cutoverId: state.captureCommand.request.cutoverId,
|
|
||||||
generation: 1,
|
|
||||||
activationDigest: state.captureCommand.request.expectedActivationDigest,
|
|
||||||
applicationPlanDigest: state.application.applicationPlanDigest,
|
|
||||||
sourceHeadDigest: state.applied.instanceHeadDigest,
|
|
||||||
domains,
|
|
||||||
adapterCount: 1,
|
|
||||||
completedAtMs: command.request.completedAtMs,
|
|
||||||
});
|
|
||||||
assert.equal(receipt.schemaVersion, 3);
|
|
||||||
assert.deepEqual(
|
|
||||||
normalizeLocalReconciliationCompletionReceipt(receipt),
|
|
||||||
receipt,
|
|
||||||
);
|
|
||||||
|
|
||||||
const currentHead = readLocalCutoverInstanceHead(
|
|
||||||
state.deploymentRoot,
|
|
||||||
state.captureCommand.request.instanceId,
|
|
||||||
state.uid,
|
|
||||||
);
|
|
||||||
const durableHead = advanceLocalCutoverInstanceHead(
|
|
||||||
{
|
|
||||||
options: { deploymentRoot: state.deploymentRoot },
|
|
||||||
request: {
|
|
||||||
cutoverId: state.captureCommand.request.cutoverId,
|
|
||||||
profile: state.captureCommand.request.profile,
|
|
||||||
instanceId: state.captureCommand.request.instanceId,
|
|
||||||
expectedActivationDigest:
|
|
||||||
state.captureCommand.request.expectedActivationDigest,
|
|
||||||
requestedAtMs: command.request.completedAtMs,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
state.uid,
|
|
||||||
'reconciliation_completed',
|
'reconciliation_completed',
|
||||||
currentHead.generation,
|
|
||||||
receipt.completionDigest,
|
|
||||||
);
|
);
|
||||||
assert.equal(durableHead.state, 'reconciliation_completed');
|
const receipt = JSON.parse(
|
||||||
const selected = localReconciliationSecretConfigApplyPaths(
|
fs.readFileSync(
|
||||||
state.secretConfigApplyRoot,
|
path.join(completionRoot, command.request.completionId, 'receipt.json'),
|
||||||
state.secretConfigId,
|
'utf8',
|
||||||
);
|
),
|
||||||
const intent = readLocalReconciliationSecretConfigApplyIntent(
|
|
||||||
selected,
|
|
||||||
state.uid,
|
|
||||||
);
|
|
||||||
collectLocalReconciliationSecretConfigCompletedStorage(
|
|
||||||
selected,
|
|
||||||
intent,
|
|
||||||
state.uid,
|
|
||||||
);
|
|
||||||
collectLocalReconciliationSecretConfigCompletedStorage(
|
|
||||||
selected,
|
|
||||||
intent,
|
|
||||||
state.uid,
|
|
||||||
);
|
|
||||||
validateLocalReconciliationSecretConfigCompletedStorage(
|
|
||||||
selected,
|
|
||||||
intent,
|
|
||||||
state.uid,
|
|
||||||
);
|
);
|
||||||
|
assert.equal(receipt.schemaVersion, 3);
|
||||||
assert.equal(fs.existsSync(backupPath), false);
|
assert.equal(fs.existsSync(backupPath), false);
|
||||||
assert.deepEqual(fs.readdirSync(backupRoot), []);
|
assert.deepEqual(fs.readdirSync(backupRoot), []);
|
||||||
assert.deepEqual(fs.readdirSync(path.join(applyRoot, 'rollback-work')), []);
|
assert.deepEqual(fs.readdirSync(path.join(applyRoot, 'rollback-work')), []);
|
||||||
@@ -4527,6 +4520,26 @@ test('completion v3 proves Secret/Config apply but preserves rollback authority
|
|||||||
evidenceDigest: state.applied.applyDigest,
|
evidenceDigest: state.applied.applyDigest,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
for (const domainName of ['identity_policy_audit', 'unknown']) {
|
||||||
|
assert.equal(
|
||||||
|
receipt.domains.find((domain) => domain.domain === domainName).action,
|
||||||
|
'no_effect',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const verified = await verifyLocalReconciliationCompletion({
|
||||||
|
schemaVersion: 3,
|
||||||
|
operation: 'local.deployment.reconciliation.complete.verify',
|
||||||
|
options: command.options,
|
||||||
|
request: {
|
||||||
|
completionId: command.request.completionId,
|
||||||
|
applicationId: command.request.applicationId,
|
||||||
|
expectedCompletionDigest: completed.completionDigest,
|
||||||
|
automation: null,
|
||||||
|
secretConfig,
|
||||||
|
runHistory: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
assert.equal(verified.status, 'verified');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('completion v3 rejects rolled-back Secret/Config evidence', async (t) => {
|
test('completion v3 rejects rolled-back Secret/Config evidence', async (t) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user