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

- 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:
iven
2026-04-04 21:09:02 +08:00
parent 8e56df74ec
commit 1fec8cfbc1
9 changed files with 123 additions and 29 deletions

View File

@@ -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};

View File

@@ -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();

View File

@@ -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,

View File

@@ -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