1use serde::{Deserialize, Serialize};
15
16use crate::Principal;
17
18#[derive(Deserialize)]
22#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
23enum ScopePrincipal {
24 Reserved { module_id: String },
25 Direct {},
26 Unverified {},
27}
28
29impl From<ScopePrincipal> for Principal {
30 fn from(value: ScopePrincipal) -> Self {
31 match value {
32 ScopePrincipal::Reserved { module_id } => Self::Reserved { module_id },
33 ScopePrincipal::Direct {} => Self::Direct,
34 ScopePrincipal::Unverified {} => Self::Unverified,
35 }
36 }
37}
38
39fn deserialize_scope_principal<'de, D: serde::Deserializer<'de>>(
40 deserializer: D,
41) -> Result<Principal, D::Error> {
42 ScopePrincipal::deserialize(deserializer).map(Into::into)
43}
44
45fn deserialize_scope_principals<'de, D: serde::Deserializer<'de>>(
46 deserializer: D,
47) -> Result<Vec<Principal>, D::Error> {
48 Vec::<ScopePrincipal>::deserialize(deserializer)
49 .map(|principals| principals.into_iter().map(Into::into).collect())
50}
51
52pub fn validate_bind_proof_sha256(hash: &str) -> Result<(), &'static str> {
54 if hash.len() == 64
55 && hash
56 .bytes()
57 .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
58 {
59 Ok(())
60 } else {
61 Err("bind_proof_sha256 must be exactly 64 lowercase hexadecimal characters")
62 }
63}
64
65fn deserialize_bind_proof_sha256<'de, D: serde::Deserializer<'de>>(
66 deserializer: D,
67) -> Result<Option<String>, D::Error> {
68 let hash = Option::<String>::deserialize(deserializer)?;
69 if let Some(hash) = &hash {
70 validate_bind_proof_sha256(hash).map_err(serde::de::Error::custom)?;
71 }
72 Ok(hash)
73}
74
75pub const CAP_SCOPES_V1: &str = "scopes/v1";
79
80pub const CAP_ROUTE_ROLE_VERSIONS_V1: &str = "route-role-versions/v1";
85
86pub const SCOPE_SYNC_OP: &str = "scope.sync";
88pub const SCOPE_APPLY_OP: &str = "scope.apply";
91pub const SCOPE_DESCRIBE_OP: &str = "scope.describe";
93
94pub const MAX_LIVE_SCOPES_PER_OWNER: usize = 10_000;
96pub const MAX_SCOPE_ATTRIBUTE_BYTES: usize = 4 * 1024;
99pub const MAX_SCOPE_TOMBSTONES_PER_OWNER: usize = 1_000;
102pub const MAX_CARRIER_TARGETS: usize = 16;
104pub const MAX_SCOPE_EXPIRY_AHEAD_MS: u64 = 86_400_000;
107
108#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
111#[serde(rename_all = "snake_case")]
112pub enum ScopeKind {
113 Head,
114 Worker,
115 Ephemeral,
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
121#[serde(deny_unknown_fields)]
122#[non_exhaustive]
123pub struct ScopeParent {
124 #[serde(deserialize_with = "deserialize_scope_principal")]
125 pub owner: Principal,
126 #[serde(rename = "ref")]
127 pub scope_ref: String,
128 pub scope_epoch: u64,
129}
130
131impl ScopeParent {
132 pub fn new(owner: Principal, scope_ref: impl Into<String>, scope_epoch: u64) -> Self {
133 Self {
134 owner,
135 scope_ref: scope_ref.into(),
136 scope_epoch,
137 }
138 }
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
148#[serde(deny_unknown_fields)]
149#[non_exhaustive]
150pub struct ScopeCarrier {
151 #[serde(deserialize_with = "deserialize_scope_principal")]
152 pub principal: Principal,
153 #[serde(default, skip_serializing_if = "Option::is_none")]
154 pub targets: Option<Vec<String>>,
155}
156
157impl ScopeCarrier {
158 pub fn new(principal: Principal) -> Self {
159 Self {
160 principal,
161 targets: None,
162 }
163 }
164
165 #[must_use]
166 pub fn with_targets(mut self, targets: Option<Vec<String>>) -> Self {
167 self.targets = targets;
168 self
169 }
170}
171
172pub const FLOW_SCOPES_CAPABILITY: &str = "flow-scopes/v1";
178
179pub const AGENT_RUN_SCOPES_CAPABILITY: &str = "agent-run-scopes/v1";
185
186#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
191#[serde(deny_unknown_fields)]
192#[non_exhaustive]
193pub struct ScopeAttributes {
194 #[serde(default, skip_serializing_if = "Option::is_none")]
196 pub agent_id: Option<String>,
197 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
199 pub delegates: bool,
200 #[serde(default, skip_serializing_if = "Option::is_none")]
216 pub flow_id: Option<String>,
217 #[serde(default, skip_serializing_if = "Option::is_none")]
227 pub run_id: Option<String>,
228}
229
230impl ScopeAttributes {
231 pub fn new() -> Self {
232 Self::default()
233 }
234
235 #[must_use]
236 pub fn with_agent_id(mut self, agent_id: Option<String>) -> Self {
237 self.agent_id = agent_id;
238 self
239 }
240
241 #[must_use]
242 pub fn with_delegates(mut self, delegates: bool) -> Self {
243 self.delegates = delegates;
244 self
245 }
246
247 #[must_use]
248 pub fn with_flow_id(mut self, flow_id: Option<String>) -> Self {
249 self.flow_id = flow_id;
250 self
251 }
252
253 #[must_use]
254 pub fn with_run_id(mut self, run_id: Option<String>) -> Self {
255 self.run_id = run_id;
256 self
257 }
258
259 pub fn is_empty(&self) -> bool {
260 self.agent_id.is_none()
261 && !self.delegates
262 && self.flow_id.is_none()
263 && self.run_id.is_none()
264 }
265}
266
267pub fn validate_flow_id(flow_id: &str) -> Result<(), crate::tool_call::OpaqueFieldError> {
271 crate::tool_call::validate_opaque_field("flow_id", flow_id)
272}
273
274pub fn validate_run_id(run_id: &str) -> Result<(), crate::tool_call::OpaqueFieldError> {
277 crate::tool_call::validate_opaque_field("run_id", run_id)
278}
279
280#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
282#[serde(deny_unknown_fields)]
283#[non_exhaustive]
284pub struct ScopeRecord {
285 #[serde(rename = "ref")]
286 pub scope_ref: String,
287 pub scope_epoch: u64,
291 pub kind: ScopeKind,
292 #[serde(
295 default,
296 skip_serializing_if = "Option::is_none",
297 deserialize_with = "deserialize_bind_proof_sha256"
298 )]
299 pub bind_proof_sha256: Option<String>,
300 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
305 pub replace_only_if_idle: bool,
306 #[serde(default, skip_serializing_if = "Option::is_none")]
310 pub expires_at_ms: Option<u64>,
311 #[serde(default, skip_serializing_if = "Option::is_none")]
312 pub parent: Option<ScopeParent>,
313 #[serde(
316 default,
317 skip_serializing_if = "Vec::is_empty",
318 deserialize_with = "deserialize_scope_principals"
319 )]
320 pub child_owners: Vec<Principal>,
321 #[serde(default, skip_serializing_if = "Vec::is_empty")]
322 pub carriers: Vec<ScopeCarrier>,
323 #[serde(default, skip_serializing_if = "ScopeAttributes::is_empty")]
324 pub attributes: ScopeAttributes,
325}
326
327impl ScopeRecord {
328 pub fn new(scope_ref: impl Into<String>, scope_epoch: u64, kind: ScopeKind) -> Self {
329 Self {
330 scope_ref: scope_ref.into(),
331 scope_epoch,
332 kind,
333 bind_proof_sha256: None,
334 replace_only_if_idle: false,
335 expires_at_ms: None,
336 parent: None,
337 child_owners: Vec::new(),
338 carriers: Vec::new(),
339 attributes: ScopeAttributes::default(),
340 }
341 }
342
343 #[must_use]
344 pub fn with_bind_proof_sha256(mut self, hash: Option<String>) -> Self {
345 self.bind_proof_sha256 = hash;
346 self
347 }
348
349 #[must_use]
350 pub fn with_replace_only_if_idle(mut self, replace_only_if_idle: bool) -> Self {
351 self.replace_only_if_idle = replace_only_if_idle;
352 self
353 }
354
355 #[must_use]
356 pub fn with_expires_at_ms(mut self, expires_at_ms: Option<u64>) -> Self {
357 self.expires_at_ms = expires_at_ms;
358 self
359 }
360
361 #[must_use]
362 pub fn with_parent(mut self, parent: Option<ScopeParent>) -> Self {
363 self.parent = parent;
364 self
365 }
366
367 #[must_use]
368 pub fn with_child_owners(mut self, child_owners: Vec<Principal>) -> Self {
369 self.child_owners = child_owners;
370 self
371 }
372
373 #[must_use]
374 pub fn with_carriers(mut self, carriers: Vec<ScopeCarrier>) -> Self {
375 self.carriers = carriers;
376 self
377 }
378
379 #[must_use]
380 pub fn with_attributes(mut self, attributes: ScopeAttributes) -> Self {
381 self.attributes = attributes;
382 self
383 }
384}
385
386#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
388#[serde(deny_unknown_fields)]
389#[non_exhaustive]
390pub struct ScopeEnd {
391 #[serde(rename = "ref")]
392 pub scope_ref: String,
393 pub scope_epoch: u64,
394}
395
396impl ScopeEnd {
397 pub fn new(scope_ref: impl Into<String>, scope_epoch: u64) -> Self {
398 Self {
399 scope_ref: scope_ref.into(),
400 scope_epoch,
401 }
402 }
403}
404
405#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
407#[serde(rename_all = "snake_case")]
408pub enum ScopeEndOutcome {
409 Ended,
411 NotLive,
414}
415
416#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
418#[serde(deny_unknown_fields)]
419#[non_exhaustive]
420pub struct ScopeEndResult {
421 #[serde(rename = "ref")]
422 pub scope_ref: String,
423 pub scope_epoch: u64,
424 pub outcome: ScopeEndOutcome,
425}
426
427impl ScopeEndResult {
428 pub fn new(scope_ref: impl Into<String>, scope_epoch: u64, outcome: ScopeEndOutcome) -> Self {
429 Self {
430 scope_ref: scope_ref.into(),
431 scope_epoch,
432 outcome,
433 }
434 }
435}
436
437#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
443#[serde(deny_unknown_fields)]
444#[non_exhaustive]
445pub struct ScopeSelector {
446 #[serde(deserialize_with = "deserialize_scope_principal")]
447 pub owner: Principal,
448 #[serde(rename = "ref")]
449 pub scope_ref: String,
450 #[serde(default, skip_serializing_if = "Option::is_none")]
451 pub scope_epoch: Option<u64>,
452 #[serde(default, skip_serializing_if = "Option::is_none")]
455 pub bind_proof: Option<String>,
456 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
459 pub hold_presence: bool,
460}
461
462impl ScopeSelector {
463 pub fn new(owner: Principal, scope_ref: impl Into<String>, scope_epoch: Option<u64>) -> Self {
464 Self {
465 owner,
466 scope_ref: scope_ref.into(),
467 scope_epoch,
468 bind_proof: None,
469 hold_presence: false,
470 }
471 }
472
473 #[must_use]
474 pub fn with_bind_proof(mut self, proof: Option<String>) -> Self {
475 self.bind_proof = proof;
476 self
477 }
478
479 #[must_use]
480 pub fn with_hold_presence(mut self, hold_presence: bool) -> Self {
481 self.hold_presence = hold_presence;
482 self
483 }
484}
485
486impl std::fmt::Debug for ScopeSelector {
487 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
488 f.debug_struct("ScopeSelector")
489 .field("owner", &self.owner)
490 .field("scope_ref", &self.scope_ref)
491 .field("scope_epoch", &self.scope_epoch)
492 .field(
493 "bind_proof",
494 &self.bind_proof.as_ref().map(|_| "<redacted>"),
495 )
496 .finish()
497 }
498}
499
500impl std::fmt::Display for ScopeSelector {
501 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
502 std::fmt::Debug::fmt(self, f)
503 }
504}
505
506#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
508#[serde(rename_all = "snake_case")]
509pub enum ParentState {
510 Linked,
512 Pending,
515 Ended,
518}
519
520#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
522#[serde(rename_all = "snake_case")]
523pub enum ScopeRecordOutcome {
524 Created,
526 Replaced,
528 Updated,
530 Unchanged,
533 Refused,
536}
537
538#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
540pub struct ScopeRecordResult {
541 #[serde(rename = "ref")]
542 pub scope_ref: String,
543 pub scope_epoch: u64,
546 pub outcome: ScopeRecordOutcome,
547 #[serde(default, skip_serializing_if = "Option::is_none")]
549 pub code: Option<String>,
550 #[serde(default, skip_serializing_if = "Option::is_none")]
551 pub message: Option<String>,
552 #[serde(default, skip_serializing_if = "Option::is_none")]
555 pub version: Option<u64>,
556 #[serde(default, skip_serializing_if = "Option::is_none")]
558 pub parent_state: Option<ParentState>,
559}
560
561#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
565pub struct ScopeEnded {
566 #[serde(rename = "ref")]
567 pub scope_ref: String,
568 pub scope_epoch: u64,
569}
570
571#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
573#[serde(rename_all = "snake_case")]
574pub enum ScopeStatus {
575 Live,
576 Ended,
578 NotLive,
584}
585
586#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
589#[non_exhaustive]
590pub struct ScopeStamp {
591 pub owner: Principal,
592 #[serde(rename = "ref")]
593 pub scope_ref: String,
594 pub scope_epoch: u64,
595 pub kind: ScopeKind,
596 #[serde(default, skip_serializing_if = "Option::is_none")]
597 pub parent: Option<ScopeParent>,
598 #[serde(default, skip_serializing_if = "Option::is_none")]
599 pub parent_state: Option<ParentState>,
600 #[serde(default, skip_serializing_if = "ScopeAttributes::is_empty")]
601 pub attributes: ScopeAttributes,
602 pub owner_authorized: bool,
605 #[serde(default, skip_serializing_if = "Option::is_none")]
610 pub expires_at_ms: Option<u64>,
611}
612
613impl ScopeStamp {
614 pub fn new(
616 owner: Principal,
617 scope_ref: impl Into<String>,
618 scope_epoch: u64,
619 kind: ScopeKind,
620 ) -> Self {
621 Self {
622 owner,
623 scope_ref: scope_ref.into(),
624 scope_epoch,
625 kind,
626 parent: None,
627 parent_state: None,
628 attributes: ScopeAttributes::new(),
629 owner_authorized: false,
630 expires_at_ms: None,
631 }
632 }
633
634 #[must_use]
635 pub fn with_parent(mut self, parent: Option<ScopeParent>) -> Self {
636 self.parent = parent;
637 self
638 }
639
640 #[must_use]
641 pub fn with_parent_state(mut self, parent_state: Option<ParentState>) -> Self {
642 self.parent_state = parent_state;
643 self
644 }
645
646 #[must_use]
647 pub fn with_attributes(mut self, attributes: ScopeAttributes) -> Self {
648 self.attributes = attributes;
649 self
650 }
651
652 #[must_use]
653 pub fn with_owner_authorized(mut self, owner_authorized: bool) -> Self {
654 self.owner_authorized = owner_authorized;
655 self
656 }
657
658 #[must_use]
659 pub fn with_expires_at_ms(mut self, expires_at_ms: Option<u64>) -> Self {
660 self.expires_at_ms = expires_at_ms;
661 self
662 }
663}
664
665#[cfg(test)]
666mod tests {
667 use super::*;
668 use crate::tool_call::OpaqueFieldError;
669
670 #[test]
671 fn flow_id_uses_the_shared_opaque_token_bounds_and_names_its_field() {
672 let field = "flow_id";
673 assert_eq!(validate_flow_id(""), Err(OpaqueFieldError::Empty { field }));
674 assert_eq!(validate_flow_id("f"), Ok(()));
675 assert_eq!(validate_flow_id(&"f".repeat(256)), Ok(()));
676 assert_eq!(
677 validate_flow_id(&"f".repeat(257)),
678 Err(OpaqueFieldError::TooLong { field, length: 257 })
679 );
680 assert_eq!(validate_flow_id("!~Flow:7/step"), Ok(()));
681 for bad in ["f é", "f\t", "fé", "f\u{7f}"] {
682 let error = validate_flow_id(bad).unwrap_err();
683 assert_eq!(
684 error,
685 OpaqueFieldError::InvalidCharacter { field, index: 1 }
686 );
687 assert_eq!(error.field(), "flow_id");
688 }
689 }
690
691 #[test]
692 fn flow_only_attributes_round_trip_and_absence_keeps_the_bytes() {
693 let attributes = ScopeAttributes::default();
694 assert!(attributes.is_empty());
695 assert_eq!(serde_json::to_string(&attributes).unwrap(), "{}");
696 assert_eq!(
697 serde_json::from_str::<ScopeAttributes>("{}").unwrap(),
698 attributes
699 );
700 let attributes = ScopeAttributes {
701 flow_id: Some("flow:7".to_string()),
702 ..ScopeAttributes::default()
703 };
704 assert!(!attributes.is_empty());
705 let encoded = serde_json::to_string(&attributes).unwrap();
706 assert_eq!(encoded, r#"{"flow_id":"flow:7"}"#);
707 assert_eq!(
708 serde_json::from_str::<ScopeAttributes>(&encoded).unwrap(),
709 attributes
710 );
711 assert!(
712 serde_json::from_str::<ScopeAttributes>(r#"{"flow_id":"flow:7","unknown":true}"#)
713 .is_err()
714 );
715 }
716}