feat(ql3): resolve secret action recovery manually

This commit is contained in:
whyour
2026-08-14 12:11:50 +08:00
parent d8489d8a0b
commit 3ca9901055
29 changed files with 3143 additions and 50 deletions
@@ -1027,6 +1027,80 @@ export const approvedActionExecutions = ql3Schema.table(
],
);
export const approvedActionManualRecoveryResolutions = ql3Schema.table(
'approved_action_manual_recovery_resolutions',
{
dispatchId: varchar('dispatch_id', { length: 128 }).primaryKey(),
dispatchDigest: char('dispatch_digest', { length: 64 }).notNull(),
projectId: varchar('project_id', { length: 128 }).notNull(),
actionType: varchar('action_type', { length: 128 }).notNull(),
actionDigest: char('action_digest', { length: 64 }).notNull(),
executionVersion: integer('execution_version').notNull(),
executionDigest: char('execution_digest', { length: 64 }).notNull(),
mutationId: varchar('mutation_id', { length: 128 }).notNull(),
decision: varchar('decision', { length: 32 }).notNull(),
evidenceDigest: char('evidence_digest', { length: 64 }).notNull(),
reasonCode: varchar('reason_code', { length: 64 }).notNull(),
resolvedByType: varchar('resolved_by_type', { length: 16 }).notNull(),
resolvedById: varchar('resolved_by_id', { length: 255 }).notNull(),
authenticationId: varchar('authentication_id', { length: 128 }).notNull(),
assurance: varchar('assurance', { length: 32 }).notNull(),
authenticatedAtMs: bigint('authenticated_at_ms', { mode: 'number' }).notNull(),
projectVersion: integer('project_version').notNull(),
bindingVersion: integer('binding_version').notNull(),
auditEventId: uuid('audit_event_id').notNull(),
resolvedAtMs: bigint('resolved_at_ms', { mode: 'number' }).notNull(),
resolutionJson: jsonb('resolution_json')
.$type<Record<string, unknown>>()
.notNull(),
resolutionDigest: char('resolution_digest', { length: 64 }).notNull(),
},
(table) => [
foreignKey({
name: 'ql3_approved_action_manual_recovery_dispatch_fk',
columns: [table.dispatchId],
foreignColumns: [approvedActionDispatches.dispatchId],
}).onDelete('restrict'),
foreignKey({
name: 'ql3_approved_action_manual_recovery_project_fk',
columns: [table.projectId],
foreignColumns: [projects.id],
}).onDelete('restrict'),
foreignKey({
name: 'ql3_approved_action_manual_recovery_audit_fk',
columns: [table.auditEventId],
foreignColumns: [securityAuditEvents.eventId],
}).onDelete('restrict'),
check(
'ql3_approved_action_manual_recovery_identity_check',
sql`${table.dispatchId} ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' and ${table.projectId} ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' and ${table.actionType} in ('plugin_package.secret_binding.bind','plugin_package.secret_binding.transition') and ${table.executionVersion} between 1 and 2147483647 and ${table.mutationId} ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' and ${table.decision} in ('confirm_failed','abandon_unknown') and ${table.reasonCode} ~ '^[a-z][a-z0-9_]{0,63}$' and ${table.resolvedByType} = 'user' and octet_length(${table.resolvedById}) between 1 and 255 and ${table.resolvedById} !~ '[[:cntrl:]]' and ${table.authenticationId} ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' and ${table.assurance} in ('multi_factor','hardware') and ${table.projectVersion} >= 1 and ${table.bindingVersion} >= 1`,
),
check(
'ql3_approved_action_manual_recovery_digest_check',
sql`${table.dispatchDigest} ~ '^[0-9a-f]{64}$' and ${table.actionDigest} ~ '^[0-9a-f]{64}$' and ${table.executionDigest} ~ '^[0-9a-f]{64}$' and ${table.evidenceDigest} ~ '^[0-9a-f]{64}$' and ${table.resolutionDigest} ~ '^[0-9a-f]{64}$'`,
),
check(
'ql3_approved_action_manual_recovery_time_check',
sql`${table.authenticatedAtMs} >= 0 and ${table.resolvedAtMs} >= ${table.authenticatedAtMs} and ${table.resolvedAtMs} - ${table.authenticatedAtMs} <= 300000`,
),
check(
'ql3_approved_action_manual_recovery_json_check',
sql`jsonb_typeof(${table.resolutionJson}) = 'object' and octet_length(${table.resolutionJson}::text) between 2 and 65536 and ${table.resolutionJson} @> jsonb_build_object('schema', 'qinglong/approved-action-manual-recovery@v1', 'dispatchId', ${table.dispatchId}, 'dispatchDigest', ${table.dispatchDigest}, 'projectId', ${table.projectId}, 'actionType', ${table.actionType}, 'actionDigest', ${table.actionDigest}, 'executionVersion', ${table.executionVersion}, 'executionDigest', ${table.executionDigest}, 'mutationId', ${table.mutationId}, 'decision', ${table.decision}, 'evidenceDigest', ${table.evidenceDigest}, 'reasonCode', ${table.reasonCode}, 'resolvedBy', jsonb_build_object('type', ${table.resolvedByType}, 'id', ${table.resolvedById}), 'authenticationId', ${table.authenticationId}, 'assurance', ${table.assurance}, 'authenticatedAtMs', ${table.authenticatedAtMs}, 'authorizationFence', jsonb_build_object('projectVersion', ${table.projectVersion}, 'bindingVersion', ${table.bindingVersion}), 'auditEventId', ${table.auditEventId}, 'resolvedAtMs', ${table.resolvedAtMs}, 'resolutionDigest', ${table.resolutionDigest})`,
),
uniqueIndex('ql3_approved_action_manual_recovery_mutation_uidx').on(
table.mutationId,
),
uniqueIndex('ql3_approved_action_manual_recovery_digest_uidx').on(
table.resolutionDigest,
),
index('ql3_approved_action_manual_recovery_project_idx').on(
table.projectId,
table.resolvedAtMs,
table.dispatchId,
),
],
);
export const pluginPackageInstallProposals = ql3Schema.table(
'plugin_package_install_proposals',
{
@@ -6103,6 +6177,7 @@ export const ql3PostgresTables = [
approvalRequests,
approvedActionDispatches,
approvedActionExecutions,
approvedActionManualRecoveryResolutions,
pluginPackageInstallProposals,
pluginPackageManagementQuotaBuckets,
workerCredentialManagementQuotaBuckets,
@@ -21,8 +21,8 @@ export interface PostgresSchemaContractTrigger {
export interface PostgresSchemaContract {
readonly schema: 'ql3';
readonly contractName: 'control-core';
readonly contractVersion: 63;
readonly migrationId: 'pg-0064-plugin-package-secret-binding-transition-approval-plans';
readonly contractVersion: 64;
readonly migrationId: 'pg-0065-approved-action-manual-recovery';
readonly minimumServerMajor: 16;
readonly maximumServerMajor: 18;
readonly capabilities: Readonly<{
@@ -38,6 +38,7 @@ export interface PostgresSchemaContract {
api_credential_pepper_binding: 1;
approved_action: 1;
approved_action_execution: 1;
approved_action_manual_recovery: 1;
approval_management_boundary: 1;
automation_management_boundary: 1;
automation_management_identity_keyset_ledger: 1;
@@ -117,8 +118,8 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
Object.freeze({
schema: 'ql3',
contractName: 'control-core',
contractVersion: 63,
migrationId: 'pg-0064-plugin-package-secret-binding-transition-approval-plans',
contractVersion: 64,
migrationId: 'pg-0065-approved-action-manual-recovery',
minimumServerMajor: 16,
maximumServerMajor: 18,
capabilities: Object.freeze({
@@ -127,6 +128,7 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
api_credential_pepper_binding: 1,
approved_action: 1,
approved_action_execution: 1,
approved_action_manual_recovery: 1,
approval_management_boundary: 1,
automation_management_boundary: 1,
automation_management_identity_keyset_ledger: 1,
@@ -770,6 +772,30 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
'execution_json',
'execution_digest',
]),
table('approved_action_manual_recovery_resolutions', [
'dispatch_id',
'dispatch_digest',
'project_id',
'action_type',
'action_digest',
'execution_version',
'execution_digest',
'mutation_id',
'decision',
'evidence_digest',
'reason_code',
'resolved_by_type',
'resolved_by_id',
'authentication_id',
'assurance',
'authenticated_at_ms',
'project_version',
'binding_version',
'audit_event_id',
'resolved_at_ms',
'resolution_json',
'resolution_digest',
]),
table('plugin_package_install_proposals', [
'action_ref',
'project_id',
@@ -1624,6 +1650,10 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
'ql3_approved_action_execution_due_idx',
'ql3_approved_action_execution_recovery_idx',
'ql3_approved_action_execution_project_idx',
'approved_action_manual_recovery_resolutions_pkey',
'ql3_approved_action_manual_recovery_mutation_uidx',
'ql3_approved_action_manual_recovery_digest_uidx',
'ql3_approved_action_manual_recovery_project_idx',
'plugin_package_install_proposals_pkey',
'ql3_plugin_package_proposal_project_idx',
'plugin_package_management_quota_buckets_pkey',
@@ -1942,6 +1972,10 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
'ql3_approved_action_execution_digest_check',
'ql3_approved_action_execution_json_check',
'ql3_approved_action_execution_time_check',
'ql3_approved_action_manual_recovery_identity_check',
'ql3_approved_action_manual_recovery_digest_check',
'ql3_approved_action_manual_recovery_time_check',
'ql3_approved_action_manual_recovery_json_check',
'ql3_plugin_package_proposal_identity_check',
'ql3_plugin_package_proposal_digest_check',
'ql3_plugin_package_proposal_json_check',
@@ -2347,6 +2381,9 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
'ql3_approved_action_dispatch_request_fk',
'ql3_approved_action_dispatch_project_fk',
'ql3_approved_action_execution_dispatch_fk',
'ql3_approved_action_manual_recovery_dispatch_fk',
'ql3_approved_action_manual_recovery_project_fk',
'ql3_approved_action_manual_recovery_audit_fk',
'ql3_approved_action_execution_project_fk',
'ql3_plugin_package_proposal_project_fk',
'ql3_plugin_package_management_quota_project_fk',
@@ -2434,6 +2471,15 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
'ql3_run_retry_policies_run_fk',
]),
functions: Object.freeze([
Object.freeze({
name: 'resolve_approved_action_manual_recovery',
identityArguments:
'p_resolution_json jsonb, p_next_execution_json jsonb, p_audit_json jsonb',
owner: 'ql3_migration',
securityDefiner: true,
volatility: 'volatile',
configuration: Object.freeze(['search_path=pg_catalog, ql3']),
}),
Object.freeze({
name: 'plugin_package_secret_binding_transition_snapshot',
identityArguments:
@@ -384,6 +384,12 @@ const REQUIRED_RUNTIME_PRIVILEGES = Object.freeze({
update: false,
delete: false,
}),
approved_action_manual_recovery_resolutions: Object.freeze({
select: false,
insert: false,
update: false,
delete: false,
}),
plugin_package_install_proposals: Object.freeze({
select: false,
insert: false,
@@ -921,6 +927,12 @@ const REQUIRED_ADMIN_PRIVILEGES = Object.freeze({
update: false,
delete: false,
}),
approved_action_manual_recovery_resolutions: Object.freeze({
select: false,
insert: false,
update: false,
delete: false,
}),
plugin_package_install_proposals: Object.freeze({
select: false,
insert: false,
@@ -1421,7 +1433,10 @@ const REQUIRED_APPROVAL_MANAGER_PRIVILEGES: RequiredPrivileges = Object.freeze(
name === 'schema_capabilities' ||
name === 'projects' ||
name === 'project_role_bindings' ||
name === 'tool_invocation_preview_artifacts'
name === 'tool_invocation_preview_artifacts' ||
name === 'approved_action_dispatches' ||
name === 'approved_action_executions' ||
name === 'approved_action_manual_recovery_resolutions'
? { ...NO_TABLE_PRIVILEGES, select: true }
: name === 'security_audit_events'
? { ...NO_TABLE_PRIVILEGES, select: true, insert: true }
@@ -1573,6 +1588,7 @@ const REQUIRED_RUNTIME_FUNCTION_PRIVILEGES: RequiredFunctionPrivileges =
plugin_package_secret_binding_transition_snapshot: false,
plugin_package_tool_start_allowed: true,
register_plugin_package_automation_disposition_event: false,
resolve_approved_action_manual_recovery: false,
});
const REQUIRED_PACKAGE_MANAGER_FUNCTION_PRIVILEGES: RequiredFunctionPrivileges =
@@ -1598,6 +1614,7 @@ const REQUIRED_PACKAGE_MANAGER_FUNCTION_PRIVILEGES: RequiredFunctionPrivileges =
plugin_package_secret_binding_transition_snapshot: true,
plugin_package_tool_start_allowed: false,
register_plugin_package_automation_disposition_event: false,
resolve_approved_action_manual_recovery: false,
});
const REQUIRED_PACKAGE_EXECUTOR_FUNCTION_PRIVILEGES: RequiredFunctionPrivileges =
@@ -1623,6 +1640,7 @@ const REQUIRED_PACKAGE_EXECUTOR_FUNCTION_PRIVILEGES: RequiredFunctionPrivileges
plugin_package_secret_binding_transition_snapshot: false,
plugin_package_tool_start_allowed: false,
register_plugin_package_automation_disposition_event: false,
resolve_approved_action_manual_recovery: false,
});
const REQUIRED_WORKER_CREDENTIAL_FUNCTION_PRIVILEGES: RequiredFunctionPrivileges =
@@ -1635,6 +1653,7 @@ const REQUIRED_APPROVAL_MANAGER_FUNCTION_PRIVILEGES: RequiredFunctionPrivileges
Object.freeze({
...NO_FUNCTION_PRIVILEGES,
lock_approval_policy_fence: true,
resolve_approved_action_manual_recovery: true,
});
const REQUIRED_RUN_MANAGER_FUNCTION_PRIVILEGES: RequiredFunctionPrivileges =