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 = "Option::is_none")]
304 pub expires_at_ms: Option<u64>,
305 #[serde(default, skip_serializing_if = "Option::is_none")]
306 pub parent: Option<ScopeParent>,
307 #[serde(
310 default,
311 skip_serializing_if = "Vec::is_empty",
312 deserialize_with = "deserialize_scope_principals"
313 )]
314 pub child_owners: Vec<Principal>,
315 #[serde(default, skip_serializing_if = "Vec::is_empty")]
316 pub carriers: Vec<ScopeCarrier>,
317 #[serde(default, skip_serializing_if = "ScopeAttributes::is_empty")]
318 pub attributes: ScopeAttributes,
319}
320
321impl ScopeRecord {
322 pub fn new(scope_ref: impl Into<String>, scope_epoch: u64, kind: ScopeKind) -> Self {
323 Self {
324 scope_ref: scope_ref.into(),
325 scope_epoch,
326 kind,
327 bind_proof_sha256: None,
328 expires_at_ms: None,
329 parent: None,
330 child_owners: Vec::new(),
331 carriers: Vec::new(),
332 attributes: ScopeAttributes::default(),
333 }
334 }
335
336 #[must_use]
337 pub fn with_bind_proof_sha256(mut self, hash: Option<String>) -> Self {
338 self.bind_proof_sha256 = hash;
339 self
340 }
341
342 #[must_use]
343 pub fn with_expires_at_ms(mut self, expires_at_ms: Option<u64>) -> Self {
344 self.expires_at_ms = expires_at_ms;
345 self
346 }
347
348 #[must_use]
349 pub fn with_parent(mut self, parent: Option<ScopeParent>) -> Self {
350 self.parent = parent;
351 self
352 }
353
354 #[must_use]
355 pub fn with_child_owners(mut self, child_owners: Vec<Principal>) -> Self {
356 self.child_owners = child_owners;
357 self
358 }
359
360 #[must_use]
361 pub fn with_carriers(mut self, carriers: Vec<ScopeCarrier>) -> Self {
362 self.carriers = carriers;
363 self
364 }
365
366 #[must_use]
367 pub fn with_attributes(mut self, attributes: ScopeAttributes) -> Self {
368 self.attributes = attributes;
369 self
370 }
371}
372
373#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
375#[serde(deny_unknown_fields)]
376#[non_exhaustive]
377pub struct ScopeEnd {
378 #[serde(rename = "ref")]
379 pub scope_ref: String,
380 pub scope_epoch: u64,
381}
382
383impl ScopeEnd {
384 pub fn new(scope_ref: impl Into<String>, scope_epoch: u64) -> Self {
385 Self {
386 scope_ref: scope_ref.into(),
387 scope_epoch,
388 }
389 }
390}
391
392#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
394#[serde(rename_all = "snake_case")]
395pub enum ScopeEndOutcome {
396 Ended,
398 NotLive,
401}
402
403#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
405#[serde(deny_unknown_fields)]
406#[non_exhaustive]
407pub struct ScopeEndResult {
408 #[serde(rename = "ref")]
409 pub scope_ref: String,
410 pub scope_epoch: u64,
411 pub outcome: ScopeEndOutcome,
412}
413
414impl ScopeEndResult {
415 pub fn new(scope_ref: impl Into<String>, scope_epoch: u64, outcome: ScopeEndOutcome) -> Self {
416 Self {
417 scope_ref: scope_ref.into(),
418 scope_epoch,
419 outcome,
420 }
421 }
422}
423
424#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
430#[serde(deny_unknown_fields)]
431#[non_exhaustive]
432pub struct ScopeSelector {
433 #[serde(deserialize_with = "deserialize_scope_principal")]
434 pub owner: Principal,
435 #[serde(rename = "ref")]
436 pub scope_ref: String,
437 #[serde(default, skip_serializing_if = "Option::is_none")]
438 pub scope_epoch: Option<u64>,
439 #[serde(default, skip_serializing_if = "Option::is_none")]
442 pub bind_proof: Option<String>,
443}
444
445impl ScopeSelector {
446 pub fn new(owner: Principal, scope_ref: impl Into<String>, scope_epoch: Option<u64>) -> Self {
447 Self {
448 owner,
449 scope_ref: scope_ref.into(),
450 scope_epoch,
451 bind_proof: None,
452 }
453 }
454
455 #[must_use]
456 pub fn with_bind_proof(mut self, proof: Option<String>) -> Self {
457 self.bind_proof = proof;
458 self
459 }
460}
461
462impl std::fmt::Debug for ScopeSelector {
463 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
464 f.debug_struct("ScopeSelector")
465 .field("owner", &self.owner)
466 .field("scope_ref", &self.scope_ref)
467 .field("scope_epoch", &self.scope_epoch)
468 .field(
469 "bind_proof",
470 &self.bind_proof.as_ref().map(|_| "<redacted>"),
471 )
472 .finish()
473 }
474}
475
476impl std::fmt::Display for ScopeSelector {
477 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
478 std::fmt::Debug::fmt(self, f)
479 }
480}
481
482#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
484#[serde(rename_all = "snake_case")]
485pub enum ParentState {
486 Linked,
488 Pending,
491 Ended,
494}
495
496#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
498#[serde(rename_all = "snake_case")]
499pub enum ScopeRecordOutcome {
500 Created,
502 Replaced,
504 Updated,
506 Unchanged,
509 Refused,
512}
513
514#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
516pub struct ScopeRecordResult {
517 #[serde(rename = "ref")]
518 pub scope_ref: String,
519 pub scope_epoch: u64,
522 pub outcome: ScopeRecordOutcome,
523 #[serde(default, skip_serializing_if = "Option::is_none")]
525 pub code: Option<String>,
526 #[serde(default, skip_serializing_if = "Option::is_none")]
527 pub message: Option<String>,
528 #[serde(default, skip_serializing_if = "Option::is_none")]
531 pub version: Option<u64>,
532 #[serde(default, skip_serializing_if = "Option::is_none")]
534 pub parent_state: Option<ParentState>,
535}
536
537#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
541pub struct ScopeEnded {
542 #[serde(rename = "ref")]
543 pub scope_ref: String,
544 pub scope_epoch: u64,
545}
546
547#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
549#[serde(rename_all = "snake_case")]
550pub enum ScopeStatus {
551 Live,
552 Ended,
554 NotLive,
560}
561
562#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
565#[non_exhaustive]
566pub struct ScopeStamp {
567 pub owner: Principal,
568 #[serde(rename = "ref")]
569 pub scope_ref: String,
570 pub scope_epoch: u64,
571 pub kind: ScopeKind,
572 #[serde(default, skip_serializing_if = "Option::is_none")]
573 pub parent: Option<ScopeParent>,
574 #[serde(default, skip_serializing_if = "Option::is_none")]
575 pub parent_state: Option<ParentState>,
576 #[serde(default, skip_serializing_if = "ScopeAttributes::is_empty")]
577 pub attributes: ScopeAttributes,
578 pub owner_authorized: bool,
581 #[serde(default, skip_serializing_if = "Option::is_none")]
586 pub expires_at_ms: Option<u64>,
587}
588
589impl ScopeStamp {
590 pub fn new(
592 owner: Principal,
593 scope_ref: impl Into<String>,
594 scope_epoch: u64,
595 kind: ScopeKind,
596 ) -> Self {
597 Self {
598 owner,
599 scope_ref: scope_ref.into(),
600 scope_epoch,
601 kind,
602 parent: None,
603 parent_state: None,
604 attributes: ScopeAttributes::new(),
605 owner_authorized: false,
606 expires_at_ms: None,
607 }
608 }
609
610 #[must_use]
611 pub fn with_parent(mut self, parent: Option<ScopeParent>) -> Self {
612 self.parent = parent;
613 self
614 }
615
616 #[must_use]
617 pub fn with_parent_state(mut self, parent_state: Option<ParentState>) -> Self {
618 self.parent_state = parent_state;
619 self
620 }
621
622 #[must_use]
623 pub fn with_attributes(mut self, attributes: ScopeAttributes) -> Self {
624 self.attributes = attributes;
625 self
626 }
627
628 #[must_use]
629 pub fn with_owner_authorized(mut self, owner_authorized: bool) -> Self {
630 self.owner_authorized = owner_authorized;
631 self
632 }
633
634 #[must_use]
635 pub fn with_expires_at_ms(mut self, expires_at_ms: Option<u64>) -> Self {
636 self.expires_at_ms = expires_at_ms;
637 self
638 }
639}
640
641#[cfg(test)]
642mod tests {
643 use super::*;
644 use crate::tool_call::OpaqueFieldError;
645
646 #[test]
647 fn flow_id_uses_the_shared_opaque_token_bounds_and_names_its_field() {
648 let field = "flow_id";
649 assert_eq!(validate_flow_id(""), Err(OpaqueFieldError::Empty { field }));
650 assert_eq!(validate_flow_id("f"), Ok(()));
651 assert_eq!(validate_flow_id(&"f".repeat(256)), Ok(()));
652 assert_eq!(
653 validate_flow_id(&"f".repeat(257)),
654 Err(OpaqueFieldError::TooLong { field, length: 257 })
655 );
656 assert_eq!(validate_flow_id("!~Flow:7/step"), Ok(()));
657 for bad in ["f é", "f\t", "fé", "f\u{7f}"] {
658 let error = validate_flow_id(bad).unwrap_err();
659 assert_eq!(
660 error,
661 OpaqueFieldError::InvalidCharacter { field, index: 1 }
662 );
663 assert_eq!(error.field(), "flow_id");
664 }
665 }
666
667 #[test]
668 fn flow_only_attributes_round_trip_and_absence_keeps_the_bytes() {
669 let attributes = ScopeAttributes::default();
670 assert!(attributes.is_empty());
671 assert_eq!(serde_json::to_string(&attributes).unwrap(), "{}");
672 assert_eq!(
673 serde_json::from_str::<ScopeAttributes>("{}").unwrap(),
674 attributes
675 );
676 let attributes = ScopeAttributes {
677 flow_id: Some("flow:7".to_string()),
678 ..ScopeAttributes::default()
679 };
680 assert!(!attributes.is_empty());
681 let encoded = serde_json::to_string(&attributes).unwrap();
682 assert_eq!(encoded, r#"{"flow_id":"flow:7"}"#);
683 assert_eq!(
684 serde_json::from_str::<ScopeAttributes>(&encoded).unwrap(),
685 attributes
686 );
687 assert!(
688 serde_json::from_str::<ScopeAttributes>(r#"{"flow_id":"flow:7","unknown":true}"#)
689 .is_err()
690 );
691 }
692}