feat(ql3): persist cluster secret transition receipts

This commit is contained in:
whyour
2026-08-13 21:48:57 +08:00
parent c877147d0f
commit 5f93e0b5d2
26 changed files with 1127 additions and 66 deletions
+1 -1
View File
@@ -29,7 +29,7 @@
- D-305/ADR-0393(已接受):Plugin Package Manifest 的逻辑 Secret requirement 获得按 resource generation 固定的不可变 binding。binding 精确覆盖 Manifest requirements,只保存同 Project、显式 version 的 `qlsecret://` 引用与 `approved-action-execution|local-owner-confirmation` authority evidence digest,不保存 Secret 明文;required 不可为空,optional 可显式为 `null`。发布由当前 active installation head、lock、generation 与 Manifest digest 联合 fencing,相同事实幂等、不同事实冲突;domain-separated digest、最多 64 项和 64 KiB 单行预算同时约束 Local 与 Cluster。SQLite 追加 `0091` ledger 与 capability v46PostgreSQL 追加 `pg-0059`、capability v58,并只向 package executor 授予 `SELECT, INSERT`。不新增 package、daemon、timer、watcher、连接、缓存或集群 workload,低配路由设备只承担一个有界表和三个索引。D-305 不冒充 Secret 已进入执行路径:现有 materialization 拒绝仍保留,D-306 再完成用户授权、Secret resolution、runtime consumption 与 lifecycle/rebinding 语义。core 509/509、SQLite 232/232、PostgreSQL 316 pass/1 条件 skip;完整 18-package clean build/test 退出 0backend 1,188 pass/2 skip,五项边界审计零 findingworkspace 仍无 single-source/shallow-source package。PostgreSQL 18.4 arm64 HA 125 项 gate 全绿、timeline `1→2`,报告 SHA-256 为 `acf0fea7ca7699989dfe70f5dd0061cdf5fb1968c691094331fea06ce01b96dc`
- D-306A/ADR-0394(已接受):Package Task source 以 `package-secret` placeholder 引用逻辑 requirementmaterialization 只用当前 generation 的 D-305 binding 编译为已有、固定 version 的 Task `SecretRef`Package source 直接携带 SecretRef、缺失 binding、未批准 `secret.use`、跨 binding 引用和 optional/required 漂移全部失败关闭。binding 快照不含明文并进入 materialized revision digestLocal/Cluster 启动发布复用既有 repository/poolTask dispatch、Local 短时解密和 Cluster offer/lease-fenced delivery 不另造协议。SQLite/PostgreSQL INSERT trigger 同时防止直接写库绕过;Local 只读 readiness 继续不加载 DDL。Local contract v47、Cluster v59;不新增 package、表、索引、连接、daemon、watcher、timer、cache 或 workload。完整 18-package clean build/test 退出 0backend 1,188 pass/2 条件 skip/0 fail;五项 package/dependency/edge/service-manager/local-image 审计零 findingworkspace 仍无 single-source/shallow-source package,两个有序 migration ledger 精确为 PostgreSQL 61、SQLite 95 个 sourcePostgreSQL 18.4 arm64 HA 125 项 gate 全绿、timeline `1→2`,报告 SHA-256 为 `f9107e8e54892a788779758f0573ac8d6a80f6d086516a1f5f5bbacb59bbb4be`。D-306A 不冒充产品闭环:Local bind/rebind 命令、Cluster Approved Action/API 与新 generation rotation/revocation 编排属于 D-306B。
- D-306B1/ADR-0395(已接受):当前 active、尚未绑定 Package generation 的首次 Secret binding 已形成 Local 与 Cluster 产品闭环,且不允许原地 rebind。共享 content-free plan 由服务端从 installation/proposal/lock/Manifest/generation 重建;Local 使用短生命周期 `ql3-package`、Owner human confirmation 与单 SQLite transactionCluster 使用既有 package-management HTTPS/CLI、package-manager separation-of-duty Approval 和短生命周期 package-executor。三节点 K3s `v1.34.3+k3s1` arm64 现场门已在真实 PostgreSQL `18.4` 上完成两个 management Pod 跨节点部署、正式 client `plan→跨副本 replay→propose→双人 decide→inspect`、真实 executor Job 与只读 Kubernetes Secret projection。management/executor 均无 Secret API 读取权和 ServiceAccount tokenmanagement 不挂载 Package valueexecutor 只验证投影元数据;最终恰好一条 immutable bindingApproval consumed、execution succeeded,数据库敏感值扫描为 0。16/16 gate 的 owner-private、低敏报告通过独立 exact-shape 审计,SHA-256 为 `aaabb5ebea77c50bce671f91dd3051671fd20875c11a8f787fe8933f29dbfa4d`。完整 18-package clean build/test、backend 与七项边界审计,以及 PostgreSQL 18.4 physical HA 125 gate/timeline `1→2` 证据继续有效;没有新增 workspace package、migration、表、索引、依赖或常驻 workload。B2 rebind/rotation/revocation 必须通过新 Package generation 独立推进。
- D-306B2/ADR-0396(进行中):Secret rebind/rotation/revocation 不更新历史 binding,而是作为下一 Package generation 的 activation 前置事实。共享 transition plan v1 同时绑定上一 active target、可选的上一 binding、durable install history 的最后尝试 generation、新 target、可选下一 binding plan、逐 requirement 与 SecretRef 差异及独立 digest;上一 active Manifest 没有 Secret requirement 时 binding 可空,但 target/lock/generation lineage 不可省略。失败 install 也永久消耗 generation,重试必须使用 `lastAttemptGeneration + 1`active lineage 继续由 `previousActiveLockDigest` 指回旧代。SQLite capability v490097/0098现新增 immutable transition receipt ledger、typed schema、readiness 与数据库 triggerLocal Owner 私有短命令已完成 staged generation 的 server-derived `plan→execute`,在一个 `BEGIN IMMEDIATE` 中原子提交可选 binding、allowed audit 与 receipt,并支持跨时钟 exact replay。安装恢复缺 receipt 时保留 staged 与旧 active,直接协调器和应用启动都复用 activation prerequisite 再次失败关闭;receipt 到位后才允许 active CAS。Runtime Core 533/533、Local SQLite 235/235、Local Admin 91/91、Local Owner CLI 165 pass/5 root-only skip、Local Application 47 pass/4 平台 skip,合计 1071 pass/9 条件 skip/0 fail;没有新增 workspace package、第三方依赖、连接、timer、watcher 或常驻资源。PostgreSQL v61 staged-target 门继续有效,但 Cluster receipt ledger、management/executor separation-of-duty 产品编排、真实 Kubernetes rotation/revoke、升级失败回滚和低配物理证据仍待完成。
- D-306B2/ADR-0396(进行中):Secret rebind/rotation/revocation 不更新历史 binding,而是作为下一 Package generation 的 activation 前置事实。共享 transition plan v1 同时绑定上一 active target、可选的上一 binding、durable install history 的最后尝试 generation、新 target、可选下一 binding plan、逐 requirement 与 SecretRef 差异及独立 digest;上一 active Manifest 没有 Secret requirement 时 binding 可空,但 target/lock/generation lineage 不可省略。失败 install 也永久消耗 generation,重试必须使用 `lastAttemptGeneration + 1`active lineage 继续由 `previousActiveLockDigest` 指回旧代。SQLite capability v490097/0098已完成 immutable transition receipt ledger、Local Owner staged `plan→execute`、单事务 binding/audit/receipt 和 activation prerequisite。PostgreSQL capability v62pg-0063)现也具有 receipt ledgerpackage-executor 在一个 SERIALIZABLE transaction 中复验 current staged head、上一 active lineage、durable 最大 generation 与可选上一 binding,并原子提交可选目标 binding 和 immutable receipt;数据库 trigger 和最小角色 readiness 防止绕过,recovery/直接 activation 缺 receipt 均失败关闭。真实 PostgreSQL 18.4 已通过 63 条 migration、原子提交、exact replay、错误状态/lineage 拒绝,并暴露和修复了 `jsonb` 键重排下不应依赖属性顺序的 plan 归一化问题。阶段门已串行完成:18 个 QL3 workspace package 统一 clean build/test 退出 0backend 1192 pass/2 条条件 skip/0 failPostgreSQL 18.4 arm64 physical HA 以 timeline `1→2` 通过 125 gate(报告 SHA-256 `a72477cfd40e9945fd97ed18dd014f4600e0049a29285990c6359054309db812`),package/dependency/edge import 审计无新增越界。没有新增 workspace package、第三方依赖、连接、timer、watcher 或常驻资源。Cluster management/executor separation-of-duty 产品编排、真实 Kubernetes rotation/revoke、升级失败回滚和低配物理证据仍待完成。
- D-302/ADR-0390(已接受)
Cluster operator context 增加无网络、无 mutation 的内建 `ql3-cluster-admin context validate` 预检。它先复用 owner-private context
reader,再让每个 entry 经过与真实请求相同的 production HTTPS/Kubernetes configuration preparation,验证精确 route、hostname、CA、
@@ -19,7 +19,7 @@ QingLong 3.0 的 workspace package 必须表达独立制品、权限、依赖隔
- `ownership_review`:尚需按 capability 继续下沉,但不得在评审前增长。
4. 超过登记 hard cap 必须失败;目录重构后降到阈值以下时,旧登记必须删除,避免永久豁免。
5. `src/` 根层继续执行 D-269 的精确 entry role、file cap、line cap 和 public-export-only 校验。内部密度门不能替代根门,也不能作为新增 workspace package 的理由。
6. 初始冻结 8 个目录:两个 migration ledger,以及 `cluster-admin` 的 prompt-output/worker-credential、`local-owner-cli` 的 deployment/cutover、`runtime-core` 的 security/tool-execution。首个收口把 cutover 的 target-run lifecycle 与 manual-resolution ceremony 下沉到两个 capability 子目录,直接文件 16→10;第二个收口把 Tool Registry facade、Project Tool snapshot 与内建 Run Read 投影下沉到 `tool-registry/``builtin-run-read/`tool-execution 直接文件 15→11;第三个收口把 Worker credential management service、transport、HTTP、mTLS、process 与 CLI 下沉到同一 `management-server/` 部署能力目录,worker-credential 直接文件 16→10,同时保留 client、executor 与 delivery 的独立 ownership;第四个收口把 Prompt Output 的 external recovery、retention GC 和 key management 分别下沉到 `external-recovery/``retention/``key-management/`prompt-output 直接文件 13→0;第五个收口把 Local deployment 的六个 Compose lifecycle 文件与四个共享 contract/file/Docker/render 基础分别下沉到 `compose/``foundation/`deployment 直接文件 13→3,顶层只保留 facade、CLI 与 status;第六个收口把 Runtime Security 的身份/API 凭据、Project Policy 与安全审计分别下沉到 `identity-credential/``project-policy/``audit/`security 直接文件 12→1,根层只保留跨域 principal/fence primitive。六项都删除 stale review;当前只剩两个按原序保留的 migration ledger,没有 ownership review。
6. 初始冻结 8 个目录:两个 migration ledger,以及 `cluster-admin` 的 prompt-output/worker-credential、`local-owner-cli` 的 deployment/cutover、`runtime-core` 的 security/tool-execution。首个收口把 cutover 的 target-run lifecycle 与 manual-resolution ceremony 下沉到两个 capability 子目录,直接文件 16→10;第二个收口把 Tool Registry facade、Project Tool snapshot 与内建 Run Read 投影下沉到 `tool-registry/``builtin-run-read/`tool-execution 直接文件 15→11;第三个收口把 Worker credential management service、transport、HTTP、mTLS、process 与 CLI 下沉到同一 `management-server/` 部署能力目录,worker-credential 直接文件 16→10,同时保留 client、executor 与 delivery 的独立 ownership;第四个收口把 Prompt Output 的 external recovery、retention GC 和 key management 分别下沉到 `external-recovery/``retention/``key-management/`prompt-output 直接文件 13→0;第五个收口把 Local deployment 的六个 Compose lifecycle 文件与四个共享 contract/file/Docker/render 基础分别下沉到 `compose/``foundation/`deployment 直接文件 13→3,顶层只保留 facade、CLI 与 status;第六个收口把 Runtime Security 的身份/API 凭据、Project Policy 与安全审计分别下沉到 `identity-credential/``project-policy/``audit/`security 直接文件 12→1,根层只保留跨域 principal/fence primitive。六项都删除 stale review;当前只剩两个按原序保留的 migration ledger,没有 ownership review。ADR-0396 追加 PostgreSQL `pg-0063` 后,该 ordered ledger 的受审 hard cap 随事实从 63 精确推进到 64;这不改变 12 文件评审阈值,也不放宽其他目录。
7. 审计只在开发、CI 和发布检查中短生命周期运行;不进入 Edge、Standalone、Worker 或 Cluster 常驻制品,不增加依赖、进程、监听、timer、内存或闪存写入。
## 机器门禁
@@ -36,5 +36,7 @@ D-306B1 只允许给当前 active 且尚未绑定的 Package generation 做首
- 本切片没有新增 workspace package、第三方依赖、表、连接、daemon、timer、watcher 或常驻资源;Edge/Standalone 只增加 SQLite schema guardCluster 复用既有 PostgreSQL Pool/role。完整 18-package 串行测试与 backend 门已通过;真实 PostgreSQL 18.4 的 migration/runtime role 门证明 active、合规 staged 与 exact replay 可写,activating、陈旧/非最大 staged 以及直接 SQL 可被拒绝。
- Local Owner 产品纵切面已完成:新增私有短命令 `plugin-package.secret-binding.transition.plan|execute`,从当前 staged head、上一 active install/proposal/lock、历史 binding 与 durable 最大尝试 generation 重建计划;Owner 重新鉴权后在一个 `BEGIN IMMEDIATE` 中原子写入可选目标 binding、allowed audit 和 immutable transition receipt。相同 plan/audit 可跨时钟 exact replayrevoke 通过 `bindingDigest: null` receipt 表达,不制造空 binding。
- SQLite capability v490097/0098)新增 transition receipt ledger、exact schema/readiness/typed schema 和插入 trigger。安装恢复在缺 receipt 时不扫描该 staged generation,旧 active 继续服务;共享 activation prerequisite 在直接安装/恢复路径再次 fail-closed 检查 receiptreceipt 到位后才允许 active CAS。没有新增连接、timer、watcher、常驻进程、workspace package 或第三方依赖,Edge/Standalone/Adopted 共用同一 SQLite authority。
- Runtime Core 533/533、Local SQLite 235/235、Local Admin 91/91、Local Owner CLI 165 pass/5 条 root-only skip、Local Application 47 pass/4 条平台 skip,合计 1071 pass/9 条条件 skip/0 fail;定向纵切面覆盖 staged 阻断、Owner plan/execute、binding+receipt 原子提交、跨时钟 replay 和 receipt 后恢复激活
- Cluster management/executor 仍未消费 transition plan;真实 Kubernetes rotation/revoke、升级失败回滚及物理低配设备证据也仍待完成。因此 ADR 继续保持 Proposed,下一切片实现 PostgreSQL receipt ledger、separation-of-duty Approval 与 package-executor,再进入 Kubernetes/低配现场门
- PostgreSQL capability v62pg-0063)新增不可变 transition receipt ledger。package-executor 在一个 SERIALIZABLE transaction 内复验 current staged head、上一 active lineage、durable 最大 generation 与可选上一 binding,随后原子写入目标 bindingrevoke 时为空)和 receipt;数据库 trigger 独立重验 staged target,角色只取得 receipt 的 `SELECT, INSERT`。安装恢复与直接 activation 共用 receipt prerequisite,缺 receipt 的 staged generation 不进入激活候选。真实 PostgreSQL 18.4 已通过 63 条 migration、最小角色 readiness、binding+receipt 原子提交、exact replay、activating/错误 lineage 拒绝;同时修复并回归了 PostgreSQL `jsonb` 对象键重排后的 transition plan 归一化,digest 与语义校验不依赖对象属性顺序
- 阶段完整性门已串行复验:18 个 `ql3-*` workspace package 的统一 clean build/test 退出 0backend 共 1194 项,1192 pass、2 条条件 skip、0 failPostgreSQL 18.4 arm64 physical HA 完成 timeline `1→2` 提升并通过 125 项门禁,报告 SHA-256 为 `a72477cfd40e9945fd97ed18dd014f4600e0049a29285990c6359054309db812`。package boundary、cluster dependency 与 edge import 审计均无新增越界;这些证据证明当前 authority 与既有运行路径兼容,不替代尚未完成的真实 Kubernetes rotation/revoke 和固定低配物理设备验证
- Runtime Core 534/534、Local SQLite 235/235、Local Admin 91/91、Local Owner CLI 165 pass/5 条 root-only skip、Local Application 47 pass/4 条平台 skip,合计 1072 pass/9 条条件 skip/0 fail;定向纵切面覆盖 staged 阻断、Owner plan/execute、binding+receipt 原子提交、跨时钟 replay 和 receipt 后恢复激活。
- Cluster management/executor 的 separation-of-duty 产品编排仍未消费 transition plan;真实 Kubernetes rotation/revoke、升级失败回滚及物理低配设备证据也仍待完成。因此 ADR 继续保持 Proposed,下一切片把既有 package-manager Approval 与 package-executor 接入这条 durable authority,再进入 Kubernetes/低配现场门。
+1 -1
View File
@@ -6,7 +6,7 @@
"reviewedDenseDirectories": [
{
"kind": "ordered_ledger",
"maxDirectSourceFiles": 63,
"maxDirectSourceFiles": 64,
"path": "packages/ql3-cluster-postgres/src/migrations",
"rationale": "PostgreSQL migrations are an append-only version ledger whose ordering and discoverability are safer in one reviewed directory."
},
@@ -40,6 +40,7 @@ import {
PostgresPluginPackageAutomationPublicationRepository,
PostgresPluginPackageMaterializedRevisionRepository,
PostgresPluginPackageSecretBindingRepository,
PostgresPluginPackageSecretBindingActivationPrerequisite,
PostgresPluginPackagePublisherProvenanceRepository,
PostgresPluginPackageTaskReconciliationRepository,
PostgresProjectToolDefinitionSnapshotRepository,
@@ -318,6 +319,10 @@ export async function recoverClusterPluginPackages(
repository,
stageProvider: stageAuthority,
publisher,
activationPrerequisite:
new PostgresPluginPackageSecretBindingActivationPrerequisite(
database.pool,
),
now: options.now,
}).recover({
...(options.pageSize === undefined ? {} : { pageSize: options.pageSize }),
@@ -171,6 +171,7 @@ function database(serverVersionNum = '160014') {
'commit_plugin_package_task_reconciliation',
'commit_plugin_package_quarantine',
'enforce_plugin_package_secret_binding_target',
'enforce_plugin_package_secret_binding_transition_receipt_target',
'enforce_plugin_package_secret_materialization',
'enforce_plugin_package_stage_provenance',
'lock_active_plugin_package_project',
@@ -35,6 +35,7 @@ function executorPrivileges() {
'plugin_package_automation_publications',
'plugin_package_automation_publication_heads',
'plugin_package_secret_bindings',
'plugin_package_secret_binding_transition_receipts',
'project_tool_definition_snapshots',
'project_tool_definition_snapshot_sources',
'plugin_package_publisher_provenance',
@@ -204,6 +205,7 @@ function database(serverVersionNum = '160014') {
functionName,
executeAllowed: ![
'enforce_plugin_package_secret_binding_target',
'enforce_plugin_package_secret_binding_transition_receipt_target',
'enforce_plugin_package_secret_materialization',
'enforce_plugin_package_stage_provenance',
'plugin_package_automation_start_allowed',
@@ -300,6 +302,7 @@ test('proves an empty executor queue and closes PostgreSQL before returning', as
retry: 0,
manualRequired: 0,
superseded: 0,
deferred: 0,
remaining: false,
safeToAdmit: true,
});
@@ -201,6 +201,12 @@ function runtimePrivileges() {
plugin_package_automation_publication_heads: [true, false, false, false],
plugin_package_secret_binding_approval_plans: [false, false, false, false],
plugin_package_secret_bindings: [false, false, false, false],
plugin_package_secret_binding_transition_receipts: [
false,
false,
false,
false,
],
plugin_package_workflow_admissions: [true, true, false, false],
plugin_package_workflow_admission_steps: [true, true, false, false],
plugin_package_workflow_task_attempt_admissions: [true, true, false, false],
@@ -115,6 +115,12 @@ function runtimePrivileges() {
plugin_package_automation_publication_heads: [true, false, false, false],
plugin_package_secret_binding_approval_plans: [false, false, false, false],
plugin_package_secret_bindings: [false, false, false, false],
plugin_package_secret_binding_transition_receipts: [
false,
false,
false,
false,
],
plugin_package_workflow_admissions: [true, true, false, false],
plugin_package_workflow_admission_steps: [true, true, false, false],
plugin_package_workflow_task_attempt_admissions: [true, true, false, false],
@@ -43,6 +43,12 @@ export { PostgresPluginPackageMaterializedRevisionRepository } from '../plugin-p
export { PostgresApprovalRequestRepository } from '../approved-action/approvalRequestRepository';
export { PostgresProjectPolicyRepository } from '../security/projectPolicyRepository';
export { PostgresPluginPackageSecretBindingRepository } from '../plugin-package/installation/pluginPackageSecretBindingRepository';
export { PostgresPluginPackageSecretBindingActivationPrerequisite } from '../plugin-package/secret-binding/pluginPackageSecretBindingActivationPrerequisite';
export {
PostgresPluginPackageSecretBindingTransitionRepository,
type ApplyPostgresPluginPackageSecretBindingTransitionInput,
type ApplyPostgresPluginPackageSecretBindingTransitionResult,
} from '../plugin-package/secret-binding/pluginPackageSecretBindingTransitionRepository';
export { PostgresPluginPackageSecretBindingApprovalPlanReader } from '../plugin-package/secret-binding/pluginPackageSecretBindingApprovalPlanRepository';
export { PostgresPluginPackageAutomationPublicationRepository } from '../plugin-package/publication/pluginPackageAutomationPublicationRepository';
export {
@@ -318,5 +318,10 @@ export const postgresqlMainMigrationManifest: MigrationStreamManifest =
checksum:
'cd4f92d8702da6b92dd9ae5153b5180400b94442f56393692b6ec038f998596b',
}),
Object.freeze({
id: 'pg-0063-plugin-package-secret-binding-transition-receipts',
checksum:
'20c4d6e640b7fb05fc2f44dd9bec6d2d081697b83acf4e7dd5deb9a07afe54df',
}),
]),
});
@@ -65,6 +65,7 @@ import { pg0059PluginPackageSecretBindingsMigration } from './pg-0059-plugin-pac
import { pg0060PluginPackageSecretMaterializationGuardMigration } from './pg-0060-plugin-package-secret-materialization-guard';
import { pg0061PluginPackageSecretBindingApprovalPlansMigration } from './pg-0061-plugin-package-secret-binding-approval-plans';
import { pg0062PluginPackageSecretBindingTargetGuardMigration } from './pg-0062-plugin-package-secret-binding-target-guard';
import { pg0063PluginPackageSecretBindingTransitionReceiptsMigration } from './pg-0063-plugin-package-secret-binding-transition-receipts';
export const postgresqlMainMigrationStream: MigrationStreamDefinition<PostgresMigrationContext> =
Object.freeze({
@@ -135,5 +136,6 @@ export const postgresqlMainMigrationStream: MigrationStreamDefinition<PostgresMi
pg0060PluginPackageSecretMaterializationGuardMigration,
pg0061PluginPackageSecretBindingApprovalPlansMigration,
pg0062PluginPackageSecretBindingTargetGuardMigration,
pg0063PluginPackageSecretBindingTransitionReceiptsMigration,
]),
});
@@ -0,0 +1,161 @@
import { CAPABILITIES_V61 } from './pg-0062-plugin-package-secret-binding-target-guard';
import { definePostgresSqlMigration } from './sqlMigration';
export const CAPABILITIES_V62 = CAPABILITIES_V61.replace(
'"plugin_package_secret_binding_transition":1,',
'"plugin_package_secret_binding_transition":1,"plugin_package_secret_binding_transition_receipt":1,',
);
export const pg0063PluginPackageSecretBindingTransitionReceiptsMigration =
definePostgresSqlMigration({
id: 'pg-0063-plugin-package-secret-binding-transition-receipts',
statements: [
`
CREATE TABLE "ql3"."plugin_package_secret_binding_transition_receipts" (
generation_digest char(64) PRIMARY KEY,
transition_digest char(64) NOT NULL,
project_id varchar(128) NOT NULL,
package_name varchar(63) NOT NULL,
installation_id varchar(128) NOT NULL,
lock_digest char(64) NOT NULL,
generation integer NOT NULL,
manifest_digest char(64) NOT NULL,
previous_active_lock_digest char(64) NOT NULL,
authority_kind varchar(32) NOT NULL,
evidence_digest char(64) NOT NULL,
binding_digest char(64),
committed_at_ms bigint NOT NULL,
receipt_digest char(64) NOT NULL,
receipt_json jsonb NOT NULL,
CONSTRAINT ql3_plugin_package_secret_binding_transition_receipt_project_fk
FOREIGN KEY (project_id) REFERENCES "ql3"."projects" (id)
ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT ql3_plugin_package_secret_binding_transition_receipt_install_fk
FOREIGN KEY (installation_id)
REFERENCES "ql3"."plugin_package_installs" (installation_id)
ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT ql3_package_secret_transition_receipt_identity_check CHECK (
project_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
package_name ~ '^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$' AND
installation_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
generation BETWEEN 2 AND 2147483647 AND
authority_kind = 'approved-action-execution' AND
committed_at_ms >= 0
),
CONSTRAINT ql3_package_secret_transition_receipt_digest_check CHECK (
generation_digest ~ '^[0-9a-f]{64}$' AND
transition_digest ~ '^[0-9a-f]{64}$' AND
lock_digest ~ '^[0-9a-f]{64}$' AND
manifest_digest ~ '^[0-9a-f]{64}$' AND
previous_active_lock_digest ~ '^[0-9a-f]{64}$' AND
evidence_digest ~ '^[0-9a-f]{64}$' AND
(binding_digest IS NULL OR binding_digest ~ '^[0-9a-f]{64}$') AND
receipt_digest ~ '^[0-9a-f]{64}$'
),
CONSTRAINT ql3_package_secret_transition_receipt_json_check CHECK (
jsonb_typeof(receipt_json) = 'object' AND
octet_length(receipt_json::text) BETWEEN 2 AND 196608 AND
receipt_json @> jsonb_build_object(
'schema', 'qinglong/plugin-package-secret-binding-transition-receipt@v1',
'transitionPlan', jsonb_build_object(
'schema', 'qinglong/plugin-package-secret-binding-transition-plan@v1',
'transitionDigest', transition_digest,
'previousActiveLockDigest', previous_active_lock_digest,
'nextTarget', jsonb_build_object(
'generationDigest', generation_digest,
'projectId', project_id,
'packageName', package_name,
'installationId', installation_id,
'lockDigest', lock_digest,
'generation', generation,
'manifestDigest', manifest_digest
)
),
'authority', jsonb_build_object(
'kind', authority_kind,
'evidenceDigest', evidence_digest
),
'bindingDigest', binding_digest,
'committedAtMs', committed_at_ms,
'receiptDigest', receipt_digest
)
)
)
`.trim(),
`CREATE UNIQUE INDEX ql3_package_secret_transition_receipt_transition_uidx ON "ql3"."plugin_package_secret_binding_transition_receipts" (transition_digest)`,
`CREATE UNIQUE INDEX ql3_package_secret_transition_receipt_digest_uidx ON "ql3"."plugin_package_secret_binding_transition_receipts" (receipt_digest)`,
`CREATE INDEX ql3_package_secret_transition_receipt_install_idx ON "ql3"."plugin_package_secret_binding_transition_receipts" (installation_id, generation_digest)`,
`
CREATE FUNCTION "ql3"."enforce_plugin_package_secret_binding_transition_receipt_target"()
RETURNS trigger
LANGUAGE plpgsql
VOLATILE
SET search_path = pg_catalog, ql3
AS $ql3$
BEGIN
PERFORM 1
FROM "ql3"."plugin_package_install_heads" AS head
JOIN "ql3"."plugin_package_installs" AS install
ON install.installation_id = head.installation_id
AND install.project_id = head.project_id
AND install.package_name = head.package_name
JOIN "ql3"."plugin_package_installs" AS previous
ON previous.project_id = install.project_id
AND previous.package_name = install.package_name
AND previous.lock_digest = install.previous_active_lock_digest
WHERE head.project_id = NEW.project_id
AND head.package_name = NEW.package_name
AND install.installation_id = NEW.installation_id
AND install.lock_digest = NEW.lock_digest
AND install.target_generation = NEW.generation
AND install.lock_json ->> 'manifestDigest' = NEW.manifest_digest
AND install.state = 'staged'
AND install.previous_active_lock_digest = NEW.previous_active_lock_digest
AND install.active_lock_digest = install.previous_active_lock_digest
AND install.target_generation = (
SELECT MAX(history.target_generation)
FROM "ql3"."plugin_package_installs" AS history
WHERE history.project_id = install.project_id
AND history.package_name = install.package_name
)
AND previous.state = 'active'
AND previous.active_lock_digest = previous.lock_digest
AND previous.target_generation < install.target_generation
AND (
(NEW.binding_digest IS NULL AND NOT EXISTS (
SELECT 1 FROM "ql3"."plugin_package_secret_bindings" AS binding
WHERE binding.generation_digest = NEW.generation_digest
)) OR
EXISTS (
SELECT 1 FROM "ql3"."plugin_package_secret_bindings" AS binding
WHERE binding.generation_digest = NEW.generation_digest
AND binding.binding_digest = NEW.binding_digest
AND binding.project_id = NEW.project_id
AND binding.package_name = NEW.package_name
AND binding.installation_id = NEW.installation_id
AND binding.lock_digest = NEW.lock_digest
AND binding.generation = NEW.generation
AND binding.manifest_digest = NEW.manifest_digest
AND binding.authority_kind = NEW.authority_kind
AND binding.evidence_digest = NEW.evidence_digest
AND binding.bound_at_ms = NEW.committed_at_ms
)
)
FOR SHARE OF head, install, previous;
IF NOT FOUND THEN
RAISE EXCEPTION
'Plugin Package Secret binding transition receipt target is not reviewed staged generation'
USING ERRCODE = 'check_violation';
END IF;
RETURN NEW;
END
$ql3$
`.trim(),
`REVOKE ALL ON "ql3"."plugin_package_secret_binding_transition_receipts" FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager, ql3_package_executor, ql3_worker_ingress`,
`GRANT SELECT, INSERT ON "ql3"."plugin_package_secret_binding_transition_receipts" TO ql3_package_executor`,
`REVOKE ALL ON FUNCTION "ql3"."enforce_plugin_package_secret_binding_transition_receipt_target"() FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager, ql3_package_executor, ql3_worker_ingress`,
`CREATE TRIGGER ql3_package_secret_transition_receipt_target_guard BEFORE INSERT ON "ql3"."plugin_package_secret_binding_transition_receipts" FOR EACH ROW EXECUTE FUNCTION "ql3"."enforce_plugin_package_secret_binding_transition_receipt_target"()`,
`DO $ql3$ BEGIN UPDATE "ql3"."schema_capabilities" SET contract_version = 62, migration_id = 'pg-0063-plugin-package-secret-binding-transition-receipts', capabilities = '${CAPABILITIES_V62}'::jsonb, updated_at_ms = floor(extract(epoch FROM transaction_timestamp()) * 1000)::bigint WHERE contract_name = 'control-core' AND contract_version = 61 AND migration_id = 'pg-0062-plugin-package-secret-binding-target-guard' AND capabilities = '${CAPABILITIES_V61}'::jsonb; IF NOT FOUND THEN RAISE EXCEPTION 'control-core capability is not at version 61' USING ERRCODE = 'check_violation'; END IF; END $ql3$`,
],
});
@@ -1345,6 +1345,44 @@ export class PostgresPluginPackageInstallRepository
JOIN "ql3"."plugin_package_installs" AS install
ON install.installation_id = head.installation_id
WHERE install.state IN ('queued', 'staged', 'activating')
AND (
install.state <> 'staged' OR
install.previous_active_lock_digest IS NULL OR
NOT (
EXISTS (
SELECT 1
FROM "ql3"."plugin_package_admission_receipts" AS admission
JOIN "ql3"."plugin_package_install_proposals" AS proposal
ON proposal.action_ref = admission.action_ref
WHERE admission.installation_id = install.installation_id
AND jsonb_array_length(
proposal.proposal_json #> '{actionInput,manifest,spec,permissions,secrets}'
) > 0
) OR
EXISTS (
SELECT 1
FROM "ql3"."plugin_package_installs" AS previous
JOIN "ql3"."plugin_package_secret_bindings" AS binding
ON binding.installation_id = previous.installation_id
WHERE previous.project_id = install.project_id
AND previous.package_name = install.package_name
AND previous.lock_digest = install.previous_active_lock_digest
)
) OR
EXISTS (
SELECT 1
FROM "ql3"."plugin_package_secret_binding_transition_receipts"
AS receipt
WHERE receipt.project_id = install.project_id
AND receipt.package_name = install.package_name
AND receipt.installation_id = install.installation_id
AND receipt.lock_digest = install.lock_digest
AND receipt.generation = install.target_generation
AND receipt.manifest_digest = install.lock_json ->> 'manifestDigest'
AND receipt.previous_active_lock_digest =
install.previous_active_lock_digest
)
)
AND NOT EXISTS (
SELECT 1
FROM "ql3"."plugin_package_quarantine_events" AS quarantine
@@ -0,0 +1,154 @@
import type { PostgresPool } from '@qinglong/runtime-core';
import {
normalizePluginPackageInstallProposal,
type PluginPackageInstallProposal,
} from '@qinglong/runtime-core/plugin-package-proposal';
import {
assertPluginPackageInstallMatchesLock,
normalizePluginPackageInstallRecord,
normalizePluginPackageLock,
type PluginPackageInstallRecord,
type PluginPackageLock,
} from '@qinglong/runtime-core/plugin-package-install';
import type {
PluginPackageActivationPrerequisite,
PluginPackageActivationPrerequisiteObservation,
} from '@qinglong/runtime-core/plugin-package-installation';
import { createPluginPackageResourceGenerationFromReferences } from '@qinglong/runtime-core/plugin-package-resource-generation';
import { createPluginPackageSecretBindingTarget } from '@qinglong/runtime-core/plugin-package-secret-binding';
import {
postgresRequiredJsonObject,
postgresRequiredString,
} from '../../repository/definitionRepositorySupport';
import { PostgresPluginPackageSecretBindingTransitionRepository } from './pluginPackageSecretBindingTransitionRepository';
type Row = Record<string, unknown>;
function unavailable(cause?: unknown): Error {
return new Error('Secret binding activation prerequisite is unavailable', {
cause: cause instanceof Error ? cause : undefined,
});
}
export class PostgresPluginPackageSecretBindingActivationPrerequisite
implements PluginPackageActivationPrerequisite
{
readonly #receipts: PostgresPluginPackageSecretBindingTransitionRepository;
constructor(private readonly pool: PostgresPool) {
if (
!pool ||
typeof pool.query !== 'function' ||
typeof pool.connect !== 'function'
) {
throw new TypeError(
'PostgreSQL Secret binding activation prerequisite pool is invalid',
);
}
this.#receipts = new PostgresPluginPackageSecretBindingTransitionRepository(
pool,
);
}
async inspect(
recordValue: Readonly<PluginPackageInstallRecord>,
lockValue: Readonly<PluginPackageLock>,
): Promise<Readonly<PluginPackageActivationPrerequisiteObservation>> {
const record = normalizePluginPackageInstallRecord(recordValue);
const lock = normalizePluginPackageLock(lockValue);
assertPluginPackageInstallMatchesLock(lock, record);
if (record.state !== 'staged') {
throw new Error(
'Secret binding activation prerequisite requires staged install',
);
}
if (record.previousActiveLockDigest === null) {
return Object.freeze({ status: 'ready' as const });
}
try {
const result = await this.pool.query<Row>(
`SELECT proposal.proposal_json AS "proposalJson",
EXISTS (
SELECT 1
FROM "ql3"."plugin_package_installs" AS previous
JOIN "ql3"."plugin_package_secret_bindings" AS binding
ON binding.installation_id = previous.installation_id
WHERE previous.project_id = install.project_id
AND previous.package_name = install.package_name
AND previous.lock_digest = install.previous_active_lock_digest
) AS "previousBindingPresent"
FROM "ql3"."plugin_package_install_heads" AS head
JOIN "ql3"."plugin_package_installs" AS install
ON install.installation_id = head.installation_id
JOIN "ql3"."plugin_package_admission_receipts" AS admission
ON admission.installation_id = install.installation_id
JOIN "ql3"."plugin_package_install_proposals" AS proposal
ON proposal.action_ref = admission.action_ref
WHERE head.project_id = $1 AND head.package_name = $2
AND install.installation_id = $3 AND install.lock_digest = $4
AND install.state = 'staged'
AND install.previous_active_lock_digest = $5
LIMIT 2`,
[
record.projectId,
record.packageName,
record.installationId,
record.lockDigest,
record.previousActiveLockDigest,
],
);
if (result.rows.length !== 1) throw unavailable();
const proposal = normalizePluginPackageInstallProposal(
postgresRequiredJsonObject(
result.rows[0]!.proposalJson,
unavailable,
) as unknown as PluginPackageInstallProposal,
);
if (
proposal.actionDigest !== lock.approval.actionDigest ||
proposal.previewDigest !== lock.approval.previewDigest ||
proposal.actionInput.targetGeneration !== record.targetGeneration ||
proposal.actionInput.source.contentDigest !== lock.source.contentDigest
) {
throw unavailable(
new Error('Secret binding activation prerequisite provenance drift'),
);
}
const required =
proposal.actionInput.manifest.spec.permissions.secrets.length > 0 ||
result.rows[0]!.previousBindingPresent === true;
if (!required) return Object.freeze({ status: 'ready' as const });
const generation = createPluginPackageResourceGenerationFromReferences({
installationId: record.installationId,
projectId: record.projectId,
packageName: record.packageName,
lockDigest: record.lockDigest,
generation: record.targetGeneration,
previousActiveLockDigest: record.previousActiveLockDigest,
contentDigest: lock.source.contentDigest,
resources: lock.resources,
});
const target = createPluginPackageSecretBindingTarget(
generation,
proposal.actionInput.manifest,
);
const receipt = await this.#receipts.find(target.generationDigest);
if (
!receipt ||
JSON.stringify(receipt.transitionPlan.nextTarget) !==
JSON.stringify(target) ||
receipt.transitionPlan.previousActiveLockDigest !==
record.previousActiveLockDigest
) {
return Object.freeze({
status: 'deferred' as const,
reason: 'secret_binding_transition_required' as const,
});
}
return Object.freeze({ status: 'ready' as const });
} catch (error) {
throw unavailable(error);
}
}
}
@@ -0,0 +1,391 @@
import type { PostgresClient, PostgresPool } from '@qinglong/runtime-core';
import {
createPluginPackageSecretBindingFromTransitionPlan,
createPluginPackageSecretBindingTransitionReceipt,
normalizePluginPackageSecretBindingTransitionReceipt,
type PluginPackageSecretBindingTransitionReceipt,
type PluginPackageSecretBindingTransitionReceiptRepository,
} from '@qinglong/runtime-core/plugin-package-secret-binding-transition-receipt';
import {
normalizePluginPackageSecretBindingTransitionPlan,
type PluginPackageSecretBindingTransitionPlan,
} from '@qinglong/runtime-core/plugin-package-secret-binding-transition-plan';
import {
PluginPackageSecretBindingConflictError,
PluginPackageSecretBindingUnavailableError,
normalizePluginPackageSecretBinding,
type PluginPackageSecretBinding,
} from '@qinglong/runtime-core/plugin-package-secret-binding';
import {
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES,
POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS,
configurePostgresDefinitionTransaction,
postgresRequiredJsonObject,
postgresRequiredString,
postgresSqlState,
rollbackPostgresDefinitionTransaction,
} from '../../repository/definitionRepositorySupport';
import { PostgresPluginPackageSecretBindingRepository } from '../installation/pluginPackageSecretBindingRepository';
type Row = Record<string, unknown>;
const DIGEST = /^[0-9a-f]{64}$/;
export interface ApplyPostgresPluginPackageSecretBindingTransitionInput {
readonly transitionPlan: Readonly<PluginPackageSecretBindingTransitionPlan>;
readonly evidenceDigest: string;
readonly committedAtMs: number;
}
export interface ApplyPostgresPluginPackageSecretBindingTransitionResult {
readonly status: 'created' | 'existing';
readonly binding: Readonly<PluginPackageSecretBinding> | null;
readonly receipt: Readonly<PluginPackageSecretBindingTransitionReceipt>;
}
function unavailable(
cause?: unknown,
): PluginPackageSecretBindingUnavailableError {
return new PluginPackageSecretBindingUnavailableError({
cause: cause instanceof Error ? cause : undefined,
});
}
function mappedError(error: unknown): Error {
if (
error instanceof PluginPackageSecretBindingConflictError ||
error instanceof PluginPackageSecretBindingUnavailableError ||
error instanceof TypeError
) {
return error;
}
const state = postgresSqlState(error);
if (state === '23503' || state === '23505' || state === '23514') {
return new PluginPackageSecretBindingConflictError(
'durable transition identity or staged generation conflicts',
);
}
return unavailable(error);
}
function digest(value: unknown, label: string): string {
if (typeof value !== 'string' || !DIGEST.test(value)) {
throw new TypeError(`${label} is invalid`);
}
return value;
}
function timestamp(value: unknown): number {
if (!Number.isSafeInteger(value) || (value as number) < 0) {
throw new TypeError('Secret binding transition commit time is invalid');
}
return value as number;
}
function same(left: unknown, right: unknown): boolean {
return JSON.stringify(left) === JSON.stringify(right);
}
function parseReceipt(
row: Row,
): Readonly<PluginPackageSecretBindingTransitionReceipt> {
try {
const receipt = normalizePluginPackageSecretBindingTransitionReceipt(
postgresRequiredJsonObject(
row.receiptJson,
unavailable,
) as unknown as PluginPackageSecretBindingTransitionReceipt,
);
if (
receipt.receiptDigest !==
postgresRequiredString(row.receiptDigest, unavailable)
) {
throw unavailable();
}
return receipt;
} catch (error) {
if (error instanceof PluginPackageSecretBindingUnavailableError)
throw error;
throw unavailable(error);
}
}
export class PostgresPluginPackageSecretBindingTransitionRepository
implements PluginPackageSecretBindingTransitionReceiptRepository
{
constructor(private readonly pool: PostgresPool) {
if (
!pool ||
typeof pool.query !== 'function' ||
typeof pool.connect !== 'function'
) {
throw new TypeError(
'PostgreSQL Secret binding transition pool is invalid',
);
}
}
private async findStored(
queryable: Pick<PostgresPool, 'query'> | Pick<PostgresClient, 'query'>,
generationDigest: string,
): Promise<Readonly<PluginPackageSecretBindingTransitionReceipt> | null> {
const result = await queryable.query<Row>(
`SELECT receipt_json AS "receiptJson", receipt_digest AS "receiptDigest"
FROM "ql3"."plugin_package_secret_binding_transition_receipts"
WHERE generation_digest = $1
LIMIT 2`,
[generationDigest],
);
if (result.rows.length === 0) return null;
if (result.rows.length !== 1) throw unavailable();
return parseReceipt(result.rows[0]!);
}
async find(
generationDigestValue: string,
): Promise<Readonly<PluginPackageSecretBindingTransitionReceipt> | null> {
try {
return await this.findStored(
this.pool,
digest(generationDigestValue, 'generation digest'),
);
} catch (error) {
throw mappedError(error);
}
}
async #transaction<T>(
work: (client: PostgresClient) => Promise<T>,
): Promise<T> {
for (
let attempt = 0;
attempt < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS;
attempt += 1
) {
let client: PostgresClient;
try {
client = await this.pool.connect();
} catch (error) {
throw mappedError(error);
}
let began = false;
try {
await configurePostgresDefinitionTransaction(client);
began = true;
const result = await work(client);
await client.query('COMMIT');
began = false;
return result;
} catch (error) {
if (began) await rollbackPostgresDefinitionTransaction(client);
const state = postgresSqlState(error);
if (
state &&
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES.has(state) &&
attempt + 1 < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS
) {
continue;
}
throw mappedError(error);
} finally {
client.release();
}
}
throw unavailable();
}
apply(
input: Readonly<ApplyPostgresPluginPackageSecretBindingTransitionInput>,
): Promise<
Readonly<ApplyPostgresPluginPackageSecretBindingTransitionResult>
> {
if (
!input ||
typeof input !== 'object' ||
Array.isArray(input) ||
Object.keys(input).sort().join('\0') !==
'committedAtMs\0evidenceDigest\0transitionPlan'
) {
throw new TypeError('Secret binding transition execution is invalid');
}
const plan = normalizePluginPackageSecretBindingTransitionPlan(
input.transitionPlan,
);
const evidenceDigest = digest(input.evidenceDigest, 'evidence digest');
const committedAtMs = timestamp(input.committedAtMs);
const proposedBinding = createPluginPackageSecretBindingFromTransitionPlan(
plan,
'approved-action-execution',
evidenceDigest,
committedAtMs,
);
const proposedReceipt = createPluginPackageSecretBindingTransitionReceipt({
transitionPlan: plan,
authority: {
kind: 'approved-action-execution',
evidenceDigest,
},
binding: proposedBinding,
committedAtMs,
});
return this.#transaction(async (client) => {
const existing = await this.findStored(
client,
plan.nextTarget.generationDigest,
);
if (existing) {
if (!same(existing, proposedReceipt)) {
throw new PluginPackageSecretBindingConflictError(
'generation is committed by another transition authority',
);
}
const binding = existing.bindingDigest
? await new PostgresPluginPackageSecretBindingRepository(client).find(
plan.nextTarget.generationDigest,
)
: null;
if (
(existing.bindingDigest === null) !== (binding === null) ||
(binding && binding.bindingDigest !== existing.bindingDigest)
) {
throw unavailable();
}
return Object.freeze({
status: 'existing' as const,
binding,
receipt: existing,
});
}
const authority = await client.query<Row>(
`SELECT previous_binding.binding_json AS "previousBindingJson"
FROM "ql3"."plugin_package_install_heads" AS head
JOIN "ql3"."plugin_package_installs" AS install
ON install.installation_id = head.installation_id
AND install.project_id = head.project_id
AND install.package_name = head.package_name
JOIN "ql3"."plugin_package_installs" AS previous
ON previous.project_id = install.project_id
AND previous.package_name = install.package_name
AND previous.lock_digest = install.previous_active_lock_digest
LEFT JOIN "ql3"."plugin_package_secret_bindings" AS previous_binding
ON previous_binding.installation_id = previous.installation_id
AND previous_binding.project_id = previous.project_id
AND previous_binding.package_name = previous.package_name
AND previous_binding.lock_digest = previous.lock_digest
AND previous_binding.generation = previous.target_generation
WHERE head.project_id = $1 AND head.package_name = $2
AND install.installation_id = $3 AND install.lock_digest = $4
AND install.target_generation = $5
AND install.lock_json ->> 'manifestDigest' = $6
AND install.state = 'staged'
AND install.previous_active_lock_digest = $7
AND install.active_lock_digest = install.previous_active_lock_digest
AND install.target_generation = (
SELECT MAX(history.target_generation)
FROM "ql3"."plugin_package_installs" AS history
WHERE history.project_id = install.project_id
AND history.package_name = install.package_name
)
AND previous.state = 'active'
AND previous.active_lock_digest = previous.lock_digest
AND previous.installation_id = $8
AND previous.lock_digest = $7
AND previous.target_generation = $9
AND previous.lock_json ->> 'manifestDigest' = $10
FOR SHARE OF head, install, previous`,
[
plan.nextTarget.projectId,
plan.nextTarget.packageName,
plan.nextTarget.installationId,
plan.nextTarget.lockDigest,
plan.nextTarget.generation,
plan.nextTarget.manifestDigest,
plan.previousActiveLockDigest,
plan.previousTarget.installationId,
plan.previousTarget.generation,
plan.previousTarget.manifestDigest,
],
);
if (authority.rows.length !== 1) {
throw new PluginPackageSecretBindingConflictError(
'transition is not the current reviewed staged generation',
);
}
const previousBindingJson = authority.rows[0]!.previousBindingJson;
const durablePreviousBinding =
previousBindingJson === null
? null
: normalizePluginPackageSecretBinding(
postgresRequiredJsonObject(previousBindingJson, unavailable),
);
if (!same(durablePreviousBinding, plan.previousBinding)) {
throw new PluginPackageSecretBindingConflictError(
'previous active binding changed after transition planning',
);
}
const bindingResult = proposedBinding
? await new PostgresPluginPackageSecretBindingRepository(
client,
).publish(proposedBinding)
: null;
const receipt = createPluginPackageSecretBindingTransitionReceipt({
transitionPlan: plan,
authority: {
kind: 'approved-action-execution',
evidenceDigest,
},
binding: bindingResult?.binding ?? null,
committedAtMs,
});
const target = plan.nextTarget;
const inserted = await client.query(
`INSERT INTO "ql3"."plugin_package_secret_binding_transition_receipts" (
generation_digest, transition_digest, project_id, package_name,
installation_id, lock_digest, generation, manifest_digest,
previous_active_lock_digest, authority_kind, evidence_digest,
binding_digest, committed_at_ms, receipt_digest, receipt_json
) VALUES (
$1::char(64), $2::char(64), $3::varchar(128), $4::varchar(63),
$5::varchar(128), $6::char(64), $7::integer, $8::char(64),
$9::char(64), $10::varchar(32), $11::char(64), $12::char(64),
$13::bigint, $14::char(64), $15::jsonb
) ON CONFLICT (generation_digest) DO NOTHING
RETURNING generation_digest`,
[
target.generationDigest,
plan.transitionDigest,
target.projectId,
target.packageName,
target.installationId,
target.lockDigest,
target.generation,
target.manifestDigest,
plan.previousActiveLockDigest,
receipt.authority.kind,
receipt.authority.evidenceDigest,
receipt.bindingDigest,
receipt.committedAtMs,
receipt.receiptDigest,
JSON.stringify(receipt),
],
);
const stored = await this.findStored(client, target.generationDigest);
if (!stored || !same(stored, receipt)) {
throw new PluginPackageSecretBindingConflictError(
'generation is bound to another transition receipt',
);
}
return Object.freeze({
status:
inserted.rows.length === 1
? ('created' as const)
: ('existing' as const),
binding: bindingResult?.binding ?? null,
receipt: stored,
});
});
}
}
@@ -452,6 +452,69 @@ export const pluginPackageSecretBindingApprovalPlans = ql3Schema.table(
],
);
export const pluginPackageSecretBindingTransitionReceipts = ql3Schema.table(
'plugin_package_secret_binding_transition_receipts',
{
generationDigest: char('generation_digest', { length: 64 }).primaryKey(),
transitionDigest: char('transition_digest', { length: 64 }).notNull(),
projectId: varchar('project_id', { length: 128 }).notNull(),
packageName: varchar('package_name', { length: 63 }).notNull(),
installationId: varchar('installation_id', { length: 128 }).notNull(),
lockDigest: char('lock_digest', { length: 64 }).notNull(),
generation: integer('generation').notNull(),
manifestDigest: char('manifest_digest', { length: 64 }).notNull(),
previousActiveLockDigest: char('previous_active_lock_digest', {
length: 64,
}).notNull(),
authorityKind: varchar('authority_kind', { length: 32 }).notNull(),
evidenceDigest: char('evidence_digest', { length: 64 }).notNull(),
bindingDigest: char('binding_digest', { length: 64 }),
committedAtMs: bigint('committed_at_ms', { mode: 'number' }).notNull(),
receiptDigest: char('receipt_digest', { length: 64 }).notNull(),
receiptJson: jsonb('receipt_json')
.$type<Record<string, unknown>>()
.notNull(),
},
(table) => [
foreignKey({
name: 'ql3_plugin_package_secret_binding_transition_receipt_project_fk',
columns: [table.projectId],
foreignColumns: [projects.id],
})
.onDelete('restrict')
.onUpdate('restrict'),
foreignKey({
name: 'ql3_plugin_package_secret_binding_transition_receipt_install_fk',
columns: [table.installationId],
foreignColumns: [pluginPackageInstalls.installationId],
})
.onDelete('restrict')
.onUpdate('restrict'),
check(
'ql3_package_secret_transition_receipt_identity_check',
sql`${table.projectId} ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' and ${table.packageName} ~ '^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$' and ${table.installationId} ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' and ${table.generation} between 2 and 2147483647 and ${table.authorityKind} = 'approved-action-execution' and ${table.committedAtMs} >= 0`,
),
check(
'ql3_package_secret_transition_receipt_digest_check',
sql`${table.generationDigest} ~ '^[0-9a-f]{64}$' and ${table.transitionDigest} ~ '^[0-9a-f]{64}$' and ${table.lockDigest} ~ '^[0-9a-f]{64}$' and ${table.manifestDigest} ~ '^[0-9a-f]{64}$' and ${table.previousActiveLockDigest} ~ '^[0-9a-f]{64}$' and ${table.evidenceDigest} ~ '^[0-9a-f]{64}$' and (${table.bindingDigest} is null or ${table.bindingDigest} ~ '^[0-9a-f]{64}$') and ${table.receiptDigest} ~ '^[0-9a-f]{64}$'`,
),
check(
'ql3_package_secret_transition_receipt_json_check',
sql`jsonb_typeof(${table.receiptJson}) = 'object' and octet_length(${table.receiptJson}::text) between 2 and 196608 and ${table.receiptJson} @> jsonb_build_object('schema', 'qinglong/plugin-package-secret-binding-transition-receipt@v1', 'transitionPlan', jsonb_build_object('schema', 'qinglong/plugin-package-secret-binding-transition-plan@v1', 'transitionDigest', ${table.transitionDigest}, 'previousActiveLockDigest', ${table.previousActiveLockDigest}, 'nextTarget', jsonb_build_object('generationDigest', ${table.generationDigest}, 'projectId', ${table.projectId}, 'packageName', ${table.packageName}, 'installationId', ${table.installationId}, 'lockDigest', ${table.lockDigest}, 'generation', ${table.generation}, 'manifestDigest', ${table.manifestDigest})), 'authority', jsonb_build_object('kind', ${table.authorityKind}, 'evidenceDigest', ${table.evidenceDigest}), 'bindingDigest', ${table.bindingDigest}, 'committedAtMs', ${table.committedAtMs}, 'receiptDigest', ${table.receiptDigest})`,
),
uniqueIndex('ql3_package_secret_transition_receipt_transition_uidx').on(
table.transitionDigest,
),
uniqueIndex('ql3_package_secret_transition_receipt_digest_uidx').on(
table.receiptDigest,
),
index('ql3_package_secret_transition_receipt_install_idx').on(
table.installationId,
table.generationDigest,
),
],
);
export const projectToolDefinitionSnapshots = ql3Schema.table(
'project_tool_definition_snapshots',
{
@@ -5940,6 +6003,7 @@ export const ql3PostgresTables = [
pluginPackageMaterializedRevisions,
pluginPackageSecretBindings,
pluginPackageSecretBindingApprovalPlans,
pluginPackageSecretBindingTransitionReceipts,
projectToolDefinitionSnapshots,
projectToolDefinitionSnapshotSources,
pluginPackageQuarantineEvents,
@@ -21,8 +21,8 @@ export interface PostgresSchemaContractTrigger {
export interface PostgresSchemaContract {
readonly schema: 'ql3';
readonly contractName: 'control-core';
readonly contractVersion: 61;
readonly migrationId: 'pg-0062-plugin-package-secret-binding-target-guard';
readonly contractVersion: 62;
readonly migrationId: 'pg-0063-plugin-package-secret-binding-transition-receipts';
readonly minimumServerMajor: 16;
readonly maximumServerMajor: 18;
readonly capabilities: Readonly<{
@@ -64,6 +64,7 @@ export interface PostgresSchemaContract {
plugin_package_secret_binding: 1;
plugin_package_secret_binding_approval_plan: 1;
plugin_package_secret_binding_transition: 1;
plugin_package_secret_binding_transition_receipt: 1;
plugin_package_secret_materialization: 1;
plugin_package_proposal: 1;
plugin_package_publisher_provenance: 1;
@@ -115,8 +116,8 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
Object.freeze({
schema: 'ql3',
contractName: 'control-core',
contractVersion: 61,
migrationId: 'pg-0062-plugin-package-secret-binding-target-guard',
contractVersion: 62,
migrationId: 'pg-0063-plugin-package-secret-binding-transition-receipts',
minimumServerMajor: 16,
maximumServerMajor: 18,
capabilities: Object.freeze({
@@ -151,6 +152,7 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
plugin_package_secret_binding: 1,
plugin_package_secret_binding_approval_plan: 1,
plugin_package_secret_binding_transition: 1,
plugin_package_secret_binding_transition_receipt: 1,
plugin_package_secret_materialization: 1,
plugin_package_proposal: 1,
plugin_package_publisher_provenance: 1,
@@ -288,6 +290,23 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
'expires_at_ms',
'plan_json',
]),
table('plugin_package_secret_binding_transition_receipts', [
'generation_digest',
'transition_digest',
'project_id',
'package_name',
'installation_id',
'lock_digest',
'generation',
'manifest_digest',
'previous_active_lock_digest',
'authority_kind',
'evidence_digest',
'binding_digest',
'committed_at_ms',
'receipt_digest',
'receipt_json',
]),
table('project_tool_definition_snapshots', [
'project_id',
'active_vector_digest',
@@ -1471,6 +1490,10 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
'ql3_plugin_package_secret_binding_approval_plan_digest_uidx',
'ql3_plugin_package_secret_binding_approval_plan_target_idx',
'ql3_plugin_package_secret_binding_approval_plan_expiry_idx',
'plugin_package_secret_binding_transition_receipts_pkey',
'ql3_package_secret_transition_receipt_transition_uidx',
'ql3_package_secret_transition_receipt_digest_uidx',
'ql3_package_secret_transition_receipt_install_idx',
'project_tool_definition_snapshots_pkey',
'ql3_project_tool_snapshot_withdrawal_key',
'ql3_project_tool_definition_snapshot_digest_uidx',
@@ -1769,6 +1792,9 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
'ql3_plugin_package_secret_binding_approval_plan_digest_check',
'ql3_plugin_package_secret_binding_approval_plan_time_check',
'ql3_plugin_package_secret_binding_approval_plan_json_check',
'ql3_package_secret_transition_receipt_identity_check',
'ql3_package_secret_transition_receipt_digest_check',
'ql3_package_secret_transition_receipt_json_check',
'ql3_plugin_package_quarantine_identity_check',
'ql3_plugin_package_quarantine_state_check',
'ql3_plugin_package_quarantine_subject_check',
@@ -2226,6 +2252,8 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
'ql3_plugin_package_secret_binding_install_fk',
'ql3_plugin_package_secret_binding_approval_plan_project_fk',
'ql3_plugin_package_secret_binding_approval_plan_install_fk',
'ql3_plugin_package_secret_binding_transition_receipt_project_fk',
'ql3_plugin_package_secret_binding_transition_receipt_install_fk',
'ql3_project_tool_definition_snapshot_project_fk',
'ql3_project_tool_definition_snapshot_source_snapshot_fk',
'ql3_project_tool_definition_snapshot_source_install_fk',
@@ -2401,6 +2429,14 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
volatility: 'volatile',
configuration: Object.freeze(['search_path=pg_catalog, ql3']),
}),
Object.freeze({
name: 'enforce_plugin_package_secret_binding_transition_receipt_target',
identityArguments: '',
owner: 'ql3_migration',
securityDefiner: false,
volatility: 'volatile',
configuration: Object.freeze(['search_path=pg_catalog, ql3']),
}),
Object.freeze({
name: 'enforce_plugin_package_secret_materialization',
identityArguments: '',
@@ -2539,6 +2575,12 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
tableName: 'plugin_package_secret_bindings',
functionName: 'enforce_plugin_package_secret_binding_target',
}),
Object.freeze({
name: 'ql3_package_secret_transition_receipt_target_guard',
tableName: 'plugin_package_secret_binding_transition_receipts',
functionName:
'enforce_plugin_package_secret_binding_transition_receipt_target',
}),
Object.freeze({
name: 'ql3_plugin_package_secret_materialization_guard',
tableName: 'plugin_package_materialized_revisions',
@@ -186,6 +186,12 @@ const REQUIRED_RUNTIME_PRIVILEGES = Object.freeze({
update: false,
delete: false,
}),
plugin_package_secret_binding_transition_receipts: Object.freeze({
select: false,
insert: false,
update: false,
delete: false,
}),
project_tool_definition_snapshots: Object.freeze({
select: false,
insert: false,
@@ -711,6 +717,12 @@ const REQUIRED_ADMIN_PRIVILEGES = Object.freeze({
update: false,
delete: false,
}),
plugin_package_secret_binding_transition_receipts: Object.freeze({
select: false,
insert: false,
update: false,
delete: false,
}),
project_tool_definition_snapshots: Object.freeze({
select: false,
insert: false,
@@ -1264,6 +1276,7 @@ const REQUIRED_PACKAGE_EXECUTOR_PRIVILEGES: RequiredPrivileges = Object.freeze(
name === 'plugin_package_install_mutations' ||
name === 'plugin_package_materialized_revisions' ||
name === 'plugin_package_secret_bindings' ||
name === 'plugin_package_secret_binding_transition_receipts' ||
name === 'project_tool_definition_snapshots' ||
name === 'project_tool_definition_snapshot_sources' ||
name === 'plugin_package_lifecycle_plans' ||
@@ -1531,6 +1544,7 @@ const REQUIRED_RUNTIME_FUNCTION_PRIVILEGES: RequiredFunctionPrivileges =
commit_plugin_package_task_reconciliation: false,
enforce_plugin_package_secret_materialization: false,
enforce_plugin_package_secret_binding_target: false,
enforce_plugin_package_secret_binding_transition_receipt_target: false,
enforce_plugin_package_stage_provenance: false,
lock_active_plugin_package_project: false,
lock_approval_policy_fence: false,
@@ -1553,6 +1567,7 @@ const REQUIRED_PACKAGE_MANAGER_FUNCTION_PRIVILEGES: RequiredFunctionPrivileges =
commit_plugin_package_task_reconciliation: false,
enforce_plugin_package_secret_materialization: false,
enforce_plugin_package_secret_binding_target: false,
enforce_plugin_package_secret_binding_transition_receipt_target: false,
enforce_plugin_package_stage_provenance: false,
lock_active_plugin_package_project: false,
lock_approval_policy_fence: true,
@@ -1575,6 +1590,7 @@ const REQUIRED_PACKAGE_EXECUTOR_FUNCTION_PRIVILEGES: RequiredFunctionPrivileges
commit_plugin_package_task_reconciliation: true,
enforce_plugin_package_secret_materialization: false,
enforce_plugin_package_secret_binding_target: false,
enforce_plugin_package_secret_binding_transition_receipt_target: false,
enforce_plugin_package_stage_provenance: false,
lock_active_plugin_package_project: true,
lock_approval_policy_fence: true,
@@ -1744,10 +1760,9 @@ async function assertSchemaContract(
constraintsResult,
functionsResult,
triggersResult,
] =
await Promise.all([
queryable.query<ColumnRow>(
`
] = await Promise.all([
queryable.query<ColumnRow>(
`
SELECT
tables.relname AS "tableName",
columns.attname AS "columnName"
@@ -1760,19 +1775,19 @@ WHERE schemas.nspname = $1
AND NOT columns.attisdropped
ORDER BY tables.relname, columns.attnum
`.trim(),
[contract.schema],
),
queryable.query<IndexRow>(
`
[contract.schema],
),
queryable.query<IndexRow>(
`
SELECT indexname AS "indexName"
FROM pg_indexes
WHERE schemaname = $1
ORDER BY indexname
`.trim(),
[contract.schema],
),
queryable.query<ConstraintRow>(
`
[contract.schema],
),
queryable.query<ConstraintRow>(
`
SELECT
constraints.conname AS "constraintName",
CASE constraints.contype
@@ -1786,10 +1801,10 @@ WHERE schemas.nspname = $1
AND constraints.contype IN ('c', 'f')
ORDER BY constraints.contype, constraints.conname
`.trim(),
[contract.schema],
),
queryable.query<FunctionRow>(
`
[contract.schema],
),
queryable.query<FunctionRow>(
`
SELECT
routines.proname AS "functionName",
pg_get_function_identity_arguments(routines.oid) AS "identityArguments",
@@ -1817,10 +1832,10 @@ JOIN pg_namespace schemas ON schemas.oid = routines.pronamespace
WHERE schemas.nspname = $1
ORDER BY routines.proname, pg_get_function_identity_arguments(routines.oid)
`.trim(),
[contract.schema],
),
queryable.query<TriggerRow>(
`
[contract.schema],
),
queryable.query<TriggerRow>(
`
SELECT
triggers.tgname AS "triggerName",
tables.relname AS "tableName",
@@ -1835,12 +1850,9 @@ WHERE schemas.nspname = $1
AND triggers.tgname = ANY($2::text[])
ORDER BY triggers.tgname
`.trim(),
[
contract.schema,
contract.triggers.map(({ name }) => name),
],
),
]);
[contract.schema, contract.triggers.map(({ name }) => name)],
),
]);
const actualTables = new Map<string, Set<string>>();
for (const row of columnsResult.rows) {
if (
@@ -8,18 +8,22 @@ const {
const {
createPluginPackageResourceGeneration,
} = require('@qinglong/runtime-core/plugin-package-resource-generation');
const {
createPluginPackageSecretBindingTarget,
} = require('@qinglong/runtime-core/plugin-package-secret-binding');
const {
createPluginPackageSecretBindingTransitionPlan,
} = require('@qinglong/runtime-core/plugin-package-secret-binding-transition-plan');
const { createSecretRef } = require('@qinglong/runtime-core/secret-reference');
const {
assertPostgresPackageExecutorSchemaReady,
createPostgresDatabaseOpener,
PostgresPluginPackageSecretBindingRepository,
PostgresPluginPackageSecretBindingTransitionRepository,
} = require('../dist/entrypoints/packageExecutor');
const {
runPostgresMigrations,
} = require('../dist/migration/migration');
const { runPostgresMigrations } = require('../dist/migration/migration');
const migrationConnectionString =
process.env.QL3_TEST_POSTGRES_MIGRATION_URL;
const migrationConnectionString = process.env.QL3_TEST_POSTGRES_MIGRATION_URL;
const executorConnectionString =
process.env.QL3_TEST_POSTGRES_PACKAGE_EXECUTOR_URL;
@@ -221,9 +225,12 @@ async function open(role, connectionString) {
}
if (!migrationConnectionString || !executorConnectionString) {
test('PostgreSQL Secret binding transition gate requires migration and executor URLs', {
skip: true,
});
test(
'PostgreSQL Secret binding transition gate requires migration and executor URLs',
{
skip: true,
},
);
} else {
test('PostgreSQL enforces active and reviewed staged Secret binding targets', async () => {
const migrationDatabase = await open(
@@ -241,7 +248,7 @@ if (!migrationConnectionString || !executorConnectionString) {
executorDatabase.pool,
);
assert.equal(readiness.ready, true);
assert.equal(readiness.contractVersion, 61);
assert.equal(readiness.contractVersion, 62);
const repository = new PostgresPluginPackageSecretBindingRepository(
executorDatabase.pool,
@@ -326,8 +333,75 @@ if (!migrationConnectionString || !executorConnectionString) {
packageName: stagedPackageName,
installationId: stagedInstallationId,
});
assert.equal((await repository.publish(stagedBinding)).status, 'created');
assert.equal((await repository.publish(stagedBinding)).status, 'existing');
const previousGeneration = createPluginPackageResourceGeneration({
installationId: previousInstallationId,
projectId: stagedProjectId,
packageName: stagedPackageName,
lockDigest: previousLockDigest,
generation: 1,
previousActiveLockDigest: null,
contentDigest: '7'.repeat(64),
contents: manifest(stagedPackageName).spec.contents,
});
const nextGeneration = createPluginPackageResourceGeneration({
installationId: stagedInstallationId,
projectId: stagedProjectId,
packageName: stagedPackageName,
lockDigest: stagedLockDigest,
generation: 2,
previousActiveLockDigest: previousLockDigest,
contentDigest: 'e'.repeat(64),
contents: manifest(stagedPackageName).spec.contents,
});
const transitionPlan = createPluginPackageSecretBindingTransitionPlan({
previousTarget: createPluginPackageSecretBindingTarget(
previousGeneration,
manifest(stagedPackageName),
),
previousBinding: null,
previousAttemptGeneration: 1,
nextGeneration,
nextManifest: manifest(stagedPackageName),
assignments: [
{
name: 'TOKEN',
secretRef: createSecretRef({
projectId: stagedProjectId,
name: 'runtime-token',
version: 2,
}),
},
],
plannedAtMs: 100,
});
const transitions =
new PostgresPluginPackageSecretBindingTransitionRepository(
executorDatabase.pool,
);
const committed = await transitions.apply({
transitionPlan,
evidenceDigest: transitionPlan.transitionDigest,
committedAtMs: 102,
});
assert.equal(committed.status, 'created');
assert.equal(
committed.receipt.bindingDigest,
committed.binding.bindingDigest,
);
assert.equal(
(
await transitions.apply({
transitionPlan,
evidenceDigest: transitionPlan.transitionDigest,
committedAtMs: 102,
})
).status,
'existing',
);
assert.deepEqual(
await transitions.find(transitionPlan.nextTarget.generationDigest),
committed.receipt,
);
const activatingProjectId = `${namespace}-activating`;
const activatingPackageName = 'activating-binding';
@@ -113,6 +113,7 @@ test('defines the immutable PostgreSQL capability and Run core stream', async ()
'pg-0060-plugin-package-secret-materialization-guard',
'pg-0061-plugin-package-secret-binding-approval-plans',
'pg-0062-plugin-package-secret-binding-target-guard',
'pg-0063-plugin-package-secret-binding-transition-receipts',
],
);
for (const migration of postgresqlMainMigrationStream.migrations) {
@@ -166,6 +167,7 @@ test('keeps local-only and legacy tables out of the cluster baseline', async ()
'plugin_package_materialized_revisions',
'plugin_package_secret_bindings',
'plugin_package_secret_binding_approval_plans',
'plugin_package_secret_binding_transition_receipts',
'plugin_package_lifecycle_events',
'plugin_package_lifecycle_heads',
'plugin_package_lifecycle_receipts',
@@ -560,6 +562,11 @@ test('freezes every published PostgreSQL migration checksum', () => {
checksum:
'cd4f92d8702da6b92dd9ae5153b5180400b94442f56393692b6ec038f998596b',
},
{
id: 'pg-0063-plugin-package-secret-binding-transition-receipts',
checksum:
'20c4d6e640b7fb05fc2f44dd9bec6d2d081697b83acf4e7dd5deb9a07afe54df',
},
];
assert.deepEqual(
postgresqlMainMigrationStream.migrations.map(({ id, checksum }) => ({
@@ -2127,3 +2134,41 @@ test('advances capability v61 with a staged-target Secret binding database guard
/migration_id = 'pg-0061-plugin-package-secret-binding-approval-plans'/,
);
});
test('advances capability v62 with immutable Secret binding transition receipts', async () => {
const migration = migrationById(
'pg-0063-plugin-package-secret-binding-transition-receipts',
);
const statements = [];
await migration.up({
async query(statement) {
statements.push(statement);
return { rows: [] };
},
});
const sql = statements.join('\n');
assert.match(
sql,
/CREATE TABLE "ql3"\."plugin_package_secret_binding_transition_receipts"/,
);
assert.match(
sql,
/CREATE FUNCTION "ql3"\."enforce_plugin_package_secret_binding_transition_receipt_target"\(\)/,
);
assert.match(sql, /install\.state = 'staged'/);
assert.match(sql, /previous\.state = 'active'/);
assert.match(sql, /MAX\(history\.target_generation\)/);
assert.match(sql, /binding\.binding_digest = NEW\.binding_digest/);
assert.doesNotMatch(sql, /SECURITY DEFINER/);
assert.match(
sql,
/GRANT SELECT, INSERT ON "ql3"\."plugin_package_secret_binding_transition_receipts" TO ql3_package_executor/,
);
assert.match(sql, /contract_version = 62/);
assert.match(sql, /"plugin_package_secret_binding_transition_receipt":1/);
assert.match(sql, /contract_version = 61/);
assert.match(
sql,
/migration_id = 'pg-0062-plugin-package-secret-binding-target-guard'/,
);
});
@@ -76,6 +76,12 @@ function validPrivileges() {
plugin_package_materialized_revisions: [false, false, false, false],
plugin_package_secret_bindings: [false, false, false, false],
plugin_package_secret_binding_approval_plans: [false, false, false, false],
plugin_package_secret_binding_transition_receipts: [
false,
false,
false,
false,
],
project_tool_definition_snapshots: [false, false, false, false],
project_tool_definition_snapshot_sources: [false, false, false, false],
plugin_package_quarantine_events: [false, false, false, false],
@@ -196,6 +202,12 @@ function validAdminPrivileges() {
plugin_package_materialized_revisions: [false, false, false, false],
plugin_package_secret_bindings: [false, false, false, false],
plugin_package_secret_binding_approval_plans: [false, false, false, false],
plugin_package_secret_binding_transition_receipts: [
false,
false,
false,
false,
],
project_tool_definition_snapshots: [false, false, false, false],
project_tool_definition_snapshot_sources: [false, false, false, false],
plugin_package_quarantine_events: [false, false, false, false],
@@ -308,6 +320,7 @@ function packagePrivileges(kind) {
'plugin_package_install_mutations',
'plugin_package_materialized_revisions',
'plugin_package_secret_bindings',
'plugin_package_secret_binding_transition_receipts',
'project_tool_definition_snapshots',
'project_tool_definition_snapshot_sources',
'plugin_package_admission_receipts',
@@ -351,6 +364,7 @@ function packagePrivileges(kind) {
'plugin_package_install_mutations',
'plugin_package_materialized_revisions',
'plugin_package_secret_bindings',
'plugin_package_secret_binding_transition_receipts',
'plugin_package_automation_publications',
'plugin_package_automation_publication_heads',
'project_tool_definition_snapshots',
@@ -776,7 +790,7 @@ test('accepts the exact PostgreSQL control schema and least-privilege runtime ro
serverMajor: 16,
currentUser: 'ql3_runtime',
contractName: 'control-core',
contractVersion: 61,
contractVersion: 62,
migrationIds: [
'pg-0001-schema-capability',
'pg-0002-run-core',
@@ -840,6 +854,7 @@ test('accepts the exact PostgreSQL control schema and least-privilege runtime ro
'pg-0060-plugin-package-secret-materialization-guard',
'pg-0061-plugin-package-secret-binding-approval-plans',
'pg-0062-plugin-package-secret-binding-target-guard',
'pg-0063-plugin-package-secret-binding-transition-receipts',
],
});
});
@@ -870,10 +885,10 @@ test('accepts the exact schema and isolated least-privilege admin role', async (
}),
);
assert.equal(report.currentUser, 'ql3_admin');
assert.equal(report.contractVersion, 61);
assert.equal(report.contractVersion, 62);
assert.equal(
report.migrationIds.at(-1),
'pg-0062-plugin-package-secret-binding-target-guard',
'pg-0063-plugin-package-secret-binding-transition-receipts',
);
});
@@ -886,10 +901,10 @@ test('accepts the isolated least-privilege automation manager role', async () =>
}),
);
assert.equal(report.currentUser, 'ql3_automation_manager');
assert.equal(report.contractVersion, 61);
assert.equal(report.contractVersion, 62);
assert.equal(
report.migrationIds.at(-1),
'pg-0062-plugin-package-secret-binding-target-guard',
'pg-0063-plugin-package-secret-binding-transition-receipts',
);
const widened = automationManagerPrivileges();
@@ -918,10 +933,10 @@ test('accepts the isolated least-privilege human Approval manager role', async (
}),
);
assert.equal(report.currentUser, 'ql3_approval_manager');
assert.equal(report.contractVersion, 61);
assert.equal(report.contractVersion, 62);
assert.equal(
report.migrationIds.at(-1),
'pg-0062-plugin-package-secret-binding-target-guard',
'pg-0063-plugin-package-secret-binding-transition-receipts',
);
const widened = approvalManagerPrivileges();
@@ -952,10 +967,10 @@ test('accepts the isolated least-privilege Run manager role', async () => {
}),
);
assert.equal(report.currentUser, 'ql3_run_manager');
assert.equal(report.contractVersion, 61);
assert.equal(report.contractVersion, 62);
assert.equal(
report.migrationIds.at(-1),
'pg-0062-plugin-package-secret-binding-target-guard',
'pg-0063-plugin-package-secret-binding-transition-receipts',
);
const widened = runManagerPrivileges();
@@ -1087,10 +1102,10 @@ test('accepts the exact schema and isolated Worker ingress role', async () => {
}),
);
assert.equal(report.currentUser, 'ql3_worker_ingress');
assert.equal(report.contractVersion, 61);
assert.equal(report.contractVersion, 62);
assert.equal(
report.migrationIds.at(-1),
'pg-0062-plugin-package-secret-binding-target-guard',
'pg-0063-plugin-package-secret-binding-transition-receipts',
);
});
@@ -1,4 +1,5 @@
import { createHash } from 'node:crypto';
import { isDeepStrictEqual } from 'node:util';
import { normalizePluginPackageResourceGeneration } from '../pluginPackageResourceGeneration';
import {
@@ -466,7 +467,7 @@ export function normalizePluginPackageSecretBindingTransitionPlan(
nextBindingPlan,
);
if (
JSON.stringify(plan.changes) !== JSON.stringify(unsigned.changes) ||
!isDeepStrictEqual(plan.changes, unsigned.changes) ||
plan.kind !== unsigned.kind
) {
return invalid('derived transition classification does not match content');
@@ -104,6 +104,20 @@ function transition(assignments, overrides = {}) {
});
}
function postgresJsonbRoundTrip(value) {
if (Array.isArray(value)) {
return value.map(postgresJsonbRoundTrip);
}
if (value && typeof value === 'object') {
return Object.fromEntries(
Object.entries(value)
.sort(([left], [right]) => left.localeCompare(right))
.map(([key, entry]) => [key, postgresJsonbRoundTrip(entry)]),
);
}
return value;
}
test('derives an exact carry-forward for an unchanged next generation', () => {
const value = transition([
{ name: 'OPTIONAL_TOKEN', secretRef: null },
@@ -136,6 +150,19 @@ test('derives an exact carry-forward for an unchanged next generation', () => {
);
});
test('normalizes a transition after PostgreSQL jsonb reorders object keys', () => {
const value = transition([
{ name: 'OPTIONAL_TOKEN', secretRef: null },
{ name: 'TOKEN', secretRef: secret('runtime-token', 3) },
]);
assert.deepEqual(
normalizePluginPackageSecretBindingTransitionPlan(
postgresJsonbRoundTrip(value),
),
value,
);
});
test('distinguishes forward rotation from rebind and version rollback', () => {
const rotated = transition([
{ name: 'OPTIONAL_TOKEN', secretRef: null },
+1
View File
@@ -2855,6 +2855,7 @@ function auditSourceImports(root, packagePath, findings) {
[
'@qinglong/local-admin/package-secret-binding',
'@qinglong/runtime-core/plugin-package-secret-binding-plan',
'@qinglong/runtime-core/plugin-package-secret-binding-transition-plan',
].includes(specifier)
) {
continue;
+8 -8
View File
@@ -74,12 +74,12 @@ test('current QL3 workspace has exactly eighteen reviewed package boundaries', (
[
{
directory: 'packages/ql3-cluster-postgres/src/migrations',
directSourceFiles: 63,
directSourceFiles: 64,
reviewKind: 'ordered_ledger',
},
{
directory: 'packages/ql3-local-sqlite/src/migrations',
directSourceFiles: 97,
directSourceFiles: 99,
reviewKind: 'ordered_ledger',
},
],
@@ -299,10 +299,10 @@ test('current QL3 workspace has exactly eighteen reviewed package boundaries', (
rootSourceFileRoles: runtimeCore.rootSourceFileRoles,
},
{
sourceFiles: 157,
sourceFiles: 158,
rootSourceFiles: 1,
rootSourceLines: 160,
nestedSourceFiles: 156,
nestedSourceFiles: 157,
rootSourceFileRoles: { 'index.ts': 'public_export' },
},
);
@@ -421,10 +421,10 @@ test('current QL3 workspace has exactly eighteen reviewed package boundaries', (
rootSourceFileRoles: clusterPostgres.rootSourceFileRoles,
},
{
sourceFiles: 160,
sourceFiles: 163,
rootSourceFiles: 1,
rootSourceLines: 126,
nestedSourceFiles: 159,
nestedSourceFiles: 162,
rootSourceFileRoles: { 'index.ts': 'public_export' },
},
);
@@ -540,10 +540,10 @@ test('current QL3 workspace has exactly eighteen reviewed package boundaries', (
rootSourceFileRoles: localSqlite.rootSourceFileRoles,
},
{
sourceFiles: 191,
sourceFiles: 196,
rootSourceFiles: 1,
rootSourceLines: 31,
nestedSourceFiles: 190,
nestedSourceFiles: 195,
rootSourceFileRoles: { 'index.ts': 'public_export' },
},
);