agent_workspace_contract/environment/
owners.rs1use std::collections::BTreeSet;
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
8#[serde(rename_all = "snake_case")]
9pub enum OwnerKind {
10 Runtime,
11 Agent,
12 User,
13}
14
15#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
16#[serde(deny_unknown_fields, rename_all = "camelCase")]
17pub struct OwnerRef {
18 pub kind: OwnerKind,
19 pub id: String,
20}
21
22impl OwnerRef {
23 pub fn runtime(id: impl Into<String>) -> Self {
24 Self {
25 kind: OwnerKind::Runtime,
26 id: id.into(),
27 }
28 }
29
30 pub fn agent(id: impl Into<String>) -> Self {
31 Self {
32 kind: OwnerKind::Agent,
33 id: id.into(),
34 }
35 }
36
37 pub fn user(id: impl Into<String>) -> Self {
38 Self {
39 kind: OwnerKind::User,
40 id: id.into(),
41 }
42 }
43
44 pub fn from_principal(principal: &crate::PrincipalRef) -> Self {
45 match principal.kind {
46 crate::PrincipalKind::User => Self::user(&principal.id),
47 crate::PrincipalKind::Agent => Self::agent(&principal.id),
48 }
49 }
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
53#[serde(deny_unknown_fields, rename_all = "camelCase")]
54pub struct PatchOwnersRequest {
55 pub expected_version: u64,
56 #[serde(default)]
57 pub add: Vec<OwnerRef>,
58 #[serde(default)]
59 pub remove: Vec<OwnerRef>,
60}
61
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub enum OwnerValidationError {
64 Empty,
65 Duplicate,
66 NotSubset,
67}
68
69pub fn normalize_owners(owners: Vec<OwnerRef>) -> Result<Vec<OwnerRef>, OwnerValidationError> {
71 if owners.is_empty() {
72 return Err(OwnerValidationError::Empty);
73 }
74 let mut set = BTreeSet::new();
75 for owner in owners {
76 if !set.insert(owner.clone()) {
77 return Err(OwnerValidationError::Duplicate);
78 }
79 }
80 Ok(set.into_iter().collect())
81}
82
83pub fn owners_subset_of(candidate: &[OwnerRef], parent: &[OwnerRef]) -> bool {
85 let parent_set: BTreeSet<_> = parent.iter().collect();
86 candidate.iter().all(|owner| parent_set.contains(owner))
87}
88
89pub fn owners_intersect(caller: &[OwnerRef], resource: &[OwnerRef]) -> bool {
91 let resource_set: BTreeSet<_> = resource.iter().collect();
92 caller.iter().any(|owner| resource_set.contains(owner))
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 #[test]
100 fn normalize_rejects_empty() {
101 assert!(matches!(
102 normalize_owners(vec![]),
103 Err(OwnerValidationError::Empty)
104 ));
105 }
106
107 #[test]
108 fn normalize_rejects_duplicate() {
109 assert!(matches!(
110 normalize_owners(vec![OwnerRef::runtime("rt-1"), OwnerRef::runtime("rt-1"),]),
111 Err(OwnerValidationError::Duplicate)
112 ));
113 }
114
115 #[test]
116 fn subset_and_intersect() {
117 let parent = vec![OwnerRef::user("u1"), OwnerRef::agent("agt-1")];
118 let child = vec![OwnerRef::agent("agt-1")];
119 assert!(owners_subset_of(&child, &parent));
120 assert!(!owners_subset_of(&parent, &child));
121 assert!(owners_intersect(
122 &[OwnerRef::runtime("rt-1")],
123 &[OwnerRef::runtime("rt-1"), OwnerRef::user("u1")]
124 ));
125 assert!(!owners_intersect(
126 &[OwnerRef::runtime("rt-2")],
127 &[OwnerRef::runtime("rt-1")]
128 ));
129 }
130}