microloop 0.2.0

Ultra-fast runtime safety layer for autonomous agents — detects agent tool call loops before they burn API credits
Documentation
#![no_std]
#![allow(clippy::not_unsafe_ptr_arg_deref)]
#![allow(clippy::missing_safety_doc)]

extern crate alloc;

pub mod state;

pub mod canonical;
pub mod config;
pub mod engine;
pub mod history;

use alloc::string::ToString;
use core::ffi::c_char;
use core::slice;
use core::str;
pub use state::MicroloopState;

#[unsafe(no_mangle)]
pub extern "C" fn microloop_init(yaml_str: *const u8, yaml_len: usize) -> *mut MicroloopState {
    if yaml_str.is_null() {
        return core::ptr::null_mut();
    }
    let yaml_slice = unsafe { slice::from_raw_parts(yaml_str, yaml_len) };
    let yaml_string = match str::from_utf8(yaml_slice) {
        Ok(s) => s,
        Err(_) => return core::ptr::null_mut(),
    };

    match MicroloopState::new(yaml_string) {
        Ok(state) => {
            let b = alloc::boxed::Box::new(state);
            alloc::boxed::Box::into_raw(b)
        }
        Err(_) => core::ptr::null_mut(),
    }
}

#[unsafe(no_mangle)]
pub extern "C" fn microloop_verify(
    state_ptr: *mut MicroloopState,
    tool_name: *const u8,
    tool_name_len: usize,
    args_json: *const u8,
    args_json_len: usize,
) -> u8 {
    if state_ptr.is_null() {
        return 0;
    }

    let state = unsafe { &mut *state_ptr };

    let tool_slice = if !tool_name.is_null() {
        unsafe { slice::from_raw_parts(tool_name, tool_name_len) }
    } else {
        &[]
    };

    let args_slice = if !args_json.is_null() {
        unsafe { slice::from_raw_parts(args_json, args_json_len) }
    } else {
        &[]
    };

    verify(state, tool_slice, args_slice)
}

pub fn verify(state: &mut MicroloopState, tool_slice: &[u8], args_slice: &[u8]) -> u8 {
    let tool_str = str::from_utf8(tool_slice).unwrap_or("").to_string();
    let args_str = str::from_utf8(args_slice).unwrap_or("").to_string();

    if let Err(msg) = state.history.check_loop(
        &tool_str,
        &args_str,
        state.ignore_args,
        state.max_repeats,
        state.history_window,
    ) {
        state.set_error(&msg);
        return state.block_result();
    }

    if let Err(msg) = state.engine.validate(&args_str) {
        state.set_error(msg);
        return state.block_result();
    }

    0
}

#[unsafe(no_mangle)]
pub extern "C" fn microloop_get_last_error(state_ptr: *mut MicroloopState) -> *const c_char {
    if state_ptr.is_null() {
        return core::ptr::null();
    }
    let state = unsafe { &*state_ptr };
    if state.error_buffer.is_empty() {
        return core::ptr::null();
    }
    state.error_buffer.as_ptr() as *const c_char
}

#[unsafe(no_mangle)]
pub extern "C" fn microloop_free(state_ptr: *mut MicroloopState) {
    if !state_ptr.is_null() {
        unsafe {
            let _ = alloc::boxed::Box::from_raw(state_ptr);
        }
    }
}