feat(ql3): materialize package secret bindings

This commit is contained in:
whyour
2026-08-13 10:31:06 +08:00
parent 55ef085a6e
commit d72631a2b0
34 changed files with 839 additions and 115 deletions
+1
View File
@@ -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/228PostgreSQL package 311 pass/1 条外部 URL 条件 skip;完整 18-package build/test 退出 0backend 1,188 pass/2 skippackage/dependency boundary 零 findingPostgreSQL 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 v46PostgreSQL 追加 `pg-0059`、capability v58,并只向 package executor 授予 `SELECT, INSERT`。不新增 package、daemon、timer、watcher、连接、缓存或集群 workload,低配路由设备只承担一个有界表和三个索引。D-305 不冒充 Secret 已进入执行路径:现有 materialization 拒绝仍保留,D-306 再完成用户授权、Secret resolution、runtime consumption 与 lifecycle/rebinding 语义。core 509/509、SQLite 232/232、PostgreSQL 316 pass/1 条件 skip;完整 18-package clean build/test 退出 0backend 1,188 pass/2 skip,五项边界审计零 findingworkspace 仍无 single-source/shallow-source package。PostgreSQL 18.4 arm64 HA 125 项 gate 全绿、timeline `1→2`,报告 SHA-256 为 `acf0fea7ca7699989dfe70f5dd0061cdf5fb1968c691094331fea06ce01b96dc`
- D-306A/ADR-0394(已接受):Package Task source 以 `package-secret` placeholder 引用逻辑 requirementmaterialization 只用当前 generation 的 D-305 binding 编译为已有、固定 version 的 Task `SecretRef`Package source 直接携带 SecretRef、缺失 binding、未批准 `secret.use`、跨 binding 引用和 optional/required 漂移全部失败关闭。binding 快照不含明文并进入 materialized revision digestLocal/Cluster 启动发布复用既有 repository/poolTask dispatch、Local 短时解密和 Cluster offer/lease-fenced delivery 不另造协议。SQLite/PostgreSQL INSERT trigger 同时防止直接写库绕过;Local 只读 readiness 继续不加载 DDL。Local contract v47、Cluster v59;不新增 package、表、索引、连接、daemon、watcher、timer、cache 或 workload。完整 18-package clean build/test 退出 0backend 1,188 pass/2 条件 skip/0 fail;五项 package/dependency/edge/service-manager/local-image 审计零 findingworkspace 仍无 single-source/shallow-source package,两个有序 migration ledger 精确为 PostgreSQL 61、SQLite 95 个 sourcePostgreSQL 18.4 arm64 HA 125 项 gate 全绿、timeline `1→2`,报告 SHA-256 为 `f9107e8e54892a788779758f0573ac8d6a80f6d086516a1f5f5bbacb59bbb4be`。D-306A 不冒充产品闭环:Local bind/rebind 命令、Cluster Approved Action/API 与新 generation rotation/revocation 编排属于 D-306B。
- D-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
- 关联 RFCQL-RFC-0001 D-306A
- 关联 ADRADR-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 contractmigration 与 readiness 共同引用,保持只读 readiness 和 rollout-safety 不加载 DDL 模块。Local contract 升至 v47Cluster 升至 v59。
6. 实现复用既有 SQLite authority、PostgreSQL pool、Task Secret resolution 与 Worker delivery,不增加 package、表、索引、连接、timer、watcher、cache、Pod 或 daemon。Local 仅增加一个 INSERT triggerCluster 增加一个 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 |
## 验证
- coreplaceholder 编译、optional-null、省略、直接 SecretRef/缺失 binding/越权引用拒绝、revision digest 与 normalize round-trip。
- SQLite:真实 migration、畸形声明与未绑定 SecretRef 直接 INSERT 拒绝、readiness DDL lazy boundary、rollout safety、edge/standalone contract。
- PostgreSQLmigration checksum、trigger/function、固定 search path、权限撤回、schema/readiness 与 physical-streaming HA gate。
- 完整 18-package clean build/test 退出 0backend 共 1,190 项,1,188 pass、2 条件 skip、0 fail。
- package boundary、cluster dependency、edge import、service-manager bridge import、local image 五项审计零 findingworkspace 保持 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`
+1
View File
@@ -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 |
## 规则
+2 -2
View File
@@ -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,
]),
});
@@ -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',
}),
]),
});
@@ -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}'`,
],
});
@@ -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) => {
@@ -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);
@@ -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({
...identity,
generationSource: this.#generationSource,
lockSource: this.#lockSource,
byteSource: this.#byteSource,
taskSpecSemanticRegistry: this.#registry,
});
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,
});
@@ -528,8 +567,8 @@ export class PluginPackageTaskPublicationRecoveryCoordinator {
published.status === 'current'
? 'settled'
: published.status === 'superseded'
? 'superseded'
: 'manual_required';
? 'superseded'
: 'manual_required';
} catch (error) {
status =
error instanceof InvalidPluginPackageTaskPublicationError ||
@@ -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: value.generation,
lock: value.lock,
manifestBytes: value.manifestBytes,
resources: value.entries,
taskSpecSemanticRegistry: value.registry,
generation: direct.generation,
lock: direct.lock,
manifestBytes: direct.manifestBytes,
resources: direct.entries,
taskSpecSemanticRegistry: direct.registry,
}),
/does not support unresolved package Secret bindings/,
/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,
});
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,
+6 -6
View File
@@ -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' },
},
);