mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-26 15:40:18 +00:00
Introduce a unified EventBus as the single broadcast channel for all system events, replacing the 6 disconnected event mechanisms. Seal Thread/Turn/ContainerState fields behind private accessors with validated transitions to prevent invalid state mutations. Fix TOCTOU races in thread_ops and session_manager. Event bus (src/event_bus/): - SystemEvent envelope with EventPayload (Domain, StateChange, Telemetry, StateTransition, ToolExecution, AuthEvent, ConfigChange) - Four sinks: SSE (→SseManager), audit (→DB with JSONL fallback), state (→StateBus), metrics (→Observer) - AuditStore trait + implementations for PostgreSQL and libSQL - V13 audit_log migration for both backends - Wired into AppComponents and AgentDeps (Option<EventBus> for compat) - Worker dual-emit through bus alongside legacy SSE+DB paths Sealed state machines: - Thread.state private with state() accessor, can_transition_to(), set_processing(), reset_to_idle() - Turn.state private with state() accessor - ContainerHandle.state private with new() constructor, mark_running/stopped/failed(), can_transition_to() - TOCTOU fix: thread_ops moves safety validation before lock, then checks state + starts turn atomically under single lock - SessionManager TOCTOU fix: atomic check-and-insert with write lock held for entire UUID adoption sequence Startup verification: - AppComponents::verify_readiness() checks component presence vs config - ToolRegistry::verify_expected_tools() validates builtin registration - Config::validate() checks cross-field invariants (Docker, WASM dir) [skip-regression-check] Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
144 lines
5.1 KiB
Rust
144 lines
5.1 KiB
Rust
//! AuditStore implementation for libSQL.
|
|
|
|
use async_trait::async_trait;
|
|
use uuid::Uuid;
|
|
|
|
use crate::db::{AuditFilter, AuditRecord, AuditStore};
|
|
use crate::error::DatabaseError;
|
|
|
|
use super::LibSqlBackend;
|
|
|
|
fn parse_opt_uuid(row: &libsql::Row, idx: i32) -> Option<Uuid> {
|
|
super::get_opt_text(row, idx).and_then(|s| Uuid::parse_str(&s).ok())
|
|
}
|
|
|
|
#[async_trait]
|
|
impl AuditStore for LibSqlBackend {
|
|
async fn append_audit_events(&self, events: &[AuditRecord]) -> Result<(), DatabaseError> {
|
|
if events.is_empty() {
|
|
return Ok(());
|
|
}
|
|
|
|
let conn = self.connect().await?;
|
|
|
|
// Use a transaction for the batch insert.
|
|
conn.execute("BEGIN", ())
|
|
.await
|
|
.map_err(|e| DatabaseError::Query(format!("audit begin: {e}")))?;
|
|
|
|
for event in events {
|
|
conn.execute(
|
|
"INSERT INTO audit_log (event_id, event_type, source_module, source_component, \
|
|
category, session_id, thread_id, job_id, user_id, payload, created_at) \
|
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)",
|
|
libsql::params![
|
|
event.event_id as i64,
|
|
event.event_type.clone(),
|
|
event.source_module.clone(),
|
|
event.source_component.clone(),
|
|
event.category.clone(),
|
|
event.session_id.map(|u| u.to_string()),
|
|
event.thread_id.map(|u| u.to_string()),
|
|
event.job_id.map(|u| u.to_string()),
|
|
event.user_id.clone(),
|
|
serde_json::to_string(&event.payload).unwrap_or_default(),
|
|
super::fmt_ts(&event.created_at),
|
|
],
|
|
)
|
|
.await
|
|
.map_err(|e| DatabaseError::Query(format!("audit insert: {e}")))?;
|
|
}
|
|
|
|
conn.execute("COMMIT", ())
|
|
.await
|
|
.map_err(|e| DatabaseError::Query(format!("audit commit: {e}")))?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn query_audit_log(
|
|
&self,
|
|
filter: &AuditFilter,
|
|
) -> Result<Vec<AuditRecord>, DatabaseError> {
|
|
let conn = self.connect().await?;
|
|
|
|
let mut query = String::from(
|
|
"SELECT event_id, event_type, source_module, source_component, category, \
|
|
session_id, thread_id, job_id, user_id, payload, created_at \
|
|
FROM audit_log WHERE 1=1",
|
|
);
|
|
let mut params: Vec<libsql::Value> = Vec::new();
|
|
let mut idx = 1;
|
|
|
|
if let Some(ref sid) = filter.session_id {
|
|
query.push_str(&format!(" AND session_id = ?{idx}"));
|
|
params.push(sid.to_string().into());
|
|
idx += 1;
|
|
}
|
|
if let Some(ref jid) = filter.job_id {
|
|
query.push_str(&format!(" AND job_id = ?{idx}"));
|
|
params.push(jid.to_string().into());
|
|
idx += 1;
|
|
}
|
|
if let Some(ref uid) = filter.user_id {
|
|
query.push_str(&format!(" AND user_id = ?{idx}"));
|
|
params.push(uid.clone().into());
|
|
idx += 1;
|
|
}
|
|
if let Some(ref et) = filter.event_type {
|
|
query.push_str(&format!(" AND event_type = ?{idx}"));
|
|
params.push(et.clone().into());
|
|
idx += 1;
|
|
}
|
|
if let Some(ref after) = filter.after {
|
|
query.push_str(&format!(" AND created_at > ?{idx}"));
|
|
params.push(super::fmt_ts(after).into());
|
|
idx += 1;
|
|
}
|
|
if let Some(ref before) = filter.before {
|
|
query.push_str(&format!(" AND created_at < ?{idx}"));
|
|
params.push(super::fmt_ts(before).into());
|
|
idx += 1;
|
|
}
|
|
|
|
query.push_str(" ORDER BY created_at DESC");
|
|
|
|
let limit = filter.limit.unwrap_or(1000);
|
|
query.push_str(&format!(" LIMIT ?{idx}"));
|
|
params.push(limit.into());
|
|
|
|
let rows = conn
|
|
.query(&query, libsql::params_from_iter(params))
|
|
.await
|
|
.map_err(|e| DatabaseError::Query(format!("audit_log query: {e}")))?;
|
|
|
|
let mut records = Vec::new();
|
|
let mut rows = rows;
|
|
while let Some(row) = rows
|
|
.next()
|
|
.await
|
|
.map_err(|e| DatabaseError::Query(format!("audit_log row: {e}")))?
|
|
{
|
|
let event_id: i64 = super::get_i64(&row, 0);
|
|
let payload_str: String = super::get_text(&row, 9);
|
|
let payload: serde_json::Value = serde_json::from_str(&payload_str).unwrap_or_default();
|
|
|
|
records.push(AuditRecord {
|
|
event_id: event_id as u64,
|
|
event_type: super::get_text(&row, 1),
|
|
source_module: super::get_text(&row, 2),
|
|
source_component: super::get_text(&row, 3),
|
|
category: super::get_text(&row, 4),
|
|
session_id: parse_opt_uuid(&row, 5),
|
|
thread_id: parse_opt_uuid(&row, 6),
|
|
job_id: parse_opt_uuid(&row, 7),
|
|
user_id: super::get_opt_text(&row, 8),
|
|
payload,
|
|
created_at: super::get_ts(&row, 10),
|
|
});
|
|
}
|
|
|
|
Ok(records)
|
|
}
|
|
}
|