mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-22 19:29:13 +08:00
feat(ql3): enforce staged secret binding persistence
This commit is contained in:
@@ -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 v46,PostgreSQL 追加 `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 退出 0,backend 1,188 pass/2 skip,五项边界审计零 finding,workspace 仍无 single-source/shallow-source package。PostgreSQL 18.4 arm64 HA 125 项 gate 全绿、timeline `1→2`,报告 SHA-256 为 `acf0fea7ca7699989dfe70f5dd0061cdf5fb1968c691094331fea06ce01b96dc`。
|
- 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 v46,PostgreSQL 追加 `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 退出 0,backend 1,188 pass/2 skip,五项边界审计零 finding,workspace 仍无 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 引用逻辑 requirement,materialization 只用当前 generation 的 D-305 binding 编译为已有、固定 version 的 Task `SecretRef`;Package source 直接携带 SecretRef、缺失 binding、未批准 `secret.use`、跨 binding 引用和 optional/required 漂移全部失败关闭。binding 快照不含明文并进入 materialized revision digest,Local/Cluster 启动发布复用既有 repository/pool,Task 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 退出 0;backend 1,188 pass/2 条件 skip/0 fail;五项 package/dependency/edge/service-manager/local-image 审计零 finding,workspace 仍无 single-source/shallow-source package,两个有序 migration ledger 精确为 PostgreSQL 61、SQLite 95 个 source;PostgreSQL 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-306A/ADR-0394(已接受):Package Task source 以 `package-secret` placeholder 引用逻辑 requirement,materialization 只用当前 generation 的 D-305 binding 编译为已有、固定 version 的 Task `SecretRef`;Package source 直接携带 SecretRef、缺失 binding、未批准 `secret.use`、跨 binding 引用和 optional/required 漂移全部失败关闭。binding 快照不含明文并进入 materialized revision digest,Local/Cluster 启动发布复用既有 repository/pool,Task 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 退出 0;backend 1,188 pass/2 条件 skip/0 fail;五项 package/dependency/edge/service-manager/local-image 审计零 finding,workspace 仍无 single-source/shallow-source package,两个有序 migration ledger 精确为 PostgreSQL 61、SQLite 95 个 source;PostgreSQL 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 transaction,Cluster 使用既有 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 token;management 不挂载 Package value,executor 只验证投影元数据;最终恰好一条 immutable binding,Approval 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-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 transaction,Cluster 使用既有 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 token;management 不挂载 Package value,executor 只验证投影元数据;最终恰好一条 immutable binding,Approval 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` 指回旧代。服务端分别推导 requirement 的 add/remove/tighten/relax 和 SecretRef 的 bind/revoke/forward-rotate/rebind;版本倒退与换名均不能冒充 rotation,删除最后一个 requirement 以 `nextBindingPlan: null` 表达而不制造空 binding。最终顺序冻结为 install approval/stage→transition approval→目标 binding publish/materialize→active CAS,失败不得移动旧 active;紧急 revoke 先复用 disable/quarantine 阻止新 admission,再做代际收敛,不声称追回运行中明文。公共 contract 归入既有 runtime-core Secret binding 聚合目录,不新增 package、依赖或常驻资源;Local/Cluster 持久化与产品纵切面仍待完成。
|
- 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` 指回旧代。服务端分别推导 requirement 的 add/remove/tighten/relax 和 SecretRef 的 bind/revoke/forward-rotate/rebind;版本倒退与换名均不能冒充 rotation,删除最后一个 requirement 以 `nextBindingPlan: null` 表达而不制造空 binding。SQLite capability v48(0095/0096)与 PostgreSQL capability v61(pg0062)现已在 repository 与数据库 trigger 两层只允许当前 active 或具完整上一 active lineage、且为 durable install history 最大 generation 的 staged target;queued/activating/failed、陈旧/跳代、断链和直接 SQL 绕过均失败关闭,readiness 校验 exact trigger/function。完整 18-package 串行门、backend 1194 项(1192 pass/2 条件 skip)及真实 PostgreSQL 18.4 B2 role/trigger 门通过。最终顺序仍为 install approval/stage→transition approval→目标 binding publish/materialize→active CAS,失败不得移动旧 active;紧急 revoke 先复用 disable/quarantine 阻止新 admission,再做代际收敛,不声称追回运行中明文。公共 contract 归入既有 runtime-core Secret binding 聚合目录;本切片未新增 package、第三方依赖、表、连接或常驻资源。Local Owner 与 Cluster management/executor 产品编排、真实 Kubernetes rotation/revoke 和低配物理证据仍待完成。
|
||||||
- D-302/ADR-0390(已接受)
|
- D-302/ADR-0390(已接受)
|
||||||
Cluster operator context 增加无网络、无 mutation 的内建 `ql3-cluster-admin context validate` 预检。它先复用 owner-private context
|
Cluster operator context 增加无网络、无 mutation 的内建 `ql3-cluster-admin context validate` 预检。它先复用 owner-private context
|
||||||
reader,再让每个 entry 经过与真实请求相同的 production HTTPS/Kubernetes configuration preparation,验证精确 route、hostname、CA、
|
reader,再让每个 entry 经过与真实请求相同的 production HTTPS/Kubernetes configuration preparation,验证精确 route、hostname、CA、
|
||||||
|
|||||||
@@ -32,4 +32,6 @@ D-306B1 只允许给当前 active 且尚未绑定的 Package generation 做首
|
|||||||
|
|
||||||
## 当前进度
|
## 当前进度
|
||||||
|
|
||||||
- 已冻结共享 transition plan v1 的 lineage 与差异分类语义;Local/Cluster activation 前发布和持久化门仍待实现,因此 ADR 保持 Proposed。
|
- 已冻结共享 transition plan v1 的 lineage 与差异分类语义,并完成 Local/Cluster 的 activation 前持久化门:SQLite capability v48 通过 migration 0095/0096、PostgreSQL capability v61 通过 migration pg0062,把 repository 写入和数据库 trigger 同时约束为“当前 active target”或“由完整上一 active lineage 支撑、且为 durable install history 最大 generation 的 staged target”。`queued|activating|failed`、陈旧/跳代 staged target、断裂 lineage 与直接 SQL 绕过均失败关闭;readiness 校验 exact trigger/function attachment,避免 schema 名义升级但约束缺失。
|
||||||
|
- 本切片没有新增 workspace package、第三方依赖、表、连接、daemon、timer、watcher 或常驻资源;Edge/Standalone 只增加 SQLite schema guard,Cluster 复用既有 PostgreSQL Pool/role。完整 18-package 串行测试与 backend 门已通过;真实 PostgreSQL 18.4 的 migration/runtime role 门证明 active、合规 staged 与 exact replay 可写,activating、陈旧/非最大 staged 以及直接 SQL 可被拒绝。
|
||||||
|
- Local Owner 与 Cluster management/executor 尚未消费 transition plan,完成“审批 → 发布 binding → materialize → active CAS”的产品纵切面;真实 Kubernetes rotation/revoke、升级失败回滚及物理低配设备证据也仍待完成。因此 ADR 继续保持 Proposed,下一切片只补产品编排,不再扩张 package 或常驻部署面。
|
||||||
|
|||||||
@@ -6,13 +6,13 @@
|
|||||||
"reviewedDenseDirectories": [
|
"reviewedDenseDirectories": [
|
||||||
{
|
{
|
||||||
"kind": "ordered_ledger",
|
"kind": "ordered_ledger",
|
||||||
"maxDirectSourceFiles": 62,
|
"maxDirectSourceFiles": 63,
|
||||||
"path": "packages/ql3-cluster-postgres/src/migrations",
|
"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."
|
"rationale": "PostgreSQL migrations are an append-only version ledger whose ordering and discoverability are safer in one reviewed directory."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ordered_ledger",
|
"kind": "ordered_ledger",
|
||||||
"maxDirectSourceFiles": 95,
|
"maxDirectSourceFiles": 97,
|
||||||
"path": "packages/ql3-local-sqlite/src/migrations",
|
"path": "packages/ql3-local-sqlite/src/migrations",
|
||||||
"rationale": "SQLite migrations are an append-only version ledger whose ordering and discoverability are safer in one reviewed directory."
|
"rationale": "SQLite migrations are an append-only version ledger whose ordering and discoverability are safer in one reviewed directory."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,6 +131,16 @@ function database(serverVersionNum = '160014') {
|
|||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (text.includes('FROM pg_trigger triggers')) {
|
||||||
|
return {
|
||||||
|
rows: contract.triggers.map((definition) => ({
|
||||||
|
triggerName: definition.name,
|
||||||
|
tableName: definition.tableName,
|
||||||
|
functionName: definition.functionName,
|
||||||
|
enabled: 'O',
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
if (text.includes('FROM pg_catalog.pg_roles')) {
|
if (text.includes('FROM pg_catalog.pg_roles')) {
|
||||||
return {
|
return {
|
||||||
rows: [
|
rows: [
|
||||||
@@ -160,6 +170,7 @@ function database(serverVersionNum = '160014') {
|
|||||||
'commit_plugin_package_lifecycle',
|
'commit_plugin_package_lifecycle',
|
||||||
'commit_plugin_package_task_reconciliation',
|
'commit_plugin_package_task_reconciliation',
|
||||||
'commit_plugin_package_quarantine',
|
'commit_plugin_package_quarantine',
|
||||||
|
'enforce_plugin_package_secret_binding_target',
|
||||||
'enforce_plugin_package_secret_materialization',
|
'enforce_plugin_package_secret_materialization',
|
||||||
'enforce_plugin_package_stage_provenance',
|
'enforce_plugin_package_stage_provenance',
|
||||||
'lock_active_plugin_package_project',
|
'lock_active_plugin_package_project',
|
||||||
|
|||||||
@@ -167,6 +167,16 @@ function database(serverVersionNum = '160014') {
|
|||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (text.includes('FROM pg_trigger triggers')) {
|
||||||
|
return {
|
||||||
|
rows: contract.triggers.map((definition) => ({
|
||||||
|
triggerName: definition.name,
|
||||||
|
tableName: definition.tableName,
|
||||||
|
functionName: definition.functionName,
|
||||||
|
enabled: 'O',
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
if (text.includes('FROM pg_catalog.pg_roles')) {
|
if (text.includes('FROM pg_catalog.pg_roles')) {
|
||||||
return {
|
return {
|
||||||
rows: [
|
rows: [
|
||||||
@@ -193,6 +203,7 @@ function database(serverVersionNum = '160014') {
|
|||||||
rows: contract.functions.map(({ name: functionName }) => ({
|
rows: contract.functions.map(({ name: functionName }) => ({
|
||||||
functionName,
|
functionName,
|
||||||
executeAllowed: ![
|
executeAllowed: ![
|
||||||
|
'enforce_plugin_package_secret_binding_target',
|
||||||
'enforce_plugin_package_secret_materialization',
|
'enforce_plugin_package_secret_materialization',
|
||||||
'enforce_plugin_package_stage_provenance',
|
'enforce_plugin_package_stage_provenance',
|
||||||
'plugin_package_automation_start_allowed',
|
'plugin_package_automation_start_allowed',
|
||||||
|
|||||||
@@ -301,6 +301,16 @@ function databaseResource(events, options = {}) {
|
|||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (text.includes('FROM pg_trigger triggers')) {
|
||||||
|
return {
|
||||||
|
rows: contract.triggers.map((definition) => ({
|
||||||
|
triggerName: definition.name,
|
||||||
|
tableName: definition.tableName,
|
||||||
|
functionName: definition.functionName,
|
||||||
|
enabled: 'O',
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
if (text.includes('FROM pg_catalog.pg_roles')) {
|
if (text.includes('FROM pg_catalog.pg_roles')) {
|
||||||
return {
|
return {
|
||||||
rows: [
|
rows: [
|
||||||
|
|||||||
@@ -215,6 +215,16 @@ function databaseResource(events, overrides = {}) {
|
|||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (text.includes('FROM pg_trigger triggers')) {
|
||||||
|
return {
|
||||||
|
rows: contract.triggers.map((definition) => ({
|
||||||
|
triggerName: definition.name,
|
||||||
|
tableName: definition.tableName,
|
||||||
|
functionName: definition.functionName,
|
||||||
|
enabled: 'O',
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
if (text.includes('FROM pg_catalog.pg_roles')) {
|
if (text.includes('FROM pg_catalog.pg_roles')) {
|
||||||
return {
|
return {
|
||||||
rows: [
|
rows: [
|
||||||
|
|||||||
@@ -313,5 +313,10 @@ export const postgresqlMainMigrationManifest: MigrationStreamManifest =
|
|||||||
checksum:
|
checksum:
|
||||||
'c995b7846ae8a57d3abb4b5523961e81aeba890e7405a030bcb505dfc6be3d25',
|
'c995b7846ae8a57d3abb4b5523961e81aeba890e7405a030bcb505dfc6be3d25',
|
||||||
}),
|
}),
|
||||||
|
Object.freeze({
|
||||||
|
id: 'pg-0062-plugin-package-secret-binding-target-guard',
|
||||||
|
checksum:
|
||||||
|
'cd4f92d8702da6b92dd9ae5153b5180400b94442f56393692b6ec038f998596b',
|
||||||
|
}),
|
||||||
]),
|
]),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ import { pg0058PluginPackageAutomationDispositionEventsMigration } from './pg-00
|
|||||||
import { pg0059PluginPackageSecretBindingsMigration } from './pg-0059-plugin-package-secret-bindings';
|
import { pg0059PluginPackageSecretBindingsMigration } from './pg-0059-plugin-package-secret-bindings';
|
||||||
import { pg0060PluginPackageSecretMaterializationGuardMigration } from './pg-0060-plugin-package-secret-materialization-guard';
|
import { pg0060PluginPackageSecretMaterializationGuardMigration } from './pg-0060-plugin-package-secret-materialization-guard';
|
||||||
import { pg0061PluginPackageSecretBindingApprovalPlansMigration } from './pg-0061-plugin-package-secret-binding-approval-plans';
|
import { pg0061PluginPackageSecretBindingApprovalPlansMigration } from './pg-0061-plugin-package-secret-binding-approval-plans';
|
||||||
|
import { pg0062PluginPackageSecretBindingTargetGuardMigration } from './pg-0062-plugin-package-secret-binding-target-guard';
|
||||||
|
|
||||||
export const postgresqlMainMigrationStream: MigrationStreamDefinition<PostgresMigrationContext> =
|
export const postgresqlMainMigrationStream: MigrationStreamDefinition<PostgresMigrationContext> =
|
||||||
Object.freeze({
|
Object.freeze({
|
||||||
@@ -133,5 +134,6 @@ export const postgresqlMainMigrationStream: MigrationStreamDefinition<PostgresMi
|
|||||||
pg0059PluginPackageSecretBindingsMigration,
|
pg0059PluginPackageSecretBindingsMigration,
|
||||||
pg0060PluginPackageSecretMaterializationGuardMigration,
|
pg0060PluginPackageSecretMaterializationGuardMigration,
|
||||||
pg0061PluginPackageSecretBindingApprovalPlansMigration,
|
pg0061PluginPackageSecretBindingApprovalPlansMigration,
|
||||||
|
pg0062PluginPackageSecretBindingTargetGuardMigration,
|
||||||
]),
|
]),
|
||||||
});
|
});
|
||||||
|
|||||||
+74
@@ -0,0 +1,74 @@
|
|||||||
|
import { CAPABILITIES_V60 } from './pg-0061-plugin-package-secret-binding-approval-plans';
|
||||||
|
import { definePostgresSqlMigration } from './sqlMigration';
|
||||||
|
|
||||||
|
export const CAPABILITIES_V61 = CAPABILITIES_V60.replace(
|
||||||
|
'"plugin_package_secret_binding":1,',
|
||||||
|
'"plugin_package_secret_binding":1,"plugin_package_secret_binding_transition":1,',
|
||||||
|
);
|
||||||
|
|
||||||
|
export const pg0062PluginPackageSecretBindingTargetGuardMigration =
|
||||||
|
definePostgresSqlMigration({
|
||||||
|
id: 'pg-0062-plugin-package-secret-binding-target-guard',
|
||||||
|
statements: [
|
||||||
|
`
|
||||||
|
CREATE FUNCTION "ql3"."enforce_plugin_package_secret_binding_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
|
||||||
|
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 = 'active' AND
|
||||||
|
install.active_lock_digest = install.lock_digest
|
||||||
|
) OR (
|
||||||
|
install.state = 'staged' AND
|
||||||
|
install.previous_active_lock_digest IS NOT NULL 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
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM "ql3"."plugin_package_installs" AS previous
|
||||||
|
WHERE previous.project_id = install.project_id
|
||||||
|
AND previous.package_name = install.package_name
|
||||||
|
AND previous.lock_digest = install.previous_active_lock_digest
|
||||||
|
AND previous.state = 'active'
|
||||||
|
AND previous.active_lock_digest = previous.lock_digest
|
||||||
|
AND previous.target_generation < install.target_generation
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
FOR SHARE OF head, install;
|
||||||
|
|
||||||
|
IF NOT FOUND THEN
|
||||||
|
RAISE EXCEPTION
|
||||||
|
'Plugin Package Secret binding target is not current active or reviewed staged generation'
|
||||||
|
USING ERRCODE = 'check_violation';
|
||||||
|
END IF;
|
||||||
|
RETURN NEW;
|
||||||
|
END
|
||||||
|
$ql3$
|
||||||
|
`.trim(),
|
||||||
|
`REVOKE ALL ON FUNCTION "ql3"."enforce_plugin_package_secret_binding_target"() FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager, ql3_package_executor, ql3_worker_ingress`,
|
||||||
|
`CREATE TRIGGER ql3_plugin_package_secret_binding_target_guard BEFORE INSERT ON "ql3"."plugin_package_secret_bindings" FOR EACH ROW EXECUTE FUNCTION "ql3"."enforce_plugin_package_secret_binding_target"()`,
|
||||||
|
`DO $ql3$ BEGIN UPDATE "ql3"."schema_capabilities" SET contract_version = 61, migration_id = 'pg-0062-plugin-package-secret-binding-target-guard', capabilities = '${CAPABILITIES_V61}'::jsonb, updated_at_ms = floor(extract(epoch FROM transaction_timestamp()) * 1000)::bigint WHERE contract_name = 'control-core' AND contract_version = 60 AND migration_id = 'pg-0061-plugin-package-secret-binding-approval-plans' AND capabilities = '${CAPABILITIES_V60}'::jsonb; IF NOT FOUND THEN RAISE EXCEPTION 'control-core capability is not at version 60' USING ERRCODE = 'check_violation'; END IF; END $ql3$`,
|
||||||
|
],
|
||||||
|
});
|
||||||
+24
-3
@@ -193,10 +193,31 @@ export class PostgresPluginPackageSecretBindingRepository
|
|||||||
AND install.project_id = $2
|
AND install.project_id = $2
|
||||||
AND install.package_name = $3
|
AND install.package_name = $3
|
||||||
AND install.lock_digest = $5
|
AND install.lock_digest = $5
|
||||||
AND install.active_lock_digest = $5
|
|
||||||
AND install.target_generation = $6
|
AND install.target_generation = $6
|
||||||
AND install.state = 'active'
|
|
||||||
AND install.lock_json ->> 'manifestDigest' = $7
|
AND install.lock_json ->> 'manifestDigest' = $7
|
||||||
|
AND (
|
||||||
|
(install.state = 'active' AND
|
||||||
|
install.active_lock_digest = install.lock_digest) OR
|
||||||
|
(install.state = 'staged' AND
|
||||||
|
install.previous_active_lock_digest IS NOT NULL 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
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM "ql3"."plugin_package_installs" AS previous
|
||||||
|
WHERE previous.project_id = install.project_id
|
||||||
|
AND previous.package_name = install.package_name
|
||||||
|
AND previous.lock_digest = install.previous_active_lock_digest
|
||||||
|
AND previous.state = 'active'
|
||||||
|
AND previous.active_lock_digest = previous.lock_digest
|
||||||
|
AND previous.target_generation < install.target_generation
|
||||||
|
))
|
||||||
|
)
|
||||||
ON CONFLICT (generation_digest) DO NOTHING
|
ON CONFLICT (generation_digest) DO NOTHING
|
||||||
RETURNING generation_digest`,
|
RETURNING generation_digest`,
|
||||||
[
|
[
|
||||||
@@ -217,7 +238,7 @@ export class PostgresPluginPackageSecretBindingRepository
|
|||||||
const stored = await this.findStored(binding.target.generationDigest);
|
const stored = await this.findStored(binding.target.generationDigest);
|
||||||
if (!stored) {
|
if (!stored) {
|
||||||
throw new PluginPackageSecretBindingConflictError(
|
throw new PluginPackageSecretBindingConflictError(
|
||||||
'binding target is not the current active Package generation',
|
'binding target is not the current active or reviewed staged Package generation',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (JSON.stringify(stored) !== bindingJson) {
|
if (JSON.stringify(stored) !== bindingJson) {
|
||||||
|
|||||||
@@ -12,11 +12,17 @@ export interface PostgresSchemaContractFunction {
|
|||||||
readonly configuration: readonly string[];
|
readonly configuration: readonly string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PostgresSchemaContractTrigger {
|
||||||
|
readonly name: string;
|
||||||
|
readonly tableName: string;
|
||||||
|
readonly functionName: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface PostgresSchemaContract {
|
export interface PostgresSchemaContract {
|
||||||
readonly schema: 'ql3';
|
readonly schema: 'ql3';
|
||||||
readonly contractName: 'control-core';
|
readonly contractName: 'control-core';
|
||||||
readonly contractVersion: 60;
|
readonly contractVersion: 61;
|
||||||
readonly migrationId: 'pg-0061-plugin-package-secret-binding-approval-plans';
|
readonly migrationId: 'pg-0062-plugin-package-secret-binding-target-guard';
|
||||||
readonly minimumServerMajor: 16;
|
readonly minimumServerMajor: 16;
|
||||||
readonly maximumServerMajor: 18;
|
readonly maximumServerMajor: 18;
|
||||||
readonly capabilities: Readonly<{
|
readonly capabilities: Readonly<{
|
||||||
@@ -57,6 +63,7 @@ export interface PostgresSchemaContract {
|
|||||||
plugin_package_materialized_revision: 1;
|
plugin_package_materialized_revision: 1;
|
||||||
plugin_package_secret_binding: 1;
|
plugin_package_secret_binding: 1;
|
||||||
plugin_package_secret_binding_approval_plan: 1;
|
plugin_package_secret_binding_approval_plan: 1;
|
||||||
|
plugin_package_secret_binding_transition: 1;
|
||||||
plugin_package_secret_materialization: 1;
|
plugin_package_secret_materialization: 1;
|
||||||
plugin_package_proposal: 1;
|
plugin_package_proposal: 1;
|
||||||
plugin_package_publisher_provenance: 1;
|
plugin_package_publisher_provenance: 1;
|
||||||
@@ -94,6 +101,7 @@ export interface PostgresSchemaContract {
|
|||||||
readonly checks: readonly string[];
|
readonly checks: readonly string[];
|
||||||
readonly foreignKeys: readonly string[];
|
readonly foreignKeys: readonly string[];
|
||||||
readonly functions: readonly PostgresSchemaContractFunction[];
|
readonly functions: readonly PostgresSchemaContractFunction[];
|
||||||
|
readonly triggers: readonly PostgresSchemaContractTrigger[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function table(
|
function table(
|
||||||
@@ -107,8 +115,8 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
|
|||||||
Object.freeze({
|
Object.freeze({
|
||||||
schema: 'ql3',
|
schema: 'ql3',
|
||||||
contractName: 'control-core',
|
contractName: 'control-core',
|
||||||
contractVersion: 60,
|
contractVersion: 61,
|
||||||
migrationId: 'pg-0061-plugin-package-secret-binding-approval-plans',
|
migrationId: 'pg-0062-plugin-package-secret-binding-target-guard',
|
||||||
minimumServerMajor: 16,
|
minimumServerMajor: 16,
|
||||||
maximumServerMajor: 18,
|
maximumServerMajor: 18,
|
||||||
capabilities: Object.freeze({
|
capabilities: Object.freeze({
|
||||||
@@ -142,6 +150,7 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
|
|||||||
plugin_package_materialized_revision: 1,
|
plugin_package_materialized_revision: 1,
|
||||||
plugin_package_secret_binding: 1,
|
plugin_package_secret_binding: 1,
|
||||||
plugin_package_secret_binding_approval_plan: 1,
|
plugin_package_secret_binding_approval_plan: 1,
|
||||||
|
plugin_package_secret_binding_transition: 1,
|
||||||
plugin_package_secret_materialization: 1,
|
plugin_package_secret_materialization: 1,
|
||||||
plugin_package_proposal: 1,
|
plugin_package_proposal: 1,
|
||||||
plugin_package_publisher_provenance: 1,
|
plugin_package_publisher_provenance: 1,
|
||||||
@@ -2384,6 +2393,14 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
|
|||||||
volatility: 'volatile',
|
volatility: 'volatile',
|
||||||
configuration: Object.freeze(['search_path=pg_catalog, ql3']),
|
configuration: Object.freeze(['search_path=pg_catalog, ql3']),
|
||||||
}),
|
}),
|
||||||
|
Object.freeze({
|
||||||
|
name: 'enforce_plugin_package_secret_binding_target',
|
||||||
|
identityArguments: '',
|
||||||
|
owner: 'ql3_migration',
|
||||||
|
securityDefiner: false,
|
||||||
|
volatility: 'volatile',
|
||||||
|
configuration: Object.freeze(['search_path=pg_catalog, ql3']),
|
||||||
|
}),
|
||||||
Object.freeze({
|
Object.freeze({
|
||||||
name: 'enforce_plugin_package_secret_materialization',
|
name: 'enforce_plugin_package_secret_materialization',
|
||||||
identityArguments: '',
|
identityArguments: '',
|
||||||
@@ -2516,4 +2533,16 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
|
|||||||
configuration: Object.freeze(['search_path=pg_catalog, ql3']),
|
configuration: Object.freeze(['search_path=pg_catalog, ql3']),
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
|
triggers: Object.freeze([
|
||||||
|
Object.freeze({
|
||||||
|
name: 'ql3_plugin_package_secret_binding_target_guard',
|
||||||
|
tableName: 'plugin_package_secret_bindings',
|
||||||
|
functionName: 'enforce_plugin_package_secret_binding_target',
|
||||||
|
}),
|
||||||
|
Object.freeze({
|
||||||
|
name: 'ql3_plugin_package_secret_materialization_guard',
|
||||||
|
tableName: 'plugin_package_materialized_revisions',
|
||||||
|
functionName: 'enforce_plugin_package_secret_materialization',
|
||||||
|
}),
|
||||||
|
]),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -89,6 +89,13 @@ interface FunctionRow extends Record<string, unknown> {
|
|||||||
publicExecute: unknown;
|
publicExecute: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface TriggerRow extends Record<string, unknown> {
|
||||||
|
triggerName: unknown;
|
||||||
|
tableName: unknown;
|
||||||
|
functionName: unknown;
|
||||||
|
enabled: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
interface SchemaPrivilegeRow extends Record<string, unknown> {
|
interface SchemaPrivilegeRow extends Record<string, unknown> {
|
||||||
schemaUsage: unknown;
|
schemaUsage: unknown;
|
||||||
schemaCreate: unknown;
|
schemaCreate: unknown;
|
||||||
@@ -1523,6 +1530,7 @@ const REQUIRED_RUNTIME_FUNCTION_PRIVILEGES: RequiredFunctionPrivileges =
|
|||||||
commit_plugin_package_quarantine: false,
|
commit_plugin_package_quarantine: false,
|
||||||
commit_plugin_package_task_reconciliation: false,
|
commit_plugin_package_task_reconciliation: false,
|
||||||
enforce_plugin_package_secret_materialization: false,
|
enforce_plugin_package_secret_materialization: false,
|
||||||
|
enforce_plugin_package_secret_binding_target: false,
|
||||||
enforce_plugin_package_stage_provenance: false,
|
enforce_plugin_package_stage_provenance: false,
|
||||||
lock_active_plugin_package_project: false,
|
lock_active_plugin_package_project: false,
|
||||||
lock_approval_policy_fence: false,
|
lock_approval_policy_fence: false,
|
||||||
@@ -1544,6 +1552,7 @@ const REQUIRED_PACKAGE_MANAGER_FUNCTION_PRIVILEGES: RequiredFunctionPrivileges =
|
|||||||
commit_plugin_package_quarantine: false,
|
commit_plugin_package_quarantine: false,
|
||||||
commit_plugin_package_task_reconciliation: false,
|
commit_plugin_package_task_reconciliation: false,
|
||||||
enforce_plugin_package_secret_materialization: false,
|
enforce_plugin_package_secret_materialization: false,
|
||||||
|
enforce_plugin_package_secret_binding_target: false,
|
||||||
enforce_plugin_package_stage_provenance: false,
|
enforce_plugin_package_stage_provenance: false,
|
||||||
lock_active_plugin_package_project: false,
|
lock_active_plugin_package_project: false,
|
||||||
lock_approval_policy_fence: true,
|
lock_approval_policy_fence: true,
|
||||||
@@ -1565,6 +1574,7 @@ const REQUIRED_PACKAGE_EXECUTOR_FUNCTION_PRIVILEGES: RequiredFunctionPrivileges
|
|||||||
commit_plugin_package_quarantine: true,
|
commit_plugin_package_quarantine: true,
|
||||||
commit_plugin_package_task_reconciliation: true,
|
commit_plugin_package_task_reconciliation: true,
|
||||||
enforce_plugin_package_secret_materialization: false,
|
enforce_plugin_package_secret_materialization: false,
|
||||||
|
enforce_plugin_package_secret_binding_target: false,
|
||||||
enforce_plugin_package_stage_provenance: false,
|
enforce_plugin_package_stage_provenance: false,
|
||||||
lock_active_plugin_package_project: true,
|
lock_active_plugin_package_project: true,
|
||||||
lock_approval_policy_fence: true,
|
lock_approval_policy_fence: true,
|
||||||
@@ -1728,7 +1738,13 @@ async function assertSchemaContract(
|
|||||||
queryable: PostgresMigrationQueryable,
|
queryable: PostgresMigrationQueryable,
|
||||||
contract: PostgresSchemaContract,
|
contract: PostgresSchemaContract,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const [columnsResult, indexesResult, constraintsResult, functionsResult] =
|
const [
|
||||||
|
columnsResult,
|
||||||
|
indexesResult,
|
||||||
|
constraintsResult,
|
||||||
|
functionsResult,
|
||||||
|
triggersResult,
|
||||||
|
] =
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
queryable.query<ColumnRow>(
|
queryable.query<ColumnRow>(
|
||||||
`
|
`
|
||||||
@@ -1803,6 +1819,27 @@ ORDER BY routines.proname, pg_get_function_identity_arguments(routines.oid)
|
|||||||
`.trim(),
|
`.trim(),
|
||||||
[contract.schema],
|
[contract.schema],
|
||||||
),
|
),
|
||||||
|
queryable.query<TriggerRow>(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
triggers.tgname AS "triggerName",
|
||||||
|
tables.relname AS "tableName",
|
||||||
|
routines.proname AS "functionName",
|
||||||
|
triggers.tgenabled AS "enabled"
|
||||||
|
FROM pg_trigger triggers
|
||||||
|
JOIN pg_class tables ON tables.oid = triggers.tgrelid
|
||||||
|
JOIN pg_namespace schemas ON schemas.oid = tables.relnamespace
|
||||||
|
JOIN pg_proc routines ON routines.oid = triggers.tgfoid
|
||||||
|
WHERE schemas.nspname = $1
|
||||||
|
AND NOT triggers.tgisinternal
|
||||||
|
AND triggers.tgname = ANY($2::text[])
|
||||||
|
ORDER BY triggers.tgname
|
||||||
|
`.trim(),
|
||||||
|
[
|
||||||
|
contract.schema,
|
||||||
|
contract.triggers.map(({ name }) => name),
|
||||||
|
],
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
const actualTables = new Map<string, Set<string>>();
|
const actualTables = new Map<string, Set<string>>();
|
||||||
for (const row of columnsResult.rows) {
|
for (const row of columnsResult.rows) {
|
||||||
@@ -1943,6 +1980,35 @@ ORDER BY routines.proname, pg_get_function_identity_arguments(routines.oid)
|
|||||||
findings.push(`unknown-function:${identity}`);
|
findings.push(`unknown-function:${identity}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const actualTriggers = new Map<string, TriggerRow>();
|
||||||
|
for (const row of triggersResult.rows) {
|
||||||
|
if (
|
||||||
|
typeof row.triggerName !== 'string' ||
|
||||||
|
typeof row.tableName !== 'string' ||
|
||||||
|
typeof row.functionName !== 'string' ||
|
||||||
|
typeof row.enabled !== 'string'
|
||||||
|
) {
|
||||||
|
throw new PostgresSchemaReadinessError('schema_contract_invalid');
|
||||||
|
}
|
||||||
|
actualTriggers.set(row.triggerName, row);
|
||||||
|
}
|
||||||
|
for (const expected of contract.triggers) {
|
||||||
|
const actual = actualTriggers.get(expected.name);
|
||||||
|
if (!actual) {
|
||||||
|
findings.push(`missing-trigger:${expected.name}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
actual.tableName !== expected.tableName ||
|
||||||
|
actual.functionName !== expected.functionName ||
|
||||||
|
actual.enabled !== 'O'
|
||||||
|
) {
|
||||||
|
findings.push(`trigger-contract:${expected.name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (actualTriggers.size !== contract.triggers.length) {
|
||||||
|
findings.push('trigger-contract-row-count');
|
||||||
|
}
|
||||||
if (findings.length > 0) {
|
if (findings.length > 0) {
|
||||||
throw new PostgresSchemaReadinessError(
|
throw new PostgresSchemaReadinessError(
|
||||||
'schema_contract_invalid',
|
'schema_contract_invalid',
|
||||||
|
|||||||
@@ -129,6 +129,14 @@ test('publishes, exact-replays and finds one binding', async () => {
|
|||||||
value.queries.find(({ text }) => text.startsWith('INSERT')).text,
|
value.queries.find(({ text }) => text.startsWith('INSERT')).text,
|
||||||
/install\.state = 'active'/,
|
/install\.state = 'active'/,
|
||||||
);
|
);
|
||||||
|
assert.match(
|
||||||
|
value.queries.find(({ text }) => text.startsWith('INSERT')).text,
|
||||||
|
/install\.state = 'staged'/,
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
value.queries.find(({ text }) => text.startsWith('INSERT')).text,
|
||||||
|
/MAX\(history\.target_generation\)/,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('rejects inactive targets and conflicting content', async () => {
|
test('rejects inactive targets and conflicting content', async () => {
|
||||||
|
|||||||
+461
@@ -0,0 +1,461 @@
|
|||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const { test } = require('node:test');
|
||||||
|
|
||||||
|
const {
|
||||||
|
PluginPackageSecretBindingConflictError,
|
||||||
|
createPluginPackageSecretBinding,
|
||||||
|
} = require('@qinglong/runtime-core/plugin-package-secret-binding');
|
||||||
|
const {
|
||||||
|
createPluginPackageResourceGeneration,
|
||||||
|
} = require('@qinglong/runtime-core/plugin-package-resource-generation');
|
||||||
|
const { createSecretRef } = require('@qinglong/runtime-core/secret-reference');
|
||||||
|
const {
|
||||||
|
assertPostgresPackageExecutorSchemaReady,
|
||||||
|
createPostgresDatabaseOpener,
|
||||||
|
PostgresPluginPackageSecretBindingRepository,
|
||||||
|
} = require('../dist/entrypoints/packageExecutor');
|
||||||
|
const {
|
||||||
|
runPostgresMigrations,
|
||||||
|
} = require('../dist/migration/migration');
|
||||||
|
|
||||||
|
const migrationConnectionString =
|
||||||
|
process.env.QL3_TEST_POSTGRES_MIGRATION_URL;
|
||||||
|
const executorConnectionString =
|
||||||
|
process.env.QL3_TEST_POSTGRES_PACKAGE_EXECUTOR_URL;
|
||||||
|
|
||||||
|
function manifest(packageName) {
|
||||||
|
return {
|
||||||
|
apiVersion: 'qinglong.io/v1alpha1',
|
||||||
|
kind: 'Package',
|
||||||
|
metadata: {
|
||||||
|
name: packageName,
|
||||||
|
displayName: packageName,
|
||||||
|
version: '1.0.0',
|
||||||
|
description: 'PostgreSQL Secret binding transition gate',
|
||||||
|
license: 'Apache-2.0',
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
compatibility: {
|
||||||
|
qinglong: '>=3.0.0-0 <4.0.0',
|
||||||
|
architectures: ['arm64'],
|
||||||
|
deploymentProfiles: ['cluster-control'],
|
||||||
|
},
|
||||||
|
runtimes: [],
|
||||||
|
resources: {
|
||||||
|
memory: { recommended: '16Mi' },
|
||||||
|
disk: { install: '4Mi', working: '8Mi' },
|
||||||
|
},
|
||||||
|
permissions: {
|
||||||
|
network: { allowedHosts: [] },
|
||||||
|
secrets: [{ name: 'TOKEN', required: true }],
|
||||||
|
tools: [],
|
||||||
|
},
|
||||||
|
contents: { tasks: [], workflows: [], prompts: [], tools: [] },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function binding({
|
||||||
|
projectId,
|
||||||
|
packageName,
|
||||||
|
installationId,
|
||||||
|
lockDigest,
|
||||||
|
generation,
|
||||||
|
previousActiveLockDigest,
|
||||||
|
digestSeed,
|
||||||
|
}) {
|
||||||
|
const packageManifest = manifest(packageName);
|
||||||
|
const resourceGeneration = createPluginPackageResourceGeneration({
|
||||||
|
installationId,
|
||||||
|
projectId,
|
||||||
|
packageName,
|
||||||
|
lockDigest,
|
||||||
|
generation,
|
||||||
|
previousActiveLockDigest,
|
||||||
|
contentDigest: digestSeed.repeat(64),
|
||||||
|
contents: packageManifest.spec.contents,
|
||||||
|
});
|
||||||
|
return createPluginPackageSecretBinding({
|
||||||
|
generation: resourceGeneration,
|
||||||
|
manifest: packageManifest,
|
||||||
|
assignments: [
|
||||||
|
{
|
||||||
|
name: 'TOKEN',
|
||||||
|
secretRef: createSecretRef({
|
||||||
|
projectId,
|
||||||
|
name: 'runtime-token',
|
||||||
|
version: generation,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
authority: {
|
||||||
|
kind: 'approved-action-execution',
|
||||||
|
evidenceDigest: digestSeed.repeat(64),
|
||||||
|
},
|
||||||
|
boundAtMs: 100 + generation,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function insertProject(pool, projectId) {
|
||||||
|
await pool.query(
|
||||||
|
`INSERT INTO "ql3"."projects" (
|
||||||
|
id, name, slug, status, version, created_at_ms, updated_at_ms
|
||||||
|
) VALUES ($1, $1, $1, 'active', 1, 1, 1)`,
|
||||||
|
[projectId],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function insertInstall(
|
||||||
|
pool,
|
||||||
|
{
|
||||||
|
projectId,
|
||||||
|
packageName,
|
||||||
|
installationId,
|
||||||
|
lockDigest,
|
||||||
|
targetGeneration,
|
||||||
|
previousActiveLockDigest,
|
||||||
|
activeLockDigest,
|
||||||
|
state,
|
||||||
|
manifestDigest,
|
||||||
|
createdAtMs,
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const recordDigest = lockDigest;
|
||||||
|
const mutationDigest = lockDigest;
|
||||||
|
const lockJson = {
|
||||||
|
lockDigest,
|
||||||
|
projectId,
|
||||||
|
packageName,
|
||||||
|
manifestDigest,
|
||||||
|
};
|
||||||
|
const recordJson = {
|
||||||
|
installationId,
|
||||||
|
projectId,
|
||||||
|
packageName,
|
||||||
|
lockDigest,
|
||||||
|
state,
|
||||||
|
version: 1,
|
||||||
|
recordDigest,
|
||||||
|
};
|
||||||
|
await pool.query(
|
||||||
|
`INSERT INTO "ql3"."plugin_package_installs" (
|
||||||
|
installation_id, project_id, package_name, package_version,
|
||||||
|
operation, lock_digest, target_generation,
|
||||||
|
previous_active_lock_digest, active_lock_digest, state, version,
|
||||||
|
last_mutation_id, last_mutation_digest, lock_json, record_json,
|
||||||
|
record_digest, created_at_ms, updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
$1, $2, $3, '1.0.0', $4, $5, $6, $7, $8, $9, 1,
|
||||||
|
$10, $11, $12::jsonb, $13::jsonb, $14, $15, $15
|
||||||
|
)`,
|
||||||
|
[
|
||||||
|
installationId,
|
||||||
|
projectId,
|
||||||
|
packageName,
|
||||||
|
targetGeneration === 1 ? 'install' : 'upgrade',
|
||||||
|
lockDigest,
|
||||||
|
targetGeneration,
|
||||||
|
previousActiveLockDigest,
|
||||||
|
activeLockDigest,
|
||||||
|
state,
|
||||||
|
`mutation-${installationId}`,
|
||||||
|
mutationDigest,
|
||||||
|
JSON.stringify(lockJson),
|
||||||
|
JSON.stringify(recordJson),
|
||||||
|
recordDigest,
|
||||||
|
createdAtMs,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function insertHead(pool, value) {
|
||||||
|
await pool.query(
|
||||||
|
`INSERT INTO "ql3"."plugin_package_install_heads" (
|
||||||
|
project_id, package_name, installation_id
|
||||||
|
) VALUES ($1, $2, $3)`,
|
||||||
|
[value.projectId, value.packageName, value.installationId],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function insertBindingDirectly(pool, value) {
|
||||||
|
return pool.query(
|
||||||
|
`INSERT INTO "ql3"."plugin_package_secret_bindings" (
|
||||||
|
generation_digest, project_id, package_name, installation_id,
|
||||||
|
lock_digest, generation, manifest_digest, authority_kind,
|
||||||
|
evidence_digest, bound_at_ms, binding_digest, binding_json
|
||||||
|
) VALUES (
|
||||||
|
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12::jsonb
|
||||||
|
)`,
|
||||||
|
[
|
||||||
|
value.target.generationDigest,
|
||||||
|
value.target.projectId,
|
||||||
|
value.target.packageName,
|
||||||
|
value.target.installationId,
|
||||||
|
value.target.lockDigest,
|
||||||
|
value.target.generation,
|
||||||
|
value.target.manifestDigest,
|
||||||
|
value.authority.kind,
|
||||||
|
value.authority.evidenceDigest,
|
||||||
|
value.boundAtMs,
|
||||||
|
value.bindingDigest,
|
||||||
|
JSON.stringify(value),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function open(role, connectionString) {
|
||||||
|
return createPostgresDatabaseOpener({
|
||||||
|
role,
|
||||||
|
connection: {
|
||||||
|
connectionString,
|
||||||
|
tls: { mode: 'disable' },
|
||||||
|
},
|
||||||
|
pool: {
|
||||||
|
maxConnections: 1,
|
||||||
|
applicationName: `ql3-b2-secret-binding-${role}`,
|
||||||
|
},
|
||||||
|
onPoolError(error) {
|
||||||
|
throw error;
|
||||||
|
},
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!migrationConnectionString || !executorConnectionString) {
|
||||||
|
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(
|
||||||
|
'migration',
|
||||||
|
migrationConnectionString,
|
||||||
|
);
|
||||||
|
let executorDatabase;
|
||||||
|
try {
|
||||||
|
await runPostgresMigrations({ pool: migrationDatabase.pool });
|
||||||
|
executorDatabase = await open(
|
||||||
|
'package-executor',
|
||||||
|
executorConnectionString,
|
||||||
|
);
|
||||||
|
const readiness = await assertPostgresPackageExecutorSchemaReady(
|
||||||
|
executorDatabase.pool,
|
||||||
|
);
|
||||||
|
assert.equal(readiness.ready, true);
|
||||||
|
assert.equal(readiness.contractVersion, 61);
|
||||||
|
|
||||||
|
const repository = new PostgresPluginPackageSecretBindingRepository(
|
||||||
|
executorDatabase.pool,
|
||||||
|
);
|
||||||
|
const namespace = `b2-${process.pid}-${Date.now()}`;
|
||||||
|
|
||||||
|
const activeProjectId = `${namespace}-active`;
|
||||||
|
const activePackageName = 'active-binding';
|
||||||
|
const activeInstallationId = `${namespace}-active-install`;
|
||||||
|
const activeLockDigest = 'a'.repeat(64);
|
||||||
|
const activeBinding = binding({
|
||||||
|
projectId: activeProjectId,
|
||||||
|
packageName: activePackageName,
|
||||||
|
installationId: activeInstallationId,
|
||||||
|
lockDigest: activeLockDigest,
|
||||||
|
generation: 1,
|
||||||
|
previousActiveLockDigest: null,
|
||||||
|
digestSeed: 'b',
|
||||||
|
});
|
||||||
|
await insertProject(migrationDatabase.pool, activeProjectId);
|
||||||
|
await insertInstall(migrationDatabase.pool, {
|
||||||
|
projectId: activeProjectId,
|
||||||
|
packageName: activePackageName,
|
||||||
|
installationId: activeInstallationId,
|
||||||
|
lockDigest: activeLockDigest,
|
||||||
|
targetGeneration: 1,
|
||||||
|
previousActiveLockDigest: null,
|
||||||
|
activeLockDigest,
|
||||||
|
state: 'active',
|
||||||
|
manifestDigest: activeBinding.target.manifestDigest,
|
||||||
|
createdAtMs: 1,
|
||||||
|
});
|
||||||
|
await insertHead(migrationDatabase.pool, {
|
||||||
|
projectId: activeProjectId,
|
||||||
|
packageName: activePackageName,
|
||||||
|
installationId: activeInstallationId,
|
||||||
|
});
|
||||||
|
assert.equal((await repository.publish(activeBinding)).status, 'created');
|
||||||
|
|
||||||
|
const stagedProjectId = `${namespace}-staged`;
|
||||||
|
const stagedPackageName = 'staged-binding';
|
||||||
|
const previousInstallationId = `${namespace}-previous-install`;
|
||||||
|
const stagedInstallationId = `${namespace}-staged-install`;
|
||||||
|
const previousLockDigest = 'c'.repeat(64);
|
||||||
|
const stagedLockDigest = 'd'.repeat(64);
|
||||||
|
const stagedBinding = binding({
|
||||||
|
projectId: stagedProjectId,
|
||||||
|
packageName: stagedPackageName,
|
||||||
|
installationId: stagedInstallationId,
|
||||||
|
lockDigest: stagedLockDigest,
|
||||||
|
generation: 2,
|
||||||
|
previousActiveLockDigest: previousLockDigest,
|
||||||
|
digestSeed: 'e',
|
||||||
|
});
|
||||||
|
await insertProject(migrationDatabase.pool, stagedProjectId);
|
||||||
|
await insertInstall(migrationDatabase.pool, {
|
||||||
|
projectId: stagedProjectId,
|
||||||
|
packageName: stagedPackageName,
|
||||||
|
installationId: previousInstallationId,
|
||||||
|
lockDigest: previousLockDigest,
|
||||||
|
targetGeneration: 1,
|
||||||
|
previousActiveLockDigest: null,
|
||||||
|
activeLockDigest: previousLockDigest,
|
||||||
|
state: 'active',
|
||||||
|
manifestDigest: stagedBinding.target.manifestDigest,
|
||||||
|
createdAtMs: 1,
|
||||||
|
});
|
||||||
|
await insertInstall(migrationDatabase.pool, {
|
||||||
|
projectId: stagedProjectId,
|
||||||
|
packageName: stagedPackageName,
|
||||||
|
installationId: stagedInstallationId,
|
||||||
|
lockDigest: stagedLockDigest,
|
||||||
|
targetGeneration: 2,
|
||||||
|
previousActiveLockDigest: previousLockDigest,
|
||||||
|
activeLockDigest: previousLockDigest,
|
||||||
|
state: 'staged',
|
||||||
|
manifestDigest: stagedBinding.target.manifestDigest,
|
||||||
|
createdAtMs: 2,
|
||||||
|
});
|
||||||
|
await insertHead(migrationDatabase.pool, {
|
||||||
|
projectId: stagedProjectId,
|
||||||
|
packageName: stagedPackageName,
|
||||||
|
installationId: stagedInstallationId,
|
||||||
|
});
|
||||||
|
assert.equal((await repository.publish(stagedBinding)).status, 'created');
|
||||||
|
assert.equal((await repository.publish(stagedBinding)).status, 'existing');
|
||||||
|
|
||||||
|
const activatingProjectId = `${namespace}-activating`;
|
||||||
|
const activatingPackageName = 'activating-binding';
|
||||||
|
const activatingPreviousId = `${namespace}-activating-previous`;
|
||||||
|
const activatingInstallationId = `${namespace}-activating-install`;
|
||||||
|
const activatingPreviousLock = 'f'.repeat(64);
|
||||||
|
const activatingLock = '1'.repeat(64);
|
||||||
|
const activatingBinding = binding({
|
||||||
|
projectId: activatingProjectId,
|
||||||
|
packageName: activatingPackageName,
|
||||||
|
installationId: activatingInstallationId,
|
||||||
|
lockDigest: activatingLock,
|
||||||
|
generation: 2,
|
||||||
|
previousActiveLockDigest: activatingPreviousLock,
|
||||||
|
digestSeed: '2',
|
||||||
|
});
|
||||||
|
await insertProject(migrationDatabase.pool, activatingProjectId);
|
||||||
|
await insertInstall(migrationDatabase.pool, {
|
||||||
|
projectId: activatingProjectId,
|
||||||
|
packageName: activatingPackageName,
|
||||||
|
installationId: activatingPreviousId,
|
||||||
|
lockDigest: activatingPreviousLock,
|
||||||
|
targetGeneration: 1,
|
||||||
|
previousActiveLockDigest: null,
|
||||||
|
activeLockDigest: activatingPreviousLock,
|
||||||
|
state: 'active',
|
||||||
|
manifestDigest: activatingBinding.target.manifestDigest,
|
||||||
|
createdAtMs: 1,
|
||||||
|
});
|
||||||
|
await insertInstall(migrationDatabase.pool, {
|
||||||
|
projectId: activatingProjectId,
|
||||||
|
packageName: activatingPackageName,
|
||||||
|
installationId: activatingInstallationId,
|
||||||
|
lockDigest: activatingLock,
|
||||||
|
targetGeneration: 2,
|
||||||
|
previousActiveLockDigest: activatingPreviousLock,
|
||||||
|
activeLockDigest: activatingPreviousLock,
|
||||||
|
state: 'activating',
|
||||||
|
manifestDigest: activatingBinding.target.manifestDigest,
|
||||||
|
createdAtMs: 2,
|
||||||
|
});
|
||||||
|
await insertHead(migrationDatabase.pool, {
|
||||||
|
projectId: activatingProjectId,
|
||||||
|
packageName: activatingPackageName,
|
||||||
|
installationId: activatingInstallationId,
|
||||||
|
});
|
||||||
|
await assert.rejects(
|
||||||
|
repository.publish(activatingBinding),
|
||||||
|
PluginPackageSecretBindingConflictError,
|
||||||
|
);
|
||||||
|
await assert.rejects(
|
||||||
|
insertBindingDirectly(executorDatabase.pool, activatingBinding),
|
||||||
|
(error) =>
|
||||||
|
error?.code === '23514' &&
|
||||||
|
/not current active or reviewed staged generation/.test(
|
||||||
|
error.message,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const staleProjectId = `${namespace}-stale`;
|
||||||
|
const stalePackageName = 'stale-binding';
|
||||||
|
const stalePreviousId = `${namespace}-stale-previous`;
|
||||||
|
const staleInstallationId = `${namespace}-stale-install`;
|
||||||
|
const newerInstallationId = `${namespace}-newer-install`;
|
||||||
|
const stalePreviousLock = '3'.repeat(64);
|
||||||
|
const staleLock = '4'.repeat(64);
|
||||||
|
const staleBinding = binding({
|
||||||
|
projectId: staleProjectId,
|
||||||
|
packageName: stalePackageName,
|
||||||
|
installationId: staleInstallationId,
|
||||||
|
lockDigest: staleLock,
|
||||||
|
generation: 2,
|
||||||
|
previousActiveLockDigest: stalePreviousLock,
|
||||||
|
digestSeed: '5',
|
||||||
|
});
|
||||||
|
await insertProject(migrationDatabase.pool, staleProjectId);
|
||||||
|
await insertInstall(migrationDatabase.pool, {
|
||||||
|
projectId: staleProjectId,
|
||||||
|
packageName: stalePackageName,
|
||||||
|
installationId: stalePreviousId,
|
||||||
|
lockDigest: stalePreviousLock,
|
||||||
|
targetGeneration: 1,
|
||||||
|
previousActiveLockDigest: null,
|
||||||
|
activeLockDigest: stalePreviousLock,
|
||||||
|
state: 'active',
|
||||||
|
manifestDigest: staleBinding.target.manifestDigest,
|
||||||
|
createdAtMs: 1,
|
||||||
|
});
|
||||||
|
await insertInstall(migrationDatabase.pool, {
|
||||||
|
projectId: staleProjectId,
|
||||||
|
packageName: stalePackageName,
|
||||||
|
installationId: staleInstallationId,
|
||||||
|
lockDigest: staleLock,
|
||||||
|
targetGeneration: 2,
|
||||||
|
previousActiveLockDigest: stalePreviousLock,
|
||||||
|
activeLockDigest: stalePreviousLock,
|
||||||
|
state: 'staged',
|
||||||
|
manifestDigest: staleBinding.target.manifestDigest,
|
||||||
|
createdAtMs: 2,
|
||||||
|
});
|
||||||
|
await insertInstall(migrationDatabase.pool, {
|
||||||
|
projectId: staleProjectId,
|
||||||
|
packageName: stalePackageName,
|
||||||
|
installationId: newerInstallationId,
|
||||||
|
lockDigest: '6'.repeat(64),
|
||||||
|
targetGeneration: 3,
|
||||||
|
previousActiveLockDigest: stalePreviousLock,
|
||||||
|
activeLockDigest: stalePreviousLock,
|
||||||
|
state: 'failed',
|
||||||
|
manifestDigest: staleBinding.target.manifestDigest,
|
||||||
|
createdAtMs: 3,
|
||||||
|
});
|
||||||
|
await insertHead(migrationDatabase.pool, {
|
||||||
|
projectId: staleProjectId,
|
||||||
|
packageName: stalePackageName,
|
||||||
|
installationId: staleInstallationId,
|
||||||
|
});
|
||||||
|
await assert.rejects(
|
||||||
|
repository.publish(staleBinding),
|
||||||
|
PluginPackageSecretBindingConflictError,
|
||||||
|
);
|
||||||
|
await assert.rejects(
|
||||||
|
insertBindingDirectly(executorDatabase.pool, staleBinding),
|
||||||
|
(error) => error?.code === '23514',
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
if (executorDatabase) await executorDatabase.close();
|
||||||
|
await migrationDatabase.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -112,6 +112,7 @@ test('defines the immutable PostgreSQL capability and Run core stream', async ()
|
|||||||
'pg-0059-plugin-package-secret-bindings',
|
'pg-0059-plugin-package-secret-bindings',
|
||||||
'pg-0060-plugin-package-secret-materialization-guard',
|
'pg-0060-plugin-package-secret-materialization-guard',
|
||||||
'pg-0061-plugin-package-secret-binding-approval-plans',
|
'pg-0061-plugin-package-secret-binding-approval-plans',
|
||||||
|
'pg-0062-plugin-package-secret-binding-target-guard',
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
for (const migration of postgresqlMainMigrationStream.migrations) {
|
for (const migration of postgresqlMainMigrationStream.migrations) {
|
||||||
@@ -554,6 +555,11 @@ test('freezes every published PostgreSQL migration checksum', () => {
|
|||||||
checksum:
|
checksum:
|
||||||
'c995b7846ae8a57d3abb4b5523961e81aeba890e7405a030bcb505dfc6be3d25',
|
'c995b7846ae8a57d3abb4b5523961e81aeba890e7405a030bcb505dfc6be3d25',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'pg-0062-plugin-package-secret-binding-target-guard',
|
||||||
|
checksum:
|
||||||
|
'cd4f92d8702da6b92dd9ae5153b5180400b94442f56393692b6ec038f998596b',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
postgresqlMainMigrationStream.migrations.map(({ id, checksum }) => ({
|
postgresqlMainMigrationStream.migrations.map(({ id, checksum }) => ({
|
||||||
@@ -2083,3 +2089,41 @@ test('advances capability v60 with least-privilege Package Secret binding approv
|
|||||||
/migration_id = 'pg-0060-plugin-package-secret-materialization-guard'/,
|
/migration_id = 'pg-0060-plugin-package-secret-materialization-guard'/,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('advances capability v61 with a staged-target Secret binding database guard', async () => {
|
||||||
|
const migration = migrationById(
|
||||||
|
'pg-0062-plugin-package-secret-binding-target-guard',
|
||||||
|
);
|
||||||
|
const statements = [];
|
||||||
|
await migration.up({
|
||||||
|
async query(statement) {
|
||||||
|
statements.push(statement);
|
||||||
|
return { rows: [] };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const sql = statements.join('\n');
|
||||||
|
assert.match(
|
||||||
|
sql,
|
||||||
|
/CREATE FUNCTION "ql3"\."enforce_plugin_package_secret_binding_target"\(\)/,
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
sql,
|
||||||
|
/CREATE TRIGGER ql3_plugin_package_secret_binding_target_guard BEFORE INSERT/,
|
||||||
|
);
|
||||||
|
assert.match(sql, /install\.state = 'active'/);
|
||||||
|
assert.match(sql, /install\.state = 'staged'/);
|
||||||
|
assert.match(sql, /MAX\(history\.target_generation\)/);
|
||||||
|
assert.match(sql, /previous\.state = 'active'/);
|
||||||
|
assert.doesNotMatch(sql, /SECURITY DEFINER/);
|
||||||
|
assert.match(
|
||||||
|
sql,
|
||||||
|
/REVOKE ALL ON FUNCTION [^;]+ FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager, ql3_package_executor, ql3_worker_ingress/,
|
||||||
|
);
|
||||||
|
assert.match(sql, /contract_version = 61/);
|
||||||
|
assert.match(sql, /"plugin_package_secret_binding_transition":1/);
|
||||||
|
assert.match(sql, /contract_version = 60/);
|
||||||
|
assert.match(
|
||||||
|
sql,
|
||||||
|
/migration_id = 'pg-0061-plugin-package-secret-binding-approval-plans'/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
@@ -716,6 +716,19 @@ function queryable(overrides = {}) {
|
|||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (text.includes('FROM pg_trigger triggers')) {
|
||||||
|
return {
|
||||||
|
rows: contract.triggers
|
||||||
|
.filter(({ name }) => name !== overrides.missingTrigger)
|
||||||
|
.map((definition) => ({
|
||||||
|
triggerName: definition.name,
|
||||||
|
tableName: definition.tableName,
|
||||||
|
functionName: definition.functionName,
|
||||||
|
enabled:
|
||||||
|
definition.name === overrides.disabledTrigger ? 'D' : 'O',
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
if (text.includes('has_schema_privilege')) {
|
if (text.includes('has_schema_privilege')) {
|
||||||
return {
|
return {
|
||||||
rows: [
|
rows: [
|
||||||
@@ -763,7 +776,7 @@ test('accepts the exact PostgreSQL control schema and least-privilege runtime ro
|
|||||||
serverMajor: 16,
|
serverMajor: 16,
|
||||||
currentUser: 'ql3_runtime',
|
currentUser: 'ql3_runtime',
|
||||||
contractName: 'control-core',
|
contractName: 'control-core',
|
||||||
contractVersion: 60,
|
contractVersion: 61,
|
||||||
migrationIds: [
|
migrationIds: [
|
||||||
'pg-0001-schema-capability',
|
'pg-0001-schema-capability',
|
||||||
'pg-0002-run-core',
|
'pg-0002-run-core',
|
||||||
@@ -826,6 +839,7 @@ test('accepts the exact PostgreSQL control schema and least-privilege runtime ro
|
|||||||
'pg-0059-plugin-package-secret-bindings',
|
'pg-0059-plugin-package-secret-bindings',
|
||||||
'pg-0060-plugin-package-secret-materialization-guard',
|
'pg-0060-plugin-package-secret-materialization-guard',
|
||||||
'pg-0061-plugin-package-secret-binding-approval-plans',
|
'pg-0061-plugin-package-secret-binding-approval-plans',
|
||||||
|
'pg-0062-plugin-package-secret-binding-target-guard',
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -856,10 +870,10 @@ test('accepts the exact schema and isolated least-privilege admin role', async (
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
assert.equal(report.currentUser, 'ql3_admin');
|
assert.equal(report.currentUser, 'ql3_admin');
|
||||||
assert.equal(report.contractVersion, 60);
|
assert.equal(report.contractVersion, 61);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
report.migrationIds.at(-1),
|
report.migrationIds.at(-1),
|
||||||
'pg-0061-plugin-package-secret-binding-approval-plans',
|
'pg-0062-plugin-package-secret-binding-target-guard',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -872,10 +886,10 @@ test('accepts the isolated least-privilege automation manager role', async () =>
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
assert.equal(report.currentUser, 'ql3_automation_manager');
|
assert.equal(report.currentUser, 'ql3_automation_manager');
|
||||||
assert.equal(report.contractVersion, 60);
|
assert.equal(report.contractVersion, 61);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
report.migrationIds.at(-1),
|
report.migrationIds.at(-1),
|
||||||
'pg-0061-plugin-package-secret-binding-approval-plans',
|
'pg-0062-plugin-package-secret-binding-target-guard',
|
||||||
);
|
);
|
||||||
|
|
||||||
const widened = automationManagerPrivileges();
|
const widened = automationManagerPrivileges();
|
||||||
@@ -904,10 +918,10 @@ test('accepts the isolated least-privilege human Approval manager role', async (
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
assert.equal(report.currentUser, 'ql3_approval_manager');
|
assert.equal(report.currentUser, 'ql3_approval_manager');
|
||||||
assert.equal(report.contractVersion, 60);
|
assert.equal(report.contractVersion, 61);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
report.migrationIds.at(-1),
|
report.migrationIds.at(-1),
|
||||||
'pg-0061-plugin-package-secret-binding-approval-plans',
|
'pg-0062-plugin-package-secret-binding-target-guard',
|
||||||
);
|
);
|
||||||
|
|
||||||
const widened = approvalManagerPrivileges();
|
const widened = approvalManagerPrivileges();
|
||||||
@@ -938,10 +952,10 @@ test('accepts the isolated least-privilege Run manager role', async () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
assert.equal(report.currentUser, 'ql3_run_manager');
|
assert.equal(report.currentUser, 'ql3_run_manager');
|
||||||
assert.equal(report.contractVersion, 60);
|
assert.equal(report.contractVersion, 61);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
report.migrationIds.at(-1),
|
report.migrationIds.at(-1),
|
||||||
'pg-0061-plugin-package-secret-binding-approval-plans',
|
'pg-0062-plugin-package-secret-binding-target-guard',
|
||||||
);
|
);
|
||||||
|
|
||||||
const widened = runManagerPrivileges();
|
const widened = runManagerPrivileges();
|
||||||
@@ -1073,10 +1087,10 @@ test('accepts the exact schema and isolated Worker ingress role', async () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
assert.equal(report.currentUser, 'ql3_worker_ingress');
|
assert.equal(report.currentUser, 'ql3_worker_ingress');
|
||||||
assert.equal(report.contractVersion, 60);
|
assert.equal(report.contractVersion, 61);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
report.migrationIds.at(-1),
|
report.migrationIds.at(-1),
|
||||||
'pg-0061-plugin-package-secret-binding-approval-plans',
|
'pg-0062-plugin-package-secret-binding-target-guard',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1159,6 +1173,35 @@ test('rejects unknown ql3 objects and an over-privileged runtime role', async ()
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('fails closed when a reviewed Package Secret trigger is missing or disabled', async () => {
|
||||||
|
await assert.rejects(
|
||||||
|
assertPostgresSchemaReady(
|
||||||
|
queryable({
|
||||||
|
missingTrigger: 'ql3_plugin_package_secret_binding_target_guard',
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
(error) =>
|
||||||
|
error instanceof PostgresSchemaReadinessError &&
|
||||||
|
error.code === 'schema_contract_invalid' &&
|
||||||
|
error.facts.includes(
|
||||||
|
'missing-trigger:ql3_plugin_package_secret_binding_target_guard',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await assert.rejects(
|
||||||
|
assertPostgresSchemaReady(
|
||||||
|
queryable({
|
||||||
|
disabledTrigger: 'ql3_plugin_package_secret_materialization_guard',
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
(error) =>
|
||||||
|
error instanceof PostgresSchemaReadinessError &&
|
||||||
|
error.code === 'schema_contract_invalid' &&
|
||||||
|
error.facts.includes(
|
||||||
|
'trigger-contract:ql3_plugin_package_secret_materialization_guard',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('preserves database availability errors for the outer readiness layer', async () => {
|
test('preserves database availability errors for the outer readiness layer', async () => {
|
||||||
const unavailable = new Error('database unavailable');
|
const unavailable = new Error('database unavailable');
|
||||||
let calls = 0;
|
let calls = 0;
|
||||||
|
|||||||
@@ -363,9 +363,9 @@ function composeDockerHarness(
|
|||||||
'/opt/qinglong/node_modules/@qinglong/local-application/dist/cli.js',
|
'/opt/qinglong/node_modules/@qinglong/local-application/dist/cli.js',
|
||||||
],
|
],
|
||||||
Labels: {
|
Labels: {
|
||||||
'io.qinglong.local.sqlite-contract-min': '47',
|
'io.qinglong.local.sqlite-contract-min': '48',
|
||||||
'io.qinglong.local.sqlite-contract-max': '47',
|
'io.qinglong.local.sqlite-contract-max': '48',
|
||||||
'io.qinglong.local.sqlite-write-contract': '47',
|
'io.qinglong.local.sqlite-write-contract': '48',
|
||||||
'io.qinglong.local.application-config': '2',
|
'io.qinglong.local.application-config': '2',
|
||||||
'io.qinglong.local.compose-selection': '1',
|
'io.qinglong.local.compose-selection': '1',
|
||||||
'io.qinglong.ai': 'excluded',
|
'io.qinglong.ai': 'excluded',
|
||||||
@@ -975,9 +975,9 @@ test('preflights exact local image, Compose merge and SQLite capability', async
|
|||||||
'/opt/qinglong/node_modules/@qinglong/local-application/dist/cli.js',
|
'/opt/qinglong/node_modules/@qinglong/local-application/dist/cli.js',
|
||||||
],
|
],
|
||||||
Labels: {
|
Labels: {
|
||||||
'io.qinglong.local.sqlite-contract-min': '47',
|
'io.qinglong.local.sqlite-contract-min': '48',
|
||||||
'io.qinglong.local.sqlite-contract-max': '47',
|
'io.qinglong.local.sqlite-contract-max': '48',
|
||||||
'io.qinglong.local.sqlite-write-contract': '47',
|
'io.qinglong.local.sqlite-write-contract': '48',
|
||||||
'io.qinglong.local.application-config': '2',
|
'io.qinglong.local.application-config': '2',
|
||||||
'io.qinglong.local.compose-selection': '1',
|
'io.qinglong.local.compose-selection': '1',
|
||||||
'io.qinglong.ai': 'excluded',
|
'io.qinglong.ai': 'excluded',
|
||||||
@@ -1029,7 +1029,7 @@ test('preflights exact local image, Compose merge and SQLite capability', async
|
|||||||
assert.equal(result.status, 'ready');
|
assert.equal(result.status, 'ready');
|
||||||
assert.equal(result.generation, 1);
|
assert.equal(result.generation, 1);
|
||||||
assert.equal(result.profile, 'edge');
|
assert.equal(result.profile, 'edge');
|
||||||
assert.equal(result.sqlite.contractVersion, 47);
|
assert.equal(result.sqlite.contractVersion, 48);
|
||||||
assert.equal(result.image.architecture, 'arm64');
|
assert.equal(result.image.architecture, 'arm64');
|
||||||
assert.equal(calls.length, 2);
|
assert.equal(calls.length, 2);
|
||||||
assert.deepEqual(calls[0].slice(0, 2), ['image', 'inspect']);
|
assert.deepEqual(calls[0].slice(0, 2), ['image', 'inspect']);
|
||||||
@@ -1129,8 +1129,8 @@ test('applies one Compose generation and exactly replays its health receipt', as
|
|||||||
assert.equal(mode(receiptPath), 0o600);
|
assert.equal(mode(receiptPath), 0o600);
|
||||||
const receipt = JSON.parse(fs.readFileSync(receiptPath, 'utf8'));
|
const receipt = JSON.parse(fs.readFileSync(receiptPath, 'utf8'));
|
||||||
assert.deepEqual(receipt.sqlite, {
|
assert.deepEqual(receipt.sqlite, {
|
||||||
contractVersion: 47,
|
contractVersion: 48,
|
||||||
writeContractVersion: 47,
|
writeContractVersion: 48,
|
||||||
writeObservation: 'unchanged',
|
writeObservation: 'unchanged',
|
||||||
backup: null,
|
backup: null,
|
||||||
});
|
});
|
||||||
@@ -1427,8 +1427,8 @@ test('rolls a failed Compose candidate forward to a healthy prior digest', async
|
|||||||
`${command.request.rolloutId}.sqlite`,
|
`${command.request.rolloutId}.sqlite`,
|
||||||
);
|
);
|
||||||
assert.equal(mode(backupPath), 0o600);
|
assert.equal(mode(backupPath), 0o600);
|
||||||
assert.equal(receipt.sqlite.contractVersion, 47);
|
assert.equal(receipt.sqlite.contractVersion, 48);
|
||||||
assert.equal(receipt.sqlite.writeContractVersion, 47);
|
assert.equal(receipt.sqlite.writeContractVersion, 48);
|
||||||
assert.equal(receipt.sqlite.writeObservation, 'changed');
|
assert.equal(receipt.sqlite.writeObservation, 'changed');
|
||||||
assert.match(receipt.sqlite.backup.sha256, /^[0-9a-f]{64}$/);
|
assert.match(receipt.sqlite.backup.sha256, /^[0-9a-f]{64}$/);
|
||||||
assert.equal(receipt.sqlite.backup.bytes > 0, true);
|
assert.equal(receipt.sqlite.backup.bytes > 0, true);
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ test('inspects the exact fresh Profile schema without exposing its path', async
|
|||||||
assert.equal(result.status, 'ready');
|
assert.equal(result.status, 'ready');
|
||||||
assert.equal(result.profile, 'edge');
|
assert.equal(result.profile, 'edge');
|
||||||
assert.equal(result.storage.contractName, 'local-control-core');
|
assert.equal(result.storage.contractName, 'local-control-core');
|
||||||
assert.equal(result.storage.contractVersion, 47);
|
assert.equal(result.storage.contractVersion, 48);
|
||||||
assert.equal(result.storage.migrationCount, 94);
|
assert.equal(result.storage.migrationCount, 96);
|
||||||
assert.equal(result.storage.journalMode, 'delete');
|
assert.equal(result.storage.journalMode, 'delete');
|
||||||
assert.equal(JSON.stringify(result).includes(state.directory), false);
|
assert.equal(JSON.stringify(result).includes(state.directory), false);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -104,6 +104,8 @@ import { local0091PluginPackageSecretBindingsMigration } from '../migrations/009
|
|||||||
import { local0092CapabilityV46Migration } from '../migrations/0092-capability-v46';
|
import { local0092CapabilityV46Migration } from '../migrations/0092-capability-v46';
|
||||||
import { local0093PluginPackageSecretMaterializationGuardMigration } from '../migrations/0093-plugin-package-secret-materialization-guard';
|
import { local0093PluginPackageSecretMaterializationGuardMigration } from '../migrations/0093-plugin-package-secret-materialization-guard';
|
||||||
import { local0094CapabilityV47Migration } from '../migrations/0094-capability-v47';
|
import { local0094CapabilityV47Migration } from '../migrations/0094-capability-v47';
|
||||||
|
import { local0095PluginPackageSecretBindingTargetGuardMigration } from '../migrations/0095-plugin-package-secret-binding-target-guard';
|
||||||
|
import { local0096CapabilityV48Migration } from '../migrations/0096-capability-v48';
|
||||||
import type { LocalSqliteMigrationContext } from '../migrations/sqlMigration';
|
import type { LocalSqliteMigrationContext } from '../migrations/sqlMigration';
|
||||||
import {
|
import {
|
||||||
LOCAL_SQLITE_MIGRATION_STREAM_ID,
|
LOCAL_SQLITE_MIGRATION_STREAM_ID,
|
||||||
@@ -220,6 +222,8 @@ export const localSqliteMigrationDefinition: MigrationStreamDefinition<LocalSqli
|
|||||||
local0092CapabilityV46Migration,
|
local0092CapabilityV46Migration,
|
||||||
local0093PluginPackageSecretMaterializationGuardMigration,
|
local0093PluginPackageSecretMaterializationGuardMigration,
|
||||||
local0094CapabilityV47Migration,
|
local0094CapabilityV47Migration,
|
||||||
|
local0095PluginPackageSecretBindingTargetGuardMigration,
|
||||||
|
local0096CapabilityV48Migration,
|
||||||
]),
|
]),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -482,5 +482,15 @@ export const localSqliteMigrationManifest: MigrationStreamManifest =
|
|||||||
checksum:
|
checksum:
|
||||||
'6ecbbef0b9d9b3c738cc47a80868e0871c47916fb67e7c37f8620f9099de735e',
|
'6ecbbef0b9d9b3c738cc47a80868e0871c47916fb67e7c37f8620f9099de735e',
|
||||||
}),
|
}),
|
||||||
|
Object.freeze({
|
||||||
|
id: '0095-plugin-package-secret-binding-target-guard',
|
||||||
|
checksum:
|
||||||
|
'dc0f4051663fef5304f462896aab9a63a259d12933a2d33873cb7397097d4b4f',
|
||||||
|
}),
|
||||||
|
Object.freeze({
|
||||||
|
id: '0096-capability-v48',
|
||||||
|
checksum:
|
||||||
|
'07118f8e2f1e4f4aa7b9bb95ba9b70276f62de0e63d63cf5dcceacd3532853d9',
|
||||||
|
}),
|
||||||
]),
|
]),
|
||||||
});
|
});
|
||||||
|
|||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||||
|
import { LOCAL_PLUGIN_PACKAGE_SECRET_BINDING_TARGET_TRIGGER_SQL } from '../plugin-package/secret-binding/pluginPackageSecretBindingTargetSchemaContract';
|
||||||
|
|
||||||
|
export const local0095PluginPackageSecretBindingTargetGuardMigration =
|
||||||
|
defineLocalSqliteMigration({
|
||||||
|
id: '0095-plugin-package-secret-binding-target-guard',
|
||||||
|
statements: [LOCAL_PLUGIN_PACKAGE_SECRET_BINDING_TARGET_TRIGGER_SQL],
|
||||||
|
});
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { CAPABILITIES_V47 } from './0094-capability-v47';
|
||||||
|
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||||
|
|
||||||
|
export const CAPABILITIES_V48 = CAPABILITIES_V47.replace(
|
||||||
|
'"plugin_package_secret_binding":1,',
|
||||||
|
'"plugin_package_secret_binding":1,"plugin_package_secret_binding_transition":1,',
|
||||||
|
);
|
||||||
|
|
||||||
|
export const local0096CapabilityV48Migration = defineLocalSqliteMigration({
|
||||||
|
id: '0096-capability-v48',
|
||||||
|
statements: [
|
||||||
|
`UPDATE "QingLong3SchemaCapabilities" SET contract_version = 48, migration_id = '0095-plugin-package-secret-binding-target-guard', capabilities = '${CAPABILITIES_V48}', updated_at_ms = CAST(unixepoch('subsec') * 1000 AS INTEGER) WHERE contract_name = 'local-control-core' AND contract_version = 47 AND migration_id = '0093-plugin-package-secret-materialization-guard' AND capabilities = '${CAPABILITIES_V47}'`,
|
||||||
|
],
|
||||||
|
});
|
||||||
+53
@@ -0,0 +1,53 @@
|
|||||||
|
export const LOCAL_PLUGIN_PACKAGE_SECRET_BINDING_TARGET_TRIGGER_NAME =
|
||||||
|
'ql3_plugin_package_secret_binding_target_guard' as const;
|
||||||
|
|
||||||
|
export const LOCAL_PLUGIN_PACKAGE_SECRET_BINDING_TARGET_TRIGGER_SQL = `
|
||||||
|
CREATE TRIGGER ${LOCAL_PLUGIN_PACKAGE_SECRET_BINDING_TARGET_TRIGGER_NAME}
|
||||||
|
BEFORE INSERT ON "QingLong3PluginPackageSecretBindings"
|
||||||
|
FOR EACH ROW
|
||||||
|
BEGIN
|
||||||
|
SELECT CASE WHEN NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM "QingLong3PluginPackageInstallHeads" AS head
|
||||||
|
JOIN "QingLong3PluginPackageInstalls" AS install
|
||||||
|
ON install.installation_id = head.installation_id
|
||||||
|
AND install.project_id = head.project_id
|
||||||
|
AND install.package_name = head.package_name
|
||||||
|
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 json_extract(install.lock_json, '$.manifestDigest') =
|
||||||
|
NEW.manifest_digest
|
||||||
|
AND (
|
||||||
|
(
|
||||||
|
install.state = 'active' AND
|
||||||
|
install.active_lock_digest = install.lock_digest
|
||||||
|
) OR (
|
||||||
|
install.state = 'staged' AND
|
||||||
|
install.previous_active_lock_digest IS NOT NULL AND
|
||||||
|
install.active_lock_digest = install.previous_active_lock_digest AND
|
||||||
|
install.target_generation = (
|
||||||
|
SELECT MAX(history.target_generation)
|
||||||
|
FROM "QingLong3PluginPackageInstalls" AS history
|
||||||
|
WHERE history.project_id = install.project_id
|
||||||
|
AND history.package_name = install.package_name
|
||||||
|
) AND
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM "QingLong3PluginPackageInstalls" AS previous
|
||||||
|
WHERE previous.project_id = install.project_id
|
||||||
|
AND previous.package_name = install.package_name
|
||||||
|
AND previous.lock_digest = install.previous_active_lock_digest
|
||||||
|
AND previous.state = 'active'
|
||||||
|
AND previous.active_lock_digest = previous.lock_digest
|
||||||
|
AND previous.target_generation < install.target_generation
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) THEN RAISE(ABORT,
|
||||||
|
'Plugin Package Secret binding target is not current active or reviewed staged generation')
|
||||||
|
END;
|
||||||
|
END
|
||||||
|
`.trim();
|
||||||
@@ -220,10 +220,31 @@ export class LocalSqlitePluginPackageSecretBindingRepository
|
|||||||
AND install.project_id = ?
|
AND install.project_id = ?
|
||||||
AND install.package_name = ?
|
AND install.package_name = ?
|
||||||
AND install.lock_digest = ?
|
AND install.lock_digest = ?
|
||||||
AND install.active_lock_digest = ?
|
|
||||||
AND install.target_generation = ?
|
AND install.target_generation = ?
|
||||||
AND install.state = 'active'
|
|
||||||
AND json_extract(install.lock_json, '$.manifestDigest') = ?
|
AND json_extract(install.lock_json, '$.manifestDigest') = ?
|
||||||
|
AND (
|
||||||
|
(install.state = 'active' AND
|
||||||
|
install.active_lock_digest = install.lock_digest) OR
|
||||||
|
(install.state = 'staged' AND
|
||||||
|
install.previous_active_lock_digest IS NOT NULL AND
|
||||||
|
install.active_lock_digest = install.previous_active_lock_digest AND
|
||||||
|
install.target_generation = (
|
||||||
|
SELECT MAX(history.target_generation)
|
||||||
|
FROM "QingLong3PluginPackageInstalls" AS history
|
||||||
|
WHERE history.project_id = install.project_id
|
||||||
|
AND history.package_name = install.package_name
|
||||||
|
) AND
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM "QingLong3PluginPackageInstalls" AS previous
|
||||||
|
WHERE previous.project_id = install.project_id
|
||||||
|
AND previous.package_name = install.package_name
|
||||||
|
AND previous.lock_digest = install.previous_active_lock_digest
|
||||||
|
AND previous.state = 'active'
|
||||||
|
AND previous.active_lock_digest = previous.lock_digest
|
||||||
|
AND previous.target_generation < install.target_generation
|
||||||
|
))
|
||||||
|
)
|
||||||
ON CONFLICT (generation_digest) DO NOTHING`,
|
ON CONFLICT (generation_digest) DO NOTHING`,
|
||||||
)
|
)
|
||||||
.run(
|
.run(
|
||||||
@@ -243,14 +264,13 @@ export class LocalSqlitePluginPackageSecretBindingRepository
|
|||||||
binding.target.projectId,
|
binding.target.projectId,
|
||||||
binding.target.packageName,
|
binding.target.packageName,
|
||||||
binding.target.lockDigest,
|
binding.target.lockDigest,
|
||||||
binding.target.lockDigest,
|
|
||||||
binding.target.generation,
|
binding.target.generation,
|
||||||
binding.target.manifestDigest,
|
binding.target.manifestDigest,
|
||||||
);
|
);
|
||||||
const stored = this.findStored(binding.target.generationDigest);
|
const stored = this.findStored(binding.target.generationDigest);
|
||||||
if (!stored) {
|
if (!stored) {
|
||||||
throw new PluginPackageSecretBindingConflictError(
|
throw new PluginPackageSecretBindingConflictError(
|
||||||
'binding target is not the current active Package generation',
|
'binding target is not the current active or reviewed staged Package generation',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (JSON.stringify(stored) !== bindingJson) {
|
if (JSON.stringify(stored) !== bindingJson) {
|
||||||
|
|||||||
@@ -3,13 +3,22 @@ import type { DatabaseSync } from 'node:sqlite';
|
|||||||
import { localSqliteMigrationManifest } from '../migration/migrationManifest';
|
import { localSqliteMigrationManifest } from '../migration/migrationManifest';
|
||||||
import { LocalSqliteMigrationStreamStore } from '../migration/migrationStreamStore';
|
import { LocalSqliteMigrationStreamStore } from '../migration/migrationStreamStore';
|
||||||
import { LOCAL_PLUGIN_PACKAGE_SECRET_MATERIALIZATION_TRIGGER_SQL } from '../plugin-package/pluginPackageSecretMaterializationSchemaContract';
|
import { LOCAL_PLUGIN_PACKAGE_SECRET_MATERIALIZATION_TRIGGER_SQL } from '../plugin-package/pluginPackageSecretMaterializationSchemaContract';
|
||||||
|
import { LOCAL_PLUGIN_PACKAGE_SECRET_BINDING_TARGET_TRIGGER_SQL } from '../plugin-package/secret-binding/pluginPackageSecretBindingTargetSchemaContract';
|
||||||
import {
|
import {
|
||||||
LOCAL_STEP_RUN_REFERENCE_TRIGGERS,
|
LOCAL_STEP_RUN_REFERENCE_TRIGGERS,
|
||||||
normalizeLocalSqliteSchemaSql,
|
normalizeLocalSqliteSchemaSql,
|
||||||
} from '../run/stepRunSchemaContract';
|
} from '../run/stepRunSchemaContract';
|
||||||
|
|
||||||
export const LOCAL_SQLITE_CONTRACT_NAME = 'local-control-core';
|
export const LOCAL_SQLITE_CONTRACT_NAME = 'local-control-core';
|
||||||
export const LOCAL_SQLITE_CONTRACT_VERSION = 47;
|
export const LOCAL_SQLITE_CONTRACT_VERSION = 48;
|
||||||
|
|
||||||
|
const PLUGIN_PACKAGE_SECRET_BINDING_TARGET_TRIGGERS = Object.freeze([
|
||||||
|
Object.freeze({
|
||||||
|
name: 'ql3_plugin_package_secret_binding_target_guard',
|
||||||
|
tableName: 'QingLong3PluginPackageSecretBindings',
|
||||||
|
sql: LOCAL_PLUGIN_PACKAGE_SECRET_BINDING_TARGET_TRIGGER_SQL,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
const PLUGIN_PACKAGE_SECRET_MATERIALIZATION_TRIGGERS = Object.freeze([
|
const PLUGIN_PACKAGE_SECRET_MATERIALIZATION_TRIGGERS = Object.freeze([
|
||||||
Object.freeze({
|
Object.freeze({
|
||||||
@@ -1752,6 +1761,7 @@ function assertRequiredSchema(client: DatabaseSync): number {
|
|||||||
const expectedTriggers = [
|
const expectedTriggers = [
|
||||||
...LOCAL_STEP_RUN_REFERENCE_TRIGGERS,
|
...LOCAL_STEP_RUN_REFERENCE_TRIGGERS,
|
||||||
...PLUGIN_PACKAGE_AUTOMATION_DISPOSITION_TRIGGERS,
|
...PLUGIN_PACKAGE_AUTOMATION_DISPOSITION_TRIGGERS,
|
||||||
|
...PLUGIN_PACKAGE_SECRET_BINDING_TARGET_TRIGGERS,
|
||||||
...PLUGIN_PACKAGE_SECRET_MATERIALIZATION_TRIGGERS,
|
...PLUGIN_PACKAGE_SECRET_MATERIALIZATION_TRIGGERS,
|
||||||
].sort((left, right) => left.name.localeCompare(right.name));
|
].sort((left, right) => left.name.localeCompare(right.name));
|
||||||
if (
|
if (
|
||||||
@@ -2564,10 +2574,10 @@ export async function auditLocalSqliteReadiness(
|
|||||||
capability.contract_name !== LOCAL_SQLITE_CONTRACT_NAME ||
|
capability.contract_name !== LOCAL_SQLITE_CONTRACT_NAME ||
|
||||||
capability.contract_version !== LOCAL_SQLITE_CONTRACT_VERSION ||
|
capability.contract_version !== LOCAL_SQLITE_CONTRACT_VERSION ||
|
||||||
capability.migration_id !==
|
capability.migration_id !==
|
||||||
'0093-plugin-package-secret-materialization-guard' ||
|
'0095-plugin-package-secret-binding-target-guard' ||
|
||||||
typeof capability.capabilities !== 'string' ||
|
typeof capability.capabilities !== 'string' ||
|
||||||
capability.capabilities !==
|
capability.capabilities !==
|
||||||
'{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_project_administration":1,"local_security_audit":1,"local_security_audit_compaction":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_identity_credential_administration":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"local_scheduler_admission":1,"plugin_package_install":1,"approved_action":1,"plugin_package_admission":1,"approved_action_execution":1,"plugin_package_proposal":1,"plugin_package_materialized_revision":1,"plugin_package_secret_binding":1,"plugin_package_secret_materialization":1,"plugin_package_task_reconciliation":1,"project_tool_definition_snapshot":1,"step_run":1,"tool_execution_evidence":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_failure_completion":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"plugin_package_quarantine":1,"plugin_package_lifecycle":1,"plugin_package_automation_publication":1,"plugin_package_automation_security_withdrawal":1,"plugin_package_workflow_admission":1,"plugin_package_workflow_run_list":1,"run_attempt_log_retention":1,"plugin_package_workflow_task_attempt_admission":1}' ||
|
'{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_project_administration":1,"local_security_audit":1,"local_security_audit_compaction":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_identity_credential_administration":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"local_scheduler_admission":1,"plugin_package_install":1,"approved_action":1,"plugin_package_admission":1,"approved_action_execution":1,"plugin_package_proposal":1,"plugin_package_materialized_revision":1,"plugin_package_secret_binding":1,"plugin_package_secret_binding_transition":1,"plugin_package_secret_materialization":1,"plugin_package_task_reconciliation":1,"project_tool_definition_snapshot":1,"step_run":1,"tool_execution_evidence":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_failure_completion":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"plugin_package_quarantine":1,"plugin_package_lifecycle":1,"plugin_package_automation_publication":1,"plugin_package_automation_security_withdrawal":1,"plugin_package_workflow_admission":1,"plugin_package_workflow_run_list":1,"run_attempt_log_retention":1,"plugin_package_workflow_task_attempt_admission":1}' ||
|
||||||
typeof capability.updated_at_ms !== 'number' ||
|
typeof capability.updated_at_ms !== 'number' ||
|
||||||
!Number.isSafeInteger(capability.updated_at_ms) ||
|
!Number.isSafeInteger(capability.updated_at_ms) ||
|
||||||
capability.updated_at_ms < 0
|
capability.updated_at_ms < 0
|
||||||
|
|||||||
@@ -144,9 +144,11 @@ test('creates a reviewed edge database and opens runtime only after readiness',
|
|||||||
'0092-capability-v46',
|
'0092-capability-v46',
|
||||||
'0093-plugin-package-secret-materialization-guard',
|
'0093-plugin-package-secret-materialization-guard',
|
||||||
'0094-capability-v47',
|
'0094-capability-v47',
|
||||||
|
'0095-plugin-package-secret-binding-target-guard',
|
||||||
|
'0096-capability-v48',
|
||||||
]);
|
]);
|
||||||
assert.equal(migrated.readiness.contractName, 'local-control-core');
|
assert.equal(migrated.readiness.contractName, 'local-control-core');
|
||||||
assert.equal(migrated.readiness.contractVersion, 47);
|
assert.equal(migrated.readiness.contractVersion, 48);
|
||||||
assert.equal(migrated.readiness.journalMode, 'delete');
|
assert.equal(migrated.readiness.journalMode, 'delete');
|
||||||
assert.equal(fs.statSync(databasePath).mode & 0o777, 0o600);
|
assert.equal(fs.statSync(databasePath).mode & 0o777, 0o600);
|
||||||
|
|
||||||
@@ -592,8 +594,8 @@ test('backfills v14 execution revisions with a verified independent digest', asy
|
|||||||
.get(),
|
.get(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
contract_version: 47,
|
contract_version: 48,
|
||||||
migration_id: '0093-plugin-package-secret-materialization-guard',
|
migration_id: '0095-plugin-package-secret-binding-target-guard',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -80,11 +80,38 @@ function fixture(boundAtMs = 100) {
|
|||||||
return { binding, generation };
|
return { binding, generation };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function harness(active = true) {
|
async function harness(state = 'active') {
|
||||||
const client = new DatabaseSync(':memory:');
|
const client = new DatabaseSync(':memory:');
|
||||||
client.exec('PRAGMA foreign_keys = ON');
|
client.exec('PRAGMA foreign_keys = ON');
|
||||||
await migrateLocalSqliteDatabase(client);
|
await migrateLocalSqliteDatabase(client);
|
||||||
const { binding, generation } = fixture();
|
const isActive = state === 'active';
|
||||||
|
const isStaged = state === 'staged';
|
||||||
|
const source = fixture();
|
||||||
|
const previousLockDigest = 'f'.repeat(64);
|
||||||
|
const generation = isStaged
|
||||||
|
? createPluginPackageResourceGeneration({
|
||||||
|
installationId: 'install-2',
|
||||||
|
projectId: 'project-1',
|
||||||
|
packageName: 'example-monitor',
|
||||||
|
lockDigest: LOCK_DIGEST,
|
||||||
|
generation: 2,
|
||||||
|
previousActiveLockDigest: previousLockDigest,
|
||||||
|
contentDigest: 'b'.repeat(64),
|
||||||
|
contents: MANIFEST.spec.contents,
|
||||||
|
})
|
||||||
|
: source.generation;
|
||||||
|
const binding = isStaged
|
||||||
|
? createPluginPackageSecretBinding({
|
||||||
|
generation,
|
||||||
|
manifest: MANIFEST,
|
||||||
|
assignments: source.binding.entries.map(({ name, secretRef }) => ({
|
||||||
|
name,
|
||||||
|
secretRef,
|
||||||
|
})),
|
||||||
|
authority: source.binding.authority,
|
||||||
|
boundAtMs: source.binding.boundAtMs,
|
||||||
|
})
|
||||||
|
: source.binding;
|
||||||
client
|
client
|
||||||
.prepare(
|
.prepare(
|
||||||
`INSERT INTO "QingLong3Projects"
|
`INSERT INTO "QingLong3Projects"
|
||||||
@@ -100,11 +127,11 @@ async function harness(active = true) {
|
|||||||
manifestDigest: binding.target.manifestDigest,
|
manifestDigest: binding.target.manifestDigest,
|
||||||
});
|
});
|
||||||
const recordJson = JSON.stringify({
|
const recordJson = JSON.stringify({
|
||||||
installationId: 'install-1',
|
installationId: isStaged ? 'install-2' : 'install-1',
|
||||||
projectId: 'project-1',
|
projectId: 'project-1',
|
||||||
packageName: 'example-monitor',
|
packageName: 'example-monitor',
|
||||||
lockDigest: LOCK_DIGEST,
|
lockDigest: LOCK_DIGEST,
|
||||||
state: active ? 'active' : 'failed',
|
state,
|
||||||
version: 1,
|
version: 1,
|
||||||
recordDigest,
|
recordDigest,
|
||||||
});
|
});
|
||||||
@@ -116,16 +143,18 @@ async function harness(active = true) {
|
|||||||
previous_active_lock_digest, active_lock_digest, state, version,
|
previous_active_lock_digest, active_lock_digest, state, version,
|
||||||
last_mutation_id, last_mutation_digest, lock_json, record_json,
|
last_mutation_id, last_mutation_digest, lock_json, record_json,
|
||||||
record_digest, created_at_ms, updated_at_ms
|
record_digest, created_at_ms, updated_at_ms
|
||||||
) VALUES (?, ?, ?, '1.0.0', 'install', ?, 1, NULL, ?, ?, 1,
|
) VALUES (?, ?, ?, '1.0.0', 'install', ?, ?, ?, ?, ?, 1,
|
||||||
'mutation-1', ?, ?, ?, ?, 1, 1)`,
|
'mutation-1', ?, ?, ?, ?, 1, 1)`,
|
||||||
)
|
)
|
||||||
.run(
|
.run(
|
||||||
'install-1',
|
isStaged ? 'install-2' : 'install-1',
|
||||||
'project-1',
|
'project-1',
|
||||||
'example-monitor',
|
'example-monitor',
|
||||||
LOCK_DIGEST,
|
LOCK_DIGEST,
|
||||||
active ? LOCK_DIGEST : null,
|
generation.generation,
|
||||||
active ? 'active' : 'failed',
|
isStaged ? previousLockDigest : null,
|
||||||
|
isActive ? LOCK_DIGEST : isStaged ? previousLockDigest : null,
|
||||||
|
state,
|
||||||
'e'.repeat(64),
|
'e'.repeat(64),
|
||||||
lockJson,
|
lockJson,
|
||||||
recordJson,
|
recordJson,
|
||||||
@@ -135,9 +164,47 @@ async function harness(active = true) {
|
|||||||
.prepare(
|
.prepare(
|
||||||
`INSERT INTO "QingLong3PluginPackageInstallHeads"
|
`INSERT INTO "QingLong3PluginPackageInstallHeads"
|
||||||
(project_id, package_name, installation_id)
|
(project_id, package_name, installation_id)
|
||||||
VALUES ('project-1', 'example-monitor', 'install-1')`,
|
VALUES ('project-1', 'example-monitor', ?)`,
|
||||||
)
|
)
|
||||||
.run();
|
.run(isStaged ? 'install-2' : 'install-1');
|
||||||
|
if (isStaged) {
|
||||||
|
const previousRecordDigest = '9'.repeat(64);
|
||||||
|
const previousLockJson = JSON.stringify({
|
||||||
|
lockDigest: previousLockDigest,
|
||||||
|
projectId: 'project-1',
|
||||||
|
packageName: 'example-monitor',
|
||||||
|
manifestDigest: '8'.repeat(64),
|
||||||
|
});
|
||||||
|
const previousRecordJson = JSON.stringify({
|
||||||
|
installationId: 'install-1',
|
||||||
|
projectId: 'project-1',
|
||||||
|
packageName: 'example-monitor',
|
||||||
|
lockDigest: previousLockDigest,
|
||||||
|
state: 'active',
|
||||||
|
version: 1,
|
||||||
|
recordDigest: previousRecordDigest,
|
||||||
|
});
|
||||||
|
client
|
||||||
|
.prepare(
|
||||||
|
`INSERT INTO "QingLong3PluginPackageInstalls" (
|
||||||
|
installation_id, project_id, package_name, package_version,
|
||||||
|
operation, lock_digest, target_generation,
|
||||||
|
previous_active_lock_digest, active_lock_digest, state, version,
|
||||||
|
last_mutation_id, last_mutation_digest, lock_json, record_json,
|
||||||
|
record_digest, created_at_ms, updated_at_ms
|
||||||
|
) VALUES ('install-1', 'project-1', 'example-monitor', '0.9.0',
|
||||||
|
'install', ?, 1, NULL, ?, 'active', 1,
|
||||||
|
'mutation-previous', ?, ?, ?, ?, 0, 0)`,
|
||||||
|
)
|
||||||
|
.run(
|
||||||
|
previousLockDigest,
|
||||||
|
previousLockDigest,
|
||||||
|
'7'.repeat(64),
|
||||||
|
previousLockJson,
|
||||||
|
previousRecordJson,
|
||||||
|
previousRecordDigest,
|
||||||
|
);
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
client,
|
client,
|
||||||
binding,
|
binding,
|
||||||
@@ -161,7 +228,7 @@ test('publishes and exact-replays one active generation binding', async (t) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('rejects inactive targets and conflicting content', async (t) => {
|
test('rejects inactive targets and conflicting content', async (t) => {
|
||||||
const inactive = await harness(false);
|
const inactive = await harness('failed');
|
||||||
t.after(() => inactive.client.close());
|
t.after(() => inactive.client.close());
|
||||||
await assert.rejects(
|
await assert.rejects(
|
||||||
inactive.repository.publish(inactive.binding),
|
inactive.repository.publish(inactive.binding),
|
||||||
@@ -177,6 +244,21 @@ test('rejects inactive targets and conflicting content', async (t) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('publishes a reviewed current staged generation but rejects post-stage states', async (t) => {
|
||||||
|
const staged = await harness('staged');
|
||||||
|
t.after(() => staged.client.close());
|
||||||
|
assert.equal((await staged.repository.publish(staged.binding)).status, 'created');
|
||||||
|
|
||||||
|
for (const state of ['queued', 'activating']) {
|
||||||
|
const rejected = await harness(state);
|
||||||
|
t.after(() => rejected.client.close());
|
||||||
|
await assert.rejects(
|
||||||
|
rejected.repository.publish(rejected.binding),
|
||||||
|
PluginPackageSecretBindingConflictError,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('fails closed when durable binding JSON is changed in place', async (t) => {
|
test('fails closed when durable binding JSON is changed in place', async (t) => {
|
||||||
const value = await harness();
|
const value = await harness();
|
||||||
t.after(() => value.client.close());
|
t.after(() => value.client.close());
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ test('atomically admits one generation-bound Workflow Run and exactly replays it
|
|||||||
},
|
},
|
||||||
{ runs: 1, steps: 2, events: 3, mutations: 2, admissions: 1 },
|
{ runs: 1, steps: 2, events: 3, mutations: 2, admissions: 1 },
|
||||||
);
|
);
|
||||||
assert.equal((await auditLocalSqliteReadiness(client)).contractVersion, 47);
|
assert.equal((await auditLocalSqliteReadiness(client)).contractVersion, 48);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('runs an optional authorization guard inside new and replay transactions', async (t) => {
|
test('runs an optional authorization guard inside new and replay transactions', async (t) => {
|
||||||
@@ -288,7 +288,7 @@ test('exactly replays immutable admission after the Workflow StepRun advances',
|
|||||||
},
|
},
|
||||||
{ status: 'running', version: 5, eventSequence: 5 },
|
{ status: 'running', version: 5, eventSequence: 5 },
|
||||||
);
|
);
|
||||||
assert.equal((await auditLocalSqliteReadiness(client)).contractVersion, 47);
|
assert.equal((await auditLocalSqliteReadiness(client)).contractVersion, 48);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('fails closed before writing when the exact installation is not active', async (t) => {
|
test('fails closed before writing when the exact installation is not active', async (t) => {
|
||||||
|
|||||||
+1
-1
@@ -231,7 +231,7 @@ test('atomically admits the exact reconciled local Task revision and replays it'
|
|||||||
stepAttemptCount: 0,
|
stepAttemptCount: 0,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
assert.equal((await auditLocalSqliteReadiness(client)).contractVersion, 47);
|
assert.equal((await auditLocalSqliteReadiness(client)).contractVersion, 48);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('bounds candidate paging before SQL and fences cancellation', async (t) => {
|
test('bounds candidate paging before SQL and fences cancellation', async (t) => {
|
||||||
|
|||||||
@@ -40,9 +40,9 @@ test('creates and exactly replays a reviewed rollout backup', async (t) => {
|
|||||||
await migrateLocalSqlitePath(state);
|
await migrateLocalSqlitePath(state);
|
||||||
const prepared = await createLocalSqliteRolloutBackup(state);
|
const prepared = await createLocalSqliteRolloutBackup(state);
|
||||||
assert.equal(prepared.status, 'prepared');
|
assert.equal(prepared.status, 'prepared');
|
||||||
assert.equal(prepared.contractVersion, 47);
|
assert.equal(prepared.contractVersion, 48);
|
||||||
assert.equal(prepared.writeContractVersion, 47);
|
assert.equal(prepared.writeContractVersion, 48);
|
||||||
assert.equal(LOCAL_SQLITE_WRITE_CONTRACT_VERSION, 47);
|
assert.equal(LOCAL_SQLITE_WRITE_CONTRACT_VERSION, 48);
|
||||||
assert.match(prepared.sha256, /^[0-9a-f]{64}$/);
|
assert.match(prepared.sha256, /^[0-9a-f]{64}$/);
|
||||||
assert.equal(prepared.bytes > 0, true);
|
assert.equal(prepared.bytes > 0, true);
|
||||||
assert.equal(prepared.pageCount > 0, true);
|
assert.equal(prepared.pageCount > 0, true);
|
||||||
|
|||||||
@@ -74,12 +74,12 @@ test('current QL3 workspace has exactly eighteen reviewed package boundaries', (
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
directory: 'packages/ql3-cluster-postgres/src/migrations',
|
directory: 'packages/ql3-cluster-postgres/src/migrations',
|
||||||
directSourceFiles: 62,
|
directSourceFiles: 63,
|
||||||
reviewKind: 'ordered_ledger',
|
reviewKind: 'ordered_ledger',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
directory: 'packages/ql3-local-sqlite/src/migrations',
|
directory: 'packages/ql3-local-sqlite/src/migrations',
|
||||||
directSourceFiles: 95,
|
directSourceFiles: 97,
|
||||||
reviewKind: 'ordered_ledger',
|
reviewKind: 'ordered_ledger',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -299,10 +299,10 @@ test('current QL3 workspace has exactly eighteen reviewed package boundaries', (
|
|||||||
rootSourceFileRoles: runtimeCore.rootSourceFileRoles,
|
rootSourceFileRoles: runtimeCore.rootSourceFileRoles,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
sourceFiles: 156,
|
sourceFiles: 157,
|
||||||
rootSourceFiles: 1,
|
rootSourceFiles: 1,
|
||||||
rootSourceLines: 160,
|
rootSourceLines: 160,
|
||||||
nestedSourceFiles: 155,
|
nestedSourceFiles: 156,
|
||||||
rootSourceFileRoles: { 'index.ts': 'public_export' },
|
rootSourceFileRoles: { 'index.ts': 'public_export' },
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -421,10 +421,10 @@ test('current QL3 workspace has exactly eighteen reviewed package boundaries', (
|
|||||||
rootSourceFileRoles: clusterPostgres.rootSourceFileRoles,
|
rootSourceFileRoles: clusterPostgres.rootSourceFileRoles,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
sourceFiles: 159,
|
sourceFiles: 160,
|
||||||
rootSourceFiles: 1,
|
rootSourceFiles: 1,
|
||||||
rootSourceLines: 126,
|
rootSourceLines: 126,
|
||||||
nestedSourceFiles: 158,
|
nestedSourceFiles: 159,
|
||||||
rootSourceFileRoles: { 'index.ts': 'public_export' },
|
rootSourceFileRoles: { 'index.ts': 'public_export' },
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -540,10 +540,10 @@ test('current QL3 workspace has exactly eighteen reviewed package boundaries', (
|
|||||||
rootSourceFileRoles: localSqlite.rootSourceFileRoles,
|
rootSourceFileRoles: localSqlite.rootSourceFileRoles,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
sourceFiles: 188,
|
sourceFiles: 191,
|
||||||
rootSourceFiles: 1,
|
rootSourceFiles: 1,
|
||||||
rootSourceLines: 31,
|
rootSourceLines: 31,
|
||||||
nestedSourceFiles: 187,
|
nestedSourceFiles: 190,
|
||||||
rootSourceFileRoles: { 'index.ts': 'public_export' },
|
rootSourceFileRoles: { 'index.ts': 'public_export' },
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user