sim-lib-agent 0.1.8

Agent runtime surfaces for SIM.
Documentation
use std::{
    any::Any,
    sync::{
        Arc,
        atomic::{AtomicUsize, Ordering},
    },
};

use super::support::{eval_cx, install_agent_lib, install_test_codec};
use crate::{exec_capability, find_capability, fs_read_capability};
use sim_kernel::{
    Args, CORE_FUNCTION_CLASS_ID, Callable, ClassRef, Cx, Error, Expr, Object, Result, Symbol,
    Value,
};

#[test]
fn read_tool_works_under_default_ceiling() {
    let mut cx = core_tools_cx();
    grant_default_core_ceiling(&mut cx);
    let table = cx
        .factory()
        .table(vec![(
            Symbol::new("alpha"),
            cx.factory().string("one".to_owned()).unwrap(),
        )])
        .unwrap();

    let key = cx.factory().symbol(Symbol::new("alpha")).unwrap();
    let value = call_tool(&mut cx, "read", vec![table, key]).unwrap();

    assert_eq!(
        value.object().as_expr(&mut cx).unwrap(),
        Expr::String("one".to_owned())
    );
}

#[test]
fn exec_tool_refused_without_grant() {
    let mut cx = core_tools_cx();
    grant_default_core_ceiling(&mut cx);
    let calls = register_fake_exec(&mut cx);

    let args = exec_args(&mut cx);
    let denied = call_tool(&mut cx, "exec", args).unwrap_err();

    assert!(matches!(
        denied,
        Error::CapabilityDenied { capability } if capability == exec_capability()
    ));
    assert_eq!(calls.load(Ordering::SeqCst), 0);
}

#[test]
fn exec_tool_works_when_granted() {
    let mut cx = core_tools_cx();
    grant_default_core_ceiling(&mut cx);
    cx.grant(exec_capability());
    let calls = register_fake_exec(&mut cx);

    let args = exec_args(&mut cx);
    let value = call_tool(&mut cx, "exec", args).unwrap();

    assert_eq!(calls.load(Ordering::SeqCst), 1);
    assert_eq!(
        value.object().as_expr(&mut cx).unwrap(),
        Expr::String("exec-ok".to_owned())
    );
}

#[test]
fn model_descriptor_lists_tools_with_capabilities() {
    let mut cx = core_tools_cx();
    let tools = cx
        .call_function(
            &Symbol::qualified("agent", "tools"),
            Args::new(vec![
                cx.factory().symbol(Symbol::new(":category")).unwrap(),
                cx.factory().symbol(Symbol::new("io")).unwrap(),
            ]),
        )
        .unwrap();
    let Expr::List(items) = tools.object().as_expr(&mut cx).unwrap() else {
        panic!("agent/tools should return a list");
    };

    assert!(items.len() >= 7);
    assert_capabilities(&items, "read", &["fs/read"]);
    assert_capabilities(&items, "write", &["fs/write"]);
    assert_capabilities(&items, "list", &["fs/read"]);
    assert_capabilities(&items, "edit", &["fs/read", "edit"]);
    assert_capabilities(&items, "find", &["fs/read", "find"]);
    assert_capabilities(&items, "exec", &["exec"]);
    assert_capabilities(&items, "fetch", &["net/http"]);
}

fn core_tools_cx() -> Cx {
    let mut cx = eval_cx();
    install_test_codec(&mut cx);
    install_agent_lib(&mut cx).unwrap();
    cx
}

fn grant_default_core_ceiling(cx: &mut Cx) {
    cx.grant(fs_read_capability());
    cx.grant(find_capability());
}

fn call_tool(cx: &mut Cx, name: &str, args: Vec<Value>) -> Result<Value> {
    let mut values = vec![cx.factory().symbol(Symbol::new(name)).unwrap()];
    values.extend(args);
    cx.call_function(&Symbol::qualified("agent", "call-tool"), Args::new(values))
}

fn exec_args(cx: &mut Cx) -> Vec<Value> {
    let argv = cx
        .factory()
        .list(vec![cx.factory().string("echo".to_owned()).unwrap()])
        .unwrap();
    let options = cx
        .factory()
        .table(vec![
            (
                Symbol::new("timeout-ms"),
                cx.factory()
                    .number_literal(Symbol::qualified("numbers", "f64"), "1000".to_owned())
                    .unwrap(),
            ),
            (
                Symbol::new("max-output-bytes"),
                cx.factory()
                    .number_literal(Symbol::qualified("numbers", "f64"), "128".to_owned())
                    .unwrap(),
            ),
        ])
        .unwrap();
    vec![argv, options]
}

fn register_fake_exec(cx: &mut Cx) -> Arc<AtomicUsize> {
    let calls = Arc::new(AtomicUsize::new(0));
    let function = cx
        .factory()
        .opaque(Arc::new(FakeExec {
            calls: calls.clone(),
        }))
        .unwrap();
    cx.registry_mut()
        .register_function_value(Symbol::new("exec"), function)
        .unwrap();
    calls
}

fn assert_capabilities(items: &[Expr], name: &str, expected: &[&str]) {
    let map = items
        .iter()
        .find_map(|item| match item {
            Expr::Map(entries)
                if entries.iter().any(|(key, value)| {
                    *key == Expr::Symbol(Symbol::new("name"))
                        && *value == Expr::Symbol(Symbol::new(name))
                }) =>
            {
                Some(entries)
            }
            _ => None,
        })
        .unwrap_or_else(|| panic!("missing {name} tool descriptor"));
    let Expr::List(capabilities) = map
        .iter()
        .find_map(|(key, value)| {
            (*key == Expr::Symbol(Symbol::new("capabilities"))).then_some(value)
        })
        .unwrap()
    else {
        panic!("{name} capabilities should be a list");
    };
    let actual = capabilities
        .iter()
        .map(|expr| match expr {
            Expr::String(text) => text.as_str(),
            _ => panic!("{name} capability should be a string"),
        })
        .collect::<Vec<_>>();
    assert_eq!(actual, expected);
}

struct FakeExec {
    calls: Arc<AtomicUsize>,
}

impl Object for FakeExec {
    fn display(&self, _cx: &mut Cx) -> Result<String> {
        Ok("#<function exec>".to_owned())
    }

    fn as_any(&self) -> &dyn Any {
        self
    }
}

impl sim_kernel::ObjectCompat for FakeExec {
    fn class(&self, cx: &mut Cx) -> Result<ClassRef> {
        cx.factory().class_stub(
            CORE_FUNCTION_CLASS_ID,
            Symbol::qualified("core", "Function"),
        )
    }

    fn as_callable(&self) -> Option<&dyn Callable> {
        Some(self)
    }
}

impl Callable for FakeExec {
    fn call(&self, cx: &mut Cx, _args: Args) -> Result<Value> {
        self.calls.fetch_add(1, Ordering::SeqCst);
        cx.factory().string("exec-ok".to_owned())
    }
}