1use std::{
6 collections::{HashMap, VecDeque},
7 path::PathBuf,
8 sync::{Arc, Mutex},
9 time::Duration,
10};
11
12use subc_protocol::{
13 error_codes,
14 session::{OperatorConfirmReply, OperatorConfirmRequest},
15 ErrorBody, Flags, FrameType, Principal, Priority,
16};
17use tokio::{
18 sync::{oneshot, Notify},
19 time::{timeout_at, Instant},
20};
21
22use crate::{
23 caller_identity::CallerIdentity,
24 forwarding::{ModuleEndpointId, ModuleRouteKey, RouteBinding},
25 router::RouteCtx,
26 ConnectionId, Frame,
27};
28
29const DECLINE_LIMIT: usize = 3;
32const DECLINE_WINDOW: Duration = Duration::from_secs(10 * 60);
33const DECLINE_COOLDOWN: Duration = Duration::from_secs(10 * 60);
36
37pub(crate) type CooldownClock = Arc<dyn Fn() -> Instant + Send + Sync>;
40
41#[derive(Clone, Debug, PartialEq, Eq, Hash)]
46pub(crate) enum DeclineKey {
47 Executable(PathBuf),
48 Connection(ConnectionId),
49}
50
51impl DeclineKey {
52 pub(crate) fn for_caller(caller: &CallerIdentity, connection: ConnectionId) -> Self {
53 match caller.executable() {
54 Some(executable) => Self::Executable(executable.to_path_buf()),
55 None => Self::Connection(connection),
56 }
57 }
58}
59
60impl std::fmt::Display for DeclineKey {
61 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62 match self {
63 Self::Executable(path) => write!(f, "{}", path.display()),
64 Self::Connection(connection) => write!(
65 f,
66 "an unidentified local process on connection {}",
67 connection.get()
68 ),
69 }
70 }
71}
72
73pub trait OperatorProvider: Send + Sync + 'static {
77 fn prompt(
78 &self,
79 text: &str,
80 publish: Box<dyn FnOnce(Arc<dyn OperatorWithdraw>) + Send>,
81 ) -> ProviderResult;
82}
83
84pub trait OperatorWithdraw: Send + Sync + 'static {
86 fn withdraw(&self, reason: &str);
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub enum ProviderResult {
91 Approved,
92 Declined,
93 Unavailable,
94 UnsupportedPlatform,
95 NoPresence,
96}
97
98#[derive(Debug, Clone, Copy)]
99pub struct OperatorLimits {
100 pub queue_wait: Duration,
101 pub prompt_timeout: Duration,
102 pub stuck_grace: Duration,
103}
104
105impl Default for OperatorLimits {
106 fn default() -> Self {
107 Self {
108 queue_wait: Duration::from_secs(150),
109 prompt_timeout: Duration::from_secs(120),
110 stuck_grace: Duration::from_secs(10),
111 }
112 }
113}
114
115impl OperatorLimits {
116 fn delivery_budget(self) -> Duration {
117 self.queue_wait + self.prompt_timeout + self.stuck_grace + Duration::from_secs(10)
118 }
119}
120
121struct OsProvider;
122
123#[cfg(any(target_os = "macos", windows))]
124struct OsWithdraw(Arc<dyn subc_presence::Withdraw>);
125#[cfg(any(target_os = "macos", windows))]
126impl OperatorWithdraw for OsWithdraw {
127 fn withdraw(&self, _: &str) {
128 self.0.withdraw();
129 }
130}
131
132impl OperatorProvider for OsProvider {
133 fn prompt(
134 &self,
135 text: &str,
136 publish: Box<dyn FnOnce(Arc<dyn OperatorWithdraw>) + Send>,
137 ) -> ProviderResult {
138 #[cfg(target_os = "linux")]
142 {
143 let _ = (text, publish);
144 ProviderResult::UnsupportedPlatform
145 }
146 #[cfg(any(target_os = "macos", windows))]
147 {
148 os_outcome(subc_presence::prompt(
149 text,
150 Box::new(move |handle| publish(Arc::new(OsWithdraw(handle)))),
151 ))
152 }
153 #[cfg(not(any(target_os = "macos", windows, target_os = "linux")))]
154 {
155 let _ = (text, publish);
156 ProviderResult::Unavailable
157 }
158 }
159}
160
161#[cfg(any(target_os = "macos", windows))]
166fn os_outcome(outcome: subc_presence::Outcome) -> ProviderResult {
167 match outcome {
168 subc_presence::Outcome::Approved => ProviderResult::Approved,
169 subc_presence::Outcome::Declined => ProviderResult::Declined,
170 subc_presence::Outcome::ProviderError => ProviderResult::Unavailable,
171 subc_presence::Outcome::NoPresence => ProviderResult::NoPresence,
172 subc_presence::Outcome::UnsupportedPlatform => ProviderResult::UnsupportedPlatform,
173 }
174}
175
176#[cfg(all(test, any(target_os = "macos", windows)))]
177mod os_outcome_tests {
178 use super::*;
179
180 #[test]
181 fn only_an_os_approval_becomes_approved() {
182 use subc_presence::Outcome as Os;
183 assert_eq!(os_outcome(Os::Approved), ProviderResult::Approved);
184 assert_eq!(os_outcome(Os::Declined), ProviderResult::Declined);
185 assert_eq!(os_outcome(Os::ProviderError), ProviderResult::Unavailable);
186 assert_eq!(os_outcome(Os::NoPresence), ProviderResult::NoPresence);
187 assert_eq!(
188 os_outcome(Os::UnsupportedPlatform),
189 ProviderResult::UnsupportedPlatform
190 );
191 }
192}
193
194#[derive(Clone, Copy, Debug)]
195pub(crate) struct Outcome {
196 pub code: &'static str,
197 pub reason: &'static str,
198}
199impl Outcome {
200 pub(crate) fn unavailable(reason: &'static str) -> Self {
201 Self {
202 code: error_codes::OPERATOR_PRESENCE_UNAVAILABLE,
203 reason,
204 }
205 }
206 fn declined(reason: &'static str) -> Self {
207 Self {
208 code: error_codes::OPERATOR_DECLINED,
209 reason,
210 }
211 }
212 pub(crate) fn refusal(code: &'static str) -> Self {
213 Self { code, reason: "" }
214 }
215 fn from_provider(result: ProviderResult) -> Self {
216 match result {
217 ProviderResult::Approved => Self {
218 code: "confirmed",
219 reason: "",
220 },
221 ProviderResult::Declined => Self::declined("person"),
222 ProviderResult::Unavailable => Self::unavailable("provider_error"),
223 ProviderResult::UnsupportedPlatform => Self::unavailable("unsupported_platform"),
224 ProviderResult::NoPresence => Self::unavailable("no_presence"),
225 }
226 }
227 pub(crate) fn frame(self, request: &Frame) -> Frame {
228 let (ty, body) = if self.code == "confirmed" {
229 (
230 FrameType::Response,
231 serde_json::to_vec(&OperatorConfirmReply::confirmed()).expect("serializable reply"),
232 )
233 } else {
234 let mut error = ErrorBody::new(self.code, "operator confirmation refused");
235 if !self.reason.is_empty() {
236 error = error.with_detail(serde_json::json!({"reason": self.reason}));
237 }
238 (
239 FrameType::Error,
240 serde_json::to_vec(&error).expect("serializable error"),
241 )
242 };
243 Frame::build_with_version(
244 request.header.ver,
245 ty,
246 Flags::new(false, Priority::Passive, false),
247 0,
248 0,
249 request.header.corr,
250 body,
251 )
252 .expect("bounded control reply")
253 }
254}
255
256pub(crate) fn audit(
257 module_id: &str,
258 summary: &str,
259 principal: &str,
260 outcome: Outcome,
261 wait: Duration,
262 prompt: Duration,
263 prompt_shown: bool,
264) {
265 tracing::info!(target: "subc_daemon::operator_confirm", module_id, summary = ?summary,
266 principal, outcome = outcome.code, reason = outcome.reason,
267 wait_ms = wait.as_millis() as u64, prompt_ms = prompt.as_millis() as u64,
268 prompt_shown, "operator_confirm_audit");
269}
270
271fn principal_label(principal: &Principal) -> String {
272 match principal {
273 Principal::Direct => "direct".into(),
274 Principal::Reserved { module_id } => format!("reserved:{module_id}"),
275 Principal::Unverified => String::new(),
276 }
277}
278
279fn valid_summary(summary: &str) -> bool {
280 let count = summary.chars().count();
281 (1..=200).contains(&count)
282 && !summary.starts_with(char::is_whitespace)
283 && !summary.ends_with(char::is_whitespace)
284 && !summary.chars().any(|c| {
285 c.is_control()
286 || matches!(c,
287 '\u{200b}'..='\u{200f}' | '\u{202a}'..='\u{202e}' | '\u{2028}' | '\u{2029}' |
288 '\u{2060}'..='\u{2069}' | '\u{feff}')
289 })
290}
291
292enum RequestKind {
293 Module(ModuleRouteKey),
294 Privileged {
295 answer: Mutex<Option<oneshot::Sender<Outcome>>>,
296 key: DeclineKey,
297 },
298}
299
300struct Request {
301 id: u64,
302 module_id: String,
303 summary: String,
304 principal: String,
305 text: String,
306 kind: RequestKind,
307 ctx: RouteCtx,
308 frame: Frame,
309 arrived: Instant,
310 delivery_deadline: Instant,
311}
312struct Active {
313 request: Arc<Request>,
314 started: Option<Instant>,
315 handle: Option<Arc<dyn OperatorWithdraw>>,
316 committed: bool,
317 withdrawn: Option<(Instant, &'static str)>,
318 withdraw_started: bool,
319}
320struct State {
321 provider: Arc<dyn OperatorProvider>,
322 limits: OperatorLimits,
323 queue: VecDeque<Arc<Request>>,
324 modules: HashMap<String, u64>,
325 backoff: HashMap<(String, String), Instant>,
326 cooldown_clock: CooldownClock,
327 declines: HashMap<DeclineKey, VecDeque<Instant>>,
329 cooldowns: HashMap<DeclineKey, Instant>,
331 prompt: Option<Active>,
332 stuck: bool,
333 running: bool,
334 next_id: u64,
335 #[cfg(test)]
336 deliveries: usize,
337}
338
339pub(crate) struct OperatorConfirms {
340 state: Mutex<State>,
341 wake: Notify,
342}
343
344struct WithdrawOnDrop {
345 confirms: Arc<OperatorConfirms>,
346 id: u64,
347}
348
349impl Drop for WithdrawOnDrop {
350 fn drop(&mut self) {
351 self.confirms
352 .withdraw_matching(|request| request.id == self.id, "caller_cancelled");
353 }
354}
355impl std::fmt::Debug for OperatorConfirms {
356 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
357 f.debug_struct("OperatorConfirms").finish_non_exhaustive()
358 }
359}
360impl Default for OperatorConfirms {
361 fn default() -> Self {
362 Self {
363 state: Mutex::new(State {
364 provider: Arc::new(OsProvider),
365 limits: OperatorLimits::default(),
366 queue: VecDeque::new(),
367 modules: HashMap::new(),
368 backoff: HashMap::new(),
369 cooldown_clock: Arc::new(Instant::now),
370 declines: HashMap::new(),
371 cooldowns: HashMap::new(),
372 prompt: None,
373 stuck: false,
374 running: false,
375 next_id: 0,
376 #[cfg(test)]
377 deliveries: 0,
378 }),
379 wake: Notify::new(),
380 }
381 }
382}
383impl OperatorConfirms {
384 fn lock(&self) -> std::sync::MutexGuard<'_, State> {
385 self.state.lock().unwrap_or_else(|p| p.into_inner())
386 }
387 pub(crate) fn configure(&self, provider: Arc<dyn OperatorProvider>, limits: OperatorLimits) {
388 let mut state = self.lock();
389 assert!(
390 !state.running && state.modules.is_empty(),
391 "configure before serving"
392 );
393 state.provider = provider;
394 state.limits = limits;
395 }
396 pub(crate) fn configure_bootstrap(&self, config: &crate::bootstrap::BootstrapConfig) {
397 let provider: Arc<dyn OperatorProvider> = Arc::new(OsProvider);
398 #[cfg(feature = "test-support")]
399 let provider =
400 test_provider::select(provider, config, std::env::current_exe().ok().as_deref());
401 self.configure(
402 provider,
403 OperatorLimits {
404 queue_wait: config.operator_queue_wait,
405 prompt_timeout: config.operator_timeout,
406 stuck_grace: config.operator_stuck_grace,
407 },
408 );
409 }
410
411 pub(crate) fn admit(
414 self: &Arc<Self>,
415 ctx: &RouteCtx,
416 frame: Frame,
417 module_id: String,
418 nonce_proven: bool,
419 request: OperatorConfirmRequest,
420 binding: Option<&RouteBinding>,
421 ) -> Vec<Frame> {
422 let principal = binding
423 .map(|b| principal_label(&b.principal))
424 .unwrap_or_default();
425 let mut state = self.lock();
426 let now = Instant::now();
427 state.backoff.retain(|_, until| *until > now);
428 let refusal = if !nonce_proven
429 || binding.is_none_or(|b| matches!(b.principal, Principal::Unverified))
430 {
431 Some(Outcome::refusal(
432 error_codes::OPERATOR_REQUEST_NOT_PERMITTED,
433 ))
434 } else if !valid_summary(&request.summary) {
435 Some(Outcome::refusal(error_codes::OPERATOR_SUMMARY_INVALID))
436 } else if state
437 .backoff
438 .contains_key(&(module_id.clone(), principal.clone()))
439 {
440 Some(Outcome::declined("backoff"))
441 } else if state.stuck {
442 Some(Outcome::unavailable("provider_stuck"))
443 } else if state.modules.contains_key(&module_id) {
444 Some(Outcome::unavailable("module_limit"))
445 } else if state.queue.len() >= 4 {
446 Some(Outcome::unavailable("queue_full"))
447 } else {
448 None
449 };
450 if let Some(outcome) = refusal {
451 audit(
452 &module_id,
453 &request.summary,
454 &principal,
455 outcome,
456 Duration::ZERO,
457 Duration::ZERO,
458 false,
459 );
460 return vec![outcome.frame(&frame)];
461 }
462 let binding = binding.expect("permission checked");
463 let opener = match &binding.principal {
464 Principal::Direct => "a local program",
465 Principal::Reserved { module_id } => module_id,
466 Principal::Unverified => unreachable!(),
467 };
468 state.next_id += 1;
469 let pending = Arc::new(Request {
470 id: state.next_id,
471 text: format!(
472 "approve a request from {module_id}: {} (requested by {opener})",
473 request.summary
474 ),
475 module_id,
476 summary: request.summary,
477 principal,
478 kind: RequestKind::Module(ModuleRouteKey {
479 endpoint: binding.module_endpoint,
480 channel: binding.module_channel,
481 }),
482 ctx: ctx.clone(),
483 frame,
484 arrived: now,
485 delivery_deadline: now + state.limits.delivery_budget(),
486 });
487 state.modules.insert(pending.module_id.clone(), pending.id);
488 self.enqueue(&mut state, pending);
489 Vec::new()
490 }
491
492 pub(crate) async fn approve_privileged(
497 self: &Arc<Self>,
498 ctx: &RouteCtx,
499 frame: &Frame,
500 operation: &str,
501 text: String,
502 caller: String,
503 key: DeclineKey,
504 ) -> Outcome {
505 let (answer, receive) = oneshot::channel();
506 let id = {
507 let mut state = self.lock();
508 let now = (state.cooldown_clock)();
509 state.cooldowns.retain(|_, until| *until > now);
510 state.declines.retain(|_, declines| {
511 declines.retain(|at| now.saturating_duration_since(*at) < DECLINE_WINDOW);
512 !declines.is_empty()
513 });
514 let refusal = if state.cooldowns.contains_key(&key) {
515 Some(Outcome::declined("backoff"))
516 } else if state.stuck {
517 Some(Outcome::unavailable("provider_stuck"))
518 } else if state.queue.len() >= 4 {
519 Some(Outcome::unavailable("queue_full"))
520 } else {
521 None
522 };
523 if let Some(outcome) = refusal {
524 audit(
525 operation,
526 &text,
527 &caller,
528 outcome,
529 Duration::ZERO,
530 Duration::ZERO,
531 false,
532 );
533 return outcome;
534 }
535 state.next_id += 1;
536 let id = state.next_id;
537 let now = Instant::now();
538 let pending = Arc::new(Request {
539 id,
540 module_id: operation.into(),
541 summary: text.clone(),
542 principal: caller,
543 text,
544 kind: RequestKind::Privileged {
545 answer: Mutex::new(Some(answer)),
546 key,
547 },
548 ctx: ctx.clone(),
549 frame: frame.clone(),
550 arrived: now,
551 delivery_deadline: now + state.limits.delivery_budget(),
552 });
553 self.enqueue(&mut state, pending);
554 id
555 };
556 let _withdraw = WithdrawOnDrop {
557 confirms: Arc::clone(self),
558 id,
559 };
560 receive
561 .await
562 .unwrap_or_else(|_| Outcome::unavailable("provider_error"))
563 }
564
565 fn enqueue(self: &Arc<Self>, state: &mut State, pending: Arc<Request>) {
566 if state.prompt.is_none() && state.queue.is_empty() {
567 state.prompt = Some(Active {
568 request: pending,
569 started: None,
570 handle: None,
571 committed: false,
572 withdrawn: None,
573 withdraw_started: false,
574 });
575 } else {
576 state.queue.push_back(pending);
577 }
578 if !state.running {
579 state.running = true;
580 tokio::spawn(Arc::clone(self).run());
581 }
582 self.wake.notify_one();
583 }
584
585 fn commit(
586 self: &Arc<Self>,
587 state: &mut State,
588 request: Arc<Request>,
589 outcome: Outcome,
590 started: Option<Instant>,
591 shown: bool,
592 ) {
593 let now = Instant::now();
594 let wait = started.unwrap_or(now).duration_since(request.arrived);
595 let prompt = started.map(|at| now.duration_since(at)).unwrap_or_default();
596 if outcome.reason == "person" && matches!(request.kind, RequestKind::Module(_)) {
597 state.backoff.insert(
598 (request.module_id.clone(), request.principal.clone()),
599 now + Duration::from_secs(30),
600 );
601 }
602 audit(
603 &request.module_id,
604 &request.summary,
605 &request.principal,
606 outcome,
607 wait,
608 prompt,
609 shown,
610 );
611 if let RequestKind::Privileged { answer, key } = &request.kind {
612 if let Some(answer) = answer.lock().unwrap_or_else(|p| p.into_inner()).take() {
613 let _ = answer.send(outcome);
614 }
615 if outcome.reason == "person" {
616 self.count_person_decline(state, key);
617 }
618 return;
619 }
620 let this = Arc::clone(self);
621 #[cfg(test)]
622 {
623 state.deliveries += 1;
624 }
625 tokio::spawn(async move {
626 let answer = outcome.frame(&request.frame);
627 match timeout_at(request.delivery_deadline, request.ctx.egress.send(answer)).await {
628 Ok(Ok(())) => {}
629 result => {
630 tracing::warn!(target: "subc_daemon::operator_confirm", module_id = request.module_id, ?result, "operator confirm answer dropped")
631 }
632 }
633 let mut state = this.lock();
634 #[cfg(test)]
635 {
636 state.deliveries -= 1;
637 }
638 if state.modules.get(&request.module_id) == Some(&request.id) {
639 state.modules.remove(&request.module_id);
640 }
641 });
642 }
643
644 fn count_person_decline(self: &Arc<Self>, state: &mut State, key: &DeclineKey) {
649 let now = (state.cooldown_clock)();
650 let declines = state.declines.entry(key.clone()).or_default();
651 declines.retain(|at| now.saturating_duration_since(*at) < DECLINE_WINDOW);
652 declines.push_back(now);
653 if declines.len() < DECLINE_LIMIT {
654 return;
655 }
656 state.declines.remove(key);
658 state.cooldowns.insert(key.clone(), now + DECLINE_COOLDOWN);
659 tracing::warn!(target: "subc_daemon::operator_confirm", caller = %key,
660 cooldown_secs = DECLINE_COOLDOWN.as_secs(),
661 "privileged requests refused without a prompt after repeated operator declines");
662 let mut index = 0;
663 while index < state.queue.len() {
664 if matches!(&state.queue[index].kind,
665 RequestKind::Privileged { key: queued, .. } if queued == key)
666 {
667 let request = state.queue.remove(index).expect("queue index");
668 self.commit(state, request, Outcome::declined("backoff"), None, false);
669 } else {
670 index += 1;
671 }
672 }
673 }
674
675 fn withdraw_matching(
676 self: &Arc<Self>,
677 predicate: impl Fn(&Request) -> bool,
678 reason: &'static str,
679 ) -> bool {
680 let mut state = self.lock();
681 let mut matched = false;
682 let mut index = 0;
683 while index < state.queue.len() {
684 if predicate(&state.queue[index]) {
685 let request = state.queue.remove(index).expect("queue index");
686 self.commit(&mut state, request, Outcome::declined(reason), None, false);
687 matched = true;
688 } else {
689 index += 1;
690 }
691 }
692 if let Some(active) = state
693 .prompt
694 .as_mut()
695 .filter(|a| !a.committed && predicate(&a.request))
696 {
697 active.committed = true;
698 active.withdrawn = Some((Instant::now(), reason));
699 let (request, started, shown) = (
700 Arc::clone(&active.request),
701 active.started,
702 active.handle.is_some(),
703 );
704 self.commit(
705 &mut state,
706 request,
707 Outcome::declined(reason),
708 started,
709 shown,
710 );
711 matched = true;
712 }
713 self.wake.notify_one();
714 matched
715 }
716 pub(crate) fn route_closed(self: &Arc<Self>, key: ModuleRouteKey) {
717 self.withdraw_matching(
718 |r| matches!(r.kind, RequestKind::Module(k) if k == key),
719 "route_closed",
720 );
721 }
722 pub(crate) fn module_closed(self: &Arc<Self>, endpoint: ModuleEndpointId) {
723 self.withdraw_matching(
724 |r| matches!(r.kind, RequestKind::Module(k) if k.endpoint == endpoint),
725 "module_closed",
726 );
727 }
728 pub(crate) fn connection_closed(self: &Arc<Self>, connection: crate::ConnectionId) {
729 self.withdraw_matching(
730 |r| {
731 matches!(r.kind, RequestKind::Privileged { .. })
732 && r.ctx.connection_id == connection
733 },
734 "caller_cancelled",
735 );
736 }
737 pub(crate) fn cancel(self: &Arc<Self>, connection: crate::ConnectionId, corr: u64) -> bool {
738 self.withdraw_matching(
739 |r| r.ctx.connection_id == connection && r.frame.header.corr == corr,
740 "caller_cancelled",
741 )
742 }
743
744 fn publish(self: &Arc<Self>, id: u64, handle: Arc<dyn OperatorWithdraw>) {
745 let mut state = self.lock();
746 if let Some(active) = state.prompt.as_mut().filter(|a| a.request.id == id) {
747 active.handle = Some(Arc::clone(&handle));
748 if let Some((_, reason)) = active.withdrawn {
749 active.withdraw_started = true;
750 tokio::task::spawn_blocking(move || handle.withdraw(reason));
751 }
752 }
753 self.wake.notify_one();
754 }
755 fn returned(self: &Arc<Self>, id: u64, result: ProviderResult) {
756 let mut state = self.lock();
757 let Some(active) = state.prompt.take() else {
758 return;
759 };
760 assert_eq!(active.request.id, id);
761 if !active.withdraw_started {
764 if let (Some(handle), Some((_, reason))) = (active.handle.as_ref(), active.withdrawn) {
765 let handle = Arc::clone(handle);
766 tokio::task::spawn_blocking(move || handle.withdraw(reason));
767 }
768 }
769 if !active.committed {
770 self.commit(
771 &mut state,
772 active.request,
773 Outcome::from_provider(result),
774 active.started,
775 active.handle.is_some(),
776 );
777 } else {
778 tracing::debug!(target: "subc_daemon::operator_confirm", ?result, "late operator provider result discarded");
779 }
780 state.stuck = false;
781 self.wake.notify_one();
782 }
783
784 async fn run(self: Arc<Self>) {
785 loop {
786 let deadline = {
787 let mut state = self.lock();
788 let now = Instant::now();
789 let mut index = 0;
790 while index < state.queue.len() {
791 if state.queue[index].arrived + state.limits.queue_wait <= now {
792 let request = state.queue.remove(index).expect("queue index");
793 self.commit(
794 &mut state,
795 request,
796 Outcome::unavailable("queue_wait"),
797 None,
798 false,
799 );
800 } else {
801 index += 1;
802 }
803 }
804 let timeout = state.limits.prompt_timeout;
805 if let Some(active) = state
806 .prompt
807 .as_mut()
808 .filter(|a| !a.committed && a.started.is_some_and(|at| at + timeout <= now))
809 {
810 active.committed = true;
811 active.withdrawn = Some((now, "timeout"));
812 let (request, started, shown) = (
813 Arc::clone(&active.request),
814 active.started,
815 active.handle.is_some(),
816 );
817 self.commit(
818 &mut state,
819 request,
820 Outcome::unavailable("timeout"),
821 started,
822 shown,
823 );
824 }
825 let grace = state.limits.stuck_grace;
826 if !state.stuck
827 && state.prompt.as_ref().is_some_and(|a| {
828 a.started.is_some() && a.withdrawn.is_some_and(|(at, _)| at + grace <= now)
829 })
830 {
831 state.stuck = true;
832 tracing::error!(target: "subc_daemon::operator_confirm", "operator provider stuck after withdraw");
833 while let Some(request) = state.queue.pop_front() {
834 self.commit(
835 &mut state,
836 request,
837 Outcome::unavailable("provider_stuck"),
838 None,
839 false,
840 );
841 }
842 }
843 if let Some(active) = state
844 .prompt
845 .as_mut()
846 .filter(|a| !a.withdraw_started && a.withdrawn.is_some() && a.handle.is_some())
847 {
848 active.withdraw_started = true;
849 let handle = Arc::clone(active.handle.as_ref().expect("handle checked"));
850 let reason = active.withdrawn.expect("withdraw checked").1;
851 tokio::task::spawn_blocking(move || handle.withdraw(reason));
852 }
853 if state
855 .prompt
856 .as_ref()
857 .is_some_and(|a| a.started.is_none() && a.committed)
858 {
859 state.prompt = None;
860 }
861 if state.prompt.is_none() {
862 if let Some(request) = state.queue.pop_front() {
863 state.prompt = Some(Active {
864 request,
865 started: None,
866 handle: None,
867 committed: false,
868 withdrawn: None,
869 withdraw_started: false,
870 });
871 }
872 }
873 if let Some(active) = state.prompt.as_mut().filter(|a| a.started.is_none()) {
874 active.started = Some(now);
875 let request = Arc::clone(&active.request);
876 let provider = Arc::clone(&state.provider);
877 let this = Arc::clone(&self);
878 tokio::spawn(async move {
879 let publish_to = Arc::clone(&this);
880 let id = request.id;
881 let result = tokio::task::spawn_blocking(move || {
882 provider.prompt(
883 &request.text,
884 Box::new(move |handle| publish_to.publish(id, handle)),
885 )
886 })
887 .await
888 .unwrap_or(ProviderResult::Unavailable);
889 this.returned(id, result);
890 });
891 }
892 if state.prompt.is_none() && state.queue.is_empty() {
893 state.running = false;
894 return;
895 }
896 let mut deadline = state
897 .queue
898 .iter()
899 .map(|r| r.arrived + state.limits.queue_wait)
900 .min();
901 if let Some(active) = &state.prompt {
902 let next = if !active.committed {
903 active.started.map(|at| at + timeout)
904 } else if !state.stuck {
905 active.withdrawn.map(|(at, _)| at + grace)
906 } else {
907 None
908 };
909 if let Some(next) = next {
910 deadline = Some(deadline.map_or(next, |d| d.min(next)));
911 }
912 }
913 deadline
914 };
915 if let Some(at) = deadline {
916 tokio::select! { _ = self.wake.notified() => {}, _ = tokio::time::sleep_until(at) => {} }
917 } else {
918 self.wake.notified().await;
919 }
920 }
921 }
922}
923
924#[cfg(feature = "test-support")]
925mod test_provider;
926#[cfg(test)]
927mod tests;