kcode_k1_chat_thread_ktools/
lib.rs1#![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}