dejadb-core 1.0.0

Core .mg format, canonical serialization, content addressing, and grain types for DejaDB.
Documentation
//! SML (an XML-like) tool envelope.
//!
//! Shape:
//!   <tool_use>
//!     <tool_name>…</tool_name>
//!     <description>…</description>
//!     <parameters>…</parameters>
//!   </tool_use>
//!
//! SR-F1: name, description, and schema JSON all run through `sml_escape`
//! so `<`, `>`, `&`, `"`, `'` become entities. A description containing
//! `</tool_use>` cannot escape the envelope.

use crate::error::Result;
use crate::types::Tool;

use super::definition_parts;
use super::escape::{compact_json, sml_escape};

pub fn render(action: &Tool) -> Result<String> {
    let (name, description, schema) = definition_parts(action)?;
    let name = sml_escape(&name);
    let description = sml_escape(&description);
    let schema = sml_escape(&compact_json(&schema));
    Ok(format!(
        "<tool_use>\n  <tool_name>{name}</tool_name>\n  <description>{description}</description>\n  <parameters>{schema}</parameters>\n</tool_use>"
    ))
}

#[cfg(test)]
mod tests {
    use super::super::tests::sample_def;
    use super::*;

    #[test]
    fn envelope_is_well_formed() {
        let s = render(&sample_def()).unwrap();
        assert!(s.starts_with("<tool_use>\n"));
        assert!(s.ends_with("</tool_use>"));
        assert!(s.contains("<tool_name>slack_post_message</tool_name>"));
    }

    #[test]
    fn adversarial_description_stays_inside_envelope() {
        let mut a = sample_def();
        a.tool_description = Some("Post </tool_use> and <script>alert(1)</script>".into());
        let s = render(&a).unwrap();
        // Exactly ONE genuine closing <tool_use> (the envelope's).
        assert_eq!(s.matches("</tool_use>").count(), 1);
        // The description's close-bracket has been entity-escaped.
        assert!(s.contains("&lt;/tool_use&gt;"));
        assert!(!s.contains("<script>"));
    }
}