fix: review fixes for libSQL backend (shared connections, panics, indexes)

- Replace .expect() with proper error propagation in 3 call sites
- Share Arc<Database> between backend and stores instead of single Connection
- Add connect-per-operation pattern to LibSqlSecretsStore and LibSqlWasmToolStore
- Wrap store() INSERT + SELECT-back in a transaction
- Add ~22 missing indexes for parity with PostgreSQL schema
- Add 18 leak_detection_patterns seed rows matching PostgreSQL V2 migration
- Fix super:: import to use crate:: style
- Gate mask_password_in_url behind #[cfg(feature = "postgres")]
- Rewrite secrets store init with or_else chain for runtime backend selection

Co-Authored-By: Claude Opus 4.6 <[email protected]>
This commit is contained in:
Illia Polosukhin
2026-02-13 17:03:58 -08:00
co-authored by Claude Opus 4.6
parent 5814d77b16
commit 09e6a7e6d8
8 changed files with 233 additions and 138 deletions
+4 -8
View File
@@ -530,11 +530,9 @@ async fn get_secrets_store() -> anyhow::Result<Arc<dyn SecretsStore + Send + Syn
.unwrap_or(&default_path);
let backend = if let Some(ref url) = config.database.libsql_url {
let token = config
.database
.libsql_auth_token
.as_ref()
.expect("LIBSQL_AUTH_TOKEN required when LIBSQL_URL is set");
let token = config.database.libsql_auth_token.as_ref().ok_or_else(|| {
anyhow::anyhow!("LIBSQL_AUTH_TOKEN is required when LIBSQL_URL is set")
})?;
LibSqlBackend::new_remote_replica(db_path, url, token.expose_secret())
.await
.map_err(|e| anyhow::anyhow!("{}", e))?
@@ -548,10 +546,8 @@ async fn get_secrets_store() -> anyhow::Result<Arc<dyn SecretsStore + Send + Syn
.await
.map_err(|e| anyhow::anyhow!("{}", e))?;
let conn = backend.connect().map_err(|e| anyhow::anyhow!("{}", e))?;
return Ok(Arc::new(crate::secrets::LibSqlSecretsStore::new(
conn,
backend.shared_db(),
Arc::new(crypto),
)));
}
+4 -8
View File
@@ -748,11 +748,9 @@ async fn auth_tool(name: String, dir: Option<PathBuf>, user_id: String) -> anyho
.unwrap_or(&default_path);
let backend = if let Some(ref url) = config.database.libsql_url {
let token = config
.database
.libsql_auth_token
.as_ref()
.expect("LIBSQL_AUTH_TOKEN required when LIBSQL_URL is set");
let token = config.database.libsql_auth_token.as_ref().ok_or_else(|| {
anyhow::anyhow!("LIBSQL_AUTH_TOKEN is required when LIBSQL_URL is set")
})?;
LibSqlBackend::new_remote_replica(db_path, url, token.expose_secret())
.await
.map_err(|e| anyhow::anyhow!("{}", e))?
@@ -766,10 +764,8 @@ async fn auth_tool(name: String, dir: Option<PathBuf>, user_id: String) -> anyho
.await
.map_err(|e| anyhow::anyhow!("{}", e))?;
let conn = backend.connect().map_err(|e| anyhow::anyhow!("{}", e))?;
Arc::new(crate::secrets::LibSqlSecretsStore::new(
conn,
backend.shared_db(),
Arc::new(crypto),
))
}
+18 -13
View File
@@ -8,6 +8,7 @@
use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;
use async_trait::async_trait;
use chrono::{DateTime, NaiveDateTime, Utc};
@@ -31,7 +32,7 @@ use crate::workspace::{
reciprocal_rank_fusion,
};
use super::libsql_migrations;
use crate::db::libsql_migrations;
/// Explicit column list for routines table (matches positional access in `row_to_routine_libsql`).
const ROUTINE_COLUMNS: &str = "\
@@ -48,8 +49,12 @@ const ROUTINE_RUN_COLUMNS: &str = "\
status, completed_at, result_summary, tokens_used, job_id, created_at";
/// libSQL/Turso database backend.
///
/// Stores the `Database` handle in an `Arc` so that the same underlying
/// database can be shared with stores (SecretsStore, WasmToolStore) that
/// create their own connections per-operation.
pub struct LibSqlBackend {
db: LibSqlDatabase,
db: Arc<LibSqlDatabase>,
}
impl LibSqlBackend {
@@ -67,7 +72,7 @@ impl LibSqlBackend {
.await
.map_err(|e| DatabaseError::Pool(format!("Failed to open libSQL database: {}", e)))?;
Ok(Self { db })
Ok(Self { db: Arc::new(db) })
}
/// Create a new in-memory database (for testing).
@@ -79,7 +84,7 @@ impl LibSqlBackend {
DatabaseError::Pool(format!("Failed to create in-memory database: {}", e))
})?;
Ok(Self { db })
Ok(Self { db: Arc::new(db) })
}
/// Create with Turso cloud sync (embedded replica).
@@ -99,18 +104,18 @@ impl LibSqlBackend {
.await
.map_err(|e| DatabaseError::Pool(format!("Failed to open remote replica: {}", e)))?;
Ok(Self { db })
Ok(Self { db: Arc::new(db) })
}
/// Get the underlying database handle (for sync operations).
pub fn database(&self) -> &LibSqlDatabase {
&self.db
}
/// Create a new connection to the same database.
/// Get a shared reference to the underlying database handle.
///
/// Used for creating separate connections for SecretsStore and WasmToolStore
/// which have their own connection needs.
/// Use this to pass the database to stores (SecretsStore, WasmToolStore)
/// that need to create their own connections per-operation.
pub fn shared_db(&self) -> Arc<LibSqlDatabase> {
Arc::clone(&self.db)
}
/// Create a new connection to the database.
pub fn connect(&self) -> Result<Connection, DatabaseError> {
self.db
.connect()
+69
View File
@@ -477,4 +477,73 @@ CREATE TABLE IF NOT EXISTS settings (
CREATE INDEX IF NOT EXISTS idx_settings_user ON settings(user_id);
-- ==================== Missing indexes (parity with PostgreSQL) ====================
-- agent_jobs
CREATE INDEX IF NOT EXISTS idx_agent_jobs_stuck ON agent_jobs(stuck_since);
-- secrets
CREATE INDEX IF NOT EXISTS idx_secrets_provider ON secrets(provider);
CREATE INDEX IF NOT EXISTS idx_secrets_expires ON secrets(expires_at);
-- wasm_tools
CREATE INDEX IF NOT EXISTS idx_wasm_tools_trust ON wasm_tools(trust_level);
-- tool_capabilities
CREATE INDEX IF NOT EXISTS idx_tool_capabilities_tool ON tool_capabilities(wasm_tool_id);
-- leak_detection_patterns
CREATE INDEX IF NOT EXISTS idx_leak_patterns_enabled ON leak_detection_patterns(enabled);
-- tool_rate_limit_state
CREATE INDEX IF NOT EXISTS idx_rate_limit_tool ON tool_rate_limit_state(wasm_tool_id);
-- secret_usage_log
CREATE INDEX IF NOT EXISTS idx_secret_usage_secret ON secret_usage_log(secret_id);
CREATE INDEX IF NOT EXISTS idx_secret_usage_tool ON secret_usage_log(wasm_tool_id);
CREATE INDEX IF NOT EXISTS idx_secret_usage_created ON secret_usage_log(created_at DESC);
-- leak_detection_events
CREATE INDEX IF NOT EXISTS idx_leak_events_pattern ON leak_detection_events(pattern_id);
CREATE INDEX IF NOT EXISTS idx_leak_events_tool ON leak_detection_events(wasm_tool_id);
CREATE INDEX IF NOT EXISTS idx_leak_events_user ON leak_detection_events(user_id);
CREATE INDEX IF NOT EXISTS idx_leak_events_created ON leak_detection_events(created_at DESC);
-- tool_failures
CREATE INDEX IF NOT EXISTS idx_tool_failures_count ON tool_failures(error_count DESC);
CREATE INDEX IF NOT EXISTS idx_tool_failures_unrepaired ON tool_failures(tool_name);
-- routines
CREATE INDEX IF NOT EXISTS idx_routines_next_fire ON routines(next_fire_at);
CREATE INDEX IF NOT EXISTS idx_routines_event_triggers ON routines(user_id);
-- routine_runs
CREATE INDEX IF NOT EXISTS idx_routine_runs_status ON routine_runs(status);
-- heartbeat_state
CREATE INDEX IF NOT EXISTS idx_heartbeat_next_run ON heartbeat_state(next_run);
-- ==================== Seed data ====================
-- Pre-populate leak detection patterns (matches PostgreSQL V2 migration).
INSERT OR IGNORE INTO leak_detection_patterns (id, name, pattern, severity, action, enabled, created_at) VALUES
('550e8400-e29b-41d4-a716-446655440001', 'openai_api_key', 'sk-(?:proj-)?[a-zA-Z0-9]{20,}(?:T3BlbkFJ[a-zA-Z0-9_-]*)?', 'critical', 'block', 1, datetime('now')),
('550e8400-e29b-41d4-a716-446655440002', 'anthropic_api_key', 'sk-ant-api[a-zA-Z0-9_-]{90,}', 'critical', 'block', 1, datetime('now')),
('550e8400-e29b-41d4-a716-446655440003', 'aws_access_key', 'AKIA[0-9A-Z]{16}', 'critical', 'block', 1, datetime('now')),
('550e8400-e29b-41d4-a716-446655440004', 'aws_secret_key', '(?<![A-Za-z0-9/+=])[A-Za-z0-9/+=]{40}(?![A-Za-z0-9/+=])', 'high', 'block', 1, datetime('now')),
('550e8400-e29b-41d4-a716-446655440005', 'github_token', 'gh[pousr]_[A-Za-z0-9_]{36,}', 'critical', 'block', 1, datetime('now')),
('550e8400-e29b-41d4-a716-446655440006', 'github_fine_grained_pat', 'github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59}', 'critical', 'block', 1, datetime('now')),
('550e8400-e29b-41d4-a716-446655440007', 'stripe_api_key', 'sk_(?:live|test)_[a-zA-Z0-9]{24,}', 'critical', 'block', 1, datetime('now')),
('550e8400-e29b-41d4-a716-446655440008', 'nearai_session', 'sess_[a-zA-Z0-9]{32,}', 'critical', 'block', 1, datetime('now')),
('550e8400-e29b-41d4-a716-446655440009', 'bearer_token', 'Bearer\s+[a-zA-Z0-9_-]{20,}', 'high', 'redact', 1, datetime('now')),
('550e8400-e29b-41d4-a716-44665544000a', 'pem_private_key', '-----BEGIN\s+(?:RSA\s+)?PRIVATE\s+KEY-----', 'critical', 'block', 1, datetime('now')),
('550e8400-e29b-41d4-a716-44665544000b', 'ssh_private_key', '-----BEGIN\s+(?:OPENSSH|EC|DSA)\s+PRIVATE\s+KEY-----', 'critical', 'block', 1, datetime('now')),
('550e8400-e29b-41d4-a716-44665544000c', 'google_api_key', 'AIza[0-9A-Za-z_-]{35}', 'high', 'block', 1, datetime('now')),
('550e8400-e29b-41d4-a716-44665544000d', 'slack_token', 'xox[baprs]-[0-9a-zA-Z-]{10,}', 'high', 'block', 1, datetime('now')),
('550e8400-e29b-41d4-a716-44665544000e', 'discord_token', '[MN][A-Za-z\d]{23,}\.[\w-]{6}\.[\w-]{27}', 'high', 'block', 1, datetime('now')),
('550e8400-e29b-41d4-a716-44665544000f', 'twilio_api_key', 'SK[a-fA-F0-9]{32}', 'high', 'block', 1, datetime('now')),
('550e8400-e29b-41d4-a716-446655440010', 'sendgrid_api_key', 'SG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43}', 'high', 'block', 1, datetime('now')),
('550e8400-e29b-41d4-a716-446655440011', 'mailchimp_api_key', '[a-f0-9]{32}-us[0-9]{1,2}', 'medium', 'block', 1, datetime('now')),
('550e8400-e29b-41d4-a716-446655440012', 'high_entropy_hex', '(?<![a-fA-F0-9])[a-fA-F0-9]{64}(?![a-fA-F0-9])', 'medium', 'warn', 1, datetime('now'));
"#;
+47 -46
View File
@@ -38,6 +38,8 @@ use ironclaw::{
workspace::{EmbeddingProvider, NearAiEmbeddings, OpenAiEmbeddings, Workspace},
};
#[cfg(feature = "libsql")]
use ironclaw::secrets::LibSqlSecretsStore;
#[cfg(feature = "postgres")]
use ironclaw::secrets::PostgresSecretsStore;
use ironclaw::secrets::SecretsCrypto;
@@ -344,12 +346,12 @@ async fn main() -> anyhow::Result<()> {
//
// NOTE: For simpler call sites (CLI commands, Memory handler) use the shared
// helper `ironclaw::db::connect_from_config()`. This block is kept inline
// because it also captures backend-specific handles (`pg_pool`, `libsql_conn`)
// because it also captures backend-specific handles (`pg_pool`, `libsql_db`)
// needed by the secrets store.
#[cfg(feature = "postgres")]
let mut pg_pool: Option<deadpool_postgres::Pool> = None;
#[cfg(feature = "libsql")]
let mut libsql_conn: Option<libsql::Connection> = None;
let mut libsql_db: Option<std::sync::Arc<libsql::Database>> = None;
let db: Option<Arc<dyn ironclaw::db::Database>> = if cli.no_db {
tracing::warn!("Running without database connection");
@@ -370,11 +372,9 @@ async fn main() -> anyhow::Result<()> {
.unwrap_or(&default_path);
let backend = if let Some(ref url) = config.database.libsql_url {
let token = config
.database
.libsql_auth_token
.as_ref()
.expect("LIBSQL_AUTH_TOKEN required when LIBSQL_URL is set");
let token = config.database.libsql_auth_token.as_ref().ok_or_else(|| {
anyhow::anyhow!("LIBSQL_AUTH_TOKEN is required when LIBSQL_URL is set")
})?;
LibSqlBackend::new_remote_replica(db_path, url, token.expose_secret()).await?
} else {
LibSqlBackend::new_local(db_path).await?
@@ -382,8 +382,8 @@ async fn main() -> anyhow::Result<()> {
backend.run_migrations().await?;
tracing::info!("libSQL database connected and migrations applied");
// Capture an extra connection for SecretsStore / WasmToolStore
libsql_conn = Some(backend.connect().map_err(|e| anyhow::anyhow!("{}", e))?);
// Capture the Database handle for SecretsStore (connection-per-op)
libsql_db = Some(backend.shared_db());
Some(Arc::new(backend) as Arc<dyn ironclaw::db::Database>)
}
@@ -516,47 +516,48 @@ async fn main() -> anyhow::Result<()> {
tracing::info!("Builder mode enabled");
}
// Create secrets store if master key is configured (needed for MCP auth and WASM channels)
let secrets_store: Option<Arc<dyn SecretsStore + Send + Sync>> = {
#[cfg(feature = "postgres")]
{
if let (Some(pool), Some(master_key)) = (&pg_pool, config.secrets.master_key()) {
match SecretsCrypto::new(master_key.clone()) {
Ok(crypto) => Some(Arc::new(PostgresSecretsStore::new(
pool.clone(),
Arc::new(crypto),
))),
Err(e) => {
tracing::warn!("Failed to initialize secrets crypto: {}", e);
None
}
// Create secrets store if master key is configured (needed for MCP auth and WASM channels).
//
// When both `postgres` and `libsql` features are compiled, the runtime-selected
// backend determines which store is created: whichever DB init branch ran will
// have set its handle (pg_pool or libsql_db), and the or_else chain picks it up.
let secrets_store: Option<Arc<dyn SecretsStore + Send + Sync>> =
if let Some(master_key) = config.secrets.master_key() {
match SecretsCrypto::new(master_key.clone()) {
Ok(crypto) => {
let crypto = Arc::new(crypto);
let store: Option<Arc<dyn SecretsStore + Send + Sync>> = None;
#[cfg(feature = "libsql")]
let store = store.or_else(|| {
libsql_db.take().map(|db| {
Arc::new(LibSqlSecretsStore::new(db, Arc::clone(&crypto)))
as Arc<dyn SecretsStore + Send + Sync>
})
});
#[cfg(feature = "postgres")]
let store = store.or_else(|| {
pg_pool.as_ref().map(|pool| {
Arc::new(PostgresSecretsStore::new(pool.clone(), Arc::clone(&crypto)))
as Arc<dyn SecretsStore + Send + Sync>
})
});
store
}
} else {
None
}
}
#[cfg(all(feature = "libsql", not(feature = "postgres")))]
{
if let (Some(conn), Some(master_key)) =
(libsql_conn.take(), config.secrets.master_key())
{
match SecretsCrypto::new(master_key.clone()) {
Ok(crypto) => Some(Arc::new(LibSqlSecretsStore::new(conn, Arc::new(crypto)))
as Arc<dyn SecretsStore + Send + Sync>),
Err(e) => {
tracing::warn!("Failed to initialize secrets crypto: {}", e);
None
}
Err(e) => {
tracing::warn!("Failed to initialize secrets crypto: {}", e);
#[cfg(feature = "libsql")]
let _ = libsql_db.take();
None
}
} else {
None
}
}
#[cfg(not(any(feature = "postgres", feature = "libsql")))]
{
} else {
#[cfg(feature = "libsql")]
let _ = libsql_db.take();
None
}
};
};
let mcp_session_manager = Arc::new(McpSessionManager::new());
+30 -21
View File
@@ -307,17 +307,26 @@ fn row_to_secret(row: &tokio_postgres::Row) -> Secret {
// ==================== libSQL implementation ====================
/// libSQL/Turso implementation of SecretsStore.
///
/// Holds an `Arc<Database>` handle and creates a fresh connection per operation,
/// matching the connection-per-request pattern used by the main `LibSqlBackend`.
#[cfg(feature = "libsql")]
pub struct LibSqlSecretsStore {
conn: libsql::Connection,
db: Arc<libsql::Database>,
crypto: Arc<SecretsCrypto>,
}
#[cfg(feature = "libsql")]
impl LibSqlSecretsStore {
/// Create a new store with the given libsql connection and crypto instance.
pub fn new(conn: libsql::Connection, crypto: Arc<SecretsCrypto>) -> Self {
Self { conn, crypto }
/// Create a new store with the given shared libsql database handle and crypto instance.
pub fn new(db: Arc<libsql::Database>, crypto: Arc<SecretsCrypto>) -> Self {
Self { db, crypto }
}
fn connect(&self) -> Result<libsql::Connection, SecretError> {
self.db
.connect()
.map_err(|e| SecretError::Database(format!("Connection failed: {}", e)))
}
}
@@ -340,8 +349,8 @@ impl SecretsStore for LibSqlSecretsStore {
.map(|dt| dt.to_rfc3339_opts(chrono::SecondsFormat::Millis, true));
// Start transaction for atomic upsert + read-back
let tx = self
.conn
let conn = self.connect()?;
let tx = conn
.transaction()
.await
.map_err(|e| SecretError::Database(e.to_string()))?;
@@ -401,8 +410,8 @@ impl SecretsStore for LibSqlSecretsStore {
}
async fn get(&self, user_id: &str, name: &str) -> Result<Secret, SecretError> {
let mut rows = self
.conn
let conn = self.connect()?;
let mut rows = conn
.query(
r#"
SELECT id, user_id, name, encrypted_value, key_salt, provider, expires_at,
@@ -446,8 +455,8 @@ impl SecretsStore for LibSqlSecretsStore {
}
async fn exists(&self, user_id: &str, name: &str) -> Result<bool, SecretError> {
let mut rows = self
.conn
let conn = self.connect()?;
let mut rows = conn
.query(
"SELECT 1 FROM secrets WHERE user_id = ?1 AND name = ?2",
libsql::params![user_id, name],
@@ -463,8 +472,8 @@ impl SecretsStore for LibSqlSecretsStore {
}
async fn list(&self, user_id: &str) -> Result<Vec<SecretRef>, SecretError> {
let mut rows = self
.conn
let conn = self.connect()?;
let mut rows = conn
.query(
"SELECT name, provider FROM secrets WHERE user_id = ?1 ORDER BY name",
libsql::params![user_id],
@@ -487,8 +496,8 @@ impl SecretsStore for LibSqlSecretsStore {
}
async fn delete(&self, user_id: &str, name: &str) -> Result<bool, SecretError> {
let affected = self
.conn
let conn = self.connect()?;
let affected = conn
.execute(
"DELETE FROM secrets WHERE user_id = ?1 AND name = ?2",
libsql::params![user_id, name],
@@ -501,18 +510,18 @@ impl SecretsStore for LibSqlSecretsStore {
async fn record_usage(&self, secret_id: Uuid) -> Result<(), SecretError> {
let now = Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
let conn = self.connect()?;
self.conn
.execute(
r#"
conn.execute(
r#"
UPDATE secrets
SET last_used_at = ?1, usage_count = usage_count + 1
WHERE id = ?2
"#,
libsql::params![now.as_str(), secret_id.to_string()],
)
.await
.map_err(|e| SecretError::Database(e.to_string()))?;
libsql::params![now.as_str(), secret_id.to_string()],
)
.await
.map_err(|e| SecretError::Database(e.to_string()))?;
Ok(())
}
+3 -4
View File
@@ -803,11 +803,8 @@ impl SetupWizard {
crypto: &Arc<SecretsCrypto>,
) -> Result<Option<Arc<dyn SecretsStore>>, SetupError> {
if let Some(ref backend) = self.db_backend {
let conn = backend
.connect()
.map_err(|e| SetupError::Database(format!("Failed to create connection: {}", e)))?;
let store: Arc<dyn SecretsStore> = Arc::new(crate::secrets::LibSqlSecretsStore::new(
conn,
backend.shared_db(),
Arc::clone(crypto),
));
Ok(Some(store))
@@ -1136,6 +1133,7 @@ impl Default for SetupWizard {
}
/// Mask password in a database URL for display.
#[cfg(feature = "postgres")]
fn mask_password_in_url(url: &str) -> String {
// URL format: scheme://user:password@host/database
// Find "://" to locate start of credentials
@@ -1326,6 +1324,7 @@ mod tests {
}
#[test]
#[cfg(feature = "postgres")]
fn test_mask_password_in_url() {
assert_eq!(
mask_password_in_url("postgres://user:secret@localhost/db"),
+58 -38
View File
@@ -567,15 +567,24 @@ fn row_to_tool(row: &tokio_postgres::Row) -> Result<StoredWasmTool, WasmStorageE
// ==================== libSQL implementation ====================
/// libSQL/Turso implementation of WasmToolStore.
///
/// Holds an `Arc<Database>` handle and creates a fresh connection per operation,
/// matching the connection-per-request pattern used by the main `LibSqlBackend`.
#[cfg(feature = "libsql")]
pub struct LibSqlWasmToolStore {
conn: libsql::Connection,
db: std::sync::Arc<libsql::Database>,
}
#[cfg(feature = "libsql")]
impl LibSqlWasmToolStore {
pub fn new(conn: libsql::Connection) -> Self {
Self { conn }
pub fn new(db: std::sync::Arc<libsql::Database>) -> Self {
Self { db }
}
fn connect(&self) -> Result<libsql::Connection, WasmStorageError> {
self.db
.connect()
.map_err(|e| WasmStorageError::Database(format!("Connection failed: {}", e)))
}
}
@@ -589,9 +598,15 @@ impl WasmToolStore for LibSqlWasmToolStore {
let schema_str = serde_json::to_string(&params.parameters_schema)
.map_err(|e| WasmStorageError::InvalidData(e.to_string()))?;
self.conn
.execute(
r#"
// Wrap INSERT + read-back in a transaction to prevent TOCTOU races
let conn = self.connect()?;
let tx = conn
.transaction()
.await
.map_err(|e| WasmStorageError::Database(e.to_string()))?;
tx.execute(
r#"
INSERT INTO wasm_tools (
id, user_id, name, version, description, wasm_binary, binary_hash,
parameters_schema, source_url, trust_level, status, created_at, updated_at
@@ -605,26 +620,25 @@ impl WasmToolStore for LibSqlWasmToolStore {
source_url = excluded.source_url,
updated_at = ?11
"#,
libsql::params![
id.to_string(),
params.user_id.as_str(),
params.name.as_str(),
params.version.as_str(),
params.description.as_str(),
libsql::Value::Blob(params.wasm_binary),
libsql::Value::Blob(binary_hash),
schema_str.as_str(),
libsql_wasm_opt_text(params.source_url.as_deref()),
params.trust_level.to_string(),
now.as_str(),
],
)
.await
.map_err(|e| WasmStorageError::Database(e.to_string()))?;
libsql::params![
id.to_string(),
params.user_id.as_str(),
params.name.as_str(),
params.version.as_str(),
params.description.as_str(),
libsql::Value::Blob(params.wasm_binary),
libsql::Value::Blob(binary_hash),
schema_str.as_str(),
libsql_wasm_opt_text(params.source_url.as_deref()),
params.trust_level.to_string(),
now.as_str(),
],
)
.await
.map_err(|e| WasmStorageError::Database(e.to_string()))?;
// Read back the row
let mut rows = self
.conn
// Read back the row within the same transaction
let mut rows = tx
.query(
r#"
SELECT id, user_id, name, version, description, parameters_schema,
@@ -647,12 +661,18 @@ impl WasmToolStore for LibSqlWasmToolStore {
WasmStorageError::Database("Insert succeeded but row not found".into())
})?;
libsql_row_to_tool(&row)
let tool = libsql_row_to_tool(&row)?;
tx.commit()
.await
.map_err(|e| WasmStorageError::Database(e.to_string()))?;
Ok(tool)
}
async fn get(&self, user_id: &str, name: &str) -> Result<StoredWasmTool, WasmStorageError> {
let mut rows = self
.conn
let conn = self.connect()?;
let mut rows = conn
.query(
r#"
SELECT id, user_id, name, version, description, parameters_schema,
@@ -689,8 +709,8 @@ impl WasmToolStore for LibSqlWasmToolStore {
user_id: &str,
name: &str,
) -> Result<StoredWasmToolWithBinary, WasmStorageError> {
let mut rows = self
.conn
let conn = self.connect()?;
let mut rows = conn
.query(
r#"
SELECT id, user_id, name, version, description, wasm_binary, binary_hash,
@@ -748,8 +768,8 @@ impl WasmToolStore for LibSqlWasmToolStore {
&self,
tool_id: Uuid,
) -> Result<Option<StoredCapabilities>, WasmStorageError> {
let mut rows = self
.conn
let conn = self.connect()?;
let mut rows = conn
.query(
r#"
SELECT id, wasm_tool_id, http_allowlist, allowed_secrets, tool_aliases,
@@ -818,8 +838,8 @@ impl WasmToolStore for LibSqlWasmToolStore {
async fn list(&self, user_id: &str) -> Result<Vec<StoredWasmTool>, WasmStorageError> {
// SQLite doesn't have DISTINCT ON, so we use a subquery to get latest version per name
let mut rows = self
.conn
let conn = self.connect()?;
let mut rows = conn
.query(
r#"
SELECT id, user_id, name, version, description, parameters_schema,
@@ -857,9 +877,9 @@ impl WasmToolStore for LibSqlWasmToolStore {
status: ToolStatus,
) -> Result<(), WasmStorageError> {
let now = Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
let conn = self.connect()?;
let result = self
.conn
let result = conn
.execute(
"UPDATE wasm_tools SET status = ?1, updated_at = ?2 WHERE user_id = ?3 AND name = ?4",
libsql::params![status.to_string(), now.as_str(), user_id, name],
@@ -875,8 +895,8 @@ impl WasmToolStore for LibSqlWasmToolStore {
}
async fn delete(&self, user_id: &str, name: &str) -> Result<bool, WasmStorageError> {
let result = self
.conn
let conn = self.connect()?;
let result = conn
.execute(
"DELETE FROM wasm_tools WHERE user_id = ?1 AND name = ?2",
libsql::params![user_id, name],