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