use std::path::Path; use std::sync::{Arc, Mutex}; use std::time::Duration; use async_trait::async_trait; use flow::{CompiledFlowDefinition, FlowSourceKind, compile_flow_source}; use rusqlite::{ Connection, OpenFlags, OptionalExtension, TransactionBehavior, backup::Backup, params, }; use serde::{Deserialize, Serialize}; use uuid::Uuid; use worker_runtime::identity::{ LegacyWorkerIdentityMapping, RuntimeWorkerRef, WorkerId, legacy_worker_identity_mapping_digest, }; use crate::{Error, Result}; const WORKSPACES_V0_COLUMNS: &[&str] = &[ "workspace_id", "display_name", "state", "created_at", "updated_at", ]; const MIGRATIONS: &[Migration] = &[ Migration { version: 1, name: "workspace db canonical schema v0 bootstrap", apply: create_schema_v0_tables, }, Migration { version: 2, name: "align legacy workspace bootstrap with schema v0", apply: align_legacy_bootstrap_schema, }, Migration { version: 3, name: "backend worker workdir registry schema", apply: create_worker_workdir_registry_tables, }, Migration { version: 4, name: "remove durable worker lifecycle state", apply: remove_worker_registry_legacy_live_state_column, }, Migration { version: 5, name: "use composite worker registry keys", apply: use_composite_worker_registry_keys, }, Migration { version: 6, name: "add workdir runtime observation states", apply: add_workdir_runtime_observation_states, }, Migration { version: 7, name: "remove workdir registry management kind", apply: remove_workdir_registry_management_kind_column, }, Migration { version: 8, name: "account identity and login flow schema", apply: create_account_identity_tables, }, Migration { version: 9, name: "webauthn challenge state", apply: add_webauthn_challenge_state, }, Migration { version: 10, name: "sqlite objective authority and memory staging import", apply: create_objective_sqlite_authority_tables, }, Migration { version: 11, name: "sqlite memory authority documents and staging resolutions", apply: create_memory_authority_tables, }, Migration { version: 12, name: "trusted remote runtime registry", apply: create_trusted_runtime_registry_tables, }, Migration { version: 13, name: "objective mutation audit events", apply: create_objective_event_tables, }, Migration { version: 14, name: "remove unused control-plane Ticket tables", apply: remove_unused_control_plane_ticket_tables, }, Migration { version: 15, name: "separate workdir creation evidence from current revision observation", apply: add_workdir_revision_observations, }, Migration { version: 16, name: "ticket worker current assignment authority", apply: create_ticket_worker_assignment_tables, }, Migration { version: 17, name: "worker workspace credentials and Ticket notification outbox", apply: create_ticket_notification_tables, }, Migration { version: 18, name: "bidirectional idempotent Ticket Worker assignments", apply: strengthen_ticket_worker_assignments, }, Migration { version: 19, name: "atomic Ticket notification identity credentials and cursors", apply: strengthen_ticket_notifications, }, Migration { version: 20, name: "reconcile Workdir revision and crash safe Worker lifecycle reservations", apply: strengthen_ticket_assignment_lifecycle_reservations, }, Migration { version: 21, name: "remove per-Worker Workspace credentials", apply: remove_worker_workspace_credentials, }, Migration { version: 22, name: "drop Ticket notification outbox", apply: drop_ticket_notification_tables, }, Migration { version: 23, name: "enforce exclusive active Worker Workdir attachments", apply: enforce_exclusive_worker_workdir_attachments, }, Migration { version: 24, name: "create Worker Workdir attachment reservations", apply: create_worker_workdir_attachment_reservations, }, Migration { version: 25, name: "create Flow source authority", apply: create_flow_source_authority, }, Migration { version: 26, name: "remove Backend-owned Flow runtime authority", apply: remove_backend_flow_runtime_authority, }, Migration { version: 27, name: "scope Repository identity and references by Workspace", apply: scope_repository_identity_by_workspace, }, Migration { version: 28, name: "create Worker retention authority", apply: crate::retention::create_worker_retention_tables, }, Migration { version: 29, name: "create Worker mutation source proof replay guard", apply: create_worker_mutation_source_proof_replay_guard, }, Migration { version: 30, name: "create Workspace virtual config source authority", apply: create_workspace_config_source_authority, }, Migration { version: 31, name: "materialize required main.dcdl Workspace config entrypoint", apply: materialize_main_config_entrypoint, }, Migration { version: 32, name: "persist Workspace config schema contribution bundles", apply: persist_workspace_config_schema_bundles, }, Migration { version: 33, name: "create durable Runtime Worker control grants", apply: create_worker_control_grant_authority, }, Migration { version: 34, name: "create Worker control delegation operation authority", apply: create_worker_control_delegation_operation_authority, }, Migration { version: 35, name: "remove Worker control delegation authority", apply: remove_worker_control_delegation_authority, }, Migration { version: 36, name: "add Objective query indexes", apply: add_objective_query_indexes, }, Migration { version: 37, name: "promote Workspace Worker UUIDv7 identity", apply: apply_workspace_worker_uuid_identity_migration, }, Migration { version: 38, name: "add Workspace resource human keys", apply: add_workspace_resource_human_keys, }, Migration { version: 39, name: "enforce Workspace resource foreign keys", apply: enforce_workspace_resource_foreign_keys, }, Migration { version: 40, name: "create atomic Workspace catalog operations", apply: create_workspace_catalog_operations, }, ]; struct Migration { version: i64, name: &'static str, apply: fn(&Connection) -> Result<()>, } #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct WorkspaceStoreMigrationPlan { pub current_schema_version: i64, pub target_schema_version: i64, pub migration_required: bool, pub worker_count: usize, pub mapping_digest: String, pub mappings: Vec, pub repairs: Vec, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct WorkspaceRecord { pub workspace_id: String, /// Account/namespace owner abstraction. `None` is allowed for legacy/local /// workspaces until a user account is bootstrapped. pub owner_account_id: Option, pub display_name: String, pub state: String, pub created_at: String, pub updated_at: String, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct RepositoryRecord { pub workspace_id: String, pub repository_id: String, pub name: String, pub kind: String, pub provider: Option, pub uri: String, pub default_ref: Option, pub auth_ref_kind: Option, pub auth_ref_key: Option, pub created_at: String, pub updated_at: String, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct WorkspaceBootstrapRecord { pub operation_key: String, pub request_fingerprint: String, pub workspace: WorkspaceRecord, pub repository: RepositoryRecord, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct WorkspaceBootstrapResult { pub workspace: WorkspaceRecord, pub repository: RepositoryRecord, pub config_revision: u64, pub replayed: bool, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct TrustedRuntimeRecord { pub runtime_id: String, pub workspace_id: Option, pub display_name: String, pub base_url: String, pub public_key: String, pub created_at: String, pub updated_at: String, pub revoked_at: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct AccountRecord { pub account_id: String, pub kind: String, pub handle: String, pub display_name: String, pub created_at: String, pub updated_at: String, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct UserRecord { pub user_id: String, pub account_id: String, pub handle: String, pub display_name: String, pub created_at: String, pub updated_at: String, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct PasskeyCredentialRecord { pub credential_id: String, pub user_id: String, pub public_key_cose: String, pub transports_json: Option, pub sign_count: u64, pub created_at: String, pub last_used_at: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct AuthChallengeRecord { pub challenge_id: String, pub ceremony: String, pub challenge: String, pub user_id: Option, pub rp_id: String, pub origin: String, pub state_json: Option, pub expires_at: String, pub created_at: String, pub consumed_at: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct BrowserSessionRecord { pub session_id: String, pub token_hash: String, pub user_id: String, pub created_at: String, pub expires_at: String, pub revoked_at: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct ApiTokenRecord { pub token_id: String, pub token_hash: String, pub user_id: String, pub label: String, pub created_at: String, pub expires_at: Option, pub revoked_at: Option, pub last_used_at: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct DeviceLoginFlowRecord { pub device_code: String, pub user_code: String, pub verification_uri: String, pub client_name: Option, pub user_id: Option, pub api_token_id: Option, pub issued_access_token: Option, pub created_at: String, pub expires_at: String, pub approved_at: Option, pub consumed_at: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct WorkerRegistryRecord { pub workspace_id: String, pub worker: RuntimeWorkerRef, pub display_name: String, pub profile: Option, /// Retention state is explicit so `pinned` can be represented before prune exists. pub retention_state: String, pub transcript_ref: Option, pub session_ref: Option, pub summary_ref: Option, pub diagnostics_ref: Option, pub created_at: String, pub updated_at: String, } /// Durable authority describing which Runtime Worker another Runtime Worker may /// discover and control. Revoked grants remain as audit evidence but are never /// returned by active-grant queries. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct WorkerControlGrantRecord { pub workspace_id: String, pub grant_id: String, pub controller: RuntimeWorkerRef, pub subject: RuntimeWorkerRef, pub relation: String, pub origin: String, pub permissions: Vec, pub operation_id: String, pub created_at: String, pub revoked_at: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct TicketWorkerAssignmentRecord { pub workspace_id: String, pub ticket_id: String, pub assignment_id: String, pub worker: RuntimeWorkerRef, pub assigned_by: String, pub assigned_at: String, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct TicketWorkerAssignmentEventRecord { pub workspace_id: String, pub ticket_id: String, pub event_id: String, pub action: String, pub assignment_id: Option, pub previous_assignment_id: Option, pub actor: String, pub created_at: String, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct TicketWorkerAssignmentUpdate { pub current: TicketWorkerAssignmentRecord, pub previous: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct WorkdirRegistryRecord { pub workspace_id: String, pub workdir_id: String, pub runtime_id: String, pub repository_id: String, pub creation_selector: Option, pub creation_ref: Option, pub current_selector: Option, pub current_ref: Option, pub materialization_status: String, pub cleanliness: String, pub created_at: String, pub updated_at: String, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct WorkerWorkdirLinkRecord { pub workspace_id: String, pub worker: RuntimeWorkerRef, pub workdir_id: String, pub role: String, pub linked_at: String, pub unlinked_at: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct ObjectiveRecord { pub workspace_id: String, pub objective_id: String, pub title: String, pub state: String, pub body_md: String, pub created_at: String, pub updated_at: String, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct ObjectiveTicketLinkRecord { pub workspace_id: String, pub objective_id: String, pub ticket_id: String, pub kind: String, pub created_at: String, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct ObjectiveEventRecord { pub workspace_id: String, pub objective_id: String, pub event_id: String, pub kind: String, pub body_md: Option, pub created_at: String, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct ObjectiveResourceRecord { pub workspace_id: String, pub objective_id: String, pub resource_path: String, pub body: String, pub media_type: Option, pub created_at: String, pub updated_at: String, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct MemoryDocumentRecord { pub workspace_id: String, pub body_md: String, pub created_at: String, pub updated_at: String, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct MemoryStagingRecord { pub workspace_id: String, pub candidate_id: String, pub raw_json: String, pub source_path: Option, pub imported_at: String, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct MemoryStagingResolutionRecord { pub workspace_id: String, pub candidate_id: String, pub action: String, pub reason: String, pub affected_refs_json: String, pub staging_raw_json: String, pub source_path: Option, pub imported_at: String, pub resolved_at: String, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct FlowSourceRecord { pub workspace_id: String, pub flow_id: String, pub source_kind: FlowSourceKind, pub name: String, pub path: String, pub content: String, pub content_digest: String, pub revision: u64, pub created_at: String, pub updated_at: String, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct FlowSourceRevisionRecord { pub workspace_id: String, pub flow_id: String, pub revision: u64, pub content: String, pub content_digest: String, pub definition: CompiledFlowDefinition, pub created_at: String, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum WorkspaceResourceKind { Ticket, Objective, Worker, } impl WorkspaceResourceKind { fn as_str(self) -> &'static str { match self { Self::Ticket => "ticket", Self::Objective => "objective", Self::Worker => "worker", } } fn prefix(self) -> &'static str { match self { Self::Ticket => "T", Self::Objective => "O", Self::Worker => "W", } } } #[async_trait] pub trait ControlPlaneStore: Send + Sync { async fn schema_version(&self) -> Result; fn resource_human_key( &self, workspace_id: &str, kind: WorkspaceResourceKind, resource_id: &str, ) -> Result>; fn resolve_resource_reference( &self, workspace_id: &str, kind: WorkspaceResourceKind, reference: &str, ) -> Result>; async fn upsert_workspace(&self, record: &WorkspaceRecord) -> Result<()>; async fn get_workspace(&self, workspace_id: &str) -> Result>; fn create_workspace_bootstrap( &self, record: &WorkspaceBootstrapRecord, ) -> Result; async fn get_trusted_runtime(&self, runtime_id: &str) -> Result>; async fn consume_worker_mutation_source_jti( &self, runtime_id: &str, jti: &str, expires_at: u64, now_seconds: u64, consumed_at: &str, ) -> Result; fn plan_worker_removal( &self, request: &crate::retention::WorkerRemovalPlanRequest, inventory: &worker_runtime::retention::WorkerRetentionInventory, ) -> std::result::Result< crate::retention::WorkerRemovalPlan, crate::retention::WorkerRetentionError, > { let _ = (request, inventory); Err(crate::retention::WorkerRetentionError::Invalid( "Worker retention authority is unavailable".to_string(), )) } fn prepare_worker_removal_execution( &self, workspace_id: &str, plan_id: &str, input_fingerprint: &str, ) -> std::result::Result< crate::retention::PreparedWorkerRemoval, crate::retention::WorkerRetentionError, > { let _ = (workspace_id, plan_id, input_fingerprint); Err(crate::retention::WorkerRetentionError::Invalid( "Worker retention authority is unavailable".to_string(), )) } fn recover_worker_removal_execution( &self, workspace_id: &str, worker: &RuntimeWorkerRef, expected_worker_revision: &str, reason: &str, ) -> std::result::Result< Option, crate::retention::WorkerRetentionError, > { let _ = (workspace_id, worker, expected_worker_revision, reason); Ok(None) } fn fail_worker_removal( &self, workspace_id: &str, operation_id: &str, input_fingerprint: &str, category: &str, ) -> std::result::Result<(), crate::retention::WorkerRetentionError> { let _ = (workspace_id, operation_id, input_fingerprint, category); Err(crate::retention::WorkerRetentionError::Invalid( "Worker retention authority is unavailable".to_string(), )) } fn commit_worker_removal( &self, workspace_id: &str, operation_id: &str, input_fingerprint: &str, result: &worker_runtime::retention::WorkerRetentionExecutionResult, ) -> std::result::Result< crate::retention::WorkerRemovalPlan, crate::retention::WorkerRetentionError, > { let _ = (workspace_id, operation_id, input_fingerprint, result); Err(crate::retention::WorkerRetentionError::Invalid( "Worker retention authority is unavailable".to_string(), )) } fn list_workspaces(&self) -> Result>; fn upsert_repository(&self, record: &RepositoryRecord) -> Result<()>; fn get_repository( &self, workspace_id: &str, repository_id: &str, ) -> Result>; fn list_repositories(&self, workspace_id: &str) -> Result>; fn put_flow_source_for_kind( &self, workspace_id: &str, source_kind: FlowSourceKind, path: &str, content: &str, now: &str, ) -> Result; fn get_flow_source_by_name( &self, workspace_id: &str, source_kind: FlowSourceKind, name: &str, ) -> Result>; fn list_flow_sources(&self, workspace_id: &str) -> Result>; fn get_flow_source( &self, workspace_id: &str, flow_id: &str, ) -> Result>; fn get_flow_source_revision( &self, workspace_id: &str, flow_id: &str, revision: u64, ) -> Result>; fn upsert_objective(&self, record: &ObjectiveRecord) -> Result<()>; fn list_objectives(&self, workspace_id: &str, limit: usize) -> Result>; fn list_objectives_for_ticket( &self, workspace_id: &str, ticket_id: &str, limit: usize, ) -> Result>; fn get_objective( &self, workspace_id: &str, objective_id: &str, ) -> Result>; fn replace_objective_ticket_links( &self, workspace_id: &str, objective_id: &str, links: &[ObjectiveTicketLinkRecord], ) -> Result<()>; fn list_objective_ticket_links( &self, workspace_id: &str, objective_id: &str, ) -> Result>; fn insert_objective_event(&self, record: &ObjectiveEventRecord) -> Result<()>; fn list_objective_events( &self, workspace_id: &str, objective_id: &str, ) -> Result>; fn upsert_objective_resource(&self, record: &ObjectiveResourceRecord) -> Result<()>; fn list_objective_resources( &self, workspace_id: &str, objective_id: &str, ) -> Result>; fn ensure_memory_document( &self, workspace_id: &str, default_body_md: &str, now: &str, ) -> Result; fn get_memory_document(&self, workspace_id: &str) -> Result>; fn upsert_memory_document(&self, record: &MemoryDocumentRecord) -> Result<()>; fn upsert_memory_staging_record(&self, record: &MemoryStagingRecord) -> Result<()>; fn list_memory_staging_records( &self, workspace_id: &str, limit: usize, ) -> Result>; fn get_memory_staging_record( &self, workspace_id: &str, candidate_id: &str, ) -> Result>; fn delete_memory_staging_record(&self, workspace_id: &str, candidate_id: &str) -> Result; fn count_memory_staging_records(&self, workspace_id: &str) -> Result; fn insert_memory_staging_resolution( &self, record: &MemoryStagingResolutionRecord, ) -> Result<()>; fn list_memory_staging_resolutions( &self, workspace_id: &str, limit: usize, ) -> Result>; fn upsert_account(&self, record: &AccountRecord) -> Result<()>; fn get_account(&self, account_id: &str) -> Result>; fn get_account_by_handle(&self, kind: &str, handle: &str) -> Result>; fn upsert_user(&self, record: &UserRecord) -> Result<()>; fn get_user(&self, user_id: &str) -> Result>; fn get_user_by_handle(&self, handle: &str) -> Result>; fn any_user(&self) -> Result>; fn upsert_passkey_credential(&self, record: &PasskeyCredentialRecord) -> Result<()>; fn get_passkey_credential( &self, credential_id: &str, ) -> Result>; fn list_passkey_credentials_for_user( &self, user_id: &str, ) -> Result>; fn put_auth_challenge(&self, record: &AuthChallengeRecord) -> Result<()>; fn consume_auth_challenge( &self, challenge: &str, ceremony: &str, consumed_at: &str, ) -> Result>; fn consume_auth_challenge_by_id( &self, challenge_id: &str, ceremony: &str, consumed_at: &str, ) -> Result>; fn create_browser_session(&self, record: &BrowserSessionRecord) -> Result<()>; fn resolve_browser_session(&self, token_hash: &str) -> Result>; fn revoke_browser_session(&self, token_hash: &str, revoked_at: &str) -> Result; fn create_api_token(&self, record: &ApiTokenRecord) -> Result<()>; fn resolve_api_token(&self, token_hash: &str) -> Result>; fn mark_api_token_used(&self, token_hash: &str, used_at: &str) -> Result<()>; fn create_device_login_flow(&self, record: &DeviceLoginFlowRecord) -> Result<()>; fn get_device_login_flow_by_user_code( &self, user_code: &str, ) -> Result>; fn get_device_login_flow_by_device_code( &self, device_code: &str, ) -> Result>; fn approve_device_login_flow( &self, device_code: &str, user_id: &str, api_token_id: &str, issued_access_token: &str, approved_at: &str, ) -> Result; fn consume_device_login_token( &self, device_code: &str, consumed_at: &str, ) -> Result>; fn upsert_worker_registry(&self, record: &WorkerRegistryRecord) -> Result<()>; fn get_worker_registry( &self, workspace_id: &str, worker: &RuntimeWorkerRef, ) -> Result>; fn list_worker_registry( &self, workspace_id: &str, limit: usize, ) -> Result>; fn update_worker_retention( &self, workspace_id: &str, worker: &RuntimeWorkerRef, retention_state: &str, updated_at: &str, ) -> Result; fn delete_worker_registry(&self, workspace_id: &str, worker: &RuntimeWorkerRef) -> Result; fn create_worker_control_grant( &self, record: &WorkerControlGrantRecord, ) -> Result; fn get_worker_control_grant( &self, workspace_id: &str, grant_id: &str, ) -> Result>; fn get_worker_control_grant_by_operation( &self, workspace_id: &str, controller: &RuntimeWorkerRef, operation_id: &str, ) -> Result>; fn get_active_worker_control_grant( &self, workspace_id: &str, controller: &RuntimeWorkerRef, subject: &RuntimeWorkerRef, ) -> Result>; fn list_active_worker_control_grants( &self, workspace_id: &str, controller: &RuntimeWorkerRef, limit: usize, ) -> Result>; fn revoke_worker_control_grant( &self, workspace_id: &str, grant_id: &str, revoked_at: &str, ) -> Result; fn get_ticket_assignment_operation( &self, workspace_id: &str, operation_id: &str, ) -> Result>; fn reserve_ticket_assignment_operation( &self, workspace_id: &str, operation_id: &str, ticket_id: &str, runtime_id: &str, worker_id: Option<&str>, request_fingerprint: &str, created_at: &str, ) -> Result<()>; fn bind_ticket_assignment_operation_worker( &self, workspace_id: &str, operation_id: &str, worker_id: &str, ) -> Result<()>; fn rollback_ticket_assignment_operation( &self, workspace_id: &str, operation_id: &str, ) -> Result<()>; fn get_current_ticket_worker_assignment( &self, workspace_id: &str, ticket_id: &str, ) -> Result>; fn set_current_ticket_worker_assignment( &self, record: &TicketWorkerAssignmentRecord, expected_assignment_id: Option<&str>, event_id: &str, operation_id: &str, allow_reassign: bool, ) -> Result; fn clear_current_ticket_worker_assignment( &self, workspace_id: &str, ticket_id: &str, expected_assignment_id: Option<&str>, operation_id: &str, event_id: &str, actor: &str, created_at: &str, ) -> Result>; fn list_ticket_worker_assignment_events( &self, workspace_id: &str, ticket_id: &str, limit: usize, ) -> Result>; fn upsert_workdir_registry(&self, record: &WorkdirRegistryRecord) -> Result<()>; fn get_workdir_registry( &self, workspace_id: &str, workdir_id: &str, ) -> Result>; fn list_workdir_registry( &self, workspace_id: &str, limit: usize, ) -> Result>; fn delete_workdir_registry(&self, workspace_id: &str, workdir_id: &str) -> Result; fn reserve_worker_workdir_attachment( &self, workspace_id: &str, workdir_id: &str, reservation_id: &str, reserved_at: &str, ) -> Result<()>; fn release_worker_workdir_attachment_reservation( &self, workspace_id: &str, workdir_id: &str, reservation_id: &str, ) -> Result<()>; fn finalize_reserved_worker_workdir_attachment( &self, record: &WorkerWorkdirLinkRecord, reservation_id: &str, ) -> Result; fn attach_worker_workdir( &self, record: &WorkerWorkdirLinkRecord, ) -> Result; fn detach_worker_workdir( &self, workspace_id: &str, worker: &RuntimeWorkerRef, expected_workdir_id: Option<&str>, unlinked_at: &str, ) -> Result>; fn worker_workdir_link_history_exists( &self, workspace_id: &str, worker: &RuntimeWorkerRef, ) -> Result; fn list_worker_workdir_links( &self, workspace_id: &str, worker: &RuntimeWorkerRef, ) -> Result>; fn list_workdir_worker_links( &self, workspace_id: &str, workdir_id: &str, ) -> Result>; } #[derive(Clone)] pub struct SqliteWorkspaceStore { conn: Arc>, } impl SqliteWorkspaceStore { pub fn migration_plan(path: impl AsRef) -> Result { let path = path.as_ref(); let source = Connection::open_with_flags( path, OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_URI, )?; let current_schema_version = current_schema_version(&source)?; let target_schema_version = MIGRATIONS .last() .map(|migration| i64::from(migration.version)) .unwrap_or(current_schema_version); let mut repairs = Vec::new(); if current_schema_version < 37 && !table_exists(&source, "worker_diagnostics_archives")? { repairs.push("create missing worker_diagnostics_archives table".to_string()); } let mut candidate = Connection::open_in_memory()?; { let backup = Backup::new(&source, &mut candidate)?; backup.run_to_completion(5, Duration::from_millis(10), None)?; } configure_sqlite(&candidate)?; let mappings = if current_schema_version < 37 { apply_migrations_through(&candidate, 36)?; let tx = candidate.unchecked_transaction()?; crate::retention::repair_worker_diagnostics_archive_table(&tx)?; let mappings = promote_workspace_worker_uuid_identity(&tx)?; tx.execute( "INSERT INTO __yoi_schema_migrations (version, name) VALUES (37, ?1)", ["promote Workspace Worker UUIDv7 identity"], )?; tx.commit()?; mappings } else { Vec::new() }; apply_migrations_through(&candidate, i64::MAX)?; ticket::migrate_sqlite_ticket_schema(&candidate)?; merge_request::migrate(&candidate).map_err(|error| Error::Store(error.to_string()))?; validate_workspace_resource_references(&candidate)?; let foreign_key_failures: i64 = candidate.query_row("SELECT COUNT(*) FROM pragma_foreign_key_check", [], |row| { row.get(0) })?; if foreign_key_failures != 0 { return Err(Error::Store(format!( "migration dry-run found {foreign_key_failures} foreign key violation(s)" ))); } Ok(WorkspaceStoreMigrationPlan { current_schema_version, target_schema_version, migration_required: current_schema_version < target_schema_version, worker_count: mappings.len(), mapping_digest: legacy_worker_identity_mapping_digest(&mappings), mappings, repairs, }) } pub fn open(path: impl AsRef) -> Result { let conn = Connection::open(path)?; Self::from_connection(conn) } pub fn in_memory() -> Result { Self::from_connection(Connection::open_in_memory()?) } pub fn from_connection(conn: Connection) -> Result { configure_sqlite(&conn)?; apply_migrations(&conn) .map_err(|error| Error::Store(format!("workspace schema migration failed: {error}")))?; ticket::migrate_sqlite_ticket_schema(&conn) .map_err(|error| Error::Store(format!("Ticket schema verification failed: {error}")))?; merge_request::migrate(&conn).map_err(|error| { Error::Store(format!("Merge Request schema verification failed: {error}")) })?; validate_workspace_resource_references(&conn)?; verify_workspace_resource_constraints(&conn)?; Ok(Self { conn: Arc::new(Mutex::new(conn)), }) } pub(crate) fn with_conn(&self, f: impl FnOnce(&Connection) -> Result) -> Result { let conn = self .conn .lock() .map_err(|_| Error::Store("sqlite connection lock poisoned".to_string()))?; f(&conn) } pub(crate) fn with_conn_mut( &self, f: impl FnOnce(&mut Connection) -> Result, ) -> Result { let mut conn = self .conn .lock() .map_err(|_| Error::Store("sqlite connection lock poisoned".to_string()))?; f(&mut conn) } pub(crate) fn reserve_worker_create( &self, workspace_id: &str, runtime_id: &str, allocation_key: &str, create_fingerprint: &str, ) -> Result { if allocation_key.trim().is_empty() || create_fingerprint.trim().is_empty() { return Err(Error::InvalidInput( "Worker create allocation key and fingerprint must be non-empty".to_string(), )); } self.with_conn_mut(|conn| { let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?; let existing = tx .query_row( "SELECT worker_id, runtime_id, create_fingerprint \ FROM worker_create_reservations \ WHERE workspace_id = ?1 AND allocation_key = ?2", params![workspace_id, allocation_key], |row| { Ok(( row.get::<_, String>(0)?, row.get::<_, String>(1)?, row.get::<_, String>(2)?, )) }, ) .optional()?; if let Some((worker_id, reserved_runtime_id, reserved_fingerprint)) = existing { if reserved_runtime_id != runtime_id || reserved_fingerprint != create_fingerprint { return Err(Error::InvalidInput(format!( "Worker create allocation `{allocation_key}` was already used with different input" ))); } return worker_id.parse::().map_err(|_| { Error::Store(format!( "Worker create allocation `{allocation_key}` has a non-UUIDv7 worker id" )) }); } let worker_id = WorkerId::now_v7(); let now = chrono::Utc::now().to_rfc3339(); allocate_resource_human_key( &tx, workspace_id, WorkspaceResourceKind::Worker, &worker_id.to_string(), &now, )?; tx.execute( "INSERT INTO worker_create_reservations(\ workspace_id, allocation_key, worker_id, runtime_id, create_fingerprint,\ state, created_at, updated_at\ ) VALUES (?1, ?2, ?3, ?4, ?5, 'reserved', ?6, ?6)", params![ workspace_id, allocation_key, worker_id.to_string(), runtime_id, create_fingerprint, now ], )?; tx.commit()?; Ok(worker_id) }) } pub(crate) fn complete_worker_create_reservation( &self, workspace_id: &str, worker_id: WorkerId, ) -> Result<()> { self.with_conn(|conn| { let changed = conn.execute( "UPDATE worker_create_reservations \ SET state = 'created', updated_at = ?3 \ WHERE workspace_id = ?1 AND worker_id = ?2", params![ workspace_id, worker_id.to_string(), chrono::Utc::now().to_rfc3339() ], )?; if changed != 1 { return Err(Error::Store(format!( "Worker create reservation {} was not found", worker_id ))); } Ok(()) }) } fn materialize_workspace_config(&self, workspace_id: &str, created_at: &str) -> Result<()> { self.with_conn_mut(|conn| { let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?; if crate::config_source::load_state(&tx, workspace_id)?.is_none() { let state = crate::config_source::initial_state()?; crate::config_source::insert_materialized_state( &tx, workspace_id, &state, created_at, )?; } tx.commit()?; Ok(()) }) } pub fn upsert_trusted_runtime(&self, record: &TrustedRuntimeRecord) -> Result<()> { self.with_conn(|conn| { conn.execute( r#"INSERT INTO trusted_runtime_records ( runtime_id, workspace_id, display_name, base_url, public_key, created_at, updated_at, revoked_at ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8) ON CONFLICT(runtime_id) DO UPDATE SET display_name = excluded.display_name, base_url = excluded.base_url, public_key = excluded.public_key, updated_at = excluded.updated_at, revoked_at = excluded.revoked_at"#, params![ record.runtime_id, record.workspace_id, record.display_name, record.base_url, record.public_key, record.created_at, record.updated_at, record.revoked_at, ], )?; Ok(()) }) } pub fn list_trusted_runtimes( &self, include_revoked: bool, ) -> Result> { self.with_conn(|conn| { let sql = if include_revoked { r#"SELECT runtime_id, workspace_id, display_name, base_url, public_key, created_at, updated_at, revoked_at FROM trusted_runtime_records ORDER BY runtime_id ASC"# } else { r#"SELECT runtime_id, workspace_id, display_name, base_url, public_key, created_at, updated_at, revoked_at FROM trusted_runtime_records WHERE revoked_at IS NULL ORDER BY runtime_id ASC"# }; let mut stmt = conn.prepare(sql)?; let rows = stmt.query_map([], read_trusted_runtime_record)?; rows.collect::, _>>().map_err(Error::from) }) } pub fn revoke_trusted_runtime(&self, runtime_id: &str, revoked_at: &str) -> Result { self.with_conn(|conn| { let changed = conn.execute( r#"UPDATE trusted_runtime_records SET revoked_at = ?2, updated_at = ?2 WHERE runtime_id = ?1 AND revoked_at IS NULL"#, params![runtime_id, revoked_at], )?; Ok(changed > 0) }) } } #[async_trait] impl ControlPlaneStore for SqliteWorkspaceStore { async fn schema_version(&self) -> Result { self.with_conn(current_schema_version) } fn resource_human_key( &self, workspace_id: &str, kind: WorkspaceResourceKind, resource_id: &str, ) -> Result> { self.with_conn(|conn| { conn.query_row( "SELECT human_key FROM workspace_resource_human_keys WHERE workspace_id = ?1 AND resource_kind = ?2 AND resource_id = ?3", params![workspace_id, kind.as_str(), resource_id], |row| row.get(0), ) .optional() .map_err(Error::from) }) } fn resolve_resource_reference( &self, workspace_id: &str, kind: WorkspaceResourceKind, reference: &str, ) -> Result> { self.with_conn(|conn| { if let Some(resource_id) = conn .query_row( "SELECT resource_id FROM workspace_resource_human_keys WHERE workspace_id = ?1 AND resource_kind = ?2 AND human_key = ?3", params![workspace_id, kind.as_str(), reference], |row| row.get(0), ) .optional()? { return Ok(Some(resource_id)); } let exists = match kind { WorkspaceResourceKind::Ticket => conn.query_row( "SELECT EXISTS(SELECT 1 FROM typed_tickets WHERE workspace_id = ?1 AND ticket_id = ?2)", params![workspace_id, reference], |row| row.get::<_, i64>(0), )?, WorkspaceResourceKind::Objective => conn.query_row( "SELECT EXISTS(SELECT 1 FROM objectives WHERE workspace_id = ?1 AND objective_id = ?2)", params![workspace_id, reference], |row| row.get::<_, i64>(0), )?, WorkspaceResourceKind::Worker => conn.query_row( "SELECT EXISTS(SELECT 1 FROM worker_registry WHERE workspace_id = ?1 AND worker_id = ?2)", params![workspace_id, reference], |row| row.get::<_, i64>(0), )?, }; Ok((exists != 0).then(|| reference.to_string())) }) } async fn upsert_workspace(&self, record: &WorkspaceRecord) -> Result<()> { self.with_conn(|conn| { conn.execute( r#"INSERT INTO workspaces ( workspace_id, owner_account_id, display_name, state, created_at, updated_at ) VALUES (?1, ?2, ?3, ?4, ?5, ?6) ON CONFLICT(workspace_id) DO UPDATE SET owner_account_id = COALESCE(excluded.owner_account_id, workspaces.owner_account_id), display_name = excluded.display_name, state = excluded.state, updated_at = excluded.updated_at"#, params![ record.workspace_id, record.owner_account_id, record.display_name, record.state, record.created_at, record.updated_at, ], )?; Ok(()) })?; self.materialize_workspace_config(&record.workspace_id, &record.created_at) } async fn get_workspace(&self, workspace_id: &str) -> Result> { self.with_conn(|conn| { conn.query_row( r#"SELECT workspace_id, owner_account_id, display_name, state, created_at, updated_at FROM workspaces WHERE workspace_id = ?1"#, params![workspace_id], read_workspace_record, ) .optional() .map_err(Error::from) }) } fn create_workspace_bootstrap( &self, record: &WorkspaceBootstrapRecord, ) -> Result { self.with_conn_mut(|conn| { let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?; if let Some((fingerprint, workspace_id)) = tx .query_row( "SELECT request_fingerprint, workspace_id FROM workspace_create_operations WHERE operation_key = ?1", params![record.operation_key], |row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)), ) .optional()? { if fingerprint != record.request_fingerprint { return Err(Error::WorkspaceConfigConflict( "Workspace create operation key was already used with different input" .to_string(), )); } let workspace = tx.query_row( r#"SELECT workspace_id, owner_account_id, display_name, state, created_at, updated_at FROM workspaces WHERE workspace_id = ?1"#, params![workspace_id], read_workspace_record, )?; let repository = tx.query_row( r#"SELECT workspace_id, repository_id, name, kind, provider, uri, default_ref, auth_ref_kind, auth_ref_key, created_at, updated_at FROM repositories WHERE workspace_id = ?1 AND repository_id = ?2"#, params![workspace.workspace_id, record.repository.repository_id], read_repository_record, )?; let config_revision = crate::config_source::load_state(&tx, &workspace.workspace_id)? .ok_or_else(|| Error::Store("Workspace config is missing".to_string()))? .snapshot .revision; tx.commit()?; return Ok(WorkspaceBootstrapResult { workspace, repository, config_revision, replayed: true, }); } if let Some(existing) = tx .query_row( r#"SELECT workspace_id, owner_account_id, display_name, state, created_at, updated_at FROM workspaces WHERE workspace_id = ?1"#, params![record.workspace.workspace_id], read_workspace_record, ) .optional()? { if existing.owner_account_id != record.workspace.owner_account_id || existing.display_name != record.workspace.display_name || existing.state != record.workspace.state { return Err(Error::WorkspaceConfigConflict( "Workspace identity already exists with different metadata".to_string(), )); } let existing_repository = tx .query_row( r#"SELECT workspace_id, repository_id, name, kind, provider, uri, default_ref, auth_ref_kind, auth_ref_key, created_at, updated_at FROM repositories WHERE workspace_id = ?1 AND repository_id = ?2"#, params![record.repository.workspace_id, record.repository.repository_id], read_repository_record, ) .optional()?; if existing_repository.as_ref() != Some(&record.repository) { return Err(Error::WorkspaceConfigConflict( "Workspace initial repository already exists with different metadata" .to_string(), )); } } else { tx.execute( r#"INSERT INTO workspaces ( workspace_id, owner_account_id, display_name, state, created_at, updated_at ) VALUES (?1, ?2, ?3, ?4, ?5, ?6)"#, params![ record.workspace.workspace_id, record.workspace.owner_account_id, record.workspace.display_name, record.workspace.state, record.workspace.created_at, record.workspace.updated_at, ], )?; tx.execute( r#"INSERT INTO repositories ( workspace_id, repository_id, name, kind, provider, uri, default_ref, auth_ref_kind, auth_ref_key, created_at, updated_at ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)"#, params![ record.repository.workspace_id, record.repository.repository_id, record.repository.name, record.repository.kind, record.repository.provider, record.repository.uri, record.repository.default_ref, record.repository.auth_ref_kind, record.repository.auth_ref_key, record.repository.created_at, record.repository.updated_at, ], )?; } if crate::config_source::load_state(&tx, &record.workspace.workspace_id)?.is_none() { let state = crate::config_source::initial_state()?; crate::config_source::insert_materialized_state( &tx, &record.workspace.workspace_id, &state, &record.workspace.created_at, )?; } for resource_kind in ["ticket", "objective", "worker"] { tx.execute( r#"INSERT OR IGNORE INTO workspace_resource_human_key_counters ( workspace_id, resource_kind, next_sequence ) VALUES (?1, ?2, 1)"#, params![record.workspace.workspace_id, resource_kind], )?; } let config_revision = crate::config_source::load_state( &tx, &record.workspace.workspace_id, )? .ok_or_else(|| Error::Store("Workspace config is missing".to_string()))? .snapshot .revision; tx.execute( r#"INSERT INTO workspace_create_operations ( operation_key, request_fingerprint, workspace_id, created_at ) VALUES (?1, ?2, ?3, ?4)"#, params![ record.operation_key, record.request_fingerprint, record.workspace.workspace_id, record.workspace.created_at, ], )?; tx.commit()?; Ok(WorkspaceBootstrapResult { workspace: record.workspace.clone(), repository: record.repository.clone(), config_revision, replayed: false, }) }) } async fn get_trusted_runtime(&self, runtime_id: &str) -> Result> { self.with_conn(|conn| { conn.query_row( r#"SELECT runtime_id, workspace_id, display_name, base_url, public_key, created_at, updated_at, revoked_at FROM trusted_runtime_records WHERE runtime_id = ?1"#, params![runtime_id], read_trusted_runtime_record, ) .optional() .map_err(Error::from) }) } async fn consume_worker_mutation_source_jti( &self, runtime_id: &str, jti: &str, expires_at: u64, now_seconds: u64, consumed_at: &str, ) -> Result { self.with_conn(|conn| { let transaction = conn.unchecked_transaction()?; transaction.execute( "DELETE FROM worker_mutation_source_proof_jtis WHERE expires_at < ?1", params![now_seconds], )?; let inserted = transaction.execute( r#"INSERT OR IGNORE INTO worker_mutation_source_proof_jtis ( runtime_id, jti, expires_at, consumed_at ) VALUES (?1, ?2, ?3, ?4)"#, params![runtime_id, jti, expires_at, consumed_at], )?; transaction.commit()?; Ok(inserted == 1) }) } fn plan_worker_removal( &self, request: &crate::retention::WorkerRemovalPlanRequest, inventory: &worker_runtime::retention::WorkerRetentionInventory, ) -> std::result::Result< crate::retention::WorkerRemovalPlan, crate::retention::WorkerRetentionError, > { SqliteWorkspaceStore::plan_worker_removal(self, request, inventory) } fn prepare_worker_removal_execution( &self, workspace_id: &str, plan_id: &str, input_fingerprint: &str, ) -> std::result::Result< crate::retention::PreparedWorkerRemoval, crate::retention::WorkerRetentionError, > { SqliteWorkspaceStore::prepare_worker_removal_execution( self, workspace_id, plan_id, input_fingerprint, ) } fn recover_worker_removal_execution( &self, workspace_id: &str, worker: &RuntimeWorkerRef, expected_worker_revision: &str, reason: &str, ) -> std::result::Result< Option, crate::retention::WorkerRetentionError, > { SqliteWorkspaceStore::recover_worker_removal_execution( self, workspace_id, worker, expected_worker_revision, reason, ) } fn fail_worker_removal( &self, workspace_id: &str, operation_id: &str, input_fingerprint: &str, category: &str, ) -> std::result::Result<(), crate::retention::WorkerRetentionError> { SqliteWorkspaceStore::fail_worker_removal( self, workspace_id, operation_id, input_fingerprint, category, ) } fn commit_worker_removal( &self, workspace_id: &str, operation_id: &str, input_fingerprint: &str, result: &worker_runtime::retention::WorkerRetentionExecutionResult, ) -> std::result::Result< crate::retention::WorkerRemovalPlan, crate::retention::WorkerRetentionError, > { SqliteWorkspaceStore::commit_worker_removal( self, workspace_id, operation_id, input_fingerprint, result, ) } fn list_workspaces(&self) -> Result> { self.with_conn(|conn| { let mut stmt = conn.prepare( r#"SELECT workspace_id, owner_account_id, display_name, state, created_at, updated_at FROM workspaces ORDER BY workspace_id ASC"#, )?; let rows = stmt.query_map([], read_workspace_record)?; rows.collect::, _>>() .map_err(Error::from) }) } fn upsert_repository(&self, record: &RepositoryRecord) -> Result<()> { self.with_conn(|conn| { conn.execute( r#"INSERT INTO repositories ( workspace_id, repository_id, name, kind, provider, uri, default_ref, auth_ref_kind, auth_ref_key, created_at, updated_at ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11) ON CONFLICT(workspace_id, repository_id) DO UPDATE SET name = excluded.name, kind = excluded.kind, provider = excluded.provider, uri = excluded.uri, default_ref = excluded.default_ref, auth_ref_kind = excluded.auth_ref_kind, auth_ref_key = excluded.auth_ref_key, updated_at = excluded.updated_at"#, params![ record.workspace_id, record.repository_id, record.name, record.kind, record.provider, record.uri, record.default_ref, record.auth_ref_kind, record.auth_ref_key, record.created_at, record.updated_at, ], )?; Ok(()) }) } fn get_repository( &self, workspace_id: &str, repository_id: &str, ) -> Result> { self.with_conn(|conn| { conn.query_row( r#"SELECT workspace_id, repository_id, name, kind, provider, uri, default_ref, auth_ref_kind, auth_ref_key, created_at, updated_at FROM repositories WHERE workspace_id = ?1 AND repository_id = ?2"#, params![workspace_id, repository_id], read_repository_record, ) .optional() .map_err(Error::from) }) } fn list_repositories(&self, workspace_id: &str) -> Result> { self.with_conn(|conn| { let mut stmt = conn.prepare( r#"SELECT workspace_id, repository_id, name, kind, provider, uri, default_ref, auth_ref_kind, auth_ref_key, created_at, updated_at FROM repositories WHERE workspace_id = ?1 ORDER BY repository_id ASC"#, )?; let rows = stmt.query_map(params![workspace_id], read_repository_record)?; rows.collect::, _>>() .map_err(Error::from) }) } fn put_flow_source_for_kind( &self, workspace_id: &str, source_kind: FlowSourceKind, path: &str, content: &str, now: &str, ) -> Result { if source_kind != FlowSourceKind::Workspace { return Err(Error::Store( "built-in Flow sources are resource authority and cannot be written to Workspace DB" .to_string(), )); } let name = flow_source_name(path)?; let definition = compile_flow_source(content).map_err(|error| { Error::Store(format!( "compile Flow source {path:?}: {:?}", error.diagnostics )) })?; if definition.name != name { return Err(Error::Store(format!( "Flow source name {:?} does not match path slug {name:?}", definition.name ))); } self.with_conn(|conn| { let tx = conn.unchecked_transaction()?; let existing = tx .query_row( r#"SELECT workspace_id, flow_id, source_kind, name, path, content, content_digest, revision, created_at, updated_at FROM flow_sources WHERE workspace_id = ?1 AND source_kind = ?2 AND name = ?3"#, params![workspace_id, source_kind.as_str(), name], read_flow_source_record, ) .optional()?; if let Some(existing) = existing { if existing.content_digest == definition.content_digest { tx.commit()?; return Ok(existing); } let revision = existing .revision .checked_add(1) .ok_or_else(|| Error::Store("Flow source revision overflowed".to_string()))?; let definition_json = serde_json::to_string(&definition) .map_err(|error| Error::Store(error.to_string()))?; tx.execute( r#"INSERT INTO flow_source_revisions ( workspace_id, flow_id, revision, content, content_digest, definition_json, created_at ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)"#, params![ workspace_id, existing.flow_id, revision, content, definition.content_digest, definition_json, now ], )?; tx.execute( r#"UPDATE flow_sources SET path = ?4, content = ?5, content_digest = ?6, revision = ?7, updated_at = ?8 WHERE workspace_id = ?1 AND source_kind = ?2 AND name = ?3"#, params![ workspace_id, source_kind.as_str(), name, path, content, definition.content_digest, revision, now ], )?; tx.commit()?; return Ok(FlowSourceRecord { revision, path: path.to_string(), content: content.to_string(), content_digest: definition.content_digest, updated_at: now.to_string(), ..existing }); } let flow_id = Uuid::now_v7().to_string(); let definition_json = serde_json::to_string(&definition) .map_err(|error| Error::Store(error.to_string()))?; tx.execute( r#"INSERT INTO flow_sources ( workspace_id, flow_id, source_kind, name, path, content, content_digest, revision, created_at, updated_at ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, 1, ?8, ?8)"#, params![ workspace_id, flow_id, source_kind.as_str(), name, path, content, definition.content_digest, now ], )?; tx.execute( r#"INSERT INTO flow_source_revisions ( workspace_id, flow_id, revision, content, content_digest, definition_json, created_at ) VALUES (?1, ?2, 1, ?3, ?4, ?5, ?6)"#, params![ workspace_id, flow_id, content, definition.content_digest, definition_json, now ], )?; tx.commit()?; Ok(FlowSourceRecord { workspace_id: workspace_id.to_string(), flow_id, source_kind, name, path: path.to_string(), content: content.to_string(), content_digest: definition.content_digest, revision: 1, created_at: now.to_string(), updated_at: now.to_string(), }) }) } fn get_flow_source_by_name( &self, workspace_id: &str, source_kind: FlowSourceKind, name: &str, ) -> Result> { self.with_conn(|conn| { conn.query_row( r#"SELECT workspace_id, flow_id, source_kind, name, path, content, content_digest, revision, created_at, updated_at FROM flow_sources WHERE workspace_id = ?1 AND source_kind = ?2 AND name = ?3"#, params![workspace_id, source_kind.as_str(), name], read_flow_source_record, ) .optional() .map_err(Error::from) }) } fn list_flow_sources(&self, workspace_id: &str) -> Result> { self.with_conn(|conn| { let mut statement = conn.prepare( r#"SELECT workspace_id, flow_id, source_kind, name, path, content, content_digest, revision, created_at, updated_at FROM flow_sources WHERE workspace_id = ?1 ORDER BY source_kind ASC, name ASC"#, )?; let rows = statement.query_map(params![workspace_id], read_flow_source_record)?; rows.collect::, _>>() .map_err(Error::from) }) } fn get_flow_source( &self, workspace_id: &str, flow_id: &str, ) -> Result> { self.with_conn(|conn| { conn.query_row( r#"SELECT workspace_id, flow_id, source_kind, name, path, content, content_digest, revision, created_at, updated_at FROM flow_sources WHERE workspace_id = ?1 AND flow_id = ?2"#, params![workspace_id, flow_id], read_flow_source_record, ) .optional() .map_err(Error::from) }) } fn get_flow_source_revision( &self, workspace_id: &str, flow_id: &str, revision: u64, ) -> Result> { self.with_conn(|conn| { conn.query_row( r#"SELECT workspace_id, flow_id, revision, content, content_digest, definition_json, created_at FROM flow_source_revisions WHERE workspace_id = ?1 AND flow_id = ?2 AND revision = ?3"#, params![workspace_id, flow_id, revision], read_flow_source_revision_record, ) .optional() .map_err(Error::from) }) } fn upsert_objective(&self, record: &ObjectiveRecord) -> Result<()> { self.with_conn(|conn| { let tx = conn.unchecked_transaction()?; allocate_resource_human_key( &tx, &record.workspace_id, WorkspaceResourceKind::Objective, &record.objective_id, &record.created_at, )?; tx.execute( r#"INSERT INTO objectives ( workspace_id, objective_id, title, state, body_md, created_at, updated_at ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7) ON CONFLICT(workspace_id, objective_id) DO UPDATE SET workspace_id = excluded.workspace_id, title = excluded.title, state = excluded.state, body_md = excluded.body_md, created_at = excluded.created_at, updated_at = excluded.updated_at"#, params![ record.workspace_id, record.objective_id, record.title, record.state, record.body_md, record.created_at, record.updated_at, ], )?; tx.commit()?; Ok(()) }) } fn list_objectives(&self, workspace_id: &str, limit: usize) -> Result> { self.with_conn(|conn| { let mut stmt = conn.prepare( r#"SELECT workspace_id, objective_id, title, state, body_md, created_at, updated_at FROM objectives WHERE workspace_id = ?1 ORDER BY updated_at DESC, objective_id ASC LIMIT ?2"#, )?; let rows = stmt.query_map(params![workspace_id, limit as i64], read_objective_record)?; rows.collect::, _>>() .map_err(Error::from) }) } fn list_objectives_for_ticket( &self, workspace_id: &str, ticket_id: &str, limit: usize, ) -> Result> { self.with_conn(|conn| { let mut stmt = conn.prepare( r#"SELECT o.workspace_id, o.objective_id, o.title, o.state, o.body_md, o.created_at, o.updated_at FROM objectives AS o INNER JOIN objective_ticket_links AS l ON l.workspace_id = o.workspace_id AND l.objective_id = o.objective_id WHERE o.workspace_id = ?1 AND l.ticket_id = ?2 ORDER BY o.updated_at DESC, o.objective_id ASC LIMIT ?3"#, )?; let rows = stmt.query_map( params![workspace_id, ticket_id, limit as i64], read_objective_record, )?; rows.collect::, _>>() .map_err(Error::from) }) } fn get_objective( &self, workspace_id: &str, objective_id: &str, ) -> Result> { self.with_conn(|conn| { conn.query_row( r#"SELECT workspace_id, objective_id, title, state, body_md, created_at, updated_at FROM objectives WHERE workspace_id = ?1 AND objective_id = ?2"#, params![workspace_id, objective_id], read_objective_record, ) .optional() .map_err(Error::from) }) } fn replace_objective_ticket_links( &self, workspace_id: &str, objective_id: &str, links: &[ObjectiveTicketLinkRecord], ) -> Result<()> { self.with_conn(|conn| { conn.execute( "DELETE FROM objective_ticket_links WHERE workspace_id = ?1 AND objective_id = ?2", params![workspace_id, objective_id], )?; for link in links { conn.execute( r#"INSERT INTO objective_ticket_links ( workspace_id, objective_id, ticket_id, kind, created_at ) VALUES (?1, ?2, ?3, ?4, ?5) ON CONFLICT(workspace_id, objective_id, ticket_id, kind) DO UPDATE SET workspace_id = excluded.workspace_id, created_at = excluded.created_at"#, params![ link.workspace_id, link.objective_id, link.ticket_id, link.kind, link.created_at, ], )?; } Ok(()) }) } fn list_objective_ticket_links( &self, workspace_id: &str, objective_id: &str, ) -> Result> { self.with_conn(|conn| { let mut stmt = conn.prepare( r#"SELECT workspace_id, objective_id, ticket_id, kind, created_at FROM objective_ticket_links WHERE workspace_id = ?1 AND objective_id = ?2 ORDER BY ticket_id ASC, kind ASC"#, )?; let rows = stmt.query_map( params![workspace_id, objective_id], read_objective_ticket_link_record, )?; rows.collect::, _>>() .map_err(Error::from) }) } fn insert_objective_event(&self, record: &ObjectiveEventRecord) -> Result<()> { self.with_conn(|conn| { conn.execute( r#"INSERT INTO objective_events ( workspace_id, objective_id, event_id, kind, body_md, created_at ) VALUES (?1, ?2, ?3, ?4, ?5, ?6)"#, params![ record.workspace_id, record.objective_id, record.event_id, record.kind, record.body_md, record.created_at, ], )?; Ok(()) }) } fn list_objective_events( &self, workspace_id: &str, objective_id: &str, ) -> Result> { self.with_conn(|conn| { let mut stmt = conn.prepare( r#"SELECT workspace_id, objective_id, event_id, kind, body_md, created_at FROM objective_events WHERE workspace_id = ?1 AND objective_id = ?2 ORDER BY created_at ASC, event_id ASC"#, )?; let rows = stmt.query_map(params![workspace_id, objective_id], |row| { Ok(ObjectiveEventRecord { workspace_id: row.get(0)?, objective_id: row.get(1)?, event_id: row.get(2)?, kind: row.get(3)?, body_md: row.get(4)?, created_at: row.get(5)?, }) })?; rows.collect::, _>>() .map_err(Error::from) }) } fn upsert_objective_resource(&self, record: &ObjectiveResourceRecord) -> Result<()> { self.with_conn(|conn| { conn.execute( r#"INSERT INTO objective_resources ( workspace_id, objective_id, resource_path, body, media_type, created_at, updated_at ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7) ON CONFLICT(workspace_id, objective_id, resource_path) DO UPDATE SET workspace_id = excluded.workspace_id, body = excluded.body, media_type = excluded.media_type, updated_at = excluded.updated_at"#, params![ record.workspace_id, record.objective_id, record.resource_path, record.body, record.media_type, record.created_at, record.updated_at, ], )?; Ok(()) }) } fn list_objective_resources( &self, workspace_id: &str, objective_id: &str, ) -> Result> { self.with_conn(|conn| { let mut stmt = conn.prepare( r#"SELECT workspace_id, objective_id, resource_path, body, media_type, created_at, updated_at FROM objective_resources WHERE workspace_id = ?1 AND objective_id = ?2 ORDER BY resource_path ASC"#, )?; let rows = stmt.query_map( params![workspace_id, objective_id], read_objective_resource_record, )?; rows.collect::, _>>() .map_err(Error::from) }) } fn ensure_memory_document( &self, workspace_id: &str, default_body_md: &str, now: &str, ) -> Result { self.with_conn(|conn| { conn.execute( r#"INSERT OR IGNORE INTO workspace_memory_documents ( workspace_id, body_md, created_at, updated_at ) VALUES (?1, ?2, ?3, ?3)"#, params![workspace_id, default_body_md, now], )?; conn.query_row( r#"SELECT workspace_id, body_md, created_at, updated_at FROM workspace_memory_documents WHERE workspace_id = ?1"#, params![workspace_id], read_memory_document_record, ) .map_err(Error::from) }) } fn get_memory_document(&self, workspace_id: &str) -> Result> { self.with_conn(|conn| { conn.query_row( r#"SELECT workspace_id, body_md, created_at, updated_at FROM workspace_memory_documents WHERE workspace_id = ?1"#, params![workspace_id], read_memory_document_record, ) .optional() .map_err(Error::from) }) } fn upsert_memory_document(&self, record: &MemoryDocumentRecord) -> Result<()> { self.with_conn(|conn| { conn.execute( r#"INSERT INTO workspace_memory_documents ( workspace_id, body_md, created_at, updated_at ) VALUES (?1, ?2, ?3, ?4) ON CONFLICT(workspace_id) DO UPDATE SET body_md = excluded.body_md, updated_at = excluded.updated_at"#, params![ record.workspace_id, record.body_md, record.created_at, record.updated_at, ], )?; Ok(()) }) } fn upsert_memory_staging_record(&self, record: &MemoryStagingRecord) -> Result<()> { self.with_conn(|conn| { conn.execute( r#"INSERT INTO memory_staging_records ( workspace_id, candidate_id, raw_json, source_path, imported_at ) VALUES (?1, ?2, ?3, ?4, ?5) ON CONFLICT(workspace_id, candidate_id) DO UPDATE SET raw_json = excluded.raw_json, source_path = excluded.source_path, imported_at = excluded.imported_at"#, params![ record.workspace_id, record.candidate_id, record.raw_json, record.source_path, record.imported_at, ], )?; Ok(()) }) } fn list_memory_staging_records( &self, workspace_id: &str, limit: usize, ) -> Result> { self.with_conn(|conn| { let mut stmt = conn.prepare( r#"SELECT workspace_id, candidate_id, raw_json, source_path, imported_at FROM memory_staging_records WHERE workspace_id = ?1 ORDER BY imported_at DESC, candidate_id ASC LIMIT ?2"#, )?; let rows = stmt.query_map( params![workspace_id, limit as i64], read_memory_staging_record, )?; rows.collect::, _>>() .map_err(Error::from) }) } fn get_memory_staging_record( &self, workspace_id: &str, candidate_id: &str, ) -> Result> { self.with_conn(|conn| { conn.query_row( r#"SELECT workspace_id, candidate_id, raw_json, source_path, imported_at FROM memory_staging_records WHERE workspace_id = ?1 AND candidate_id = ?2"#, params![workspace_id, candidate_id], read_memory_staging_record, ) .optional() .map_err(Error::from) }) } fn delete_memory_staging_record(&self, workspace_id: &str, candidate_id: &str) -> Result { self.with_conn(|conn| { let changed = conn.execute( "DELETE FROM memory_staging_records WHERE workspace_id = ?1 AND candidate_id = ?2", params![workspace_id, candidate_id], )?; Ok(changed > 0) }) } fn count_memory_staging_records(&self, workspace_id: &str) -> Result { self.with_conn(|conn| { conn.query_row( "SELECT COUNT(*) FROM memory_staging_records WHERE workspace_id = ?1", params![workspace_id], |row| row.get::<_, i64>(0), ) .map(|count| count as usize) .map_err(Error::from) }) } fn insert_memory_staging_resolution( &self, record: &MemoryStagingResolutionRecord, ) -> Result<()> { self.with_conn(|conn| { conn.execute( r#"INSERT INTO memory_staging_resolutions ( workspace_id, candidate_id, action, reason, affected_refs_json, staging_raw_json, source_path, imported_at, resolved_at ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)"#, params![ record.workspace_id, record.candidate_id, record.action, record.reason, record.affected_refs_json, record.staging_raw_json, record.source_path, record.imported_at, record.resolved_at, ], )?; Ok(()) }) } fn list_memory_staging_resolutions( &self, workspace_id: &str, limit: usize, ) -> Result> { self.with_conn(|conn| { let mut stmt = conn.prepare( r#"SELECT workspace_id, candidate_id, action, reason, affected_refs_json, staging_raw_json, source_path, imported_at, resolved_at FROM memory_staging_resolutions WHERE workspace_id = ?1 ORDER BY resolved_at DESC, candidate_id ASC LIMIT ?2"#, )?; let rows = stmt.query_map( params![workspace_id, limit as i64], read_memory_staging_resolution_record, )?; rows.collect::, _>>() .map_err(Error::from) }) } fn upsert_account(&self, record: &AccountRecord) -> Result<()> { self.with_conn(|conn| { conn.execute( r#"INSERT INTO accounts (account_id, kind, handle, display_name, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6) ON CONFLICT(account_id) DO UPDATE SET handle = excluded.handle, display_name = excluded.display_name, updated_at = excluded.updated_at"#, params![record.account_id, record.kind, record.handle, record.display_name, record.created_at, record.updated_at], )?; Ok(()) }) } fn get_account(&self, account_id: &str) -> Result> { self.with_conn(|conn| { conn.query_row( account_select_sql("WHERE account_id = ?1").as_str(), params![account_id], read_account_record, ) .optional() .map_err(Error::from) }) } fn get_account_by_handle(&self, kind: &str, handle: &str) -> Result> { self.with_conn(|conn| { conn.query_row( account_select_sql("WHERE kind = ?1 AND handle = ?2").as_str(), params![kind, handle], read_account_record, ) .optional() .map_err(Error::from) }) } fn upsert_user(&self, record: &UserRecord) -> Result<()> { self.with_conn(|conn| { conn.execute( r#"INSERT INTO users (user_id, account_id, handle, display_name, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6) ON CONFLICT(user_id) DO UPDATE SET handle = excluded.handle, display_name = excluded.display_name, updated_at = excluded.updated_at"#, params![record.user_id, record.account_id, record.handle, record.display_name, record.created_at, record.updated_at], )?; Ok(()) }) } fn get_user(&self, user_id: &str) -> Result> { self.with_conn(|conn| { conn.query_row( user_select_sql("WHERE user_id = ?1").as_str(), params![user_id], read_user_record, ) .optional() .map_err(Error::from) }) } fn get_user_by_handle(&self, handle: &str) -> Result> { self.with_conn(|conn| { conn.query_row( user_select_sql("WHERE handle = ?1").as_str(), params![handle], read_user_record, ) .optional() .map_err(Error::from) }) } fn any_user(&self) -> Result> { self.with_conn(|conn| { conn.query_row( user_select_sql("ORDER BY created_at ASC LIMIT 1").as_str(), [], read_user_record, ) .optional() .map_err(Error::from) }) } fn upsert_passkey_credential(&self, record: &PasskeyCredentialRecord) -> Result<()> { self.with_conn(|conn| { conn.execute( r#"INSERT INTO passkey_credentials (credential_id, user_id, public_key_cose, transports_json, sign_count, created_at, last_used_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7) ON CONFLICT(credential_id) DO UPDATE SET public_key_cose = excluded.public_key_cose, transports_json = excluded.transports_json, sign_count = excluded.sign_count, last_used_at = excluded.last_used_at"#, params![record.credential_id, record.user_id, record.public_key_cose, record.transports_json, record.sign_count, record.created_at, record.last_used_at], )?; Ok(()) }) } fn get_passkey_credential( &self, credential_id: &str, ) -> Result> { self.with_conn(|conn| { conn.query_row( passkey_select_sql("WHERE credential_id = ?1").as_str(), params![credential_id], read_passkey_credential_record, ) .optional() .map_err(Error::from) }) } fn list_passkey_credentials_for_user( &self, user_id: &str, ) -> Result> { self.with_conn(|conn| { let mut stmt = conn.prepare( passkey_select_sql("WHERE user_id = ?1 ORDER BY created_at ASC").as_str(), )?; let rows = stmt.query_map(params![user_id], read_passkey_credential_record)?; rows.collect::, _>>() .map_err(Error::from) }) } fn put_auth_challenge(&self, record: &AuthChallengeRecord) -> Result<()> { self.with_conn(|conn| { conn.execute( r#"INSERT INTO auth_challenges (challenge_id, ceremony, challenge, user_id, rp_id, origin, state_json, expires_at, created_at, consumed_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)"#, params![record.challenge_id, record.ceremony, record.challenge, record.user_id, record.rp_id, record.origin, record.state_json, record.expires_at, record.created_at, record.consumed_at], )?; Ok(()) }) } fn consume_auth_challenge( &self, challenge: &str, ceremony: &str, consumed_at: &str, ) -> Result> { self.with_conn(|conn| { let record = conn .query_row( auth_challenge_select_sql( "WHERE challenge = ?1 AND ceremony = ?2 AND consumed_at IS NULL", ) .as_str(), params![challenge, ceremony], read_auth_challenge_record, ) .optional()?; if let Some(record) = record.as_ref() { conn.execute( "UPDATE auth_challenges SET consumed_at = ?2 WHERE challenge_id = ?1", params![record.challenge_id, consumed_at], )?; } Ok(record) }) } fn consume_auth_challenge_by_id( &self, challenge_id: &str, ceremony: &str, consumed_at: &str, ) -> Result> { self.with_conn(|conn| { let record = conn .query_row( auth_challenge_select_sql( "WHERE challenge_id = ?1 AND ceremony = ?2 AND consumed_at IS NULL", ) .as_str(), params![challenge_id, ceremony], read_auth_challenge_record, ) .optional()?; if let Some(record) = record.as_ref() { conn.execute( "UPDATE auth_challenges SET consumed_at = ?2 WHERE challenge_id = ?1", params![record.challenge_id, consumed_at], )?; } Ok(record) }) } fn create_browser_session(&self, record: &BrowserSessionRecord) -> Result<()> { self.with_conn(|conn| { conn.execute( r#"INSERT INTO browser_sessions (session_id, token_hash, user_id, created_at, expires_at, revoked_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6)"#, params![record.session_id, record.token_hash, record.user_id, record.created_at, record.expires_at, record.revoked_at], )?; Ok(()) }) } fn resolve_browser_session(&self, token_hash: &str) -> Result> { self.with_conn(|conn| { conn.query_row( browser_session_select_sql("WHERE token_hash = ?1 AND revoked_at IS NULL").as_str(), params![token_hash], read_browser_session_record, ) .optional() .map_err(Error::from) }) } fn revoke_browser_session(&self, token_hash: &str, revoked_at: &str) -> Result { self.with_conn(|conn| { let changed = conn.execute( "UPDATE browser_sessions SET revoked_at = ?2 WHERE token_hash = ?1 AND revoked_at IS NULL", params![token_hash, revoked_at], )?; Ok(changed > 0) }) } fn create_api_token(&self, record: &ApiTokenRecord) -> Result<()> { self.with_conn(|conn| { conn.execute( r#"INSERT INTO api_tokens (token_id, token_hash, user_id, label, created_at, expires_at, revoked_at, last_used_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)"#, params![record.token_id, record.token_hash, record.user_id, record.label, record.created_at, record.expires_at, record.revoked_at, record.last_used_at], )?; Ok(()) }) } fn resolve_api_token(&self, token_hash: &str) -> Result> { self.with_conn(|conn| { conn.query_row( api_token_select_sql("WHERE token_hash = ?1 AND revoked_at IS NULL").as_str(), params![token_hash], read_api_token_record, ) .optional() .map_err(Error::from) }) } fn mark_api_token_used(&self, token_hash: &str, used_at: &str) -> Result<()> { self.with_conn(|conn| { conn.execute( "UPDATE api_tokens SET last_used_at = ?2 WHERE token_hash = ?1", params![token_hash, used_at], )?; Ok(()) }) } fn create_device_login_flow(&self, record: &DeviceLoginFlowRecord) -> Result<()> { self.with_conn(|conn| { conn.execute( r#"INSERT INTO device_login_flows (device_code, user_code, verification_uri, client_name, user_id, api_token_id, issued_access_token, created_at, expires_at, approved_at, consumed_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)"#, params![record.device_code, record.user_code, record.verification_uri, record.client_name, record.user_id, record.api_token_id, record.issued_access_token, record.created_at, record.expires_at, record.approved_at, record.consumed_at], )?; Ok(()) }) } fn get_device_login_flow_by_user_code( &self, user_code: &str, ) -> Result> { self.with_conn(|conn| { conn.query_row( device_login_select_sql("WHERE user_code = ?1").as_str(), params![user_code], read_device_login_flow_record, ) .optional() .map_err(Error::from) }) } fn get_device_login_flow_by_device_code( &self, device_code: &str, ) -> Result> { self.with_conn(|conn| { conn.query_row( device_login_select_sql("WHERE device_code = ?1").as_str(), params![device_code], read_device_login_flow_record, ) .optional() .map_err(Error::from) }) } fn approve_device_login_flow( &self, device_code: &str, user_id: &str, api_token_id: &str, issued_access_token: &str, approved_at: &str, ) -> Result { self.with_conn(|conn| { let changed = conn.execute( r#"UPDATE device_login_flows SET user_id = ?2, api_token_id = ?3, issued_access_token = ?4, approved_at = ?5 WHERE device_code = ?1 AND approved_at IS NULL AND consumed_at IS NULL"#, params![ device_code, user_id, api_token_id, issued_access_token, approved_at ], )?; Ok(changed > 0) }) } fn consume_device_login_token( &self, device_code: &str, consumed_at: &str, ) -> Result> { self.with_conn(|conn| { let record = conn.query_row(device_login_select_sql("WHERE device_code = ?1 AND approved_at IS NOT NULL AND consumed_at IS NULL").as_str(), params![device_code], read_device_login_flow_record).optional()?; if record.is_some() { conn.execute("UPDATE device_login_flows SET consumed_at = ?2, issued_access_token = NULL WHERE device_code = ?1", params![device_code, consumed_at])?; } Ok(record) }) } fn upsert_worker_registry(&self, record: &WorkerRegistryRecord) -> Result<()> { self.with_conn(|conn| { let removal_blocks_upsert: bool = conn.query_row( "SELECT EXISTS( SELECT 1 FROM worker_removal_operations WHERE workspace_id = ?1 AND runtime_id = ?2 AND worker_id = ?3 AND state IN ('executing', 'failed', 'succeeded') )", params![ record.workspace_id, record.worker.runtime_id, record.worker.worker_id ], |row| row.get(0), )?; if removal_blocks_upsert { return Ok(()); } conn.execute( r#"INSERT INTO worker_registry ( workspace_id, runtime_id, worker_id, display_name, profile, retention_state, transcript_ref, session_ref, summary_ref, diagnostics_ref, created_at, updated_at ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12) ON CONFLICT(workspace_id, worker_id) DO UPDATE SET runtime_id = excluded.runtime_id, display_name = excluded.display_name, profile = excluded.profile, retention_state = CASE WHEN worker_registry.retention_state = 'pinned' AND excluded.retention_state = 'normal' THEN worker_registry.retention_state ELSE excluded.retention_state END, transcript_ref = excluded.transcript_ref, session_ref = excluded.session_ref, summary_ref = excluded.summary_ref, diagnostics_ref = excluded.diagnostics_ref, updated_at = excluded.updated_at WHERE NOT EXISTS ( SELECT 1 FROM worker_removal_operations retention WHERE retention.workspace_id = excluded.workspace_id AND retention.runtime_id = excluded.runtime_id AND retention.worker_id = excluded.worker_id AND retention.state IN ('executing', 'failed', 'succeeded') )"#, params![ record.workspace_id, record.worker.runtime_id, record.worker.worker_id, record.display_name, record.profile, record.retention_state, record.transcript_ref, record.session_ref, record.summary_ref, record.diagnostics_ref, record.created_at, record.updated_at, ], )?; Ok(()) }) } fn get_worker_registry( &self, workspace_id: &str, worker: &RuntimeWorkerRef, ) -> Result> { self.with_conn(|conn| { conn.query_row( worker_registry_select_sql( "WHERE workspace_id = ?1 AND runtime_id = ?2 AND worker_id = ?3", ) .as_str(), params![workspace_id, worker.runtime_id, worker.worker_id], read_worker_registry_record, ) .optional() .map_err(Error::from) }) } fn list_worker_registry( &self, workspace_id: &str, limit: usize, ) -> Result> { self.with_conn(|conn| { let sql = worker_registry_select_sql( "WHERE workspace_id = ?1 ORDER BY updated_at DESC LIMIT ?2", ); let mut stmt = conn.prepare(sql.as_str())?; let rows = stmt.query_map( params![workspace_id, limit as i64], read_worker_registry_record, )?; rows.collect::, _>>() .map_err(Error::from) }) } fn update_worker_retention( &self, workspace_id: &str, worker: &RuntimeWorkerRef, retention_state: &str, updated_at: &str, ) -> Result { self.with_conn(|conn| { let changed = conn.execute( r#"UPDATE worker_registry SET retention_state = ?4, updated_at = ?5 WHERE workspace_id = ?1 AND runtime_id = ?2 AND worker_id = ?3 AND NOT EXISTS ( SELECT 1 FROM worker_removal_operations retention WHERE retention.workspace_id = ?1 AND retention.runtime_id = ?2 AND retention.worker_id = ?3 AND retention.state IN ('executing', 'failed') )"#, params![ workspace_id, worker.runtime_id, worker.worker_id, retention_state, updated_at ], )?; Ok(changed > 0) }) } fn delete_worker_registry( &self, workspace_id: &str, worker: &RuntimeWorkerRef, ) -> Result { self.with_conn(|conn| { let tx = conn.unchecked_transaction()?; tx.execute( r#"UPDATE worker_workdir_links SET unlinked_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE workspace_id = ?1 AND runtime_id = ?2 AND worker_id = ?3 AND unlinked_at IS NULL"#, params![workspace_id, worker.runtime_id, worker.worker_id], )?; let changed = tx.execute( "DELETE FROM worker_registry WHERE workspace_id = ?1 AND runtime_id = ?2 AND worker_id = ?3", params![workspace_id, worker.runtime_id, worker.worker_id], )?; tx.commit()?; Ok(changed > 0) }) } fn create_worker_control_grant( &self, record: &WorkerControlGrantRecord, ) -> Result { self.with_conn(|conn| { let permissions_json = serde_json::to_string(&record.permissions) .map_err(|error| Error::Store(error.to_string()))?; conn.execute( r#"INSERT INTO worker_control_grants ( workspace_id, grant_id, controller_runtime_id, controller_worker_id, subject_runtime_id, subject_worker_id, relation, origin, permissions_json, operation_id, created_at, revoked_at ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12) ON CONFLICT ( workspace_id, controller_worker_id, operation_id ) DO NOTHING"#, params![ record.workspace_id, record.grant_id, record.controller.runtime_id, record.controller.worker_id, record.subject.runtime_id, record.subject.worker_id, record.relation, record.origin, permissions_json, record.operation_id, record.created_at, record.revoked_at, ], )?; let persisted = read_worker_control_grant_by_operation( conn, record.workspace_id.as_str(), &record.controller, record.operation_id.as_str(), )? .ok_or_else(|| Error::Store("worker control grant was not persisted".to_string()))?; if persisted.subject != record.subject || persisted.relation != record.relation || persisted.origin != record.origin || persisted.permissions != record.permissions { return Err(Error::InvalidInput(format!( "worker control operation `{}` was already used with different input", record.operation_id ))); } Ok(persisted) }) } fn get_worker_control_grant( &self, workspace_id: &str, grant_id: &str, ) -> Result> { self.with_conn(|conn| { conn.query_row( r#"SELECT workspace_id, grant_id, controller_runtime_id, controller_worker_id, subject_runtime_id, subject_worker_id, relation, origin, permissions_json, operation_id, created_at, revoked_at FROM worker_control_grants WHERE workspace_id = ?1 AND grant_id = ?2"#, params![workspace_id, grant_id], read_worker_control_grant_record, ) .optional() .map_err(Error::from) }) } fn get_worker_control_grant_by_operation( &self, workspace_id: &str, controller: &RuntimeWorkerRef, operation_id: &str, ) -> Result> { self.with_conn(|conn| { read_worker_control_grant_by_operation(conn, workspace_id, controller, operation_id) }) } fn get_active_worker_control_grant( &self, workspace_id: &str, controller: &RuntimeWorkerRef, subject: &RuntimeWorkerRef, ) -> Result> { self.with_conn(|conn| { conn.query_row( r#"SELECT workspace_id, grant_id, controller_runtime_id, controller_worker_id, subject_runtime_id, subject_worker_id, relation, origin, permissions_json, operation_id, created_at, revoked_at FROM worker_control_grants WHERE workspace_id = ?1 AND controller_runtime_id = ?2 AND controller_worker_id = ?3 AND subject_runtime_id = ?4 AND subject_worker_id = ?5 AND revoked_at IS NULL ORDER BY created_at DESC LIMIT 1"#, params![ workspace_id, controller.runtime_id, controller.worker_id, subject.runtime_id, subject.worker_id, ], read_worker_control_grant_record, ) .optional() .map_err(Error::from) }) } fn list_active_worker_control_grants( &self, workspace_id: &str, controller: &RuntimeWorkerRef, limit: usize, ) -> Result> { self.with_conn(|conn| { let mut stmt = conn.prepare( r#"SELECT workspace_id, grant_id, controller_runtime_id, controller_worker_id, subject_runtime_id, subject_worker_id, relation, origin, permissions_json, operation_id, created_at, revoked_at FROM worker_control_grants WHERE workspace_id = ?1 AND controller_runtime_id = ?2 AND controller_worker_id = ?3 AND revoked_at IS NULL ORDER BY created_at ASC, grant_id ASC LIMIT ?4"#, )?; let rows = stmt.query_map( params![ workspace_id, controller.runtime_id, controller.worker_id, limit as i64, ], read_worker_control_grant_record, )?; rows.collect::, _>>() .map_err(Error::from) }) } fn revoke_worker_control_grant( &self, workspace_id: &str, grant_id: &str, revoked_at: &str, ) -> Result { self.with_conn(|conn| { let changed = conn.execute( r#"UPDATE worker_control_grants SET revoked_at = ?3 WHERE workspace_id = ?1 AND grant_id = ?2 AND revoked_at IS NULL"#, params![workspace_id, grant_id, revoked_at], )?; Ok(changed > 0) }) } fn get_ticket_assignment_operation( &self, workspace_id: &str, operation_id: &str, ) -> Result> { self.with_conn(|conn| read_assignment_operation(conn, workspace_id, operation_id)) } fn reserve_ticket_assignment_operation( &self, workspace_id: &str, operation_id: &str, ticket_id: &str, runtime_id: &str, worker_id: Option<&str>, request_fingerprint: &str, created_at: &str, ) -> Result<()> { self.with_conn(|conn| { let inserted = conn.execute( r#"INSERT OR IGNORE INTO ticket_assignment_operations ( workspace_id, operation_id, action, ticket_id, runtime_id, worker_id, assignment_id, expected_assignment_id, created_at, request_fingerprint ) VALUES (?1, ?2, 'assign', ?3, ?4, ?5, NULL, NULL, ?6, ?7)"#, params![ workspace_id, operation_id, ticket_id, runtime_id, worker_id, created_at, request_fingerprint, ], )?; if inserted > 0 { return Ok(()); } let existing = read_assignment_operation(conn, workspace_id, operation_id)? .ok_or_else(|| { Error::TicketAssignmentConflict(format!( "assignment operation {operation_id} could not be reserved" )) })?; if existing.action == "assign" && existing.ticket_id == ticket_id && existing.runtime_id.as_deref() == Some(runtime_id) && (worker_id.is_none() || existing .worker .as_ref() .map(|worker| worker.worker_id.as_str()) == worker_id) && existing.expected_assignment_id.is_none() && existing.request_fingerprint.as_deref() == Some(request_fingerprint) { Ok(()) } else { Err(Error::TicketAssignmentConflict(format!( "assignment operation {operation_id} was already used with different input" ))) } }) } fn bind_ticket_assignment_operation_worker( &self, workspace_id: &str, operation_id: &str, worker_id: &str, ) -> Result<()> { self.with_conn(|conn| { let updated = conn.execute( r#"UPDATE ticket_assignment_operations SET worker_id = ?3 WHERE workspace_id = ?1 AND operation_id = ?2 AND assignment_id IS NULL AND (worker_id IS NULL OR worker_id = ?3)"#, params![workspace_id, operation_id, worker_id], )?; if updated == 1 { return Ok(()); } Err(Error::TicketAssignmentConflict(format!( "assignment operation {operation_id} cannot bind Worker {worker_id}" ))) }) } fn rollback_ticket_assignment_operation( &self, workspace_id: &str, operation_id: &str, ) -> Result<()> { self.with_conn(|conn| { let transaction = conn.unchecked_transaction()?; let operation = read_assignment_operation(&transaction, workspace_id, operation_id)?; let Some(operation) = operation else { transaction.commit()?; return Ok(()); }; if operation.action != "assign" { return Err(Error::TicketAssignmentConflict(format!( "Ticket operation `{operation_id}` is `{}`, expected `assign`", operation.action ))); } transaction.execute( "DELETE FROM ticket_assignment_operations WHERE workspace_id = ?1 AND operation_id = ?2", params![workspace_id, operation_id], )?; if let Some(assignment_id) = operation.assignment_id { transaction.execute( "DELETE FROM ticket_worker_assignment_events WHERE workspace_id = ?1 AND ticket_id = ?2 AND assignment_id = ?3", params![workspace_id, operation.ticket_id, assignment_id], )?; transaction.execute( "DELETE FROM ticket_worker_assignments WHERE workspace_id = ?1 AND ticket_id = ?2 AND assignment_id = ?3", params![workspace_id, operation.ticket_id, assignment_id], )?; } transaction.commit()?; Ok(()) }) } fn get_current_ticket_worker_assignment( &self, workspace_id: &str, ticket_id: &str, ) -> Result> { self.with_conn(|conn| { conn.query_row( current_ticket_worker_assignment_select_sql().as_str(), params![workspace_id, ticket_id], read_ticket_worker_assignment_record, ) .optional() .map_err(Error::from) }) } fn set_current_ticket_worker_assignment( &self, record: &TicketWorkerAssignmentRecord, expected_assignment_id: Option<&str>, event_id: &str, operation_id: &str, allow_reassign: bool, ) -> Result { self.with_conn(|conn| { let tx = conn.unchecked_transaction()?; let removal_blocks_assignment: bool = tx.query_row( "SELECT EXISTS( SELECT 1 FROM worker_removal_operations WHERE workspace_id = ?1 AND runtime_id = ?2 AND worker_id = ?3 AND state IN ('executing', 'failed', 'succeeded') UNION ALL SELECT 1 FROM worker_tombstones WHERE workspace_id = ?1 AND runtime_id = ?2 AND worker_id = ?3 )", params![ record.workspace_id, record.worker.runtime_id, record.worker.worker_id ], |row| row.get(0), )?; if removal_blocks_assignment { return Err(Error::TicketAssignmentConflict(format!( "Worker {}/{} is being retained or has been removed", record.worker.runtime_id, record.worker.worker_id ))); } let mut reserved_operation = false; if let Some(existing) = read_assignment_operation(&tx, &record.workspace_id, operation_id)? { if existing.action != if allow_reassign { "reassign" } else { "assign" } || existing.ticket_id != record.ticket_id || existing.worker.as_ref() != Some(&record.worker) || existing.expected_assignment_id.as_deref() != expected_assignment_id { return Err(Error::TicketAssignmentConflict(format!( "assignment operation {operation_id} was already used with different input" ))); } if let Some(assignment_id) = existing.assignment_id { let current = tx.query_row( r#"SELECT workspace_id, ticket_id, assignment_id, runtime_id, worker_id, assigned_by, assigned_at FROM ticket_worker_assignments WHERE workspace_id = ?1 AND ticket_id = ?2 AND assignment_id = ?3"#, params![record.workspace_id, record.ticket_id, assignment_id], read_ticket_worker_assignment_record, )?; let previous = if existing.action == "reassign" { existing .expected_assignment_id .as_deref() .map(|previous_assignment_id| { tx.query_row( r#"SELECT workspace_id, ticket_id, assignment_id, runtime_id, worker_id, assigned_by, assigned_at FROM ticket_worker_assignments WHERE workspace_id = ?1 AND ticket_id = ?2 AND assignment_id = ?3"#, params![ record.workspace_id, record.ticket_id, previous_assignment_id, ], read_ticket_worker_assignment_record, ) }) .transpose()? } else { None }; tx.commit()?; return Ok(TicketWorkerAssignmentUpdate { current, previous }); } reserved_operation = true; } let previous = tx .query_row( current_ticket_worker_assignment_select_sql().as_str(), params![record.workspace_id, record.ticket_id], read_ticket_worker_assignment_record, ) .optional()?; if previous.is_some() && !allow_reassign { return Err(Error::TicketAssignmentConflict(format!( "Ticket {} is already assigned; use the explicit reassign operation", record.ticket_id ))); } if allow_reassign { let expected_assignment_id = expected_assignment_id.ok_or_else(|| { Error::TicketAssignmentConflict( "reassign requires expected_assignment_id".to_string(), ) })?; require_expected_ticket_assignment( record.ticket_id.as_str(), previous.as_ref(), Some(expected_assignment_id), )?; } else if expected_assignment_id.is_some() { return Err(Error::TicketAssignmentConflict( "assign does not accept expected_assignment_id".to_string(), )); } tx.execute( r#"INSERT INTO ticket_worker_assignments ( workspace_id, ticket_id, assignment_id, runtime_id, worker_id, assigned_by, assigned_at ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)"#, params![ record.workspace_id, record.ticket_id, record.assignment_id, record.worker.runtime_id, record.worker.worker_id, record.assigned_by, record.assigned_at, ], )?; let current_write = if allow_reassign { tx.execute( r#"UPDATE ticket_current_worker_assignments SET assignment_id = ?3, runtime_id = ?4, worker_id = ?5, updated_at = ?6 WHERE workspace_id = ?1 AND ticket_id = ?2"#, params![ record.workspace_id, record.ticket_id, record.assignment_id, record.worker.runtime_id, record.worker.worker_id, record.assigned_at, ], ) } else { tx.execute( r#"INSERT INTO ticket_current_worker_assignments ( workspace_id, ticket_id, assignment_id, runtime_id, worker_id, updated_at ) VALUES (?1, ?2, ?3, ?4, ?5, ?6)"#, params![ record.workspace_id, record.ticket_id, record.assignment_id, record.worker.runtime_id, record.worker.worker_id, record.assigned_at, ], ) }; if let Err(error) = current_write { return Err(map_assignment_constraint( error, &record.ticket_id, &record.worker.worker_id, )); } tx.execute( r#"INSERT INTO ticket_worker_assignment_events ( workspace_id, ticket_id, event_id, action, assignment_id, previous_assignment_id, actor, created_at ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)"#, params![ record.workspace_id, record.ticket_id, event_id, if previous.is_some() { "reassigned" } else { "assigned" }, record.assignment_id, previous .as_ref() .map(|assignment| assignment.assignment_id.as_str()), record.assigned_by, record.assigned_at, ], )?; if reserved_operation { let updated = tx.execute( r#"UPDATE ticket_assignment_operations SET assignment_id = ?3 WHERE workspace_id = ?1 AND operation_id = ?2 AND assignment_id IS NULL"#, params![record.workspace_id, operation_id, record.assignment_id], )?; if updated != 1 { return Err(Error::TicketAssignmentConflict(format!( "assignment operation {operation_id} reservation was not current" ))); } } else { tx.execute( r#"INSERT INTO ticket_assignment_operations ( workspace_id, operation_id, action, ticket_id, runtime_id, worker_id, assignment_id, expected_assignment_id, created_at ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)"#, params![ record.workspace_id, operation_id, if allow_reassign { "reassign" } else { "assign" }, record.ticket_id, record.worker.runtime_id, record.worker.worker_id, record.assignment_id, expected_assignment_id, record.assigned_at, ], )?; } tx.commit()?; Ok(TicketWorkerAssignmentUpdate { current: record.clone(), previous, }) }) } fn clear_current_ticket_worker_assignment( &self, workspace_id: &str, ticket_id: &str, expected_assignment_id: Option<&str>, operation_id: &str, event_id: &str, actor: &str, created_at: &str, ) -> Result> { self.with_conn(|conn| { let tx = conn.unchecked_transaction()?; if let Some(existing) = read_assignment_operation(&tx, workspace_id, operation_id)? { if existing.action != "unassign" || existing.ticket_id != ticket_id || existing.expected_assignment_id.as_deref() != expected_assignment_id { return Err(Error::TicketAssignmentConflict(format!( "assignment operation {operation_id} was already used with different input" ))); } let assignment = existing .assignment_id .map(|assignment_id| { tx.query_row( r#"SELECT workspace_id, ticket_id, assignment_id, runtime_id, worker_id, assigned_by, assigned_at FROM ticket_worker_assignments WHERE workspace_id = ?1 AND ticket_id = ?2 AND assignment_id = ?3"#, params![workspace_id, ticket_id, assignment_id], read_ticket_worker_assignment_record, ) }) .transpose()?; tx.commit()?; return Ok(assignment); } let previous = tx .query_row( current_ticket_worker_assignment_select_sql().as_str(), params![workspace_id, ticket_id], read_ticket_worker_assignment_record, ) .optional()?; require_expected_ticket_assignment(ticket_id, previous.as_ref(), expected_assignment_id)?; let Some(previous) = previous else { tx.commit()?; return Ok(None); }; tx.execute( "DELETE FROM ticket_current_worker_assignments WHERE workspace_id = ?1 AND ticket_id = ?2", params![workspace_id, ticket_id], )?; tx.execute( r#"INSERT INTO ticket_worker_assignment_events ( workspace_id, ticket_id, event_id, action, assignment_id, previous_assignment_id, actor, created_at ) VALUES (?1, ?2, ?3, 'unassigned', NULL, ?4, ?5, ?6)"#, params![ workspace_id, ticket_id, event_id, previous.assignment_id, actor, created_at, ], )?; tx.execute( r#"INSERT INTO ticket_assignment_operations ( workspace_id, operation_id, action, ticket_id, runtime_id, worker_id, assignment_id, expected_assignment_id, created_at ) VALUES (?1, ?2, 'unassign', ?3, ?4, ?5, ?6, ?7, ?8)"#, params![ workspace_id, operation_id, ticket_id, previous.worker.runtime_id, previous.worker.worker_id, previous.assignment_id, expected_assignment_id, created_at, ], )?; tx.commit()?; Ok(Some(previous)) }) } fn list_ticket_worker_assignment_events( &self, workspace_id: &str, ticket_id: &str, limit: usize, ) -> Result> { self.with_conn(|conn| { let mut stmt = conn.prepare( r#"SELECT workspace_id, ticket_id, event_id, action, assignment_id, previous_assignment_id, actor, created_at FROM ticket_worker_assignment_events WHERE workspace_id = ?1 AND ticket_id = ?2 ORDER BY created_at DESC, event_id DESC LIMIT ?3"#, )?; let rows = stmt.query_map( params![workspace_id, ticket_id, limit as i64], read_ticket_worker_assignment_event_record, )?; rows.collect::, _>>() .map_err(Error::from) }) } fn upsert_workdir_registry(&self, record: &WorkdirRegistryRecord) -> Result<()> { self.with_conn(|conn| { conn.execute( r#"INSERT INTO workdir_registry ( workspace_id, workdir_id, runtime_id, repository_id, creation_selector, creation_ref, current_selector, current_ref, materialization_status, cleanliness, created_at, updated_at ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12) ON CONFLICT(workspace_id, workdir_id) DO UPDATE SET runtime_id = excluded.runtime_id, repository_id = excluded.repository_id, creation_selector = excluded.creation_selector, creation_ref = excluded.creation_ref, current_selector = excluded.current_selector, current_ref = excluded.current_ref, materialization_status = excluded.materialization_status, cleanliness = excluded.cleanliness, updated_at = excluded.updated_at"#, params![ record.workspace_id, record.workdir_id, record.runtime_id, record.repository_id, record.creation_selector, record.creation_ref, record.current_selector, record.current_ref, record.materialization_status, record.cleanliness, record.created_at, record.updated_at, ], )?; Ok(()) }) } fn get_workdir_registry( &self, workspace_id: &str, workdir_id: &str, ) -> Result> { self.with_conn(|conn| { conn.query_row( workdir_registry_select_sql("WHERE workspace_id = ?1 AND workdir_id = ?2").as_str(), params![workspace_id, workdir_id], read_workdir_registry_record, ) .optional() .map_err(Error::from) }) } fn list_workdir_registry( &self, workspace_id: &str, limit: usize, ) -> Result> { self.with_conn(|conn| { let sql = workdir_registry_select_sql( "WHERE workspace_id = ?1 ORDER BY updated_at DESC LIMIT ?2", ); let mut stmt = conn.prepare(sql.as_str())?; let rows = stmt.query_map( params![workspace_id, limit as i64], read_workdir_registry_record, )?; rows.collect::, _>>() .map_err(Error::from) }) } fn delete_workdir_registry(&self, workspace_id: &str, workdir_id: &str) -> Result { self.with_conn(|conn| { let tx = rusqlite::Transaction::new_unchecked( conn, rusqlite::TransactionBehavior::Immediate, )?; let blocked: bool = tx.query_row( r#"SELECT EXISTS( SELECT 1 FROM worker_workdir_links WHERE workspace_id = ?1 AND workdir_id = ?2 AND unlinked_at IS NULL UNION ALL SELECT 1 FROM worker_workdir_attachment_reservations WHERE workspace_id = ?1 AND workdir_id = ?2 )"#, params![workspace_id, workdir_id], |row| row.get(0), )?; if blocked { return Err(Error::WorkdirAttachmentConflict(format!( "Workdir {workdir_id} has an active or pending Worker attachment" ))); } let changed = tx.execute( "DELETE FROM workdir_registry WHERE workspace_id = ?1 AND workdir_id = ?2", params![workspace_id, workdir_id], )?; tx.commit()?; Ok(changed > 0) }) } fn reserve_worker_workdir_attachment( &self, workspace_id: &str, workdir_id: &str, reservation_id: &str, reserved_at: &str, ) -> Result<()> { self.with_conn(|conn| { let tx = rusqlite::Transaction::new_unchecked( conn, rusqlite::TransactionBehavior::Immediate, )?; let registered: bool = tx.query_row( r#"SELECT EXISTS( SELECT 1 FROM workdir_registry WHERE workspace_id = ?1 AND workdir_id = ?2 )"#, params![workspace_id, workdir_id], |row| row.get(0), )?; if !registered { return Err(Error::WorkdirAttachmentConflict(format!( "Workdir {workdir_id} is not registered in Workspace {workspace_id}" ))); } let occupied: bool = tx.query_row( r#"SELECT EXISTS( SELECT 1 FROM worker_workdir_links WHERE workspace_id = ?1 AND workdir_id = ?2 AND unlinked_at IS NULL )"#, params![workspace_id, workdir_id], |row| row.get(0), )?; if occupied { return Err(Error::WorkdirAttachmentConflict(format!( "Workdir {workdir_id} already has an active attachment" ))); } tx.execute( r#"INSERT INTO worker_workdir_attachment_reservations ( workspace_id, workdir_id, reservation_id, reserved_at ) VALUES (?1, ?2, ?3, ?4)"#, params![workspace_id, workdir_id, reservation_id, reserved_at], ) .map_err(|error| { if matches!(error, rusqlite::Error::SqliteFailure(ref code, _) if code.extended_code == rusqlite::ffi::SQLITE_CONSTRAINT_PRIMARYKEY || code.extended_code == rusqlite::ffi::SQLITE_CONSTRAINT_UNIQUE) { Error::WorkdirAttachmentConflict(format!( "Workdir {workdir_id} already has a pending attachment reservation" )) } else { error.into() } })?; tx.commit()?; Ok(()) }) } fn release_worker_workdir_attachment_reservation( &self, workspace_id: &str, workdir_id: &str, reservation_id: &str, ) -> Result<()> { self.with_conn(|conn| { conn.execute( r#"DELETE FROM worker_workdir_attachment_reservations WHERE workspace_id = ?1 AND workdir_id = ?2 AND reservation_id = ?3"#, params![workspace_id, workdir_id, reservation_id], )?; Ok(()) }) } fn finalize_reserved_worker_workdir_attachment( &self, record: &WorkerWorkdirLinkRecord, reservation_id: &str, ) -> Result { if record.role != "attachment" || record.unlinked_at.is_some() { return Err(Error::WorkdirAttachmentConflict( "reserved attachment finalization requires an active canonical attachment" .to_string(), )); } self.with_conn(|conn| { let tx = rusqlite::Transaction::new_unchecked( conn, rusqlite::TransactionBehavior::Immediate, )?; let owns_reservation: bool = tx.query_row( r#"SELECT EXISTS( SELECT 1 FROM worker_workdir_attachment_reservations WHERE workspace_id = ?1 AND workdir_id = ?2 AND reservation_id = ?3 )"#, params![record.workspace_id, record.workdir_id, reservation_id], |row| row.get(0), )?; if !owns_reservation { return Err(Error::WorkdirAttachmentConflict(format!( "Workdir {} attachment reservation is missing or owned by another spawn", record.workdir_id ))); } tx.execute( r#"INSERT INTO worker_workdir_links ( workspace_id, runtime_id, worker_id, workdir_id, role, linked_at, unlinked_at ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, NULL) ON CONFLICT(workspace_id, worker_id, workdir_id, role) DO UPDATE SET linked_at = excluded.linked_at, unlinked_at = NULL"#, params![ record.workspace_id, record.worker.runtime_id, record.worker.worker_id, record.workdir_id, record.role, record.linked_at, ], ) .map_err(|error| { if matches!(error, rusqlite::Error::SqliteFailure(ref code, _) if code.extended_code == rusqlite::ffi::SQLITE_CONSTRAINT_UNIQUE) { Error::WorkdirAttachmentConflict( "Worker or Workdir acquired another active attachment".to_string(), ) } else { error.into() } })?; tx.execute( r#"DELETE FROM worker_workdir_attachment_reservations WHERE workspace_id = ?1 AND workdir_id = ?2 AND reservation_id = ?3"#, params![record.workspace_id, record.workdir_id, reservation_id], )?; tx.commit()?; Ok(record.clone()) }) } fn attach_worker_workdir( &self, record: &WorkerWorkdirLinkRecord, ) -> Result { if record.role != "attachment" { return Err(Error::WorkdirAttachmentConflict(format!( "unsupported Workdir attachment role `{}`", record.role ))); } if record.unlinked_at.is_some() { return Err(Error::WorkdirAttachmentConflict( "a new attachment cannot already be unlinked".to_string(), )); } self.with_conn(|conn| { let tx = rusqlite::Transaction::new_unchecked( conn, rusqlite::TransactionBehavior::Immediate, )?; let registered: bool = tx.query_row( r#"SELECT EXISTS( SELECT 1 FROM workdir_registry WHERE workspace_id = ?1 AND workdir_id = ?2 )"#, params![record.workspace_id, record.workdir_id], |row| row.get(0), )?; if !registered { return Err(Error::WorkdirAttachmentConflict(format!( "Workdir {} is not registered in Workspace {}", record.workdir_id, record.workspace_id ))); } let reserved: bool = tx.query_row( r#"SELECT EXISTS( SELECT 1 FROM worker_workdir_attachment_reservations WHERE workspace_id = ?1 AND workdir_id = ?2 )"#, params![record.workspace_id, record.workdir_id], |row| row.get(0), )?; if reserved { return Err(Error::WorkdirAttachmentConflict(format!( "Workdir {} has a pending attachment reservation", record.workdir_id ))); } let active_for_worker = tx .query_row( r#"SELECT workspace_id, runtime_id, worker_id, workdir_id, role, linked_at, unlinked_at FROM worker_workdir_links WHERE workspace_id = ?1 AND runtime_id = ?2 AND worker_id = ?3 AND unlinked_at IS NULL"#, params![ record.workspace_id, record.worker.runtime_id, record.worker.worker_id, ], read_worker_workdir_link_record, ) .optional()?; if let Some(active) = active_for_worker { if active.workdir_id == record.workdir_id { tx.commit()?; return Ok(active); } return Err(Error::WorkdirAttachmentConflict(format!( "Worker {}:{} is already attached to Workdir {}", record.worker.runtime_id, record.worker.worker_id, active.workdir_id ))); } let active_for_workdir = tx .query_row( r#"SELECT workspace_id, runtime_id, worker_id, workdir_id, role, linked_at, unlinked_at FROM worker_workdir_links WHERE workspace_id = ?1 AND workdir_id = ?2 AND unlinked_at IS NULL"#, params![record.workspace_id, record.workdir_id], read_worker_workdir_link_record, ) .optional()?; if let Some(active) = active_for_workdir { return Err(Error::WorkdirAttachmentConflict(format!( "Workdir {} is already attached to Worker {}:{}", record.workdir_id, active.worker.runtime_id, active.worker.worker_id ))); } let write = tx.execute( r#"INSERT INTO worker_workdir_links ( workspace_id, runtime_id, worker_id, workdir_id, role, linked_at, unlinked_at ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, NULL) ON CONFLICT(workspace_id, worker_id, workdir_id, role) DO UPDATE SET linked_at = excluded.linked_at, unlinked_at = NULL"#, params![ record.workspace_id, record.worker.runtime_id, record.worker.worker_id, record.workdir_id, record.role, record.linked_at, ], ); if let Err(error) = write { if matches!(error, rusqlite::Error::SqliteFailure(ref code, _) if code.extended_code == rusqlite::ffi::SQLITE_CONSTRAINT_UNIQUE) { return Err(Error::WorkdirAttachmentConflict( "Worker or Workdir acquired another active attachment".to_string(), )); } return Err(error.into()); } tx.commit()?; Ok(record.clone()) }) } fn detach_worker_workdir( &self, workspace_id: &str, worker: &RuntimeWorkerRef, expected_workdir_id: Option<&str>, unlinked_at: &str, ) -> Result> { self.with_conn(|conn| { let tx = rusqlite::Transaction::new_unchecked( conn, rusqlite::TransactionBehavior::Immediate, )?; let active = tx .query_row( r#"SELECT workspace_id, runtime_id, worker_id, workdir_id, role, linked_at, unlinked_at FROM worker_workdir_links WHERE workspace_id = ?1 AND runtime_id = ?2 AND worker_id = ?3 AND unlinked_at IS NULL"#, params![workspace_id, worker.runtime_id, worker.worker_id], read_worker_workdir_link_record, ) .optional()?; let Some(active) = active else { tx.commit()?; return Ok(None); }; if let Some(expected_workdir_id) = expected_workdir_id { if active.workdir_id != expected_workdir_id { return Err(Error::WorkdirAttachmentConflict(format!( "Worker {}:{} is attached to Workdir {}, not {expected_workdir_id}", worker.runtime_id, worker.worker_id, active.workdir_id ))); } } let changed = tx.execute( r#"UPDATE worker_workdir_links SET unlinked_at = ?4 WHERE workspace_id = ?1 AND runtime_id = ?2 AND worker_id = ?3 AND unlinked_at IS NULL"#, params![workspace_id, worker.runtime_id, worker.worker_id, unlinked_at], )?; if changed != 1 { return Err(Error::WorkdirAttachmentConflict(format!( "Worker {}:{} attachment changed during detach", worker.runtime_id, worker.worker_id ))); } tx.commit()?; Ok(Some(WorkerWorkdirLinkRecord { unlinked_at: Some(unlinked_at.to_string()), ..active })) }) } fn worker_workdir_link_history_exists( &self, workspace_id: &str, worker: &RuntimeWorkerRef, ) -> Result { self.with_conn(|conn| { let exists = conn.query_row( r#"SELECT EXISTS( SELECT 1 FROM worker_workdir_links WHERE workspace_id = ?1 AND runtime_id = ?2 AND worker_id = ?3 )"#, params![workspace_id, worker.runtime_id, worker.worker_id], |row| row.get(0), )?; Ok(exists) }) } fn list_worker_workdir_links( &self, workspace_id: &str, worker: &RuntimeWorkerRef, ) -> Result> { self.with_conn(|conn| { let mut stmt = conn.prepare( r#"SELECT workspace_id, runtime_id, worker_id, workdir_id, role, linked_at, unlinked_at FROM worker_workdir_links WHERE workspace_id = ?1 AND runtime_id = ?2 AND worker_id = ?3 AND unlinked_at IS NULL ORDER BY linked_at DESC"#, )?; let rows = stmt.query_map( params![workspace_id, worker.runtime_id, worker.worker_id], read_worker_workdir_link_record, )?; rows.collect::, _>>() .map_err(Error::from) }) } fn list_workdir_worker_links( &self, workspace_id: &str, workdir_id: &str, ) -> Result> { self.with_conn(|conn| { let mut stmt = conn.prepare( r#"SELECT workspace_id, runtime_id, worker_id, workdir_id, role, linked_at, unlinked_at FROM worker_workdir_links WHERE workspace_id = ?1 AND workdir_id = ?2 AND unlinked_at IS NULL ORDER BY linked_at DESC"#, )?; let rows = stmt.query_map( params![workspace_id, workdir_id], read_worker_workdir_link_record, )?; rows.collect::, _>>() .map_err(Error::from) }) } } fn flow_source_name(path: &str) -> Result { let file_name = path .rsplit('/') .next() .filter(|value| !value.is_empty()) .ok_or_else(|| Error::Store("Flow source path has no file name".to_string()))?; let name = file_name .strip_suffix(".dcdl") .filter(|value| !value.is_empty()) .ok_or_else(|| Error::Store("Flow source path must end in .dcdl".to_string()))?; flow::FlowSelector::builtin(name) .map_err(|error| Error::Store(format!("invalid Flow source slug: {error}")))?; Ok(name.to_string()) } fn read_flow_source_record(row: &rusqlite::Row<'_>) -> rusqlite::Result { let source_kind = match row.get::<_, String>(2)?.as_str() { "builtin" => FlowSourceKind::Builtin, "workspace" => FlowSourceKind::Workspace, other => { return Err(rusqlite::Error::FromSqlConversionFailure( 2, rusqlite::types::Type::Text, format!("invalid Flow source kind {other:?}").into(), )); } }; let revision = row.get::<_, i64>(7)?; Ok(FlowSourceRecord { workspace_id: row.get(0)?, flow_id: row.get(1)?, source_kind, name: row.get(3)?, path: row.get(4)?, content: row.get(5)?, content_digest: row.get(6)?, revision: u64::try_from(revision).map_err(|error| { rusqlite::Error::FromSqlConversionFailure( 7, rusqlite::types::Type::Integer, Box::new(error), ) })?, created_at: row.get(8)?, updated_at: row.get(9)?, }) } fn read_flow_source_revision_record( row: &rusqlite::Row<'_>, ) -> rusqlite::Result { let revision = row.get::<_, i64>(2)?; let definition_json = row.get::<_, String>(5)?; let definition = serde_json::from_str(&definition_json).map_err(|error| { rusqlite::Error::FromSqlConversionFailure(5, rusqlite::types::Type::Text, Box::new(error)) })?; Ok(FlowSourceRevisionRecord { workspace_id: row.get(0)?, flow_id: row.get(1)?, revision: u64::try_from(revision).map_err(|error| { rusqlite::Error::FromSqlConversionFailure( 2, rusqlite::types::Type::Integer, Box::new(error), ) })?, content: row.get(3)?, content_digest: row.get(4)?, definition, created_at: row.get(6)?, }) } fn read_workspace_record(row: &rusqlite::Row<'_>) -> rusqlite::Result { Ok(WorkspaceRecord { workspace_id: row.get(0)?, owner_account_id: row.get(1)?, display_name: row.get(2)?, state: row.get(3)?, created_at: row.get(4)?, updated_at: row.get(5)?, }) } fn read_repository_record(row: &rusqlite::Row<'_>) -> rusqlite::Result { Ok(RepositoryRecord { workspace_id: row.get(0)?, repository_id: row.get(1)?, name: row.get(2)?, kind: row.get(3)?, provider: row.get(4)?, uri: row.get(5)?, default_ref: row.get(6)?, auth_ref_kind: row.get(7)?, auth_ref_key: row.get(8)?, created_at: row.get(9)?, updated_at: row.get(10)?, }) } fn account_select_sql(where_clause: &str) -> String { format!( "SELECT account_id, kind, handle, display_name, created_at, updated_at FROM accounts {where_clause}" ) } fn read_trusted_runtime_record(row: &rusqlite::Row<'_>) -> rusqlite::Result { Ok(TrustedRuntimeRecord { runtime_id: row.get(0)?, workspace_id: row.get(1)?, display_name: row.get(2)?, base_url: row.get(3)?, public_key: row.get(4)?, created_at: row.get(5)?, updated_at: row.get(6)?, revoked_at: row.get(7)?, }) } fn read_account_record(row: &rusqlite::Row<'_>) -> rusqlite::Result { Ok(AccountRecord { account_id: row.get(0)?, kind: row.get(1)?, handle: row.get(2)?, display_name: row.get(3)?, created_at: row.get(4)?, updated_at: row.get(5)?, }) } fn user_select_sql(where_clause: &str) -> String { format!( "SELECT user_id, account_id, handle, display_name, created_at, updated_at FROM users {where_clause}" ) } fn read_user_record(row: &rusqlite::Row<'_>) -> rusqlite::Result { Ok(UserRecord { user_id: row.get(0)?, account_id: row.get(1)?, handle: row.get(2)?, display_name: row.get(3)?, created_at: row.get(4)?, updated_at: row.get(5)?, }) } fn passkey_select_sql(where_clause: &str) -> String { format!( "SELECT credential_id, user_id, public_key_cose, transports_json, sign_count, created_at, last_used_at FROM passkey_credentials {where_clause}" ) } fn read_passkey_credential_record( row: &rusqlite::Row<'_>, ) -> rusqlite::Result { Ok(PasskeyCredentialRecord { credential_id: row.get(0)?, user_id: row.get(1)?, public_key_cose: row.get(2)?, transports_json: row.get(3)?, sign_count: row.get(4)?, created_at: row.get(5)?, last_used_at: row.get(6)?, }) } fn auth_challenge_select_sql(where_clause: &str) -> String { format!( "SELECT challenge_id, ceremony, challenge, user_id, rp_id, origin, state_json, expires_at, created_at, consumed_at FROM auth_challenges {where_clause}" ) } fn read_auth_challenge_record(row: &rusqlite::Row<'_>) -> rusqlite::Result { Ok(AuthChallengeRecord { challenge_id: row.get(0)?, ceremony: row.get(1)?, challenge: row.get(2)?, user_id: row.get(3)?, rp_id: row.get(4)?, origin: row.get(5)?, state_json: row.get(6)?, expires_at: row.get(7)?, created_at: row.get(8)?, consumed_at: row.get(9)?, }) } fn browser_session_select_sql(where_clause: &str) -> String { format!( "SELECT session_id, token_hash, user_id, created_at, expires_at, revoked_at FROM browser_sessions {where_clause}" ) } fn read_browser_session_record(row: &rusqlite::Row<'_>) -> rusqlite::Result { Ok(BrowserSessionRecord { session_id: row.get(0)?, token_hash: row.get(1)?, user_id: row.get(2)?, created_at: row.get(3)?, expires_at: row.get(4)?, revoked_at: row.get(5)?, }) } fn api_token_select_sql(where_clause: &str) -> String { format!( "SELECT token_id, token_hash, user_id, label, created_at, expires_at, revoked_at, last_used_at FROM api_tokens {where_clause}" ) } fn read_api_token_record(row: &rusqlite::Row<'_>) -> rusqlite::Result { Ok(ApiTokenRecord { token_id: row.get(0)?, token_hash: row.get(1)?, user_id: row.get(2)?, label: row.get(3)?, created_at: row.get(4)?, expires_at: row.get(5)?, revoked_at: row.get(6)?, last_used_at: row.get(7)?, }) } fn device_login_select_sql(where_clause: &str) -> String { format!( "SELECT device_code, user_code, verification_uri, client_name, user_id, api_token_id, issued_access_token, created_at, expires_at, approved_at, consumed_at FROM device_login_flows {where_clause}" ) } fn read_device_login_flow_record( row: &rusqlite::Row<'_>, ) -> rusqlite::Result { Ok(DeviceLoginFlowRecord { device_code: row.get(0)?, user_code: row.get(1)?, verification_uri: row.get(2)?, client_name: row.get(3)?, user_id: row.get(4)?, api_token_id: row.get(5)?, issued_access_token: row.get(6)?, created_at: row.get(7)?, expires_at: row.get(8)?, approved_at: row.get(9)?, consumed_at: row.get(10)?, }) } fn read_worker_workdir_link_record( row: &rusqlite::Row<'_>, ) -> rusqlite::Result { Ok(WorkerWorkdirLinkRecord { workspace_id: row.get(0)?, worker: RuntimeWorkerRef::new(row.get::<_, String>(1)?, row.get::<_, String>(2)?), workdir_id: row.get(3)?, role: row.get(4)?, linked_at: row.get(5)?, unlinked_at: row.get(6)?, }) } fn read_objective_record(row: &rusqlite::Row<'_>) -> rusqlite::Result { Ok(ObjectiveRecord { workspace_id: row.get(0)?, objective_id: row.get(1)?, title: row.get(2)?, state: row.get(3)?, body_md: row.get(4)?, created_at: row.get(5)?, updated_at: row.get(6)?, }) } fn read_objective_ticket_link_record( row: &rusqlite::Row<'_>, ) -> rusqlite::Result { Ok(ObjectiveTicketLinkRecord { workspace_id: row.get(0)?, objective_id: row.get(1)?, ticket_id: row.get(2)?, kind: row.get(3)?, created_at: row.get(4)?, }) } fn read_objective_resource_record( row: &rusqlite::Row<'_>, ) -> rusqlite::Result { Ok(ObjectiveResourceRecord { workspace_id: row.get(0)?, objective_id: row.get(1)?, resource_path: row.get(2)?, body: row.get(3)?, media_type: row.get(4)?, created_at: row.get(5)?, updated_at: row.get(6)?, }) } fn read_memory_document_record(row: &rusqlite::Row<'_>) -> rusqlite::Result { Ok(MemoryDocumentRecord { workspace_id: row.get(0)?, body_md: row.get(1)?, created_at: row.get(2)?, updated_at: row.get(3)?, }) } fn read_memory_staging_record(row: &rusqlite::Row<'_>) -> rusqlite::Result { Ok(MemoryStagingRecord { workspace_id: row.get(0)?, candidate_id: row.get(1)?, raw_json: row.get(2)?, source_path: row.get(3)?, imported_at: row.get(4)?, }) } fn read_memory_staging_resolution_record( row: &rusqlite::Row<'_>, ) -> rusqlite::Result { Ok(MemoryStagingResolutionRecord { workspace_id: row.get(0)?, candidate_id: row.get(1)?, action: row.get(2)?, reason: row.get(3)?, affected_refs_json: row.get(4)?, staging_raw_json: row.get(5)?, source_path: row.get(6)?, imported_at: row.get(7)?, resolved_at: row.get(8)?, }) } fn worker_registry_select_sql(where_clause: &str) -> String { format!( "SELECT workspace_id, runtime_id, worker_id, display_name, profile, \ retention_state, transcript_ref, session_ref, summary_ref, diagnostics_ref, \ created_at, updated_at FROM worker_registry {where_clause}" ) } fn read_worker_registry_record(row: &rusqlite::Row<'_>) -> rusqlite::Result { Ok(WorkerRegistryRecord { workspace_id: row.get(0)?, worker: RuntimeWorkerRef::new(row.get::<_, String>(1)?, row.get::<_, String>(2)?), display_name: row.get(3)?, profile: row.get(4)?, retention_state: row.get(5)?, transcript_ref: row.get(6)?, session_ref: row.get(7)?, summary_ref: row.get(8)?, diagnostics_ref: row.get(9)?, created_at: row.get(10)?, updated_at: row.get(11)?, }) } fn read_worker_control_grant_record( row: &rusqlite::Row<'_>, ) -> rusqlite::Result { let permissions_json: String = row.get(8)?; let permissions = serde_json::from_str(&permissions_json).map_err(|error| { rusqlite::Error::FromSqlConversionFailure(8, rusqlite::types::Type::Text, Box::new(error)) })?; Ok(WorkerControlGrantRecord { workspace_id: row.get(0)?, grant_id: row.get(1)?, controller: RuntimeWorkerRef::new(row.get::<_, String>(2)?, row.get::<_, String>(3)?), subject: RuntimeWorkerRef::new(row.get::<_, String>(4)?, row.get::<_, String>(5)?), relation: row.get(6)?, origin: row.get(7)?, permissions, operation_id: row.get(9)?, created_at: row.get(10)?, revoked_at: row.get(11)?, }) } fn read_worker_control_grant_by_operation( conn: &Connection, workspace_id: &str, controller: &RuntimeWorkerRef, operation_id: &str, ) -> Result> { conn.query_row( r#"SELECT workspace_id, grant_id, controller_runtime_id, controller_worker_id, subject_runtime_id, subject_worker_id, relation, origin, permissions_json, operation_id, created_at, revoked_at FROM worker_control_grants WHERE workspace_id = ?1 AND controller_runtime_id = ?2 AND controller_worker_id = ?3 AND operation_id = ?4"#, params![ workspace_id, controller.runtime_id, controller.worker_id, operation_id, ], read_worker_control_grant_record, ) .optional() .map_err(Error::from) } fn current_ticket_worker_assignment_select_sql() -> String { "SELECT a.workspace_id, a.ticket_id, a.assignment_id, a.runtime_id, a.worker_id, \ a.assigned_by, a.assigned_at \ FROM ticket_current_worker_assignments AS current \ JOIN ticket_worker_assignments AS a \ ON a.workspace_id = current.workspace_id \ AND a.ticket_id = current.ticket_id \ AND a.assignment_id = current.assignment_id \ WHERE current.workspace_id = ?1 AND current.ticket_id = ?2" .to_owned() } fn read_ticket_worker_assignment_record( row: &rusqlite::Row<'_>, ) -> rusqlite::Result { Ok(TicketWorkerAssignmentRecord { workspace_id: row.get(0)?, ticket_id: row.get(1)?, assignment_id: row.get(2)?, worker: RuntimeWorkerRef::new(row.get::<_, String>(3)?, row.get::<_, String>(4)?), assigned_by: row.get(5)?, assigned_at: row.get(6)?, }) } fn read_ticket_worker_assignment_event_record( row: &rusqlite::Row<'_>, ) -> rusqlite::Result { Ok(TicketWorkerAssignmentEventRecord { workspace_id: row.get(0)?, ticket_id: row.get(1)?, event_id: row.get(2)?, action: row.get(3)?, assignment_id: row.get(4)?, previous_assignment_id: row.get(5)?, actor: row.get(6)?, created_at: row.get(7)?, }) } #[derive(Debug, Clone, PartialEq, Eq)] pub struct TicketAssignmentOperationRecord { pub action: String, pub ticket_id: String, pub runtime_id: Option, pub worker: Option, pub assignment_id: Option, pub expected_assignment_id: Option, pub request_fingerprint: Option, } fn read_assignment_operation( conn: &Connection, workspace_id: &str, operation_id: &str, ) -> Result> { conn.query_row( r#"SELECT action, ticket_id, runtime_id, worker_id, assignment_id, expected_assignment_id, request_fingerprint FROM ticket_assignment_operations WHERE workspace_id = ?1 AND operation_id = ?2"#, params![workspace_id, operation_id], |row| { let runtime_id: Option = row.get(2)?; let worker_id: Option = row.get(3)?; Ok(TicketAssignmentOperationRecord { action: row.get(0)?, ticket_id: row.get(1)?, runtime_id: runtime_id.clone(), worker: runtime_id .zip(worker_id) .map(|(runtime_id, worker_id)| RuntimeWorkerRef::new(runtime_id, worker_id)), assignment_id: row.get(4)?, expected_assignment_id: row.get(5)?, request_fingerprint: row.get(6)?, }) }, ) .optional() .map_err(Error::from) } fn map_assignment_constraint(error: rusqlite::Error, ticket_id: &str, worker_id: &str) -> Error { if matches!(error, rusqlite::Error::SqliteFailure(_, _)) { Error::TicketAssignmentConflict(format!( "Ticket {ticket_id} or Worker {worker_id} already has a current assignment" )) } else { Error::Sqlite(error) } } fn require_expected_ticket_assignment( ticket_id: &str, current: Option<&TicketWorkerAssignmentRecord>, expected_assignment_id: Option<&str>, ) -> Result<()> { let Some(expected_assignment_id) = expected_assignment_id else { return Ok(()); }; if current.map(|assignment| assignment.assignment_id.as_str()) == Some(expected_assignment_id) { return Ok(()); } Err(Error::TicketAssignmentConflict(format!( "Ticket {ticket_id} is no longer assigned to {expected_assignment_id}" ))) } fn workdir_registry_select_sql(where_clause: &str) -> String { format!( "SELECT workspace_id, workdir_id, runtime_id, repository_id, \ creation_selector, creation_ref, current_selector, current_ref, \ materialization_status, cleanliness, created_at, updated_at \ FROM workdir_registry {where_clause}" ) } fn read_workdir_registry_record( row: &rusqlite::Row<'_>, ) -> rusqlite::Result { Ok(WorkdirRegistryRecord { workspace_id: row.get(0)?, workdir_id: row.get(1)?, runtime_id: row.get(2)?, repository_id: row.get(3)?, creation_selector: row.get(4)?, creation_ref: row.get(5)?, current_selector: row.get(6)?, current_ref: row.get(7)?, materialization_status: row.get(8)?, cleanliness: row.get(9)?, created_at: row.get(10)?, updated_at: row.get(11)?, }) } fn create_worker_workdir_registry_tables(conn: &Connection) -> Result<()> { conn.execute_batch( r#" CREATE TABLE IF NOT EXISTS worker_registry ( workspace_id TEXT NOT NULL, runtime_id TEXT NOT NULL, runtime_worker_id INTEGER NOT NULL, display_name TEXT NOT NULL, profile TEXT, retention_state TEXT NOT NULL CHECK (retention_state IN ('normal', 'pinned')), transcript_ref TEXT, session_ref TEXT, summary_ref TEXT, diagnostics_ref TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, PRIMARY KEY (workspace_id, runtime_id, runtime_worker_id), FOREIGN KEY (workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS workdir_registry ( workspace_id TEXT NOT NULL, workdir_id TEXT NOT NULL, runtime_id TEXT NOT NULL, repository_id TEXT NOT NULL, selector TEXT, resolved_commit TEXT, materialization_status TEXT NOT NULL CHECK (materialization_status IN ('pending', 'present', 'not_found', 'corrupted', 'unknown', 'failed')), cleanliness TEXT NOT NULL CHECK (cleanliness IN ('clean', 'dirty', 'unknown')), created_at TEXT NOT NULL, updated_at TEXT NOT NULL, PRIMARY KEY (workspace_id, workdir_id), FOREIGN KEY (workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS worker_workdir_links ( workspace_id TEXT NOT NULL, runtime_id TEXT NOT NULL, runtime_worker_id INTEGER NOT NULL, workdir_id TEXT NOT NULL, role TEXT NOT NULL, linked_at TEXT NOT NULL, unlinked_at TEXT, PRIMARY KEY (workspace_id, runtime_id, runtime_worker_id, workdir_id, role), FOREIGN KEY (workspace_id, runtime_id, runtime_worker_id) REFERENCES worker_registry(workspace_id, runtime_id, runtime_worker_id) ON DELETE CASCADE, FOREIGN KEY (workspace_id, workdir_id) REFERENCES workdir_registry(workspace_id, workdir_id) ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS idx_worker_registry_workspace_updated ON worker_registry(workspace_id, updated_at DESC); CREATE INDEX IF NOT EXISTS idx_workdir_registry_workspace_updated ON workdir_registry(workspace_id, updated_at DESC); CREATE INDEX IF NOT EXISTS idx_worker_workdir_links_worker ON worker_workdir_links(workspace_id, runtime_id, runtime_worker_id, linked_at DESC); "#, )?; Ok(()) } fn add_webauthn_challenge_state(conn: &Connection) -> Result<()> { if !column_exists(conn, "auth_challenges", "state_json")? { conn.execute_batch("ALTER TABLE auth_challenges ADD COLUMN state_json TEXT;")?; } Ok(()) } fn create_objective_sqlite_authority_tables(conn: &Connection) -> Result<()> { conn.execute_batch( r#" PRAGMA foreign_keys = OFF; ALTER TABLE objective_ticket_links RENAME TO objective_ticket_links_old; CREATE TABLE objective_ticket_links ( workspace_id TEXT NOT NULL, objective_id TEXT NOT NULL, ticket_id TEXT NOT NULL, kind TEXT NOT NULL, created_at TEXT NOT NULL, PRIMARY KEY (objective_id, ticket_id, kind), FOREIGN KEY (workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE, FOREIGN KEY (objective_id) REFERENCES objectives(objective_id) ON DELETE CASCADE ); INSERT OR IGNORE INTO objective_ticket_links (workspace_id, objective_id, ticket_id, kind, created_at) SELECT workspace_id, objective_id, ticket_id, kind, created_at FROM objective_ticket_links_old; DROP TABLE objective_ticket_links_old; PRAGMA foreign_keys = ON; CREATE TABLE IF NOT EXISTS objective_resources ( workspace_id TEXT NOT NULL, objective_id TEXT NOT NULL, resource_path TEXT NOT NULL, body TEXT NOT NULL, media_type TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, PRIMARY KEY (objective_id, resource_path), FOREIGN KEY (workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE, FOREIGN KEY (objective_id) REFERENCES objectives(objective_id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS memory_staging_records ( workspace_id TEXT NOT NULL, candidate_id TEXT NOT NULL, raw_json TEXT NOT NULL, source_path TEXT, imported_at TEXT NOT NULL, PRIMARY KEY (workspace_id, candidate_id), FOREIGN KEY (workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ); "#, )?; Ok(()) } fn create_memory_authority_tables(conn: &Connection) -> Result<()> { conn.execute_batch( r#" CREATE TABLE IF NOT EXISTS workspace_memory_documents ( workspace_id TEXT PRIMARY KEY REFERENCES workspaces(workspace_id) ON DELETE CASCADE, body_md TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS memory_staging_resolutions ( workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE, candidate_id TEXT NOT NULL, action TEXT NOT NULL, reason TEXT NOT NULL, affected_refs_json TEXT NOT NULL, staging_raw_json TEXT NOT NULL, source_path TEXT, imported_at TEXT NOT NULL, resolved_at TEXT NOT NULL, PRIMARY KEY (workspace_id, candidate_id, resolved_at) ); "#, )?; Ok(()) } fn remove_unused_control_plane_ticket_tables(conn: &Connection) -> Result<()> { conn.execute_batch( r#" DROP TABLE IF EXISTS ticket_target_paths; DROP TABLE IF EXISTS ticket_worker_links; DROP TABLE IF EXISTS ticket_targets; DROP TABLE IF EXISTS ticket_relations; DROP TABLE IF EXISTS ticket_events; DROP TABLE IF EXISTS tickets; "#, )?; Ok(()) } fn add_workdir_revision_observations(conn: &Connection) -> Result<()> { if column_exists(conn, "workdir_registry", "selector")? && !column_exists(conn, "workdir_registry", "creation_selector")? { conn.execute_batch( "ALTER TABLE workdir_registry RENAME COLUMN selector TO creation_selector;", )?; } if column_exists(conn, "workdir_registry", "resolved_commit")? && !column_exists(conn, "workdir_registry", "creation_ref")? { conn.execute_batch( "ALTER TABLE workdir_registry RENAME COLUMN resolved_commit TO creation_ref;", )?; } if !column_exists(conn, "workdir_registry", "current_selector")? { conn.execute_batch("ALTER TABLE workdir_registry ADD COLUMN current_selector TEXT;")?; } if !column_exists(conn, "workdir_registry", "current_ref")? { conn.execute_batch("ALTER TABLE workdir_registry ADD COLUMN current_ref TEXT;")?; } Ok(()) } fn create_ticket_worker_assignment_tables(conn: &Connection) -> Result<()> { conn.execute_batch( r#" CREATE TABLE IF NOT EXISTS ticket_worker_assignments ( workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE, ticket_id TEXT NOT NULL, assignment_id TEXT NOT NULL, runtime_id TEXT NOT NULL, worker_id TEXT NOT NULL, assigned_by TEXT NOT NULL, assigned_at TEXT NOT NULL, PRIMARY KEY (workspace_id, assignment_id), UNIQUE (workspace_id, ticket_id, assignment_id) ); CREATE TABLE IF NOT EXISTS ticket_current_worker_assignments ( workspace_id TEXT NOT NULL, ticket_id TEXT NOT NULL, assignment_id TEXT NOT NULL, updated_at TEXT NOT NULL, PRIMARY KEY (workspace_id, ticket_id), FOREIGN KEY (workspace_id, ticket_id, assignment_id) REFERENCES ticket_worker_assignments(workspace_id, ticket_id, assignment_id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS ticket_worker_assignment_events ( workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE, ticket_id TEXT NOT NULL, event_id TEXT NOT NULL, action TEXT NOT NULL CHECK (action IN ('assigned', 'reassigned', 'unassigned')), assignment_id TEXT, previous_assignment_id TEXT, actor TEXT NOT NULL, created_at TEXT NOT NULL, PRIMARY KEY (workspace_id, event_id) ); CREATE INDEX IF NOT EXISTS idx_ticket_assignments_worker ON ticket_worker_assignments(workspace_id, runtime_id, worker_id, assigned_at DESC); CREATE INDEX IF NOT EXISTS idx_ticket_assignment_events_ticket ON ticket_worker_assignment_events(workspace_id, ticket_id, created_at DESC); "#, )?; Ok(()) } fn create_ticket_notification_tables(conn: &Connection) -> Result<()> { conn.execute_batch( r#" CREATE TABLE IF NOT EXISTS worker_workspace_credentials ( credential_id TEXT PRIMARY KEY, token TEXT NOT NULL UNIQUE, workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE, runtime_id TEXT NOT NULL, worker_id TEXT, created_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS ticket_notification_outbox ( notification_id TEXT PRIMARY KEY, workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE, ticket_id TEXT NOT NULL, event_sequence INTEGER NOT NULL, source_runtime_id TEXT NOT NULL, source_worker_id TEXT NOT NULL, previous_state TEXT NOT NULL, current_state TEXT NOT NULL, created_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS ticket_notification_deliveries ( notification_id TEXT NOT NULL REFERENCES ticket_notification_outbox(notification_id) ON DELETE CASCADE, recipient_runtime_id TEXT NOT NULL, recipient_worker_id TEXT NOT NULL, recipient_kind TEXT NOT NULL CHECK (recipient_kind IN ('assigned', 'orchestrator')), attempts INTEGER NOT NULL DEFAULT 0, delivered_at TEXT, last_error TEXT, PRIMARY KEY (notification_id, recipient_runtime_id, recipient_worker_id) ); CREATE INDEX IF NOT EXISTS idx_ticket_notification_pending ON ticket_notification_deliveries(delivered_at, attempts); "#, )?; Ok(()) } fn strengthen_ticket_worker_assignments(conn: &Connection) -> Result<()> { conn.execute_batch( r#" ALTER TABLE ticket_current_worker_assignments RENAME TO ticket_current_worker_assignments_v16; CREATE TABLE ticket_current_worker_assignments ( workspace_id TEXT NOT NULL, ticket_id TEXT NOT NULL, assignment_id TEXT NOT NULL, runtime_id TEXT NOT NULL, worker_id TEXT NOT NULL, updated_at TEXT NOT NULL, PRIMARY KEY (workspace_id, ticket_id), UNIQUE (workspace_id, runtime_id, worker_id), FOREIGN KEY (workspace_id, ticket_id, assignment_id) REFERENCES ticket_worker_assignments(workspace_id, ticket_id, assignment_id) ON DELETE CASCADE ); INSERT INTO ticket_current_worker_assignments ( workspace_id, ticket_id, assignment_id, runtime_id, worker_id, updated_at ) SELECT current.workspace_id, current.ticket_id, current.assignment_id, assignment.runtime_id, assignment.worker_id, current.updated_at FROM ticket_current_worker_assignments_v16 AS current JOIN ticket_worker_assignments AS assignment ON assignment.workspace_id = current.workspace_id AND assignment.ticket_id = current.ticket_id AND assignment.assignment_id = current.assignment_id; DROP TABLE ticket_current_worker_assignments_v16; CREATE TABLE ticket_assignment_operations ( workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE, operation_id TEXT NOT NULL, action TEXT NOT NULL CHECK (action IN ('assign', 'reassign', 'unassign')), ticket_id TEXT NOT NULL, runtime_id TEXT, worker_id TEXT, assignment_id TEXT, expected_assignment_id TEXT, created_at TEXT NOT NULL, PRIMARY KEY (workspace_id, operation_id) ); "#, )?; Ok(()) } fn strengthen_ticket_notifications(conn: &Connection) -> Result<()> { conn.execute_batch( r#" ALTER TABLE worker_workspace_credentials ADD COLUMN expires_at TEXT; ALTER TABLE worker_workspace_credentials ADD COLUMN revoked_at TEXT; ALTER TABLE ticket_notification_outbox ADD COLUMN event_kind TEXT NOT NULL DEFAULT 'comment'; ALTER TABLE ticket_notification_outbox ADD COLUMN source_operation_kind TEXT NOT NULL DEFAULT 'unknown'; ALTER TABLE ticket_notification_outbox ADD COLUMN source_actor_role TEXT NOT NULL DEFAULT 'worker'; ALTER TABLE ticket_notification_outbox ADD COLUMN source_assignment_id TEXT; CREATE TABLE ticket_notification_cursors ( workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE, ticket_id TEXT NOT NULL, runtime_id TEXT NOT NULL, worker_id TEXT NOT NULL, last_event_index INTEGER NOT NULL, updated_at TEXT NOT NULL, PRIMARY KEY (workspace_id, ticket_id, runtime_id, worker_id) ); "#, )?; Ok(()) } fn strengthen_ticket_assignment_lifecycle_reservations(conn: &Connection) -> Result<()> { if !column_exists(conn, "ticket_assignment_operations", "request_fingerprint")? { conn.execute_batch( "ALTER TABLE ticket_assignment_operations ADD COLUMN request_fingerprint TEXT;", )?; } // The Workdir and Ticket branches both used schema version 15 before // integration. A database that ran the Ticket branch through v19 still // needs the Workdir revision projection while already having the lifecycle // reservation column. Reconcile both shapes at the combined v20 boundary. add_workdir_revision_observations(conn)?; Ok(()) } fn remove_worker_workspace_credentials(conn: &Connection) -> Result<()> { conn.execute_batch("DROP TABLE IF EXISTS worker_workspace_credentials;")?; Ok(()) } fn drop_ticket_notification_tables(conn: &Connection) -> Result<()> { conn.execute_batch( r#" DROP TABLE IF EXISTS ticket_notification_cursors; DROP TABLE IF EXISTS ticket_notification_deliveries; DROP TABLE IF EXISTS ticket_notification_outbox; "#, )?; Ok(()) } fn enforce_exclusive_worker_workdir_attachments(conn: &Connection) -> Result<()> { conn.execute_batch( r#" CREATE UNIQUE INDEX IF NOT EXISTS ux_worker_workdir_links_active_worker ON worker_workdir_links(workspace_id, runtime_id, runtime_worker_id) WHERE unlinked_at IS NULL; CREATE UNIQUE INDEX IF NOT EXISTS ux_worker_workdir_links_active_workdir ON worker_workdir_links(workspace_id, workdir_id) WHERE unlinked_at IS NULL; "#, )?; Ok(()) } fn create_worker_workdir_attachment_reservations(conn: &Connection) -> Result<()> { conn.execute_batch( r#" CREATE TABLE IF NOT EXISTS worker_workdir_attachment_reservations ( workspace_id TEXT NOT NULL, workdir_id TEXT NOT NULL, reservation_id TEXT NOT NULL, reserved_at TEXT NOT NULL, PRIMARY KEY (workspace_id, workdir_id) ); CREATE UNIQUE INDEX IF NOT EXISTS ux_worker_workdir_attachment_reservation_id ON worker_workdir_attachment_reservations(workspace_id, reservation_id); "#, )?; Ok(()) } fn create_objective_event_tables(conn: &Connection) -> Result<()> { conn.execute_batch( r#" CREATE TABLE IF NOT EXISTS objective_events ( workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE, objective_id TEXT NOT NULL REFERENCES objectives(objective_id) ON DELETE CASCADE, event_id TEXT PRIMARY KEY, kind TEXT NOT NULL, body_md TEXT, created_at TEXT NOT NULL ); "#, )?; Ok(()) } fn create_trusted_runtime_registry_tables(conn: &Connection) -> Result<()> { conn.execute_batch( r#" CREATE TABLE IF NOT EXISTS trusted_runtime_records ( runtime_id TEXT PRIMARY KEY, display_name TEXT NOT NULL, base_url TEXT NOT NULL, public_key TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, revoked_at TEXT ); "#, )?; Ok(()) } fn create_account_identity_tables(conn: &Connection) -> Result<()> { conn.execute_batch( r#" CREATE TABLE IF NOT EXISTS accounts ( account_id TEXT PRIMARY KEY, kind TEXT NOT NULL CHECK (kind IN ('user', 'organization')), handle TEXT NOT NULL, display_name TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, UNIQUE (kind, handle) ); CREATE TABLE IF NOT EXISTS users ( user_id TEXT PRIMARY KEY, account_id TEXT NOT NULL UNIQUE REFERENCES accounts(account_id) ON DELETE CASCADE, handle TEXT NOT NULL UNIQUE, display_name TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS passkey_credentials ( credential_id TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE, public_key_cose TEXT NOT NULL, transports_json TEXT, sign_count INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL, last_used_at TEXT ); CREATE TABLE IF NOT EXISTS auth_challenges ( challenge_id TEXT PRIMARY KEY, ceremony TEXT NOT NULL CHECK (ceremony IN ('passkey_registration', 'passkey_login')), challenge TEXT NOT NULL UNIQUE, user_id TEXT REFERENCES users(user_id) ON DELETE CASCADE, rp_id TEXT NOT NULL, origin TEXT NOT NULL, state_json TEXT, expires_at TEXT NOT NULL, created_at TEXT NOT NULL, consumed_at TEXT ); CREATE TABLE IF NOT EXISTS browser_sessions ( session_id TEXT PRIMARY KEY, token_hash TEXT NOT NULL UNIQUE, user_id TEXT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE, created_at TEXT NOT NULL, expires_at TEXT NOT NULL, revoked_at TEXT ); CREATE TABLE IF NOT EXISTS api_tokens ( token_id TEXT PRIMARY KEY, token_hash TEXT NOT NULL UNIQUE, user_id TEXT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE, label TEXT NOT NULL, created_at TEXT NOT NULL, expires_at TEXT, revoked_at TEXT, last_used_at TEXT ); CREATE TABLE IF NOT EXISTS device_login_flows ( device_code TEXT PRIMARY KEY, user_code TEXT NOT NULL UNIQUE, verification_uri TEXT NOT NULL, client_name TEXT, user_id TEXT REFERENCES users(user_id) ON DELETE SET NULL, api_token_id TEXT REFERENCES api_tokens(token_id) ON DELETE SET NULL, issued_access_token TEXT, created_at TEXT NOT NULL, expires_at TEXT NOT NULL, approved_at TEXT, consumed_at TEXT ); CREATE INDEX IF NOT EXISTS idx_browser_sessions_token_hash ON browser_sessions(token_hash); CREATE INDEX IF NOT EXISTS idx_api_tokens_token_hash ON api_tokens(token_hash); CREATE INDEX IF NOT EXISTS idx_device_login_user_code ON device_login_flows(user_code); "#, )?; if !column_exists(conn, "workspaces", "owner_account_id")? { conn.execute_batch( "ALTER TABLE workspaces ADD COLUMN owner_account_id TEXT REFERENCES accounts(account_id) ON DELETE SET NULL;", )?; } Ok(()) } pub(crate) fn configure_sqlite(conn: &Connection) -> Result<()> { conn.busy_timeout(Duration::from_millis(5_000))?; conn.execute_batch( r#" PRAGMA foreign_keys = ON; PRAGMA journal_mode = WAL; PRAGMA busy_timeout = 5000; CREATE TABLE IF NOT EXISTS __yoi_schema_migrations ( version INTEGER PRIMARY KEY, name TEXT NOT NULL, applied_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ); "#, )?; Ok(()) } fn create_flow_source_authority(conn: &Connection) -> Result<()> { conn.execute_batch( r#" CREATE TABLE flow_sources ( workspace_id TEXT NOT NULL, flow_id TEXT NOT NULL, source_kind TEXT NOT NULL CHECK (source_kind IN ('builtin', 'workspace')), name TEXT NOT NULL, path TEXT NOT NULL, content TEXT NOT NULL, content_digest TEXT NOT NULL, revision INTEGER NOT NULL CHECK (revision > 0), created_at TEXT NOT NULL, updated_at TEXT NOT NULL, PRIMARY KEY (workspace_id, flow_id), UNIQUE (workspace_id, source_kind, name), FOREIGN KEY (workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ); CREATE TABLE flow_source_revisions ( workspace_id TEXT NOT NULL, flow_id TEXT NOT NULL, revision INTEGER NOT NULL CHECK (revision > 0), content TEXT NOT NULL, content_digest TEXT NOT NULL, definition_json TEXT NOT NULL, created_at TEXT NOT NULL, PRIMARY KEY (workspace_id, flow_id, revision), FOREIGN KEY (workspace_id, flow_id) REFERENCES flow_sources(workspace_id, flow_id) ON DELETE CASCADE ); "#, )?; Ok(()) } fn remove_backend_flow_runtime_authority(conn: &Connection) -> Result<()> { conn.execute_batch( r#" DROP TABLE IF EXISTS flow_events; DROP TABLE IF EXISTS flow_transition_attempts; DROP TABLE IF EXISTS flow_instances; "#, )?; Ok(()) } fn scope_repository_identity_by_workspace(conn: &Connection) -> Result<()> { validate_workspace_resource_references(conn)?; conn.execute_batch( r#" CREATE TABLE repositories_v27 ( workspace_id TEXT NOT NULL, repository_id TEXT NOT NULL, name TEXT NOT NULL, kind TEXT NOT NULL, provider TEXT, uri TEXT NOT NULL, default_ref TEXT, auth_ref_kind TEXT, auth_ref_key TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, PRIMARY KEY (workspace_id, repository_id), FOREIGN KEY (workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ); INSERT INTO repositories_v27 ( workspace_id, repository_id, name, kind, provider, uri, default_ref, auth_ref_kind, auth_ref_key, created_at, updated_at ) SELECT workspace_id, repository_id, name, kind, provider, uri, default_ref, auth_ref_kind, auth_ref_key, created_at, updated_at FROM repositories; CREATE TABLE artifacts_v27 ( workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE, artifact_id TEXT PRIMARY KEY, kind TEXT NOT NULL, uri TEXT NOT NULL, media_type TEXT, sha256 TEXT, size_bytes INTEGER, summary TEXT, created_at TEXT NOT NULL, created_by_kind TEXT NOT NULL, created_by_key TEXT NOT NULL, created_by_display TEXT NOT NULL, created_by_source_kind TEXT, created_by_source_key TEXT, ticket_id TEXT, objective_id TEXT, event_id TEXT, worker_ref_kind TEXT, worker_ref_key TEXT, worker_display TEXT, repository_id TEXT, source_kind TEXT, source_revision TEXT, FOREIGN KEY (workspace_id, repository_id) REFERENCES repositories_v27(workspace_id, repository_id) ); INSERT INTO artifacts_v27 ( workspace_id, artifact_id, kind, uri, media_type, sha256, size_bytes, summary, created_at, created_by_kind, created_by_key, created_by_display, created_by_source_kind, created_by_source_key, ticket_id, objective_id, event_id, worker_ref_kind, worker_ref_key, worker_display, repository_id, source_kind, source_revision ) SELECT workspace_id, artifact_id, kind, uri, media_type, sha256, size_bytes, summary, created_at, created_by_kind, created_by_key, created_by_display, created_by_source_kind, created_by_source_key, ticket_id, objective_id, event_id, worker_ref_kind, worker_ref_key, worker_display, repository_id, source_kind, source_revision FROM artifacts; CREATE TABLE workdir_registry_v27 ( workspace_id TEXT NOT NULL, workdir_id TEXT NOT NULL, runtime_id TEXT NOT NULL, repository_id TEXT NOT NULL, creation_selector TEXT, creation_ref TEXT, materialization_status TEXT NOT NULL CHECK (materialization_status IN ('pending', 'present', 'not_found', 'corrupted', 'unknown', 'failed')), cleanliness TEXT NOT NULL CHECK (cleanliness IN ('clean', 'dirty', 'unknown')), created_at TEXT NOT NULL, updated_at TEXT NOT NULL, current_selector TEXT, current_ref TEXT, PRIMARY KEY (workspace_id, workdir_id), FOREIGN KEY (workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE, FOREIGN KEY (workspace_id, repository_id) REFERENCES repositories_v27(workspace_id, repository_id) ); INSERT INTO workdir_registry_v27 ( workspace_id, workdir_id, runtime_id, repository_id, creation_selector, creation_ref, current_selector, current_ref, materialization_status, cleanliness, created_at, updated_at ) SELECT workspace_id, workdir_id, runtime_id, repository_id, creation_selector, creation_ref, current_selector, current_ref, materialization_status, cleanliness, created_at, updated_at FROM workdir_registry; CREATE TABLE worker_workdir_links_v27 ( workspace_id TEXT NOT NULL, runtime_id TEXT NOT NULL, runtime_worker_id INTEGER NOT NULL, workdir_id TEXT NOT NULL, role TEXT NOT NULL, linked_at TEXT NOT NULL, unlinked_at TEXT, PRIMARY KEY (workspace_id, runtime_id, runtime_worker_id, workdir_id, role), FOREIGN KEY (workspace_id, runtime_id, runtime_worker_id) REFERENCES worker_registry(workspace_id, runtime_id, runtime_worker_id) ON DELETE CASCADE, FOREIGN KEY (workspace_id, workdir_id) REFERENCES workdir_registry_v27(workspace_id, workdir_id) ON DELETE CASCADE ); INSERT INTO worker_workdir_links_v27 ( workspace_id, runtime_id, runtime_worker_id, workdir_id, role, linked_at, unlinked_at ) SELECT workspace_id, runtime_id, runtime_worker_id, workdir_id, role, linked_at, unlinked_at FROM worker_workdir_links; CREATE TABLE worker_workdir_attachment_reservations_v27 ( workspace_id TEXT NOT NULL, workdir_id TEXT NOT NULL, reservation_id TEXT NOT NULL, reserved_at TEXT NOT NULL, PRIMARY KEY (workspace_id, workdir_id), FOREIGN KEY (workspace_id, workdir_id) REFERENCES workdir_registry_v27(workspace_id, workdir_id) ON DELETE CASCADE ); INSERT INTO worker_workdir_attachment_reservations_v27 ( workspace_id, workdir_id, reservation_id, reserved_at ) SELECT workspace_id, workdir_id, reservation_id, reserved_at FROM worker_workdir_attachment_reservations; DROP TABLE worker_workdir_links; DROP TABLE worker_workdir_attachment_reservations; DROP TABLE workdir_registry; DROP TABLE artifacts; DROP TABLE repositories; ALTER TABLE repositories_v27 RENAME TO repositories; ALTER TABLE artifacts_v27 RENAME TO artifacts; ALTER TABLE workdir_registry_v27 RENAME TO workdir_registry; ALTER TABLE worker_workdir_links_v27 RENAME TO worker_workdir_links; ALTER TABLE worker_workdir_attachment_reservations_v27 RENAME TO worker_workdir_attachment_reservations; CREATE INDEX idx_workdir_registry_workspace_updated ON workdir_registry(workspace_id, updated_at DESC); CREATE INDEX idx_worker_workdir_links_worker ON worker_workdir_links(workspace_id, runtime_id, runtime_worker_id, linked_at DESC); CREATE UNIQUE INDEX ux_worker_workdir_links_active_worker ON worker_workdir_links(workspace_id, runtime_id, runtime_worker_id) WHERE unlinked_at IS NULL; CREATE UNIQUE INDEX ux_worker_workdir_links_active_workdir ON worker_workdir_links(workspace_id, workdir_id) WHERE unlinked_at IS NULL; CREATE UNIQUE INDEX ux_worker_workdir_attachment_reservation_id ON worker_workdir_attachment_reservations(workspace_id, reservation_id); "#, )?; Ok(()) } fn create_workspace_catalog_operations(conn: &Connection) -> Result<()> { conn.execute_batch( r#" ALTER TABLE trusted_runtime_records ADD COLUMN workspace_id TEXT REFERENCES workspaces(workspace_id) ON DELETE RESTRICT; CREATE INDEX idx_trusted_runtime_records_workspace ON trusted_runtime_records(workspace_id, revoked_at, runtime_id); CREATE TABLE workspace_create_operations ( operation_key TEXT PRIMARY KEY, request_fingerprint TEXT NOT NULL, workspace_id TEXT NOT NULL UNIQUE, created_at TEXT NOT NULL, FOREIGN KEY (workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ); "#, )?; Ok(()) } fn verify_workspace_resource_constraints(conn: &Connection) -> Result<()> { if current_schema_version(conn)? < 39 { return Ok(()); } for trigger in [ "ticket_assignment_ticket_parent_tombstone", "ticket_assignment_worker_parent_tombstone_delete", "ticket_assignment_worker_parent_tombstone_move", "ticket_worker_assignments_validate_insert", "ticket_worker_assignments_validate_update", "ticket_worker_assignment_events_validate_insert", "ticket_assignment_operations_validate_insert", ] { let exists = conn.query_row( "SELECT EXISTS(SELECT 1 FROM sqlite_schema WHERE type = 'trigger' AND name = ?1)", [trigger], |row| row.get::<_, bool>(0), )?; if !exists { return Err(Error::Store(format!( "Workspace resource constraint trigger `{trigger}` is missing" ))); } } Ok(()) } fn validate_workspace_resource_references(conn: &Connection) -> Result<()> { let diagnostics = workspace_resource_reference_diagnostics(conn)?; if diagnostics.is_empty() { return Ok(()); } Err(Error::Store(format!( "Workspace resource foreign-key preflight failed:\n- {}", diagnostics.join("\n- ") ))) } fn workspace_resource_reference_diagnostics(conn: &Connection) -> Result> { let mut diagnostics = Vec::new(); for (table, repository_nullable) in [ ("workdir_registry", false), ("artifacts", true), ("typed_tickets", true), ("merge_requests", false), ] { if !table_exists(conn, table)? || !column_exists(conn, table, "repository_id")? { continue; } let null_filter = if repository_nullable { "child.repository_id IS NOT NULL AND" } else { "" }; collect_reference_diagnostics( conn, &format!( "SELECT child.workspace_id || '/' || child.repository_id FROM {table} AS child \ WHERE {null_filter} NOT EXISTS (\ SELECT 1 FROM repositories AS parent \ WHERE parent.workspace_id = child.workspace_id \ AND parent.repository_id = child.repository_id\ ) LIMIT 100" ), &format!("{table}.repository_id"), &mut diagnostics, )?; } for (label, sql) in [ ( "typed_ticket_relations.target", "SELECT relation.workspace_id || '/' || relation.ticket_id || ' -> ' || relation.target \ FROM typed_ticket_relations AS relation \ WHERE NOT EXISTS (SELECT 1 FROM typed_tickets AS target \ WHERE target.workspace_id = relation.workspace_id \ AND target.ticket_id = relation.target) LIMIT 100", ), ( "objective_events.objective_id", "SELECT child.workspace_id || '/' || child.event_id || ' -> ' || child.objective_id \ FROM objective_events AS child \ WHERE NOT EXISTS (SELECT 1 FROM objectives AS parent \ WHERE parent.workspace_id = child.workspace_id \ AND parent.objective_id = child.objective_id) LIMIT 100", ), ( "objective_resources.objective_id", "SELECT child.workspace_id || '/' || child.resource_path || ' -> ' || child.objective_id \ FROM objective_resources AS child \ WHERE NOT EXISTS (SELECT 1 FROM objectives AS parent \ WHERE parent.workspace_id = child.workspace_id \ AND parent.objective_id = child.objective_id) LIMIT 100", ), ( "objective_ticket_links.objective_id", "SELECT link.workspace_id || '/' || link.objective_id || ' -> ' || link.ticket_id \ FROM objective_ticket_links AS link \ WHERE NOT EXISTS (SELECT 1 FROM objectives AS objective \ WHERE objective.workspace_id = link.workspace_id \ AND objective.objective_id = link.objective_id) LIMIT 100", ), ( "objective_ticket_links.ticket_id", "SELECT link.workspace_id || '/' || link.objective_id || ' -> ' || link.ticket_id \ FROM objective_ticket_links AS link \ WHERE NOT EXISTS (SELECT 1 FROM typed_tickets AS ticket \ WHERE ticket.workspace_id = link.workspace_id \ AND ticket.ticket_id = link.ticket_id) LIMIT 100", ), ( "ticket_current_worker_assignments.assignment_id", "SELECT current.workspace_id || '/' || current.assignment_id \ FROM ticket_current_worker_assignments AS current \ WHERE NOT EXISTS (SELECT 1 FROM ticket_worker_assignments AS assignment \ WHERE assignment.workspace_id = current.workspace_id \ AND assignment.ticket_id = current.ticket_id \ AND assignment.assignment_id = current.assignment_id \ AND assignment.runtime_id = current.runtime_id \ AND assignment.worker_id = current.worker_id) LIMIT 100", ), ( "ticket_worker_assignment_events.assignment_id", "SELECT event.workspace_id || '/' || event.event_id \ FROM ticket_worker_assignment_events AS event \ WHERE (event.assignment_id IS NOT NULL AND NOT EXISTS (\ SELECT 1 FROM ticket_worker_assignments AS assignment \ WHERE assignment.workspace_id = event.workspace_id \ AND assignment.ticket_id = event.ticket_id \ AND assignment.assignment_id = event.assignment_id)) \ OR (event.previous_assignment_id IS NOT NULL AND NOT EXISTS (\ SELECT 1 FROM ticket_worker_assignments AS assignment \ WHERE assignment.workspace_id = event.workspace_id \ AND assignment.ticket_id = event.ticket_id \ AND assignment.assignment_id = event.previous_assignment_id)) LIMIT 100", ), ( "artifacts.ticket_id", "SELECT artifact.workspace_id || '/' || artifact.artifact_id || ' -> ' || artifact.ticket_id \ FROM artifacts AS artifact WHERE artifact.ticket_id IS NOT NULL \ AND NOT EXISTS (SELECT 1 FROM typed_tickets AS ticket \ WHERE ticket.workspace_id = artifact.workspace_id \ AND ticket.ticket_id = artifact.ticket_id) LIMIT 100", ), ( "artifacts.worker_ref", "SELECT artifact.workspace_id || '/' || artifact.artifact_id \ FROM artifacts AS artifact \ WHERE (artifact.worker_ref_kind IS NULL) != (artifact.worker_ref_key IS NULL) LIMIT 100", ), ( "artifacts.objective_id", "SELECT artifact.workspace_id || '/' || artifact.artifact_id || ' -> ' || artifact.objective_id \ FROM artifacts AS artifact WHERE artifact.objective_id IS NOT NULL \ AND NOT EXISTS (SELECT 1 FROM objectives AS objective \ WHERE objective.workspace_id = artifact.workspace_id \ AND objective.objective_id = artifact.objective_id) LIMIT 100", ), ] { let Some(table) = label.split('.').next() else { continue; }; if !table_exists(conn, table)? { continue; } if sql.contains("typed_tickets") && !table_exists(conn, "typed_tickets")? { continue; } if sql.contains("worker.worker_id") && !column_exists(conn, "worker_registry", "worker_id")? { continue; } if sql.contains("assignment.worker_id") && !column_exists(conn, "ticket_worker_assignments", "worker_id")? { continue; } if sql.contains("current.worker_id") && !column_exists(conn, "ticket_current_worker_assignments", "worker_id")? { continue; } collect_reference_diagnostics(conn, sql, label, &mut diagnostics)?; } // Assignment and operation rows are historical soft references. Schema v39 records an // explicit tombstone before a live Ticket or Worker parent is deleted/moved; older schemas // have no tombstone authority, so every missing live parent remains migration-blocking drift. if table_exists(conn, "ticket_worker_assignments")? && table_exists(conn, "typed_tickets")? { let tombstone_filter = if table_exists(conn, "ticket_assignment_ticket_tombstones")? { "AND NOT EXISTS (SELECT 1 FROM ticket_assignment_ticket_tombstones AS tombstone \ WHERE tombstone.workspace_id = assignment.workspace_id \ AND tombstone.ticket_id = assignment.ticket_id)" } else { "" }; collect_reference_diagnostics( conn, &format!( "SELECT assignment.workspace_id || '/' || assignment.assignment_id || ' -> ' || assignment.ticket_id \ FROM ticket_worker_assignments AS assignment \ WHERE NOT EXISTS (SELECT 1 FROM typed_tickets AS ticket \ WHERE ticket.workspace_id = assignment.workspace_id \ AND ticket.ticket_id = assignment.ticket_id) \ {tombstone_filter} LIMIT 100" ), "ticket_worker_assignments.ticket_id", &mut diagnostics, )?; } if table_exists(conn, "ticket_worker_assignments")? && table_exists(conn, "worker_registry")? && column_exists(conn, "ticket_worker_assignments", "worker_id")? && column_exists(conn, "worker_registry", "worker_id")? { let tombstone_filter = if table_exists(conn, "ticket_assignment_worker_tombstones")? { "AND NOT EXISTS (SELECT 1 FROM ticket_assignment_worker_tombstones AS tombstone \ WHERE tombstone.workspace_id = assignment.workspace_id \ AND tombstone.runtime_id = assignment.runtime_id \ AND tombstone.worker_id = assignment.worker_id)" } else { "" }; collect_reference_diagnostics( conn, &format!( "SELECT assignment.workspace_id || '/' || assignment.assignment_id || ' -> ' || assignment.runtime_id || '/' || assignment.worker_id \ FROM ticket_worker_assignments AS assignment \ WHERE NOT EXISTS (SELECT 1 FROM worker_registry AS worker \ WHERE worker.workspace_id = assignment.workspace_id \ AND worker.runtime_id = assignment.runtime_id \ AND worker.worker_id = assignment.worker_id) \ {tombstone_filter} LIMIT 100" ), "ticket_worker_assignments.worker_id", &mut diagnostics, )?; } if table_exists(conn, "ticket_assignment_operations")? && table_exists(conn, "typed_tickets")? { let tombstone_filter = if table_exists(conn, "ticket_assignment_ticket_tombstones")? { "AND NOT EXISTS (SELECT 1 FROM ticket_assignment_ticket_tombstones AS tombstone \ WHERE tombstone.workspace_id = operation.workspace_id \ AND tombstone.ticket_id = operation.ticket_id)" } else { "" }; collect_reference_diagnostics( conn, &format!( "SELECT operation.workspace_id || '/' || operation.operation_id || ' -> ' || operation.ticket_id \ FROM ticket_assignment_operations AS operation \ WHERE NOT EXISTS (SELECT 1 FROM typed_tickets AS ticket \ WHERE ticket.workspace_id = operation.workspace_id \ AND ticket.ticket_id = operation.ticket_id) \ {tombstone_filter} LIMIT 100" ), "ticket_assignment_operations.ticket_id", &mut diagnostics, )?; } Ok(diagnostics) } fn collect_reference_diagnostics( conn: &Connection, sql: &str, label: &str, diagnostics: &mut Vec, ) -> Result<()> { let mut statement = conn.prepare(sql)?; let rows = statement.query_map([], |row| row.get::<_, String>(0))?; for row in rows { diagnostics.push(format!("{label}: {}", row?)); } Ok(()) } fn current_schema_version(conn: &Connection) -> Result { conn.query_row( "SELECT COALESCE(MAX(version), 0) FROM __yoi_schema_migrations", [], |row| row.get(0), ) .map_err(Error::from) } fn create_workspace_config_source_authority(conn: &Connection) -> Result<()> { conn.execute_batch( r#" CREATE TABLE IF NOT EXISTS workspace_config_trees ( workspace_id TEXT PRIMARY KEY, revision INTEGER NOT NULL CHECK (revision >= 0), tree_digest TEXT NOT NULL, schema_version INTEGER NOT NULL, entrypoints_json TEXT NOT NULL, decodal_version TEXT NOT NULL, import_policy_version INTEGER NOT NULL, toolchain_fingerprint TEXT NOT NULL, projection_digest TEXT NOT NULL, updated_at TEXT NOT NULL, FOREIGN KEY (workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS workspace_config_entries ( workspace_id TEXT NOT NULL, path TEXT NOT NULL, content_type TEXT NOT NULL, content TEXT NOT NULL, content_digest TEXT NOT NULL, PRIMARY KEY (workspace_id, path), FOREIGN KEY (workspace_id) REFERENCES workspace_config_trees(workspace_id) ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS idx_workspace_config_entries_prefix ON workspace_config_entries(workspace_id, path); CREATE TABLE IF NOT EXISTS workspace_config_tree_revisions ( workspace_id TEXT NOT NULL, revision INTEGER NOT NULL, tree_digest TEXT NOT NULL, toolchain_fingerprint TEXT NOT NULL, projection_digest TEXT NOT NULL, manifest_json TEXT NOT NULL, created_at TEXT NOT NULL, PRIMARY KEY (workspace_id, revision), FOREIGN KEY (workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ); "#, )?; Ok(()) } pub(crate) fn persist_workspace_config_schema_bundles(conn: &Connection) -> Result<()> { conn.execute_batch( r#" ALTER TABLE workspace_config_trees ADD COLUMN schema_bundle_json TEXT NOT NULL DEFAULT '{"contributions":[],"source":"{}","fingerprint":""}'; ALTER TABLE workspace_config_tree_revisions ADD COLUMN schema_bundle_json TEXT NOT NULL DEFAULT '{"contributions":[],"source":"{}","fingerprint":""}'; "#, )?; let bundle = config_source::WorkspaceConfigSchemaBundle::empty(); let bundle_json = serde_json::to_string(&bundle).map_err(|error| Error::Store(error.to_string()))?; let contract = config_source::ToolchainContract::with_schema_bundle( config_source::DEFAULT_SCHEMA_VERSION, vec![ config_source::VirtualPath::parse(crate::config_source::MAIN_CONFIG_ENTRYPOINT) .map_err(|error| Error::Store(error.to_string()))?, ], config_source::DEFAULT_IMPORT_POLICY_VERSION, bundle, ); conn.execute( "UPDATE workspace_config_trees SET schema_bundle_json = ?1, toolchain_fingerprint = ?2", params![bundle_json, contract.fingerprint], )?; conn.execute( "UPDATE workspace_config_tree_revisions SET schema_bundle_json = ?1, toolchain_fingerprint = ?2", params![bundle_json, contract.fingerprint], )?; Ok(()) } fn create_worker_control_grant_authority(conn: &Connection) -> Result<()> { conn.execute_batch( r#" CREATE TABLE worker_control_grants ( workspace_id TEXT NOT NULL, grant_id TEXT NOT NULL, controller_runtime_id TEXT NOT NULL, controller_worker_id INTEGER NOT NULL, subject_runtime_id TEXT NOT NULL, subject_worker_id INTEGER NOT NULL, relation TEXT NOT NULL, origin TEXT NOT NULL, permissions_json TEXT NOT NULL, operation_id TEXT NOT NULL, created_at TEXT NOT NULL, revoked_at TEXT, PRIMARY KEY (workspace_id, grant_id), UNIQUE ( workspace_id, controller_runtime_id, controller_worker_id, operation_id ), FOREIGN KEY (workspace_id, controller_runtime_id, controller_worker_id) REFERENCES worker_registry (workspace_id, runtime_id, runtime_worker_id) ON DELETE CASCADE, FOREIGN KEY (workspace_id, subject_runtime_id, subject_worker_id) REFERENCES worker_registry (workspace_id, runtime_id, runtime_worker_id) ON DELETE CASCADE ); CREATE INDEX idx_worker_control_grants_controller_active ON worker_control_grants ( workspace_id, controller_runtime_id, controller_worker_id, revoked_at, created_at ); CREATE INDEX idx_worker_control_grants_subject_active ON worker_control_grants ( workspace_id, subject_runtime_id, subject_worker_id, revoked_at ); "#, )?; Ok(()) } fn create_worker_control_delegation_operation_authority(conn: &Connection) -> Result<()> { conn.execute_batch( r#" CREATE TABLE worker_control_delegation_operations ( workspace_id TEXT NOT NULL, source_controller_runtime_id TEXT NOT NULL, source_controller_worker_id INTEGER NOT NULL, source_grant_id TEXT NOT NULL, operation_id TEXT NOT NULL, input_fingerprint TEXT NOT NULL, delegated_grant_id TEXT, created_at TEXT NOT NULL, completed_at TEXT, PRIMARY KEY ( workspace_id, source_controller_runtime_id, source_controller_worker_id, operation_id ), FOREIGN KEY (workspace_id, source_controller_runtime_id, source_controller_worker_id) REFERENCES worker_registry (workspace_id, runtime_id, runtime_worker_id) ON DELETE CASCADE, FOREIGN KEY (workspace_id, source_grant_id) REFERENCES worker_control_grants (workspace_id, grant_id) ON DELETE CASCADE, FOREIGN KEY (workspace_id, delegated_grant_id) REFERENCES worker_control_grants (workspace_id, grant_id) ON DELETE SET NULL ); "#, )?; Ok(()) } fn add_objective_query_indexes(conn: &Connection) -> Result<()> { conn.execute_batch( r#" CREATE INDEX IF NOT EXISTS objectives_workspace_state_updated ON objectives(workspace_id, state, updated_at DESC, objective_id); CREATE INDEX IF NOT EXISTS objectives_workspace_updated ON objectives(workspace_id, updated_at DESC, objective_id); CREATE INDEX IF NOT EXISTS objectives_workspace_created ON objectives(workspace_id, created_at DESC, objective_id); CREATE INDEX IF NOT EXISTS objectives_workspace_title ON objectives(workspace_id, title COLLATE NOCASE, objective_id); CREATE INDEX IF NOT EXISTS objective_ticket_links_workspace_ticket_objective ON objective_ticket_links(workspace_id, ticket_id, objective_id); "#, )?; Ok(()) } fn collect_legacy_text_worker_bindings( conn: &Connection, table: &str, bindings: &mut std::collections::BTreeSet<(String, String, u64)>, ) -> Result<()> { if !table_exists(conn, table)? { return Ok(()); } let sql = format!( "SELECT workspace_id, runtime_id, worker_id FROM {table} WHERE worker_id IS NOT NULL" ); let mut statement = conn.prepare(&sql)?; let rows = statement.query_map([], |row| { Ok(( row.get::<_, String>(0)?, row.get::<_, String>(1)?, row.get::<_, String>(2)?, )) })?; for row in rows { let (workspace_id, runtime_id, worker_id) = row?; let legacy_id = worker_id .strip_prefix("worker-") .unwrap_or(&worker_id) .parse::() .map_err(|_| { Error::InvalidInput(format!( "{table} contains non-legacy Worker id `{worker_id}` before schema v37" )) })?; bindings.insert((workspace_id, runtime_id, legacy_id)); } Ok(()) } fn apply_workspace_worker_uuid_identity_migration(conn: &Connection) -> Result<()> { promote_workspace_worker_uuid_identity(conn).map(|_| ()) } fn promote_workspace_worker_uuid_identity( conn: &Connection, ) -> Result> { conn.execute_batch( r#" PRAGMA defer_foreign_keys = ON; CREATE TEMP TABLE worker_identity_v37 ( workspace_id TEXT NOT NULL, runtime_id TEXT NOT NULL, runtime_worker_id INTEGER NOT NULL, worker_id TEXT NOT NULL UNIQUE, PRIMARY KEY (workspace_id, runtime_id, runtime_worker_id) ); "#, )?; let mut legacy_workers = std::collections::BTreeSet::new(); { let mut statement = conn.prepare( "SELECT workspace_id, runtime_id, runtime_worker_id FROM worker_registry \ ORDER BY workspace_id, runtime_id, runtime_worker_id", )?; let rows = statement.query_map([], |row| { Ok(( row.get::<_, String>(0)?, row.get::<_, String>(1)?, row.get::<_, i64>(2)?, )) })?; for row in rows { let (workspace_id, runtime_id, runtime_worker_id) = row?; legacy_workers.insert(( workspace_id, runtime_id, u64::try_from(runtime_worker_id).map_err(|_| { Error::InvalidInput(format!( "legacy Runtime Worker id {runtime_worker_id} is negative" )) })?, )); } } // v21/v22 removed the legacy per-Worker credential and Ticket notification // tables. Every Worker-reference table that still exists at the v36 boundary // participates in this map, including retention-only Workers no longer present // in worker_registry. for table in [ "ticket_worker_assignments", "ticket_current_worker_assignments", "ticket_assignment_operations", "worker_removal_operations", "worker_session_archives", "worker_diagnostics_archives", "worker_tombstones", "worker_orphan_diagnostics", ] { collect_legacy_text_worker_bindings(conn, table, &mut legacy_workers)?; } let mut mappings = Vec::with_capacity(legacy_workers.len()); for (workspace_id, runtime_id, runtime_worker_id) in &legacy_workers { let worker_id = WorkerId::from_legacy_binding(workspace_id, runtime_id, *runtime_worker_id); conn.execute( "INSERT INTO worker_identity_v37(\ workspace_id, runtime_id, runtime_worker_id, worker_id\ ) VALUES (?1, ?2, ?3, ?4)", params![ workspace_id, runtime_id, runtime_worker_id, worker_id.to_string() ], )?; mappings.push(LegacyWorkerIdentityMapping { workspace_id: workspace_id.clone(), runtime_id: runtime_id.clone(), legacy_worker_id: *runtime_worker_id, worker_id, }); } conn.execute_batch( r#" CREATE TABLE worker_registry_v37 ( workspace_id TEXT NOT NULL, worker_id TEXT NOT NULL, runtime_id TEXT NOT NULL, display_name TEXT NOT NULL, profile TEXT, retention_state TEXT NOT NULL CHECK (retention_state IN ('normal', 'pinned')), transcript_ref TEXT, session_ref TEXT, summary_ref TEXT, diagnostics_ref TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, PRIMARY KEY (workspace_id, worker_id), FOREIGN KEY (workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ); INSERT INTO worker_registry_v37( workspace_id, worker_id, runtime_id, display_name, profile, retention_state, transcript_ref, session_ref, summary_ref, diagnostics_ref, created_at, updated_at ) SELECT r.workspace_id, m.worker_id, r.runtime_id, r.display_name, r.profile, r.retention_state, r.transcript_ref, r.session_ref, r.summary_ref, r.diagnostics_ref, r.created_at, r.updated_at FROM worker_registry r JOIN worker_identity_v37 m ON m.workspace_id = r.workspace_id AND m.runtime_id = r.runtime_id AND m.runtime_worker_id = r.runtime_worker_id; CREATE TABLE worker_workdir_links_v37 ( workspace_id TEXT NOT NULL, runtime_id TEXT NOT NULL, worker_id TEXT NOT NULL, workdir_id TEXT NOT NULL, role TEXT NOT NULL, linked_at TEXT NOT NULL, unlinked_at TEXT, PRIMARY KEY (workspace_id, worker_id, workdir_id, role), FOREIGN KEY (workspace_id, worker_id) REFERENCES worker_registry_v37(workspace_id, worker_id) ON DELETE CASCADE, FOREIGN KEY (workspace_id, workdir_id) REFERENCES workdir_registry(workspace_id, workdir_id) ON DELETE CASCADE ); INSERT INTO worker_workdir_links_v37( workspace_id, runtime_id, worker_id, workdir_id, role, linked_at, unlinked_at ) SELECT l.workspace_id, l.runtime_id, m.worker_id, l.workdir_id, l.role, l.linked_at, l.unlinked_at FROM worker_workdir_links l JOIN worker_identity_v37 m ON m.workspace_id = l.workspace_id AND m.runtime_id = l.runtime_id AND m.runtime_worker_id = l.runtime_worker_id; CREATE TABLE worker_control_grants_v37 ( grant_id TEXT NOT NULL, workspace_id TEXT NOT NULL, controller_runtime_id TEXT NOT NULL, controller_worker_id TEXT NOT NULL, subject_runtime_id TEXT NOT NULL, subject_worker_id TEXT NOT NULL, relation TEXT NOT NULL, origin TEXT NOT NULL, permissions_json TEXT NOT NULL, operation_id TEXT NOT NULL, created_at TEXT NOT NULL, revoked_at TEXT, PRIMARY KEY (workspace_id, grant_id), UNIQUE (workspace_id, controller_worker_id, operation_id), FOREIGN KEY (workspace_id, controller_worker_id) REFERENCES worker_registry_v37(workspace_id, worker_id) ON DELETE CASCADE, FOREIGN KEY (workspace_id, subject_worker_id) REFERENCES worker_registry_v37(workspace_id, worker_id) ON DELETE CASCADE ); INSERT INTO worker_control_grants_v37( grant_id, workspace_id, controller_runtime_id, controller_worker_id, subject_runtime_id, subject_worker_id, relation, origin, permissions_json, operation_id, created_at, revoked_at ) SELECT g.grant_id, g.workspace_id, g.controller_runtime_id, controller.worker_id, g.subject_runtime_id, subject.worker_id, g.relation, g.origin, g.permissions_json, g.operation_id, g.created_at, g.revoked_at FROM worker_control_grants g JOIN worker_identity_v37 controller ON controller.workspace_id = g.workspace_id AND controller.runtime_id = g.controller_runtime_id AND controller.runtime_worker_id = g.controller_worker_id JOIN worker_identity_v37 subject ON subject.workspace_id = g.workspace_id AND subject.runtime_id = g.subject_runtime_id AND subject.runtime_worker_id = g.subject_worker_id; UPDATE ticket_worker_assignments SET worker_id = ( SELECT m.worker_id FROM worker_identity_v37 m WHERE m.workspace_id = ticket_worker_assignments.workspace_id AND m.runtime_id = ticket_worker_assignments.runtime_id AND CAST(m.runtime_worker_id AS TEXT) = ticket_worker_assignments.worker_id ); UPDATE ticket_current_worker_assignments SET worker_id = ( SELECT m.worker_id FROM worker_identity_v37 m WHERE m.workspace_id = ticket_current_worker_assignments.workspace_id AND m.runtime_id = ticket_current_worker_assignments.runtime_id AND CAST(m.runtime_worker_id AS TEXT) = ticket_current_worker_assignments.worker_id ); UPDATE ticket_assignment_operations SET worker_id = ( SELECT m.worker_id FROM worker_identity_v37 m WHERE m.workspace_id = ticket_assignment_operations.workspace_id AND m.runtime_id = ticket_assignment_operations.runtime_id AND CAST(m.runtime_worker_id AS TEXT) = ticket_assignment_operations.worker_id ) WHERE worker_id IS NOT NULL; UPDATE worker_removal_operations SET worker_id = ( SELECT m.worker_id FROM worker_identity_v37 m WHERE m.workspace_id = worker_removal_operations.workspace_id AND m.runtime_id = worker_removal_operations.runtime_id AND CAST(m.runtime_worker_id AS TEXT) = worker_removal_operations.worker_id ); UPDATE worker_session_archives SET worker_id = ( SELECT m.worker_id FROM worker_identity_v37 m WHERE m.workspace_id = worker_session_archives.workspace_id AND m.runtime_id = worker_session_archives.runtime_id AND CAST(m.runtime_worker_id AS TEXT) = worker_session_archives.worker_id ); UPDATE worker_diagnostics_archives SET worker_id = ( SELECT m.worker_id FROM worker_identity_v37 m WHERE m.workspace_id = worker_diagnostics_archives.workspace_id AND m.runtime_id = worker_diagnostics_archives.runtime_id AND CAST(m.runtime_worker_id AS TEXT) = worker_diagnostics_archives.worker_id ); UPDATE worker_tombstones SET worker_id = ( SELECT m.worker_id FROM worker_identity_v37 m WHERE m.workspace_id = worker_tombstones.workspace_id AND m.runtime_id = worker_tombstones.runtime_id AND CAST(m.runtime_worker_id AS TEXT) = worker_tombstones.worker_id ); UPDATE worker_orphan_diagnostics SET worker_id = ( SELECT m.worker_id FROM worker_identity_v37 m WHERE m.workspace_id = worker_orphan_diagnostics.workspace_id AND m.runtime_id = worker_orphan_diagnostics.runtime_id AND CAST(m.runtime_worker_id AS TEXT) = worker_orphan_diagnostics.worker_id ); DROP TABLE worker_control_grants; DROP TABLE worker_workdir_links; DROP TABLE worker_registry; ALTER TABLE worker_registry_v37 RENAME TO worker_registry; ALTER TABLE worker_workdir_links_v37 RENAME TO worker_workdir_links; ALTER TABLE worker_control_grants_v37 RENAME TO worker_control_grants; CREATE INDEX worker_registry_runtime ON worker_registry(workspace_id, runtime_id, worker_id); CREATE INDEX worker_workdir_links_workdir ON worker_workdir_links(workspace_id, workdir_id); CREATE UNIQUE INDEX worker_workdir_links_active_worker_unique ON worker_workdir_links(workspace_id, worker_id) WHERE unlinked_at IS NULL; CREATE UNIQUE INDEX worker_workdir_links_active_workdir_unique ON worker_workdir_links(workspace_id, workdir_id) WHERE unlinked_at IS NULL; CREATE INDEX worker_control_grants_controller ON worker_control_grants( workspace_id, controller_worker_id, revoked_at ); CREATE INDEX worker_control_grants_subject ON worker_control_grants( workspace_id, subject_worker_id, revoked_at ); CREATE TABLE worker_create_reservations ( workspace_id TEXT NOT NULL, allocation_key TEXT NOT NULL, worker_id TEXT NOT NULL, runtime_id TEXT NOT NULL, create_fingerprint TEXT NOT NULL, state TEXT NOT NULL CHECK (state IN ('reserved', 'created')), created_at TEXT NOT NULL, updated_at TEXT NOT NULL, PRIMARY KEY (workspace_id, allocation_key), UNIQUE (workspace_id, worker_id), FOREIGN KEY (workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ); CREATE INDEX worker_create_reservations_worker ON worker_create_reservations(workspace_id, worker_id); DROP TABLE worker_identity_v37; "#, )?; Ok(mappings) } fn allocate_resource_human_key( conn: &Connection, workspace_id: &str, kind: WorkspaceResourceKind, resource_id: &str, allocated_at: &str, ) -> Result { if let Some(existing) = conn .query_row( "SELECT human_key FROM workspace_resource_human_keys WHERE workspace_id = ?1 AND resource_kind = ?2 AND resource_id = ?3", params![workspace_id, kind.as_str(), resource_id], |row| row.get(0), ) .optional()? { return Ok(existing); } conn.execute( "INSERT OR IGNORE INTO workspace_resource_human_key_counters (workspace_id, resource_kind, next_sequence) VALUES (?1, ?2, 1)", params![workspace_id, kind.as_str()], )?; let sequence: i64 = conn.query_row( "SELECT next_sequence FROM workspace_resource_human_key_counters WHERE workspace_id = ?1 AND resource_kind = ?2", params![workspace_id, kind.as_str()], |row| row.get(0), )?; conn.execute( "UPDATE workspace_resource_human_key_counters SET next_sequence = ?3 WHERE workspace_id = ?1 AND resource_kind = ?2", params![workspace_id, kind.as_str(), sequence + 1], )?; let human_key = format!("{}-{sequence}", kind.prefix()); conn.execute( "INSERT INTO workspace_resource_human_keys (workspace_id, resource_kind, resource_id, sequence, human_key, allocated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", params![ workspace_id, kind.as_str(), resource_id, sequence, human_key, allocated_at ], )?; Ok(human_key) } fn add_workspace_resource_human_keys(conn: &Connection) -> Result<()> { conn.execute_batch( r#" CREATE TABLE IF NOT EXISTS workspace_resource_human_keys ( workspace_id TEXT NOT NULL, resource_kind TEXT NOT NULL CHECK (resource_kind IN ('ticket', 'objective', 'worker')), resource_id TEXT NOT NULL, sequence INTEGER NOT NULL CHECK (sequence > 0), human_key TEXT NOT NULL, allocated_at TEXT NOT NULL, PRIMARY KEY (workspace_id, resource_kind, resource_id), UNIQUE (workspace_id, resource_kind, sequence), UNIQUE (workspace_id, human_key) ); CREATE TABLE IF NOT EXISTS workspace_resource_human_key_counters ( workspace_id TEXT NOT NULL, resource_kind TEXT NOT NULL CHECK (resource_kind IN ('ticket', 'objective', 'worker')), next_sequence INTEGER NOT NULL CHECK (next_sequence > 0), PRIMARY KEY (workspace_id, resource_kind) ); INSERT OR IGNORE INTO workspace_resource_human_keys ( workspace_id, resource_kind, resource_id, sequence, human_key, allocated_at ) SELECT workspace_id, 'objective', objective_id, ROW_NUMBER() OVER (PARTITION BY workspace_id ORDER BY created_at ASC, objective_id ASC), 'O-' || ROW_NUMBER() OVER (PARTITION BY workspace_id ORDER BY created_at ASC, objective_id ASC), created_at FROM objectives; INSERT OR IGNORE INTO workspace_resource_human_keys ( workspace_id, resource_kind, resource_id, sequence, human_key, allocated_at ) SELECT workspace_id, 'worker', worker_id, ROW_NUMBER() OVER (PARTITION BY workspace_id ORDER BY created_at ASC, worker_id ASC), 'W-' || ROW_NUMBER() OVER (PARTITION BY workspace_id ORDER BY created_at ASC, worker_id ASC), created_at FROM worker_registry; INSERT INTO workspace_resource_human_key_counters (workspace_id, resource_kind, next_sequence) SELECT workspace_id, resource_kind, MAX(sequence) + 1 FROM workspace_resource_human_keys GROUP BY workspace_id, resource_kind ON CONFLICT(workspace_id, resource_kind) DO UPDATE SET next_sequence = MAX(next_sequence, excluded.next_sequence); "#, )?; if table_exists(conn, "typed_tickets")? { conn.execute_batch( r#" INSERT OR IGNORE INTO workspace_resource_human_keys ( workspace_id, resource_kind, resource_id, sequence, human_key, allocated_at ) SELECT workspace_id, 'ticket', ticket_id, ROW_NUMBER() OVER (PARTITION BY workspace_id ORDER BY created_at ASC, ticket_id ASC), 'T-' || ROW_NUMBER() OVER (PARTITION BY workspace_id ORDER BY created_at ASC, ticket_id ASC), created_at FROM typed_tickets; INSERT INTO workspace_resource_human_key_counters ( workspace_id, resource_kind, next_sequence ) SELECT workspace_id, 'ticket', MAX(sequence) + 1 FROM workspace_resource_human_keys WHERE resource_kind = 'ticket' GROUP BY workspace_id ON CONFLICT(workspace_id, resource_kind) DO UPDATE SET next_sequence = MAX(next_sequence, excluded.next_sequence); "#, )?; } Ok(()) } fn remove_worker_control_delegation_authority(conn: &Connection) -> Result<()> { let mut statement = conn.prepare("SELECT workspace_id, grant_id, permissions_json FROM worker_control_grants")?; let rows = statement.query_map([], |row| { Ok(( row.get::<_, String>(0)?, row.get::<_, String>(1)?, row.get::<_, String>(2)?, )) })?; let mut permission_updates = Vec::new(); for row in rows { let (workspace_id, grant_id, permissions_json) = row?; let mut permissions: Vec = serde_json::from_str(&permissions_json).map_err(|error| { Error::Store(format!( "failed to decode Worker control grant `{grant_id}` permissions during delegation removal: {error}" )) })?; let previous_len = permissions.len(); permissions .retain(|permission| !matches!(permission.as_str(), "share" | "transfer" | "revoke")); if permissions.len() != previous_len { permission_updates.push(( workspace_id, grant_id, serde_json::to_string(&permissions).map_err(|error| { Error::Store(format!( "failed to encode Worker control grant permissions during delegation removal: {error}" )) })?, )); } } drop(statement); for (workspace_id, grant_id, permissions_json) in permission_updates { conn.execute( "UPDATE worker_control_grants SET permissions_json = ?3 WHERE workspace_id = ?1 AND grant_id = ?2", params![workspace_id, grant_id, permissions_json], )?; } conn.execute( r#"UPDATE worker_control_grants SET revoked_at = COALESCE(revoked_at, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) WHERE relation IN ('shared', 'transferred')"#, [], )?; conn.execute_batch("DROP TABLE IF EXISTS worker_control_delegation_operations;")?; Ok(()) } fn create_worker_mutation_source_proof_replay_guard(conn: &Connection) -> Result<()> { conn.execute_batch( r#" CREATE TABLE IF NOT EXISTS worker_mutation_source_proof_jtis ( runtime_id TEXT NOT NULL, jti TEXT NOT NULL, expires_at INTEGER NOT NULL, consumed_at TEXT NOT NULL, PRIMARY KEY (runtime_id, jti) ); CREATE INDEX IF NOT EXISTS idx_worker_mutation_source_proof_jtis_expiry ON worker_mutation_source_proof_jtis(expires_at); "#, )?; Ok(()) } pub(crate) fn materialize_main_config_entrypoint(conn: &Connection) -> Result<()> { let mut statement = conn.prepare("SELECT workspace_id, created_at FROM workspaces ORDER BY workspace_id")?; let workspaces = statement .query_map([], |row| { Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)) })? .collect::, _>>()?; drop(statement); for (workspace_id, created_at) in workspaces { let existing = crate::config_source::load_state(conn, &workspace_id)?; let state = match existing { None => crate::config_source::initial_state()?, Some(existing) => { let main = config_source::VirtualPath::parse(crate::config_source::MAIN_CONFIG_ENTRYPOINT) .map_err(|error| Error::Store(error.to_string()))?; let snapshot = if existing.snapshot.entries.contains_key(&main) { existing.snapshot } else { existing .snapshot .apply(&[config_source::ConfigTreeChange::Create { path: main.clone(), content_type: config_source::ConfigContentType::Decodal, content: crate::config_source::DEFAULT_MAIN_CONFIG_SOURCE.to_string(), }]) .map_err(|error| Error::Store(error.to_string()))? }; let contract = config_source::ToolchainContract::new( config_source::DEFAULT_SCHEMA_VERSION, vec![main], config_source::DEFAULT_IMPORT_POLICY_VERSION, ); let evaluation = config_source::SnapshotEnvironment::new(snapshot.clone()) .evaluate_contract(&contract) .map_err(|diagnostics| { Error::Store(format!( "cannot materialize main.dcdl for Workspace {workspace_id}: {}", serde_json::to_string(&diagnostics) .unwrap_or_else(|_| "config evaluation failed".to_string()) )) })?; crate::config_source::WorkspaceConfigState { snapshot, contract, projection_digest: evaluation.projection_digest, } } }; conn.execute( "DELETE FROM workspace_config_tree_revisions WHERE workspace_id = ?1", [&workspace_id], )?; conn.execute( "DELETE FROM workspace_config_entries WHERE workspace_id = ?1", [&workspace_id], )?; crate::config_source::insert_materialized_state(conn, &workspace_id, &state, &created_at)?; } Ok(()) } fn enforce_workspace_resource_foreign_keys(conn: &Connection) -> Result<()> { let schema = r#" CREATE UNIQUE INDEX IF NOT EXISTS idx_worker_registry_workspace_runtime_worker ON worker_registry(workspace_id, runtime_id, worker_id); CREATE TABLE objectives_v39 ( workspace_id TEXT NOT NULL, objective_id TEXT NOT NULL, title TEXT NOT NULL, state TEXT NOT NULL, body_md TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, PRIMARY KEY (workspace_id, objective_id), FOREIGN KEY (workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ); INSERT INTO objectives_v39 SELECT * FROM objectives; CREATE TABLE objective_events_v39 ( workspace_id TEXT NOT NULL, objective_id TEXT NOT NULL, event_id TEXT NOT NULL, kind TEXT NOT NULL, body_md TEXT, created_at TEXT NOT NULL, PRIMARY KEY (workspace_id, event_id), FOREIGN KEY (workspace_id, objective_id) REFERENCES objectives(workspace_id, objective_id) ON DELETE CASCADE ); INSERT INTO objective_events_v39 SELECT workspace_id, objective_id, event_id, kind, body_md, created_at FROM objective_events; CREATE TABLE objective_resources_v39 ( workspace_id TEXT NOT NULL, objective_id TEXT NOT NULL, resource_path TEXT NOT NULL, body TEXT NOT NULL, media_type TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, PRIMARY KEY (workspace_id, objective_id, resource_path), FOREIGN KEY (workspace_id, objective_id) REFERENCES objectives(workspace_id, objective_id) ON DELETE CASCADE ); INSERT INTO objective_resources_v39 SELECT workspace_id, objective_id, resource_path, body, media_type, created_at, updated_at FROM objective_resources; CREATE TABLE typed_tickets_v39 ( workspace_id TEXT NOT NULL, ticket_id TEXT NOT NULL, slug TEXT NOT NULL, title TEXT NOT NULL, status TEXT NOT NULL, kind TEXT NOT NULL, priority TEXT NOT NULL, body TEXT NOT NULL, created_at TEXT, updated_at TEXT, assignee TEXT, readiness TEXT, workflow_state TEXT NOT NULL, workflow_state_explicit INTEGER NOT NULL, queued_by TEXT, queued_at TEXT, resolution TEXT, repository_id TEXT, ref_selector TEXT, PRIMARY KEY (workspace_id, ticket_id), FOREIGN KEY (workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE, FOREIGN KEY (workspace_id, repository_id) REFERENCES repositories(workspace_id, repository_id) ON DELETE RESTRICT ); INSERT INTO typed_tickets_v39 SELECT * FROM typed_tickets; CREATE TABLE typed_ticket_relations_v39 ( workspace_id TEXT NOT NULL, ticket_id TEXT NOT NULL, kind TEXT NOT NULL, target TEXT NOT NULL, note TEXT, author TEXT NOT NULL, at TEXT NOT NULL, PRIMARY KEY (workspace_id, ticket_id, kind, target), FOREIGN KEY (workspace_id, ticket_id) REFERENCES typed_tickets(workspace_id, ticket_id) ON DELETE CASCADE, FOREIGN KEY (workspace_id, target) REFERENCES typed_tickets(workspace_id, ticket_id) ON DELETE CASCADE ); INSERT INTO typed_ticket_relations_v39 SELECT * FROM typed_ticket_relations; CREATE TABLE objective_ticket_links_v39 ( workspace_id TEXT NOT NULL, objective_id TEXT NOT NULL, ticket_id TEXT NOT NULL, kind TEXT NOT NULL, created_at TEXT NOT NULL, PRIMARY KEY (workspace_id, objective_id, ticket_id, kind), FOREIGN KEY (workspace_id, objective_id) REFERENCES objectives(workspace_id, objective_id) ON DELETE CASCADE, FOREIGN KEY (workspace_id, ticket_id) REFERENCES typed_tickets(workspace_id, ticket_id) ON DELETE CASCADE ); INSERT INTO objective_ticket_links_v39 SELECT * FROM objective_ticket_links; CREATE TABLE ticket_assignment_ticket_tombstones ( workspace_id TEXT NOT NULL, ticket_id TEXT NOT NULL, deleted_at TEXT NOT NULL, PRIMARY KEY (workspace_id, ticket_id), FOREIGN KEY (workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ); CREATE TABLE ticket_assignment_worker_tombstones ( workspace_id TEXT NOT NULL, runtime_id TEXT NOT NULL, worker_id TEXT NOT NULL, deleted_at TEXT NOT NULL, PRIMARY KEY (workspace_id, runtime_id, worker_id), FOREIGN KEY (workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ); CREATE TABLE ticket_worker_assignments_v39 ( workspace_id TEXT NOT NULL, ticket_id TEXT NOT NULL, assignment_id TEXT NOT NULL, runtime_id TEXT NOT NULL, worker_id TEXT NOT NULL, assigned_by TEXT NOT NULL, assigned_at TEXT NOT NULL, PRIMARY KEY (workspace_id, assignment_id), UNIQUE (workspace_id, ticket_id, assignment_id), UNIQUE (workspace_id, ticket_id, assignment_id, runtime_id, worker_id), FOREIGN KEY (workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ); INSERT INTO ticket_worker_assignments_v39 SELECT * FROM ticket_worker_assignments; CREATE TABLE ticket_worker_assignment_events_v39 ( workspace_id TEXT NOT NULL, ticket_id TEXT NOT NULL, event_id TEXT NOT NULL, action TEXT NOT NULL CHECK (action IN ('assigned', 'reassigned', 'unassigned')), assignment_id TEXT, previous_assignment_id TEXT, actor TEXT NOT NULL, created_at TEXT NOT NULL, PRIMARY KEY (workspace_id, event_id), FOREIGN KEY (workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ); INSERT INTO ticket_worker_assignment_events_v39 SELECT * FROM ticket_worker_assignment_events; CREATE TABLE ticket_current_worker_assignments_v39 ( workspace_id TEXT NOT NULL, ticket_id TEXT NOT NULL, assignment_id TEXT NOT NULL, runtime_id TEXT NOT NULL, worker_id TEXT NOT NULL, updated_at TEXT NOT NULL, PRIMARY KEY (workspace_id, ticket_id), UNIQUE (workspace_id, runtime_id, worker_id), FOREIGN KEY (workspace_id, ticket_id, assignment_id, runtime_id, worker_id) REFERENCES ticket_worker_assignments( workspace_id, ticket_id, assignment_id, runtime_id, worker_id ) ON DELETE CASCADE ); INSERT INTO ticket_current_worker_assignments_v39 SELECT * FROM ticket_current_worker_assignments; CREATE TABLE ticket_assignment_operations_v39 ( workspace_id TEXT NOT NULL, operation_id TEXT NOT NULL, action TEXT NOT NULL CHECK (action IN ('assign', 'reassign', 'unassign')), ticket_id TEXT NOT NULL, runtime_id TEXT, worker_id TEXT, assignment_id TEXT, expected_assignment_id TEXT, created_at TEXT NOT NULL, request_fingerprint TEXT, PRIMARY KEY (workspace_id, operation_id), FOREIGN KEY (workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ); INSERT INTO ticket_assignment_operations_v39 SELECT * FROM ticket_assignment_operations; CREATE TABLE artifacts_v39 ( workspace_id TEXT NOT NULL, artifact_id TEXT NOT NULL, kind TEXT NOT NULL, uri TEXT NOT NULL, media_type TEXT, sha256 TEXT, size_bytes INTEGER, summary TEXT, created_at TEXT NOT NULL, created_by_kind TEXT NOT NULL, created_by_key TEXT NOT NULL, created_by_display TEXT NOT NULL, created_by_source_kind TEXT, created_by_source_key TEXT, ticket_id TEXT, objective_id TEXT, event_id TEXT, worker_ref_kind TEXT, worker_ref_key TEXT, worker_display TEXT, repository_id TEXT, source_kind TEXT, source_revision TEXT, PRIMARY KEY (workspace_id, artifact_id), CHECK ((worker_ref_kind IS NULL) = (worker_ref_key IS NULL)), FOREIGN KEY (workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE, FOREIGN KEY (workspace_id, ticket_id) REFERENCES typed_tickets(workspace_id, ticket_id) ON DELETE CASCADE, FOREIGN KEY (workspace_id, objective_id) REFERENCES objectives(workspace_id, objective_id) ON DELETE CASCADE, FOREIGN KEY (workspace_id, repository_id) REFERENCES repositories(workspace_id, repository_id) ON DELETE RESTRICT ); INSERT INTO artifacts_v39 SELECT * FROM artifacts; CREATE TABLE workspace_resource_human_key_counters_v39 ( workspace_id TEXT NOT NULL, resource_kind TEXT NOT NULL CHECK (resource_kind IN ('ticket', 'objective', 'worker')), next_sequence INTEGER NOT NULL CHECK (next_sequence > 0), PRIMARY KEY (workspace_id, resource_kind), FOREIGN KEY (workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ); INSERT INTO workspace_resource_human_key_counters_v39 SELECT * FROM workspace_resource_human_key_counters; CREATE TABLE workspace_resource_human_keys_v39 ( workspace_id TEXT NOT NULL, resource_kind TEXT NOT NULL CHECK (resource_kind IN ('ticket', 'objective', 'worker')), resource_id TEXT NOT NULL, sequence INTEGER NOT NULL CHECK (sequence > 0), human_key TEXT NOT NULL, allocated_at TEXT NOT NULL, PRIMARY KEY (workspace_id, resource_kind, resource_id), UNIQUE (workspace_id, resource_kind, sequence), UNIQUE (workspace_id, human_key), FOREIGN KEY (workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ); INSERT INTO workspace_resource_human_keys_v39 SELECT * FROM workspace_resource_human_keys; DROP TABLE ticket_current_worker_assignments; DROP TABLE ticket_worker_assignment_events; DROP TABLE ticket_assignment_operations; DROP TABLE objective_ticket_links; DROP TABLE objective_events; DROP TABLE objective_resources; DROP TABLE typed_ticket_relations; ALTER TABLE ticket_worker_assignments RENAME TO ticket_worker_assignments_v38; ALTER TABLE typed_tickets RENAME TO typed_tickets_v38; ALTER TABLE objectives RENAME TO objectives_v38; ALTER TABLE objectives_v39 RENAME TO objectives; ALTER TABLE typed_tickets_v39 RENAME TO typed_tickets; ALTER TABLE ticket_worker_assignments_v39 RENAME TO ticket_worker_assignments; ALTER TABLE objective_events_v39 RENAME TO objective_events; ALTER TABLE objective_resources_v39 RENAME TO objective_resources; ALTER TABLE typed_ticket_relations_v39 RENAME TO typed_ticket_relations; ALTER TABLE objective_ticket_links_v39 RENAME TO objective_ticket_links; ALTER TABLE ticket_worker_assignment_events_v39 RENAME TO ticket_worker_assignment_events; ALTER TABLE ticket_current_worker_assignments_v39 RENAME TO ticket_current_worker_assignments; ALTER TABLE ticket_assignment_operations_v39 RENAME TO ticket_assignment_operations; DROP TABLE ticket_worker_assignments_v38; DROP TABLE typed_tickets_v38; DROP TABLE objectives_v38; DROP TABLE artifacts; ALTER TABLE artifacts_v39 RENAME TO artifacts; DROP TABLE workspace_resource_human_keys; DROP TABLE workspace_resource_human_key_counters; ALTER TABLE workspace_resource_human_key_counters_v39 RENAME TO workspace_resource_human_key_counters; ALTER TABLE workspace_resource_human_keys_v39 RENAME TO workspace_resource_human_keys; CREATE INDEX IF NOT EXISTS idx_objectives_workspace_updated ON objectives(workspace_id, updated_at DESC); CREATE INDEX IF NOT EXISTS idx_objective_events_workspace_created ON objective_events(workspace_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_objective_resources_workspace_objective ON objective_resources(workspace_id, objective_id); CREATE INDEX IF NOT EXISTS idx_objective_ticket_links_workspace_objective ON objective_ticket_links(workspace_id, objective_id); CREATE INDEX IF NOT EXISTS idx_objective_ticket_links_workspace_ticket ON objective_ticket_links(workspace_id, ticket_id); CREATE INDEX IF NOT EXISTS idx_typed_tickets_workspace_state_updated ON typed_tickets(workspace_id, workflow_state, updated_at DESC, ticket_id); CREATE INDEX IF NOT EXISTS idx_typed_tickets_workspace_updated ON typed_tickets(workspace_id, updated_at DESC, ticket_id); CREATE INDEX IF NOT EXISTS idx_typed_ticket_relations_workspace_target ON typed_ticket_relations(workspace_id, target, at DESC); CREATE INDEX IF NOT EXISTS idx_ticket_worker_assignments_ticket ON ticket_worker_assignments(workspace_id, ticket_id, assigned_at DESC); CREATE INDEX IF NOT EXISTS idx_ticket_worker_assignments_worker ON ticket_worker_assignments(workspace_id, runtime_id, worker_id, assigned_at DESC); CREATE INDEX IF NOT EXISTS idx_ticket_worker_assignment_events_ticket ON ticket_worker_assignment_events(workspace_id, ticket_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_ticket_assignment_operations_ticket ON ticket_assignment_operations(workspace_id, ticket_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_artifacts_workspace_created ON artifacts(workspace_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_workspace_resource_human_keys_reverse ON workspace_resource_human_keys(workspace_id, resource_kind, human_key); "#; for statement in schema .split(';') .map(str::trim) .filter(|sql| !sql.is_empty()) { conn.execute_batch(statement).map_err(|error| { Error::Store(format!( "Workspace resource FK migration statement failed: {statement}: {error}" )) })?; } // Assignment rows and events are historical evidence and intentionally survive Ticket or // Worker retention deletion, so parent FKs would impose the wrong delete semantics. Parent // delete/move triggers record an exact tombstone before authority disappears; insertion // triggers require every new assignment to resolve both authorities in the same Workspace, // and event references resolve a committed assignment for the same Ticket. Operation // assignment/Worker ids remain unconstrained because reservations are persisted before // assignment/Worker creation and expected ids may intentionally be stale. conn.execute_batch( r#" CREATE TRIGGER ticket_assignment_ticket_parent_tombstone BEFORE DELETE ON typed_tickets WHEN EXISTS ( SELECT 1 FROM ticket_worker_assignments AS assignment WHERE assignment.workspace_id = OLD.workspace_id AND assignment.ticket_id = OLD.ticket_id ) OR EXISTS ( SELECT 1 FROM ticket_assignment_operations AS operation WHERE operation.workspace_id = OLD.workspace_id AND operation.ticket_id = OLD.ticket_id ) BEGIN INSERT OR IGNORE INTO ticket_assignment_ticket_tombstones ( workspace_id, ticket_id, deleted_at ) VALUES (OLD.workspace_id, OLD.ticket_id, CURRENT_TIMESTAMP); END; CREATE TRIGGER ticket_assignment_worker_parent_tombstone_delete BEFORE DELETE ON worker_registry WHEN EXISTS ( SELECT 1 FROM ticket_worker_assignments AS assignment WHERE assignment.workspace_id = OLD.workspace_id AND assignment.runtime_id = OLD.runtime_id AND assignment.worker_id = OLD.worker_id ) BEGIN INSERT OR IGNORE INTO ticket_assignment_worker_tombstones ( workspace_id, runtime_id, worker_id, deleted_at ) VALUES (OLD.workspace_id, OLD.runtime_id, OLD.worker_id, CURRENT_TIMESTAMP); END; CREATE TRIGGER ticket_assignment_worker_parent_tombstone_move BEFORE UPDATE OF runtime_id ON worker_registry WHEN OLD.runtime_id != NEW.runtime_id AND EXISTS ( SELECT 1 FROM ticket_worker_assignments AS assignment WHERE assignment.workspace_id = OLD.workspace_id AND assignment.runtime_id = OLD.runtime_id AND assignment.worker_id = OLD.worker_id ) BEGIN INSERT OR IGNORE INTO ticket_assignment_worker_tombstones ( workspace_id, runtime_id, worker_id, deleted_at ) VALUES (OLD.workspace_id, OLD.runtime_id, OLD.worker_id, CURRENT_TIMESTAMP); END; CREATE TRIGGER ticket_worker_assignments_validate_insert BEFORE INSERT ON ticket_worker_assignments WHEN NOT EXISTS ( SELECT 1 FROM typed_tickets AS ticket WHERE ticket.workspace_id = NEW.workspace_id AND ticket.ticket_id = NEW.ticket_id ) OR NOT EXISTS ( SELECT 1 FROM worker_registry AS worker WHERE worker.workspace_id = NEW.workspace_id AND worker.runtime_id = NEW.runtime_id AND worker.worker_id = NEW.worker_id ) BEGIN SELECT RAISE(ABORT, 'ticket_worker_assignments must reference a Ticket and Worker in the same Workspace'); END; CREATE TRIGGER ticket_worker_assignments_validate_update BEFORE UPDATE OF workspace_id, ticket_id, runtime_id, worker_id ON ticket_worker_assignments WHEN NOT EXISTS ( SELECT 1 FROM typed_tickets AS ticket WHERE ticket.workspace_id = NEW.workspace_id AND ticket.ticket_id = NEW.ticket_id ) OR NOT EXISTS ( SELECT 1 FROM worker_registry AS worker WHERE worker.workspace_id = NEW.workspace_id AND worker.runtime_id = NEW.runtime_id AND worker.worker_id = NEW.worker_id ) BEGIN SELECT RAISE(ABORT, 'ticket_worker_assignments must reference a Ticket and Worker in the same Workspace'); END; CREATE TRIGGER ticket_worker_assignment_events_validate_insert BEFORE INSERT ON ticket_worker_assignment_events WHEN (NEW.assignment_id IS NOT NULL AND NOT EXISTS ( SELECT 1 FROM ticket_worker_assignments AS assignment WHERE assignment.workspace_id = NEW.workspace_id AND assignment.ticket_id = NEW.ticket_id AND assignment.assignment_id = NEW.assignment_id )) OR (NEW.previous_assignment_id IS NOT NULL AND NOT EXISTS ( SELECT 1 FROM ticket_worker_assignments AS assignment WHERE assignment.workspace_id = NEW.workspace_id AND assignment.ticket_id = NEW.ticket_id AND assignment.assignment_id = NEW.previous_assignment_id )) BEGIN SELECT RAISE(ABORT, 'ticket_worker_assignment_events must reference assignments in the same Workspace'); END; CREATE TRIGGER ticket_assignment_operations_validate_insert BEFORE INSERT ON ticket_assignment_operations WHEN NOT EXISTS ( SELECT 1 FROM typed_tickets AS ticket WHERE ticket.workspace_id = NEW.workspace_id AND ticket.ticket_id = NEW.ticket_id ) BEGIN SELECT RAISE(ABORT, 'ticket_assignment_operations must reference a Ticket in the same Workspace'); END; "#, )?; Ok(()) } pub(crate) fn apply_migrations_through(conn: &Connection, through_version: i64) -> Result<()> { let current = current_schema_version(conn)?; for migration in MIGRATIONS.iter().filter(|migration| { i64::from(migration.version) > current && i64::from(migration.version) <= through_version }) { if migration.version == 39 { ticket::migrate_sqlite_ticket_schema(conn).map_err(|error| { Error::Store(format!( "migration 39 Ticket schema preparation failed: {error}" )) })?; if !table_exists(conn, "typed_tickets")? { return Err(Error::Store( "migration 39 Ticket schema preparation created no typed_tickets".to_string(), )); } merge_request::migrate(conn).map_err(|error| { Error::Store(format!( "migration 39 Merge Request schema preparation failed: {error}" )) })?; validate_workspace_resource_references(conn) .map_err(|error| Error::Store(format!("migration 39 preflight failed: {error}")))?; conn.execute_batch("PRAGMA foreign_keys = OFF; PRAGMA legacy_alter_table = ON;")?; let result = (|| -> Result<()> { let tx = conn.unchecked_transaction()?; (migration.apply)(&tx)?; if !table_exists(&tx, "typed_tickets")? { return Err(Error::Store( "migration 39 did not materialize `typed_tickets`".to_string(), )); } let dangling_foreign_key: Option<(String, String)> = tx .query_row( "SELECT name, sql FROM sqlite_schema \ WHERE type = 'table' AND (sql LIKE '%_v38%' OR sql LIKE '%_v39%') LIMIT 1", [], |row| Ok((row.get(0)?, row.get(1)?)), ) .optional()?; if let Some((child, sql)) = dangling_foreign_key { return Err(Error::Store(format!( "migration 39 left a temporary reference in `{child}`: {sql}" ))); } let foreign_key_failures: i64 = tx .query_row("SELECT COUNT(*) FROM pragma_foreign_key_check", [], |row| { row.get(0) }) .map_err(|error| { Error::Store(format!( "migration 39 could not evaluate foreign keys: {error}" )) })?; if foreign_key_failures != 0 { return Err(Error::Store(format!( "migration 39 found {foreign_key_failures} foreign key violation(s)" ))); } tx.execute( "INSERT INTO __yoi_schema_migrations (version, name) VALUES (?1, ?2)", params![migration.version, migration.name], ) .map_err(|error| { Error::Store(format!("migration 39 version insert failed: {error}")) })?; tx.commit().map_err(|error| { Error::Store(format!("migration 39 commit failed: {error}")) })?; Ok(()) })(); conn.execute_batch("PRAGMA legacy_alter_table = OFF; PRAGMA foreign_keys = ON;") .map_err(|error| { Error::Store(format!( "migration 39 could not restore FK enforcement: {error}" )) })?; result?; continue; } let tx = conn.unchecked_transaction()?; if migration.version == 37 { crate::retention::repair_worker_diagnostics_archive_table(&tx)?; } (migration.apply)(&tx)?; tx.execute( "INSERT INTO __yoi_schema_migrations (version, name) VALUES (?1, ?2)", params![migration.version, migration.name], )?; tx.commit()?; } Ok(()) } fn apply_migrations(conn: &Connection) -> Result<()> { let latest = i64::from(MIGRATIONS.last().expect("at least one migration").version); let current = current_schema_version(conn)?; if current > latest { return Err(Error::Store(format!( "database schema version {current} is newer than this server supports ({latest}); refusing to serve with an older binary" ))); } apply_migrations_through(conn, latest) } fn align_legacy_bootstrap_schema(conn: &Connection) -> Result<()> { if table_exists(conn, "repositories")? && column_exists(conn, "repositories", "local_root")? && !column_exists(conn, "repositories", "uri")? { rename_legacy_table(conn, "repositories", "legacy_repositories")?; } if table_exists(conn, "runs")? { rename_legacy_table(conn, "runs", "legacy_runs")?; } if table_exists(conn, "artifacts")? && (column_exists(conn, "artifacts", "run_id")? || column_exists(conn, "artifacts", "path")? || !column_exists(conn, "artifacts", "uri")?) { rename_legacy_table(conn, "artifacts", "legacy_artifacts")?; } if table_exists(conn, "ticket_projections")? { rename_legacy_table(conn, "ticket_projections", "legacy_ticket_projections")?; } if table_exists(conn, "objective_projections")? { rename_legacy_table( conn, "objective_projections", "legacy_objective_projections", )?; } let legacy_workspaces = preserve_noncanonical_workspaces(conn)?; create_schema_v0_tables(conn)?; if let Some(legacy_table) = legacy_workspaces { copy_legacy_workspaces(conn, &legacy_table)?; } Ok(()) } fn preserve_noncanonical_workspaces(conn: &Connection) -> Result> { if !table_exists(conn, "workspaces")? { return Ok(None); } let columns = table_columns(conn, "workspaces")?; if columns .iter() .map(String::as_str) .eq(WORKSPACES_V0_COLUMNS.iter().copied()) { return Ok(None); } let legacy_table = "legacy_workspaces"; rename_legacy_table(conn, "workspaces", legacy_table)?; Ok(Some(legacy_table.to_string())) } fn copy_legacy_workspaces(conn: &Connection, legacy_table: &str) -> Result<()> { let columns = table_columns(conn, legacy_table)?; for required_column in ["workspace_id", "display_name", "created_at", "updated_at"] { if !columns.iter().any(|column| column == required_column) { return Err(Error::Store(format!( "cannot migrate legacy workspaces: `{legacy_table}` is missing `{required_column}`" ))); } } let state_expr = if columns.iter().any(|column| column == "state") { "COALESCE(NULLIF(state, ''), 'active')" } else { "'active'" }; conn.execute_batch(&format!( r#"INSERT OR IGNORE INTO workspaces ( workspace_id, display_name, state, created_at, updated_at ) SELECT workspace_id, display_name, {state_expr}, created_at, updated_at FROM {legacy_table};"# ))?; Ok(()) } fn rename_legacy_table(conn: &Connection, table_name: &str, legacy_name: &str) -> Result<()> { if table_exists(conn, legacy_name)? { return Err(Error::Store(format!( "cannot preserve legacy table `{table_name}` because `{legacy_name}` already exists" ))); } conn.execute_batch(&format!( "ALTER TABLE {table_name} RENAME TO {legacy_name};" ))?; Ok(()) } fn remove_worker_registry_legacy_live_state_column(conn: &Connection) -> Result<()> { if !table_exists(conn, "worker_registry")? || !table_columns(conn, "worker_registry")? .iter() .any(|column| column == "lifecycle_state") { return Ok(()); } conn.execute_batch( r#" CREATE TABLE worker_registry_v4 ( workspace_id TEXT NOT NULL, worker_id TEXT NOT NULL, runtime_id TEXT NOT NULL, runtime_worker_id INTEGER NOT NULL, display_name TEXT NOT NULL, profile TEXT, retention_state TEXT NOT NULL CHECK (retention_state IN ('normal', 'pinned')), transcript_ref TEXT, session_ref TEXT, summary_ref TEXT, diagnostics_ref TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, PRIMARY KEY (workspace_id, worker_id), UNIQUE (workspace_id, runtime_id, runtime_worker_id), FOREIGN KEY (workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ); INSERT INTO worker_registry_v4 ( workspace_id, worker_id, runtime_id, runtime_worker_id, display_name, profile, retention_state, transcript_ref, session_ref, summary_ref, diagnostics_ref, created_at, updated_at ) SELECT workspace_id, worker_id, runtime_id, runtime_worker_id, display_name, profile, retention_state, transcript_ref, session_ref, summary_ref, diagnostics_ref, created_at, updated_at FROM worker_registry; DROP TABLE worker_registry; ALTER TABLE worker_registry_v4 RENAME TO worker_registry; CREATE INDEX IF NOT EXISTS idx_worker_registry_workspace_updated ON worker_registry(workspace_id, updated_at DESC); "#, )?; Ok(()) } fn use_composite_worker_registry_keys(conn: &Connection) -> Result<()> { if !table_exists(conn, "worker_registry")? || !table_columns(conn, "worker_registry")? .iter() .any(|column| column == "worker_id") { return Ok(()); } conn.execute_batch( r#" CREATE TABLE worker_registry_v5 ( workspace_id TEXT NOT NULL, runtime_id TEXT NOT NULL, runtime_worker_id INTEGER NOT NULL, display_name TEXT NOT NULL, profile TEXT, retention_state TEXT NOT NULL CHECK (retention_state IN ('normal', 'pinned')), transcript_ref TEXT, session_ref TEXT, summary_ref TEXT, diagnostics_ref TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, PRIMARY KEY (workspace_id, runtime_id, runtime_worker_id), FOREIGN KEY (workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ); INSERT OR REPLACE INTO worker_registry_v5 ( workspace_id, runtime_id, runtime_worker_id, display_name, profile, retention_state, transcript_ref, session_ref, summary_ref, diagnostics_ref, created_at, updated_at ) SELECT workspace_id, runtime_id, CASE WHEN typeof(runtime_worker_id) = 'integer' THEN runtime_worker_id WHEN runtime_worker_id GLOB 'worker-[0-9]*' THEN CAST(substr(runtime_worker_id, 8) AS INTEGER) WHEN runtime_worker_id GLOB '[0-9]*' THEN CAST(runtime_worker_id AS INTEGER) ELSE rowid END, display_name, profile, retention_state, transcript_ref, session_ref, summary_ref, diagnostics_ref, created_at, updated_at FROM worker_registry; CREATE TABLE worker_workdir_links_v5 ( workspace_id TEXT NOT NULL, runtime_id TEXT NOT NULL, runtime_worker_id INTEGER NOT NULL, workdir_id TEXT NOT NULL, role TEXT NOT NULL, linked_at TEXT NOT NULL, unlinked_at TEXT, PRIMARY KEY (workspace_id, runtime_id, runtime_worker_id, workdir_id, role), FOREIGN KEY (workspace_id, runtime_id, runtime_worker_id) REFERENCES worker_registry_v5(workspace_id, runtime_id, runtime_worker_id) ON DELETE CASCADE, FOREIGN KEY (workspace_id, workdir_id) REFERENCES workdir_registry(workspace_id, workdir_id) ON DELETE CASCADE ); INSERT OR REPLACE INTO worker_workdir_links_v5 ( workspace_id, runtime_id, runtime_worker_id, workdir_id, role, linked_at, unlinked_at ) SELECT links.workspace_id, registry.runtime_id, CASE WHEN typeof(registry.runtime_worker_id) = 'integer' THEN registry.runtime_worker_id WHEN registry.runtime_worker_id GLOB 'worker-[0-9]*' THEN CAST(substr(registry.runtime_worker_id, 8) AS INTEGER) WHEN registry.runtime_worker_id GLOB '[0-9]*' THEN CAST(registry.runtime_worker_id AS INTEGER) ELSE registry.rowid END, links.workdir_id, links.role, links.linked_at, links.unlinked_at FROM worker_workdir_links AS links JOIN worker_registry AS registry ON registry.workspace_id = links.workspace_id AND registry.worker_id = links.worker_id; DROP TABLE worker_workdir_links; DROP TABLE worker_registry; ALTER TABLE worker_registry_v5 RENAME TO worker_registry; ALTER TABLE worker_workdir_links_v5 RENAME TO worker_workdir_links; CREATE INDEX IF NOT EXISTS idx_worker_registry_workspace_updated ON worker_registry(workspace_id, updated_at DESC); CREATE INDEX IF NOT EXISTS idx_worker_workdir_links_worker ON worker_workdir_links(workspace_id, runtime_id, runtime_worker_id, linked_at DESC); "#, )?; Ok(()) } fn add_workdir_runtime_observation_states(conn: &Connection) -> Result<()> { if !table_exists(conn, "workdir_registry")? { return Ok(()); } conn.execute_batch( r#" CREATE TABLE workdir_registry_v6 ( workspace_id TEXT NOT NULL, workdir_id TEXT NOT NULL, runtime_id TEXT NOT NULL, repository_id TEXT NOT NULL, selector TEXT, resolved_commit TEXT, materialization_status TEXT NOT NULL CHECK (materialization_status IN ('pending', 'present', 'not_found', 'corrupted', 'unknown', 'failed')), cleanliness TEXT NOT NULL CHECK (cleanliness IN ('clean', 'dirty', 'unknown')), created_at TEXT NOT NULL, updated_at TEXT NOT NULL, PRIMARY KEY (workspace_id, workdir_id), FOREIGN KEY (workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ); INSERT OR REPLACE INTO workdir_registry_v6 ( workspace_id, workdir_id, runtime_id, repository_id, selector, resolved_commit, materialization_status, cleanliness, created_at, updated_at ) SELECT workspace_id, workdir_id, runtime_id, repository_id, selector, resolved_commit, CASE materialization_status WHEN 'missing' THEN 'not_found' WHEN 'removed' THEN 'not_found' ELSE materialization_status END, cleanliness, created_at, updated_at FROM workdir_registry; DROP TABLE workdir_registry; ALTER TABLE workdir_registry_v6 RENAME TO workdir_registry; CREATE INDEX IF NOT EXISTS idx_workdir_registry_workspace_updated ON workdir_registry(workspace_id, updated_at DESC); "#, )?; Ok(()) } fn remove_workdir_registry_management_kind_column(conn: &Connection) -> Result<()> { if !table_exists(conn, "workdir_registry")? || !table_columns(conn, "workdir_registry")? .iter() .any(|column| column == "management_kind") { return Ok(()); } conn.execute_batch( r#" CREATE TABLE workdir_registry_v7 ( workspace_id TEXT NOT NULL, workdir_id TEXT NOT NULL, runtime_id TEXT NOT NULL, repository_id TEXT NOT NULL, selector TEXT, resolved_commit TEXT, materialization_status TEXT NOT NULL CHECK (materialization_status IN ('pending', 'present', 'not_found', 'corrupted', 'unknown', 'failed')), cleanliness TEXT NOT NULL CHECK (cleanliness IN ('clean', 'dirty', 'unknown')), created_at TEXT NOT NULL, updated_at TEXT NOT NULL, PRIMARY KEY (workspace_id, workdir_id), FOREIGN KEY (workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ); INSERT OR REPLACE INTO workdir_registry_v7 ( workspace_id, workdir_id, runtime_id, repository_id, selector, resolved_commit, materialization_status, cleanliness, created_at, updated_at ) SELECT workspace_id, workdir_id, runtime_id, repository_id, selector, resolved_commit, materialization_status, cleanliness, created_at, updated_at FROM workdir_registry; DROP TABLE workdir_registry; ALTER TABLE workdir_registry_v7 RENAME TO workdir_registry; CREATE INDEX IF NOT EXISTS idx_workdir_registry_workspace_updated ON workdir_registry(workspace_id, updated_at DESC); "#, )?; Ok(()) } fn create_schema_v0_tables(conn: &Connection) -> Result<()> { conn.execute_batch( r#" CREATE TABLE IF NOT EXISTS workspaces ( workspace_id TEXT PRIMARY KEY, display_name TEXT NOT NULL, state TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS objectives ( workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE, objective_id TEXT PRIMARY KEY, title TEXT NOT NULL, state TEXT NOT NULL, body_md TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS objective_ticket_links ( workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE, objective_id TEXT NOT NULL REFERENCES objectives(objective_id) ON DELETE CASCADE, ticket_id TEXT NOT NULL, kind TEXT NOT NULL, created_at TEXT NOT NULL, PRIMARY KEY (objective_id, ticket_id, kind) ); CREATE TABLE IF NOT EXISTS objective_events ( workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE, objective_id TEXT NOT NULL REFERENCES objectives(objective_id) ON DELETE CASCADE, event_id TEXT PRIMARY KEY, kind TEXT NOT NULL, body_md TEXT, created_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS objective_resources ( workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE, objective_id TEXT NOT NULL REFERENCES objectives(objective_id) ON DELETE CASCADE, resource_path TEXT NOT NULL, body TEXT NOT NULL, media_type TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, PRIMARY KEY (objective_id, resource_path) ); CREATE TABLE IF NOT EXISTS memory_staging_records ( workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE, candidate_id TEXT NOT NULL, raw_json TEXT NOT NULL, source_path TEXT, imported_at TEXT NOT NULL, PRIMARY KEY (workspace_id, candidate_id) ); CREATE TABLE IF NOT EXISTS workspace_memory_documents ( workspace_id TEXT PRIMARY KEY REFERENCES workspaces(workspace_id) ON DELETE CASCADE, body_md TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS memory_staging_resolutions ( workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE, candidate_id TEXT NOT NULL, action TEXT NOT NULL, reason TEXT NOT NULL, affected_refs_json TEXT NOT NULL, staging_raw_json TEXT NOT NULL, source_path TEXT, imported_at TEXT NOT NULL, resolved_at TEXT NOT NULL, PRIMARY KEY (workspace_id, candidate_id, resolved_at) ); CREATE TABLE IF NOT EXISTS repositories ( workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE, repository_id TEXT PRIMARY KEY, name TEXT NOT NULL, kind TEXT NOT NULL, provider TEXT, uri TEXT NOT NULL, default_ref TEXT, auth_ref_kind TEXT, auth_ref_key TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS artifacts ( workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE, artifact_id TEXT PRIMARY KEY, kind TEXT NOT NULL, uri TEXT NOT NULL, media_type TEXT, sha256 TEXT, size_bytes INTEGER, summary TEXT, created_at TEXT NOT NULL, created_by_kind TEXT NOT NULL, created_by_key TEXT NOT NULL, created_by_display TEXT NOT NULL, created_by_source_kind TEXT, created_by_source_key TEXT, ticket_id TEXT, objective_id TEXT, event_id TEXT, worker_ref_kind TEXT, worker_ref_key TEXT, worker_display TEXT, repository_id TEXT, source_kind TEXT, source_revision TEXT ); CREATE TABLE IF NOT EXISTS audit_events ( workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE, audit_event_id TEXT PRIMARY KEY, created_at TEXT NOT NULL, actor_kind TEXT NOT NULL, actor_key TEXT NOT NULL, actor_display TEXT NOT NULL, actor_source_kind TEXT, actor_source_key TEXT, action TEXT NOT NULL, target_kind TEXT NOT NULL, target_id TEXT, outcome TEXT NOT NULL, request_id TEXT, summary TEXT ); "#, )?; Ok(()) } fn table_exists(conn: &Connection, table_name: &str) -> Result { conn.query_row( "SELECT EXISTS(SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = ?1)", params![table_name], |row| row.get::<_, bool>(0), ) .map_err(Error::from) } fn column_exists(conn: &Connection, table_name: &str, column_name: &str) -> Result { Ok(table_columns(conn, table_name)? .iter() .any(|column| column == column_name)) } fn table_columns(conn: &Connection, table_name: &str) -> Result> { let mut stmt = conn.prepare(&format!("PRAGMA table_info({table_name})"))?; let rows = stmt.query_map([], |row| row.get::<_, String>(1))?; rows.collect::>>() .map_err(Error::from) } #[cfg(test)] mod tests { use super::*; use std::collections::BTreeSet; #[test] fn startup_composes_ticket_migrations_when_control_plane_is_current() { let conn = Connection::open_in_memory().unwrap(); configure_sqlite(&conn).unwrap(); apply_migrations(&conn).unwrap(); assert!(table_exists(&conn, "ticket_schema_migrations").unwrap()); let store = SqliteWorkspaceStore::from_connection(conn).unwrap(); store .with_conn(|conn| { ticket::verify_sqlite_ticket_schema(conn)?; let latest = conn.query_row( "SELECT MAX(version) FROM ticket_schema_migrations", [], |row| row.get::<_, i64>(0), )?; assert_eq!(latest, ticket::LATEST_SQLITE_TICKET_SCHEMA_VERSION); Ok(()) }) .unwrap(); } #[test] fn migration_dry_run_repairs_missing_diagnostics_archive_without_mutating_source() { let temp = tempfile::tempdir().unwrap(); let path = temp.path().join("server.db"); { let conn = Connection::open(&path).unwrap(); configure_sqlite(&conn).unwrap(); apply_migrations_through(&conn, 36).unwrap(); conn.execute_batch( "DROP TABLE worker_diagnostics_archives; INSERT INTO workspaces(workspace_id, display_name, state, created_at, updated_at) VALUES ('workspace-a', 'Workspace A', 'active', '1', '1'); INSERT INTO worker_registry( workspace_id, runtime_id, runtime_worker_id, display_name, retention_state, created_at, updated_at ) VALUES ('workspace-a', 'runtime-a', 7, 'Worker 7', 'normal', '1', '1');", ) .unwrap(); } let before = std::fs::read(&path).unwrap(); let plan = SqliteWorkspaceStore::migration_plan(&path).unwrap(); assert_eq!(plan.current_schema_version, 36); assert_eq!(plan.target_schema_version, 40); assert!(plan.migration_required); assert_eq!(plan.worker_count, 1); assert_eq!(plan.mappings[0].legacy_worker_id, 7); assert_eq!( plan.repairs, vec!["create missing worker_diagnostics_archives table"] ); assert_eq!(std::fs::read(&path).unwrap(), before); let store = SqliteWorkspaceStore::open(&path).unwrap(); store .with_conn(|conn| { assert!(table_exists(conn, "worker_diagnostics_archives")?); assert_eq!(current_schema_version(conn)?, 40); Ok(()) }) .unwrap(); } #[test] fn v38_backfills_workspace_scoped_objective_and_worker_human_keys() { let conn = Connection::open_in_memory().unwrap(); configure_sqlite(&conn).unwrap(); apply_migrations_through(&conn, 37).unwrap(); conn.execute( "INSERT INTO workspaces(workspace_id, display_name, state, created_at, updated_at) VALUES ('workspace-a', 'Workspace A', 'active', '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z')", [], ).unwrap(); for (id, created_at) in [ ("objective-later", "2026-01-02T00:00:00Z"), ("objective-earlier", "2026-01-01T00:00:00Z"), ] { conn.execute( "INSERT INTO objectives(workspace_id, objective_id, title, body_md, state, created_at, updated_at) VALUES ('workspace-a', ?1, ?1, '', 'active', ?2, ?2)", params![id, created_at], ).unwrap(); } for (id, created_at) in [ ( "019b57c8-5c00-7000-8000-000000000002", "2026-01-02T00:00:00Z", ), ( "019b5280-0000-7000-8000-000000000001", "2026-01-01T00:00:00Z", ), ] { conn.execute( "INSERT INTO worker_registry(workspace_id, worker_id, runtime_id, display_name, retention_state, created_at, updated_at) VALUES ('workspace-a', ?1, 'runtime-a', ?1, 'normal', ?2, ?2)", params![id, created_at], ).unwrap(); } ticket::migrate_sqlite_ticket_schema(&conn).unwrap(); apply_migrations(&conn).unwrap(); let mut statement = conn .prepare( "SELECT resource_kind, resource_id, human_key FROM workspace_resource_human_keys ORDER BY resource_kind, sequence", ) .unwrap(); let keys = statement .query_map([], |row| { Ok(( row.get::<_, String>(0)?, row.get::<_, String>(1)?, row.get::<_, String>(2)?, )) }) .unwrap() .collect::, _>>() .unwrap(); assert_eq!( keys, vec![ ("objective".into(), "objective-earlier".into(), "O-1".into()), ("objective".into(), "objective-later".into(), "O-2".into()), ( "worker".into(), "019b5280-0000-7000-8000-000000000001".into(), "W-1".into() ), ( "worker".into(), "019b57c8-5c00-7000-8000-000000000002".into(), "W-2".into() ), ] ); assert_eq!(current_schema_version(&conn).unwrap(), 40); let foreign_key_error: Option = conn .query_row("PRAGMA foreign_key_check", [], |row| row.get(0)) .optional() .unwrap(); assert!(foreign_key_error.is_none()); } #[test] fn startup_fails_closed_when_current_ticket_schema_has_drifted() { let conn = Connection::open_in_memory().unwrap(); configure_sqlite(&conn).unwrap(); apply_migrations(&conn).unwrap(); ticket::migrate_sqlite_ticket_schema(&conn).unwrap(); conn.execute_batch("DROP TABLE typed_ticket_artifacts") .unwrap(); let result = SqliteWorkspaceStore::from_connection(conn); let error = match result { Ok(_) => panic!("schema drift unexpectedly passed startup verification"), Err(error) => error, }; assert!(error.to_string().contains("typed_ticket_artifacts")); } #[test] fn removes_unused_control_plane_ticket_tables() { let conn = Connection::open_in_memory().unwrap(); conn.execute_batch( r#" CREATE TABLE tickets (ticket_id TEXT PRIMARY KEY); CREATE TABLE ticket_events (event_id TEXT PRIMARY KEY, ticket_id TEXT REFERENCES tickets(ticket_id)); CREATE TABLE ticket_relations (source_ticket_id TEXT, target_ticket_id TEXT); CREATE TABLE ticket_targets (ticket_id TEXT, target_id TEXT, PRIMARY KEY (ticket_id, target_id)); CREATE TABLE ticket_target_paths (ticket_id TEXT, target_id TEXT, path TEXT); CREATE TABLE ticket_worker_links (ticket_id TEXT, worker_ref_key TEXT); "#, ) .unwrap(); remove_unused_control_plane_ticket_tables(&conn).unwrap(); for table in [ "tickets", "ticket_events", "ticket_relations", "ticket_targets", "ticket_target_paths", "ticket_worker_links", "ticket_notification_outbox", "ticket_notification_deliveries", "ticket_notification_cursors", ] { assert!(!table_exists(&conn, table).unwrap(), "{table} still exists"); } } #[test] fn schema_v35_removes_worker_control_delegation_authority() { let conn = Connection::open_in_memory().unwrap(); configure_sqlite(&conn).unwrap(); for migration in MIGRATIONS .iter() .filter(|migration| migration.version <= 34) { let tx = conn.unchecked_transaction().unwrap(); (migration.apply)(&tx).unwrap(); tx.execute( "INSERT INTO __yoi_schema_migrations (version, name) VALUES (?1, ?2)", params![migration.version, migration.name], ) .unwrap(); tx.commit().unwrap(); } ticket::migrate_sqlite_ticket_schema(&conn).unwrap(); conn.execute_batch( r#" INSERT INTO workspaces ( workspace_id, display_name, state, created_at, updated_at ) VALUES ('workspace-a', 'Workspace A', 'active', '1', '1'); INSERT INTO typed_tickets ( workspace_id, ticket_id, slug, title, status, kind, priority, body, workflow_state, workflow_state_explicit ) VALUES ('workspace-a', 'ticket-a', 'ticket-a', 'Ticket A', 'open', 'task', 'normal', '', 'planning', 1); INSERT INTO worker_registry ( workspace_id, runtime_id, runtime_worker_id, display_name, retention_state, created_at, updated_at ) VALUES ('workspace-a', 'runtime-a', 1, 'Controller', 'normal', '1', '1'), ('workspace-a', 'runtime-a', 2, 'Spawned Worker', 'normal', '1', '1'), ('workspace-a', 'runtime-a', 3, 'Shared Worker', 'normal', '1', '1'), ('workspace-a', 'runtime-a', 4, 'Transferred Worker', 'normal', '1', '1'); INSERT INTO worker_control_grants ( workspace_id, grant_id, controller_runtime_id, controller_worker_id, subject_runtime_id, subject_worker_id, relation, origin, permissions_json, operation_id, created_at, revoked_at ) VALUES ('workspace-a', 'spawned', 'runtime-a', 1, 'runtime-a', 2, 'spawned', 'spawn', '["observe","share","transfer","revoke","stop"]', 'spawn-op', '1', NULL), ('workspace-a', 'shared', 'runtime-a', 1, 'runtime-a', 3, 'shared', 'share', '["observe"]', 'share-op', '1', NULL), ('workspace-a', 'transferred', 'runtime-a', 1, 'runtime-a', 4, 'transferred', 'transfer', '["observe"]', 'transfer-op', '1', NULL); INSERT INTO ticket_worker_assignments ( workspace_id, ticket_id, assignment_id, runtime_id, worker_id, assigned_by, assigned_at ) VALUES ('workspace-a', 'ticket-a', 'assignment-a', 'runtime-a', '1', 'test', '1'); INSERT INTO ticket_current_worker_assignments ( workspace_id, ticket_id, assignment_id, runtime_id, worker_id, updated_at ) VALUES ('workspace-a', 'ticket-a', 'assignment-a', 'runtime-a', '1', '1'); INSERT INTO ticket_assignment_operations ( workspace_id, operation_id, action, ticket_id, runtime_id, worker_id, assignment_id, request_fingerprint, created_at ) VALUES ( 'workspace-a', 'assignment-operation-a', 'assign', 'ticket-a', 'runtime-a', '1', 'assignment-a', 'sha256:test', '1' ); INSERT INTO worker_orphan_diagnostics ( diagnostic_id, workspace_id, runtime_id, worker_id, category, detail, observed_at ) VALUES ( 'orphan-a', 'workspace-a', 'runtime-a', '9', 'runtime_aggregate_without_backend_registry', 'legacy orphan', '1' ); "#, ) .unwrap(); assert_eq!(current_schema_version(&conn).unwrap(), 34); assert!(table_exists(&conn, "worker_control_delegation_operations").unwrap()); apply_migrations(&conn).unwrap(); assert_eq!(current_schema_version(&conn).unwrap(), 40); assert!(!table_exists(&conn, "worker_control_delegation_operations").unwrap()); let controller_worker_id: String = conn .query_row( "SELECT worker_id FROM worker_registry WHERE display_name = 'Controller'", [], |row| row.get(0), ) .unwrap(); assert_eq!( controller_worker_id, WorkerId::from_legacy_binding("workspace-a", "runtime-a", 1).to_string() ); let (grant_controller, grant_subject): (String, String) = conn .query_row( "SELECT controller_worker_id, subject_worker_id \ FROM worker_control_grants WHERE grant_id = 'spawned'", [], |row| Ok((row.get(0)?, row.get(1)?)), ) .unwrap(); assert_eq!(grant_controller, controller_worker_id); assert_eq!( grant_subject, WorkerId::from_legacy_binding("workspace-a", "runtime-a", 2).to_string() ); let expected_assignment_worker = WorkerId::from_legacy_binding("workspace-a", "runtime-a", 1).to_string(); for table in [ "ticket_worker_assignments", "ticket_current_worker_assignments", "ticket_assignment_operations", ] { let sql = format!("SELECT worker_id FROM {table} WHERE ticket_id = 'ticket-a'"); let migrated_worker_id: String = conn.query_row(&sql, [], |row| row.get(0)).unwrap(); assert_eq!( migrated_worker_id, expected_assignment_worker, "{table} retained a legacy Worker id" ); } let orphan_worker_id: String = conn .query_row( "SELECT worker_id FROM worker_orphan_diagnostics WHERE diagnostic_id = 'orphan-a'", [], |row| row.get(0), ) .unwrap(); assert_eq!( orphan_worker_id, WorkerId::from_legacy_binding("workspace-a", "runtime-a", 9).to_string() ); let worker_registry_pk = { let mut statement = conn.prepare("PRAGMA table_info(worker_registry)").unwrap(); statement .query_map([], |row| { Ok((row.get::<_, String>(1)?, row.get::<_, i64>(5)?)) }) .unwrap() .filter_map(|row| { let (name, position) = row.unwrap(); (position > 0).then_some((position, name)) }) .collect::>() }; assert_eq!( worker_registry_pk, vec![ (1, "workspace_id".to_string()), (2, "worker_id".to_string()) ] ); let (permissions_json, revoked_at): (String, Option) = conn .query_row( "SELECT permissions_json, revoked_at FROM worker_control_grants WHERE grant_id = 'spawned'", [], |row| Ok((row.get(0)?, row.get(1)?)), ) .unwrap(); assert_eq!( serde_json::from_str::>(&permissions_json).unwrap(), vec!["observe", "stop"] ); assert!(revoked_at.is_none()); for grant_id in ["shared", "transferred"] { let revoked_at: Option = conn .query_row( "SELECT revoked_at FROM worker_control_grants WHERE grant_id = ?1", [grant_id], |row| row.get(0), ) .unwrap(); assert!(revoked_at.is_some(), "{grant_id} grant remained active"); } } #[test] fn schema_v24_adds_attachment_reservations_to_already_applied_v23() { let conn = Connection::open_in_memory().unwrap(); configure_sqlite(&conn).unwrap(); for migration in MIGRATIONS .iter() .filter(|migration| migration.version <= 23) { let tx = conn.unchecked_transaction().unwrap(); (migration.apply)(&tx).unwrap(); tx.execute( "INSERT INTO __yoi_schema_migrations (version, name) VALUES (?1, ?2)", params![migration.version, migration.name], ) .unwrap(); tx.commit().unwrap(); } assert_eq!(current_schema_version(&conn).unwrap(), 23); assert!(!table_exists(&conn, "worker_workdir_attachment_reservations").unwrap()); apply_migrations(&conn).unwrap(); assert_eq!(current_schema_version(&conn).unwrap(), 40); assert!(table_exists(&conn, "worker_workdir_attachment_reservations").unwrap()); } #[test] fn schema_v26_removes_legacy_backend_flow_runtime_tables() { let conn = Connection::open_in_memory().unwrap(); configure_sqlite(&conn).unwrap(); for migration in MIGRATIONS .iter() .filter(|migration| migration.version <= 25) { let tx = conn.unchecked_transaction().unwrap(); (migration.apply)(&tx).unwrap(); tx.execute( "INSERT INTO __yoi_schema_migrations (version, name) VALUES (?1, ?2)", params![migration.version, migration.name], ) .unwrap(); tx.commit().unwrap(); } conn.execute_batch( r#" CREATE TABLE flow_instances (instance_id TEXT PRIMARY KEY); CREATE TABLE flow_transition_attempts (attempt_id TEXT PRIMARY KEY); CREATE TABLE flow_events (event_id TEXT PRIMARY KEY); "#, ) .unwrap(); assert_eq!(current_schema_version(&conn).unwrap(), 25); apply_migrations(&conn).unwrap(); assert_eq!(current_schema_version(&conn).unwrap(), 40); assert!(table_exists(&conn, "flow_sources").unwrap()); assert!(table_exists(&conn, "flow_source_revisions").unwrap()); assert!(!table_exists(&conn, "flow_instances").unwrap()); assert!(!table_exists(&conn, "flow_transition_attempts").unwrap()); assert!(!table_exists(&conn, "flow_events").unwrap()); } #[test] fn schema_v27_upgrades_repository_identity_without_losing_workspace_owned_references() { let conn = Connection::open_in_memory().unwrap(); configure_sqlite(&conn).unwrap(); for migration in MIGRATIONS .iter() .filter(|migration| migration.version <= 26) { let tx = conn.unchecked_transaction().unwrap(); (migration.apply)(&tx).unwrap(); tx.execute( "INSERT INTO __yoi_schema_migrations (version, name) VALUES (?1, ?2)", params![migration.version, migration.name], ) .unwrap(); tx.commit().unwrap(); } conn.execute_batch( r#" INSERT INTO workspaces ( workspace_id, display_name, state, created_at, updated_at ) VALUES ('workspace-a', 'Workspace A', 'active', '1', '1'), ('workspace-b', 'Workspace B', 'active', '1', '1'); INSERT INTO repositories ( repository_id, workspace_id, name, kind, uri, created_at, updated_at ) VALUES ('main', 'workspace-a', 'Main', 'git', '/repo-a', '1', '1'); INSERT INTO artifacts ( workspace_id, artifact_id, kind, uri, created_at, created_by_kind, created_by_key, created_by_display, repository_id ) VALUES ( 'workspace-a', 'artifact-1', 'report', 'artifact://1', '1', 'worker', 'worker-1', 'Worker 1', 'main' ); INSERT INTO worker_registry ( workspace_id, runtime_id, runtime_worker_id, display_name, retention_state, created_at, updated_at ) VALUES ('workspace-a', 'runtime-a', 1, 'Worker 1', 'normal', '1', '1'); INSERT INTO workdir_registry ( workspace_id, workdir_id, runtime_id, repository_id, creation_selector, creation_ref, materialization_status, cleanliness, created_at, updated_at, current_selector, current_ref ) VALUES ( 'workspace-a', 'workdir-1', 'runtime-a', 'main', 'develop', 'abc', 'present', 'clean', '1', '1', 'develop', 'abc' ); INSERT INTO worker_workdir_links ( workspace_id, runtime_id, runtime_worker_id, workdir_id, role, linked_at, unlinked_at ) VALUES ('workspace-a', 'runtime-a', 1, 'workdir-1', 'attachment', '1', NULL); INSERT INTO worker_workdir_attachment_reservations ( workspace_id, workdir_id, reservation_id, reserved_at ) VALUES ('workspace-a', 'workdir-1', 'reservation-1', '1'); "#, ) .unwrap(); apply_migrations(&conn).unwrap(); assert_eq!(current_schema_version(&conn).unwrap(), 40); let repositories_sql: String = conn .query_row( "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'repositories'", [], |row| row.get(0), ) .unwrap(); assert!(repositories_sql.contains("PRIMARY KEY (workspace_id, repository_id)")); let preserved: (i64, i64, i64) = ( conn.query_row("SELECT COUNT(*) FROM artifacts", [], |row| row.get(0)) .unwrap(), conn.query_row("SELECT COUNT(*) FROM worker_workdir_links", [], |row| { row.get(0) }) .unwrap(), conn.query_row( "SELECT COUNT(*) FROM worker_workdir_attachment_reservations", [], |row| row.get(0), ) .unwrap(), ); assert_eq!(preserved, (1, 1, 1)); let foreign_key_violations: i64 = conn .query_row("SELECT COUNT(*) FROM pragma_foreign_key_check", [], |row| { row.get(0) }) .unwrap(); assert_eq!(foreign_key_violations, 0); conn.execute( r#"INSERT INTO repositories ( workspace_id, repository_id, name, kind, uri, created_at, updated_at ) VALUES ('workspace-b', 'main', 'Other Main', 'git', '/repo-b', '2', '2')"#, [], ) .unwrap(); assert_eq!( conn.query_row( "SELECT COUNT(*) FROM repositories WHERE repository_id = 'main'", [], |row| row.get::<_, i64>(0), ) .unwrap(), 2 ); assert!( conn.execute( r#"INSERT INTO workdir_registry ( workspace_id, workdir_id, runtime_id, repository_id, materialization_status, cleanliness, created_at, updated_at ) VALUES ('workspace-b', 'invalid', 'runtime-b', 'missing', 'present', 'clean', '2', '2')"#, [], ) .is_err() ); assert!( conn.execute( r#"INSERT INTO artifacts ( workspace_id, artifact_id, kind, uri, created_at, created_by_kind, created_by_key, created_by_display, repository_id ) VALUES ('workspace-b', 'invalid-artifact', 'report', 'artifact://invalid', '2', 'worker', 'worker-2', 'Worker 2', 'missing')"#, [], ) .is_err() ); } #[test] fn schema_v27_rejects_cross_workspace_legacy_repository_references() { let conn = Connection::open_in_memory().unwrap(); configure_sqlite(&conn).unwrap(); for migration in MIGRATIONS .iter() .filter(|migration| migration.version <= 26) { let tx = conn.unchecked_transaction().unwrap(); (migration.apply)(&tx).unwrap(); tx.execute( "INSERT INTO __yoi_schema_migrations (version, name) VALUES (?1, ?2)", params![migration.version, migration.name], ) .unwrap(); tx.commit().unwrap(); } conn.execute_batch( r#" INSERT INTO workspaces ( workspace_id, display_name, state, created_at, updated_at ) VALUES ('workspace-a', 'Workspace A', 'active', '1', '1'), ('workspace-b', 'Workspace B', 'active', '1', '1'); INSERT INTO repositories ( repository_id, workspace_id, name, kind, uri, created_at, updated_at ) VALUES ('main', 'workspace-a', 'Main', 'git', '/repo-a', '1', '1'); INSERT INTO workdir_registry ( workspace_id, workdir_id, runtime_id, repository_id, materialization_status, cleanliness, created_at, updated_at ) VALUES ('workspace-b', 'foreign-workdir', 'runtime-b', 'main', 'present', 'clean', '1', '1'); "#, ) .unwrap(); let error = apply_migrations(&conn).unwrap_err(); assert!(error.to_string().contains("workdir_registry")); assert!(error.to_string().contains("workspace-b")); assert_eq!(current_schema_version(&conn).unwrap(), 26); assert_eq!( conn.query_row("SELECT COUNT(*) FROM workdir_registry", [], |row| { row.get::<_, i64>(0) }) .unwrap(), 1 ); } #[tokio::test] async fn workspace_schema_rejects_cross_workspace_ticket_repository_reference_at_write_time() { let dir = tempfile::tempdir().unwrap(); let database_path = dir.path().join("workspace.sqlite"); let store = SqliteWorkspaceStore::open(&database_path).unwrap(); for workspace_id in ["workspace-a", "workspace-b"] { store .upsert_workspace(&WorkspaceRecord { workspace_id: workspace_id.to_string(), owner_account_id: None, display_name: workspace_id.to_string(), state: "active".to_string(), created_at: "1".to_string(), updated_at: "1".to_string(), }) .await .unwrap(); } store .upsert_repository(&RepositoryRecord { workspace_id: "workspace-a".to_string(), repository_id: "main".to_string(), name: "Main".to_string(), kind: "git".to_string(), provider: Some("git".to_string()), uri: "/repo-a".to_string(), default_ref: Some("HEAD".to_string()), auth_ref_kind: None, auth_ref_key: None, created_at: "1".to_string(), updated_at: "1".to_string(), }) .unwrap(); drop(store); let backend = ticket::SqliteTicketBackend::open_verified( database_path.clone(), "workspace-b".to_string(), ) .unwrap(); let mut input = ticket::NewTicket::new("Foreign repository"); input.repository_id = Some("main".to_string()); let error = ticket::TicketBackend::create(&backend, input).unwrap_err(); assert!( error.to_string().contains("FOREIGN KEY constraint failed"), "{error}" ); drop(backend); SqliteWorkspaceStore::open(&database_path).unwrap(); } #[tokio::test] async fn migrates_sqlite_and_preserves_workspace_record() { let dir = tempfile::tempdir().unwrap(); let db = dir.path().join("control-plane.sqlite"); let store = SqliteWorkspaceStore::open(&db).unwrap(); assert_eq!(store.schema_version().await.unwrap(), 40); assert!( !store .with_conn(|conn| table_exists(conn, "worker_workspace_credentials")) .unwrap() ); let record = WorkspaceRecord { workspace_id: "local-dev".to_string(), owner_account_id: None, display_name: "Yoi Dev".to_string(), state: "active".to_string(), created_at: "2026-01-01T00:00:00Z".to_string(), updated_at: "2026-01-01T00:00:00Z".to_string(), }; store.upsert_workspace(&record).await.unwrap(); let reopened = SqliteWorkspaceStore::open(&db).unwrap(); assert_eq!(reopened.schema_version().await.unwrap(), 40); assert_eq!( reopened.get_workspace("local-dev").await.unwrap(), Some(record) ); } #[tokio::test] async fn objective_creation_allocates_and_resolves_workspace_human_key() { let dir = tempfile::tempdir().unwrap(); let store = SqliteWorkspaceStore::open(dir.path().join("server.db")).unwrap(); store .upsert_workspace(&WorkspaceRecord { workspace_id: "workspace-a".into(), owner_account_id: None, display_name: "Workspace A".into(), state: "active".into(), created_at: "1".into(), updated_at: "1".into(), }) .await .unwrap(); store .upsert_objective(&ObjectiveRecord { workspace_id: "workspace-a".into(), objective_id: "objective-internal".into(), title: "Ship it".into(), state: "active".into(), body_md: String::new(), created_at: "2".into(), updated_at: "2".into(), }) .unwrap(); assert_eq!( store .resource_human_key( "workspace-a", WorkspaceResourceKind::Objective, "objective-internal" ) .unwrap() .as_deref(), Some("O-1") ); assert_eq!( store .resolve_resource_reference("workspace-a", WorkspaceResourceKind::Objective, "O-1") .unwrap() .as_deref(), Some("objective-internal") ); } #[tokio::test] async fn worker_create_reservation_allocates_uuid_before_runtime_and_replays_exact_input() { let dir = tempfile::tempdir().unwrap(); let store = SqliteWorkspaceStore::open(dir.path().join("server.db")).unwrap(); store .upsert_workspace(&WorkspaceRecord { workspace_id: "workspace-a".to_string(), owner_account_id: None, display_name: "Workspace A".to_string(), state: "active".to_string(), created_at: "2026-08-06T00:00:00Z".to_string(), updated_at: "2026-08-06T00:00:00Z".to_string(), }) .await .unwrap(); let reserved = store .reserve_worker_create("workspace-a", "arcadia", "operation-1", "sha256:one") .unwrap(); assert_eq!( reserved.as_uuid().get_version(), Some(uuid::Version::SortRand) ); assert_eq!( store .reserve_worker_create("workspace-a", "arcadia", "operation-1", "sha256:one") .unwrap(), reserved ); assert!( store .reserve_worker_create("workspace-a", "arcadia", "operation-1", "sha256:different") .is_err() ); assert_eq!( store .resource_human_key( "workspace-a", WorkspaceResourceKind::Worker, &reserved.to_string() ) .unwrap() .as_deref(), Some("W-1") ); assert_eq!( store .resolve_resource_reference("workspace-a", WorkspaceResourceKind::Worker, "W-1") .unwrap(), Some(reserved.to_string()) ); let second = store .reserve_worker_create("workspace-a", "arcadia", "operation-2", "sha256:two") .unwrap(); assert_eq!( store .resource_human_key( "workspace-a", WorkspaceResourceKind::Worker, &second.to_string() ) .unwrap() .as_deref(), Some("W-2") ); store .complete_worker_create_reservation("workspace-a", reserved) .unwrap(); let state: String = store .with_conn(|conn| { conn.query_row( "SELECT state FROM worker_create_reservations \ WHERE workspace_id = 'workspace-a' AND worker_id = ?1", [reserved.to_string()], |row| row.get(0), ) .map_err(Error::from) }) .unwrap(); assert_eq!(state, "created"); } #[tokio::test] async fn workspace_flow_sources_keep_revisions_and_builtins_stay_resources() { let dir = tempfile::tempdir().unwrap(); let store = SqliteWorkspaceStore::open(dir.path().join("server.db")).unwrap(); store .upsert_workspace(&WorkspaceRecord { workspace_id: "workspace-a".to_string(), owner_account_id: None, display_name: "Workspace A".to_string(), state: "active".to_string(), created_at: "2026-08-06T00:00:00Z".to_string(), updated_at: "2026-08-06T00:00:00Z".to_string(), }) .await .unwrap(); let workspace_source = r#"{ schema_version = 1; name = "coder-review"; initial = "work"; states = { work = { instructions = "Workspace revision one."; transitions = { done = { target = "done"; condition = "Done."; }; }; }; done = { instructions = ""; terminal = true; }; }; }"#; let workspace = store .put_flow_source_for_kind( "workspace-a", FlowSourceKind::Workspace, "flows/coder-review.dcdl", workspace_source, "2026-08-06T00:00:01Z", ) .unwrap(); let builtin = flow::builtin_flow_source("coder-review").unwrap(); assert_eq!(builtin.slug, workspace.name); assert!( store .put_flow_source_for_kind( "workspace-a", FlowSourceKind::Builtin, builtin.path, builtin.content, "2026-08-06T00:00:02Z", ) .is_err() ); assert_eq!( store.list_flow_sources("workspace-a").unwrap(), vec![workspace.clone()] ); let revision_two = workspace_source.replace("Workspace revision one.", "Workspace revision two."); let updated = store .put_flow_source_for_kind( "workspace-a", FlowSourceKind::Workspace, "flows/coder-review.dcdl", &revision_two, "2026-08-06T00:00:03Z", ) .unwrap(); assert_eq!(updated.flow_id, workspace.flow_id); assert_eq!(updated.revision, 2); let pinned = store .get_flow_source_revision("workspace-a", &workspace.flow_id, 1) .unwrap() .unwrap(); assert_eq!(pinned.content, workspace_source); assert_eq!(pinned.definition.name, "coder-review"); } #[tokio::test] async fn ticket_worker_assignment_replaces_current_and_preserves_audit_history() { let dir = tempfile::tempdir().unwrap(); let db = dir.path().join("server.db"); let store = SqliteWorkspaceStore::open(&db).unwrap(); store .upsert_workspace(&WorkspaceRecord { workspace_id: "workspace-a".to_string(), owner_account_id: None, display_name: "Workspace A".to_string(), state: "active".to_string(), created_at: "2026-07-32T00:00:00Z".to_string(), updated_at: "2026-07-32T00:00:00Z".to_string(), }) .await .unwrap(); store .with_conn(|conn| { conn.execute_batch( r#" INSERT INTO typed_tickets ( workspace_id, ticket_id, slug, title, status, kind, priority, body, workflow_state, workflow_state_explicit ) VALUES ('workspace-a', 'ticket-1', 'ticket-1', 'Ticket 1', 'open', 'task', 'normal', '', 'planning', 1), ('workspace-a', 'ticket-2', 'ticket-2', 'Ticket 2', 'open', 'task', 'normal', '', 'planning', 1), ('workspace-a', 'ticket-3', 'ticket-3', 'Ticket 3', 'open', 'task', 'normal', '', 'planning', 1); INSERT INTO worker_registry ( workspace_id, runtime_id, worker_id, display_name, retention_state, created_at, updated_at ) VALUES ('workspace-a', 'runtime-1', 'worker-1', 'Worker 1', 'normal', '1', '1'), ('workspace-a', 'runtime-1', 'worker-other', 'Other Worker', 'normal', '1', '1'), ('workspace-a', 'runtime-2', 'worker-2', 'Worker 2', 'normal', '1', '1'), ('workspace-a', 'runtime-3', 'worker-3', 'Worker 3', 'normal', '1', '1'); "#, )?; Ok(()) }) .unwrap(); let first = TicketWorkerAssignmentRecord { workspace_id: "workspace-a".to_string(), ticket_id: "ticket-1".to_string(), assignment_id: "assignment-1".to_string(), worker: RuntimeWorkerRef::new("runtime-1", "worker-1"), assigned_by: "user-1".to_string(), assigned_at: "2026-07-32T00:00:01Z".to_string(), }; let created = store .set_current_ticket_worker_assignment(&first, None, "event-1", "operation-1", false) .unwrap(); assert_eq!(created.current, first); assert_eq!(created.previous, None); let retried = store .set_current_ticket_worker_assignment( &TicketWorkerAssignmentRecord { assignment_id: "ignored-retry-assignment".to_string(), ..first.clone() }, None, "ignored-retry-event", "operation-1", false, ) .unwrap(); assert_eq!(retried.current, first); assert_eq!( store .list_ticket_worker_assignment_events("workspace-a", "ticket-1", 10) .unwrap() .len(), 1, "idempotent retry must not append another assignment event" ); let implicit_reassign = store .set_current_ticket_worker_assignment( &TicketWorkerAssignmentRecord { assignment_id: "implicit-reassign".to_string(), worker: RuntimeWorkerRef::new("runtime-1", "worker-other"), ..first.clone() }, None, "implicit-event", "implicit-operation", false, ) .unwrap_err(); assert!(matches!( implicit_reassign, Error::TicketAssignmentConflict(_) )); let worker_conflict = store .set_current_ticket_worker_assignment( &TicketWorkerAssignmentRecord { ticket_id: "ticket-2".to_string(), assignment_id: "worker-conflict".to_string(), ..first.clone() }, None, "worker-conflict-event", "worker-conflict-operation", false, ) .unwrap_err(); assert!(matches!( worker_conflict, Error::TicketAssignmentConflict(_) )); let second = TicketWorkerAssignmentRecord { assignment_id: "assignment-2".to_string(), worker: RuntimeWorkerRef::new("runtime-2", "worker-2"), assigned_by: "user-2".to_string(), assigned_at: "2026-07-32T00:00:02Z".to_string(), ..first.clone() }; let replaced = store .set_current_ticket_worker_assignment( &second, Some("assignment-1"), "event-2", "operation-2", true, ) .unwrap(); assert_eq!(replaced.current, second); assert_eq!(replaced.previous, Some(first.clone())); let replayed_reassignment = store .set_current_ticket_worker_assignment( &second, Some("assignment-1"), "ignored-reassign-event", "operation-2", true, ) .unwrap(); assert_eq!(replayed_reassignment, replaced); assert_eq!( store .get_current_ticket_worker_assignment("workspace-a", "ticket-1") .unwrap(), Some(second.clone()) ); let stale = store .clear_current_ticket_worker_assignment( "workspace-a", "ticket-1", Some("assignment-1"), "unassign-operation-stale", "event-stale", "user-1", "2026-07-32T00:00:03Z", ) .unwrap_err(); assert!(matches!(stale, Error::TicketAssignmentConflict(_))); let cleared = store .clear_current_ticket_worker_assignment( "workspace-a", "ticket-1", Some("assignment-2"), "unassign-operation-2", "event-3", "user-2", "2026-07-32T00:00:03Z", ) .unwrap(); assert_eq!(cleared, Some(second.clone())); let retried_clear = store .clear_current_ticket_worker_assignment( "workspace-a", "ticket-1", Some("assignment-2"), "unassign-operation-2", "ignored-clear-event", "user-2", "2026-07-32T00:00:04Z", ) .unwrap(); assert_eq!(retried_clear, Some(second)); store .reserve_ticket_assignment_operation( "workspace-a", "reserved-operation", "ticket-3", "runtime-3", None, "sha256:reserved", "2026-07-32T00:00:05Z", ) .unwrap(); drop(store); let store = SqliteWorkspaceStore::open(&db).unwrap(); let pending = store .get_ticket_assignment_operation("workspace-a", "reserved-operation") .unwrap() .unwrap(); assert_eq!(pending.worker, None); assert_eq!( pending.request_fingerprint.as_deref(), Some("sha256:reserved") ); store .bind_ticket_assignment_operation_worker( "workspace-a", "reserved-operation", "worker-3", ) .unwrap(); let reserved_assignment = TicketWorkerAssignmentRecord { workspace_id: "workspace-a".to_string(), ticket_id: "ticket-3".to_string(), assignment_id: "assignment-3".to_string(), worker: RuntimeWorkerRef::new("runtime-3", "worker-3"), assigned_by: "runtime".to_string(), assigned_at: "2026-07-32T00:00:06Z".to_string(), }; let completed_reservation = store .set_current_ticket_worker_assignment( &reserved_assignment, None, "reserved-event", "reserved-operation", false, ) .unwrap(); assert_eq!(completed_reservation.current, reserved_assignment); assert_eq!( store .get_ticket_assignment_operation("workspace-a", "reserved-operation") .unwrap() .and_then(|operation| operation.assignment_id), Some("assignment-3".to_string()) ); assert_eq!( store .get_current_ticket_worker_assignment("workspace-a", "ticket-1") .unwrap(), None ); store .rollback_ticket_assignment_operation("workspace-a", "reserved-operation") .unwrap(); assert_eq!( store .get_ticket_assignment_operation("workspace-a", "reserved-operation") .unwrap(), None ); assert_eq!( store .get_current_ticket_worker_assignment("workspace-a", "ticket-3") .unwrap(), None ); assert!( store .list_ticket_worker_assignment_events("workspace-a", "ticket-3", 10) .unwrap() .is_empty() ); store .rollback_ticket_assignment_operation("workspace-a", "reserved-operation") .unwrap(); let events = store .list_ticket_worker_assignment_events("workspace-a", "ticket-1", 10) .unwrap(); assert_eq!( events .iter() .map(|event| event.action.as_str()) .collect::>(), vec!["unassigned", "reassigned", "assigned"] ); assert_eq!(events[1].assignment_id.as_deref(), Some("assignment-2")); assert_eq!( events[1].previous_assignment_id.as_deref(), Some("assignment-1") ); } #[test] fn server_refuses_a_database_from_a_newer_schema_generation() { let conn = Connection::open_in_memory().unwrap(); configure_sqlite(&conn).unwrap(); apply_migrations(&conn).unwrap(); conn.execute( "INSERT INTO __yoi_schema_migrations (version, name) VALUES (41, 'future')", [], ) .unwrap(); let error = apply_migrations(&conn).unwrap_err().to_string(); assert!(error.contains("schema version 41 is newer"), "{error}"); assert!(error.contains("refusing to serve"), "{error}"); } #[test] fn startup_rejects_missing_workspace_resource_constraint_trigger() { let temp = tempfile::tempdir().unwrap(); let path = temp.path().join("server.db"); let store = SqliteWorkspaceStore::open(&path).unwrap(); store .with_conn(|conn| { conn.execute_batch("DROP TRIGGER ticket_worker_assignments_validate_insert")?; Ok(()) }) .unwrap(); drop(store); let error = match SqliteWorkspaceStore::open(&path) { Ok(_) => panic!("missing assignment constraint trigger must fail closed"), Err(error) => error, }; assert!( error .to_string() .contains("ticket_worker_assignments_validate_insert"), "{error}" ); } #[test] fn startup_accepts_retained_assignment_history_after_parent_cleanup() { let temp = tempfile::tempdir().unwrap(); let path = temp.path().join("server.db"); let store = SqliteWorkspaceStore::open(&path).unwrap(); store .with_conn(|conn| { conn.execute_batch( r#" INSERT INTO workspaces (workspace_id, display_name, state, created_at, updated_at) VALUES ('workspace-a', 'A', 'active', '2026-01-01', '2026-01-01'); INSERT INTO typed_tickets ( workspace_id, ticket_id, slug, title, status, kind, priority, body, workflow_state, workflow_state_explicit ) VALUES ('workspace-a', 'ticket-a', 'ticket-a', 'A', 'open', 'task', 'normal', '', 'planning', 1); INSERT INTO worker_registry ( workspace_id, runtime_id, worker_id, display_name, retention_state, created_at, updated_at ) VALUES ( 'workspace-a', 'runtime-a', '00000000-0000-7000-8000-000000000001', 'Worker A', 'normal', '2026-01-01', '2026-01-01' ); INSERT INTO ticket_worker_assignments ( workspace_id, ticket_id, assignment_id, runtime_id, worker_id, assigned_by, assigned_at ) VALUES ( 'workspace-a', 'ticket-a', 'assignment-a', 'runtime-a', '00000000-0000-7000-8000-000000000001', 'tester', '2026-01-01' ); DELETE FROM worker_registry WHERE workspace_id = 'workspace-a' AND worker_id = '00000000-0000-7000-8000-000000000001'; DELETE FROM typed_tickets WHERE workspace_id = 'workspace-a' AND ticket_id = 'ticket-a'; INSERT INTO workspaces (workspace_id, display_name, state, created_at, updated_at) VALUES ('workspace-b', 'B', 'active', '2026-01-01', '2026-01-01'); INSERT INTO typed_tickets ( workspace_id, ticket_id, slug, title, status, kind, priority, body, workflow_state, workflow_state_explicit ) VALUES ('workspace-b', 'ticket-a', 'ticket-a-b', 'B', 'open', 'task', 'normal', '', 'planning', 1); INSERT INTO worker_registry ( workspace_id, runtime_id, worker_id, display_name, retention_state, created_at, updated_at ) VALUES ( 'workspace-b', 'runtime-b', '00000000-0000-7000-8000-000000000001', 'Worker B', 'normal', '2026-01-01', '2026-01-01' ); "#, )?; Ok(()) }) .unwrap(); drop(store); let reopened = SqliteWorkspaceStore::open(&path).unwrap(); reopened .with_conn(|conn| { let retained: i64 = conn.query_row( "SELECT COUNT(*) FROM ticket_worker_assignments WHERE workspace_id = 'workspace-a'", [], |row| row.get(0), )?; assert_eq!(retained, 1); let ticket_tombstones: i64 = conn.query_row( "SELECT COUNT(*) FROM ticket_assignment_ticket_tombstones WHERE workspace_id = 'workspace-a' AND ticket_id = 'ticket-a'", [], |row| row.get(0), )?; let worker_tombstones: i64 = conn.query_row( "SELECT COUNT(*) FROM ticket_assignment_worker_tombstones WHERE workspace_id = 'workspace-a' AND runtime_id = 'runtime-a' AND worker_id = '00000000-0000-7000-8000-000000000001'", [], |row| row.get(0), )?; assert_eq!(ticket_tombstones, 1); assert_eq!(worker_tombstones, 1); Ok(()) }) .unwrap(); } #[test] fn migration_plan_lists_workspace_reference_violations_without_mutating_source() { let temp = tempfile::tempdir().unwrap(); let path = temp.path().join("server.db"); let conn = Connection::open(&path).unwrap(); configure_sqlite(&conn).unwrap(); apply_migrations_through(&conn, 38).unwrap(); ticket::migrate_sqlite_ticket_schema(&conn).unwrap(); merge_request::migrate(&conn).unwrap(); conn.execute_batch( r#" INSERT INTO workspaces (workspace_id, display_name, state, created_at, updated_at) VALUES ('workspace-a', 'A', 'active', '2026-01-01', '2026-01-01'), ('workspace-b', 'B', 'active', '2026-01-01', '2026-01-01'); INSERT INTO typed_tickets ( workspace_id, ticket_id, slug, title, status, kind, priority, body, workflow_state, workflow_state_explicit ) VALUES ('workspace-a', 'ticket-a', 'ticket-a', 'A', 'open', 'task', 'normal', '', 'planning', 1), ('workspace-b', 'ticket-b', 'ticket-b', 'B', 'open', 'task', 'normal', '', 'planning', 1); INSERT INTO typed_ticket_relations (workspace_id, ticket_id, kind, target, note, author, at) VALUES ('workspace-b', 'ticket-b', 'related', 'ticket-a', NULL, 'tester', '2026-01-01'); "#, ) .unwrap(); drop(conn); let error = SqliteWorkspaceStore::migration_plan(&path) .unwrap_err() .to_string(); assert!(error.contains("typed_ticket_relations.target"), "{error}"); assert!( error.contains("workspace-b/ticket-b -> ticket-a"), "{error}" ); let source = Connection::open(&path).unwrap(); assert_eq!(current_schema_version(&source).unwrap(), 38); assert_eq!( source .query_row("SELECT COUNT(*) FROM typed_ticket_relations", [], |row| { row.get::<_, i64>(0) }) .unwrap(), 1 ); } #[test] fn workspace_resource_fk_migration_rolls_back_constraint_failures() { let conn = Connection::open_in_memory().unwrap(); configure_sqlite(&conn).unwrap(); apply_migrations_through(&conn, 38).unwrap(); ticket::migrate_sqlite_ticket_schema(&conn).unwrap(); merge_request::migrate(&conn).unwrap(); conn.execute( "INSERT INTO workspaces (workspace_id, display_name, state, created_at, updated_at) \ VALUES ('workspace-a', 'A', 'active', '2026-01-01', '2026-01-01')", [], ) .unwrap(); conn.execute( "INSERT INTO typed_tickets (workspace_id, ticket_id, slug, title, status, kind, priority, body, \ workflow_state, workflow_state_explicit, repository_id, ref_selector) \ VALUES ('workspace-a', 'ticket-a', 'ticket-a', 'A', 'open', 'task', 'normal', '', \ 'planning', 1, NULL, 'develop')", [], ) .unwrap(); conn.execute_batch("CREATE TABLE typed_tickets_v39 (sentinel TEXT)") .unwrap(); let error = apply_migrations_through(&conn, 39).unwrap_err().to_string(); assert!(error.contains("CREATE TABLE typed_tickets_v39"), "{error}"); assert_eq!(current_schema_version(&conn).unwrap(), 38); assert_eq!( conn.query_row( "SELECT ref_selector FROM typed_tickets WHERE workspace_id = 'workspace-a' AND ticket_id = 'ticket-a'", [], |row| row.get::<_, String>(0), ) .unwrap(), "develop" ); assert!(table_exists(&conn, "typed_tickets_v39").unwrap()); assert!(!table_exists(&conn, "objectives_v39").unwrap()); let foreign_keys_enabled: i64 = conn .query_row("PRAGMA foreign_keys", [], |row| row.get(0)) .unwrap(); assert_eq!(foreign_keys_enabled, 1); } #[test] fn schema_v40_adds_workspace_create_operations_and_fail_closed_runtime_assignment() { let mut conn = Connection::open_in_memory().unwrap(); configure_sqlite(&conn).unwrap(); apply_migrations_through(&conn, 39).unwrap(); let now = "2026-01-01T00:00:00Z"; conn.execute( r#"INSERT INTO workspaces ( workspace_id, display_name, state, created_at, updated_at ) VALUES ('workspace-a', 'Workspace A', 'active', ?1, ?1)"#, params![now], ) .unwrap(); conn.execute( r#"INSERT INTO trusted_runtime_records ( runtime_id, display_name, base_url, public_key, created_at, updated_at ) VALUES ('runtime-a', 'Runtime A', 'http://runtime-a.test', 'key', ?1, ?1)"#, params![now], ) .unwrap(); apply_migrations(&mut conn).unwrap(); assert_eq!(current_schema_version(&conn).unwrap(), 40); let workspace_id: Option = conn .query_row( "SELECT workspace_id FROM trusted_runtime_records WHERE runtime_id = 'runtime-a'", [], |row| row.get(0), ) .unwrap(); assert_eq!(workspace_id, None); assert!(table_exists(&conn, "workspace_create_operations").unwrap()); } #[test] fn workspace_resource_fk_migration_preflights_and_enforces_composite_identity() { let conn = Connection::open_in_memory().unwrap(); configure_sqlite(&conn).unwrap(); apply_migrations_through(&conn, 38).unwrap(); ticket::migrate_sqlite_ticket_schema(&conn).unwrap(); merge_request::migrate(&conn).unwrap(); conn.execute_batch( r#" INSERT INTO workspaces ( workspace_id, display_name, state, created_at, updated_at ) VALUES ('workspace-a', 'A', 'active', '2026-01-01', '2026-01-01'), ('workspace-b', 'B', 'active', '2026-01-01', '2026-01-01'); INSERT INTO repositories ( workspace_id, repository_id, name, kind, provider, uri, default_ref, created_at, updated_at ) VALUES ('workspace-a', 'repo-a', 'Repo A', 'git', 'git', '/repo-a', 'develop', '2026-01-01', '2026-01-01'); INSERT INTO typed_tickets ( workspace_id, ticket_id, slug, title, status, kind, priority, body, created_at, updated_at, workflow_state, workflow_state_explicit, repository_id, ref_selector ) VALUES ('workspace-a', 'ticket-a', 'ticket-a', 'A', 'open', 'task', 'normal', '', '2026-01-01', '2026-01-01', 'planning', 1, 'repo-a', 'develop'), ('workspace-b', 'ticket-b', 'ticket-b', 'B', 'open', 'task', 'normal', '', '2026-01-01', '2026-01-01', 'planning', 1, NULL, NULL), ('workspace-b', 'ticket-b2', 'ticket-b2', 'B2', 'open', 'task', 'normal', '', '2026-01-01', '2026-01-01', 'planning', 1, NULL, NULL); INSERT INTO objectives ( workspace_id, objective_id, title, state, body_md, created_at, updated_at ) VALUES ('workspace-a', 'objective-a', 'A', 'active', '', '2026-01-01', '2026-01-01'); INSERT INTO typed_ticket_relations ( workspace_id, ticket_id, kind, target, note, author, at ) VALUES ('workspace-b', 'ticket-b', 'related', 'ticket-a', NULL, 'tester', '2026-01-01'); INSERT INTO objective_ticket_links ( workspace_id, objective_id, ticket_id, kind, created_at ) VALUES ('workspace-b', 'objective-a', 'ticket-b', 'tracks', '2026-01-01'); INSERT INTO worker_registry ( workspace_id, runtime_id, worker_id, display_name, retention_state, created_at, updated_at ) VALUES ('workspace-a', 'runtime-a', '00000000-0000-7000-8000-000000000001', 'Worker A', 'normal', '2026-01-01', '2026-01-01'), ('workspace-b', 'runtime-b', '00000000-0000-7000-8000-000000000002', 'Worker B', 'normal', '2026-01-01', '2026-01-01'); INSERT INTO ticket_worker_assignments ( workspace_id, ticket_id, assignment_id, runtime_id, worker_id, assigned_by, assigned_at ) VALUES ( 'workspace-b', 'ticket-b', 'assignment-cross-worker', 'runtime-a', '00000000-0000-7000-8000-000000000001', 'tester', '2026-01-01' ); INSERT INTO ticket_worker_assignments ( workspace_id, ticket_id, assignment_id, runtime_id, worker_id, assigned_by, assigned_at ) VALUES ( 'workspace-b', 'ticket-b', 'assignment-runtime-mismatch', 'runtime-wrong', '00000000-0000-7000-8000-000000000002', 'tester', '2026-01-01' ); INSERT INTO ticket_worker_assignments ( workspace_id, ticket_id, assignment_id, runtime_id, worker_id, assigned_by, assigned_at ) VALUES ( 'workspace-b', 'ticket-missing', 'assignment-missing-parents', 'runtime-missing', '00000000-0000-7000-8000-000000000003', 'tester', '2026-01-01' ); INSERT INTO ticket_worker_assignments ( workspace_id, ticket_id, assignment_id, runtime_id, worker_id, assigned_by, assigned_at ) VALUES ( 'workspace-b', 'ticket-a', 'assignment-cross-ticket', 'runtime-b', '00000000-0000-7000-8000-000000000002', 'tester', '2026-01-01' ); INSERT INTO ticket_worker_assignments ( workspace_id, ticket_id, assignment_id, runtime_id, worker_id, assigned_by, assigned_at ) VALUES ( 'workspace-b', 'ticket-b', 'assignment-event-source', 'runtime-b', '00000000-0000-7000-8000-000000000002', 'tester', '2026-01-01' ); INSERT INTO ticket_worker_assignment_events ( workspace_id, ticket_id, event_id, action, assignment_id, actor, created_at ) VALUES ( 'workspace-b', 'ticket-b2', 'event-cross-ticket', 'assigned', 'assignment-event-source', 'tester', '2026-01-01' ); "#, ) .unwrap(); let error = apply_migrations_through(&conn, 39).unwrap_err().to_string(); assert!(error.contains("typed_ticket_relations.target"), "{error}"); assert!( error.contains("workspace-b/ticket-b -> ticket-a"), "{error}" ); assert!( error.contains("objective_ticket_links.objective_id"), "{error}" ); assert!( error.contains("ticket_worker_assignments.ticket_id"), "{error}" ); assert!(error.contains("assignment-cross-ticket"), "{error}"); assert!( error.contains("ticket_worker_assignments.worker_id"), "{error}" ); assert!(error.contains("assignment-cross-worker"), "{error}"); assert!(error.contains("assignment-runtime-mismatch"), "{error}"); assert!(error.contains("assignment-missing-parents"), "{error}"); assert!( error.contains("ticket_worker_assignment_events.assignment_id"), "{error}" ); assert!(error.contains("event-cross-ticket"), "{error}"); assert_eq!(current_schema_version(&conn).unwrap(), 38); conn.execute("DELETE FROM typed_ticket_relations", []) .unwrap(); conn.execute("DELETE FROM objective_ticket_links", []) .unwrap(); conn.execute("DELETE FROM ticket_worker_assignment_events", []) .unwrap(); conn.execute("DELETE FROM ticket_worker_assignments", []) .unwrap(); apply_migrations_through(&conn, 39).unwrap(); assert_eq!(current_schema_version(&conn).unwrap(), 39); let bad_target = conn.execute( "UPDATE typed_tickets SET repository_id = 'repo-a', ref_selector = 'develop' \ WHERE workspace_id = 'workspace-b' AND ticket_id = 'ticket-b'", [], ); assert!(bad_target.is_err()); let cross_relation = conn.execute( "INSERT INTO typed_ticket_relations \ (workspace_id, ticket_id, kind, target, note, author, at) \ VALUES ('workspace-b', 'ticket-b', 'related', 'ticket-a', NULL, 'tester', '2026-01-01')", [], ); assert!(cross_relation.is_err()); let cross_objective_link = conn.execute( "INSERT INTO objective_ticket_links \ (workspace_id, objective_id, ticket_id, kind, created_at) \ VALUES ('workspace-b', 'objective-a', 'ticket-b', 'tracks', '2026-01-01')", [], ); assert!(cross_objective_link.is_err()); let cross_ticket_assignment = conn.execute( "INSERT INTO ticket_worker_assignments \ (workspace_id, ticket_id, assignment_id, runtime_id, worker_id, assigned_by, assigned_at) \ VALUES ('workspace-b', 'ticket-a', 'assignment-cross-ticket', 'runtime-b', \ '00000000-0000-7000-8000-000000000002', 'tester', '2026-01-01')", [], ); assert!(cross_ticket_assignment.is_err()); let cross_worker_assignment = conn.execute( "INSERT INTO ticket_worker_assignments \ (workspace_id, ticket_id, assignment_id, runtime_id, worker_id, assigned_by, assigned_at) \ VALUES ('workspace-b', 'ticket-b', 'assignment-cross-worker', 'runtime-a', \ '00000000-0000-7000-8000-000000000001', 'tester', '2026-01-01')", [], ); assert!(cross_worker_assignment.is_err()); let runtime_mismatch_assignment = conn.execute( "INSERT INTO ticket_worker_assignments \ (workspace_id, ticket_id, assignment_id, runtime_id, worker_id, assigned_by, assigned_at) \ VALUES ('workspace-b', 'ticket-b', 'assignment-runtime-mismatch', 'runtime-wrong', \ '00000000-0000-7000-8000-000000000002', 'tester', '2026-01-01')", [], ); assert!(runtime_mismatch_assignment.is_err()); conn.execute( "INSERT INTO ticket_worker_assignments \ (workspace_id, ticket_id, assignment_id, runtime_id, worker_id, assigned_by, assigned_at) \ VALUES ('workspace-b', 'ticket-b', 'assignment-b', 'runtime-b', \ '00000000-0000-7000-8000-000000000002', 'tester', '2026-01-01')", [], ) .unwrap(); let mismatched_current_assignment = conn.execute( "INSERT INTO ticket_current_worker_assignments \ (workspace_id, ticket_id, assignment_id, runtime_id, worker_id, updated_at) \ VALUES ('workspace-b', 'ticket-b', 'assignment-b', 'runtime-a', \ '00000000-0000-7000-8000-000000000001', '2026-01-01')", [], ); assert!(mismatched_current_assignment.is_err()); let cross_assignment_event = conn.execute( "INSERT INTO ticket_worker_assignment_events \ (workspace_id, ticket_id, event_id, action, assignment_id, actor, created_at) \ VALUES ('workspace-b', 'ticket-b2', 'event-cross-assignment', 'assigned', \ 'assignment-b', 'tester', '2026-01-01')", [], ); assert!(cross_assignment_event.is_err()); let cross_operation_ticket = conn.execute( "INSERT INTO ticket_assignment_operations \ (workspace_id, operation_id, action, ticket_id, created_at) \ VALUES ('workspace-b', 'operation-cross-ticket', 'assign', 'ticket-a', '2026-01-01')", [], ); assert!(cross_operation_ticket.is_err()); assert!( conn.execute( "DELETE FROM repositories WHERE workspace_id = 'workspace-a' AND repository_id = 'repo-a'", [], ) .is_err() ); conn.execute( "INSERT INTO typed_ticket_relations \ (workspace_id, ticket_id, kind, target, note, author, at) \ VALUES ('workspace-b', 'ticket-b', 'related', 'ticket-b2', NULL, 'tester', '2026-01-01')", [], ) .unwrap(); conn.execute( "DELETE FROM typed_tickets WHERE workspace_id = 'workspace-b' AND ticket_id = 'ticket-b2'", [], ) .unwrap(); assert_eq!( conn.query_row( "SELECT COUNT(*) FROM typed_ticket_relations WHERE workspace_id = 'workspace-b'", [], |row| row.get::<_, i64>(0), ) .unwrap(), 0 ); conn.execute( "DELETE FROM worker_registry WHERE workspace_id = 'workspace-b' AND worker_id = '00000000-0000-7000-8000-000000000002'", [], ) .unwrap(); conn.execute( "DELETE FROM typed_tickets WHERE workspace_id = 'workspace-b' AND ticket_id = 'ticket-b'", [], ) .unwrap(); validate_workspace_resource_references(&conn).unwrap(); assert_eq!( conn.query_row( "SELECT COUNT(*) FROM ticket_worker_assignments WHERE workspace_id = 'workspace-b'", [], |row| row.get::<_, i64>(0), ) .unwrap(), 1, "historical assignments survive Worker and Ticket retention deletion" ); conn.execute( "DELETE FROM workspaces WHERE workspace_id = 'workspace-b'", [], ) .unwrap(); assert_eq!( conn.query_row( "SELECT COUNT(*) FROM typed_tickets WHERE workspace_id = 'workspace-b'", [], |row| row.get::<_, i64>(0), ) .unwrap(), 0 ); assert_eq!( conn.query_row( "SELECT COUNT(*) FROM ticket_worker_assignments WHERE workspace_id = 'workspace-b'", [], |row| row.get::<_, i64>(0), ) .unwrap(), 0 ); let foreign_key_failures: i64 = conn .query_row("SELECT COUNT(*) FROM pragma_foreign_key_check", [], |row| { row.get(0) }) .unwrap(); assert_eq!(foreign_key_failures, 0); let integrity: String = conn .query_row("PRAGMA integrity_check", [], |row| row.get(0)) .unwrap(); assert_eq!(integrity, "ok"); } #[test] fn fresh_schema_matches_workspace_db_v0_boundaries() { let conn = Connection::open_in_memory().unwrap(); configure_sqlite(&conn).unwrap(); apply_migrations(&conn).unwrap(); let tables = table_names(&conn); for expected in [ "workspaces", "objectives", "objective_ticket_links", "objective_resources", "memory_staging_records", "workspace_memory_documents", "memory_staging_resolutions", "repositories", "artifacts", "audit_events", "worker_registry", "worker_create_reservations", "ticket_worker_assignments", "ticket_current_worker_assignments", "ticket_worker_assignment_events", "workdir_registry", "worker_workdir_links", "accounts", "users", "passkey_credentials", "auth_challenges", "browser_sessions", "api_tokens", "device_login_flows", ] { assert!( tables.contains(expected), "missing expected v0 table {expected}" ); } for forbidden in [ "runs", "hosts", "workers", "actors", "validation_results", "ci_results", "tickets", "ticket_events", "ticket_relations", "ticket_targets", "ticket_target_paths", "ticket_worker_links", "ticket_notification_outbox", "ticket_notification_deliveries", "ticket_notification_cursors", ] { assert!( !tables.contains(forbidden), "fresh v0 schema must not create forbidden table {forbidden}" ); } assert!( !tables.iter().any(|table| table.starts_with("legacy_")), "fresh v0 schema should not create legacy compatibility tables: {tables:?}" ); assert_columns( &conn, "workspaces", [ "workspace_id", "display_name", "state", "created_at", "updated_at", "owner_account_id", ], ); assert_columns( &conn, "repositories", [ "workspace_id", "repository_id", "name", "kind", "provider", "uri", "default_ref", "auth_ref_kind", "auth_ref_key", "created_at", "updated_at", ], ); assert_columns( &conn, "worker_registry", [ "workspace_id", "worker_id", "runtime_id", "display_name", "profile", "retention_state", "transcript_ref", "session_ref", "summary_ref", "diagnostics_ref", "created_at", "updated_at", ], ); assert_columns( &conn, "workdir_registry", [ "workspace_id", "workdir_id", "runtime_id", "repository_id", "creation_selector", "creation_ref", "materialization_status", "cleanliness", "created_at", "updated_at", "current_selector", "current_ref", ], ); assert_columns( &conn, "artifacts", [ "workspace_id", "artifact_id", "kind", "uri", "media_type", "sha256", "size_bytes", "summary", "created_at", "created_by_kind", "created_by_key", "created_by_display", "created_by_source_kind", "created_by_source_key", "ticket_id", "objective_id", "event_id", "worker_ref_kind", "worker_ref_key", "worker_display", "repository_id", "source_kind", "source_revision", ], ); for table in ["workspaces", "repositories", "artifacts"] { let columns = table_columns(&conn, table).unwrap(); for forbidden_column in [ "payload", "payload_json", "metadata", "metadata_json", "diagnostics_json", "run_id", "local_root", "record_authority", ] { assert!( !columns.iter().any(|column| column == forbidden_column), "{table} must not contain obsolete/generic column {forbidden_column}" ); } } } #[tokio::test] async fn upgrades_legacy_bootstrap_without_canonical_runs_table() { let conn = Connection::open_in_memory().unwrap(); configure_sqlite(&conn).unwrap(); conn.execute_batch(LEGACY_BOOTSTRAP_SQL).unwrap(); conn.execute( r#"INSERT INTO workspaces ( workspace_id, display_name, local_root, record_authority, created_at, updated_at ) VALUES (?1, ?2, ?3, ?4, ?5, ?6)"#, params![ "legacy-workspace", "Legacy Workspace", "/tmp/legacy-workspace", "local_yoi_project_records", "2026-01-01T00:00:00Z", "2026-01-02T00:00:00Z", ], ) .unwrap(); conn.execute( "INSERT INTO __yoi_schema_migrations (version, name) VALUES (1, 'bootstrap workspace control plane')", [], ) .unwrap(); let store = SqliteWorkspaceStore::from_connection(conn).unwrap(); assert_eq!(store.schema_version().await.unwrap(), 40); store .with_conn(|conn| { let tables = table_names(conn); for expected in [ "workspaces", "repositories", "artifacts", "audit_events", "workspace_memory_documents", "memory_staging_records", "memory_staging_resolutions", "legacy_workspaces", "legacy_repositories", "legacy_runs", "legacy_artifacts", "legacy_ticket_projections", "legacy_objective_projections", ] { assert!( tables.contains(expected), "missing {expected} after upgrade" ); } for forbidden in [ "runs", "hosts", "workers", "actors", "validation_results", "tickets", "ticket_events", "ticket_relations", "ticket_targets", "ticket_target_paths", "ticket_worker_links", "ticket_notification_outbox", "ticket_notification_deliveries", "ticket_notification_cursors", ] { assert!( !tables.contains(forbidden), "upgraded schema must not retain forbidden canonical table {forbidden}" ); } assert_columns( conn, "workspaces", [ "workspace_id", "display_name", "state", "created_at", "updated_at", "owner_account_id", ], ); let legacy_workspace_columns = table_columns(conn, "legacy_workspaces")?; assert!( legacy_workspace_columns .iter() .any(|column| column == "local_root") ); assert!( legacy_workspace_columns .iter() .any(|column| column == "record_authority") ); let artifact_columns = table_columns(conn, "artifacts")?; assert!(artifact_columns.iter().any(|column| column == "uri")); assert!(!artifact_columns.iter().any(|column| column == "run_id")); Ok(()) }) .unwrap(); assert_eq!( store.get_workspace("legacy-workspace").await.unwrap(), Some(WorkspaceRecord { workspace_id: "legacy-workspace".to_string(), owner_account_id: None, display_name: "Legacy Workspace".to_string(), state: "active".to_string(), created_at: "2026-01-01T00:00:00Z".to_string(), updated_at: "2026-01-02T00:00:00Z".to_string(), }) ); let new_record = WorkspaceRecord { workspace_id: "new-workspace".to_string(), owner_account_id: None, display_name: "New Workspace".to_string(), state: "active".to_string(), created_at: "2026-02-01T00:00:00Z".to_string(), updated_at: "2026-02-01T00:00:00Z".to_string(), }; store.upsert_workspace(&new_record).await.unwrap(); assert_eq!( store.get_workspace("new-workspace").await.unwrap(), Some(new_record) ); } #[test] fn workdir_revision_migration_preserves_creation_evidence_and_leaves_observation_unknown() { let conn = Connection::open_in_memory().unwrap(); conn.execute_batch( r#" CREATE TABLE workdir_registry ( workspace_id TEXT NOT NULL, workdir_id TEXT NOT NULL, runtime_id TEXT NOT NULL, repository_id TEXT NOT NULL, selector TEXT, resolved_commit TEXT, materialization_status TEXT NOT NULL, cleanliness TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, PRIMARY KEY (workspace_id, workdir_id) ); INSERT INTO workdir_registry ( workspace_id, workdir_id, runtime_id, repository_id, selector, resolved_commit, materialization_status, cleanliness, created_at, updated_at ) VALUES ( 'workspace', 'workdir', 'runtime', 'repository', 'develop', 'abcdef', 'present', 'clean', '1', '2' ); "#, ) .unwrap(); add_workdir_revision_observations(&conn).unwrap(); let values = conn .query_row( "SELECT creation_selector, creation_ref, current_selector, current_ref FROM workdir_registry", [], |row| { Ok(( row.get::<_, Option>(0)?, row.get::<_, Option>(1)?, row.get::<_, Option>(2)?, row.get::<_, Option>(3)?, )) }, ) .unwrap(); assert_eq!( values, ( Some("develop".to_string()), Some("abcdef".to_string()), None, None, ) ); } #[test] fn combined_v20_migration_reconciles_ticket_branch_schema() { let conn = Connection::open_in_memory().unwrap(); conn.execute_batch( r#" CREATE TABLE workdir_registry ( selector TEXT, resolved_commit TEXT ); CREATE TABLE ticket_assignment_operations ( request_fingerprint TEXT ); "#, ) .unwrap(); strengthen_ticket_assignment_lifecycle_reservations(&conn).unwrap(); assert!(column_exists(&conn, "workdir_registry", "creation_selector").unwrap()); assert!(column_exists(&conn, "workdir_registry", "creation_ref").unwrap()); assert!(column_exists(&conn, "workdir_registry", "current_selector").unwrap()); assert!(column_exists(&conn, "workdir_registry", "current_ref").unwrap()); assert!( column_exists(&conn, "ticket_assignment_operations", "request_fingerprint").unwrap() ); } #[tokio::test] async fn repository_records_round_trip() { let store = SqliteWorkspaceStore::in_memory().unwrap(); assert_eq!(store.schema_version().await.unwrap(), 40); let workspace = WorkspaceRecord { workspace_id: "local-dev".to_string(), owner_account_id: None, display_name: "Local Dev".to_string(), state: "active".to_string(), created_at: "1".to_string(), updated_at: "1".to_string(), }; store.upsert_workspace(&workspace).await.unwrap(); let repository = RepositoryRecord { workspace_id: "local-dev".to_string(), repository_id: "main".to_string(), name: "Yoi".to_string(), kind: "git".to_string(), provider: Some("git".to_string()), uri: ".".to_string(), default_ref: Some("HEAD".to_string()), auth_ref_kind: None, auth_ref_key: None, created_at: "2".to_string(), updated_at: "2".to_string(), }; store.upsert_repository(&repository).unwrap(); assert_eq!( store.get_repository("local-dev", "main").unwrap(), Some(repository.clone()) ); assert_eq!( store.list_repositories("local-dev").unwrap(), vec![repository.clone()] ); assert_eq!( store.list_repositories("other-workspace").unwrap(), Vec::new() ); let other_workspace = WorkspaceRecord { workspace_id: "other-workspace".to_string(), owner_account_id: None, display_name: "Other Workspace".to_string(), state: "active".to_string(), created_at: "3".to_string(), updated_at: "3".to_string(), }; store.upsert_workspace(&other_workspace).await.unwrap(); let mut other_repository = repository.clone(); other_repository.workspace_id = other_workspace.workspace_id.clone(); other_repository.name = "Other Yoi".to_string(); other_repository.uri = "/other/yoi".to_string(); store.upsert_repository(&other_repository).unwrap(); assert_eq!( store.get_repository("local-dev", "main").unwrap(), Some(repository) ); assert_eq!( store.get_repository("other-workspace", "main").unwrap(), Some(other_repository) ); } #[tokio::test] async fn memory_authority_records_round_trip_and_close_staging() { let store = SqliteWorkspaceStore::in_memory().unwrap(); assert_eq!(store.schema_version().await.unwrap(), 40); let workspace = WorkspaceRecord { workspace_id: "local-dev".to_string(), owner_account_id: None, display_name: "Local Dev".to_string(), state: "active".to_string(), created_at: "1".to_string(), updated_at: "1".to_string(), }; store.upsert_workspace(&workspace).await.unwrap(); let document = store .ensure_memory_document("local-dev", "# Memory\n", "2") .unwrap(); assert_eq!(document.body_md, "# Memory\n"); let updated = MemoryDocumentRecord { workspace_id: "local-dev".to_string(), body_md: "# Memory\n\n- fact\n".to_string(), created_at: document.created_at.clone(), updated_at: "3".to_string(), }; store.upsert_memory_document(&updated).unwrap(); assert_eq!( store.get_memory_document("local-dev").unwrap(), Some(updated) ); let staging = MemoryStagingRecord { workspace_id: "local-dev".to_string(), candidate_id: "candidate-a".to_string(), raw_json: r#"{"claim":"candidate"}"#.to_string(), source_path: Some("memory/_staging/candidate-a.json".to_string()), imported_at: "4".to_string(), }; store.upsert_memory_staging_record(&staging).unwrap(); assert_eq!( store .get_memory_staging_record("local-dev", "candidate-a") .unwrap(), Some(staging.clone()) ); assert_eq!( store.list_memory_staging_records("local-dev", 10).unwrap(), vec![staging.clone()] ); let resolution = MemoryStagingResolutionRecord { workspace_id: "local-dev".to_string(), candidate_id: staging.candidate_id.clone(), action: "apply".to_string(), reason: "accepted".to_string(), affected_refs_json: r#"["summary"]"#.to_string(), staging_raw_json: staging.raw_json.clone(), source_path: staging.source_path.clone(), imported_at: staging.imported_at.clone(), resolved_at: "5".to_string(), }; store.insert_memory_staging_resolution(&resolution).unwrap(); assert!( store .delete_memory_staging_record("local-dev", "candidate-a") .unwrap() ); assert_eq!(store.count_memory_staging_records("local-dev").unwrap(), 0); assert_eq!( store .list_memory_staging_resolutions("local-dev", 10) .unwrap(), vec![resolution] ); } #[tokio::test] async fn worker_workdir_registry_round_trips_and_preserves_pinned_retention() { let temp = tempfile::tempdir().unwrap(); let db = temp.path().join("workspace.db"); let store = SqliteWorkspaceStore::open(&db).unwrap(); let workspace = WorkspaceRecord { workspace_id: "local-dev".to_string(), owner_account_id: None, display_name: "Local Dev".to_string(), state: "active".to_string(), created_at: "1".to_string(), updated_at: "1".to_string(), }; store.upsert_workspace(&workspace).await.unwrap(); store .upsert_repository(&RepositoryRecord { workspace_id: workspace.workspace_id.clone(), repository_id: "repo".to_string(), name: "Repository".to_string(), kind: "git".to_string(), provider: Some("git".to_string()), uri: ".".to_string(), default_ref: Some("HEAD".to_string()), auth_ref_kind: None, auth_ref_key: None, created_at: "1".to_string(), updated_at: "1".to_string(), }) .unwrap(); let worker = WorkerRegistryRecord { workspace_id: "local-dev".to_string(), worker: RuntimeWorkerRef::new("embedded", "1"), display_name: "Browser 1".to_string(), profile: Some("builtin:companion".to_string()), retention_state: "pinned".to_string(), transcript_ref: Some("runtime://embedded/workers/00000001/transcript".to_string()), session_ref: None, summary_ref: None, diagnostics_ref: None, created_at: "2".to_string(), updated_at: "2".to_string(), }; store.upsert_worker_registry(&worker).unwrap(); let mut runtime_sync_worker = worker.clone(); runtime_sync_worker.retention_state = "normal".to_string(); runtime_sync_worker.updated_at = "5".to_string(); store.upsert_worker_registry(&runtime_sync_worker).unwrap(); let mut expected_worker = worker.clone(); expected_worker.updated_at = "5".to_string(); let workdir = WorkdirRegistryRecord { workspace_id: "local-dev".to_string(), workdir_id: "0000019a00000000001".to_string(), runtime_id: "embedded".to_string(), repository_id: "repo".to_string(), creation_selector: Some("develop".to_string()), creation_ref: Some("abcdef".to_string()), current_selector: None, current_ref: Some("abcdef".to_string()), materialization_status: "not_found".to_string(), cleanliness: "clean".to_string(), created_at: "2".to_string(), updated_at: "3".to_string(), }; store.upsert_workdir_registry(&workdir).unwrap(); let unmanaged_workdir = WorkdirRegistryRecord { workspace_id: "local-dev".to_string(), workdir_id: "runtime-direct".to_string(), runtime_id: "embedded".to_string(), repository_id: "repo".to_string(), creation_selector: Some("feature".to_string()), creation_ref: Some("123456".to_string()), current_selector: Some("feature".to_string()), current_ref: Some("123456".to_string()), materialization_status: "present".to_string(), cleanliness: "unknown".to_string(), created_at: "3".to_string(), updated_at: "4".to_string(), }; store.upsert_workdir_registry(&unmanaged_workdir).unwrap(); let link = WorkerWorkdirLinkRecord { workspace_id: "local-dev".to_string(), worker: worker.worker.clone(), workdir_id: workdir.workdir_id.clone(), role: "attachment".to_string(), linked_at: "4".to_string(), unlinked_at: None, }; store .reserve_worker_workdir_attachment("local-dev", &workdir.workdir_id, "spawn-1", "4") .unwrap(); assert!(matches!( store.reserve_worker_workdir_attachment( "local-dev", &workdir.workdir_id, "spawn-2", "4" ), Err(Error::WorkdirAttachmentConflict(_)) )); assert!(matches!( store.delete_workdir_registry("local-dev", &workdir.workdir_id), Err(Error::WorkdirAttachmentConflict(_)) )); assert!(matches!( store.attach_worker_workdir(&link), Err(Error::WorkdirAttachmentConflict(_)) )); assert_eq!( store .finalize_reserved_worker_workdir_attachment(&link, "spawn-1") .unwrap(), link ); assert_eq!(store.attach_worker_workdir(&link).unwrap(), link); assert_eq!( store .get_worker_registry("local-dev", &worker.worker) .unwrap(), Some(expected_worker.clone()) ); assert_eq!( store .get_workdir_registry("local-dev", "0000019a00000000001") .unwrap(), Some(workdir.clone()) ); assert_eq!( store.list_workdir_registry("local-dev", 10).unwrap(), vec![unmanaged_workdir.clone(), workdir.clone()] ); assert_eq!( store .list_worker_workdir_links("local-dev", &worker.worker) .unwrap(), vec![link.clone()] ); let worker_conflict = WorkerWorkdirLinkRecord { workdir_id: unmanaged_workdir.workdir_id.clone(), linked_at: "5".to_string(), ..link.clone() }; assert!(matches!( store.attach_worker_workdir(&worker_conflict), Err(Error::WorkdirAttachmentConflict(_)) )); let second_worker = WorkerRegistryRecord { worker: RuntimeWorkerRef::new("embedded", "2"), display_name: "Browser 2".to_string(), created_at: "5".to_string(), updated_at: "5".to_string(), ..worker.clone() }; store.upsert_worker_registry(&second_worker).unwrap(); let workdir_conflict = WorkerWorkdirLinkRecord { worker: second_worker.worker.clone(), linked_at: "5".to_string(), ..link.clone() }; assert!(matches!( store.attach_worker_workdir(&workdir_conflict), Err(Error::WorkdirAttachmentConflict(_)) )); assert!(matches!( store.detach_worker_workdir("local-dev", &worker.worker, Some("wrong-workdir"), "6",), Err(Error::WorkdirAttachmentConflict(_)) )); let detached = store .detach_worker_workdir("local-dev", &worker.worker, Some(&workdir.workdir_id), "6") .unwrap() .unwrap(); assert_eq!(detached.unlinked_at.as_deref(), Some("6")); assert!( store .worker_workdir_link_history_exists("local-dev", &worker.worker) .unwrap() ); assert_eq!( store.attach_worker_workdir(&workdir_conflict).unwrap(), workdir_conflict ); } #[tokio::test] async fn worker_control_grants_are_idempotent_scoped_and_support_internal_invalidation() { let dir = tempfile::tempdir().unwrap(); let database = dir.path().join("control-grants.db"); let store = SqliteWorkspaceStore::open(&database).unwrap(); store .upsert_workspace(&WorkspaceRecord { workspace_id: "workspace-control".to_string(), owner_account_id: None, display_name: "Control grants".to_string(), state: "active".to_string(), created_at: "2026-07-27T00:00:00Z".to_string(), updated_at: "2026-07-27T00:00:00Z".to_string(), }) .await .unwrap(); let worker_record = |worker_id: &str, display_name: &str| WorkerRegistryRecord { workspace_id: "workspace-control".to_string(), worker: RuntimeWorkerRef::new("runtime-a", worker_id), display_name: display_name.to_string(), profile: None, retention_state: "normal".to_string(), transcript_ref: None, session_ref: None, summary_ref: None, diagnostics_ref: None, created_at: "2026-07-27T00:00:00Z".to_string(), updated_at: "2026-07-27T00:00:00Z".to_string(), }; let controller_record = worker_record("1", "Controller"); let subject_record = worker_record("2", "Subject"); store.upsert_worker_registry(&controller_record).unwrap(); store.upsert_worker_registry(&subject_record).unwrap(); let grant = WorkerControlGrantRecord { workspace_id: "workspace-control".to_string(), grant_id: "grant-1".to_string(), controller: controller_record.worker.clone(), subject: subject_record.worker.clone(), relation: "spawned".to_string(), origin: "worker_spawn".to_string(), permissions: vec![ "observe".to_string(), "send_input".to_string(), "stop".to_string(), ], operation_id: "spawn-op-1".to_string(), created_at: "2026-07-27T00:00:01Z".to_string(), revoked_at: None, }; assert_eq!(store.create_worker_control_grant(&grant).unwrap(), grant); assert_eq!(store.create_worker_control_grant(&grant).unwrap(), grant); assert_eq!( store .list_active_worker_control_grants( "workspace-control", &controller_record.worker, 10, ) .unwrap(), vec![grant.clone()] ); assert_eq!( store .get_active_worker_control_grant( "workspace-control", &controller_record.worker, &subject_record.worker, ) .unwrap(), Some(grant.clone()) ); drop(store); let store = SqliteWorkspaceStore::open(&database).unwrap(); assert_eq!( store .list_active_worker_control_grants( "workspace-control", &controller_record.worker, 10, ) .unwrap(), vec![grant.clone()], "known Runtime Worker grants survive Backend restart" ); let conflicting_replay = WorkerControlGrantRecord { subject: controller_record.worker.clone(), ..grant.clone() }; assert!(matches!( store.create_worker_control_grant(&conflicting_replay), Err(Error::InvalidInput(_)) )); assert!( store .revoke_worker_control_grant( "workspace-control", &grant.grant_id, "2026-07-27T00:00:02Z", ) .unwrap() ); assert!( store .list_active_worker_control_grants( "workspace-control", &controller_record.worker, 10, ) .unwrap() .is_empty() ); assert!( store .delete_worker_registry("workspace-control", &subject_record.worker) .unwrap() ); assert!( store .get_worker_control_grant("workspace-control", &grant.grant_id) .unwrap() .is_none(), "deleting a subject Worker cascades its durable control grants" ); } #[tokio::test] async fn account_and_login_records_round_trip() { let store = SqliteWorkspaceStore::in_memory().unwrap(); assert_eq!(store.schema_version().await.unwrap(), 40); let now = "2026-07-22T00:00:00Z".to_string(); let account = AccountRecord { account_id: "acct-user-alice".to_string(), kind: "user".to_string(), handle: "alice".to_string(), display_name: "Alice".to_string(), created_at: now.clone(), updated_at: now.clone(), }; store.upsert_account(&account).unwrap(); let user = UserRecord { user_id: "user-alice".to_string(), account_id: account.account_id.clone(), handle: account.handle.clone(), display_name: account.display_name.clone(), created_at: now.clone(), updated_at: now.clone(), }; store.upsert_user(&user).unwrap(); assert_eq!( store.get_account_by_handle("user", "alice").unwrap(), Some(account.clone()) ); assert_eq!( store.get_user_by_handle("alice").unwrap(), Some(user.clone()) ); let workspace = WorkspaceRecord { workspace_id: "workspace".to_string(), owner_account_id: Some(account.account_id.clone()), display_name: "Workspace".to_string(), state: "active".to_string(), created_at: now.clone(), updated_at: now.clone(), }; store.upsert_workspace(&workspace).await.unwrap(); assert_eq!( store.get_workspace("workspace").await.unwrap(), Some(workspace) ); let passkey = PasskeyCredentialRecord { credential_id: "cred-1".to_string(), user_id: user.user_id.clone(), public_key_cose: "public-key".to_string(), transports_json: Some("[\\\"internal\\\"]".to_string()), sign_count: 0, created_at: now.clone(), last_used_at: None, }; store.upsert_passkey_credential(&passkey).unwrap(); assert_eq!( store.get_passkey_credential("cred-1").unwrap(), Some(passkey.clone()) ); assert_eq!( store .list_passkey_credentials_for_user(&user.user_id) .unwrap(), vec![passkey] ); let challenge = AuthChallengeRecord { challenge_id: "challenge-id".to_string(), ceremony: "passkey_login".to_string(), challenge: "challenge".to_string(), user_id: Some(user.user_id.clone()), rp_id: "127.0.0.1".to_string(), origin: "http://127.0.0.1:8787".to_string(), state_json: None, expires_at: "2026-07-22T00:05:00Z".to_string(), created_at: now.clone(), consumed_at: None, }; store.put_auth_challenge(&challenge).unwrap(); assert_eq!( store .consume_auth_challenge("challenge", "passkey_login", "2026-07-22T00:01:00Z") .unwrap(), Some(challenge) ); assert_eq!( store .consume_auth_challenge("challenge", "passkey_login", "2026-07-22T00:01:00Z") .unwrap(), None ); let browser_session = BrowserSessionRecord { session_id: "session-id".to_string(), token_hash: "session-hash".to_string(), user_id: user.user_id.clone(), created_at: now.clone(), expires_at: "2026-07-22T12:00:00Z".to_string(), revoked_at: None, }; store.create_browser_session(&browser_session).unwrap(); assert_eq!( store.resolve_browser_session("session-hash").unwrap(), Some(browser_session) ); assert!( store .revoke_browser_session("session-hash", "2026-07-22T00:02:00Z") .unwrap() ); assert_eq!(store.resolve_browser_session("session-hash").unwrap(), None); assert!( !store .revoke_browser_session("session-hash", "2026-07-22T00:03:00Z") .unwrap() ); let api_token = ApiTokenRecord { token_id: "token-id".to_string(), token_hash: "hash".to_string(), user_id: user.user_id.clone(), label: "cli".to_string(), created_at: now.clone(), expires_at: None, revoked_at: None, last_used_at: None, }; store.create_api_token(&api_token).unwrap(); assert_eq!( store.resolve_api_token("hash").unwrap(), Some(api_token.clone()) ); store .mark_api_token_used("hash", "2026-07-22T00:02:00Z") .unwrap(); assert_eq!( store .resolve_api_token("hash") .unwrap() .unwrap() .last_used_at, Some("2026-07-22T00:02:00Z".to_string()) ); let flow = DeviceLoginFlowRecord { device_code: "device".to_string(), user_code: "USER-CODE".to_string(), verification_uri: "http://127.0.0.1:8787/login/device".to_string(), client_name: Some("yoi".to_string()), user_id: None, api_token_id: None, issued_access_token: None, created_at: now.clone(), expires_at: "2026-07-22T00:10:00Z".to_string(), approved_at: None, consumed_at: None, }; store.create_device_login_flow(&flow).unwrap(); store .approve_device_login_flow( "device", &user.user_id, "token-id", "access", "2026-07-22T00:03:00Z", ) .unwrap(); let approved = store .consume_device_login_token("device", "2026-07-22T00:04:00Z") .unwrap() .unwrap(); assert_eq!(approved.issued_access_token, Some("access".to_string())); assert_eq!( store .consume_device_login_token("device", "2026-07-22T00:05:00Z") .unwrap(), None ); } #[tokio::test] async fn worker_mutation_source_jti_replay_guard_survives_reopen() { let temp = tempfile::tempdir().unwrap(); let path = temp.path().join("server.db"); let store = SqliteWorkspaceStore::open(&path).unwrap(); store .upsert_trusted_runtime(&TrustedRuntimeRecord { runtime_id: "runtime-a".to_string(), workspace_id: None, display_name: "Runtime A".to_string(), base_url: "https://runtime.invalid".to_string(), public_key: "public-key".to_string(), created_at: "2026-08-11T00:00:00Z".to_string(), updated_at: "2026-08-11T00:00:00Z".to_string(), revoked_at: None, }) .unwrap(); assert!( store .consume_worker_mutation_source_jti( "runtime-a", "proof-1", 2_000, 1_000, "2026-08-11T00:00:00Z", ) .await .unwrap() ); drop(store); let reopened = SqliteWorkspaceStore::open(&path).unwrap(); assert!( !reopened .consume_worker_mutation_source_jti( "runtime-a", "proof-1", 2_000, 1_001, "2026-08-11T00:00:01Z", ) .await .unwrap() ); } fn table_names(conn: &Connection) -> BTreeSet { let mut stmt = conn .prepare( "SELECT name FROM sqlite_schema WHERE type = 'table' AND name NOT LIKE 'sqlite_%'", ) .unwrap(); let rows = stmt.query_map([], |row| row.get::<_, String>(0)).unwrap(); rows.collect::>>().unwrap() } fn assert_columns(conn: &Connection, table: &str, expected: [&str; N]) { let columns = table_columns(conn, table).unwrap(); let expected = expected.map(str::to_string).to_vec(); assert_eq!(columns, expected, "unexpected columns for {table}"); } const LEGACY_BOOTSTRAP_SQL: &str = r#" CREATE TABLE workspaces ( workspace_id TEXT PRIMARY KEY, display_name TEXT NOT NULL, local_root TEXT NOT NULL, record_authority TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE TABLE repositories ( repository_id TEXT PRIMARY KEY, workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE, local_root TEXT NOT NULL, role TEXT NOT NULL, created_at TEXT NOT NULL ); CREATE TABLE ticket_projections ( workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE, ticket_id TEXT NOT NULL, title TEXT NOT NULL, state TEXT NOT NULL, updated_at TEXT NOT NULL, PRIMARY KEY (workspace_id, ticket_id) ); CREATE TABLE objective_projections ( workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE, objective_id TEXT NOT NULL, title TEXT NOT NULL, state TEXT NOT NULL, updated_at TEXT NOT NULL, PRIMARY KEY (workspace_id, objective_id) ); CREATE TABLE runs ( run_id TEXT PRIMARY KEY, workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE, subject_kind TEXT NOT NULL, subject_id TEXT NOT NULL, status TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE TABLE artifacts ( artifact_id TEXT PRIMARY KEY, workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE, run_id TEXT REFERENCES runs(run_id) ON DELETE SET NULL, path TEXT NOT NULL, content_type TEXT, created_at TEXT NOT NULL ); "#; }