agent-runtime-definition-loader 0.1.0

Internal safe Agent definition loader and catalog
Documentation
//! Filesystem loader and immutable catalog for declarative Agent definitions.

use std::{
    collections::BTreeMap,
    fs,
    path::{Path, PathBuf},
    sync::Arc,
};

use async_trait::async_trait;
use runtime_control_tools::CompleteTool;
use runtime_extension_api::{AgentDefinition, CompletionMode};
use runtime_extension_sdk::AgentDefinitionBuilder;
use runtime_kernel_api::{AgentDefinitionResolver, KernelFailure};
use runtime_ports::{OperationContext, ResolvedExecutionContext};
use serde::Deserialize;
use sha2::{Digest, Sha256};
use thiserror::Error;

const MAX_RESOURCE_BYTES: u64 = 1024 * 1024;

#[derive(Debug, Error)]
pub enum DefinitionLoadError {
    #[error("definition I/O failed: {0}")]
    Io(#[from] std::io::Error),
    #[error("definition manifest is invalid: {0}")]
    Manifest(#[from] toml::de::Error),
    #[error("definition contract is invalid: {0}")]
    Invalid(String),
}

#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Manifest {
    schema_version: String,
    id: String,
    version: String,
    runtime_api_version: u32,
    runtime_type: String,
    system: String,
    tools: Tools,
    #[serde(default)]
    eval: Option<Eval>,
}

#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Tools {
    #[serde(default)]
    required: Vec<String>,
    #[serde(default)]
    enabled: Vec<String>,
}

#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Eval {
    suite: String,
}

pub fn load_definition(root: &Path) -> Result<Arc<AgentDefinition>, DefinitionLoadError> {
    Ok(load_definition_report(root)?.definition)
}

pub struct DefinitionLoadReport {
    pub definition: Arc<AgentDefinition>,
}

pub fn load_definition_report(root: &Path) -> Result<DefinitionLoadReport, DefinitionLoadError> {
    let root = root.canonicalize()?;
    if !root.is_dir() {
        return Err(DefinitionLoadError::Invalid(
            "definition root is not a directory".into(),
        ));
    }
    let manifest_path = safe_file(&root, Path::new("agent.toml"))?;
    let manifest_bytes = bounded_read(&manifest_path)?;
    let manifest: Manifest = toml::from_slice(&manifest_bytes)?;
    if manifest.schema_version != "v1" || manifest.runtime_api_version != 3 {
        return Err(DefinitionLoadError::Invalid(
            "definition requires schema v1 and Runtime API version 3".into(),
        ));
    }
    if let Some(eval) = &manifest.eval {
        let _ = safe_file(&root, Path::new(&eval.suite))?;
    }
    let system_path = safe_file(&root, Path::new(&manifest.system))?;
    let system_bytes = bounded_read(&system_path)?;
    let system = String::from_utf8(system_bytes.clone())
        .map_err(|_| DefinitionLoadError::Invalid("system prompt must be UTF-8".into()))?;
    let mut tool_names = manifest.tools.enabled.clone();
    tool_names.extend(manifest.tools.required.clone());
    tool_names.sort();
    tool_names.dedup();
    if tool_names != ["runtime.complete"] {
        return Err(DefinitionLoadError::Invalid(
            "declarative v1 definitions may enable only the built-in runtime.complete tool; add typed tools through an extension catalog".into(),
        ));
    }
    let completion = if manifest
        .tools
        .required
        .iter()
        .any(|name| name == "runtime.complete")
    {
        CompletionMode::RequiredTool("runtime.complete".into())
    } else {
        CompletionMode::ModelOrTool
    };
    let definition = AgentDefinitionBuilder::new(&manifest.id, &manifest.version)
        .runtime_type(&manifest.runtime_type)
        .system_prompt(system)
        .tool(Arc::new(CompleteTool))
        .completion_mode(completion)
        .build()
        .map_err(|error| DefinitionLoadError::Invalid(error.to_string()))?;
    let mut definition = Arc::into_inner(definition).ok_or_else(|| {
        DefinitionLoadError::Invalid("definition builder returned a shared definition".into())
    })?;
    let mut digest = Sha256::new();
    for bytes in [&manifest_bytes, &system_bytes] {
        digest.update(bytes.len().to_be_bytes());
        digest.update(bytes);
    }
    definition.manifest.digest = format!("sha256:{:x}", digest.finalize());
    Ok(DefinitionLoadReport {
        definition: Arc::new(definition),
    })
}

pub struct DefinitionCatalogResolver {
    definitions: BTreeMap<(String, String), Arc<AgentDefinition>>,
}

impl DefinitionCatalogResolver {
    pub fn new(
        definitions: impl IntoIterator<Item = Arc<AgentDefinition>>,
    ) -> Result<Self, DefinitionLoadError> {
        let mut catalog = BTreeMap::new();
        for definition in definitions {
            let key = (
                definition.manifest.id.to_string(),
                definition.manifest.version.clone(),
            );
            if catalog.insert(key.clone(), definition).is_some() {
                return Err(DefinitionLoadError::Invalid(format!(
                    "duplicate definition {}@{}",
                    key.0, key.1
                )));
            }
        }
        if catalog.is_empty() {
            return Err(DefinitionLoadError::Invalid(
                "definition catalog is empty".into(),
            ));
        }
        Ok(Self {
            definitions: catalog,
        })
    }
}

#[async_trait]
impl AgentDefinitionResolver for DefinitionCatalogResolver {
    async fn resolve(
        &self,
        _: &OperationContext,
        resolved: &ResolvedExecutionContext,
    ) -> Result<Arc<AgentDefinition>, KernelFailure> {
        let definition = self
            .definitions
            .get(&(
                resolved.definition_id.clone(),
                resolved.definition_version.clone(),
            ))
            .cloned()
            .ok_or_else(|| {
                KernelFailure::new("DEFINITION_NOT_FOUND", "Agent definition is not loaded")
            })?;
        if definition.manifest.runtime_type != resolved.runtime_type
            || resolved
                .definition_digest
                .as_ref()
                .is_some_and(|digest| digest != &definition.manifest.digest)
        {
            return Err(KernelFailure::new(
                "DEFINITION_BINDING_MISMATCH",
                "Agent definition does not match the immutable Runtime binding",
            ));
        }
        Ok(definition)
    }
}

fn safe_file(root: &Path, relative: &Path) -> Result<PathBuf, DefinitionLoadError> {
    if relative.as_os_str().is_empty()
        || relative.is_absolute()
        || relative
            .components()
            .any(|part| !matches!(part, std::path::Component::Normal(_)))
    {
        return Err(DefinitionLoadError::Invalid(format!(
            "unsafe resource path {}",
            relative.display()
        )));
    }
    let path = root.join(relative).canonicalize()?;
    if !path.starts_with(root) || !path.is_file() {
        return Err(DefinitionLoadError::Invalid(format!(
            "resource escapes definition root: {}",
            relative.display()
        )));
    }
    Ok(path)
}

fn bounded_read(path: &Path) -> Result<Vec<u8>, DefinitionLoadError> {
    if fs::metadata(path)?.len() > MAX_RESOURCE_BYTES {
        return Err(DefinitionLoadError::Invalid(format!(
            "resource exceeds {MAX_RESOURCE_BYTES} bytes: {}",
            path.display()
        )));
    }
    Ok(fs::read(path)?)
}

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

    #[test]
    fn loads_a_minimal_non_coding_definition_and_rejects_escape() {
        let temp = tempfile::tempdir().unwrap();
        fs::write(temp.path().join("system.md"), "Finish the task.").unwrap();
        fs::write(
            temp.path().join("agent.toml"),
            r#"schema_version="v1"
id="com.example.fixture"
version="0.1.0"
runtime_api_version=3
runtime_type="generic"
system="system.md"
[tools]
required=["runtime.complete"]
enabled=["runtime.complete"]
"#,
        )
        .unwrap();
        let definition = load_definition(temp.path()).unwrap();
        assert_eq!(definition.manifest.runtime_type, "generic");
        assert!(definition.tool("runtime.complete").is_some());
        fs::write(
            temp.path().join("agent.toml"),
            fs::read_to_string(temp.path().join("agent.toml"))
                .unwrap()
                .replace("system.md", "../outside.md"),
        )
        .unwrap();
        assert!(load_definition(temp.path()).is_err());
    }
}