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 const CAP_SCOPES_V1: &str = "scopes/v1";
56
57pub const CAP_ROUTE_ROLE_VERSIONS_V1: &str = "route-role-versions/v1";
62
63pub const SCOPE_SYNC_OP: &str = "scope.sync";
65pub const SCOPE_APPLY_OP: &str = "scope.apply";
68pub const SCOPE_DESCRIBE_OP: &str = "scope.describe";
70
71pub const MAX_LIVE_SCOPES_PER_OWNER: usize = 10_000;
73pub const MAX_SCOPE_ATTRIBUTE_BYTES: usize = 4 * 1024;
76pub const MAX_SCOPE_TOMBSTONES_PER_OWNER: usize = 1_000;
79pub const MAX_CARRIER_TARGETS: usize = 16;
81pub const MAX_SCOPE_EXPIRY_AHEAD_MS: u64 = 86_400_000;
84
85#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
88#[serde(rename_all = "snake_case")]
89pub enum ScopeKind {
90 Head,
91 Worker,
92 Ephemeral,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
98#[serde(deny_unknown_fields)]
99#[non_exhaustive]
100pub struct ScopeParent {
101 #[serde(deserialize_with = "deserialize_scope_principal")]
102 pub owner: Principal,
103 #[serde(rename = "ref")]
104 pub scope_ref: String,
105 pub scope_epoch: u64,
106}
107
108impl ScopeParent {
109 pub fn new(owner: Principal, scope_ref: impl Into<String>, scope_epoch: u64) -> Self {
110 Self {
111 owner,
112 scope_ref: scope_ref.into(),
113 scope_epoch,
114 }
115 }
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
125#[serde(deny_unknown_fields)]
126#[non_exhaustive]
127pub struct ScopeCarrier {
128 #[serde(deserialize_with = "deserialize_scope_principal")]
129 pub principal: Principal,
130 #[serde(default, skip_serializing_if = "Option::is_none")]
131 pub targets: Option<Vec<String>>,
132}
133
134impl ScopeCarrier {
135 pub fn new(principal: Principal) -> Self {
136 Self {
137 principal,
138 targets: None,
139 }
140 }
141
142 #[must_use]
143 pub fn with_targets(mut self, targets: Option<Vec<String>>) -> Self {
144 self.targets = targets;
145 self
146 }
147}
148
149pub const FLOW_SCOPES_CAPABILITY: &str = "flow-scopes/v1";
155
156pub const AGENT_RUN_SCOPES_CAPABILITY: &str = "agent-run-scopes/v1";
162
163#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
168#[serde(deny_unknown_fields)]
169#[non_exhaustive]
170pub struct ScopeAttributes {
171 #[serde(default, skip_serializing_if = "Option::is_none")]
173 pub agent_id: Option<String>,
174 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
176 pub delegates: bool,
177 #[serde(default, skip_serializing_if = "Option::is_none")]
193 pub flow_id: Option<String>,
194 #[serde(default, skip_serializing_if = "Option::is_none")]
204 pub run_id: Option<String>,
205}
206
207impl ScopeAttributes {
208 pub fn new() -> Self {
209 Self::default()
210 }
211
212 #[must_use]
213 pub fn with_agent_id(mut self, agent_id: Option<String>) -> Self {
214 self.agent_id = agent_id;
215 self
216 }
217
218 #[must_use]
219 pub fn with_delegates(mut self, delegates: bool) -> Self {
220 self.delegates = delegates;
221 self
222 }
223
224 #[must_use]
225 pub fn with_flow_id(mut self, flow_id: Option<String>) -> Self {
226 self.flow_id = flow_id;
227 self
228 }
229
230 #[must_use]
231 pub fn with_run_id(mut self, run_id: Option<String>) -> Self {
232 self.run_id = run_id;
233 self
234 }
235
236 pub fn is_empty(&self) -> bool {
237 self.agent_id.is_none()
238 && !self.delegates
239 && self.flow_id.is_none()
240 && self.run_id.is_none()
241 }
242}
243
244pub fn validate_flow_id(flow_id: &str) -> Result<(), crate::tool_call::OpaqueFieldError> {
248 crate::tool_call::validate_opaque_field("flow_id", flow_id)
249}
250
251pub fn validate_run_id(run_id: &str) -> Result<(), crate::tool_call::OpaqueFieldError> {
254 crate::tool_call::validate_opaque_field("run_id", run_id)
255}
256
257#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
259#[serde(deny_unknown_fields)]
260#[non_exhaustive]
261pub struct ScopeRecord {
262 #[serde(rename = "ref")]
263 pub scope_ref: String,
264 pub scope_epoch: u64,
268 pub kind: ScopeKind,
269 #[serde(default, skip_serializing_if = "Option::is_none")]
273 pub expires_at_ms: Option<u64>,
274 #[serde(default, skip_serializing_if = "Option::is_none")]
275 pub parent: Option<ScopeParent>,
276 #[serde(
279 default,
280 skip_serializing_if = "Vec::is_empty",
281 deserialize_with = "deserialize_scope_principals"
282 )]
283 pub child_owners: Vec<Principal>,
284 #[serde(default, skip_serializing_if = "Vec::is_empty")]
285 pub carriers: Vec<ScopeCarrier>,
286 #[serde(default, skip_serializing_if = "ScopeAttributes::is_empty")]
287 pub attributes: ScopeAttributes,
288}
289
290impl ScopeRecord {
291 pub fn new(scope_ref: impl Into<String>, scope_epoch: u64, kind: ScopeKind) -> Self {
292 Self {
293 scope_ref: scope_ref.into(),
294 scope_epoch,
295 kind,
296 expires_at_ms: None,
297 parent: None,
298 child_owners: Vec::new(),
299 carriers: Vec::new(),
300 attributes: ScopeAttributes::default(),
301 }
302 }
303
304 #[must_use]
305 pub fn with_expires_at_ms(mut self, expires_at_ms: Option<u64>) -> Self {
306 self.expires_at_ms = expires_at_ms;
307 self
308 }
309
310 #[must_use]
311 pub fn with_parent(mut self, parent: Option<ScopeParent>) -> Self {
312 self.parent = parent;
313 self
314 }
315
316 #[must_use]
317 pub fn with_child_owners(mut self, child_owners: Vec<Principal>) -> Self {
318 self.child_owners = child_owners;
319 self
320 }
321
322 #[must_use]
323 pub fn with_carriers(mut self, carriers: Vec<ScopeCarrier>) -> Self {
324 self.carriers = carriers;
325 self
326 }
327
328 #[must_use]
329 pub fn with_attributes(mut self, attributes: ScopeAttributes) -> Self {
330 self.attributes = attributes;
331 self
332 }
333}
334
335#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
337#[serde(deny_unknown_fields)]
338#[non_exhaustive]
339pub struct ScopeEnd {
340 #[serde(rename = "ref")]
341 pub scope_ref: String,
342 pub scope_epoch: u64,
343}
344
345impl ScopeEnd {
346 pub fn new(scope_ref: impl Into<String>, scope_epoch: u64) -> Self {
347 Self {
348 scope_ref: scope_ref.into(),
349 scope_epoch,
350 }
351 }
352}
353
354#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
356#[serde(rename_all = "snake_case")]
357pub enum ScopeEndOutcome {
358 Ended,
360 NotLive,
363}
364
365#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
367#[serde(deny_unknown_fields)]
368#[non_exhaustive]
369pub struct ScopeEndResult {
370 #[serde(rename = "ref")]
371 pub scope_ref: String,
372 pub scope_epoch: u64,
373 pub outcome: ScopeEndOutcome,
374}
375
376impl ScopeEndResult {
377 pub fn new(scope_ref: impl Into<String>, scope_epoch: u64, outcome: ScopeEndOutcome) -> Self {
378 Self {
379 scope_ref: scope_ref.into(),
380 scope_epoch,
381 outcome,
382 }
383 }
384}
385
386#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
392#[serde(deny_unknown_fields)]
393pub struct ScopeSelector {
394 #[serde(deserialize_with = "deserialize_scope_principal")]
395 pub owner: Principal,
396 #[serde(rename = "ref")]
397 pub scope_ref: String,
398 #[serde(default, skip_serializing_if = "Option::is_none")]
399 pub scope_epoch: Option<u64>,
400}
401
402#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
404#[serde(rename_all = "snake_case")]
405pub enum ParentState {
406 Linked,
408 Pending,
411 Ended,
414}
415
416#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
418#[serde(rename_all = "snake_case")]
419pub enum ScopeRecordOutcome {
420 Created,
422 Replaced,
424 Updated,
426 Unchanged,
429 Refused,
432}
433
434#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
436pub struct ScopeRecordResult {
437 #[serde(rename = "ref")]
438 pub scope_ref: String,
439 pub scope_epoch: u64,
442 pub outcome: ScopeRecordOutcome,
443 #[serde(default, skip_serializing_if = "Option::is_none")]
445 pub code: Option<String>,
446 #[serde(default, skip_serializing_if = "Option::is_none")]
447 pub message: Option<String>,
448 #[serde(default, skip_serializing_if = "Option::is_none")]
451 pub version: Option<u64>,
452 #[serde(default, skip_serializing_if = "Option::is_none")]
454 pub parent_state: Option<ParentState>,
455}
456
457#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
461pub struct ScopeEnded {
462 #[serde(rename = "ref")]
463 pub scope_ref: String,
464 pub scope_epoch: u64,
465}
466
467#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
469#[serde(rename_all = "snake_case")]
470pub enum ScopeStatus {
471 Live,
472 Ended,
474 NotLive,
480}
481
482#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
485#[non_exhaustive]
486pub struct ScopeStamp {
487 pub owner: Principal,
488 #[serde(rename = "ref")]
489 pub scope_ref: String,
490 pub scope_epoch: u64,
491 pub kind: ScopeKind,
492 #[serde(default, skip_serializing_if = "Option::is_none")]
493 pub parent: Option<ScopeParent>,
494 #[serde(default, skip_serializing_if = "Option::is_none")]
495 pub parent_state: Option<ParentState>,
496 #[serde(default, skip_serializing_if = "ScopeAttributes::is_empty")]
497 pub attributes: ScopeAttributes,
498 pub owner_authorized: bool,
501 #[serde(default, skip_serializing_if = "Option::is_none")]
506 pub expires_at_ms: Option<u64>,
507}
508
509impl ScopeStamp {
510 pub fn new(
512 owner: Principal,
513 scope_ref: impl Into<String>,
514 scope_epoch: u64,
515 kind: ScopeKind,
516 ) -> Self {
517 Self {
518 owner,
519 scope_ref: scope_ref.into(),
520 scope_epoch,
521 kind,
522 parent: None,
523 parent_state: None,
524 attributes: ScopeAttributes::new(),
525 owner_authorized: false,
526 expires_at_ms: None,
527 }
528 }
529
530 #[must_use]
531 pub fn with_parent(mut self, parent: Option<ScopeParent>) -> Self {
532 self.parent = parent;
533 self
534 }
535
536 #[must_use]
537 pub fn with_parent_state(mut self, parent_state: Option<ParentState>) -> Self {
538 self.parent_state = parent_state;
539 self
540 }
541
542 #[must_use]
543 pub fn with_attributes(mut self, attributes: ScopeAttributes) -> Self {
544 self.attributes = attributes;
545 self
546 }
547
548 #[must_use]
549 pub fn with_owner_authorized(mut self, owner_authorized: bool) -> Self {
550 self.owner_authorized = owner_authorized;
551 self
552 }
553
554 #[must_use]
555 pub fn with_expires_at_ms(mut self, expires_at_ms: Option<u64>) -> Self {
556 self.expires_at_ms = expires_at_ms;
557 self
558 }
559}
560
561#[cfg(test)]
562mod tests {
563 use super::*;
564 use crate::tool_call::OpaqueFieldError;
565
566 #[test]
567 fn flow_id_uses_the_shared_opaque_token_bounds_and_names_its_field() {
568 let field = "flow_id";
569 assert_eq!(validate_flow_id(""), Err(OpaqueFieldError::Empty { field }));
570 assert_eq!(validate_flow_id("f"), Ok(()));
571 assert_eq!(validate_flow_id(&"f".repeat(256)), Ok(()));
572 assert_eq!(
573 validate_flow_id(&"f".repeat(257)),
574 Err(OpaqueFieldError::TooLong { field, length: 257 })
575 );
576 assert_eq!(validate_flow_id("!~Flow:7/step"), Ok(()));
577 for bad in ["f é", "f\t", "fé", "f\u{7f}"] {
578 let error = validate_flow_id(bad).unwrap_err();
579 assert_eq!(
580 error,
581 OpaqueFieldError::InvalidCharacter { field, index: 1 }
582 );
583 assert_eq!(error.field(), "flow_id");
584 }
585 }
586
587 #[test]
588 fn flow_only_attributes_round_trip_and_absence_keeps_the_bytes() {
589 let attributes = ScopeAttributes::default();
590 assert!(attributes.is_empty());
591 assert_eq!(serde_json::to_string(&attributes).unwrap(), "{}");
592 assert_eq!(
593 serde_json::from_str::<ScopeAttributes>("{}").unwrap(),
594 attributes
595 );
596 let attributes = ScopeAttributes {
597 flow_id: Some("flow:7".to_string()),
598 ..ScopeAttributes::default()
599 };
600 assert!(!attributes.is_empty());
601 let encoded = serde_json::to_string(&attributes).unwrap();
602 assert_eq!(encoded, r#"{"flow_id":"flow:7"}"#);
603 assert_eq!(
604 serde_json::from_str::<ScopeAttributes>(&encoded).unwrap(),
605 attributes
606 );
607 assert!(
608 serde_json::from_str::<ScopeAttributes>(r#"{"flow_id":"flow:7","unknown":true}"#)
609 .is_err()
610 );
611 }
612}