mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): materialize package secret bindings
This commit is contained in:
@@ -27,6 +27,7 @@
|
||||
|
||||
- D-304/ADR-0392(已接受):Plugin Package 进入安全 quarantine 时,Workflow/Prompt automation publication 不再仅依赖运行时 start guard 间接拒绝,而是与 quarantine event、Package-owned Task disabled revisions、Project Tool snapshot 和 withdrawal receipt 在同一 SQLite/PostgreSQL 事务中收敛为 `withdrawn`。原先仅能引用普通 lifecycle event 的外键升级为 append-only disposition-event 联合引用,历史 migration 与 publication digest schema 保持不变;SQLite edge/standalone 崩溃矩阵覆盖 automation publication insert 后、event/task/receipt/COMMIT 前后,PostgreSQL 通过触发器在既有 `SECURITY DEFINER` quarantine commit 内登记 disposition,再由同一外层 SERIALIZABLE transaction CAS publication head。能力位为 `plugin_package_automation_security_withdrawal@1`;不新增 package、daemon、timer、连接或常驻缓存,适用于低配路由设备和集群节点。SQLite 全量 228/228;PostgreSQL package 311 pass/1 条外部 URL 条件 skip;完整 18-package build/test 退出 0,backend 1,188 pass/2 skip,package/dependency boundary 零 finding;PostgreSQL 18.4 arm64 HA 125 项 gate 全绿、timeline `1→2`,报告 SHA-256 为 `ab156901b9c96ec5a62259c44d83d24ded011e0616dc827d928f3e13efd11786`。
|
||||
- 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-302/ADR-0390(已接受)
|
||||
Cluster operator context 增加无网络、无 mutation 的内建 `ql3-cluster-admin context validate` 预检。它先复用 owner-private context
|
||||
reader,再让每个 entry 经过与真实请求相同的 production HTTPS/Kubernetes configuration preparation,验证精确 route、hostname、CA、
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# ADR-0394:按 Generation 固定的 Plugin Package Secret Materialization
|
||||
|
||||
- 状态:Accepted
|
||||
- 日期:2026-08-13
|
||||
- 关联 RFC:QL-RFC-0001 D-306A
|
||||
- 关联 ADR:ADR-0151、ADR-0153、ADR-0393
|
||||
|
||||
## 问题
|
||||
|
||||
D-305 已把 Package 的逻辑 Secret requirement 固定为不可变、按 generation 绑定的版本化 `qlsecret://` 引用,但 Task source 仍不能消费该绑定。若在 dispatch 时才按 requirement 名称查找,会使 materialized revision 与实际 Secret 版本脱节;若允许 Package source 直接携带部署方 SecretRef,又会绕过安装方授权。
|
||||
|
||||
Local/Edge 还要求只读 readiness 不加载 migration 执行模块,且不能因 Secret-aware Package 引入 watcher、缓存、连接或常驻进程。Cluster 必须在直接数据库写入时也拒绝未绑定或越权的 SecretRef。
|
||||
|
||||
## 决策
|
||||
|
||||
1. Package Task source 以 `{kind:"package-secret", requirement:"NAME"}` 引用 Manifest 的逻辑 requirement,不允许直接写 `kind:"secret"` 或 `qlsecret://`。
|
||||
2. materialization 读取当前 generation 的 D-305 binding,把逻辑 requirement 编译为已有、固定版本的 Task `SecretRef`;optional 且显式绑定为 `null` 的环境项被省略。普通 Task dispatch、Local 短时解密和 Cluster offer/lease-fenced delivery 继续复用现有执行链,不增加新的 Secret 传输协议。
|
||||
3. materialized revision 嵌入完整 binding 快照但不含明文,binding 与最终资源共同进入 revision digest。任何 Task SecretRef 必须属于该快照;Manifest 声明 Secret 时缺失 binding 或 `secret.use` capability 均失败关闭。
|
||||
4. SQLite 与 PostgreSQL 在 materialized revision INSERT 边界增加触发器,拒绝畸形 Secret 声明、未解析 placeholder、缺失/不匹配 binding 和 binding 外 SecretRef,防止绕过 core repository 直接写库。
|
||||
5. SQLite 的触发器 SQL 真源位于非执行型 schema contract;migration 与 readiness 共同引用,保持只读 readiness 和 rollout-safety 不加载 DDL 模块。Local contract 升至 v47,Cluster 升至 v59。
|
||||
6. 实现复用既有 SQLite authority、PostgreSQL pool、Task Secret resolution 与 Worker delivery,不增加 package、表、索引、连接、timer、watcher、cache、Pod 或 daemon。Local 仅增加一个 INSERT trigger,Cluster 增加一个 trigger function 与一个 INSERT trigger。
|
||||
|
||||
## 生命周期与阶段边界
|
||||
|
||||
- binding 与 materialized revision 均按 generation 不可变;Secret 新版本不会静默改变既有 generation。
|
||||
- Package disable/quarantine/uninstall 继续由现有 generation/lifecycle start guard 阻止新执行;已开始执行仍遵循既有 Run/Worker 清理语义。
|
||||
- D-306A 完成 runtime consumption 和数据库防绕过,不宣称产品授权闭环完成。面向用户的 Local bind/rebind 命令、Cluster Approved Action/API,以及通过新 generation 执行 rotation/rebinding/revocation 的编排属于 D-306B。
|
||||
|
||||
## 资源与部署影响
|
||||
|
||||
| Profile | 增量 | 常驻开销 |
|
||||
| --- | --- | --- |
|
||||
| Edge / Standalone | 一个 INSERT trigger;启动发布按需点查一个既有 binding row | 无新增进程、连接、缓存、timer 或 watcher |
|
||||
| Cluster | 一个非 `SECURITY DEFINER` trigger function 和一个 INSERT trigger;复用 executor pool | 无新增角色、连接、workload、sidecar 或 worker |
|
||||
|
||||
## 验证
|
||||
|
||||
- core:placeholder 编译、optional-null、省略、直接 SecretRef/缺失 binding/越权引用拒绝、revision digest 与 normalize round-trip。
|
||||
- SQLite:真实 migration、畸形声明与未绑定 SecretRef 直接 INSERT 拒绝、readiness DDL lazy boundary、rollout safety、edge/standalone contract。
|
||||
- PostgreSQL:migration checksum、trigger/function、固定 search path、权限撤回、schema/readiness 与 physical-streaming HA gate。
|
||||
- 完整 18-package clean build/test 退出 0;backend 共 1,190 项,1,188 pass、2 条件 skip、0 fail。
|
||||
- package boundary、cluster dependency、edge import、service-manager bridge import、local image 五项审计零 finding;workspace 保持 18 package、1,090 source、1,072 nested source,`singleSourcePackages=[]`、`shallowSourcePackages=[]`。两个经审查的 `ordered_ledger` 目录精确为 PostgreSQL 61 与 SQLite 95 个 migration source,通用密集目录门禁未放宽。
|
||||
- PostgreSQL 18.4 arm64 HA Docker gate 共 125 项全绿,timeline `1→2`;私有报告 SHA-256 为 `f9107e8e54892a788779758f0573ac8d6a80f6d086516a1f5f5bbacb59bbb4be`。
|
||||
@@ -397,6 +397,7 @@
|
||||
| [ADR-0391](./ADR-0391-read-only-cluster-operator-context-readiness.md) | Read-only Cluster Operator Context Readiness | Accepted |
|
||||
| [ADR-0392](./ADR-0392-atomic-plugin-package-automation-security-withdrawal.md) | Plugin Package Automation 安全隔离原子撤回 | Accepted |
|
||||
| [ADR-0393](./ADR-0393-generation-bound-plugin-package-secret-binding-ledger.md) | 按 Generation 固定的 Plugin Package Secret 绑定账本 | Accepted |
|
||||
| [ADR-0394](./ADR-0394-generation-bound-plugin-package-secret-materialization.md) | 按 Generation 固定的 Plugin Package Secret Materialization | Accepted |
|
||||
|
||||
## 规则
|
||||
|
||||
|
||||
@@ -6,13 +6,13 @@
|
||||
"reviewedDenseDirectories": [
|
||||
{
|
||||
"kind": "ordered_ledger",
|
||||
"maxDirectSourceFiles": 60,
|
||||
"maxDirectSourceFiles": 61,
|
||||
"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."
|
||||
},
|
||||
{
|
||||
"kind": "ordered_ledger",
|
||||
"maxDirectSourceFiles": 93,
|
||||
"maxDirectSourceFiles": 95,
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
assertPostgresPackageExecutorSchemaReady,
|
||||
PostgresPluginPackageAutomationPublicationRepository,
|
||||
PostgresPluginPackageMaterializedRevisionRepository,
|
||||
PostgresPluginPackageSecretBindingRepository,
|
||||
PostgresPluginPackagePublisherProvenanceRepository,
|
||||
PostgresPluginPackageTaskReconciliationRepository,
|
||||
PostgresProjectToolDefinitionSnapshotRepository,
|
||||
@@ -102,8 +103,7 @@ export class ClusterPluginPackagePublisherProvenanceRecoveryRequiredError extend
|
||||
readonly recovery: Readonly<ClusterPluginPackagePublisherProvenanceRecoveryResult>,
|
||||
) {
|
||||
super('Cluster has unresolved Plugin Package publisher provenance work');
|
||||
this.name =
|
||||
'ClusterPluginPackagePublisherProvenanceRecoveryRequiredError';
|
||||
this.name = 'ClusterPluginPackagePublisherProvenanceRecoveryRequiredError';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,9 +166,7 @@ function assertOptions(options: ClusterPluginPackageRecoveryOptions): void {
|
||||
(!options.resourceByteSource ||
|
||||
typeof options.resourceByteSource.open !== 'function')) ||
|
||||
typeof options.trustAuthorityId !== 'string' ||
|
||||
!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(
|
||||
options.trustAuthorityId,
|
||||
) ||
|
||||
!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(options.trustAuthorityId) ||
|
||||
typeof options.now !== 'function' ||
|
||||
(options.pageSize !== undefined &&
|
||||
(!Number.isSafeInteger(options.pageSize) ||
|
||||
@@ -313,8 +311,7 @@ export async function recoverClusterPluginPackages(
|
||||
const resourceByteSource =
|
||||
options.resourceByteSource ??
|
||||
new ClusterPluginPackageOciResourceByteSource({
|
||||
authority:
|
||||
stageAuthority as ClusterPluginPackageOciStageAuthority,
|
||||
authority: stageAuthority as ClusterPluginPackageOciStageAuthority,
|
||||
lockSource: repository,
|
||||
});
|
||||
const recovery = await new PluginPackageRecoveryCoordinator({
|
||||
@@ -340,6 +337,8 @@ export async function recoverClusterPluginPackages(
|
||||
database.pool,
|
||||
taskSpecSemanticRegistry,
|
||||
);
|
||||
const secretBindingRepository =
|
||||
new PostgresPluginPackageSecretBindingRepository(database.pool);
|
||||
const taskPublicationRecovery =
|
||||
await new PluginPackageTaskPublicationRecoveryCoordinator({
|
||||
source: taskReconciliationRepository,
|
||||
@@ -348,6 +347,7 @@ export async function recoverClusterPluginPackages(
|
||||
lockSource: repository,
|
||||
byteSource: resourceByteSource,
|
||||
materializedRepository,
|
||||
secretBindingSource: secretBindingRepository,
|
||||
reconciliationRepository: taskReconciliationRepository,
|
||||
taskSpecSemanticRegistry,
|
||||
}),
|
||||
|
||||
@@ -160,6 +160,7 @@ function database(serverVersionNum = '160014') {
|
||||
'commit_plugin_package_lifecycle',
|
||||
'commit_plugin_package_task_reconciliation',
|
||||
'commit_plugin_package_quarantine',
|
||||
'enforce_plugin_package_secret_materialization',
|
||||
'enforce_plugin_package_stage_provenance',
|
||||
'lock_active_plugin_package_project',
|
||||
'lock_approval_policy_fence',
|
||||
|
||||
@@ -192,6 +192,7 @@ function database(serverVersionNum = '160014') {
|
||||
rows: contract.functions.map(({ name: functionName }) => ({
|
||||
functionName,
|
||||
executeAllowed: ![
|
||||
'enforce_plugin_package_secret_materialization',
|
||||
'enforce_plugin_package_stage_provenance',
|
||||
'plugin_package_automation_start_allowed',
|
||||
'plugin_package_run_start_allowed',
|
||||
|
||||
@@ -303,5 +303,10 @@ export const postgresqlMainMigrationManifest: MigrationStreamManifest =
|
||||
checksum:
|
||||
'87582d256c868bd7f5af352c4b052fdab9f3714e1e7179e35d33bfa5d62957be',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0060-plugin-package-secret-materialization-guard',
|
||||
checksum:
|
||||
'28284ca860b39ff9de5b2aa1a2a60ef2c463fd6a72798d237040174272b64b1e',
|
||||
}),
|
||||
]),
|
||||
});
|
||||
|
||||
@@ -62,6 +62,7 @@ import { pg0056RunManagementBoundaryMigration } from '../run-management/pg-0056-
|
||||
import { pg0057RunManagementStopBoundaryMigration } from '../run-management/pg-0057-run-management-stop-boundary';
|
||||
import { pg0058PluginPackageAutomationDispositionEventsMigration } from './pg-0058-plugin-package-automation-disposition-events';
|
||||
import { pg0059PluginPackageSecretBindingsMigration } from './pg-0059-plugin-package-secret-bindings';
|
||||
import { pg0060PluginPackageSecretMaterializationGuardMigration } from './pg-0060-plugin-package-secret-materialization-guard';
|
||||
|
||||
export const postgresqlMainMigrationStream: MigrationStreamDefinition<PostgresMigrationContext> =
|
||||
Object.freeze({
|
||||
@@ -129,5 +130,6 @@ export const postgresqlMainMigrationStream: MigrationStreamDefinition<PostgresMi
|
||||
pg0057RunManagementStopBoundaryMigration,
|
||||
pg0058PluginPackageAutomationDispositionEventsMigration,
|
||||
pg0059PluginPackageSecretBindingsMigration,
|
||||
pg0060PluginPackageSecretMaterializationGuardMigration,
|
||||
]),
|
||||
});
|
||||
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
import { CAPABILITIES_V58 } from './pg-0059-plugin-package-secret-bindings';
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
export const CAPABILITIES_V59 = CAPABILITIES_V58.replace(
|
||||
'"plugin_package_secret_binding":1,',
|
||||
'"plugin_package_secret_binding":1,"plugin_package_secret_materialization":1,',
|
||||
);
|
||||
|
||||
export const pg0060PluginPackageSecretMaterializationGuardMigration =
|
||||
definePostgresSqlMigration({
|
||||
id: 'pg-0060-plugin-package-secret-materialization-guard',
|
||||
statements: [
|
||||
`
|
||||
CREATE FUNCTION "ql3"."enforce_plugin_package_secret_materialization"()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
VOLATILE
|
||||
SET search_path = pg_catalog, ql3
|
||||
AS $ql3$
|
||||
DECLARE
|
||||
secret_count integer;
|
||||
embedded_binding jsonb;
|
||||
stored_binding jsonb;
|
||||
BEGIN
|
||||
IF jsonb_typeof(
|
||||
NEW.revision_json #> '{manifest,spec,permissions,secrets}'
|
||||
) IS DISTINCT FROM 'array' THEN
|
||||
RAISE EXCEPTION 'Package Secret permission declarations are malformed'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
|
||||
secret_count := jsonb_array_length(
|
||||
NEW.revision_json #> '{manifest,spec,permissions,secrets}'
|
||||
);
|
||||
embedded_binding := NEW.revision_json -> 'secretBinding';
|
||||
|
||||
IF secret_count = 0 THEN
|
||||
IF embedded_binding IS NOT NULL THEN
|
||||
RAISE EXCEPTION 'unexpected Package Secret binding'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
ELSE
|
||||
SELECT binding.binding_json
|
||||
INTO stored_binding
|
||||
FROM "ql3"."plugin_package_secret_bindings" AS binding
|
||||
WHERE binding.generation_digest = NEW.generation_digest;
|
||||
IF stored_binding IS NULL OR stored_binding <> embedded_binding THEN
|
||||
RAISE EXCEPTION 'Package Secret binding is absent or mismatched'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM jsonb_array_elements(NEW.revision_json -> 'resources') AS resource
|
||||
CROSS JOIN LATERAL jsonb_array_elements(
|
||||
COALESCE(
|
||||
resource #> '{value,spec,config,environment}',
|
||||
'[]'::jsonb
|
||||
)
|
||||
) AS environment
|
||||
WHERE resource ->> 'kind' = 'task'
|
||||
AND environment ->> 'kind' = 'package-secret'
|
||||
) THEN
|
||||
RAISE EXCEPTION 'unresolved Package Secret placeholder'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM jsonb_array_elements(NEW.revision_json -> 'resources') AS resource
|
||||
CROSS JOIN LATERAL jsonb_array_elements(
|
||||
COALESCE(
|
||||
resource #> '{value,spec,config,environment}',
|
||||
'[]'::jsonb
|
||||
)
|
||||
) AS environment
|
||||
WHERE resource ->> 'kind' = 'task'
|
||||
AND environment ->> 'kind' = 'secret'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM jsonb_array_elements(
|
||||
COALESCE(embedded_binding -> 'entries', '[]'::jsonb)
|
||||
) AS binding_entry
|
||||
WHERE binding_entry ->> 'secretRef' =
|
||||
environment ->> 'secretRef'
|
||||
)
|
||||
) THEN
|
||||
RAISE EXCEPTION 'Task SecretRef is outside Package binding'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
`REVOKE ALL ON FUNCTION "ql3"."enforce_plugin_package_secret_materialization"() FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager, ql3_package_executor, ql3_worker_ingress`,
|
||||
`CREATE TRIGGER ql3_plugin_package_secret_materialization_guard BEFORE INSERT ON "ql3"."plugin_package_materialized_revisions" FOR EACH ROW EXECUTE FUNCTION "ql3"."enforce_plugin_package_secret_materialization"()`,
|
||||
`DO $ql3$ BEGIN UPDATE "ql3"."schema_capabilities" SET contract_version = 59, migration_id = 'pg-0060-plugin-package-secret-materialization-guard', capabilities = '${CAPABILITIES_V59}'::jsonb, updated_at_ms = floor(extract(epoch FROM transaction_timestamp()) * 1000)::bigint WHERE contract_name = 'control-core' AND contract_version = 58 AND migration_id = 'pg-0059-plugin-package-secret-bindings' AND capabilities = '${CAPABILITIES_V58}'::jsonb; IF NOT FOUND THEN RAISE EXCEPTION 'control-core capability is not at version 58' USING ERRCODE = 'check_violation'; END IF; END $ql3$`,
|
||||
],
|
||||
});
|
||||
@@ -15,8 +15,8 @@ export interface PostgresSchemaContractFunction {
|
||||
export interface PostgresSchemaContract {
|
||||
readonly schema: 'ql3';
|
||||
readonly contractName: 'control-core';
|
||||
readonly contractVersion: 58;
|
||||
readonly migrationId: 'pg-0059-plugin-package-secret-bindings';
|
||||
readonly contractVersion: 59;
|
||||
readonly migrationId: 'pg-0060-plugin-package-secret-materialization-guard';
|
||||
readonly minimumServerMajor: 16;
|
||||
readonly maximumServerMajor: 18;
|
||||
readonly capabilities: Readonly<{
|
||||
@@ -56,6 +56,7 @@ export interface PostgresSchemaContract {
|
||||
plugin_package_management_quota: 1;
|
||||
plugin_package_materialized_revision: 1;
|
||||
plugin_package_secret_binding: 1;
|
||||
plugin_package_secret_materialization: 1;
|
||||
plugin_package_proposal: 1;
|
||||
plugin_package_publisher_provenance: 1;
|
||||
plugin_package_publisher_trust_authority: 1;
|
||||
@@ -105,8 +106,8 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
|
||||
Object.freeze({
|
||||
schema: 'ql3',
|
||||
contractName: 'control-core',
|
||||
contractVersion: 58,
|
||||
migrationId: 'pg-0059-plugin-package-secret-bindings',
|
||||
contractVersion: 59,
|
||||
migrationId: 'pg-0060-plugin-package-secret-materialization-guard',
|
||||
minimumServerMajor: 16,
|
||||
maximumServerMajor: 18,
|
||||
capabilities: Object.freeze({
|
||||
@@ -139,6 +140,7 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
|
||||
plugin_package_management_quota: 1,
|
||||
plugin_package_materialized_revision: 1,
|
||||
plugin_package_secret_binding: 1,
|
||||
plugin_package_secret_materialization: 1,
|
||||
plugin_package_proposal: 1,
|
||||
plugin_package_publisher_provenance: 1,
|
||||
plugin_package_publisher_trust_authority: 1,
|
||||
@@ -2336,6 +2338,14 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
|
||||
'ql3_run_retry_policies_run_fk',
|
||||
]),
|
||||
functions: Object.freeze([
|
||||
Object.freeze({
|
||||
name: 'enforce_plugin_package_secret_materialization',
|
||||
identityArguments: '',
|
||||
owner: 'ql3_migration',
|
||||
securityDefiner: false,
|
||||
volatility: 'volatile',
|
||||
configuration: Object.freeze(['search_path=pg_catalog, ql3']),
|
||||
}),
|
||||
Object.freeze({
|
||||
name: 'register_plugin_package_automation_disposition_event',
|
||||
identityArguments: '',
|
||||
|
||||
@@ -1507,6 +1507,7 @@ const REQUIRED_RUNTIME_FUNCTION_PRIVILEGES: RequiredFunctionPrivileges =
|
||||
commit_plugin_package_lifecycle: false,
|
||||
commit_plugin_package_quarantine: false,
|
||||
commit_plugin_package_task_reconciliation: false,
|
||||
enforce_plugin_package_secret_materialization: false,
|
||||
enforce_plugin_package_stage_provenance: false,
|
||||
lock_active_plugin_package_project: false,
|
||||
lock_approval_policy_fence: false,
|
||||
@@ -1525,6 +1526,7 @@ const REQUIRED_PACKAGE_MANAGER_FUNCTION_PRIVILEGES: RequiredFunctionPrivileges =
|
||||
commit_plugin_package_lifecycle: false,
|
||||
commit_plugin_package_quarantine: false,
|
||||
commit_plugin_package_task_reconciliation: false,
|
||||
enforce_plugin_package_secret_materialization: false,
|
||||
enforce_plugin_package_stage_provenance: false,
|
||||
lock_active_plugin_package_project: false,
|
||||
lock_approval_policy_fence: true,
|
||||
@@ -1543,6 +1545,7 @@ const REQUIRED_PACKAGE_EXECUTOR_FUNCTION_PRIVILEGES: RequiredFunctionPrivileges
|
||||
commit_plugin_package_lifecycle: true,
|
||||
commit_plugin_package_quarantine: true,
|
||||
commit_plugin_package_task_reconciliation: true,
|
||||
enforce_plugin_package_secret_materialization: false,
|
||||
enforce_plugin_package_stage_provenance: false,
|
||||
lock_active_plugin_package_project: true,
|
||||
lock_approval_policy_fence: true,
|
||||
|
||||
@@ -110,6 +110,7 @@ test('defines the immutable PostgreSQL capability and Run core stream', async ()
|
||||
'pg-0057-run-management-stop-boundary',
|
||||
'pg-0058-plugin-package-automation-disposition-events',
|
||||
'pg-0059-plugin-package-secret-bindings',
|
||||
'pg-0060-plugin-package-secret-materialization-guard',
|
||||
],
|
||||
);
|
||||
for (const migration of postgresqlMainMigrationStream.migrations) {
|
||||
@@ -541,6 +542,11 @@ test('freezes every published PostgreSQL migration checksum', () => {
|
||||
checksum:
|
||||
'87582d256c868bd7f5af352c4b052fdab9f3714e1e7179e35d33bfa5d62957be',
|
||||
},
|
||||
{
|
||||
id: 'pg-0060-plugin-package-secret-materialization-guard',
|
||||
checksum:
|
||||
'28284ca860b39ff9de5b2aa1a2a60ef2c463fd6a72798d237040174272b64b1e',
|
||||
},
|
||||
];
|
||||
assert.deepEqual(
|
||||
postgresqlMainMigrationStream.migrations.map(({ id, checksum }) => ({
|
||||
@@ -1988,3 +1994,39 @@ test('advances capability v58 with immutable generation-bound Package Secret bin
|
||||
/migration_id = 'pg-0058-plugin-package-automation-disposition-events'/,
|
||||
);
|
||||
});
|
||||
|
||||
test('advances capability v59 with fail-closed Package Secret materialization', async () => {
|
||||
const migration = migrationById(
|
||||
'pg-0060-plugin-package-secret-materialization-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_materialization"\(\)/,
|
||||
);
|
||||
assert.match(
|
||||
sql,
|
||||
/jsonb_typeof\([\s\S]+IS DISTINCT FROM 'array'[\s\S]+permission declarations are malformed/,
|
||||
);
|
||||
assert.match(sql, /unresolved Package Secret placeholder/);
|
||||
assert.match(sql, /Task SecretRef is outside Package binding/);
|
||||
assert.match(
|
||||
sql,
|
||||
/CREATE TRIGGER ql3_plugin_package_secret_materialization_guard BEFORE INSERT/,
|
||||
);
|
||||
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 = 59/);
|
||||
assert.match(sql, /"plugin_package_secret_materialization":1/);
|
||||
assert.match(sql, /contract_version = 58/);
|
||||
assert.match(sql, /migration_id = 'pg-0059-plugin-package-secret-bindings'/);
|
||||
});
|
||||
|
||||
@@ -754,7 +754,7 @@ test('accepts the exact PostgreSQL control schema and least-privilege runtime ro
|
||||
serverMajor: 16,
|
||||
currentUser: 'ql3_runtime',
|
||||
contractName: 'control-core',
|
||||
contractVersion: 58,
|
||||
contractVersion: 59,
|
||||
migrationIds: [
|
||||
'pg-0001-schema-capability',
|
||||
'pg-0002-run-core',
|
||||
@@ -815,6 +815,7 @@ test('accepts the exact PostgreSQL control schema and least-privilege runtime ro
|
||||
'pg-0057-run-management-stop-boundary',
|
||||
'pg-0058-plugin-package-automation-disposition-events',
|
||||
'pg-0059-plugin-package-secret-bindings',
|
||||
'pg-0060-plugin-package-secret-materialization-guard',
|
||||
],
|
||||
});
|
||||
});
|
||||
@@ -845,10 +846,10 @@ test('accepts the exact schema and isolated least-privilege admin role', async (
|
||||
}),
|
||||
);
|
||||
assert.equal(report.currentUser, 'ql3_admin');
|
||||
assert.equal(report.contractVersion, 58);
|
||||
assert.equal(report.contractVersion, 59);
|
||||
assert.equal(
|
||||
report.migrationIds.at(-1),
|
||||
'pg-0059-plugin-package-secret-bindings',
|
||||
'pg-0060-plugin-package-secret-materialization-guard',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -861,10 +862,10 @@ test('accepts the isolated least-privilege automation manager role', async () =>
|
||||
}),
|
||||
);
|
||||
assert.equal(report.currentUser, 'ql3_automation_manager');
|
||||
assert.equal(report.contractVersion, 58);
|
||||
assert.equal(report.contractVersion, 59);
|
||||
assert.equal(
|
||||
report.migrationIds.at(-1),
|
||||
'pg-0059-plugin-package-secret-bindings',
|
||||
'pg-0060-plugin-package-secret-materialization-guard',
|
||||
);
|
||||
|
||||
const widened = automationManagerPrivileges();
|
||||
@@ -893,10 +894,10 @@ test('accepts the isolated least-privilege human Approval manager role', async (
|
||||
}),
|
||||
);
|
||||
assert.equal(report.currentUser, 'ql3_approval_manager');
|
||||
assert.equal(report.contractVersion, 58);
|
||||
assert.equal(report.contractVersion, 59);
|
||||
assert.equal(
|
||||
report.migrationIds.at(-1),
|
||||
'pg-0059-plugin-package-secret-bindings',
|
||||
'pg-0060-plugin-package-secret-materialization-guard',
|
||||
);
|
||||
|
||||
const widened = approvalManagerPrivileges();
|
||||
@@ -927,10 +928,10 @@ test('accepts the isolated least-privilege Run manager role', async () => {
|
||||
}),
|
||||
);
|
||||
assert.equal(report.currentUser, 'ql3_run_manager');
|
||||
assert.equal(report.contractVersion, 58);
|
||||
assert.equal(report.contractVersion, 59);
|
||||
assert.equal(
|
||||
report.migrationIds.at(-1),
|
||||
'pg-0059-plugin-package-secret-bindings',
|
||||
'pg-0060-plugin-package-secret-materialization-guard',
|
||||
);
|
||||
|
||||
const widened = runManagerPrivileges();
|
||||
@@ -1062,10 +1063,10 @@ test('accepts the exact schema and isolated Worker ingress role', async () => {
|
||||
}),
|
||||
);
|
||||
assert.equal(report.currentUser, 'ql3_worker_ingress');
|
||||
assert.equal(report.contractVersion, 58);
|
||||
assert.equal(report.contractVersion, 59);
|
||||
assert.equal(
|
||||
report.migrationIds.at(-1),
|
||||
'pg-0059-plugin-package-secret-bindings',
|
||||
'pg-0060-plugin-package-secret-materialization-guard',
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -30,6 +30,8 @@ type PluginPackageInstallRepository =
|
||||
ReadyLocalStorage['pluginPackageInstalls'];
|
||||
type PluginPackageMaterializedRevisionRepository =
|
||||
ReadyLocalStorage['pluginPackageMaterializedRevisions'];
|
||||
type PluginPackageSecretBindingRepository =
|
||||
ReadyLocalStorage['pluginPackageSecretBindings'];
|
||||
type PluginPackageTaskReconciliationRepository =
|
||||
ReadyLocalStorage['pluginPackageTaskReconciliations'];
|
||||
type PluginPackageAutomationPublicationRepository =
|
||||
@@ -97,6 +99,7 @@ export type LocalAdoptedProfileBootstrapResult =
|
||||
readonly ownerPepper: LocalOwnerPepperRepository;
|
||||
readonly pluginPackageInstalls: PluginPackageInstallRepository;
|
||||
readonly pluginPackageMaterializedRevisions: PluginPackageMaterializedRevisionRepository;
|
||||
readonly pluginPackageSecretBindings: PluginPackageSecretBindingRepository;
|
||||
readonly pluginPackageTaskReconciliations: PluginPackageTaskReconciliationRepository;
|
||||
readonly pluginPackageAutomationPublications: PluginPackageAutomationPublicationRepository;
|
||||
readonly projectToolDefinitionSnapshots: ProjectToolDefinitionSnapshotRepository;
|
||||
@@ -244,6 +247,7 @@ export async function bootstrapLocalAdoptedProfileStorage(
|
||||
pluginPackageInstalls: readyStorage.pluginPackageInstalls,
|
||||
pluginPackageMaterializedRevisions:
|
||||
readyStorage.pluginPackageMaterializedRevisions,
|
||||
pluginPackageSecretBindings: readyStorage.pluginPackageSecretBindings,
|
||||
pluginPackageTaskReconciliations:
|
||||
readyStorage.pluginPackageTaskReconciliations,
|
||||
pluginPackageAutomationPublications:
|
||||
|
||||
@@ -79,6 +79,8 @@ export async function recoverLocalApplicationPluginPackages(
|
||||
await storage.pluginPackageTaskReconciliations();
|
||||
const pluginPackageMaterializedRevisions =
|
||||
await storage.pluginPackageMaterializedRevisions();
|
||||
const pluginPackageSecretBindings =
|
||||
await storage.pluginPackageSecretBindings();
|
||||
const pluginPackageTaskPublicationRecovery =
|
||||
await new PluginPackageTaskPublicationRecoveryCoordinator({
|
||||
source: pluginPackageTaskReconciliations,
|
||||
@@ -89,6 +91,7 @@ export async function recoverLocalApplicationPluginPackages(
|
||||
stagingRoot: options.pluginPackages.stagingRoot,
|
||||
}),
|
||||
materializedRepository: pluginPackageMaterializedRevisions,
|
||||
secretBindingSource: pluginPackageSecretBindings,
|
||||
reconciliationRepository: pluginPackageTaskReconciliations,
|
||||
taskSpecSemanticRegistry,
|
||||
}),
|
||||
|
||||
@@ -363,9 +363,9 @@ function composeDockerHarness(
|
||||
'/opt/qinglong/node_modules/@qinglong/local-application/dist/cli.js',
|
||||
],
|
||||
Labels: {
|
||||
'io.qinglong.local.sqlite-contract-min': '46',
|
||||
'io.qinglong.local.sqlite-contract-max': '46',
|
||||
'io.qinglong.local.sqlite-write-contract': '46',
|
||||
'io.qinglong.local.sqlite-contract-min': '47',
|
||||
'io.qinglong.local.sqlite-contract-max': '47',
|
||||
'io.qinglong.local.sqlite-write-contract': '47',
|
||||
'io.qinglong.local.application-config': '2',
|
||||
'io.qinglong.local.compose-selection': '1',
|
||||
'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',
|
||||
],
|
||||
Labels: {
|
||||
'io.qinglong.local.sqlite-contract-min': '46',
|
||||
'io.qinglong.local.sqlite-contract-max': '46',
|
||||
'io.qinglong.local.sqlite-write-contract': '46',
|
||||
'io.qinglong.local.sqlite-contract-min': '47',
|
||||
'io.qinglong.local.sqlite-contract-max': '47',
|
||||
'io.qinglong.local.sqlite-write-contract': '47',
|
||||
'io.qinglong.local.application-config': '2',
|
||||
'io.qinglong.local.compose-selection': '1',
|
||||
'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.generation, 1);
|
||||
assert.equal(result.profile, 'edge');
|
||||
assert.equal(result.sqlite.contractVersion, 46);
|
||||
assert.equal(result.sqlite.contractVersion, 47);
|
||||
assert.equal(result.image.architecture, 'arm64');
|
||||
assert.equal(calls.length, 2);
|
||||
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);
|
||||
const receipt = JSON.parse(fs.readFileSync(receiptPath, 'utf8'));
|
||||
assert.deepEqual(receipt.sqlite, {
|
||||
contractVersion: 46,
|
||||
writeContractVersion: 46,
|
||||
contractVersion: 47,
|
||||
writeContractVersion: 47,
|
||||
writeObservation: 'unchanged',
|
||||
backup: null,
|
||||
});
|
||||
@@ -1427,8 +1427,8 @@ test('rolls a failed Compose candidate forward to a healthy prior digest', async
|
||||
`${command.request.rolloutId}.sqlite`,
|
||||
);
|
||||
assert.equal(mode(backupPath), 0o600);
|
||||
assert.equal(receipt.sqlite.contractVersion, 46);
|
||||
assert.equal(receipt.sqlite.writeContractVersion, 46);
|
||||
assert.equal(receipt.sqlite.contractVersion, 47);
|
||||
assert.equal(receipt.sqlite.writeContractVersion, 47);
|
||||
assert.equal(receipt.sqlite.writeObservation, 'changed');
|
||||
assert.match(receipt.sqlite.backup.sha256, /^[0-9a-f]{64}$/);
|
||||
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.profile, 'edge');
|
||||
assert.equal(result.storage.contractName, 'local-control-core');
|
||||
assert.equal(result.storage.contractVersion, 46);
|
||||
assert.equal(result.storage.migrationCount, 92);
|
||||
assert.equal(result.storage.contractVersion, 47);
|
||||
assert.equal(result.storage.migrationCount, 94);
|
||||
assert.equal(result.storage.journalMode, 'delete');
|
||||
assert.equal(JSON.stringify(result).includes(state.directory), false);
|
||||
});
|
||||
|
||||
@@ -102,6 +102,8 @@ import { local0089PluginPackageAutomationDispositionEventsMigration } from '../m
|
||||
import { local0090CapabilityV45Migration } from '../migrations/0090-capability-v45';
|
||||
import { local0091PluginPackageSecretBindingsMigration } from '../migrations/0091-plugin-package-secret-bindings';
|
||||
import { local0092CapabilityV46Migration } from '../migrations/0092-capability-v46';
|
||||
import { local0093PluginPackageSecretMaterializationGuardMigration } from '../migrations/0093-plugin-package-secret-materialization-guard';
|
||||
import { local0094CapabilityV47Migration } from '../migrations/0094-capability-v47';
|
||||
import type { LocalSqliteMigrationContext } from '../migrations/sqlMigration';
|
||||
import {
|
||||
LOCAL_SQLITE_MIGRATION_STREAM_ID,
|
||||
@@ -216,6 +218,8 @@ export const localSqliteMigrationDefinition: MigrationStreamDefinition<LocalSqli
|
||||
local0090CapabilityV45Migration,
|
||||
local0091PluginPackageSecretBindingsMigration,
|
||||
local0092CapabilityV46Migration,
|
||||
local0093PluginPackageSecretMaterializationGuardMigration,
|
||||
local0094CapabilityV47Migration,
|
||||
]),
|
||||
});
|
||||
|
||||
|
||||
@@ -472,5 +472,15 @@ export const localSqliteMigrationManifest: MigrationStreamManifest =
|
||||
checksum:
|
||||
'a1b058bb7b0259069202632d27d4a55466828cde831c49821eb862feeba6ce35',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0093-plugin-package-secret-materialization-guard',
|
||||
checksum:
|
||||
'e549ea0932c5b3e48f954ff72f205d84795ae03d473312070c3a9c4939169c42',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0094-capability-v47',
|
||||
checksum:
|
||||
'6ecbbef0b9d9b3c738cc47a80868e0871c47916fb67e7c37f8620f9099de735e',
|
||||
}),
|
||||
]),
|
||||
});
|
||||
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
import { LOCAL_PLUGIN_PACKAGE_SECRET_MATERIALIZATION_TRIGGER_SQL } from '../plugin-package/pluginPackageSecretMaterializationSchemaContract';
|
||||
|
||||
export const local0093PluginPackageSecretMaterializationGuardMigration =
|
||||
defineLocalSqliteMigration({
|
||||
id: '0093-plugin-package-secret-materialization-guard',
|
||||
statements: [LOCAL_PLUGIN_PACKAGE_SECRET_MATERIALIZATION_TRIGGER_SQL],
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import { CAPABILITIES_V46 } from './0092-capability-v46';
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const CAPABILITIES_V47 = CAPABILITIES_V46.replace(
|
||||
'"plugin_package_secret_binding":1,',
|
||||
'"plugin_package_secret_binding":1,"plugin_package_secret_materialization":1,',
|
||||
);
|
||||
|
||||
export const local0094CapabilityV47Migration = defineLocalSqliteMigration({
|
||||
id: '0094-capability-v47',
|
||||
statements: [
|
||||
`UPDATE "QingLong3SchemaCapabilities" SET contract_version = 47, migration_id = '0093-plugin-package-secret-materialization-guard', capabilities = '${CAPABILITIES_V47}', updated_at_ms = CAST(unixepoch('subsec') * 1000 AS INTEGER) WHERE contract_name = 'local-control-core' AND contract_version = 46 AND migration_id = '0091-plugin-package-secret-bindings' AND capabilities = '${CAPABILITIES_V46}'`,
|
||||
],
|
||||
});
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Immutable DDL contract shared by the migration writer and read-only
|
||||
* readiness auditor. Keeping it outside /migrations preserves lazy DDL
|
||||
* loading on constrained Local Profiles.
|
||||
*/
|
||||
export const LOCAL_PLUGIN_PACKAGE_SECRET_MATERIALIZATION_TRIGGER_SQL = `
|
||||
CREATE TRIGGER ql3_plugin_package_secret_materialization_guard
|
||||
BEFORE INSERT ON "QingLong3PluginPackageMaterializedRevisions"
|
||||
BEGIN
|
||||
SELECT CASE
|
||||
WHEN json_type(
|
||||
NEW.revision_json,
|
||||
'$.manifest.spec.permissions.secrets'
|
||||
) IS NOT 'array'
|
||||
THEN RAISE(ABORT, 'Package Secret permission declarations are malformed')
|
||||
WHEN json_array_length(json_extract(
|
||||
NEW.revision_json,
|
||||
'$.manifest.spec.permissions.secrets'
|
||||
)) = 0 AND json_type(NEW.revision_json, '$.secretBinding') IS NOT NULL
|
||||
THEN RAISE(ABORT, 'unexpected Package Secret binding')
|
||||
WHEN json_array_length(json_extract(
|
||||
NEW.revision_json,
|
||||
'$.manifest.spec.permissions.secrets'
|
||||
)) > 0 AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM "QingLong3PluginPackageSecretBindings" AS binding
|
||||
WHERE binding.generation_digest = NEW.generation_digest
|
||||
AND json(binding.binding_json) = json_extract(
|
||||
NEW.revision_json,
|
||||
'$.secretBinding'
|
||||
)
|
||||
)
|
||||
THEN RAISE(ABORT, 'Package Secret binding is absent or mismatched')
|
||||
WHEN EXISTS (
|
||||
SELECT 1
|
||||
FROM json_each(NEW.revision_json, '$.resources') AS resource
|
||||
JOIN json_each(
|
||||
resource.value,
|
||||
'$.value.spec.config.environment'
|
||||
) AS environment
|
||||
WHERE json_extract(resource.value, '$.kind') = 'task'
|
||||
AND json_extract(environment.value, '$.kind') = 'package-secret'
|
||||
)
|
||||
THEN RAISE(ABORT, 'unresolved Package Secret placeholder')
|
||||
WHEN EXISTS (
|
||||
SELECT 1
|
||||
FROM json_each(NEW.revision_json, '$.resources') AS resource
|
||||
JOIN json_each(
|
||||
resource.value,
|
||||
'$.value.spec.config.environment'
|
||||
) AS environment
|
||||
WHERE json_extract(resource.value, '$.kind') = 'task'
|
||||
AND json_extract(environment.value, '$.kind') = 'secret'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM json_each(
|
||||
NEW.revision_json,
|
||||
'$.secretBinding.entries'
|
||||
) AS binding_entry
|
||||
WHERE json_extract(binding_entry.value, '$.secretRef') =
|
||||
json_extract(environment.value, '$.secretRef')
|
||||
)
|
||||
)
|
||||
THEN RAISE(ABORT, 'Task SecretRef is outside Package binding')
|
||||
END;
|
||||
END
|
||||
`.trim();
|
||||
@@ -56,6 +56,7 @@ export type LocalProfileStorageBootstrapResult =
|
||||
readonly ownerPepper: LocalSqliteRuntimeDatabase['ownerPepper'];
|
||||
readonly pluginPackageInstalls: LocalSqliteRuntimeDatabase['pluginPackageInstalls'];
|
||||
readonly pluginPackageMaterializedRevisions: LocalSqliteRuntimeDatabase['pluginPackageMaterializedRevisions'];
|
||||
readonly pluginPackageSecretBindings: LocalSqliteRuntimeDatabase['pluginPackageSecretBindings'];
|
||||
readonly pluginPackageTaskReconciliations: LocalSqliteRuntimeDatabase['pluginPackageTaskReconciliations'];
|
||||
readonly pluginPackageAutomationPublications: LocalSqliteRuntimeDatabase['pluginPackageAutomationPublications'];
|
||||
readonly projectToolDefinitionSnapshots: LocalSqliteRuntimeDatabase['projectToolDefinitionSnapshots'];
|
||||
@@ -151,6 +152,7 @@ export async function bootstrapLocalProfileStorage(
|
||||
pluginPackageInstalls: database.pluginPackageInstalls,
|
||||
pluginPackageMaterializedRevisions:
|
||||
database.pluginPackageMaterializedRevisions,
|
||||
pluginPackageSecretBindings: database.pluginPackageSecretBindings,
|
||||
pluginPackageTaskReconciliations:
|
||||
database.pluginPackageTaskReconciliations,
|
||||
pluginPackageAutomationPublications:
|
||||
|
||||
@@ -2,13 +2,22 @@ import { auditMigrationStreamHistory } from '@qinglong/runtime-core/migration-st
|
||||
import type { DatabaseSync } from 'node:sqlite';
|
||||
import { localSqliteMigrationManifest } from '../migration/migrationManifest';
|
||||
import { LocalSqliteMigrationStreamStore } from '../migration/migrationStreamStore';
|
||||
import { LOCAL_PLUGIN_PACKAGE_SECRET_MATERIALIZATION_TRIGGER_SQL } from '../plugin-package/pluginPackageSecretMaterializationSchemaContract';
|
||||
import {
|
||||
LOCAL_STEP_RUN_REFERENCE_TRIGGERS,
|
||||
normalizeLocalSqliteSchemaSql,
|
||||
} from '../run/stepRunSchemaContract';
|
||||
|
||||
export const LOCAL_SQLITE_CONTRACT_NAME = 'local-control-core';
|
||||
export const LOCAL_SQLITE_CONTRACT_VERSION = 46;
|
||||
export const LOCAL_SQLITE_CONTRACT_VERSION = 47;
|
||||
|
||||
const PLUGIN_PACKAGE_SECRET_MATERIALIZATION_TRIGGERS = Object.freeze([
|
||||
Object.freeze({
|
||||
name: 'ql3_plugin_package_secret_materialization_guard',
|
||||
tableName: 'QingLong3PluginPackageMaterializedRevisions',
|
||||
sql: LOCAL_PLUGIN_PACKAGE_SECRET_MATERIALIZATION_TRIGGER_SQL,
|
||||
}),
|
||||
]);
|
||||
|
||||
const PLUGIN_PACKAGE_AUTOMATION_DISPOSITION_TRIGGERS = Object.freeze([
|
||||
Object.freeze({
|
||||
@@ -1743,6 +1752,7 @@ function assertRequiredSchema(client: DatabaseSync): number {
|
||||
const expectedTriggers = [
|
||||
...LOCAL_STEP_RUN_REFERENCE_TRIGGERS,
|
||||
...PLUGIN_PACKAGE_AUTOMATION_DISPOSITION_TRIGGERS,
|
||||
...PLUGIN_PACKAGE_SECRET_MATERIALIZATION_TRIGGERS,
|
||||
].sort((left, right) => left.name.localeCompare(right.name));
|
||||
if (
|
||||
triggerRows.length !== expectedTriggers.length ||
|
||||
@@ -2554,10 +2564,10 @@ export async function auditLocalSqliteReadiness(
|
||||
capability.contract_name !== LOCAL_SQLITE_CONTRACT_NAME ||
|
||||
capability.contract_version !== LOCAL_SQLITE_CONTRACT_VERSION ||
|
||||
capability.migration_id !==
|
||||
'0091-plugin-package-secret-bindings' ||
|
||||
'0093-plugin-package-secret-materialization-guard' ||
|
||||
typeof capability.capabilities !== 'string' ||
|
||||
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_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_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' ||
|
||||
!Number.isSafeInteger(capability.updated_at_ms) ||
|
||||
capability.updated_at_ms < 0
|
||||
|
||||
@@ -29,6 +29,7 @@ import type {
|
||||
PluginPackageAutomationPublicationRecoverySource,
|
||||
} from '@qinglong/runtime-core/plugin-package-automation-publication';
|
||||
import type { PluginPackageMaterializedRevisionRepository } from '@qinglong/runtime-core/plugin-package-resource-materialization';
|
||||
import type { PluginPackageSecretBindingRepository } from '@qinglong/runtime-core/plugin-package-secret-binding';
|
||||
import type { PluginPackageTaskReconciliationRepository } from '@qinglong/runtime-core/plugin-package-task-reconciliation';
|
||||
import type { PluginPackageTaskPublicationRecoverySource } from '@qinglong/runtime-core/plugin-package-task-publication';
|
||||
import type { StepRunRepository } from '@qinglong/runtime-core/step-run';
|
||||
@@ -109,6 +110,7 @@ export interface LocalSqliteRuntimeDatabase {
|
||||
readonly ownerPepper: LocalOwnerPepperRepository;
|
||||
pluginPackageInstalls(): Promise<PluginPackageInstallRepository>;
|
||||
pluginPackageMaterializedRevisions(): Promise<PluginPackageMaterializedRevisionRepository>;
|
||||
pluginPackageSecretBindings(): Promise<PluginPackageSecretBindingRepository>;
|
||||
pluginPackageTaskReconciliations(): Promise<
|
||||
PluginPackageTaskReconciliationRepository &
|
||||
PluginPackageTaskPublicationRecoverySource
|
||||
@@ -195,6 +197,9 @@ export async function openLocalSqliteRuntimeDatabase(
|
||||
let pluginPackageMaterializedRevisionsPromise:
|
||||
| Promise<PluginPackageMaterializedRevisionRepository>
|
||||
| undefined;
|
||||
let pluginPackageSecretBindingsPromise:
|
||||
| Promise<PluginPackageSecretBindingRepository>
|
||||
| undefined;
|
||||
let pluginPackageTaskReconciliationsPromise:
|
||||
| Promise<
|
||||
PluginPackageTaskReconciliationRepository &
|
||||
@@ -271,6 +276,15 @@ export async function openLocalSqliteRuntimeDatabase(
|
||||
);
|
||||
return pluginPackageMaterializedRevisionsPromise;
|
||||
},
|
||||
pluginPackageSecretBindings() {
|
||||
pluginPackageSecretBindingsPromise ??= import(
|
||||
'../plugin-package/pluginPackageSecretBindingRepository.js'
|
||||
).then(
|
||||
({ LocalSqlitePluginPackageSecretBindingRepository }) =>
|
||||
new LocalSqlitePluginPackageSecretBindingRepository(authority),
|
||||
);
|
||||
return pluginPackageSecretBindingsPromise;
|
||||
},
|
||||
pluginPackageTaskReconciliations() {
|
||||
pluginPackageTaskReconciliationsPromise ??= import(
|
||||
'../plugin-package/pluginPackageTaskReconciliationRepository.js'
|
||||
|
||||
@@ -142,9 +142,11 @@ test('creates a reviewed edge database and opens runtime only after readiness',
|
||||
'0090-capability-v45',
|
||||
'0091-plugin-package-secret-bindings',
|
||||
'0092-capability-v46',
|
||||
'0093-plugin-package-secret-materialization-guard',
|
||||
'0094-capability-v47',
|
||||
]);
|
||||
assert.equal(migrated.readiness.contractName, 'local-control-core');
|
||||
assert.equal(migrated.readiness.contractVersion, 46);
|
||||
assert.equal(migrated.readiness.contractVersion, 47);
|
||||
assert.equal(migrated.readiness.journalMode, 'delete');
|
||||
assert.equal(fs.statSync(databasePath).mode & 0o777, 0o600);
|
||||
|
||||
@@ -228,6 +230,89 @@ test('creates a reviewed edge database and opens runtime only after readiness',
|
||||
}
|
||||
});
|
||||
|
||||
test('rejects malformed and unbound Package Secret materialized revisions at the database boundary', async () => {
|
||||
const client = new DatabaseSync(':memory:');
|
||||
try {
|
||||
client.exec('PRAGMA foreign_keys = ON');
|
||||
await runMigrationStream({
|
||||
stream: localSqliteMigrationDefinition,
|
||||
store: new LocalSqliteMigrationStreamStore(client),
|
||||
});
|
||||
const insert = client.prepare(
|
||||
`INSERT INTO "QingLong3PluginPackageMaterializedRevisions" (
|
||||
generation_digest, project_id, package_name, generation,
|
||||
lock_digest, manifest_digest, revision_digest, revision_json,
|
||||
created_at_ms
|
||||
) VALUES (?, 'default', 'secret-guard', 1, ?, ?, ?, ?, 1)`,
|
||||
);
|
||||
const digest = 'a'.repeat(64);
|
||||
const revision = ({ manifest, resources = [] }) => ({
|
||||
schema: 'qinglong/plugin-package-materialized-revision@v1',
|
||||
generation: {
|
||||
installationId: 'install-secret-guard',
|
||||
projectId: 'default',
|
||||
packageName: 'secret-guard',
|
||||
lockDigest: digest,
|
||||
generation: 1,
|
||||
generationDigest: digest,
|
||||
},
|
||||
manifestDigest: digest,
|
||||
manifest,
|
||||
resources,
|
||||
revisionDigest: digest,
|
||||
createdAtMs: 1,
|
||||
});
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
insert.run(
|
||||
digest,
|
||||
digest,
|
||||
digest,
|
||||
digest,
|
||||
JSON.stringify(revision({ manifest: {} })),
|
||||
),
|
||||
/permission declarations are malformed/,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
insert.run(
|
||||
digest,
|
||||
digest,
|
||||
digest,
|
||||
digest,
|
||||
JSON.stringify(
|
||||
revision({
|
||||
manifest: { spec: { permissions: { secrets: [] } } },
|
||||
resources: [
|
||||
{
|
||||
kind: 'task',
|
||||
value: {
|
||||
spec: {
|
||||
config: {
|
||||
environment: [
|
||||
{
|
||||
name: 'TOKEN',
|
||||
kind: 'secret',
|
||||
secretRef:
|
||||
'qlsecret://default/runtime-token?version=1',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
),
|
||||
/outside Package binding/,
|
||||
);
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('standalone opts into bounded WAL while edge keeps rollback journal', async (t) => {
|
||||
const { databasePath } = fixture(t);
|
||||
const options = { databasePath, profile: 'standalone' };
|
||||
@@ -507,8 +592,8 @@ test('backfills v14 execution revisions with a verified independent digest', asy
|
||||
.get(),
|
||||
},
|
||||
{
|
||||
contract_version: 46,
|
||||
migration_id: '0091-plugin-package-secret-bindings',
|
||||
contract_version: 47,
|
||||
migration_id: '0093-plugin-package-secret-materialization-guard',
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
|
||||
@@ -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 },
|
||||
);
|
||||
assert.equal((await auditLocalSqliteReadiness(client)).contractVersion, 46);
|
||||
assert.equal((await auditLocalSqliteReadiness(client)).contractVersion, 47);
|
||||
});
|
||||
|
||||
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 },
|
||||
);
|
||||
assert.equal((await auditLocalSqliteReadiness(client)).contractVersion, 46);
|
||||
assert.equal((await auditLocalSqliteReadiness(client)).contractVersion, 47);
|
||||
});
|
||||
|
||||
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,
|
||||
},
|
||||
);
|
||||
assert.equal((await auditLocalSqliteReadiness(client)).contractVersion, 46);
|
||||
assert.equal((await auditLocalSqliteReadiness(client)).contractVersion, 47);
|
||||
});
|
||||
|
||||
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);
|
||||
const prepared = await createLocalSqliteRolloutBackup(state);
|
||||
assert.equal(prepared.status, 'prepared');
|
||||
assert.equal(prepared.contractVersion, 46);
|
||||
assert.equal(prepared.writeContractVersion, 46);
|
||||
assert.equal(LOCAL_SQLITE_WRITE_CONTRACT_VERSION, 46);
|
||||
assert.equal(prepared.contractVersion, 47);
|
||||
assert.equal(prepared.writeContractVersion, 47);
|
||||
assert.equal(LOCAL_SQLITE_WRITE_CONTRACT_VERSION, 47);
|
||||
assert.match(prepared.sha256, /^[0-9a-f]{64}$/);
|
||||
assert.equal(prepared.bytes > 0, true);
|
||||
assert.equal(prepared.pageCount > 0, true);
|
||||
|
||||
+152
-19
@@ -23,6 +23,11 @@ import {
|
||||
type PluginPackageResourceKind,
|
||||
type PluginPackageResourceReference,
|
||||
} from './pluginPackageResourceGeneration';
|
||||
import {
|
||||
assertPluginPackageSecretBindingMatches,
|
||||
type PluginPackageSecretBinding,
|
||||
type PluginPackageSecretBindingRepository,
|
||||
} from './pluginPackageSecretBinding';
|
||||
import {
|
||||
normalizeTaskDefinitionLabels,
|
||||
normalizeTaskDefinitionSpec,
|
||||
@@ -125,6 +130,7 @@ export interface PluginPackageMaterializedRevision {
|
||||
readonly lock: Readonly<PluginPackageLock>;
|
||||
readonly manifest: Readonly<PluginPackageManifest>;
|
||||
readonly manifestDigest: string;
|
||||
readonly secretBinding?: Readonly<PluginPackageSecretBinding>;
|
||||
readonly resources: readonly Readonly<PluginPackageMaterializedResource>[];
|
||||
readonly revisionDigest: string;
|
||||
}
|
||||
@@ -138,6 +144,7 @@ export interface MaterializePluginPackageResourcesInput {
|
||||
readonly generation: Readonly<PluginPackageResourceGeneration>;
|
||||
readonly lock: Readonly<PluginPackageLock>;
|
||||
readonly manifestBytes: Uint8Array;
|
||||
readonly secretBinding?: Readonly<PluginPackageSecretBinding>;
|
||||
readonly resources: readonly Readonly<PluginPackageResourceMaterializationEntry>[];
|
||||
readonly taskSpecSemanticRegistry: TaskSpecSemanticRegistry;
|
||||
}
|
||||
@@ -171,6 +178,10 @@ export interface MaterializeActivePluginPackageResourcesOptions {
|
||||
readonly generationSource: PluginPackageResourceGenerationSource;
|
||||
readonly lockSource: PluginPackageResourceLockSource;
|
||||
readonly byteSource: PluginPackageResourceByteSource;
|
||||
readonly secretBindingSource?: Pick<
|
||||
PluginPackageSecretBindingRepository,
|
||||
'find'
|
||||
>;
|
||||
readonly taskSpecSemanticRegistry: TaskSpecSemanticRegistry;
|
||||
}
|
||||
|
||||
@@ -498,6 +509,8 @@ function normalizeTaskResource(
|
||||
projectId: string,
|
||||
manifest: Readonly<PluginPackageManifest>,
|
||||
registry: TaskSpecSemanticRegistry,
|
||||
secretBinding: Readonly<PluginPackageSecretBinding> | undefined,
|
||||
source: boolean,
|
||||
): Readonly<PluginPackageTaskResource> {
|
||||
const task = dataRecord(value, 'Task resource');
|
||||
exactKeys(
|
||||
@@ -512,7 +525,7 @@ function normalizeTaskResource(
|
||||
const id = resourceId(task.id, 'Task id');
|
||||
const taskId = taskIdentity(packageName, id);
|
||||
const kind = task.kind;
|
||||
const spec = normalizeTaskDefinitionSpec(task.spec as TaskDefinitionSpec);
|
||||
let spec = normalizeTaskDefinitionSpec(task.spec as TaskDefinitionSpec);
|
||||
if (kind !== 'command' || spec.schema !== BUILT_IN_COMMAND_TASK_SPEC_SCHEMA) {
|
||||
return invalid(
|
||||
'Task resource v1 only supports qinglong/command@v1 command tasks',
|
||||
@@ -526,6 +539,9 @@ function normalizeTaskResource(
|
||||
if (!(registry instanceof TaskSpecSemanticRegistry)) {
|
||||
return invalid('TaskSpec semantic registry is invalid');
|
||||
}
|
||||
if (source) {
|
||||
spec = compilePackageSecretEnvironment(spec, secretBinding);
|
||||
}
|
||||
let normalizedSpec: TaskDefinitionSpec;
|
||||
try {
|
||||
normalizedSpec = registry.normalize({
|
||||
@@ -537,22 +553,7 @@ function normalizeTaskResource(
|
||||
} catch {
|
||||
return invalid('Task resource spec semantics are invalid or unsupported');
|
||||
}
|
||||
const environment = normalizedSpec.config.environment;
|
||||
if (
|
||||
Array.isArray(environment) &&
|
||||
environment.some(
|
||||
(entry) =>
|
||||
entry !== null &&
|
||||
typeof entry === 'object' &&
|
||||
!Array.isArray(entry) &&
|
||||
(entry as Readonly<Record<string, TaskDefinitionJson>>).kind ===
|
||||
'secret',
|
||||
)
|
||||
) {
|
||||
return invalid(
|
||||
'Task resource v1 does not support unresolved package Secret bindings',
|
||||
);
|
||||
}
|
||||
assertResolvedPackageSecretEnvironment(normalizedSpec, secretBinding);
|
||||
const description = optionalDescription(task.description);
|
||||
if (typeof task.enabled !== 'boolean') {
|
||||
return invalid('Task enabled is invalid');
|
||||
@@ -571,6 +572,78 @@ function normalizeTaskResource(
|
||||
});
|
||||
}
|
||||
|
||||
function compilePackageSecretEnvironment(
|
||||
spec: Readonly<TaskDefinitionSpec>,
|
||||
secretBinding: Readonly<PluginPackageSecretBinding> | undefined,
|
||||
): Readonly<TaskDefinitionSpec> {
|
||||
const environment = spec.config.environment;
|
||||
if (!Array.isArray(environment)) return spec;
|
||||
const entries = new Map(
|
||||
(secretBinding?.entries ?? []).map((entry) => [entry.name, entry]),
|
||||
);
|
||||
const compiled = environment.flatMap((value) => {
|
||||
const entry = dataRecord(value, 'Task environment');
|
||||
if (entry.kind === 'secret') {
|
||||
return invalid('Package Task source cannot contain a direct SecretRef');
|
||||
}
|
||||
if (entry.kind !== 'package-secret') return [value];
|
||||
exactKeys(
|
||||
entry,
|
||||
['kind', 'name', 'requirement'],
|
||||
[],
|
||||
'Package Secret environment',
|
||||
);
|
||||
const requirement = boundedText(
|
||||
entry.requirement,
|
||||
'Package Secret requirement',
|
||||
128,
|
||||
);
|
||||
const binding = entries.get(requirement);
|
||||
if (!binding) {
|
||||
return invalid(
|
||||
'Package Task references an undeclared Secret requirement',
|
||||
);
|
||||
}
|
||||
if (binding.secretRef === null) return [];
|
||||
return [
|
||||
Object.freeze({
|
||||
name: entry.name as TaskDefinitionJson,
|
||||
kind: 'secret' as const,
|
||||
secretRef: binding.secretRef,
|
||||
}),
|
||||
];
|
||||
});
|
||||
return normalizeTaskDefinitionSpec(
|
||||
Object.freeze({
|
||||
schema: spec.schema,
|
||||
config: Object.freeze({
|
||||
...spec.config,
|
||||
environment: Object.freeze(compiled),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function assertResolvedPackageSecretEnvironment(
|
||||
spec: Readonly<TaskDefinitionSpec>,
|
||||
secretBinding: Readonly<PluginPackageSecretBinding> | undefined,
|
||||
): void {
|
||||
const allowed = new Set(
|
||||
(secretBinding?.entries ?? []).flatMap((entry) =>
|
||||
entry.secretRef === null ? [] : [entry.secretRef],
|
||||
),
|
||||
);
|
||||
const environment = spec.config.environment;
|
||||
if (!Array.isArray(environment)) return;
|
||||
for (const value of environment) {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) continue;
|
||||
const entry = value as Readonly<Record<string, TaskDefinitionJson>>;
|
||||
if (entry.kind === 'secret' && !allowed.has(entry.secretRef as string)) {
|
||||
invalid('Task SecretRef is not authorized by the Package binding');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeWorkflowResource(
|
||||
value: unknown,
|
||||
): Readonly<PluginPackageWorkflowResource> {
|
||||
@@ -784,6 +857,8 @@ function normalizeMaterializedValue(
|
||||
projectId: string,
|
||||
manifest: Readonly<PluginPackageManifest>,
|
||||
registry: TaskSpecSemanticRegistry,
|
||||
secretBinding: Readonly<PluginPackageSecretBinding> | undefined,
|
||||
source: boolean,
|
||||
): PluginPackageMaterializedResourceValue {
|
||||
switch (kind) {
|
||||
case 'task':
|
||||
@@ -793,6 +868,8 @@ function normalizeMaterializedValue(
|
||||
projectId,
|
||||
manifest,
|
||||
registry,
|
||||
secretBinding,
|
||||
source,
|
||||
);
|
||||
case 'workflow':
|
||||
return normalizeWorkflowResource(value);
|
||||
@@ -877,6 +954,7 @@ function normalizeRevisionResources(
|
||||
generation: Readonly<PluginPackageResourceGeneration>,
|
||||
manifest: Readonly<PluginPackageManifest>,
|
||||
registry: TaskSpecSemanticRegistry,
|
||||
secretBinding: Readonly<PluginPackageSecretBinding> | undefined,
|
||||
): readonly Readonly<PluginPackageMaterializedResource>[] {
|
||||
const entries = boundedDenseArray(
|
||||
values,
|
||||
@@ -918,6 +996,8 @@ function normalizeRevisionResources(
|
||||
generation.projectId,
|
||||
manifest,
|
||||
registry,
|
||||
secretBinding,
|
||||
false,
|
||||
),
|
||||
});
|
||||
});
|
||||
@@ -972,7 +1052,7 @@ export function normalizePluginPackageMaterializedRevision(
|
||||
'resources',
|
||||
'revisionDigest',
|
||||
],
|
||||
[],
|
||||
['secretBinding'],
|
||||
'materialized revision',
|
||||
);
|
||||
if (revision.schema !== PLUGIN_PACKAGE_MATERIALIZED_REVISION_SCHEMA) {
|
||||
@@ -994,11 +1074,17 @@ export function normalizePluginPackageMaterializedRevision(
|
||||
return invalid('materialized manifest identity or digest is invalid');
|
||||
}
|
||||
assertManifestMatchesGeneration(manifest, generation);
|
||||
const secretBinding = normalizeMaterializationSecretBinding(
|
||||
revision.secretBinding as Readonly<PluginPackageSecretBinding> | undefined,
|
||||
generation,
|
||||
manifest,
|
||||
);
|
||||
const resources = normalizeRevisionResources(
|
||||
revision.resources,
|
||||
generation,
|
||||
manifest,
|
||||
taskSpecSemanticRegistry,
|
||||
secretBinding,
|
||||
);
|
||||
const descriptors: PluginPackageContentEntryDescriptor[] = resources
|
||||
.map((resource) =>
|
||||
@@ -1020,6 +1106,7 @@ export function normalizePluginPackageMaterializedRevision(
|
||||
lock,
|
||||
manifest,
|
||||
manifestDigest,
|
||||
...(secretBinding === undefined ? {} : { secretBinding }),
|
||||
resources,
|
||||
});
|
||||
const revisionDigest = digest(revision.revisionDigest, 'revision digest');
|
||||
@@ -1042,7 +1129,7 @@ export function materializePluginPackageResources(
|
||||
'resources',
|
||||
'taskSpecSemanticRegistry',
|
||||
],
|
||||
[],
|
||||
['secretBinding'],
|
||||
'materialization input',
|
||||
);
|
||||
const generation = normalizePluginPackageResourceGeneration(value.generation);
|
||||
@@ -1050,6 +1137,11 @@ export function materializePluginPackageResources(
|
||||
assertGenerationMatchesLock(generation, lock);
|
||||
const manifest = normalizeManifestBytes(value.manifestBytes, lock);
|
||||
assertManifestMatchesGeneration(manifest, generation);
|
||||
const secretBinding = normalizeMaterializationSecretBinding(
|
||||
value.secretBinding,
|
||||
generation,
|
||||
manifest,
|
||||
);
|
||||
const entries = boundedDenseArray(
|
||||
value.resources,
|
||||
generation.resources.length,
|
||||
@@ -1095,6 +1187,8 @@ export function materializePluginPackageResources(
|
||||
generation.projectId,
|
||||
manifest,
|
||||
value.taskSpecSemanticRegistry,
|
||||
secretBinding,
|
||||
true,
|
||||
),
|
||||
});
|
||||
});
|
||||
@@ -1119,6 +1213,7 @@ export function materializePluginPackageResources(
|
||||
lock,
|
||||
manifest,
|
||||
manifestDigest: lock.manifestDigest,
|
||||
...(secretBinding === undefined ? {} : { secretBinding }),
|
||||
resources: Object.freeze(resources),
|
||||
});
|
||||
return normalizePluginPackageMaterializedRevision(
|
||||
@@ -1130,6 +1225,32 @@ export function materializePluginPackageResources(
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeMaterializationSecretBinding(
|
||||
value: Readonly<PluginPackageSecretBinding> | undefined,
|
||||
generation: Readonly<PluginPackageResourceGeneration>,
|
||||
manifest: Readonly<PluginPackageManifest>,
|
||||
): Readonly<PluginPackageSecretBinding> | undefined {
|
||||
if (manifest.spec.permissions.secrets.length === 0) {
|
||||
if (value !== undefined) {
|
||||
invalid('Secret binding is forbidden when Manifest declares no Secrets');
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
if (!manifest.spec.permissions.tools.includes('secret.use')) {
|
||||
return invalid(
|
||||
'Secret-aware Package requires the approved secret.use permission',
|
||||
);
|
||||
}
|
||||
if (value === undefined) {
|
||||
return invalid('Secret-aware Package requires an approved binding');
|
||||
}
|
||||
try {
|
||||
return assertPluginPackageSecretBindingMatches(value, generation, manifest);
|
||||
} catch {
|
||||
return invalid('Secret binding does not match generation and Manifest');
|
||||
}
|
||||
}
|
||||
|
||||
function materializationSources(
|
||||
value: MaterializeActivePluginPackageResourcesOptions,
|
||||
): void {
|
||||
@@ -1143,6 +1264,9 @@ function materializationSources(
|
||||
typeof value.lockSource.findLock !== 'function' ||
|
||||
!value.byteSource ||
|
||||
typeof value.byteSource.open !== 'function' ||
|
||||
(value.secretBindingSource !== undefined &&
|
||||
(!value.secretBindingSource ||
|
||||
typeof value.secretBindingSource.find !== 'function')) ||
|
||||
!(value.taskSpecSemanticRegistry instanceof TaskSpecSemanticRegistry)
|
||||
) {
|
||||
invalid('active materialization sources are invalid');
|
||||
@@ -1226,10 +1350,19 @@ export async function materializeActivePluginPackageResources(
|
||||
});
|
||||
}
|
||||
}
|
||||
const activeManifest = normalizeManifestBytes(manifestBytes, lock);
|
||||
const activeSecretBindingValue =
|
||||
activeManifest.spec.permissions.secrets.length === 0
|
||||
? undefined
|
||||
: await value.secretBindingSource?.find(generation.generationDigest);
|
||||
const activeSecretBinding = activeSecretBindingValue ?? undefined;
|
||||
const revision = materializePluginPackageResources({
|
||||
generation,
|
||||
lock,
|
||||
manifestBytes,
|
||||
...(activeSecretBinding === undefined
|
||||
? {}
|
||||
: { secretBinding: activeSecretBinding }),
|
||||
resources: Object.freeze(resources),
|
||||
taskSpecSemanticRegistry: value.taskSpecSemanticRegistry,
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
type PluginPackageResourceLockSource,
|
||||
} from './pluginPackageResourceMaterialization';
|
||||
import type { PluginPackageResourceGenerationSource } from './pluginPackageResourceGeneration';
|
||||
import type { PluginPackageSecretBindingRepository } from './pluginPackageSecretBinding';
|
||||
import {
|
||||
InvalidPluginPackageTaskReconciliationError,
|
||||
PluginPackageTaskReconciliationConflictError,
|
||||
@@ -103,7 +104,10 @@ export class PluginPackageTaskPublicationConflictError extends Error {
|
||||
readonly code = 'PLUGIN_PACKAGE_TASK_PUBLICATION_CONFLICT';
|
||||
|
||||
constructor(message: string, options?: ErrorOptions) {
|
||||
super(`Plugin Package Task publication conflicts with state: ${message}`, options);
|
||||
super(
|
||||
`Plugin Package Task publication conflicts with state: ${message}`,
|
||||
options,
|
||||
);
|
||||
this.name = 'PluginPackageTaskPublicationConflictError';
|
||||
}
|
||||
}
|
||||
@@ -172,7 +176,11 @@ function packageName(value: unknown): string {
|
||||
return value;
|
||||
}
|
||||
|
||||
function positiveInteger(value: unknown, maximum: number, label: string): number {
|
||||
function positiveInteger(
|
||||
value: unknown,
|
||||
maximum: number,
|
||||
label: string,
|
||||
): number {
|
||||
if (
|
||||
!Number.isSafeInteger(value) ||
|
||||
(value as number) < 1 ||
|
||||
@@ -242,8 +250,7 @@ function normalizePage(
|
||||
if (
|
||||
candidates.some(
|
||||
(candidate, index) =>
|
||||
index > 0 &&
|
||||
compareCandidates(candidates[index - 1]!, candidate) >= 0,
|
||||
index > 0 && compareCandidates(candidates[index - 1]!, candidate) >= 0,
|
||||
)
|
||||
) {
|
||||
return invalid('recovery candidates must be uniquely sorted');
|
||||
@@ -274,6 +281,10 @@ function publicationAuthorities(options: {
|
||||
readonly lockSource: PluginPackageResourceLockSource;
|
||||
readonly byteSource: PluginPackageResourceByteSource;
|
||||
readonly materializedRepository: PluginPackageMaterializedRevisionRepository;
|
||||
readonly secretBindingSource?: Pick<
|
||||
PluginPackageSecretBindingRepository,
|
||||
'find'
|
||||
>;
|
||||
readonly reconciliationRepository: PluginPackageTaskReconciliationRepository;
|
||||
readonly taskSpecSemanticRegistry: TaskSpecSemanticRegistry;
|
||||
}): void {
|
||||
@@ -288,12 +299,13 @@ function publicationAuthorities(options: {
|
||||
'reconciliationRepository',
|
||||
'taskSpecSemanticRegistry',
|
||||
],
|
||||
[],
|
||||
['secretBindingSource'],
|
||||
'publication coordinator options',
|
||||
);
|
||||
if (
|
||||
!options.generationSource ||
|
||||
typeof options.generationSource.findActiveResourceGeneration !== 'function' ||
|
||||
typeof options.generationSource.findActiveResourceGeneration !==
|
||||
'function' ||
|
||||
!options.lockSource ||
|
||||
typeof options.lockSource.findLock !== 'function' ||
|
||||
!options.byteSource ||
|
||||
@@ -301,6 +313,9 @@ function publicationAuthorities(options: {
|
||||
!options.materializedRepository ||
|
||||
typeof options.materializedRepository.find !== 'function' ||
|
||||
typeof options.materializedRepository.publish !== 'function' ||
|
||||
(options.secretBindingSource !== undefined &&
|
||||
(!options.secretBindingSource ||
|
||||
typeof options.secretBindingSource.find !== 'function')) ||
|
||||
!options.reconciliationRepository ||
|
||||
typeof options.reconciliationRepository.find !== 'function' ||
|
||||
typeof options.reconciliationRepository.reconcile !== 'function' ||
|
||||
@@ -316,6 +331,9 @@ export class PluginPackageTaskPublicationCoordinator {
|
||||
readonly #byteSource: PluginPackageResourceByteSource;
|
||||
readonly #materializedRepository: PluginPackageMaterializedRevisionRepository;
|
||||
readonly #reconciliationRepository: PluginPackageTaskReconciliationRepository;
|
||||
readonly #secretBindingSource:
|
||||
| Pick<PluginPackageSecretBindingRepository, 'find'>
|
||||
| undefined;
|
||||
readonly #registry: TaskSpecSemanticRegistry;
|
||||
|
||||
constructor(options: {
|
||||
@@ -323,6 +341,10 @@ export class PluginPackageTaskPublicationCoordinator {
|
||||
readonly lockSource: PluginPackageResourceLockSource;
|
||||
readonly byteSource: PluginPackageResourceByteSource;
|
||||
readonly materializedRepository: PluginPackageMaterializedRevisionRepository;
|
||||
readonly secretBindingSource?: Pick<
|
||||
PluginPackageSecretBindingRepository,
|
||||
'find'
|
||||
>;
|
||||
readonly reconciliationRepository: PluginPackageTaskReconciliationRepository;
|
||||
readonly taskSpecSemanticRegistry: TaskSpecSemanticRegistry;
|
||||
}) {
|
||||
@@ -331,6 +353,7 @@ export class PluginPackageTaskPublicationCoordinator {
|
||||
this.#lockSource = options.lockSource;
|
||||
this.#byteSource = options.byteSource;
|
||||
this.#materializedRepository = options.materializedRepository;
|
||||
this.#secretBindingSource = options.secretBindingSource;
|
||||
this.#reconciliationRepository = options.reconciliationRepository;
|
||||
this.#registry = options.taskSpecSemanticRegistry;
|
||||
}
|
||||
@@ -362,20 +385,28 @@ export class PluginPackageTaskPublicationCoordinator {
|
||||
);
|
||||
let materialized: 'created' | 'existing' = 'existing';
|
||||
if (revision === null) {
|
||||
const materializedValue = await materializeActivePluginPackageResources({
|
||||
const materializedValue = await materializeActivePluginPackageResources(
|
||||
{
|
||||
...identity,
|
||||
generationSource: this.#generationSource,
|
||||
lockSource: this.#lockSource,
|
||||
byteSource: this.#byteSource,
|
||||
...(this.#secretBindingSource === undefined
|
||||
? {}
|
||||
: { secretBindingSource: this.#secretBindingSource }),
|
||||
taskSpecSemanticRegistry: this.#registry,
|
||||
});
|
||||
},
|
||||
);
|
||||
if (materializedValue === null) {
|
||||
return Object.freeze({
|
||||
status: 'superseded',
|
||||
generationDigest: first.generationDigest,
|
||||
});
|
||||
}
|
||||
if (materializedValue.generation.generationDigest !== first.generationDigest) {
|
||||
if (
|
||||
materializedValue.generation.generationDigest !==
|
||||
first.generationDigest
|
||||
) {
|
||||
return Object.freeze({
|
||||
status: 'superseded',
|
||||
generationDigest: first.generationDigest,
|
||||
@@ -454,7 +485,9 @@ export class PluginPackageTaskPublicationCoordinator {
|
||||
error instanceof PluginPackageResourceMaterializationUnavailableError ||
|
||||
error instanceof PluginPackageTaskReconciliationUnavailableError
|
||||
) {
|
||||
throw new PluginPackageTaskPublicationUnavailableError({ cause: error });
|
||||
throw new PluginPackageTaskPublicationUnavailableError({
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
throw new PluginPackageTaskPublicationUnavailableError({
|
||||
cause: error instanceof Error ? error : undefined,
|
||||
@@ -472,7 +505,12 @@ export class PluginPackageTaskPublicationRecoveryCoordinator {
|
||||
readonly publisher: PluginPackageTaskPublicationCoordinator;
|
||||
}) {
|
||||
const value = record(options, 'recovery coordinator options');
|
||||
exactKeys(value, ['publisher', 'source'], [], 'recovery coordinator options');
|
||||
exactKeys(
|
||||
value,
|
||||
['publisher', 'source'],
|
||||
[],
|
||||
'recovery coordinator options',
|
||||
);
|
||||
if (
|
||||
!options.source ||
|
||||
typeof options.source.listPendingPage !== 'function' ||
|
||||
@@ -511,7 +549,8 @@ export class PluginPackageTaskPublicationRecoveryCoordinator {
|
||||
options.limit,
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof InvalidPluginPackageTaskPublicationError) throw error;
|
||||
if (error instanceof InvalidPluginPackageTaskPublicationError)
|
||||
throw error;
|
||||
throw new PluginPackageTaskPublicationUnavailableError({
|
||||
cause: error instanceof Error ? error : undefined,
|
||||
});
|
||||
@@ -595,7 +634,10 @@ export class PluginPackageTaskPublicationRecoveryCoordinator {
|
||||
}
|
||||
let probe: Readonly<PluginPackageTaskPublicationRecoveryPage>;
|
||||
try {
|
||||
probe = normalizePage(await this.#source.listPendingPage({ limit: 1 }), 1);
|
||||
probe = normalizePage(
|
||||
await this.#source.listPendingPage({ limit: 1 }),
|
||||
1,
|
||||
);
|
||||
} catch (error) {
|
||||
throw new PluginPackageTaskPublicationUnavailableError({
|
||||
cause: error instanceof Error ? error : undefined,
|
||||
|
||||
@@ -23,6 +23,9 @@ const {
|
||||
createBuiltInTaskSpecSemanticRegistry,
|
||||
} = require('../dist/task-definition/taskSpecSemantic');
|
||||
const { createSecretRef } = require('../dist/secret/secretReference');
|
||||
const {
|
||||
createPluginPackageSecretBinding,
|
||||
} = require('../dist/plugin-package/pluginPackageSecretBinding');
|
||||
const {
|
||||
InvalidPluginPackageResourceMaterializationError,
|
||||
MAX_PLUGIN_PACKAGE_MATERIALIZED_RESOURCE_BYTES,
|
||||
@@ -287,10 +290,7 @@ test('materializes exact Task, Workflow, Prompt and Tool JSON into one immutable
|
||||
revision,
|
||||
);
|
||||
|
||||
const drafts = pluginPackageTaskDefinitionDrafts(
|
||||
revision,
|
||||
value.registry,
|
||||
);
|
||||
const drafts = pluginPackageTaskDefinitionDrafts(revision, value.registry);
|
||||
assert.deepEqual(JSON.parse(JSON.stringify(drafts)), [
|
||||
{
|
||||
projectId: 'project-001',
|
||||
@@ -366,9 +366,7 @@ test('fails closed on unapproved capabilities, unresolved references and source
|
||||
|
||||
const drift = fixture();
|
||||
const changed = drift.entries.map((entry, index) =>
|
||||
index === 0
|
||||
? { ...entry, bytes: Buffer.from('{}') }
|
||||
: entry,
|
||||
index === 0 ? { ...entry, bytes: Buffer.from('{}') } : entry,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
@@ -383,8 +381,8 @@ test('fails closed on unapproved capabilities, unresolved references and source
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects secret-bearing package Tasks until an approved binding format exists', () => {
|
||||
const value = fixture({
|
||||
test('compiles approved Package Secret requirements into pinned Task SecretRefs', () => {
|
||||
const direct = fixture({
|
||||
manifest: {
|
||||
spec: {
|
||||
permissions: {
|
||||
@@ -427,13 +425,128 @@ test('rejects secret-bearing package Tasks until an approved binding format exis
|
||||
assert.throws(
|
||||
() =>
|
||||
materializePluginPackageResources({
|
||||
generation: direct.generation,
|
||||
lock: direct.lock,
|
||||
manifestBytes: direct.manifestBytes,
|
||||
resources: direct.entries,
|
||||
taskSpecSemanticRegistry: direct.registry,
|
||||
}),
|
||||
/requires an approved binding/,
|
||||
);
|
||||
|
||||
const directBinding = createPluginPackageSecretBinding({
|
||||
generation: direct.generation,
|
||||
manifest: direct.manifest,
|
||||
assignments: [
|
||||
{
|
||||
name: 'TOKEN',
|
||||
secretRef: createSecretRef({
|
||||
projectId: 'project-001',
|
||||
name: 'TOKEN',
|
||||
version: 1,
|
||||
}),
|
||||
},
|
||||
],
|
||||
authority: {
|
||||
kind: 'local-owner-confirmation',
|
||||
evidenceDigest: 'e'.repeat(64),
|
||||
},
|
||||
boundAtMs: 200,
|
||||
});
|
||||
assert.throws(
|
||||
() =>
|
||||
materializePluginPackageResources({
|
||||
generation: direct.generation,
|
||||
lock: direct.lock,
|
||||
manifestBytes: direct.manifestBytes,
|
||||
secretBinding: directBinding,
|
||||
resources: direct.entries,
|
||||
taskSpecSemanticRegistry: direct.registry,
|
||||
}),
|
||||
/cannot contain a direct SecretRef/,
|
||||
);
|
||||
|
||||
const value = fixture({
|
||||
manifest: {
|
||||
spec: {
|
||||
permissions: {
|
||||
secrets: [
|
||||
{ name: 'OPTIONAL_TOKEN', required: false },
|
||||
{ name: 'TOKEN', required: true },
|
||||
],
|
||||
tools: ['run.read', 'secret.use', 'system.command'],
|
||||
},
|
||||
},
|
||||
},
|
||||
resourceValues: {
|
||||
'tasks/collect.json': {
|
||||
schema: 'qinglong/plugin-package-task-resource@v1',
|
||||
id: 'collect',
|
||||
name: 'Collect',
|
||||
labels: {},
|
||||
enabled: true,
|
||||
kind: 'command',
|
||||
spec: {
|
||||
schema: 'qinglong/command@v1',
|
||||
config: {
|
||||
command: {
|
||||
kind: 'argv',
|
||||
file: '/usr/bin/printf',
|
||||
args: ['ok'],
|
||||
},
|
||||
environment: [
|
||||
{
|
||||
name: 'OPTIONAL_TOKEN',
|
||||
kind: 'package-secret',
|
||||
requirement: 'OPTIONAL_TOKEN',
|
||||
},
|
||||
{
|
||||
name: 'API_TOKEN',
|
||||
kind: 'package-secret',
|
||||
requirement: 'TOKEN',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const secretRef = createSecretRef({
|
||||
projectId: 'project-001',
|
||||
name: 'runtime-token',
|
||||
version: 3,
|
||||
});
|
||||
const binding = createPluginPackageSecretBinding({
|
||||
generation: value.generation,
|
||||
manifest: value.manifest,
|
||||
assignments: [
|
||||
{ name: 'OPTIONAL_TOKEN', secretRef: null },
|
||||
{ name: 'TOKEN', secretRef },
|
||||
],
|
||||
authority: {
|
||||
kind: 'approved-action-execution',
|
||||
evidenceDigest: 'e'.repeat(64),
|
||||
},
|
||||
boundAtMs: 200,
|
||||
});
|
||||
const revision = materializePluginPackageResources({
|
||||
generation: value.generation,
|
||||
lock: value.lock,
|
||||
manifestBytes: value.manifestBytes,
|
||||
secretBinding: binding,
|
||||
resources: value.entries,
|
||||
taskSpecSemanticRegistry: value.registry,
|
||||
}),
|
||||
/does not support unresolved package Secret bindings/,
|
||||
});
|
||||
assert.equal(revision.secretBinding.bindingDigest, binding.bindingDigest);
|
||||
assert.deepEqual(
|
||||
JSON.parse(
|
||||
JSON.stringify(revision.resources[1].value.spec.config.environment),
|
||||
),
|
||||
[{ kind: 'secret', name: 'API_TOKEN', secretRef }],
|
||||
);
|
||||
assert.deepEqual(
|
||||
normalizePluginPackageMaterializedRevision(revision, value.registry),
|
||||
revision,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -523,7 +636,10 @@ test('reads active bytes sequentially with explicit bounds and rejects a generat
|
||||
},
|
||||
byteSource: {
|
||||
async open(generation) {
|
||||
assert.equal(generation.generationDigest, value.generation.generationDigest);
|
||||
assert.equal(
|
||||
generation.generationDigest,
|
||||
value.generation.generationDigest,
|
||||
);
|
||||
return {
|
||||
async read(path, maximumBytes) {
|
||||
reads.push({ path, maximumBytes });
|
||||
@@ -568,7 +684,11 @@ test('reads active bytes sequentially with explicit bounds and rejects a generat
|
||||
return this.calls === 1 ? value.generation : changed;
|
||||
},
|
||||
},
|
||||
lockSource: { async findLock() { return value.lock; } },
|
||||
lockSource: {
|
||||
async findLock() {
|
||||
return value.lock;
|
||||
},
|
||||
},
|
||||
byteSource: {
|
||||
async open() {
|
||||
return {
|
||||
@@ -588,10 +708,7 @@ test('reads active bytes sequentially with explicit bounds and rejects a generat
|
||||
});
|
||||
|
||||
test('publishes materialization only through the explicit runtime-core subpath', () => {
|
||||
assert.equal(
|
||||
require('../dist').materializePluginPackageResources,
|
||||
undefined,
|
||||
);
|
||||
assert.equal(require('../dist').materializePluginPackageResources, undefined);
|
||||
assert.equal(
|
||||
require('@qinglong/runtime-core/plugin-package-resource-materialization')
|
||||
.materializePluginPackageResources,
|
||||
|
||||
@@ -74,12 +74,12 @@ test('current QL3 workspace has exactly eighteen reviewed package boundaries', (
|
||||
[
|
||||
{
|
||||
directory: 'packages/ql3-cluster-postgres/src/migrations',
|
||||
directSourceFiles: 60,
|
||||
directSourceFiles: 61,
|
||||
reviewKind: 'ordered_ledger',
|
||||
},
|
||||
{
|
||||
directory: 'packages/ql3-local-sqlite/src/migrations',
|
||||
directSourceFiles: 93,
|
||||
directSourceFiles: 95,
|
||||
reviewKind: 'ordered_ledger',
|
||||
},
|
||||
],
|
||||
@@ -421,10 +421,10 @@ test('current QL3 workspace has exactly eighteen reviewed package boundaries', (
|
||||
rootSourceFileRoles: clusterPostgres.rootSourceFileRoles,
|
||||
},
|
||||
{
|
||||
sourceFiles: 156,
|
||||
sourceFiles: 157,
|
||||
rootSourceFiles: 1,
|
||||
rootSourceLines: 126,
|
||||
nestedSourceFiles: 155,
|
||||
nestedSourceFiles: 156,
|
||||
rootSourceFileRoles: { 'index.ts': 'public_export' },
|
||||
},
|
||||
);
|
||||
@@ -540,10 +540,10 @@ test('current QL3 workspace has exactly eighteen reviewed package boundaries', (
|
||||
rootSourceFileRoles: localSqlite.rootSourceFileRoles,
|
||||
},
|
||||
{
|
||||
sourceFiles: 184,
|
||||
sourceFiles: 187,
|
||||
rootSourceFiles: 1,
|
||||
rootSourceLines: 31,
|
||||
nestedSourceFiles: 183,
|
||||
nestedSourceFiles: 186,
|
||||
rootSourceFileRoles: { 'index.ts': 'public_export' },
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user