feat: complete Epic 1 production foundation

This commit is contained in:
2026-08-25 01:24:11 +03:00
parent 767428436d
commit 182bde8ac0
298 changed files with 35719 additions and 5299 deletions
@@ -0,0 +1,44 @@
use sqlx::{Postgres, Transaction, query};
use super::authority::{MigrationDescriptor, MigrationError};
pub(super) const SOURCE: &str = include_str!("admin_auth_lifecycle_v8.sql");
pub(super) const SOURCE_SHA256: &str =
"6361f3321a1442a77c695cad9b30c4702de1aa3e565bfbf1a7415d653302611f";
pub(super) async fn apply(
transaction: &mut Transaction<'_, Postgres>,
descriptor: &MigrationDescriptor,
) -> Result<(), MigrationError> {
sqlx::raw_sql(SOURCE)
.execute(&mut **transaction)
.await
.map_err(|_| {
MigrationError::new(
"apply_failed",
"apply.admin_auth_lifecycle",
Some(8),
"restore_known_good_backup",
)
})?;
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(8),
"restore_known_good_backup",
)
})?;
Ok(())
}
@@ -0,0 +1,122 @@
create table admin_bootstrap_contracts (
id text primary key,
token_hash text not null unique,
email text not null,
display_name text not null,
status text not null,
attempts integer not null default 0,
expires_at timestamptz not null,
created_at timestamptz not null default now(),
used_at timestamptz,
used_by_user_id text references users(id),
constraint admin_bootstrap_contracts_id_check check (
octet_length(id) between 1 and 128
and id !~ '[[:cntrl:]]'
),
constraint admin_bootstrap_contracts_token_hash_check check (
token_hash ~ '^[A-Za-z0-9_-]{43,86}$'
),
constraint admin_bootstrap_contracts_email_check check (
octet_length(email) between 3 and 254
and email like '%@%'
and email !~ '[[:space:][:cntrl:]<>]'
),
constraint admin_bootstrap_contracts_display_name_check check (
octet_length(display_name) between 1 and 160
and display_name !~ '[[:cntrl:]<>]'
),
constraint admin_bootstrap_contracts_status_check check (
status in ('active', 'used', 'expired', 'revoked')
),
constraint admin_bootstrap_contracts_attempts_check check (
attempts between 0 and 100
),
constraint admin_bootstrap_contracts_used_shape_check check (
(status = 'used' and used_at is not null and used_by_user_id is not null)
or (status <> 'used' and used_by_user_id is null)
)
);
create unique index admin_bootstrap_contracts_single_active_idx
on admin_bootstrap_contracts (status)
where status = 'active';
create index admin_bootstrap_contracts_token_hash_idx
on admin_bootstrap_contracts (token_hash);
alter table user_sessions
add column csrf_hash text,
add column revoked_at timestamptz,
add constraint user_sessions_csrf_hash_check check (
csrf_hash is null or csrf_hash ~ '^[A-Za-z0-9_-]{43,86}$'
);
create table admin_login_backoff (
scope_hash text primary key,
failure_count integer not null default 0,
locked_until timestamptz,
last_attempt_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint admin_login_backoff_scope_hash_check check (
scope_hash ~ '^[A-Za-z0-9_-]{43,86}$'
),
constraint admin_login_backoff_failure_count_check check (
failure_count between 0 and 1000
)
);
create table admin_security_audit_events (
id text primary key,
action text not null,
outcome text not null,
actor_user_id text references users(id),
session_id text,
request_id text,
trace_id text,
source text not null,
created_at timestamptz not null default now(),
constraint admin_security_audit_events_id_check check (
octet_length(id) between 1 and 128
and id !~ '[[:cntrl:]]'
),
constraint admin_security_audit_events_action_check check (
action in (
'bootstrap_created',
'bootstrap_completed',
'bootstrap_rejected',
'login_succeeded',
'login_rejected',
'logout',
'password_rotated',
'recovery_completed',
'session_revoked'
)
),
constraint admin_security_audit_events_outcome_check check (
outcome in ('success', 'rejected', 'rate_limited', 'expired', 'conflict')
),
constraint admin_security_audit_events_session_id_check check (
session_id is null
or (
octet_length(session_id) between 1 and 128
and session_id !~ '[[:cntrl:]]'
)
),
constraint admin_security_audit_events_request_id_check check (
request_id is null
or (
octet_length(request_id) between 1 and 128
and request_id !~ '[[:cntrl:],;]'
)
),
constraint admin_security_audit_events_trace_id_check check (
trace_id is null or trace_id ~ '^[0-9a-f]{32}$'
),
constraint admin_security_audit_events_source_check check (
octet_length(source) between 1 and 128
and source !~ '[[:cntrl:]]'
)
);
create index admin_security_audit_events_created_idx
on admin_security_audit_events (created_at desc);
@@ -0,0 +1,44 @@
use sqlx::{Postgres, Transaction, query};
use super::authority::{MigrationDescriptor, MigrationError};
pub(super) const SOURCE: &str = include_str!("agent_catalog_lifecycle_v9.sql");
pub(super) const SOURCE_SHA256: &str =
"b49bf4b53691407ec27e3810b0531c0d440da7dddc62ea71eb219933c0a30211";
pub(super) async fn apply(
transaction: &mut Transaction<'_, Postgres>,
descriptor: &MigrationDescriptor,
) -> Result<(), MigrationError> {
sqlx::raw_sql(SOURCE)
.execute(&mut **transaction)
.await
.map_err(|_| {
MigrationError::new(
"apply_failed",
"apply.agent_catalog_lifecycle",
Some(9),
"restore_known_good_backup",
)
})?;
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(9),
"restore_known_good_backup",
)
})?;
Ok(())
}
@@ -0,0 +1,100 @@
alter table agents
add column catalog_revision bigint not null default 0,
add constraint agents_catalog_revision_check check (catalog_revision >= 0);
alter table published_agents
add column catalog_revision bigint not null default 1,
add constraint published_agents_catalog_revision_check check (catalog_revision > 0);
update agents a
set catalog_revision = case when pa.agent_id is null then 0 else greatest(pa.catalog_revision, 1) end
from agents source
left join published_agents pa on pa.agent_id = source.id
where a.id = source.id;
create function crank_reject_published_agent_version_mutation()
returns trigger
language plpgsql
as $$
begin
if old.status = 'published' then
if tg_op = 'DELETE' then
raise exception 'published Agent Version is immutable'
using errcode = '23514';
end if;
if row(old.*) is distinct from row(new.*) then
raise exception 'published Agent Version is immutable'
using errcode = '23514';
end if;
end if;
if tg_op = 'DELETE' then
return old;
end if;
return new;
end;
$$;
create trigger agent_versions_immutable_guard
before update or delete on agent_versions
for each row
execute function crank_reject_published_agent_version_mutation();
create function crank_reject_published_agent_binding_mutation()
returns trigger
language plpgsql
as $$
declare
bound_status text;
checked_agent_id text;
checked_agent_version integer;
begin
checked_agent_id := coalesce(new.agent_id, old.agent_id);
checked_agent_version := coalesce(new.agent_version, old.agent_version);
select status
into bound_status
from agent_versions
where agent_id = checked_agent_id and version = checked_agent_version;
if bound_status = 'published' then
raise exception 'published Agent catalog bindings are immutable'
using errcode = '23514';
end if;
if tg_op = 'DELETE' then
return old;
end if;
return new;
end;
$$;
create trigger agent_operation_bindings_immutable_guard
before insert or update or delete on agent_operation_bindings
for each row
execute function crank_reject_published_agent_binding_mutation();
create function crank_reject_published_agent_pointer_rewind()
returns trigger
language plpgsql
as $$
begin
if tg_op = 'DELETE' then
raise exception 'published Agent catalog pointer is immutable'
using errcode = '23514';
end if;
if tg_op = 'INSERT' then
return new;
end if;
if new.catalog_revision <= old.catalog_revision then
raise exception 'published Agent catalog revision must increase'
using errcode = '23514';
end if;
if new.version < old.version then
raise exception 'published Agent catalog version cannot rewind'
using errcode = '23514';
end if;
return new;
end;
$$;
create trigger published_agents_monotonic_guard
before insert or update or delete on published_agents
for each row
execute function crank_reject_published_agent_pointer_rewind();
@@ -0,0 +1,44 @@
use sqlx::{Postgres, Transaction, query};
use super::authority::{MigrationDescriptor, MigrationError};
pub(super) const SOURCE: &str = include_str!("approval_side_effects_v10.sql");
pub(super) const SOURCE_SHA256: &str =
"6c87f680efdbc275fa2b920826b3e3e390aa34d6f2f1db48fe074a5d7691ba5b";
pub(super) async fn apply(
transaction: &mut Transaction<'_, Postgres>,
descriptor: &MigrationDescriptor,
) -> Result<(), MigrationError> {
sqlx::raw_sql(SOURCE)
.execute(&mut **transaction)
.await
.map_err(|_| {
MigrationError::new(
"apply_failed",
"apply.approval_side_effects",
Some(10),
"restore_known_good_backup",
)
})?;
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(10),
"restore_known_good_backup",
)
})?;
Ok(())
}
@@ -0,0 +1,37 @@
alter table approval_requests
add column if not exists request_id text null;
alter table approval_requests
add column if not exists trace_id text null;
alter table approval_requests
add constraint approval_requests_request_id_check check (
request_id is null
or (
octet_length(request_id) between 1 and 128
and request_id !~ '[[:cntrl:],;]'
)
) not valid;
alter table approval_requests
add constraint approval_requests_trace_id_check check (
trace_id is null
or (
trace_id ~ '^[0-9a-f]{32}$'
and trace_id <> '00000000000000000000000000000000'
)
) not valid;
create unique index if not exists approval_requests_pending_scope_fingerprint_idx
on approval_requests(
workspace_id,
agent_id,
operation_id,
operation_version,
request_fingerprint
)
where status = 'pending' and request_fingerprint is not null;
create index if not exists approval_requests_workspace_request_trace_idx
on approval_requests(workspace_id, request_id, trace_id)
where request_id is not null or trace_id is not null;
+174 -113
View File
@@ -1,17 +1,22 @@
use std::fmt;
use sha2::{Digest, Sha256};
use sqlx::{PgConnection, PgPool, Row, Transaction, query};
use super::admin_auth_lifecycle_v8;
use super::agent_catalog_lifecycle_v9;
use super::approval_side_effects_v10;
use super::execution_outcome_v5;
use super::master_key_identity_v7;
use super::onboarding_product_events_v11;
use super::owned_relations;
use super::platform_key_name_reuse_v6;
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;
use sha2::{Digest, Sha256};
use sqlx::{PgConnection, PgPool, Row, Transaction, query};
use std::fmt;
const MIGRATION_LOCK_ID: i64 = 0x4352_414E_4B4D_4947;
const CURRENT_VERSION: i64 = 3;
const IMPLEMENTED_VERSIONS: &[i64] = &[1, 2, 3];
const CURRENT_VERSION: i64 = 11;
const IMPLEMENTED_VERSIONS: &[i64] = &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
const BASELINE_SOURCE_SHA256: &str =
"eb1656fc5b4b5be9ee390d237d1d58e4b2274ae5ba9b7ba06a2f3f860dfda675";
const CONSOLIDATION_SOURCE: &str = include_str!("consolidation_v2.sql");
@@ -20,41 +25,10 @@ const CONSOLIDATION_SOURCE_SHA256: &str =
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 OPERATION_LIFECYCLE_SOURCE: &str = include_str!("operation_lifecycle_v4.sql");
const OPERATION_LIFECYCLE_SOURCE_SHA256: &str =
"45723712a1ea49cd8bbf59d77148c225f3ec376df7433983d982cc6f9d7fb39c";
const REGISTERED_EXTENSION_MIGRATIONS: &[(&str, ExtensionMigration)] = &[];
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MigrationDescriptor {
pub version: i64,
@@ -70,7 +44,6 @@ pub struct MigrationDescriptor {
pub readable_schema_max: i64,
pub contract_evidence: Option<&'static str>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BackfillPolicy {
None,
@@ -80,14 +53,12 @@ pub enum BackfillPolicy {
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 {
@@ -113,19 +84,16 @@ impl BackfillBatch {
}
}
}
#[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,
@@ -133,7 +101,6 @@ pub struct MigrationError {
version: Option<i64>,
recovery: &'static str,
}
impl MigrationError {
pub(super) fn new(
code: &'static str,
@@ -148,28 +115,22 @@ impl MigrationError {
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!(
@@ -183,16 +144,33 @@ impl fmt::Display for MigrationError {
)
}
}
impl std::error::Error for MigrationError {}
pub struct MigrationAuthority;
fn expand_descriptor(
version: i64,
name: &'static str,
checksum: &'static str,
readable_schema_min: i64,
) -> MigrationDescriptor {
MigrationDescriptor {
version,
name,
checksum: checksum.to_owned(),
source_digest: checksum.to_owned(),
phase: "expand",
compatibility: "n-minus-one-readable",
owner: "crank-registry",
transactional: true,
backfill: BackfillPolicy::None,
readable_schema_min,
readable_schema_max: version,
contract_evidence: None,
}
}
impl MigrationAuthority {
pub fn registered_extension_migrations() -> &'static [(&'static str, ExtensionMigration)] {
REGISTERED_EXTENSION_MIGRATIONS
}
pub fn sequence() -> Vec<MigrationDescriptor> {
vec![
MigrationDescriptor {
@@ -209,41 +187,66 @@ impl MigrationAuthority {
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,
},
expand_descriptor(2, "legacy-consolidation-v2", CONSOLIDATION_SOURCE_SHA256, 1),
expand_descriptor(
3,
"request-trace-identity-v3",
REQUEST_TRACE_IDENTITY_SOURCE_SHA256,
2,
),
expand_descriptor(
4,
"operation-lifecycle-v4",
OPERATION_LIFECYCLE_SOURCE_SHA256,
3,
),
expand_descriptor(
5,
"execution-outcome-v5",
execution_outcome_v5::SOURCE_SHA256,
4,
),
expand_descriptor(
6,
"platform-key-name-reuse-v6",
platform_key_name_reuse_v6::SOURCE_SHA256,
5,
),
expand_descriptor(
7,
"master-key-identity-v7",
master_key_identity_v7::SOURCE_SHA256,
6,
),
expand_descriptor(
8,
"admin-auth-lifecycle-v8",
admin_auth_lifecycle_v8::SOURCE_SHA256,
7,
),
expand_descriptor(
9,
"agent-catalog-lifecycle-v9",
agent_catalog_lifecycle_v9::SOURCE_SHA256,
8,
),
expand_descriptor(
10,
"approval-side-effects-v10",
approval_side_effects_v10::SOURCE_SHA256,
9,
),
expand_descriptor(
11,
"onboarding-product-events-v11",
onboarding_product_events_v11::SOURCE_SHA256,
10,
),
]
}
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
@@ -252,7 +255,6 @@ impl MigrationAuthority {
.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(()),
@@ -270,7 +272,6 @@ impl MigrationAuthority {
)),
}
}
pub async fn apply(pool: &PgPool) -> Result<MigrationApplyResult, MigrationError> {
Self::validate_sequence()?;
let mut transaction = pool
@@ -296,7 +297,6 @@ impl MigrationAuthority {
MigrationError::storage("apply.lock")
}
})?;
let before = inspect(&mut transaction).await?;
let from = match before {
MigrationPreflight::Current { version } => {
@@ -308,7 +308,6 @@ impl MigrationAuthority {
}
MigrationPreflight::MigrationRequired { current, .. } => current,
};
if from == 0 {
create_core_ledger(&mut transaction).await?;
apply_baseline(&mut transaction).await.map_err(|_| {
@@ -335,14 +334,36 @@ impl MigrationAuthority {
)
})?;
}
if from < 2 {
apply_consolidation(&mut transaction).await?;
}
if from < 3 {
apply_request_trace_identity(&mut transaction).await?;
}
if from < 4 {
apply_operation_lifecycle(&mut transaction).await?;
}
if from < 5 {
execution_outcome_v5::apply(&mut transaction, &Self::sequence()[4]).await?;
}
if from < 6 {
platform_key_name_reuse_v6::apply(&mut transaction, &Self::sequence()[5]).await?;
}
if from < 7 {
master_key_identity_v7::apply(&mut transaction, &Self::sequence()[6]).await?;
}
if from < 8 {
admin_auth_lifecycle_v8::apply(&mut transaction, &Self::sequence()[7]).await?;
}
if from < 9 {
agent_catalog_lifecycle_v9::apply(&mut transaction, &Self::sequence()[8]).await?;
}
if from < 10 {
approval_side_effects_v10::apply(&mut transaction, &Self::sequence()[9]).await?;
}
if from < 11 {
onboarding_product_events_v11::apply(&mut transaction, &Self::sequence()[10]).await?;
}
transaction
.commit()
.await
@@ -353,11 +374,9 @@ impl MigrationAuthority {
})
}
}
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");
@@ -369,7 +388,6 @@ fn baseline_source_digest() -> String {
.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(
@@ -461,6 +479,19 @@ fn validate_descriptors(descriptors: &[MigrationDescriptor]) -> Result<(), Migra
|| sha256_hex(CONSOLIDATION_SOURCE.as_bytes()) != CONSOLIDATION_SOURCE_SHA256
|| sha256_hex(REQUEST_TRACE_IDENTITY_SOURCE.as_bytes())
!= REQUEST_TRACE_IDENTITY_SOURCE_SHA256
|| sha256_hex(OPERATION_LIFECYCLE_SOURCE.as_bytes()) != OPERATION_LIFECYCLE_SOURCE_SHA256
|| sha256_hex(execution_outcome_v5::SOURCE.as_bytes())
!= execution_outcome_v5::SOURCE_SHA256
|| sha256_hex(platform_key_name_reuse_v6::SOURCE.as_bytes())
!= platform_key_name_reuse_v6::SOURCE_SHA256
|| sha256_hex(master_key_identity_v7::SOURCE.as_bytes())
!= master_key_identity_v7::SOURCE_SHA256
|| sha256_hex(admin_auth_lifecycle_v8::SOURCE.as_bytes())
!= admin_auth_lifecycle_v8::SOURCE_SHA256
|| sha256_hex(agent_catalog_lifecycle_v9::SOURCE.as_bytes())
!= agent_catalog_lifecycle_v9::SOURCE_SHA256
|| sha256_hex(approval_side_effects_v10::SOURCE.as_bytes())
!= approval_side_effects_v10::SOURCE_SHA256
{
return Err(MigrationError::new(
"invalid_contract",
@@ -471,11 +502,9 @@ fn validate_descriptors(descriptors: &[MigrationDescriptor]) -> Result<(), Migra
}
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 {
@@ -495,18 +524,15 @@ async fn inspect(connection: &mut PgConnection) -> Result<MigrationPreflight, Mi
target: CURRENT_VERSION,
});
}
validate_core_ledger(connection).await?;
validate_required_relations(connection, BASELINE_RELATIONS, 1).await?;
validate_required_relations(connection, owned_relations::BASELINE, 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)
@@ -580,7 +606,6 @@ async fn inspect(connection: &mut PgConnection) -> Result<MigrationPreflight, Mi
));
}
}
let current = rows
.last()
.and_then(|row| row.try_get::<i64, _>("version").ok())
@@ -600,13 +625,19 @@ async fn inspect(connection: &mut PgConnection) -> Result<MigrationPreflight, Mi
target: CURRENT_VERSION,
})
} else {
validate_required_relations(connection, CONSOLIDATION_RELATIONS, CURRENT_VERSION).await?;
validate_required_relations(connection, owned_relations::CONSOLIDATION, CURRENT_VERSION)
.await?;
validate_required_relations(
connection,
owned_relations::ONBOARDING_PRODUCT_EVENTS,
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)
@@ -642,7 +673,6 @@ async fn validate_core_ledger(connection: &mut PgConnection) -> Result<(), Migra
}
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?;
@@ -749,7 +779,6 @@ async fn inspect_optional_legacy(connection: &mut PgConnection) -> Result<(), Mi
}
Ok(())
}
async fn validate_legacy_audit(connection: &mut PgConnection) -> Result<(), MigrationError> {
let rows = query(
"select source, source_version, source_checksum
@@ -800,7 +829,6 @@ async fn validate_legacy_audit(connection: &mut PgConnection) -> Result<(), Migr
}
Ok(())
}
async fn create_core_ledger(
transaction: &mut Transaction<'_, sqlx::Postgres>,
) -> Result<(), MigrationError> {
@@ -824,7 +852,6 @@ async fn create_core_ledger(
})?;
Ok(())
}
async fn apply_consolidation(
transaction: &mut Transaction<'_, sqlx::Postgres>,
) -> Result<(), MigrationError> {
@@ -883,7 +910,6 @@ async fn apply_consolidation(
})?;
Ok(())
}
async fn apply_request_trace_identity(
transaction: &mut Transaction<'_, sqlx::Postgres>,
) -> Result<(), MigrationError> {
@@ -920,7 +946,42 @@ async fn apply_request_trace_identity(
})?;
Ok(())
}
async fn apply_operation_lifecycle(
transaction: &mut Transaction<'_, sqlx::Postgres>,
) -> Result<(), MigrationError> {
sqlx::raw_sql(OPERATION_LIFECYCLE_SOURCE)
.execute(&mut **transaction)
.await
.map_err(|_| {
MigrationError::new(
"apply_failed",
"apply.operation_lifecycle",
Some(4),
"restore_known_good_backup",
)
})?;
let descriptor = &MigrationAuthority::sequence()[3];
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(4),
"restore_known_good_backup",
)
})?;
Ok(())
}
#[cfg(test)]
#[path = "authority_tests.rs"]
mod tests;
@@ -8,7 +8,7 @@ fn sequence_is_deterministic_and_append_only() {
MigrationAuthority::validate_sequence().unwrap();
assert_eq!(
first.iter().map(|item| item.version).collect::<Vec<_>>(),
vec![1, 2, 3]
vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
);
assert_eq!(first[0].checksum, "crank-community-baseline-v1");
assert_eq!(first[0].source_digest, BASELINE_SOURCE_SHA256);
@@ -0,0 +1,44 @@
use sqlx::{Transaction, query};
use super::authority::{MigrationDescriptor, MigrationError};
pub(super) const SOURCE: &str = include_str!("execution_outcome_v5.sql");
pub(super) const SOURCE_SHA256: &str =
"bd6703249cc407789586327eb7fbbd5776ba27923c5dc7eb85b05d332585bf42";
pub(super) async fn apply(
transaction: &mut Transaction<'_, sqlx::Postgres>,
descriptor: &MigrationDescriptor,
) -> Result<(), MigrationError> {
sqlx::raw_sql(SOURCE)
.execute(&mut **transaction)
.await
.map_err(|_| {
MigrationError::new(
"apply_failed",
"apply.execution_outcome",
Some(5),
"restore_known_good_backup",
)
})?;
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(5),
"restore_known_good_backup",
)
})?;
Ok(())
}
@@ -0,0 +1,41 @@
alter table invocation_logs
add column operation_version integer null,
add column execution_stage text null,
add column execution_error_code text null,
add column retryability text null,
add column outcome_certainty text null;
alter table invocation_logs
add constraint invocation_logs_operation_version_check
check (operation_version is null or operation_version > 0),
add constraint invocation_logs_execution_stage_check
check (execution_stage is null or execution_stage in (
'authorization', 'input_schema', 'input_mapping', 'request_preparation',
'admission', 'adapter', 'upstream', 'output_mapping', 'output_schema',
'mandatory_persistence', 'runtime'
)),
add constraint invocation_logs_execution_error_code_check
check (execution_error_code is null or execution_error_code in (
'authorization_denied', 'auth_profile_not_found', 'secret_not_found',
'secret_invalid', 'input_schema_invalid', 'input_mapping_invalid',
'prepared_request_invalid', 'execution_overloaded', 'safety_store_unavailable',
'protocol_unsupported', 'execution_mode_unsupported',
'adapter_configuration_invalid', 'outbound_target_rejected',
'upstream_auth_error', 'upstream_not_found', 'upstream_rate_limited',
'upstream_server_error', 'upstream_status_error', 'upstream_timeout',
'upstream_transport_error', 'upstream_request_too_large',
'upstream_response_too_large',
'output_mapping_invalid', 'output_schema_invalid', 'persistence_unavailable',
'runtime_internal', 'confirmation_required', 'confirmation_invalid',
'idempotency_in_progress', 'idempotency_conflict', 'idempotency_outcome_unknown'
)),
add constraint invocation_logs_retryability_check
check (retryability is null or retryability in (
'never', 'safe', 'after_delay', 'manual_reconcile', 'requires_confirmation'
)),
add constraint invocation_logs_outcome_certainty_check
check (outcome_certainty is null or outcome_certainty in ('certain', 'outcome_unknown'));
create index invocation_logs_workspace_operation_version_idx
on invocation_logs(workspace_id, operation_id, operation_version)
where operation_version is not null;
@@ -0,0 +1,44 @@
use sqlx::{Transaction, query};
use super::authority::{MigrationDescriptor, MigrationError};
pub(super) const SOURCE: &str = include_str!("master_key_identity_v7.sql");
pub(super) const SOURCE_SHA256: &str =
"06e5d4667d9a7346474e9dcb176b8b2b9680c4b89089bea5a18eb5a88d5ac60e";
pub(super) async fn apply(
transaction: &mut Transaction<'_, sqlx::Postgres>,
descriptor: &MigrationDescriptor,
) -> Result<(), MigrationError> {
sqlx::raw_sql(SOURCE)
.execute(&mut **transaction)
.await
.map_err(|_| {
MigrationError::new(
"apply_failed",
"apply.master_key_identity",
Some(7),
"restore_known_good_backup",
)
})?;
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(7),
"restore_known_good_backup",
)
})?;
Ok(())
}
@@ -0,0 +1,104 @@
create table master_key_identities (
epoch bigint primary key,
fingerprint text not null unique,
cipher_contract text not null,
status text not null,
backup_ref text,
created_at timestamptz not null default now(),
activated_at timestamptz,
retired_at timestamptz,
constraint master_key_identities_epoch_check check (epoch > 0),
constraint master_key_identities_fingerprint_check check (
fingerprint ~ '^[0-9a-f]{64}$'
),
constraint master_key_identities_cipher_contract_check check (
cipher_contract = 'secret-envelope-v2/aes-256-gcm-hkdf-sha256'
),
constraint master_key_identities_status_check check (
status in ('active', 'pending', 'retired', 'revoked')
),
constraint master_key_identities_backup_ref_check check (
backup_ref is null
or (
octet_length(backup_ref) <= 256
and backup_ref !~ '[[:cntrl:]]'
and backup_ref !~ '^[A-Za-z][A-Za-z0-9+.-]*://'
)
)
);
create unique index master_key_identities_active_idx
on master_key_identities (status)
where status = 'active';
create table master_key_rotations (
id text primary key,
source_epoch bigint not null,
target_epoch bigint not null,
target_fingerprint text not null,
state text not null,
backup_ref text,
checkpoint_secret_id text,
total_secret_versions bigint not null default 0,
processed_secret_versions bigint not null default 0,
verified_secret_versions bigint not null default 0,
failure_code text,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint master_key_rotations_id_check check (
octet_length(id) between 1 and 128
and id !~ '[[:cntrl:]]'
),
constraint master_key_rotations_source_epoch_check check (source_epoch > 0),
constraint master_key_rotations_target_epoch_check check (target_epoch > source_epoch),
constraint master_key_rotations_target_fingerprint_check check (
target_fingerprint ~ '^[0-9a-f]{64}$'
),
constraint master_key_rotations_state_check check (
state in ('preflighted', 'running', 'verifying', 'verified', 'promoted', 'aborted', 'failed')
),
constraint master_key_rotations_backup_ref_check check (
backup_ref is null
or (
octet_length(backup_ref) <= 256
and backup_ref !~ '[[:cntrl:]]'
and backup_ref !~ '^[A-Za-z][A-Za-z0-9+.-]*://'
)
),
constraint master_key_rotations_counts_check check (
total_secret_versions >= 0
and processed_secret_versions >= 0
and verified_secret_versions >= 0
and processed_secret_versions <= total_secret_versions
and verified_secret_versions <= total_secret_versions
),
constraint master_key_rotations_failure_code_check check (
failure_code is null
or (
octet_length(failure_code) between 1 and 128
and failure_code !~ '[[:cntrl:]]'
)
)
);
alter table secret_versions
add column master_key_epoch bigint not null default 1,
add column target_ciphertext text,
add column target_key_version text,
add column target_master_key_epoch bigint,
add constraint secret_versions_master_key_epoch_check check (master_key_epoch > 0),
add constraint secret_versions_target_epoch_check check (
target_master_key_epoch is null or target_master_key_epoch > master_key_epoch
),
add constraint secret_versions_target_all_or_none_check check (
(
target_ciphertext is null
and target_key_version is null
and target_master_key_epoch is null
)
or (
target_ciphertext is not null
and target_key_version is not null
and target_master_key_epoch is not null
)
);
@@ -0,0 +1,44 @@
use sqlx::{Postgres, Transaction, query};
use super::authority::{MigrationDescriptor, MigrationError};
pub(super) const SOURCE: &str = include_str!("onboarding_product_events_v11.sql");
pub(super) const SOURCE_SHA256: &str =
"a439bbcdc9cc909d717ed5a868c7a51bd1ad3c49c4e85fd20933743ee3f31166";
pub(super) async fn apply(
transaction: &mut Transaction<'_, Postgres>,
descriptor: &MigrationDescriptor,
) -> Result<(), MigrationError> {
sqlx::raw_sql(SOURCE)
.execute(&mut **transaction)
.await
.map_err(|_| {
MigrationError::new(
"apply_failed",
"apply.onboarding_product_events",
Some(11),
"restore_known_good_backup",
)
})?;
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(11),
"restore_known_good_backup",
)
})?;
Ok(())
}
@@ -0,0 +1,110 @@
alter table platform_api_keys
add constraint platform_api_keys_workspace_agent_id_unique
unique (workspace_id, agent_id, id);
alter table invocation_logs
add column platform_api_key_id text null;
alter table invocation_logs
add constraint invocation_logs_platform_key_scope_fk
foreign key (workspace_id, agent_id, platform_api_key_id)
references platform_api_keys(workspace_id, agent_id, id)
on delete set null (platform_api_key_id);
create index invocation_logs_workspace_agent_key_success_idx
on invocation_logs(workspace_id, agent_id, platform_api_key_id, created_at desc)
where platform_api_key_id is not null
and source = 'agent_tool_call'
and status = 'ok';
create table product_events (
id text primary key,
workspace_id text not null references workspaces(id) on delete cascade,
event_name text not null,
schema_version integer not null,
occurred_at timestamptz not null,
idempotency_key text not null,
properties_json jsonb not null default '{}'::jsonb,
constraint product_events_id_check check (id ~ '^pe_[A-Za-z0-9_-]{1,128}$'),
constraint product_events_name_check check (event_name in (
'onboarding_eligible', 'onboarding_started', 'onboarding_resumed', 'onboarding_dismissed',
'onboarding_abandoned', 'onboarding_completed'
)),
constraint product_events_schema_version_check check (schema_version = 1),
constraint product_events_idempotency_key_check check (
octet_length(idempotency_key) between 1 and 256
),
constraint product_events_properties_check check (
jsonb_typeof(properties_json) = 'object'
and pg_column_size(properties_json) <= 4096
and (
event_name <> 'onboarding_eligible'
or (
properties_json @> '{"eligible": true}'::jsonb
and jsonb_typeof(properties_json -> 'eligible_since') = 'string'
)
)
),
unique (workspace_id, idempotency_key)
);
create index product_events_workspace_occurred_idx
on product_events(workspace_id, occurred_at, id);
create table product_event_daily_rollups (
workspace_id text not null references workspaces(id) on delete cascade,
event_name text not null,
day date not null,
events_total bigint not null default 0,
eligible_total bigint not null default 0,
constraint product_event_daily_rollups_name_check check (event_name in (
'onboarding_eligible', 'onboarding_started', 'onboarding_resumed', 'onboarding_dismissed',
'onboarding_abandoned', 'onboarding_completed'
)),
constraint product_event_daily_rollups_counts_check check (
events_total >= 0 and eligible_total >= 0 and eligible_total <= events_total
),
primary key (workspace_id, event_name, day)
);
create table onboarding_selections (
workspace_id text primary key references workspaces(id) on delete cascade,
operation_id text null,
operation_version integer null,
agent_id text null,
catalog_revision bigint null,
platform_api_key_id text null,
invocation_log_id text null,
test_log_id text null,
evidence_after timestamptz not null,
selected_at timestamptz null,
constraint onboarding_selections_shape_check check (
(
operation_id is null and operation_version is null and agent_id is null
and catalog_revision is null and platform_api_key_id is null
and invocation_log_id is null and test_log_id is null and selected_at is null
) or (
operation_id is not null and operation_version is not null and operation_version > 0
and agent_id is not null and catalog_revision is not null and catalog_revision > 0
and platform_api_key_id is not null and invocation_log_id is not null
and selected_at is not null
)
)
);
create function crank_reject_product_event_mutation()
returns trigger
language plpgsql
as $$
begin
if tg_op = 'DELETE'
and not exists (select 1 from workspaces where id = old.workspace_id) then
return old;
end if;
raise exception 'ProductEvent is append-only' using errcode = '23514';
end;
$$;
create trigger product_events_append_only_guard
before update or delete on product_events
for each row execute function crank_reject_product_event_mutation();
@@ -0,0 +1,142 @@
alter table operation_versions
add column name text,
add column display_name text,
add column category text,
add column protocol text,
add column security_level text,
add column snapshot_provenance text,
add column snapshot_observed_at timestamptz,
add column published_at timestamptz,
add column published_by text;
update operation_versions ov
set name = o.name,
display_name = o.display_name,
category = o.category,
protocol = o.protocol,
security_level = o.security_level,
snapshot_provenance = 'legacy_observed',
snapshot_observed_at = statement_timestamp()
from operations o
where o.id = ov.operation_id;
update operation_versions ov
set published_at = po.published_at,
published_by = po.published_by
from published_operations po
where po.operation_id = ov.operation_id
and po.version = ov.version;
alter table operation_versions
alter column name set not null,
alter column display_name set not null,
alter column category set not null,
alter column protocol set not null,
alter column security_level set not null,
alter column snapshot_provenance set not null,
alter column snapshot_observed_at set not null,
add constraint operation_versions_snapshot_provenance_check
check (snapshot_provenance in ('legacy_observed', 'native_v4'));
create or replace function crank_guard_operation_version_immutable()
returns trigger
language plpgsql
as $guard$
begin
if tg_op = 'DELETE' then
if old.status = 'published' then
raise exception 'published operation version is immutable'
using errcode = '55000';
end if;
return old;
end if;
if old.status = 'draft'
and new.status = 'published'
and old.operation_id = new.operation_id
and old.version = new.version
and old.name = new.name
and old.display_name = new.display_name
and old.category = new.category
and old.protocol = new.protocol
and old.security_level = new.security_level
and old.target_json = new.target_json
and old.input_schema_json = new.input_schema_json
and old.output_schema_json = new.output_schema_json
and old.input_mapping_json = new.input_mapping_json
and old.output_mapping_json = new.output_mapping_json
and old.execution_config_json = new.execution_config_json
and old.tool_description_json = new.tool_description_json
and old.samples_json is not distinct from new.samples_json
and old.generated_draft_json is not distinct from new.generated_draft_json
and old.config_export_json is not distinct from new.config_export_json
and old.wizard_state_json is not distinct from new.wizard_state_json
and old.change_note is not distinct from new.change_note
and old.created_at = new.created_at
and old.created_by is not distinct from new.created_by
and old.snapshot_provenance = new.snapshot_provenance
and old.snapshot_observed_at = new.snapshot_observed_at
and old.published_at is null
and new.published_at is not null
then
return new;
end if;
raise exception 'operation version is append-only'
using errcode = '55000';
end;
$guard$;
create trigger operation_versions_immutable_guard
before update or delete on operation_versions
for each row execute function crank_guard_operation_version_immutable();
create or replace function crank_guard_published_operation_pointer()
returns trigger
language plpgsql
as $guard$
begin
if tg_op = 'DELETE' then
raise exception 'published operation pointer is immutable'
using errcode = '55000';
end if;
if new.operation_id = old.operation_id and new.version > old.version then
return new;
end if;
raise exception 'published operation pointer cannot rewind'
using errcode = '55000';
end;
$guard$;
create trigger published_operations_monotonic_guard
before update or delete on published_operations
for each row execute function crank_guard_published_operation_pointer();
create or replace function crank_guard_operation_latest_pointer()
returns trigger
language plpgsql
as $guard$
begin
if old.latest_published_version is not null
and (new.latest_published_version is null
or new.latest_published_version < old.latest_published_version) then
raise exception 'latest published operation version cannot rewind'
using errcode = '55000';
end if;
if new.latest_published_version is distinct from old.latest_published_version
and new.latest_published_version is not null
and not exists (
select 1 from published_operations po
where po.operation_id = new.id
and po.version = new.latest_published_version
) then
raise exception 'latest published operation version has no authoritative pointer'
using errcode = '55000';
end if;
return new;
end;
$guard$;
create trigger operations_latest_pointer_monotonic_guard
before update on operations
for each row execute function crank_guard_operation_latest_pointer();
@@ -0,0 +1,40 @@
pub(super) const BASELINE: &[&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",
];
pub(super) const ONBOARDING_PRODUCT_EVENTS: &[&str] = &[
"product_events",
"product_event_daily_rollups",
"onboarding_selections",
];
pub(super) const CONSOLIDATION: &[&str] = &[
"__crank_migrations",
"__crank_migration_legacy_audit",
"__crank_mcp_migrations",
"mcp_transport_sessions",
"__crank_ext_migrations",
];
@@ -0,0 +1,44 @@
use sqlx::{Transaction, query};
use super::authority::{MigrationDescriptor, MigrationError};
pub(super) const SOURCE: &str = include_str!("platform_key_name_reuse_v6.sql");
pub(super) const SOURCE_SHA256: &str =
"94dba9b9dd3364607bc37e7698478ba6644607a7f268a1621cd3acea6bc96c2d";
pub(super) async fn apply(
transaction: &mut Transaction<'_, sqlx::Postgres>,
descriptor: &MigrationDescriptor,
) -> Result<(), MigrationError> {
sqlx::raw_sql(SOURCE)
.execute(&mut **transaction)
.await
.map_err(|_| {
MigrationError::new(
"apply_failed",
"apply.platform_key_name_reuse",
Some(6),
"restore_known_good_backup",
)
})?;
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(6),
"restore_known_good_backup",
)
})?;
Ok(())
}
@@ -0,0 +1,5 @@
drop index if exists platform_api_keys_workspace_name_idx;
create unique index if not exists platform_api_keys_workspace_name_active_idx
on platform_api_keys(workspace_id, name)
where status <> 'deleted';
@@ -26,6 +26,8 @@ pub(super) const OWNED_RELATIONS: &[&str] = &[
"agent_operation_bindings",
"secrets",
"secret_versions",
"master_key_identities",
"master_key_rotations",
"auth_profiles",
"workspace_upstreams",
"yaml_import_jobs",
@@ -77,6 +79,37 @@ const REQUIRED_COLUMNS: &[(&str, &[&str])] = &[
"expires_at",
],
),
(
"master_key_identities",
&[
"epoch",
"fingerprint",
"cipher_contract",
"status",
"backup_ref",
"created_at",
"activated_at",
"retired_at",
],
),
(
"master_key_rotations",
&[
"id",
"source_epoch",
"target_epoch",
"target_fingerprint",
"state",
"backup_ref",
"checkpoint_secret_id",
"total_secret_versions",
"processed_secret_versions",
"verified_secret_versions",
"failure_code",
"created_at",
"updated_at",
],
),
];
const REQUIRED_COLUMN_TYPES: &[(&str, &str, &str, bool)] = &[
@@ -165,6 +198,67 @@ const REQUIRED_COLUMN_TYPES: &[(&str, &str, &str, bool)] = &[
"timestamp with time zone",
true,
),
("master_key_identities", "epoch", "bigint", false),
("master_key_identities", "fingerprint", "text", false),
("master_key_identities", "cipher_contract", "text", false),
("master_key_identities", "status", "text", false),
("master_key_identities", "backup_ref", "text", true),
(
"master_key_identities",
"created_at",
"timestamp with time zone",
false,
),
(
"master_key_identities",
"activated_at",
"timestamp with time zone",
true,
),
(
"master_key_identities",
"retired_at",
"timestamp with time zone",
true,
),
("master_key_rotations", "id", "text", false),
("master_key_rotations", "source_epoch", "bigint", false),
("master_key_rotations", "target_epoch", "bigint", false),
("master_key_rotations", "target_fingerprint", "text", false),
("master_key_rotations", "state", "text", false),
("master_key_rotations", "backup_ref", "text", true),
("master_key_rotations", "checkpoint_secret_id", "text", true),
(
"master_key_rotations",
"total_secret_versions",
"bigint",
false,
),
(
"master_key_rotations",
"processed_secret_versions",
"bigint",
false,
),
(
"master_key_rotations",
"verified_secret_versions",
"bigint",
false,
),
("master_key_rotations", "failure_code", "text", true),
(
"master_key_rotations",
"created_at",
"timestamp with time zone",
false,
),
(
"master_key_rotations",
"updated_at",
"timestamp with time zone",
false,
),
];
pub(super) async fn relation_exists(
@@ -363,9 +457,397 @@ pub(super) async fn validate_schema_fingerprint(
)
.await?;
}
let lifecycle_columns = [
"name",
"display_name",
"category",
"protocol",
"security_level",
"snapshot_provenance",
"snapshot_observed_at",
"published_at",
"published_by",
];
for column in lifecycle_columns {
let present = query(
"select exists (
select 1 from information_schema.columns
where table_schema = current_schema()
and table_name = 'operation_versions'
and column_name = $1
) as present",
)
.bind(column)
.fetch_one(&mut *connection)
.await
.map_err(|_| MigrationError::storage("preflight.schema"))?
.try_get::<bool, _>("present")
.map_err(|_| MigrationError::storage("preflight.schema"))?;
if present != (current_version >= 4) {
return Err(schema_error(current_version));
}
}
if current_version >= 4 {
for (table, trigger) in [
("operation_versions", "operation_versions_immutable_guard"),
(
"published_operations",
"published_operations_monotonic_guard",
),
("operations", "operations_latest_pointer_monotonic_guard"),
] {
let trigger_present = query(
"select exists (
select 1 from pg_catalog.pg_trigger tg
join pg_catalog.pg_class t on t.oid = tg.tgrelid
join pg_catalog.pg_namespace n on n.oid = t.relnamespace
where n.nspname = current_schema()
and t.relname = $1
and tg.tgname = $2
and not tg.tgisinternal
) as present",
)
.bind(table)
.bind(trigger)
.fetch_one(&mut *connection)
.await
.map_err(|_| MigrationError::storage("preflight.schema"))?
.try_get::<bool, _>("present")
.map_err(|_| MigrationError::storage("preflight.schema"))?;
if !trigger_present {
return Err(schema_error(current_version));
}
}
}
let outcome_columns = [
"operation_version",
"execution_stage",
"execution_error_code",
"retryability",
"outcome_certainty",
];
for column in outcome_columns {
let row = query(
"select data_type, is_nullable from information_schema.columns
where table_schema = current_schema()
and table_name = 'invocation_logs'
and column_name = $1",
)
.bind(column)
.fetch_optional(&mut *connection)
.await
.map_err(|_| MigrationError::storage("preflight.schema"))?;
if current_version < 5 {
if row.is_some() {
return Err(schema_error(current_version));
}
} else {
let expected_type = if column == "operation_version" {
"integer"
} else {
"text"
};
let valid = row.is_some_and(|row| {
row.try_get::<String, _>("data_type").ok().as_deref() == Some(expected_type)
&& row.try_get::<String, _>("is_nullable").ok().as_deref() == Some("YES")
});
if !valid {
return Err(schema_error(current_version));
}
}
}
if current_version >= 5 {
validate_v5_execution_contract(connection).await?;
}
if current_version >= 6 {
validate_v6_platform_key_name_reuse(connection).await?;
}
if current_version < 7 {
let v7_relations_present = relation_exists(connection, "master_key_identities").await?
|| relation_exists(connection, "master_key_rotations").await?;
let mut v7_secret_columns_present = false;
for column in [
"master_key_epoch",
"target_ciphertext",
"target_key_version",
"target_master_key_epoch",
] {
v7_secret_columns_present |=
column_exists(connection, "secret_versions", column).await?;
}
if v7_relations_present || v7_secret_columns_present {
return Err(schema_error(current_version));
}
} else {
super::schema_guard_v7::validate_v7_master_key_identity(connection).await?;
}
if current_version < 8 {
let v8_relations_present = relation_exists(connection, "admin_bootstrap_contracts").await?
|| relation_exists(connection, "admin_login_backoff").await?
|| relation_exists(connection, "admin_security_audit_events").await?;
let v8_session_columns_present = column_exists(connection, "user_sessions", "csrf_hash")
.await?
|| column_exists(connection, "user_sessions", "revoked_at").await?;
if v8_relations_present || v8_session_columns_present {
return Err(schema_error(current_version));
}
} else {
super::schema_guard_v8::validate_v8_admin_auth_lifecycle(connection).await?;
}
if current_version < 9 {
if !super::schema_guard_v9::validate_v9_absent(connection).await? {
return Err(schema_error(current_version));
}
} else {
super::schema_guard_v9::validate_v9_agent_catalog_lifecycle(connection).await?;
}
if current_version < 10 {
if !super::schema_guard_v10::validate_v10_absent(connection).await? {
return Err(schema_error(current_version));
}
} else {
super::schema_guard_v10::validate_v10_approval_side_effects(connection).await?;
}
if current_version < 11 {
if !super::schema_guard_v11::validate_v11_absent(connection).await? {
return Err(schema_error(current_version));
}
} else {
super::schema_guard_v11::validate_v11_onboarding_product_events(connection).await?;
}
Ok(())
}
pub(super) async fn column_exists(
connection: &mut PgConnection,
table: &str,
column: &str,
) -> Result<bool, MigrationError> {
query(
"select exists (
select 1 from information_schema.columns
where table_schema = current_schema()
and table_name = $1
and column_name = $2
) as present",
)
.bind(table)
.bind(column)
.fetch_one(connection)
.await
.map_err(|_| MigrationError::storage("preflight.schema"))?
.try_get::<bool, _>("present")
.map_err(|_| MigrationError::storage("preflight.schema"))
}
async fn validate_v5_execution_contract(
connection: &mut PgConnection,
) -> Result<(), MigrationError> {
let operation_version = constraint_expression(
connection,
"invocation_logs",
"invocation_logs_operation_version_check",
)
.await?;
if normalize_definition(&operation_version) != "operation_versionisnulloroperation_version>0" {
return Err(schema_error(5));
}
for (constraint, column, allowed) in [
(
"invocation_logs_execution_stage_check",
"execution_stage",
&[
"authorization",
"input_schema",
"input_mapping",
"request_preparation",
"admission",
"adapter",
"upstream",
"output_mapping",
"output_schema",
"mandatory_persistence",
"runtime",
][..],
),
(
"invocation_logs_execution_error_code_check",
"execution_error_code",
&[
"authorization_denied",
"auth_profile_not_found",
"secret_not_found",
"secret_invalid",
"input_schema_invalid",
"input_mapping_invalid",
"prepared_request_invalid",
"execution_overloaded",
"safety_store_unavailable",
"protocol_unsupported",
"execution_mode_unsupported",
"adapter_configuration_invalid",
"outbound_target_rejected",
"upstream_auth_error",
"upstream_not_found",
"upstream_rate_limited",
"upstream_server_error",
"upstream_status_error",
"upstream_timeout",
"upstream_transport_error",
"upstream_request_too_large",
"upstream_response_too_large",
"output_mapping_invalid",
"output_schema_invalid",
"persistence_unavailable",
"runtime_internal",
"confirmation_required",
"confirmation_invalid",
"idempotency_in_progress",
"idempotency_conflict",
"idempotency_outcome_unknown",
][..],
),
(
"invocation_logs_retryability_check",
"retryability",
&[
"never",
"safe",
"after_delay",
"manual_reconcile",
"requires_confirmation",
][..],
),
(
"invocation_logs_outcome_certainty_check",
"outcome_certainty",
&["certain", "outcome_unknown"][..],
),
] {
let expression = constraint_expression(connection, "invocation_logs", constraint).await?;
if !enum_constraint_matches(&expression, column, allowed) {
return Err(schema_error(5));
}
}
validate_v5_index(connection).await
}
pub(super) async fn constraint_expression(
connection: &mut PgConnection,
table: &str,
constraint: &str,
) -> Result<String, MigrationError> {
query(
"select pg_get_expr(c.conbin, c.conrelid) as expression
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 and c.contype = 'c'",
)
.bind(table)
.bind(constraint)
.fetch_optional(connection)
.await
.map_err(|_| MigrationError::storage("preflight.schema"))?
.and_then(|row| row.try_get::<String, _>("expression").ok())
.ok_or_else(|| schema_error(5))
}
pub(super) fn enum_constraint_matches(expression: &str, column: &str, allowed: &[&str]) -> bool {
let normalized = normalize_definition(expression);
if !normalized.contains(column) || normalized.contains("ortrue") {
return false;
}
let mut values = expression
.split('\'')
.enumerate()
.filter_map(|(index, value)| (index % 2 == 1).then_some(value))
.collect::<Vec<_>>();
values.sort_unstable();
values.dedup();
let mut expected = allowed.to_vec();
expected.sort_unstable();
values == expected
}
async fn validate_v5_index(connection: &mut PgConnection) -> 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_indexdef(i.indexrelid, 3, true) as third_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 = 'invocation_logs_workspace_operation_version_idx'",
)
.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("operation_id")
&& row.try_get::<String, _>("third_column").ok().as_deref() == Some("operation_version")
&& row
.try_get::<String, _>("predicate")
.ok()
.is_some_and(|value| normalize_definition(&value) == "operation_versionisnotnull")
});
if valid { Ok(()) } else { Err(schema_error(5)) }
}
async fn validate_v6_platform_key_name_reuse(
connection: &mut PgConnection,
) -> Result<(), MigrationError> {
if relation_exists(connection, "platform_api_keys_workspace_name_idx").await? {
return Err(schema_error(6));
}
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 = 'platform_api_keys_workspace_name_active_idx'",
)
.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("platform_api_keys")
&& 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(true)
&& row.try_get::<String, _>("first_column").ok().as_deref() == Some("workspace_id")
&& row.try_get::<String, _>("second_column").ok().as_deref() == Some("name")
&& row
.try_get::<String, _>("predicate")
.ok()
.is_some_and(|value| {
matches!(
normalize_definition(&value).as_str(),
"status<>'deleted'::text" | "status!='deleted'::text"
)
})
});
if valid { Ok(()) } else { Err(schema_error(6)) }
}
async fn named_constraint_exists(
connection: &mut PgConnection,
table: &str,
@@ -433,7 +915,7 @@ async fn validate_index(
if valid { Ok(()) } else { Err(schema_error(3)) }
}
fn normalize_definition(value: &str) -> String {
pub(super) fn normalize_definition(value: &str) -> String {
value
.chars()
.filter(|character| !character.is_ascii_whitespace() && !matches!(character, '(' | ')'))
@@ -441,7 +923,7 @@ fn normalize_definition(value: &str) -> String {
.collect()
}
fn schema_error(version: i64) -> MigrationError {
pub(super) fn schema_error(version: i64) -> MigrationError {
MigrationError::new(
"partial_sequence",
"preflight.schema",
@@ -0,0 +1,83 @@
use sqlx::{PgConnection, Row, query};
use super::authority::MigrationError;
use super::schema_guard::{normalize_definition, relation_exists, schema_error};
pub(super) async fn validate_v10_absent(
connection: &mut PgConnection,
) -> Result<bool, MigrationError> {
let old_present =
relation_exists(connection, "approval_requests_pending_fingerprint_idx").await?;
let new_present = relation_exists(
connection,
"approval_requests_pending_scope_fingerprint_idx",
)
.await?;
Ok(old_present && !new_present)
}
pub(super) async fn validate_v10_approval_side_effects(
connection: &mut PgConnection,
) -> Result<(), MigrationError> {
if !relation_exists(connection, "approval_requests_pending_fingerprint_idx").await? {
return Err(schema_error(10));
}
for column in ["request_id", "trace_id"] {
let present = query(
"select 1
from information_schema.columns
where table_schema = current_schema()
and table_name = 'approval_requests'
and column_name = $1",
)
.bind(column)
.fetch_optional(&mut *connection)
.await
.map_err(|_| MigrationError::storage("preflight.schema"))?
.is_some();
if !present {
return Err(schema_error(10));
}
}
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_indexdef(i.indexrelid, 3, true) as third_column,
pg_get_indexdef(i.indexrelid, 4, true) as fourth_column,
pg_get_indexdef(i.indexrelid, 5, true) as fifth_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 = 'approval_requests_pending_scope_fingerprint_idx'",
)
.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("approval_requests")
&& 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(true)
&& row.try_get::<String, _>("first_column").ok().as_deref() == Some("workspace_id")
&& row.try_get::<String, _>("second_column").ok().as_deref() == Some("agent_id")
&& row.try_get::<String, _>("third_column").ok().as_deref() == Some("operation_id")
&& row.try_get::<String, _>("fourth_column").ok().as_deref()
== Some("operation_version")
&& row.try_get::<String, _>("fifth_column").ok().as_deref()
== Some("request_fingerprint")
&& row
.try_get::<String, _>("predicate")
.ok()
.is_some_and(|value| {
normalize_definition(&value)
== "status='pending'::textandrequest_fingerprintisnotnull"
})
});
if valid { Ok(()) } else { Err(schema_error(10)) }
}
@@ -0,0 +1,331 @@
use sqlx::{PgConnection, Row, query};
use super::authority::MigrationError;
use super::schema_guard::{
column_exists, constraint_expression, normalize_definition, relation_exists, schema_error,
};
pub(super) async fn validate_v11_absent(
connection: &mut PgConnection,
) -> Result<bool, MigrationError> {
Ok(!relation_exists(connection, "product_events").await?
&& !relation_exists(connection, "product_event_daily_rollups").await?
&& !relation_exists(connection, "onboarding_selections").await?
&& !column_exists(connection, "invocation_logs", "platform_api_key_id").await?)
}
pub(super) async fn validate_v11_onboarding_product_events(
connection: &mut PgConnection,
) -> Result<(), MigrationError> {
for relation in [
"product_events",
"product_event_daily_rollups",
"onboarding_selections",
] {
if !relation_exists(connection, relation).await? {
return Err(schema_error(11));
}
}
if !column_exists(connection, "invocation_logs", "platform_api_key_id").await? {
return Err(schema_error(11));
}
for relation in [
"product_events_workspace_id_idempotency_key_key",
"product_events_workspace_occurred_idx",
"invocation_logs_workspace_agent_key_success_idx",
] {
if !relation_exists(connection, relation).await? {
return Err(schema_error(11));
}
}
for (table, constraint, required) in [
(
"product_events",
"product_events_id_check",
&["id~", "^pe_[a-za-z0-9_-]{1,128}$"][..],
),
(
"product_events",
"product_events_name_check",
&[
"event_name=any",
"onboarding_eligible",
"onboarding_started",
"onboarding_resumed",
"onboarding_dismissed",
"onboarding_abandoned",
"onboarding_completed",
][..],
),
(
"product_events",
"product_events_schema_version_check",
&["schema_version=1"][..],
),
(
"product_events",
"product_events_idempotency_key_check",
&[
"octet_lengthidempotency_key>=1",
"octet_lengthidempotency_key<=256",
][..],
),
(
"product_events",
"product_events_properties_check",
&[
"jsonb_typeofproperties_json='object'",
"pg_column_sizeproperties_json<=4096",
"event_name<>'onboarding_eligible'",
"properties_json@>'{\"eligible\":true}'",
"jsonb_typeofproperties_json->'eligible_since'",
"='string'",
][..],
),
(
"product_event_daily_rollups",
"product_event_daily_rollups_name_check",
&[
"event_name=any",
"onboarding_eligible",
"onboarding_completed",
][..],
),
(
"product_event_daily_rollups",
"product_event_daily_rollups_counts_check",
&[
"events_total>=0",
"eligible_total>=0",
"eligible_total<=events_total",
][..],
),
] {
let definition =
normalize_definition(&constraint_expression(connection, table, constraint).await?);
if required.iter().any(|snippet| !definition.contains(snippet)) {
return Err(schema_error(11));
}
}
validate_index(
connection,
"product_events_workspace_occurred_idx",
"product_events",
false,
&["workspace_id", "occurred_at", "id"],
&[],
)
.await?;
validate_index(
connection,
"invocation_logs_workspace_agent_key_success_idx",
"invocation_logs",
false,
&[
"workspace_id",
"agent_id",
"platform_api_key_id",
"created_atdesc",
],
&[
"platform_api_key_idisnotnull",
"source='agent_tool_call'",
"status='ok'",
],
)
.await?;
let key_scope_fk = constraint_definition(
connection,
"invocation_logs",
"invocation_logs_platform_key_scope_fk",
)
.await?;
for required in [
"foreignkeyworkspace_id,agent_id,platform_api_key_id",
"referencesplatform_api_keysworkspace_id,agent_id,id",
"ondeletesetnullplatform_api_key_id",
] {
if !key_scope_fk.contains(required) {
return Err(schema_error(11));
}
}
for (kind, name) in [
("constraint", "invocation_logs_platform_key_scope_fk"),
("trigger", "product_events_append_only_guard"),
] {
let present: bool = match kind {
"constraint" => query(
"select exists (select 1 from pg_constraint c
join pg_namespace n on n.oid = c.connamespace
where n.nspname = current_schema() and c.conname = $1) as present",
),
_ => query(
"select exists (select 1 from pg_trigger t
join pg_class c on c.oid = t.tgrelid
join pg_namespace n on n.oid = c.relnamespace
where n.nspname = current_schema() and t.tgname = $1 and not t.tgisinternal) as present",
),
}
.bind(name)
.fetch_one(&mut *connection)
.await
.map_err(|_| MigrationError::storage("preflight.schema"))?
.try_get("present")
.map_err(|_| MigrationError::storage("preflight.schema"))?;
if !present {
return Err(schema_error(11));
}
}
let nullable = query(
"select is_nullable from information_schema.columns
where table_schema = current_schema()
and table_name = 'invocation_logs'
and column_name = 'platform_api_key_id'",
)
.fetch_optional(&mut *connection)
.await
.map_err(|_| MigrationError::storage("preflight.schema"))?
.and_then(|row| row.try_get::<String, _>("is_nullable").ok());
if nullable.as_deref() != Some("YES") {
return Err(schema_error(11));
}
let shape = constraint_expression(
connection,
"onboarding_selections",
"onboarding_selections_shape_check",
)
.await?;
let normalized_shape = normalize_definition(&shape);
for required in [
"operation_idisnull",
"operation_version>0",
"catalog_revision>0",
"invocation_log_idisnotnull",
"selected_atisnull",
"selected_atisnotnull",
] {
if !normalized_shape.contains(required) {
return Err(schema_error(11));
}
}
let function_definition = query(
"select p.prosrc as definition
from pg_proc p
join pg_namespace n on n.oid = p.pronamespace
where n.nspname = current_schema()
and p.proname = 'crank_reject_product_event_mutation'",
)
.fetch_optional(connection)
.await
.map_err(|_| MigrationError::storage("preflight.schema"))?
.and_then(|row| row.try_get::<String, _>("definition").ok());
if !function_definition.is_some_and(|definition| {
let normalized = normalize_definition(&definition);
[
"notexists",
"fromworkspaces",
"old.workspace_id",
"returnold",
"producteventisappend-only",
]
.into_iter()
.all(|required| normalized.contains(required))
}) {
return Err(schema_error(11));
}
Ok(())
}
async fn constraint_definition(
connection: &mut PgConnection,
table: &str,
constraint: &str,
) -> Result<String, MigrationError> {
query(
"select pg_get_constraintdef(c.oid, true) as definition
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",
)
.bind(table)
.bind(constraint)
.fetch_optional(&mut *connection)
.await
.map_err(|_| MigrationError::storage("preflight.schema"))?
.ok_or_else(|| schema_error(11))?
.try_get::<String, _>("definition")
.map(|definition| normalize_definition(&definition))
.map_err(|_| MigrationError::storage("preflight.schema"))
}
async fn validate_index(
connection: &mut PgConnection,
index: &str,
expected_table: &str,
expected_unique: bool,
expected_columns: &[&str],
predicate_snippets: &[&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_indexdef(i.indexrelid, 3, true) as third_column,
pg_get_indexdef(i.indexrelid, 4, true) as fourth_column,
pg_get_indexdef(i.indexrelid) as definition,
coalesce(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(&mut *connection)
.await
.map_err(|_| MigrationError::storage("preflight.schema"))?;
let valid = row.is_some_and(|row| {
let actual_columns = [
"first_column",
"second_column",
"third_column",
"fourth_column",
]
.into_iter()
.filter_map(|field| row.try_get::<Option<String>, _>(field).ok().flatten())
.map(|value| normalize_definition(&value))
.filter(|value| !value.is_empty())
.collect::<Vec<_>>();
let expected_base_columns = expected_columns
.iter()
.map(|column| column.strip_suffix("desc").unwrap_or(column).to_owned())
.collect::<Vec<_>>();
let definition = row
.try_get::<String, _>("definition")
.ok()
.map(|value| normalize_definition(&value))
.unwrap_or_default();
let predicate = row
.try_get::<String, _>("predicate")
.ok()
.map(|value| normalize_definition(&value))
.unwrap_or_default();
row.try_get::<String, _>("table_name").ok().as_deref() == Some(expected_table)
&& 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(expected_unique)
&& actual_columns == expected_base_columns
&& expected_columns
.iter()
.filter(|column| column.ends_with("desc"))
.all(|column| definition.contains(column))
&& predicate_snippets
.iter()
.all(|snippet| predicate.contains(snippet))
});
if valid { Ok(()) } else { Err(schema_error(11)) }
}
@@ -0,0 +1,231 @@
use sqlx::{PgConnection, Row, query};
use super::{
authority::MigrationError,
schema_guard::{
column_exists, constraint_expression, enum_constraint_matches, normalize_definition,
relation_exists, schema_error,
},
};
pub(super) async fn validate_v7_master_key_identity(
connection: &mut PgConnection,
) -> Result<(), MigrationError> {
for relation in ["master_key_identities", "master_key_rotations"] {
if !relation_exists(connection, relation).await? {
return Err(schema_error(7));
}
}
for (column, data_type, nullable) in [
("master_key_epoch", "bigint", false),
("target_ciphertext", "text", true),
("target_key_version", "text", true),
("target_master_key_epoch", "bigint", true),
] {
if !secret_version_column_matches(connection, column, data_type, nullable).await? {
return Err(schema_error(7));
}
}
validate_identity_constraints(connection).await?;
validate_rotation_constraints(connection).await?;
validate_secret_version_constraints(connection).await?;
validate_active_identity_index(connection).await
}
async fn secret_version_column_matches(
connection: &mut PgConnection,
column: &str,
data_type: &str,
nullable: bool,
) -> Result<bool, MigrationError> {
if !column_exists(connection, "secret_versions", column).await? {
return Ok(false);
}
let row = query(
"select data_type, is_nullable from information_schema.columns
where table_schema = current_schema()
and table_name = 'secret_versions'
and column_name = $1",
)
.bind(column)
.fetch_optional(connection)
.await
.map_err(|_| MigrationError::storage("preflight.schema"))?;
Ok(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" })
}))
}
async fn validate_identity_constraints(
connection: &mut PgConnection,
) -> Result<(), MigrationError> {
let identity_epoch = constraint_expression(
connection,
"master_key_identities",
"master_key_identities_epoch_check",
)
.await?;
if normalize_definition(&identity_epoch) != "epoch>0" {
return Err(schema_error(7));
}
let identity_fingerprint = constraint_expression(
connection,
"master_key_identities",
"master_key_identities_fingerprint_check",
)
.await?;
if !regex_constraint_matches(&identity_fingerprint, "fingerprint", "^[0-9a-f]{64}$") {
return Err(schema_error(7));
}
let identity_cipher = constraint_expression(
connection,
"master_key_identities",
"master_key_identities_cipher_contract_check",
)
.await?;
if !enum_constraint_matches(
&identity_cipher,
"cipher_contract",
&["secret-envelope-v2/aes-256-gcm-hkdf-sha256"],
) {
return Err(schema_error(7));
}
let identity_status = constraint_expression(
connection,
"master_key_identities",
"master_key_identities_status_check",
)
.await?;
if enum_constraint_matches(
&identity_status,
"status",
&["active", "pending", "retired", "revoked"],
) {
Ok(())
} else {
Err(schema_error(7))
}
}
async fn validate_rotation_constraints(
connection: &mut PgConnection,
) -> Result<(), MigrationError> {
let rotation_state = constraint_expression(
connection,
"master_key_rotations",
"master_key_rotations_state_check",
)
.await?;
if enum_constraint_matches(
&rotation_state,
"state",
&[
"preflighted",
"running",
"verifying",
"verified",
"promoted",
"aborted",
"failed",
],
) {
Ok(())
} else {
Err(schema_error(7))
}
}
async fn validate_secret_version_constraints(
connection: &mut PgConnection,
) -> Result<(), MigrationError> {
let secret_epoch = constraint_expression(
connection,
"secret_versions",
"secret_versions_master_key_epoch_check",
)
.await?;
if normalize_definition(&secret_epoch) != "master_key_epoch>0" {
return Err(schema_error(7));
}
let secret_target_epoch = constraint_expression(
connection,
"secret_versions",
"secret_versions_target_epoch_check",
)
.await?;
if normalize_definition(&secret_target_epoch)
!= "target_master_key_epochisnullortarget_master_key_epoch>master_key_epoch"
{
return Err(schema_error(7));
}
let secret_all_or_none = constraint_expression(
connection,
"secret_versions",
"secret_versions_target_all_or_none_check",
)
.await?;
if target_ciphertext_all_or_none_matches(&secret_all_or_none) {
Ok(())
} else {
Err(schema_error(7))
}
}
async fn validate_active_identity_index(
connection: &mut PgConnection,
) -> Result<(), MigrationError> {
let active_index = 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_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 = 'master_key_identities_active_idx'",
)
.fetch_optional(connection)
.await
.map_err(|_| MigrationError::storage("preflight.schema"))?;
let valid = active_index.is_some_and(|row| {
row.try_get::<String, _>("table_name").ok().as_deref() == Some("master_key_identities")
&& 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(true)
&& row.try_get::<String, _>("first_column").ok().as_deref() == Some("status")
&& row
.try_get::<String, _>("predicate")
.ok()
.is_some_and(|value| normalize_definition(&value) == "status='active'::text")
});
if valid { Ok(()) } else { Err(schema_error(7)) }
}
fn regex_constraint_matches(expression: &str, column: &str, pattern: &str) -> bool {
let normalized = normalize_definition(expression);
if !normalized.contains(column) || normalized.contains("ortrue") {
return false;
}
let values = expression
.split('\'')
.enumerate()
.filter_map(|(index, value)| (index % 2 == 1).then_some(value))
.collect::<Vec<_>>();
values == [pattern]
}
fn target_ciphertext_all_or_none_matches(expression: &str) -> bool {
let normalized = normalize_definition(expression);
!normalized.contains("ortrue")
&& normalized.contains("target_ciphertextisnull")
&& normalized.contains("target_key_versionisnull")
&& normalized.contains("target_master_key_epochisnull")
&& normalized.contains("target_ciphertextisnotnull")
&& normalized.contains("target_key_versionisnotnull")
&& normalized.contains("target_master_key_epochisnotnull")
}
@@ -0,0 +1,27 @@
use sqlx::PgConnection;
use super::authority::MigrationError;
use super::schema_guard::{column_exists, relation_exists, schema_error};
pub(super) async fn validate_v8_admin_auth_lifecycle(
connection: &mut PgConnection,
) -> Result<(), MigrationError> {
for relation in [
"admin_bootstrap_contracts",
"admin_bootstrap_contracts_single_active_idx",
"admin_bootstrap_contracts_token_hash_idx",
"admin_login_backoff",
"admin_security_audit_events",
"admin_security_audit_events_created_idx",
] {
if !relation_exists(connection, relation).await? {
return Err(schema_error(8));
}
}
for column in ["csrf_hash", "revoked_at"] {
if !column_exists(connection, "user_sessions", column).await? {
return Err(schema_error(8));
}
}
Ok(())
}
@@ -0,0 +1,201 @@
use sqlx::{PgConnection, Row, query};
use super::authority::MigrationError;
use super::schema_guard::{
column_exists, constraint_expression, normalize_definition, schema_error,
};
pub(super) async fn validate_v9_absent(
connection: &mut PgConnection,
) -> Result<bool, MigrationError> {
let columns_present = column_exists(connection, "agents", "catalog_revision").await?
|| column_exists(connection, "published_agents", "catalog_revision").await?;
let triggers_present = trigger_exists(
connection,
"agent_versions",
"agent_versions_immutable_guard",
)
.await?
|| trigger_exists(
connection,
"agent_operation_bindings",
"agent_operation_bindings_immutable_guard",
)
.await?
|| trigger_exists(
connection,
"published_agents",
"published_agents_monotonic_guard",
)
.await?;
Ok(!columns_present && !triggers_present)
}
pub(super) async fn validate_v9_agent_catalog_lifecycle(
connection: &mut PgConnection,
) -> Result<(), MigrationError> {
for (table, column) in [
("agents", "catalog_revision"),
("published_agents", "catalog_revision"),
] {
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("bigint")
&& row.try_get::<String, _>("is_nullable").ok().as_deref() == Some("NO")
});
if !valid {
return Err(schema_error(9));
}
}
let agents_revision =
constraint_expression(connection, "agents", "agents_catalog_revision_check").await?;
if normalize_definition(&agents_revision) != "catalog_revision>=0" {
return Err(schema_error(9));
}
let published_revision = constraint_expression(
connection,
"published_agents",
"published_agents_catalog_revision_check",
)
.await?;
if normalize_definition(&published_revision) != "catalog_revision>0" {
return Err(schema_error(9));
}
for (table, trigger, expected_events, expected_function, function_snippets) in [
(
"agent_versions",
"agent_versions_immutable_guard",
&["before", "update", "delete"][..],
"crank_reject_published_agent_version_mutation",
&["old.status='published'", "returnold", "returnnew"][..],
),
(
"agent_operation_bindings",
"agent_operation_bindings_immutable_guard",
&["before", "insert", "update", "delete"][..],
"crank_reject_published_agent_binding_mutation",
&[
"coalescenew.agent_id,old.agent_id",
"bound_status='published'",
"returnold",
"returnnew",
][..],
),
(
"published_agents",
"published_agents_monotonic_guard",
&["before", "insert", "update", "delete"][..],
"crank_reject_published_agent_pointer_rewind",
&[
"tg_op='delete'",
"new.catalog_revision<=old.catalog_revision",
"new.version<old.version",
][..],
),
] {
let Some(trigger_definition) = trigger_definition(connection, table, trigger).await? else {
return Err(schema_error(9));
};
let normalized_trigger = normalize_definition(&trigger_definition);
if expected_events
.iter()
.any(|event| !normalized_trigger.contains(event))
|| !normalized_trigger.contains(expected_function)
{
return Err(schema_error(9));
}
let function_definition = function_definition(connection, expected_function).await?;
let normalized_function = normalize_definition(&function_definition);
if function_snippets
.iter()
.any(|snippet| !normalized_function.contains(snippet))
{
return Err(schema_error(9));
}
}
Ok(())
}
async fn trigger_exists(
connection: &mut PgConnection,
table: &str,
trigger: &str,
) -> Result<bool, MigrationError> {
query(
"select exists (
select 1
from pg_catalog.pg_trigger trg
join pg_catalog.pg_class t on t.oid = trg.tgrelid
join pg_catalog.pg_namespace n on n.oid = t.relnamespace
where n.nspname = current_schema()
and t.relname = $1
and trg.tgname = $2
and not trg.tgisinternal
) as present",
)
.bind(table)
.bind(trigger)
.fetch_one(connection)
.await
.map_err(|_| MigrationError::storage("preflight.schema"))?
.try_get::<bool, _>("present")
.map_err(|_| MigrationError::storage("preflight.schema"))
}
async fn trigger_definition(
connection: &mut PgConnection,
table: &str,
trigger: &str,
) -> Result<Option<String>, MigrationError> {
let row = query(
"select pg_get_triggerdef(trg.oid) as definition
from pg_catalog.pg_trigger trg
join pg_catalog.pg_class t on t.oid = trg.tgrelid
join pg_catalog.pg_namespace n on n.oid = t.relnamespace
where n.nspname = current_schema()
and t.relname = $1
and trg.tgname = $2
and not trg.tgisinternal",
)
.bind(table)
.bind(trigger)
.fetch_optional(&mut *connection)
.await
.map_err(|_| MigrationError::storage("preflight.schema"))?;
row.map(|row| row.try_get::<String, _>("definition"))
.transpose()
.map_err(|_| MigrationError::storage("preflight.schema"))
}
async fn function_definition(
connection: &mut PgConnection,
function_name: &str,
) -> Result<String, MigrationError> {
query(
"select pg_get_functiondef(p.oid) as definition
from pg_catalog.pg_proc p
join pg_catalog.pg_namespace n on n.oid = p.pronamespace
where n.nspname = current_schema()
and p.proname = $1",
)
.bind(function_name)
.fetch_optional(connection)
.await
.map_err(|_| MigrationError::storage("preflight.schema"))?
.ok_or_else(|| schema_error(9))?
.try_get::<String, _>("definition")
.map_err(|_| MigrationError::storage("preflight.schema"))
}