1use std::fmt;
2use std::future::Future;
3use std::marker::PhantomData;
4use std::sync::{Arc, Mutex, Weak};
5
6use async_trait::async_trait;
7use serde::Serialize;
8use tokio_util::sync::CancellationToken;
9
10use super::supervisor::{
11 remove_registered_child, EffectSupervisor, SupervisedChild, SupervisorInner,
12};
13use super::{
14 CapabilityCeiling, CapabilityEffect, CapabilityEffectError, CapabilityLease,
15 CapabilityScopeError, CapabilitySet, RetainedUseGeneration, ScopeClosePolicy, ScopeCloseReport,
16 Sha256Digest, SupervisedTaskId, SupervisedTaskSpawner, UseCapabilityGeneration,
17 MAX_CAPABILITY_IDENTIFIER_BYTES,
18};
19
20#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
22#[serde(rename_all = "kebab-case")]
23pub enum CapabilityScopeKind {
24 Session,
25 Run,
26 Turn,
27 Subtask,
28}
29
30impl CapabilityScopeKind {
31 pub const fn as_str(self) -> &'static str {
32 match self {
33 Self::Session => "session",
34 Self::Run => "run",
35 Self::Turn => "turn",
36 Self::Subtask => "subtask",
37 }
38 }
39}
40
41impl fmt::Display for CapabilityScopeKind {
42 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
43 formatter.write_str(self.as_str())
44 }
45}
46
47mod sealed {
48 pub trait Sealed {}
49}
50
51pub trait ScopeKind: sealed::Sealed + Send + Sync + 'static {
53 const KIND: CapabilityScopeKind;
54}
55
56#[derive(Debug)]
57pub struct Session;
58
59#[derive(Debug)]
60pub struct Run;
61
62#[derive(Debug)]
63pub struct Turn;
64
65#[derive(Debug)]
66pub struct Subtask;
67
68impl sealed::Sealed for Session {}
69impl sealed::Sealed for Run {}
70impl sealed::Sealed for Turn {}
71impl sealed::Sealed for Subtask {}
72
73impl ScopeKind for Session {
74 const KIND: CapabilityScopeKind = CapabilityScopeKind::Session;
75}
76
77impl ScopeKind for Run {
78 const KIND: CapabilityScopeKind = CapabilityScopeKind::Run;
79}
80
81impl ScopeKind for Turn {
82 const KIND: CapabilityScopeKind = CapabilityScopeKind::Turn;
83}
84
85impl ScopeKind for Subtask {
86 const KIND: CapabilityScopeKind = CapabilityScopeKind::Subtask;
87}
88
89#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
91#[serde(transparent)]
92pub struct CapabilityScopeId(Box<str>);
93
94impl CapabilityScopeId {
95 fn root(
96 kind: CapabilityScopeKind,
97 local_id: impl Into<String>,
98 ) -> Result<Self, CapabilityScopeError> {
99 let local_id = local_id.into();
100 validate_scope_local_id(&local_id)?;
101 Self::from_complete(format!("{kind}/{local_id}"))
102 }
103
104 fn child(
105 parent: &Self,
106 kind: CapabilityScopeKind,
107 local_id: impl Into<String>,
108 ) -> Result<Self, CapabilityScopeError> {
109 let local_id = local_id.into();
110 validate_scope_local_id(&local_id)?;
111 Self::from_complete(format!("{}/{kind}/{local_id}", parent.as_str()))
112 }
113
114 fn from_complete(value: String) -> Result<Self, CapabilityScopeError> {
115 if value.len() > MAX_CAPABILITY_IDENTIFIER_BYTES {
116 return Err(CapabilityScopeError::BoundExceeded {
117 field: "scope_id",
118 max: MAX_CAPABILITY_IDENTIFIER_BYTES,
119 });
120 }
121 Ok(Self(value.into_boxed_str()))
122 }
123
124 pub fn as_str(&self) -> &str {
125 &self.0
126 }
127}
128
129impl fmt::Display for CapabilityScopeId {
130 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
131 formatter.write_str(self.as_str())
132 }
133}
134
135struct ParentRegistration {
136 supervisor: Weak<SupervisorInner>,
137 id: u64,
138}
139
140pub(super) struct ScopeInner {
141 id: CapabilityScopeId,
142 kind: CapabilityScopeKind,
143 parent_id: Option<CapabilityScopeId>,
144 set: Arc<CapabilitySet>,
145 ceiling: CapabilityCeiling,
146 use_generation: Option<UseCapabilityGeneration>,
147 supervisor: EffectSupervisor,
148 parent_registration: Mutex<Option<ParentRegistration>>,
149}
150
151impl ScopeInner {
152 pub(super) fn id(&self) -> &CapabilityScopeId {
153 &self.id
154 }
155
156 pub(super) fn parent_id(&self) -> Option<&CapabilityScopeId> {
157 self.parent_id.as_ref()
158 }
159
160 pub(super) fn set(&self) -> &CapabilitySet {
161 &self.set
162 }
163
164 pub(super) fn ceiling(&self) -> &CapabilityCeiling {
165 &self.ceiling
166 }
167
168 pub(super) fn use_generation(&self) -> Option<&UseCapabilityGeneration> {
169 self.use_generation.as_ref()
170 }
171
172 pub(super) fn supervisor_cancellation(&self) -> CancellationToken {
173 self.supervisor.cancellation()
174 }
175
176 pub(super) fn ensure_active(&self) -> Result<(), CapabilityScopeError> {
177 if !self.supervisor.is_open() {
178 return Err(CapabilityScopeError::ScopeInactive {
179 scope_id: self.id.to_string(),
180 });
181 }
182 Ok(())
183 }
184
185 async fn close(&self) -> Result<ScopeCloseReport, CapabilityScopeError> {
186 let report = self.supervisor.close().await?;
187 self.detach_from_parent();
188 Ok(report)
189 }
190
191 fn set_parent_registration(&self, supervisor: Weak<SupervisorInner>, id: u64) {
192 *self
193 .parent_registration
194 .lock()
195 .unwrap_or_else(std::sync::PoisonError::into_inner) =
196 Some(ParentRegistration { supervisor, id });
197 }
198
199 fn detach_from_parent(&self) {
200 let registration = self
201 .parent_registration
202 .lock()
203 .unwrap_or_else(std::sync::PoisonError::into_inner)
204 .take();
205 if let Some(registration) = registration {
206 remove_registered_child(®istration.supervisor, registration.id);
207 }
208 }
209}
210
211impl fmt::Debug for ScopeInner {
212 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
213 formatter
214 .debug_struct("ScopeInner")
215 .field("id", &self.id)
216 .field("kind", &self.kind)
217 .field("parent_id", &self.parent_id)
218 .field("catalog_digest", &self.set.digest())
219 .field("ceiling", &self.ceiling)
220 .field("use_generation", &self.use_generation)
221 .field("active", &self.supervisor.is_open())
222 .finish()
223 }
224}
225
226struct ChildScopeOwner {
227 inner: Arc<ScopeInner>,
228}
229
230#[async_trait]
231impl SupervisedChild for ChildScopeOwner {
232 fn name(&self) -> &str {
233 self.inner.id.as_str()
234 }
235
236 fn cancel(&self) {
237 self.inner.supervisor.cancel();
238 }
239
240 async fn close(self: Box<Self>) -> Result<ScopeCloseReport, CapabilityScopeError> {
241 self.inner.close().await
242 }
243}
244
245#[must_use = "capability scopes own effects and must be closed explicitly"]
252pub struct CapabilityScope<K: ScopeKind> {
253 inner: Arc<ScopeInner>,
254 _kind: PhantomData<K>,
255}
256
257pub struct CapabilityScopeHandle<K: ScopeKind> {
265 inner: Weak<ScopeInner>,
266 id: CapabilityScopeId,
267 _kind: PhantomData<K>,
268}
269
270impl<K: ScopeKind> Clone for CapabilityScopeHandle<K> {
271 fn clone(&self) -> Self {
272 Self {
273 inner: self.inner.clone(),
274 id: self.id.clone(),
275 _kind: PhantomData,
276 }
277 }
278}
279
280impl<K: ScopeKind> fmt::Debug for CapabilityScopeHandle<K> {
281 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
282 formatter
283 .debug_struct("CapabilityScopeHandle")
284 .field("marker", &K::KIND)
285 .field("id", &self.id)
286 .field("active", &self.is_active())
287 .finish()
288 }
289}
290
291impl<K: ScopeKind> fmt::Debug for CapabilityScope<K> {
292 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
293 formatter
294 .debug_struct("CapabilityScope")
295 .field("marker", &K::KIND)
296 .field("inner", &self.inner)
297 .finish()
298 }
299}
300
301impl CapabilityScope<Session> {
302 pub fn new_session(
303 local_id: impl Into<String>,
304 set: Arc<CapabilitySet>,
305 ceiling: CapabilityCeiling,
306 ) -> Result<Self, CapabilityScopeError> {
307 Self::new_session_with_close_policy(local_id, set, ceiling, ScopeClosePolicy::default())
308 }
309
310 pub fn new_session_with_close_policy(
311 local_id: impl Into<String>,
312 set: Arc<CapabilitySet>,
313 ceiling: CapabilityCeiling,
314 close_policy: ScopeClosePolicy,
315 ) -> Result<Self, CapabilityScopeError> {
316 Self::new_session_with_close_policy_and_cancellation(
317 local_id,
318 set,
319 ceiling,
320 close_policy,
321 CancellationToken::new(),
322 )
323 }
324
325 pub fn new_session_with_cancellation(
331 local_id: impl Into<String>,
332 set: Arc<CapabilitySet>,
333 ceiling: CapabilityCeiling,
334 cancellation: CancellationToken,
335 ) -> Result<Self, CapabilityScopeError> {
336 Self::new_session_with_close_policy_and_cancellation(
337 local_id,
338 set,
339 ceiling,
340 ScopeClosePolicy::default(),
341 cancellation,
342 )
343 }
344
345 pub fn new_session_with_close_policy_and_cancellation(
346 local_id: impl Into<String>,
347 set: Arc<CapabilitySet>,
348 ceiling: CapabilityCeiling,
349 close_policy: ScopeClosePolicy,
350 cancellation: CancellationToken,
351 ) -> Result<Self, CapabilityScopeError> {
352 if ceiling.catalog_digest() != set.digest() {
353 return Err(CapabilityScopeError::CeilingCatalogMismatch);
354 }
355 let id = CapabilityScopeId::root(CapabilityScopeKind::Session, local_id)?;
356 if cancellation.is_cancelled() {
357 return Err(CapabilityScopeError::ScopeInactive {
358 scope_id: id.to_string(),
359 });
360 }
361 let supervisor = EffectSupervisor::new(id.to_string(), cancellation, close_policy);
362 let use_generation = set.use_capability_generation().cloned();
363 Ok(Self {
364 inner: Arc::new(ScopeInner {
365 id,
366 kind: CapabilityScopeKind::Session,
367 parent_id: None,
368 set,
369 ceiling,
370 use_generation,
371 supervisor,
372 parent_registration: Mutex::new(None),
373 }),
374 _kind: PhantomData,
375 })
376 }
377
378 pub fn admit_run(
380 &self,
381 local_id: impl Into<String>,
382 ceiling: CapabilityCeiling,
383 ) -> Result<CapabilityScope<Run>, CapabilityScopeError> {
384 if self.inner.use_generation.is_some() {
385 return Err(CapabilityScopeError::MissingUseGenerationLease);
386 }
387 self.create_child(local_id, ceiling, None, None)
388 }
389
390 pub fn admit_use_run<L>(
393 &self,
394 local_id: impl Into<String>,
395 ceiling: CapabilityCeiling,
396 lease: L,
397 ) -> Result<CapabilityScope<Run>, CapabilityScopeError>
398 where
399 L: RetainedUseGeneration,
400 {
401 let Some(expected) = self.inner.use_generation.as_ref() else {
402 return Err(CapabilityScopeError::UnexpectedUseGenerationLease);
403 };
404 let actual = lease.use_generation();
405 if actual != expected {
406 return Err(CapabilityScopeError::UseGenerationLeaseMismatch {
407 expected_generation: expected.generation(),
408 actual_generation: actual.generation(),
409 revision_mismatch: expected.revision() != actual.revision(),
410 registry_revision_mismatch: expected.registry_revision()
411 != actual.registry_revision(),
412 });
413 }
414 self.create_child(
415 local_id,
416 ceiling,
417 Some(Box::new(lease)),
418 Some(expected.clone()),
419 )
420 }
421}
422
423impl CapabilityScope<Run> {
424 pub fn turn(
425 &self,
426 local_id: impl Into<String>,
427 ceiling: CapabilityCeiling,
428 ) -> Result<CapabilityScope<Turn>, CapabilityScopeError> {
429 self.create_child(local_id, ceiling, None, self.inner.use_generation.clone())
430 }
431
432 pub fn subtask(
433 &self,
434 local_id: impl Into<String>,
435 ceiling: CapabilityCeiling,
436 ) -> Result<CapabilityScope<Subtask>, CapabilityScopeError> {
437 self.create_child(local_id, ceiling, None, self.inner.use_generation.clone())
438 }
439}
440
441impl CapabilityScope<Turn> {
442 pub fn subtask(
443 &self,
444 local_id: impl Into<String>,
445 ceiling: CapabilityCeiling,
446 ) -> Result<CapabilityScope<Subtask>, CapabilityScopeError> {
447 self.create_child(local_id, ceiling, None, self.inner.use_generation.clone())
448 }
449}
450
451impl CapabilityScope<Subtask> {
452 pub fn turn(
457 &self,
458 local_id: impl Into<String>,
459 ceiling: CapabilityCeiling,
460 ) -> Result<CapabilityScope<Turn>, CapabilityScopeError> {
461 self.create_child(local_id, ceiling, None, self.inner.use_generation.clone())
462 }
463
464 pub fn subtask(
465 &self,
466 local_id: impl Into<String>,
467 ceiling: CapabilityCeiling,
468 ) -> Result<CapabilityScope<Subtask>, CapabilityScopeError> {
469 self.create_child(local_id, ceiling, None, self.inner.use_generation.clone())
470 }
471}
472
473impl CapabilityScopeHandle<Run> {
474 pub fn turn(
475 &self,
476 local_id: impl Into<String>,
477 ceiling: CapabilityCeiling,
478 ) -> Result<CapabilityScope<Turn>, CapabilityScopeError> {
479 self.create_child(local_id, ceiling)
480 }
481
482 pub fn turn_inheriting(
483 &self,
484 local_id: impl Into<String>,
485 ) -> Result<CapabilityScope<Turn>, CapabilityScopeError> {
486 self.create_child_inheriting(local_id)
487 }
488
489 pub fn subtask(
490 &self,
491 local_id: impl Into<String>,
492 ceiling: CapabilityCeiling,
493 ) -> Result<CapabilityScope<Subtask>, CapabilityScopeError> {
494 self.create_child(local_id, ceiling)
495 }
496
497 pub fn subtask_inheriting(
498 &self,
499 local_id: impl Into<String>,
500 ) -> Result<CapabilityScope<Subtask>, CapabilityScopeError> {
501 self.create_child_inheriting(local_id)
502 }
503}
504
505impl CapabilityScopeHandle<Turn> {
506 pub fn subtask(
507 &self,
508 local_id: impl Into<String>,
509 ceiling: CapabilityCeiling,
510 ) -> Result<CapabilityScope<Subtask>, CapabilityScopeError> {
511 self.create_child(local_id, ceiling)
512 }
513
514 pub fn subtask_inheriting(
515 &self,
516 local_id: impl Into<String>,
517 ) -> Result<CapabilityScope<Subtask>, CapabilityScopeError> {
518 self.create_child_inheriting(local_id)
519 }
520}
521
522impl CapabilityScopeHandle<Subtask> {
523 pub fn turn(
524 &self,
525 local_id: impl Into<String>,
526 ceiling: CapabilityCeiling,
527 ) -> Result<CapabilityScope<Turn>, CapabilityScopeError> {
528 self.create_child(local_id, ceiling)
529 }
530
531 pub fn turn_inheriting(
532 &self,
533 local_id: impl Into<String>,
534 ) -> Result<CapabilityScope<Turn>, CapabilityScopeError> {
535 self.create_child_inheriting(local_id)
536 }
537
538 pub fn subtask(
539 &self,
540 local_id: impl Into<String>,
541 ceiling: CapabilityCeiling,
542 ) -> Result<CapabilityScope<Subtask>, CapabilityScopeError> {
543 self.create_child(local_id, ceiling)
544 }
545
546 pub fn subtask_inheriting(
547 &self,
548 local_id: impl Into<String>,
549 ) -> Result<CapabilityScope<Subtask>, CapabilityScopeError> {
550 self.create_child_inheriting(local_id)
551 }
552}
553
554impl<K: ScopeKind> CapabilityScopeHandle<K> {
555 pub fn id(&self) -> &CapabilityScopeId {
556 &self.id
557 }
558
559 pub const fn kind(&self) -> CapabilityScopeKind {
560 K::KIND
561 }
562
563 pub fn is_active(&self) -> bool {
564 self.inner
565 .upgrade()
566 .is_some_and(|inner| inner.supervisor.is_open())
567 }
568
569 pub fn cancellation(&self) -> Result<CancellationToken, CapabilityScopeError> {
570 let inner = self.upgrade()?;
571 inner.ensure_active()?;
572 Ok(inner.supervisor.cancellation())
573 }
574
575 pub fn register_effect<E>(&self, effect: E) -> Result<(), CapabilityScopeError>
576 where
577 E: CapabilityEffect,
578 {
579 let inner = self.upgrade()?;
580 inner.ensure_active()?;
581 inner.supervisor.register_effect(Box::new(effect))
582 }
583
584 pub fn spawn_task<F>(
585 &self,
586 name: impl Into<String>,
587 task: F,
588 ) -> Result<SupervisedTaskId, CapabilityScopeError>
589 where
590 F: Future<Output = Result<(), CapabilityEffectError>> + Send + 'static,
591 {
592 let inner = self.upgrade()?;
593 inner.ensure_active()?;
594 inner.supervisor.spawn_task(name, task)
595 }
596
597 pub fn cancel(&self) -> Result<(), CapabilityScopeError> {
598 let inner = self.upgrade()?;
599 inner.ensure_active()?;
600 inner.supervisor.cancel();
601 Ok(())
602 }
603
604 fn create_child<C: ScopeKind>(
605 &self,
606 local_id: impl Into<String>,
607 ceiling: CapabilityCeiling,
608 ) -> Result<CapabilityScope<C>, CapabilityScopeError> {
609 let parent = self.upgrade()?;
610 create_child_scope(
611 &parent,
612 local_id,
613 ceiling,
614 None,
615 parent.use_generation.clone(),
616 )
617 }
618
619 fn create_child_inheriting<C: ScopeKind>(
620 &self,
621 local_id: impl Into<String>,
622 ) -> Result<CapabilityScope<C>, CapabilityScopeError> {
623 let parent = self.upgrade()?;
624 create_child_scope(
625 &parent,
626 local_id,
627 parent.ceiling.clone(),
628 None,
629 parent.use_generation.clone(),
630 )
631 }
632
633 fn upgrade(&self) -> Result<Arc<ScopeInner>, CapabilityScopeError> {
634 self.inner
635 .upgrade()
636 .ok_or_else(|| CapabilityScopeError::ScopeInactive {
637 scope_id: self.id.to_string(),
638 })
639 }
640}
641
642impl<K: ScopeKind> CapabilityScope<K> {
643 pub fn id(&self) -> &CapabilityScopeId {
644 &self.inner.id
645 }
646
647 pub const fn kind(&self) -> CapabilityScopeKind {
648 K::KIND
649 }
650
651 pub fn parent_id(&self) -> Option<&CapabilityScopeId> {
652 self.inner.parent_id.as_ref()
653 }
654
655 pub fn catalog_digest(&self) -> &Sha256Digest {
656 self.inner.set.digest()
657 }
658
659 pub fn ceiling(&self) -> &CapabilityCeiling {
660 &self.inner.ceiling
661 }
662
663 pub fn use_generation(&self) -> Option<&UseCapabilityGeneration> {
664 self.inner.use_generation.as_ref()
665 }
666
667 pub fn is_active(&self) -> bool {
668 self.inner.supervisor.is_open()
669 }
670
671 pub fn cancellation(&self) -> CancellationToken {
672 self.inner.supervisor.cancellation()
673 }
674
675 pub fn handle(&self) -> CapabilityScopeHandle<K> {
676 CapabilityScopeHandle {
677 inner: Arc::downgrade(&self.inner),
678 id: self.inner.id.clone(),
679 _kind: PhantomData,
680 }
681 }
682
683 pub fn lease(&self) -> Result<CapabilityLease<'_, K>, CapabilityScopeError> {
684 self.inner.ensure_active()?;
685 Ok(CapabilityLease::new(&self.inner))
686 }
687
688 pub fn register_effect<E>(&self, effect: E) -> Result<(), CapabilityScopeError>
689 where
690 E: CapabilityEffect,
691 {
692 self.inner.supervisor.register_effect(Box::new(effect))
693 }
694
695 pub fn spawn_task<F>(
696 &self,
697 name: impl Into<String>,
698 task: F,
699 ) -> Result<SupervisedTaskId, CapabilityScopeError>
700 where
701 F: Future<Output = Result<(), CapabilityEffectError>> + Send + 'static,
702 {
703 self.inner.supervisor.spawn_task(name, task)
704 }
705
706 pub(crate) fn task_spawner(&self) -> SupervisedTaskSpawner {
707 self.inner.supervisor.task_spawner()
708 }
709
710 pub fn cancel(&self) {
711 self.inner.supervisor.cancel();
712 }
713
714 pub async fn close(&self) -> Result<ScopeCloseReport, CapabilityScopeError> {
715 self.inner.close().await
716 }
717
718 fn create_child<C: ScopeKind>(
719 &self,
720 local_id: impl Into<String>,
721 ceiling: CapabilityCeiling,
722 generation_lease: Option<Box<dyn RetainedUseGeneration>>,
723 use_generation: Option<UseCapabilityGeneration>,
724 ) -> Result<CapabilityScope<C>, CapabilityScopeError> {
725 create_child_scope(
726 &self.inner,
727 local_id,
728 ceiling,
729 generation_lease,
730 use_generation,
731 )
732 }
733}
734
735impl<K: ScopeKind> Drop for CapabilityScope<K> {
736 fn drop(&mut self) {
737 self.inner.supervisor.cancel();
738 }
739}
740
741fn create_child_scope<C: ScopeKind>(
742 parent: &Arc<ScopeInner>,
743 local_id: impl Into<String>,
744 ceiling: CapabilityCeiling,
745 generation_lease: Option<Box<dyn RetainedUseGeneration>>,
746 use_generation: Option<UseCapabilityGeneration>,
747) -> Result<CapabilityScope<C>, CapabilityScopeError> {
748 parent.ensure_active()?;
749 ceiling.ensure_within(&parent.ceiling)?;
750 let id = CapabilityScopeId::child(&parent.id, C::KIND, local_id)?;
751 let cancellation = parent.supervisor.cancellation().child_token();
752 let supervisor =
753 EffectSupervisor::new(id.to_string(), cancellation, parent.supervisor.policy());
754 if let Some(lease) = generation_lease {
755 supervisor.register_generation_lease(lease)?;
756 }
757 let child = Arc::new(ScopeInner {
758 id,
759 kind: C::KIND,
760 parent_id: Some(parent.id.clone()),
761 set: Arc::clone(&parent.set),
762 ceiling,
763 use_generation,
764 supervisor,
765 parent_registration: Mutex::new(None),
766 });
767 let registration_id = parent.supervisor.register_child(Box::new(ChildScopeOwner {
768 inner: Arc::clone(&child),
769 }))?;
770 child.set_parent_registration(parent.supervisor.downgrade(), registration_id);
771 Ok(CapabilityScope {
772 inner: child,
773 _kind: PhantomData,
774 })
775}
776
777fn validate_scope_local_id(value: &str) -> Result<(), CapabilityScopeError> {
778 if value.is_empty() {
779 return Err(CapabilityScopeError::InvalidScopeId {
780 reason: "it is empty",
781 });
782 }
783 if value.len() > MAX_CAPABILITY_IDENTIFIER_BYTES {
784 return Err(CapabilityScopeError::BoundExceeded {
785 field: "scope_local_id",
786 max: MAX_CAPABILITY_IDENTIFIER_BYTES,
787 });
788 }
789 if !value.bytes().all(|byte| {
790 byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'-' | b'_' | b'.')
791 }) || !value
792 .as_bytes()
793 .first()
794 .is_some_and(u8::is_ascii_alphanumeric)
795 || !value
796 .as_bytes()
797 .last()
798 .is_some_and(u8::is_ascii_alphanumeric)
799 {
800 return Err(CapabilityScopeError::InvalidScopeId {
801 reason: "it contains non-canonical characters or boundaries",
802 });
803 }
804 Ok(())
805}