mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): enforce staged secret binding persistence
This commit is contained in:
@@ -104,6 +104,8 @@ import { local0091PluginPackageSecretBindingsMigration } from '../migrations/009
|
||||
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 { local0095PluginPackageSecretBindingTargetGuardMigration } from '../migrations/0095-plugin-package-secret-binding-target-guard';
|
||||
import { local0096CapabilityV48Migration } from '../migrations/0096-capability-v48';
|
||||
import type { LocalSqliteMigrationContext } from '../migrations/sqlMigration';
|
||||
import {
|
||||
LOCAL_SQLITE_MIGRATION_STREAM_ID,
|
||||
@@ -220,6 +222,8 @@ export const localSqliteMigrationDefinition: MigrationStreamDefinition<LocalSqli
|
||||
local0092CapabilityV46Migration,
|
||||
local0093PluginPackageSecretMaterializationGuardMigration,
|
||||
local0094CapabilityV47Migration,
|
||||
local0095PluginPackageSecretBindingTargetGuardMigration,
|
||||
local0096CapabilityV48Migration,
|
||||
]),
|
||||
});
|
||||
|
||||
|
||||
@@ -482,5 +482,15 @@ export const localSqliteMigrationManifest: MigrationStreamManifest =
|
||||
checksum:
|
||||
'6ecbbef0b9d9b3c738cc47a80868e0871c47916fb67e7c37f8620f9099de735e',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0095-plugin-package-secret-binding-target-guard',
|
||||
checksum:
|
||||
'dc0f4051663fef5304f462896aab9a63a259d12933a2d33873cb7397097d4b4f',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0096-capability-v48',
|
||||
checksum:
|
||||
'07118f8e2f1e4f4aa7b9bb95ba9b70276f62de0e63d63cf5dcceacd3532853d9',
|
||||
}),
|
||||
]),
|
||||
});
|
||||
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
import { LOCAL_PLUGIN_PACKAGE_SECRET_BINDING_TARGET_TRIGGER_SQL } from '../plugin-package/secret-binding/pluginPackageSecretBindingTargetSchemaContract';
|
||||
|
||||
export const local0095PluginPackageSecretBindingTargetGuardMigration =
|
||||
defineLocalSqliteMigration({
|
||||
id: '0095-plugin-package-secret-binding-target-guard',
|
||||
statements: [LOCAL_PLUGIN_PACKAGE_SECRET_BINDING_TARGET_TRIGGER_SQL],
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import { CAPABILITIES_V47 } from './0094-capability-v47';
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const CAPABILITIES_V48 = CAPABILITIES_V47.replace(
|
||||
'"plugin_package_secret_binding":1,',
|
||||
'"plugin_package_secret_binding":1,"plugin_package_secret_binding_transition":1,',
|
||||
);
|
||||
|
||||
export const local0096CapabilityV48Migration = defineLocalSqliteMigration({
|
||||
id: '0096-capability-v48',
|
||||
statements: [
|
||||
`UPDATE "QingLong3SchemaCapabilities" SET contract_version = 48, migration_id = '0095-plugin-package-secret-binding-target-guard', capabilities = '${CAPABILITIES_V48}', updated_at_ms = CAST(unixepoch('subsec') * 1000 AS INTEGER) WHERE contract_name = 'local-control-core' AND contract_version = 47 AND migration_id = '0093-plugin-package-secret-materialization-guard' AND capabilities = '${CAPABILITIES_V47}'`,
|
||||
],
|
||||
});
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
export const LOCAL_PLUGIN_PACKAGE_SECRET_BINDING_TARGET_TRIGGER_NAME =
|
||||
'ql3_plugin_package_secret_binding_target_guard' as const;
|
||||
|
||||
export const LOCAL_PLUGIN_PACKAGE_SECRET_BINDING_TARGET_TRIGGER_SQL = `
|
||||
CREATE TRIGGER ${LOCAL_PLUGIN_PACKAGE_SECRET_BINDING_TARGET_TRIGGER_NAME}
|
||||
BEFORE INSERT ON "QingLong3PluginPackageSecretBindings"
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
SELECT CASE WHEN NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM "QingLong3PluginPackageInstallHeads" AS head
|
||||
JOIN "QingLong3PluginPackageInstalls" AS install
|
||||
ON install.installation_id = head.installation_id
|
||||
AND install.project_id = head.project_id
|
||||
AND install.package_name = head.package_name
|
||||
WHERE head.project_id = NEW.project_id
|
||||
AND head.package_name = NEW.package_name
|
||||
AND install.installation_id = NEW.installation_id
|
||||
AND install.lock_digest = NEW.lock_digest
|
||||
AND install.target_generation = NEW.generation
|
||||
AND json_extract(install.lock_json, '$.manifestDigest') =
|
||||
NEW.manifest_digest
|
||||
AND (
|
||||
(
|
||||
install.state = 'active' AND
|
||||
install.active_lock_digest = install.lock_digest
|
||||
) OR (
|
||||
install.state = 'staged' AND
|
||||
install.previous_active_lock_digest IS NOT NULL AND
|
||||
install.active_lock_digest = install.previous_active_lock_digest AND
|
||||
install.target_generation = (
|
||||
SELECT MAX(history.target_generation)
|
||||
FROM "QingLong3PluginPackageInstalls" AS history
|
||||
WHERE history.project_id = install.project_id
|
||||
AND history.package_name = install.package_name
|
||||
) AND
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM "QingLong3PluginPackageInstalls" AS previous
|
||||
WHERE previous.project_id = install.project_id
|
||||
AND previous.package_name = install.package_name
|
||||
AND previous.lock_digest = install.previous_active_lock_digest
|
||||
AND previous.state = 'active'
|
||||
AND previous.active_lock_digest = previous.lock_digest
|
||||
AND previous.target_generation < install.target_generation
|
||||
)
|
||||
)
|
||||
)
|
||||
) THEN RAISE(ABORT,
|
||||
'Plugin Package Secret binding target is not current active or reviewed staged generation')
|
||||
END;
|
||||
END
|
||||
`.trim();
|
||||
@@ -220,10 +220,31 @@ export class LocalSqlitePluginPackageSecretBindingRepository
|
||||
AND install.project_id = ?
|
||||
AND install.package_name = ?
|
||||
AND install.lock_digest = ?
|
||||
AND install.active_lock_digest = ?
|
||||
AND install.target_generation = ?
|
||||
AND install.state = 'active'
|
||||
AND json_extract(install.lock_json, '$.manifestDigest') = ?
|
||||
AND (
|
||||
(install.state = 'active' AND
|
||||
install.active_lock_digest = install.lock_digest) OR
|
||||
(install.state = 'staged' AND
|
||||
install.previous_active_lock_digest IS NOT NULL AND
|
||||
install.active_lock_digest = install.previous_active_lock_digest AND
|
||||
install.target_generation = (
|
||||
SELECT MAX(history.target_generation)
|
||||
FROM "QingLong3PluginPackageInstalls" AS history
|
||||
WHERE history.project_id = install.project_id
|
||||
AND history.package_name = install.package_name
|
||||
) AND
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM "QingLong3PluginPackageInstalls" AS previous
|
||||
WHERE previous.project_id = install.project_id
|
||||
AND previous.package_name = install.package_name
|
||||
AND previous.lock_digest = install.previous_active_lock_digest
|
||||
AND previous.state = 'active'
|
||||
AND previous.active_lock_digest = previous.lock_digest
|
||||
AND previous.target_generation < install.target_generation
|
||||
))
|
||||
)
|
||||
ON CONFLICT (generation_digest) DO NOTHING`,
|
||||
)
|
||||
.run(
|
||||
@@ -243,14 +264,13 @@ export class LocalSqlitePluginPackageSecretBindingRepository
|
||||
binding.target.projectId,
|
||||
binding.target.packageName,
|
||||
binding.target.lockDigest,
|
||||
binding.target.lockDigest,
|
||||
binding.target.generation,
|
||||
binding.target.manifestDigest,
|
||||
);
|
||||
const stored = this.findStored(binding.target.generationDigest);
|
||||
if (!stored) {
|
||||
throw new PluginPackageSecretBindingConflictError(
|
||||
'binding target is not the current active Package generation',
|
||||
'binding target is not the current active or reviewed staged Package generation',
|
||||
);
|
||||
}
|
||||
if (JSON.stringify(stored) !== bindingJson) {
|
||||
|
||||
@@ -3,13 +3,22 @@ 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_PLUGIN_PACKAGE_SECRET_BINDING_TARGET_TRIGGER_SQL } from '../plugin-package/secret-binding/pluginPackageSecretBindingTargetSchemaContract';
|
||||
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 = 47;
|
||||
export const LOCAL_SQLITE_CONTRACT_VERSION = 48;
|
||||
|
||||
const PLUGIN_PACKAGE_SECRET_BINDING_TARGET_TRIGGERS = Object.freeze([
|
||||
Object.freeze({
|
||||
name: 'ql3_plugin_package_secret_binding_target_guard',
|
||||
tableName: 'QingLong3PluginPackageSecretBindings',
|
||||
sql: LOCAL_PLUGIN_PACKAGE_SECRET_BINDING_TARGET_TRIGGER_SQL,
|
||||
}),
|
||||
]);
|
||||
|
||||
const PLUGIN_PACKAGE_SECRET_MATERIALIZATION_TRIGGERS = Object.freeze([
|
||||
Object.freeze({
|
||||
@@ -1752,6 +1761,7 @@ function assertRequiredSchema(client: DatabaseSync): number {
|
||||
const expectedTriggers = [
|
||||
...LOCAL_STEP_RUN_REFERENCE_TRIGGERS,
|
||||
...PLUGIN_PACKAGE_AUTOMATION_DISPOSITION_TRIGGERS,
|
||||
...PLUGIN_PACKAGE_SECRET_BINDING_TARGET_TRIGGERS,
|
||||
...PLUGIN_PACKAGE_SECRET_MATERIALIZATION_TRIGGERS,
|
||||
].sort((left, right) => left.name.localeCompare(right.name));
|
||||
if (
|
||||
@@ -2564,10 +2574,10 @@ export async function auditLocalSqliteReadiness(
|
||||
capability.contract_name !== LOCAL_SQLITE_CONTRACT_NAME ||
|
||||
capability.contract_version !== LOCAL_SQLITE_CONTRACT_VERSION ||
|
||||
capability.migration_id !==
|
||||
'0093-plugin-package-secret-materialization-guard' ||
|
||||
'0095-plugin-package-secret-binding-target-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_secret_materialization":1,"plugin_package_task_reconciliation":1,"project_tool_definition_snapshot":1,"step_run":1,"tool_execution_evidence":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_failure_completion":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"plugin_package_quarantine":1,"plugin_package_lifecycle":1,"plugin_package_automation_publication":1,"plugin_package_automation_security_withdrawal":1,"plugin_package_workflow_admission":1,"plugin_package_workflow_run_list":1,"run_attempt_log_retention":1,"plugin_package_workflow_task_attempt_admission":1}' ||
|
||||
'{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_project_administration":1,"local_security_audit":1,"local_security_audit_compaction":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_identity_credential_administration":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"local_scheduler_admission":1,"plugin_package_install":1,"approved_action":1,"plugin_package_admission":1,"approved_action_execution":1,"plugin_package_proposal":1,"plugin_package_materialized_revision":1,"plugin_package_secret_binding":1,"plugin_package_secret_binding_transition":1,"plugin_package_secret_materialization":1,"plugin_package_task_reconciliation":1,"project_tool_definition_snapshot":1,"step_run":1,"tool_execution_evidence":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_failure_completion":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"plugin_package_quarantine":1,"plugin_package_lifecycle":1,"plugin_package_automation_publication":1,"plugin_package_automation_security_withdrawal":1,"plugin_package_workflow_admission":1,"plugin_package_workflow_run_list":1,"run_attempt_log_retention":1,"plugin_package_workflow_task_attempt_admission":1}' ||
|
||||
typeof capability.updated_at_ms !== 'number' ||
|
||||
!Number.isSafeInteger(capability.updated_at_ms) ||
|
||||
capability.updated_at_ms < 0
|
||||
|
||||
Reference in New Issue
Block a user