наблюдаемость: завершить базовый контур Community
CI / Rust Checks (push) Failing after 4m28s
CI / UI Checks (push) Has been skipped
CI / Frontend E2E (push) Has been skipped
CI / Community Image Smoke (push) Has been skipped
CI / Deploy (push) Has been skipped

Добавить структурированные журналы, метрики, трассировку и безопасный канал критических ошибок. Усилить границы рантайма, тесты, проверку зависимостей и сценарии развёртывания.
This commit is contained in:
2026-07-31 01:01:14 +03:00
parent 99bd05c145
commit 0e8f1ca03a
160 changed files with 13506 additions and 1499 deletions
+2
View File
@@ -74,6 +74,8 @@ pub enum RegistryError {
YamlImportJobNotFound { job_id: String },
#[error("import job {job_id} was not found")]
ImportJobNotFound { job_id: String },
#[error("import job {job_id} was already applied with different parameters")]
ImportJobAlreadyApplied { job_id: String },
#[error("unsupported enum representation for field {field}")]
InvalidEnumRepresentation { field: &'static str },
#[error("invalid numeric value for field {field}: {value}")]
+31 -25
View File
@@ -9,12 +9,14 @@ pub use ext::{ExtensionMigration, RegistryExtension, apply_extension_migrations}
pub mod records {
pub use crate::model::{
AgentSummary, AgentVersionRecord, ApprovalRequestRecord, AuthUserRecord, DescriptorKind,
DescriptorMetadata, ImportJob, ImportJobId, ImportJobKind, ImportJobStatus,
InvitationRecord, InvocationLogRecord, MembershipRecord, OperationAgentRef,
OperationSampleMetadata, OperationSummary, OperationUsageSummary, OperationVersionRecord,
Page, PlatformApiKeyRecord, PublishedAgentCatalog, PublishedAgentTool, RegistryOperation,
SampleKind, SecretRecord, SecretVersionRecord, SessionRecord, UsageAgentBreakdown,
AgentSummary, AgentVersionRecord, AppliedImportOperation, ApprovalRequestRecord,
AuthUserRecord, DescriptorKind, DescriptorMetadata, ImportJob, ImportJobApplyResult,
ImportJobId, ImportJobKind, ImportJobStatus, InvitationRecord, InvocationHistoryLoss,
InvocationHistoryLossCategory, InvocationHistoryWriteOutcome, InvocationLogRecord,
MembershipRecord, OperationAgentRef, OperationSampleMetadata, OperationSummary,
OperationUsageSummary, OperationVersionRecord, Page, PlatformApiKeyRecord,
PublishedAgentCatalog, PublishedAgentTool, RegistryOperation, SampleKind, SecretRecord,
SecretVersionRecord, SessionRecord, SkippedImportOperation, UsageAgentBreakdown,
UsageBucket, UsageOperationBreakdown, UsageRollupRecord, UsageSummary, UsageTimelinePoint,
WorkspaceMembershipRecord, WorkspaceRecord, WorkspaceUpstream, WorkspaceUpstreamId,
YamlImportJob, YamlImportJobCompletion, YamlImportJobId, YamlImportJobStatus,
@@ -23,11 +25,12 @@ pub mod records {
pub mod requests {
pub use crate::model::{
CreateAgentDraftVersionRequest, CreateAgentRequest, CreateApprovalRequest,
CreateImportJobRequest, CreateInvitationRequest, CreateInvocationLogRequest,
CreatePlatformApiKeyRequest, CreateSecretRequest, CreateVersionRequest,
CreateWorkspaceRequest, CreateYamlImportJobRequest, DecideApprovalRequest,
ExpireApprovalRequest, FinishApprovalRequest, FinishImportJobRequest,
ApplyImportJobRequest, CreateAgentDraftVersionRequest, CreateAgentRequest,
CreateApprovalRequest, CreateImportJobRequest, CreateInvitationRequest,
CreateInvocationLogRequest, CreatePlatformApiKeyRequest, CreateSecretRequest,
CreateVersionRequest, CreateWorkspaceRequest, CreateYamlImportJobRequest,
DecideApprovalRequest, ExpireApprovalRequest, FinishApprovalRequest,
FinishImportJobRequest, ImportConflictMode, ImportOperationDraft,
ListApprovalRequestsQuery, ListInvocationLogsQuery, PublishAgentRequest, PublishRequest,
RotateSecretRequest, SaveAgentBindingsRequest, SaveAgentCatalogConfigRequest,
SaveAuthProfileRequest, SaveDescriptorMetadataRequest, SaveSampleMetadataRequest,
@@ -41,20 +44,23 @@ pub mod infrastructure {
}
pub use model::{
AgentSummary, AgentVersionRecord, ApprovalRequestRecord, AuthUserRecord,
CreateAgentDraftVersionRequest, CreateAgentRequest, CreateApprovalRequest,
CreateImportJobRequest, CreateInvitationRequest, CreateInvocationLogRequest,
CreatePlatformApiKeyRequest, CreateSecretRequest, CreateVersionRequest, CreateWorkspaceRequest,
CreateYamlImportJobRequest, DecideApprovalRequest, DescriptorKind, DescriptorMetadata,
ExpireApprovalRequest, FinishApprovalRequest, FinishImportJobRequest, ImportJob, ImportJobId,
ImportJobKind, ImportJobStatus, InvitationRecord, InvocationLogRecord,
ListApprovalRequestsQuery, ListInvocationLogsQuery, MembershipRecord, OperationAgentRef,
OperationSampleMetadata, OperationSummary, OperationUsageSummary, OperationVersionRecord, Page,
PlatformApiKeyRecord, PublishAgentRequest, PublishRequest, PublishedAgentCatalog,
PublishedAgentTool, RegistryOperation, RotateSecretRequest, SampleKind,
SaveAgentBindingsRequest, SaveAgentCatalogConfigRequest, SaveAuthProfileRequest,
SaveDescriptorMetadataRequest, SaveSampleMetadataRequest, SaveWorkspaceUpstreamRequest,
SecretRecord, SecretVersionRecord, SessionRecord, UpdateWorkspaceRequest, UsageAgentBreakdown,
AgentSummary, AgentVersionRecord, AppliedImportOperation, ApplyImportJobRequest,
ApprovalRequestRecord, AuthUserRecord, CreateAgentDraftVersionRequest, CreateAgentRequest,
CreateApprovalRequest, CreateImportJobRequest, CreateInvitationRequest,
CreateInvocationLogRequest, CreatePlatformApiKeyRequest, CreateSecretRequest,
CreateVersionRequest, CreateWorkspaceRequest, CreateYamlImportJobRequest,
DecideApprovalRequest, DescriptorKind, DescriptorMetadata, ExpireApprovalRequest,
FinishApprovalRequest, FinishImportJobRequest, ImportConflictMode, ImportJob,
ImportJobApplyResult, ImportJobId, ImportJobKind, ImportJobStatus, ImportOperationDraft,
InvitationRecord, InvocationHistoryLoss, InvocationHistoryLossCategory,
InvocationHistoryWriteOutcome, InvocationLogRecord, ListApprovalRequestsQuery,
ListInvocationLogsQuery, MembershipRecord, OperationAgentRef, OperationSampleMetadata,
OperationSummary, OperationUsageSummary, OperationVersionRecord, Page, PlatformApiKeyRecord,
PublishAgentRequest, PublishRequest, PublishedAgentCatalog, PublishedAgentTool,
RegistryOperation, RotateSecretRequest, SampleKind, SaveAgentBindingsRequest,
SaveAgentCatalogConfigRequest, SaveAuthProfileRequest, SaveDescriptorMetadataRequest,
SaveSampleMetadataRequest, SaveWorkspaceUpstreamRequest, SecretRecord, SecretVersionRecord,
SessionRecord, SkippedImportOperation, UpdateWorkspaceRequest, UsageAgentBreakdown,
UsageBucket, UsageOperationBreakdown, UsageQuery, UsageRollupRecord, UsageSummary,
UsageTimelinePoint, WorkspaceMembershipRecord, WorkspaceRecord, WorkspaceUpstream,
WorkspaceUpstreamId, YamlImportJob, YamlImportJobCompletion, YamlImportJobId,
+118 -69
View File
@@ -1,6 +1,61 @@
use sqlx::{PgPool, query};
use sqlx::{PgPool, Postgres, Row, Transaction, query};
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 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,
@@ -12,7 +67,7 @@ pub async fn apply_postgres(pool: &PgPool) -> Result<(), sqlx::Error> {
updated_at timestamptz not null
)",
)
.execute(pool)
.execute(&mut **transaction)
.await?;
query(
@@ -25,11 +80,11 @@ pub async fn apply_postgres(pool: &PgPool) -> Result<(), sqlx::Error> {
created_at timestamptz not null
)",
)
.execute(pool)
.execute(&mut **transaction)
.await?;
query("alter table users add column if not exists password_hash text null")
.execute(pool)
.execute(&mut **transaction)
.await?;
query(
@@ -48,7 +103,7 @@ pub async fn apply_postgres(pool: &PgPool) -> Result<(), sqlx::Error> {
)
on conflict (id) do nothing",
)
.execute(pool)
.execute(&mut **transaction)
.await?;
query(
@@ -60,7 +115,7 @@ pub async fn apply_postgres(pool: &PgPool) -> Result<(), sqlx::Error> {
primary key (workspace_id, user_id)
)",
)
.execute(pool)
.execute(&mut **transaction)
.await?;
query(
@@ -75,14 +130,14 @@ pub async fn apply_postgres(pool: &PgPool) -> Result<(), sqlx::Error> {
created_at timestamptz not null
)",
)
.execute(pool)
.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(pool)
.execute(&mut **transaction)
.await?;
query(
@@ -105,7 +160,7 @@ pub async fn apply_postgres(pool: &PgPool) -> Result<(), sqlx::Error> {
)
on conflict (id) do nothing",
)
.execute(pool)
.execute(&mut **transaction)
.await?;
query(
@@ -122,7 +177,7 @@ pub async fn apply_postgres(pool: &PgPool) -> Result<(), sqlx::Error> {
)
on conflict (workspace_id, user_id) do nothing",
)
.execute(pool)
.execute(&mut **transaction)
.await?;
query(
@@ -137,7 +192,7 @@ pub async fn apply_postgres(pool: &PgPool) -> Result<(), sqlx::Error> {
created_at timestamptz not null
)",
)
.execute(pool)
.execute(&mut **transaction)
.await?;
query(
@@ -158,29 +213,29 @@ pub async fn apply_postgres(pool: &PgPool) -> Result<(), sqlx::Error> {
allowed_origins_json jsonb not null default '[]'::jsonb
)",
)
.execute(pool)
.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(pool)
.execute(&mut **transaction)
.await?;
query("alter table platform_api_keys add column if not exists agent_id text null")
.execute(pool)
.execute(&mut **transaction)
.await?;
query(
"alter table platform_api_keys add column if not exists key_kind text not null default 'mcp_client'",
)
.execute(pool)
.execute(&mut **transaction)
.await?;
query("alter table platform_api_keys add column if not exists expires_at timestamptz null")
.execute(pool)
.execute(&mut **transaction)
.await?;
query(
"alter table platform_api_keys add column if not exists allowed_origins_json jsonb not null default '[]'::jsonb",
)
.execute(pool)
.execute(&mut **transaction)
.await?;
query(
@@ -203,7 +258,7 @@ pub async fn apply_postgres(pool: &PgPool) -> Result<(), sqlx::Error> {
)
on conflict (id) do nothing",
)
.execute(pool)
.execute(&mut **transaction)
.await?;
query(
@@ -223,35 +278,35 @@ pub async fn apply_postgres(pool: &PgPool) -> Result<(), sqlx::Error> {
published_at timestamptz null
)",
)
.execute(pool)
.execute(&mut **transaction)
.await?;
query("alter table operations add column if not exists workspace_id text null references workspaces(id) on delete cascade")
.execute(pool)
.execute(&mut **transaction)
.await?;
query(
"alter table operations add column if not exists category text not null default 'general'",
)
.execute(pool)
.execute(&mut **transaction)
.await?;
query(
"alter table operations add column if not exists security_level text not null default 'standard'",
)
.execute(pool)
.execute(&mut **transaction)
.await?;
query("update operations set workspace_id = 'ws_default' where workspace_id is null")
.execute(pool)
.execute(&mut **transaction)
.await?;
query("alter table operations alter column workspace_id set not null")
.execute(pool)
.execute(&mut **transaction)
.await?;
query("alter table operations drop constraint if exists operations_name_key")
.execute(pool)
.execute(&mut **transaction)
.await?;
query(
"create unique index if not exists operations_workspace_name_idx on operations(workspace_id, name)",
)
.execute(pool)
.execute(&mut **transaction)
.await?;
query(
@@ -276,11 +331,11 @@ pub async fn apply_postgres(pool: &PgPool) -> Result<(), sqlx::Error> {
primary key (operation_id, version)
)",
)
.execute(pool)
.execute(&mut **transaction)
.await?;
query("alter table operation_versions add column if not exists wizard_state_json jsonb null")
.execute(pool)
.execute(&mut **transaction)
.await?;
query(
@@ -292,7 +347,7 @@ pub async fn apply_postgres(pool: &PgPool) -> Result<(), sqlx::Error> {
foreign key (operation_id, version) references operation_versions(operation_id, version) on delete cascade
)",
)
.execute(pool)
.execute(&mut **transaction)
.await?;
query(
@@ -308,7 +363,7 @@ pub async fn apply_postgres(pool: &PgPool) -> Result<(), sqlx::Error> {
foreign key (operation_id, version) references operation_versions(operation_id, version) on delete cascade
)",
)
.execute(pool)
.execute(&mut **transaction)
.await?;
query(
@@ -324,7 +379,7 @@ pub async fn apply_postgres(pool: &PgPool) -> Result<(), sqlx::Error> {
foreign key (operation_id, version) references operation_versions(operation_id, version) on delete cascade
)",
)
.execute(pool)
.execute(&mut **transaction)
.await?;
query(
@@ -338,25 +393,25 @@ pub async fn apply_postgres(pool: &PgPool) -> Result<(), sqlx::Error> {
updated_at timestamptz not null
)",
)
.execute(pool)
.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(pool)
.execute(&mut **transaction)
.await?;
query("update auth_profiles set workspace_id = 'ws_default' where workspace_id is null")
.execute(pool)
.execute(&mut **transaction)
.await?;
query("alter table auth_profiles alter column workspace_id set not null")
.execute(pool)
.execute(&mut **transaction)
.await?;
query("alter table auth_profiles drop constraint if exists auth_profiles_name_key")
.execute(pool)
.execute(&mut **transaction)
.await?;
query(
"create unique index if not exists auth_profiles_workspace_name_idx on auth_profiles(workspace_id, name)",
)
.execute(pool)
.execute(&mut **transaction)
.await?;
query(
@@ -371,17 +426,17 @@ pub async fn apply_postgres(pool: &PgPool) -> Result<(), sqlx::Error> {
updated_at timestamptz not null
)",
)
.execute(pool)
.execute(&mut **transaction)
.await?;
query(
"create unique index if not exists workspace_upstreams_workspace_name_idx on workspace_upstreams(workspace_id, name)",
)
.execute(pool)
.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(pool)
.execute(&mut **transaction)
.await?;
query(
"insert into workspace_upstreams (
@@ -411,7 +466,7 @@ pub async fn apply_postgres(pool: &PgPool) -> Result<(), sqlx::Error> {
and wu.name = 'Frankfurter'
)",
)
.execute(pool)
.execute(&mut **transaction)
.await?;
query(
@@ -427,13 +482,13 @@ pub async fn apply_postgres(pool: &PgPool) -> Result<(), sqlx::Error> {
updated_at timestamptz not null
)",
)
.execute(pool)
.execute(&mut **transaction)
.await?;
query(
"create unique index if not exists secrets_workspace_name_idx on secrets(workspace_id, name)",
)
.execute(pool)
.execute(&mut **transaction)
.await?;
query(
@@ -447,7 +502,7 @@ pub async fn apply_postgres(pool: &PgPool) -> Result<(), sqlx::Error> {
primary key (secret_id, version)
)",
)
.execute(pool)
.execute(&mut **transaction)
.await?;
query(
@@ -464,7 +519,7 @@ pub async fn apply_postgres(pool: &PgPool) -> Result<(), sqlx::Error> {
finished_at timestamptz null
)",
)
.execute(pool)
.execute(&mut **transaction)
.await?;
query(
@@ -483,7 +538,7 @@ pub async fn apply_postgres(pool: &PgPool) -> Result<(), sqlx::Error> {
finished_at timestamptz null
)",
)
.execute(pool)
.execute(&mut **transaction)
.await?;
query(
@@ -501,13 +556,13 @@ pub async fn apply_postgres(pool: &PgPool) -> Result<(), sqlx::Error> {
published_at timestamptz null
)",
)
.execute(pool)
.execute(&mut **transaction)
.await?;
query(
"create unique index if not exists agents_workspace_slug_idx on agents(workspace_id, slug)",
)
.execute(pool)
.execute(&mut **transaction)
.await?;
query(
@@ -521,7 +576,7 @@ pub async fn apply_postgres(pool: &PgPool) -> Result<(), sqlx::Error> {
primary key (agent_id, version)
)",
)
.execute(pool)
.execute(&mut **transaction)
.await?;
query(
@@ -538,13 +593,13 @@ pub async fn apply_postgres(pool: &PgPool) -> Result<(), sqlx::Error> {
foreign key (operation_id, operation_version) references operation_versions(operation_id, version) on delete cascade
)",
)
.execute(pool)
.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(pool)
.execute(&mut **transaction)
.await?;
query(
@@ -556,7 +611,7 @@ pub async fn apply_postgres(pool: &PgPool) -> Result<(), sqlx::Error> {
foreign key (agent_id, version) references agent_versions(agent_id, version) on delete cascade
)",
)
.execute(pool)
.execute(&mut **transaction)
.await?;
query(
@@ -577,36 +632,30 @@ pub async fn apply_postgres(pool: &PgPool) -> Result<(), sqlx::Error> {
decision_note text null
)",
)
.execute(pool)
.execute(&mut **transaction)
.await?;
query("alter table approval_requests drop column if exists confirmation_title")
.execute(pool)
.await?;
query("alter table approval_requests drop column if exists confirmation_body")
.execute(pool)
.await?;
query("alter table approval_requests add column if not exists execution_started_at timestamptz null")
.execute(pool)
.execute(&mut **transaction)
.await?;
query("alter table approval_requests add column if not exists execution_attempts integer not null default 0")
.execute(pool)
.execute(&mut **transaction)
.await?;
query("alter table approval_requests add column if not exists request_fingerprint text null")
.execute(pool)
.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(pool)
.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(pool)
.execute(&mut **transaction)
.await?;
query(
@@ -629,25 +678,25 @@ pub async fn apply_postgres(pool: &PgPool) -> Result<(), sqlx::Error> {
created_at timestamptz not null
)",
)
.execute(pool)
.execute(&mut **transaction)
.await?;
query(
"create index if not exists invocation_logs_workspace_created_idx on invocation_logs(workspace_id, created_at desc)",
)
.execute(pool)
.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(pool)
.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(pool)
.execute(&mut **transaction)
.await?;
query(
@@ -665,7 +714,7 @@ pub async fn apply_postgres(pool: &PgPool) -> Result<(), sqlx::Error> {
updated_at timestamptz not null
)",
)
.execute(pool)
.execute(&mut **transaction)
.await?;
Ok(())
+81
View File
@@ -395,11 +395,92 @@ pub struct FinishImportJobRequest<'a> {
pub finished_at: &'a OffsetDateTime,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ImportConflictMode {
Skip,
Rename,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ImportOperationDraft {
pub operation_key: String,
pub operation: RegistryOperation,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct AppliedImportOperation {
pub operation_key: String,
pub operation_id: OperationId,
pub name: String,
pub version: u32,
pub renamed_from: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SkippedImportOperation {
pub operation_key: String,
pub name: String,
pub reason: String,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ImportJobApplyResult {
pub application_key: String,
pub created: Vec<AppliedImportOperation>,
pub skipped: Vec<SkippedImportOperation>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ApplyImportJobRequest<'a> {
pub id: &'a ImportJobId,
pub workspace_id: &'a WorkspaceId,
pub application_key: &'a str,
pub conflict_mode: ImportConflictMode,
pub operations: &'a [ImportOperationDraft],
pub finished_at: &'a OffsetDateTime,
}
#[derive(Clone, Debug, PartialEq)]
pub struct CreateInvocationLogRequest<'a> {
pub log: &'a InvocationLog,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum InvocationHistoryLossCategory {
Unavailable,
InvalidRecord,
}
impl InvocationHistoryLossCategory {
pub fn as_str(self) -> &'static str {
match self {
Self::Unavailable => "unavailable",
Self::InvalidRecord => "invalid_record",
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct InvocationHistoryLoss {
pub category: InvocationHistoryLossCategory,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum InvocationHistoryWriteOutcome {
Recorded,
Lost(InvocationHistoryLoss),
}
impl InvocationHistoryWriteOutcome {
pub fn loss(self) -> Option<InvocationHistoryLoss> {
match self {
Self::Recorded => None,
Self::Lost(loss) => Some(loss),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ListInvocationLogsQuery<'a> {
pub workspace_id: &'a WorkspaceId,
+19 -6
View File
@@ -397,18 +397,31 @@ impl PostgresRegistry {
key_id: &PlatformApiKeyId,
used_at: &time::OffsetDateTime,
) -> Result<(), RegistryError> {
let result = sqlx::query(
"update platform_api_keys
set last_used_at = $3::timestamptz
where workspace_id = $1 and id = $2",
let exists = sqlx::query_scalar::<_, bool>(
"with target as (
select id
from platform_api_keys
where workspace_id = $1 and id = $2
), updated as (
update platform_api_keys
set last_used_at = $3::timestamptz
where workspace_id = $1
and id = $2
and (
last_used_at is null
or last_used_at < $3::timestamptz - interval '1 minute'
)
returning id
)
select exists(select 1 from target)",
)
.bind(workspace_id.as_str())
.bind(key_id.as_str())
.bind(used_at)
.execute(&self.pool)
.fetch_one(&self.pool)
.await?;
if result.rows_affected() == 0 {
if !exists {
return Err(RegistryError::PlatformApiKeyNotFound {
key_id: key_id.as_str().to_owned(),
});
+47 -5
View File
@@ -357,21 +357,20 @@ impl PostgresRegistry {
&self,
started_at: OffsetDateTime,
approved_before: OffsetDateTime,
stale_before: OffsetDateTime,
) -> Result<Option<ApprovalRequestRecord>, RegistryError> {
let row = sqlx::query(
"with candidate as (
select id
from approval_requests
where (status = 'approved' and decided_at <= $1)
or (status = 'executing' and execution_started_at < $2)
where status = 'approved'
and decided_at <= $1
order by decided_at asc nulls last, created_at asc
for update skip locked
limit 1
)
update approval_requests as approval
set status = 'executing',
execution_started_at = $3,
execution_started_at = $2,
execution_attempts = approval.execution_attempts + 1
from candidate
where approval.id = candidate.id
@@ -383,7 +382,6 @@ impl PostgresRegistry {
approval.decided_at, approval.decided_by_key_id, approval.decision_note",
)
.bind(approved_before)
.bind(stale_before)
.bind(started_at)
.fetch_optional(&self.pool)
.await?;
@@ -391,6 +389,50 @@ impl PostgresRegistry {
row.map(map_approval_request_row).transpose()
}
pub async fn fail_next_interrupted_approval_request(
&self,
stale_before: OffsetDateTime,
) -> Result<Option<ApprovalRequestRecord>, RegistryError> {
let response_payload = serde_json::json!({
"error": {
"code": "approval_execution_outcome_unknown",
"message": "execution was interrupted; the operation was not retried automatically"
}
});
let row = sqlx::query(
"with candidate as (
select id
from approval_requests
where status = 'executing'
and execution_started_at < $1
order by execution_started_at asc, created_at asc
for update skip locked
limit 1
)
update approval_requests as approval
set status = 'failed',
response_payload_json = $2,
decision_note = coalesce(
approval.decision_note,
'execution interrupted; outcome unknown'
)
from candidate
where approval.id = candidate.id
returning
approval.id, approval.workspace_id, approval.agent_id,
approval.operation_id, approval.operation_version, approval.status,
approval.risk_level, approval.request_payload_json,
approval.response_payload_json, approval.created_at, approval.expires_at,
approval.decided_at, approval.decided_by_key_id, approval.decision_note",
)
.bind(stale_before)
.bind(Json(response_payload))
.fetch_optional(&self.pool)
.await?;
row.map(map_approval_request_row).transpose()
}
pub async fn expire_approval_request(
&self,
request: ExpireApprovalRequest<'_>,
+101 -42
View File
@@ -1,15 +1,31 @@
use super::*;
use time::OffsetDateTime;
fn map_auth_user_row(row: &PgRow) -> Result<AuthUserRecord, RegistryError> {
let status = row.try_get::<String, _>("status")?;
Ok(AuthUserRecord {
user: User {
id: UserId::new(row.try_get::<String, _>("id")?),
email: row.try_get("email")?,
display_name: row.try_get("display_name")?,
status: deserialize_enum_text(&status, "status")?,
created_at: row.try_get::<OffsetDateTime, _>("created_at")?,
},
password_hash: row
.try_get::<Option<String>, _>("password_hash")?
.unwrap_or_default(),
})
}
impl PostgresRegistry {
pub async fn upsert_bootstrap_user(
pub async fn ensure_bootstrap_user(
&self,
email: &str,
display_name: &str,
password_hash: &str,
) -> Result<UserId, RegistryError> {
let user_id = format!("user_{}", uuid::Uuid::now_v7().simple());
let row = sqlx::query!(
if let Some(id) = sqlx::query_scalar::<_, String>(
"insert into users (
id,
email,
@@ -24,16 +40,40 @@ impl PostgresRegistry {
set display_name = excluded.display_name,
password_hash = excluded.password_hash,
status = 'active'
where users.password_hash is null
returning id",
user_id,
email,
display_name,
password_hash,
)
.bind(user_id)
.bind(email)
.bind(display_name)
.bind(password_hash)
.fetch_optional(&self.pool)
.await?
{
return Ok(UserId::new(id));
}
let existing = sqlx::query_scalar::<_, String>(
"select id
from users
where email = $1
limit 1",
)
.bind(email)
.fetch_one(&self.pool)
.await?;
Ok(UserId::new(row.id))
Ok(UserId::new(existing))
}
pub async fn upsert_bootstrap_user(
&self,
email: &str,
display_name: &str,
password_hash: &str,
) -> Result<UserId, RegistryError> {
self.ensure_bootstrap_user(email, display_name, password_hash)
.await
}
pub async fn ensure_membership(
@@ -67,70 +107,46 @@ impl PostgresRegistry {
&self,
email: &str,
) -> Result<Option<AuthUserRecord>, RegistryError> {
let row = sqlx::query!(
let row = sqlx::query(
"select
id,
email,
display_name,
password_hash as \"password_hash!\",
password_hash,
status,
created_at as \"created_at!: OffsetDateTime\"
created_at
from users
where email = $1
limit 1",
email,
)
.bind(email)
.fetch_optional(&self.pool)
.await?;
row.map(|row| {
Ok(AuthUserRecord {
user: User {
id: UserId::new(row.id),
email: row.email,
display_name: row.display_name,
status: deserialize_enum_text(&row.status, "status")?,
created_at: row.created_at,
},
password_hash: row.password_hash,
})
})
.transpose()
row.as_ref().map(map_auth_user_row).transpose()
}
pub async fn get_auth_user_by_id(
&self,
user_id: &UserId,
) -> Result<Option<AuthUserRecord>, RegistryError> {
let row = sqlx::query!(
let row = sqlx::query(
"select
id,
email,
display_name,
password_hash as \"password_hash!\",
password_hash,
status,
created_at as \"created_at!: OffsetDateTime\"
created_at
from users
where id = $1
limit 1",
user_id.as_str(),
)
.bind(user_id.as_str())
.fetch_optional(&self.pool)
.await?;
row.map(|row| {
Ok(AuthUserRecord {
user: User {
id: UserId::new(row.id),
email: row.email,
display_name: row.display_name,
status: deserialize_enum_text(&row.status, "status")?,
created_at: row.created_at,
},
password_hash: row.password_hash,
})
})
.transpose()
row.as_ref().map(map_auth_user_row).transpose()
}
pub async fn update_user_profile(
@@ -191,6 +207,45 @@ impl PostgresRegistry {
Ok(())
}
pub async fn update_user_password_and_revoke_other_sessions(
&self,
user_id: &UserId,
current_session_id: &UserSessionId,
password_hash: &str,
) -> Result<(), RegistryError> {
let mut tx = self.pool.begin().await?;
let result = sqlx::query(
"update users
set password_hash = $2
where id = $1",
)
.bind(user_id.as_str())
.bind(password_hash)
.execute(&mut *tx)
.await?;
if result.rows_affected() == 0 {
return Err(RegistryError::UserNotFound {
user_id: user_id.as_str().to_owned(),
});
}
sqlx::query(
"update user_sessions
set status = 'revoked'
where user_id = $1
and id <> $2
and status = 'active'",
)
.bind(user_id.as_str())
.bind(current_session_id.as_str())
.execute(&mut *tx)
.await?;
tx.commit().await?;
Ok(())
}
pub async fn create_user_session(
&self,
session_id: &UserSessionId,
@@ -305,7 +360,11 @@ impl PostgresRegistry {
sqlx::query(
"update user_sessions
set last_seen_at = now()
where id = $1",
where id = $1
and (
last_seen_at is null
or last_seen_at < now() - interval '1 minute'
)",
)
.bind(session_id.as_str())
.execute(&self.pool)
@@ -41,6 +41,11 @@ impl PostgresRegistry {
&self.pool
}
pub async fn ping(&self) -> Result<(), RegistryError> {
sqlx::query("select 1").execute(&self.pool).await?;
Ok(())
}
pub async fn migrate(&self) -> Result<(), RegistryError> {
migrations::apply_postgres(&self.pool).await?;
Ok(())
@@ -1,5 +1,7 @@
use super::*;
const APPLICATION_RESULT_KEY: &str = "_crank_application_result";
impl PostgresRegistry {
pub async fn create_import_job(
&self,
@@ -97,6 +99,43 @@ impl PostgresRegistry {
Ok(())
}
pub async fn apply_import_job(
&self,
request: ApplyImportJobRequest<'_>,
) -> Result<ImportJobApplyResult, RegistryError> {
let mut tx = self.pool.begin().await?;
let applied = apply_import_job_transaction(&mut tx, &request).await;
match applied {
Ok(result) => {
tx.commit().await?;
Ok(result)
}
Err(error) => {
tx.rollback().await?;
let error_text = error.to_string();
let _ = sqlx::query(
"update import_jobs
set status = $3,
error_text = $4,
finished_at = $5::timestamptz
where id = $1
and workspace_id = $2
and status <> $6",
)
.bind(request.id.as_str())
.bind(request.workspace_id.as_str())
.bind(serialize_enum_text(&ImportJobStatus::Failed, "status")?)
.bind(error_text)
.bind(request.finished_at)
.bind(serialize_enum_text(&ImportJobStatus::Completed, "status")?)
.execute(&self.pool)
.await;
Err(error)
}
}
}
pub async fn delete_expired_import_jobs(&self) -> Result<u64, RegistryError> {
let result = sqlx::query("delete from import_jobs where expires_at < now()")
.execute(&self.pool)
@@ -105,3 +144,160 @@ impl PostgresRegistry {
Ok(result.rows_affected())
}
}
async fn apply_import_job_transaction(
tx: &mut Transaction<'_, Postgres>,
request: &ApplyImportJobRequest<'_>,
) -> Result<ImportJobApplyResult, RegistryError> {
let row = sqlx::query(
"select status, preview_payload
from import_jobs
where id = $1 and workspace_id = $2
for update",
)
.bind(request.id.as_str())
.bind(request.workspace_id.as_str())
.fetch_optional(&mut **tx)
.await?
.ok_or_else(|| RegistryError::ImportJobNotFound {
job_id: request.id.as_str().to_owned(),
})?;
let status = deserialize_enum_text::<ImportJobStatus>(row.try_get("status")?, "status")?;
let mut preview_payload = row.try_get::<Value, _>("preview_payload")?;
if status == ImportJobStatus::Completed
&& let Some(result) = stored_application_result(&preview_payload)?
{
if result.application_key != request.application_key {
return Err(RegistryError::ImportJobAlreadyApplied {
job_id: request.id.as_str().to_owned(),
});
}
return Ok(result);
}
sqlx::query("select id from workspaces where id = $1 for update")
.bind(request.workspace_id.as_str())
.fetch_one(&mut **tx)
.await?;
let mut result = ImportJobApplyResult {
application_key: request.application_key.to_owned(),
..ImportJobApplyResult::default()
};
for draft in request.operations {
if draft.operation.version != 1 {
return Err(RegistryError::InvalidInitialVersion {
operation_id: draft.operation.id.as_str().to_owned(),
version: draft.operation.version,
});
}
let mut operation = draft.operation.clone();
let original_name = operation.name.clone();
if operation_name_exists(tx, request.workspace_id, &operation.name).await? {
match request.conflict_mode {
ImportConflictMode::Skip => {
result.skipped.push(SkippedImportOperation {
operation_key: draft.operation_key.clone(),
name: operation.name,
reason: "operation_name_conflict".to_owned(),
});
continue;
}
ImportConflictMode::Rename => {
operation.name =
next_available_operation_name(tx, request.workspace_id, &operation.name)
.await?;
}
}
}
insert_operation_rows(tx, request.workspace_id, &operation, None).await?;
result.created.push(AppliedImportOperation {
operation_key: draft.operation_key.clone(),
operation_id: operation.id,
name: operation.name.clone(),
version: operation.version,
renamed_from: (operation.name != original_name).then_some(original_name),
});
}
let stored_result = serde_json::to_value(&result)?;
if let Some(object) = preview_payload.as_object_mut() {
object.insert(APPLICATION_RESULT_KEY.to_owned(), stored_result);
} else {
preview_payload = serde_json::json!({
"preview": preview_payload,
"_crank_application_result": stored_result,
});
}
let created_operation_ids = serde_json::to_value(
result
.created
.iter()
.map(|operation| operation.operation_id.as_str())
.collect::<Vec<_>>(),
)?;
sqlx::query(
"update import_jobs
set status = $3,
preview_payload = $4,
created_operation_ids = $5,
error_text = null,
finished_at = $6::timestamptz
where id = $1 and workspace_id = $2",
)
.bind(request.id.as_str())
.bind(request.workspace_id.as_str())
.bind(serialize_enum_text(&ImportJobStatus::Completed, "status")?)
.bind(preview_payload)
.bind(created_operation_ids)
.bind(request.finished_at)
.execute(&mut **tx)
.await?;
Ok(result)
}
fn stored_application_result(
preview_payload: &Value,
) -> Result<Option<ImportJobApplyResult>, RegistryError> {
preview_payload
.get(APPLICATION_RESULT_KEY)
.cloned()
.map(serde_json::from_value)
.transpose()
.map_err(RegistryError::from)
}
async fn operation_name_exists(
tx: &mut Transaction<'_, Postgres>,
workspace_id: &WorkspaceId,
name: &str,
) -> Result<bool, RegistryError> {
Ok(sqlx::query_scalar::<_, bool>(
"select exists(
select 1 from operations where workspace_id = $1 and name = $2
)",
)
.bind(workspace_id.as_str())
.bind(name)
.fetch_one(&mut **tx)
.await?)
}
async fn next_available_operation_name(
tx: &mut Transaction<'_, Postgres>,
workspace_id: &WorkspaceId,
base_name: &str,
) -> Result<String, RegistryError> {
for index in 2.. {
let candidate = format!("{base_name}_{index}");
if !operation_name_exists(tx, workspace_id, &candidate).await? {
return Ok(candidate);
}
}
unreachable!()
}
+68 -11
View File
@@ -30,23 +30,25 @@ pub use pool_config::{PostgresPoolConfig, PostgresPoolConfigError};
use crate::{
error::RegistryError,
model::{
AgentSummary, AgentVersionRecord, ApprovalRequestRecord, AuthUserRecord,
CreateAgentDraftVersionRequest, CreateAgentRequest, CreateApprovalRequest,
CreateImportJobRequest, CreateInvitationRequest, CreateInvocationLogRequest,
CreatePlatformApiKeyRequest, CreateSecretRequest, CreateVersionRequest,
CreateWorkspaceRequest, CreateYamlImportJobRequest, DecideApprovalRequest,
DescriptorMetadata, ExpireApprovalRequest, FinishApprovalRequest, FinishImportJobRequest,
ImportJob, ImportJobId, InvitationRecord, InvocationLogRecord, ListApprovalRequestsQuery,
AgentSummary, AgentVersionRecord, AppliedImportOperation, ApplyImportJobRequest,
ApprovalRequestRecord, AuthUserRecord, CreateAgentDraftVersionRequest, CreateAgentRequest,
CreateApprovalRequest, CreateImportJobRequest, CreateInvitationRequest,
CreateInvocationLogRequest, CreatePlatformApiKeyRequest, CreateSecretRequest,
CreateVersionRequest, CreateWorkspaceRequest, CreateYamlImportJobRequest,
DecideApprovalRequest, DescriptorMetadata, ExpireApprovalRequest, FinishApprovalRequest,
FinishImportJobRequest, ImportConflictMode, ImportJob, ImportJobApplyResult, ImportJobId,
ImportJobStatus, InvitationRecord, InvocationHistoryLoss, InvocationHistoryLossCategory,
InvocationHistoryWriteOutcome, InvocationLogRecord, ListApprovalRequestsQuery,
ListInvocationLogsQuery, MembershipRecord, OperationAgentRef, OperationSampleMetadata,
OperationSummary, OperationUsageSummary, OperationVersionRecord, PlatformApiKeyRecord,
PublishAgentRequest, PublishRequest, PublishedAgentCatalog, PublishedAgentTool,
RegistryOperation, RotateSecretRequest, SaveAgentBindingsRequest,
SaveAgentCatalogConfigRequest, SaveAuthProfileRequest, SaveDescriptorMetadataRequest,
SaveSampleMetadataRequest, SaveWorkspaceUpstreamRequest, SecretRecord, SecretVersionRecord,
SessionRecord, UpdateWorkspaceRequest, UsageAgentBreakdown, UsageOperationBreakdown,
UsageQuery, UsageRollupRecord, UsageSummary, UsageTimelinePoint, WorkspaceMembershipRecord,
WorkspaceRecord, WorkspaceUpstream, YamlImportJob, YamlImportJobCompletion,
YamlImportJobId, YamlImportJobStatus,
SessionRecord, SkippedImportOperation, UpdateWorkspaceRequest, UsageAgentBreakdown,
UsageOperationBreakdown, UsageQuery, UsageRollupRecord, UsageSummary, UsageTimelinePoint,
WorkspaceMembershipRecord, WorkspaceRecord, WorkspaceUpstream, YamlImportJob,
YamlImportJobCompletion, YamlImportJobId, YamlImportJobStatus,
},
};
@@ -135,6 +137,61 @@ async fn insert_version_row(
Ok(())
}
async fn insert_operation_rows(
tx: &mut Transaction<'_, Postgres>,
workspace_id: &WorkspaceId,
snapshot: &RegistryOperation,
created_by: Option<&str>,
) -> Result<(), RegistryError> {
sqlx::query(
"insert into operations (
id,
workspace_id,
name,
display_name,
category,
protocol,
security_level,
status,
current_draft_version,
latest_published_version,
created_at,
updated_at,
published_at
) values (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10,
$11::timestamptz,
$12::timestamptz,
$13::timestamptz
)",
)
.bind(snapshot.id.as_str())
.bind(workspace_id.as_str())
.bind(&snapshot.name)
.bind(&snapshot.display_name)
.bind(&snapshot.category)
.bind(serialize_enum_text(&snapshot.protocol, "protocol")?)
.bind(serialize_enum_text(
&snapshot.security_level,
"security_level",
)?)
.bind(serialize_enum_text(&snapshot.status, "status")?)
.bind(to_db_version(snapshot.version))
.bind(
snapshot
.published_at
.as_ref()
.map(|_| to_db_version(snapshot.version)),
)
.bind(snapshot.created_at)
.bind(snapshot.updated_at)
.bind(snapshot.published_at)
.execute(&mut **tx)
.await?;
insert_version_row(tx, snapshot, None, created_by).await
}
async fn insert_agent_version_row(
tx: &mut Transaction<'_, Postgres>,
version: &AgentVersion,
@@ -1,9 +1,47 @@
use super::*;
fn invocation_history_loss_category(error: &RegistryError) -> InvocationHistoryLossCategory {
match error {
RegistryError::Storage(error)
if error
.as_database_error()
.and_then(|database_error| database_error.code())
.is_some_and(|code| code.starts_with("22") || code.starts_with("23")) =>
{
InvocationHistoryLossCategory::InvalidRecord
}
RegistryError::Storage(_) => InvocationHistoryLossCategory::Unavailable,
_ => InvocationHistoryLossCategory::InvalidRecord,
}
}
impl PostgresRegistry {
pub async fn delete_invocation_logs_before(
&self,
cutoff: OffsetDateTime,
) -> Result<u64, RegistryError> {
let result = sqlx::query("delete from invocation_logs where created_at < $1::timestamptz")
.bind(cutoff)
.execute(&self.pool)
.await?;
Ok(result.rows_affected())
}
pub async fn create_invocation_log(
&self,
request: CreateInvocationLogRequest<'_>,
) -> InvocationHistoryWriteOutcome {
match self.try_create_invocation_log(request).await {
Ok(()) => InvocationHistoryWriteOutcome::Recorded,
Err(error) => InvocationHistoryWriteOutcome::Lost(InvocationHistoryLoss {
category: invocation_history_loss_category(&error),
}),
}
}
async fn try_create_invocation_log(
&self,
request: CreateInvocationLogRequest<'_>,
) -> Result<(), RegistryError> {
let request_preview = crank_core::sanitize_invocation_preview(&request.log.request_preview);
let response_preview =
@@ -26,53 +26,7 @@ impl PostgresRegistry {
let mut tx = self.pool.begin().await?;
sqlx::query(
"insert into operations (
id,
workspace_id,
name,
display_name,
category,
protocol,
security_level,
status,
current_draft_version,
latest_published_version,
created_at,
updated_at,
published_at
) values (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10,
$11::timestamptz,
$12::timestamptz,
$13::timestamptz
)",
)
.bind(snapshot.id.as_str())
.bind(workspace_id.as_str())
.bind(&snapshot.name)
.bind(&snapshot.display_name)
.bind(&snapshot.category)
.bind(serialize_enum_text(&snapshot.protocol, "protocol")?)
.bind(serialize_enum_text(
&snapshot.security_level,
"security_level",
)?)
.bind(serialize_enum_text(&snapshot.status, "status")?)
.bind(to_db_version(snapshot.version))
.bind(
snapshot
.published_at
.as_ref()
.map(|_| to_db_version(snapshot.version)),
)
.bind(snapshot.created_at)
.bind(snapshot.updated_at)
.bind(snapshot.published_at)
.execute(&mut *tx)
.await?;
insert_version_row(&mut tx, snapshot, None, created_by).await?;
insert_operation_rows(&mut tx, workspace_id, snapshot, created_by).await?;
tx.commit().await?;
Ok(())
+19 -6
View File
@@ -282,18 +282,31 @@ impl PostgresRegistry {
secret_id: &SecretId,
used_at: &OffsetDateTime,
) -> Result<(), RegistryError> {
let result = sqlx::query(
"update secrets
set last_used_at = $3::timestamptz
where workspace_id = $1 and id = $2",
let exists = sqlx::query_scalar::<_, bool>(
"with target as (
select id
from secrets
where workspace_id = $1 and id = $2
), updated as (
update secrets
set last_used_at = $3::timestamptz
where workspace_id = $1
and id = $2
and (
last_used_at is null
or last_used_at < $3::timestamptz - interval '1 minute'
)
returning id
)
select exists(select 1 from target)",
)
.bind(workspace_id.as_str())
.bind(secret_id.as_str())
.bind(used_at)
.execute(&self.pool)
.fetch_one(&self.pool)
.await?;
if result.rows_affected() == 0 {
if !exists {
return Err(RegistryError::SecretNotFound {
secret_id: secret_id.as_str().to_owned(),
});