Files
zclaw_openfang/crates/zclaw-kernel/src/kernel/approvals.rs
iven e10549a1b9 fix: 发布前审计 Batch 2 — Debug遮蔽 + unwrap + 静默吞错 + MCP锁 + 索引 + Config验证
安全:
- LlmConfig 自定义 Debug impl,api_key 显示为 "***REDACTED***"
- tsconfig.json 移除 ErrorBoundary.tsx 排除项(安全关键组件)
- billing/handlers.rs Response builder unwrap → map_err 错误传播
- classroom_commands/mod.rs db_path.parent().unwrap() → ok_or_else

静默吞错:
- approvals.rs 3处 warn→error(审批状态丢失是严重事件)
- events.rs publish() 添加 Event dropped debug 日志
- mcp_transport.rs eprintln→tracing::warn (僵尸进程风险)
- zclaw-growth sqlite.rs 4处迁移:区分 duplicate column name 与真实错误

MCP Transport:
- 合并 stdin+stdout 为单一 Mutex<TransportHandles>
- send_request write-then-read 原子化,防止并发响应错配

数据库:
- 新迁移 20260418000001: idx_rle_created_at + idx_billing_sub_plan + idx_ki_created_by

配置验证:
- SaaSConfig::load() 添加 jwt_expiration_hours>=1, max_connections>0, min<=max
2026-04-18 14:09:36 +08:00

156 lines
6.3 KiB
Rust

//! Approval management
use std::sync::Arc;
use serde_json::Value;
use zclaw_types::{Result, HandRun, HandRunId, HandRunStatus, TriggerSource};
use zclaw_hands::HandContext;
use super::Kernel;
impl Kernel {
// ============================================================
// Approval Management
// ============================================================
/// List pending approvals
pub async fn list_approvals(&self) -> Vec<super::ApprovalEntry> {
let approvals = self.pending_approvals.lock().await;
approvals.iter().filter(|a| a.status == "pending").cloned().collect()
}
/// Get a single approval by ID (any status, not just pending)
///
/// Returns None if no approval with the given ID exists.
pub async fn get_approval(&self, id: &str) -> Option<super::ApprovalEntry> {
let approvals = self.pending_approvals.lock().await;
approvals.iter().find(|a| a.id == id).cloned()
}
/// Create a pending approval (called when a needs_approval hand is triggered)
pub async fn create_approval(&self, hand_id: String, input: serde_json::Value) -> super::ApprovalEntry {
let entry = super::ApprovalEntry {
id: uuid::Uuid::new_v4().to_string(),
hand_id,
status: "pending".to_string(),
created_at: chrono::Utc::now(),
input,
reject_reason: None,
};
let mut approvals = self.pending_approvals.lock().await;
approvals.push(entry.clone());
entry
}
/// Respond to an approval
pub async fn respond_to_approval(
&self,
id: &str,
approved: bool,
reason: Option<String>,
) -> Result<()> {
let mut approvals = self.pending_approvals.lock().await;
let entry = approvals.iter_mut().find(|a| a.id == id && a.status == "pending")
.ok_or_else(|| zclaw_types::ZclawError::NotFound(format!("Approval not found: {}", id)))?;
entry.status = if approved { "approved".to_string() } else { "rejected".to_string() };
if let Some(r) = reason {
entry.reject_reason = Some(r);
}
if approved {
let hand_id = entry.hand_id.clone();
let input = entry.input.clone();
drop(approvals); // Release lock before async hand execution
// Execute the hand in background with HandRun tracking
let hands = self.hands.clone();
let approvals = self.pending_approvals.clone();
let memory = self.memory.clone();
let running_hand_runs = self.running_hand_runs.clone();
let id_owned = id.to_string();
tokio::spawn(async move {
// Create HandRun record for tracking
let run_id = HandRunId::new();
let now = chrono::Utc::now().to_rfc3339();
let mut run = HandRun {
id: run_id,
hand_name: hand_id.clone(),
trigger_source: TriggerSource::Manual,
params: input.clone(),
status: HandRunStatus::Pending,
result: None,
error: None,
duration_ms: None,
created_at: now.clone(),
started_at: None,
completed_at: None,
};
let _ = memory.save_hand_run(&run).await.map_err(|e| {
tracing::error!("[Approval] Failed to save hand run: {}", e);
});
run.status = HandRunStatus::Running;
run.started_at = Some(chrono::Utc::now().to_rfc3339());
let _ = memory.update_hand_run(&run).await.map_err(|e| {
tracing::error!("[Approval] Failed to update hand run (running): {}", e);
});
// Register cancellation flag
let cancel_flag = Arc::new(std::sync::atomic::AtomicBool::new(false));
running_hand_runs.insert(run.id, cancel_flag.clone());
let context = HandContext::default();
let start = std::time::Instant::now();
let result = hands.execute(&hand_id, &context, input).await;
let duration = start.elapsed();
// Remove from running map
running_hand_runs.remove(&run.id);
// Update HandRun with result
let completed_at = chrono::Utc::now().to_rfc3339();
match &result {
Ok(res) => {
run.status = HandRunStatus::Completed;
run.result = Some(res.output.clone());
run.error = res.error.clone();
}
Err(e) => {
run.status = HandRunStatus::Failed;
run.error = Some(e.to_string());
}
}
run.duration_ms = Some(duration.as_millis() as u64);
run.completed_at = Some(completed_at);
let _ = memory.update_hand_run(&run).await.map_err(|e| {
tracing::error!("[Approval] Failed to update hand run (completed): {}", e);
});
// Update approval status based on execution result
let mut approvals = approvals.lock().await;
if let Some(entry) = approvals.iter_mut().find(|a| a.id == id_owned) {
match result {
Ok(_) => entry.status = "completed".to_string(),
Err(e) => {
entry.status = "failed".to_string();
if let Some(obj) = entry.input.as_object_mut() {
obj.insert("error".to_string(), Value::String(format!("{}", e)));
}
}
}
}
});
}
Ok(())
}
/// Cancel a pending approval
pub async fn cancel_approval(&self, id: &str) -> Result<()> {
let mut approvals = self.pending_approvals.lock().await;
let entry = approvals.iter_mut().find(|a| a.id == id && a.status == "pending")
.ok_or_else(|| zclaw_types::ZclawError::NotFound(format!("Approval not found: {}", id)))?;
entry.status = "cancelled".to_string();
Ok(())
}
}