use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use serde_json::Value;
use crate::error::{Error, Result};
#[async_trait::async_trait]
pub trait Skill: Send + Sync {
fn name(&self) -> &str;
fn description(&self) -> &str;
fn input_schema(&self) -> Value {
serde_json::json!({ "type": "object" })
}
fn required_permissions(&self) -> Vec<String> {
Vec::new()
}
async fn execute(&self, input: Value) -> Result<Value>;
}
#[derive(Debug, Clone)]
pub struct SkillPolicy {
allowed_skills: Option<std::collections::HashSet<String>>,
granted_permissions: std::collections::HashSet<String>,
timeout: Option<std::time::Duration>,
}
impl Default for SkillPolicy {
fn default() -> Self {
Self {
allowed_skills: None,
granted_permissions: std::collections::HashSet::new(),
timeout: Some(std::time::Duration::from_secs(30)),
}
}
}
impl SkillPolicy {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn allow_only<I, S>(mut self, names: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.allowed_skills = Some(names.into_iter().map(Into::into).collect());
self
}
#[must_use]
pub fn grant(mut self, permission: impl Into<String>) -> Self {
self.granted_permissions.insert(permission.into());
self
}
#[must_use]
pub fn with_timeout(mut self, timeout: std::time::Duration) -> Self {
self.timeout = Some(timeout);
self
}
#[must_use]
pub fn without_timeout(mut self) -> Self {
self.timeout = None;
self
}
pub fn timeout(&self) -> Option<std::time::Duration> {
self.timeout
}
pub fn check(&self, skill: &dyn Skill) -> Result<()> {
if let Some(allowed) = &self.allowed_skills {
if !allowed.contains(skill.name()) {
return Err(Error::PermissionDenied(format!(
"skill '{}' is not on the allowlist",
skill.name()
)));
}
}
for permission in skill.required_permissions() {
if !self.granted_permissions.contains(&permission) {
return Err(Error::PermissionDenied(format!(
"skill '{}' requires permission '{permission}' which is not granted",
skill.name()
)));
}
}
Ok(())
}
}
type SkillFuture = Pin<Box<dyn Future<Output = Result<Value>> + Send>>;
type SkillFn = dyn Fn(Value) -> SkillFuture + Send + Sync;
pub struct FnSkill {
name: String,
description: String,
schema: Value,
permissions: Vec<String>,
handler: Arc<SkillFn>,
}
impl FnSkill {
pub fn new<F, Fut>(name: impl Into<String>, description: impl Into<String>, handler: F) -> Self
where
F: Fn(Value) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<Value>> + Send + 'static,
{
Self {
name: name.into(),
description: description.into(),
schema: serde_json::json!({ "type": "object" }),
permissions: Vec::new(),
handler: Arc::new(move |input| Box::pin(handler(input))),
}
}
#[must_use]
pub fn with_schema(mut self, schema: Value) -> Self {
self.schema = schema;
self
}
#[must_use]
pub fn with_permissions<I, S>(mut self, permissions: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.permissions = permissions.into_iter().map(Into::into).collect();
self
}
}
#[async_trait::async_trait]
impl Skill for FnSkill {
fn name(&self) -> &str {
&self.name
}
fn description(&self) -> &str {
&self.description
}
fn input_schema(&self) -> Value {
self.schema.clone()
}
fn required_permissions(&self) -> Vec<String> {
self.permissions.clone()
}
async fn execute(&self, input: Value) -> Result<Value> {
(self.handler)(input).await
}
}
#[derive(Default, Clone)]
pub struct SkillRegistry {
skills: HashMap<String, Arc<dyn Skill>>,
}
impl SkillRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn register(&mut self, skill: Arc<dyn Skill>) {
self.skills.insert(skill.name().to_string(), skill);
}
pub fn get(&self, name: &str) -> Option<Arc<dyn Skill>> {
self.skills.get(name).cloned()
}
pub async fn execute(&self, name: &str, input: Value) -> Result<Value> {
let skill = self
.get(name)
.ok_or_else(|| Error::SkillNotFound(name.to_string()))?;
skill.execute(input).await
}
pub fn list(&self) -> Vec<Arc<dyn Skill>> {
self.skills.values().cloned().collect()
}
pub fn len(&self) -> usize {
self.skills.len()
}
pub fn is_empty(&self) -> bool {
self.skills.is_empty()
}
}
impl std::fmt::Debug for SkillRegistry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SkillRegistry")
.field("skills", &self.skills.keys().collect::<Vec<_>>())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[tokio::test]
async fn fn_skill_executes() {
let mut registry = SkillRegistry::new();
registry.register(Arc::new(FnSkill::new(
"double",
"Doubles n",
|input| async move {
let n = input["n"].as_i64().unwrap_or(0);
Ok(json!({ "n": n * 2 }))
},
)));
let out = registry
.execute("double", json!({ "n": 21 }))
.await
.unwrap();
assert_eq!(out["n"], 42);
}
#[tokio::test]
async fn missing_skill_errors() {
let registry = SkillRegistry::new();
let err = registry.execute("nope", json!({})).await.unwrap_err();
assert!(matches!(err, Error::SkillNotFound(_)));
}
}