Skip to main content

zeroclaw_config/
autonomy.rs

1use serde::{Deserialize, Serialize};
2
3/// How much autonomy the agent has.
4///
5/// Variants are ordered from least to most autonomous so that
6/// [`Ord`] / [`PartialOrd`] compare a child's level against a
7/// parent's during SubAgent escalation checks (`child <= parent`).
8#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
9#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
10#[serde(rename_all = "lowercase")]
11pub enum AutonomyLevel {
12    /// Read-only: can observe but not act
13    ReadOnly,
14    /// Supervised: acts but requires approval for risky operations
15    #[default]
16    Supervised,
17    /// Full: autonomous execution within policy bounds
18    Full,
19}
20
21/// Delegation mode for a risk profile.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
23#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
24#[serde(rename_all = "lowercase")]
25pub enum DelegationMode {
26    /// No delegation permitted.
27    #[default]
28    Forbidden,
29    /// Delegation permitted to the agents named in the allow-list.
30    Allow,
31}
32
33impl crate::config::HasPropKind for DelegationMode {
34    const PROP_KIND: crate::config::PropKind = crate::config::PropKind::Enum;
35}
36
37/// Whether a risk profile may delegate work to other agents.
38///
39/// `Forbidden` (the default) means a profile cannot delegate at all; `Allow`
40/// permits delegation. The set of reachable targets is *not* an explicit
41/// allow-list — delegation is gated on the caller and target sharing a risk
42/// profile, so the shared profile determines who is reachable.
43///
44/// Wire format: `{ mode = "forbidden" }` or `{ mode = "allow" }`. The struct
45/// shape lets the prop layer expose `mode` as an editable enum leaf.
46#[derive(
47    Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default, zeroclaw_macros::Configurable,
48)]
49#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
50pub struct DelegationPolicy {
51    #[serde(default)]
52    pub mode: DelegationMode,
53}
54
55impl DelegationPolicy {
56    /// Whether this profile may delegate. The set of reachable targets is
57    /// determined by shared risk profile at the call site — this only gates
58    /// whether delegation is permitted at all.
59    pub fn permits(&self) -> bool {
60        matches!(self.mode, DelegationMode::Allow)
61    }
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    #[test]
69    fn delegation_default_is_forbidden() {
70        assert_eq!(DelegationPolicy::default().mode, DelegationMode::Forbidden);
71        assert!(!DelegationPolicy::default().permits());
72    }
73
74    #[test]
75    fn delegation_allow_permits() {
76        let p = DelegationPolicy {
77            mode: DelegationMode::Allow,
78        };
79        assert!(p.permits());
80        assert!(!DelegationPolicy::default().permits());
81    }
82
83    #[test]
84    fn delegation_wire_format() {
85        // Forbidden serializes to `{ mode = "forbidden" }`.
86        let forbidden = toml::to_string(&DelegationPolicy::default()).unwrap();
87        assert!(forbidden.contains("mode = \"forbidden\""), "{forbidden}");
88
89        // Allow round-trips `{ mode = "allow" }`.
90        let allow = DelegationPolicy {
91            mode: DelegationMode::Allow,
92        };
93        let s = toml::to_string(&allow).unwrap();
94        assert!(s.contains("mode = \"allow\""), "{s}");
95        let back: DelegationPolicy = toml::from_str(&s).unwrap();
96        assert_eq!(back, allow);
97    }
98}
99
100#[cfg(test)]
101mod prop_exposure_tests {
102    use crate::schema::RiskProfileConfig;
103    use crate::traits::PropKind;
104
105    #[test]
106    fn delegation_policy_exposes_mode_enum_leaf() {
107        let p = RiskProfileConfig::default();
108        let mode = p
109            .prop_fields()
110            .into_iter()
111            .find(|f| f.name.ends_with("delegation_policy.mode"))
112            .expect("delegation_policy.mode leaf missing");
113        assert_eq!(mode.kind, PropKind::Enum);
114    }
115}
116
117#[cfg(all(test, feature = "schema-export"))]
118mod enum_variant_tests {
119    use super::DelegationMode;
120    use crate::schema::RiskProfileConfig;
121
122    #[test]
123    fn delegation_mode_variants_surface() {
124        let v = crate::helpers::enum_variants::<DelegationMode>();
125        assert!(v.contains("forbidden"), "{v}");
126        assert!(v.contains("allow"), "{v}");
127    }
128
129    #[test]
130    fn delegation_mode_field_carries_variants() {
131        let p = RiskProfileConfig::default();
132        let mode = p
133            .prop_fields()
134            .into_iter()
135            .find(|f| f.name.ends_with("delegation_policy.mode"))
136            .expect("mode leaf missing");
137        let variants = mode.enum_variants.map(|f| f()).unwrap_or_default();
138        assert!(
139            !variants.is_empty(),
140            "enum_variants empty — UI would render as text"
141        );
142        assert!(variants.iter().any(|v| v == "forbidden"));
143        assert!(variants.iter().any(|v| v == "allow"));
144    }
145}