1use chio_core::capability::scope::MonetaryAmount;
9use serde::{Deserialize, Serialize};
10
11pub const SKILL_GRANT_SCHEMA: &str = "chio.skill-grant.v1";
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct SkillGrant {
20 pub schema: String,
22
23 pub skill_id: String,
25
26 pub skill_version: String,
28
29 pub authorized_steps: Vec<String>,
32
33 #[serde(default, skip_serializing_if = "Option::is_none")]
35 pub max_executions: Option<u32>,
36
37 #[serde(default, skip_serializing_if = "Option::is_none")]
39 pub budget_envelope: Option<MonetaryAmount>,
40
41 #[serde(default, skip_serializing_if = "Option::is_none")]
43 pub max_duration_secs: Option<u64>,
44
45 #[serde(default = "default_strict_ordering")]
48 pub strict_ordering: bool,
49}
50
51fn default_strict_ordering() -> bool {
52 true
53}
54
55impl SkillGrant {
56 pub fn new(skill_id: String, skill_version: String, authorized_steps: Vec<String>) -> Self {
58 Self {
59 schema: SKILL_GRANT_SCHEMA.to_string(),
60 skill_id,
61 skill_version,
62 authorized_steps,
63 max_executions: None,
64 budget_envelope: None,
65 max_duration_secs: None,
66 strict_ordering: true,
67 }
68 }
69
70 #[must_use]
72 pub fn authorizes_step(&self, server_id: &str, tool_name: &str) -> bool {
73 let key = format!("{server_id}:{tool_name}");
74 self.authorized_steps.contains(&key)
75 }
76
77 #[must_use]
83 pub fn is_step_in_order(&self, step_index: usize, completed_steps: usize) -> bool {
84 if !self.strict_ordering {
85 return true;
86 }
87 step_index == completed_steps
88 }
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 #[test]
96 fn skill_grant_roundtrip() {
97 let grant = SkillGrant::new(
98 "search-and-summarize".to_string(),
99 "1.0.0".to_string(),
100 vec![
101 "search-server:search".to_string(),
102 "llm-server:summarize".to_string(),
103 ],
104 );
105
106 let json = serde_json::to_string(&grant).unwrap();
107 let deserialized: SkillGrant = serde_json::from_str(&json).unwrap();
108 assert_eq!(deserialized.skill_id, "search-and-summarize");
109 assert_eq!(deserialized.authorized_steps.len(), 2);
110 assert!(deserialized.strict_ordering);
111 }
112
113 #[test]
114 fn authorizes_step() {
115 let grant = SkillGrant::new(
116 "s1".to_string(),
117 "1.0".to_string(),
118 vec!["srv:tool_a".to_string(), "srv:tool_b".to_string()],
119 );
120 assert!(grant.authorizes_step("srv", "tool_a"));
121 assert!(grant.authorizes_step("srv", "tool_b"));
122 assert!(!grant.authorizes_step("srv", "tool_c"));
123 }
124
125 #[test]
126 fn strict_ordering() {
127 let grant = SkillGrant::new(
128 "s1".to_string(),
129 "1.0".to_string(),
130 vec!["a:t1".to_string(), "a:t2".to_string()],
131 );
132 assert!(grant.is_step_in_order(0, 0)); assert!(grant.is_step_in_order(1, 1)); assert!(!grant.is_step_in_order(1, 0)); }
136
137 #[test]
138 fn relaxed_ordering() {
139 let mut grant = SkillGrant::new(
140 "s1".to_string(),
141 "1.0".to_string(),
142 vec!["a:t1".to_string(), "a:t2".to_string()],
143 );
144 grant.strict_ordering = false;
145 assert!(grant.is_step_in_order(1, 0)); }
147}