1use kaynine_core::error::KaynineError;
10use kaynine_core::event::{EventEnvelope, RealtimeEvent};
11use kaynine_core::ids::{BranchId, RunId, SessionId, ToolCallId};
12use kaynine_core::policy::{ApprovalDecision, ApprovalHandler, ApprovalRequest};
13use kaynine_core::store::{AppendOutcome, AuthoritativeEvent, LeaseOwner, SessionStore};
14use std::collections::HashMap;
15use std::sync::{Arc, Mutex};
16use std::time::Duration;
17use tokio::sync::{mpsc, oneshot, Mutex as AsyncMutex};
18use tokio_util::sync::CancellationToken;
19
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22pub enum ResolutionOutcome {
23 Delivered,
24 Expired,
25 NotFound,
26}
27
28impl From<ResolutionOutcome> for crate::service::ApprovalOutcome {
29 fn from(outcome: ResolutionOutcome) -> Self {
30 match outcome {
31 ResolutionOutcome::Delivered => crate::service::ApprovalOutcome::Delivered,
32 ResolutionOutcome::Expired => crate::service::ApprovalOutcome::Expired,
33 ResolutionOutcome::NotFound => crate::service::ApprovalOutcome::NotFound,
34 }
35 }
36}
37
38fn host_unix_now() -> i64 {
39 std::time::SystemTime::now()
40 .duration_since(std::time::UNIX_EPOCH)
41 .map(|d| d.as_secs() as i64)
42 .unwrap_or(0)
43}
44
45#[derive(Default)]
48pub(crate) struct ApprovalShared {
49 pub(crate) pending: Mutex<HashMap<ToolCallId, oneshot::Sender<bool>>>,
51 pub(crate) expired: Mutex<HashMap<ToolCallId, i64>>,
54}
55
56pub struct InteractiveApprovalHandler {
57 store: Arc<dyn SessionStore>,
58 session_id: SessionId,
59 branch_id: BranchId,
60 run_id: RunId,
61 revision: Arc<AsyncMutex<u64>>,
62 owner: LeaseOwner,
63 events: mpsc::Sender<EventEnvelope<RealtimeEvent>>,
64 shared: Arc<ApprovalShared>,
65 timeout: Duration,
66}
67
68impl InteractiveApprovalHandler {
69 #[allow(clippy::too_many_arguments)]
70 pub(crate) fn new(
71 store: Arc<dyn SessionStore>,
72 session_id: SessionId,
73 branch_id: BranchId,
74 run_id: RunId,
75 revision: Arc<AsyncMutex<u64>>,
76 owner: LeaseOwner,
77 events: mpsc::Sender<EventEnvelope<RealtimeEvent>>,
78 shared: Arc<ApprovalShared>,
79 timeout: Duration,
80 ) -> Self {
81 Self {
82 store,
83 session_id,
84 branch_id,
85 run_id,
86 revision,
87 owner,
88 events,
89 shared,
90 timeout,
91 }
92 }
93
94 pub(crate) fn resolve(
97 shared: &ApprovalShared,
98 call_id: &ToolCallId,
99 approved: bool,
100 ) -> ResolutionOutcome {
101 let sender = shared
102 .pending
103 .lock()
104 .expect("pending mutex poisoned")
105 .remove(call_id);
106 match sender {
107 Some(tx) => match tx.send(approved) {
108 Ok(()) => ResolutionOutcome::Delivered,
109 Err(_) => {
111 if shared
112 .expired
113 .lock()
114 .expect("expired mutex poisoned")
115 .contains_key(call_id)
116 {
117 ResolutionOutcome::Expired
118 } else {
119 ResolutionOutcome::NotFound
120 }
121 }
122 },
123 None => {
124 if shared
125 .expired
126 .lock()
127 .expect("expired mutex poisoned")
128 .contains_key(call_id)
129 {
130 ResolutionOutcome::Expired
131 } else {
132 ResolutionOutcome::NotFound
133 }
134 }
135 }
136 }
137
138 async fn append(&self, events: Vec<AuthoritativeEvent>) -> Result<u64, KaynineError> {
146 for _ in 0..8 {
147 let expected = *self.revision.lock().await;
148 match self
149 .store
150 .append_events(
151 &self.session_id,
152 expected,
153 &self.owner,
154 events.clone(),
155 Vec::new(),
156 )
157 .await
158 {
159 Ok(AppendOutcome::Appended { new_revision }) => {
160 *self.revision.lock().await = new_revision;
161 return Ok(new_revision);
162 }
163 Ok(AppendOutcome::RevisionConflict { current_revision }) => {
164 *self.revision.lock().await = current_revision;
165 tokio::time::sleep(Duration::from_millis(10)).await;
166 continue;
167 }
168 Ok(AppendOutcome::NotLeaseHolder | AppendOutcome::LeaseExpired) => {
169 return Err(KaynineError::Store(
170 kaynine_core::error::StoreError::Internal(
171 "lease lost during approval append".into(),
172 ),
173 ));
174 }
175 Err(error) => return Err(error),
176 }
177 }
178 Err(KaynineError::Store(
180 kaynine_core::error::StoreError::Internal(
181 "approval append failed after 8 revision-conflict retries".into(),
182 ),
183 ))
184 }
185
186 async fn broadcast(&self, payload: RealtimeEvent) {
187 let _ = self
189 .events
190 .send(EventEnvelope {
191 session_id: self.session_id.clone(),
192 branch_id: self.branch_id.clone(),
193 run_id: Some(self.run_id.clone()),
194 revision: 0,
195 run_seq: None,
196 payload,
197 })
198 .await;
199 }
200
201 fn cleanup(&self, call_id: &ToolCallId, expired_at: Option<i64>) {
202 if let Some(deadline) = expired_at {
203 self.shared
204 .expired
205 .lock()
206 .expect("expired mutex poisoned")
207 .insert(call_id.clone(), deadline);
208 }
209 self.shared
210 .pending
211 .lock()
212 .expect("pending mutex poisoned")
213 .remove(call_id);
214 }
215}
216
217#[async_trait::async_trait]
218impl ApprovalHandler for InteractiveApprovalHandler {
219 async fn wait(&self, request: ApprovalRequest, cancel: CancellationToken) -> ApprovalDecision {
220 let deadline_unix = host_unix_now() + self.timeout.as_secs() as i64;
221
222 if self
224 .append(vec![AuthoritativeEvent::ApprovalRequested {
225 call_id: request.call_id.clone(),
226 deadline_unix,
227 }])
228 .await
229 .is_err()
230 {
231 return ApprovalDecision::Denied {
232 reason: "审批持久化失败".into(),
233 };
234 }
235
236 self.broadcast(RealtimeEvent::ApprovalPending {
238 call_id: request.call_id.clone(),
239 deadline_unix,
240 })
241 .await;
242
243 let (tx, rx) = oneshot::channel();
244 self.shared
245 .pending
246 .lock()
247 .expect("pending mutex poisoned")
248 .insert(request.call_id.clone(), tx);
249 let mut rx = rx;
250
251 let outcome = tokio::select! {
253 biased;
254 _ = cancel.cancelled() => ApprovalDecision::Denied {
255 reason: "工具在执行前被取消".into(),
256 },
257 _ = tokio::time::sleep(self.timeout) => ApprovalDecision::Denied {
258 reason: "审批超时".into(),
259 },
260 res = &mut rx => match res {
261 Ok(true) => ApprovalDecision::Approved,
262 Ok(false) => ApprovalDecision::Denied {
263 reason: "审批被拒绝".into(),
264 },
265 Err(_) => ApprovalDecision::Denied {
266 reason: "审批通道关闭".into(),
267 },
268 },
269 };
270
271 let approved = outcome == ApprovalDecision::Approved;
272 let timed_out =
273 matches!(&outcome, ApprovalDecision::Denied { reason } if reason == "审批超时");
274
275 if self
277 .append(vec![AuthoritativeEvent::ApprovalResolved {
278 call_id: request.call_id.clone(),
279 approved,
280 }])
281 .await
282 .is_err()
283 {
284 tracing::error!(call_id = %request.call_id, "approval resolved append failed");
285 }
286 self.broadcast(RealtimeEvent::ApprovalResolved {
287 call_id: request.call_id.clone(),
288 approved,
289 })
290 .await;
291 self.cleanup(&request.call_id, timed_out.then_some(deadline_unix));
292
293 outcome
294 }
295}