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 acquire_control(&self) -> Result<RuntimeLeaseSnapshot, SdkError> {
502 self.coordinator
503 .leases()
504 .claim_control(self.client_id.clone(), &self.authorization, epoch_ms())
505 .map_err(|error| lease_sdk_error(error, SdkOperation::Input))
506 }
507
508 pub fn heartbeat(&self) -> Result<RuntimeLeaseSnapshot, SdkError> {
511 let now_ms = epoch_ms();
512 let mut leases = self.coordinator.leases();
513 let mut snapshot = leases
514 .heartbeat(&self.client_id, &self.authorization, now_ms)
515 .map_err(|error| lease_sdk_error(error, SdkOperation::Events))?;
516 if snapshot
517 .controller
518 .as_ref()
519 .is_some_and(|lease| lease.client_id == self.client_id)
520 {
521 snapshot = leases
522 .authorize_controller(
523 &self.client_id,
524 &self.authorization,
525 RuntimePermission::Interact,
526 now_ms,
527 )
528 .map_err(|error| lease_sdk_error(error, SdkOperation::Input))?;
529 }
530 Ok(snapshot)
531 }
532
533 pub fn detach(&self) -> RuntimeLeaseSnapshot {
536 self.coordinator
537 .leases()
538 .detach(&self.client_id, epoch_ms())
539 }
540
541 pub fn lease_snapshot(&self) -> Result<RuntimeLeaseSnapshot, SdkError> {
543 self.coordinator
544 .leases()
545 .authorize(&self.authorization, RuntimePermission::Observe, epoch_ms())
546 .map_err(|error| lease_sdk_error(error, SdkOperation::Events))
547 }
548
549 fn authorize_controller(
550 &self,
551 permission: RuntimePermission,
552 operation: SdkOperation,
553 ) -> Result<(), SdkError> {
554 let now_ms = epoch_ms();
555 let mut leases = self.coordinator.leases();
556 leases
557 .claim_control(self.client_id.clone(), &self.authorization, now_ms)
558 .map_err(|error| lease_sdk_error(error, operation))?;
559 if permission != RuntimePermission::Interact {
560 leases
561 .authorize_controller(&self.client_id, &self.authorization, permission, now_ms)
562 .map_err(|error| lease_sdk_error(error, operation))?;
563 }
564 Ok(())
565 }
566
567 fn authorize_lifecycle(
568 &self,
569 permission: RuntimePermission,
570 operation: SdkOperation,
571 ) -> Result<(), SdkError> {
572 self.coordinator
573 .leases()
574 .authorize(&self.authorization, permission, epoch_ms())
575 .map(|_| ())
576 .map_err(|error| lease_sdk_error(error, operation))
577 }
578
579 async fn descriptor(&self) -> Result<FrontendRuntimeDescriptor, SdkError> {
580 let mut descriptor = self.coordinator.runtime.describe().await?;
581 descriptor.actions.submit &= self.authorization.allows(RuntimePermission::Interact);
582 descriptor.actions.interrupt &= self.authorization.allows(RuntimePermission::Interact);
583 descriptor.actions.steer &= self.authorization.allows(RuntimePermission::Interact);
584 descriptor.actions.respond &= self.authorization.allows(RuntimePermission::Approve)
585 && self.authorization.allows(RuntimePermission::Interact);
586 descriptor.actions.close &= self.authorization.allows(RuntimePermission::Terminate);
587 descriptor.actions.detach &= self.authorization.allows(RuntimePermission::Observe);
588 Ok(descriptor)
589 }
590}
591
592#[async_trait]
593impl SdkRuntime for CoordinatedRuntimeClient {
594 async fn describe(&self) -> Result<FrontendRuntimeDescriptor, SdkError> {
595 self.authorize_lifecycle(RuntimePermission::Observe, SdkOperation::Events)?;
596 self.descriptor().await
597 }
598
599 async fn attach(&self, history_limit: usize) -> Result<FrontendAttachment, SdkError> {
600 self.observe()?;
601 let mut attachment = self.coordinator.runtime.attach(history_limit).await?;
602 attachment.descriptor = self.descriptor().await?;
603 Ok(attachment)
604 }
605
606 async fn send_input(self: Arc<Self>, prompt: String) -> Result<(), SdkError> {
607 self.authorize_controller(RuntimePermission::Interact, SdkOperation::Input)?;
608 self.coordinator.runtime.clone().send_input(prompt).await
609 }
610
611 async fn send_input_with_images(
612 self: Arc<Self>,
613 prompt: String,
614 image_urls: Vec<String>,
615 ) -> Result<(), SdkError> {
616 self.authorize_controller(RuntimePermission::Interact, SdkOperation::Input)?;
617 self.coordinator
618 .runtime
619 .clone()
620 .send_input_with_images(prompt, image_urls)
621 .await
622 }
623
624 async fn submit(&self, prompt: String) -> Result<String, SdkError> {
625 self.authorize_controller(RuntimePermission::Interact, SdkOperation::Input)?;
626 self.coordinator.runtime.submit(prompt).await
627 }
628
629 async fn submit_with_images(
630 &self,
631 prompt: String,
632 image_urls: Vec<String>,
633 ) -> Result<String, SdkError> {
634 self.authorize_controller(RuntimePermission::Interact, SdkOperation::Input)?;
635 self.coordinator
636 .runtime
637 .submit_with_images(prompt, image_urls)
638 .await
639 }
640
641 async fn interrupt(&self) -> Result<bool, SdkError> {
642 self.authorize_controller(RuntimePermission::Interact, SdkOperation::Interrupt)?;
643 self.coordinator.runtime.interrupt().await
644 }
645
646 async fn steer(&self, prompt: String) -> Result<(), SdkError> {
647 self.authorize_controller(RuntimePermission::Interact, SdkOperation::Steer)?;
648 self.coordinator.runtime.steer(prompt).await
649 }
650
651 async fn respond(&self, response: FrontendResponse) -> Result<(), SdkError> {
652 self.authorize_controller(RuntimePermission::Approve, SdkOperation::Respond)?;
653 self.coordinator.runtime.respond(response).await
654 }
655
656 async fn invoke(
657 &self,
658 operation: FrontendOperationInvocation,
659 ) -> Result<FrontendOperationResult, SdkError> {
660 self.authorize_controller(RuntimePermission::Interact, SdkOperation::Input)?;
661 self.coordinator.runtime.invoke(operation).await
662 }
663
664 async fn lease_snapshot(&self) -> Result<RuntimeLeaseSnapshot, SdkError> {
665 CoordinatedRuntimeClient::lease_snapshot(self)
666 }
667
668 async fn take_control(&self) -> Result<RuntimeLeaseSnapshot, SdkError> {
669 CoordinatedRuntimeClient::take_control(self)
670 }
671
672 async fn acquire_control(&self) -> Result<RuntimeLeaseSnapshot, SdkError> {
673 CoordinatedRuntimeClient::acquire_control(self)
674 }
675
676 async fn heartbeat(&self) -> Result<RuntimeLeaseSnapshot, SdkError> {
677 CoordinatedRuntimeClient::heartbeat(self)
678 }
679
680 async fn detach(&self) -> Result<RuntimeLeaseSnapshot, SdkError> {
681 Ok(CoordinatedRuntimeClient::detach(self))
682 }
683
684 async fn close(&self) -> Result<(), SdkError> {
685 self.authorize_lifecycle(RuntimePermission::Terminate, SdkOperation::Close)?;
686 self.coordinator.runtime.close().await
687 }
688}
689
690fn lease_sdk_error(error: RuntimeLeaseError, operation: SdkOperation) -> SdkError {
691 match error {
692 error @ (RuntimeLeaseError::InvalidClientId | RuntimeLeaseError::InvalidAuthorization) => {
693 SdkError::InvalidArgument {
694 operation,
695 message: error.to_string(),
696 }
697 }
698 RuntimeLeaseError::Unauthorized(permission) => SdkError::Unauthorized {
699 permission: permission.as_str().into(),
700 },
701 RuntimeLeaseError::ControllerHeld {
702 holder,
703 expires_at_ms,
704 } => SdkError::ControllerRequired {
705 holder: Some(holder.to_string()),
706 expires_at_ms: Some(expires_at_ms),
707 },
708 RuntimeLeaseError::ControllerRequired => SdkError::ControllerRequired {
709 holder: None,
710 expires_at_ms: None,
711 },
712 RuntimeLeaseError::LeaseExpired => SdkError::LeaseExpired,
713 }
714}
715
716fn epoch_ms() -> u64 {
717 SystemTime::now()
718 .duration_since(UNIX_EPOCH)
719 .unwrap_or_default()
720 .as_millis()
721 .min(u64::MAX as u128) as u64
722}
723
724fn require(
725 authorization: &RuntimeAuthorization,
726 permission: RuntimePermission,
727) -> Result<(), RuntimeLeaseError> {
728 if authorization.allows(permission) {
729 Ok(())
730 } else {
731 Err(RuntimeLeaseError::Unauthorized(permission))
732 }
733}
734
735#[cfg(test)]
736mod tests {
737 use super::*;
738
739 fn client(value: &str) -> RuntimeClientId {
740 RuntimeClientId::parse(value).unwrap()
741 }
742
743 #[test]
744 fn many_observers_share_one_explicit_controller() {
745 let mut leases = RuntimeLeaseCoordinator::new(100);
746 let owner = RuntimeAuthorization::owner();
747 let observer = RuntimeAuthorization::observer();
748 leases.attach(client("viewer-a"), &observer, 10).unwrap();
749 leases.attach(client("viewer-b"), &observer, 11).unwrap();
750 let snapshot = leases.claim_control(client("owner"), &owner, 12).unwrap();
751 assert_eq!(snapshot.observers.len(), 3);
752 assert_eq!(snapshot.controller.unwrap().client_id, client("owner"));
753
754 let error = leases
755 .claim_control(client("viewer-b"), &owner, 13)
756 .unwrap_err();
757 assert!(matches!(error, RuntimeLeaseError::ControllerHeld { .. }));
758 }
759
760 #[test]
761 fn observer_cannot_claim_control_approve_or_terminate() {
762 let mut leases = RuntimeLeaseCoordinator::new(100);
763 let observer = RuntimeAuthorization::observer();
764 leases.attach(client("viewer"), &observer, 1).unwrap();
765 assert_eq!(
766 leases
767 .claim_control(client("viewer"), &observer, 2)
768 .unwrap_err(),
769 RuntimeLeaseError::Unauthorized(RuntimePermission::Interact)
770 );
771 assert_eq!(
772 leases
773 .authorize_controller(&client("viewer"), &observer, RuntimePermission::Approve, 2)
774 .unwrap_err(),
775 RuntimeLeaseError::Unauthorized(RuntimePermission::Approve)
776 );
777 assert_eq!(
778 leases
779 .authorize(&observer, RuntimePermission::Terminate, 2)
780 .unwrap_err(),
781 RuntimeLeaseError::Unauthorized(RuntimePermission::Terminate)
782 );
783 }
784
785 #[test]
786 fn expiry_is_deterministic_and_requires_a_new_claim() {
787 let mut leases = RuntimeLeaseCoordinator::new(10);
788 let owner = RuntimeAuthorization::owner();
789 leases.claim_control(client("a"), &owner, 5).unwrap();
790 assert_eq!(
791 leases.claim_control(client("a"), &owner, 15).unwrap_err(),
792 RuntimeLeaseError::LeaseExpired
793 );
794 assert_eq!(
795 leases
796 .authorize_controller(&client("a"), &owner, RuntimePermission::Interact, 15)
797 .unwrap_err(),
798 RuntimeLeaseError::LeaseExpired
799 );
800 let snapshot = leases.claim_control(client("b"), &owner, 15).unwrap();
801 assert_eq!(snapshot.controller.unwrap().client_id, client("b"));
802 }
803
804 #[test]
805 fn successful_mutation_renews_controller_and_observer_activity() {
806 let mut leases = RuntimeLeaseCoordinator::new(10);
807 let owner = RuntimeAuthorization::owner();
808 leases.claim_control(client("a"), &owner, 5).unwrap();
809 let snapshot = leases
810 .authorize_controller(&client("a"), &owner, RuntimePermission::Approve, 9)
811 .unwrap();
812 assert_eq!(snapshot.controller.unwrap().expires_at_ms, 19);
813 assert_eq!(snapshot.observers[0].last_seen_ms, 9);
814 }
815
816 #[test]
817 fn takeover_and_detach_are_explicit_and_release_control() {
818 let mut leases = RuntimeLeaseCoordinator::new(10);
819 let owner = RuntimeAuthorization::owner();
820 leases.claim_control(client("a"), &owner, 1).unwrap();
821 let snapshot = leases.take_control(client("b"), &owner, 2).unwrap();
822 assert_eq!(snapshot.controller.unwrap().client_id, client("b"));
823 let snapshot = leases.detach(&client("b"), 3);
824 assert!(snapshot.controller.is_none());
825 assert_eq!(
826 snapshot
827 .observers
828 .into_iter()
829 .map(|observer| observer.client_id)
830 .collect::<Vec<_>>(),
831 vec![client("a")]
832 );
833 }
834
835 #[test]
836 fn client_ids_are_opaque_bounded_and_header_safe() {
837 for invalid in ["", "space here", "slash/here", "💥"] {
838 assert_eq!(
839 RuntimeClientId::parse(invalid).unwrap_err(),
840 RuntimeLeaseError::InvalidClientId
841 );
842 }
843 assert_eq!(
844 RuntimeClientId::parse("a".repeat(129)).unwrap_err(),
845 RuntimeLeaseError::InvalidClientId
846 );
847 assert_eq!(
848 RuntimeClientId::parse("client-1.v2_ok").unwrap().as_str(),
849 "client-1.v2_ok"
850 );
851 let owner = RuntimeAuthorization::owner();
852 let requested = RuntimeAuthorization::parse_header("observe,interact").unwrap();
853 assert_eq!(
854 owner.restrict_to(&requested).header_value(),
855 "observe,interact"
856 );
857 assert_eq!(
858 RuntimeAuthorization::parse_header("observe,admin").unwrap_err(),
859 RuntimeLeaseError::InvalidAuthorization
860 );
861 }
862}