feat: harden community production foundation through story 1.5

This commit is contained in:
2026-08-14 00:21:59 +03:00
parent c30461cc92
commit f6fc2e5c9b
161 changed files with 16758 additions and 2515 deletions
+4
View File
@@ -2,6 +2,8 @@ use thiserror::Error;
#[derive(Debug, Error)]
pub enum RegistryError {
#[error(transparent)]
Migration(#[from] crate::migrations::MigrationError),
#[error(transparent)]
Storage(#[from] sqlx::Error),
#[error(transparent)]
@@ -80,4 +82,6 @@ pub enum RegistryError {
InvalidEnumRepresentation { field: &'static str },
#[error("invalid numeric value for field {field}: {value}")]
InvalidNumericValue { field: &'static str, value: i64 },
#[error("invalid correlation identity for field {field}")]
InvalidCorrelationIdentity { field: &'static str },
}
+5 -68
View File
@@ -1,77 +1,14 @@
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 checksum: &'static str,
pub source_digest: &'static str,
pub phase: &'static str,
pub compatibility: &'static str,
pub owner: &'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<dyn RegistryExtension>],
) -> 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(())
}
+10 -2
View File
@@ -5,7 +5,11 @@ mod model;
mod postgres;
pub use error::RegistryError;
pub use ext::{ExtensionMigration, RegistryExtension, apply_extension_migrations};
pub use ext::{ExtensionMigration, RegistryExtension};
pub use migrations::{
BackfillBatch, BackfillPolicy, MigrationApplyResult, MigrationAuthority, MigrationDescriptor,
MigrationError, MigrationPreflight,
};
pub mod records {
pub use crate::model::{
@@ -39,8 +43,12 @@ pub mod requests {
}
pub mod infrastructure {
pub use crate::ext::{ExtensionMigration, RegistryExtension, apply_extension_migrations};
pub use crate::ext::{ExtensionMigration, RegistryExtension};
pub use crate::postgres::{PostgresPoolConfig, PostgresPoolConfigError, PostgresRegistry};
pub use crate::{
BackfillBatch, BackfillPolicy, MigrationApplyResult, MigrationAuthority,
MigrationDescriptor, MigrationError, MigrationPreflight,
};
}
pub use model::{
+8 -719
View File
@@ -1,721 +1,10 @@
use sqlx::{PgPool, Postgres, Row, Transaction, query};
mod authority;
mod baseline_v1;
mod schema_guard;
const CORE_MIGRATION_LOCK_ID: i64 = 0x43_52_41_4E_4B;
const BASELINE_VERSION: i32 = 1;
const BASELINE_CHECKSUM: &str = "crank-community-baseline-v1";
pub use authority::{
BackfillBatch, BackfillPolicy, MigrationApplyResult, MigrationAuthority, MigrationDescriptor,
MigrationError, MigrationPreflight,
};
pub async fn apply_postgres(pool: &PgPool) -> Result<(), sqlx::Error> {
let mut transaction = pool.begin().await?;
query("select pg_advisory_xact_lock($1)")
.bind(CORE_MIGRATION_LOCK_ID)
.execute(&mut *transaction)
.await?;
query(
"create table if not exists __crank_core_migrations (
version integer primary key,
description text not null,
checksum text not null,
applied_at timestamptz not null default now()
)",
)
.execute(&mut *transaction)
.await?;
let applied = query(
"select version, checksum
from __crank_core_migrations
order by version",
)
.fetch_all(&mut *transaction)
.await?;
for row in &applied {
let version = row.try_get::<i32, _>("version")?;
let checksum = row.try_get::<String, _>("checksum")?;
if version != BASELINE_VERSION || checksum != BASELINE_CHECKSUM {
return Err(sqlx::Error::Protocol(format!(
"unsupported or modified core migration: version={version}, checksum={checksum}"
)));
}
}
if applied.is_empty() {
apply_baseline(&mut transaction).await?;
query(
"insert into __crank_core_migrations (version, description, checksum)
values ($1, $2, $3)",
)
.bind(BASELINE_VERSION)
.bind("community baseline")
.bind(BASELINE_CHECKSUM)
.execute(&mut *transaction)
.await?;
}
transaction.commit().await?;
Ok(())
}
async fn apply_baseline(transaction: &mut Transaction<'_, Postgres>) -> Result<(), sqlx::Error> {
query(
"create table if not exists workspaces (
id text primary key,
slug text not null unique,
display_name text not null,
status text not null,
settings_json jsonb not null default '{}'::jsonb,
created_at timestamptz not null,
updated_at timestamptz not null
)",
)
.execute(&mut **transaction)
.await?;
query(
"create table if not exists users (
id text primary key,
email text not null unique,
display_name text not null,
password_hash text null,
status text not null,
created_at timestamptz not null
)",
)
.execute(&mut **transaction)
.await?;
query("alter table users add column if not exists password_hash text null")
.execute(&mut **transaction)
.await?;
query(
"insert into users (
id,
email,
display_name,
status,
created_at
) values (
'user_default_owner',
'owner@crank.local',
'Workspace Owner',
'active',
now()
)
on conflict (id) do nothing",
)
.execute(&mut **transaction)
.await?;
query(
"create table if not exists memberships (
workspace_id text not null references workspaces(id) on delete cascade,
user_id text not null references users(id) on delete cascade,
role text not null,
created_at timestamptz not null,
primary key (workspace_id, user_id)
)",
)
.execute(&mut **transaction)
.await?;
query(
"create table if not exists user_sessions (
id text primary key,
user_id text not null references users(id) on delete cascade,
current_workspace_id text null references workspaces(id) on delete set null,
secret_hash text not null,
status text not null,
expires_at timestamptz not null,
last_seen_at timestamptz null,
created_at timestamptz not null
)",
)
.execute(&mut **transaction)
.await?;
query(
"alter table user_sessions
add column if not exists current_workspace_id text null references workspaces(id) on delete set null",
)
.execute(&mut **transaction)
.await?;
query(
"insert into workspaces (
id,
slug,
display_name,
status,
settings_json,
created_at,
updated_at
) values (
'ws_default',
'default',
'Default Workspace',
'active',
'{}'::jsonb,
now(),
now()
)
on conflict (id) do nothing",
)
.execute(&mut **transaction)
.await?;
query(
"insert into memberships (
workspace_id,
user_id,
role,
created_at
) values (
'ws_default',
'user_default_owner',
'owner',
now()
)
on conflict (workspace_id, user_id) do nothing",
)
.execute(&mut **transaction)
.await?;
query(
"create table if not exists invitation_tokens (
id text primary key,
workspace_id text not null references workspaces(id) on delete cascade,
email text not null,
role text not null,
status text not null,
token_hash text not null,
expires_at timestamptz not null,
created_at timestamptz not null
)",
)
.execute(&mut **transaction)
.await?;
query(
"create table if not exists platform_api_keys (
id text primary key,
workspace_id text not null references workspaces(id) on delete cascade,
agent_id text null,
name text not null,
prefix text not null,
secret_hash text not null,
key_kind text not null default 'mcp_client',
scopes_json jsonb not null,
status text not null,
created_at timestamptz not null,
last_used_at timestamptz null,
revoked_at timestamptz null,
expires_at timestamptz null,
allowed_origins_json jsonb not null default '[]'::jsonb
)",
)
.execute(&mut **transaction)
.await?;
query(
"create unique index if not exists platform_api_keys_workspace_name_idx on platform_api_keys(workspace_id, name)",
)
.execute(&mut **transaction)
.await?;
query("alter table platform_api_keys add column if not exists agent_id text null")
.execute(&mut **transaction)
.await?;
query(
"alter table platform_api_keys add column if not exists key_kind text not null default 'mcp_client'",
)
.execute(&mut **transaction)
.await?;
query("alter table platform_api_keys add column if not exists expires_at timestamptz null")
.execute(&mut **transaction)
.await?;
query(
"alter table platform_api_keys add column if not exists allowed_origins_json jsonb not null default '[]'::jsonb",
)
.execute(&mut **transaction)
.await?;
query(
"insert into workspaces (
id,
slug,
display_name,
status,
settings_json,
created_at,
updated_at
) values (
'ws_default',
'default',
'Default Workspace',
'active',
'{}'::jsonb,
now(),
now()
)
on conflict (id) do nothing",
)
.execute(&mut **transaction)
.await?;
query(
"create table if not exists operations (
id text primary key,
workspace_id text null references workspaces(id) on delete cascade,
name text not null,
display_name text not null,
category text not null default 'general',
protocol text not null,
security_level text not null default 'standard',
status text not null,
current_draft_version integer not null default 1,
latest_published_version integer null,
created_at timestamptz not null,
updated_at timestamptz not null,
published_at timestamptz null
)",
)
.execute(&mut **transaction)
.await?;
query("alter table operations add column if not exists workspace_id text null references workspaces(id) on delete cascade")
.execute(&mut **transaction)
.await?;
query(
"alter table operations add column if not exists category text not null default 'general'",
)
.execute(&mut **transaction)
.await?;
query(
"alter table operations add column if not exists security_level text not null default 'standard'",
)
.execute(&mut **transaction)
.await?;
query("update operations set workspace_id = 'ws_default' where workspace_id is null")
.execute(&mut **transaction)
.await?;
query("alter table operations alter column workspace_id set not null")
.execute(&mut **transaction)
.await?;
query("alter table operations drop constraint if exists operations_name_key")
.execute(&mut **transaction)
.await?;
query(
"create unique index if not exists operations_workspace_name_idx on operations(workspace_id, name)",
)
.execute(&mut **transaction)
.await?;
query(
"create table if not exists operation_versions (
operation_id text not null references operations(id) on delete cascade,
version integer not null,
status text not null,
target_json jsonb not null,
input_schema_json jsonb not null,
output_schema_json jsonb not null,
input_mapping_json jsonb not null,
output_mapping_json jsonb not null,
execution_config_json jsonb not null,
tool_description_json jsonb not null,
samples_json jsonb null,
generated_draft_json jsonb null,
config_export_json jsonb null,
wizard_state_json jsonb null,
change_note text null,
created_at timestamptz not null,
created_by text null,
primary key (operation_id, version)
)",
)
.execute(&mut **transaction)
.await?;
query("alter table operation_versions add column if not exists wizard_state_json jsonb null")
.execute(&mut **transaction)
.await?;
query(
"create table if not exists published_operations (
operation_id text primary key references operations(id) on delete cascade,
version integer not null,
published_at timestamptz not null,
published_by text null,
foreign key (operation_id, version) references operation_versions(operation_id, version) on delete cascade
)",
)
.execute(&mut **transaction)
.await?;
query(
"create table if not exists operation_samples (
id text primary key,
operation_id text not null references operations(id) on delete cascade,
version integer not null,
sample_kind text not null,
storage_ref text not null,
content_type text not null,
file_name text null,
created_at timestamptz not null,
foreign key (operation_id, version) references operation_versions(operation_id, version) on delete cascade
)",
)
.execute(&mut **transaction)
.await?;
query(
"create table if not exists descriptors (
id text primary key,
operation_id text null references operations(id) on delete cascade,
version integer null,
descriptor_kind text not null,
storage_ref text not null,
source_name text null,
package_index_json jsonb null,
created_at timestamptz not null,
foreign key (operation_id, version) references operation_versions(operation_id, version) on delete cascade
)",
)
.execute(&mut **transaction)
.await?;
query(
"create table if not exists auth_profiles (
id text primary key,
workspace_id text null references workspaces(id) on delete cascade,
name text not null,
kind text not null,
config_json jsonb not null,
created_at timestamptz not null,
updated_at timestamptz not null
)",
)
.execute(&mut **transaction)
.await?;
query("alter table auth_profiles add column if not exists workspace_id text null references workspaces(id) on delete cascade")
.execute(&mut **transaction)
.await?;
query("update auth_profiles set workspace_id = 'ws_default' where workspace_id is null")
.execute(&mut **transaction)
.await?;
query("alter table auth_profiles alter column workspace_id set not null")
.execute(&mut **transaction)
.await?;
query("alter table auth_profiles drop constraint if exists auth_profiles_name_key")
.execute(&mut **transaction)
.await?;
query(
"create unique index if not exists auth_profiles_workspace_name_idx on auth_profiles(workspace_id, name)",
)
.execute(&mut **transaction)
.await?;
query(
"create table if not exists workspace_upstreams (
id text primary key,
workspace_id text not null references workspaces(id) on delete cascade,
name text not null,
base_url text not null,
static_headers_json jsonb not null default '{}'::jsonb,
auth_profile_id text null references auth_profiles(id) on delete set null,
created_at timestamptz not null,
updated_at timestamptz not null
)",
)
.execute(&mut **transaction)
.await?;
query(
"create unique index if not exists workspace_upstreams_workspace_name_idx on workspace_upstreams(workspace_id, name)",
)
.execute(&mut **transaction)
.await?;
query(
"create unique index if not exists workspace_upstreams_workspace_base_auth_idx on workspace_upstreams(workspace_id, base_url, coalesce(auth_profile_id, ''))",
)
.execute(&mut **transaction)
.await?;
query(
"insert into workspace_upstreams (
id,
workspace_id,
name,
base_url,
static_headers_json,
auth_profile_id,
created_at,
updated_at
)
select
'upstream_frankfurter_' || w.id,
w.id,
'Frankfurter',
'https://api.frankfurter.dev',
'{}'::jsonb,
null,
now(),
now()
from workspaces w
where not exists (
select 1
from workspace_upstreams wu
where wu.workspace_id = w.id
and wu.name = 'Frankfurter'
)",
)
.execute(&mut **transaction)
.await?;
query(
"create table if not exists secrets (
id text primary key,
workspace_id text not null references workspaces(id) on delete cascade,
name text not null,
kind text not null,
status text not null,
current_version integer not null,
last_used_at timestamptz null,
created_at timestamptz not null,
updated_at timestamptz not null
)",
)
.execute(&mut **transaction)
.await?;
query(
"create unique index if not exists secrets_workspace_name_idx on secrets(workspace_id, name)",
)
.execute(&mut **transaction)
.await?;
query(
"create table if not exists secret_versions (
secret_id text not null references secrets(id) on delete cascade,
version integer not null,
ciphertext text not null,
key_version text not null,
created_at timestamptz not null,
created_by text null references users(id) on delete set null,
primary key (secret_id, version)
)",
)
.execute(&mut **transaction)
.await?;
query(
"create table if not exists yaml_import_jobs (
id text primary key,
source_sample_id text null references operation_samples(id) on delete set null,
status text not null,
format_version text not null,
mode text not null,
result_operation_id text null references operations(id) on delete set null,
result_version integer null,
error_text text null,
created_at timestamptz not null,
finished_at timestamptz null
)",
)
.execute(&mut **transaction)
.await?;
query(
"create table if not exists import_jobs (
id text primary key,
workspace_id text not null references workspaces(id) on delete cascade,
kind text not null,
source_format text not null,
source_version text null,
status text not null,
preview_payload jsonb not null,
created_operation_ids jsonb not null default '[]'::jsonb,
error_text text null,
created_at timestamptz not null,
expires_at timestamptz not null,
finished_at timestamptz null
)",
)
.execute(&mut **transaction)
.await?;
query(
"create table if not exists agents (
id text primary key,
workspace_id text not null references workspaces(id) on delete cascade,
slug text not null,
display_name text not null,
description text not null,
status text not null,
current_draft_version integer not null default 1,
latest_published_version integer null,
created_at timestamptz not null,
updated_at timestamptz not null,
published_at timestamptz null
)",
)
.execute(&mut **transaction)
.await?;
query(
"create unique index if not exists agents_workspace_slug_idx on agents(workspace_id, slug)",
)
.execute(&mut **transaction)
.await?;
query(
"create table if not exists agent_versions (
agent_id text not null references agents(id) on delete cascade,
version integer not null,
status text not null,
instructions_json jsonb not null,
tool_selection_policy_json jsonb not null,
created_at timestamptz not null,
primary key (agent_id, version)
)",
)
.execute(&mut **transaction)
.await?;
query(
"create table if not exists agent_operation_bindings (
agent_id text not null references agents(id) on delete cascade,
agent_version integer not null,
operation_id text not null references operations(id) on delete cascade,
operation_version integer not null,
tool_name text not null,
tool_title text not null,
tool_description_override text null,
enabled boolean not null default true,
foreign key (agent_id, agent_version) references agent_versions(agent_id, version) on delete cascade,
foreign key (operation_id, operation_version) references operation_versions(operation_id, version) on delete cascade
)",
)
.execute(&mut **transaction)
.await?;
query(
"create unique index if not exists agent_bindings_tool_name_idx on agent_operation_bindings(agent_id, agent_version, tool_name)",
)
.execute(&mut **transaction)
.await?;
query(
"create table if not exists published_agents (
agent_id text primary key references agents(id) on delete cascade,
version integer not null,
published_at timestamptz not null,
published_by text null,
foreign key (agent_id, version) references agent_versions(agent_id, version) on delete cascade
)",
)
.execute(&mut **transaction)
.await?;
query(
"create table if not exists approval_requests (
id text primary key,
workspace_id text not null references workspaces(id) on delete cascade,
agent_id text not null references agents(id) on delete cascade,
operation_id text not null references operations(id) on delete cascade,
operation_version integer not null,
status text not null,
risk_level text not null,
request_payload_json jsonb not null,
response_payload_json jsonb null,
created_at timestamptz not null,
expires_at timestamptz not null,
decided_at timestamptz null,
decided_by_key_id text null references platform_api_keys(id) on delete set null,
decision_note text null
)",
)
.execute(&mut **transaction)
.await?;
query("alter table approval_requests add column if not exists execution_started_at timestamptz null")
.execute(&mut **transaction)
.await?;
query("alter table approval_requests add column if not exists execution_attempts integer not null default 0")
.execute(&mut **transaction)
.await?;
query("alter table approval_requests add column if not exists request_fingerprint text null")
.execute(&mut **transaction)
.await?;
query(
"create unique index if not exists approval_requests_pending_fingerprint_idx
on approval_requests(agent_id, operation_id, operation_version, request_fingerprint)
where status = 'pending' and request_fingerprint is not null",
)
.execute(&mut **transaction)
.await?;
query(
"create index if not exists approval_requests_agent_status_idx
on approval_requests(workspace_id, agent_id, status, expires_at)",
)
.execute(&mut **transaction)
.await?;
query(
"create table if not exists invocation_logs (
id text primary key,
workspace_id text not null references workspaces(id) on delete cascade,
agent_id text null references agents(id) on delete set null,
operation_id text not null references operations(id) on delete cascade,
source text not null,
level text not null,
status text not null,
tool_name text not null,
message text not null,
request_id text null,
status_code integer null,
duration_ms bigint not null,
error_kind text null,
request_preview_json jsonb not null,
response_preview_json jsonb not null,
created_at timestamptz not null
)",
)
.execute(&mut **transaction)
.await?;
query(
"create index if not exists invocation_logs_workspace_created_idx on invocation_logs(workspace_id, created_at desc)",
)
.execute(&mut **transaction)
.await?;
query(
"create index if not exists invocation_logs_workspace_operation_created_idx on invocation_logs(workspace_id, operation_id, created_at desc)",
)
.execute(&mut **transaction)
.await?;
query(
"create index if not exists invocation_logs_workspace_agent_created_idx on invocation_logs(workspace_id, agent_id, created_at desc)",
)
.execute(&mut **transaction)
.await?;
query(
"create table if not exists usage_rollups (
workspace_id text not null references workspaces(id) on delete cascade,
agent_id text null references agents(id) on delete cascade,
operation_id text null references operations(id) on delete cascade,
period text not null,
calls_total bigint not null,
calls_ok bigint not null,
calls_error bigint not null,
p50_ms bigint not null,
p95_ms bigint not null,
p99_ms bigint not null,
updated_at timestamptz not null
)",
)
.execute(&mut **transaction)
.await?;
Ok(())
}
use baseline_v1::{BASELINE_CHECKSUM, BASELINE_VERSION, apply_baseline};
@@ -0,0 +1,926 @@
use std::fmt;
use sha2::{Digest, Sha256};
use sqlx::{PgConnection, PgPool, Row, Transaction, query};
use super::schema_guard::{
OWNED_RELATIONS, relation_exists, validate_required_relations, validate_schema_fingerprint,
};
use super::{BASELINE_CHECKSUM, BASELINE_VERSION, apply_baseline};
use crate::ext::ExtensionMigration;
const MIGRATION_LOCK_ID: i64 = 0x4352_414E_4B4D_4947;
const CURRENT_VERSION: i64 = 3;
const IMPLEMENTED_VERSIONS: &[i64] = &[1, 2, 3];
const BASELINE_SOURCE_SHA256: &str =
"eb1656fc5b4b5be9ee390d237d1d58e4b2274ae5ba9b7ba06a2f3f860dfda675";
const CONSOLIDATION_SOURCE: &str = include_str!("consolidation_v2.sql");
const CONSOLIDATION_SOURCE_SHA256: &str =
"1908b97ca7fe8a85d146b0eebf007d5018a9ff406cb099614270f3428ec1db48";
const REQUEST_TRACE_IDENTITY_SOURCE: &str = include_str!("request_trace_identity_v3.sql");
const REQUEST_TRACE_IDENTITY_SOURCE_SHA256: &str =
"36487625503a8d4d8f18d5771c3c9b6705f845e267acbfcb7244c330f640cd94";
const BASELINE_RELATIONS: &[&str] = &[
"workspaces",
"users",
"memberships",
"user_sessions",
"invitation_tokens",
"platform_api_keys",
"operations",
"operation_versions",
"published_operations",
"operation_samples",
"descriptors",
"agents",
"agent_versions",
"published_agents",
"agent_operation_bindings",
"secrets",
"secret_versions",
"auth_profiles",
"workspace_upstreams",
"yaml_import_jobs",
"import_jobs",
"approval_requests",
"invocation_logs",
"usage_rollups",
];
const CONSOLIDATION_RELATIONS: &[&str] = &[
"__crank_migrations",
"__crank_migration_legacy_audit",
"__crank_mcp_migrations",
"mcp_transport_sessions",
"__crank_ext_migrations",
];
const REGISTERED_EXTENSION_MIGRATIONS: &[(&str, ExtensionMigration)] = &[];
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MigrationDescriptor {
pub version: i64,
pub name: &'static str,
pub checksum: String,
pub source_digest: String,
pub phase: &'static str,
pub compatibility: &'static str,
pub owner: &'static str,
pub transactional: bool,
pub backfill: BackfillPolicy,
pub readable_schema_min: i64,
pub readable_schema_max: i64,
pub contract_evidence: Option<&'static str>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BackfillPolicy {
None,
Bounded {
max_batch_rows: u32,
max_batch_ms: u32,
resumable: bool,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BackfillBatch {
pub cursor: Option<String>,
pub max_rows: u32,
pub max_ms: u32,
}
impl BackfillBatch {
pub fn validate(&self, policy: BackfillPolicy) -> Result<(), MigrationError> {
match policy {
BackfillPolicy::Bounded {
max_batch_rows,
max_batch_ms,
resumable: true,
} if (1..=max_batch_rows).contains(&self.max_rows)
&& (1..=max_batch_ms).contains(&self.max_ms)
&& self
.cursor
.as_ref()
.is_none_or(|cursor| cursor.len() <= 256) =>
{
Ok(())
}
_ => Err(MigrationError::new(
"invalid_contract",
"contract.backfill",
None,
"contact_operator",
)),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MigrationPreflight {
Current { version: i64 },
MigrationRequired { current: i64, target: i64 },
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MigrationApplyResult {
Applied { from: i64, to: i64 },
AlreadyCurrent { version: i64 },
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MigrationError {
code: &'static str,
stage: &'static str,
version: Option<i64>,
recovery: &'static str,
}
impl MigrationError {
pub(super) fn new(
code: &'static str,
stage: &'static str,
version: Option<i64>,
recovery: &'static str,
) -> Self {
Self {
code,
stage,
version,
recovery,
}
}
pub(super) fn storage(stage: &'static str) -> Self {
Self::new("storage_unavailable", stage, None, "contact_operator")
}
pub fn code(&self) -> &'static str {
self.code
}
pub fn stage(&self) -> &'static str {
self.stage
}
pub fn version(&self) -> Option<i64> {
self.version
}
pub fn recovery(&self) -> &'static str {
self.recovery
}
}
impl fmt::Display for MigrationError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"migration_error code={} stage={} version={} recovery={}",
self.code,
self.stage,
self.version
.map_or_else(|| "none".to_owned(), |value| value.to_string()),
self.recovery
)
}
}
impl std::error::Error for MigrationError {}
pub struct MigrationAuthority;
impl MigrationAuthority {
pub fn registered_extension_migrations() -> &'static [(&'static str, ExtensionMigration)] {
REGISTERED_EXTENSION_MIGRATIONS
}
pub fn sequence() -> Vec<MigrationDescriptor> {
vec![
MigrationDescriptor {
version: i64::from(BASELINE_VERSION),
name: "community-baseline-v1",
checksum: BASELINE_CHECKSUM.to_owned(),
source_digest: BASELINE_SOURCE_SHA256.to_owned(),
phase: "expand",
compatibility: "legacy-baseline",
owner: "crank-registry",
transactional: true,
backfill: BackfillPolicy::None,
readable_schema_min: 1,
readable_schema_max: 1,
contract_evidence: None,
},
MigrationDescriptor {
version: 2,
name: "legacy-consolidation-v2",
checksum: CONSOLIDATION_SOURCE_SHA256.to_owned(),
source_digest: CONSOLIDATION_SOURCE_SHA256.to_owned(),
phase: "expand",
compatibility: "n-minus-one-readable",
owner: "crank-registry",
transactional: true,
backfill: BackfillPolicy::None,
readable_schema_min: 1,
readable_schema_max: 2,
contract_evidence: None,
},
MigrationDescriptor {
version: 3,
name: "request-trace-identity-v3",
checksum: REQUEST_TRACE_IDENTITY_SOURCE_SHA256.to_owned(),
source_digest: REQUEST_TRACE_IDENTITY_SOURCE_SHA256.to_owned(),
phase: "expand",
compatibility: "n-minus-one-readable",
owner: "crank-registry",
transactional: true,
backfill: BackfillPolicy::None,
readable_schema_min: 2,
readable_schema_max: 3,
contract_evidence: None,
},
]
}
pub fn validate_sequence() -> Result<(), MigrationError> {
validate_descriptors(&Self::sequence())
}
pub async fn preflight(pool: &PgPool) -> Result<MigrationPreflight, MigrationError> {
Self::validate_sequence()?;
let mut connection = pool
.acquire()
.await
.map_err(|_| MigrationError::storage("preflight.connect"))?;
inspect(&mut connection).await
}
pub async fn require_current(pool: &PgPool) -> Result<(), MigrationError> {
match Self::preflight(pool).await? {
MigrationPreflight::Current { .. } => Ok(()),
MigrationPreflight::MigrationRequired { current: 0, .. } => Err(MigrationError::new(
"schema_missing",
"preflight.compatibility",
Some(0),
"run_controlled_migration",
)),
MigrationPreflight::MigrationRequired { current, .. } => Err(MigrationError::new(
"migration_required",
"preflight.compatibility",
Some(current),
"run_controlled_migration",
)),
}
}
pub async fn apply(pool: &PgPool) -> Result<MigrationApplyResult, MigrationError> {
Self::validate_sequence()?;
let mut transaction = pool
.begin()
.await
.map_err(|_| MigrationError::storage("apply.begin"))?;
query("set local lock_timeout = '30s'")
.execute(&mut *transaction)
.await
.map_err(|_| MigrationError::storage("apply.lock_policy"))?;
query("select pg_advisory_xact_lock($1)")
.bind(MIGRATION_LOCK_ID)
.execute(&mut *transaction)
.await
.map_err(|error| {
if error
.as_database_error()
.and_then(|database| database.code())
.is_some_and(|code| matches!(code.as_ref(), "55P03" | "57014"))
{
MigrationError::new("lock_timeout", "apply.lock", None, "run_preflight")
} else {
MigrationError::storage("apply.lock")
}
})?;
let before = inspect(&mut transaction).await?;
let from = match before {
MigrationPreflight::Current { version } => {
transaction
.commit()
.await
.map_err(|_| MigrationError::storage("apply.commit"))?;
return Ok(MigrationApplyResult::AlreadyCurrent { version });
}
MigrationPreflight::MigrationRequired { current, .. } => current,
};
if from == 0 {
create_core_ledger(&mut transaction).await?;
apply_baseline(&mut transaction).await.map_err(|_| {
MigrationError::new(
"apply_failed",
"apply.baseline",
Some(1),
"restore_known_good_backup",
)
})?;
query(
"insert into __crank_core_migrations (version, description, checksum)
values (1, 'community baseline', $1)",
)
.bind(BASELINE_CHECKSUM)
.execute(&mut *transaction)
.await
.map_err(|_| {
MigrationError::new(
"apply_failed",
"apply.baseline_ledger",
Some(1),
"restore_known_good_backup",
)
})?;
}
if from < 2 {
apply_consolidation(&mut transaction).await?;
}
if from < 3 {
apply_request_trace_identity(&mut transaction).await?;
}
transaction
.commit()
.await
.map_err(|_| MigrationError::storage("apply.commit"))?;
Ok(MigrationApplyResult::Applied {
from,
to: CURRENT_VERSION,
})
}
}
fn sha256_hex(bytes: &[u8]) -> String {
format!("{:x}", Sha256::digest(bytes))
}
#[cfg(test)]
fn baseline_source_digest() -> String {
let source = include_str!("baseline_v1.rs");
let (_, baseline) = source
.split_once("// baseline-v1:start\n")
.expect("baseline start marker must exist");
let (baseline, _) = baseline
.split_once("// baseline-v1:end")
.expect("baseline end marker must exist");
sha256_hex(baseline.as_bytes())
}
fn validate_descriptors(descriptors: &[MigrationDescriptor]) -> Result<(), MigrationError> {
if descriptors.is_empty() || descriptors.len() > 1_024 {
return Err(MigrationError::new(
"invalid_contract",
"contract.sequence",
None,
"contact_operator",
));
}
for (index, descriptor) in descriptors.iter().enumerate() {
let expected_version = i64::try_from(index + 1).unwrap_or(i64::MAX);
let valid_name = !descriptor.name.is_empty()
&& descriptor.name.len() <= 128
&& descriptor
.name
.bytes()
.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-')
&& !descriptors[..index]
.iter()
.any(|prior| prior.name == descriptor.name);
let valid_source_digest = descriptor.source_digest.len() == 64
&& descriptor
.source_digest
.bytes()
.all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase());
let valid_checksum = if descriptor.version == 1 {
descriptor.checksum == BASELINE_CHECKSUM
} else {
descriptor.checksum == descriptor.source_digest
};
let valid_window = descriptor.readable_schema_min >= 1
&& descriptor.readable_schema_min <= descriptor.readable_schema_max
&& descriptor.readable_schema_max <= descriptor.version;
let valid_phase = match descriptor.phase {
"expand" => descriptor.backfill == BackfillPolicy::None,
"migrate" => matches!(
descriptor.backfill,
BackfillPolicy::Bounded {
max_batch_rows: 1..=10_000,
max_batch_ms: 1..=60_000,
resumable: true,
}
),
"contract" => {
descriptor.compatibility == "window-closed"
&& descriptor.contract_evidence.is_some_and(|evidence| {
!evidence.is_empty()
&& evidence.len() <= 256
&& !evidence.starts_with('/')
&& !evidence.contains("..")
})
&& descriptor.backfill == BackfillPolicy::None
}
_ => false,
};
let valid_compatibility = matches!(
descriptor.compatibility,
"legacy-baseline" | "n-minus-one-readable" | "window-open" | "window-closed"
);
let valid_owner = !descriptor.owner.is_empty()
&& descriptor.owner.len() <= 128
&& descriptor
.owner
.bytes()
.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-');
if descriptor.version != expected_version
|| !valid_name
|| !valid_source_digest
|| !valid_checksum
|| !valid_phase
|| !valid_compatibility
|| !valid_owner
|| !valid_window
|| !descriptor.transactional
{
return Err(MigrationError::new(
"invalid_contract",
"contract.sequence",
Some(descriptor.version),
"contact_operator",
));
}
}
if descriptors.len() != usize::try_from(CURRENT_VERSION).unwrap_or_default()
|| descriptors
.iter()
.map(|descriptor| descriptor.version)
.ne(IMPLEMENTED_VERSIONS.iter().copied())
|| sha256_hex(CONSOLIDATION_SOURCE.as_bytes()) != CONSOLIDATION_SOURCE_SHA256
|| sha256_hex(REQUEST_TRACE_IDENTITY_SOURCE.as_bytes())
!= REQUEST_TRACE_IDENTITY_SOURCE_SHA256
{
return Err(MigrationError::new(
"invalid_contract",
"contract.implementation",
Some(CURRENT_VERSION),
"contact_operator",
));
}
Ok(())
}
async fn inspect(connection: &mut PgConnection) -> Result<MigrationPreflight, MigrationError> {
let core_exists = relation_exists(connection, "__crank_core_migrations").await?;
let canonical_exists = relation_exists(connection, "__crank_migrations").await?;
if !core_exists {
let mut owned_exists = canonical_exists;
for relation in OWNED_RELATIONS {
owned_exists |= relation_exists(connection, relation).await?;
}
if owned_exists {
return Err(MigrationError::new(
"partial_sequence",
"preflight.core",
None,
"restore_known_good_backup",
));
}
inspect_optional_legacy(connection).await?;
return Ok(MigrationPreflight::MigrationRequired {
current: 0,
target: CURRENT_VERSION,
});
}
validate_core_ledger(connection).await?;
validate_required_relations(connection, BASELINE_RELATIONS, 1).await?;
inspect_optional_legacy(connection).await?;
if !canonical_exists {
return Ok(MigrationPreflight::MigrationRequired {
current: 1,
target: CURRENT_VERSION,
});
}
let descriptors = MigrationAuthority::sequence();
let rows = query("select version, name, checksum, phase, compatibility from __crank_migrations order by version limit 1025")
.fetch_all(&mut *connection)
.await
.map_err(|_| MigrationError::storage("preflight.canonical"))?;
if rows.is_empty() {
return Err(MigrationError::new(
"partial_sequence",
"preflight.canonical",
None,
"restore_known_good_backup",
));
}
for (index, row) in rows.iter().enumerate() {
let version = row
.try_get::<i64, _>("version")
.map_err(|_| MigrationError::storage("preflight.canonical"))?;
if version > CURRENT_VERSION {
return Err(MigrationError::new(
"future_version",
"preflight.canonical",
Some(version),
"install_matching_application",
));
}
let Some(expected) = descriptors.get(index) else {
return Err(MigrationError::new(
"future_version",
"preflight.canonical",
Some(version),
"install_matching_application",
));
};
if version != expected.version {
return Err(MigrationError::new(
"partial_sequence",
"preflight.canonical",
Some(version),
"restore_known_good_backup",
));
}
let checksum = row
.try_get::<String, _>("checksum")
.map_err(|_| MigrationError::storage("preflight.canonical"))?;
if checksum != expected.checksum {
return Err(MigrationError::new(
"checksum_mismatch",
"preflight.canonical",
Some(version),
"restore_known_good_backup",
));
}
let name = row
.try_get::<String, _>("name")
.map_err(|_| MigrationError::storage("preflight.canonical"))?;
let phase = row
.try_get::<String, _>("phase")
.map_err(|_| MigrationError::storage("preflight.canonical"))?;
let compatibility = row
.try_get::<String, _>("compatibility")
.map_err(|_| MigrationError::storage("preflight.canonical"))?;
if name != expected.name
|| phase != expected.phase
|| compatibility != expected.compatibility
{
return Err(MigrationError::new(
"checksum_mismatch",
"preflight.metadata",
Some(version),
"restore_known_good_backup",
));
}
}
let current = rows
.last()
.and_then(|row| row.try_get::<i64, _>("version").ok())
.ok_or_else(|| MigrationError::storage("preflight.canonical"))?;
if rows.len() != usize::try_from(current).unwrap_or_default() {
return Err(MigrationError::new(
"partial_sequence",
"preflight.canonical",
Some(current),
"restore_known_good_backup",
));
}
validate_schema_fingerprint(connection, current).await?;
if current < CURRENT_VERSION {
Ok(MigrationPreflight::MigrationRequired {
current,
target: CURRENT_VERSION,
})
} else {
validate_required_relations(connection, CONSOLIDATION_RELATIONS, CURRENT_VERSION).await?;
validate_schema_fingerprint(connection, CURRENT_VERSION).await?;
validate_legacy_audit(connection).await?;
Ok(MigrationPreflight::Current { version: current })
}
}
async fn validate_core_ledger(connection: &mut PgConnection) -> Result<(), MigrationError> {
let rows = query("select version, description, checksum from __crank_core_migrations order by version limit 2")
.fetch_all(connection)
.await
.map_err(|_| MigrationError::storage("preflight.core"))?;
if rows.len() != 1 {
return Err(MigrationError::new(
"partial_sequence",
"preflight.core",
None,
"restore_known_good_backup",
));
}
let version = rows[0]
.try_get::<i32, _>("version")
.map_err(|_| MigrationError::storage("preflight.core"))?;
let checksum = rows[0]
.try_get::<String, _>("checksum")
.map_err(|_| MigrationError::storage("preflight.core"))?;
let description = rows[0]
.try_get::<String, _>("description")
.map_err(|_| MigrationError::storage("preflight.core"))?;
if version != BASELINE_VERSION
|| description != "community baseline"
|| checksum != BASELINE_CHECKSUM
{
return Err(MigrationError::new(
"checksum_mismatch",
"preflight.core",
Some(i64::from(version)),
"restore_known_good_backup",
));
}
Ok(())
}
async fn inspect_optional_legacy(connection: &mut PgConnection) -> Result<(), MigrationError> {
let mcp_ledger = relation_exists(connection, "__crank_mcp_migrations").await?;
let mcp_sessions = relation_exists(connection, "mcp_transport_sessions").await?;
if mcp_ledger != mcp_sessions {
return Err(MigrationError::new(
"legacy_conflict",
"preflight.legacy_mcp",
None,
"restore_known_good_backup",
));
}
if mcp_ledger {
let rows =
query("select version, checksum from __crank_mcp_migrations order by version limit 2")
.fetch_all(&mut *connection)
.await
.map_err(|_| MigrationError::storage("preflight.legacy_mcp"))?;
if rows.len() != 1
|| rows[0].try_get::<i32, _>("version").ok() != Some(1)
|| rows[0].try_get::<String, _>("checksum").ok().as_deref()
!= Some("mcp-transport-sessions-v1")
{
return Err(MigrationError::new(
"legacy_conflict",
"preflight.legacy_mcp",
None,
"restore_known_good_backup",
));
}
}
if relation_exists(connection, "__crank_ext_migrations").await? {
let count = query("select count(*)::bigint as count from __crank_ext_migrations")
.fetch_one(&mut *connection)
.await
.map_err(|_| MigrationError::storage("preflight.legacy_extension"))?
.try_get::<i64, _>("count")
.map_err(|_| MigrationError::storage("preflight.legacy_extension"))?;
if count > 1_000 {
return Err(MigrationError::new(
"legacy_conflict",
"preflight.legacy_extension",
None,
"contact_operator",
));
}
if count > 0 {
let checksum_column = query(
"select exists (
select 1 from information_schema.columns
where table_schema = current_schema()
and table_name = '__crank_ext_migrations'
and column_name = 'checksum'
) as present",
)
.fetch_one(&mut *connection)
.await
.map_err(|_| MigrationError::storage("preflight.legacy_extension"))?
.try_get::<bool, _>("present")
.map_err(|_| MigrationError::storage("preflight.legacy_extension"))?;
if !checksum_column {
return Err(MigrationError::new(
"legacy_conflict",
"preflight.legacy_extension",
None,
"contact_operator",
));
}
let rows = query(
"select extension_name, version, checksum
from __crank_ext_migrations
order by extension_name, version
limit 1001",
)
.fetch_all(&mut *connection)
.await
.map_err(|_| MigrationError::storage("preflight.legacy_extension"))?;
for row in rows {
let name = row
.try_get::<String, _>("extension_name")
.map_err(|_| MigrationError::storage("preflight.legacy_extension"))?;
let version = row
.try_get::<i32, _>("version")
.map_err(|_| MigrationError::storage("preflight.legacy_extension"))?;
let checksum = row
.try_get::<Option<String>, _>("checksum")
.map_err(|_| MigrationError::storage("preflight.legacy_extension"))?;
let registered = MigrationAuthority::registered_extension_migrations()
.iter()
.any(|(registered_name, descriptor)| {
*registered_name == name
&& descriptor.version == u32::try_from(version).unwrap_or_default()
&& checksum.as_deref() == Some(descriptor.checksum)
});
if !registered {
return Err(MigrationError::new(
"legacy_conflict",
"preflight.legacy_extension",
Some(i64::from(version)),
"contact_operator",
));
}
}
}
}
Ok(())
}
async fn validate_legacy_audit(connection: &mut PgConnection) -> Result<(), MigrationError> {
let rows = query(
"select source, source_version, source_checksum
from __crank_migration_legacy_audit
order by source, source_version
limit 1002",
)
.fetch_all(&mut *connection)
.await
.map_err(|_| MigrationError::storage("preflight.legacy_audit"))?;
if rows.len() > 1001 {
return Err(MigrationError::new(
"legacy_conflict",
"preflight.legacy_audit",
None,
"contact_operator",
));
}
for row in rows {
let source = row
.try_get::<String, _>("source")
.map_err(|_| MigrationError::storage("preflight.legacy_audit"))?;
let version = row
.try_get::<i64, _>("source_version")
.map_err(|_| MigrationError::storage("preflight.legacy_audit"))?;
let checksum = row
.try_get::<String, _>("source_checksum")
.map_err(|_| MigrationError::storage("preflight.legacy_audit"))?;
let valid = (source == "core" && version == 1 && checksum == BASELINE_CHECKSUM)
|| (source == "mcp-session" && version == 1 && checksum == "mcp-transport-sessions-v1")
|| source.strip_prefix("extension:").is_some_and(|name| {
MigrationAuthority::registered_extension_migrations()
.iter()
.any(|(registered_name, descriptor)| {
*registered_name == name
&& i64::from(descriptor.version) == version
&& descriptor.checksum == checksum
})
});
if !valid {
return Err(MigrationError::new(
"legacy_conflict",
"preflight.legacy_audit",
Some(version),
"restore_known_good_backup",
));
}
}
Ok(())
}
async fn create_core_ledger(
transaction: &mut Transaction<'_, sqlx::Postgres>,
) -> Result<(), MigrationError> {
query(
"create table __crank_core_migrations (
version integer primary key,
description text not null,
checksum text not null,
applied_at timestamptz not null default now()
)",
)
.execute(&mut **transaction)
.await
.map_err(|_| {
MigrationError::new(
"apply_failed",
"apply.core_ledger",
Some(1),
"restore_known_good_backup",
)
})?;
Ok(())
}
async fn apply_consolidation(
transaction: &mut Transaction<'_, sqlx::Postgres>,
) -> Result<(), MigrationError> {
sqlx::raw_sql(CONSOLIDATION_SOURCE)
.execute(&mut **transaction)
.await
.map_err(|_| {
MigrationError::new(
"apply_failed",
"apply.consolidation",
Some(2),
"restore_known_good_backup",
)
})?;
for (name, extension) in MigrationAuthority::registered_extension_migrations() {
query(
"insert into __crank_migration_legacy_audit
(source, source_version, source_checksum)
select 'extension:' || $1, $2, $3
from __crank_ext_migrations
where extension_name = $1 and version = $2 and checksum = $3",
)
.bind(name)
.bind(i64::from(extension.version))
.bind(extension.checksum)
.execute(&mut **transaction)
.await
.map_err(|_| {
MigrationError::new(
"apply_failed",
"apply.extension_audit",
Some(2),
"restore_known_good_backup",
)
})?;
}
let descriptor = &MigrationAuthority::sequence()[1];
query(
"insert into __crank_migrations (version, name, checksum, phase, compatibility)
values ($1, $2, $3, $4, $5)",
)
.bind(descriptor.version)
.bind(descriptor.name)
.bind(&descriptor.checksum)
.bind(descriptor.phase)
.bind(descriptor.compatibility)
.execute(&mut **transaction)
.await
.map_err(|_| {
MigrationError::new(
"apply_failed",
"apply.canonical_ledger",
Some(2),
"restore_known_good_backup",
)
})?;
Ok(())
}
async fn apply_request_trace_identity(
transaction: &mut Transaction<'_, sqlx::Postgres>,
) -> Result<(), MigrationError> {
sqlx::raw_sql(REQUEST_TRACE_IDENTITY_SOURCE)
.execute(&mut **transaction)
.await
.map_err(|_| {
MigrationError::new(
"apply_failed",
"apply.request_trace_identity",
Some(3),
"restore_known_good_backup",
)
})?;
let descriptor = &MigrationAuthority::sequence()[2];
query(
"insert into __crank_migrations (version, name, checksum, phase, compatibility)
values ($1, $2, $3, $4, $5)",
)
.bind(descriptor.version)
.bind(descriptor.name)
.bind(&descriptor.checksum)
.bind(descriptor.phase)
.bind(descriptor.compatibility)
.execute(&mut **transaction)
.await
.map_err(|_| {
MigrationError::new(
"apply_failed",
"apply.canonical_ledger",
Some(3),
"restore_known_good_backup",
)
})?;
Ok(())
}
#[cfg(test)]
#[path = "authority_tests.rs"]
mod tests;
@@ -0,0 +1,115 @@
use super::*;
#[test]
fn sequence_is_deterministic_and_append_only() {
let first = MigrationAuthority::sequence();
let second = MigrationAuthority::sequence();
assert_eq!(first, second);
MigrationAuthority::validate_sequence().unwrap();
assert_eq!(
first.iter().map(|item| item.version).collect::<Vec<_>>(),
vec![1, 2, 3]
);
assert_eq!(first[0].checksum, "crank-community-baseline-v1");
assert_eq!(first[0].source_digest, BASELINE_SOURCE_SHA256);
assert_eq!(
baseline_source_digest(),
BASELINE_SOURCE_SHA256,
"baseline v1 source changed; add a new migration instead"
);
assert_eq!(first[1].checksum.len(), 64);
assert!(
first[1]
.checksum
.bytes()
.all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase())
);
}
#[test]
fn invalid_gap_checksum_phase_and_unbounded_backfill_are_rejected() {
let base = MigrationAuthority::sequence();
for invalid in [
{
let mut value = base.clone();
value[1].version = 3;
value
},
{
let mut value = base.clone();
value[1].checksum = "0".repeat(64);
value
},
{
let mut value = base.clone();
value[1].phase = "unknown";
value
},
{
let mut value = base.clone();
value[1].name = value[0].name;
value
},
{
let mut value = base.clone();
value[1].transactional = false;
value
},
{
let mut value = base.clone();
value[1].phase = "migrate";
value[1].backfill = BackfillPolicy::Bounded {
max_batch_rows: 10_001,
max_batch_ms: 60_001,
resumable: false,
};
value
},
{
let mut value = base.clone();
value[1].phase = "contract";
value[1].compatibility = "window-closed";
value[1].contract_evidence = None;
value
},
] {
assert_eq!(
validate_descriptors(&invalid).unwrap_err().code(),
"invalid_contract"
);
}
}
#[test]
fn backfill_batches_are_bounded_and_resumable() {
let policy = BackfillPolicy::Bounded {
max_batch_rows: 100,
max_batch_ms: 1_000,
resumable: true,
};
BackfillBatch {
cursor: Some("next-100".to_owned()),
max_rows: 100,
max_ms: 1_000,
}
.validate(policy)
.unwrap();
assert!(
BackfillBatch {
cursor: None,
max_rows: 101,
max_ms: 1_000,
}
.validate(policy)
.is_err()
);
}
#[test]
fn diagnostic_is_bounded_and_does_not_echo_storage_details() {
let error = MigrationError::storage("preflight.connect");
let rendered = error.to_string();
assert!(rendered.len() < 512);
assert!(!rendered.contains("postgres://"));
assert_eq!(error.code(), "storage_unavailable");
}
@@ -0,0 +1,673 @@
use sqlx::{Postgres, Transaction, query};
pub(super) const BASELINE_VERSION: i32 = 1;
pub(super) const BASELINE_CHECKSUM: &str = "crank-community-baseline-v1";
// baseline-v1:start
pub(super) async fn apply_baseline(
transaction: &mut Transaction<'_, Postgres>,
) -> Result<(), sqlx::Error> {
query(
"create table if not exists workspaces (
id text primary key,
slug text not null unique,
display_name text not null,
status text not null,
settings_json jsonb not null default '{}'::jsonb,
created_at timestamptz not null,
updated_at timestamptz not null
)",
)
.execute(&mut **transaction)
.await?;
query(
"create table if not exists users (
id text primary key,
email text not null unique,
display_name text not null,
password_hash text null,
status text not null,
created_at timestamptz not null
)",
)
.execute(&mut **transaction)
.await?;
query("alter table users add column if not exists password_hash text null")
.execute(&mut **transaction)
.await?;
query(
"insert into users (
id,
email,
display_name,
status,
created_at
) values (
'user_default_owner',
'owner@crank.local',
'Workspace Owner',
'active',
now()
)
on conflict (id) do nothing",
)
.execute(&mut **transaction)
.await?;
query(
"create table if not exists memberships (
workspace_id text not null references workspaces(id) on delete cascade,
user_id text not null references users(id) on delete cascade,
role text not null,
created_at timestamptz not null,
primary key (workspace_id, user_id)
)",
)
.execute(&mut **transaction)
.await?;
query(
"create table if not exists user_sessions (
id text primary key,
user_id text not null references users(id) on delete cascade,
current_workspace_id text null references workspaces(id) on delete set null,
secret_hash text not null,
status text not null,
expires_at timestamptz not null,
last_seen_at timestamptz null,
created_at timestamptz not null
)",
)
.execute(&mut **transaction)
.await?;
query(
"alter table user_sessions
add column if not exists current_workspace_id text null references workspaces(id) on delete set null",
)
.execute(&mut **transaction)
.await?;
query(
"insert into workspaces (
id,
slug,
display_name,
status,
settings_json,
created_at,
updated_at
) values (
'ws_default',
'default',
'Default Workspace',
'active',
'{}'::jsonb,
now(),
now()
)
on conflict (id) do nothing",
)
.execute(&mut **transaction)
.await?;
query(
"insert into memberships (
workspace_id,
user_id,
role,
created_at
) values (
'ws_default',
'user_default_owner',
'owner',
now()
)
on conflict (workspace_id, user_id) do nothing",
)
.execute(&mut **transaction)
.await?;
query(
"create table if not exists invitation_tokens (
id text primary key,
workspace_id text not null references workspaces(id) on delete cascade,
email text not null,
role text not null,
status text not null,
token_hash text not null,
expires_at timestamptz not null,
created_at timestamptz not null
)",
)
.execute(&mut **transaction)
.await?;
query(
"create table if not exists platform_api_keys (
id text primary key,
workspace_id text not null references workspaces(id) on delete cascade,
agent_id text null,
name text not null,
prefix text not null,
secret_hash text not null,
key_kind text not null default 'mcp_client',
scopes_json jsonb not null,
status text not null,
created_at timestamptz not null,
last_used_at timestamptz null,
revoked_at timestamptz null,
expires_at timestamptz null,
allowed_origins_json jsonb not null default '[]'::jsonb
)",
)
.execute(&mut **transaction)
.await?;
query(
"create unique index if not exists platform_api_keys_workspace_name_idx on platform_api_keys(workspace_id, name)",
)
.execute(&mut **transaction)
.await?;
query("alter table platform_api_keys add column if not exists agent_id text null")
.execute(&mut **transaction)
.await?;
query(
"alter table platform_api_keys add column if not exists key_kind text not null default 'mcp_client'",
)
.execute(&mut **transaction)
.await?;
query("alter table platform_api_keys add column if not exists expires_at timestamptz null")
.execute(&mut **transaction)
.await?;
query(
"alter table platform_api_keys add column if not exists allowed_origins_json jsonb not null default '[]'::jsonb",
)
.execute(&mut **transaction)
.await?;
query(
"insert into workspaces (
id,
slug,
display_name,
status,
settings_json,
created_at,
updated_at
) values (
'ws_default',
'default',
'Default Workspace',
'active',
'{}'::jsonb,
now(),
now()
)
on conflict (id) do nothing",
)
.execute(&mut **transaction)
.await?;
query(
"create table if not exists operations (
id text primary key,
workspace_id text null references workspaces(id) on delete cascade,
name text not null,
display_name text not null,
category text not null default 'general',
protocol text not null,
security_level text not null default 'standard',
status text not null,
current_draft_version integer not null default 1,
latest_published_version integer null,
created_at timestamptz not null,
updated_at timestamptz not null,
published_at timestamptz null
)",
)
.execute(&mut **transaction)
.await?;
query("alter table operations add column if not exists workspace_id text null references workspaces(id) on delete cascade")
.execute(&mut **transaction)
.await?;
query(
"alter table operations add column if not exists category text not null default 'general'",
)
.execute(&mut **transaction)
.await?;
query(
"alter table operations add column if not exists security_level text not null default 'standard'",
)
.execute(&mut **transaction)
.await?;
query("update operations set workspace_id = 'ws_default' where workspace_id is null")
.execute(&mut **transaction)
.await?;
query("alter table operations alter column workspace_id set not null")
.execute(&mut **transaction)
.await?;
query("alter table operations drop constraint if exists operations_name_key")
.execute(&mut **transaction)
.await?;
query(
"create unique index if not exists operations_workspace_name_idx on operations(workspace_id, name)",
)
.execute(&mut **transaction)
.await?;
query(
"create table if not exists operation_versions (
operation_id text not null references operations(id) on delete cascade,
version integer not null,
status text not null,
target_json jsonb not null,
input_schema_json jsonb not null,
output_schema_json jsonb not null,
input_mapping_json jsonb not null,
output_mapping_json jsonb not null,
execution_config_json jsonb not null,
tool_description_json jsonb not null,
samples_json jsonb null,
generated_draft_json jsonb null,
config_export_json jsonb null,
wizard_state_json jsonb null,
change_note text null,
created_at timestamptz not null,
created_by text null,
primary key (operation_id, version)
)",
)
.execute(&mut **transaction)
.await?;
query("alter table operation_versions add column if not exists wizard_state_json jsonb null")
.execute(&mut **transaction)
.await?;
query(
"create table if not exists published_operations (
operation_id text primary key references operations(id) on delete cascade,
version integer not null,
published_at timestamptz not null,
published_by text null,
foreign key (operation_id, version) references operation_versions(operation_id, version) on delete cascade
)",
)
.execute(&mut **transaction)
.await?;
query(
"create table if not exists operation_samples (
id text primary key,
operation_id text not null references operations(id) on delete cascade,
version integer not null,
sample_kind text not null,
storage_ref text not null,
content_type text not null,
file_name text null,
created_at timestamptz not null,
foreign key (operation_id, version) references operation_versions(operation_id, version) on delete cascade
)",
)
.execute(&mut **transaction)
.await?;
query(
"create table if not exists descriptors (
id text primary key,
operation_id text null references operations(id) on delete cascade,
version integer null,
descriptor_kind text not null,
storage_ref text not null,
source_name text null,
package_index_json jsonb null,
created_at timestamptz not null,
foreign key (operation_id, version) references operation_versions(operation_id, version) on delete cascade
)",
)
.execute(&mut **transaction)
.await?;
query(
"create table if not exists auth_profiles (
id text primary key,
workspace_id text null references workspaces(id) on delete cascade,
name text not null,
kind text not null,
config_json jsonb not null,
created_at timestamptz not null,
updated_at timestamptz not null
)",
)
.execute(&mut **transaction)
.await?;
query("alter table auth_profiles add column if not exists workspace_id text null references workspaces(id) on delete cascade")
.execute(&mut **transaction)
.await?;
query("update auth_profiles set workspace_id = 'ws_default' where workspace_id is null")
.execute(&mut **transaction)
.await?;
query("alter table auth_profiles alter column workspace_id set not null")
.execute(&mut **transaction)
.await?;
query("alter table auth_profiles drop constraint if exists auth_profiles_name_key")
.execute(&mut **transaction)
.await?;
query(
"create unique index if not exists auth_profiles_workspace_name_idx on auth_profiles(workspace_id, name)",
)
.execute(&mut **transaction)
.await?;
query(
"create table if not exists workspace_upstreams (
id text primary key,
workspace_id text not null references workspaces(id) on delete cascade,
name text not null,
base_url text not null,
static_headers_json jsonb not null default '{}'::jsonb,
auth_profile_id text null references auth_profiles(id) on delete set null,
created_at timestamptz not null,
updated_at timestamptz not null
)",
)
.execute(&mut **transaction)
.await?;
query(
"create unique index if not exists workspace_upstreams_workspace_name_idx on workspace_upstreams(workspace_id, name)",
)
.execute(&mut **transaction)
.await?;
query(
"create unique index if not exists workspace_upstreams_workspace_base_auth_idx on workspace_upstreams(workspace_id, base_url, coalesce(auth_profile_id, ''))",
)
.execute(&mut **transaction)
.await?;
query(
"insert into workspace_upstreams (
id,
workspace_id,
name,
base_url,
static_headers_json,
auth_profile_id,
created_at,
updated_at
)
select
'upstream_frankfurter_' || w.id,
w.id,
'Frankfurter',
'https://api.frankfurter.dev',
'{}'::jsonb,
null,
now(),
now()
from workspaces w
where not exists (
select 1
from workspace_upstreams wu
where wu.workspace_id = w.id
and wu.name = 'Frankfurter'
)",
)
.execute(&mut **transaction)
.await?;
query(
"create table if not exists secrets (
id text primary key,
workspace_id text not null references workspaces(id) on delete cascade,
name text not null,
kind text not null,
status text not null,
current_version integer not null,
last_used_at timestamptz null,
created_at timestamptz not null,
updated_at timestamptz not null
)",
)
.execute(&mut **transaction)
.await?;
query(
"create unique index if not exists secrets_workspace_name_idx on secrets(workspace_id, name)",
)
.execute(&mut **transaction)
.await?;
query(
"create table if not exists secret_versions (
secret_id text not null references secrets(id) on delete cascade,
version integer not null,
ciphertext text not null,
key_version text not null,
created_at timestamptz not null,
created_by text null references users(id) on delete set null,
primary key (secret_id, version)
)",
)
.execute(&mut **transaction)
.await?;
query(
"create table if not exists yaml_import_jobs (
id text primary key,
source_sample_id text null references operation_samples(id) on delete set null,
status text not null,
format_version text not null,
mode text not null,
result_operation_id text null references operations(id) on delete set null,
result_version integer null,
error_text text null,
created_at timestamptz not null,
finished_at timestamptz null
)",
)
.execute(&mut **transaction)
.await?;
query(
"create table if not exists import_jobs (
id text primary key,
workspace_id text not null references workspaces(id) on delete cascade,
kind text not null,
source_format text not null,
source_version text null,
status text not null,
preview_payload jsonb not null,
created_operation_ids jsonb not null default '[]'::jsonb,
error_text text null,
created_at timestamptz not null,
expires_at timestamptz not null,
finished_at timestamptz null
)",
)
.execute(&mut **transaction)
.await?;
query(
"create table if not exists agents (
id text primary key,
workspace_id text not null references workspaces(id) on delete cascade,
slug text not null,
display_name text not null,
description text not null,
status text not null,
current_draft_version integer not null default 1,
latest_published_version integer null,
created_at timestamptz not null,
updated_at timestamptz not null,
published_at timestamptz null
)",
)
.execute(&mut **transaction)
.await?;
query(
"create unique index if not exists agents_workspace_slug_idx on agents(workspace_id, slug)",
)
.execute(&mut **transaction)
.await?;
query(
"create table if not exists agent_versions (
agent_id text not null references agents(id) on delete cascade,
version integer not null,
status text not null,
instructions_json jsonb not null,
tool_selection_policy_json jsonb not null,
created_at timestamptz not null,
primary key (agent_id, version)
)",
)
.execute(&mut **transaction)
.await?;
query(
"create table if not exists agent_operation_bindings (
agent_id text not null references agents(id) on delete cascade,
agent_version integer not null,
operation_id text not null references operations(id) on delete cascade,
operation_version integer not null,
tool_name text not null,
tool_title text not null,
tool_description_override text null,
enabled boolean not null default true,
foreign key (agent_id, agent_version) references agent_versions(agent_id, version) on delete cascade,
foreign key (operation_id, operation_version) references operation_versions(operation_id, version) on delete cascade
)",
)
.execute(&mut **transaction)
.await?;
query(
"create unique index if not exists agent_bindings_tool_name_idx on agent_operation_bindings(agent_id, agent_version, tool_name)",
)
.execute(&mut **transaction)
.await?;
query(
"create table if not exists published_agents (
agent_id text primary key references agents(id) on delete cascade,
version integer not null,
published_at timestamptz not null,
published_by text null,
foreign key (agent_id, version) references agent_versions(agent_id, version) on delete cascade
)",
)
.execute(&mut **transaction)
.await?;
query(
"create table if not exists approval_requests (
id text primary key,
workspace_id text not null references workspaces(id) on delete cascade,
agent_id text not null references agents(id) on delete cascade,
operation_id text not null references operations(id) on delete cascade,
operation_version integer not null,
status text not null,
risk_level text not null,
request_payload_json jsonb not null,
response_payload_json jsonb null,
created_at timestamptz not null,
expires_at timestamptz not null,
decided_at timestamptz null,
decided_by_key_id text null references platform_api_keys(id) on delete set null,
decision_note text null
)",
)
.execute(&mut **transaction)
.await?;
query("alter table approval_requests add column if not exists execution_started_at timestamptz null")
.execute(&mut **transaction)
.await?;
query("alter table approval_requests add column if not exists execution_attempts integer not null default 0")
.execute(&mut **transaction)
.await?;
query("alter table approval_requests add column if not exists request_fingerprint text null")
.execute(&mut **transaction)
.await?;
query(
"create unique index if not exists approval_requests_pending_fingerprint_idx
on approval_requests(agent_id, operation_id, operation_version, request_fingerprint)
where status = 'pending' and request_fingerprint is not null",
)
.execute(&mut **transaction)
.await?;
query(
"create index if not exists approval_requests_agent_status_idx
on approval_requests(workspace_id, agent_id, status, expires_at)",
)
.execute(&mut **transaction)
.await?;
query(
"create table if not exists invocation_logs (
id text primary key,
workspace_id text not null references workspaces(id) on delete cascade,
agent_id text null references agents(id) on delete set null,
operation_id text not null references operations(id) on delete cascade,
source text not null,
level text not null,
status text not null,
tool_name text not null,
message text not null,
request_id text null,
status_code integer null,
duration_ms bigint not null,
error_kind text null,
request_preview_json jsonb not null,
response_preview_json jsonb not null,
created_at timestamptz not null
)",
)
.execute(&mut **transaction)
.await?;
query(
"create index if not exists invocation_logs_workspace_created_idx on invocation_logs(workspace_id, created_at desc)",
)
.execute(&mut **transaction)
.await?;
query(
"create index if not exists invocation_logs_workspace_operation_created_idx on invocation_logs(workspace_id, operation_id, created_at desc)",
)
.execute(&mut **transaction)
.await?;
query(
"create index if not exists invocation_logs_workspace_agent_created_idx on invocation_logs(workspace_id, agent_id, created_at desc)",
)
.execute(&mut **transaction)
.await?;
query(
"create table if not exists usage_rollups (
workspace_id text not null references workspaces(id) on delete cascade,
agent_id text null references agents(id) on delete cascade,
operation_id text null references operations(id) on delete cascade,
period text not null,
calls_total bigint not null,
calls_ok bigint not null,
calls_error bigint not null,
p50_ms bigint not null,
p95_ms bigint not null,
p99_ms bigint not null,
updated_at timestamptz not null
)",
)
.execute(&mut **transaction)
.await?;
Ok(())
}
// baseline-v1:end
@@ -0,0 +1,84 @@
create temporary table __crank_v2_context (
had_mcp_ledger boolean not null
) on commit drop;
insert into __crank_v2_context (had_mcp_ledger)
values (to_regclass(format('%I.%I', current_schema(), '__crank_mcp_migrations')) is not null);
create table __crank_migrations (
version bigint primary key,
name text not null unique,
checksum text not null,
phase text not null,
compatibility text not null,
applied_at timestamptz not null default now()
);
create table __crank_migration_legacy_audit (
source text not null,
source_version bigint not null,
source_checksum text not null,
imported_at timestamptz not null default now(),
primary key (source, source_version)
);
insert into __crank_migrations (version, name, checksum, phase, compatibility)
values (
1,
'community-baseline-v1',
'crank-community-baseline-v1',
'expand',
'legacy-baseline'
);
insert into __crank_migration_legacy_audit (source, source_version, source_checksum)
values ('core', 1, 'crank-community-baseline-v1');
create table if not exists __crank_mcp_migrations (
version integer primary key,
checksum text not null,
applied_at timestamptz not null default now()
);
create table if not exists mcp_transport_sessions (
id text primary key,
protocol_version text not null,
initialized boolean not null default false,
supports_elicitation boolean not null default false,
workspace_slug text not null,
agent_slug text not null,
created_at timestamptz not null,
updated_at timestamptz not null,
expires_at timestamptz null
);
alter table mcp_transport_sessions
add column if not exists supports_elicitation boolean not null default false;
alter table mcp_transport_sessions
add column if not exists expires_at timestamptz null;
create index if not exists mcp_transport_sessions_workspace_agent_idx
on mcp_transport_sessions(workspace_slug, agent_slug, updated_at desc);
create index if not exists mcp_transport_sessions_expires_at_idx
on mcp_transport_sessions(expires_at)
where expires_at is not null;
insert into __crank_mcp_migrations (version, checksum)
values (1, 'mcp-transport-sessions-v1')
on conflict (version) do nothing;
insert into __crank_migration_legacy_audit (source, source_version, source_checksum)
select 'mcp-session', 1, 'mcp-transport-sessions-v1'
from __crank_v2_context
where had_mcp_ledger;
create table if not exists __crank_ext_migrations (
extension_name text not null,
version integer not null,
checksum text null,
applied_at timestamptz not null default now(),
primary key (extension_name, version)
);
alter table __crank_ext_migrations
add column if not exists checksum text null;
@@ -0,0 +1,20 @@
alter table invocation_logs
add column trace_id text;
alter table invocation_logs
add constraint invocation_logs_trace_id_format_check
check (
trace_id is null
or (
trace_id ~ '^[0-9a-f]{32}$'
and trace_id <> '00000000000000000000000000000000'
)
) not valid;
create index invocation_logs_workspace_request_id_idx
on invocation_logs(workspace_id, request_id)
where request_id is not null and octet_length(request_id) <= 128;
create index invocation_logs_workspace_trace_id_idx
on invocation_logs(workspace_id, trace_id)
where trace_id is not null;
@@ -0,0 +1,451 @@
use sqlx::{PgConnection, Row, query};
use super::authority::MigrationError;
pub(super) const OWNED_RELATIONS: &[&str] = &[
"__crank_core_migrations",
"__crank_migrations",
"__crank_migration_legacy_audit",
"__crank_mcp_migrations",
"__crank_ext_migrations",
"mcp_transport_sessions",
"workspaces",
"users",
"memberships",
"user_sessions",
"invitation_tokens",
"platform_api_keys",
"operations",
"operation_versions",
"published_operations",
"operation_samples",
"descriptors",
"agents",
"agent_versions",
"published_agents",
"agent_operation_bindings",
"secrets",
"secret_versions",
"auth_profiles",
"workspace_upstreams",
"yaml_import_jobs",
"import_jobs",
"approval_requests",
"invocation_logs",
"usage_rollups",
];
const REQUIRED_COLUMNS: &[(&str, &[&str])] = &[
(
"__crank_core_migrations",
&["version", "description", "checksum", "applied_at"],
),
(
"__crank_migrations",
&[
"version",
"name",
"checksum",
"phase",
"compatibility",
"applied_at",
],
),
(
"__crank_migration_legacy_audit",
&["source", "source_version", "source_checksum", "imported_at"],
),
(
"__crank_mcp_migrations",
&["version", "checksum", "applied_at"],
),
(
"__crank_ext_migrations",
&["extension_name", "version", "checksum", "applied_at"],
),
(
"mcp_transport_sessions",
&[
"id",
"protocol_version",
"initialized",
"supports_elicitation",
"workspace_slug",
"agent_slug",
"created_at",
"updated_at",
"expires_at",
],
),
];
const REQUIRED_COLUMN_TYPES: &[(&str, &str, &str, bool)] = &[
("__crank_core_migrations", "version", "integer", false),
("__crank_core_migrations", "description", "text", false),
("__crank_core_migrations", "checksum", "text", false),
(
"__crank_core_migrations",
"applied_at",
"timestamp with time zone",
false,
),
("__crank_migrations", "version", "bigint", false),
("__crank_migrations", "name", "text", false),
("__crank_migrations", "checksum", "text", false),
("__crank_migrations", "phase", "text", false),
("__crank_migrations", "compatibility", "text", false),
(
"__crank_migrations",
"applied_at",
"timestamp with time zone",
false,
),
("__crank_migration_legacy_audit", "source", "text", false),
(
"__crank_migration_legacy_audit",
"source_version",
"bigint",
false,
),
(
"__crank_migration_legacy_audit",
"source_checksum",
"text",
false,
),
(
"__crank_migration_legacy_audit",
"imported_at",
"timestamp with time zone",
false,
),
("__crank_mcp_migrations", "version", "integer", false),
("__crank_mcp_migrations", "checksum", "text", false),
(
"__crank_mcp_migrations",
"applied_at",
"timestamp with time zone",
false,
),
("__crank_ext_migrations", "extension_name", "text", false),
("__crank_ext_migrations", "version", "integer", false),
("__crank_ext_migrations", "checksum", "text", true),
(
"__crank_ext_migrations",
"applied_at",
"timestamp with time zone",
false,
),
("mcp_transport_sessions", "id", "text", false),
("mcp_transport_sessions", "protocol_version", "text", false),
("mcp_transport_sessions", "initialized", "boolean", false),
(
"mcp_transport_sessions",
"supports_elicitation",
"boolean",
false,
),
("mcp_transport_sessions", "workspace_slug", "text", false),
("mcp_transport_sessions", "agent_slug", "text", false),
(
"mcp_transport_sessions",
"created_at",
"timestamp with time zone",
false,
),
(
"mcp_transport_sessions",
"updated_at",
"timestamp with time zone",
false,
),
(
"mcp_transport_sessions",
"expires_at",
"timestamp with time zone",
true,
),
];
pub(super) async fn relation_exists(
connection: &mut PgConnection,
relation: &str,
) -> Result<bool, MigrationError> {
query("select to_regclass(format('%I.%I', current_schema(), $1))::text is not null as present")
.bind(relation)
.fetch_one(connection)
.await
.map_err(|_| MigrationError::storage("preflight.inventory"))?
.try_get::<bool, _>("present")
.map_err(|_| MigrationError::storage("preflight.inventory"))
}
pub(super) async fn validate_required_relations(
connection: &mut PgConnection,
relations: &[&str],
version: i64,
) -> Result<(), MigrationError> {
for relation in relations {
if !relation_exists(connection, relation).await? {
return Err(schema_error(version));
}
let kind = query(
"select c.relkind::text as kind
from pg_catalog.pg_class c
join pg_catalog.pg_namespace n on n.oid = c.relnamespace
where n.nspname = current_schema() and c.relname = $1",
)
.bind(relation)
.fetch_one(&mut *connection)
.await
.map_err(|_| MigrationError::storage("preflight.schema"))?
.try_get::<String, _>("kind")
.map_err(|_| MigrationError::storage("preflight.schema"))?;
if !matches!(kind.as_str(), "r" | "p") {
return Err(schema_error(version));
}
}
Ok(())
}
pub(super) async fn validate_schema_fingerprint(
connection: &mut PgConnection,
current_version: i64,
) -> Result<(), MigrationError> {
for (table, required) in REQUIRED_COLUMNS {
if !relation_exists(connection, table).await? {
continue;
}
let rows = query(
"select column_name from information_schema.columns
where table_schema = current_schema() and table_name = $1
order by ordinal_position limit 257",
)
.bind(table)
.fetch_all(&mut *connection)
.await
.map_err(|_| MigrationError::storage("preflight.schema"))?;
let actual = rows
.iter()
.filter_map(|row| row.try_get::<String, _>("column_name").ok())
.collect::<Vec<_>>();
if actual.len() != required.len()
|| required
.iter()
.any(|column| !actual.iter().any(|actual| actual == column))
{
return Err(schema_error(current_version));
}
}
for (table, column, data_type, nullable) in REQUIRED_COLUMN_TYPES {
if !relation_exists(connection, table).await? {
continue;
}
let row = query(
"select data_type, is_nullable from information_schema.columns
where table_schema = current_schema() and table_name = $1 and column_name = $2",
)
.bind(table)
.bind(column)
.fetch_optional(&mut *connection)
.await
.map_err(|_| MigrationError::storage("preflight.schema"))?;
let valid = row.is_some_and(|row| {
row.try_get::<String, _>("data_type").ok().as_deref() == Some(*data_type)
&& row.try_get::<String, _>("is_nullable").ok().as_deref()
== Some(if *nullable { "YES" } else { "NO" })
});
if !valid {
return Err(schema_error(current_version));
}
}
if current_version < 3 {
let trace_column_present = query(
"select exists (
select 1 from information_schema.columns
where table_schema = current_schema()
and table_name = 'invocation_logs'
and column_name = 'trace_id'
) as present",
)
.fetch_one(&mut *connection)
.await
.map_err(|_| MigrationError::storage("preflight.schema"))?
.try_get::<bool, _>("present")
.map_err(|_| MigrationError::storage("preflight.schema"))?;
let trace_constraint_present = named_constraint_exists(
connection,
"invocation_logs",
"invocation_logs_trace_id_format_check",
)
.await?;
let v3_index_present =
relation_exists(connection, "invocation_logs_workspace_request_id_idx").await?
|| relation_exists(connection, "invocation_logs_workspace_trace_id_idx").await?;
if trace_column_present || trace_constraint_present || v3_index_present {
return Err(schema_error(current_version));
}
} else {
let trace_id = query(
"select data_type, is_nullable from information_schema.columns
where table_schema = current_schema()
and table_name = 'invocation_logs'
and column_name = 'trace_id'",
)
.fetch_optional(&mut *connection)
.await
.map_err(|_| MigrationError::storage("preflight.schema"))?;
let valid = trace_id.is_some_and(|row| {
row.try_get::<String, _>("data_type").ok().as_deref() == Some("text")
&& row.try_get::<String, _>("is_nullable").ok().as_deref() == Some("YES")
});
if !valid {
return Err(schema_error(current_version));
}
let constraint = query(
"select pg_get_expr(c.conbin, c.conrelid) as expression, c.convalidated
from pg_catalog.pg_constraint c
join pg_catalog.pg_class t on t.oid = c.conrelid
join pg_catalog.pg_namespace n on n.oid = t.relnamespace
where n.nspname = current_schema()
and t.relname = 'invocation_logs'
and c.conname = 'invocation_logs_trace_id_format_check'
and c.contype = 'c'",
)
.fetch_optional(&mut *connection)
.await
.map_err(|_| MigrationError::storage("preflight.schema"))?;
let constraint_valid = constraint.is_some_and(|row| {
row.try_get::<String, _>("expression")
.ok()
.is_some_and(|value| {
normalize_definition(&value)
== "trace_idisnullortrace_id~'^[0-9a-f]{32}$'::textandtrace_id<>'00000000000000000000000000000000'::text"
})
&& row.try_get::<bool, _>("convalidated").ok() == Some(false)
});
if !constraint_valid {
return Err(schema_error(current_version));
}
}
let required_indexes = [
"mcp_transport_sessions_workspace_agent_idx",
"mcp_transport_sessions_expires_at_idx",
];
for index in required_indexes {
let present = query(
"select exists (select 1 from pg_catalog.pg_indexes
where schemaname = current_schema() and indexname = $1) as present",
)
.bind(index)
.fetch_one(&mut *connection)
.await
.map_err(|_| MigrationError::storage("preflight.schema"))?
.try_get::<bool, _>("present")
.map_err(|_| MigrationError::storage("preflight.schema"))?;
if !present {
return Err(schema_error(current_version));
}
}
if current_version >= 3 {
validate_index(
connection,
"invocation_logs_workspace_request_id_idx",
"request_id",
"request_idisnotnullandoctet_lengthrequest_id<=128",
)
.await?;
validate_index(
connection,
"invocation_logs_workspace_trace_id_idx",
"trace_id",
"trace_idisnotnull",
)
.await?;
}
Ok(())
}
async fn named_constraint_exists(
connection: &mut PgConnection,
table: &str,
constraint: &str,
) -> Result<bool, MigrationError> {
query(
"select exists (
select 1 from pg_catalog.pg_constraint c
join pg_catalog.pg_class t on t.oid = c.conrelid
join pg_catalog.pg_namespace n on n.oid = t.relnamespace
where n.nspname = current_schema()
and t.relname = $1
and c.conname = $2
) as present",
)
.bind(table)
.bind(constraint)
.fetch_one(connection)
.await
.map_err(|_| MigrationError::storage("preflight.schema"))?
.try_get::<bool, _>("present")
.map_err(|_| MigrationError::storage("preflight.schema"))
}
async fn validate_index(
connection: &mut PgConnection,
index: &str,
second_column: &str,
expected_predicate: &str,
) -> Result<(), MigrationError> {
let row = query(
"select
t.relname as table_name,
am.amname as access_method,
i.indisvalid,
i.indisready,
i.indisunique,
pg_get_indexdef(i.indexrelid, 1, true) as first_column,
pg_get_indexdef(i.indexrelid, 2, true) as second_column,
pg_get_expr(i.indpred, i.indrelid) as predicate
from pg_catalog.pg_index i
join pg_catalog.pg_class idx on idx.oid = i.indexrelid
join pg_catalog.pg_class t on t.oid = i.indrelid
join pg_catalog.pg_namespace n on n.oid = t.relnamespace
join pg_catalog.pg_am am on am.oid = idx.relam
where n.nspname = current_schema() and idx.relname = $1",
)
.bind(index)
.fetch_optional(connection)
.await
.map_err(|_| MigrationError::storage("preflight.schema"))?;
let valid = row.is_some_and(|row| {
row.try_get::<String, _>("table_name").ok().as_deref() == Some("invocation_logs")
&& row.try_get::<String, _>("access_method").ok().as_deref() == Some("btree")
&& row.try_get::<bool, _>("indisvalid").ok() == Some(true)
&& row.try_get::<bool, _>("indisready").ok() == Some(true)
&& row.try_get::<bool, _>("indisunique").ok() == Some(false)
&& row.try_get::<String, _>("first_column").ok().as_deref() == Some("workspace_id")
&& row.try_get::<String, _>("second_column").ok().as_deref() == Some(second_column)
&& row
.try_get::<String, _>("predicate")
.ok()
.is_some_and(|value| normalize_definition(&value) == expected_predicate)
});
if valid { Ok(()) } else { Err(schema_error(3)) }
}
fn normalize_definition(value: &str) -> String {
value
.chars()
.filter(|character| !character.is_ascii_whitespace() && !matches!(character, '(' | ')'))
.flat_map(char::to_lowercase)
.collect()
}
fn schema_error(version: i64) -> MigrationError {
MigrationError::new(
"partial_sequence",
"preflight.schema",
Some(version),
"restore_known_good_backup",
)
}
@@ -5,7 +5,7 @@ use sqlx::{
postgres::{PgConnectOptions, PgPoolOptions},
};
use crate::{error::RegistryError, migrations};
use crate::{MigrationAuthority, error::RegistryError};
use super::{PostgresPoolConfig, PostgresRegistry};
@@ -33,7 +33,7 @@ impl PostgresRegistry {
.max_lifetime(Duration::from_millis(pool_config.max_lifetime_ms))
.connect_with(connect_options)
.await?;
migrations::apply_postgres(&pool).await?;
MigrationAuthority::require_current(&pool).await?;
Ok(Self { pool })
}
@@ -46,11 +46,6 @@ impl PostgresRegistry {
Ok(())
}
pub async fn migrate(&self) -> Result<(), RegistryError> {
migrations::apply_postgres(&self.pool).await?;
Ok(())
}
async fn connect_in_schema(
database_url: &str,
schema: Option<&str>,
@@ -69,7 +64,7 @@ impl PostgresRegistry {
}
let registry = Self { pool };
registry.migrate().await?;
MigrationAuthority::require_current(&registry.pool).await?;
Ok(registry)
}
}
@@ -331,6 +331,7 @@ fn map_invocation_log_record(row: &PgRow) -> Result<InvocationLogRecord, Registr
tool_name: row.try_get("tool_name")?,
message: row.try_get("message")?,
request_id: row.try_get("request_id")?,
trace_id: row.try_get("trace_id")?,
status_code: match row.try_get::<Option<i32>, _>("status_code")? {
Some(value) => {
Some(
@@ -43,6 +43,26 @@ impl PostgresRegistry {
&self,
request: CreateInvocationLogRequest<'_>,
) -> Result<(), RegistryError> {
let request_id =
request
.log
.request_id
.as_deref()
.ok_or(RegistryError::InvalidCorrelationIdentity {
field: "request_id",
})?;
crank_core::RequestId::parse(request_id).map_err(|_| {
RegistryError::InvalidCorrelationIdentity {
field: "request_id",
}
})?;
let trace_id = request
.log
.trace_id
.as_deref()
.ok_or(RegistryError::InvalidCorrelationIdentity { field: "trace_id" })?;
crank_core::TraceId::parse(trace_id)
.map_err(|_| RegistryError::InvalidCorrelationIdentity { field: "trace_id" })?;
let request_preview = crank_core::sanitize_invocation_preview(&request.log.request_preview);
let response_preview =
crank_core::sanitize_invocation_preview(&request.log.response_preview);
@@ -58,6 +78,7 @@ impl PostgresRegistry {
tool_name,
message,
request_id,
trace_id,
status_code,
duration_ms,
error_kind,
@@ -65,7 +86,7 @@ impl PostgresRegistry {
response_preview_json,
created_at
) values (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16::timestamptz
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17::timestamptz
)",
)
.bind(request.log.id.as_str())
@@ -78,6 +99,7 @@ impl PostgresRegistry {
.bind(&request.log.tool_name)
.bind(&request.log.message)
.bind(&request.log.request_id)
.bind(&request.log.trace_id)
.bind(request.log.status_code.map(i32::from))
.bind(i64::try_from(request.log.duration_ms).map_err(|_| {
RegistryError::InvalidNumericValue {
@@ -111,6 +133,7 @@ impl PostgresRegistry {
l.tool_name,
l.message,
l.request_id,
l.trace_id,
l.status_code,
l.duration_ms,
l.error_kind,
@@ -183,6 +206,7 @@ impl PostgresRegistry {
l.tool_name,
l.message,
l.request_id,
l.trace_id,
l.status_code,
l.duration_ms,
l.error_kind,
+38 -155
View File
@@ -1,5 +1,3 @@
use std::{collections::BTreeMap, env};
use thiserror::Error;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -13,11 +11,9 @@ pub struct PostgresPoolConfig {
#[derive(Debug, Error, PartialEq, Eq)]
pub enum PostgresPoolConfigError {
#[error("invalid postgres pool setting {name}={value}")]
InvalidValue { name: &'static str, value: String },
#[error("POSTGRES_MAX_CONNECTIONS must be greater than zero")]
ZeroMaxConnections,
#[error("POSTGRES_MIN_CONNECTIONS must not exceed POSTGRES_MAX_CONNECTIONS")]
#[error("postgres pool setting is outside its allowed bounds: {field}")]
OutOfRange { field: &'static str },
#[error("minimum connections must not exceed maximum connections")]
MinConnectionsExceedMax,
}
@@ -34,173 +30,60 @@ impl Default for PostgresPoolConfig {
}
impl PostgresPoolConfig {
pub fn from_env() -> Result<Self, PostgresPoolConfigError> {
Self::from_vars(env::vars())
}
fn from_vars<I, K, V>(vars: I) -> Result<Self, PostgresPoolConfigError>
where
I: IntoIterator<Item = (K, V)>,
K: AsRef<str>,
V: AsRef<str>,
{
let vars = vars
.into_iter()
.map(|(name, value)| (name.as_ref().to_owned(), value.as_ref().to_owned()))
.collect::<BTreeMap<_, _>>();
let defaults = Self::default();
let config = Self {
max_connections: parse_u32_setting(
&vars,
"POSTGRES_MAX_CONNECTIONS",
defaults.max_connections,
)?,
min_connections: parse_u32_setting(
&vars,
"POSTGRES_MIN_CONNECTIONS",
defaults.min_connections,
)?,
acquire_timeout_ms: parse_u64_setting(
&vars,
"POSTGRES_ACQUIRE_TIMEOUT_MS",
defaults.acquire_timeout_ms,
)?,
idle_timeout_ms: parse_u64_setting(
&vars,
"POSTGRES_IDLE_TIMEOUT_MS",
defaults.idle_timeout_ms,
)?,
max_lifetime_ms: parse_u64_setting(
&vars,
"POSTGRES_MAX_LIFETIME_MS",
defaults.max_lifetime_ms,
)?,
};
config.validate()?;
Ok(config)
}
fn validate(self) -> Result<Self, PostgresPoolConfigError> {
if self.max_connections == 0 {
return Err(PostgresPoolConfigError::ZeroMaxConnections);
pub fn try_new(
max_connections: u32,
min_connections: u32,
acquire_timeout_ms: u64,
idle_timeout_ms: u64,
max_lifetime_ms: u64,
) -> Result<Self, PostgresPoolConfigError> {
for (field, value, minimum, maximum) in [
("max_connections", u64::from(max_connections), 1, 1024),
("min_connections", u64::from(min_connections), 0, 1024),
("acquire_timeout_ms", acquire_timeout_ms, 1, 300_000),
("idle_timeout_ms", idle_timeout_ms, 1_000, 86_400_000),
("max_lifetime_ms", max_lifetime_ms, 1_000, 86_400_000),
] {
if !(minimum..=maximum).contains(&value) {
return Err(PostgresPoolConfigError::OutOfRange { field });
}
}
if self.min_connections > self.max_connections {
if min_connections > max_connections {
return Err(PostgresPoolConfigError::MinConnectionsExceedMax);
}
Ok(self)
Ok(Self {
max_connections,
min_connections,
acquire_timeout_ms,
idle_timeout_ms,
max_lifetime_ms,
})
}
}
fn parse_u32_setting(
vars: &BTreeMap<String, String>,
name: &'static str,
default: u32,
) -> Result<u32, PostgresPoolConfigError> {
vars.get(name)
.map(|value| {
value
.parse::<u32>()
.map_err(|_| PostgresPoolConfigError::InvalidValue {
name,
value: value.clone(),
})
})
.transpose()
.map(|value| value.unwrap_or(default))
}
fn parse_u64_setting(
vars: &BTreeMap<String, String>,
name: &'static str,
default: u64,
) -> Result<u64, PostgresPoolConfigError> {
vars.get(name)
.map(|value| {
value
.parse::<u64>()
.map_err(|_| PostgresPoolConfigError::InvalidValue {
name,
value: value.clone(),
})
})
.transpose()
.map(|value| value.unwrap_or(default))
}
#[cfg(test)]
mod pool_config_tests {
mod tests {
use super::{PostgresPoolConfig, PostgresPoolConfigError};
#[test]
fn pool_config_uses_explicit_defaults() {
let config = PostgresPoolConfig::from_vars(std::iter::empty::<(&str, &str)>()).unwrap();
fn explicit_defaults_remain_valid() {
assert_eq!(
config,
PostgresPoolConfig {
max_connections: 20,
min_connections: 2,
acquire_timeout_ms: 5_000,
idle_timeout_ms: 600_000,
max_lifetime_ms: 1_800_000,
}
PostgresPoolConfig::try_new(20, 2, 5_000, 600_000, 1_800_000).unwrap(),
PostgresPoolConfig::default()
);
}
#[test]
fn pool_config_parses_overrides() {
let config = PostgresPoolConfig::from_vars([
("POSTGRES_MAX_CONNECTIONS", "32"),
("POSTGRES_MIN_CONNECTIONS", "4"),
("POSTGRES_ACQUIRE_TIMEOUT_MS", "7000"),
("POSTGRES_IDLE_TIMEOUT_MS", "900000"),
("POSTGRES_MAX_LIFETIME_MS", "3600000"),
])
.unwrap();
fn rejects_invalid_bounds_and_cross_fields() {
assert_eq!(
config,
PostgresPoolConfig {
max_connections: 32,
min_connections: 4,
acquire_timeout_ms: 7_000,
idle_timeout_ms: 900_000,
max_lifetime_ms: 3_600_000,
PostgresPoolConfig::try_new(0, 0, 5_000, 600_000, 1_800_000).unwrap_err(),
PostgresPoolConfigError::OutOfRange {
field: "max_connections"
}
);
}
#[test]
fn pool_config_rejects_invalid_numeric_value() {
let error =
PostgresPoolConfig::from_vars([("POSTGRES_MAX_CONNECTIONS", "abc")]).unwrap_err();
assert_eq!(
error,
PostgresPoolConfigError::InvalidValue {
name: "POSTGRES_MAX_CONNECTIONS",
value: "abc".to_owned(),
}
PostgresPoolConfig::try_new(2, 3, 5_000, 600_000, 1_800_000).unwrap_err(),
PostgresPoolConfigError::MinConnectionsExceedMax
);
}
#[test]
fn pool_config_rejects_zero_max_connections() {
let error = PostgresPoolConfig::from_vars([("POSTGRES_MAX_CONNECTIONS", "0")]).unwrap_err();
assert_eq!(error, PostgresPoolConfigError::ZeroMaxConnections);
}
#[test]
fn pool_config_rejects_min_connections_above_max() {
let error = PostgresPoolConfig::from_vars([
("POSTGRES_MAX_CONNECTIONS", "2"),
("POSTGRES_MIN_CONNECTIONS", "3"),
])
.unwrap_err();
assert_eq!(error, PostgresPoolConfigError::MinConnectionsExceedMax);
}
}
@@ -216,6 +216,7 @@ pub(super) fn test_invocation_log(
tool_name: "create_lead".to_owned(),
message: "invocation".to_owned(),
request_id: Some(format!("req_{id}")),
trace_id: Some("0af7651916cd43dd8448eb211c80319c".to_owned()),
status_code: Some(200),
duration_ms,
error_kind: None,
@@ -256,12 +257,15 @@ impl TestDatabase {
}
pub(super) async fn registry(&self) -> PostgresRegistry {
PostgresRegistry::connect(&format!(
let database_url = format!(
"{}?options=-csearch_path%3D{}",
self.database_url, self.schema
))
.await
.unwrap()
);
let pool = sqlx::PgPool::connect(&database_url).await.unwrap();
crank_registry::MigrationAuthority::apply(&pool)
.await
.unwrap();
PostgresRegistry::connect(&database_url).await.unwrap()
}
pub(super) async fn cleanup(&self) {
@@ -1,34 +1,46 @@
use crank_registry::PostgresRegistry;
use crank_registry::{MigrationAuthority, MigrationPreflight, PostgresRegistry};
use sqlx::Row;
static EVENT_TRIGGER_TEST_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
#[tokio::test]
async fn core_migration_is_versioned_and_safe_under_concurrent_startup() {
async fn controlled_authority_is_versioned_and_safe_under_concurrent_apply() {
let database_url = crank_test_support::postgres_schema_url("test_core_migration").await;
let pool = sqlx::PgPool::connect(&database_url).await.unwrap();
let (first, second) = tokio::join!(
PostgresRegistry::connect(&database_url),
PostgresRegistry::connect(&database_url),
MigrationAuthority::apply(&pool),
MigrationAuthority::apply(&pool),
);
let first = first.expect("first service startup must apply the migration");
second.expect("second service startup must observe the applied migration");
first.expect("first controlled runner must apply the sequence");
second.expect("second controlled runner must observe the applied sequence");
let rows = sqlx::query(
"select version, description, checksum from __crank_core_migrations order by version",
)
.fetch_all(first.pool())
.await
.expect("migration ledger must be readable");
let first = PostgresRegistry::connect(&database_url)
.await
.expect("service startup must verify the migrated schema");
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].get::<i32, _>("version"), 1);
assert_eq!(
rows[0].get::<String, _>("description"),
"community baseline"
);
let rows =
sqlx::query("select version, name, checksum from __crank_migrations order by version")
.fetch_all(first.pool())
.await
.expect("migration ledger must be readable");
assert_eq!(rows.len(), 3);
assert_eq!(rows[0].get::<i64, _>("version"), 1);
assert_eq!(rows[0].get::<String, _>("name"), "community-baseline-v1");
assert_eq!(
rows[0].get::<String, _>("checksum"),
"crank-community-baseline-v1"
);
assert_eq!(rows[1].get::<i64, _>("version"), 2);
assert_eq!(rows[1].get::<String, _>("name"), "legacy-consolidation-v2");
assert_eq!(rows[1].get::<String, _>("checksum").len(), 64);
assert_eq!(rows[2].get::<i64, _>("version"), 3);
assert_eq!(
rows[2].get::<String, _>("name"),
"request-trace-identity-v3"
);
assert_eq!(rows[2].get::<String, _>("checksum").len(), 64);
let approval_columns = sqlx::query(
"select column_name
@@ -41,4 +53,657 @@ async fn core_migration_is_versioned_and_safe_under_concurrent_startup() {
.await
.expect("approval schema must be readable");
assert_eq!(approval_columns.len(), 3);
assert_eq!(
MigrationAuthority::preflight(first.pool()).await.unwrap(),
MigrationPreflight::Current { version: 3 }
);
}
#[tokio::test]
async fn service_connect_is_read_only_on_fresh_database() {
let database_url = crank_test_support::postgres_schema_url("test_read_only_startup").await;
let error = PostgresRegistry::connect(&database_url)
.await
.expect_err("fresh schema must require the controlled migration command");
assert!(error.to_string().contains("schema_missing"));
let pool = sqlx::PgPool::connect(&database_url).await.unwrap();
let ledger = sqlx::query("select to_regclass('__crank_migrations')::text as name")
.fetch_one(&pool)
.await
.unwrap()
.try_get::<Option<String>, _>("name")
.unwrap();
assert_eq!(
ledger, None,
"startup compatibility check must not create DDL"
);
}
#[tokio::test]
async fn changed_checksum_fails_closed_without_repair() {
let database_url = crank_test_support::postgres_schema_url("test_changed_checksum").await;
let pool = sqlx::PgPool::connect(&database_url).await.unwrap();
MigrationAuthority::apply(&pool).await.unwrap();
sqlx::query("update __crank_migrations set checksum = 'changed' where version = 1")
.execute(&pool)
.await
.unwrap();
let error = MigrationAuthority::apply(&pool)
.await
.expect_err("published checksum mismatch must fail closed");
assert_eq!(error.code(), "checksum_mismatch");
let checksum = sqlx::query("select checksum from __crank_migrations where version = 1")
.fetch_one(&pool)
.await
.unwrap()
.get::<String, _>("checksum");
assert_eq!(
checksum, "changed",
"authority must not rewrite corrupt history"
);
}
#[tokio::test]
async fn legacy_core_baseline_is_consolidated_without_data_loss() {
let database_url = crank_test_support::postgres_schema_url("test_legacy_core").await;
let pool = sqlx::PgPool::connect(&database_url).await.unwrap();
MigrationAuthority::apply(&pool).await.unwrap();
sqlx::query(
"insert into workspaces (id, slug, display_name, status, settings_json, created_at, updated_at)
values ('ws_preserved', 'preserved', 'Preserved', 'active', '{}'::jsonb, now(), now())",
)
.execute(&pool)
.await
.unwrap();
sqlx::raw_sql(
"insert into operations
(id, workspace_id, name, display_name, protocol, status, created_at, updated_at)
values ('op_preserved', 'ws_default', 'preserved', 'Preserved', 'rest', 'draft', now(), now());
insert into operation_versions
(operation_id, version, status, target_json, input_schema_json, output_schema_json,
input_mapping_json, output_mapping_json, execution_config_json,
tool_description_json, created_at)
values ('op_preserved', 1, 'draft', '{}'::jsonb, '{}'::jsonb, '{}'::jsonb,
'{}'::jsonb, '{}'::jsonb, '{}'::jsonb, '{}'::jsonb, now());
insert into agents
(id, workspace_id, slug, display_name, description, status, created_at, updated_at)
values ('agent_preserved', 'ws_default', 'preserved', 'Preserved', '', 'draft', now(), now());
insert into agent_versions
(agent_id, version, status, instructions_json, tool_selection_policy_json, created_at)
values ('agent_preserved', 1, 'draft', '{}'::jsonb, '{}'::jsonb, now());
insert into platform_api_keys
(id, workspace_id, agent_id, name, prefix, secret_hash, scopes_json, status, created_at)
values ('key_preserved', 'ws_default', 'agent_preserved', 'Preserved', 'cp_', 'hash', '[]'::jsonb, 'active', now());
insert into approval_requests
(id, workspace_id, agent_id, operation_id, operation_version, status, risk_level,
request_payload_json, created_at, expires_at)
values ('approval_preserved', 'ws_default', 'agent_preserved', 'op_preserved', 1,
'pending', 'high', '{}'::jsonb, now(), now() + interval '1 hour');
insert into invocation_logs
(id, workspace_id, agent_id, operation_id, source, level, status, tool_name,
message, duration_ms, request_preview_json, response_preview_json, created_at)
values ('log_preserved', 'ws_default', 'agent_preserved', 'op_preserved', 'mcp',
'info', 'success', 'preserved', 'safe', 1, '{}'::jsonb, '{}'::jsonb, now());",
)
.execute(&pool)
.await
.unwrap();
remove_v3_schema(&pool).await;
let tables = [
"operations",
"operation_versions",
"agents",
"agent_versions",
"platform_api_keys",
"approval_requests",
"invocation_logs",
];
let mut before = Vec::new();
for table in tables {
let row = if table == "invocation_logs" {
"to_jsonb(t) - 'trace_id'"
} else {
"to_jsonb(t)"
};
let sql = format!("select jsonb_agg({row} order by {row}::text)::text from {table} t");
before.push(
sqlx::query_scalar::<_, Option<String>>(sqlx::AssertSqlSafe(sql))
.fetch_one(&pool)
.await
.unwrap(),
);
}
sqlx::query(
"drop table __crank_migrations, __crank_migration_legacy_audit,
__crank_mcp_migrations, mcp_transport_sessions, __crank_ext_migrations",
)
.execute(&pool)
.await
.unwrap();
assert_eq!(
MigrationAuthority::preflight(&pool).await.unwrap(),
MigrationPreflight::MigrationRequired {
current: 1,
target: 3,
}
);
MigrationAuthority::apply(&pool).await.unwrap();
let display_name = sqlx::query("select display_name from workspaces where id = 'ws_preserved'")
.fetch_one(&pool)
.await
.unwrap()
.get::<String, _>("display_name");
assert_eq!(display_name, "Preserved");
let mut after = Vec::new();
for table in tables {
let row = if table == "invocation_logs" {
"to_jsonb(t) - 'trace_id'"
} else {
"to_jsonb(t)"
};
let sql = format!("select jsonb_agg({row} order by {row}::text)::text from {table} t");
after.push(
sqlx::query_scalar::<_, Option<String>>(sqlx::AssertSqlSafe(sql))
.fetch_one(&pool)
.await
.unwrap(),
);
}
assert_eq!(before, after, "brownfield rows must remain byte-equivalent");
}
#[tokio::test]
async fn legacy_mcp_sessions_survive_consolidation() {
let database_url = crank_test_support::postgres_schema_url("test_legacy_mcp").await;
let pool = sqlx::PgPool::connect(&database_url).await.unwrap();
MigrationAuthority::apply(&pool).await.unwrap();
sqlx::query(
"insert into mcp_transport_sessions (
id, protocol_version, initialized, supports_elicitation,
workspace_slug, agent_slug, created_at, updated_at, expires_at
) values ('session_preserved', '2025-11-25', true, false,
'default', 'agent', now(), now(), null)",
)
.execute(&pool)
.await
.unwrap();
remove_v3_schema(&pool).await;
sqlx::query("drop table __crank_migrations, __crank_migration_legacy_audit")
.execute(&pool)
.await
.unwrap();
MigrationAuthority::apply(&pool).await.unwrap();
let count = sqlx::query("select count(*)::bigint as count from mcp_transport_sessions where id = 'session_preserved'")
.fetch_one(&pool)
.await
.unwrap()
.get::<i64, _>("count");
assert_eq!(count, 1);
}
#[tokio::test]
async fn repeated_apply_does_not_rewrite_audit_timestamps() {
let database_url = crank_test_support::postgres_schema_url("test_repeat_apply").await;
let pool = sqlx::PgPool::connect(&database_url).await.unwrap();
MigrationAuthority::apply(&pool).await.unwrap();
let before = sqlx::query("select applied_at from __crank_migrations order by version")
.fetch_all(&pool)
.await
.unwrap()
.into_iter()
.map(|row| row.get::<time::OffsetDateTime, _>("applied_at"))
.collect::<Vec<_>>();
MigrationAuthority::apply(&pool).await.unwrap();
let after = sqlx::query("select applied_at from __crank_migrations order by version")
.fetch_all(&pool)
.await
.unwrap()
.into_iter()
.map(|row| row.get::<time::OffsetDateTime, _>("applied_at"))
.collect::<Vec<_>>();
assert_eq!(before, after);
}
#[tokio::test]
async fn future_sequence_fails_closed() {
let database_url = crank_test_support::postgres_schema_url("test_future_sequence").await;
let pool = sqlx::PgPool::connect(&database_url).await.unwrap();
MigrationAuthority::apply(&pool).await.unwrap();
sqlx::query("update __crank_migrations set version = 4 where version = 3")
.execute(&pool)
.await
.unwrap();
assert_eq!(
MigrationAuthority::preflight(&pool)
.await
.unwrap_err()
.code(),
"future_version"
);
}
#[tokio::test]
async fn healthy_v2_is_reported_as_migration_required_and_upgrades_to_v3() {
let database_url = crank_test_support::postgres_schema_url("test_v2_to_v3_identity").await;
let pool = sqlx::PgPool::connect(&database_url).await.unwrap();
MigrationAuthority::apply(&pool).await.unwrap();
sqlx::query("delete from __crank_migrations where version = 3")
.execute(&pool)
.await
.unwrap();
remove_v3_schema(&pool).await;
assert_eq!(
MigrationAuthority::preflight(&pool).await.unwrap(),
MigrationPreflight::MigrationRequired {
current: 2,
target: 3,
}
);
MigrationAuthority::apply(&pool).await.unwrap();
assert_eq!(
MigrationAuthority::preflight(&pool).await.unwrap(),
MigrationPreflight::Current { version: 3 }
);
let trace_column: bool = sqlx::query_scalar(
"select exists (
select 1 from information_schema.columns
where table_schema = current_schema()
and table_name = 'invocation_logs'
and column_name = 'trace_id'
)",
)
.fetch_one(&pool)
.await
.unwrap();
assert!(trace_column);
}
#[tokio::test]
async fn v2_with_partial_v3_objects_fails_before_apply() {
let database_url = crank_test_support::postgres_schema_url("test_v2_partial_v3").await;
let pool = sqlx::PgPool::connect(&database_url).await.unwrap();
MigrationAuthority::apply(&pool).await.unwrap();
sqlx::query("delete from __crank_migrations where version = 3")
.execute(&pool)
.await
.unwrap();
sqlx::raw_sql(
"drop index invocation_logs_workspace_request_id_idx;
drop index invocation_logs_workspace_trace_id_idx;
alter table invocation_logs drop constraint invocation_logs_trace_id_format_check;",
)
.execute(&pool)
.await
.unwrap();
let error = MigrationAuthority::preflight(&pool).await.unwrap_err();
assert_eq!(error.code(), "partial_sequence");
assert_eq!(error.stage(), "preflight.schema");
}
#[tokio::test]
async fn v3_rejects_same_named_constraint_or_index_with_wrong_definition() {
let database_url = crank_test_support::postgres_schema_url("test_v3_named_drift").await;
let pool = sqlx::PgPool::connect(&database_url).await.unwrap();
MigrationAuthority::apply(&pool).await.unwrap();
sqlx::raw_sql(
"alter table invocation_logs drop constraint invocation_logs_trace_id_format_check;
alter table invocation_logs add constraint invocation_logs_trace_id_format_check
check (true) not valid;",
)
.execute(&pool)
.await
.unwrap();
assert_eq!(
MigrationAuthority::preflight(&pool)
.await
.unwrap_err()
.code(),
"partial_sequence"
);
sqlx::raw_sql(
"alter table invocation_logs drop constraint invocation_logs_trace_id_format_check;
alter table invocation_logs add constraint invocation_logs_trace_id_format_check
check (
trace_id is null or (
trace_id ~ '^[0-9a-f]{32}$'
and trace_id <> '00000000000000000000000000000000'
)
) not valid;
drop index invocation_logs_workspace_trace_id_idx;
create index invocation_logs_workspace_trace_id_idx on invocation_logs(trace_id);",
)
.execute(&pool)
.await
.unwrap();
assert_eq!(
MigrationAuthority::preflight(&pool)
.await
.unwrap_err()
.code(),
"partial_sequence"
);
}
#[tokio::test]
async fn v3_upgrade_ignores_oversized_legacy_request_ids_in_partial_index() {
let database_url = crank_test_support::postgres_schema_url("test_v3_legacy_request_id").await;
let pool = sqlx::PgPool::connect(&database_url).await.unwrap();
MigrationAuthority::apply(&pool).await.unwrap();
sqlx::raw_sql(
"insert into operations
(id, workspace_id, name, display_name, protocol, status, created_at, updated_at)
values ('op_legacy_request', 'ws_default', 'legacy-request', 'Legacy Request',
'rest', 'draft', now(), now());",
)
.execute(&pool)
.await
.unwrap();
sqlx::query(
"insert into invocation_logs
(id, workspace_id, operation_id, source, level, status, tool_name, message,
request_id, duration_ms, request_preview_json, response_preview_json, created_at)
values ('legacy_request_log', 'ws_default', 'op_legacy_request', 'admin', 'info',
'success', 'legacy_request', 'safe', $1, 1, '{}'::jsonb, '{}'::jsonb, now())",
)
.bind("x".repeat(10_000))
.execute(&pool)
.await
.unwrap();
sqlx::query("delete from __crank_migrations where version = 3")
.execute(&pool)
.await
.unwrap();
remove_v3_schema(&pool).await;
MigrationAuthority::apply(&pool).await.unwrap();
assert_eq!(
MigrationAuthority::preflight(&pool).await.unwrap(),
MigrationPreflight::Current { version: 3 }
);
}
async fn remove_v3_schema(pool: &sqlx::PgPool) {
sqlx::raw_sql(
"drop index if exists invocation_logs_workspace_request_id_idx;
alter table invocation_logs drop column if exists trace_id;",
)
.execute(pool)
.await
.unwrap();
}
#[tokio::test]
async fn partial_sequence_fails_closed() {
let database_url = crank_test_support::postgres_schema_url("test_partial_sequence").await;
let pool = sqlx::PgPool::connect(&database_url).await.unwrap();
MigrationAuthority::apply(&pool).await.unwrap();
sqlx::query("delete from __crank_migrations where version = 1")
.execute(&pool)
.await
.unwrap();
assert_eq!(
MigrationAuthority::preflight(&pool)
.await
.unwrap_err()
.code(),
"partial_sequence"
);
}
#[tokio::test]
async fn missing_relation_with_current_ledger_fails_closed() {
let database_url = crank_test_support::postgres_schema_url("test_missing_relation").await;
let pool = sqlx::PgPool::connect(&database_url).await.unwrap();
MigrationAuthority::apply(&pool).await.unwrap();
sqlx::query("drop table usage_rollups")
.execute(&pool)
.await
.unwrap();
let error = MigrationAuthority::preflight(&pool).await.unwrap_err();
assert_eq!(error.code(), "partial_sequence");
assert_eq!(error.stage(), "preflight.schema");
}
#[tokio::test]
async fn unregistered_legacy_extension_provenance_is_rejected() {
let database_url = crank_test_support::postgres_schema_url("test_legacy_extension").await;
let pool = sqlx::PgPool::connect(&database_url).await.unwrap();
MigrationAuthority::apply(&pool).await.unwrap();
sqlx::query("drop table __crank_migrations, __crank_migration_legacy_audit")
.execute(&pool)
.await
.unwrap();
let checksum = "a".repeat(64);
sqlx::query(
"insert into __crank_ext_migrations (extension_name, version, checksum)
values ('known-extension', 1, $1)",
)
.bind(&checksum)
.execute(&pool)
.await
.unwrap();
let error = MigrationAuthority::apply(&pool).await.unwrap_err();
assert_eq!(error.code(), "legacy_conflict");
}
#[tokio::test]
async fn any_owned_relation_without_core_ledger_is_partial() {
let database_url = crank_test_support::postgres_schema_url("test_owned_partial").await;
let pool = sqlx::PgPool::connect(&database_url).await.unwrap();
sqlx::query("create table users (id text primary key)")
.execute(&pool)
.await
.unwrap();
let error = MigrationAuthority::preflight(&pool).await.unwrap_err();
assert_eq!(error.code(), "partial_sequence");
}
#[tokio::test]
async fn current_ledger_with_structural_drift_fails_closed() {
let database_url = crank_test_support::postgres_schema_url("test_structural_drift").await;
let pool = sqlx::PgPool::connect(&database_url).await.unwrap();
MigrationAuthority::apply(&pool).await.unwrap();
sqlx::query("alter table mcp_transport_sessions drop column supports_elicitation")
.execute(&pool)
.await
.unwrap();
let error = MigrationAuthority::preflight(&pool).await.unwrap_err();
assert_eq!(error.code(), "partial_sequence");
assert_eq!(error.stage(), "preflight.schema");
}
#[tokio::test]
async fn tampered_legacy_audit_fails_closed() {
let database_url = crank_test_support::postgres_schema_url("test_tampered_audit").await;
let pool = sqlx::PgPool::connect(&database_url).await.unwrap();
MigrationAuthority::apply(&pool).await.unwrap();
sqlx::query(
"update __crank_migration_legacy_audit set source_checksum = 'tampered' where source = 'core'",
)
.execute(&pool)
.await
.unwrap();
let error = MigrationAuthority::preflight(&pool).await.unwrap_err();
assert_eq!(error.code(), "legacy_conflict");
}
#[tokio::test]
async fn failed_consolidation_rolls_back_all_changes() {
let _event_trigger_guard = EVENT_TRIGGER_TEST_LOCK.lock().await;
let database_url = crank_test_support::postgres_schema_url("test_apply_rollback").await;
let pool = sqlx::PgPool::connect(&database_url).await.unwrap();
MigrationAuthority::apply(&pool).await.unwrap();
sqlx::query(
"insert into workspaces (id, slug, display_name, status, settings_json, created_at, updated_at)
values ('rollback_preserved', 'rollback-preserved', 'Rollback Preserved', 'active', '{}'::jsonb, now(), now())",
)
.execute(&pool)
.await
.unwrap();
sqlx::query(
"drop table __crank_migrations, __crank_migration_legacy_audit,
__crank_mcp_migrations, mcp_transport_sessions, __crank_ext_migrations",
)
.execute(&pool)
.await
.unwrap();
let schema: String = sqlx::query_scalar("select current_schema()")
.fetch_one(&pool)
.await
.unwrap();
let failure_trigger = format!(
"create function reject_story14_v2() returns event_trigger language plpgsql as $$
begin
if current_schema() = '{schema}' and current_query() like '%__crank_migrations%' then
raise exception 'injected v2 ddl failure';
end if;
end $$;
create event trigger reject_story14_v2 on ddl_command_start
execute function reject_story14_v2();"
);
sqlx::raw_sql(sqlx::AssertSqlSafe(failure_trigger))
.execute(&pool)
.await
.unwrap();
let error = MigrationAuthority::apply(&pool).await.unwrap_err();
sqlx::raw_sql(
"drop event trigger reject_story14_v2;
drop function reject_story14_v2();",
)
.execute(&pool)
.await
.unwrap();
assert_eq!(error.code(), "apply_failed");
for relation in [
"__crank_migrations",
"__crank_migration_legacy_audit",
"__crank_mcp_migrations",
"mcp_transport_sessions",
"__crank_ext_migrations",
] {
let present: bool = sqlx::query_scalar(
"select to_regclass(format('%I.%I', current_schema(), $1)) is not null",
)
.bind(relation)
.fetch_one(&pool)
.await
.unwrap();
assert!(!present, "{relation} must roll back with failed v2 DDL");
}
let preserved: String =
sqlx::query_scalar("select display_name from workspaces where id = 'rollback_preserved'")
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(preserved, "Rollback Preserved");
}
#[tokio::test]
async fn failed_request_trace_identity_migration_rolls_back_all_changes() {
let _event_trigger_guard = EVENT_TRIGGER_TEST_LOCK.lock().await;
let database_url =
crank_test_support::postgres_schema_url("test_trace_identity_rollback").await;
let pool = sqlx::PgPool::connect(&database_url).await.unwrap();
MigrationAuthority::apply(&pool).await.unwrap();
sqlx::raw_sql(
"insert into operations
(id, workspace_id, name, display_name, protocol, status, created_at, updated_at)
values ('op_trace_rollback', 'ws_default', 'trace-rollback', 'Trace rollback',
'rest', 'draft', now(), now());
insert into invocation_logs
(id, workspace_id, operation_id, source, level, status, tool_name, message,
duration_ms, request_preview_json, response_preview_json, created_at)
values ('trace_rollback_preserved', 'ws_default', 'op_trace_rollback', 'admin',
'info', 'success', 'trace_rollback', 'safe preserved row', 1,
'{}'::jsonb, '{}'::jsonb, now());",
)
.execute(&pool)
.await
.unwrap();
sqlx::query("delete from __crank_migrations where version = 3")
.execute(&pool)
.await
.unwrap();
remove_v3_schema(&pool).await;
let schema: String = sqlx::query_scalar("select current_schema()")
.fetch_one(&pool)
.await
.unwrap();
let failure_trigger = format!(
"create function reject_story15_v3() returns event_trigger language plpgsql as $$
begin
if current_schema() = '{schema}' and current_query() like '%invocation_logs_workspace_trace_id_idx%' then
raise exception 'injected v3 ddl failure';
end if;
end $$;
create event trigger reject_story15_v3 on ddl_command_start
execute function reject_story15_v3();"
);
sqlx::raw_sql(sqlx::AssertSqlSafe(failure_trigger))
.execute(&pool)
.await
.unwrap();
let error = MigrationAuthority::apply(&pool).await.unwrap_err();
sqlx::raw_sql(
"drop event trigger reject_story15_v3;
drop function reject_story15_v3();",
)
.execute(&pool)
.await
.unwrap();
assert_eq!(error.code(), "apply_failed");
assert_eq!(error.version(), Some(3));
let trace_column: bool = sqlx::query_scalar(
"select exists (
select 1 from information_schema.columns
where table_schema = current_schema()
and table_name = 'invocation_logs'
and column_name = 'trace_id'
)",
)
.fetch_one(&pool)
.await
.unwrap();
assert!(!trace_column, "trace_id column must roll back with v3");
for index in [
"invocation_logs_workspace_request_id_idx",
"invocation_logs_workspace_trace_id_idx",
] {
let present: bool = sqlx::query_scalar(
"select to_regclass(format('%I.%I', current_schema(), $1)) is not null",
)
.bind(index)
.fetch_one(&pool)
.await
.unwrap();
assert!(!present, "{index} must roll back with failed v3 DDL");
}
let ledger_v3: i64 =
sqlx::query_scalar("select count(*) from __crank_migrations where version = 3")
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(ledger_v3, 0, "failed v3 must not be recorded as applied");
let preserved: String = sqlx::query_scalar(
"select message from invocation_logs where id = 'trace_rollback_preserved'",
)
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(preserved, "safe preserved row");
}