refactor(desktop): split kernel_commands/pipeline_commands into modules, add SaaS client libs and gateway modules
Split monolithic kernel_commands.rs (2185 lines) and pipeline_commands.rs (1391 lines) into focused sub-modules under kernel_commands/ and pipeline_commands/ directories. Add gateway module (commands, config, io, runtime), health_check, and 15 new TypeScript client libraries for SaaS relay, auth, admin, telemetry, and kernel sub-systems (a2a, agent, chat, hands, skills, triggers). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
140
desktop/src-tauri/src/kernel_commands/approval.rs
Normal file
140
desktop/src-tauri/src/kernel_commands/approval.rs
Normal file
@@ -0,0 +1,140 @@
|
||||
//! Approval commands: list and respond
|
||||
//!
|
||||
//! When approved, kernel's `respond_to_approval` internally spawns the Hand execution
|
||||
//! and emits `hand-execution-complete` events to the frontend.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json;
|
||||
use tauri::{AppHandle, Emitter, State};
|
||||
|
||||
use super::KernelState;
|
||||
|
||||
// ============================================================
|
||||
// Approval Commands
|
||||
// ============================================================
|
||||
|
||||
/// Approval response
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ApprovalResponse {
|
||||
pub id: String,
|
||||
pub hand_id: String,
|
||||
pub status: String,
|
||||
pub created_at: String,
|
||||
pub input: serde_json::Value,
|
||||
}
|
||||
|
||||
/// List pending approvals
|
||||
#[tauri::command]
|
||||
pub async fn approval_list(
|
||||
state: State<'_, KernelState>,
|
||||
) -> Result<Vec<ApprovalResponse>, String> {
|
||||
let kernel_lock = state.lock().await;
|
||||
let kernel = kernel_lock.as_ref()
|
||||
.ok_or_else(|| "Kernel not initialized".to_string())?;
|
||||
|
||||
let approvals = kernel.list_approvals().await;
|
||||
Ok(approvals.into_iter().map(|a| ApprovalResponse {
|
||||
id: a.id,
|
||||
hand_id: a.hand_id,
|
||||
status: a.status,
|
||||
created_at: a.created_at.to_rfc3339(),
|
||||
input: a.input,
|
||||
}).collect())
|
||||
}
|
||||
|
||||
/// Respond to an approval
|
||||
///
|
||||
/// When approved, the kernel's `respond_to_approval` internally spawns the Hand
|
||||
/// execution. We additionally emit Tauri events so the frontend can track when
|
||||
/// the execution finishes, since the kernel layer has no access to the AppHandle.
|
||||
#[tauri::command]
|
||||
pub async fn approval_respond(
|
||||
app: AppHandle,
|
||||
state: State<'_, KernelState>,
|
||||
id: String,
|
||||
approved: bool,
|
||||
reason: Option<String>,
|
||||
) -> Result<(), String> {
|
||||
// Capture hand info before calling respond_to_approval (which mutates the approval)
|
||||
let hand_id = {
|
||||
let kernel_lock = state.lock().await;
|
||||
let kernel = kernel_lock.as_ref()
|
||||
.ok_or_else(|| "Kernel not initialized".to_string())?;
|
||||
|
||||
let approvals = kernel.list_approvals().await;
|
||||
let entry = approvals.iter().find(|a| a.id == id && a.status == "pending")
|
||||
.ok_or_else(|| format!("Approval not found or already resolved: {}", id))?;
|
||||
entry.hand_id.clone()
|
||||
};
|
||||
|
||||
// Call kernel respond_to_approval (this updates status and spawns Hand execution)
|
||||
{
|
||||
let kernel_lock = state.lock().await;
|
||||
let kernel = kernel_lock.as_ref()
|
||||
.ok_or_else(|| "Kernel not initialized".to_string())?;
|
||||
|
||||
kernel.respond_to_approval(&id, approved, reason).await
|
||||
.map_err(|e| format!("Failed to respond to approval: {}", e))?;
|
||||
}
|
||||
|
||||
// When approved, monitor the Hand execution and emit events to the frontend.
|
||||
// The kernel's respond_to_approval changes status to "approved" immediately,
|
||||
// then the spawned task sets it to "completed" or "failed" when done.
|
||||
if approved {
|
||||
let approval_id = id.clone();
|
||||
let kernel_state: KernelState = (*state).clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let timeout = tokio::time::Duration::from_secs(300);
|
||||
let poll_interval = tokio::time::Duration::from_millis(500);
|
||||
|
||||
let result = tokio::time::timeout(timeout, async {
|
||||
loop {
|
||||
tokio::time::sleep(poll_interval).await;
|
||||
|
||||
let kernel_lock = kernel_state.lock().await;
|
||||
if let Some(kernel) = kernel_lock.as_ref() {
|
||||
// Use get_approval to check any status (not just "pending")
|
||||
if let Some(entry) = kernel.get_approval(&approval_id).await {
|
||||
match entry.status.as_str() {
|
||||
"completed" => {
|
||||
tracing::info!("[approval_respond] Hand '{}' completed for approval {}", hand_id, approval_id);
|
||||
return (true, None::<String>);
|
||||
}
|
||||
"failed" => {
|
||||
let error_msg = entry.input.get("error")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("Unknown error")
|
||||
.to_string();
|
||||
tracing::warn!("[approval_respond] Hand '{}' failed for approval {}: {}", hand_id, approval_id, error_msg);
|
||||
return (false, Some(error_msg));
|
||||
}
|
||||
_ => {} // "approved" = still running
|
||||
}
|
||||
} else {
|
||||
// Entry disappeared entirely — kernel was likely restarted
|
||||
return (false, Some("Approval entry disappeared".to_string()));
|
||||
}
|
||||
} else {
|
||||
return (false, Some("Kernel not available".to_string()));
|
||||
}
|
||||
}
|
||||
}).await;
|
||||
|
||||
let (success, error) = match result {
|
||||
Ok((s, e)) => (s, e),
|
||||
Err(_) => (false, Some("Hand execution timed out (5 minutes)".to_string())),
|
||||
};
|
||||
|
||||
let _ = app.emit("hand-execution-complete", serde_json::json!({
|
||||
"approvalId": approval_id,
|
||||
"handId": hand_id,
|
||||
"success": success,
|
||||
"error": error,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user