返回 CodeWhale
approval.rs
根目录 / crates / tui / src / core / engine / approval.rs
1 //! Approval + user-input handshake for the agent loop.
2 //!
3 //! Extracted from `core/engine.rs` (P1.3). The agent loop blocks on these
4 //! two futures whenever a tool requires explicit approval (`await_tool_approval`)
5 //! or whenever a tool requests live user input (`await_user_input`). Channels
6 //! and engine state stay private to the parent module.
7
8 use std::time::Duration;
9
10 use crate::core::events::Event;
11 use crate::tools::spec::ToolError;
12 use crate::tools::user_input::{UserInputRequest, UserInputResponse};
13
14 const USER_INPUT_TIMEOUT: Duration = Duration::from_secs(300);
15
16 use super::Engine;
17
18 #[derive(Debug, Clone)]
19 pub(super) enum ApprovalDecision {
20 Approved {
21 id: String,
22 },
23 Denied {
24 id: String,
25 },
26 /// Retry a tool with an elevated sandbox policy.
27 RetryWithPolicy {
28 id: String,
29 policy: crate::sandbox::SandboxPolicy,
30 },
31 }
32
33 #[derive(Debug, Clone)]
34 pub(super) enum UserInputDecision {
35 Submitted {
36 id: String,
37 response: UserInputResponse,
38 },
39 Cancelled {
40 id: String,
41 },
42 }
43
44 /// Result of awaiting tool approval from the user.
45 #[derive(Debug)]
46 pub(super) enum ApprovalResult {
47 /// User approved the tool execution.
48 Approved,
49 /// User denied the tool execution.
50 Denied,
51 /// User requested retry with an elevated sandbox policy.
52 RetryWithPolicy(crate::sandbox::SandboxPolicy),
53 }
54
55 impl Engine {
56 /// Format a cancellation suffix when the engine knows the cause.
57 /// Some internal cancellation paths still use the raw token while
58 /// #1541 is open; those keep the legacy message without a guessed
59 /// reason.
60 fn cancel_reason_suffix(&self) -> String {
61 let reason = match self.cancel_reason.lock() {
62 Ok(slot) => *slot,
63 Err(poisoned) => *poisoned.into_inner(),
64 };
65 match reason {
66 Some(reason) => format!(" (reason: {})", reason.describe()),
67 None => String::new(),
68 }
69 }
70
71 pub(super) async fn await_tool_approval(
72 &mut self,
73 tool_id: &str,
74 ) -> Result<ApprovalResult, ToolError> {
75 loop {
76 tokio::select! {
77 _ = self.cancel_token.cancelled() => {
78 let suffix = self.cancel_reason_suffix();
79 return Err(ToolError::cancelled(
80 format!("Request cancelled while awaiting approval{suffix}"),
81 ));
82 }
83 decision = self.rx_approval.recv() => {
84 let Some(decision) = decision else {
85 return Err(ToolError::execution_failed(
86 "Approval channel closed — engine is shutting down. \
87 The approval modal can no longer reach the engine; \
88 this is typically a teardown race, not a user action."
89 .to_string(),
90 ));
91 };
92 match decision {
93 ApprovalDecision::Approved { id } if id == tool_id => {
94 return Ok(ApprovalResult::Approved);
95 }
96 ApprovalDecision::Denied { id } if id == tool_id => {
97 return Ok(ApprovalResult::Denied);
98 }
99 ApprovalDecision::RetryWithPolicy { id, policy } if id == tool_id => {
100 return Ok(ApprovalResult::RetryWithPolicy(policy));
101 }
102 _ => continue,
103 }
104 }
105 }
106 }
107 }
108
109 pub(super) async fn await_user_input(
110 &mut self,
111 tool_id: &str,
112 request: UserInputRequest,
113 ) -> Result<UserInputResponse, ToolError> {
114 let _ = self
115 .tx_event
116 .send(Event::UserInputRequired {
117 id: tool_id.to_string(),
118 request,
119 })
120 .await;
121
122 loop {
123 tokio::select! {
124 _ = self.cancel_token.cancelled() => {
125 let suffix = self.cancel_reason_suffix();
126 return Err(ToolError::cancelled(
127 format!("Request cancelled while awaiting user input{suffix}"),
128 ));
129 }
130 result = tokio::time::timeout(USER_INPUT_TIMEOUT, self.rx_user_input.recv()) => {
131 match result {
132 Ok(Some(decision)) => {
133 match decision {
134 UserInputDecision::Submitted { id, response } if id == tool_id => {
135 return Ok(response);
136 }
137 UserInputDecision::Cancelled { id } if id == tool_id => {
138 return Err(ToolError::cancelled(
139 "User input cancelled".to_string(),
140 ));
141 }
142 _ => continue,
143 }
144 }
145 Ok(None) => {
146 return Err(ToolError::execution_failed(
147 "User input channel closed".to_string(),
148 ));
149 }
150 Err(_) => {
151 let _ = self
152 .tx_event
153 .send(Event::Status {
154 message: format!(
155 "User input timed out after {}s",
156 USER_INPUT_TIMEOUT.as_secs()
157 ),
158 })
159 .await;
160 return Err(ToolError::Timeout {
161 seconds: USER_INPUT_TIMEOUT.as_secs(),
162 });
163 }
164 }
165 }
166 }
167 }
168 }
169 }
170
170 lines RUST