1use std::collections::{BTreeMap, HashMap, VecDeque};
67#[cfg(feature = "adapter-api")]
68use std::net::SocketAddr;
69use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
70use std::sync::{Arc, Mutex as StdMutex};
71
72use async_trait::async_trait;
73#[cfg(feature = "adapter-api")]
74use futures::{SinkExt, StreamExt};
75use serde_json::{json, Value};
76use tokio::io::AsyncBufRead;
77#[cfg(feature = "adapter-api")]
78use tokio::io::{AsyncRead, AsyncReadExt};
79#[cfg(feature = "adapter-api")]
80use tokio::io::{AsyncWrite, AsyncWriteExt};
81#[cfg(feature = "adapter-api")]
82use tokio::net::TcpListener;
83#[cfg(feature = "adapter-api")]
84use tokio::sync::mpsc;
85use tokio::sync::{broadcast, Mutex, Notify, RwLock};
86
87use crate::agent::SteerInbox;
88use crate::frontend::{
89 FrontendActions, FrontendApprovalDecision, FrontendAttachSnapshot, FrontendAttachment,
90 FrontendCommandDescriptor, FrontendConnectionState, FrontendDisplayCapabilities, FrontendEvent,
91 FrontendOperationDescriptor, FrontendOperationInvocation, FrontendOperationKind,
92 FrontendOperationResult, FrontendProjectionState, FrontendRequest, FrontendRequestKind,
93 FrontendResponse, FrontendRuntime, FrontendRuntimeDescriptor, FrontendRuntimeError,
94 FrontendRuntimeMetadata, FrontendTurnState, FRONTEND_EVENT_SCHEMA_VERSION,
95 FRONTEND_REPLAY_CAPACITY, FRONTEND_RUNTIME_SCHEMA_VERSION,
96};
97use crate::mcp::{
98 ElicitationAction, ElicitationRequest, ElicitationResponse, McpElicitationHandler,
99};
100use crate::message::ChatMessage;
101use crate::permissions::{ApprovalOutcome, ApprovalRequest, PermissionsApprovalHandler};
102pub use crate::sdk::RuntimeSubmitError;
103use crate::sdk::SdkAgent;
104#[cfg(feature = "adapter-api")]
105use crate::{CoordinatedRuntime, CoordinatedRuntimeClient, RuntimeAuthorization, RuntimeClientId};
106
107pub const SERVER_EVENT_CHANNEL_CAPACITY: usize = 1024;
114
115pub(crate) const SERVER_HISTORY_CAPACITY: usize = 200;
119
120pub const SERVER_MAX_LINE_BYTES: usize = 16 * 1024 * 1024;
127
128#[cfg(feature = "adapter-api")]
133const MAX_HEADER_LINES: usize = 200;
134
135#[derive(Debug, Clone, serde::Deserialize)]
140pub struct RpcRequest {
141 pub id: Value,
145 pub method: String,
147 #[serde(default)]
149 pub params: Value,
150}
151
152fn rpc_ok(id: Value, result: Value) -> Value {
154 json!({"id": id, "result": result})
155}
156
157fn rpc_error(id: Value, code: i32, message: impl Into<String>) -> Value {
164 json!({"id": id, "error": {"code": code, "message": message.into()}})
165}
166
167fn sdk_runtime_rpc_error(id: Value, code: i32, error: &FrontendRuntimeError) -> Value {
168 let code = match error.code() {
169 crate::SdkErrorCode::Unauthenticated => -32030,
170 crate::SdkErrorCode::Unauthorized => -32031,
171 crate::SdkErrorCode::ControllerRequired => -32032,
172 crate::SdkErrorCode::LeaseExpired => -32033,
173 _ => code,
174 };
175 let mut envelope = json!({
176 "id": id,
177 "error": {
178 "code": code,
179 "name": error.code(),
180 "operation": error.operation(),
181 "message": error.to_string(),
182 }
183 });
184 if let Some(detail) = envelope.get_mut("error").and_then(Value::as_object_mut) {
185 match error {
186 FrontendRuntimeError::Unauthorized { permission } => {
187 detail.insert("permission".into(), Value::String(permission.clone()));
188 }
189 FrontendRuntimeError::ControllerRequired {
190 holder,
191 expires_at_ms,
192 } => {
193 if let Some(holder) = holder {
194 detail.insert("holder".into(), Value::String(holder.clone()));
195 }
196 if let Some(expires_at_ms) = expires_at_ms {
197 detail.insert("expiresAtMs".into(), json!(expires_at_ms));
198 }
199 }
200 _ => {}
201 }
202 }
203 envelope
204}
205
206async fn read_bounded_line<R>(reader: &mut R, cap: usize) -> std::io::Result<Option<String>>
214where
215 R: AsyncBufRead + Unpin,
216{
217 use tokio::io::AsyncBufReadExt;
218 let mut out: Vec<u8> = Vec::new();
219 loop {
220 let buf = reader.fill_buf().await?;
221 if buf.is_empty() {
222 return Ok(if out.is_empty() {
223 None
224 } else {
225 Some(strip_crlf(out))
226 });
227 }
228 if let Some(pos) = buf.iter().position(|&b| b == b'\n') {
229 if out.len() + pos > cap {
230 reader.consume(pos + 1);
231 return Err(std::io::Error::new(
232 std::io::ErrorKind::InvalidData,
233 format!("line exceeded {cap} byte cap"),
234 ));
235 }
236 out.extend_from_slice(&buf[..pos]);
237 reader.consume(pos + 1);
238 return Ok(Some(strip_crlf(out)));
239 }
240 let take = buf.len();
241 if out.len() + take > cap {
242 reader.consume(take);
243 loop {
246 let b = reader.fill_buf().await?;
247 if b.is_empty() {
248 break;
249 }
250 if let Some(p) = b.iter().position(|&x| x == b'\n') {
251 reader.consume(p + 1);
252 break;
253 }
254 let n = b.len();
255 reader.consume(n);
256 }
257 return Err(std::io::Error::new(
258 std::io::ErrorKind::InvalidData,
259 format!("line exceeded {cap} byte cap"),
260 ));
261 }
262 out.extend_from_slice(buf);
263 reader.consume(take);
264 }
265}
266
267fn strip_crlf(mut v: Vec<u8>) -> String {
268 if v.last() == Some(&b'\r') {
269 v.pop();
270 }
271 String::from_utf8_lossy(&v).into_owned()
272}
273
274#[cfg(feature = "adapter-api")]
278fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
279 if a.len() != b.len() {
280 return false;
281 }
282 let mut diff = 0u8;
283 for (x, y) in a.iter().zip(b.iter()) {
284 diff |= x ^ y;
285 }
286 diff == 0
287}
288
289pub fn generate_token() -> String {
296 let mut bytes = [0u8; 32];
297 getrandom::getrandom(&mut bytes).expect("OS entropy source for the server bearer token");
303 bytes.iter().map(|b| format!("{b:02x}")).collect()
304}
305
306type TurnCompleteHook = Box<dyn Fn(&SdkAgent) + Send + Sync>;
310
311#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
316pub struct RuntimeStatus {
317 pub session_id: String,
319 pub model: String,
321 pub busy: bool,
323 pub shutting_down: bool,
325}
326
327type PendingFrontendResponses = StdMutex<
328 HashMap<
329 u64,
330 (
331 FrontendRequestKind,
332 std::sync::mpsc::Sender<AcceptedFrontendResponse>,
333 ),
334 >,
335>;
336
337struct AcceptedFrontendResponse {
338 response: FrontendResponse,
339 published: std::sync::mpsc::Receiver<()>,
342}
343
344struct FrontendRequestBroker {
347 next_id: std::sync::atomic::AtomicU64,
348 pending: PendingFrontendResponses,
349 transport: StdMutex<Option<FrontendRequestTransport>>,
350}
351
352#[derive(Clone)]
353struct FrontendRequestTransport {
354 events: broadcast::Sender<FrontendEvent>,
355 state: Arc<StdMutex<FrontendProjectionState>>,
356}
357
358impl FrontendRequestBroker {
359 fn new() -> Arc<Self> {
360 Arc::new(Self {
361 next_id: std::sync::atomic::AtomicU64::new(1),
362 pending: StdMutex::new(HashMap::new()),
363 transport: StdMutex::new(None),
364 })
365 }
366
367 fn bind(
368 &self,
369 events: broadcast::Sender<FrontendEvent>,
370 state: Arc<StdMutex<FrontendProjectionState>>,
371 ) {
372 *self
373 .transport
374 .lock()
375 .unwrap_or_else(std::sync::PoisonError::into_inner) =
376 Some(FrontendRequestTransport { events, state });
377 }
378
379 fn transport(&self) -> Option<FrontendRequestTransport> {
380 self.transport
381 .lock()
382 .unwrap_or_else(std::sync::PoisonError::into_inner)
383 .clone()
384 }
385
386 fn publish(&self, request: &FrontendRequest) -> bool {
387 self.publish_payload(json!({"type": "request", "request": request}))
388 }
389
390 fn publish_payload(&self, payload: Value) -> bool {
391 let Some(transport) = self.transport() else {
392 return false;
393 };
394 let event = {
395 let mut state = transport
396 .state
397 .lock()
398 .unwrap_or_else(std::sync::PoisonError::into_inner);
399 let event = FrontendEvent::new(state.next_sequence, payload);
400 state.next_sequence = state.next_sequence.saturating_add(1);
401 state.replay.push_back(event.clone());
402 while state.replay.len() > FRONTEND_REPLAY_CAPACITY {
403 state.replay.pop_front();
404 }
405 event
406 };
407 transport.events.send(event).is_ok()
408 }
409
410 fn respond(&self, response: FrontendResponse) -> Result<(), FrontendRuntimeError> {
411 let request_id = response.request_id();
412 let response_kind = match &response {
413 FrontendResponse::Approval { .. } => FrontendRequestKind::Approval,
414 FrontendResponse::Elicitation { .. } => FrontendRequestKind::Elicitation,
415 FrontendResponse::Other { .. } => FrontendRequestKind::Other,
416 };
417 let mut pending = self
418 .pending
419 .lock()
420 .unwrap_or_else(std::sync::PoisonError::into_inner);
421 let expected = pending
422 .get(&request_id)
423 .map(|(kind, _)| *kind)
424 .ok_or(FrontendRuntimeError::UnknownRequest(request_id))?;
425 if expected != response_kind {
426 return Err(FrontendRuntimeError::InvalidResponse(format!(
427 "request {request_id} expects {expected:?}, got {response_kind:?}"
428 )));
429 }
430 let (_, sender) = pending
431 .remove(&request_id)
432 .ok_or(FrontendRuntimeError::UnknownRequest(request_id))?;
433 drop(pending);
434 let payload = json!({
435 "type": "request_resolved",
436 "request_id": request_id,
437 "response": &response,
438 });
439 let (published_tx, published_rx) = std::sync::mpsc::channel();
440 sender
441 .send(AcceptedFrontendResponse {
442 response,
443 published: published_rx,
444 })
445 .map_err(|_| FrontendRuntimeError::UnknownRequest(request_id))?;
446 self.publish_payload(payload);
447 let _ = published_tx.send(());
448 Ok(())
449 }
450
451 fn ask_approval(
452 &self,
453 req: &ApprovalRequest<'_>,
454 child: Option<(&str, &Arc<StdMutex<Vec<crate::subagents::QueuedApproval>>>)>,
455 ) -> ApprovalOutcome {
456 let queued = child.and_then(|(child_agent_id, queue)| {
460 crate::subagents::queue_approval(
461 queue,
462 crate::subagents::QueuedApproval {
463 child_agent_id: child_agent_id.to_string(),
464 tool: req.tool.to_string(),
465 subject: req.subject.map(String::from),
466 queued_at_ms: std::time::SystemTime::now()
467 .duration_since(std::time::UNIX_EPOCH)
468 .map(|duration| duration.as_millis() as i64)
469 .unwrap_or_default(),
470 outcome: None,
471 },
472 )
473 .map(|index| (queue.clone(), index))
474 });
475 let outcome = self.decide_approval(req, child.map(|(id, _)| id));
476 if let Some((queue, index)) = queued {
477 crate::subagents::record_queued_outcome(&queue, index, outcome.into());
478 }
479 outcome
480 }
481
482 fn decide_approval(
483 &self,
484 req: &ApprovalRequest<'_>,
485 child_agent_id: Option<&str>,
486 ) -> ApprovalOutcome {
487 let Some(transport) = self.transport() else {
490 return ApprovalOutcome::Deny;
491 };
492 if transport.events.receiver_count() == 0 {
493 return ApprovalOutcome::Deny;
494 }
495 let id = self.next_id.fetch_add(1, Ordering::SeqCst);
496 let mut payload = json!({
497 "tool": req.tool,
498 "subject": req.subject,
499 "raw_args": req.raw_args,
500 });
501 if let Some(child_agent_id) = child_agent_id {
502 payload["child_agent_id"] = Value::String(child_agent_id.to_string());
503 }
504 let request = FrontendRequest {
505 id,
506 kind: FrontendRequestKind::Approval,
507 payload,
508 };
509 let (tx, rx) = std::sync::mpsc::channel();
510 self.pending
511 .lock()
512 .unwrap_or_else(std::sync::PoisonError::into_inner)
513 .insert(id, (FrontendRequestKind::Approval, tx));
514 if !self.publish(&request) {
515 self.pending
516 .lock()
517 .unwrap_or_else(std::sync::PoisonError::into_inner)
518 .remove(&id);
519 return ApprovalOutcome::Deny;
520 }
521 let wait_for_response = || loop {
522 match rx.recv_timeout(std::time::Duration::from_millis(100)) {
523 Ok(accepted) => {
524 let _ = accepted.published.recv();
525 let FrontendResponse::Approval { decision, .. } = accepted.response else {
526 return ApprovalOutcome::Deny;
527 };
528 return match decision {
529 FrontendApprovalDecision::Deny => ApprovalOutcome::Deny,
530 FrontendApprovalDecision::Allow => ApprovalOutcome::Allow,
531 FrontendApprovalDecision::AllowForSession => {
532 ApprovalOutcome::AllowForSession
533 }
534 };
535 }
536 Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
537 return ApprovalOutcome::Deny;
538 }
539 Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
540 if self
541 .transport()
542 .map(|transport| transport.events.receiver_count() == 0)
543 .unwrap_or(true)
544 {
545 self.pending
546 .lock()
547 .unwrap_or_else(std::sync::PoisonError::into_inner)
548 .remove(&id);
549 return ApprovalOutcome::Deny;
550 }
551 }
552 }
553 };
554 if tokio::runtime::Handle::try_current()
555 .map(|handle| handle.runtime_flavor() == tokio::runtime::RuntimeFlavor::MultiThread)
556 .unwrap_or(false)
557 {
558 tokio::task::block_in_place(wait_for_response)
559 } else {
560 wait_for_response()
561 }
562 }
563
564 async fn ask_elicitation(self: Arc<Self>, req: &ElicitationRequest) -> ElicitationResponse {
565 let cancel = || ElicitationResponse {
566 action: ElicitationAction::Cancel,
567 content: None,
568 };
569 let Some(transport) = self.transport() else {
570 return cancel();
571 };
572 if transport.events.receiver_count() == 0 {
573 return cancel();
574 }
575 let id = self.next_id.fetch_add(1, Ordering::SeqCst);
576 let request = FrontendRequest {
577 id,
578 kind: FrontendRequestKind::Elicitation,
579 payload: json!({
580 "message": req.message,
581 "requested_schema": req.requested_schema,
582 }),
583 };
584 let (tx, rx) = std::sync::mpsc::channel();
585 self.pending
586 .lock()
587 .unwrap_or_else(std::sync::PoisonError::into_inner)
588 .insert(id, (FrontendRequestKind::Elicitation, tx));
589 if !self.publish(&request) {
590 self.pending
591 .lock()
592 .unwrap_or_else(std::sync::PoisonError::into_inner)
593 .remove(&id);
594 return cancel();
595 }
596 let broker = self.clone();
597 tokio::task::spawn_blocking(move || loop {
598 match rx.recv_timeout(std::time::Duration::from_millis(100)) {
599 Ok(accepted) => {
600 let _ = accepted.published.recv();
601 let FrontendResponse::Elicitation {
602 action, content, ..
603 } = accepted.response
604 else {
605 return cancel();
606 };
607 return ElicitationResponse {
608 action: match action {
609 crate::frontend::FrontendElicitationAction::Accept => {
610 ElicitationAction::Accept
611 }
612 crate::frontend::FrontendElicitationAction::Decline => {
613 ElicitationAction::Decline
614 }
615 crate::frontend::FrontendElicitationAction::Cancel => {
616 ElicitationAction::Cancel
617 }
618 },
619 content,
620 };
621 }
622 Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => return cancel(),
623 Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
624 if broker
625 .transport()
626 .map(|transport| transport.events.receiver_count() == 0)
627 .unwrap_or(true)
628 {
629 broker
630 .pending
631 .lock()
632 .unwrap_or_else(std::sync::PoisonError::into_inner)
633 .remove(&id);
634 return cancel();
635 }
636 }
637 }
638 })
639 .await
640 .unwrap_or_else(|_| cancel())
641 }
642}
643
644struct FrontendApprovalHandler(Arc<FrontendRequestBroker>);
645
646impl PermissionsApprovalHandler for FrontendApprovalHandler {
647 fn ask(&self, req: &ApprovalRequest<'_>) -> ApprovalOutcome {
648 self.0.ask_approval(req, None)
649 }
650}
651
652struct FrontendChildApprovalHandler {
653 broker: Arc<FrontendRequestBroker>,
654 child_agent_id: String,
655 queue: Arc<StdMutex<Vec<crate::subagents::QueuedApproval>>>,
656}
657
658impl PermissionsApprovalHandler for FrontendChildApprovalHandler {
659 fn ask(&self, req: &ApprovalRequest<'_>) -> ApprovalOutcome {
660 self.broker
661 .ask_approval(req, Some((&self.child_agent_id, &self.queue)))
662 }
663}
664
665#[derive(Clone)]
668pub struct FrontendRequestBridge {
669 broker: Arc<FrontendRequestBroker>,
670}
671
672impl FrontendRequestBridge {
673 pub fn new() -> Self {
676 Self {
677 broker: FrontendRequestBroker::new(),
678 }
679 }
680
681 pub fn elicitation_handler(&self) -> Arc<dyn McpElicitationHandler> {
683 Arc::new(FrontendElicitationHandler(self.broker.clone()))
684 }
685}
686
687impl Default for FrontendRequestBridge {
688 fn default() -> Self {
689 Self::new()
690 }
691}
692
693struct FrontendElicitationHandler(Arc<FrontendRequestBroker>);
694
695#[async_trait]
696impl McpElicitationHandler for FrontendElicitationHandler {
697 async fn handle(&self, request: &ElicitationRequest) -> ElicitationResponse {
698 self.0.clone().ask_elicitation(request).await
699 }
700}
701
702pub struct RpcEngine {
708 agent: Mutex<SdkAgent>,
709 history_snapshot: RwLock<Vec<ChatMessage>>,
713 session_id: String,
714 model: String,
718 busy: Arc<AtomicBool>,
719 current_cancel: Arc<StdMutex<Option<Arc<Notify>>>>,
725 turn_finished: Arc<Notify>,
728 steer_queue: Arc<StdMutex<SteerInbox>>,
731 events: broadcast::Sender<Value>,
732 frontend_events: broadcast::Sender<FrontendEvent>,
734 frontend_state: Arc<StdMutex<FrontendProjectionState>>,
736 frontend_metadata: FrontendRuntimeMetadata,
737 frontend_active_modules: Vec<String>,
738 frontend_commands: Vec<FrontendCommandDescriptor>,
739 frontend_operations: Vec<FrontendOperationDescriptor>,
740 frontend_requests: Option<Arc<FrontendRequestBroker>>,
741 shutdown: Notify,
742 shutting_down: AtomicBool,
743 accepting_submits: AtomicBool,
747 shutdown_barrier: Mutex<()>,
750 on_turn_complete: Option<TurnCompleteHook>,
756}
757
758struct SdkSubmitClaim {
765 inbox: Arc<StdMutex<SteerInbox>>,
766 busy: Arc<AtomicBool>,
767 cancel: Arc<Notify>,
768 current_cancel: Arc<StdMutex<Option<Arc<Notify>>>>,
769 turn_finished: Arc<Notify>,
770 frontend_events: broadcast::Sender<FrontendEvent>,
771 frontend_state: Arc<StdMutex<FrontendProjectionState>>,
772 lifecycle_started: bool,
773}
774
775impl SdkSubmitClaim {
776 fn mark_lifecycle_started(&mut self) {
777 self.lifecycle_started = true;
778 }
779
780 fn mark_lifecycle_finished(&mut self) {
781 self.lifecycle_started = false;
782 }
783}
784
785impl Drop for SdkSubmitClaim {
786 fn drop(&mut self) {
787 if self.lifecycle_started {
795 let event = {
796 let mut state = self
797 .frontend_state
798 .lock()
799 .unwrap_or_else(std::sync::PoisonError::into_inner);
800 let event = FrontendEvent::new(
801 state.next_sequence,
802 json!({
803 "type": "turn_interrupted",
804 "schema_version": FRONTEND_EVENT_SCHEMA_VERSION
805 }),
806 );
807 state.next_sequence = state.next_sequence.saturating_add(1);
808 state.replay.push_back(event.clone());
809 while state.replay.len() > FRONTEND_REPLAY_CAPACITY {
810 state.replay.pop_front();
811 }
812 event
813 };
814 let _ = self.frontend_events.send(event);
815 }
816 self.inbox
817 .lock()
818 .unwrap_or_else(std::sync::PoisonError::into_inner)
819 .close();
820 *self
821 .current_cancel
822 .lock()
823 .unwrap_or_else(std::sync::PoisonError::into_inner) = None;
824 self.busy.store(false, Ordering::SeqCst);
825 self.turn_finished.notify_waiters();
826 self.turn_finished.notify_one();
827 }
828}
829
830impl RpcEngine {
831 pub fn new(
839 agent: impl Into<SdkAgent>,
840 on_turn_complete: Option<TurnCompleteHook>,
841 ) -> Arc<Self> {
842 let agent = agent.into();
843 let session_id = agent
844 .session_name()
845 .map(str::to_owned)
846 .unwrap_or_else(|| format!("supercode-{}", std::process::id()));
847 Self::new_named(agent, session_id, on_turn_complete)
848 }
849
850 pub fn new_named(
854 agent: impl Into<SdkAgent>,
855 session_id: impl Into<String>,
856 on_turn_complete: Option<TurnCompleteHook>,
857 ) -> Arc<Self> {
858 Self::new_named_with_frontend_metadata(
859 agent.into(),
860 session_id,
861 FrontendRuntimeMetadata::default(),
862 on_turn_complete,
863 )
864 }
865
866 pub fn new_named_with_frontend_metadata(
869 agent: impl Into<SdkAgent>,
870 session_id: impl Into<String>,
871 frontend_metadata: FrontendRuntimeMetadata,
872 on_turn_complete: Option<TurnCompleteHook>,
873 ) -> Arc<Self> {
874 Self::build(
875 agent.into(),
876 session_id.into(),
877 frontend_metadata,
878 None,
879 on_turn_complete,
880 )
881 }
882
883 pub fn new_named_with_frontend_requests(
887 agent: impl Into<SdkAgent>,
888 session_id: impl Into<String>,
889 frontend_metadata: FrontendRuntimeMetadata,
890 on_turn_complete: Option<TurnCompleteHook>,
891 ) -> Arc<Self> {
892 let bridge = FrontendRequestBridge::new();
893 Self::new_named_with_frontend_bridge(
894 agent.into(),
895 session_id,
896 frontend_metadata,
897 bridge,
898 on_turn_complete,
899 )
900 }
901
902 pub fn new_named_with_frontend_bridge(
905 agent: impl Into<SdkAgent>,
906 session_id: impl Into<String>,
907 frontend_metadata: FrontendRuntimeMetadata,
908 bridge: FrontendRequestBridge,
909 on_turn_complete: Option<TurnCompleteHook>,
910 ) -> Arc<Self> {
911 Self::build(
912 agent.into(),
913 session_id.into(),
914 frontend_metadata,
915 Some(bridge.broker),
916 on_turn_complete,
917 )
918 }
919
920 fn build(
921 mut agent: SdkAgent,
922 session_id: String,
923 frontend_metadata: FrontendRuntimeMetadata,
924 frontend_requests: Option<Arc<FrontendRequestBroker>>,
925 on_turn_complete: Option<TurnCompleteHook>,
926 ) -> Arc<Self> {
927 let (tx, _rx) = broadcast::channel(SERVER_EVENT_CHANNEL_CAPACITY);
928 let events_tx = tx.clone();
929 let (frontend_tx, _frontend_rx) = broadcast::channel(SERVER_EVENT_CHANNEL_CAPACITY);
930 let frontend_events_tx = frontend_tx.clone();
931 let model = agent.config().model.clone();
932 let steer_queue = agent.inner().steer_queue_handle();
933 let history_snapshot = bounded_history_snapshot(agent.history());
934 let frontend_state = Arc::new(StdMutex::new(FrontendProjectionState {
935 history: history_snapshot.clone(),
936 history_cursor: 0,
937 next_sequence: 1,
938 replay: VecDeque::new(),
939 }));
940 if let Some(broker) = &frontend_requests {
941 broker.bind(frontend_tx.clone(), frontend_state.clone());
942 let legacy_broker = broker.clone();
943 agent
944 .inner_mut()
945 .set_legacy_approval_handler(Box::new(move |call| {
946 let Ok(raw_args) = call.function.parsed_arguments() else {
947 return false;
948 };
949 let subject = raw_args
950 .get("command")
951 .or_else(|| raw_args.get("path"))
952 .or_else(|| raw_args.get("file_path"))
953 .or_else(|| raw_args.get("patch"))
954 .and_then(Value::as_str);
955 matches!(
956 legacy_broker.ask_approval(
957 &ApprovalRequest {
958 tool: &call.function.name,
959 subject,
960 raw_args: &raw_args,
961 },
962 None,
963 ),
964 ApprovalOutcome::Allow | ApprovalOutcome::AllowForSession
965 )
966 }));
967 agent
968 .inner_mut()
969 .set_permissions_approval_handler(FrontendApprovalHandler(broker.clone()));
970 agent
977 .inner_mut()
978 .set_user_question_handler(Arc::new(FrontendElicitationHandler(broker.clone())));
979 let broker = broker.clone();
980 agent
981 .inner_mut()
982 .set_child_approval_handler_factory(move |child_agent_id, queue| {
983 Arc::new(FrontendChildApprovalHandler {
984 broker: broker.clone(),
985 child_agent_id,
986 queue,
987 }) as Arc<dyn PermissionsApprovalHandler>
988 });
989 }
990 let event_frontend_state = frontend_state.clone();
991 let frontend_active_modules = agent
992 .config()
993 .module_activation
994 .iter()
995 .map(ToString::to_string)
996 .collect();
997 let mut frontend_operations = agent
998 .config()
999 .prompts
1000 .keys()
1001 .filter(|name| valid_frontend_command_name(name))
1002 .map(|name| FrontendOperationDescriptor {
1003 id: format!("prompt:{name}"),
1004 kind: FrontendOperationKind::Prompt,
1005 command: Some(FrontendCommandDescriptor {
1006 name: name.clone(),
1007 description: None,
1008 argument_hint: Some("[arguments]".into()),
1009 }),
1010 })
1011 .collect::<Vec<_>>();
1012 frontend_operations.sort_by(|left, right| left.id.cmp(&right.id));
1013 if agent.config().model_switch_allow_switch {
1026 frontend_operations.push(FrontendOperationDescriptor {
1027 id: "model:switch".into(),
1028 kind: FrontendOperationKind::Model,
1029 command: Some(FrontendCommandDescriptor {
1030 name: "model".into(),
1031 description: Some("show or switch this session's model".into()),
1032 argument_hint: Some("[model]".into()),
1033 }),
1034 });
1035 }
1036 frontend_operations.push(FrontendOperationDescriptor {
1037 id: "context:usage".into(),
1038 kind: FrontendOperationKind::Context,
1039 command: Some(FrontendCommandDescriptor {
1040 name: "context".into(),
1041 description: Some("context-window usage for this session".into()),
1042 argument_hint: None,
1043 }),
1044 });
1045 let frontend_commands = frontend_operations
1048 .iter()
1049 .filter_map(|operation| operation.command.as_ref())
1050 .map(|command| FrontendCommandDescriptor {
1051 name: command.name.clone(),
1052 description: command.description.clone(),
1053 argument_hint: None,
1054 })
1055 .collect();
1056 agent.inner_mut().set_event_sink(Box::new(move |event| {
1057 let payload = event.to_json();
1061 let _ = events_tx.send(payload.clone());
1062 let sequenced = {
1063 let mut state = event_frontend_state
1064 .lock()
1065 .unwrap_or_else(std::sync::PoisonError::into_inner);
1066 let event = FrontendEvent::new(state.next_sequence, payload);
1067 state.next_sequence = state.next_sequence.saturating_add(1);
1068 state.replay.push_back(event.clone());
1069 while state.replay.len() > FRONTEND_REPLAY_CAPACITY {
1070 state.replay.pop_front();
1071 }
1072 event
1073 };
1074 let _ = frontend_events_tx.send(sequenced);
1075 }));
1076 Arc::new(RpcEngine {
1077 agent: Mutex::new(agent),
1078 history_snapshot: RwLock::new(history_snapshot),
1079 session_id,
1080 model,
1081 busy: Arc::new(AtomicBool::new(false)),
1082 current_cancel: Arc::new(StdMutex::new(None)),
1083 turn_finished: Arc::new(Notify::new()),
1084 steer_queue,
1085 events: tx,
1086 frontend_events: frontend_tx,
1087 frontend_state,
1088 frontend_metadata,
1089 frontend_active_modules,
1090 frontend_commands,
1091 frontend_operations,
1092 frontend_requests,
1093 shutdown: Notify::new(),
1094 shutting_down: AtomicBool::new(false),
1095 accepting_submits: AtomicBool::new(true),
1096 shutdown_barrier: Mutex::new(()),
1097 on_turn_complete,
1098 })
1099 }
1100
1101 pub fn subscribe(&self) -> broadcast::Receiver<Value> {
1105 self.events.subscribe()
1106 }
1107
1108 pub fn frontend_descriptor(&self) -> FrontendRuntimeDescriptor {
1110 FrontendRuntimeDescriptor {
1111 schema_version: FRONTEND_RUNTIME_SCHEMA_VERSION,
1112 session_id: self.session_id.clone(),
1113 source_harness: self.frontend_metadata.source_harness.clone(),
1114 emulation_profile: self.frontend_metadata.emulation_profile.clone(),
1115 active_modules: self.frontend_active_modules.clone(),
1116 commands: self.frontend_commands.clone(),
1117 operations: self.frontend_operations.clone(),
1118 actions: FrontendActions {
1119 submit: true,
1120 interrupt: true,
1121 steer: true,
1122 respond: self.frontend_requests.is_some(),
1123 detach: true,
1124 close: true,
1128 },
1129 display: FrontendDisplayCapabilities {
1130 event_kinds: vec![
1131 "user_message".into(),
1132 "turn_started".into(),
1133 "turn_succeeded".into(),
1134 "turn_interrupted".into(),
1135 "turn_failed".into(),
1136 "text_delta".into(),
1137 "turn_completed".into(),
1138 "tool_call_started".into(),
1139 "tool_call_completed".into(),
1140 "cache_warning".into(),
1141 "usage".into(),
1142 "background_output".into(),
1143 "request".into(),
1144 "request_resolved".into(),
1145 "scheduled_prompt_started".into(),
1146 "scheduled_prompt_deferred".into(),
1147 "scheduled_prompt_completed".into(),
1148 "scheduler_error".into(),
1149 ],
1150 opaque_fallback: true,
1151 },
1152 model: self.model.clone(),
1153 turn_state: if self.busy.load(Ordering::SeqCst) {
1154 FrontendTurnState::Busy
1155 } else {
1156 FrontendTurnState::Idle
1157 },
1158 connection_state: if self.is_shutting_down() {
1159 FrontendConnectionState::ShuttingDown
1160 } else {
1161 FrontendConnectionState::Connected
1162 },
1163 extensions: Default::default(),
1164 }
1165 }
1166
1167 pub fn frontend_attach(
1172 &self,
1173 history_limit: usize,
1174 ) -> Result<FrontendAttachment, FrontendRuntimeError> {
1175 let live = self.frontend_subscribe();
1176 let snapshot = self.frontend_snapshot(history_limit)?;
1177 Ok(FrontendAttachment::new(
1178 snapshot.descriptor,
1179 snapshot.history,
1180 snapshot.history_cursor,
1181 snapshot.replay,
1182 live,
1183 None,
1184 ))
1185 }
1186
1187 pub fn frontend_subscribe(&self) -> broadcast::Receiver<FrontendEvent> {
1191 self.frontend_events.subscribe()
1192 }
1193
1194 pub fn frontend_snapshot(
1196 &self,
1197 history_limit: usize,
1198 ) -> Result<FrontendAttachSnapshot, FrontendRuntimeError> {
1199 let state = self
1200 .frontend_state
1201 .lock()
1202 .unwrap_or_else(std::sync::PoisonError::into_inner);
1203 let limit = history_limit.min(SERVER_HISTORY_CAPACITY);
1204 let start = state.history.len().saturating_sub(limit);
1205 let replay = state
1206 .replay
1207 .iter()
1208 .filter(|event| event.sequence > state.history_cursor)
1209 .cloned()
1210 .collect::<VecDeque<_>>();
1211 if let Some(first) = replay.front() {
1212 let expected = state.history_cursor.saturating_add(1);
1213 if first.sequence > expected {
1214 return Err(FrontendRuntimeError::ReplayGap(first.sequence - expected));
1215 }
1216 }
1217 Ok(FrontendAttachSnapshot {
1218 descriptor: self.frontend_descriptor(),
1219 history: state.history[start..].to_vec(),
1220 history_cursor: state.history_cursor,
1221 replay,
1222 })
1223 }
1224
1225 fn publish_frontend_payload(&self, payload: Value) {
1226 let event = {
1227 let mut state = self
1228 .frontend_state
1229 .lock()
1230 .unwrap_or_else(std::sync::PoisonError::into_inner);
1231 let event = FrontendEvent::new(state.next_sequence, payload);
1232 state.next_sequence = state.next_sequence.saturating_add(1);
1233 state.replay.push_back(event.clone());
1234 while state.replay.len() > FRONTEND_REPLAY_CAPACITY {
1235 state.replay.pop_front();
1236 }
1237 event
1238 };
1239 let _ = self.frontend_events.send(event);
1240 }
1241
1242 pub fn session_id(&self) -> &str {
1244 &self.session_id
1245 }
1246
1247 fn claim_submit(&self) -> Result<SdkSubmitClaim, RuntimeSubmitError> {
1248 let mut current_cancel = self
1253 .current_cancel
1254 .lock()
1255 .unwrap_or_else(std::sync::PoisonError::into_inner);
1256 if !self.accepting_submits.load(Ordering::SeqCst) {
1257 return Err(RuntimeSubmitError::Interrupted);
1258 }
1259 if self.busy.swap(true, Ordering::SeqCst) {
1260 return Err(RuntimeSubmitError::Busy);
1261 }
1262 if !self.accepting_submits.load(Ordering::SeqCst) {
1266 self.busy.store(false, Ordering::SeqCst);
1267 self.turn_finished.notify_waiters();
1268 return Err(RuntimeSubmitError::Interrupted);
1269 }
1270 let cancel = Arc::new(Notify::new());
1271 *current_cancel = Some(cancel.clone());
1272 drop(current_cancel);
1273 self.steer_queue
1274 .lock()
1275 .unwrap_or_else(std::sync::PoisonError::into_inner)
1276 .open();
1277 Ok(SdkSubmitClaim {
1278 inbox: self.steer_queue.clone(),
1279 busy: self.busy.clone(),
1280 cancel,
1281 current_cancel: self.current_cancel.clone(),
1282 turn_finished: self.turn_finished.clone(),
1283 frontend_events: self.frontend_events.clone(),
1284 frontend_state: self.frontend_state.clone(),
1285 lifecycle_started: false,
1286 })
1287 }
1288
1289 async fn submit_claimed(
1290 &self,
1291 prompt: String,
1292 image_urls: Vec<String>,
1293 mut submit_claim: SdkSubmitClaim,
1294 ) -> Result<String, RuntimeSubmitError> {
1295 let cancel = submit_claim.cancel.clone();
1296 self.publish_frontend_payload(json!({"type": "user_message", "text": &prompt}));
1297 self.publish_frontend_payload(json!({
1298 "type": "turn_started",
1299 "schema_version": FRONTEND_EVENT_SCHEMA_VERSION
1300 }));
1301 submit_claim.mark_lifecycle_started();
1302 let outcome = {
1303 let mut agent = self.agent.lock().await;
1304 let result = tokio::select! {
1305 biased;
1306 _ = cancel.notified() => Err(RuntimeSubmitError::Interrupted),
1307 result = async {
1308 if image_urls.is_empty() {
1309 agent.inner_mut().send(&prompt).await
1310 } else {
1311 agent.inner_mut().send_with_images(&prompt, &image_urls).await
1312 }
1313 } => result.map_err(|error| RuntimeSubmitError::Agent(error.to_string())),
1314 };
1315 if result.is_ok() {
1316 if let Some(hook) = &self.on_turn_complete {
1317 hook(&agent);
1318 }
1319 }
1320 let history = bounded_history_snapshot(agent.history());
1324 *self.history_snapshot.write().await = history.clone();
1325 let mut state = self
1326 .frontend_state
1327 .lock()
1328 .unwrap_or_else(std::sync::PoisonError::into_inner);
1329 state.history = history;
1330 state.history_cursor = state.next_sequence.saturating_sub(1);
1331 let request_history = compact_frontend_request_history(&state.replay);
1338 state.replay.clear();
1339 for payload in request_history {
1340 let event = FrontendEvent::new(state.next_sequence, payload);
1341 state.next_sequence = state.next_sequence.saturating_add(1);
1342 state.replay.push_back(event);
1343 while state.replay.len() > FRONTEND_REPLAY_CAPACITY {
1344 state.replay.pop_front();
1345 }
1346 }
1347 result
1348 };
1349 *self
1350 .current_cancel
1351 .lock()
1352 .unwrap_or_else(std::sync::PoisonError::into_inner) = None;
1353 let lifecycle = match &outcome {
1354 Ok(reply) => json!({
1355 "type": "turn_succeeded",
1356 "schema_version": FRONTEND_EVENT_SCHEMA_VERSION,
1357 "reply": reply,
1358 }),
1359 Err(RuntimeSubmitError::Interrupted) => json!({
1360 "type": "turn_interrupted",
1361 "schema_version": FRONTEND_EVENT_SCHEMA_VERSION
1362 }),
1363 Err(error) => json!({
1364 "type": "turn_failed",
1365 "schema_version": FRONTEND_EVENT_SCHEMA_VERSION,
1366 "message": error.to_string()
1367 }),
1368 };
1369 self.publish_frontend_payload(lifecycle);
1370 submit_claim.mark_lifecycle_finished();
1371 drop(submit_claim);
1375 outcome
1376 }
1377
1378 pub async fn submit(&self, prompt: impl Into<String>) -> Result<String, RuntimeSubmitError> {
1380 let submit_claim = self.claim_submit()?;
1381 self.submit_claimed(prompt.into(), Vec::new(), submit_claim)
1382 .await
1383 }
1384
1385 pub async fn submit_with_images(
1387 &self,
1388 prompt: impl Into<String>,
1389 image_urls: Vec<String>,
1390 ) -> Result<String, RuntimeSubmitError> {
1391 let submit_claim = self.claim_submit()?;
1392 self.submit_claimed(prompt.into(), image_urls, submit_claim)
1393 .await
1394 }
1395
1396 pub fn send_input(self: &Arc<Self>, prompt: String) -> Result<(), RuntimeSubmitError> {
1399 self.send_input_with_images(prompt, Vec::new())
1400 }
1401
1402 pub fn send_input_with_images(
1405 self: &Arc<Self>,
1406 prompt: String,
1407 image_urls: Vec<String>,
1408 ) -> Result<(), RuntimeSubmitError> {
1409 let submit_claim = self.claim_submit()?;
1410 let runtime = self.clone();
1411 tokio::spawn(async move {
1412 let _ = runtime
1413 .submit_claimed(prompt, image_urls, submit_claim)
1414 .await;
1415 });
1416 Ok(())
1417 }
1418
1419 pub fn steer(&self, prompt: impl Into<String>) -> Result<(), FrontendRuntimeError> {
1423 let accepted = self
1424 .steer_queue
1425 .lock()
1426 .unwrap_or_else(std::sync::PoisonError::into_inner)
1427 .enqueue(prompt.into());
1428 if accepted {
1429 Ok(())
1430 } else {
1431 Err(FrontendRuntimeError::UnsupportedAction("steer"))
1432 }
1433 }
1434
1435 pub fn respond(&self, response: FrontendResponse) -> Result<(), FrontendRuntimeError> {
1437 self.frontend_requests
1438 .as_ref()
1439 .ok_or(FrontendRuntimeError::UnsupportedAction("respond"))?
1440 .respond(response)
1441 }
1442
1443 pub async fn invoke(
1446 &self,
1447 operation: FrontendOperationInvocation,
1448 ) -> Result<FrontendOperationResult, FrontendRuntimeError> {
1449 match operation {
1450 FrontendOperationInvocation::Prompt {
1451 operation_id,
1452 arguments,
1453 } => {
1454 let prompt_name = self
1455 .frontend_operations
1456 .iter()
1457 .find(|descriptor| {
1458 descriptor.id == operation_id
1459 && descriptor.kind == FrontendOperationKind::Prompt
1460 })
1461 .and_then(|descriptor| descriptor.command.as_ref())
1462 .map(|command| command.name.as_str())
1463 .ok_or_else(|| {
1464 FrontendRuntimeError::UnsupportedOperation(operation_id.clone())
1465 })?;
1466 let prompt = if arguments.is_empty() {
1467 format!("/{prompt_name}")
1468 } else {
1469 format!("/{prompt_name} {arguments}")
1470 };
1471 let reply = self.submit(prompt).await?;
1472 Ok(FrontendOperationResult::Prompt { reply })
1473 }
1474 FrontendOperationInvocation::Context { operation_id } => {
1475 if !self.frontend_operations.iter().any(|descriptor| {
1476 descriptor.id == operation_id
1477 && descriptor.kind == FrontendOperationKind::Context
1478 }) {
1479 return Err(FrontendRuntimeError::UnsupportedOperation(operation_id));
1480 }
1481 let usage = self.agent.lock().await.context_usage();
1485 Ok(FrontendOperationResult::Context { usage })
1486 }
1487 FrontendOperationInvocation::Model {
1488 operation_id,
1489 model,
1490 } => {
1491 if !self.frontend_operations.iter().any(|descriptor| {
1492 descriptor.id == operation_id && descriptor.kind == FrontendOperationKind::Model
1493 }) {
1494 return Err(FrontendRuntimeError::UnsupportedOperation(operation_id));
1495 }
1496 let mut agent = self.agent.lock().await;
1497 let previous = agent.model().to_string();
1498 if model.trim().is_empty() {
1499 return Ok(FrontendOperationResult::Model {
1500 model: previous.clone(),
1501 previous,
1502 });
1503 }
1504 let resolved = agent
1508 .config()
1509 .model_routing
1510 .resolve_alias(model.trim())
1511 .to_string();
1512 if let Some(refusal) = agent.config().model_routing.refusal(&resolved) {
1513 return Err(FrontendRuntimeError::UnsupportedOperation(refusal));
1514 }
1515 agent.switch_model(resolved.clone());
1520 Ok(FrontendOperationResult::Model {
1521 model: resolved,
1522 previous,
1523 })
1524 }
1525 }
1526 }
1527
1528 pub async fn interrupt(&self) -> bool {
1530 let cancel = self
1531 .current_cancel
1532 .lock()
1533 .unwrap_or_else(std::sync::PoisonError::into_inner)
1534 .clone();
1535 match cancel {
1536 Some(cancel) => {
1537 cancel.notify_one();
1538 true
1539 }
1540 None => false,
1541 }
1542 }
1543
1544 pub fn status(&self) -> RuntimeStatus {
1546 RuntimeStatus {
1547 session_id: self.session_id.clone(),
1548 model: self.model.clone(),
1549 busy: self.busy.load(Ordering::SeqCst),
1550 shutting_down: self.is_shutting_down(),
1551 }
1552 }
1553
1554 pub async fn history(&self, limit: usize) -> Vec<ChatMessage> {
1558 let history = self.history_snapshot.read().await;
1559 let start = history.len().saturating_sub(limit);
1560 history[start..].to_vec()
1561 }
1562
1563 pub async fn finalize_with<R>(&self, finalize: impl FnOnce(&SdkAgent) -> R) -> R {
1568 let agent = self.agent.lock().await;
1569 finalize(&agent)
1570 }
1571
1572 pub async fn shutdown(&self) {
1575 let _barrier = self.shutdown_barrier.lock().await;
1576 self.accepting_submits.store(false, Ordering::SeqCst);
1581 self.interrupt().await;
1582 loop {
1583 let finished = self.turn_finished.notified();
1584 if !self.busy.load(Ordering::SeqCst) {
1585 break;
1586 }
1587 finished.await;
1588 }
1589 self.signal_shutdown();
1590 }
1591
1592 pub fn is_shutting_down(&self) -> bool {
1595 self.shutting_down.load(Ordering::SeqCst)
1596 }
1597
1598 pub async fn wait_for_shutdown(&self) {
1601 if self.is_shutting_down() {
1606 return;
1607 }
1608 self.shutdown.notified().await;
1609 }
1610
1611 fn signal_shutdown(&self) {
1621 self.shutting_down.store(true, Ordering::SeqCst);
1622 self.shutdown.notify_waiters();
1623 }
1624
1625 pub async fn handle_request(self: &Arc<Self>, req: RpcRequest) -> Value {
1629 match req.method.as_str() {
1630 "submit" => self.handle_submit(req).await,
1631 "frontend.send_input" => self.handle_frontend_send_input(req),
1632 "interrupt" => self.handle_interrupt(req).await,
1633 "steer" => self.handle_steer(req),
1634 "respond" => self.handle_respond(req),
1635 "status" => self.handle_status(req).await,
1636 "history" => self.handle_history(req).await,
1637 "frontend.describe" => self.handle_frontend_describe(req),
1638 "frontend.attach" => self.handle_frontend_attach(req),
1639 "frontend.invoke" => self.handle_frontend_invoke(req).await,
1640 "shutdown" => self.handle_shutdown(req).await,
1641 other => rpc_error(req.id, -32601, format!("unknown method `{other}`")),
1642 }
1643 }
1644
1645 fn handle_frontend_send_input(self: &Arc<Self>, req: RpcRequest) -> Value {
1646 let Some(prompt) = req.params.get("prompt").and_then(Value::as_str) else {
1647 return rpc_error(
1648 req.id,
1649 -32602,
1650 "frontend.send_input requires a string `params.prompt`",
1651 );
1652 };
1653 let image_urls = match parse_image_urls(&req.params, "frontend.send_input") {
1654 Ok(image_urls) => image_urls,
1655 Err(message) => return rpc_error(req.id, -32602, message),
1656 };
1657 match self.send_input_with_images(prompt.to_string(), image_urls) {
1658 Ok(()) => rpc_ok(req.id, json!({"accepted": true})),
1659 Err(RuntimeSubmitError::Busy) => {
1660 rpc_error(req.id, -32000, "a turn is already in progress")
1661 }
1662 Err(RuntimeSubmitError::Interrupted) => rpc_error(req.id, -32001, "turn interrupted"),
1663 Err(RuntimeSubmitError::Agent(error)) => rpc_error(req.id, -32002, error),
1664 }
1665 }
1666
1667 async fn handle_submit(&self, req: RpcRequest) -> Value {
1678 let Some(prompt) = req.params.get("prompt").and_then(|v| v.as_str()) else {
1679 return rpc_error(req.id, -32602, "submit requires a string `params.prompt`");
1680 };
1681 match self.submit(prompt).await {
1682 Ok(reply) => rpc_ok(req.id, json!({"reply": reply})),
1683 Err(RuntimeSubmitError::Busy) => rpc_error(
1684 req.id,
1685 -32000,
1686 "a turn is already in progress; `interrupt` it or wait for its response before submitting another",
1687 ),
1688 Err(RuntimeSubmitError::Interrupted) => {
1689 rpc_error(req.id, -32001, "turn interrupted")
1690 }
1691 Err(RuntimeSubmitError::Agent(error)) => rpc_error(req.id, -32002, error),
1692 }
1693 }
1694
1695 async fn handle_interrupt(&self, req: RpcRequest) -> Value {
1701 if self.interrupt().await {
1702 rpc_ok(req.id, json!({"interrupted": true}))
1703 } else {
1704 rpc_ok(
1705 req.id,
1706 json!({"interrupted": false, "reason": "no turn in progress"}),
1707 )
1708 }
1709 }
1710
1711 fn handle_steer(&self, req: RpcRequest) -> Value {
1714 let Some(prompt) = req.params.get("prompt").and_then(Value::as_str) else {
1715 return rpc_error(req.id, -32602, "steer requires a string `params.prompt`");
1716 };
1717 match self.steer(prompt) {
1718 Ok(()) => rpc_ok(req.id, json!({"queued": true})),
1719 Err(error) => rpc_error(req.id, -32020, error.to_string()),
1720 }
1721 }
1722
1723 fn handle_respond(&self, req: RpcRequest) -> Value {
1724 let response = match req.params.get("response").cloned() {
1725 Some(value) => match serde_json::from_value::<FrontendResponse>(value) {
1726 Ok(response) => response,
1727 Err(error) => return rpc_error(req.id, -32602, error.to_string()),
1728 },
1729 None => return rpc_error(req.id, -32602, "respond requires `params.response`"),
1730 };
1731 match self.respond(response) {
1732 Ok(()) => rpc_ok(req.id, json!({"accepted": true})),
1733 Err(FrontendRuntimeError::UnsupportedAction(_)) => {
1734 rpc_error(req.id, -32020, "frontend respond is not enabled")
1735 }
1736 Err(FrontendRuntimeError::UnknownRequest(id)) => rpc_error(
1737 req.id,
1738 -32021,
1739 format!("frontend request {id} is not pending"),
1740 ),
1741 Err(error) => rpc_error(req.id, -32022, error.to_string()),
1742 }
1743 }
1744
1745 async fn handle_status(&self, req: RpcRequest) -> Value {
1749 rpc_ok(
1750 req.id,
1751 serde_json::to_value(self.status()).unwrap_or_default(),
1752 )
1753 }
1754
1755 async fn handle_history(&self, req: RpcRequest) -> Value {
1758 let limit = req
1759 .params
1760 .get("limit")
1761 .and_then(Value::as_u64)
1762 .unwrap_or(50)
1763 .clamp(1, SERVER_HISTORY_CAPACITY as u64) as usize;
1764 rpc_ok(req.id, json!({"messages": self.history(limit).await}))
1765 }
1766
1767 fn handle_frontend_describe(&self, req: RpcRequest) -> Value {
1768 rpc_ok(
1769 req.id,
1770 serde_json::to_value(self.frontend_descriptor()).unwrap_or_default(),
1771 )
1772 }
1773
1774 fn handle_frontend_attach(&self, req: RpcRequest) -> Value {
1775 let limit = req
1776 .params
1777 .get("limit")
1778 .and_then(Value::as_u64)
1779 .unwrap_or(50)
1780 .clamp(1, SERVER_HISTORY_CAPACITY as u64) as usize;
1781 match self.frontend_snapshot(limit) {
1782 Ok(snapshot) => rpc_ok(req.id, serde_json::to_value(snapshot).unwrap_or_default()),
1783 Err(error) => rpc_error(req.id, -32010, error.to_string()),
1784 }
1785 }
1786
1787 async fn handle_frontend_invoke(&self, req: RpcRequest) -> Value {
1788 let operation = match req.params.get("operation").cloned() {
1789 Some(value) => match serde_json::from_value::<FrontendOperationInvocation>(value) {
1790 Ok(operation) => operation,
1791 Err(error) => return rpc_error(req.id, -32602, error.to_string()),
1792 },
1793 None => {
1794 return rpc_error(
1795 req.id,
1796 -32602,
1797 "frontend.invoke requires `params.operation`",
1798 )
1799 }
1800 };
1801 match self.invoke(operation).await {
1802 Ok(result) => rpc_ok(req.id, serde_json::to_value(result).unwrap_or_default()),
1803 Err(FrontendRuntimeError::UnsupportedOperation(id)) => rpc_error(
1804 req.id,
1805 -32023,
1806 FrontendRuntimeError::UnsupportedOperation(id).to_string(),
1807 ),
1808 Err(FrontendRuntimeError::Submit(RuntimeSubmitError::Busy)) => {
1809 rpc_error(req.id, -32000, "a turn is already in progress")
1810 }
1811 Err(error) => rpc_error(req.id, -32022, error.to_string()),
1812 }
1813 }
1814
1815 async fn handle_shutdown(&self, req: RpcRequest) -> Value {
1821 self.shutdown().await;
1822 rpc_ok(req.id, json!({"shutting_down": true}))
1823 }
1824}
1825
1826fn compact_frontend_request_history(replay: &VecDeque<FrontendEvent>) -> Vec<Value> {
1833 let mut by_id: BTreeMap<u64, (Option<Value>, Option<Value>)> = BTreeMap::new();
1834 for event in replay {
1835 let (request_id, resolved) = match event.kind.as_str() {
1836 "request" => (
1837 event.payload.pointer("/request/id").and_then(Value::as_u64),
1838 false,
1839 ),
1840 "request_resolved" => (
1841 event.payload.get("request_id").and_then(Value::as_u64),
1842 true,
1843 ),
1844 _ => continue,
1845 };
1846 let Some(request_id) = request_id else {
1847 continue;
1848 };
1849 let entry = by_id.entry(request_id).or_default();
1850 let slot = if resolved { &mut entry.1 } else { &mut entry.0 };
1851 slot.get_or_insert_with(|| event.payload.clone());
1852 }
1853 by_id
1854 .into_values()
1855 .flat_map(|(request, resolution)| request.into_iter().chain(resolution))
1856 .collect()
1857}
1858
1859fn valid_frontend_command_name(name: &str) -> bool {
1860 !name.is_empty()
1861 && !name.starts_with('/')
1862 && name
1863 .chars()
1864 .all(|character| !character.is_whitespace() && !character.is_control())
1865}
1866
1867fn bounded_history_snapshot(history: &[ChatMessage]) -> Vec<ChatMessage> {
1868 let start = history.len().saturating_sub(SERVER_HISTORY_CAPACITY);
1869 history[start..].to_vec()
1870}
1871
1872#[cfg(feature = "adapter-api")]
1897pub async fn run_stdio<R, W>(engine: Arc<RpcEngine>, reader: R, writer: W) -> std::io::Result<()>
1898where
1899 R: AsyncBufRead + Unpin + Send + 'static,
1900 W: AsyncWrite + Unpin + Send + 'static,
1901{
1902 let (out_tx, mut out_rx) = mpsc::unbounded_channel::<Value>();
1903
1904 let writer_task = tokio::spawn(async move {
1905 let mut writer = writer;
1906 while let Some(v) = out_rx.recv().await {
1907 let line = format!("{v}\n");
1908 if writer.write_all(line.as_bytes()).await.is_err() {
1909 break;
1910 }
1911 if writer.flush().await.is_err() {
1912 break;
1913 }
1914 }
1915 });
1916
1917 let mut events = engine.subscribe();
1918 let evt_tx = out_tx.clone();
1919 let evt_engine = engine.clone();
1920 let event_task = tokio::spawn(async move {
1921 loop {
1922 tokio::select! {
1923 biased;
1924 _ = evt_engine.wait_for_shutdown() => break,
1925 recv = events.recv() => {
1926 match recv {
1927 Ok(v) => {
1928 if evt_tx.send(json!({"event": v})).is_err() {
1929 break;
1930 }
1931 }
1932 Err(broadcast::error::RecvError::Lagged(_)) => continue,
1933 Err(broadcast::error::RecvError::Closed) => break,
1934 }
1935 }
1936 }
1937 }
1938 });
1939
1940 let mut reader = reader;
1941 loop {
1942 if engine.is_shutting_down() {
1943 break;
1944 }
1945 tokio::select! {
1946 biased;
1947 _ = engine.wait_for_shutdown() => break,
1948 line = read_bounded_line(&mut reader, SERVER_MAX_LINE_BYTES) => {
1949 match line {
1950 Ok(None) => {
1951 engine.signal_shutdown();
1959 break;
1960 }
1961 Ok(Some(text)) => {
1962 let text = text.trim();
1963 if text.is_empty() {
1964 continue;
1965 }
1966 match serde_json::from_str::<RpcRequest>(text) {
1967 Ok(req) => {
1968 let engine = engine.clone();
1969 let out_tx = out_tx.clone();
1970 tokio::spawn(async move {
1971 let resp = engine.handle_request(req).await;
1972 let _ = out_tx.send(resp);
1973 });
1974 }
1975 Err(e) => {
1976 let _ = out_tx.send(rpc_error(Value::Null, -32700, format!("parse error: {e}")));
1977 }
1978 }
1979 }
1980 Err(e) => {
1981 let _ = out_tx.send(rpc_error(Value::Null, -32700, format!("{e}")));
1982 }
1983 }
1984 }
1985 }
1986 }
1987 drop(out_tx);
1997 let _ = event_task.await;
1998 let _ = writer_task.await;
1999 Ok(())
2000}
2001
2002#[cfg(feature = "adapter-api")]
2005struct HttpRequest {
2006 method: String,
2007 path: String,
2009 query: String,
2010 headers: HashMap<String, String>,
2011 body: Vec<u8>,
2012}
2013
2014#[cfg(feature = "adapter-api")]
2022async fn read_http_request<R>(reader: &mut R) -> std::io::Result<Option<HttpRequest>>
2023where
2024 R: AsyncBufRead + AsyncRead + Unpin,
2025{
2026 const HEAD_LINE_CAP: usize = 8 * 1024;
2027 let Some(request_line) = read_bounded_line(reader, HEAD_LINE_CAP).await? else {
2028 return Ok(None);
2029 };
2030 let mut parts = request_line.split_whitespace();
2031 let method = parts.next().unwrap_or("").to_string();
2032 let target = parts.next().unwrap_or("").to_string();
2033 if method.is_empty() || target.is_empty() {
2034 return Err(std::io::Error::new(
2035 std::io::ErrorKind::InvalidData,
2036 "malformed request line",
2037 ));
2038 }
2039 let (path, query) = match target.split_once('?') {
2040 Some((p, q)) => (p.to_string(), q.to_string()),
2041 None => (target, String::new()),
2042 };
2043
2044 let mut headers = HashMap::new();
2045 let mut content_length: usize = 0;
2046 for _ in 0..MAX_HEADER_LINES {
2047 let Some(line) = read_bounded_line(reader, HEAD_LINE_CAP).await? else {
2048 return Ok(None);
2049 };
2050 if line.is_empty() {
2051 break;
2052 }
2053 if let Some((k, v)) = line.split_once(':') {
2054 let k = k.trim().to_ascii_lowercase();
2055 let v = v.trim().to_string();
2056 if k == "content-length" {
2057 content_length = v.parse().unwrap_or(0);
2058 }
2059 headers.insert(k, v);
2060 }
2061 }
2062 if content_length > SERVER_MAX_LINE_BYTES {
2063 return Err(std::io::Error::new(
2064 std::io::ErrorKind::InvalidData,
2065 format!("request body exceeded {SERVER_MAX_LINE_BYTES} byte cap"),
2066 ));
2067 }
2068 let mut body = vec![0u8; content_length];
2069 if content_length > 0 {
2070 reader.read_exact(&mut body).await?;
2071 }
2072 Ok(Some(HttpRequest {
2073 method,
2074 path,
2075 query,
2076 headers,
2077 body,
2078 }))
2079}
2080
2081#[cfg(feature = "adapter-api")]
2082async fn write_http_response<W: AsyncWrite + Unpin>(
2083 writer: &mut W,
2084 status: u16,
2085 reason: &str,
2086 content_type: &str,
2087 body: &[u8],
2088) -> std::io::Result<()> {
2089 let head = format!(
2090 "HTTP/1.1 {status} {reason}\r\nContent-Type: {content_type}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
2091 body.len()
2092 );
2093 writer.write_all(head.as_bytes()).await?;
2094 writer.write_all(body).await?;
2095 writer.flush().await
2096}
2097
2098#[cfg(feature = "adapter-api")]
2099fn browser_observer_asset(path: &str) -> Option<(&'static str, &'static [u8])> {
2100 match path {
2101 "/observer" | "/observer/" => Some((
2102 "text/html; charset=utf-8",
2103 include_bytes!("../embedded/frontend-browser/index.html"),
2104 )),
2105 "/observer/app.mjs" => Some((
2106 "text/javascript; charset=utf-8",
2107 include_bytes!("../embedded/frontend-browser/app.mjs"),
2108 )),
2109 "/observer/client.mjs" => Some((
2110 "text/javascript; charset=utf-8",
2111 include_bytes!("../embedded/frontend-browser/client.mjs"),
2112 )),
2113 "/observer/view.mjs" => Some((
2114 "text/javascript; charset=utf-8",
2115 include_bytes!("../embedded/frontend-browser/view.mjs"),
2116 )),
2117 "/observer/style.css" => Some((
2118 "text/css; charset=utf-8",
2119 include_bytes!("../embedded/frontend-browser/style.css"),
2120 )),
2121 "/observer/favicon.svg" | "/favicon.ico" => Some((
2122 "image/svg+xml",
2123 include_bytes!("../embedded/frontend-browser/favicon.svg"),
2124 )),
2125 "/frontend/client.mjs" => Some((
2126 "text/javascript; charset=utf-8",
2127 include_bytes!("../embedded/frontend/client.mjs"),
2128 )),
2129 "/frontend/generated-client.mjs" => Some((
2130 "text/javascript; charset=utf-8",
2131 include_bytes!("../embedded/frontend/generated-client.mjs"),
2132 )),
2133 "/frontend/generated.mjs" => Some((
2134 "text/javascript; charset=utf-8",
2135 include_bytes!("../embedded/frontend/generated.mjs"),
2136 )),
2137 _ => None,
2138 }
2139}
2140
2141#[cfg(feature = "adapter-api")]
2142async fn write_browser_observer_asset<W: AsyncWrite + Unpin>(
2143 writer: &mut W,
2144 content_type: &str,
2145 body: &[u8],
2146) -> std::io::Result<()> {
2147 let head = format!(
2148 "HTTP/1.1 200 OK\r\n\
2149 Content-Type: {content_type}\r\n\
2150 Content-Length: {}\r\n\
2151 Cache-Control: no-store\r\n\
2152 Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self'; connect-src 'self'; img-src 'self'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'\r\n\
2153 Referrer-Policy: no-referrer\r\n\
2154 X-Content-Type-Options: nosniff\r\n\
2155 Connection: close\r\n\r\n",
2156 body.len()
2157 );
2158 writer.write_all(head.as_bytes()).await?;
2159 writer.write_all(body).await?;
2160 writer.flush().await
2161}
2162
2163#[cfg(feature = "adapter-api")]
2165#[derive(Clone)]
2166pub struct RuntimeHttpCredential {
2167 token: Arc<str>,
2168 authorization: RuntimeAuthorization,
2169 client_id: Option<crate::RuntimeClientId>,
2170 bootstrap: bool,
2171 runtime_id: Option<Arc<str>>,
2172 generation: Option<[u8; 16]>,
2173 revocation: Option<Arc<RuntimeCredentialRevocation>>,
2174}
2175
2176#[cfg(feature = "adapter-api")]
2177impl RuntimeHttpCredential {
2178 pub fn new(token: impl Into<Arc<str>>, authorization: RuntimeAuthorization) -> Self {
2181 Self {
2182 token: token.into(),
2183 authorization,
2184 client_id: None,
2185 bootstrap: false,
2186 runtime_id: None,
2187 generation: None,
2188 revocation: None,
2189 }
2190 }
2191
2192 pub fn owner(token: impl Into<Arc<str>>) -> Self {
2197 Self {
2198 token: token.into(),
2199 authorization: RuntimeAuthorization::owner(),
2200 client_id: None,
2201 bootstrap: true,
2202 runtime_id: None,
2203 generation: None,
2204 revocation: None,
2205 }
2206 }
2207
2208 pub fn observer(token: impl Into<Arc<str>>) -> Self {
2210 Self::new(token, RuntimeAuthorization::observer())
2211 }
2212
2213 fn frontend(
2214 token: impl Into<Arc<str>>,
2215 client_id: crate::RuntimeClientId,
2216 authorization: RuntimeAuthorization,
2217 runtime_id: impl Into<Arc<str>>,
2218 generation: [u8; 16],
2219 ) -> Self {
2220 Self {
2221 token: token.into(),
2222 authorization,
2223 client_id: Some(client_id),
2224 bootstrap: false,
2225 runtime_id: Some(runtime_id.into()),
2226 generation: Some(generation),
2227 revocation: Some(Arc::new(RuntimeCredentialRevocation::new())),
2228 }
2229 }
2230}
2231
2232#[cfg(feature = "adapter-api")]
2233struct AuthenticatedRuntimeHttpCredential {
2234 authorization: RuntimeAuthorization,
2235 client_id: Option<crate::RuntimeClientId>,
2236 bootstrap: bool,
2237 revocation: Option<tokio::sync::watch::Receiver<bool>>,
2238 attachment: Option<RuntimeCredentialAttachment>,
2239 via_bearer_header: bool,
2240}
2241
2242#[cfg(feature = "adapter-api")]
2243struct RuntimeCredentialRevocation {
2244 signal: tokio::sync::watch::Sender<bool>,
2245 active_attachments: AtomicUsize,
2246 drained: tokio::sync::Notify,
2247}
2248
2249#[cfg(feature = "adapter-api")]
2250impl RuntimeCredentialRevocation {
2251 fn new() -> Self {
2252 let (signal, _) = tokio::sync::watch::channel(false);
2253 Self {
2254 signal,
2255 active_attachments: AtomicUsize::new(0),
2256 drained: tokio::sync::Notify::new(),
2257 }
2258 }
2259
2260 fn register(self: &Arc<Self>) -> RuntimeCredentialAttachment {
2261 self.active_attachments.fetch_add(1, Ordering::AcqRel);
2262 RuntimeCredentialAttachment {
2263 revocation: self.clone(),
2264 }
2265 }
2266
2267 async fn revoke_and_wait(&self) {
2268 let _ = self.signal.send(true);
2269 loop {
2270 let drained = self.drained.notified();
2271 if self.active_attachments.load(Ordering::Acquire) == 0 {
2272 return;
2273 }
2274 drained.await;
2275 }
2276 }
2277}
2278
2279#[cfg(feature = "adapter-api")]
2280struct RuntimeCredentialAttachment {
2281 revocation: Arc<RuntimeCredentialRevocation>,
2282}
2283
2284#[cfg(feature = "adapter-api")]
2285impl Drop for RuntimeCredentialAttachment {
2286 fn drop(&mut self) {
2287 if self
2288 .revocation
2289 .active_attachments
2290 .fetch_sub(1, Ordering::AcqRel)
2291 == 1
2292 {
2293 self.revocation.drained.notify_one();
2298 }
2299 }
2300}
2301
2302#[cfg(feature = "adapter-api")]
2303struct IssuedRuntimeHttpCredential(Arc<str>);
2304
2305#[cfg(feature = "adapter-api")]
2306impl IssuedRuntimeHttpCredential {
2307 fn as_bytes(&self) -> &[u8] {
2308 self.0.as_bytes()
2309 }
2310}
2311
2312#[cfg(feature = "adapter-api")]
2313struct RuntimeHttpCredentialRegistry {
2314 credentials: StdMutex<Vec<RuntimeHttpCredential>>,
2315 runtime_id: String,
2316 generation: [u8; 16],
2317}
2318
2319#[cfg(feature = "adapter-api")]
2320impl RuntimeHttpCredentialRegistry {
2321 fn new(
2322 runtime_id: impl Into<String>,
2323 credentials: Vec<RuntimeHttpCredential>,
2324 ) -> std::io::Result<Arc<Self>> {
2325 let mut generation = [0_u8; 16];
2326 getrandom::getrandom(&mut generation).map_err(|error| {
2327 std::io::Error::other(format!(
2328 "cannot create runtime credential generation: {error}"
2329 ))
2330 })?;
2331 Ok(Arc::new(Self {
2332 credentials: StdMutex::new(credentials),
2333 runtime_id: runtime_id.into(),
2334 generation,
2335 }))
2336 }
2337
2338 fn authenticate(&self, request: &HttpRequest) -> Option<AuthenticatedRuntimeHttpCredential> {
2339 let credentials = self
2340 .credentials
2341 .lock()
2342 .unwrap_or_else(std::sync::PoisonError::into_inner);
2343 debug_assert!(credentials.iter().all(|credential| {
2344 credential.client_id.is_none()
2345 || (credential.runtime_id.as_deref() == Some(self.runtime_id.as_str())
2346 && credential.generation == Some(self.generation))
2347 }));
2348 check_auth(request, &credentials)
2349 }
2350
2351 fn issue_frontend(
2352 &self,
2353 client_id: crate::RuntimeClientId,
2354 observer: bool,
2355 ) -> std::io::Result<IssuedRuntimeHttpCredential> {
2356 let authorization = if observer {
2357 RuntimeAuthorization::observer()
2358 } else {
2359 RuntimeAuthorization::interactive()
2360 };
2361 for _ in 0..3 {
2362 let mut secret = [0_u8; 32];
2363 getrandom::getrandom(&mut secret).map_err(|error| {
2364 std::io::Error::other(format!("cannot mint frontend credential: {error}"))
2365 })?;
2366 let token: Arc<str> = encode_credential(&secret).into();
2367 secret.fill(0);
2368 let mut credentials = self
2369 .credentials
2370 .lock()
2371 .unwrap_or_else(std::sync::PoisonError::into_inner);
2372 if credentials
2373 .iter()
2374 .any(|credential| constant_time_eq(token.as_bytes(), credential.token.as_bytes()))
2375 {
2376 continue;
2377 }
2378 credentials.push(RuntimeHttpCredential::frontend(
2379 token.clone(),
2380 client_id,
2381 authorization,
2382 self.runtime_id.clone(),
2383 self.generation,
2384 ));
2385 return Ok(IssuedRuntimeHttpCredential(token));
2386 }
2387 Err(std::io::Error::new(
2388 std::io::ErrorKind::AlreadyExists,
2389 "frontend credential collision limit exceeded",
2390 ))
2391 }
2392
2393 async fn revoke_client(&self, client_id: &crate::RuntimeClientId) -> bool {
2394 let revocations = {
2397 let mut credentials = self
2398 .credentials
2399 .lock()
2400 .unwrap_or_else(std::sync::PoisonError::into_inner);
2401 let mut revocations = Vec::new();
2402 credentials.retain(|credential| {
2403 if credential.client_id.as_ref() == Some(client_id) {
2404 if let Some(revocation) = &credential.revocation {
2405 revocations.push(revocation.clone());
2406 }
2407 false
2408 } else {
2409 true
2410 }
2411 });
2412 revocations
2413 };
2414 let revoked = !revocations.is_empty();
2415 for revocation in revocations {
2416 revocation.revoke_and_wait().await;
2417 }
2418 revoked
2419 }
2420}
2421
2422#[cfg(feature = "adapter-api")]
2423fn encode_credential(secret: &[u8; 32]) -> String {
2424 const HEX: &[u8; 16] = b"0123456789abcdef";
2425 let mut encoded = String::with_capacity(64);
2426 for byte in secret {
2427 encoded.push(HEX[(byte >> 4) as usize] as char);
2428 encoded.push(HEX[(byte & 0x0f) as usize] as char);
2429 }
2430 encoded
2431}
2432
2433#[cfg(feature = "adapter-api")]
2440fn check_auth(
2441 req: &HttpRequest,
2442 credentials: &[RuntimeHttpCredential],
2443) -> Option<AuthenticatedRuntimeHttpCredential> {
2444 if let Some(auth) = req.headers.get("authorization") {
2445 if let Some(t) = auth.strip_prefix("Bearer ") {
2446 for credential in credentials {
2447 if constant_time_eq(t.as_bytes(), credential.token.as_bytes()) {
2448 return Some(AuthenticatedRuntimeHttpCredential {
2449 authorization: credential.authorization.clone(),
2450 client_id: credential.client_id.clone(),
2451 bootstrap: credential.bootstrap,
2452 revocation: credential
2453 .revocation
2454 .as_ref()
2455 .map(|revocation| revocation.signal.subscribe()),
2456 attachment: credential.revocation.as_ref().and_then(|revocation| {
2457 matches!(req.path.as_str(), "/events" | "/frontend/events")
2458 .then(|| revocation.register())
2459 }),
2460 via_bearer_header: true,
2461 });
2462 }
2463 }
2464 }
2465 }
2466 for pair in req.query.split('&') {
2467 if let Some((k, v)) = pair.split_once('=') {
2468 if k == "token" {
2469 for credential in credentials
2470 .iter()
2471 .filter(|credential| credential.client_id.is_none())
2472 {
2473 if constant_time_eq(v.as_bytes(), credential.token.as_bytes()) {
2474 return Some(AuthenticatedRuntimeHttpCredential {
2475 authorization: credential.authorization.clone(),
2476 client_id: credential.client_id.clone(),
2477 bootstrap: credential.bootstrap,
2478 revocation: None,
2479 attachment: None,
2480 via_bearer_header: false,
2481 });
2482 }
2483 }
2484 }
2485 }
2486 }
2487 None
2488}
2489
2490#[cfg(feature = "adapter-api")]
2491fn coordinated_http_client(
2492 request: &HttpRequest,
2493 coordinator: &Arc<CoordinatedRuntime>,
2494 credential: AuthenticatedRuntimeHttpCredential,
2495) -> Result<Arc<CoordinatedRuntimeClient>, crate::RuntimeLeaseError> {
2496 let supplied_client_id = request
2500 .headers
2501 .get("x-supercode-client-id")
2502 .map(String::as_str);
2503 let client_id = match credential.client_id.as_ref() {
2504 Some(bound) if supplied_client_id == Some(bound.as_str()) => bound.as_str(),
2505 Some(_) => return Err(crate::RuntimeLeaseError::InvalidClientId),
2506 None => supplied_client_id.unwrap_or("legacy-owner"),
2507 };
2508 let mut authorization = credential.authorization;
2509 if let Some(requested) = request.headers.get("x-supercode-permissions") {
2510 authorization = authorization.restrict_to(&RuntimeAuthorization::parse_header(requested)?);
2511 }
2512 Ok(coordinator.client(RuntimeClientId::parse(client_id)?, authorization))
2513}
2514
2515#[cfg(feature = "adapter-api")]
2516async fn coordinated_runtime_rpc(
2517 client: Arc<CoordinatedRuntimeClient>,
2518 request: RpcRequest,
2519) -> Value {
2520 let id = request.id.clone();
2521 let method = crate::FrontendFacadeMethod::from_wire_name(&request.method);
2522 let result = match method {
2523 Some(crate::FrontendFacadeMethod::TakeControl) => client
2524 .take_control()
2525 .and_then(|snapshot| serde_json::to_value(snapshot).map_err(json_sdk_error)),
2526 Some(crate::FrontendFacadeMethod::Heartbeat) => client
2527 .heartbeat()
2528 .and_then(|snapshot| serde_json::to_value(snapshot).map_err(json_sdk_error)),
2529 Some(crate::FrontendFacadeMethod::Lease) => client
2530 .lease_snapshot()
2531 .and_then(|snapshot| serde_json::to_value(snapshot).map_err(json_sdk_error)),
2532 Some(crate::FrontendFacadeMethod::Detach) => {
2533 serde_json::to_value(client.detach()).map_err(json_sdk_error)
2534 }
2535 Some(crate::FrontendFacadeMethod::Close) => match client.close().await {
2536 Ok(()) => Ok(json!({"closed":true})),
2537 Err(error) => Err(error),
2538 },
2539 None if request.method == "shutdown" => match client.close().await {
2540 Ok(()) => Ok(json!({"shutting_down":true})),
2541 Err(error) => Err(error),
2542 },
2543 _ => return frontend_http_rpc(client, request).await,
2544 };
2545 match result {
2546 Ok(value) => rpc_ok(id, value),
2547 Err(error) => sdk_runtime_rpc_error(id, -32002, &error),
2548 }
2549}
2550
2551#[cfg(feature = "adapter-api")]
2552fn json_sdk_error(error: serde_json::Error) -> FrontendRuntimeError {
2553 FrontendRuntimeError::Transport(error.to_string())
2554}
2555
2556#[cfg(feature = "adapter-api")]
2557async fn handle_http_conn(
2558 stream: tokio::net::TcpStream,
2559 engine: Arc<RpcEngine>,
2560 coordinator: Arc<CoordinatedRuntime>,
2561 credentials: Arc<RuntimeHttpCredentialRegistry>,
2562) -> std::io::Result<()> {
2563 let peer_is_loopback = stream.peer_addr()?.ip().is_loopback();
2564 let (read_half, mut write_half) = stream.into_split();
2565 let mut reader = tokio::io::BufReader::new(read_half);
2566 let Some(req) = read_http_request(&mut reader).await? else {
2567 return Ok(());
2568 };
2569
2570 if req.method == "GET" {
2574 if let Some((content_type, body)) = browser_observer_asset(&req.path) {
2575 return write_browser_observer_asset(&mut write_half, content_type, body).await;
2576 }
2577 }
2578
2579 let Some(credential) = credentials.authenticate(&req) else {
2580 let body =
2581 sdk_runtime_rpc_error(Value::Null, -32030, &FrontendRuntimeError::Unauthenticated)
2582 .to_string();
2583 return write_http_response(
2584 &mut write_half,
2585 401,
2586 "Unauthorized",
2587 "application/json",
2588 body.as_bytes(),
2589 )
2590 .await;
2591 };
2592
2593 if matches!(
2594 req.path.as_str(),
2595 "/_supercode/frontend-credentials/mint" | "/_supercode/frontend-credentials/revoke"
2596 ) && !credential.via_bearer_header
2597 {
2598 let body =
2599 sdk_runtime_rpc_error(Value::Null, -32030, &FrontendRuntimeError::Unauthenticated)
2600 .to_string();
2601 return write_http_response(
2602 &mut write_half,
2603 401,
2604 "Unauthorized",
2605 "application/json",
2606 body.as_bytes(),
2607 )
2608 .await;
2609 }
2610
2611 if req.path == "/_supercode/frontend-credentials/mint" {
2612 if req.method != "POST" || !peer_is_loopback || !credential.bootstrap {
2613 let body = sdk_runtime_rpc_error(
2614 Value::Null,
2615 -32031,
2616 &FrontendRuntimeError::Unauthorized {
2617 permission: "bootstrap".into(),
2618 },
2619 )
2620 .to_string();
2621 return write_http_response(
2622 &mut write_half,
2623 403,
2624 "Forbidden",
2625 "application/json",
2626 body.as_bytes(),
2627 )
2628 .await;
2629 }
2630 let request: Value = match serde_json::from_slice(&req.body) {
2631 Ok(request) => request,
2632 Err(error) => {
2633 return write_http_response(
2634 &mut write_half,
2635 400,
2636 "Bad Request",
2637 "application/json",
2638 format!("{{\"error\":{}}}", json!(error.to_string())).as_bytes(),
2639 )
2640 .await;
2641 }
2642 };
2643 let Some(client_id) = request.get("clientId").and_then(Value::as_str) else {
2644 return write_http_response(
2645 &mut write_half,
2646 400,
2647 "Bad Request",
2648 "application/json",
2649 b"{\"error\":\"mint request omitted clientId\"}",
2650 )
2651 .await;
2652 };
2653 let client_id = match crate::RuntimeClientId::parse(client_id) {
2654 Ok(client_id) => client_id,
2655 Err(error) => {
2656 return write_http_response(
2657 &mut write_half,
2658 400,
2659 "Bad Request",
2660 "application/json",
2661 format!("{{\"error\":{}}}", json!(error.to_string())).as_bytes(),
2662 )
2663 .await;
2664 }
2665 };
2666 let observer = match request.get("grant").and_then(Value::as_str) {
2667 Some("observer") => true,
2668 Some("interactive") => false,
2669 _ => {
2670 return write_http_response(
2671 &mut write_half,
2672 400,
2673 "Bad Request",
2674 "application/json",
2675 b"{\"error\":\"grant must be observer or interactive\"}",
2676 )
2677 .await;
2678 }
2679 };
2680 let token = credentials.issue_frontend(client_id, observer)?;
2681 return write_http_response(
2682 &mut write_half,
2683 200,
2684 "OK",
2685 "application/octet-stream",
2686 token.as_bytes(),
2687 )
2688 .await;
2689 }
2690
2691 if req.path == "/_supercode/frontend-credentials/revoke" {
2692 if req.method != "POST" || !peer_is_loopback || !credential.bootstrap {
2693 let body = sdk_runtime_rpc_error(
2694 Value::Null,
2695 -32031,
2696 &FrontendRuntimeError::Unauthorized {
2697 permission: "bootstrap".into(),
2698 },
2699 )
2700 .to_string();
2701 return write_http_response(
2702 &mut write_half,
2703 403,
2704 "Forbidden",
2705 "application/json",
2706 body.as_bytes(),
2707 )
2708 .await;
2709 }
2710 let request: Value = match serde_json::from_slice(&req.body) {
2711 Ok(request) => request,
2712 Err(error) => {
2713 return write_http_response(
2714 &mut write_half,
2715 400,
2716 "Bad Request",
2717 "application/json",
2718 format!("{{\"error\":{}}}", json!(error.to_string())).as_bytes(),
2719 )
2720 .await;
2721 }
2722 };
2723 let Some(client_id) = request.get("clientId").and_then(Value::as_str) else {
2724 return write_http_response(
2725 &mut write_half,
2726 400,
2727 "Bad Request",
2728 "application/json",
2729 b"{\"error\":\"revoke request omitted clientId\"}",
2730 )
2731 .await;
2732 };
2733 let client_id = match crate::RuntimeClientId::parse(client_id) {
2734 Ok(client_id) => client_id,
2735 Err(error) => {
2736 return write_http_response(
2737 &mut write_half,
2738 400,
2739 "Bad Request",
2740 "application/json",
2741 format!("{{\"error\":{}}}", json!(error.to_string())).as_bytes(),
2742 )
2743 .await;
2744 }
2745 };
2746 let revoked = credentials.revoke_client(&client_id).await;
2747 if revoked {
2748 coordinator
2749 .client(client_id, RuntimeAuthorization::observer())
2750 .detach();
2751 }
2752 return write_http_response(
2753 &mut write_half,
2754 200,
2755 "OK",
2756 "application/json",
2757 if revoked {
2758 b"{\"revoked\":true}"
2759 } else {
2760 b"{\"revoked\":false}"
2761 },
2762 )
2763 .await;
2764 }
2765
2766 let mut revocation = credential.revocation.clone();
2767 let mut attachment = credential.attachment;
2768 let credential = AuthenticatedRuntimeHttpCredential {
2769 authorization: credential.authorization,
2770 client_id: credential.client_id,
2771 bootstrap: credential.bootstrap,
2772 revocation: None,
2773 attachment: None,
2774 via_bearer_header: credential.via_bearer_header,
2775 };
2776 let client = match coordinated_http_client(&req, &coordinator, credential) {
2777 Ok(client) => client,
2778 Err(error) => {
2779 let permission = match error {
2780 crate::RuntimeLeaseError::InvalidClientId => "client_id",
2781 crate::RuntimeLeaseError::InvalidAuthorization => "authorization",
2782 _ => "runtime",
2783 };
2784 let body = sdk_runtime_rpc_error(
2785 Value::Null,
2786 -32031,
2787 &FrontendRuntimeError::Unauthorized {
2788 permission: permission.into(),
2789 },
2790 )
2791 .to_string();
2792 return write_http_response(
2793 &mut write_half,
2794 403,
2795 "Forbidden",
2796 "application/json",
2797 body.as_bytes(),
2798 )
2799 .await;
2800 }
2801 };
2802
2803 match (req.method.as_str(), req.path.as_str()) {
2804 ("POST", "/rpc") => {
2805 let body_text = String::from_utf8_lossy(&req.body);
2806 let resp = match serde_json::from_str::<RpcRequest>(&body_text) {
2807 Ok(rpc_req) if matches!(rpc_req.method.as_str(), "status" | "history") => {
2808 engine.handle_request(rpc_req).await
2809 }
2810 Ok(rpc_req) => coordinated_runtime_rpc(client.clone(), rpc_req).await,
2811 Err(e) => rpc_error(Value::Null, -32700, format!("parse error: {e}")),
2812 };
2813 let body = resp.to_string();
2814 write_http_response(
2815 &mut write_half,
2816 200,
2817 "OK",
2818 "application/json",
2819 body.as_bytes(),
2820 )
2821 .await
2822 }
2823 ("GET", "/events") => {
2824 if let Err(error) = client.observe() {
2825 let body = sdk_runtime_rpc_error(Value::Null, -32002, &error).to_string();
2826 return write_http_response(
2827 &mut write_half,
2828 403,
2829 "Forbidden",
2830 "application/json",
2831 body.as_bytes(),
2832 )
2833 .await;
2834 }
2835 let mut events = engine.subscribe();
2839 let head = "HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nCache-Control: no-cache\r\nConnection: close\r\n\r\n";
2840 if write_half.write_all(head.as_bytes()).await.is_err() {
2841 client.detach();
2842 return Ok(());
2843 }
2844 let _ = write_half.flush().await;
2845 loop {
2846 tokio::select! {
2847 biased;
2848 recv = events.recv() => {
2849 match recv {
2850 Ok(v) => {
2851 let line = format!("data: {v}\n\n");
2852 if write_half.write_all(line.as_bytes()).await.is_err() {
2853 break;
2854 }
2855 if write_half.flush().await.is_err() {
2856 break;
2857 }
2858 }
2859 Err(broadcast::error::RecvError::Lagged(_)) => continue,
2860 Err(broadcast::error::RecvError::Closed) => break,
2861 }
2862 }
2863 _ = engine.wait_for_shutdown() => break,
2868 _ = wait_for_credential_revocation(&mut revocation) => break,
2869 _ = reader.read_u8() => break,
2870 }
2871 }
2872 let _ = write_half.shutdown().await;
2873 client.detach();
2874 drop(attachment.take());
2875 Ok(())
2876 }
2877 ("GET", "/frontend/events") => {
2878 if let Err(error) = client.observe() {
2879 let body = sdk_runtime_rpc_error(Value::Null, -32002, &error).to_string();
2880 return write_http_response(
2881 &mut write_half,
2882 403,
2883 "Forbidden",
2884 "application/json",
2885 body.as_bytes(),
2886 )
2887 .await;
2888 }
2889 let mut events = engine.frontend_subscribe();
2894 let head = "HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nCache-Control: no-cache\r\nConnection: close\r\n\r\n";
2895 if write_half.write_all(head.as_bytes()).await.is_err() {
2896 client.detach();
2897 return Ok(());
2898 }
2899 let _ = write_half.flush().await;
2900 loop {
2901 tokio::select! {
2902 biased;
2903 recv = events.recv() => {
2904 match recv {
2905 Ok(event) => {
2906 let value = serde_json::to_string(&event).unwrap_or_default();
2907 let line = format!("data: {value}\n\n");
2908 if write_half.write_all(line.as_bytes()).await.is_err() {
2909 break;
2910 }
2911 if write_half.flush().await.is_err() {
2912 break;
2913 }
2914 }
2915 Err(broadcast::error::RecvError::Lagged(_)) => break,
2916 Err(broadcast::error::RecvError::Closed) => break,
2917 }
2918 }
2919 _ = engine.wait_for_shutdown() => break,
2923 _ = wait_for_credential_revocation(&mut revocation) => break,
2924 _ = reader.read_u8() => break,
2925 }
2926 }
2927 let _ = write_half.shutdown().await;
2928 client.detach();
2929 drop(attachment.take());
2930 Ok(())
2931 }
2932 _ => {
2933 write_http_response(
2934 &mut write_half,
2935 404,
2936 "Not Found",
2937 "application/json",
2938 b"{\"error\":\"not found\"}",
2939 )
2940 .await
2941 }
2942 }
2943}
2944
2945#[cfg(feature = "adapter-api")]
2946async fn wait_for_credential_revocation(receiver: &mut Option<tokio::sync::watch::Receiver<bool>>) {
2947 let Some(receiver) = receiver else {
2948 std::future::pending::<()>().await;
2949 return;
2950 };
2951 if *receiver.borrow() {
2952 return;
2953 }
2954 while receiver.changed().await.is_ok() {
2955 if *receiver.borrow() {
2956 return;
2957 }
2958 }
2959}
2960
2961#[cfg(feature = "adapter-api")]
2962async fn handle_frontend_http_conn(
2963 stream: tokio::net::TcpStream,
2964 coordinator: Arc<CoordinatedRuntime>,
2965 events: broadcast::Sender<FrontendEvent>,
2966 credentials: Arc<[RuntimeHttpCredential]>,
2967) -> std::io::Result<()> {
2968 let (read_half, mut write_half) = stream.into_split();
2969 let mut reader = tokio::io::BufReader::new(read_half);
2970 let Some(req) = read_http_request(&mut reader).await? else {
2971 return Ok(());
2972 };
2973 if req.method == "GET" {
2974 if let Some((content_type, body)) = browser_observer_asset(&req.path) {
2975 return write_browser_observer_asset(&mut write_half, content_type, body).await;
2976 }
2977 }
2978 let Some(credential) = check_auth(&req, &credentials) else {
2979 return write_http_response(
2980 &mut write_half,
2981 401,
2982 "Unauthorized",
2983 "application/json",
2984 b"{\"error\":\"missing or invalid bearer token\"}",
2985 )
2986 .await;
2987 };
2988 let client = match coordinated_http_client(&req, &coordinator, credential) {
2989 Ok(client) => client,
2990 Err(error) => {
2991 let body = json!({"error":error.to_string()}).to_string();
2992 return write_http_response(
2993 &mut write_half,
2994 400,
2995 "Bad Request",
2996 "application/json",
2997 body.as_bytes(),
2998 )
2999 .await;
3000 }
3001 };
3002 match (req.method.as_str(), req.path.as_str()) {
3003 ("POST", "/rpc") => {
3004 let body_text = String::from_utf8_lossy(&req.body);
3005 let response = match serde_json::from_str::<RpcRequest>(&body_text) {
3006 Ok(request) => coordinated_runtime_rpc(client.clone(), request).await,
3007 Err(error) => rpc_error(Value::Null, -32700, format!("parse error: {error}")),
3008 };
3009 let body = response.to_string();
3010 write_http_response(
3011 &mut write_half,
3012 200,
3013 "OK",
3014 "application/json",
3015 body.as_bytes(),
3016 )
3017 .await
3018 }
3019 ("GET", "/frontend/events") => {
3020 if let Err(error) = client.observe() {
3021 let body = sdk_runtime_rpc_error(Value::Null, -32002, &error).to_string();
3022 return write_http_response(
3023 &mut write_half,
3024 403,
3025 "Forbidden",
3026 "application/json",
3027 body.as_bytes(),
3028 )
3029 .await;
3030 }
3031 let mut receiver = events.subscribe();
3032 let head = "HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nCache-Control: no-cache\r\nConnection: close\r\n\r\n";
3033 if write_half.write_all(head.as_bytes()).await.is_err() {
3034 client.detach();
3035 return Ok(());
3036 }
3037 let _ = write_half.flush().await;
3038 loop {
3039 tokio::select! {
3040 _ = reader.read_u8() => break,
3041 event = receiver.recv() => match event {
3042 Ok(event) => {
3043 let value = serde_json::to_string(&event).unwrap_or_default();
3044 let line = format!("data: {value}\n\n");
3045 if write_half.write_all(line.as_bytes()).await.is_err() || write_half.flush().await.is_err() {
3046 break;
3047 }
3048 }
3049 Err(broadcast::error::RecvError::Lagged(_)) => break,
3050 Err(broadcast::error::RecvError::Closed) => break,
3051 }
3052 }
3053 }
3054 client.detach();
3055 Ok(())
3056 }
3057 _ => {
3058 write_http_response(
3059 &mut write_half,
3060 404,
3061 "Not Found",
3062 "application/json",
3063 b"{\"error\":\"not found\"}",
3064 )
3065 .await
3066 }
3067 }
3068}
3069
3070#[cfg(feature = "adapter-api")]
3071async fn frontend_http_rpc(runtime: Arc<dyn FrontendRuntime>, request: RpcRequest) -> Value {
3072 let id = request.id;
3073 let Some(method) = crate::FrontendFacadeMethod::from_wire_name(&request.method) else {
3074 return rpc_error(id, -32601, format!("unknown method `{}`", request.method));
3075 };
3076 match method {
3077 crate::FrontendFacadeMethod::Describe => match runtime.describe().await {
3078 Ok(descriptor) => rpc_ok(id, serde_json::to_value(descriptor).unwrap_or_default()),
3079 Err(error) => sdk_runtime_rpc_error(id, -32010, &error),
3080 },
3081 crate::FrontendFacadeMethod::Attach => {
3082 let limit = request
3083 .params
3084 .get("limit")
3085 .and_then(Value::as_u64)
3086 .unwrap_or(50)
3087 .clamp(1, SERVER_HISTORY_CAPACITY as u64) as usize;
3088 match runtime.attach(limit).await {
3089 Ok(attachment) => rpc_ok(
3090 id,
3091 serde_json::to_value(FrontendAttachSnapshot {
3092 descriptor: attachment.descriptor,
3093 history: attachment.history,
3094 history_cursor: attachment.history_cursor,
3095 replay: attachment.replay,
3096 })
3097 .unwrap_or_default(),
3098 ),
3099 Err(error) => sdk_runtime_rpc_error(id, -32010, &error),
3100 }
3101 }
3102 crate::FrontendFacadeMethod::SendInput => {
3103 let Some(prompt) = request.params.get("prompt").and_then(Value::as_str) else {
3104 return rpc_error(
3105 id,
3106 -32602,
3107 "frontend.send_input requires a string `params.prompt`",
3108 );
3109 };
3110 let image_urls = match parse_image_urls(&request.params, "frontend.send_input") {
3111 Ok(image_urls) => image_urls,
3112 Err(message) => return rpc_error(id, -32602, message),
3113 };
3114 match runtime
3115 .clone()
3116 .send_input_with_images(prompt.to_string(), image_urls)
3117 .await
3118 {
3119 Ok(()) => rpc_ok(id, json!({"accepted": true})),
3120 Err(error @ FrontendRuntimeError::Submit(RuntimeSubmitError::Busy)) => {
3121 sdk_runtime_rpc_error(id, -32000, &error)
3122 }
3123 Err(error) => sdk_runtime_rpc_error(id, -32002, &error),
3124 }
3125 }
3126 crate::FrontendFacadeMethod::Invoke => {
3127 let operation = request
3128 .params
3129 .get("operation")
3130 .cloned()
3131 .ok_or("frontend.invoke requires `params.operation`")
3132 .and_then(|value| {
3133 serde_json::from_value(value).map_err(|_| "invalid frontend operation")
3134 });
3135 match operation {
3136 Ok(operation) => match runtime.invoke(operation).await {
3137 Ok(result) => rpc_ok(id, serde_json::to_value(result).unwrap_or_default()),
3138 Err(error @ FrontendRuntimeError::UnsupportedOperation(_)) => {
3139 sdk_runtime_rpc_error(id, -32023, &error)
3140 }
3141 Err(error @ FrontendRuntimeError::Submit(RuntimeSubmitError::Busy)) => {
3142 sdk_runtime_rpc_error(id, -32000, &error)
3143 }
3144 Err(error @ FrontendRuntimeError::Submit(RuntimeSubmitError::Interrupted)) => {
3145 sdk_runtime_rpc_error(id, -32001, &error)
3146 }
3147 Err(error) => sdk_runtime_rpc_error(id, -32022, &error),
3148 },
3149 Err(message) => rpc_error(id, -32602, message),
3150 }
3151 }
3152 crate::FrontendFacadeMethod::Submit => {
3153 let Some(prompt) = request.params.get("prompt").and_then(Value::as_str) else {
3154 return rpc_error(id, -32602, "submit requires a string `params.prompt`");
3155 };
3156 let image_urls = match request.params.get("image_urls") {
3157 None => Vec::new(),
3158 Some(Value::Array(values)) => {
3159 let Some(urls) = values.iter().map(Value::as_str).collect::<Option<Vec<_>>>()
3160 else {
3161 return rpc_error(
3162 id,
3163 -32602,
3164 "submit requires string entries in `params.image_urls`",
3165 );
3166 };
3167 urls.into_iter().map(str::to_owned).collect()
3168 }
3169 Some(_) => {
3170 return rpc_error(id, -32602, "submit requires array `params.image_urls`")
3171 }
3172 };
3173 match runtime
3174 .submit_with_images(prompt.to_string(), image_urls)
3175 .await
3176 {
3177 Ok(reply) => rpc_ok(id, json!({"reply":reply})),
3178 Err(error @ FrontendRuntimeError::Submit(RuntimeSubmitError::Busy)) => {
3179 sdk_runtime_rpc_error(id, -32000, &error)
3180 }
3181 Err(error @ FrontendRuntimeError::Submit(RuntimeSubmitError::Interrupted)) => {
3182 sdk_runtime_rpc_error(id, -32001, &error)
3183 }
3184 Err(error) => sdk_runtime_rpc_error(id, -32002, &error),
3185 }
3186 }
3187 crate::FrontendFacadeMethod::Interrupt => match runtime.interrupt().await {
3188 Ok(interrupted) => rpc_ok(id, json!({"interrupted":interrupted})),
3189 Err(error) => sdk_runtime_rpc_error(id, -32002, &error),
3190 },
3191 crate::FrontendFacadeMethod::Steer => {
3192 let Some(prompt) = request.params.get("prompt").and_then(Value::as_str) else {
3193 return rpc_error(id, -32602, "steer requires a string `params.prompt`");
3194 };
3195 match runtime.steer(prompt.to_string()).await {
3196 Ok(()) => rpc_ok(id, json!({"queued":true})),
3197 Err(error @ FrontendRuntimeError::UnsupportedAction(_)) => {
3198 sdk_runtime_rpc_error(id, -32020, &error)
3199 }
3200 Err(error) => sdk_runtime_rpc_error(id, -32022, &error),
3201 }
3202 }
3203 crate::FrontendFacadeMethod::Respond => {
3204 let response = request
3205 .params
3206 .get("response")
3207 .cloned()
3208 .ok_or("respond requires `params.response`")
3209 .and_then(|value| serde_json::from_value(value).map_err(|_| "invalid response"));
3210 match response {
3211 Ok(response) => match runtime.respond(response).await {
3212 Ok(()) => rpc_ok(id, json!({"accepted":true})),
3213 Err(error @ FrontendRuntimeError::UnsupportedAction(_)) => {
3214 sdk_runtime_rpc_error(id, -32020, &error)
3215 }
3216 Err(error) => sdk_runtime_rpc_error(id, -32022, &error),
3217 },
3218 Err(message) => rpc_error(id, -32602, message),
3219 }
3220 }
3221 crate::FrontendFacadeMethod::Lease
3222 | crate::FrontendFacadeMethod::TakeControl
3223 | crate::FrontendFacadeMethod::Heartbeat
3224 | crate::FrontendFacadeMethod::Detach
3225 | crate::FrontendFacadeMethod::Close => rpc_error(
3226 id,
3227 -32020,
3228 format!(
3229 "frontend action `{}` requires a coordinated runtime",
3230 method.id()
3231 ),
3232 ),
3233 }
3234}
3235
3236fn parse_image_urls(params: &Value, operation: &str) -> std::result::Result<Vec<String>, String> {
3237 let urls = match params.get("image_urls") {
3238 None => Ok(Vec::new()),
3239 Some(Value::Array(values)) => values
3240 .iter()
3241 .map(|value| {
3242 value.as_str().map(str::to_owned).ok_or_else(|| {
3243 format!("{operation} requires string entries in `params.image_urls`")
3244 })
3245 })
3246 .collect(),
3247 Some(_) => Err(format!("{operation} requires array `params.image_urls`")),
3248 }?;
3249 validate_frontend_image_urls(urls, operation)
3250}
3251
3252fn validate_frontend_image_urls(
3253 urls: Vec<String>,
3254 operation: &str,
3255) -> std::result::Result<Vec<String>, String> {
3256 if urls.len() > 4 {
3257 return Err(format!("{operation} accepts at most 4 images"));
3258 }
3259 let mut total = 0usize;
3260 for url in &urls {
3261 if !(url.starts_with("data:image/")
3262 || url.starts_with("https://")
3263 || url.starts_with("http://"))
3264 {
3265 return Err(format!(
3266 "{operation} images must be image data URLs or HTTP(S) URLs"
3267 ));
3268 }
3269 if url.len() > 12 * 1024 * 1024 {
3270 return Err(format!("{operation} image exceeds the encoded size limit"));
3271 }
3272 total = total.saturating_add(url.len());
3273 }
3274 if total > 32 * 1024 * 1024 {
3275 return Err(format!(
3276 "{operation} images exceed the encoded total size limit"
3277 ));
3278 }
3279 Ok(urls)
3280}
3281
3282#[cfg(feature = "adapter-api")]
3284pub(crate) struct FrontendHttpServer {
3285 address: SocketAddr,
3286 task: tokio::task::JoinHandle<()>,
3287}
3288
3289#[cfg(feature = "adapter-api")]
3290impl FrontendHttpServer {
3291 pub(crate) fn address(&self) -> SocketAddr {
3293 self.address
3294 }
3295}
3296
3297#[cfg(feature = "adapter-api")]
3298impl Drop for FrontendHttpServer {
3299 fn drop(&mut self) {
3300 self.task.abort();
3301 }
3302}
3303
3304#[cfg(feature = "adapter-api")]
3307pub(crate) async fn run_frontend_http(
3308 runtime: Arc<dyn FrontendRuntime>,
3309 events: broadcast::Sender<FrontendEvent>,
3310 bind: &str,
3311 token: Arc<str>,
3312) -> std::io::Result<FrontendHttpServer> {
3313 let listener = TcpListener::bind(bind).await?;
3314 let address = listener.local_addr()?;
3315 let coordinator = CoordinatedRuntime::new(runtime);
3316 let credentials: Arc<[RuntimeHttpCredential]> =
3317 vec![RuntimeHttpCredential::owner(token)].into();
3318 let task = tokio::spawn(async move {
3319 while let Ok((stream, _)) = listener.accept().await {
3320 let coordinator = coordinator.clone();
3321 let events = events.clone();
3322 let credentials = credentials.clone();
3323 tokio::spawn(async move {
3324 let _ = handle_frontend_http_conn(stream, coordinator, events, credentials).await;
3325 });
3326 }
3327 });
3328 Ok(FrontendHttpServer { address, task })
3329}
3330
3331#[cfg(feature = "adapter-api")]
3334pub struct FrontendWebSocketServer {
3335 address: SocketAddr,
3336 task: tokio::task::JoinHandle<()>,
3337}
3338
3339#[cfg(feature = "adapter-api")]
3340impl FrontendWebSocketServer {
3341 pub fn address(&self) -> SocketAddr {
3343 self.address
3344 }
3345}
3346
3347#[cfg(feature = "adapter-api")]
3348impl Drop for FrontendWebSocketServer {
3349 fn drop(&mut self) {
3350 self.task.abort();
3351 }
3352}
3353
3354#[cfg(feature = "adapter-api")]
3358pub async fn run_frontend_websocket(
3359 engine: Arc<RpcEngine>,
3360 bind: &str,
3361 credentials: Vec<RuntimeHttpCredential>,
3362) -> std::io::Result<FrontendWebSocketServer> {
3363 let runtime: Arc<dyn FrontendRuntime> = engine.clone();
3364 let events = engine.frontend_events.clone();
3365 run_frontend_websocket_runtime_inner(runtime, events, Some(engine), bind, credentials).await
3366}
3367
3368#[cfg(all(feature = "adapter-api", test))]
3373pub(crate) async fn run_frontend_websocket_runtime(
3374 runtime: Arc<dyn FrontendRuntime>,
3375 events: broadcast::Sender<FrontendEvent>,
3376 bind: &str,
3377 credentials: Vec<RuntimeHttpCredential>,
3378) -> std::io::Result<FrontendWebSocketServer> {
3379 run_frontend_websocket_runtime_inner(runtime, events, None, bind, credentials).await
3380}
3381
3382#[cfg(feature = "adapter-api")]
3383async fn run_frontend_websocket_runtime_inner(
3384 runtime: Arc<dyn FrontendRuntime>,
3385 events: broadcast::Sender<FrontendEvent>,
3386 shutdown_engine: Option<Arc<RpcEngine>>,
3387 bind: &str,
3388 credentials: Vec<RuntimeHttpCredential>,
3389) -> std::io::Result<FrontendWebSocketServer> {
3390 if credentials.is_empty()
3391 || credentials
3392 .iter()
3393 .any(|credential| credential.token.is_empty())
3394 {
3395 return Err(std::io::Error::new(
3396 std::io::ErrorKind::InvalidInput,
3397 "at least one non-empty runtime WebSocket credential is required",
3398 ));
3399 }
3400 let listener = TcpListener::bind(bind).await?;
3401 let address = listener.local_addr()?;
3402 let coordinator = CoordinatedRuntime::new(runtime);
3403 let credentials: Arc<[RuntimeHttpCredential]> = credentials.into();
3404 let task = tokio::spawn(async move {
3405 loop {
3406 tokio::select! {
3407 biased;
3408 _ = wait_for_optional_runtime_shutdown(shutdown_engine.as_ref()) => break,
3409 accepted = listener.accept() => {
3410 let Ok((stream, _)) = accepted else { continue };
3411 let coordinator = coordinator.clone();
3412 let credentials = credentials.clone();
3413 let events = events.clone();
3414 let shutdown_engine = shutdown_engine.clone();
3415 tokio::spawn(async move {
3416 let _ = handle_frontend_websocket(stream, events, shutdown_engine, coordinator, credentials).await;
3417 });
3418 }
3419 }
3420 }
3421 });
3422 Ok(FrontendWebSocketServer { address, task })
3423}
3424
3425#[cfg(feature = "adapter-api")]
3426async fn wait_for_optional_runtime_shutdown(engine: Option<&Arc<RpcEngine>>) {
3427 match engine {
3428 Some(engine) => engine.wait_for_shutdown().await,
3429 None => std::future::pending().await,
3430 }
3431}
3432
3433#[cfg(feature = "adapter-api")]
3434#[allow(clippy::result_large_err)] async fn handle_frontend_websocket(
3436 stream: tokio::net::TcpStream,
3437 events: broadcast::Sender<FrontendEvent>,
3438 shutdown_engine: Option<Arc<RpcEngine>>,
3439 coordinator: Arc<CoordinatedRuntime>,
3440 credentials: Arc<[RuntimeHttpCredential]>,
3441) -> Result<(), tokio_tungstenite::tungstenite::Error> {
3442 use std::sync::Mutex as SyncMutex;
3443 use tokio_tungstenite::tungstenite::handshake::server::{ErrorResponse, Request, Response};
3444
3445 let selected = Arc::new(SyncMutex::new(None::<Arc<CoordinatedRuntimeClient>>));
3446 let selected_by_callback = selected.clone();
3447 let socket = tokio_tungstenite::accept_hdr_async(
3448 stream,
3449 move |request: &Request, response: Response| -> Result<Response, ErrorResponse> {
3450 let reject = |status, message: &str| {
3451 tokio_tungstenite::tungstenite::http::Response::builder()
3452 .status(status)
3453 .body(Some(message.to_string()))
3454 .expect("static WebSocket rejection is valid")
3455 };
3456 if request.uri().path() != "/frontend/v2" {
3457 return Err(reject(404, "frontend WebSocket route not found"));
3458 }
3459 let token = request
3460 .headers()
3461 .get("authorization")
3462 .and_then(|value| value.to_str().ok())
3463 .and_then(|value| value.strip_prefix("Bearer "));
3464 let Some(credential) = token.and_then(|token| {
3465 credentials.iter().find(|credential| {
3466 constant_time_eq(token.as_bytes(), credential.token.as_bytes())
3467 })
3468 }) else {
3469 return Err(reject(401, "missing or invalid bearer token"));
3470 };
3471 let client_id = request
3472 .headers()
3473 .get("x-supercode-client-id")
3474 .and_then(|value| value.to_str().ok())
3475 .unwrap_or("legacy-websocket-owner");
3476 let Ok(client_id) = RuntimeClientId::parse(client_id) else {
3477 return Err(reject(400, "invalid runtime client id"));
3478 };
3479 let mut authorization = credential.authorization.clone();
3480 if let Some(requested) = request
3481 .headers()
3482 .get("x-supercode-permissions")
3483 .and_then(|value| value.to_str().ok())
3484 {
3485 let Ok(requested) = RuntimeAuthorization::parse_header(requested) else {
3486 return Err(reject(400, "invalid runtime authorization grant"));
3487 };
3488 authorization = authorization.restrict_to(&requested);
3489 }
3490 *selected_by_callback
3491 .lock()
3492 .unwrap_or_else(std::sync::PoisonError::into_inner) =
3493 Some(coordinator.client(client_id, authorization));
3494 Ok(response)
3495 },
3496 )
3497 .await?;
3498 let client = selected
3499 .lock()
3500 .unwrap_or_else(std::sync::PoisonError::into_inner)
3501 .take()
3502 .expect("successful WebSocket handshake selects a runtime client");
3503 if let Err(error) = client.observe() {
3504 let mut socket = socket;
3505 let value = sdk_runtime_rpc_error(Value::Null, -32002, &error).to_string();
3506 socket
3507 .send(tokio_tungstenite::tungstenite::Message::Text(value.into()))
3508 .await?;
3509 socket.close(None).await?;
3510 return Ok(());
3511 }
3512
3513 let mut events = events.subscribe();
3514 let (mut writer, mut reader) = socket.split();
3515 loop {
3516 tokio::select! {
3517 biased;
3518 incoming = reader.next() => match incoming {
3519 Some(Ok(tokio_tungstenite::tungstenite::Message::Text(text))) => {
3520 let response = match serde_json::from_str::<RpcRequest>(&text) {
3521 Ok(request) => coordinated_runtime_rpc(client.clone(), request).await,
3522 Err(error) => rpc_error(Value::Null, -32700, format!("parse error: {error}")),
3523 };
3524 writer.send(tokio_tungstenite::tungstenite::Message::Text(response.to_string().into())).await?;
3525 }
3526 Some(Ok(tokio_tungstenite::tungstenite::Message::Ping(payload))) => {
3527 writer.send(tokio_tungstenite::tungstenite::Message::Pong(payload)).await?;
3528 }
3529 Some(Ok(tokio_tungstenite::tungstenite::Message::Close(_))) | None => break,
3530 Some(Ok(_)) => {}
3531 Some(Err(error)) => {
3532 client.detach();
3533 return Err(error);
3534 }
3535 },
3536 event = events.recv() => match event {
3537 Ok(event) => {
3538 let notification = json!({
3539 "jsonrpc":"2.0",
3540 "method":"frontend.v2.event",
3541 "params":{"event":event},
3542 });
3543 writer.send(tokio_tungstenite::tungstenite::Message::Text(notification.to_string().into())).await?;
3544 }
3545 Err(broadcast::error::RecvError::Lagged(count)) => {
3546 let notification = json!({
3547 "jsonrpc":"2.0",
3548 "method":"frontend.v2.event",
3549 "params":{"error":{"name":"transport","message":format!("event replay gap: {count}")}},
3550 });
3551 writer.send(tokio_tungstenite::tungstenite::Message::Text(notification.to_string().into())).await?;
3552 break;
3553 }
3554 Err(broadcast::error::RecvError::Closed) => break,
3555 },
3556 _ = wait_for_optional_runtime_shutdown(shutdown_engine.as_ref()) => break,
3557 }
3558 }
3559 client.detach();
3560 Ok(())
3561}
3562
3563#[cfg(feature = "adapter-api")]
3571pub async fn run_http(
3572 engine: Arc<RpcEngine>,
3573 bind: &str,
3574 token: Arc<str>,
3575) -> std::io::Result<SocketAddr> {
3576 run_http_authorized(engine, bind, vec![RuntimeHttpCredential::owner(token)]).await
3577}
3578
3579#[cfg(feature = "adapter-api")]
3584pub async fn run_http_authorized(
3585 engine: Arc<RpcEngine>,
3586 bind: &str,
3587 credentials: Vec<RuntimeHttpCredential>,
3588) -> std::io::Result<SocketAddr> {
3589 run_http_authorized_with_lease_ttl(
3590 engine,
3591 bind,
3592 credentials,
3593 crate::DEFAULT_RUNTIME_LEASE_TTL_MS,
3594 )
3595 .await
3596}
3597
3598#[cfg(feature = "adapter-api")]
3601pub async fn run_http_authorized_with_lease_ttl(
3602 engine: Arc<RpcEngine>,
3603 bind: &str,
3604 credentials: Vec<RuntimeHttpCredential>,
3605 lease_ttl_ms: u64,
3606) -> std::io::Result<SocketAddr> {
3607 if credentials.is_empty()
3608 || credentials
3609 .iter()
3610 .any(|credential| credential.token.is_empty())
3611 {
3612 return Err(std::io::Error::new(
3613 std::io::ErrorKind::InvalidInput,
3614 "at least one non-empty runtime HTTP credential is required",
3615 ));
3616 }
3617 if lease_ttl_ms == 0 {
3618 return Err(std::io::Error::new(
3619 std::io::ErrorKind::InvalidInput,
3620 "runtime lease TTL must be non-zero",
3621 ));
3622 }
3623 let listener = TcpListener::bind(bind).await?;
3624 let local_addr = listener.local_addr()?;
3625 let credentials = RuntimeHttpCredentialRegistry::new(engine.session_id(), credentials)?;
3626 let eng = engine;
3627 let runtime: Arc<dyn FrontendRuntime> = eng.clone();
3628 let coordinator = CoordinatedRuntime::with_lease_ttl(runtime, lease_ttl_ms);
3629 tokio::spawn(async move {
3630 loop {
3631 tokio::select! {
3632 biased;
3633 _ = eng.wait_for_shutdown() => break,
3634 accepted = listener.accept() => {
3635 let Ok((stream, _addr)) = accepted else { continue };
3636 let eng = eng.clone();
3637 let coordinator = coordinator.clone();
3638 let credentials = credentials.clone();
3639 tokio::spawn(async move {
3640 let _ = handle_http_conn(stream, eng, coordinator, credentials).await;
3641 });
3642 }
3643 }
3644 }
3645 });
3646 Ok(local_addr)
3647}
3648
3649#[cfg(test)]
3650mod frontend_binding_conformance_tests;
3651
3652#[cfg(test)]
3653mod tests {
3654 use super::*;
3655 use tokio::io::BufReader;
3656
3657 fn cursor(data: &[u8]) -> BufReader<std::io::Cursor<Vec<u8>>> {
3658 BufReader::new(std::io::Cursor::new(data.to_vec()))
3659 }
3660
3661 #[cfg(all(feature = "adapter-api", supercode_workspace_assets))]
3662 #[test]
3663 fn packaged_observer_assets_match_the_sdk_sources() {
3664 let pairs: &[(&str, &[u8], &[u8])] = &[
3665 (
3666 "frontend-browser/index.html",
3667 include_bytes!("../embedded/frontend-browser/index.html"),
3668 include_bytes!("../../../sdk/frontend-browser/index.html"),
3669 ),
3670 (
3671 "frontend-browser/app.mjs",
3672 include_bytes!("../embedded/frontend-browser/app.mjs"),
3673 include_bytes!("../../../sdk/frontend-browser/app.mjs"),
3674 ),
3675 (
3676 "frontend-browser/client.mjs",
3677 include_bytes!("../embedded/frontend-browser/client.mjs"),
3678 include_bytes!("../../../sdk/frontend-browser/client.mjs"),
3679 ),
3680 (
3681 "frontend-browser/view.mjs",
3682 include_bytes!("../embedded/frontend-browser/view.mjs"),
3683 include_bytes!("../../../sdk/frontend-browser/view.mjs"),
3684 ),
3685 (
3686 "frontend-browser/style.css",
3687 include_bytes!("../embedded/frontend-browser/style.css"),
3688 include_bytes!("../../../sdk/frontend-browser/style.css"),
3689 ),
3690 (
3691 "frontend-browser/favicon.svg",
3692 include_bytes!("../embedded/frontend-browser/favicon.svg"),
3693 include_bytes!("../../../sdk/frontend-browser/favicon.svg"),
3694 ),
3695 (
3696 "frontend/client.mjs",
3697 include_bytes!("../embedded/frontend/client.mjs"),
3698 include_bytes!("../../../sdk/frontend/client.mjs"),
3699 ),
3700 (
3701 "frontend/generated-client.mjs",
3702 include_bytes!("../embedded/frontend/generated-client.mjs"),
3703 include_bytes!("../../../sdk/frontend/generated-client.mjs"),
3704 ),
3705 (
3706 "frontend/generated.mjs",
3707 include_bytes!("../embedded/frontend/generated.mjs"),
3708 include_bytes!("../../../sdk/frontend/generated.mjs"),
3709 ),
3710 ];
3711 for (name, packaged, source) in pairs {
3712 assert_eq!(packaged, source, "packaged observer asset drifted: {name}");
3713 }
3714 }
3715
3716 #[tokio::test]
3717 async fn admitted_submit_has_a_cancel_token_before_shutdown_observes_busy() {
3718 let agent =
3719 crate::Agent::new(crate::Config::builder().api_key("test-only-key").build()).unwrap();
3720 let engine = RpcEngine::new(agent, None);
3721 let claim = engine.claim_submit().unwrap();
3722 assert!(engine.busy.load(Ordering::SeqCst));
3723 assert!(engine
3724 .current_cancel
3725 .lock()
3726 .unwrap_or_else(std::sync::PoisonError::into_inner)
3727 .is_some());
3728
3729 let cancel = claim.cancel.clone();
3730 let shutdown_engine = engine.clone();
3731 let shutdown = tokio::spawn(async move { shutdown_engine.shutdown().await });
3732 tokio::time::timeout(std::time::Duration::from_secs(1), cancel.notified())
3733 .await
3734 .expect("shutdown must interrupt an admitted claim before its future starts");
3735 assert!(
3736 !shutdown.is_finished(),
3737 "shutdown must retain the barrier until the admitted claim drains"
3738 );
3739 drop(claim);
3740 tokio::time::timeout(std::time::Duration::from_secs(1), shutdown)
3741 .await
3742 .expect("claim drain must release shutdown")
3743 .unwrap();
3744 }
3745
3746 #[tokio::test]
3747 async fn read_bounded_line_reads_a_normal_line() {
3748 let mut r = cursor(b"hello\nworld\n");
3749 assert_eq!(
3750 read_bounded_line(&mut r, 1024).await.unwrap(),
3751 Some("hello".to_string())
3752 );
3753 assert_eq!(
3754 read_bounded_line(&mut r, 1024).await.unwrap(),
3755 Some("world".to_string())
3756 );
3757 assert_eq!(read_bounded_line(&mut r, 1024).await.unwrap(), None);
3758 }
3759
3760 #[tokio::test]
3761 async fn read_bounded_line_strips_trailing_cr() {
3762 let mut r = cursor(b"hello\r\n");
3763 assert_eq!(
3764 read_bounded_line(&mut r, 1024).await.unwrap(),
3765 Some("hello".to_string())
3766 );
3767 }
3768
3769 #[tokio::test]
3770 async fn read_bounded_line_returns_final_line_without_trailing_newline() {
3771 let mut r = cursor(b"no newline at eof");
3772 assert_eq!(
3773 read_bounded_line(&mut r, 1024).await.unwrap(),
3774 Some("no newline at eof".to_string())
3775 );
3776 assert_eq!(read_bounded_line(&mut r, 1024).await.unwrap(), None);
3777 }
3778
3779 #[tokio::test]
3780 async fn read_bounded_line_errors_and_resyncs_on_an_oversized_line() {
3781 let mut data = vec![b'x'; 20];
3782 data.push(b'\n');
3783 data.extend_from_slice(b"next\n");
3784 let mut r = cursor(&data);
3785 let err = read_bounded_line(&mut r, 10).await.unwrap_err();
3786 assert!(err.to_string().contains("10 byte cap"));
3787 assert_eq!(
3790 read_bounded_line(&mut r, 1024).await.unwrap(),
3791 Some("next".to_string())
3792 );
3793 }
3794
3795 #[test]
3796 fn constant_time_eq_matches_equal_slices() {
3797 assert!(constant_time_eq(b"abc123", b"abc123"));
3798 }
3799
3800 #[test]
3801 fn constant_time_eq_rejects_different_length_or_content() {
3802 assert!(!constant_time_eq(b"abc123", b"abc1234"));
3803 assert!(!constant_time_eq(b"abc123", b"xbc123"));
3804 }
3805
3806 #[test]
3807 fn generate_token_is_64_hex_chars_and_varies() {
3808 let a = generate_token();
3809 let b = generate_token();
3810 assert_eq!(a.len(), 64);
3811 assert!(a.chars().all(|c| c.is_ascii_hexdigit()));
3812 assert_ne!(a, b, "two calls must not mint the same token");
3813 }
3814
3815 #[cfg(feature = "adapter-api")]
3816 #[tokio::test]
3817 async fn credential_revocation_waits_for_registered_attachment_ack() {
3818 let revocation = Arc::new(RuntimeCredentialRevocation::new());
3819 let attachment = revocation.register();
3820 let (started_tx, started_rx) = tokio::sync::oneshot::channel();
3821 let task = tokio::spawn({
3822 let revocation = revocation.clone();
3823 async move {
3824 let _ = started_tx.send(());
3825 revocation.revoke_and_wait().await;
3826 }
3827 });
3828
3829 started_rx.await.unwrap();
3830 tokio::task::yield_now().await;
3831 assert!(
3832 !task.is_finished(),
3833 "revoke must remain pending while the attachment is registered"
3834 );
3835
3836 drop(attachment);
3837 tokio::time::timeout(std::time::Duration::from_secs(1), task)
3838 .await
3839 .expect("attachment acknowledgement must release revoke")
3840 .unwrap();
3841 }
3842
3843 #[cfg(feature = "adapter-api")]
3844 #[tokio::test]
3845 async fn credential_revocation_has_no_check_to_wait_lost_wakeup() {
3846 for _ in 0..10_000 {
3847 let revocation = Arc::new(RuntimeCredentialRevocation::new());
3848 let attachment = revocation.register();
3849 let (started_tx, started_rx) = tokio::sync::oneshot::channel();
3850 let task = tokio::spawn({
3851 let revocation = revocation.clone();
3852 async move {
3853 let _ = started_tx.send(());
3854 revocation.revoke_and_wait().await;
3855 }
3856 });
3857
3858 started_rx.await.unwrap();
3859 drop(attachment);
3860 tokio::time::timeout(std::time::Duration::from_secs(1), task)
3861 .await
3862 .expect("revoke lost its attachment-drained wakeup")
3863 .unwrap();
3864 }
3865 }
3866
3867 #[test]
3868 fn request_history_compaction_deduplicates_and_orders_resolutions() {
3869 let request = |sequence, id| {
3870 FrontendEvent::new(
3871 sequence,
3872 json!({"type": "request", "request": {"id": id, "kind": "approval", "payload": {}}}),
3873 )
3874 };
3875 let resolved = |sequence, id| {
3876 FrontendEvent::new(
3877 sequence,
3878 json!({"type": "request_resolved", "request_id": id, "response": {"kind": "approval", "request_id": id, "decision": "allow"}}),
3879 )
3880 };
3881 let replay = VecDeque::from([
3882 request(1, 2),
3883 resolved(2, 2),
3884 request(3, 1),
3885 resolved(4, 1),
3886 request(5, 2),
3887 resolved(6, 1),
3888 ]);
3889
3890 let compacted = compact_frontend_request_history(&replay);
3891 assert_eq!(compacted.len(), 4);
3892 assert_eq!(compacted[0]["request"]["id"], 1);
3893 assert_eq!(compacted[1]["request_id"], 1);
3894 assert_eq!(compacted[2]["request"]["id"], 2);
3895 assert_eq!(compacted[3]["request_id"], 2);
3896 }
3897}