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 if let Some((child_agent_id, queue)) = child {
457 queue
458 .lock()
459 .unwrap_or_else(std::sync::PoisonError::into_inner)
460 .push(crate::subagents::QueuedApproval {
461 child_agent_id: child_agent_id.to_string(),
462 tool: req.tool.to_string(),
463 subject: req.subject.map(String::from),
464 queued_at_ms: std::time::SystemTime::now()
465 .duration_since(std::time::UNIX_EPOCH)
466 .map(|duration| duration.as_millis() as i64)
467 .unwrap_or_default(),
468 });
469 }
470 let Some(transport) = self.transport() else {
473 return ApprovalOutcome::Deny;
474 };
475 if transport.events.receiver_count() == 0 {
476 return ApprovalOutcome::Deny;
477 }
478 let id = self.next_id.fetch_add(1, Ordering::SeqCst);
479 let mut payload = json!({
480 "tool": req.tool,
481 "subject": req.subject,
482 "raw_args": req.raw_args,
483 });
484 if let Some((child_agent_id, _)) = child {
485 payload["child_agent_id"] = Value::String(child_agent_id.to_string());
486 }
487 let request = FrontendRequest {
488 id,
489 kind: FrontendRequestKind::Approval,
490 payload,
491 };
492 let (tx, rx) = std::sync::mpsc::channel();
493 self.pending
494 .lock()
495 .unwrap_or_else(std::sync::PoisonError::into_inner)
496 .insert(id, (FrontendRequestKind::Approval, tx));
497 if !self.publish(&request) {
498 self.pending
499 .lock()
500 .unwrap_or_else(std::sync::PoisonError::into_inner)
501 .remove(&id);
502 return ApprovalOutcome::Deny;
503 }
504 let wait_for_response = || loop {
505 match rx.recv_timeout(std::time::Duration::from_millis(100)) {
506 Ok(accepted) => {
507 let _ = accepted.published.recv();
508 let FrontendResponse::Approval { decision, .. } = accepted.response else {
509 return ApprovalOutcome::Deny;
510 };
511 return match decision {
512 FrontendApprovalDecision::Deny => ApprovalOutcome::Deny,
513 FrontendApprovalDecision::Allow => ApprovalOutcome::Allow,
514 FrontendApprovalDecision::AllowForSession => {
515 ApprovalOutcome::AllowForSession
516 }
517 };
518 }
519 Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
520 return ApprovalOutcome::Deny;
521 }
522 Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
523 if self
524 .transport()
525 .map(|transport| transport.events.receiver_count() == 0)
526 .unwrap_or(true)
527 {
528 self.pending
529 .lock()
530 .unwrap_or_else(std::sync::PoisonError::into_inner)
531 .remove(&id);
532 return ApprovalOutcome::Deny;
533 }
534 }
535 }
536 };
537 if tokio::runtime::Handle::try_current()
538 .map(|handle| handle.runtime_flavor() == tokio::runtime::RuntimeFlavor::MultiThread)
539 .unwrap_or(false)
540 {
541 tokio::task::block_in_place(wait_for_response)
542 } else {
543 wait_for_response()
544 }
545 }
546
547 async fn ask_elicitation(self: Arc<Self>, req: &ElicitationRequest) -> ElicitationResponse {
548 let cancel = || ElicitationResponse {
549 action: ElicitationAction::Cancel,
550 content: None,
551 };
552 let Some(transport) = self.transport() else {
553 return cancel();
554 };
555 if transport.events.receiver_count() == 0 {
556 return cancel();
557 }
558 let id = self.next_id.fetch_add(1, Ordering::SeqCst);
559 let request = FrontendRequest {
560 id,
561 kind: FrontendRequestKind::Elicitation,
562 payload: json!({
563 "message": req.message,
564 "requested_schema": req.requested_schema,
565 }),
566 };
567 let (tx, rx) = std::sync::mpsc::channel();
568 self.pending
569 .lock()
570 .unwrap_or_else(std::sync::PoisonError::into_inner)
571 .insert(id, (FrontendRequestKind::Elicitation, tx));
572 if !self.publish(&request) {
573 self.pending
574 .lock()
575 .unwrap_or_else(std::sync::PoisonError::into_inner)
576 .remove(&id);
577 return cancel();
578 }
579 let broker = self.clone();
580 tokio::task::spawn_blocking(move || loop {
581 match rx.recv_timeout(std::time::Duration::from_millis(100)) {
582 Ok(accepted) => {
583 let _ = accepted.published.recv();
584 let FrontendResponse::Elicitation {
585 action, content, ..
586 } = accepted.response
587 else {
588 return cancel();
589 };
590 return ElicitationResponse {
591 action: match action {
592 crate::frontend::FrontendElicitationAction::Accept => {
593 ElicitationAction::Accept
594 }
595 crate::frontend::FrontendElicitationAction::Decline => {
596 ElicitationAction::Decline
597 }
598 crate::frontend::FrontendElicitationAction::Cancel => {
599 ElicitationAction::Cancel
600 }
601 },
602 content,
603 };
604 }
605 Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => return cancel(),
606 Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
607 if broker
608 .transport()
609 .map(|transport| transport.events.receiver_count() == 0)
610 .unwrap_or(true)
611 {
612 broker
613 .pending
614 .lock()
615 .unwrap_or_else(std::sync::PoisonError::into_inner)
616 .remove(&id);
617 return cancel();
618 }
619 }
620 }
621 })
622 .await
623 .unwrap_or_else(|_| cancel())
624 }
625}
626
627struct FrontendApprovalHandler(Arc<FrontendRequestBroker>);
628
629impl PermissionsApprovalHandler for FrontendApprovalHandler {
630 fn ask(&self, req: &ApprovalRequest<'_>) -> ApprovalOutcome {
631 self.0.ask_approval(req, None)
632 }
633}
634
635struct FrontendChildApprovalHandler {
636 broker: Arc<FrontendRequestBroker>,
637 child_agent_id: String,
638 queue: Arc<StdMutex<Vec<crate::subagents::QueuedApproval>>>,
639}
640
641impl PermissionsApprovalHandler for FrontendChildApprovalHandler {
642 fn ask(&self, req: &ApprovalRequest<'_>) -> ApprovalOutcome {
643 self.broker
644 .ask_approval(req, Some((&self.child_agent_id, &self.queue)))
645 }
646}
647
648#[derive(Clone)]
651pub struct FrontendRequestBridge {
652 broker: Arc<FrontendRequestBroker>,
653}
654
655impl FrontendRequestBridge {
656 pub fn new() -> Self {
659 Self {
660 broker: FrontendRequestBroker::new(),
661 }
662 }
663
664 pub fn elicitation_handler(&self) -> Arc<dyn McpElicitationHandler> {
666 Arc::new(FrontendElicitationHandler(self.broker.clone()))
667 }
668}
669
670impl Default for FrontendRequestBridge {
671 fn default() -> Self {
672 Self::new()
673 }
674}
675
676struct FrontendElicitationHandler(Arc<FrontendRequestBroker>);
677
678#[async_trait]
679impl McpElicitationHandler for FrontendElicitationHandler {
680 async fn handle(&self, request: &ElicitationRequest) -> ElicitationResponse {
681 self.0.clone().ask_elicitation(request).await
682 }
683}
684
685pub struct RpcEngine {
691 agent: Mutex<SdkAgent>,
692 history_snapshot: RwLock<Vec<ChatMessage>>,
696 session_id: String,
697 model: String,
701 busy: Arc<AtomicBool>,
702 current_cancel: Arc<StdMutex<Option<Arc<Notify>>>>,
708 turn_finished: Arc<Notify>,
711 steer_queue: Arc<StdMutex<SteerInbox>>,
714 events: broadcast::Sender<Value>,
715 frontend_events: broadcast::Sender<FrontendEvent>,
717 frontend_state: Arc<StdMutex<FrontendProjectionState>>,
719 frontend_metadata: FrontendRuntimeMetadata,
720 frontend_active_modules: Vec<String>,
721 frontend_commands: Vec<FrontendCommandDescriptor>,
722 frontend_operations: Vec<FrontendOperationDescriptor>,
723 frontend_requests: Option<Arc<FrontendRequestBroker>>,
724 shutdown: Notify,
725 shutting_down: AtomicBool,
726 accepting_submits: AtomicBool,
730 shutdown_barrier: Mutex<()>,
733 scheduler_started: AtomicBool,
734 scheduler_task: StdMutex<Option<tokio::task::JoinHandle<()>>>,
738 scheduler_changed: Arc<Notify>,
739 on_turn_complete: Option<TurnCompleteHook>,
745}
746
747struct SdkSubmitClaim {
754 inbox: Arc<StdMutex<SteerInbox>>,
755 busy: Arc<AtomicBool>,
756 cancel: Arc<Notify>,
757 current_cancel: Arc<StdMutex<Option<Arc<Notify>>>>,
758 turn_finished: Arc<Notify>,
759 scheduler_changed: Arc<Notify>,
760 frontend_events: broadcast::Sender<FrontendEvent>,
761 frontend_state: Arc<StdMutex<FrontendProjectionState>>,
762 lifecycle_started: bool,
763}
764
765impl SdkSubmitClaim {
766 fn mark_lifecycle_started(&mut self) {
767 self.lifecycle_started = true;
768 }
769
770 fn mark_lifecycle_finished(&mut self) {
771 self.lifecycle_started = false;
772 }
773}
774
775impl Drop for SdkSubmitClaim {
776 fn drop(&mut self) {
777 if self.lifecycle_started {
785 let event = {
786 let mut state = self
787 .frontend_state
788 .lock()
789 .unwrap_or_else(std::sync::PoisonError::into_inner);
790 let event = FrontendEvent::new(
791 state.next_sequence,
792 json!({
793 "type": "turn_interrupted",
794 "schema_version": FRONTEND_EVENT_SCHEMA_VERSION
795 }),
796 );
797 state.next_sequence = state.next_sequence.saturating_add(1);
798 state.replay.push_back(event.clone());
799 while state.replay.len() > FRONTEND_REPLAY_CAPACITY {
800 state.replay.pop_front();
801 }
802 event
803 };
804 let _ = self.frontend_events.send(event);
805 }
806 self.inbox
807 .lock()
808 .unwrap_or_else(std::sync::PoisonError::into_inner)
809 .close();
810 *self
811 .current_cancel
812 .lock()
813 .unwrap_or_else(std::sync::PoisonError::into_inner) = None;
814 self.busy.store(false, Ordering::SeqCst);
815 self.turn_finished.notify_waiters();
816 self.turn_finished.notify_one();
817 self.scheduler_changed.notify_waiters();
818 }
819}
820
821impl RpcEngine {
822 pub fn new(
830 agent: impl Into<SdkAgent>,
831 on_turn_complete: Option<TurnCompleteHook>,
832 ) -> Arc<Self> {
833 let agent = agent.into();
834 let session_id = agent
835 .session_name()
836 .map(str::to_owned)
837 .unwrap_or_else(|| format!("supercode-{}", std::process::id()));
838 Self::new_named(agent, session_id, on_turn_complete)
839 }
840
841 pub fn new_named(
845 agent: impl Into<SdkAgent>,
846 session_id: impl Into<String>,
847 on_turn_complete: Option<TurnCompleteHook>,
848 ) -> Arc<Self> {
849 Self::new_named_with_frontend_metadata(
850 agent.into(),
851 session_id,
852 FrontendRuntimeMetadata::default(),
853 on_turn_complete,
854 )
855 }
856
857 pub fn new_named_with_frontend_metadata(
860 agent: impl Into<SdkAgent>,
861 session_id: impl Into<String>,
862 frontend_metadata: FrontendRuntimeMetadata,
863 on_turn_complete: Option<TurnCompleteHook>,
864 ) -> Arc<Self> {
865 Self::build(
866 agent.into(),
867 session_id.into(),
868 frontend_metadata,
869 None,
870 on_turn_complete,
871 )
872 }
873
874 pub fn new_named_with_frontend_requests(
878 agent: impl Into<SdkAgent>,
879 session_id: impl Into<String>,
880 frontend_metadata: FrontendRuntimeMetadata,
881 on_turn_complete: Option<TurnCompleteHook>,
882 ) -> Arc<Self> {
883 let bridge = FrontendRequestBridge::new();
884 Self::new_named_with_frontend_bridge(
885 agent.into(),
886 session_id,
887 frontend_metadata,
888 bridge,
889 on_turn_complete,
890 )
891 }
892
893 pub fn new_named_with_frontend_bridge(
896 agent: impl Into<SdkAgent>,
897 session_id: impl Into<String>,
898 frontend_metadata: FrontendRuntimeMetadata,
899 bridge: FrontendRequestBridge,
900 on_turn_complete: Option<TurnCompleteHook>,
901 ) -> Arc<Self> {
902 Self::build(
903 agent.into(),
904 session_id.into(),
905 frontend_metadata,
906 Some(bridge.broker),
907 on_turn_complete,
908 )
909 }
910
911 fn build(
912 mut agent: SdkAgent,
913 session_id: String,
914 frontend_metadata: FrontendRuntimeMetadata,
915 frontend_requests: Option<Arc<FrontendRequestBroker>>,
916 on_turn_complete: Option<TurnCompleteHook>,
917 ) -> Arc<Self> {
918 let (tx, _rx) = broadcast::channel(SERVER_EVENT_CHANNEL_CAPACITY);
919 let events_tx = tx.clone();
920 let (frontend_tx, _frontend_rx) = broadcast::channel(SERVER_EVENT_CHANNEL_CAPACITY);
921 let frontend_events_tx = frontend_tx.clone();
922 let model = agent.config().model.clone();
923 let steer_queue = agent.inner().steer_queue_handle();
924 let history_snapshot = bounded_history_snapshot(agent.history());
925 let frontend_state = Arc::new(StdMutex::new(FrontendProjectionState {
926 history: history_snapshot.clone(),
927 history_cursor: 0,
928 next_sequence: 1,
929 replay: VecDeque::new(),
930 }));
931 if let Some(broker) = &frontend_requests {
932 broker.bind(frontend_tx.clone(), frontend_state.clone());
933 let legacy_broker = broker.clone();
934 agent
935 .inner_mut()
936 .set_legacy_approval_handler(Box::new(move |call| {
937 let Ok(raw_args) = call.function.parsed_arguments() else {
938 return false;
939 };
940 let subject = raw_args
941 .get("command")
942 .or_else(|| raw_args.get("path"))
943 .or_else(|| raw_args.get("file_path"))
944 .or_else(|| raw_args.get("patch"))
945 .and_then(Value::as_str);
946 matches!(
947 legacy_broker.ask_approval(
948 &ApprovalRequest {
949 tool: &call.function.name,
950 subject,
951 raw_args: &raw_args,
952 },
953 None,
954 ),
955 ApprovalOutcome::Allow | ApprovalOutcome::AllowForSession
956 )
957 }));
958 agent
959 .inner_mut()
960 .set_permissions_approval_handler(FrontendApprovalHandler(broker.clone()));
961 let broker = broker.clone();
962 agent
963 .inner_mut()
964 .set_child_approval_handler_factory(move |child_agent_id, queue| {
965 Arc::new(FrontendChildApprovalHandler {
966 broker: broker.clone(),
967 child_agent_id,
968 queue,
969 }) as Arc<dyn PermissionsApprovalHandler>
970 });
971 }
972 let event_frontend_state = frontend_state.clone();
973 let frontend_active_modules = agent
974 .config()
975 .module_activation
976 .iter()
977 .map(ToString::to_string)
978 .collect();
979 let mut frontend_operations = agent
980 .config()
981 .prompts
982 .keys()
983 .filter(|name| valid_frontend_command_name(name))
984 .map(|name| FrontendOperationDescriptor {
985 id: format!("prompt:{name}"),
986 kind: FrontendOperationKind::Prompt,
987 command: Some(FrontendCommandDescriptor {
988 name: name.clone(),
989 description: None,
990 argument_hint: Some("[arguments]".into()),
991 }),
992 })
993 .collect::<Vec<_>>();
994 frontend_operations.sort_by(|left, right| left.id.cmp(&right.id));
995 let frontend_commands = frontend_operations
998 .iter()
999 .filter_map(|operation| operation.command.as_ref())
1000 .map(|command| FrontendCommandDescriptor {
1001 name: command.name.clone(),
1002 description: command.description.clone(),
1003 argument_hint: None,
1004 })
1005 .collect();
1006 agent.inner_mut().set_event_sink(Box::new(move |event| {
1007 let payload = event.to_json();
1011 let _ = events_tx.send(payload.clone());
1012 let sequenced = {
1013 let mut state = event_frontend_state
1014 .lock()
1015 .unwrap_or_else(std::sync::PoisonError::into_inner);
1016 let event = FrontendEvent::new(state.next_sequence, payload);
1017 state.next_sequence = state.next_sequence.saturating_add(1);
1018 state.replay.push_back(event.clone());
1019 while state.replay.len() > FRONTEND_REPLAY_CAPACITY {
1020 state.replay.pop_front();
1021 }
1022 event
1023 };
1024 let _ = frontend_events_tx.send(sequenced);
1025 }));
1026 Arc::new(RpcEngine {
1027 agent: Mutex::new(agent),
1028 history_snapshot: RwLock::new(history_snapshot),
1029 session_id,
1030 model,
1031 busy: Arc::new(AtomicBool::new(false)),
1032 current_cancel: Arc::new(StdMutex::new(None)),
1033 turn_finished: Arc::new(Notify::new()),
1034 steer_queue,
1035 events: tx,
1036 frontend_events: frontend_tx,
1037 frontend_state,
1038 frontend_metadata,
1039 frontend_active_modules,
1040 frontend_commands,
1041 frontend_operations,
1042 frontend_requests,
1043 shutdown: Notify::new(),
1044 shutting_down: AtomicBool::new(false),
1045 accepting_submits: AtomicBool::new(true),
1046 shutdown_barrier: Mutex::new(()),
1047 scheduler_started: AtomicBool::new(false),
1048 scheduler_task: StdMutex::new(None),
1049 scheduler_changed: Arc::new(Notify::new()),
1050 on_turn_complete,
1051 })
1052 }
1053
1054 pub fn subscribe(&self) -> broadcast::Receiver<Value> {
1058 self.events.subscribe()
1059 }
1060
1061 pub fn frontend_descriptor(&self) -> FrontendRuntimeDescriptor {
1063 FrontendRuntimeDescriptor {
1064 schema_version: FRONTEND_RUNTIME_SCHEMA_VERSION,
1065 session_id: self.session_id.clone(),
1066 source_harness: self.frontend_metadata.source_harness.clone(),
1067 emulation_profile: self.frontend_metadata.emulation_profile.clone(),
1068 active_modules: self.frontend_active_modules.clone(),
1069 commands: self.frontend_commands.clone(),
1070 operations: self.frontend_operations.clone(),
1071 actions: FrontendActions {
1072 submit: true,
1073 interrupt: true,
1074 steer: true,
1075 respond: self.frontend_requests.is_some(),
1076 detach: true,
1077 close: true,
1081 },
1082 display: FrontendDisplayCapabilities {
1083 event_kinds: vec![
1084 "user_message".into(),
1085 "turn_started".into(),
1086 "turn_succeeded".into(),
1087 "turn_interrupted".into(),
1088 "turn_failed".into(),
1089 "text_delta".into(),
1090 "turn_completed".into(),
1091 "tool_call_started".into(),
1092 "tool_call_completed".into(),
1093 "cache_warning".into(),
1094 "usage".into(),
1095 "background_output".into(),
1096 "request".into(),
1097 "request_resolved".into(),
1098 "scheduled_prompt_started".into(),
1099 "scheduled_prompt_deferred".into(),
1100 "scheduled_prompt_completed".into(),
1101 "scheduler_error".into(),
1102 ],
1103 opaque_fallback: true,
1104 },
1105 model: self.model.clone(),
1106 turn_state: if self.busy.load(Ordering::SeqCst) {
1107 FrontendTurnState::Busy
1108 } else {
1109 FrontendTurnState::Idle
1110 },
1111 connection_state: if self.is_shutting_down() {
1112 FrontendConnectionState::ShuttingDown
1113 } else {
1114 FrontendConnectionState::Connected
1115 },
1116 extensions: Default::default(),
1117 }
1118 }
1119
1120 pub fn frontend_attach(
1125 &self,
1126 history_limit: usize,
1127 ) -> Result<FrontendAttachment, FrontendRuntimeError> {
1128 let live = self.frontend_subscribe();
1129 let snapshot = self.frontend_snapshot(history_limit)?;
1130 Ok(FrontendAttachment::new(
1131 snapshot.descriptor,
1132 snapshot.history,
1133 snapshot.history_cursor,
1134 snapshot.replay,
1135 live,
1136 None,
1137 ))
1138 }
1139
1140 pub fn frontend_subscribe(&self) -> broadcast::Receiver<FrontendEvent> {
1144 self.frontend_events.subscribe()
1145 }
1146
1147 pub fn frontend_snapshot(
1149 &self,
1150 history_limit: usize,
1151 ) -> Result<FrontendAttachSnapshot, FrontendRuntimeError> {
1152 let state = self
1153 .frontend_state
1154 .lock()
1155 .unwrap_or_else(std::sync::PoisonError::into_inner);
1156 let limit = history_limit.min(SERVER_HISTORY_CAPACITY);
1157 let start = state.history.len().saturating_sub(limit);
1158 let replay = state
1159 .replay
1160 .iter()
1161 .filter(|event| event.sequence > state.history_cursor)
1162 .cloned()
1163 .collect::<VecDeque<_>>();
1164 if let Some(first) = replay.front() {
1165 let expected = state.history_cursor.saturating_add(1);
1166 if first.sequence > expected {
1167 return Err(FrontendRuntimeError::ReplayGap(first.sequence - expected));
1168 }
1169 }
1170 Ok(FrontendAttachSnapshot {
1171 descriptor: self.frontend_descriptor(),
1172 history: state.history[start..].to_vec(),
1173 history_cursor: state.history_cursor,
1174 replay,
1175 })
1176 }
1177
1178 fn publish_frontend_payload(&self, payload: Value) {
1179 let event = {
1180 let mut state = self
1181 .frontend_state
1182 .lock()
1183 .unwrap_or_else(std::sync::PoisonError::into_inner);
1184 let event = FrontendEvent::new(state.next_sequence, payload);
1185 state.next_sequence = state.next_sequence.saturating_add(1);
1186 state.replay.push_back(event.clone());
1187 while state.replay.len() > FRONTEND_REPLAY_CAPACITY {
1188 state.replay.pop_front();
1189 }
1190 event
1191 };
1192 let _ = self.frontend_events.send(event);
1193 }
1194
1195 fn publish_runtime_payload(&self, payload: Value) {
1200 let _ = self.events.send(payload.clone());
1201 self.publish_frontend_payload(payload);
1202 }
1203
1204 pub fn session_id(&self) -> &str {
1206 &self.session_id
1207 }
1208
1209 fn claim_submit(&self) -> Result<SdkSubmitClaim, RuntimeSubmitError> {
1210 let mut current_cancel = self
1215 .current_cancel
1216 .lock()
1217 .unwrap_or_else(std::sync::PoisonError::into_inner);
1218 if !self.accepting_submits.load(Ordering::SeqCst) {
1219 return Err(RuntimeSubmitError::Interrupted);
1220 }
1221 if self.busy.swap(true, Ordering::SeqCst) {
1222 return Err(RuntimeSubmitError::Busy);
1223 }
1224 if !self.accepting_submits.load(Ordering::SeqCst) {
1228 self.busy.store(false, Ordering::SeqCst);
1229 self.turn_finished.notify_waiters();
1230 return Err(RuntimeSubmitError::Interrupted);
1231 }
1232 let cancel = Arc::new(Notify::new());
1233 *current_cancel = Some(cancel.clone());
1234 drop(current_cancel);
1235 self.steer_queue
1236 .lock()
1237 .unwrap_or_else(std::sync::PoisonError::into_inner)
1238 .open();
1239 Ok(SdkSubmitClaim {
1240 inbox: self.steer_queue.clone(),
1241 busy: self.busy.clone(),
1242 cancel,
1243 current_cancel: self.current_cancel.clone(),
1244 turn_finished: self.turn_finished.clone(),
1245 scheduler_changed: self.scheduler_changed.clone(),
1246 frontend_events: self.frontend_events.clone(),
1247 frontend_state: self.frontend_state.clone(),
1248 lifecycle_started: false,
1249 })
1250 }
1251
1252 async fn submit_claimed(
1253 &self,
1254 prompt: String,
1255 image_urls: Vec<String>,
1256 mut submit_claim: SdkSubmitClaim,
1257 ) -> Result<String, RuntimeSubmitError> {
1258 let cancel = submit_claim.cancel.clone();
1259 self.publish_frontend_payload(json!({"type": "user_message", "text": &prompt}));
1260 self.publish_frontend_payload(json!({
1261 "type": "turn_started",
1262 "schema_version": FRONTEND_EVENT_SCHEMA_VERSION
1263 }));
1264 submit_claim.mark_lifecycle_started();
1265 let outcome = {
1266 let mut agent = self.agent.lock().await;
1267 let result = tokio::select! {
1268 biased;
1269 _ = cancel.notified() => Err(RuntimeSubmitError::Interrupted),
1270 result = async {
1271 if image_urls.is_empty() {
1272 agent.inner_mut().send(&prompt).await
1273 } else {
1274 agent.inner_mut().send_with_images(&prompt, &image_urls).await
1275 }
1276 } => result.map_err(|error| RuntimeSubmitError::Agent(error.to_string())),
1277 };
1278 if result.is_ok() {
1279 if let Some(hook) = &self.on_turn_complete {
1280 hook(&agent);
1281 }
1282 }
1283 let history = bounded_history_snapshot(agent.history());
1287 *self.history_snapshot.write().await = history.clone();
1288 let mut state = self
1289 .frontend_state
1290 .lock()
1291 .unwrap_or_else(std::sync::PoisonError::into_inner);
1292 state.history = history;
1293 state.history_cursor = state.next_sequence.saturating_sub(1);
1294 let request_history = compact_frontend_request_history(&state.replay);
1301 state.replay.clear();
1302 for payload in request_history {
1303 let event = FrontendEvent::new(state.next_sequence, payload);
1304 state.next_sequence = state.next_sequence.saturating_add(1);
1305 state.replay.push_back(event);
1306 while state.replay.len() > FRONTEND_REPLAY_CAPACITY {
1307 state.replay.pop_front();
1308 }
1309 }
1310 result
1311 };
1312 *self
1313 .current_cancel
1314 .lock()
1315 .unwrap_or_else(std::sync::PoisonError::into_inner) = None;
1316 let lifecycle = match &outcome {
1317 Ok(reply) => json!({
1318 "type": "turn_succeeded",
1319 "schema_version": FRONTEND_EVENT_SCHEMA_VERSION,
1320 "reply": reply,
1321 }),
1322 Err(RuntimeSubmitError::Interrupted) => json!({
1323 "type": "turn_interrupted",
1324 "schema_version": FRONTEND_EVENT_SCHEMA_VERSION
1325 }),
1326 Err(error) => json!({
1327 "type": "turn_failed",
1328 "schema_version": FRONTEND_EVENT_SCHEMA_VERSION,
1329 "message": error.to_string()
1330 }),
1331 };
1332 self.publish_frontend_payload(lifecycle);
1333 submit_claim.mark_lifecycle_finished();
1334 drop(submit_claim);
1338 outcome
1339 }
1340
1341 pub async fn submit(&self, prompt: impl Into<String>) -> Result<String, RuntimeSubmitError> {
1343 let submit_claim = self.claim_submit()?;
1344 self.submit_claimed(prompt.into(), Vec::new(), submit_claim)
1345 .await
1346 }
1347
1348 pub async fn submit_with_images(
1350 &self,
1351 prompt: impl Into<String>,
1352 image_urls: Vec<String>,
1353 ) -> Result<String, RuntimeSubmitError> {
1354 let submit_claim = self.claim_submit()?;
1355 self.submit_claimed(prompt.into(), image_urls, submit_claim)
1356 .await
1357 }
1358
1359 pub fn send_input(self: &Arc<Self>, prompt: String) -> Result<(), RuntimeSubmitError> {
1362 let submit_claim = self.claim_submit()?;
1363 let runtime = self.clone();
1364 tokio::spawn(async move {
1365 let _ = runtime
1366 .submit_claimed(prompt, Vec::new(), submit_claim)
1367 .await;
1368 });
1369 Ok(())
1370 }
1371
1372 pub fn steer(&self, prompt: impl Into<String>) -> Result<(), FrontendRuntimeError> {
1376 let accepted = self
1377 .steer_queue
1378 .lock()
1379 .unwrap_or_else(std::sync::PoisonError::into_inner)
1380 .enqueue(prompt.into());
1381 if accepted {
1382 Ok(())
1383 } else {
1384 Err(FrontendRuntimeError::UnsupportedAction("steer"))
1385 }
1386 }
1387
1388 pub fn respond(&self, response: FrontendResponse) -> Result<(), FrontendRuntimeError> {
1390 self.frontend_requests
1391 .as_ref()
1392 .ok_or(FrontendRuntimeError::UnsupportedAction("respond"))?
1393 .respond(response)
1394 }
1395
1396 pub async fn invoke(
1399 &self,
1400 operation: FrontendOperationInvocation,
1401 ) -> Result<FrontendOperationResult, FrontendRuntimeError> {
1402 match operation {
1403 FrontendOperationInvocation::Prompt {
1404 operation_id,
1405 arguments,
1406 } => {
1407 let prompt_name = self
1408 .frontend_operations
1409 .iter()
1410 .find(|descriptor| {
1411 descriptor.id == operation_id
1412 && descriptor.kind == FrontendOperationKind::Prompt
1413 })
1414 .and_then(|descriptor| descriptor.command.as_ref())
1415 .map(|command| command.name.as_str())
1416 .ok_or_else(|| {
1417 FrontendRuntimeError::UnsupportedOperation(operation_id.clone())
1418 })?;
1419 let prompt = if arguments.is_empty() {
1420 format!("/{prompt_name}")
1421 } else {
1422 format!("/{prompt_name} {arguments}")
1423 };
1424 let reply = self.submit(prompt).await?;
1425 Ok(FrontendOperationResult::Prompt { reply })
1426 }
1427 }
1428 }
1429
1430 pub async fn interrupt(&self) -> bool {
1432 let cancel = self
1433 .current_cancel
1434 .lock()
1435 .unwrap_or_else(std::sync::PoisonError::into_inner)
1436 .clone();
1437 match cancel {
1438 Some(cancel) => {
1439 cancel.notify_one();
1440 true
1441 }
1442 None => false,
1443 }
1444 }
1445
1446 pub fn status(&self) -> RuntimeStatus {
1448 RuntimeStatus {
1449 session_id: self.session_id.clone(),
1450 model: self.model.clone(),
1451 busy: self.busy.load(Ordering::SeqCst),
1452 shutting_down: self.is_shutting_down(),
1453 }
1454 }
1455
1456 pub async fn history(&self, limit: usize) -> Vec<ChatMessage> {
1460 let history = self.history_snapshot.read().await;
1461 let start = history.len().saturating_sub(limit);
1462 history[start..].to_vec()
1463 }
1464
1465 pub async fn finalize_with<R>(&self, finalize: impl FnOnce(&SdkAgent) -> R) -> R {
1470 let agent = self.agent.lock().await;
1471 finalize(&agent)
1472 }
1473
1474 pub async fn shutdown(&self) {
1477 let _barrier = self.shutdown_barrier.lock().await;
1478 self.accepting_submits.store(false, Ordering::SeqCst);
1483 self.interrupt().await;
1484 loop {
1485 let finished = self.turn_finished.notified();
1486 if !self.busy.load(Ordering::SeqCst) {
1487 break;
1488 }
1489 finished.await;
1490 }
1491 self.scheduler_changed.notify_one();
1495 let scheduler = self
1496 .scheduler_task
1497 .lock()
1498 .unwrap_or_else(std::sync::PoisonError::into_inner)
1499 .take();
1500 if let Some(scheduler) = scheduler {
1501 let _ = scheduler.await;
1502 }
1503 self.signal_shutdown();
1504 }
1505
1506 pub fn start_claude_scheduler(self: &Arc<Self>) -> bool {
1514 let mut task = self
1515 .scheduler_task
1516 .lock()
1517 .unwrap_or_else(std::sync::PoisonError::into_inner);
1518 if !self.accepting_submits.load(Ordering::SeqCst)
1519 || self.is_shutting_down()
1520 || self.scheduler_started.swap(true, Ordering::SeqCst)
1521 {
1522 return false;
1523 }
1524 let runtime = self.clone();
1525 *task = Some(tokio::spawn(
1526 async move { runtime.run_claude_scheduler().await },
1527 ));
1528 true
1529 }
1530
1531 pub fn claude_scheduler_started(&self) -> bool {
1533 self.scheduler_started.load(Ordering::SeqCst)
1534 }
1535
1536 async fn run_claude_scheduler(self: Arc<Self>) {
1537 use crate::claude_runtime_scheduler::ClaudeRuntimeTrigger;
1538
1539 loop {
1540 if !self.accepting_submits.load(Ordering::SeqCst) || self.is_shutting_down() {
1541 return;
1542 }
1543 let now = unix_seconds();
1544 let next = {
1545 let agent = self.agent.lock().await;
1546 agent
1547 .claude_runtime_manifest()
1548 .and_then(|manifest| manifest.next_due(now).ok().flatten())
1549 };
1550 let Some(next) = next else {
1551 tokio::select! {
1552 _ = self.wait_for_shutdown() => return,
1553 _ = self.scheduler_changed.notified() => continue,
1554 }
1555 };
1556 let delay =
1557 std::time::Duration::from_secs(next.due_unix.saturating_sub(now).max(0) as u64);
1558 if !delay.is_zero() {
1559 tokio::select! {
1560 _ = self.wait_for_shutdown() => return,
1561 _ = self.scheduler_changed.notified() => continue,
1562 _ = tokio::time::sleep(delay) => {}
1563 }
1564 }
1565
1566 let claimed: Vec<ClaudeRuntimeTrigger> = {
1567 let mut agent = self.agent.lock().await;
1568 let claimed = match agent.claude_runtime_manifest_mut() {
1569 Some(manifest) => manifest.claim_due(unix_seconds()),
1570 None => continue,
1571 };
1572 match claimed {
1573 Ok(claimed) => {
1574 if !claimed.is_empty() {
1575 if let Some(hook) = &self.on_turn_complete {
1576 hook(&agent);
1579 }
1580 }
1581 claimed
1582 }
1583 Err(error) => {
1584 self.publish_runtime_payload(json!({
1585 "type": "scheduler_error",
1586 "message": error.to_string(),
1587 "terminal": false
1588 }));
1589 Vec::new()
1590 }
1591 }
1592 };
1593
1594 for trigger in claimed {
1595 if !self.accepting_submits.load(Ordering::SeqCst) || self.is_shutting_down() {
1596 return;
1597 }
1598 self.deliver_scheduled_prompt(trigger).await;
1599 }
1600 }
1601 }
1602
1603 async fn deliver_scheduled_prompt(
1604 &self,
1605 trigger: crate::claude_runtime_scheduler::ClaudeRuntimeTrigger,
1606 ) {
1607 let kind = trigger.kind;
1608 let id = trigger.id.clone();
1609 let prompt = trigger
1610 .prompt
1611 .unwrap_or_else(|| "Scheduled wakeup".to_string());
1612 self.publish_runtime_payload(json!({
1613 "type": "scheduled_prompt_started",
1614 "kind": kind,
1615 "id": id,
1616 "due_unix": trigger.due_unix
1617 }));
1618 let result = self.submit(prompt).await;
1619 let now = unix_seconds();
1620 let mut agent = self.agent.lock().await;
1621 let update = match (agent.claude_runtime_manifest_mut(), &result) {
1622 (Some(manifest), Ok(_)) => manifest.complete_delivery(kind, &id),
1623 (Some(manifest), Err(RuntimeSubmitError::Busy)) => {
1624 manifest.defer_delivery(kind, &id, now.saturating_add(1))
1625 }
1626 (Some(manifest), Err(_)) => manifest.defer_delivery(kind, &id, now.saturating_add(60)),
1627 (None, _) => return,
1628 };
1629 if let Err(error) = update {
1630 self.publish_runtime_payload(json!({
1631 "type": "scheduler_error",
1632 "message": error.to_string(),
1633 "terminal": false
1634 }));
1635 return;
1636 }
1637 if let Some(hook) = &self.on_turn_complete {
1638 hook(&agent);
1639 }
1640 self.publish_runtime_payload(match result {
1641 Ok(_) => json!({"type": "scheduled_prompt_completed", "kind": kind, "id": id}),
1642 Err(error) => json!({
1643 "type": "scheduled_prompt_deferred",
1644 "kind": kind,
1645 "id": id,
1646 "message": error.to_string()
1647 }),
1648 });
1649 self.scheduler_changed.notify_waiters();
1650 }
1651
1652 pub fn is_shutting_down(&self) -> bool {
1655 self.shutting_down.load(Ordering::SeqCst)
1656 }
1657
1658 pub async fn wait_for_shutdown(&self) {
1661 if self.is_shutting_down() {
1666 return;
1667 }
1668 self.shutdown.notified().await;
1669 }
1670
1671 fn signal_shutdown(&self) {
1681 self.shutting_down.store(true, Ordering::SeqCst);
1682 self.shutdown.notify_waiters();
1683 }
1684
1685 pub async fn handle_request(self: &Arc<Self>, req: RpcRequest) -> Value {
1689 match req.method.as_str() {
1690 "submit" => self.handle_submit(req).await,
1691 "frontend.send_input" => self.handle_frontend_send_input(req),
1692 "interrupt" => self.handle_interrupt(req).await,
1693 "steer" => self.handle_steer(req),
1694 "respond" => self.handle_respond(req),
1695 "status" => self.handle_status(req).await,
1696 "history" => self.handle_history(req).await,
1697 "frontend.describe" => self.handle_frontend_describe(req),
1698 "frontend.attach" => self.handle_frontend_attach(req),
1699 "frontend.invoke" => self.handle_frontend_invoke(req).await,
1700 "shutdown" => self.handle_shutdown(req).await,
1701 other => rpc_error(req.id, -32601, format!("unknown method `{other}`")),
1702 }
1703 }
1704
1705 fn handle_frontend_send_input(self: &Arc<Self>, req: RpcRequest) -> Value {
1706 let Some(prompt) = req.params.get("prompt").and_then(Value::as_str) else {
1707 return rpc_error(
1708 req.id,
1709 -32602,
1710 "frontend.send_input requires a string `params.prompt`",
1711 );
1712 };
1713 match self.send_input(prompt.to_string()) {
1714 Ok(()) => rpc_ok(req.id, json!({"accepted": true})),
1715 Err(RuntimeSubmitError::Busy) => {
1716 rpc_error(req.id, -32000, "a turn is already in progress")
1717 }
1718 Err(RuntimeSubmitError::Interrupted) => rpc_error(req.id, -32001, "turn interrupted"),
1719 Err(RuntimeSubmitError::Agent(error)) => rpc_error(req.id, -32002, error),
1720 }
1721 }
1722
1723 async fn handle_submit(&self, req: RpcRequest) -> Value {
1734 let Some(prompt) = req.params.get("prompt").and_then(|v| v.as_str()) else {
1735 return rpc_error(req.id, -32602, "submit requires a string `params.prompt`");
1736 };
1737 match self.submit(prompt).await {
1738 Ok(reply) => rpc_ok(req.id, json!({"reply": reply})),
1739 Err(RuntimeSubmitError::Busy) => rpc_error(
1740 req.id,
1741 -32000,
1742 "a turn is already in progress; `interrupt` it or wait for its response before submitting another",
1743 ),
1744 Err(RuntimeSubmitError::Interrupted) => {
1745 rpc_error(req.id, -32001, "turn interrupted")
1746 }
1747 Err(RuntimeSubmitError::Agent(error)) => rpc_error(req.id, -32002, error),
1748 }
1749 }
1750
1751 async fn handle_interrupt(&self, req: RpcRequest) -> Value {
1757 if self.interrupt().await {
1758 rpc_ok(req.id, json!({"interrupted": true}))
1759 } else {
1760 rpc_ok(
1761 req.id,
1762 json!({"interrupted": false, "reason": "no turn in progress"}),
1763 )
1764 }
1765 }
1766
1767 fn handle_steer(&self, req: RpcRequest) -> Value {
1770 let Some(prompt) = req.params.get("prompt").and_then(Value::as_str) else {
1771 return rpc_error(req.id, -32602, "steer requires a string `params.prompt`");
1772 };
1773 match self.steer(prompt) {
1774 Ok(()) => rpc_ok(req.id, json!({"queued": true})),
1775 Err(error) => rpc_error(req.id, -32020, error.to_string()),
1776 }
1777 }
1778
1779 fn handle_respond(&self, req: RpcRequest) -> Value {
1780 let response = match req.params.get("response").cloned() {
1781 Some(value) => match serde_json::from_value::<FrontendResponse>(value) {
1782 Ok(response) => response,
1783 Err(error) => return rpc_error(req.id, -32602, error.to_string()),
1784 },
1785 None => return rpc_error(req.id, -32602, "respond requires `params.response`"),
1786 };
1787 match self.respond(response) {
1788 Ok(()) => rpc_ok(req.id, json!({"accepted": true})),
1789 Err(FrontendRuntimeError::UnsupportedAction(_)) => {
1790 rpc_error(req.id, -32020, "frontend respond is not enabled")
1791 }
1792 Err(FrontendRuntimeError::UnknownRequest(id)) => rpc_error(
1793 req.id,
1794 -32021,
1795 format!("frontend request {id} is not pending"),
1796 ),
1797 Err(error) => rpc_error(req.id, -32022, error.to_string()),
1798 }
1799 }
1800
1801 async fn handle_status(&self, req: RpcRequest) -> Value {
1805 rpc_ok(
1806 req.id,
1807 serde_json::to_value(self.status()).unwrap_or_default(),
1808 )
1809 }
1810
1811 async fn handle_history(&self, req: RpcRequest) -> Value {
1814 let limit = req
1815 .params
1816 .get("limit")
1817 .and_then(Value::as_u64)
1818 .unwrap_or(50)
1819 .clamp(1, SERVER_HISTORY_CAPACITY as u64) as usize;
1820 rpc_ok(req.id, json!({"messages": self.history(limit).await}))
1821 }
1822
1823 fn handle_frontend_describe(&self, req: RpcRequest) -> Value {
1824 rpc_ok(
1825 req.id,
1826 serde_json::to_value(self.frontend_descriptor()).unwrap_or_default(),
1827 )
1828 }
1829
1830 fn handle_frontend_attach(&self, req: RpcRequest) -> Value {
1831 let limit = req
1832 .params
1833 .get("limit")
1834 .and_then(Value::as_u64)
1835 .unwrap_or(50)
1836 .clamp(1, SERVER_HISTORY_CAPACITY as u64) as usize;
1837 match self.frontend_snapshot(limit) {
1838 Ok(snapshot) => rpc_ok(req.id, serde_json::to_value(snapshot).unwrap_or_default()),
1839 Err(error) => rpc_error(req.id, -32010, error.to_string()),
1840 }
1841 }
1842
1843 async fn handle_frontend_invoke(&self, req: RpcRequest) -> Value {
1844 let operation = match req.params.get("operation").cloned() {
1845 Some(value) => match serde_json::from_value::<FrontendOperationInvocation>(value) {
1846 Ok(operation) => operation,
1847 Err(error) => return rpc_error(req.id, -32602, error.to_string()),
1848 },
1849 None => {
1850 return rpc_error(
1851 req.id,
1852 -32602,
1853 "frontend.invoke requires `params.operation`",
1854 )
1855 }
1856 };
1857 match self.invoke(operation).await {
1858 Ok(result) => rpc_ok(req.id, serde_json::to_value(result).unwrap_or_default()),
1859 Err(FrontendRuntimeError::UnsupportedOperation(id)) => rpc_error(
1860 req.id,
1861 -32023,
1862 FrontendRuntimeError::UnsupportedOperation(id).to_string(),
1863 ),
1864 Err(FrontendRuntimeError::Submit(RuntimeSubmitError::Busy)) => {
1865 rpc_error(req.id, -32000, "a turn is already in progress")
1866 }
1867 Err(error) => rpc_error(req.id, -32022, error.to_string()),
1868 }
1869 }
1870
1871 async fn handle_shutdown(&self, req: RpcRequest) -> Value {
1877 self.shutdown().await;
1878 rpc_ok(req.id, json!({"shutting_down": true}))
1879 }
1880}
1881
1882fn compact_frontend_request_history(replay: &VecDeque<FrontendEvent>) -> Vec<Value> {
1889 let mut by_id: BTreeMap<u64, (Option<Value>, Option<Value>)> = BTreeMap::new();
1890 for event in replay {
1891 let (request_id, resolved) = match event.kind.as_str() {
1892 "request" => (
1893 event.payload.pointer("/request/id").and_then(Value::as_u64),
1894 false,
1895 ),
1896 "request_resolved" => (
1897 event.payload.get("request_id").and_then(Value::as_u64),
1898 true,
1899 ),
1900 _ => continue,
1901 };
1902 let Some(request_id) = request_id else {
1903 continue;
1904 };
1905 let entry = by_id.entry(request_id).or_default();
1906 let slot = if resolved { &mut entry.1 } else { &mut entry.0 };
1907 slot.get_or_insert_with(|| event.payload.clone());
1908 }
1909 by_id
1910 .into_values()
1911 .flat_map(|(request, resolution)| request.into_iter().chain(resolution))
1912 .collect()
1913}
1914
1915fn valid_frontend_command_name(name: &str) -> bool {
1916 !name.is_empty()
1917 && !name.starts_with('/')
1918 && name
1919 .chars()
1920 .all(|character| !character.is_whitespace() && !character.is_control())
1921}
1922
1923fn bounded_history_snapshot(history: &[ChatMessage]) -> Vec<ChatMessage> {
1924 let start = history.len().saturating_sub(SERVER_HISTORY_CAPACITY);
1925 history[start..].to_vec()
1926}
1927
1928fn unix_seconds() -> i64 {
1929 std::time::SystemTime::now()
1930 .duration_since(std::time::UNIX_EPOCH)
1931 .map(|duration| duration.as_secs().min(i64::MAX as u64) as i64)
1932 .unwrap_or(0)
1933}
1934
1935#[cfg(feature = "adapter-api")]
1960pub async fn run_stdio<R, W>(engine: Arc<RpcEngine>, reader: R, writer: W) -> std::io::Result<()>
1961where
1962 R: AsyncBufRead + Unpin + Send + 'static,
1963 W: AsyncWrite + Unpin + Send + 'static,
1964{
1965 let (out_tx, mut out_rx) = mpsc::unbounded_channel::<Value>();
1966
1967 let writer_task = tokio::spawn(async move {
1968 let mut writer = writer;
1969 while let Some(v) = out_rx.recv().await {
1970 let line = format!("{v}\n");
1971 if writer.write_all(line.as_bytes()).await.is_err() {
1972 break;
1973 }
1974 if writer.flush().await.is_err() {
1975 break;
1976 }
1977 }
1978 });
1979
1980 let mut events = engine.subscribe();
1981 let evt_tx = out_tx.clone();
1982 let evt_engine = engine.clone();
1983 let event_task = tokio::spawn(async move {
1984 loop {
1985 tokio::select! {
1986 biased;
1987 _ = evt_engine.wait_for_shutdown() => break,
1988 recv = events.recv() => {
1989 match recv {
1990 Ok(v) => {
1991 if evt_tx.send(json!({"event": v})).is_err() {
1992 break;
1993 }
1994 }
1995 Err(broadcast::error::RecvError::Lagged(_)) => continue,
1996 Err(broadcast::error::RecvError::Closed) => break,
1997 }
1998 }
1999 }
2000 }
2001 });
2002
2003 let mut reader = reader;
2004 loop {
2005 if engine.is_shutting_down() {
2006 break;
2007 }
2008 tokio::select! {
2009 biased;
2010 _ = engine.wait_for_shutdown() => break,
2011 line = read_bounded_line(&mut reader, SERVER_MAX_LINE_BYTES) => {
2012 match line {
2013 Ok(None) => {
2014 engine.signal_shutdown();
2022 break;
2023 }
2024 Ok(Some(text)) => {
2025 let text = text.trim();
2026 if text.is_empty() {
2027 continue;
2028 }
2029 match serde_json::from_str::<RpcRequest>(text) {
2030 Ok(req) => {
2031 let engine = engine.clone();
2032 let out_tx = out_tx.clone();
2033 tokio::spawn(async move {
2034 let resp = engine.handle_request(req).await;
2035 let _ = out_tx.send(resp);
2036 });
2037 }
2038 Err(e) => {
2039 let _ = out_tx.send(rpc_error(Value::Null, -32700, format!("parse error: {e}")));
2040 }
2041 }
2042 }
2043 Err(e) => {
2044 let _ = out_tx.send(rpc_error(Value::Null, -32700, format!("{e}")));
2045 }
2046 }
2047 }
2048 }
2049 }
2050 drop(out_tx);
2060 let _ = event_task.await;
2061 let _ = writer_task.await;
2062 Ok(())
2063}
2064
2065#[cfg(feature = "adapter-api")]
2068struct HttpRequest {
2069 method: String,
2070 path: String,
2072 query: String,
2073 headers: HashMap<String, String>,
2074 body: Vec<u8>,
2075}
2076
2077#[cfg(feature = "adapter-api")]
2085async fn read_http_request<R>(reader: &mut R) -> std::io::Result<Option<HttpRequest>>
2086where
2087 R: AsyncBufRead + AsyncRead + Unpin,
2088{
2089 const HEAD_LINE_CAP: usize = 8 * 1024;
2090 let Some(request_line) = read_bounded_line(reader, HEAD_LINE_CAP).await? else {
2091 return Ok(None);
2092 };
2093 let mut parts = request_line.split_whitespace();
2094 let method = parts.next().unwrap_or("").to_string();
2095 let target = parts.next().unwrap_or("").to_string();
2096 if method.is_empty() || target.is_empty() {
2097 return Err(std::io::Error::new(
2098 std::io::ErrorKind::InvalidData,
2099 "malformed request line",
2100 ));
2101 }
2102 let (path, query) = match target.split_once('?') {
2103 Some((p, q)) => (p.to_string(), q.to_string()),
2104 None => (target, String::new()),
2105 };
2106
2107 let mut headers = HashMap::new();
2108 let mut content_length: usize = 0;
2109 for _ in 0..MAX_HEADER_LINES {
2110 let Some(line) = read_bounded_line(reader, HEAD_LINE_CAP).await? else {
2111 return Ok(None);
2112 };
2113 if line.is_empty() {
2114 break;
2115 }
2116 if let Some((k, v)) = line.split_once(':') {
2117 let k = k.trim().to_ascii_lowercase();
2118 let v = v.trim().to_string();
2119 if k == "content-length" {
2120 content_length = v.parse().unwrap_or(0);
2121 }
2122 headers.insert(k, v);
2123 }
2124 }
2125 if content_length > SERVER_MAX_LINE_BYTES {
2126 return Err(std::io::Error::new(
2127 std::io::ErrorKind::InvalidData,
2128 format!("request body exceeded {SERVER_MAX_LINE_BYTES} byte cap"),
2129 ));
2130 }
2131 let mut body = vec![0u8; content_length];
2132 if content_length > 0 {
2133 reader.read_exact(&mut body).await?;
2134 }
2135 Ok(Some(HttpRequest {
2136 method,
2137 path,
2138 query,
2139 headers,
2140 body,
2141 }))
2142}
2143
2144#[cfg(feature = "adapter-api")]
2145async fn write_http_response<W: AsyncWrite + Unpin>(
2146 writer: &mut W,
2147 status: u16,
2148 reason: &str,
2149 content_type: &str,
2150 body: &[u8],
2151) -> std::io::Result<()> {
2152 let head = format!(
2153 "HTTP/1.1 {status} {reason}\r\nContent-Type: {content_type}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
2154 body.len()
2155 );
2156 writer.write_all(head.as_bytes()).await?;
2157 writer.write_all(body).await?;
2158 writer.flush().await
2159}
2160
2161#[cfg(feature = "adapter-api")]
2162fn browser_observer_asset(path: &str) -> Option<(&'static str, &'static [u8])> {
2163 match path {
2164 "/observer" | "/observer/" => Some((
2165 "text/html; charset=utf-8",
2166 include_bytes!("../embedded/frontend-browser/index.html"),
2167 )),
2168 "/observer/app.mjs" => Some((
2169 "text/javascript; charset=utf-8",
2170 include_bytes!("../embedded/frontend-browser/app.mjs"),
2171 )),
2172 "/observer/client.mjs" => Some((
2173 "text/javascript; charset=utf-8",
2174 include_bytes!("../embedded/frontend-browser/client.mjs"),
2175 )),
2176 "/observer/view.mjs" => Some((
2177 "text/javascript; charset=utf-8",
2178 include_bytes!("../embedded/frontend-browser/view.mjs"),
2179 )),
2180 "/observer/style.css" => Some((
2181 "text/css; charset=utf-8",
2182 include_bytes!("../embedded/frontend-browser/style.css"),
2183 )),
2184 "/observer/favicon.svg" | "/favicon.ico" => Some((
2185 "image/svg+xml",
2186 include_bytes!("../embedded/frontend-browser/favicon.svg"),
2187 )),
2188 "/frontend/client.mjs" => Some((
2189 "text/javascript; charset=utf-8",
2190 include_bytes!("../embedded/frontend/client.mjs"),
2191 )),
2192 "/frontend/generated-client.mjs" => Some((
2193 "text/javascript; charset=utf-8",
2194 include_bytes!("../embedded/frontend/generated-client.mjs"),
2195 )),
2196 "/frontend/generated.mjs" => Some((
2197 "text/javascript; charset=utf-8",
2198 include_bytes!("../embedded/frontend/generated.mjs"),
2199 )),
2200 _ => None,
2201 }
2202}
2203
2204#[cfg(feature = "adapter-api")]
2205async fn write_browser_observer_asset<W: AsyncWrite + Unpin>(
2206 writer: &mut W,
2207 content_type: &str,
2208 body: &[u8],
2209) -> std::io::Result<()> {
2210 let head = format!(
2211 "HTTP/1.1 200 OK\r\n\
2212 Content-Type: {content_type}\r\n\
2213 Content-Length: {}\r\n\
2214 Cache-Control: no-store\r\n\
2215 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\
2216 Referrer-Policy: no-referrer\r\n\
2217 X-Content-Type-Options: nosniff\r\n\
2218 Connection: close\r\n\r\n",
2219 body.len()
2220 );
2221 writer.write_all(head.as_bytes()).await?;
2222 writer.write_all(body).await?;
2223 writer.flush().await
2224}
2225
2226#[cfg(feature = "adapter-api")]
2228#[derive(Clone)]
2229pub struct RuntimeHttpCredential {
2230 token: Arc<str>,
2231 authorization: RuntimeAuthorization,
2232 client_id: Option<crate::RuntimeClientId>,
2233 bootstrap: bool,
2234 runtime_id: Option<Arc<str>>,
2235 generation: Option<[u8; 16]>,
2236 revocation: Option<Arc<RuntimeCredentialRevocation>>,
2237}
2238
2239#[cfg(feature = "adapter-api")]
2240impl RuntimeHttpCredential {
2241 pub fn new(token: impl Into<Arc<str>>, authorization: RuntimeAuthorization) -> Self {
2244 Self {
2245 token: token.into(),
2246 authorization,
2247 client_id: None,
2248 bootstrap: false,
2249 runtime_id: None,
2250 generation: None,
2251 revocation: None,
2252 }
2253 }
2254
2255 pub fn owner(token: impl Into<Arc<str>>) -> Self {
2260 Self {
2261 token: token.into(),
2262 authorization: RuntimeAuthorization::owner(),
2263 client_id: None,
2264 bootstrap: true,
2265 runtime_id: None,
2266 generation: None,
2267 revocation: None,
2268 }
2269 }
2270
2271 pub fn observer(token: impl Into<Arc<str>>) -> Self {
2273 Self::new(token, RuntimeAuthorization::observer())
2274 }
2275
2276 fn frontend(
2277 token: impl Into<Arc<str>>,
2278 client_id: crate::RuntimeClientId,
2279 authorization: RuntimeAuthorization,
2280 runtime_id: impl Into<Arc<str>>,
2281 generation: [u8; 16],
2282 ) -> Self {
2283 Self {
2284 token: token.into(),
2285 authorization,
2286 client_id: Some(client_id),
2287 bootstrap: false,
2288 runtime_id: Some(runtime_id.into()),
2289 generation: Some(generation),
2290 revocation: Some(Arc::new(RuntimeCredentialRevocation::new())),
2291 }
2292 }
2293}
2294
2295#[cfg(feature = "adapter-api")]
2296struct AuthenticatedRuntimeHttpCredential {
2297 authorization: RuntimeAuthorization,
2298 client_id: Option<crate::RuntimeClientId>,
2299 bootstrap: bool,
2300 revocation: Option<tokio::sync::watch::Receiver<bool>>,
2301 attachment: Option<RuntimeCredentialAttachment>,
2302 via_bearer_header: bool,
2303}
2304
2305#[cfg(feature = "adapter-api")]
2306struct RuntimeCredentialRevocation {
2307 signal: tokio::sync::watch::Sender<bool>,
2308 active_attachments: AtomicUsize,
2309 drained: tokio::sync::Notify,
2310}
2311
2312#[cfg(feature = "adapter-api")]
2313impl RuntimeCredentialRevocation {
2314 fn new() -> Self {
2315 let (signal, _) = tokio::sync::watch::channel(false);
2316 Self {
2317 signal,
2318 active_attachments: AtomicUsize::new(0),
2319 drained: tokio::sync::Notify::new(),
2320 }
2321 }
2322
2323 fn register(self: &Arc<Self>) -> RuntimeCredentialAttachment {
2324 self.active_attachments.fetch_add(1, Ordering::AcqRel);
2325 RuntimeCredentialAttachment {
2326 revocation: self.clone(),
2327 }
2328 }
2329
2330 async fn revoke_and_wait(&self) {
2331 let _ = self.signal.send(true);
2332 loop {
2333 let drained = self.drained.notified();
2334 if self.active_attachments.load(Ordering::Acquire) == 0 {
2335 return;
2336 }
2337 drained.await;
2338 }
2339 }
2340}
2341
2342#[cfg(feature = "adapter-api")]
2343struct RuntimeCredentialAttachment {
2344 revocation: Arc<RuntimeCredentialRevocation>,
2345}
2346
2347#[cfg(feature = "adapter-api")]
2348impl Drop for RuntimeCredentialAttachment {
2349 fn drop(&mut self) {
2350 if self
2351 .revocation
2352 .active_attachments
2353 .fetch_sub(1, Ordering::AcqRel)
2354 == 1
2355 {
2356 self.revocation.drained.notify_one();
2361 }
2362 }
2363}
2364
2365#[cfg(feature = "adapter-api")]
2366struct IssuedRuntimeHttpCredential(Arc<str>);
2367
2368#[cfg(feature = "adapter-api")]
2369impl IssuedRuntimeHttpCredential {
2370 fn as_bytes(&self) -> &[u8] {
2371 self.0.as_bytes()
2372 }
2373}
2374
2375#[cfg(feature = "adapter-api")]
2376struct RuntimeHttpCredentialRegistry {
2377 credentials: StdMutex<Vec<RuntimeHttpCredential>>,
2378 runtime_id: String,
2379 generation: [u8; 16],
2380}
2381
2382#[cfg(feature = "adapter-api")]
2383impl RuntimeHttpCredentialRegistry {
2384 fn new(
2385 runtime_id: impl Into<String>,
2386 credentials: Vec<RuntimeHttpCredential>,
2387 ) -> std::io::Result<Arc<Self>> {
2388 let mut generation = [0_u8; 16];
2389 getrandom::getrandom(&mut generation).map_err(|error| {
2390 std::io::Error::other(format!(
2391 "cannot create runtime credential generation: {error}"
2392 ))
2393 })?;
2394 Ok(Arc::new(Self {
2395 credentials: StdMutex::new(credentials),
2396 runtime_id: runtime_id.into(),
2397 generation,
2398 }))
2399 }
2400
2401 fn authenticate(&self, request: &HttpRequest) -> Option<AuthenticatedRuntimeHttpCredential> {
2402 let credentials = self
2403 .credentials
2404 .lock()
2405 .unwrap_or_else(std::sync::PoisonError::into_inner);
2406 debug_assert!(credentials.iter().all(|credential| {
2407 credential.client_id.is_none()
2408 || (credential.runtime_id.as_deref() == Some(self.runtime_id.as_str())
2409 && credential.generation == Some(self.generation))
2410 }));
2411 check_auth(request, &credentials)
2412 }
2413
2414 fn issue_frontend(
2415 &self,
2416 client_id: crate::RuntimeClientId,
2417 observer: bool,
2418 ) -> std::io::Result<IssuedRuntimeHttpCredential> {
2419 let authorization = if observer {
2420 RuntimeAuthorization::observer()
2421 } else {
2422 RuntimeAuthorization::interactive()
2423 };
2424 for _ in 0..3 {
2425 let mut secret = [0_u8; 32];
2426 getrandom::getrandom(&mut secret).map_err(|error| {
2427 std::io::Error::other(format!("cannot mint frontend credential: {error}"))
2428 })?;
2429 let token: Arc<str> = encode_credential(&secret).into();
2430 secret.fill(0);
2431 let mut credentials = self
2432 .credentials
2433 .lock()
2434 .unwrap_or_else(std::sync::PoisonError::into_inner);
2435 if credentials
2436 .iter()
2437 .any(|credential| constant_time_eq(token.as_bytes(), credential.token.as_bytes()))
2438 {
2439 continue;
2440 }
2441 credentials.push(RuntimeHttpCredential::frontend(
2442 token.clone(),
2443 client_id,
2444 authorization,
2445 self.runtime_id.clone(),
2446 self.generation,
2447 ));
2448 return Ok(IssuedRuntimeHttpCredential(token));
2449 }
2450 Err(std::io::Error::new(
2451 std::io::ErrorKind::AlreadyExists,
2452 "frontend credential collision limit exceeded",
2453 ))
2454 }
2455
2456 async fn revoke_client(&self, client_id: &crate::RuntimeClientId) -> bool {
2457 let revocations = {
2460 let mut credentials = self
2461 .credentials
2462 .lock()
2463 .unwrap_or_else(std::sync::PoisonError::into_inner);
2464 let mut revocations = Vec::new();
2465 credentials.retain(|credential| {
2466 if credential.client_id.as_ref() == Some(client_id) {
2467 if let Some(revocation) = &credential.revocation {
2468 revocations.push(revocation.clone());
2469 }
2470 false
2471 } else {
2472 true
2473 }
2474 });
2475 revocations
2476 };
2477 let revoked = !revocations.is_empty();
2478 for revocation in revocations {
2479 revocation.revoke_and_wait().await;
2480 }
2481 revoked
2482 }
2483}
2484
2485#[cfg(feature = "adapter-api")]
2486fn encode_credential(secret: &[u8; 32]) -> String {
2487 const HEX: &[u8; 16] = b"0123456789abcdef";
2488 let mut encoded = String::with_capacity(64);
2489 for byte in secret {
2490 encoded.push(HEX[(byte >> 4) as usize] as char);
2491 encoded.push(HEX[(byte & 0x0f) as usize] as char);
2492 }
2493 encoded
2494}
2495
2496#[cfg(feature = "adapter-api")]
2503fn check_auth(
2504 req: &HttpRequest,
2505 credentials: &[RuntimeHttpCredential],
2506) -> Option<AuthenticatedRuntimeHttpCredential> {
2507 if let Some(auth) = req.headers.get("authorization") {
2508 if let Some(t) = auth.strip_prefix("Bearer ") {
2509 for credential in credentials {
2510 if constant_time_eq(t.as_bytes(), credential.token.as_bytes()) {
2511 return Some(AuthenticatedRuntimeHttpCredential {
2512 authorization: credential.authorization.clone(),
2513 client_id: credential.client_id.clone(),
2514 bootstrap: credential.bootstrap,
2515 revocation: credential
2516 .revocation
2517 .as_ref()
2518 .map(|revocation| revocation.signal.subscribe()),
2519 attachment: credential.revocation.as_ref().and_then(|revocation| {
2520 matches!(req.path.as_str(), "/events" | "/frontend/events")
2521 .then(|| revocation.register())
2522 }),
2523 via_bearer_header: true,
2524 });
2525 }
2526 }
2527 }
2528 }
2529 for pair in req.query.split('&') {
2530 if let Some((k, v)) = pair.split_once('=') {
2531 if k == "token" {
2532 for credential in credentials
2533 .iter()
2534 .filter(|credential| credential.client_id.is_none())
2535 {
2536 if constant_time_eq(v.as_bytes(), credential.token.as_bytes()) {
2537 return Some(AuthenticatedRuntimeHttpCredential {
2538 authorization: credential.authorization.clone(),
2539 client_id: credential.client_id.clone(),
2540 bootstrap: credential.bootstrap,
2541 revocation: None,
2542 attachment: None,
2543 via_bearer_header: false,
2544 });
2545 }
2546 }
2547 }
2548 }
2549 }
2550 None
2551}
2552
2553#[cfg(feature = "adapter-api")]
2554fn coordinated_http_client(
2555 request: &HttpRequest,
2556 coordinator: &Arc<CoordinatedRuntime>,
2557 credential: AuthenticatedRuntimeHttpCredential,
2558) -> Result<Arc<CoordinatedRuntimeClient>, crate::RuntimeLeaseError> {
2559 let supplied_client_id = request
2563 .headers
2564 .get("x-supercode-client-id")
2565 .map(String::as_str);
2566 let client_id = match credential.client_id.as_ref() {
2567 Some(bound) if supplied_client_id == Some(bound.as_str()) => bound.as_str(),
2568 Some(_) => return Err(crate::RuntimeLeaseError::InvalidClientId),
2569 None => supplied_client_id.unwrap_or("legacy-owner"),
2570 };
2571 let mut authorization = credential.authorization;
2572 if let Some(requested) = request.headers.get("x-supercode-permissions") {
2573 authorization = authorization.restrict_to(&RuntimeAuthorization::parse_header(requested)?);
2574 }
2575 Ok(coordinator.client(RuntimeClientId::parse(client_id)?, authorization))
2576}
2577
2578#[cfg(feature = "adapter-api")]
2579async fn coordinated_runtime_rpc(
2580 client: Arc<CoordinatedRuntimeClient>,
2581 request: RpcRequest,
2582) -> Value {
2583 let id = request.id.clone();
2584 let method = crate::FrontendFacadeMethod::from_wire_name(&request.method);
2585 let result = match method {
2586 Some(crate::FrontendFacadeMethod::TakeControl) => client
2587 .take_control()
2588 .and_then(|snapshot| serde_json::to_value(snapshot).map_err(json_sdk_error)),
2589 Some(crate::FrontendFacadeMethod::Heartbeat) => client
2590 .heartbeat()
2591 .and_then(|snapshot| serde_json::to_value(snapshot).map_err(json_sdk_error)),
2592 Some(crate::FrontendFacadeMethod::Lease) => client
2593 .lease_snapshot()
2594 .and_then(|snapshot| serde_json::to_value(snapshot).map_err(json_sdk_error)),
2595 Some(crate::FrontendFacadeMethod::Detach) => {
2596 serde_json::to_value(client.detach()).map_err(json_sdk_error)
2597 }
2598 Some(crate::FrontendFacadeMethod::Close) => match client.close().await {
2599 Ok(()) => Ok(json!({"closed":true})),
2600 Err(error) => Err(error),
2601 },
2602 None if request.method == "shutdown" => match client.close().await {
2603 Ok(()) => Ok(json!({"shutting_down":true})),
2604 Err(error) => Err(error),
2605 },
2606 _ => return frontend_http_rpc(client, request).await,
2607 };
2608 match result {
2609 Ok(value) => rpc_ok(id, value),
2610 Err(error) => sdk_runtime_rpc_error(id, -32002, &error),
2611 }
2612}
2613
2614#[cfg(feature = "adapter-api")]
2615fn json_sdk_error(error: serde_json::Error) -> FrontendRuntimeError {
2616 FrontendRuntimeError::Transport(error.to_string())
2617}
2618
2619#[cfg(feature = "adapter-api")]
2620async fn handle_http_conn(
2621 stream: tokio::net::TcpStream,
2622 engine: Arc<RpcEngine>,
2623 coordinator: Arc<CoordinatedRuntime>,
2624 credentials: Arc<RuntimeHttpCredentialRegistry>,
2625) -> std::io::Result<()> {
2626 let peer_is_loopback = stream.peer_addr()?.ip().is_loopback();
2627 let (read_half, mut write_half) = stream.into_split();
2628 let mut reader = tokio::io::BufReader::new(read_half);
2629 let Some(req) = read_http_request(&mut reader).await? else {
2630 return Ok(());
2631 };
2632
2633 if req.method == "GET" {
2637 if let Some((content_type, body)) = browser_observer_asset(&req.path) {
2638 return write_browser_observer_asset(&mut write_half, content_type, body).await;
2639 }
2640 }
2641
2642 let Some(credential) = credentials.authenticate(&req) else {
2643 let body =
2644 sdk_runtime_rpc_error(Value::Null, -32030, &FrontendRuntimeError::Unauthenticated)
2645 .to_string();
2646 return write_http_response(
2647 &mut write_half,
2648 401,
2649 "Unauthorized",
2650 "application/json",
2651 body.as_bytes(),
2652 )
2653 .await;
2654 };
2655
2656 if matches!(
2657 req.path.as_str(),
2658 "/_supercode/frontend-credentials/mint" | "/_supercode/frontend-credentials/revoke"
2659 ) && !credential.via_bearer_header
2660 {
2661 let body =
2662 sdk_runtime_rpc_error(Value::Null, -32030, &FrontendRuntimeError::Unauthenticated)
2663 .to_string();
2664 return write_http_response(
2665 &mut write_half,
2666 401,
2667 "Unauthorized",
2668 "application/json",
2669 body.as_bytes(),
2670 )
2671 .await;
2672 }
2673
2674 if req.path == "/_supercode/frontend-credentials/mint" {
2675 if req.method != "POST" || !peer_is_loopback || !credential.bootstrap {
2676 let body = sdk_runtime_rpc_error(
2677 Value::Null,
2678 -32031,
2679 &FrontendRuntimeError::Unauthorized {
2680 permission: "bootstrap".into(),
2681 },
2682 )
2683 .to_string();
2684 return write_http_response(
2685 &mut write_half,
2686 403,
2687 "Forbidden",
2688 "application/json",
2689 body.as_bytes(),
2690 )
2691 .await;
2692 }
2693 let request: Value = match serde_json::from_slice(&req.body) {
2694 Ok(request) => request,
2695 Err(error) => {
2696 return write_http_response(
2697 &mut write_half,
2698 400,
2699 "Bad Request",
2700 "application/json",
2701 format!("{{\"error\":{}}}", json!(error.to_string())).as_bytes(),
2702 )
2703 .await;
2704 }
2705 };
2706 let Some(client_id) = request.get("clientId").and_then(Value::as_str) else {
2707 return write_http_response(
2708 &mut write_half,
2709 400,
2710 "Bad Request",
2711 "application/json",
2712 b"{\"error\":\"mint request omitted clientId\"}",
2713 )
2714 .await;
2715 };
2716 let client_id = match crate::RuntimeClientId::parse(client_id) {
2717 Ok(client_id) => client_id,
2718 Err(error) => {
2719 return write_http_response(
2720 &mut write_half,
2721 400,
2722 "Bad Request",
2723 "application/json",
2724 format!("{{\"error\":{}}}", json!(error.to_string())).as_bytes(),
2725 )
2726 .await;
2727 }
2728 };
2729 let observer = match request.get("grant").and_then(Value::as_str) {
2730 Some("observer") => true,
2731 Some("interactive") => false,
2732 _ => {
2733 return write_http_response(
2734 &mut write_half,
2735 400,
2736 "Bad Request",
2737 "application/json",
2738 b"{\"error\":\"grant must be observer or interactive\"}",
2739 )
2740 .await;
2741 }
2742 };
2743 let token = credentials.issue_frontend(client_id, observer)?;
2744 return write_http_response(
2745 &mut write_half,
2746 200,
2747 "OK",
2748 "application/octet-stream",
2749 token.as_bytes(),
2750 )
2751 .await;
2752 }
2753
2754 if req.path == "/_supercode/frontend-credentials/revoke" {
2755 if req.method != "POST" || !peer_is_loopback || !credential.bootstrap {
2756 let body = sdk_runtime_rpc_error(
2757 Value::Null,
2758 -32031,
2759 &FrontendRuntimeError::Unauthorized {
2760 permission: "bootstrap".into(),
2761 },
2762 )
2763 .to_string();
2764 return write_http_response(
2765 &mut write_half,
2766 403,
2767 "Forbidden",
2768 "application/json",
2769 body.as_bytes(),
2770 )
2771 .await;
2772 }
2773 let request: Value = match serde_json::from_slice(&req.body) {
2774 Ok(request) => request,
2775 Err(error) => {
2776 return write_http_response(
2777 &mut write_half,
2778 400,
2779 "Bad Request",
2780 "application/json",
2781 format!("{{\"error\":{}}}", json!(error.to_string())).as_bytes(),
2782 )
2783 .await;
2784 }
2785 };
2786 let Some(client_id) = request.get("clientId").and_then(Value::as_str) else {
2787 return write_http_response(
2788 &mut write_half,
2789 400,
2790 "Bad Request",
2791 "application/json",
2792 b"{\"error\":\"revoke request omitted clientId\"}",
2793 )
2794 .await;
2795 };
2796 let client_id = match crate::RuntimeClientId::parse(client_id) {
2797 Ok(client_id) => client_id,
2798 Err(error) => {
2799 return write_http_response(
2800 &mut write_half,
2801 400,
2802 "Bad Request",
2803 "application/json",
2804 format!("{{\"error\":{}}}", json!(error.to_string())).as_bytes(),
2805 )
2806 .await;
2807 }
2808 };
2809 let revoked = credentials.revoke_client(&client_id).await;
2810 if revoked {
2811 coordinator
2812 .client(client_id, RuntimeAuthorization::observer())
2813 .detach();
2814 }
2815 return write_http_response(
2816 &mut write_half,
2817 200,
2818 "OK",
2819 "application/json",
2820 if revoked {
2821 b"{\"revoked\":true}"
2822 } else {
2823 b"{\"revoked\":false}"
2824 },
2825 )
2826 .await;
2827 }
2828
2829 let mut revocation = credential.revocation.clone();
2830 let mut attachment = credential.attachment;
2831 let credential = AuthenticatedRuntimeHttpCredential {
2832 authorization: credential.authorization,
2833 client_id: credential.client_id,
2834 bootstrap: credential.bootstrap,
2835 revocation: None,
2836 attachment: None,
2837 via_bearer_header: credential.via_bearer_header,
2838 };
2839 let client = match coordinated_http_client(&req, &coordinator, credential) {
2840 Ok(client) => client,
2841 Err(error) => {
2842 let permission = match error {
2843 crate::RuntimeLeaseError::InvalidClientId => "client_id",
2844 crate::RuntimeLeaseError::InvalidAuthorization => "authorization",
2845 _ => "runtime",
2846 };
2847 let body = sdk_runtime_rpc_error(
2848 Value::Null,
2849 -32031,
2850 &FrontendRuntimeError::Unauthorized {
2851 permission: permission.into(),
2852 },
2853 )
2854 .to_string();
2855 return write_http_response(
2856 &mut write_half,
2857 403,
2858 "Forbidden",
2859 "application/json",
2860 body.as_bytes(),
2861 )
2862 .await;
2863 }
2864 };
2865
2866 match (req.method.as_str(), req.path.as_str()) {
2867 ("POST", "/rpc") => {
2868 let body_text = String::from_utf8_lossy(&req.body);
2869 let resp = match serde_json::from_str::<RpcRequest>(&body_text) {
2870 Ok(rpc_req) if matches!(rpc_req.method.as_str(), "status" | "history") => {
2871 engine.handle_request(rpc_req).await
2872 }
2873 Ok(rpc_req) => coordinated_runtime_rpc(client.clone(), rpc_req).await,
2874 Err(e) => rpc_error(Value::Null, -32700, format!("parse error: {e}")),
2875 };
2876 let body = resp.to_string();
2877 write_http_response(
2878 &mut write_half,
2879 200,
2880 "OK",
2881 "application/json",
2882 body.as_bytes(),
2883 )
2884 .await
2885 }
2886 ("GET", "/events") => {
2887 if let Err(error) = client.observe() {
2888 let body = sdk_runtime_rpc_error(Value::Null, -32002, &error).to_string();
2889 return write_http_response(
2890 &mut write_half,
2891 403,
2892 "Forbidden",
2893 "application/json",
2894 body.as_bytes(),
2895 )
2896 .await;
2897 }
2898 let mut events = engine.subscribe();
2902 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";
2903 if write_half.write_all(head.as_bytes()).await.is_err() {
2904 client.detach();
2905 return Ok(());
2906 }
2907 let _ = write_half.flush().await;
2908 loop {
2909 tokio::select! {
2910 biased;
2911 recv = events.recv() => {
2912 match recv {
2913 Ok(v) => {
2914 let line = format!("data: {v}\n\n");
2915 if write_half.write_all(line.as_bytes()).await.is_err() {
2916 break;
2917 }
2918 if write_half.flush().await.is_err() {
2919 break;
2920 }
2921 }
2922 Err(broadcast::error::RecvError::Lagged(_)) => continue,
2923 Err(broadcast::error::RecvError::Closed) => break,
2924 }
2925 }
2926 _ = engine.wait_for_shutdown() => break,
2931 _ = wait_for_credential_revocation(&mut revocation) => break,
2932 _ = reader.read_u8() => break,
2933 }
2934 }
2935 let _ = write_half.shutdown().await;
2936 client.detach();
2937 drop(attachment.take());
2938 Ok(())
2939 }
2940 ("GET", "/frontend/events") => {
2941 if let Err(error) = client.observe() {
2942 let body = sdk_runtime_rpc_error(Value::Null, -32002, &error).to_string();
2943 return write_http_response(
2944 &mut write_half,
2945 403,
2946 "Forbidden",
2947 "application/json",
2948 body.as_bytes(),
2949 )
2950 .await;
2951 }
2952 let mut events = engine.frontend_subscribe();
2957 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";
2958 if write_half.write_all(head.as_bytes()).await.is_err() {
2959 client.detach();
2960 return Ok(());
2961 }
2962 let _ = write_half.flush().await;
2963 loop {
2964 tokio::select! {
2965 biased;
2966 recv = events.recv() => {
2967 match recv {
2968 Ok(event) => {
2969 let value = serde_json::to_string(&event).unwrap_or_default();
2970 let line = format!("data: {value}\n\n");
2971 if write_half.write_all(line.as_bytes()).await.is_err() {
2972 break;
2973 }
2974 if write_half.flush().await.is_err() {
2975 break;
2976 }
2977 }
2978 Err(broadcast::error::RecvError::Lagged(_)) => break,
2979 Err(broadcast::error::RecvError::Closed) => break,
2980 }
2981 }
2982 _ = engine.wait_for_shutdown() => break,
2986 _ = wait_for_credential_revocation(&mut revocation) => break,
2987 _ = reader.read_u8() => break,
2988 }
2989 }
2990 let _ = write_half.shutdown().await;
2991 client.detach();
2992 drop(attachment.take());
2993 Ok(())
2994 }
2995 _ => {
2996 write_http_response(
2997 &mut write_half,
2998 404,
2999 "Not Found",
3000 "application/json",
3001 b"{\"error\":\"not found\"}",
3002 )
3003 .await
3004 }
3005 }
3006}
3007
3008#[cfg(feature = "adapter-api")]
3009async fn wait_for_credential_revocation(receiver: &mut Option<tokio::sync::watch::Receiver<bool>>) {
3010 let Some(receiver) = receiver else {
3011 std::future::pending::<()>().await;
3012 return;
3013 };
3014 if *receiver.borrow() {
3015 return;
3016 }
3017 while receiver.changed().await.is_ok() {
3018 if *receiver.borrow() {
3019 return;
3020 }
3021 }
3022}
3023
3024#[cfg(feature = "adapter-api")]
3025async fn handle_frontend_http_conn(
3026 stream: tokio::net::TcpStream,
3027 coordinator: Arc<CoordinatedRuntime>,
3028 events: broadcast::Sender<FrontendEvent>,
3029 credentials: Arc<[RuntimeHttpCredential]>,
3030) -> std::io::Result<()> {
3031 let (read_half, mut write_half) = stream.into_split();
3032 let mut reader = tokio::io::BufReader::new(read_half);
3033 let Some(req) = read_http_request(&mut reader).await? else {
3034 return Ok(());
3035 };
3036 if req.method == "GET" {
3037 if let Some((content_type, body)) = browser_observer_asset(&req.path) {
3038 return write_browser_observer_asset(&mut write_half, content_type, body).await;
3039 }
3040 }
3041 let Some(credential) = check_auth(&req, &credentials) else {
3042 return write_http_response(
3043 &mut write_half,
3044 401,
3045 "Unauthorized",
3046 "application/json",
3047 b"{\"error\":\"missing or invalid bearer token\"}",
3048 )
3049 .await;
3050 };
3051 let client = match coordinated_http_client(&req, &coordinator, credential) {
3052 Ok(client) => client,
3053 Err(error) => {
3054 let body = json!({"error":error.to_string()}).to_string();
3055 return write_http_response(
3056 &mut write_half,
3057 400,
3058 "Bad Request",
3059 "application/json",
3060 body.as_bytes(),
3061 )
3062 .await;
3063 }
3064 };
3065 match (req.method.as_str(), req.path.as_str()) {
3066 ("POST", "/rpc") => {
3067 let body_text = String::from_utf8_lossy(&req.body);
3068 let response = match serde_json::from_str::<RpcRequest>(&body_text) {
3069 Ok(request) => coordinated_runtime_rpc(client.clone(), request).await,
3070 Err(error) => rpc_error(Value::Null, -32700, format!("parse error: {error}")),
3071 };
3072 let body = response.to_string();
3073 write_http_response(
3074 &mut write_half,
3075 200,
3076 "OK",
3077 "application/json",
3078 body.as_bytes(),
3079 )
3080 .await
3081 }
3082 ("GET", "/frontend/events") => {
3083 if let Err(error) = client.observe() {
3084 let body = sdk_runtime_rpc_error(Value::Null, -32002, &error).to_string();
3085 return write_http_response(
3086 &mut write_half,
3087 403,
3088 "Forbidden",
3089 "application/json",
3090 body.as_bytes(),
3091 )
3092 .await;
3093 }
3094 let mut receiver = events.subscribe();
3095 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";
3096 if write_half.write_all(head.as_bytes()).await.is_err() {
3097 client.detach();
3098 return Ok(());
3099 }
3100 let _ = write_half.flush().await;
3101 loop {
3102 tokio::select! {
3103 _ = reader.read_u8() => break,
3104 event = receiver.recv() => match event {
3105 Ok(event) => {
3106 let value = serde_json::to_string(&event).unwrap_or_default();
3107 let line = format!("data: {value}\n\n");
3108 if write_half.write_all(line.as_bytes()).await.is_err() || write_half.flush().await.is_err() {
3109 break;
3110 }
3111 }
3112 Err(broadcast::error::RecvError::Lagged(_)) => break,
3113 Err(broadcast::error::RecvError::Closed) => break,
3114 }
3115 }
3116 }
3117 client.detach();
3118 Ok(())
3119 }
3120 _ => {
3121 write_http_response(
3122 &mut write_half,
3123 404,
3124 "Not Found",
3125 "application/json",
3126 b"{\"error\":\"not found\"}",
3127 )
3128 .await
3129 }
3130 }
3131}
3132
3133#[cfg(feature = "adapter-api")]
3134async fn frontend_http_rpc(runtime: Arc<dyn FrontendRuntime>, request: RpcRequest) -> Value {
3135 let id = request.id;
3136 let Some(method) = crate::FrontendFacadeMethod::from_wire_name(&request.method) else {
3137 return rpc_error(id, -32601, format!("unknown method `{}`", request.method));
3138 };
3139 match method {
3140 crate::FrontendFacadeMethod::Describe => match runtime.describe().await {
3141 Ok(descriptor) => rpc_ok(id, serde_json::to_value(descriptor).unwrap_or_default()),
3142 Err(error) => sdk_runtime_rpc_error(id, -32010, &error),
3143 },
3144 crate::FrontendFacadeMethod::Attach => {
3145 let limit = request
3146 .params
3147 .get("limit")
3148 .and_then(Value::as_u64)
3149 .unwrap_or(50)
3150 .clamp(1, SERVER_HISTORY_CAPACITY as u64) as usize;
3151 match runtime.attach(limit).await {
3152 Ok(attachment) => rpc_ok(
3153 id,
3154 serde_json::to_value(FrontendAttachSnapshot {
3155 descriptor: attachment.descriptor,
3156 history: attachment.history,
3157 history_cursor: attachment.history_cursor,
3158 replay: attachment.replay,
3159 })
3160 .unwrap_or_default(),
3161 ),
3162 Err(error) => sdk_runtime_rpc_error(id, -32010, &error),
3163 }
3164 }
3165 crate::FrontendFacadeMethod::SendInput => {
3166 let Some(prompt) = request.params.get("prompt").and_then(Value::as_str) else {
3167 return rpc_error(
3168 id,
3169 -32602,
3170 "frontend.send_input requires a string `params.prompt`",
3171 );
3172 };
3173 match runtime.clone().send_input(prompt.to_string()).await {
3174 Ok(()) => rpc_ok(id, json!({"accepted": true})),
3175 Err(error @ FrontendRuntimeError::Submit(RuntimeSubmitError::Busy)) => {
3176 sdk_runtime_rpc_error(id, -32000, &error)
3177 }
3178 Err(error) => sdk_runtime_rpc_error(id, -32002, &error),
3179 }
3180 }
3181 crate::FrontendFacadeMethod::Invoke => {
3182 let operation = request
3183 .params
3184 .get("operation")
3185 .cloned()
3186 .ok_or("frontend.invoke requires `params.operation`")
3187 .and_then(|value| {
3188 serde_json::from_value(value).map_err(|_| "invalid frontend operation")
3189 });
3190 match operation {
3191 Ok(operation) => match runtime.invoke(operation).await {
3192 Ok(result) => rpc_ok(id, serde_json::to_value(result).unwrap_or_default()),
3193 Err(error @ FrontendRuntimeError::UnsupportedOperation(_)) => {
3194 sdk_runtime_rpc_error(id, -32023, &error)
3195 }
3196 Err(error @ FrontendRuntimeError::Submit(RuntimeSubmitError::Busy)) => {
3197 sdk_runtime_rpc_error(id, -32000, &error)
3198 }
3199 Err(error @ FrontendRuntimeError::Submit(RuntimeSubmitError::Interrupted)) => {
3200 sdk_runtime_rpc_error(id, -32001, &error)
3201 }
3202 Err(error) => sdk_runtime_rpc_error(id, -32022, &error),
3203 },
3204 Err(message) => rpc_error(id, -32602, message),
3205 }
3206 }
3207 crate::FrontendFacadeMethod::Submit => {
3208 let Some(prompt) = request.params.get("prompt").and_then(Value::as_str) else {
3209 return rpc_error(id, -32602, "submit requires a string `params.prompt`");
3210 };
3211 let image_urls = match request.params.get("image_urls") {
3212 None => Vec::new(),
3213 Some(Value::Array(values)) => {
3214 let Some(urls) = values.iter().map(Value::as_str).collect::<Option<Vec<_>>>()
3215 else {
3216 return rpc_error(
3217 id,
3218 -32602,
3219 "submit requires string entries in `params.image_urls`",
3220 );
3221 };
3222 urls.into_iter().map(str::to_owned).collect()
3223 }
3224 Some(_) => {
3225 return rpc_error(id, -32602, "submit requires array `params.image_urls`")
3226 }
3227 };
3228 match runtime
3229 .submit_with_images(prompt.to_string(), image_urls)
3230 .await
3231 {
3232 Ok(reply) => rpc_ok(id, json!({"reply":reply})),
3233 Err(error @ FrontendRuntimeError::Submit(RuntimeSubmitError::Busy)) => {
3234 sdk_runtime_rpc_error(id, -32000, &error)
3235 }
3236 Err(error @ FrontendRuntimeError::Submit(RuntimeSubmitError::Interrupted)) => {
3237 sdk_runtime_rpc_error(id, -32001, &error)
3238 }
3239 Err(error) => sdk_runtime_rpc_error(id, -32002, &error),
3240 }
3241 }
3242 crate::FrontendFacadeMethod::Interrupt => match runtime.interrupt().await {
3243 Ok(interrupted) => rpc_ok(id, json!({"interrupted":interrupted})),
3244 Err(error) => sdk_runtime_rpc_error(id, -32002, &error),
3245 },
3246 crate::FrontendFacadeMethod::Steer => {
3247 let Some(prompt) = request.params.get("prompt").and_then(Value::as_str) else {
3248 return rpc_error(id, -32602, "steer requires a string `params.prompt`");
3249 };
3250 match runtime.steer(prompt.to_string()).await {
3251 Ok(()) => rpc_ok(id, json!({"queued":true})),
3252 Err(error @ FrontendRuntimeError::UnsupportedAction(_)) => {
3253 sdk_runtime_rpc_error(id, -32020, &error)
3254 }
3255 Err(error) => sdk_runtime_rpc_error(id, -32022, &error),
3256 }
3257 }
3258 crate::FrontendFacadeMethod::Respond => {
3259 let response = request
3260 .params
3261 .get("response")
3262 .cloned()
3263 .ok_or("respond requires `params.response`")
3264 .and_then(|value| serde_json::from_value(value).map_err(|_| "invalid response"));
3265 match response {
3266 Ok(response) => match runtime.respond(response).await {
3267 Ok(()) => rpc_ok(id, json!({"accepted":true})),
3268 Err(error @ FrontendRuntimeError::UnsupportedAction(_)) => {
3269 sdk_runtime_rpc_error(id, -32020, &error)
3270 }
3271 Err(error) => sdk_runtime_rpc_error(id, -32022, &error),
3272 },
3273 Err(message) => rpc_error(id, -32602, message),
3274 }
3275 }
3276 crate::FrontendFacadeMethod::Lease
3277 | crate::FrontendFacadeMethod::TakeControl
3278 | crate::FrontendFacadeMethod::Heartbeat
3279 | crate::FrontendFacadeMethod::Detach
3280 | crate::FrontendFacadeMethod::Close => rpc_error(
3281 id,
3282 -32020,
3283 format!(
3284 "frontend action `{}` requires a coordinated runtime",
3285 method.id()
3286 ),
3287 ),
3288 }
3289}
3290
3291#[cfg(feature = "adapter-api")]
3293pub(crate) struct FrontendHttpServer {
3294 address: SocketAddr,
3295 task: tokio::task::JoinHandle<()>,
3296}
3297
3298#[cfg(feature = "adapter-api")]
3299impl FrontendHttpServer {
3300 pub(crate) fn address(&self) -> SocketAddr {
3302 self.address
3303 }
3304}
3305
3306#[cfg(feature = "adapter-api")]
3307impl Drop for FrontendHttpServer {
3308 fn drop(&mut self) {
3309 self.task.abort();
3310 }
3311}
3312
3313#[cfg(feature = "adapter-api")]
3316pub(crate) async fn run_frontend_http(
3317 runtime: Arc<dyn FrontendRuntime>,
3318 events: broadcast::Sender<FrontendEvent>,
3319 bind: &str,
3320 token: Arc<str>,
3321) -> std::io::Result<FrontendHttpServer> {
3322 let listener = TcpListener::bind(bind).await?;
3323 let address = listener.local_addr()?;
3324 let coordinator = CoordinatedRuntime::new(runtime);
3325 let credentials: Arc<[RuntimeHttpCredential]> =
3326 vec![RuntimeHttpCredential::owner(token)].into();
3327 let task = tokio::spawn(async move {
3328 while let Ok((stream, _)) = listener.accept().await {
3329 let coordinator = coordinator.clone();
3330 let events = events.clone();
3331 let credentials = credentials.clone();
3332 tokio::spawn(async move {
3333 let _ = handle_frontend_http_conn(stream, coordinator, events, credentials).await;
3334 });
3335 }
3336 });
3337 Ok(FrontendHttpServer { address, task })
3338}
3339
3340#[cfg(feature = "adapter-api")]
3343pub struct FrontendWebSocketServer {
3344 address: SocketAddr,
3345 task: tokio::task::JoinHandle<()>,
3346}
3347
3348#[cfg(feature = "adapter-api")]
3349impl FrontendWebSocketServer {
3350 pub fn address(&self) -> SocketAddr {
3352 self.address
3353 }
3354}
3355
3356#[cfg(feature = "adapter-api")]
3357impl Drop for FrontendWebSocketServer {
3358 fn drop(&mut self) {
3359 self.task.abort();
3360 }
3361}
3362
3363#[cfg(feature = "adapter-api")]
3367pub async fn run_frontend_websocket(
3368 engine: Arc<RpcEngine>,
3369 bind: &str,
3370 credentials: Vec<RuntimeHttpCredential>,
3371) -> std::io::Result<FrontendWebSocketServer> {
3372 let runtime: Arc<dyn FrontendRuntime> = engine.clone();
3373 let events = engine.frontend_events.clone();
3374 run_frontend_websocket_runtime_inner(runtime, events, Some(engine), bind, credentials).await
3375}
3376
3377#[cfg(all(feature = "adapter-api", test))]
3382pub(crate) async fn run_frontend_websocket_runtime(
3383 runtime: Arc<dyn FrontendRuntime>,
3384 events: broadcast::Sender<FrontendEvent>,
3385 bind: &str,
3386 credentials: Vec<RuntimeHttpCredential>,
3387) -> std::io::Result<FrontendWebSocketServer> {
3388 run_frontend_websocket_runtime_inner(runtime, events, None, bind, credentials).await
3389}
3390
3391#[cfg(feature = "adapter-api")]
3392async fn run_frontend_websocket_runtime_inner(
3393 runtime: Arc<dyn FrontendRuntime>,
3394 events: broadcast::Sender<FrontendEvent>,
3395 shutdown_engine: Option<Arc<RpcEngine>>,
3396 bind: &str,
3397 credentials: Vec<RuntimeHttpCredential>,
3398) -> std::io::Result<FrontendWebSocketServer> {
3399 if credentials.is_empty()
3400 || credentials
3401 .iter()
3402 .any(|credential| credential.token.is_empty())
3403 {
3404 return Err(std::io::Error::new(
3405 std::io::ErrorKind::InvalidInput,
3406 "at least one non-empty runtime WebSocket credential is required",
3407 ));
3408 }
3409 let listener = TcpListener::bind(bind).await?;
3410 let address = listener.local_addr()?;
3411 let coordinator = CoordinatedRuntime::new(runtime);
3412 let credentials: Arc<[RuntimeHttpCredential]> = credentials.into();
3413 let task = tokio::spawn(async move {
3414 loop {
3415 tokio::select! {
3416 biased;
3417 _ = wait_for_optional_runtime_shutdown(shutdown_engine.as_ref()) => break,
3418 accepted = listener.accept() => {
3419 let Ok((stream, _)) = accepted else { continue };
3420 let coordinator = coordinator.clone();
3421 let credentials = credentials.clone();
3422 let events = events.clone();
3423 let shutdown_engine = shutdown_engine.clone();
3424 tokio::spawn(async move {
3425 let _ = handle_frontend_websocket(stream, events, shutdown_engine, coordinator, credentials).await;
3426 });
3427 }
3428 }
3429 }
3430 });
3431 Ok(FrontendWebSocketServer { address, task })
3432}
3433
3434#[cfg(feature = "adapter-api")]
3435async fn wait_for_optional_runtime_shutdown(engine: Option<&Arc<RpcEngine>>) {
3436 match engine {
3437 Some(engine) => engine.wait_for_shutdown().await,
3438 None => std::future::pending().await,
3439 }
3440}
3441
3442#[cfg(feature = "adapter-api")]
3443#[allow(clippy::result_large_err)] async fn handle_frontend_websocket(
3445 stream: tokio::net::TcpStream,
3446 events: broadcast::Sender<FrontendEvent>,
3447 shutdown_engine: Option<Arc<RpcEngine>>,
3448 coordinator: Arc<CoordinatedRuntime>,
3449 credentials: Arc<[RuntimeHttpCredential]>,
3450) -> Result<(), tokio_tungstenite::tungstenite::Error> {
3451 use std::sync::Mutex as SyncMutex;
3452 use tokio_tungstenite::tungstenite::handshake::server::{ErrorResponse, Request, Response};
3453
3454 let selected = Arc::new(SyncMutex::new(None::<Arc<CoordinatedRuntimeClient>>));
3455 let selected_by_callback = selected.clone();
3456 let socket = tokio_tungstenite::accept_hdr_async(
3457 stream,
3458 move |request: &Request, response: Response| -> Result<Response, ErrorResponse> {
3459 let reject = |status, message: &str| {
3460 tokio_tungstenite::tungstenite::http::Response::builder()
3461 .status(status)
3462 .body(Some(message.to_string()))
3463 .expect("static WebSocket rejection is valid")
3464 };
3465 if request.uri().path() != "/frontend/v2" {
3466 return Err(reject(404, "frontend WebSocket route not found"));
3467 }
3468 let token = request
3469 .headers()
3470 .get("authorization")
3471 .and_then(|value| value.to_str().ok())
3472 .and_then(|value| value.strip_prefix("Bearer "));
3473 let Some(credential) = token.and_then(|token| {
3474 credentials.iter().find(|credential| {
3475 constant_time_eq(token.as_bytes(), credential.token.as_bytes())
3476 })
3477 }) else {
3478 return Err(reject(401, "missing or invalid bearer token"));
3479 };
3480 let client_id = request
3481 .headers()
3482 .get("x-supercode-client-id")
3483 .and_then(|value| value.to_str().ok())
3484 .unwrap_or("legacy-websocket-owner");
3485 let Ok(client_id) = RuntimeClientId::parse(client_id) else {
3486 return Err(reject(400, "invalid runtime client id"));
3487 };
3488 let mut authorization = credential.authorization.clone();
3489 if let Some(requested) = request
3490 .headers()
3491 .get("x-supercode-permissions")
3492 .and_then(|value| value.to_str().ok())
3493 {
3494 let Ok(requested) = RuntimeAuthorization::parse_header(requested) else {
3495 return Err(reject(400, "invalid runtime authorization grant"));
3496 };
3497 authorization = authorization.restrict_to(&requested);
3498 }
3499 *selected_by_callback
3500 .lock()
3501 .unwrap_or_else(std::sync::PoisonError::into_inner) =
3502 Some(coordinator.client(client_id, authorization));
3503 Ok(response)
3504 },
3505 )
3506 .await?;
3507 let client = selected
3508 .lock()
3509 .unwrap_or_else(std::sync::PoisonError::into_inner)
3510 .take()
3511 .expect("successful WebSocket handshake selects a runtime client");
3512 if let Err(error) = client.observe() {
3513 let mut socket = socket;
3514 let value = sdk_runtime_rpc_error(Value::Null, -32002, &error).to_string();
3515 socket
3516 .send(tokio_tungstenite::tungstenite::Message::Text(value.into()))
3517 .await?;
3518 socket.close(None).await?;
3519 return Ok(());
3520 }
3521
3522 let mut events = events.subscribe();
3523 let (mut writer, mut reader) = socket.split();
3524 loop {
3525 tokio::select! {
3526 biased;
3527 incoming = reader.next() => match incoming {
3528 Some(Ok(tokio_tungstenite::tungstenite::Message::Text(text))) => {
3529 let response = match serde_json::from_str::<RpcRequest>(&text) {
3530 Ok(request) => coordinated_runtime_rpc(client.clone(), request).await,
3531 Err(error) => rpc_error(Value::Null, -32700, format!("parse error: {error}")),
3532 };
3533 writer.send(tokio_tungstenite::tungstenite::Message::Text(response.to_string().into())).await?;
3534 }
3535 Some(Ok(tokio_tungstenite::tungstenite::Message::Ping(payload))) => {
3536 writer.send(tokio_tungstenite::tungstenite::Message::Pong(payload)).await?;
3537 }
3538 Some(Ok(tokio_tungstenite::tungstenite::Message::Close(_))) | None => break,
3539 Some(Ok(_)) => {}
3540 Some(Err(error)) => {
3541 client.detach();
3542 return Err(error);
3543 }
3544 },
3545 event = events.recv() => match event {
3546 Ok(event) => {
3547 let notification = json!({
3548 "jsonrpc":"2.0",
3549 "method":"frontend.v2.event",
3550 "params":{"event":event},
3551 });
3552 writer.send(tokio_tungstenite::tungstenite::Message::Text(notification.to_string().into())).await?;
3553 }
3554 Err(broadcast::error::RecvError::Lagged(count)) => {
3555 let notification = json!({
3556 "jsonrpc":"2.0",
3557 "method":"frontend.v2.event",
3558 "params":{"error":{"name":"transport","message":format!("event replay gap: {count}")}},
3559 });
3560 writer.send(tokio_tungstenite::tungstenite::Message::Text(notification.to_string().into())).await?;
3561 break;
3562 }
3563 Err(broadcast::error::RecvError::Closed) => break,
3564 },
3565 _ = wait_for_optional_runtime_shutdown(shutdown_engine.as_ref()) => break,
3566 }
3567 }
3568 client.detach();
3569 Ok(())
3570}
3571
3572#[cfg(feature = "adapter-api")]
3580pub async fn run_http(
3581 engine: Arc<RpcEngine>,
3582 bind: &str,
3583 token: Arc<str>,
3584) -> std::io::Result<SocketAddr> {
3585 run_http_authorized(engine, bind, vec![RuntimeHttpCredential::owner(token)]).await
3586}
3587
3588#[cfg(feature = "adapter-api")]
3593pub async fn run_http_authorized(
3594 engine: Arc<RpcEngine>,
3595 bind: &str,
3596 credentials: Vec<RuntimeHttpCredential>,
3597) -> std::io::Result<SocketAddr> {
3598 run_http_authorized_with_lease_ttl(
3599 engine,
3600 bind,
3601 credentials,
3602 crate::DEFAULT_RUNTIME_LEASE_TTL_MS,
3603 )
3604 .await
3605}
3606
3607#[cfg(feature = "adapter-api")]
3610pub async fn run_http_authorized_with_lease_ttl(
3611 engine: Arc<RpcEngine>,
3612 bind: &str,
3613 credentials: Vec<RuntimeHttpCredential>,
3614 lease_ttl_ms: u64,
3615) -> std::io::Result<SocketAddr> {
3616 if credentials.is_empty()
3617 || credentials
3618 .iter()
3619 .any(|credential| credential.token.is_empty())
3620 {
3621 return Err(std::io::Error::new(
3622 std::io::ErrorKind::InvalidInput,
3623 "at least one non-empty runtime HTTP credential is required",
3624 ));
3625 }
3626 if lease_ttl_ms == 0 {
3627 return Err(std::io::Error::new(
3628 std::io::ErrorKind::InvalidInput,
3629 "runtime lease TTL must be non-zero",
3630 ));
3631 }
3632 let listener = TcpListener::bind(bind).await?;
3633 let local_addr = listener.local_addr()?;
3634 let credentials = RuntimeHttpCredentialRegistry::new(engine.session_id(), credentials)?;
3635 let eng = engine;
3636 let runtime: Arc<dyn FrontendRuntime> = eng.clone();
3637 let coordinator = CoordinatedRuntime::with_lease_ttl(runtime, lease_ttl_ms);
3638 tokio::spawn(async move {
3639 loop {
3640 tokio::select! {
3641 biased;
3642 _ = eng.wait_for_shutdown() => break,
3643 accepted = listener.accept() => {
3644 let Ok((stream, _addr)) = accepted else { continue };
3645 let eng = eng.clone();
3646 let coordinator = coordinator.clone();
3647 let credentials = credentials.clone();
3648 tokio::spawn(async move {
3649 let _ = handle_http_conn(stream, eng, coordinator, credentials).await;
3650 });
3651 }
3652 }
3653 }
3654 });
3655 Ok(local_addr)
3656}
3657
3658#[cfg(test)]
3659mod frontend_binding_conformance_tests;
3660
3661#[cfg(test)]
3662mod tests {
3663 use super::*;
3664 use tokio::io::BufReader;
3665
3666 fn cursor(data: &[u8]) -> BufReader<std::io::Cursor<Vec<u8>>> {
3667 BufReader::new(std::io::Cursor::new(data.to_vec()))
3668 }
3669
3670 #[cfg(all(feature = "adapter-api", supercode_workspace_assets))]
3671 #[test]
3672 fn packaged_observer_assets_match_the_sdk_sources() {
3673 let pairs: &[(&str, &[u8], &[u8])] = &[
3674 (
3675 "frontend-browser/index.html",
3676 include_bytes!("../embedded/frontend-browser/index.html"),
3677 include_bytes!("../../../sdk/frontend-browser/index.html"),
3678 ),
3679 (
3680 "frontend-browser/app.mjs",
3681 include_bytes!("../embedded/frontend-browser/app.mjs"),
3682 include_bytes!("../../../sdk/frontend-browser/app.mjs"),
3683 ),
3684 (
3685 "frontend-browser/client.mjs",
3686 include_bytes!("../embedded/frontend-browser/client.mjs"),
3687 include_bytes!("../../../sdk/frontend-browser/client.mjs"),
3688 ),
3689 (
3690 "frontend-browser/view.mjs",
3691 include_bytes!("../embedded/frontend-browser/view.mjs"),
3692 include_bytes!("../../../sdk/frontend-browser/view.mjs"),
3693 ),
3694 (
3695 "frontend-browser/style.css",
3696 include_bytes!("../embedded/frontend-browser/style.css"),
3697 include_bytes!("../../../sdk/frontend-browser/style.css"),
3698 ),
3699 (
3700 "frontend-browser/favicon.svg",
3701 include_bytes!("../embedded/frontend-browser/favicon.svg"),
3702 include_bytes!("../../../sdk/frontend-browser/favicon.svg"),
3703 ),
3704 (
3705 "frontend/client.mjs",
3706 include_bytes!("../embedded/frontend/client.mjs"),
3707 include_bytes!("../../../sdk/frontend/client.mjs"),
3708 ),
3709 (
3710 "frontend/generated-client.mjs",
3711 include_bytes!("../embedded/frontend/generated-client.mjs"),
3712 include_bytes!("../../../sdk/frontend/generated-client.mjs"),
3713 ),
3714 (
3715 "frontend/generated.mjs",
3716 include_bytes!("../embedded/frontend/generated.mjs"),
3717 include_bytes!("../../../sdk/frontend/generated.mjs"),
3718 ),
3719 ];
3720 for (name, packaged, source) in pairs {
3721 assert_eq!(packaged, source, "packaged observer asset drifted: {name}");
3722 }
3723 }
3724
3725 #[tokio::test]
3726 async fn admitted_submit_has_a_cancel_token_before_shutdown_observes_busy() {
3727 let agent =
3728 crate::Agent::new(crate::Config::builder().api_key("test-only-key").build()).unwrap();
3729 let engine = RpcEngine::new(agent, None);
3730 let claim = engine.claim_submit().unwrap();
3731 assert!(engine.busy.load(Ordering::SeqCst));
3732 assert!(engine
3733 .current_cancel
3734 .lock()
3735 .unwrap_or_else(std::sync::PoisonError::into_inner)
3736 .is_some());
3737
3738 let cancel = claim.cancel.clone();
3739 let shutdown_engine = engine.clone();
3740 let shutdown = tokio::spawn(async move { shutdown_engine.shutdown().await });
3741 tokio::time::timeout(std::time::Duration::from_secs(1), cancel.notified())
3742 .await
3743 .expect("shutdown must interrupt an admitted claim before its future starts");
3744 assert!(
3745 !shutdown.is_finished(),
3746 "shutdown must retain the barrier until the admitted claim drains"
3747 );
3748 drop(claim);
3749 tokio::time::timeout(std::time::Duration::from_secs(1), shutdown)
3750 .await
3751 .expect("claim drain must release shutdown")
3752 .unwrap();
3753 }
3754
3755 #[tokio::test]
3756 async fn read_bounded_line_reads_a_normal_line() {
3757 let mut r = cursor(b"hello\nworld\n");
3758 assert_eq!(
3759 read_bounded_line(&mut r, 1024).await.unwrap(),
3760 Some("hello".to_string())
3761 );
3762 assert_eq!(
3763 read_bounded_line(&mut r, 1024).await.unwrap(),
3764 Some("world".to_string())
3765 );
3766 assert_eq!(read_bounded_line(&mut r, 1024).await.unwrap(), None);
3767 }
3768
3769 #[tokio::test]
3770 async fn read_bounded_line_strips_trailing_cr() {
3771 let mut r = cursor(b"hello\r\n");
3772 assert_eq!(
3773 read_bounded_line(&mut r, 1024).await.unwrap(),
3774 Some("hello".to_string())
3775 );
3776 }
3777
3778 #[tokio::test]
3779 async fn read_bounded_line_returns_final_line_without_trailing_newline() {
3780 let mut r = cursor(b"no newline at eof");
3781 assert_eq!(
3782 read_bounded_line(&mut r, 1024).await.unwrap(),
3783 Some("no newline at eof".to_string())
3784 );
3785 assert_eq!(read_bounded_line(&mut r, 1024).await.unwrap(), None);
3786 }
3787
3788 #[tokio::test]
3789 async fn read_bounded_line_errors_and_resyncs_on_an_oversized_line() {
3790 let mut data = vec![b'x'; 20];
3791 data.push(b'\n');
3792 data.extend_from_slice(b"next\n");
3793 let mut r = cursor(&data);
3794 let err = read_bounded_line(&mut r, 10).await.unwrap_err();
3795 assert!(err.to_string().contains("10 byte cap"));
3796 assert_eq!(
3799 read_bounded_line(&mut r, 1024).await.unwrap(),
3800 Some("next".to_string())
3801 );
3802 }
3803
3804 #[test]
3805 fn constant_time_eq_matches_equal_slices() {
3806 assert!(constant_time_eq(b"abc123", b"abc123"));
3807 }
3808
3809 #[test]
3810 fn constant_time_eq_rejects_different_length_or_content() {
3811 assert!(!constant_time_eq(b"abc123", b"abc1234"));
3812 assert!(!constant_time_eq(b"abc123", b"xbc123"));
3813 }
3814
3815 #[test]
3816 fn generate_token_is_64_hex_chars_and_varies() {
3817 let a = generate_token();
3818 let b = generate_token();
3819 assert_eq!(a.len(), 64);
3820 assert!(a.chars().all(|c| c.is_ascii_hexdigit()));
3821 assert_ne!(a, b, "two calls must not mint the same token");
3822 }
3823
3824 #[cfg(feature = "adapter-api")]
3825 #[tokio::test]
3826 async fn credential_revocation_waits_for_registered_attachment_ack() {
3827 let revocation = Arc::new(RuntimeCredentialRevocation::new());
3828 let attachment = revocation.register();
3829 let (started_tx, started_rx) = tokio::sync::oneshot::channel();
3830 let task = tokio::spawn({
3831 let revocation = revocation.clone();
3832 async move {
3833 let _ = started_tx.send(());
3834 revocation.revoke_and_wait().await;
3835 }
3836 });
3837
3838 started_rx.await.unwrap();
3839 tokio::task::yield_now().await;
3840 assert!(
3841 !task.is_finished(),
3842 "revoke must remain pending while the attachment is registered"
3843 );
3844
3845 drop(attachment);
3846 tokio::time::timeout(std::time::Duration::from_secs(1), task)
3847 .await
3848 .expect("attachment acknowledgement must release revoke")
3849 .unwrap();
3850 }
3851
3852 #[cfg(feature = "adapter-api")]
3853 #[tokio::test]
3854 async fn credential_revocation_has_no_check_to_wait_lost_wakeup() {
3855 for _ in 0..10_000 {
3856 let revocation = Arc::new(RuntimeCredentialRevocation::new());
3857 let attachment = revocation.register();
3858 let (started_tx, started_rx) = tokio::sync::oneshot::channel();
3859 let task = tokio::spawn({
3860 let revocation = revocation.clone();
3861 async move {
3862 let _ = started_tx.send(());
3863 revocation.revoke_and_wait().await;
3864 }
3865 });
3866
3867 started_rx.await.unwrap();
3868 drop(attachment);
3869 tokio::time::timeout(std::time::Duration::from_secs(1), task)
3870 .await
3871 .expect("revoke lost its attachment-drained wakeup")
3872 .unwrap();
3873 }
3874 }
3875
3876 #[test]
3877 fn request_history_compaction_deduplicates_and_orders_resolutions() {
3878 let request = |sequence, id| {
3879 FrontendEvent::new(
3880 sequence,
3881 json!({"type": "request", "request": {"id": id, "kind": "approval", "payload": {}}}),
3882 )
3883 };
3884 let resolved = |sequence, id| {
3885 FrontendEvent::new(
3886 sequence,
3887 json!({"type": "request_resolved", "request_id": id, "response": {"kind": "approval", "request_id": id, "decision": "allow"}}),
3888 )
3889 };
3890 let replay = VecDeque::from([
3891 request(1, 2),
3892 resolved(2, 2),
3893 request(3, 1),
3894 resolved(4, 1),
3895 request(5, 2),
3896 resolved(6, 1),
3897 ]);
3898
3899 let compacted = compact_frontend_request_history(&replay);
3900 assert_eq!(compacted.len(), 4);
3901 assert_eq!(compacted[0]["request"]["id"], 1);
3902 assert_eq!(compacted[1]["request_id"], 1);
3903 assert_eq!(compacted[2]["request"]["id"], 2);
3904 assert_eq!(compacted[3]["request_id"], 2);
3905 }
3906}