From 3f1638c98bc0154bd0a71f1c111fe0d895cf2c66 Mon Sep 17 00:00:00 2001 From: Hibryda Date: Sun, 8 Mar 2026 20:10:54 +0100 Subject: [PATCH] fix: resolve medium/low audit findings across backend and frontend - ctx CLI: validate int() limit arg, wrap FTS5 MATCH in try/except - ctx.rs: FTS5 error message clarity, Mutex::lock() returns Err not panic - sdk-messages.ts: runtime type guards (str/num) replace bare `as` casts - agent-runner.ts: strip ANTHROPIC_* env vars alongside CLAUDE* - agent-dispatcher.ts: timestamps use seconds (match session.rs convention) - remote.rs: disconnect handler uses lock().await not try_lock() - session.rs: propagate pane_ids serialization error - watcher.rs: reject root-level paths instead of silent no-op - lib.rs: log warnings on profile.toml read failure and resource_dir error - agent-bridge.ts: validate event payload is object before cast --- ctx | 30 +++++++++---- v2/sidecar/agent-runner.ts | 2 +- v2/src-tauri/src/ctx.rs | 19 ++++++--- v2/src-tauri/src/lib.rs | 10 ++++- v2/src-tauri/src/remote.rs | 9 ++-- v2/src-tauri/src/session.rs | 3 +- v2/src-tauri/src/watcher.rs | 4 +- v2/src/lib/adapters/agent-bridge.ts | 4 +- v2/src/lib/adapters/sdk-messages.ts | 66 +++++++++++++++++------------ v2/src/lib/agent-dispatcher.ts | 7 +-- 10 files changed, 97 insertions(+), 57 deletions(-) diff --git a/ctx b/ctx index bb2995b..315964b 100755 --- a/ctx +++ b/ctx @@ -278,7 +278,11 @@ def cmd_history(args): print("Usage: ctx history [limit]") sys.exit(1) project = args[0] - limit = int(args[1]) if len(args) > 1 else 10 + try: + limit = int(args[1]) if len(args) > 1 else 10 + except ValueError: + print(f"Error: limit must be an integer, got '{args[1]}'") + sys.exit(1) db = get_db() rows = db.execute( "SELECT summary, created_at FROM summaries WHERE project = ? ORDER BY created_at DESC LIMIT ?", @@ -301,16 +305,24 @@ def cmd_search(args): query = " ".join(args) db = get_db() - # Search project contexts - results_ctx = db.execute( - "SELECT project, key, value FROM contexts_fts WHERE contexts_fts MATCH ?", - (query,), - ).fetchall() + # Search project contexts (FTS5 MATCH can fail on malformed query syntax) + try: + results_ctx = db.execute( + "SELECT project, key, value FROM contexts_fts WHERE contexts_fts MATCH ?", + (query,), + ).fetchall() + except sqlite3.OperationalError: + print(f"Invalid search query: '{query}' (FTS5 syntax error)") + db.close() + sys.exit(1) # Search shared contexts - results_shared = db.execute( - "SELECT key, value FROM shared_fts WHERE shared_fts MATCH ?", (query,) - ).fetchall() + try: + results_shared = db.execute( + "SELECT key, value FROM shared_fts WHERE shared_fts MATCH ?", (query,) + ).fetchall() + except sqlite3.OperationalError: + results_shared = [] # Search summaries (simple LIKE since no FTS on summaries) results_sum = db.execute( diff --git a/v2/sidecar/agent-runner.ts b/v2/sidecar/agent-runner.ts index 7027b7f..f8b4125 100644 --- a/v2/sidecar/agent-runner.ts +++ b/v2/sidecar/agent-runner.ts @@ -86,7 +86,7 @@ async function handleQuery(msg: QueryMessage) { // Strip CLAUDE* env vars to prevent nesting detection by the spawned CLI const cleanEnv: Record = {}; for (const [key, value] of Object.entries(process.env)) { - if (!key.startsWith('CLAUDE')) { + if (!key.startsWith('CLAUDE') && !key.startsWith('ANTHROPIC_')) { cleanEnv[key] = value; } } diff --git a/v2/src-tauri/src/ctx.rs b/v2/src-tauri/src/ctx.rs index 5149ebf..93bec31 100644 --- a/v2/src-tauri/src/ctx.rs +++ b/v2/src-tauri/src/ctx.rs @@ -127,7 +127,7 @@ impl CtxDb { rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY | rusqlite::OpenFlags::SQLITE_OPEN_NO_MUTEX, ).map_err(|e| format!("Failed to reopen database: {e}"))?; - let mut lock = self.conn.lock().unwrap(); + let mut lock = self.conn.lock().map_err(|_| "ctx database lock poisoned".to_string())?; *lock = Some(ro_conn); Ok(()) @@ -149,7 +149,7 @@ impl CtxDb { } pub fn get_context(&self, project: &str) -> Result, String> { - let lock = self.conn.lock().unwrap(); + let lock = self.conn.lock().map_err(|_| "ctx database lock poisoned".to_string())?; let conn = lock.as_ref().ok_or("ctx database not found")?; let mut stmt = conn @@ -173,7 +173,7 @@ impl CtxDb { } pub fn get_shared(&self) -> Result, String> { - let lock = self.conn.lock().unwrap(); + let lock = self.conn.lock().map_err(|_| "ctx database lock poisoned".to_string())?; let conn = lock.as_ref().ok_or("ctx database not found")?; let mut stmt = conn @@ -197,7 +197,7 @@ impl CtxDb { } pub fn get_summaries(&self, project: &str, limit: i64) -> Result, String> { - let lock = self.conn.lock().unwrap(); + let lock = self.conn.lock().map_err(|_| "ctx database lock poisoned".to_string())?; let conn = lock.as_ref().ok_or("ctx database not found")?; let mut stmt = conn @@ -220,7 +220,7 @@ impl CtxDb { } pub fn search(&self, query: &str) -> Result, String> { - let lock = self.conn.lock().unwrap(); + let lock = self.conn.lock().map_err(|_| "ctx database lock poisoned".to_string())?; let conn = lock.as_ref().ok_or("ctx database not found")?; let mut stmt = conn @@ -236,7 +236,14 @@ impl CtxDb { updated_at: String::new(), // FTS5 virtual table doesn't store updated_at }) }) - .map_err(|e| format!("ctx search failed: {e}"))? + .map_err(|e| { + let msg = e.to_string(); + if msg.contains("fts5") || msg.contains("syntax") { + format!("Invalid search query syntax: {e}") + } else { + format!("ctx search failed: {e}") + } + })? .collect::, _>>() .map_err(|e| format!("ctx row read failed: {e}"))?; diff --git a/v2/src-tauri/src/lib.rs b/v2/src-tauri/src/lib.rs index 6aeb1f5..1c4d1d7 100644 --- a/v2/src-tauri/src/lib.rs +++ b/v2/src-tauri/src/lib.rs @@ -240,7 +240,10 @@ fn claude_list_profiles() -> Vec { // Read profile.toml for metadata let toml_path = entry.path().join("profile.toml"); let (email, subscription_type, display_name) = if toml_path.exists() { - let content = std::fs::read_to_string(&toml_path).unwrap_or_default(); + let content = std::fs::read_to_string(&toml_path).unwrap_or_else(|e| { + log::warn!("Failed to read {}: {e}", toml_path.display()); + String::new() + }); ( extract_toml_value(&content, "email"), extract_toml_value(&content, "subscription_type"), @@ -606,7 +609,10 @@ pub fn run() { .handle() .path() .resource_dir() - .unwrap_or_default(); + .unwrap_or_else(|e| { + log::warn!("Failed to resolve resource_dir: {e}"); + std::path::PathBuf::new() + }); let dev_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) .parent() .unwrap() diff --git a/v2/src-tauri/src/remote.rs b/v2/src-tauri/src/remote.rs index f87e613..2887473 100644 --- a/v2/src-tauri/src/remote.rs +++ b/v2/src-tauri/src/remote.rs @@ -226,11 +226,10 @@ impl RemoteManager { // Mark disconnected and clear connection { - if let Ok(mut machines) = machines_ref.try_lock() { - if let Some(machine) = machines.get_mut(&mid) { - machine.status = "disconnected".to_string(); - machine.connection = None; - } + let mut machines = machines_ref.lock().await; + if let Some(machine) = machines.get_mut(&mid) { + machine.status = "disconnected".to_string(); + machine.connection = None; } } let _ = app_handle.emit("remote-machine-disconnected", &serde_json::json!({ diff --git a/v2/src-tauri/src/session.rs b/v2/src-tauri/src/session.rs index 197ce60..3a61abb 100644 --- a/v2/src-tauri/src/session.rs +++ b/v2/src-tauri/src/session.rs @@ -284,7 +284,8 @@ impl SessionDb { pub fn save_layout(&self, layout: &LayoutState) -> Result<(), String> { let conn = self.conn.lock().unwrap(); - let pane_ids_json = serde_json::to_string(&layout.pane_ids).unwrap_or_default(); + let pane_ids_json = serde_json::to_string(&layout.pane_ids) + .map_err(|e| format!("Serialize pane_ids failed: {e}"))?; conn.execute( "UPDATE layout_state SET preset = ?1, pane_ids = ?2 WHERE id = 1", params![layout.preset, pane_ids_json], diff --git a/v2/src-tauri/src/watcher.rs b/v2/src-tauri/src/watcher.rs index 24b044b..9aee5e5 100644 --- a/v2/src-tauri/src/watcher.rs +++ b/v2/src-tauri/src/watcher.rs @@ -72,8 +72,10 @@ impl FileWatcherManager { ) .map_err(|e| format!("Failed to create watcher: {e}"))?; + let watch_dir = file_path.parent() + .ok_or_else(|| format!("Cannot watch root-level path: {path}"))?; watcher - .watch(file_path.parent().unwrap_or(&file_path), RecursiveMode::NonRecursive) + .watch(watch_dir, RecursiveMode::NonRecursive) .map_err(|e| format!("Failed to watch path: {e}"))?; let mut watchers = self.watchers.lock().unwrap(); diff --git a/v2/src/lib/adapters/agent-bridge.ts b/v2/src/lib/adapters/agent-bridge.ts index 344d28b..54730c7 100644 --- a/v2/src/lib/adapters/agent-bridge.ts +++ b/v2/src/lib/adapters/agent-bridge.ts @@ -56,7 +56,9 @@ export async function onSidecarMessage( callback: (msg: SidecarMessage) => void, ): Promise { return listen('sidecar-message', (event) => { - callback(event.payload as SidecarMessage); + const payload = event.payload; + if (typeof payload !== 'object' || payload === null) return; + callback(payload as SidecarMessage); }); } diff --git a/v2/src/lib/adapters/sdk-messages.ts b/v2/src/lib/adapters/sdk-messages.ts index e27509e..aa9e7a8 100644 --- a/v2/src/lib/adapters/sdk-messages.ts +++ b/v2/src/lib/adapters/sdk-messages.ts @@ -66,14 +66,24 @@ export interface ErrorContent { message: string; } +/** Runtime guard — returns value if it's a string, fallback otherwise */ +function str(v: unknown, fallback = ''): string { + return typeof v === 'string' ? v : fallback; +} + +/** Runtime guard — returns value if it's a number, fallback otherwise */ +function num(v: unknown, fallback = 0): number { + return typeof v === 'number' ? v : fallback; +} + /** * Adapt a raw SDK stream-json message to our internal format. * When SDK changes wire format, only this function needs updating. */ export function adaptSDKMessage(raw: Record): AgentMessage[] { - const uuid = (raw.uuid as string) ?? crypto.randomUUID(); + const uuid = str(raw.uuid) || crypto.randomUUID(); const timestamp = Date.now(); - const parentId = raw.parent_tool_use_id as string | undefined; + const parentId = typeof raw.parent_tool_use_id === 'string' ? raw.parent_tool_use_id : undefined; switch (raw.type) { case 'system': @@ -99,17 +109,17 @@ function adaptSystemMessage( uuid: string, timestamp: number, ): AgentMessage[] { - const subtype = raw.subtype as string; + const subtype = str(raw.subtype); if (subtype === 'init') { return [{ id: uuid, type: 'init', content: { - sessionId: raw.session_id as string, - model: raw.model as string, - cwd: raw.cwd as string, - tools: (raw.tools as string[]) ?? [], + sessionId: str(raw.session_id), + model: str(raw.model), + cwd: str(raw.cwd), + tools: Array.isArray(raw.tools) ? raw.tools.filter((t): t is string => typeof t === 'string') : [], } satisfies InitContent, timestamp, }]; @@ -120,7 +130,7 @@ function adaptSystemMessage( type: 'status', content: { subtype, - message: raw.status as string | undefined, + message: typeof raw.status === 'string' ? raw.status : undefined, } satisfies StatusContent, timestamp, }]; @@ -133,11 +143,11 @@ function adaptAssistantMessage( parentId?: string, ): AgentMessage[] { const messages: AgentMessage[] = []; - const msg = raw.message as Record | undefined; + const msg = typeof raw.message === 'object' && raw.message !== null ? raw.message as Record : undefined; if (!msg) return messages; - const content = msg.content as Array> | undefined; - if (!Array.isArray(content)) return messages; + const content = Array.isArray(msg.content) ? msg.content as Array> : undefined; + if (!content) return messages; for (const block of content) { switch (block.type) { @@ -146,7 +156,7 @@ function adaptAssistantMessage( id: `${uuid}-text-${messages.length}`, type: 'text', parentId, - content: { text: block.text as string } satisfies TextContent, + content: { text: str(block.text) } satisfies TextContent, timestamp, }); break; @@ -155,7 +165,7 @@ function adaptAssistantMessage( id: `${uuid}-think-${messages.length}`, type: 'thinking', parentId, - content: { text: (block.thinking ?? block.text) as string } satisfies ThinkingContent, + content: { text: str(block.thinking ?? block.text) } satisfies ThinkingContent, timestamp, }); break; @@ -165,8 +175,8 @@ function adaptAssistantMessage( type: 'tool_call', parentId, content: { - toolUseId: block.id as string, - name: block.name as string, + toolUseId: str(block.id), + name: str(block.name), input: block.input, } satisfies ToolCallContent, timestamp, @@ -185,11 +195,11 @@ function adaptUserMessage( parentId?: string, ): AgentMessage[] { const messages: AgentMessage[] = []; - const msg = raw.message as Record | undefined; + const msg = typeof raw.message === 'object' && raw.message !== null ? raw.message as Record : undefined; if (!msg) return messages; - const content = msg.content as Array> | undefined; - if (!Array.isArray(content)) return messages; + const content = Array.isArray(msg.content) ? msg.content as Array> : undefined; + if (!content) return messages; for (const block of content) { if (block.type === 'tool_result') { @@ -198,7 +208,7 @@ function adaptUserMessage( type: 'tool_result', parentId, content: { - toolUseId: block.tool_use_id as string, + toolUseId: str(block.tool_use_id), output: block.content ?? raw.tool_use_result, } satisfies ToolResultContent, timestamp, @@ -214,20 +224,20 @@ function adaptResultMessage( uuid: string, timestamp: number, ): AgentMessage[] { - const usage = raw.usage as Record | undefined; + const usage = typeof raw.usage === 'object' && raw.usage !== null ? raw.usage as Record : undefined; return [{ id: uuid, type: 'cost', content: { - totalCostUsd: (raw.total_cost_usd as number) ?? 0, - durationMs: (raw.duration_ms as number) ?? 0, - inputTokens: usage?.input_tokens ?? 0, - outputTokens: usage?.output_tokens ?? 0, - numTurns: (raw.num_turns as number) ?? 0, - isError: (raw.is_error as boolean) ?? false, - result: raw.result as string | undefined, - errors: raw.errors as string[] | undefined, + totalCostUsd: num(raw.total_cost_usd), + durationMs: num(raw.duration_ms), + inputTokens: num(usage?.input_tokens), + outputTokens: num(usage?.output_tokens), + numTurns: num(raw.num_turns), + isError: raw.is_error === true, + result: typeof raw.result === 'string' ? raw.result : undefined, + errors: Array.isArray(raw.errors) ? raw.errors.filter((e): e is string => typeof e === 'string') : undefined, } satisfies CostContent, timestamp, }]; diff --git a/v2/src/lib/agent-dispatcher.ts b/v2/src/lib/agent-dispatcher.ts index fe78766..261a474 100644 --- a/v2/src/lib/agent-dispatcher.ts +++ b/v2/src/lib/agent-dispatcher.ts @@ -278,10 +278,11 @@ async function persistSessionForProject(sessionId: string): Promise { input_tokens: session.inputTokens, output_tokens: session.outputTokens, last_prompt: session.prompt, - updated_at: Date.now(), + updated_at: Math.floor(Date.now() / 1000), }); - // Save messages + // Save messages (use seconds to match session.rs convention) + const nowSecs = Math.floor(Date.now() / 1000); const records: AgentMessageRecord[] = session.messages.map((m, i) => ({ id: i, session_id: sessionId, @@ -290,7 +291,7 @@ async function persistSessionForProject(sessionId: string): Promise { message_type: m.type, content: JSON.stringify(m.content), parent_id: m.parentId ?? null, - created_at: Date.now(), + created_at: nowSecs, })); if (records.length > 0) {