use super::*;
pub(super) fn script_scan_dirs(
blueprint_path: &str,
extra: Option<std::path::PathBuf>,
) -> Vec<std::path::PathBuf> {
std::path::Path::new(blueprint_path)
.parent()
.map(|d| d.join("tools"))
.into_iter()
.chain(extra)
.chain(leviath_core::tools_dir())
.collect()
}
pub(super) fn script_within_blueprint(
base: &std::path::Path,
declared: &str,
what: &str,
) -> Result<std::path::PathBuf, String> {
let full = base.join(declared);
match leviath_core::resolves_within(&full, base) {
true => Ok(full),
false => Err(format!(
"{what} '{declared}' resolves outside the blueprint's directory ({}); a script must \
live beside the agent that declares it",
base.display()
)),
}
}
pub(crate) fn resolve_output_validators(
blueprint: &Blueprint,
blueprint_path: &str,
) -> Result<HashMap<String, Arc<leviath_scripting::output_validator::OutputValidator>>, String> {
let base = std::path::Path::new(blueprint_path)
.parent()
.map(std::path::Path::to_path_buf)
.unwrap_or_default();
let mut compiled = HashMap::new();
let specs = blueprint
.output
.iter()
.chain(blueprint.stages.iter().filter_map(|s| s.output.as_ref()));
for spec in specs {
let Some(script) = spec.validator.as_deref() else {
continue;
};
if compiled.contains_key(script) {
continue;
}
let path = script_within_blueprint(&base, script, "output validator")?;
let source = std::fs::read_to_string(&path)
.map_err(|e| format!("cannot read output validator '{}': {e}", path.display()))?;
let validator = leviath_scripting::output_validator::compile(script, &source)
.map_err(|e| format!("output validator failed to compile: {e}"))?;
compiled.insert(script.to_string(), Arc::new(validator));
}
Ok(compiled)
}
pub(crate) fn resolve_stage_hook_scripts(
blueprint: &Blueprint,
blueprint_path: &str,
) -> Result<HashMap<String, Arc<leviath_scripting::stage_hook::HookScript>>, String> {
let base = std::path::Path::new(blueprint_path)
.parent()
.map(std::path::Path::to_path_buf)
.unwrap_or_default();
let mut wanted: HashMap<&str, Vec<&str>> = HashMap::new();
for stage in &blueprint.stages {
for (hook, path) in stage.hooks.declared() {
wanted.entry(path).or_default().push(hook);
}
}
let mut scripts = HashMap::new();
for (path, hooks) in wanted {
let full = script_within_blueprint(&base, path, "stage hook script")?;
let source = std::fs::read_to_string(&full)
.map_err(|e| format!("cannot read stage hook script '{}': {e}", full.display()))?;
let compiled = leviath_scripting::stage_hook::compile(path, &source, &hooks)
.map_err(|e| format!("stage hook script '{path}' failed to compile: {e}"))?;
scripts.insert(path.to_string(), Arc::new(compiled));
}
Ok(scripts)
}
pub(crate) fn resolve_region_scripts(
blueprint: &Blueprint,
blueprint_path: &str,
) -> Result<HashMap<String, Arc<leviath_scripting::region_hook::RegionScript>>, String> {
let base = std::path::Path::new(blueprint_path)
.parent()
.map(std::path::Path::to_path_buf)
.unwrap_or_default();
let mut scripts = HashMap::new();
let layouts = std::iter::once(&blueprint.context_layout).chain(
blueprint
.stages
.iter()
.filter_map(|s| s.context_layout.as_ref()),
);
for layout in layouts {
for region in &layout.regions {
let leviath_core::RegionKind::Custom { script, .. } = ®ion.kind else {
continue;
};
if scripts.contains_key(script) {
continue;
}
let path = script_within_blueprint(&base, script, "custom region script")
.map_err(|e| format!("region '{}': {e}", region.name))?;
let source = std::fs::read_to_string(&path).map_err(|e| {
format!(
"region '{}': cannot read custom region script '{}': {e}",
region.name,
path.display()
)
})?;
let compiled =
leviath_scripting::region_hook::compile(script, &source).map_err(|e| {
format!(
"region '{}': custom region script failed to compile: {e}",
region.name
)
})?;
scripts.insert(script.clone(), Arc::new(compiled));
}
}
Ok(scripts)
}
pub(super) fn reserved_tool_names(
builtin_names: &HashSet<String>,
mcp_tool_defs: &[Tool],
) -> HashSet<String> {
let mut reserved: HashSet<String> = builtin_names.clone();
reserved.extend(leviath_tools::BuiltinTools::subagent_tool_names());
reserved.extend(mcp_tool_defs.iter().map(|t| t.name.clone()));
reserved
}
pub(super) fn script_cap(name: &str) -> Option<leviath_tools::ToolCapability> {
match name {
"network" | "net" | "http" => Some(leviath_tools::ToolCapability::Network),
"shell" | "process" | "process_spawn" => Some(leviath_tools::ToolCapability::ProcessSpawn),
"filesystem" | "file" | "fs" => Some(leviath_tools::ToolCapability::FileSystem),
_ => None,
}
}
pub(super) fn platform_satisfies_caps(
platform: &leviath_tools::PlatformCapabilities,
required_caps: &[String],
) -> bool {
required_caps
.iter()
.all(|c| script_cap(c).is_some_and(|cap| platform.supports(cap)))
}
pub(crate) fn current_platform_satisfies(required_caps: &[String]) -> bool {
platform_satisfies_caps(
&leviath_tools::PlatformCapabilities::current(),
required_caps,
)
}
pub(crate) fn discover_script_tools_in(
dirs: &[std::path::PathBuf],
reserved: &HashSet<String>,
) -> (leviath_scripting::ScriptToolSet, HashSet<String>, Vec<Tool>) {
let (set, skipped) = leviath_scripting::ScriptToolSet::discover(dirs);
for s in &skipped {
let path = s.path.display().to_string();
tracing::warn!(tool = %path, reason = %s.reason, "skipping invalid script tool");
}
let platform = leviath_tools::PlatformCapabilities::current();
let mut names = HashSet::new();
let mut defs = Vec::new();
for meta in set.metas() {
if reserved.contains(&meta.name) {
tracing::warn!(tool = %meta.name, "script tool name collides with an existing tool - ignoring");
continue;
}
if !platform_satisfies_caps(&platform, &meta.required_caps) {
let caps = meta.required_caps.join(", ");
tracing::warn!(tool = %meta.name, requires = %caps, "script tool requires a capability this platform lacks - ignoring");
continue;
}
names.insert(meta.name.clone());
defs.push(Tool {
name: meta.name.clone(),
description: meta.description.clone(),
parameters: meta.parameters_schema(),
});
}
(set, names, defs)
}
pub(super) fn discover_script_tools(
blueprint_path: &str,
builtin_names: &HashSet<String>,
mcp_tool_defs: &[Tool],
extra_dir: Option<std::path::PathBuf>,
) -> (leviath_scripting::ScriptToolSet, HashSet<String>, Vec<Tool>) {
let dirs = script_scan_dirs(blueprint_path, extra_dir);
let reserved = reserved_tool_names(builtin_names, mcp_tool_defs);
discover_script_tools_in(&dirs, &reserved)
}