Skip to main content

kcode_k1_chat_thread_ktools/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../Documentation.md")]
3
4use std::sync::{Arc, Mutex, MutexGuard};
5
6use kcode_k1_access_kmap::K1AccessKmap;
7use kcode_k1_chat_thread_actions::ChatThreadActions;
8pub use kcode_k1_chat_thread_actions::{AccessContext, AccessPolicy, ProfileId};
9pub use kcode_k1_ktool_set_launch_node::SetLaunchNodeKtool;
10use kcode_k1_ktool_social::SocialKtools;
11
12const DIFFERENT_AUTHORIZATION: &str =
13    "different Kmap access context, profile ID, or policy is already active";
14const NO_SET_LAUNCH_NODE_AUTHORIZATION: &str = "SetLaunchNode authorization is not active";
15const NO_SOCIAL_AUTHORIZATION: &str = "social authorization is not active";
16const NO_SOCIAL_KTOOLS: &str = "social Ktools are unavailable";
17const POISONED: &str = "chat-thread Ktool dispatcher lock poisoned";
18
19#[derive(Clone, Eq, PartialEq)]
20struct Authorization {
21    context: AccessContext,
22    profile_id: ProfileId,
23    policy: AccessPolicy,
24}
25
26#[derive(Debug, Eq, PartialEq)]
27enum SetLaunchNodeRoute {
28    NotOwned,
29    Fallback,
30    AuthorizationRequired,
31    Launch,
32}
33
34fn set_launch_node_route(
35    name: &str,
36    configured: bool,
37    authorization_active: bool,
38) -> SetLaunchNodeRoute {
39    if name != "SetLaunchNode" {
40        return SetLaunchNodeRoute::NotOwned;
41    }
42    match (configured, authorization_active) {
43        (false, _) => SetLaunchNodeRoute::Fallback,
44        (true, false) => SetLaunchNodeRoute::AuthorizationRequired,
45        (true, true) => SetLaunchNodeRoute::Launch,
46    }
47}
48
49pub struct ChatThreadKtools {
50    actions: ChatThreadActions,
51    social: Option<SocialKtools>,
52    set_launch_node: Option<SetLaunchNodeKtool>,
53    authorization: Option<Authorization>,
54}
55
56impl ChatThreadKtools {
57    pub fn new(kmap: Arc<K1AccessKmap>) -> Self {
58        Self {
59            actions: ChatThreadActions::new(kmap),
60            social: None,
61            set_launch_node: None,
62            authorization: None,
63        }
64    }
65
66    pub fn new_with_social(kmap: Arc<K1AccessKmap>, social: SocialKtools) -> Self {
67        Self {
68            actions: ChatThreadActions::new(kmap),
69            social: Some(social),
70            set_launch_node: None,
71            authorization: None,
72        }
73    }
74
75    pub fn new_with_social_and_set_launch_node(
76        kmap: Arc<K1AccessKmap>,
77        social: SocialKtools,
78        set_launch_node: SetLaunchNodeKtool,
79    ) -> Self {
80        Self {
81            actions: ChatThreadActions::new(kmap),
82            social: Some(social),
83            set_launch_node: Some(set_launch_node),
84            authorization: None,
85        }
86    }
87
88    pub fn bind_authorization(
89        &mut self,
90        context: AccessContext,
91        profile_id: ProfileId,
92        policy: AccessPolicy,
93    ) -> Result<(), String> {
94        let candidate = Authorization {
95            context,
96            profile_id,
97            policy,
98        };
99        if self
100            .authorization
101            .as_ref()
102            .is_some_and(|active| active != &candidate)
103        {
104            return Err(DIFFERENT_AUTHORIZATION.to_owned());
105        }
106        self.actions.bind_authorization(
107            candidate.context.clone(),
108            candidate.profile_id,
109            candidate.policy.clone(),
110        )?;
111        self.authorization = Some(candidate);
112        Ok(())
113    }
114
115    pub fn clear_authorization(&mut self) {
116        self.actions.clear_authorization();
117        self.authorization = None;
118    }
119
120    pub fn supports(&self, name: &str) -> bool {
121        matches!(
122            name,
123            "CurrentTime"
124                | "KmapCreateNode"
125                | "KmapOpenNode"
126                | "KmapUpdateNode"
127                | "KmapPenalizeNodes"
128                | "KmapConnectNodes"
129        ) || (self.social.is_some() && matches!(name, "ListContacts" | "ListGroups" | "GetGroup"))
130            || (self.set_launch_node.is_some() && name == "SetLaunchNode")
131    }
132
133    pub fn launch(&mut self, name: &str, arguments: &str) -> Result<String, String> {
134        match set_launch_node_route(
135            name,
136            self.set_launch_node.is_some(),
137            self.authorization.is_some(),
138        ) {
139            SetLaunchNodeRoute::AuthorizationRequired => {
140                return Err(NO_SET_LAUNCH_NODE_AUTHORIZATION.to_owned());
141            }
142            SetLaunchNodeRoute::Launch => {
143                let tool = self
144                    .set_launch_node
145                    .as_ref()
146                    .expect("configured SetLaunchNode route has a tool");
147                let authorization = self
148                    .authorization
149                    .as_ref()
150                    .expect("authorized SetLaunchNode route has authorization");
151                return tool.launch(
152                    &authorization.context,
153                    authorization.profile_id,
154                    authorization.policy.clone(),
155                    arguments,
156                );
157            }
158            SetLaunchNodeRoute::NotOwned | SetLaunchNodeRoute::Fallback => {}
159        }
160        if matches!(name, "ListContacts" | "ListGroups" | "GetGroup") {
161            let social = self
162                .social
163                .as_ref()
164                .ok_or_else(|| NO_SOCIAL_KTOOLS.to_owned())?;
165            let context = self
166                .authorization
167                .as_ref()
168                .map(|authorization| &authorization.context)
169                .ok_or_else(|| NO_SOCIAL_AUTHORIZATION.to_owned())?;
170            return social.launch(context, name, arguments);
171        }
172        self.actions.launch(name, arguments)
173    }
174}
175
176#[derive(Clone)]
177pub struct ChatThreadKtoolExecutor(Arc<Mutex<ChatThreadKtools>>);
178
179impl ChatThreadKtoolExecutor {
180    pub fn new(ktools: ChatThreadKtools) -> Self {
181        Self(Arc::new(Mutex::new(ktools)))
182    }
183
184    fn lock(&self) -> Result<MutexGuard<'_, ChatThreadKtools>, String> {
185        self.0.lock().map_err(|_| POISONED.to_owned())
186    }
187
188    pub fn bind_authorization(
189        &self,
190        context: AccessContext,
191        profile_id: ProfileId,
192        policy: AccessPolicy,
193    ) -> Result<(), String> {
194        self.lock()?.bind_authorization(context, profile_id, policy)
195    }
196
197    pub fn clear_authorization(&self) -> Result<(), String> {
198        self.lock()?.clear_authorization();
199        Ok(())
200    }
201
202    pub fn supports(&self, name: &str) -> Result<bool, String> {
203        Ok(self.lock()?.supports(name))
204    }
205
206    pub fn launch(&self, name: &str, arguments: &str) -> Result<String, String> {
207        self.lock()?.launch(name, arguments)
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214
215    #[test]
216    fn set_launch_node_owns_only_its_exact_name() {
217        for name in [
218            "SetLaunchNode",
219            "setLaunchNode",
220            "SetLaunchNodes",
221            "WebSearch",
222        ] {
223            let expected = if name == "SetLaunchNode" {
224                SetLaunchNodeRoute::Fallback
225            } else {
226                SetLaunchNodeRoute::NotOwned
227            };
228            assert_eq!(set_launch_node_route(name, false, false), expected);
229        }
230    }
231
232    #[test]
233    fn set_launch_node_configuration_and_authorization_are_independent() {
234        assert_eq!(
235            set_launch_node_route("SetLaunchNode", false, true),
236            SetLaunchNodeRoute::Fallback
237        );
238        assert_eq!(
239            set_launch_node_route("SetLaunchNode", true, false),
240            SetLaunchNodeRoute::AuthorizationRequired
241        );
242        assert_eq!(
243            set_launch_node_route("SetLaunchNode", true, true),
244            SetLaunchNodeRoute::Launch
245        );
246    }
247
248    #[test]
249    fn concrete_support_is_narrow() {
250        let names = [
251            "CurrentTime",
252            "KmapCreateNode",
253            "KmapOpenNode",
254            "KmapUpdateNode",
255            "KmapPenalizeNodes",
256            "KmapConnectNodes",
257        ];
258        assert_eq!(names.len(), 6);
259        for name in ["KtoolDocs", "SendMessage", "WebSearch", "SetLaunchNode"] {
260            assert!(!matches!(
261                name,
262                "CurrentTime" | "KmapCreateNode" | "KmapOpenNode"
263            ));
264        }
265    }
266
267    #[test]
268    fn complete_package_stays_below_the_managed_limit() {
269        let files = [
270            include_str!("../Cargo.toml"),
271            include_str!("../Documentation.md"),
272            include_str!("lib.rs"),
273        ];
274        let count = files
275            .iter()
276            .flat_map(|file| file.lines())
277            .filter(|line| !line.trim().is_empty())
278            .count();
279        assert!(count < 500, "complete package has {count} nonblank lines");
280    }
281}