Skip to main content

agent_workspace_contract/environment/
owners.rs

1use std::collections::BTreeSet;
2
3use serde::{Deserialize, Serialize};
4
5/// Resource owner identity. At least one owner is required on every Environment
6/// resource; callers authenticate with runtime, agent, or user credentials.
7#[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
69/// Normalize owners: non-empty, deduplicated, stable order.
70pub 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
83/// True when `candidate` is a subset of `parent` (every element of candidate in parent).
84pub 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
89/// True when caller and resource owners share at least one identity.
90pub 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}