#![forbid(unsafe_code)]
#![doc = include_str!("../Documentation.md")]
use std::sync::{Arc, Mutex, MutexGuard};
use kcode_k1_access_kmap::K1AccessKmap;
use kcode_k1_chat_thread_actions::ChatThreadActions;
pub use kcode_k1_chat_thread_actions::{AccessContext, AccessPolicy, ProfileId};
pub use kcode_k1_ktool_set_launch_node::{GetLaunchNodeKtool, SetLaunchNodeKtool};
use kcode_k1_ktool_social::SocialKtools;
const DIFFERENT_AUTHORIZATION: &str =
"different Kmap access context, profile ID, or policy is already active";
const NO_GET_LAUNCH_NODE_AUTHORIZATION: &str = "GetLaunchNode authorization is not active";
const NO_LAUNCH_NODE_KTOOLS: &str = "launch-node Ktools are unavailable";
const NO_SET_LAUNCH_NODE_AUTHORIZATION: &str = "SetLaunchNode authorization is not active";
const NO_SOCIAL_AUTHORIZATION: &str = "social authorization is not active";
const NO_SOCIAL_KTOOLS: &str = "social Ktools are unavailable";
const POISONED: &str = "chat-thread Ktool dispatcher lock poisoned";
#[derive(Clone, Eq, PartialEq)]
struct Authorization {
context: AccessContext,
profile_id: ProfileId,
policy: AccessPolicy,
}
#[derive(Debug, Eq, PartialEq)]
enum GetLaunchNodeRoute {
NotOwned,
Unavailable,
AuthorizationRequired,
Launch,
}
fn get_launch_node_route(
name: &str,
configured: bool,
authorization_active: bool,
) -> GetLaunchNodeRoute {
if name != "GetLaunchNode" {
return GetLaunchNodeRoute::NotOwned;
}
match (configured, authorization_active) {
(false, _) => GetLaunchNodeRoute::Unavailable,
(true, false) => GetLaunchNodeRoute::AuthorizationRequired,
(true, true) => GetLaunchNodeRoute::Launch,
}
}
#[derive(Debug, Eq, PartialEq)]
enum SetLaunchNodeRoute {
NotOwned,
Fallback,
AuthorizationRequired,
Launch,
}
fn set_launch_node_route(
name: &str,
configured: bool,
authorization_active: bool,
) -> SetLaunchNodeRoute {
if name != "SetLaunchNode" {
return SetLaunchNodeRoute::NotOwned;
}
match (configured, authorization_active) {
(false, _) => SetLaunchNodeRoute::Fallback,
(true, false) => SetLaunchNodeRoute::AuthorizationRequired,
(true, true) => SetLaunchNodeRoute::Launch,
}
}
pub struct ChatThreadKtools {
actions: ChatThreadActions,
social: Option<SocialKtools>,
get_launch_node: Option<GetLaunchNodeKtool>,
set_launch_node: Option<SetLaunchNodeKtool>,
authorization: Option<Authorization>,
}
impl ChatThreadKtools {
pub fn new(kmap: Arc<K1AccessKmap>) -> Self {
Self {
actions: ChatThreadActions::new(kmap),
social: None,
get_launch_node: None,
set_launch_node: None,
authorization: None,
}
}
pub fn new_with_social(kmap: Arc<K1AccessKmap>, social: SocialKtools) -> Self {
Self {
actions: ChatThreadActions::new(kmap),
social: Some(social),
get_launch_node: None,
set_launch_node: None,
authorization: None,
}
}
pub fn new_with_social_and_set_launch_node(
kmap: Arc<K1AccessKmap>,
social: SocialKtools,
set_launch_node: SetLaunchNodeKtool,
) -> Self {
let get_launch_node = set_launch_node.get_launch_node();
Self {
actions: ChatThreadActions::new(kmap),
social: Some(social),
get_launch_node: Some(get_launch_node),
set_launch_node: Some(set_launch_node),
authorization: None,
}
}
pub fn bind_authorization(
&mut self,
context: AccessContext,
profile_id: ProfileId,
policy: AccessPolicy,
) -> Result<(), String> {
let candidate = Authorization {
context,
profile_id,
policy,
};
if self
.authorization
.as_ref()
.is_some_and(|active| active != &candidate)
{
return Err(DIFFERENT_AUTHORIZATION.to_owned());
}
self.actions.bind_authorization(
candidate.context.clone(),
candidate.profile_id,
candidate.policy.clone(),
)?;
self.authorization = Some(candidate);
Ok(())
}
pub fn clear_authorization(&mut self) {
self.actions.clear_authorization();
self.authorization = None;
}
pub fn supports(&self, name: &str) -> bool {
matches!(
name,
"CurrentTime"
| "KmapCreateNode"
| "KmapOpenNode"
| "KmapUpdateNode"
| "KmapPenalizeNodes"
| "KmapConnectNodes"
) || (self.social.is_some() && matches!(name, "ListContacts" | "ListGroups" | "GetGroup"))
|| (self.get_launch_node.is_some() && name == "GetLaunchNode")
|| (self.set_launch_node.is_some() && name == "SetLaunchNode")
}
pub fn launch(&mut self, name: &str, arguments: &str) -> Result<String, String> {
match get_launch_node_route(
name,
self.get_launch_node.is_some(),
self.authorization.is_some(),
) {
GetLaunchNodeRoute::Unavailable => return Err(NO_LAUNCH_NODE_KTOOLS.to_owned()),
GetLaunchNodeRoute::AuthorizationRequired => {
return Err(NO_GET_LAUNCH_NODE_AUTHORIZATION.to_owned());
}
GetLaunchNodeRoute::Launch => {
let tool = self
.get_launch_node
.as_ref()
.expect("configured GetLaunchNode route has a tool");
let context = self
.authorization
.as_ref()
.map(|authorization| &authorization.context)
.expect("authorized GetLaunchNode route has authorization");
return tool.launch(context, arguments);
}
GetLaunchNodeRoute::NotOwned => {}
}
match set_launch_node_route(
name,
self.set_launch_node.is_some(),
self.authorization.is_some(),
) {
SetLaunchNodeRoute::AuthorizationRequired => {
return Err(NO_SET_LAUNCH_NODE_AUTHORIZATION.to_owned());
}
SetLaunchNodeRoute::Launch => {
let tool = self
.set_launch_node
.as_ref()
.expect("configured SetLaunchNode route has a tool");
let authorization = self
.authorization
.as_ref()
.expect("authorized SetLaunchNode route has authorization");
return tool.launch(
&authorization.context,
authorization.profile_id,
authorization.policy.clone(),
arguments,
);
}
SetLaunchNodeRoute::NotOwned | SetLaunchNodeRoute::Fallback => {}
}
if matches!(name, "ListContacts" | "ListGroups" | "GetGroup") {
let social = self
.social
.as_ref()
.ok_or_else(|| NO_SOCIAL_KTOOLS.to_owned())?;
let context = self
.authorization
.as_ref()
.map(|authorization| &authorization.context)
.ok_or_else(|| NO_SOCIAL_AUTHORIZATION.to_owned())?;
return social.launch(context, name, arguments);
}
self.actions.launch(name, arguments)
}
}
#[derive(Clone)]
pub struct ChatThreadKtoolExecutor(Arc<Mutex<ChatThreadKtools>>);
impl ChatThreadKtoolExecutor {
pub fn new(ktools: ChatThreadKtools) -> Self {
Self(Arc::new(Mutex::new(ktools)))
}
fn lock(&self) -> Result<MutexGuard<'_, ChatThreadKtools>, String> {
self.0.lock().map_err(|_| POISONED.to_owned())
}
pub fn bind_authorization(
&self,
context: AccessContext,
profile_id: ProfileId,
policy: AccessPolicy,
) -> Result<(), String> {
self.lock()?.bind_authorization(context, profile_id, policy)
}
pub fn clear_authorization(&self) -> Result<(), String> {
self.lock()?.clear_authorization();
Ok(())
}
pub fn supports(&self, name: &str) -> Result<bool, String> {
Ok(self.lock()?.supports(name))
}
pub fn launch(&self, name: &str, arguments: &str) -> Result<String, String> {
self.lock()?.launch(name, arguments)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn get_launch_node_owns_only_its_exact_name() {
for name in [
"GetLaunchNode",
"getLaunchNode",
"GetLaunchNodes",
"WebSearch",
] {
let expected = if name == "GetLaunchNode" {
GetLaunchNodeRoute::Unavailable
} else {
GetLaunchNodeRoute::NotOwned
};
assert_eq!(get_launch_node_route(name, false, false), expected);
}
}
#[test]
fn get_launch_node_configuration_and_authorization_are_independent() {
assert_eq!(
get_launch_node_route("GetLaunchNode", false, true),
GetLaunchNodeRoute::Unavailable
);
assert_eq!(
get_launch_node_route("GetLaunchNode", true, false),
GetLaunchNodeRoute::AuthorizationRequired
);
assert_eq!(
get_launch_node_route("GetLaunchNode", true, true),
GetLaunchNodeRoute::Launch
);
}
#[test]
fn set_launch_node_owns_only_its_exact_name() {
for name in [
"SetLaunchNode",
"setLaunchNode",
"SetLaunchNodes",
"WebSearch",
] {
let expected = if name == "SetLaunchNode" {
SetLaunchNodeRoute::Fallback
} else {
SetLaunchNodeRoute::NotOwned
};
assert_eq!(set_launch_node_route(name, false, false), expected);
}
}
#[test]
fn set_launch_node_configuration_and_authorization_are_independent() {
assert_eq!(
set_launch_node_route("SetLaunchNode", false, true),
SetLaunchNodeRoute::Fallback
);
assert_eq!(
set_launch_node_route("SetLaunchNode", true, false),
SetLaunchNodeRoute::AuthorizationRequired
);
assert_eq!(
set_launch_node_route("SetLaunchNode", true, true),
SetLaunchNodeRoute::Launch
);
}
#[test]
fn concrete_support_is_narrow() {
let names = [
"CurrentTime",
"KmapCreateNode",
"KmapOpenNode",
"KmapUpdateNode",
"KmapPenalizeNodes",
"KmapConnectNodes",
];
assert_eq!(names.len(), 6);
for name in [
"KtoolDocs",
"SendMessage",
"WebSearch",
"GetLaunchNode",
"SetLaunchNode",
] {
assert!(!matches!(
name,
"CurrentTime" | "KmapCreateNode" | "KmapOpenNode"
));
}
}
#[test]
fn complete_package_stays_below_the_managed_limit() {
let files = [
include_str!("../Cargo.toml"),
include_str!("../Documentation.md"),
include_str!("lib.rs"),
];
let count = files
.iter()
.flat_map(|file| file.lines())
.filter(|line| !line.trim().is_empty())
.count();
assert!(count < 500, "complete package has {count} nonblank lines");
}
}