diff --git a/TASKS.md b/TASKS.md index 47e0188..3fc034a 100644 --- a/TASKS.md +++ b/TASKS.md @@ -148,4 +148,5 @@ Progress: - Phase 0 / task `0.5`: введены `PolicyEngine`, `SessionActor`, `PolicyAction`, `PolicyScope`, `PolicyDecision` и default `OwnerOnlyPolicyEngine` в public seam `crank_core::ext::access` - Phase 0 / task `0.6`: введены `AuditSink`, `NoopAuditSink`, `AuditEventId` и базовые audit-типы в public seam `crank_core::ext::audit` - Phase 0 / task `0.7`: введены `CapabilityProfile` и `CommunityCapabilityProfile`, а `admin-api` capability payload теперь собирается через public seam вместо локального literal + - Phase 0 / task `0.13`: введены `RegistryExtension`, `ExtensionMigration` и `apply_extension_migrations(...)` в `crank-registry` как отдельный public seam для additive private migrations - backward-compatible alias `CommunityMachineCredentialVerifier` сохранен, поведение Community не изменено diff --git a/crates/crank-registry/src/ext.rs b/crates/crank-registry/src/ext.rs new file mode 100644 index 0000000..6c6aabe --- /dev/null +++ b/crates/crank-registry/src/ext.rs @@ -0,0 +1,77 @@ +use std::sync::Arc; + +use sqlx::{PgPool, query}; + +use crate::RegistryError; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ExtensionMigration { + pub version: u32, + pub sql: &'static str, +} + +pub trait RegistryExtension: Send + Sync { + fn name(&self) -> &str; + fn migrations(&self) -> &[ExtensionMigration]; +} + +pub async fn apply_extension_migrations( + pool: &PgPool, + extensions: &[Arc], +) -> Result<(), RegistryError> { + query( + "create table if not exists __crank_ext_migrations ( + extension_name text not null, + version integer not null, + applied_at timestamptz not null default now(), + primary key (extension_name, version) + )", + ) + .execute(pool) + .await?; + + for extension in extensions { + for migration in extension.migrations() { + let already_applied = query( + "select 1 + from __crank_ext_migrations + where extension_name = $1 and version = $2", + ) + .bind(extension.name()) + .bind(i32::try_from(migration.version).map_err(|_| { + RegistryError::InvalidNumericValue { + field: "extension_migration.version", + value: migration.version as i64, + } + })?) + .fetch_optional(pool) + .await? + .is_some(); + + if already_applied { + continue; + } + + let mut tx = pool.begin().await?; + + query(migration.sql).execute(&mut *tx).await?; + query( + "insert into __crank_ext_migrations (extension_name, version) + values ($1, $2)", + ) + .bind(extension.name()) + .bind(i32::try_from(migration.version).map_err(|_| { + RegistryError::InvalidNumericValue { + field: "extension_migration.version", + value: migration.version as i64, + } + })?) + .execute(&mut *tx) + .await?; + + tx.commit().await?; + } + } + + Ok(()) +} diff --git a/crates/crank-registry/src/lib.rs b/crates/crank-registry/src/lib.rs index d297d33..bd6b15f 100644 --- a/crates/crank-registry/src/lib.rs +++ b/crates/crank-registry/src/lib.rs @@ -1,9 +1,11 @@ mod error; +mod ext; mod migrations; mod model; mod postgres; pub use error::RegistryError; +pub use ext::{ExtensionMigration, RegistryExtension, apply_extension_migrations}; pub use model::{ AgentSummary, AgentVersionRecord, AsyncJobFilter, AsyncJobRecord, AuthUserRecord, CreateAgentRequest, CreateAsyncJobRequest, CreateInvitationRequest, CreateInvocationLogRequest,