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());
}
}