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::AcquireControl) => client
2524 .acquire_control()
2525 .and_then(|snapshot| serde_json::to_value(snapshot).map_err(json_sdk_error)),
2526 Some(crate::FrontendFacadeMethod::TakeControl) => client
2527 .take_control()
2528 .and_then(|snapshot| serde_json::to_value(snapshot).map_err(json_sdk_error)),
2529 Some(crate::FrontendFacadeMethod::Heartbeat) => client
2530 .heartbeat()
2531 .and_then(|snapshot| serde_json::to_value(snapshot).map_err(json_sdk_error)),
2532 Some(crate::FrontendFacadeMethod::Lease) => client
2533 .lease_snapshot()
2534 .and_then(|snapshot| serde_json::to_value(snapshot).map_err(json_sdk_error)),
2535 Some(crate::FrontendFacadeMethod::Detach) => {
2536 serde_json::to_value(client.detach()).map_err(json_sdk_error)
2537 }
2538 Some(crate::FrontendFacadeMethod::Close) => match client.close().await {
2539 Ok(()) => Ok(json!({"closed":true})),
2540 Err(error) => Err(error),
2541 },
2542 None if request.method == "shutdown" => match client.close().await {
2543 Ok(()) => Ok(json!({"shutting_down":true})),
2544 Err(error) => Err(error),
2545 },
2546 _ => return frontend_http_rpc(client, request).await,
2547 };
2548 match result {
2549 Ok(value) => rpc_ok(id, value),
2550 Err(error) => sdk_runtime_rpc_error(id, -32002, &error),
2551 }
2552}
2553
2554#[cfg(feature = "adapter-api")]
2555fn json_sdk_error(error: serde_json::Error) -> FrontendRuntimeError {
2556 FrontendRuntimeError::Transport(error.to_string())
2557}
2558
2559#[cfg(feature = "adapter-api")]
2571async fn serve_frontend_credential_door<W: AsyncWrite + Unpin>(
2572 req: &HttpRequest,
2573 write_half: &mut W,
2574 peer_is_loopback: bool,
2575 credential: &AuthenticatedRuntimeHttpCredential,
2576 credentials: &RuntimeHttpCredentialRegistry,
2577 coordinator: &Arc<CoordinatedRuntime>,
2578) -> Option<std::io::Result<()>> {
2579 if matches!(
2580 req.path.as_str(),
2581 "/_supercode/frontend-credentials/mint" | "/_supercode/frontend-credentials/revoke"
2582 ) && !credential.via_bearer_header
2583 {
2584 let body =
2585 sdk_runtime_rpc_error(Value::Null, -32030, &FrontendRuntimeError::Unauthenticated)
2586 .to_string();
2587 return Some(
2588 write_http_response(
2589 write_half,
2590 401,
2591 "Unauthorized",
2592 "application/json",
2593 body.as_bytes(),
2594 )
2595 .await,
2596 );
2597 }
2598 if req.path == "/_supercode/frontend-credentials/mint" {
2599 if req.method != "POST" || !peer_is_loopback || !credential.bootstrap {
2600 let body = sdk_runtime_rpc_error(
2601 Value::Null,
2602 -32031,
2603 &FrontendRuntimeError::Unauthorized {
2604 permission: "bootstrap".into(),
2605 },
2606 )
2607 .to_string();
2608 return Some(
2609 write_http_response(
2610 write_half,
2611 403,
2612 "Forbidden",
2613 "application/json",
2614 body.as_bytes(),
2615 )
2616 .await,
2617 );
2618 }
2619 let request: Value = match serde_json::from_slice(&req.body) {
2620 Ok(request) => request,
2621 Err(error) => {
2622 return Some(
2623 write_http_response(
2624 write_half,
2625 400,
2626 "Bad Request",
2627 "application/json",
2628 format!("{{\"error\":{}}}", json!(error.to_string())).as_bytes(),
2629 )
2630 .await,
2631 );
2632 }
2633 };
2634 let Some(client_id) = request.get("clientId").and_then(Value::as_str) else {
2635 return Some(
2636 write_http_response(
2637 write_half,
2638 400,
2639 "Bad Request",
2640 "application/json",
2641 b"{\"error\":\"mint request omitted clientId\"}",
2642 )
2643 .await,
2644 );
2645 };
2646 let client_id = match crate::RuntimeClientId::parse(client_id) {
2647 Ok(client_id) => client_id,
2648 Err(error) => {
2649 return Some(
2650 write_http_response(
2651 write_half,
2652 400,
2653 "Bad Request",
2654 "application/json",
2655 format!("{{\"error\":{}}}", json!(error.to_string())).as_bytes(),
2656 )
2657 .await,
2658 );
2659 }
2660 };
2661 let observer = match request.get("grant").and_then(Value::as_str) {
2662 Some("observer") => true,
2663 Some("interactive") => false,
2664 _ => {
2665 return Some(
2666 write_http_response(
2667 write_half,
2668 400,
2669 "Bad Request",
2670 "application/json",
2671 b"{\"error\":\"grant must be observer or interactive\"}",
2672 )
2673 .await,
2674 );
2675 }
2676 };
2677 let token = match credentials.issue_frontend(client_id, observer) {
2678 Ok(token) => token,
2679 Err(error) => return Some(Err(error)),
2680 };
2681 return Some(
2682 write_http_response(
2683 write_half,
2684 200,
2685 "OK",
2686 "application/octet-stream",
2687 token.as_bytes(),
2688 )
2689 .await,
2690 );
2691 }
2692
2693 if req.path == "/_supercode/frontend-credentials/revoke" {
2694 if req.method != "POST" || !peer_is_loopback || !credential.bootstrap {
2695 let body = sdk_runtime_rpc_error(
2696 Value::Null,
2697 -32031,
2698 &FrontendRuntimeError::Unauthorized {
2699 permission: "bootstrap".into(),
2700 },
2701 )
2702 .to_string();
2703 return Some(
2704 write_http_response(
2705 write_half,
2706 403,
2707 "Forbidden",
2708 "application/json",
2709 body.as_bytes(),
2710 )
2711 .await,
2712 );
2713 }
2714 let request: Value = match serde_json::from_slice(&req.body) {
2715 Ok(request) => request,
2716 Err(error) => {
2717 return Some(
2718 write_http_response(
2719 write_half,
2720 400,
2721 "Bad Request",
2722 "application/json",
2723 format!("{{\"error\":{}}}", json!(error.to_string())).as_bytes(),
2724 )
2725 .await,
2726 );
2727 }
2728 };
2729 let Some(client_id) = request.get("clientId").and_then(Value::as_str) else {
2730 return Some(
2731 write_http_response(
2732 write_half,
2733 400,
2734 "Bad Request",
2735 "application/json",
2736 b"{\"error\":\"revoke request omitted clientId\"}",
2737 )
2738 .await,
2739 );
2740 };
2741 let client_id = match crate::RuntimeClientId::parse(client_id) {
2742 Ok(client_id) => client_id,
2743 Err(error) => {
2744 return Some(
2745 write_http_response(
2746 write_half,
2747 400,
2748 "Bad Request",
2749 "application/json",
2750 format!("{{\"error\":{}}}", json!(error.to_string())).as_bytes(),
2751 )
2752 .await,
2753 );
2754 }
2755 };
2756 let revoked = credentials.revoke_client(&client_id).await;
2757 if revoked {
2758 coordinator
2759 .client(client_id, RuntimeAuthorization::observer())
2760 .detach();
2761 }
2762 return Some(
2763 write_http_response(
2764 write_half,
2765 200,
2766 "OK",
2767 "application/json",
2768 if revoked {
2769 b"{\"revoked\":true}"
2770 } else {
2771 b"{\"revoked\":false}"
2772 },
2773 )
2774 .await,
2775 );
2776 }
2777
2778 None
2779}
2780
2781#[cfg(feature = "adapter-api")]
2782async fn handle_http_conn(
2783 stream: tokio::net::TcpStream,
2784 engine: Arc<RpcEngine>,
2785 coordinator: Arc<CoordinatedRuntime>,
2786 credentials: Arc<RuntimeHttpCredentialRegistry>,
2787) -> std::io::Result<()> {
2788 let peer_is_loopback = stream.peer_addr()?.ip().is_loopback();
2789 let (read_half, mut write_half) = stream.into_split();
2790 let mut reader = tokio::io::BufReader::new(read_half);
2791 let Some(req) = read_http_request(&mut reader).await? else {
2792 return Ok(());
2793 };
2794
2795 if req.method == "GET" {
2799 if let Some((content_type, body)) = browser_observer_asset(&req.path) {
2800 return write_browser_observer_asset(&mut write_half, content_type, body).await;
2801 }
2802 }
2803
2804 let Some(credential) = credentials.authenticate(&req) else {
2805 let body =
2806 sdk_runtime_rpc_error(Value::Null, -32030, &FrontendRuntimeError::Unauthenticated)
2807 .to_string();
2808 return write_http_response(
2809 &mut write_half,
2810 401,
2811 "Unauthorized",
2812 "application/json",
2813 body.as_bytes(),
2814 )
2815 .await;
2816 };
2817
2818 if let Some(result) = serve_frontend_credential_door(
2819 &req,
2820 &mut write_half,
2821 peer_is_loopback,
2822 &credential,
2823 &credentials,
2824 &coordinator,
2825 )
2826 .await
2827 {
2828 return result;
2829 }
2830
2831 let mut revocation = credential.revocation.clone();
2832 let mut attachment = credential.attachment;
2833 let credential = AuthenticatedRuntimeHttpCredential {
2834 authorization: credential.authorization,
2835 client_id: credential.client_id,
2836 bootstrap: credential.bootstrap,
2837 revocation: None,
2838 attachment: None,
2839 via_bearer_header: credential.via_bearer_header,
2840 };
2841 let client = match coordinated_http_client(&req, &coordinator, credential) {
2842 Ok(client) => client,
2843 Err(error) => {
2844 let permission = match error {
2845 crate::RuntimeLeaseError::InvalidClientId => "client_id",
2846 crate::RuntimeLeaseError::InvalidAuthorization => "authorization",
2847 _ => "runtime",
2848 };
2849 let body = sdk_runtime_rpc_error(
2850 Value::Null,
2851 -32031,
2852 &FrontendRuntimeError::Unauthorized {
2853 permission: permission.into(),
2854 },
2855 )
2856 .to_string();
2857 return write_http_response(
2858 &mut write_half,
2859 403,
2860 "Forbidden",
2861 "application/json",
2862 body.as_bytes(),
2863 )
2864 .await;
2865 }
2866 };
2867
2868 match (req.method.as_str(), req.path.as_str()) {
2869 ("POST", "/rpc") => {
2870 let body_text = String::from_utf8_lossy(&req.body);
2871 let resp = match serde_json::from_str::<RpcRequest>(&body_text) {
2872 Ok(rpc_req) if matches!(rpc_req.method.as_str(), "status" | "history") => {
2873 engine.handle_request(rpc_req).await
2874 }
2875 Ok(rpc_req) => coordinated_runtime_rpc(client.clone(), rpc_req).await,
2876 Err(e) => rpc_error(Value::Null, -32700, format!("parse error: {e}")),
2877 };
2878 let body = resp.to_string();
2879 write_http_response(
2880 &mut write_half,
2881 200,
2882 "OK",
2883 "application/json",
2884 body.as_bytes(),
2885 )
2886 .await
2887 }
2888 ("GET", "/events") => {
2889 if let Err(error) = client.observe() {
2890 let body = sdk_runtime_rpc_error(Value::Null, -32002, &error).to_string();
2891 return write_http_response(
2892 &mut write_half,
2893 403,
2894 "Forbidden",
2895 "application/json",
2896 body.as_bytes(),
2897 )
2898 .await;
2899 }
2900 let mut events = engine.subscribe();
2904 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";
2905 if write_half.write_all(head.as_bytes()).await.is_err() {
2906 client.detach();
2907 return Ok(());
2908 }
2909 let _ = write_half.flush().await;
2910 loop {
2911 tokio::select! {
2912 biased;
2913 recv = events.recv() => {
2914 match recv {
2915 Ok(v) => {
2916 let line = format!("data: {v}\n\n");
2917 if write_half.write_all(line.as_bytes()).await.is_err() {
2918 break;
2919 }
2920 if write_half.flush().await.is_err() {
2921 break;
2922 }
2923 }
2924 Err(broadcast::error::RecvError::Lagged(_)) => continue,
2925 Err(broadcast::error::RecvError::Closed) => break,
2926 }
2927 }
2928 _ = engine.wait_for_shutdown() => break,
2933 _ = wait_for_credential_revocation(&mut revocation) => break,
2934 _ = reader.read_u8() => break,
2935 }
2936 }
2937 let _ = write_half.shutdown().await;
2938 client.detach();
2939 drop(attachment.take());
2940 Ok(())
2941 }
2942 ("GET", "/frontend/events") => {
2943 if let Err(error) = client.observe() {
2944 let body = sdk_runtime_rpc_error(Value::Null, -32002, &error).to_string();
2945 return write_http_response(
2946 &mut write_half,
2947 403,
2948 "Forbidden",
2949 "application/json",
2950 body.as_bytes(),
2951 )
2952 .await;
2953 }
2954 let mut events = engine.frontend_subscribe();
2959 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";
2960 if write_half.write_all(head.as_bytes()).await.is_err() {
2961 client.detach();
2962 return Ok(());
2963 }
2964 let _ = write_half.flush().await;
2965 loop {
2966 tokio::select! {
2967 biased;
2968 recv = events.recv() => {
2969 match recv {
2970 Ok(event) => {
2971 let value = serde_json::to_string(&event).unwrap_or_default();
2972 let line = format!("data: {value}\n\n");
2973 if write_half.write_all(line.as_bytes()).await.is_err() {
2974 break;
2975 }
2976 if write_half.flush().await.is_err() {
2977 break;
2978 }
2979 }
2980 Err(broadcast::error::RecvError::Lagged(_)) => break,
2981 Err(broadcast::error::RecvError::Closed) => break,
2982 }
2983 }
2984 _ = engine.wait_for_shutdown() => break,
2988 _ = wait_for_credential_revocation(&mut revocation) => break,
2989 _ = reader.read_u8() => break,
2990 }
2991 }
2992 let _ = write_half.shutdown().await;
2993 client.detach();
2994 drop(attachment.take());
2995 Ok(())
2996 }
2997 _ => {
2998 write_http_response(
2999 &mut write_half,
3000 404,
3001 "Not Found",
3002 "application/json",
3003 b"{\"error\":\"not found\"}",
3004 )
3005 .await
3006 }
3007 }
3008}
3009
3010#[cfg(feature = "adapter-api")]
3011async fn wait_for_credential_revocation(receiver: &mut Option<tokio::sync::watch::Receiver<bool>>) {
3012 let Some(receiver) = receiver else {
3013 std::future::pending::<()>().await;
3014 return;
3015 };
3016 if *receiver.borrow() {
3017 return;
3018 }
3019 while receiver.changed().await.is_ok() {
3020 if *receiver.borrow() {
3021 return;
3022 }
3023 }
3024}
3025
3026#[cfg(feature = "adapter-api")]
3027async fn handle_frontend_http_conn(
3028 stream: tokio::net::TcpStream,
3029 coordinator: Arc<CoordinatedRuntime>,
3030 events: broadcast::Sender<FrontendEvent>,
3031 credentials: Arc<RuntimeHttpCredentialRegistry>,
3032) -> std::io::Result<()> {
3033 let peer_is_loopback = stream.peer_addr()?.ip().is_loopback();
3034 let (read_half, mut write_half) = stream.into_split();
3035 let mut reader = tokio::io::BufReader::new(read_half);
3036 let Some(req) = read_http_request(&mut reader).await? else {
3037 return Ok(());
3038 };
3039 if req.method == "GET" {
3040 if let Some((content_type, body)) = browser_observer_asset(&req.path) {
3041 return write_browser_observer_asset(&mut write_half, content_type, body).await;
3042 }
3043 }
3044 let Some(credential) = credentials.authenticate(&req) else {
3045 return write_http_response(
3046 &mut write_half,
3047 401,
3048 "Unauthorized",
3049 "application/json",
3050 b"{\"error\":\"missing or invalid bearer token\"}",
3051 )
3052 .await;
3053 };
3054 if let Some(result) = serve_frontend_credential_door(
3057 &req,
3058 &mut write_half,
3059 peer_is_loopback,
3060 &credential,
3061 &credentials,
3062 &coordinator,
3063 )
3064 .await
3065 {
3066 return result;
3067 }
3068 let client = match coordinated_http_client(&req, &coordinator, credential) {
3069 Ok(client) => client,
3070 Err(error) => {
3071 let body = json!({"error":error.to_string()}).to_string();
3072 return write_http_response(
3073 &mut write_half,
3074 400,
3075 "Bad Request",
3076 "application/json",
3077 body.as_bytes(),
3078 )
3079 .await;
3080 }
3081 };
3082 match (req.method.as_str(), req.path.as_str()) {
3083 ("POST", "/rpc") => {
3084 let body_text = String::from_utf8_lossy(&req.body);
3085 let response = match serde_json::from_str::<RpcRequest>(&body_text) {
3086 Ok(request) => coordinated_runtime_rpc(client.clone(), request).await,
3087 Err(error) => rpc_error(Value::Null, -32700, format!("parse error: {error}")),
3088 };
3089 let body = response.to_string();
3090 write_http_response(
3091 &mut write_half,
3092 200,
3093 "OK",
3094 "application/json",
3095 body.as_bytes(),
3096 )
3097 .await
3098 }
3099 ("GET", "/frontend/events") => {
3100 if let Err(error) = client.observe() {
3101 let body = sdk_runtime_rpc_error(Value::Null, -32002, &error).to_string();
3102 return write_http_response(
3103 &mut write_half,
3104 403,
3105 "Forbidden",
3106 "application/json",
3107 body.as_bytes(),
3108 )
3109 .await;
3110 }
3111 let mut receiver = events.subscribe();
3112 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";
3113 if write_half.write_all(head.as_bytes()).await.is_err() {
3114 client.detach();
3115 return Ok(());
3116 }
3117 let _ = write_half.flush().await;
3118 loop {
3119 tokio::select! {
3120 _ = reader.read_u8() => break,
3121 event = receiver.recv() => match event {
3122 Ok(event) => {
3123 let value = serde_json::to_string(&event).unwrap_or_default();
3124 let line = format!("data: {value}\n\n");
3125 if write_half.write_all(line.as_bytes()).await.is_err() || write_half.flush().await.is_err() {
3126 break;
3127 }
3128 }
3129 Err(broadcast::error::RecvError::Lagged(_)) => break,
3130 Err(broadcast::error::RecvError::Closed) => break,
3131 }
3132 }
3133 }
3134 client.detach();
3135 Ok(())
3136 }
3137 _ => {
3138 write_http_response(
3139 &mut write_half,
3140 404,
3141 "Not Found",
3142 "application/json",
3143 b"{\"error\":\"not found\"}",
3144 )
3145 .await
3146 }
3147 }
3148}
3149
3150#[cfg(feature = "adapter-api")]
3151async fn frontend_http_rpc(runtime: Arc<dyn FrontendRuntime>, request: RpcRequest) -> Value {
3152 let id = request.id;
3153 let Some(method) = crate::FrontendFacadeMethod::from_wire_name(&request.method) else {
3154 return rpc_error(id, -32601, format!("unknown method `{}`", request.method));
3155 };
3156 match method {
3157 crate::FrontendFacadeMethod::Describe => match runtime.describe().await {
3158 Ok(descriptor) => rpc_ok(id, serde_json::to_value(descriptor).unwrap_or_default()),
3159 Err(error) => sdk_runtime_rpc_error(id, -32010, &error),
3160 },
3161 crate::FrontendFacadeMethod::Attach => {
3162 let limit = request
3163 .params
3164 .get("limit")
3165 .and_then(Value::as_u64)
3166 .unwrap_or(50)
3167 .clamp(1, SERVER_HISTORY_CAPACITY as u64) as usize;
3168 match runtime.attach(limit).await {
3169 Ok(attachment) => rpc_ok(
3170 id,
3171 serde_json::to_value(FrontendAttachSnapshot {
3172 descriptor: attachment.descriptor,
3173 history: attachment.history,
3174 history_cursor: attachment.history_cursor,
3175 replay: attachment.replay,
3176 })
3177 .unwrap_or_default(),
3178 ),
3179 Err(error) => sdk_runtime_rpc_error(id, -32010, &error),
3180 }
3181 }
3182 crate::FrontendFacadeMethod::SendInput => {
3183 let Some(prompt) = request.params.get("prompt").and_then(Value::as_str) else {
3184 return rpc_error(
3185 id,
3186 -32602,
3187 "frontend.send_input requires a string `params.prompt`",
3188 );
3189 };
3190 let image_urls = match parse_image_urls(&request.params, "frontend.send_input") {
3191 Ok(image_urls) => image_urls,
3192 Err(message) => return rpc_error(id, -32602, message),
3193 };
3194 match runtime
3195 .clone()
3196 .send_input_with_images(prompt.to_string(), image_urls)
3197 .await
3198 {
3199 Ok(()) => rpc_ok(id, json!({"accepted": true})),
3200 Err(error @ FrontendRuntimeError::Submit(RuntimeSubmitError::Busy)) => {
3201 sdk_runtime_rpc_error(id, -32000, &error)
3202 }
3203 Err(error) => sdk_runtime_rpc_error(id, -32002, &error),
3204 }
3205 }
3206 crate::FrontendFacadeMethod::Invoke => {
3207 let operation = request
3208 .params
3209 .get("operation")
3210 .cloned()
3211 .ok_or("frontend.invoke requires `params.operation`")
3212 .and_then(|value| {
3213 serde_json::from_value(value).map_err(|_| "invalid frontend operation")
3214 });
3215 match operation {
3216 Ok(operation) => match runtime.invoke(operation).await {
3217 Ok(result) => rpc_ok(id, serde_json::to_value(result).unwrap_or_default()),
3218 Err(error @ FrontendRuntimeError::UnsupportedOperation(_)) => {
3219 sdk_runtime_rpc_error(id, -32023, &error)
3220 }
3221 Err(error @ FrontendRuntimeError::Submit(RuntimeSubmitError::Busy)) => {
3222 sdk_runtime_rpc_error(id, -32000, &error)
3223 }
3224 Err(error @ FrontendRuntimeError::Submit(RuntimeSubmitError::Interrupted)) => {
3225 sdk_runtime_rpc_error(id, -32001, &error)
3226 }
3227 Err(error) => sdk_runtime_rpc_error(id, -32022, &error),
3228 },
3229 Err(message) => rpc_error(id, -32602, message),
3230 }
3231 }
3232 crate::FrontendFacadeMethod::Submit => {
3233 let Some(prompt) = request.params.get("prompt").and_then(Value::as_str) else {
3234 return rpc_error(id, -32602, "submit requires a string `params.prompt`");
3235 };
3236 let image_urls = match request.params.get("image_urls") {
3237 None => Vec::new(),
3238 Some(Value::Array(values)) => {
3239 let Some(urls) = values.iter().map(Value::as_str).collect::<Option<Vec<_>>>()
3240 else {
3241 return rpc_error(
3242 id,
3243 -32602,
3244 "submit requires string entries in `params.image_urls`",
3245 );
3246 };
3247 urls.into_iter().map(str::to_owned).collect()
3248 }
3249 Some(_) => {
3250 return rpc_error(id, -32602, "submit requires array `params.image_urls`")
3251 }
3252 };
3253 match runtime
3254 .submit_with_images(prompt.to_string(), image_urls)
3255 .await
3256 {
3257 Ok(reply) => rpc_ok(id, json!({"reply":reply})),
3258 Err(error @ FrontendRuntimeError::Submit(RuntimeSubmitError::Busy)) => {
3259 sdk_runtime_rpc_error(id, -32000, &error)
3260 }
3261 Err(error @ FrontendRuntimeError::Submit(RuntimeSubmitError::Interrupted)) => {
3262 sdk_runtime_rpc_error(id, -32001, &error)
3263 }
3264 Err(error) => sdk_runtime_rpc_error(id, -32002, &error),
3265 }
3266 }
3267 crate::FrontendFacadeMethod::Interrupt => match runtime.interrupt().await {
3268 Ok(interrupted) => rpc_ok(id, json!({"interrupted":interrupted})),
3269 Err(error) => sdk_runtime_rpc_error(id, -32002, &error),
3270 },
3271 crate::FrontendFacadeMethod::Steer => {
3272 let Some(prompt) = request.params.get("prompt").and_then(Value::as_str) else {
3273 return rpc_error(id, -32602, "steer requires a string `params.prompt`");
3274 };
3275 match runtime.steer(prompt.to_string()).await {
3276 Ok(()) => rpc_ok(id, json!({"queued":true})),
3277 Err(error @ FrontendRuntimeError::UnsupportedAction(_)) => {
3278 sdk_runtime_rpc_error(id, -32020, &error)
3279 }
3280 Err(error) => sdk_runtime_rpc_error(id, -32022, &error),
3281 }
3282 }
3283 crate::FrontendFacadeMethod::Respond => {
3284 let response = request
3285 .params
3286 .get("response")
3287 .cloned()
3288 .ok_or("respond requires `params.response`")
3289 .and_then(|value| serde_json::from_value(value).map_err(|_| "invalid response"));
3290 match response {
3291 Ok(response) => match runtime.respond(response).await {
3292 Ok(()) => rpc_ok(id, json!({"accepted":true})),
3293 Err(error @ FrontendRuntimeError::UnsupportedAction(_)) => {
3294 sdk_runtime_rpc_error(id, -32020, &error)
3295 }
3296 Err(error) => sdk_runtime_rpc_error(id, -32022, &error),
3297 },
3298 Err(message) => rpc_error(id, -32602, message),
3299 }
3300 }
3301 crate::FrontendFacadeMethod::Lease
3302 | crate::FrontendFacadeMethod::AcquireControl
3303 | crate::FrontendFacadeMethod::TakeControl
3304 | crate::FrontendFacadeMethod::Heartbeat
3305 | crate::FrontendFacadeMethod::Detach
3306 | crate::FrontendFacadeMethod::Close => rpc_error(
3307 id,
3308 -32020,
3309 format!(
3310 "frontend action `{}` requires a coordinated runtime",
3311 method.id()
3312 ),
3313 ),
3314 }
3315}
3316
3317fn parse_image_urls(params: &Value, operation: &str) -> std::result::Result<Vec<String>, String> {
3318 let urls = match params.get("image_urls") {
3319 None => Ok(Vec::new()),
3320 Some(Value::Array(values)) => values
3321 .iter()
3322 .map(|value| {
3323 value.as_str().map(str::to_owned).ok_or_else(|| {
3324 format!("{operation} requires string entries in `params.image_urls`")
3325 })
3326 })
3327 .collect(),
3328 Some(_) => Err(format!("{operation} requires array `params.image_urls`")),
3329 }?;
3330 validate_frontend_image_urls(urls, operation)
3331}
3332
3333fn validate_frontend_image_urls(
3334 urls: Vec<String>,
3335 operation: &str,
3336) -> std::result::Result<Vec<String>, String> {
3337 if urls.len() > 4 {
3338 return Err(format!("{operation} accepts at most 4 images"));
3339 }
3340 let mut total = 0usize;
3341 for url in &urls {
3342 if !(url.starts_with("data:image/")
3343 || url.starts_with("https://")
3344 || url.starts_with("http://"))
3345 {
3346 return Err(format!(
3347 "{operation} images must be image data URLs or HTTP(S) URLs"
3348 ));
3349 }
3350 if url.len() > 12 * 1024 * 1024 {
3351 return Err(format!("{operation} image exceeds the encoded size limit"));
3352 }
3353 total = total.saturating_add(url.len());
3354 }
3355 if total > 32 * 1024 * 1024 {
3356 return Err(format!(
3357 "{operation} images exceed the encoded total size limit"
3358 ));
3359 }
3360 Ok(urls)
3361}
3362
3363#[cfg(feature = "adapter-api")]
3365pub(crate) struct FrontendHttpServer {
3366 address: SocketAddr,
3367 task: tokio::task::JoinHandle<()>,
3368}
3369
3370#[cfg(feature = "adapter-api")]
3371impl FrontendHttpServer {
3372 pub(crate) fn address(&self) -> SocketAddr {
3374 self.address
3375 }
3376}
3377
3378#[cfg(feature = "adapter-api")]
3379impl Drop for FrontendHttpServer {
3380 fn drop(&mut self) {
3381 self.task.abort();
3382 }
3383}
3384
3385#[cfg(feature = "adapter-api")]
3388pub(crate) async fn run_frontend_http(
3389 runtime: Arc<dyn FrontendRuntime>,
3390 events: broadcast::Sender<FrontendEvent>,
3391 bind: &str,
3392 token: Arc<str>,
3393 runtime_id: impl Into<String>,
3394) -> std::io::Result<FrontendHttpServer> {
3395 let listener = TcpListener::bind(bind).await?;
3396 let address = listener.local_addr()?;
3397 let coordinator = CoordinatedRuntime::new(runtime);
3398 let credentials =
3402 RuntimeHttpCredentialRegistry::new(runtime_id, vec![RuntimeHttpCredential::owner(token)])?;
3403 let task = tokio::spawn(async move {
3404 while let Ok((stream, _)) = listener.accept().await {
3405 let coordinator = coordinator.clone();
3406 let events = events.clone();
3407 let credentials = credentials.clone();
3408 tokio::spawn(async move {
3409 let _ = handle_frontend_http_conn(stream, coordinator, events, credentials).await;
3410 });
3411 }
3412 });
3413 Ok(FrontendHttpServer { address, task })
3414}
3415
3416#[cfg(feature = "adapter-api")]
3419pub struct FrontendWebSocketServer {
3420 address: SocketAddr,
3421 task: tokio::task::JoinHandle<()>,
3422}
3423
3424#[cfg(feature = "adapter-api")]
3425impl FrontendWebSocketServer {
3426 pub fn address(&self) -> SocketAddr {
3428 self.address
3429 }
3430}
3431
3432#[cfg(feature = "adapter-api")]
3433impl Drop for FrontendWebSocketServer {
3434 fn drop(&mut self) {
3435 self.task.abort();
3436 }
3437}
3438
3439#[cfg(feature = "adapter-api")]
3443pub async fn run_frontend_websocket(
3444 engine: Arc<RpcEngine>,
3445 bind: &str,
3446 credentials: Vec<RuntimeHttpCredential>,
3447) -> std::io::Result<FrontendWebSocketServer> {
3448 let runtime: Arc<dyn FrontendRuntime> = engine.clone();
3449 let events = engine.frontend_events.clone();
3450 run_frontend_websocket_runtime_inner(runtime, events, Some(engine), bind, credentials).await
3451}
3452
3453#[cfg(all(feature = "adapter-api", test))]
3458pub(crate) async fn run_frontend_websocket_runtime(
3459 runtime: Arc<dyn FrontendRuntime>,
3460 events: broadcast::Sender<FrontendEvent>,
3461 bind: &str,
3462 credentials: Vec<RuntimeHttpCredential>,
3463) -> std::io::Result<FrontendWebSocketServer> {
3464 run_frontend_websocket_runtime_inner(runtime, events, None, bind, credentials).await
3465}
3466
3467#[cfg(feature = "adapter-api")]
3468async fn run_frontend_websocket_runtime_inner(
3469 runtime: Arc<dyn FrontendRuntime>,
3470 events: broadcast::Sender<FrontendEvent>,
3471 shutdown_engine: Option<Arc<RpcEngine>>,
3472 bind: &str,
3473 credentials: Vec<RuntimeHttpCredential>,
3474) -> std::io::Result<FrontendWebSocketServer> {
3475 if credentials.is_empty()
3476 || credentials
3477 .iter()
3478 .any(|credential| credential.token.is_empty())
3479 {
3480 return Err(std::io::Error::new(
3481 std::io::ErrorKind::InvalidInput,
3482 "at least one non-empty runtime WebSocket credential is required",
3483 ));
3484 }
3485 let listener = TcpListener::bind(bind).await?;
3486 let address = listener.local_addr()?;
3487 let coordinator = CoordinatedRuntime::new(runtime);
3488 let credentials: Arc<[RuntimeHttpCredential]> = credentials.into();
3489 let task = tokio::spawn(async move {
3490 loop {
3491 tokio::select! {
3492 biased;
3493 _ = wait_for_optional_runtime_shutdown(shutdown_engine.as_ref()) => break,
3494 accepted = listener.accept() => {
3495 let Ok((stream, _)) = accepted else { continue };
3496 let coordinator = coordinator.clone();
3497 let credentials = credentials.clone();
3498 let events = events.clone();
3499 let shutdown_engine = shutdown_engine.clone();
3500 tokio::spawn(async move {
3501 let _ = handle_frontend_websocket(stream, events, shutdown_engine, coordinator, credentials).await;
3502 });
3503 }
3504 }
3505 }
3506 });
3507 Ok(FrontendWebSocketServer { address, task })
3508}
3509
3510#[cfg(feature = "adapter-api")]
3511async fn wait_for_optional_runtime_shutdown(engine: Option<&Arc<RpcEngine>>) {
3512 match engine {
3513 Some(engine) => engine.wait_for_shutdown().await,
3514 None => std::future::pending().await,
3515 }
3516}
3517
3518#[cfg(feature = "adapter-api")]
3519#[allow(clippy::result_large_err)] async fn handle_frontend_websocket(
3521 stream: tokio::net::TcpStream,
3522 events: broadcast::Sender<FrontendEvent>,
3523 shutdown_engine: Option<Arc<RpcEngine>>,
3524 coordinator: Arc<CoordinatedRuntime>,
3525 credentials: Arc<[RuntimeHttpCredential]>,
3526) -> Result<(), tokio_tungstenite::tungstenite::Error> {
3527 use std::sync::Mutex as SyncMutex;
3528 use tokio_tungstenite::tungstenite::handshake::server::{ErrorResponse, Request, Response};
3529
3530 let selected = Arc::new(SyncMutex::new(None::<Arc<CoordinatedRuntimeClient>>));
3531 let selected_by_callback = selected.clone();
3532 let socket = tokio_tungstenite::accept_hdr_async(
3533 stream,
3534 move |request: &Request, response: Response| -> Result<Response, ErrorResponse> {
3535 let reject = |status, message: &str| {
3536 tokio_tungstenite::tungstenite::http::Response::builder()
3537 .status(status)
3538 .body(Some(message.to_string()))
3539 .expect("static WebSocket rejection is valid")
3540 };
3541 if request.uri().path() != "/frontend/v2" {
3542 return Err(reject(404, "frontend WebSocket route not found"));
3543 }
3544 let token = request
3545 .headers()
3546 .get("authorization")
3547 .and_then(|value| value.to_str().ok())
3548 .and_then(|value| value.strip_prefix("Bearer "));
3549 let Some(credential) = token.and_then(|token| {
3550 credentials.iter().find(|credential| {
3551 constant_time_eq(token.as_bytes(), credential.token.as_bytes())
3552 })
3553 }) else {
3554 return Err(reject(401, "missing or invalid bearer token"));
3555 };
3556 let client_id = request
3557 .headers()
3558 .get("x-supercode-client-id")
3559 .and_then(|value| value.to_str().ok())
3560 .unwrap_or("legacy-websocket-owner");
3561 let Ok(client_id) = RuntimeClientId::parse(client_id) else {
3562 return Err(reject(400, "invalid runtime client id"));
3563 };
3564 let mut authorization = credential.authorization.clone();
3565 if let Some(requested) = request
3566 .headers()
3567 .get("x-supercode-permissions")
3568 .and_then(|value| value.to_str().ok())
3569 {
3570 let Ok(requested) = RuntimeAuthorization::parse_header(requested) else {
3571 return Err(reject(400, "invalid runtime authorization grant"));
3572 };
3573 authorization = authorization.restrict_to(&requested);
3574 }
3575 *selected_by_callback
3576 .lock()
3577 .unwrap_or_else(std::sync::PoisonError::into_inner) =
3578 Some(coordinator.client(client_id, authorization));
3579 Ok(response)
3580 },
3581 )
3582 .await?;
3583 let client = selected
3584 .lock()
3585 .unwrap_or_else(std::sync::PoisonError::into_inner)
3586 .take()
3587 .expect("successful WebSocket handshake selects a runtime client");
3588 if let Err(error) = client.observe() {
3589 let mut socket = socket;
3590 let value = sdk_runtime_rpc_error(Value::Null, -32002, &error).to_string();
3591 socket
3592 .send(tokio_tungstenite::tungstenite::Message::Text(value.into()))
3593 .await?;
3594 socket.close(None).await?;
3595 return Ok(());
3596 }
3597
3598 let mut events = events.subscribe();
3599 let (mut writer, mut reader) = socket.split();
3600 loop {
3601 tokio::select! {
3602 biased;
3603 incoming = reader.next() => match incoming {
3604 Some(Ok(tokio_tungstenite::tungstenite::Message::Text(text))) => {
3605 let response = match serde_json::from_str::<RpcRequest>(&text) {
3606 Ok(request) => coordinated_runtime_rpc(client.clone(), request).await,
3607 Err(error) => rpc_error(Value::Null, -32700, format!("parse error: {error}")),
3608 };
3609 writer.send(tokio_tungstenite::tungstenite::Message::Text(response.to_string().into())).await?;
3610 }
3611 Some(Ok(tokio_tungstenite::tungstenite::Message::Ping(payload))) => {
3612 writer.send(tokio_tungstenite::tungstenite::Message::Pong(payload)).await?;
3613 }
3614 Some(Ok(tokio_tungstenite::tungstenite::Message::Close(_))) | None => break,
3615 Some(Ok(_)) => {}
3616 Some(Err(error)) => {
3617 client.detach();
3618 return Err(error);
3619 }
3620 },
3621 event = events.recv() => match event {
3622 Ok(event) => {
3623 let notification = json!({
3624 "jsonrpc":"2.0",
3625 "method":"frontend.v2.event",
3626 "params":{"event":event},
3627 });
3628 writer.send(tokio_tungstenite::tungstenite::Message::Text(notification.to_string().into())).await?;
3629 }
3630 Err(broadcast::error::RecvError::Lagged(count)) => {
3631 let notification = json!({
3632 "jsonrpc":"2.0",
3633 "method":"frontend.v2.event",
3634 "params":{"error":{"name":"transport","message":format!("event replay gap: {count}")}},
3635 });
3636 writer.send(tokio_tungstenite::tungstenite::Message::Text(notification.to_string().into())).await?;
3637 break;
3638 }
3639 Err(broadcast::error::RecvError::Closed) => break,
3640 },
3641 _ = wait_for_optional_runtime_shutdown(shutdown_engine.as_ref()) => break,
3642 }
3643 }
3644 client.detach();
3645 Ok(())
3646}
3647
3648#[cfg(feature = "adapter-api")]
3656pub async fn run_http(
3657 engine: Arc<RpcEngine>,
3658 bind: &str,
3659 token: Arc<str>,
3660) -> std::io::Result<SocketAddr> {
3661 run_http_authorized(engine, bind, vec![RuntimeHttpCredential::owner(token)]).await
3662}
3663
3664#[cfg(feature = "adapter-api")]
3669pub async fn run_http_authorized(
3670 engine: Arc<RpcEngine>,
3671 bind: &str,
3672 credentials: Vec<RuntimeHttpCredential>,
3673) -> std::io::Result<SocketAddr> {
3674 run_http_authorized_with_lease_ttl(
3675 engine,
3676 bind,
3677 credentials,
3678 crate::DEFAULT_RUNTIME_LEASE_TTL_MS,
3679 )
3680 .await
3681}
3682
3683#[cfg(feature = "adapter-api")]
3686pub async fn run_http_authorized_with_lease_ttl(
3687 engine: Arc<RpcEngine>,
3688 bind: &str,
3689 credentials: Vec<RuntimeHttpCredential>,
3690 lease_ttl_ms: u64,
3691) -> std::io::Result<SocketAddr> {
3692 if credentials.is_empty()
3693 || credentials
3694 .iter()
3695 .any(|credential| credential.token.is_empty())
3696 {
3697 return Err(std::io::Error::new(
3698 std::io::ErrorKind::InvalidInput,
3699 "at least one non-empty runtime HTTP credential is required",
3700 ));
3701 }
3702 if lease_ttl_ms == 0 {
3703 return Err(std::io::Error::new(
3704 std::io::ErrorKind::InvalidInput,
3705 "runtime lease TTL must be non-zero",
3706 ));
3707 }
3708 let listener = TcpListener::bind(bind).await?;
3709 let local_addr = listener.local_addr()?;
3710 let credentials = RuntimeHttpCredentialRegistry::new(engine.session_id(), credentials)?;
3711 let eng = engine;
3712 let runtime: Arc<dyn FrontendRuntime> = eng.clone();
3713 let coordinator = CoordinatedRuntime::with_lease_ttl(runtime, lease_ttl_ms);
3714 tokio::spawn(async move {
3715 loop {
3716 tokio::select! {
3717 biased;
3718 _ = eng.wait_for_shutdown() => break,
3719 accepted = listener.accept() => {
3720 let Ok((stream, _addr)) = accepted else { continue };
3721 let eng = eng.clone();
3722 let coordinator = coordinator.clone();
3723 let credentials = credentials.clone();
3724 tokio::spawn(async move {
3725 let _ = handle_http_conn(stream, eng, coordinator, credentials).await;
3726 });
3727 }
3728 }
3729 }
3730 });
3731 Ok(local_addr)
3732}
3733
3734#[cfg(test)]
3735mod frontend_binding_conformance_tests;
3736
3737#[cfg(test)]
3738mod tests {
3739 use super::*;
3740 use tokio::io::BufReader;
3741
3742 fn cursor(data: &[u8]) -> BufReader<std::io::Cursor<Vec<u8>>> {
3743 BufReader::new(std::io::Cursor::new(data.to_vec()))
3744 }
3745
3746 #[cfg(all(feature = "adapter-api", supercode_workspace_assets))]
3747 #[test]
3748 fn packaged_observer_assets_match_the_sdk_sources() {
3749 let pairs: &[(&str, &[u8], &[u8])] = &[
3750 (
3751 "frontend-browser/index.html",
3752 include_bytes!("../embedded/frontend-browser/index.html"),
3753 include_bytes!("../../../sdk/frontend-browser/index.html"),
3754 ),
3755 (
3756 "frontend-browser/app.mjs",
3757 include_bytes!("../embedded/frontend-browser/app.mjs"),
3758 include_bytes!("../../../sdk/frontend-browser/app.mjs"),
3759 ),
3760 (
3761 "frontend-browser/client.mjs",
3762 include_bytes!("../embedded/frontend-browser/client.mjs"),
3763 include_bytes!("../../../sdk/frontend-browser/client.mjs"),
3764 ),
3765 (
3766 "frontend-browser/view.mjs",
3767 include_bytes!("../embedded/frontend-browser/view.mjs"),
3768 include_bytes!("../../../sdk/frontend-browser/view.mjs"),
3769 ),
3770 (
3771 "frontend-browser/style.css",
3772 include_bytes!("../embedded/frontend-browser/style.css"),
3773 include_bytes!("../../../sdk/frontend-browser/style.css"),
3774 ),
3775 (
3776 "frontend-browser/favicon.svg",
3777 include_bytes!("../embedded/frontend-browser/favicon.svg"),
3778 include_bytes!("../../../sdk/frontend-browser/favicon.svg"),
3779 ),
3780 (
3781 "frontend/client.mjs",
3782 include_bytes!("../embedded/frontend/client.mjs"),
3783 include_bytes!("../../../sdk/frontend/client.mjs"),
3784 ),
3785 (
3786 "frontend/generated-client.mjs",
3787 include_bytes!("../embedded/frontend/generated-client.mjs"),
3788 include_bytes!("../../../sdk/frontend/generated-client.mjs"),
3789 ),
3790 (
3791 "frontend/generated.mjs",
3792 include_bytes!("../embedded/frontend/generated.mjs"),
3793 include_bytes!("../../../sdk/frontend/generated.mjs"),
3794 ),
3795 ];
3796 for (name, packaged, source) in pairs {
3797 assert_eq!(packaged, source, "packaged observer asset drifted: {name}");
3798 }
3799 }
3800
3801 #[tokio::test]
3802 async fn admitted_submit_has_a_cancel_token_before_shutdown_observes_busy() {
3803 let agent =
3804 crate::Agent::new(crate::Config::builder().api_key("test-only-key").build()).unwrap();
3805 let engine = RpcEngine::new(agent, None);
3806 let claim = engine.claim_submit().unwrap();
3807 assert!(engine.busy.load(Ordering::SeqCst));
3808 assert!(engine
3809 .current_cancel
3810 .lock()
3811 .unwrap_or_else(std::sync::PoisonError::into_inner)
3812 .is_some());
3813
3814 let cancel = claim.cancel.clone();
3815 let shutdown_engine = engine.clone();
3816 let shutdown = tokio::spawn(async move { shutdown_engine.shutdown().await });
3817 tokio::time::timeout(std::time::Duration::from_secs(1), cancel.notified())
3818 .await
3819 .expect("shutdown must interrupt an admitted claim before its future starts");
3820 assert!(
3821 !shutdown.is_finished(),
3822 "shutdown must retain the barrier until the admitted claim drains"
3823 );
3824 drop(claim);
3825 tokio::time::timeout(std::time::Duration::from_secs(1), shutdown)
3826 .await
3827 .expect("claim drain must release shutdown")
3828 .unwrap();
3829 }
3830
3831 #[tokio::test]
3832 async fn read_bounded_line_reads_a_normal_line() {
3833 let mut r = cursor(b"hello\nworld\n");
3834 assert_eq!(
3835 read_bounded_line(&mut r, 1024).await.unwrap(),
3836 Some("hello".to_string())
3837 );
3838 assert_eq!(
3839 read_bounded_line(&mut r, 1024).await.unwrap(),
3840 Some("world".to_string())
3841 );
3842 assert_eq!(read_bounded_line(&mut r, 1024).await.unwrap(), None);
3843 }
3844
3845 #[tokio::test]
3846 async fn read_bounded_line_strips_trailing_cr() {
3847 let mut r = cursor(b"hello\r\n");
3848 assert_eq!(
3849 read_bounded_line(&mut r, 1024).await.unwrap(),
3850 Some("hello".to_string())
3851 );
3852 }
3853
3854 #[tokio::test]
3855 async fn read_bounded_line_returns_final_line_without_trailing_newline() {
3856 let mut r = cursor(b"no newline at eof");
3857 assert_eq!(
3858 read_bounded_line(&mut r, 1024).await.unwrap(),
3859 Some("no newline at eof".to_string())
3860 );
3861 assert_eq!(read_bounded_line(&mut r, 1024).await.unwrap(), None);
3862 }
3863
3864 #[tokio::test]
3865 async fn read_bounded_line_errors_and_resyncs_on_an_oversized_line() {
3866 let mut data = vec![b'x'; 20];
3867 data.push(b'\n');
3868 data.extend_from_slice(b"next\n");
3869 let mut r = cursor(&data);
3870 let err = read_bounded_line(&mut r, 10).await.unwrap_err();
3871 assert!(err.to_string().contains("10 byte cap"));
3872 assert_eq!(
3875 read_bounded_line(&mut r, 1024).await.unwrap(),
3876 Some("next".to_string())
3877 );
3878 }
3879
3880 #[test]
3881 fn constant_time_eq_matches_equal_slices() {
3882 assert!(constant_time_eq(b"abc123", b"abc123"));
3883 }
3884
3885 #[test]
3886 fn constant_time_eq_rejects_different_length_or_content() {
3887 assert!(!constant_time_eq(b"abc123", b"abc1234"));
3888 assert!(!constant_time_eq(b"abc123", b"xbc123"));
3889 }
3890
3891 #[test]
3892 fn generate_token_is_64_hex_chars_and_varies() {
3893 let a = generate_token();
3894 let b = generate_token();
3895 assert_eq!(a.len(), 64);
3896 assert!(a.chars().all(|c| c.is_ascii_hexdigit()));
3897 assert_ne!(a, b, "two calls must not mint the same token");
3898 }
3899
3900 #[cfg(feature = "adapter-api")]
3901 #[tokio::test]
3902 async fn credential_revocation_waits_for_registered_attachment_ack() {
3903 let revocation = Arc::new(RuntimeCredentialRevocation::new());
3904 let attachment = revocation.register();
3905 let (started_tx, started_rx) = tokio::sync::oneshot::channel();
3906 let task = tokio::spawn({
3907 let revocation = revocation.clone();
3908 async move {
3909 let _ = started_tx.send(());
3910 revocation.revoke_and_wait().await;
3911 }
3912 });
3913
3914 started_rx.await.unwrap();
3915 tokio::task::yield_now().await;
3916 assert!(
3917 !task.is_finished(),
3918 "revoke must remain pending while the attachment is registered"
3919 );
3920
3921 drop(attachment);
3922 tokio::time::timeout(std::time::Duration::from_secs(1), task)
3923 .await
3924 .expect("attachment acknowledgement must release revoke")
3925 .unwrap();
3926 }
3927
3928 #[cfg(feature = "adapter-api")]
3929 #[tokio::test]
3930 async fn credential_revocation_has_no_check_to_wait_lost_wakeup() {
3931 for _ in 0..10_000 {
3932 let revocation = Arc::new(RuntimeCredentialRevocation::new());
3933 let attachment = revocation.register();
3934 let (started_tx, started_rx) = tokio::sync::oneshot::channel();
3935 let task = tokio::spawn({
3936 let revocation = revocation.clone();
3937 async move {
3938 let _ = started_tx.send(());
3939 revocation.revoke_and_wait().await;
3940 }
3941 });
3942
3943 started_rx.await.unwrap();
3944 drop(attachment);
3945 tokio::time::timeout(std::time::Duration::from_secs(1), task)
3946 .await
3947 .expect("revoke lost its attachment-drained wakeup")
3948 .unwrap();
3949 }
3950 }
3951
3952 #[test]
3953 fn request_history_compaction_deduplicates_and_orders_resolutions() {
3954 let request = |sequence, id| {
3955 FrontendEvent::new(
3956 sequence,
3957 json!({"type": "request", "request": {"id": id, "kind": "approval", "payload": {}}}),
3958 )
3959 };
3960 let resolved = |sequence, id| {
3961 FrontendEvent::new(
3962 sequence,
3963 json!({"type": "request_resolved", "request_id": id, "response": {"kind": "approval", "request_id": id, "decision": "allow"}}),
3964 )
3965 };
3966 let replay = VecDeque::from([
3967 request(1, 2),
3968 resolved(2, 2),
3969 request(3, 1),
3970 resolved(4, 1),
3971 request(5, 2),
3972 resolved(6, 1),
3973 ]);
3974
3975 let compacted = compact_frontend_request_history(&replay);
3976 assert_eq!(compacted.len(), 4);
3977 assert_eq!(compacted[0]["request"]["id"], 1);
3978 assert_eq!(compacted[1]["request_id"], 1);
3979 assert_eq!(compacted[2]["request"]["id"], 2);
3980 assert_eq!(compacted[3]["request_id"], 2);
3981 }
3982}