sim-lib-agent 0.1.2

Agent runtime surfaces for SIM.
Documentation
use super::support::{
    as_component, eval_cx, install_agent_lib, install_roundtrip_codecs, request_frame,
};
use sim_kernel::{Expr, Symbol};
use sim_lib_server::{EvalSite, eval_reply_from_frame};
use std::{
    io::{ErrorKind, Read, Write},
    net::TcpListener,
    thread,
    time::Duration,
};

#[test]
fn a6_phase6_local_only_rejects_http_runner_before_encoding() {
    let mut cx = http_privacy_cx();
    let runner = cx
        .call_function(
            &Symbol::qualified("runner", "openai-compatible"),
            sim_kernel::Args::new(vec![
                cx.factory().symbol(Symbol::new(":endpoint")).unwrap(),
                cx.factory()
                    .string("http://127.0.0.1:9/v1".to_owned())
                    .unwrap(),
                cx.factory().symbol(Symbol::new(":api-key-env")).unwrap(),
                cx.factory().string("HOME".to_owned()).unwrap(),
            ]),
        )
        .unwrap();
    let frame = request_frame(
        &mut cx,
        request_expr(
            "must not encode",
            vec![key_expr("privacy", Expr::Symbol(Symbol::new("local-only")))],
        ),
    );

    let error = as_component(&runner).answer(&mut cx, frame).unwrap_err();

    assert!(error.to_string().contains("privacy local-only rejected"));
}

#[test]
fn a6_phase6_no_raw_suppresses_http_raw_capture() {
    let mut cx = http_privacy_cx();
    cx.grant_named("ai-runner-raw-log");

    let Some(listener) = bind_loopback_listener() else {
        return;
    };
    let port = listener.local_addr().unwrap().port();
    let body = r#"{"choices":[{"index":0,"finish_reason":"stop","message":{"role":"assistant","content":"private raw ok"}}],"usage":{"prompt_tokens":1}}"#;
    let server = thread::spawn(move || {
        let (mut stream, _) = listener.accept().unwrap();
        let mut request = [0u8; 2048];
        let _ = stream.read(&mut request).unwrap();
        stream
            .write_all(
                format!(
                    "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n{}",
                    body.len(),
                    body
                )
                .as_bytes(),
            )
            .unwrap();
    });
    let runner = cx
        .call_function(
            &Symbol::qualified("runner", "openai-compatible"),
            sim_kernel::Args::new(vec![
                cx.factory().symbol(Symbol::new(":endpoint")).unwrap(),
                cx.factory()
                    .string(format!("http://127.0.0.1:{port}/v1"))
                    .unwrap(),
                cx.factory().symbol(Symbol::new(":api-key-env")).unwrap(),
                cx.factory().string("HOME".to_owned()).unwrap(),
            ]),
        )
        .unwrap();
    let frame = request_frame(
        &mut cx,
        request_expr(
            "raw denied",
            vec![key_expr("privacy", Expr::Symbol(Symbol::new("no-raw")))],
        ),
    );

    let reply = as_component(&runner).answer(&mut cx, frame).unwrap();
    let expr = eval_reply_from_frame(&mut cx, &reply)
        .unwrap()
        .value
        .object()
        .as_expr(&mut cx)
        .unwrap();

    assert!(!format!("{expr:?}").contains("raw-provider-response"));
    server.join().unwrap();
}

fn http_privacy_cx() -> sim_kernel::Cx {
    let mut cx = eval_cx();
    install_roundtrip_codecs(&mut cx);
    install_agent_lib(&mut cx).unwrap();
    cx.grant_named("ai-runner");
    cx.grant_named("ai-runner-network");
    cx.grant_named("ai-runner-secret");
    cx
}

fn request_expr(task: &str, extra: Vec<(Expr, Expr)>) -> Expr {
    let mut entries = vec![
        key_expr("model-request", Expr::Bool(true)),
        key_expr("task", Expr::String(task.to_owned())),
        key_expr("messages", Expr::List(Vec::new())),
    ];
    entries.extend(extra);
    Expr::Map(entries)
}

fn key_expr(name: &str, value: Expr) -> (Expr, Expr) {
    (Expr::Symbol(Symbol::new(name)), value)
}

fn bind_loopback_listener() -> Option<TcpListener> {
    for _ in 0..3 {
        match TcpListener::bind(("127.0.0.1", 0)) {
            Ok(listener) => return Some(listener),
            Err(error) if error.kind() == ErrorKind::PermissionDenied => {
                thread::sleep(Duration::from_millis(25));
            }
            Err(error) => panic!("failed to bind loopback listener: {error}"),
        }
    }
    None
}