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::{GetLaunchNodeKtool, 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_GET_LAUNCH_NODE_AUTHORIZATION: &str = "GetLaunchNode authorization is not active";
15const NO_LAUNCH_NODE_KTOOLS: &str = "launch-node Ktools are unavailable";
16const NO_SET_LAUNCH_NODE_AUTHORIZATION: &str = "SetLaunchNode authorization is not active";
17const NO_SOCIAL_AUTHORIZATION: &str = "social authorization is not active";
18const NO_SOCIAL_KTOOLS: &str = "social Ktools are unavailable";
19const POISONED: &str = "chat-thread Ktool dispatcher lock poisoned";
20
21#[derive(Clone, Eq, PartialEq)]
22struct Authorization {
23 context: AccessContext,
24 profile_id: ProfileId,
25 policy: AccessPolicy,
26}
27
28#[derive(Debug, Eq, PartialEq)]
29enum GetLaunchNodeRoute {
30 NotOwned,
31 Unavailable,
32 AuthorizationRequired,
33 Launch,
34}
35
36fn get_launch_node_route(
37 name: &str,
38 configured: bool,
39 authorization_active: bool,
40) -> GetLaunchNodeRoute {
41 if name != "GetLaunchNode" {
42 return GetLaunchNodeRoute::NotOwned;
43 }
44 match (configured, authorization_active) {
45 (false, _) => GetLaunchNodeRoute::Unavailable,
46 (true, false) => GetLaunchNodeRoute::AuthorizationRequired,
47 (true, true) => GetLaunchNodeRoute::Launch,
48 }
49}
50
51#[derive(Debug, Eq, PartialEq)]
52enum SetLaunchNodeRoute {
53 NotOwned,
54 Fallback,
55 AuthorizationRequired,
56 Launch,
57}
58
59fn set_launch_node_route(
60 name: &str,
61 configured: bool,
62 authorization_active: bool,
63) -> SetLaunchNodeRoute {
64 if name != "SetLaunchNode" {
65 return SetLaunchNodeRoute::NotOwned;
66 }
67 match (configured, authorization_active) {
68 (false, _) => SetLaunchNodeRoute::Fallback,
69 (true, false) => SetLaunchNodeRoute::AuthorizationRequired,
70 (true, true) => SetLaunchNodeRoute::Launch,
71 }
72}
73
74pub struct ChatThreadKtools {
75 actions: ChatThreadActions,
76 social: Option<SocialKtools>,
77 get_launch_node: Option<GetLaunchNodeKtool>,
78 set_launch_node: Option<SetLaunchNodeKtool>,
79 authorization: Option<Authorization>,
80}
81
82impl ChatThreadKtools {
83 pub fn new(kmap: Arc<K1AccessKmap>) -> Self {
84 Self {
85 actions: ChatThreadActions::new(kmap),
86 social: None,
87 get_launch_node: None,
88 set_launch_node: None,
89 authorization: None,
90 }
91 }
92
93 pub fn new_with_social(kmap: Arc<K1AccessKmap>, social: SocialKtools) -> Self {
94 Self {
95 actions: ChatThreadActions::new(kmap),
96 social: Some(social),
97 get_launch_node: None,
98 set_launch_node: None,
99 authorization: None,
100 }
101 }
102
103 pub fn new_with_social_and_set_launch_node(
104 kmap: Arc<K1AccessKmap>,
105 social: SocialKtools,
106 set_launch_node: SetLaunchNodeKtool,
107 ) -> Self {
108 let get_launch_node = set_launch_node.get_launch_node();
109 Self {
110 actions: ChatThreadActions::new(kmap),
111 social: Some(social),
112 get_launch_node: Some(get_launch_node),
113 set_launch_node: Some(set_launch_node),
114 authorization: None,
115 }
116 }
117
118 pub fn bind_authorization(
119 &mut self,
120 context: AccessContext,
121 profile_id: ProfileId,
122 policy: AccessPolicy,
123 ) -> Result<(), String> {
124 let candidate = Authorization {
125 context,
126 profile_id,
127 policy,
128 };
129 if self
130 .authorization
131 .as_ref()
132 .is_some_and(|active| active != &candidate)
133 {
134 return Err(DIFFERENT_AUTHORIZATION.to_owned());
135 }
136 self.actions.bind_authorization(
137 candidate.context.clone(),
138 candidate.profile_id,
139 candidate.policy.clone(),
140 )?;
141 self.authorization = Some(candidate);
142 Ok(())
143 }
144
145 pub fn clear_authorization(&mut self) {
146 self.actions.clear_authorization();
147 self.authorization = None;
148 }
149
150 pub fn supports(&self, name: &str) -> bool {
151 matches!(
152 name,
153 "CurrentTime"
154 | "KmapCreateNode"
155 | "KmapOpenNode"
156 | "KmapUpdateNode"
157 | "KmapPenalizeNodes"
158 | "KmapConnectNodes"
159 ) || (self.social.is_some() && matches!(name, "ListContacts" | "ListGroups" | "GetGroup"))
160 || (self.get_launch_node.is_some() && name == "GetLaunchNode")
161 || (self.set_launch_node.is_some() && name == "SetLaunchNode")
162 }
163
164 pub fn launch(&mut self, name: &str, arguments: &str) -> Result<String, String> {
165 match get_launch_node_route(
166 name,
167 self.get_launch_node.is_some(),
168 self.authorization.is_some(),
169 ) {
170 GetLaunchNodeRoute::Unavailable => return Err(NO_LAUNCH_NODE_KTOOLS.to_owned()),
171 GetLaunchNodeRoute::AuthorizationRequired => {
172 return Err(NO_GET_LAUNCH_NODE_AUTHORIZATION.to_owned());
173 }
174 GetLaunchNodeRoute::Launch => {
175 let tool = self
176 .get_launch_node
177 .as_ref()
178 .expect("configured GetLaunchNode route has a tool");
179 let context = self
180 .authorization
181 .as_ref()
182 .map(|authorization| &authorization.context)
183 .expect("authorized GetLaunchNode route has authorization");
184 return tool.launch(context, arguments);
185 }
186 GetLaunchNodeRoute::NotOwned => {}
187 }
188 match set_launch_node_route(
189 name,
190 self.set_launch_node.is_some(),
191 self.authorization.is_some(),
192 ) {
193 SetLaunchNodeRoute::AuthorizationRequired => {
194 return Err(NO_SET_LAUNCH_NODE_AUTHORIZATION.to_owned());
195 }
196 SetLaunchNodeRoute::Launch => {
197 let tool = self
198 .set_launch_node
199 .as_ref()
200 .expect("configured SetLaunchNode route has a tool");
201 let authorization = self
202 .authorization
203 .as_ref()
204 .expect("authorized SetLaunchNode route has authorization");
205 return tool.launch(
206 &authorization.context,
207 authorization.profile_id,
208 authorization.policy.clone(),
209 arguments,
210 );
211 }
212 SetLaunchNodeRoute::NotOwned | SetLaunchNodeRoute::Fallback => {}
213 }
214 if matches!(name, "ListContacts" | "ListGroups" | "GetGroup") {
215 let social = self
216 .social
217 .as_ref()
218 .ok_or_else(|| NO_SOCIAL_KTOOLS.to_owned())?;
219 let context = self
220 .authorization
221 .as_ref()
222 .map(|authorization| &authorization.context)
223 .ok_or_else(|| NO_SOCIAL_AUTHORIZATION.to_owned())?;
224 return social.launch(context, name, arguments);
225 }
226 self.actions.launch(name, arguments)
227 }
228}
229
230#[derive(Clone)]
231pub struct ChatThreadKtoolExecutor(Arc<Mutex<ChatThreadKtools>>);
232
233impl ChatThreadKtoolExecutor {
234 pub fn new(ktools: ChatThreadKtools) -> Self {
235 Self(Arc::new(Mutex::new(ktools)))
236 }
237
238 fn lock(&self) -> Result<MutexGuard<'_, ChatThreadKtools>, String> {
239 self.0.lock().map_err(|_| POISONED.to_owned())
240 }
241
242 pub fn bind_authorization(
243 &self,
244 context: AccessContext,
245 profile_id: ProfileId,
246 policy: AccessPolicy,
247 ) -> Result<(), String> {
248 self.lock()?.bind_authorization(context, profile_id, policy)
249 }
250
251 pub fn clear_authorization(&self) -> Result<(), String> {
252 self.lock()?.clear_authorization();
253 Ok(())
254 }
255
256 pub fn supports(&self, name: &str) -> Result<bool, String> {
257 Ok(self.lock()?.supports(name))
258 }
259
260 pub fn launch(&self, name: &str, arguments: &str) -> Result<String, String> {
261 self.lock()?.launch(name, arguments)
262 }
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268
269 #[test]
270 fn get_launch_node_owns_only_its_exact_name() {
271 for name in [
272 "GetLaunchNode",
273 "getLaunchNode",
274 "GetLaunchNodes",
275 "WebSearch",
276 ] {
277 let expected = if name == "GetLaunchNode" {
278 GetLaunchNodeRoute::Unavailable
279 } else {
280 GetLaunchNodeRoute::NotOwned
281 };
282 assert_eq!(get_launch_node_route(name, false, false), expected);
283 }
284 }
285
286 #[test]
287 fn get_launch_node_configuration_and_authorization_are_independent() {
288 assert_eq!(
289 get_launch_node_route("GetLaunchNode", false, true),
290 GetLaunchNodeRoute::Unavailable
291 );
292 assert_eq!(
293 get_launch_node_route("GetLaunchNode", true, false),
294 GetLaunchNodeRoute::AuthorizationRequired
295 );
296 assert_eq!(
297 get_launch_node_route("GetLaunchNode", true, true),
298 GetLaunchNodeRoute::Launch
299 );
300 }
301
302 #[test]
303 fn set_launch_node_owns_only_its_exact_name() {
304 for name in [
305 "SetLaunchNode",
306 "setLaunchNode",
307 "SetLaunchNodes",
308 "WebSearch",
309 ] {
310 let expected = if name == "SetLaunchNode" {
311 SetLaunchNodeRoute::Fallback
312 } else {
313 SetLaunchNodeRoute::NotOwned
314 };
315 assert_eq!(set_launch_node_route(name, false, false), expected);
316 }
317 }
318
319 #[test]
320 fn set_launch_node_configuration_and_authorization_are_independent() {
321 assert_eq!(
322 set_launch_node_route("SetLaunchNode", false, true),
323 SetLaunchNodeRoute::Fallback
324 );
325 assert_eq!(
326 set_launch_node_route("SetLaunchNode", true, false),
327 SetLaunchNodeRoute::AuthorizationRequired
328 );
329 assert_eq!(
330 set_launch_node_route("SetLaunchNode", true, true),
331 SetLaunchNodeRoute::Launch
332 );
333 }
334
335 #[test]
336 fn concrete_support_is_narrow() {
337 let names = [
338 "CurrentTime",
339 "KmapCreateNode",
340 "KmapOpenNode",
341 "KmapUpdateNode",
342 "KmapPenalizeNodes",
343 "KmapConnectNodes",
344 ];
345 assert_eq!(names.len(), 6);
346 for name in [
347 "KtoolDocs",
348 "SendMessage",
349 "WebSearch",
350 "GetLaunchNode",
351 "SetLaunchNode",
352 ] {
353 assert!(!matches!(
354 name,
355 "CurrentTime" | "KmapCreateNode" | "KmapOpenNode"
356 ));
357 }
358 }
359
360 #[test]
361 fn complete_package_stays_below_the_managed_limit() {
362 let files = [
363 include_str!("../Cargo.toml"),
364 include_str!("../Documentation.md"),
365 include_str!("lib.rs"),
366 ];
367 let count = files
368 .iter()
369 .flat_map(|file| file.lines())
370 .filter(|line| !line.trim().is_empty())
371 .count();
372 assert!(count < 500, "complete package has {count} nonblank lines");
373 }
374}