Skip to main content

oxios_kernel/capability/
resolve.rs

1//! CSpace resolution — determines an agent's initial capability space from
2//! Seed + Config inputs.
3//!
4//! The resolution follows a priority chain:
5//!
6//! 1. **Explicit cspace hint** on the seed → parse and use it.
7//! 2. **Persona role** → map known roles to built-in templates.
8//! 3. **Default** → fall back to the `worker` template.
9//!
10//! # Example
11//!
12//! ```ignore
13//! use oxios_kernel::capability::resolve::resolve_cspace;
14//! use oxios_kernel::types::AgentId;
15//!
16//! let cspace = resolve_cspace(None, Some("operator"), None, AgentId::new_v4());
17//! assert!(cspace.len() > 2);
18//! ```
19
20use crate::types::AgentId;
21
22use super::template::CapabilityTemplate;
23use super::types::CSpace;
24
25/// Known role names that map to built-in capability templates.
26const ROLE_WORKER: &str = "worker";
27const ROLE_STANDARD: &str = "standard";
28const ROLE_OPERATOR: &str = "operator";
29const ROLE_SUPERVISOR: &str = "supervisor";
30
31/// Resolve an agent's initial CSpace from the available context.
32///
33/// # Arguments
34///
35/// * `cspace_hint` — Optional hint string from the Seed. Can be a known
36///   template name ("worker", "standard", "operator", "supervisor") or a
37///   JSON object describing custom capabilities.
38/// * `persona_role` — The role field of the assigned persona, if any.
39/// * `default_template` — Optional override for the fallback template name.
40///   Defaults to "worker" if not specified.
41/// * `agent_id` — The agent that will own the resolved CSpace.
42///
43/// # Priority
44///
45/// 1. `cspace_hint` (if present and non-empty)
46/// 2. `persona_role` (if present and matches a known role)
47/// 3. `default_template` or `"worker"` as fallback
48pub fn resolve_cspace(
49    cspace_hint: Option<&str>,
50    persona_role: Option<&str>,
51    default_template: Option<&str>,
52    agent_id: AgentId,
53) -> CSpace {
54    // 1. Explicit hint from seed takes highest priority.
55    if let Some(hint) = cspace_hint {
56        let trimmed = hint.trim();
57        if !trimmed.is_empty() {
58            return resolve_from_template_name(trimmed, agent_id);
59        }
60    }
61
62    // 2. Persona role maps to a template.
63    if let Some(role) = persona_role {
64        let trimmed = role.trim().to_lowercase();
65        if !trimmed.is_empty() {
66            return resolve_from_template_name(&trimmed, agent_id);
67        }
68    }
69
70    // 3. Default fallback.
71    let fallback = default_template.unwrap_or(ROLE_WORKER);
72    resolve_from_template_name(fallback, agent_id)
73}
74
75/// Map a template name to a built-in CapabilityTemplate.
76///
77/// If the name is a JSON object, we try to parse it as a custom template
78/// (future: parse JSON capabilities). For now, unknown names fall back to
79/// worker.
80fn resolve_from_template_name(name: &str, agent_id: AgentId) -> CSpace {
81    match name {
82        ROLE_WORKER => CapabilityTemplate::worker().build_for(agent_id),
83        ROLE_STANDARD => CapabilityTemplate::standard().build_for(agent_id),
84        ROLE_OPERATOR => CapabilityTemplate::operator().build_for(agent_id),
85        ROLE_SUPERVISOR => CapabilityTemplate::supervisor().build_for(agent_id),
86        _ => {
87            // If it looks like JSON, log a warning and fall back.
88            // Full JSON capability parsing is a future enhancement.
89            if name.starts_with('{') {
90                tracing::warn!(
91                    "JSON cspace_hint not yet supported, falling back to worker: {}",
92                    name
93                );
94            } else {
95                tracing::warn!(
96                    "Unknown capability template '{}', falling back to worker",
97                    name
98                );
99            }
100            CapabilityTemplate::worker().build_for(agent_id)
101        }
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn hint_takes_priority_over_role() {
111        let id = AgentId::new_v4();
112        let cs = resolve_cspace(Some("supervisor"), Some("worker"), None, id);
113        // supervisor has security domain, worker does not
114        use super::super::types::{ResourceRef, Rights};
115        assert!(cs.can(
116            &ResourceRef::KernelDomain {
117                domain: "security".into()
118            },
119            Rights::ALL,
120        ));
121    }
122
123    #[test]
124    fn role_used_when_no_hint() {
125        let id = AgentId::new_v4();
126        let cs = resolve_cspace(None, Some("operator"), None, id);
127        use super::super::types::{ResourceRef, Rights};
128        assert!(cs.can(&ResourceRef::A2a, Rights::EXECUTE));
129    }
130
131    #[test]
132    fn default_is_worker() {
133        let id = AgentId::new_v4();
134        let cs = resolve_cspace(None, None, None, id);
135        use super::super::types::{ResourceRef, Rights};
136        assert!(cs.can(
137            &ResourceRef::Exec {
138                mode: "shell".into()
139            },
140            Rights::EXECUTE
141        ));
142        // Worker should NOT have A2A
143        assert!(!cs.can(&ResourceRef::A2a, Rights::READ));
144    }
145
146    #[test]
147    fn custom_default_template() {
148        let id = AgentId::new_v4();
149        let cs = resolve_cspace(None, None, Some("standard"), id);
150        use super::super::types::{ResourceRef, Rights};
151        assert!(cs.can(
152            &ResourceRef::KernelDomain {
153                domain: "memory".into()
154            },
155            Rights::READ
156        ));
157    }
158
159    #[test]
160    fn empty_hint_falls_through() {
161        let id = AgentId::new_v4();
162        let cs = resolve_cspace(Some(""), Some("operator"), None, id);
163        use super::super::types::{ResourceRef, Rights};
164        assert!(cs.can(&ResourceRef::A2a, Rights::EXECUTE));
165    }
166
167    #[test]
168    fn unknown_name_falls_back_to_worker() {
169        let id = AgentId::new_v4();
170        let cs = resolve_cspace(Some("nonexistent"), None, None, id);
171        use super::super::types::{ResourceRef, Rights};
172        assert!(cs.can(
173            &ResourceRef::Exec {
174                mode: "shell".into()
175            },
176            Rights::EXECUTE
177        ));
178    }
179
180    #[test]
181    fn json_hint_falls_back_gracefully() {
182        let id = AgentId::new_v4();
183        let cs = resolve_cspace(Some(r#"{"custom": true}"#), None, None, id);
184        use super::super::types::ResourceRef;
185        // Falls back to worker
186        assert!(cs.can(
187            &ResourceRef::Exec {
188                mode: "shell".into()
189            },
190            super::super::types::Rights::EXECUTE
191        ));
192    }
193}