1use std::collections::{BTreeMap, BTreeSet};
8use std::sync::{Arc, Mutex};
9use std::time::{SystemTime, UNIX_EPOCH};
10
11use async_trait::async_trait;
12use serde::{Deserialize, Serialize};
13
14use crate::{
15 FrontendAttachment, FrontendOperationInvocation, FrontendOperationResult, FrontendResponse,
16 FrontendRuntimeDescriptor, SdkError, SdkOperation, SdkRuntime,
17};
18
19pub const DEFAULT_RUNTIME_LEASE_TTL_MS: u64 = 30_000;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
25#[serde(rename_all = "snake_case")]
26pub enum RuntimePermission {
27 Observe,
29 Interact,
31 Approve,
33 Terminate,
35}
36
37impl RuntimePermission {
38 pub const fn as_str(self) -> &'static str {
40 match self {
41 Self::Observe => "observe",
42 Self::Interact => "interact",
43 Self::Approve => "approve",
44 Self::Terminate => "terminate",
45 }
46 }
47}
48
49#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
51pub struct RuntimeAuthorization {
52 permissions: BTreeSet<RuntimePermission>,
53}
54
55impl RuntimeAuthorization {
56 pub fn new(permissions: impl IntoIterator<Item = RuntimePermission>) -> Self {
58 Self {
59 permissions: permissions.into_iter().collect(),
60 }
61 }
62
63 pub fn owner() -> Self {
65 Self::new([
66 RuntimePermission::Observe,
67 RuntimePermission::Interact,
68 RuntimePermission::Approve,
69 RuntimePermission::Terminate,
70 ])
71 }
72
73 pub fn interactive() -> Self {
77 Self::new([
78 RuntimePermission::Observe,
79 RuntimePermission::Interact,
80 RuntimePermission::Approve,
81 ])
82 }
83
84 pub fn observer() -> Self {
86 Self::new([RuntimePermission::Observe])
87 }
88
89 pub fn allows(&self, permission: RuntimePermission) -> bool {
91 self.permissions.contains(&permission)
92 }
93
94 pub fn permissions(&self) -> impl Iterator<Item = RuntimePermission> + '_ {
96 self.permissions.iter().copied()
97 }
98
99 pub fn restrict_to(&self, requested: &Self) -> Self {
101 Self::new(
102 self.permissions
103 .intersection(&requested.permissions)
104 .copied(),
105 )
106 }
107
108 pub fn header_value(&self) -> String {
111 self.permissions()
112 .map(RuntimePermission::as_str)
113 .collect::<Vec<_>>()
114 .join(",")
115 }
116
117 pub fn parse_header(value: &str) -> Result<Self, RuntimeLeaseError> {
120 if value.is_empty() {
121 return Ok(Self::default());
122 }
123 let mut permissions = Vec::new();
124 for name in value.split(',') {
125 let permission = match name {
126 "observe" => RuntimePermission::Observe,
127 "interact" => RuntimePermission::Interact,
128 "approve" => RuntimePermission::Approve,
129 "terminate" => RuntimePermission::Terminate,
130 _ => return Err(RuntimeLeaseError::InvalidAuthorization),
131 };
132 permissions.push(permission);
133 }
134 Ok(Self::new(permissions))
135 }
136}
137
138#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
140#[serde(transparent)]
141pub struct RuntimeClientId(String);
142
143impl RuntimeClientId {
144 pub fn parse(value: impl Into<String>) -> Result<Self, RuntimeLeaseError> {
146 let value = value.into();
147 if value.is_empty()
148 || value.len() > 128
149 || !value
150 .bytes()
151 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.'))
152 {
153 return Err(RuntimeLeaseError::InvalidClientId);
154 }
155 Ok(Self(value))
156 }
157
158 pub fn as_str(&self) -> &str {
160 &self.0
161 }
162}
163
164impl std::fmt::Display for RuntimeClientId {
165 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166 formatter.write_str(&self.0)
167 }
168}
169
170#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
172pub struct RuntimeControllerLease {
173 pub client_id: RuntimeClientId,
175 pub expires_at_ms: u64,
178}
179
180#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
182pub struct RuntimeObserverLease {
183 pub client_id: RuntimeClientId,
185 pub last_seen_ms: u64,
187}
188
189#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
191pub struct RuntimeLeaseSnapshot {
192 pub controller: Option<RuntimeControllerLease>,
194 pub observers: Vec<RuntimeObserverLease>,
196 pub lease_ttl_ms: u64,
198}
199
200#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
202pub enum RuntimeLeaseError {
203 #[error("invalid runtime client id")]
205 InvalidClientId,
206 #[error("invalid runtime authorization grant")]
208 InvalidAuthorization,
209 #[error("runtime permission `{0:?}` is required")]
211 Unauthorized(RuntimePermission),
212 #[error("controller lease is held by `{holder}` until {expires_at_ms}")]
214 ControllerHeld {
215 holder: RuntimeClientId,
217 expires_at_ms: u64,
219 },
220 #[error("controller lease required")]
222 ControllerRequired,
223 #[error("controller lease expired")]
225 LeaseExpired,
226}
227
228#[derive(Debug)]
230pub struct RuntimeLeaseCoordinator {
231 lease_ttl_ms: u64,
232 controller: Option<RuntimeControllerLease>,
233 observers: BTreeMap<RuntimeClientId, RuntimeObserverLease>,
234 expired_controller: Option<RuntimeClientId>,
235}
236
237impl RuntimeLeaseCoordinator {
238 pub fn new(lease_ttl_ms: u64) -> Self {
240 assert!(lease_ttl_ms > 0, "runtime lease TTL must be non-zero");
241 Self {
242 lease_ttl_ms,
243 controller: None,
244 observers: BTreeMap::new(),
245 expired_controller: None,
246 }
247 }
248
249 pub fn attach(
251 &mut self,
252 client_id: RuntimeClientId,
253 authorization: &RuntimeAuthorization,
254 now_ms: u64,
255 ) -> Result<RuntimeLeaseSnapshot, RuntimeLeaseError> {
256 require(authorization, RuntimePermission::Observe)?;
257 self.reconcile(now_ms);
258 self.observers.insert(
259 client_id.clone(),
260 RuntimeObserverLease {
261 client_id,
262 last_seen_ms: now_ms,
263 },
264 );
265 Ok(self.snapshot(now_ms))
266 }
267
268 pub fn heartbeat(
270 &mut self,
271 client_id: &RuntimeClientId,
272 authorization: &RuntimeAuthorization,
273 now_ms: u64,
274 ) -> Result<RuntimeLeaseSnapshot, RuntimeLeaseError> {
275 require(authorization, RuntimePermission::Observe)?;
276 self.reconcile(now_ms);
277 self.observers
278 .entry(client_id.clone())
279 .and_modify(|observer| observer.last_seen_ms = now_ms)
280 .or_insert_with(|| RuntimeObserverLease {
281 client_id: client_id.clone(),
282 last_seen_ms: now_ms,
283 });
284 Ok(self.snapshot(now_ms))
285 }
286
287 pub fn authorize(
292 &mut self,
293 authorization: &RuntimeAuthorization,
294 permission: RuntimePermission,
295 now_ms: u64,
296 ) -> Result<RuntimeLeaseSnapshot, RuntimeLeaseError> {
297 require(authorization, permission)?;
298 Ok(self.snapshot(now_ms))
299 }
300
301 pub fn claim_control(
304 &mut self,
305 client_id: RuntimeClientId,
306 authorization: &RuntimeAuthorization,
307 now_ms: u64,
308 ) -> Result<RuntimeLeaseSnapshot, RuntimeLeaseError> {
309 require(authorization, RuntimePermission::Interact)?;
310 self.attach(client_id.clone(), authorization, now_ms)?;
311 if self.controller.is_none() && self.expired_controller.as_ref() == Some(&client_id) {
312 return Err(RuntimeLeaseError::LeaseExpired);
313 }
314 match &self.controller {
315 Some(lease) if lease.client_id != client_id => {
316 return Err(RuntimeLeaseError::ControllerHeld {
317 holder: lease.client_id.clone(),
318 expires_at_ms: lease.expires_at_ms,
319 });
320 }
321 _ => {}
322 }
323 self.controller = Some(RuntimeControllerLease {
324 client_id,
325 expires_at_ms: now_ms.saturating_add(self.lease_ttl_ms),
326 });
327 self.expired_controller = None;
328 Ok(self.snapshot(now_ms))
329 }
330
331 pub fn take_control(
333 &mut self,
334 client_id: RuntimeClientId,
335 authorization: &RuntimeAuthorization,
336 now_ms: u64,
337 ) -> Result<RuntimeLeaseSnapshot, RuntimeLeaseError> {
338 require(authorization, RuntimePermission::Interact)?;
339 self.attach(client_id.clone(), authorization, now_ms)?;
340 self.controller = Some(RuntimeControllerLease {
341 client_id,
342 expires_at_ms: now_ms.saturating_add(self.lease_ttl_ms),
343 });
344 self.expired_controller = None;
345 Ok(self.snapshot(now_ms))
346 }
347
348 pub fn authorize_controller(
350 &mut self,
351 client_id: &RuntimeClientId,
352 authorization: &RuntimeAuthorization,
353 permission: RuntimePermission,
354 now_ms: u64,
355 ) -> Result<RuntimeLeaseSnapshot, RuntimeLeaseError> {
356 require(authorization, permission)?;
357 let was_expired = self
358 .controller
359 .as_ref()
360 .is_some_and(|lease| lease.client_id == *client_id && lease.expires_at_ms <= now_ms);
361 self.reconcile(now_ms);
362 let Some(controller) = &mut self.controller else {
363 return Err(
364 if was_expired || self.expired_controller.as_ref() == Some(client_id) {
365 RuntimeLeaseError::LeaseExpired
366 } else {
367 RuntimeLeaseError::ControllerRequired
368 },
369 );
370 };
371 if controller.client_id != *client_id {
372 return Err(RuntimeLeaseError::ControllerRequired);
373 }
374 controller.expires_at_ms = now_ms.saturating_add(self.lease_ttl_ms);
375 self.expired_controller = None;
376 if let Some(observer) = self.observers.get_mut(client_id) {
377 observer.last_seen_ms = now_ms;
378 }
379 Ok(self.snapshot(now_ms))
380 }
381
382 pub fn detach(&mut self, client_id: &RuntimeClientId, now_ms: u64) -> RuntimeLeaseSnapshot {
384 self.reconcile(now_ms);
385 self.observers.remove(client_id);
386 if self
387 .controller
388 .as_ref()
389 .is_some_and(|lease| lease.client_id == *client_id)
390 {
391 self.controller = None;
392 }
393 if self.expired_controller.as_ref() == Some(client_id) {
394 self.expired_controller = None;
395 }
396 self.snapshot(now_ms)
397 }
398
399 pub fn snapshot(&mut self, now_ms: u64) -> RuntimeLeaseSnapshot {
401 self.reconcile(now_ms);
402 RuntimeLeaseSnapshot {
403 controller: self.controller.clone(),
404 observers: self.observers.values().cloned().collect(),
405 lease_ttl_ms: self.lease_ttl_ms,
406 }
407 }
408
409 fn reconcile(&mut self, now_ms: u64) {
410 if self
411 .controller
412 .as_ref()
413 .is_some_and(|lease| lease.expires_at_ms <= now_ms)
414 {
415 self.expired_controller = self
416 .controller
417 .take()
418 .map(|controller| controller.client_id);
419 }
420 }
421}
422
423pub struct CoordinatedRuntime {
430 runtime: Arc<dyn SdkRuntime>,
431 leases: Mutex<RuntimeLeaseCoordinator>,
432}
433
434impl CoordinatedRuntime {
435 pub fn new(runtime: Arc<dyn SdkRuntime>) -> Arc<Self> {
437 Self::with_lease_ttl(runtime, DEFAULT_RUNTIME_LEASE_TTL_MS)
438 }
439
440 pub fn with_lease_ttl(runtime: Arc<dyn SdkRuntime>, lease_ttl_ms: u64) -> Arc<Self> {
443 Arc::new(Self {
444 runtime,
445 leases: Mutex::new(RuntimeLeaseCoordinator::new(lease_ttl_ms)),
446 })
447 }
448
449 pub fn client(
452 self: &Arc<Self>,
453 client_id: RuntimeClientId,
454 authorization: RuntimeAuthorization,
455 ) -> Arc<CoordinatedRuntimeClient> {
456 Arc::new(CoordinatedRuntimeClient {
457 coordinator: self.clone(),
458 client_id,
459 authorization,
460 })
461 }
462
463 fn leases(&self) -> std::sync::MutexGuard<'_, RuntimeLeaseCoordinator> {
464 self.leases
465 .lock()
466 .unwrap_or_else(std::sync::PoisonError::into_inner)
467 }
468}
469
470pub struct CoordinatedRuntimeClient {
472 coordinator: Arc<CoordinatedRuntime>,
473 client_id: RuntimeClientId,
474 authorization: RuntimeAuthorization,
475}
476
477impl CoordinatedRuntimeClient {
478 pub fn client_id(&self) -> &RuntimeClientId {
480 &self.client_id
481 }
482
483 pub fn observe(&self) -> Result<RuntimeLeaseSnapshot, SdkError> {
485 self.coordinator
486 .leases()
487 .attach(self.client_id.clone(), &self.authorization, epoch_ms())
488 .map_err(|error| lease_sdk_error(error, SdkOperation::Events))
489 }
490
491 pub fn take_control(&self) -> Result<RuntimeLeaseSnapshot, SdkError> {
493 self.coordinator
494 .leases()
495 .take_control(self.client_id.clone(), &self.authorization, epoch_ms())
496 .map_err(|error| lease_sdk_error(error, SdkOperation::Input))
497 }
498
499 pub fn heartbeat(&self) -> Result<RuntimeLeaseSnapshot, SdkError> {
502 let now_ms = epoch_ms();
503 let mut leases = self.coordinator.leases();
504 let mut snapshot = leases
505 .heartbeat(&self.client_id, &self.authorization, now_ms)
506 .map_err(|error| lease_sdk_error(error, SdkOperation::Events))?;
507 if snapshot
508 .controller
509 .as_ref()
510 .is_some_and(|lease| lease.client_id == self.client_id)
511 {
512 snapshot = leases
513 .authorize_controller(
514 &self.client_id,
515 &self.authorization,
516 RuntimePermission::Interact,
517 now_ms,
518 )
519 .map_err(|error| lease_sdk_error(error, SdkOperation::Input))?;
520 }
521 Ok(snapshot)
522 }
523
524 pub fn detach(&self) -> RuntimeLeaseSnapshot {
527 self.coordinator
528 .leases()
529 .detach(&self.client_id, epoch_ms())
530 }
531
532 pub fn lease_snapshot(&self) -> Result<RuntimeLeaseSnapshot, SdkError> {
534 self.coordinator
535 .leases()
536 .authorize(&self.authorization, RuntimePermission::Observe, epoch_ms())
537 .map_err(|error| lease_sdk_error(error, SdkOperation::Events))
538 }
539
540 fn authorize_controller(
541 &self,
542 permission: RuntimePermission,
543 operation: SdkOperation,
544 ) -> Result<(), SdkError> {
545 let now_ms = epoch_ms();
546 let mut leases = self.coordinator.leases();
547 leases
548 .claim_control(self.client_id.clone(), &self.authorization, now_ms)
549 .map_err(|error| lease_sdk_error(error, operation))?;
550 if permission != RuntimePermission::Interact {
551 leases
552 .authorize_controller(&self.client_id, &self.authorization, permission, now_ms)
553 .map_err(|error| lease_sdk_error(error, operation))?;
554 }
555 Ok(())
556 }
557
558 fn authorize_lifecycle(
559 &self,
560 permission: RuntimePermission,
561 operation: SdkOperation,
562 ) -> Result<(), SdkError> {
563 self.coordinator
564 .leases()
565 .authorize(&self.authorization, permission, epoch_ms())
566 .map(|_| ())
567 .map_err(|error| lease_sdk_error(error, operation))
568 }
569
570 async fn descriptor(&self) -> Result<FrontendRuntimeDescriptor, SdkError> {
571 let mut descriptor = self.coordinator.runtime.describe().await?;
572 descriptor.actions.submit &= self.authorization.allows(RuntimePermission::Interact);
573 descriptor.actions.interrupt &= self.authorization.allows(RuntimePermission::Interact);
574 descriptor.actions.steer &= self.authorization.allows(RuntimePermission::Interact);
575 descriptor.actions.respond &= self.authorization.allows(RuntimePermission::Approve)
576 && self.authorization.allows(RuntimePermission::Interact);
577 descriptor.actions.close &= self.authorization.allows(RuntimePermission::Terminate);
578 descriptor.actions.detach &= self.authorization.allows(RuntimePermission::Observe);
579 Ok(descriptor)
580 }
581}
582
583#[async_trait]
584impl SdkRuntime for CoordinatedRuntimeClient {
585 async fn describe(&self) -> Result<FrontendRuntimeDescriptor, SdkError> {
586 self.authorize_lifecycle(RuntimePermission::Observe, SdkOperation::Events)?;
587 self.descriptor().await
588 }
589
590 async fn attach(&self, history_limit: usize) -> Result<FrontendAttachment, SdkError> {
591 self.observe()?;
592 let mut attachment = self.coordinator.runtime.attach(history_limit).await?;
593 attachment.descriptor = self.descriptor().await?;
594 Ok(attachment)
595 }
596
597 async fn send_input(self: Arc<Self>, prompt: String) -> Result<(), SdkError> {
598 self.authorize_controller(RuntimePermission::Interact, SdkOperation::Input)?;
599 self.coordinator.runtime.clone().send_input(prompt).await
600 }
601
602 async fn send_input_with_images(
603 self: Arc<Self>,
604 prompt: String,
605 image_urls: Vec<String>,
606 ) -> Result<(), SdkError> {
607 self.authorize_controller(RuntimePermission::Interact, SdkOperation::Input)?;
608 self.coordinator
609 .runtime
610 .clone()
611 .send_input_with_images(prompt, image_urls)
612 .await
613 }
614
615 async fn submit(&self, prompt: String) -> Result<String, SdkError> {
616 self.authorize_controller(RuntimePermission::Interact, SdkOperation::Input)?;
617 self.coordinator.runtime.submit(prompt).await
618 }
619
620 async fn submit_with_images(
621 &self,
622 prompt: String,
623 image_urls: Vec<String>,
624 ) -> Result<String, SdkError> {
625 self.authorize_controller(RuntimePermission::Interact, SdkOperation::Input)?;
626 self.coordinator
627 .runtime
628 .submit_with_images(prompt, image_urls)
629 .await
630 }
631
632 async fn interrupt(&self) -> Result<bool, SdkError> {
633 self.authorize_controller(RuntimePermission::Interact, SdkOperation::Interrupt)?;
634 self.coordinator.runtime.interrupt().await
635 }
636
637 async fn steer(&self, prompt: String) -> Result<(), SdkError> {
638 self.authorize_controller(RuntimePermission::Interact, SdkOperation::Steer)?;
639 self.coordinator.runtime.steer(prompt).await
640 }
641
642 async fn respond(&self, response: FrontendResponse) -> Result<(), SdkError> {
643 self.authorize_controller(RuntimePermission::Approve, SdkOperation::Respond)?;
644 self.coordinator.runtime.respond(response).await
645 }
646
647 async fn invoke(
648 &self,
649 operation: FrontendOperationInvocation,
650 ) -> Result<FrontendOperationResult, SdkError> {
651 self.authorize_controller(RuntimePermission::Interact, SdkOperation::Input)?;
652 self.coordinator.runtime.invoke(operation).await
653 }
654
655 async fn lease_snapshot(&self) -> Result<RuntimeLeaseSnapshot, SdkError> {
656 CoordinatedRuntimeClient::lease_snapshot(self)
657 }
658
659 async fn take_control(&self) -> Result<RuntimeLeaseSnapshot, SdkError> {
660 CoordinatedRuntimeClient::take_control(self)
661 }
662
663 async fn heartbeat(&self) -> Result<RuntimeLeaseSnapshot, SdkError> {
664 CoordinatedRuntimeClient::heartbeat(self)
665 }
666
667 async fn detach(&self) -> Result<RuntimeLeaseSnapshot, SdkError> {
668 Ok(CoordinatedRuntimeClient::detach(self))
669 }
670
671 async fn close(&self) -> Result<(), SdkError> {
672 self.authorize_lifecycle(RuntimePermission::Terminate, SdkOperation::Close)?;
673 self.coordinator.runtime.close().await
674 }
675}
676
677fn lease_sdk_error(error: RuntimeLeaseError, operation: SdkOperation) -> SdkError {
678 match error {
679 error @ (RuntimeLeaseError::InvalidClientId | RuntimeLeaseError::InvalidAuthorization) => {
680 SdkError::InvalidArgument {
681 operation,
682 message: error.to_string(),
683 }
684 }
685 RuntimeLeaseError::Unauthorized(permission) => SdkError::Unauthorized {
686 permission: permission.as_str().into(),
687 },
688 RuntimeLeaseError::ControllerHeld {
689 holder,
690 expires_at_ms,
691 } => SdkError::ControllerRequired {
692 holder: Some(holder.to_string()),
693 expires_at_ms: Some(expires_at_ms),
694 },
695 RuntimeLeaseError::ControllerRequired => SdkError::ControllerRequired {
696 holder: None,
697 expires_at_ms: None,
698 },
699 RuntimeLeaseError::LeaseExpired => SdkError::LeaseExpired,
700 }
701}
702
703fn epoch_ms() -> u64 {
704 SystemTime::now()
705 .duration_since(UNIX_EPOCH)
706 .unwrap_or_default()
707 .as_millis()
708 .min(u64::MAX as u128) as u64
709}
710
711fn require(
712 authorization: &RuntimeAuthorization,
713 permission: RuntimePermission,
714) -> Result<(), RuntimeLeaseError> {
715 if authorization.allows(permission) {
716 Ok(())
717 } else {
718 Err(RuntimeLeaseError::Unauthorized(permission))
719 }
720}
721
722#[cfg(test)]
723mod tests {
724 use super::*;
725
726 fn client(value: &str) -> RuntimeClientId {
727 RuntimeClientId::parse(value).unwrap()
728 }
729
730 #[test]
731 fn many_observers_share_one_explicit_controller() {
732 let mut leases = RuntimeLeaseCoordinator::new(100);
733 let owner = RuntimeAuthorization::owner();
734 let observer = RuntimeAuthorization::observer();
735 leases.attach(client("viewer-a"), &observer, 10).unwrap();
736 leases.attach(client("viewer-b"), &observer, 11).unwrap();
737 let snapshot = leases.claim_control(client("owner"), &owner, 12).unwrap();
738 assert_eq!(snapshot.observers.len(), 3);
739 assert_eq!(snapshot.controller.unwrap().client_id, client("owner"));
740
741 let error = leases
742 .claim_control(client("viewer-b"), &owner, 13)
743 .unwrap_err();
744 assert!(matches!(error, RuntimeLeaseError::ControllerHeld { .. }));
745 }
746
747 #[test]
748 fn observer_cannot_claim_control_approve_or_terminate() {
749 let mut leases = RuntimeLeaseCoordinator::new(100);
750 let observer = RuntimeAuthorization::observer();
751 leases.attach(client("viewer"), &observer, 1).unwrap();
752 assert_eq!(
753 leases
754 .claim_control(client("viewer"), &observer, 2)
755 .unwrap_err(),
756 RuntimeLeaseError::Unauthorized(RuntimePermission::Interact)
757 );
758 assert_eq!(
759 leases
760 .authorize_controller(&client("viewer"), &observer, RuntimePermission::Approve, 2)
761 .unwrap_err(),
762 RuntimeLeaseError::Unauthorized(RuntimePermission::Approve)
763 );
764 assert_eq!(
765 leases
766 .authorize(&observer, RuntimePermission::Terminate, 2)
767 .unwrap_err(),
768 RuntimeLeaseError::Unauthorized(RuntimePermission::Terminate)
769 );
770 }
771
772 #[test]
773 fn expiry_is_deterministic_and_requires_a_new_claim() {
774 let mut leases = RuntimeLeaseCoordinator::new(10);
775 let owner = RuntimeAuthorization::owner();
776 leases.claim_control(client("a"), &owner, 5).unwrap();
777 assert_eq!(
778 leases.claim_control(client("a"), &owner, 15).unwrap_err(),
779 RuntimeLeaseError::LeaseExpired
780 );
781 assert_eq!(
782 leases
783 .authorize_controller(&client("a"), &owner, RuntimePermission::Interact, 15)
784 .unwrap_err(),
785 RuntimeLeaseError::LeaseExpired
786 );
787 let snapshot = leases.claim_control(client("b"), &owner, 15).unwrap();
788 assert_eq!(snapshot.controller.unwrap().client_id, client("b"));
789 }
790
791 #[test]
792 fn successful_mutation_renews_controller_and_observer_activity() {
793 let mut leases = RuntimeLeaseCoordinator::new(10);
794 let owner = RuntimeAuthorization::owner();
795 leases.claim_control(client("a"), &owner, 5).unwrap();
796 let snapshot = leases
797 .authorize_controller(&client("a"), &owner, RuntimePermission::Approve, 9)
798 .unwrap();
799 assert_eq!(snapshot.controller.unwrap().expires_at_ms, 19);
800 assert_eq!(snapshot.observers[0].last_seen_ms, 9);
801 }
802
803 #[test]
804 fn takeover_and_detach_are_explicit_and_release_control() {
805 let mut leases = RuntimeLeaseCoordinator::new(10);
806 let owner = RuntimeAuthorization::owner();
807 leases.claim_control(client("a"), &owner, 1).unwrap();
808 let snapshot = leases.take_control(client("b"), &owner, 2).unwrap();
809 assert_eq!(snapshot.controller.unwrap().client_id, client("b"));
810 let snapshot = leases.detach(&client("b"), 3);
811 assert!(snapshot.controller.is_none());
812 assert_eq!(
813 snapshot
814 .observers
815 .into_iter()
816 .map(|observer| observer.client_id)
817 .collect::<Vec<_>>(),
818 vec![client("a")]
819 );
820 }
821
822 #[test]
823 fn client_ids_are_opaque_bounded_and_header_safe() {
824 for invalid in ["", "space here", "slash/here", "💥"] {
825 assert_eq!(
826 RuntimeClientId::parse(invalid).unwrap_err(),
827 RuntimeLeaseError::InvalidClientId
828 );
829 }
830 assert_eq!(
831 RuntimeClientId::parse("a".repeat(129)).unwrap_err(),
832 RuntimeLeaseError::InvalidClientId
833 );
834 assert_eq!(
835 RuntimeClientId::parse("client-1.v2_ok").unwrap().as_str(),
836 "client-1.v2_ok"
837 );
838 let owner = RuntimeAuthorization::owner();
839 let requested = RuntimeAuthorization::parse_header("observe,interact").unwrap();
840 assert_eq!(
841 owner.restrict_to(&requested).header_value(),
842 "observe,interact"
843 );
844 assert_eq!(
845 RuntimeAuthorization::parse_header("observe,admin").unwrap_err(),
846 RuntimeLeaseError::InvalidAuthorization
847 );
848 }
849}