fix(arch): unify TS/Rust types + classroom persistence registration + approval audit
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
- 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.
This commit is contained in:
@@ -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};
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<Arc<TauriExtractionDriver>> =
|
||||
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<dyn LlmDriver>, 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<Arc<TauriExtractionDriver>> {
|
||||
EXTRACTION_DRIVER.get().cloned()
|
||||
|
||||
@@ -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::<classroom_commands::ClassroomStore>().inner().clone();
|
||||
let chat_store = app.state::<classroom_commands::ChatStore>().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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -264,7 +264,13 @@
|
||||
### Batch 6: 持久化 (`88172aa`)
|
||||
| M11-03 | Classroom 数据内存丢失 → **FIXED** | SQLite persistence (persist.rs) + 自动保存 |
|
||||
|
||||
### Batch 7: 架构统一性 (`<pending>`)
|
||||
| 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) |
|
||||
|
||||
Reference in New Issue
Block a user