From 619bad30cb3e17c1b2ac8d120adb74c6992019c5 Mon Sep 17 00:00:00 2001 From: iven Date: Sat, 4 Apr 2026 19:15:50 +0800 Subject: [PATCH] fix(security): Gemini API key header + Mutex safety + Agent validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit M1-01: Move Gemini API key from URL query param to x-goog-api-key header, preventing key leakage in logs/proxy/telemetry (matches Anthropic/OpenAI pattern) M1-03/M1-04: Replace Mutex .unwrap() with .unwrap_or_else(|e| e.into_inner()) in MemoryMiddleware and LoopGuardMiddleware — recovers from poison instead of panicking async runtime M2-08: Add input validation to agent_create — reject empty names, out-of-range temperature (0-2), and zero max_tokens M11-06: Replace Date.now() message ID with crypto.randomUUID() to prevent collisions in classroom chat --- crates/zclaw-runtime/src/driver/gemini.rs | 8 ++++---- crates/zclaw-runtime/src/middleware/loop_guard.rs | 2 +- crates/zclaw-runtime/src/middleware/memory.rs | 2 +- desktop/src-tauri/src/kernel_commands/agent.rs | 12 ++++++++++++ desktop/src/store/classroomStore.ts | 2 +- 5 files changed, 19 insertions(+), 7 deletions(-) diff --git a/crates/zclaw-runtime/src/driver/gemini.rs b/crates/zclaw-runtime/src/driver/gemini.rs index ecb6f03..ec7a2c1 100644 --- a/crates/zclaw-runtime/src/driver/gemini.rs +++ b/crates/zclaw-runtime/src/driver/gemini.rs @@ -68,10 +68,9 @@ impl LlmDriver for GeminiDriver { async fn complete(&self, request: CompletionRequest) -> Result { let api_request = self.build_api_request(&request); let url = format!( - "{}/models/{}:generateContent?key={}", + "{}/models/{}:generateContent", self.base_url, request.model, - self.api_key.expose_secret() ); tracing::debug!(target: "gemini_driver", "Sending request to: {}", url); @@ -79,6 +78,7 @@ impl LlmDriver for GeminiDriver { let response = self.client .post(&url) .header("content-type", "application/json") + .header("x-goog-api-key", self.api_key.expose_secret()) .json(&api_request) .send() .await @@ -105,10 +105,9 @@ impl LlmDriver for GeminiDriver { ) -> Pin> + Send + '_>> { let api_request = self.build_api_request(&request); let url = format!( - "{}/models/{}:streamGenerateContent?alt=sse&key={}", + "{}/models/{}:streamGenerateContent?alt=sse", self.base_url, request.model, - self.api_key.expose_secret() ); tracing::debug!(target: "gemini_driver", "Starting stream request to: {}", url); @@ -117,6 +116,7 @@ impl LlmDriver for GeminiDriver { let response = match self.client .post(&url) .header("content-type", "application/json") + .header("x-goog-api-key", self.api_key.expose_secret()) .timeout(std::time::Duration::from_secs(120)) .json(&api_request) .send() diff --git a/crates/zclaw-runtime/src/middleware/loop_guard.rs b/crates/zclaw-runtime/src/middleware/loop_guard.rs index 5041d7e..3fa2f8b 100644 --- a/crates/zclaw-runtime/src/middleware/loop_guard.rs +++ b/crates/zclaw-runtime/src/middleware/loop_guard.rs @@ -37,7 +37,7 @@ impl AgentMiddleware for LoopGuardMiddleware { tool_name: &str, tool_input: &Value, ) -> Result { - let result = self.guard.lock().unwrap().check(tool_name, tool_input); + let result = self.guard.lock().unwrap_or_else(|e| e.into_inner()).check(tool_name, tool_input); match result { LoopGuardResult::CircuitBreaker => { tracing::warn!("[LoopGuardMiddleware] Circuit breaker triggered by tool '{}'", tool_name); diff --git a/crates/zclaw-runtime/src/middleware/memory.rs b/crates/zclaw-runtime/src/middleware/memory.rs index d4041fd..bd996d1 100644 --- a/crates/zclaw-runtime/src/middleware/memory.rs +++ b/crates/zclaw-runtime/src/middleware/memory.rs @@ -43,7 +43,7 @@ impl MemoryMiddleware { /// Check if enough time has passed since the last extraction for this agent. fn should_extract(&self, agent_id: &str) -> bool { let now = std::time::Instant::now(); - let mut map = self.last_extraction.lock().unwrap(); + let mut map = self.last_extraction.lock().unwrap_or_else(|e| e.into_inner()); if let Some(last) = map.get(agent_id) { if now.duration_since(*last).as_secs() < self.debounce_secs { return false; diff --git a/desktop/src-tauri/src/kernel_commands/agent.rs b/desktop/src-tauri/src/kernel_commands/agent.rs index 9c2e38c..b038e95 100644 --- a/desktop/src-tauri/src/kernel_commands/agent.rs +++ b/desktop/src-tauri/src/kernel_commands/agent.rs @@ -73,6 +73,18 @@ pub async fn agent_create( state: State<'_, KernelState>, request: CreateAgentRequest, ) -> Result { + // Input validation + let name_trimmed = request.name.trim(); + if name_trimmed.is_empty() { + return Err("Agent name cannot be empty".to_string()); + } + if request.temperature < 0.0 || request.temperature > 2.0 { + return Err(format!("Temperature must be between 0 and 2, got {}", request.temperature)); + } + if request.max_tokens == 0 { + return Err("max_tokens must be greater than 0".to_string()); + } + let kernel_lock = state.lock().await; let kernel = kernel_lock.as_ref() diff --git a/desktop/src/store/classroomStore.ts b/desktop/src/store/classroomStore.ts index 31673ab..12c62cc 100644 --- a/desktop/src/store/classroomStore.ts +++ b/desktop/src/store/classroomStore.ts @@ -173,7 +173,7 @@ export const useClassroomStore = create()((set, get) => ({ // Create a local user message for display const userMsg: ClassroomChatMessage = { - id: `user-${Date.now()}`, + id: `user-${crypto.randomUUID()}`, agentId: 'user', agentName: '你', agentAvatar: '👤',