From 1fec8cfbc1ce7ecff77dcddcc32cc61ba98b36c4 Mon Sep 17 00:00:00 2001 From: iven Date: Sat, 4 Apr 2026 21:09:02 +0800 Subject: [PATCH] fix(arch): unify TS/Rust types + classroom persistence registration + approval audit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - M11-03: Register ClassroomPersistence via Tauri .setup() hook with in-memory fallback. Previously missing — classroom commands would crash at runtime. - M3-02: Document BrowserHand as schema validator + TypeScript delegation passthrough (dual-path architecture explicitly documented). - M4-04: Add defense-in-depth audit logging in execute_hand() and execute_hand_with_source() when needs_approval hands bypass approval gate. - TYPE-01: Add #[serde(rename_all = "camelCase")] to Rust AgentInfo. Add missing fields to TS AgentInfo (messageCount, createdAt, updatedAt). Fix KernelStatus TS interface to match Rust KernelStatusResponse (baseUrl/model instead of defaultProvider/defaultModel). - SEC2-P1-01: Document EXTRACTION_DRIVER OnceCell as legacy path; Kernel struct field is the active path. - TriggerSource: Add #[derive(PartialEq)] for approval audit comparisons. --- crates/zclaw-hands/src/hands/browser.rs | 25 +++++---- crates/zclaw-kernel/src/kernel/hands.rs | 52 ++++++++++++++++++- crates/zclaw-types/src/agent.rs | 1 + crates/zclaw-types/src/hand_run.rs | 2 +- .../src/intelligence/extraction_adapter.rs | 23 +++++--- desktop/src-tauri/src/lib.rs | 22 ++++++++ desktop/src/lib/kernel-client.ts | 4 +- desktop/src/lib/kernel-types.ts | 11 ++-- docs/features/AUDIT_TRACKER.md | 12 +++-- 9 files changed, 123 insertions(+), 29 deletions(-) diff --git a/crates/zclaw-hands/src/hands/browser.rs b/crates/zclaw-hands/src/hands/browser.rs index acfe8e4..d748247 100644 --- a/crates/zclaw-hands/src/hands/browser.rs +++ b/crates/zclaw-hands/src/hands/browser.rs @@ -1,14 +1,19 @@ -//! Browser Hand - Web automation capabilities +//! Browser Hand - Web automation capabilities (TypeScript delegation) //! -//! Provides browser automation actions for web interaction: -//! - navigate: Navigate to a URL -//! - click: Click on an element -//! - type: Type text into an input field -//! - scrape: Extract content from the page -//! - screenshot: Take a screenshot -//! - fill_form: Fill out a form -//! - wait: Wait for an element to appear -//! - execute: Execute JavaScript +//! **Architecture note (M3-02):** This Rust Hand is a **schema validator and passthrough**. +//! Every action returns `{"status": "pending_execution"}` — no real browser work happens here. +//! +//! The actual execution path is: +//! 1. Frontend `HandsPanel.tsx` intercepts browser hands → routes to `BrowserHandCard` +//! 2. `BrowserHandCard` calls `browserHandStore.executeTemplate/executeScript` +//! 3. TypeScript calls Tauri `browser_*` commands (Fantoccini-based, defined in `browser/commands.rs`) +//! +//! This dual-path exists because browser automation requires a WebDriver session managed +//! on the TypeScript side (session lifecycle, error recovery, UI feedback). The Rust Hand +//! serves as a typed schema for the action DSL and satisfies the HandRegistry contract. +//! +//! Supported actions: navigate, click, type, scrape, screenshot, fill_form, wait, execute, +//! get_source, get_url, get_title, scroll, back, forward, refresh, hover, press_key, upload, select use async_trait::async_trait; use serde::{Deserialize, Serialize}; diff --git a/crates/zclaw-kernel/src/kernel/hands.rs b/crates/zclaw-kernel/src/kernel/hands.rs index b846f7a..0446c0a 100644 --- a/crates/zclaw-kernel/src/kernel/hands.rs +++ b/crates/zclaw-kernel/src/kernel/hands.rs @@ -1,4 +1,19 @@ //! Hand execution and run tracking +//! +//! # Approval Architecture +//! +//! Hands with `needs_approval: true` go through a two-phase flow: +//! 1. **Entry point** (Tauri command `hand_execute`): checks `needs_approval` flag and +//! `autonomy_level`. If approval is required, creates a `PendingApproval` and returns +//! immediately — the hand is NOT executed yet. +//! 2. **Approval** (Tauri command `hand_approve`): user approves → `respond_to_approval()` +//! spawns `hands.execute()` directly (bypassing this `execute_hand()` method). +//! +//! This method (`execute_hand`) is the **direct execution path** used when approval is +//! NOT required, or when the user has opted into autonomous mode. For defense-in-depth, +//! we log a warning if a `needs_approval` hand reaches this path — it means the approval +//! gate was bypassed (e.g., by the scheduler or trigger manager, which intentionally bypass +//! approval for automated triggers). use std::sync::Arc; use zclaw_types::{Result, HandRun, HandRunId, HandRunStatus, HandRunFilter, TriggerSource}; @@ -17,12 +32,27 @@ impl Kernel { self.hands.list().await } - /// Execute a hand with the given input, tracking the run + /// Execute a hand with the given input, tracking the run. + /// + /// **Note:** For hands with `needs_approval: true`, the Tauri command layer should + /// route through the approval flow instead of calling this method directly. Automated + /// triggers (scheduler, trigger manager) intentionally bypass approval. pub async fn execute_hand( &self, hand_id: &str, input: serde_json::Value, ) -> Result<(HandResult, HandRunId)> { + // Defense-in-depth audit: log if a needs_approval hand reaches the direct path + let configs = self.hands.list().await; + if let Some(config) = configs.iter().find(|c| c.id == hand_id) { + if config.needs_approval { + tracing::warn!( + "[Kernel] Hand '{}' has needs_approval=true but reached direct execution path. \ + Caller should route through approval flow instead.", + hand_id + ); + } + } let run_id = HandRunId::new(); let now = chrono::Utc::now().to_rfc3339(); @@ -119,13 +149,31 @@ impl Kernel { hand_result.map(|res| (res, run_id)) } - /// Execute a hand with a specific trigger source (for scheduled/event triggers) + /// Execute a hand with a specific trigger source (for scheduled/event triggers). + /// + /// Automated trigger sources (Scheduler, Event, System) bypass the approval gate + /// by design — the user explicitly configured these automated triggers. + /// Manual trigger sources should go through the approval flow at the Tauri command layer. pub async fn execute_hand_with_source( &self, hand_id: &str, input: serde_json::Value, trigger_source: TriggerSource, ) -> Result<(HandResult, HandRunId)> { + // Audit: warn if a Manual trigger bypasses approval + if trigger_source == TriggerSource::Manual { + let configs = self.hands.list().await; + if let Some(config) = configs.iter().find(|c| c.id == hand_id) { + if config.needs_approval { + tracing::warn!( + "[Kernel] Hand '{}' (Manual trigger) has needs_approval=true but bypassed approval. \ + This should go through the approval flow.", + hand_id + ); + } + } + } + let run_id = HandRunId::new(); let now = chrono::Utc::now().to_rfc3339(); diff --git a/crates/zclaw-types/src/agent.rs b/crates/zclaw-types/src/agent.rs index 946cae2..3d8d315 100644 --- a/crates/zclaw-types/src/agent.rs +++ b/crates/zclaw-types/src/agent.rs @@ -155,6 +155,7 @@ impl std::fmt::Display for AgentState { /// Agent information for display #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct AgentInfo { pub id: AgentId, pub name: String, diff --git a/crates/zclaw-types/src/hand_run.rs b/crates/zclaw-types/src/hand_run.rs index e5721ed..1a7e7fb 100644 --- a/crates/zclaw-types/src/hand_run.rs +++ b/crates/zclaw-types/src/hand_run.rs @@ -81,7 +81,7 @@ impl std::str::FromStr for HandRunStatus { } /// What triggered the hand execution -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum TriggerSource { /// Manual invocation from user diff --git a/desktop/src-tauri/src/intelligence/extraction_adapter.rs b/desktop/src-tauri/src/intelligence/extraction_adapter.rs index 70ef871..3d13619 100644 --- a/desktop/src-tauri/src/intelligence/extraction_adapter.rs +++ b/desktop/src-tauri/src/intelligence/extraction_adapter.rs @@ -227,28 +227,37 @@ impl LlmDriverForExtraction for TauriExtractionDriver { } } -/// Global extraction driver instance (lazy-initialized). +/// Global extraction driver instance (legacy path, kept for compatibility). +/// +/// **Architecture note:** The Kernel struct now holds its own `extraction_driver` field +/// (set via `kernel.set_extraction_driver()`), which is the primary path used by +/// the middleware chain. This OnceCell global is a legacy artifact — its accessors +/// are dead code. The `configure_extraction_driver()` function is still called during +/// kernel_init for backward compatibility but the primary consumption path is +/// through the Kernel struct. static EXTRACTION_DRIVER: tokio::sync::OnceCell> = tokio::sync::OnceCell::const_new(); -/// Configure the global extraction driver. +/// Configure the global extraction driver (legacy path). /// -/// Call this during kernel initialization after the Kernel's LLM driver is available. +/// Called during kernel initialization. The primary path is via +/// `kernel.set_extraction_driver()` which stores the driver in the Kernel struct +/// for use by the middleware chain. pub fn configure_extraction_driver(driver: Arc, model: String) { let adapter = TauriExtractionDriver::new(driver, model); let _ = EXTRACTION_DRIVER.set(Arc::new(adapter)); - tracing::info!("[ExtractionAdapter] Extraction driver configured"); + tracing::info!("[ExtractionAdapter] Extraction driver configured (legacy OnceCell path)"); } -/// Check if the extraction driver is available. +/// Check if the extraction driver is available (legacy OnceCell path). #[allow(dead_code)] pub fn is_extraction_driver_configured() -> bool { EXTRACTION_DRIVER.get().is_some() } -/// Get the global extraction driver. +/// Get the global extraction driver (legacy OnceCell path). /// -/// Returns `None` if not yet configured via `configure_extraction_driver`. +/// Prefer accessing via `kernel.extraction_driver()` when the Kernel is available. #[allow(dead_code)] pub fn get_extraction_driver() -> Option> { EXTRACTION_DRIVER.get().cloned() diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index fb32ac4..a861c45 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -123,6 +123,28 @@ pub fn run() { .manage(classroom_chat_state) .manage(classroom_gen_tasks) .manage(kernel_commands::mcp::McpManagerState::default()) + .setup(|app| { + // Initialize classroom persistence (async SQLite + data loading). + // Must complete before the event loop starts so that classroom + // commands have a valid persistence layer available. + use tauri::Manager; + let classroom_store = app.state::().inner().clone(); + let chat_store = app.state::().inner().clone(); + let handle = app.handle().clone(); + + let rt = tokio::runtime::Runtime::new() + .expect("Failed to create runtime for classroom persistence init"); + + let persistence = rt.block_on( + classroom_commands::init_persistence(&handle, &classroom_store, &chat_store) + ).unwrap_or_else(|e| { + tracing::error!("[Classroom] Persistence init failed: {}, using in-memory fallback", e); + rt.block_on(classroom_commands::persist::ClassroomPersistence::open_in_memory()) + .expect("In-memory SQLite should never fail") + }); + app.manage(persistence); + Ok(()) + }) .invoke_handler(tauri::generate_handler![ // Internal ZCLAW Kernel commands (preferred) kernel_commands::lifecycle::kernel_init, diff --git a/desktop/src/lib/kernel-client.ts b/desktop/src/lib/kernel-client.ts index 72ac7f4..17c892a 100644 --- a/desktop/src/lib/kernel-client.ts +++ b/desktop/src/lib/kernel-client.ts @@ -275,8 +275,8 @@ export class KernelClient { return { initialized: status.initialized, agentCount: status.agentCount, - defaultProvider: status.defaultProvider, - defaultModel: status.defaultModel, + defaultProvider: status.baseUrl, + defaultModel: status.model, }; } diff --git a/desktop/src/lib/kernel-types.ts b/desktop/src/lib/kernel-types.ts index 366a8d5..f9516ac 100644 --- a/desktop/src/lib/kernel-types.ts +++ b/desktop/src/lib/kernel-types.ts @@ -13,8 +13,8 @@ export interface KernelStatus { initialized: boolean; agentCount: number; databaseUrl: string | null; - defaultProvider: string | null; - defaultModel: string | null; + baseUrl: string | null; + model: string | null; } // === Agent Types === @@ -23,9 +23,12 @@ export interface AgentInfo { id: string; name: string; description?: string; + model: string; + provider: string; state: string; - model?: string; - provider?: string; + messageCount: number; + createdAt: string; + updatedAt: string; } export interface CreateAgentRequest { diff --git a/docs/features/AUDIT_TRACKER.md b/docs/features/AUDIT_TRACKER.md index 6b0f51e..58364f8 100644 --- a/docs/features/AUDIT_TRACKER.md +++ b/docs/features/AUDIT_TRACKER.md @@ -264,7 +264,13 @@ ### Batch 6: 持久化 (`88172aa`) | M11-03 | Classroom 数据内存丢失 → **FIXED** | SQLite persistence (persist.rs) + 自动保存 | +### Batch 7: 架构统一性 (``) +| M11-03 | lib.rs 集成 persistence state 注册 → **FIXED** | Tauri setup hook + in-memory fallback | +| M3-02 | Browser Hand 双路径 → **DOCUMENTED** | Rust BrowserHand 明确标注为 schema validator + passthrough,实际执行走 Tauri browser_* 命令 | +| M4-04 | 自主授权后端无强制 → **AUDITED** | execute_hand/execute_hand_with_source 添加 defense-in-depth 审计日志 | +| TYPE-01 | TS/Rust 类型不一致 → **FIXED** | AgentInfo 添加 camelCase serde + TS 补齐字段;KernelStatus 对齐 baseUrl/model | +| TriggerSource | 缺少 PartialEq derive → **FIXED** | 添加 #[derive(PartialEq)] | +| SEC2-P1-01 | EXTRACTION_DRIVER OnceCell 死代码 → **DOCUMENTED** | 标注为 legacy path,Kernel struct 为 active path | + ### 待后续决策的 P1 -| M3-02 | Browser Hand 双路径 | OPEN | 需架构决策:移除 Rust BrowserHand 或统一路径 | -| M4-04 | 自主授权后端无强制 | OPEN | 需在 Rust middleware 层加授权检查 | -| M11-03 相关 | lib.rs 集成 persistence state 注册 | IN_PROGRESS | 需 Tauri setup hook 完成 | +| M4-04 深层 | 自主授权后端强制阻断 | OPEN | 需在 execute_hand() 硬性阻断 needs_approval hands (当前仅 audit log) |