Compare commits

...
Author SHA1 Message Date
ZakiandClaude Opus 4.6 51cec37514 fix(libsql): standardize timestamp storage to RFC 3339 with UTC offset (#663)
Replace all `datetime('now')` defaults in libsql_migrations.rs with
`strftime('%Y-%m-%dT%H:%M:%fZ', 'now')` so new rows get proper RFC 3339
timestamps (e.g. `2024-01-15T10:30:00.123Z`) instead of naive datetimes
(e.g. `2024-01-15 10:30:00`).

Add tracing::warn! to parse_timestamp() naive fallback paths so legacy
timestamps are still accepted but produce a visible deprecation signal.

Backward compatible: no data migration needed; existing naive timestamps
continue to parse correctly via the multi-format fallback.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
2026-03-07 17:32:18 -08:00
2 changed files with 157 additions and 55 deletions
+102
View File
@@ -169,10 +169,18 @@ pub(crate) fn parse_timestamp(s: &str) -> Result<DateTime<Utc>, String> {
} }
// Naive with fractional seconds (legacy or SQLite datetime() output) // Naive with fractional seconds (legacy or SQLite datetime() output)
if let Ok(ndt) = NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S%.f") { if let Ok(ndt) = NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S%.f") {
tracing::warn!(
timestamp = s,
"parsing naive timestamp without timezone; assuming UTC — consider re-running migrations"
);
return Ok(ndt.and_utc()); return Ok(ndt.and_utc());
} }
// Naive without fractional seconds (legacy format) // Naive without fractional seconds (legacy format)
if let Ok(ndt) = NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S") { if let Ok(ndt) = NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S") {
tracing::warn!(
timestamp = s,
"parsing naive timestamp without timezone; assuming UTC — consider re-running migrations"
);
return Ok(ndt.and_utc()); return Ok(ndt.and_utc());
} }
Err(format!("unparseable timestamp: {:?}", s)) Err(format!("unparseable timestamp: {:?}", s))
@@ -510,4 +518,98 @@ mod tests {
); );
} }
} }
#[test]
fn test_parse_timestamp_rfc3339() {
use super::parse_timestamp;
// Standard RFC 3339 with Z suffix
let dt = parse_timestamp("2024-01-15T10:30:00.123Z").unwrap();
assert_eq!(
dt.to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
"2024-01-15T10:30:00.123Z"
);
// RFC 3339 with +00:00 offset
let dt = parse_timestamp("2024-01-15T10:30:00.000+00:00").unwrap();
assert_eq!(
dt.to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
"2024-01-15T10:30:00.000Z"
);
}
#[test]
fn test_parse_timestamp_naive_fallback() {
use super::parse_timestamp;
// Naive with fractional seconds (legacy datetime('now') output)
let dt = parse_timestamp("2024-01-15 10:30:00.123").unwrap();
assert_eq!(
dt.to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
"2024-01-15T10:30:00.123Z"
);
// Naive without fractional seconds
let dt = parse_timestamp("2024-01-15 10:30:00").unwrap();
assert_eq!(
dt.to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
"2024-01-15T10:30:00.000Z"
);
}
#[test]
fn test_parse_timestamp_invalid() {
use super::parse_timestamp;
assert!(parse_timestamp("not-a-timestamp").is_err());
assert!(parse_timestamp("").is_err());
}
#[tokio::test]
async fn test_default_timestamps_are_rfc3339() {
// Verify that DEFAULT column values produce RFC 3339 timestamps
// after the migration change from datetime('now') to strftime.
// Use file-based DB because in-memory doesn't share schema across connections.
let dir = tempfile::tempdir().unwrap();
let db_path = dir.path().join("test_ts.db");
let backend = LibSqlBackend::new_local(&db_path).await.unwrap();
backend.run_migrations().await.unwrap();
let conn = backend.connect().await.unwrap();
let id = uuid::Uuid::new_v4().to_string();
conn.execute(
"INSERT INTO conversations (id, channel, user_id) VALUES (?1, ?2, ?3)",
libsql::params![id.clone(), "test", "user1"],
)
.await
.unwrap();
let mut rows = conn
.query(
"SELECT started_at, last_activity FROM conversations WHERE id = ?1",
libsql::params![id],
)
.await
.unwrap();
let row = rows.next().await.unwrap().unwrap();
let started_at: String = row.get(0).unwrap();
let last_activity: String = row.get(1).unwrap();
// Must end with 'Z' (RFC 3339 UTC) and contain 'T' separator
assert!(
started_at.ends_with('Z') && started_at.contains('T'),
"started_at should be RFC 3339, got: {started_at}"
);
assert!(
last_activity.ends_with('Z') && last_activity.contains('T'),
"last_activity should be RFC 3339, got: {last_activity}"
);
// Must be parseable by the RFC 3339 parser directly (not just naive fallback)
use chrono::DateTime;
assert!(
DateTime::parse_from_rfc3339(&started_at).is_ok(),
"started_at not valid RFC 3339: {started_at}"
);
}
} }
+55 -55
View File
@@ -26,7 +26,7 @@ pub const SCHEMA: &str = r#"
CREATE TABLE IF NOT EXISTS _migrations ( CREATE TABLE IF NOT EXISTS _migrations (
version INTEGER PRIMARY KEY, version INTEGER PRIMARY KEY,
name TEXT NOT NULL, name TEXT NOT NULL,
applied_at TEXT NOT NULL DEFAULT (datetime('now')) applied_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
); );
-- ==================== Conversations ==================== -- ==================== Conversations ====================
@@ -36,8 +36,8 @@ CREATE TABLE IF NOT EXISTS conversations (
channel TEXT NOT NULL, channel TEXT NOT NULL,
user_id TEXT NOT NULL, user_id TEXT NOT NULL,
thread_id TEXT, thread_id TEXT,
started_at TEXT NOT NULL DEFAULT (datetime('now')), started_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
last_activity TEXT NOT NULL DEFAULT (datetime('now')), last_activity TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
metadata TEXT NOT NULL DEFAULT '{}' metadata TEXT NOT NULL DEFAULT '{}'
); );
@@ -59,7 +59,7 @@ CREATE TABLE IF NOT EXISTS conversation_messages (
conversation_id TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE, conversation_id TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
role TEXT NOT NULL, role TEXT NOT NULL,
content TEXT NOT NULL, content TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')) created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
); );
CREATE INDEX IF NOT EXISTS idx_conversation_messages_conversation CREATE INDEX IF NOT EXISTS idx_conversation_messages_conversation
@@ -91,7 +91,7 @@ CREATE TABLE IF NOT EXISTS agent_jobs (
failure_reason TEXT, failure_reason TEXT,
stuck_since TEXT, stuck_since TEXT,
repair_attempts INTEGER NOT NULL DEFAULT 0, repair_attempts INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now')), created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
started_at TEXT, started_at TEXT,
completed_at TEXT completed_at TEXT
); );
@@ -116,7 +116,7 @@ CREATE TABLE IF NOT EXISTS job_actions (
duration_ms INTEGER, duration_ms INTEGER,
success INTEGER NOT NULL, success INTEGER NOT NULL,
error_message TEXT, error_message TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')), created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
UNIQUE(job_id, sequence_num) UNIQUE(job_id, sequence_num)
); );
@@ -137,8 +137,8 @@ CREATE TABLE IF NOT EXISTS dynamic_tools (
failure_count INTEGER NOT NULL DEFAULT 0, failure_count INTEGER NOT NULL DEFAULT 0,
last_error TEXT, last_error TEXT,
status TEXT NOT NULL DEFAULT 'active', status TEXT NOT NULL DEFAULT 'active',
created_at TEXT NOT NULL DEFAULT (datetime('now')), created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now')) updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
); );
CREATE INDEX IF NOT EXISTS idx_dynamic_tools_status ON dynamic_tools(status); CREATE INDEX IF NOT EXISTS idx_dynamic_tools_status ON dynamic_tools(status);
@@ -156,7 +156,7 @@ CREATE TABLE IF NOT EXISTS llm_calls (
output_tokens INTEGER NOT NULL, output_tokens INTEGER NOT NULL,
cost TEXT NOT NULL, cost TEXT NOT NULL,
purpose TEXT, purpose TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')) created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
); );
CREATE INDEX IF NOT EXISTS idx_llm_calls_job ON llm_calls(job_id); CREATE INDEX IF NOT EXISTS idx_llm_calls_job ON llm_calls(job_id);
@@ -176,7 +176,7 @@ CREATE TABLE IF NOT EXISTS estimation_snapshots (
actual_time_secs INTEGER, actual_time_secs INTEGER,
estimated_value TEXT NOT NULL, estimated_value TEXT NOT NULL,
actual_value TEXT, actual_value TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')) created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
); );
CREATE INDEX IF NOT EXISTS idx_estimation_category ON estimation_snapshots(category); CREATE INDEX IF NOT EXISTS idx_estimation_category ON estimation_snapshots(category);
@@ -192,7 +192,7 @@ CREATE TABLE IF NOT EXISTS repair_attempts (
action_taken TEXT NOT NULL, action_taken TEXT NOT NULL,
success INTEGER NOT NULL, success INTEGER NOT NULL,
error_message TEXT, error_message TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')) created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
); );
CREATE INDEX IF NOT EXISTS idx_repair_attempts_target ON repair_attempts(target_type, target_id); CREATE INDEX IF NOT EXISTS idx_repair_attempts_target ON repair_attempts(target_type, target_id);
@@ -206,8 +206,8 @@ CREATE TABLE IF NOT EXISTS memory_documents (
agent_id TEXT, agent_id TEXT,
path TEXT NOT NULL, path TEXT NOT NULL,
content TEXT NOT NULL, content TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')), created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
metadata TEXT NOT NULL DEFAULT '{}', metadata TEXT NOT NULL DEFAULT '{}',
UNIQUE (user_id, agent_id, path) UNIQUE (user_id, agent_id, path)
); );
@@ -222,7 +222,7 @@ CREATE TRIGGER IF NOT EXISTS update_memory_documents_updated_at
FOR EACH ROW FOR EACH ROW
WHEN NEW.updated_at = OLD.updated_at WHEN NEW.updated_at = OLD.updated_at
BEGIN BEGIN
UPDATE memory_documents SET updated_at = datetime('now') WHERE id = NEW.id; UPDATE memory_documents SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = NEW.id;
END; END;
-- ==================== Workspace: Memory Chunks ==================== -- ==================== Workspace: Memory Chunks ====================
@@ -234,7 +234,7 @@ CREATE TABLE IF NOT EXISTS memory_chunks (
chunk_index INTEGER NOT NULL, chunk_index INTEGER NOT NULL,
content TEXT NOT NULL, content TEXT NOT NULL,
embedding BLOB, embedding BLOB,
created_at TEXT NOT NULL DEFAULT (datetime('now')), created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
UNIQUE (document_id, chunk_index) UNIQUE (document_id, chunk_index)
); );
@@ -296,8 +296,8 @@ CREATE TABLE IF NOT EXISTS secrets (
expires_at TEXT, expires_at TEXT,
last_used_at TEXT, last_used_at TEXT,
usage_count INTEGER NOT NULL DEFAULT 0, usage_count INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now')), created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
UNIQUE (user_id, name) UNIQUE (user_id, name)
); );
@@ -318,8 +318,8 @@ CREATE TABLE IF NOT EXISTS wasm_tools (
source_url TEXT, source_url TEXT,
trust_level TEXT NOT NULL DEFAULT 'user', trust_level TEXT NOT NULL DEFAULT 'user',
status TEXT NOT NULL DEFAULT 'active', status TEXT NOT NULL DEFAULT 'active',
created_at TEXT NOT NULL DEFAULT (datetime('now')), created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
UNIQUE (user_id, name, version) UNIQUE (user_id, name, version)
); );
@@ -340,8 +340,8 @@ CREATE TABLE IF NOT EXISTS wasm_channels (
binary_hash BLOB NOT NULL, binary_hash BLOB NOT NULL,
capabilities_json TEXT NOT NULL DEFAULT '{}', capabilities_json TEXT NOT NULL DEFAULT '{}',
status TEXT NOT NULL DEFAULT 'active', status TEXT NOT NULL DEFAULT 'active',
created_at TEXT NOT NULL DEFAULT (datetime('now')), created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
UNIQUE (user_id, name) UNIQUE (user_id, name)
); );
@@ -359,8 +359,8 @@ CREATE TABLE IF NOT EXISTS tool_capabilities (
max_response_body_bytes INTEGER NOT NULL DEFAULT 10485760, max_response_body_bytes INTEGER NOT NULL DEFAULT 10485760,
workspace_read_prefixes TEXT NOT NULL DEFAULT '[]', workspace_read_prefixes TEXT NOT NULL DEFAULT '[]',
http_timeout_secs INTEGER NOT NULL DEFAULT 30, http_timeout_secs INTEGER NOT NULL DEFAULT 30,
created_at TEXT NOT NULL DEFAULT (datetime('now')), created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
UNIQUE (wasm_tool_id) UNIQUE (wasm_tool_id)
); );
@@ -373,7 +373,7 @@ CREATE TABLE IF NOT EXISTS leak_detection_patterns (
severity TEXT NOT NULL DEFAULT 'high', severity TEXT NOT NULL DEFAULT 'high',
action TEXT NOT NULL DEFAULT 'block', action TEXT NOT NULL DEFAULT 'block',
enabled INTEGER NOT NULL DEFAULT 1, enabled INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL DEFAULT (datetime('now')) created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
); );
-- ==================== Rate Limit State ==================== -- ==================== Rate Limit State ====================
@@ -382,9 +382,9 @@ CREATE TABLE IF NOT EXISTS tool_rate_limit_state (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
wasm_tool_id TEXT NOT NULL REFERENCES wasm_tools(id) ON DELETE CASCADE, wasm_tool_id TEXT NOT NULL REFERENCES wasm_tools(id) ON DELETE CASCADE,
user_id TEXT NOT NULL, user_id TEXT NOT NULL,
minute_window_start TEXT NOT NULL DEFAULT (datetime('now')), minute_window_start TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
minute_count INTEGER NOT NULL DEFAULT 0, minute_count INTEGER NOT NULL DEFAULT 0,
hour_window_start TEXT NOT NULL DEFAULT (datetime('now')), hour_window_start TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
hour_count INTEGER NOT NULL DEFAULT 0, hour_count INTEGER NOT NULL DEFAULT 0,
UNIQUE (wasm_tool_id, user_id) UNIQUE (wasm_tool_id, user_id)
); );
@@ -400,7 +400,7 @@ CREATE TABLE IF NOT EXISTS secret_usage_log (
target_path TEXT, target_path TEXT,
success INTEGER NOT NULL, success INTEGER NOT NULL,
error_message TEXT, error_message TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')) created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
); );
CREATE INDEX IF NOT EXISTS idx_secret_usage_user ON secret_usage_log(user_id); CREATE INDEX IF NOT EXISTS idx_secret_usage_user ON secret_usage_log(user_id);
@@ -415,7 +415,7 @@ CREATE TABLE IF NOT EXISTS leak_detection_events (
source TEXT NOT NULL, source TEXT NOT NULL,
action_taken TEXT NOT NULL, action_taken TEXT NOT NULL,
context_preview TEXT, context_preview TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')) created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
); );
-- ==================== Tool Failures ==================== -- ==================== Tool Failures ====================
@@ -425,8 +425,8 @@ CREATE TABLE IF NOT EXISTS tool_failures (
tool_name TEXT NOT NULL UNIQUE, tool_name TEXT NOT NULL UNIQUE,
error_message TEXT, error_message TEXT,
error_count INTEGER DEFAULT 1, error_count INTEGER DEFAULT 1,
first_failure TEXT DEFAULT (datetime('now')), first_failure TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
last_failure TEXT DEFAULT (datetime('now')), last_failure TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
last_build_result TEXT, last_build_result TEXT,
repaired_at TEXT, repaired_at TEXT,
repair_attempts INTEGER DEFAULT 0 repair_attempts INTEGER DEFAULT 0
@@ -441,7 +441,7 @@ CREATE TABLE IF NOT EXISTS job_events (
job_id TEXT NOT NULL REFERENCES agent_jobs(id), job_id TEXT NOT NULL REFERENCES agent_jobs(id),
event_type TEXT NOT NULL, event_type TEXT NOT NULL,
data TEXT NOT NULL, data TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')) created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
); );
CREATE INDEX IF NOT EXISTS idx_job_events_job ON job_events(job_id, id); CREATE INDEX IF NOT EXISTS idx_job_events_job ON job_events(job_id, id);
@@ -471,8 +471,8 @@ CREATE TABLE IF NOT EXISTS routines (
next_fire_at TEXT, next_fire_at TEXT,
run_count INTEGER NOT NULL DEFAULT 0, run_count INTEGER NOT NULL DEFAULT 0,
consecutive_failures INTEGER NOT NULL DEFAULT 0, consecutive_failures INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now')), created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
UNIQUE (user_id, name) UNIQUE (user_id, name)
); );
@@ -485,13 +485,13 @@ CREATE TABLE IF NOT EXISTS routine_runs (
routine_id TEXT NOT NULL REFERENCES routines(id) ON DELETE CASCADE, routine_id TEXT NOT NULL REFERENCES routines(id) ON DELETE CASCADE,
trigger_type TEXT NOT NULL, trigger_type TEXT NOT NULL,
trigger_detail TEXT, trigger_detail TEXT,
started_at TEXT NOT NULL DEFAULT (datetime('now')), started_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
completed_at TEXT, completed_at TEXT,
status TEXT NOT NULL DEFAULT 'running', status TEXT NOT NULL DEFAULT 'running',
result_summary TEXT, result_summary TEXT,
tokens_used INTEGER, tokens_used INTEGER,
job_id TEXT REFERENCES agent_jobs(id), job_id TEXT REFERENCES agent_jobs(id),
created_at TEXT NOT NULL DEFAULT (datetime('now')) created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
); );
CREATE INDEX IF NOT EXISTS idx_routine_runs_routine ON routine_runs(routine_id); CREATE INDEX IF NOT EXISTS idx_routine_runs_routine ON routine_runs(routine_id);
@@ -502,7 +502,7 @@ CREATE TABLE IF NOT EXISTS settings (
user_id TEXT NOT NULL, user_id TEXT NOT NULL,
key TEXT NOT NULL, key TEXT NOT NULL,
value TEXT NOT NULL, value TEXT NOT NULL,
updated_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
PRIMARY KEY (user_id, key) PRIMARY KEY (user_id, key)
); );
@@ -558,24 +558,24 @@ CREATE INDEX IF NOT EXISTS idx_heartbeat_next_run ON heartbeat_state(next_run);
-- Pre-populate leak detection patterns (matches PostgreSQL V2 migration). -- 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 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-446655440001', 'openai_api_key', 'sk-(?:proj-)?[a-zA-Z0-9]{20,}(?:T3BlbkFJ[a-zA-Z0-9_-]*)?', 'critical', 'block', 1, strftime('%Y-%m-%dT%H:%M:%fZ', '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-446655440002', 'anthropic_api_key', 'sk-ant-api[a-zA-Z0-9_-]{90,}', 'critical', 'block', 1, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
('550e8400-e29b-41d4-a716-446655440003', 'aws_access_key', 'AKIA[0-9A-Z]{16}', 'critical', 'block', 1, datetime('now')), ('550e8400-e29b-41d4-a716-446655440003', 'aws_access_key', 'AKIA[0-9A-Z]{16}', 'critical', 'block', 1, strftime('%Y-%m-%dT%H:%M:%fZ', '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-446655440004', 'aws_secret_key', '(?<![A-Za-z0-9/+=])[A-Za-z0-9/+=]{40}(?![A-Za-z0-9/+=])', 'high', 'block', 1, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
('550e8400-e29b-41d4-a716-446655440005', 'github_token', 'gh[pousr]_[A-Za-z0-9_]{36,}', 'critical', 'block', 1, datetime('now')), ('550e8400-e29b-41d4-a716-446655440005', 'github_token', 'gh[pousr]_[A-Za-z0-9_]{36,}', 'critical', 'block', 1, strftime('%Y-%m-%dT%H:%M:%fZ', '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-446655440006', 'github_fine_grained_pat', 'github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59}', 'critical', 'block', 1, strftime('%Y-%m-%dT%H:%M:%fZ', '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-446655440007', 'stripe_api_key', 'sk_(?:live|test)_[a-zA-Z0-9]{24,}', 'critical', 'block', 1, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
('550e8400-e29b-41d4-a716-446655440008', 'nearai_session', 'sess_[a-zA-Z0-9]{32,}', 'critical', 'block', 1, datetime('now')), ('550e8400-e29b-41d4-a716-446655440008', 'nearai_session', 'sess_[a-zA-Z0-9]{32,}', 'critical', 'block', 1, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
('550e8400-e29b-41d4-a716-446655440009', 'bearer_token', 'Bearer\s+[a-zA-Z0-9_-]{20,}', 'high', 'redact', 1, datetime('now')), ('550e8400-e29b-41d4-a716-446655440009', 'bearer_token', 'Bearer\s+[a-zA-Z0-9_-]{20,}', 'high', 'redact', 1, strftime('%Y-%m-%dT%H:%M:%fZ', '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-44665544000a', 'pem_private_key', '-----BEGIN\s+(?:RSA\s+)?PRIVATE\s+KEY-----', 'critical', 'block', 1, strftime('%Y-%m-%dT%H:%M:%fZ', '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-44665544000b', 'ssh_private_key', '-----BEGIN\s+(?:OPENSSH|EC|DSA)\s+PRIVATE\s+KEY-----', 'critical', 'block', 1, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
('550e8400-e29b-41d4-a716-44665544000c', 'google_api_key', 'AIza[0-9A-Za-z_-]{35}', 'high', 'block', 1, datetime('now')), ('550e8400-e29b-41d4-a716-44665544000c', 'google_api_key', 'AIza[0-9A-Za-z_-]{35}', 'high', 'block', 1, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
('550e8400-e29b-41d4-a716-44665544000d', 'slack_token', 'xox[baprs]-[0-9a-zA-Z-]{10,}', 'high', 'block', 1, datetime('now')), ('550e8400-e29b-41d4-a716-44665544000d', 'slack_token', 'xox[baprs]-[0-9a-zA-Z-]{10,}', 'high', 'block', 1, strftime('%Y-%m-%dT%H:%M:%fZ', '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-44665544000e', 'discord_token', '[MN][A-Za-z\d]{23,}\.[\w-]{6}\.[\w-]{27}', 'high', 'block', 1, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
('550e8400-e29b-41d4-a716-44665544000f', 'twilio_api_key', 'SK[a-fA-F0-9]{32}', 'high', 'block', 1, datetime('now')), ('550e8400-e29b-41d4-a716-44665544000f', 'twilio_api_key', 'SK[a-fA-F0-9]{32}', 'high', 'block', 1, strftime('%Y-%m-%dT%H:%M:%fZ', '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-446655440010', 'sendgrid_api_key', 'SG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43}', 'high', 'block', 1, strftime('%Y-%m-%dT%H:%M:%fZ', '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-446655440011', 'mailchimp_api_key', '[a-f0-9]{32}-us[0-9]{1,2}', 'medium', 'block', 1, strftime('%Y-%m-%dT%H:%M:%fZ', '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')); ('550e8400-e29b-41d4-a716-446655440012', 'high_entropy_hex', '(?<![a-fA-F0-9])[a-fA-F0-9]{64}(?![a-fA-F0-9])', 'medium', 'warn', 1, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'));
"#; "#;
@@ -613,7 +613,7 @@ CREATE TABLE IF NOT EXISTS memory_chunks_new (
chunk_index INTEGER NOT NULL, chunk_index INTEGER NOT NULL,
content TEXT NOT NULL, content TEXT NOT NULL,
embedding BLOB, embedding BLOB,
created_at TEXT NOT NULL DEFAULT (datetime('now')), created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
UNIQUE (document_id, chunk_index) UNIQUE (document_id, chunk_index)
); );