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