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