0e8f1ca03a
Добавить структурированные журналы, метрики, трассировку и безопасный канал критических ошибок. Усилить границы рантайма, тесты, проверку зависимостей и сценарии развёртывания.
722 lines
24 KiB
Rust
722 lines
24 KiB
Rust
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,
|
|
slug text not null unique,
|
|
display_name text not null,
|
|
status text not null,
|
|
settings_json jsonb not null default '{}'::jsonb,
|
|
created_at timestamptz not null,
|
|
updated_at timestamptz not null
|
|
)",
|
|
)
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
|
|
query(
|
|
"create table if not exists users (
|
|
id text primary key,
|
|
email text not null unique,
|
|
display_name text not null,
|
|
password_hash text null,
|
|
status text not null,
|
|
created_at timestamptz not null
|
|
)",
|
|
)
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
|
|
query("alter table users add column if not exists password_hash text null")
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
|
|
query(
|
|
"insert into users (
|
|
id,
|
|
email,
|
|
display_name,
|
|
status,
|
|
created_at
|
|
) values (
|
|
'user_default_owner',
|
|
'owner@crank.local',
|
|
'Workspace Owner',
|
|
'active',
|
|
now()
|
|
)
|
|
on conflict (id) do nothing",
|
|
)
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
|
|
query(
|
|
"create table if not exists memberships (
|
|
workspace_id text not null references workspaces(id) on delete cascade,
|
|
user_id text not null references users(id) on delete cascade,
|
|
role text not null,
|
|
created_at timestamptz not null,
|
|
primary key (workspace_id, user_id)
|
|
)",
|
|
)
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
|
|
query(
|
|
"create table if not exists user_sessions (
|
|
id text primary key,
|
|
user_id text not null references users(id) on delete cascade,
|
|
current_workspace_id text null references workspaces(id) on delete set null,
|
|
secret_hash text not null,
|
|
status text not null,
|
|
expires_at timestamptz not null,
|
|
last_seen_at timestamptz null,
|
|
created_at timestamptz not null
|
|
)",
|
|
)
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
|
|
query(
|
|
"alter table user_sessions
|
|
add column if not exists current_workspace_id text null references workspaces(id) on delete set null",
|
|
)
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
|
|
query(
|
|
"insert into workspaces (
|
|
id,
|
|
slug,
|
|
display_name,
|
|
status,
|
|
settings_json,
|
|
created_at,
|
|
updated_at
|
|
) values (
|
|
'ws_default',
|
|
'default',
|
|
'Default Workspace',
|
|
'active',
|
|
'{}'::jsonb,
|
|
now(),
|
|
now()
|
|
)
|
|
on conflict (id) do nothing",
|
|
)
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
|
|
query(
|
|
"insert into memberships (
|
|
workspace_id,
|
|
user_id,
|
|
role,
|
|
created_at
|
|
) values (
|
|
'ws_default',
|
|
'user_default_owner',
|
|
'owner',
|
|
now()
|
|
)
|
|
on conflict (workspace_id, user_id) do nothing",
|
|
)
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
|
|
query(
|
|
"create table if not exists invitation_tokens (
|
|
id text primary key,
|
|
workspace_id text not null references workspaces(id) on delete cascade,
|
|
email text not null,
|
|
role text not null,
|
|
status text not null,
|
|
token_hash text not null,
|
|
expires_at timestamptz not null,
|
|
created_at timestamptz not null
|
|
)",
|
|
)
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
|
|
query(
|
|
"create table if not exists platform_api_keys (
|
|
id text primary key,
|
|
workspace_id text not null references workspaces(id) on delete cascade,
|
|
agent_id text null,
|
|
name text not null,
|
|
prefix text not null,
|
|
secret_hash text not null,
|
|
key_kind text not null default 'mcp_client',
|
|
scopes_json jsonb not null,
|
|
status text not null,
|
|
created_at timestamptz not null,
|
|
last_used_at timestamptz null,
|
|
revoked_at timestamptz null,
|
|
expires_at timestamptz null,
|
|
allowed_origins_json jsonb not null default '[]'::jsonb
|
|
)",
|
|
)
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
|
|
query(
|
|
"create unique index if not exists platform_api_keys_workspace_name_idx on platform_api_keys(workspace_id, name)",
|
|
)
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
query("alter table platform_api_keys add column if not exists agent_id text null")
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
query(
|
|
"alter table platform_api_keys add column if not exists key_kind text not null default 'mcp_client'",
|
|
)
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
query("alter table platform_api_keys add column if not exists expires_at timestamptz null")
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
query(
|
|
"alter table platform_api_keys add column if not exists allowed_origins_json jsonb not null default '[]'::jsonb",
|
|
)
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
|
|
query(
|
|
"insert into workspaces (
|
|
id,
|
|
slug,
|
|
display_name,
|
|
status,
|
|
settings_json,
|
|
created_at,
|
|
updated_at
|
|
) values (
|
|
'ws_default',
|
|
'default',
|
|
'Default Workspace',
|
|
'active',
|
|
'{}'::jsonb,
|
|
now(),
|
|
now()
|
|
)
|
|
on conflict (id) do nothing",
|
|
)
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
|
|
query(
|
|
"create table if not exists operations (
|
|
id text primary key,
|
|
workspace_id text null references workspaces(id) on delete cascade,
|
|
name text not null,
|
|
display_name text not null,
|
|
category text not null default 'general',
|
|
protocol text not null,
|
|
security_level text not null default 'standard',
|
|
status text not null,
|
|
current_draft_version integer not null default 1,
|
|
latest_published_version integer null,
|
|
created_at timestamptz not null,
|
|
updated_at timestamptz not null,
|
|
published_at timestamptz null
|
|
)",
|
|
)
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
|
|
query("alter table operations add column if not exists workspace_id text null references workspaces(id) on delete cascade")
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
query(
|
|
"alter table operations add column if not exists category text not null default 'general'",
|
|
)
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
query(
|
|
"alter table operations add column if not exists security_level text not null default 'standard'",
|
|
)
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
query("update operations set workspace_id = 'ws_default' where workspace_id is null")
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
query("alter table operations alter column workspace_id set not null")
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
query("alter table operations drop constraint if exists operations_name_key")
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
query(
|
|
"create unique index if not exists operations_workspace_name_idx on operations(workspace_id, name)",
|
|
)
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
|
|
query(
|
|
"create table if not exists operation_versions (
|
|
operation_id text not null references operations(id) on delete cascade,
|
|
version integer not null,
|
|
status text not null,
|
|
target_json jsonb not null,
|
|
input_schema_json jsonb not null,
|
|
output_schema_json jsonb not null,
|
|
input_mapping_json jsonb not null,
|
|
output_mapping_json jsonb not null,
|
|
execution_config_json jsonb not null,
|
|
tool_description_json jsonb not null,
|
|
samples_json jsonb null,
|
|
generated_draft_json jsonb null,
|
|
config_export_json jsonb null,
|
|
wizard_state_json jsonb null,
|
|
change_note text null,
|
|
created_at timestamptz not null,
|
|
created_by text null,
|
|
primary key (operation_id, version)
|
|
)",
|
|
)
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
|
|
query("alter table operation_versions add column if not exists wizard_state_json jsonb null")
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
|
|
query(
|
|
"create table if not exists published_operations (
|
|
operation_id text primary key references operations(id) on delete cascade,
|
|
version integer not null,
|
|
published_at timestamptz not null,
|
|
published_by text null,
|
|
foreign key (operation_id, version) references operation_versions(operation_id, version) on delete cascade
|
|
)",
|
|
)
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
|
|
query(
|
|
"create table if not exists operation_samples (
|
|
id text primary key,
|
|
operation_id text not null references operations(id) on delete cascade,
|
|
version integer not null,
|
|
sample_kind text not null,
|
|
storage_ref text not null,
|
|
content_type text not null,
|
|
file_name text null,
|
|
created_at timestamptz not null,
|
|
foreign key (operation_id, version) references operation_versions(operation_id, version) on delete cascade
|
|
)",
|
|
)
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
|
|
query(
|
|
"create table if not exists descriptors (
|
|
id text primary key,
|
|
operation_id text null references operations(id) on delete cascade,
|
|
version integer null,
|
|
descriptor_kind text not null,
|
|
storage_ref text not null,
|
|
source_name text null,
|
|
package_index_json jsonb null,
|
|
created_at timestamptz not null,
|
|
foreign key (operation_id, version) references operation_versions(operation_id, version) on delete cascade
|
|
)",
|
|
)
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
|
|
query(
|
|
"create table if not exists auth_profiles (
|
|
id text primary key,
|
|
workspace_id text null references workspaces(id) on delete cascade,
|
|
name text not null,
|
|
kind text not null,
|
|
config_json jsonb not null,
|
|
created_at timestamptz not null,
|
|
updated_at timestamptz not null
|
|
)",
|
|
)
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
|
|
query("alter table auth_profiles add column if not exists workspace_id text null references workspaces(id) on delete cascade")
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
query("update auth_profiles set workspace_id = 'ws_default' where workspace_id is null")
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
query("alter table auth_profiles alter column workspace_id set not null")
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
query("alter table auth_profiles drop constraint if exists auth_profiles_name_key")
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
query(
|
|
"create unique index if not exists auth_profiles_workspace_name_idx on auth_profiles(workspace_id, name)",
|
|
)
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
|
|
query(
|
|
"create table if not exists workspace_upstreams (
|
|
id text primary key,
|
|
workspace_id text not null references workspaces(id) on delete cascade,
|
|
name text not null,
|
|
base_url text not null,
|
|
static_headers_json jsonb not null default '{}'::jsonb,
|
|
auth_profile_id text null references auth_profiles(id) on delete set null,
|
|
created_at timestamptz not null,
|
|
updated_at timestamptz not null
|
|
)",
|
|
)
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
query(
|
|
"create unique index if not exists workspace_upstreams_workspace_name_idx on workspace_upstreams(workspace_id, name)",
|
|
)
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
query(
|
|
"create unique index if not exists workspace_upstreams_workspace_base_auth_idx on workspace_upstreams(workspace_id, base_url, coalesce(auth_profile_id, ''))",
|
|
)
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
query(
|
|
"insert into workspace_upstreams (
|
|
id,
|
|
workspace_id,
|
|
name,
|
|
base_url,
|
|
static_headers_json,
|
|
auth_profile_id,
|
|
created_at,
|
|
updated_at
|
|
)
|
|
select
|
|
'upstream_frankfurter_' || w.id,
|
|
w.id,
|
|
'Frankfurter',
|
|
'https://api.frankfurter.dev',
|
|
'{}'::jsonb,
|
|
null,
|
|
now(),
|
|
now()
|
|
from workspaces w
|
|
where not exists (
|
|
select 1
|
|
from workspace_upstreams wu
|
|
where wu.workspace_id = w.id
|
|
and wu.name = 'Frankfurter'
|
|
)",
|
|
)
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
|
|
query(
|
|
"create table if not exists secrets (
|
|
id text primary key,
|
|
workspace_id text not null references workspaces(id) on delete cascade,
|
|
name text not null,
|
|
kind text not null,
|
|
status text not null,
|
|
current_version integer not null,
|
|
last_used_at timestamptz null,
|
|
created_at timestamptz not null,
|
|
updated_at timestamptz not null
|
|
)",
|
|
)
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
|
|
query(
|
|
"create unique index if not exists secrets_workspace_name_idx on secrets(workspace_id, name)",
|
|
)
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
|
|
query(
|
|
"create table if not exists secret_versions (
|
|
secret_id text not null references secrets(id) on delete cascade,
|
|
version integer not null,
|
|
ciphertext text not null,
|
|
key_version text not null,
|
|
created_at timestamptz not null,
|
|
created_by text null references users(id) on delete set null,
|
|
primary key (secret_id, version)
|
|
)",
|
|
)
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
|
|
query(
|
|
"create table if not exists yaml_import_jobs (
|
|
id text primary key,
|
|
source_sample_id text null references operation_samples(id) on delete set null,
|
|
status text not null,
|
|
format_version text not null,
|
|
mode text not null,
|
|
result_operation_id text null references operations(id) on delete set null,
|
|
result_version integer null,
|
|
error_text text null,
|
|
created_at timestamptz not null,
|
|
finished_at timestamptz null
|
|
)",
|
|
)
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
|
|
query(
|
|
"create table if not exists import_jobs (
|
|
id text primary key,
|
|
workspace_id text not null references workspaces(id) on delete cascade,
|
|
kind text not null,
|
|
source_format text not null,
|
|
source_version text null,
|
|
status text not null,
|
|
preview_payload jsonb not null,
|
|
created_operation_ids jsonb not null default '[]'::jsonb,
|
|
error_text text null,
|
|
created_at timestamptz not null,
|
|
expires_at timestamptz not null,
|
|
finished_at timestamptz null
|
|
)",
|
|
)
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
|
|
query(
|
|
"create table if not exists agents (
|
|
id text primary key,
|
|
workspace_id text not null references workspaces(id) on delete cascade,
|
|
slug text not null,
|
|
display_name text not null,
|
|
description text not null,
|
|
status text not null,
|
|
current_draft_version integer not null default 1,
|
|
latest_published_version integer null,
|
|
created_at timestamptz not null,
|
|
updated_at timestamptz not null,
|
|
published_at timestamptz null
|
|
)",
|
|
)
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
|
|
query(
|
|
"create unique index if not exists agents_workspace_slug_idx on agents(workspace_id, slug)",
|
|
)
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
|
|
query(
|
|
"create table if not exists agent_versions (
|
|
agent_id text not null references agents(id) on delete cascade,
|
|
version integer not null,
|
|
status text not null,
|
|
instructions_json jsonb not null,
|
|
tool_selection_policy_json jsonb not null,
|
|
created_at timestamptz not null,
|
|
primary key (agent_id, version)
|
|
)",
|
|
)
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
|
|
query(
|
|
"create table if not exists agent_operation_bindings (
|
|
agent_id text not null references agents(id) on delete cascade,
|
|
agent_version integer not null,
|
|
operation_id text not null references operations(id) on delete cascade,
|
|
operation_version integer not null,
|
|
tool_name text not null,
|
|
tool_title text not null,
|
|
tool_description_override text null,
|
|
enabled boolean not null default true,
|
|
foreign key (agent_id, agent_version) references agent_versions(agent_id, version) on delete cascade,
|
|
foreign key (operation_id, operation_version) references operation_versions(operation_id, version) on delete cascade
|
|
)",
|
|
)
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
|
|
query(
|
|
"create unique index if not exists agent_bindings_tool_name_idx on agent_operation_bindings(agent_id, agent_version, tool_name)",
|
|
)
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
|
|
query(
|
|
"create table if not exists published_agents (
|
|
agent_id text primary key references agents(id) on delete cascade,
|
|
version integer not null,
|
|
published_at timestamptz not null,
|
|
published_by text null,
|
|
foreign key (agent_id, version) references agent_versions(agent_id, version) on delete cascade
|
|
)",
|
|
)
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
|
|
query(
|
|
"create table if not exists approval_requests (
|
|
id text primary key,
|
|
workspace_id text not null references workspaces(id) on delete cascade,
|
|
agent_id text not null references agents(id) on delete cascade,
|
|
operation_id text not null references operations(id) on delete cascade,
|
|
operation_version integer not null,
|
|
status text not null,
|
|
risk_level text not null,
|
|
request_payload_json jsonb not null,
|
|
response_payload_json jsonb null,
|
|
created_at timestamptz not null,
|
|
expires_at timestamptz not null,
|
|
decided_at timestamptz null,
|
|
decided_by_key_id text null references platform_api_keys(id) on delete set null,
|
|
decision_note text null
|
|
)",
|
|
)
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
|
|
query("alter table approval_requests add column if not exists execution_started_at timestamptz null")
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
query("alter table approval_requests add column if not exists execution_attempts integer not null default 0")
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
query("alter table approval_requests add column if not exists request_fingerprint text null")
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
query(
|
|
"create unique index if not exists approval_requests_pending_fingerprint_idx
|
|
on approval_requests(agent_id, operation_id, operation_version, request_fingerprint)
|
|
where status = 'pending' and request_fingerprint is not null",
|
|
)
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
query(
|
|
"create index if not exists approval_requests_agent_status_idx
|
|
on approval_requests(workspace_id, agent_id, status, expires_at)",
|
|
)
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
|
|
query(
|
|
"create table if not exists invocation_logs (
|
|
id text primary key,
|
|
workspace_id text not null references workspaces(id) on delete cascade,
|
|
agent_id text null references agents(id) on delete set null,
|
|
operation_id text not null references operations(id) on delete cascade,
|
|
source text not null,
|
|
level text not null,
|
|
status text not null,
|
|
tool_name text not null,
|
|
message text not null,
|
|
request_id text null,
|
|
status_code integer null,
|
|
duration_ms bigint not null,
|
|
error_kind text null,
|
|
request_preview_json jsonb not null,
|
|
response_preview_json jsonb not null,
|
|
created_at timestamptz not null
|
|
)",
|
|
)
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
|
|
query(
|
|
"create index if not exists invocation_logs_workspace_created_idx on invocation_logs(workspace_id, created_at desc)",
|
|
)
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
|
|
query(
|
|
"create index if not exists invocation_logs_workspace_operation_created_idx on invocation_logs(workspace_id, operation_id, created_at desc)",
|
|
)
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
|
|
query(
|
|
"create index if not exists invocation_logs_workspace_agent_created_idx on invocation_logs(workspace_id, agent_id, created_at desc)",
|
|
)
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
|
|
query(
|
|
"create table if not exists usage_rollups (
|
|
workspace_id text not null references workspaces(id) on delete cascade,
|
|
agent_id text null references agents(id) on delete cascade,
|
|
operation_id text null references operations(id) on delete cascade,
|
|
period text not null,
|
|
calls_total bigint not null,
|
|
calls_ok bigint not null,
|
|
calls_error bigint not null,
|
|
p50_ms bigint not null,
|
|
p95_ms bigint not null,
|
|
p99_ms bigint not null,
|
|
updated_at timestamptz not null
|
|
)",
|
|
)
|
|
.execute(&mut **transaction)
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|