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";
153
154pub const AGENT_RUN_SCOPES_CAPABILITY: &str = "agent-run-scopes/v1";
158
159#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
164#[serde(deny_unknown_fields)]
165#[non_exhaustive]
166pub struct ScopeAttributes {
167 #[serde(default, skip_serializing_if = "Option::is_none")]
169 pub agent_id: Option<String>,
170 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
172 pub delegates: bool,
173 #[serde(default, skip_serializing_if = "Option::is_none")]
188 pub flow_id: Option<String>,
189 #[serde(default, skip_serializing_if = "Option::is_none")]
197 pub run_id: Option<String>,
198}
199
200impl ScopeAttributes {
201 pub fn new() -> Self {
202 Self::default()
203 }
204
205 #[must_use]
206 pub fn with_agent_id(mut self, agent_id: Option<String>) -> Self {
207 self.agent_id = agent_id;
208 self
209 }
210
211 #[must_use]
212 pub fn with_delegates(mut self, delegates: bool) -> Self {
213 self.delegates = delegates;
214 self
215 }
216
217 #[must_use]
218 pub fn with_flow_id(mut self, flow_id: Option<String>) -> Self {
219 self.flow_id = flow_id;
220 self
221 }
222
223 #[must_use]
224 pub fn with_run_id(mut self, run_id: Option<String>) -> Self {
225 self.run_id = run_id;
226 self
227 }
228
229 pub fn is_empty(&self) -> bool {
230 self.agent_id.is_none()
231 && !self.delegates
232 && self.flow_id.is_none()
233 && self.run_id.is_none()
234 }
235}
236
237pub fn validate_flow_id(flow_id: &str) -> Result<(), crate::tool_call::OpaqueFieldError> {
241 crate::tool_call::validate_opaque_field("flow_id", flow_id)
242}
243
244pub fn validate_run_id(run_id: &str) -> Result<(), crate::tool_call::OpaqueFieldError> {
247 crate::tool_call::validate_opaque_field("run_id", run_id)
248}
249
250#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
252#[serde(deny_unknown_fields)]
253#[non_exhaustive]
254pub struct ScopeRecord {
255 #[serde(rename = "ref")]
256 pub scope_ref: String,
257 pub scope_epoch: u64,
261 pub kind: ScopeKind,
262 #[serde(default, skip_serializing_if = "Option::is_none")]
266 pub expires_at_ms: Option<u64>,
267 #[serde(default, skip_serializing_if = "Option::is_none")]
268 pub parent: Option<ScopeParent>,
269 #[serde(
272 default,
273 skip_serializing_if = "Vec::is_empty",
274 deserialize_with = "deserialize_scope_principals"
275 )]
276 pub child_owners: Vec<Principal>,
277 #[serde(default, skip_serializing_if = "Vec::is_empty")]
278 pub carriers: Vec<ScopeCarrier>,
279 #[serde(default, skip_serializing_if = "ScopeAttributes::is_empty")]
280 pub attributes: ScopeAttributes,
281}
282
283impl ScopeRecord {
284 pub fn new(scope_ref: impl Into<String>, scope_epoch: u64, kind: ScopeKind) -> Self {
285 Self {
286 scope_ref: scope_ref.into(),
287 scope_epoch,
288 kind,
289 expires_at_ms: None,
290 parent: None,
291 child_owners: Vec::new(),
292 carriers: Vec::new(),
293 attributes: ScopeAttributes::default(),
294 }
295 }
296
297 #[must_use]
298 pub fn with_expires_at_ms(mut self, expires_at_ms: Option<u64>) -> Self {
299 self.expires_at_ms = expires_at_ms;
300 self
301 }
302
303 #[must_use]
304 pub fn with_parent(mut self, parent: Option<ScopeParent>) -> Self {
305 self.parent = parent;
306 self
307 }
308
309 #[must_use]
310 pub fn with_child_owners(mut self, child_owners: Vec<Principal>) -> Self {
311 self.child_owners = child_owners;
312 self
313 }
314
315 #[must_use]
316 pub fn with_carriers(mut self, carriers: Vec<ScopeCarrier>) -> Self {
317 self.carriers = carriers;
318 self
319 }
320
321 #[must_use]
322 pub fn with_attributes(mut self, attributes: ScopeAttributes) -> Self {
323 self.attributes = attributes;
324 self
325 }
326}
327
328#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
330#[serde(deny_unknown_fields)]
331#[non_exhaustive]
332pub struct ScopeEnd {
333 #[serde(rename = "ref")]
334 pub scope_ref: String,
335 pub scope_epoch: u64,
336}
337
338impl ScopeEnd {
339 pub fn new(scope_ref: impl Into<String>, scope_epoch: u64) -> Self {
340 Self {
341 scope_ref: scope_ref.into(),
342 scope_epoch,
343 }
344 }
345}
346
347#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
349#[serde(rename_all = "snake_case")]
350pub enum ScopeEndOutcome {
351 Ended,
353 NotLive,
356}
357
358#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
360#[serde(deny_unknown_fields)]
361#[non_exhaustive]
362pub struct ScopeEndResult {
363 #[serde(rename = "ref")]
364 pub scope_ref: String,
365 pub scope_epoch: u64,
366 pub outcome: ScopeEndOutcome,
367}
368
369impl ScopeEndResult {
370 pub fn new(scope_ref: impl Into<String>, scope_epoch: u64, outcome: ScopeEndOutcome) -> Self {
371 Self {
372 scope_ref: scope_ref.into(),
373 scope_epoch,
374 outcome,
375 }
376 }
377}
378
379#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
385#[serde(deny_unknown_fields)]
386pub struct ScopeSelector {
387 #[serde(deserialize_with = "deserialize_scope_principal")]
388 pub owner: Principal,
389 #[serde(rename = "ref")]
390 pub scope_ref: String,
391 #[serde(default, skip_serializing_if = "Option::is_none")]
392 pub scope_epoch: Option<u64>,
393}
394
395#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
397#[serde(rename_all = "snake_case")]
398pub enum ParentState {
399 Linked,
401 Pending,
404 Ended,
407}
408
409#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
411#[serde(rename_all = "snake_case")]
412pub enum ScopeRecordOutcome {
413 Created,
415 Replaced,
417 Updated,
419 Unchanged,
422 Refused,
425}
426
427#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
429pub struct ScopeRecordResult {
430 #[serde(rename = "ref")]
431 pub scope_ref: String,
432 pub scope_epoch: u64,
435 pub outcome: ScopeRecordOutcome,
436 #[serde(default, skip_serializing_if = "Option::is_none")]
438 pub code: Option<String>,
439 #[serde(default, skip_serializing_if = "Option::is_none")]
440 pub message: Option<String>,
441 #[serde(default, skip_serializing_if = "Option::is_none")]
444 pub version: Option<u64>,
445 #[serde(default, skip_serializing_if = "Option::is_none")]
447 pub parent_state: Option<ParentState>,
448}
449
450#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
454pub struct ScopeEnded {
455 #[serde(rename = "ref")]
456 pub scope_ref: String,
457 pub scope_epoch: u64,
458}
459
460#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
462#[serde(rename_all = "snake_case")]
463pub enum ScopeStatus {
464 Live,
465 Ended,
467 NotLive,
473}
474
475#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
477pub struct ScopeStamp {
478 pub owner: Principal,
479 #[serde(rename = "ref")]
480 pub scope_ref: String,
481 pub scope_epoch: u64,
482 pub kind: ScopeKind,
483 #[serde(default, skip_serializing_if = "Option::is_none")]
484 pub parent: Option<ScopeParent>,
485 #[serde(default, skip_serializing_if = "Option::is_none")]
486 pub parent_state: Option<ParentState>,
487 #[serde(default, skip_serializing_if = "ScopeAttributes::is_empty")]
488 pub attributes: ScopeAttributes,
489 pub owner_authorized: bool,
492}
493
494#[cfg(test)]
495mod tests {
496 use super::*;
497 use crate::tool_call::OpaqueFieldError;
498
499 #[test]
500 fn flow_id_uses_the_shared_opaque_token_bounds_and_names_its_field() {
501 let field = "flow_id";
502 assert_eq!(validate_flow_id(""), Err(OpaqueFieldError::Empty { field }));
503 assert_eq!(validate_flow_id("f"), Ok(()));
504 assert_eq!(validate_flow_id(&"f".repeat(256)), Ok(()));
505 assert_eq!(
506 validate_flow_id(&"f".repeat(257)),
507 Err(OpaqueFieldError::TooLong { field, length: 257 })
508 );
509 assert_eq!(validate_flow_id("!~Flow:7/step"), Ok(()));
510 for bad in ["f é", "f\t", "fé", "f\u{7f}"] {
511 let error = validate_flow_id(bad).unwrap_err();
512 assert_eq!(
513 error,
514 OpaqueFieldError::InvalidCharacter { field, index: 1 }
515 );
516 assert_eq!(error.field(), "flow_id");
517 }
518 }
519
520 #[test]
521 fn flow_only_attributes_round_trip_and_absence_keeps_the_bytes() {
522 let attributes = ScopeAttributes::default();
523 assert!(attributes.is_empty());
524 assert_eq!(serde_json::to_string(&attributes).unwrap(), "{}");
525 assert_eq!(
526 serde_json::from_str::<ScopeAttributes>("{}").unwrap(),
527 attributes
528 );
529 let attributes = ScopeAttributes {
530 flow_id: Some("flow:7".to_string()),
531 ..ScopeAttributes::default()
532 };
533 assert!(!attributes.is_empty());
534 let encoded = serde_json::to_string(&attributes).unwrap();
535 assert_eq!(encoded, r#"{"flow_id":"flow:7"}"#);
536 assert_eq!(
537 serde_json::from_str::<ScopeAttributes>(&encoded).unwrap(),
538 attributes
539 );
540 assert!(
541 serde_json::from_str::<ScopeAttributes>(r#"{"flow_id":"flow:7","unknown":true}"#)
542 .is_err()
543 );
544 }
545}