zagens-cli 0.8.3

Zagens headless CLI + HTTP/SSE runtime sidecar (`zagens`, `zagens-runtime` binaries)
Documentation
//! Approval + user-input handshake — TUI shell over `zagens-core::engine::approval`.

use std::time::Duration;

use crate::core::events::Event;
use crate::tools::approval_cache::{ApprovalCacheStatus, ApprovalKey};
use crate::tools::user_input::{UserInputRequest, UserInputResponse};

use zagens_core::engine::approval::{
    ApprovalDecision as CoreApprovalDecision, ApprovalResult as CoreApprovalResult,
    UserInputDecision as CoreUserInputDecision, recv_user_input_for_tool,
};

use super::Engine;

pub(super) type ApprovalDecision = CoreApprovalDecision<crate::sandbox::SandboxPolicy>;
pub(super) type ApprovalResult = CoreApprovalResult<crate::sandbox::SandboxPolicy>;
pub(super) type UserInputDecision = CoreUserInputDecision<UserInputResponse>;

const DEFAULT_APPROVAL_TIMEOUT_SECS: u64 = 120;

fn approval_wait_timeout() -> Duration {
    Duration::from_secs(
        std::env::var("DEEPSEEK_RUNTIME_APPROVAL_TIMEOUT_SECS")
            .ok()
            .and_then(|v| v.parse::<u64>().ok())
            .unwrap_or(DEFAULT_APPROVAL_TIMEOUT_SECS)
            .max(1),
    )
}

impl Engine {
    pub(super) async fn await_tool_approval(
        &mut self,
        tool_id: &str,
    ) -> Result<ApprovalResult, crate::tools::spec::ToolError> {
        let deadline = tokio::time::Instant::now() + approval_wait_timeout();
        loop {
            tokio::select! {
                _ = self.0.cancel_token.cancelled() => {
                    return Err(crate::tools::spec::ToolError::execution_failed(
                        "Request cancelled while awaiting approval".to_string(),
                    ));
                }
                _ = tokio::time::sleep_until(deadline) => {
                    return Err(crate::tools::spec::ToolError::execution_failed(format!(
                        "Timed out waiting for approval on tool `{tool_id}`"
                    )));
                }
                decision = self.0.rx_approval.recv() => {
                    let Some(decision) = decision else {
                        return Err(crate::tools::spec::ToolError::execution_failed(
                            "Approval channel closed".to_string(),
                        ));
                    };
                    match decision {
                        ApprovalDecision::Approved {
                            id,
                            cache_key,
                            remember_for_session,
                        } if id == tool_id => {
                            if remember_for_session
                                && let Some(ref key) = cache_key {
                                    self.runtime_ext_mut()
                                        .approval_cache
                                        .insert(ApprovalKey(key.clone()), true);
                                }
                            return Ok(ApprovalResult::Approved {
                                cache_key,
                                remember_for_session,
                            });
                        }
                        ApprovalDecision::Approved { id, .. } => {
                            tracing::warn!(
                                target: "approval",
                                expected = %tool_id,
                                received = %id,
                                "discarded approval decision for unexpected tool id"
                            );
                        }
                        ApprovalDecision::Denied { id } if id == tool_id => {
                            return Ok(ApprovalResult::Denied);
                        }
                        ApprovalDecision::Denied { id } => {
                            tracing::warn!(
                                target: "approval",
                                expected = %tool_id,
                                received = %id,
                                "discarded denial decision for unexpected tool id"
                            );
                        }
                        ApprovalDecision::RetryWithPolicy { id, policy } if id == tool_id => {
                            return Ok(ApprovalResult::RetryWithPolicy(policy));
                        }
                        ApprovalDecision::RetryWithPolicy { id, .. } => {
                            tracing::warn!(
                                target: "approval",
                                expected = %tool_id,
                                received = %id,
                                "discarded retry-with-policy decision for unexpected tool id"
                            );
                        }
                    }
                }
            }
        }
    }

    pub(super) fn approval_cache_hit(
        &self,
        tool_name: &str,
        tool_input: &serde_json::Value,
    ) -> bool {
        let key = crate::tools::approval_cache::build_approval_key(tool_name, tool_input);
        matches!(
            self.runtime_ext().approval_cache.check(&key),
            ApprovalCacheStatus::Approved
        )
    }

    pub(super) async fn await_user_input(
        &mut self,
        tool_id: &str,
        request: UserInputRequest,
    ) -> Result<UserInputResponse, crate::tools::spec::ToolError> {
        let _ = self
            .tx_event
            .send(Event::UserInputRequired {
                id: tool_id.to_string(),
                request,
            })
            .await;

        recv_user_input_for_tool(tool_id, &self.0.cancel_token, &mut self.0.rx_user_input).await
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn approval_wait_timeout_default_is_positive() {
        assert!(approval_wait_timeout() >= Duration::from_secs(1));
    }
}