use std::collections::{HashMap, HashSet};
use std::sync::{Arc, Mutex as StdMutex};
use bevy_ecs::entity::Entity;
use bevy_ecs::world::World;
use leviath_core::blueprint::Blueprint;
use leviath_providers::Tool;
use leviath_runtime::host::{SpawnArgs, SubAgentOp};
use leviath_runtime::interaction_hub::InteractionHub;
use leviath_runtime::persistence::{RunMetadata, TokenTotals};
use leviath_runtime::pipeline::{
CompactionSettings, ModelDefaults, PersistWatermark, Providers, resolve_stages,
spawn_agent_seeded,
};
use tokio::sync::Mutex;
use tokio::sync::mpsc::UnboundedSender;
use crate::config::Config;
use crate::daemon::seed_command::SeedCommandPolicy;
use crate::daemon::subagent::SubAgentHandle;
use crate::daemon::tool_service::{AgentToolState, CliToolService};
const DEFAULT_SUBAGENT_DEPTH: usize = 3;
pub(crate) fn model_defaults(config: &Config) -> ModelDefaults {
ModelDefaults {
provider: config.default_provider.clone(),
model: config.default_model.clone(),
fallback_order: parse_fallback_order(&config.providers.fallback_order),
}
}
fn parse_fallback_order(entries: &[String]) -> Vec<leviath_core::blueprint::ModelEntry> {
entries
.iter()
.filter_map(|raw| match raw.split_once('/') {
Some((provider, model)) if !provider.is_empty() && !model.is_empty() => Some(
leviath_core::blueprint::ModelEntry::new(provider.to_string(), model.to_string()),
),
_ => {
tracing::warn!(
entry = %raw,
"ignoring [providers] fallback_order entry: expected \"provider/model\""
);
None
}
})
.collect()
}
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(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 = base.join(script);
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)
}
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
}
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,
}
}
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)
}
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)
}
#[allow(clippy::too_many_arguments)]
fn build_tool_state(
builtins: Arc<leviath_tools::BuiltinTools>,
builtin_names: HashSet<String>,
mcp: Arc<Mutex<leviath_mcp::ToolExecutor>>,
config: &Config,
hub: &InteractionHub,
run_id: &str,
entry_stage: &str,
entry_index: usize,
stage_perms_by_index: Vec<HashMap<String, String>>,
stage_required_by_index: Vec<HashSet<String>>,
agent_perms: HashMap<String, String>,
agent_name: &str,
launch_overrides: HashMap<String, crate::config::ToolPolicy>,
subagent: Option<SubAgentHandle>,
sandbox: Option<Arc<crate::daemon::sandbox_manager::SandboxManager>>,
script_tools: leviath_scripting::ScriptToolSet,
script_tool_names: HashSet<String>,
script_host: Arc<dyn leviath_scripting::ScriptHost>,
dynamic: Option<Arc<crate::daemon::tool_service::DynamicToolCtx>>,
unattended: bool,
) -> Arc<AgentToolState> {
let entry_perms = stage_perms_by_index
.get(entry_index)
.cloned()
.unwrap_or_default();
let entry_required = stage_required_by_index
.get(entry_index)
.cloned()
.unwrap_or_default();
Arc::new(AgentToolState {
builtins,
mcp,
builtin_names,
launch_overrides: Arc::new(launch_overrides),
session_allows: Arc::new(Mutex::new(HashSet::new())),
stage_perms: Arc::new(StdMutex::new(entry_perms)),
stage_perms_by_index: Arc::new(stage_perms_by_index),
stage_required: Arc::new(StdMutex::new(entry_required)),
stage_required_by_index: Arc::new(stage_required_by_index),
agent_perms: Arc::new(agent_perms),
global_perms: Arc::new(config.permissions_for_agent(agent_name)),
interaction: hub.backend_for(run_id),
unattended,
stage_name: Arc::new(StdMutex::new(entry_stage.to_string())),
subagent,
sandbox,
script_tools: Arc::new(StdMutex::new(script_tools)),
script_tool_names: Arc::new(StdMutex::new(script_tool_names)),
script_host,
dynamic,
})
}
fn resolve_seeds(
blueprint: &Blueprint,
args: &SpawnArgs,
workdir: &str,
commands: &SeedCommandPolicy,
) -> Result<HashMap<String, String>, String> {
use leviath_core::layout::RegionSeed;
let mut caller: HashMap<String, String> = HashMap::new();
caller.insert("task".to_string(), args.task.clone());
for (k, v) in &args.regions {
caller.insert(k.clone(), v.clone());
}
let base = std::path::Path::new(workdir);
let mut seeds: HashMap<String, String> = HashMap::new();
for region in &blueprint.context_layout.regions {
let Some(seed) = ®ion.seed else { continue };
match seed {
RegionSeed::CallerInput { name } => {
let value = caller.get(name).map(|s| s.as_str()).unwrap_or("");
if value.trim().is_empty() {
if region.required {
return Err(region.required_message.clone().unwrap_or_else(|| {
format!(
"required region '{}' was not provided; supply it via \
--{name} <text|@file> (CLI), a ---region:{name}--- block \
(ACP), or the API `regions` field",
region.name
)
}));
}
continue;
}
seeds.insert(region.name.clone(), value.to_string());
}
RegionSeed::Literal { text } => {
seeds.insert(region.name.clone(), text.clone());
}
RegionSeed::Files { paths } => {
let content = read_and_concat(
®ion.name,
paths.iter().map(|p| base.join(p)),
region.required,
)?;
if let Some(content) = content {
seeds.insert(region.name.clone(), content);
}
}
RegionSeed::Glob { pattern } => {
let full = base.join(pattern);
let full = full.to_string_lossy();
let matches = glob::glob(&full)
.map_err(|e| format!("region '{}': bad glob '{pattern}': {e}", region.name))?;
let paths: Vec<std::path::PathBuf> = matches.filter_map(|m| m.ok()).collect();
let content = read_and_concat(®ion.name, paths.into_iter(), region.required)?;
match content {
Some(content) => {
seeds.insert(region.name.clone(), content);
}
None if region.required => {
return Err(format!(
"required region '{}': glob '{pattern}' matched no files",
region.name
));
}
None => {}
}
}
RegionSeed::Rhai { script } => {
let path = base.join(script);
let src = std::fs::read_to_string(&path).map_err(|e| {
format!(
"region '{}': read rhai seed '{}': {e}",
region.name,
path.display()
)
})?;
let mut input = rhai::Map::new();
input.insert("task".into(), rhai::Dynamic::from(args.task.clone()));
input.insert("workdir".into(), rhai::Dynamic::from(workdir.to_string()));
let out = leviath_scripting::ScriptEngine::new()
.transform(&src, input)
.map_err(|e| format!("region '{}': rhai seed failed: {e}", region.name))?;
if !out.trim().is_empty() {
seeds.insert(region.name.clone(), out);
} else if region.required {
return Err(format!(
"required region '{}': rhai seed '{script}' returned empty",
region.name
));
}
}
RegionSeed::Command { command } => {
if !commands.allowed {
if region.required {
return Err(format!(
"required region '{}': command seeds are disabled \
(`[security] allow_seed_commands = false` or --no-seed-commands)",
region.name
));
}
tracing::warn!(
region = %region.name,
"command seed skipped: command seeds are disabled"
);
continue;
}
match commands.run(command, base) {
Ok(out) if !out.trim().is_empty() => {
seeds.insert(region.name.clone(), out);
}
Ok(_) => {
if region.required {
return Err(format!(
"required region '{}': command seed '{command}' returned empty",
region.name
));
}
tracing::warn!(
region = %region.name,
command = %command,
"command seed returned no output; region left empty"
);
}
Err(e) => {
if region.required {
return Err(format!(
"required region '{}': command seed '{command}' failed: {e}",
region.name
));
}
tracing::warn!(
region = %region.name,
command = %command,
error = %e,
"command seed failed; region left empty"
);
}
}
}
}
}
Ok(seeds)
}
fn read_and_concat(
region: &str,
paths: impl Iterator<Item = std::path::PathBuf>,
required: bool,
) -> Result<Option<String>, String> {
let mut parts: Vec<String> = Vec::new();
for path in paths {
match std::fs::read_to_string(&path) {
Ok(text) => parts.push(format!("--- {} ---\n{}", path.display(), text)),
Err(e) => {
if required {
return Err(format!(
"region '{region}': read seed file '{}': {e}",
path.display()
));
}
}
}
}
Ok((!parts.is_empty()).then(|| parts.join("\n\n")))
}
fn build_read_path_policy(
blueprint: &leviath_core::Blueprint,
config: &crate::config::Config,
workdir: &std::path::Path,
) -> Result<(leviath_core::ReadPathPolicy, Option<String>), String> {
let Some(rp) = blueprint
.read_paths
.as_ref()
.filter(|rp| !rp.allow.is_empty())
else {
return Ok((leviath_core::ReadPathPolicy::inactive(), None));
};
let home = leviath_core::home_dir();
let declared =
leviath_core::ReadPathSet::compile(&rp.allow, workdir, home.as_deref(), cfg!(windows))
.map_err(|e| format!("agent '{}' [read_paths]: {e}", blueprint.name))?;
let grant_entries = config.read_path_grants_for_agent(&blueprint.name);
let grants =
leviath_core::ReadPathSet::compile(&grant_entries, workdir, home.as_deref(), cfg!(windows))
.map_err(|e| format!("read_paths grant in your config.toml: {e}"))?;
let allow_blueprint = config.security.allow_blueprint_read_paths;
let warning = (!allow_blueprint && grants.is_empty()).then(|| {
let entries = rp
.allow
.iter()
.map(|e| format!("\"{e}\""))
.collect::<Vec<_>>()
.join(", ");
format!(
"agent '{name}' declares [read_paths] but nothing grants them; reads outside \
the workdir will be refused. To grant them, add to your config.toml either:\n\
[security]\nallow_blueprint_read_paths = true\n\
or the specific paths:\n[agent_read_paths.{name}]\nallow = [{entries}]",
name = blueprint.name,
)
});
Ok((
leviath_core::ReadPathPolicy {
agent: blueprint.name.clone(),
blueprint: declared,
grants,
allow_blueprint,
},
warning,
))
}
fn read_path_grant_counts(
blueprint: &leviath_core::Blueprint,
config: &crate::config::Config,
workdir: &std::path::Path,
) -> Option<leviath_core::run_meta::ReadPathGrantCounts> {
let report = crate::read_path_report::build(blueprint, config, workdir)?.ok()?;
Some(leviath_core::run_meta::ReadPathGrantCounts {
declared: report.declared(),
granted: report.granted(),
})
}
fn bump_read_sensitivities(
map: &mut HashMap<String, leviath_core::TaintLevel>,
read_paths_granted: bool,
) {
if !read_paths_granted {
return;
}
for tool in ["read_file", "read_files", "list_dir"] {
if let Some(level) = map.get_mut(tool) {
*level = (*level).max(leviath_core::TaintLevel::Private);
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn build_agent(
world: &mut World,
tool_service: &CliToolService,
config: &Config,
shared_mcp: Arc<Mutex<leviath_mcp::ToolExecutor>>,
mcp_tool_defs: &[Tool],
hub: &InteractionHub,
args: &SpawnArgs,
now_secs: i64,
subagent_tx: UnboundedSender<SubAgentOp>,
) -> Result<Entity, String> {
build_agent_inner(
world,
tool_service,
config,
shared_mcp,
mcp_tool_defs,
hub,
args,
now_secs,
subagent_tx,
true,
)
}
#[allow(clippy::too_many_arguments)]
pub fn build_agent_for_reload(
world: &mut World,
tool_service: &CliToolService,
config: &Config,
shared_mcp: Arc<Mutex<leviath_mcp::ToolExecutor>>,
mcp_tool_defs: &[Tool],
hub: &InteractionHub,
args: &SpawnArgs,
now_secs: i64,
subagent_tx: UnboundedSender<SubAgentOp>,
) -> Result<Entity, String> {
build_agent_inner(
world,
tool_service,
config,
shared_mcp,
mcp_tool_defs,
hub,
args,
now_secs,
subagent_tx,
false,
)
}
fn log_blueprint_lint(content: &str, blueprint: &Blueprint, manifest_path: &str) {
let agent_dir = std::path::Path::new(manifest_path)
.parent()
.map(std::path::Path::to_path_buf)
.unwrap_or_default();
let env = crate::lint::LintEnv::offline(&agent_dir);
for finding in crate::lint::lint_manifest(content, blueprint, &env) {
if finding.severity == crate::lint::LintSeverity::Note {
continue;
}
let line = format!(
"blueprint '{}': {} [{}]",
blueprint.name,
finding.one_line(),
finding.code
);
tracing::warn!("{line}");
}
}
#[allow(clippy::too_many_arguments)]
fn build_agent_inner(
world: &mut World,
tool_service: &CliToolService,
config: &Config,
shared_mcp: Arc<Mutex<leviath_mcp::ToolExecutor>>,
mcp_tool_defs: &[Tool],
hub: &InteractionHub,
args: &SpawnArgs,
now_secs: i64,
subagent_tx: UnboundedSender<SubAgentOp>,
enforce_seeds: bool,
) -> Result<Entity, String> {
if !std::fs::metadata(&args.workdir).is_ok_and(|m| m.is_dir()) {
return Err(format!(
"workspace '{}' does not exist or is not a directory",
args.workdir
));
}
let content = std::fs::read_to_string(&args.blueprint_path)
.map_err(|e| format!("read manifest '{}': {e}", args.blueprint_path))?;
let mut blueprint = leviath_core::manifest::parse_manifest(&content)
.map_err(|e| format!("parse manifest: {e}"))?;
blueprint
.validate()
.map_err(|e| format!("invalid blueprint: {e}"))?;
log_blueprint_lint(&content, &blueprint, &args.blueprint_path);
if let Some(md) = args.max_depth {
blueprint.max_child_depth = Some(md);
}
if let Some(default_max) = config.limits.default_max_iterations {
for stage in &mut blueprint.stages {
match stage.max_iterations {
None | Some(0) => stage.max_iterations = Some(default_max),
Some(_) => {}
}
}
}
let entry_stage = blueprint
.entry_stage
.clone()
.or_else(|| blueprint.stages.first().map(|s| s.name.clone()))
.unwrap_or_default();
let entry_index = blueprint
.stages
.iter()
.position(|s| s.name == entry_stage)
.unwrap_or(0);
let stage_sandbox_by_index: Vec<leviath_core::ToolSandboxConfig> = blueprint
.stages
.iter()
.map(|s| {
leviath_core::resolve_sandbox(
config.sandbox.as_ref(),
blueprint.sandbox.as_ref(),
s.sandbox.as_ref(),
)
})
.collect();
let sandbox = crate::daemon::sandbox_manager::SandboxManager::build(
&args.run_id,
stage_sandbox_by_index,
&args.workdir,
entry_index,
)?
.map(Arc::new);
let (read_path_policy, read_path_warning) =
build_read_path_policy(&blueprint, config, std::path::Path::new(&args.workdir))?;
if let Some(warning) = &read_path_warning {
tracing::warn!(agent_name = %blueprint.name, "{warning}");
}
let read_paths_granted = read_path_policy.is_active()
&& (read_path_policy.allow_blueprint || !read_path_policy.grants.is_empty());
let read_path_counts =
read_path_grant_counts(&blueprint, config, std::path::Path::new(&args.workdir));
let tool_ctx = leviath_tools::ToolContext::new(std::path::PathBuf::from(&args.workdir))
.with_read_paths(read_path_policy);
let mut builtins = leviath_tools::BuiltinTools::new(tool_ctx);
if let Some(mgr) = &sandbox {
builtins =
builtins.with_shell_executor(mgr.clone() as Arc<dyn leviath_tools::ShellExecutor>);
}
let builtins = Arc::new(builtins);
let builtin_names: HashSet<String> = builtins.names().into_iter().collect();
let mut all_tool_defs = builtins.tool_defs();
all_tool_defs.extend(leviath_tools::BuiltinTools::subagent_tool_defs());
all_tool_defs.extend(mcp_tool_defs.iter().cloned());
let static_tool_defs = all_tool_defs.clone();
let dynamic_tools = blueprint.dynamic_tools;
let workdir_tools_dir =
dynamic_tools.then(|| std::path::PathBuf::from(&args.workdir).join("tools"));
let (script_tools, script_tool_names, script_defs) = discover_script_tools(
&args.blueprint_path,
&builtin_names,
mcp_tool_defs,
workdir_tools_dir.clone(),
);
all_tool_defs.extend(script_defs);
let stages = {
let registry = &world
.get_resource::<Providers>()
.expect("Providers resource present in a PipelineWorld")
.0;
resolve_stages(
&blueprint,
args.model.as_deref(),
&model_defaults(config),
registry,
&all_tool_defs,
args.yolo,
)?
};
let agent_name = blueprint.name.clone();
let num_stages = blueprint.stages.len();
let compaction = blueprint.compaction_config.clone();
let max_child_depth = blueprint.max_child_depth.unwrap_or(DEFAULT_SUBAGENT_DEPTH);
let security = leviath_core::taint::resolve_security(
config.taint_tracking,
blueprint.security.as_ref(),
None,
);
let mcp_overrides = world
.get_resource::<leviath_runtime::pipeline::PolicyGate>()
.map(|p| p.0.mcp_overrides.clone())
.unwrap_or_default();
let tool_sensitivities: Option<HashMap<String, leviath_core::TaintLevel>> =
security.taint_tracking.then(|| {
let mut gate = leviath_runtime::TaintGate::new(security.clone());
gate.apply_mcp_overrides(&mcp_overrides);
let mut map: HashMap<String, leviath_core::TaintLevel> = all_tool_defs
.iter()
.map(|t| {
(
t.name.clone(),
gate.tool_classification(&t.name).sensitivity,
)
})
.collect();
bump_read_sensitivities(&mut map, read_paths_granted);
map
});
let stage_perms_by_index: Vec<HashMap<String, String>> = blueprint
.stages
.iter()
.map(|s| s.tool_permissions.clone())
.collect();
let agent_perms = blueprint.agent_tool_permissions();
let stage_available: Vec<Vec<String>> = blueprint
.stages
.iter()
.map(|s| s.available_tools.clone())
.collect();
let stage_required: Vec<Vec<String>> = blueprint
.stages
.iter()
.map(|s| s.required_tools.clone())
.collect();
let stage_required_by_index: Vec<HashSet<String>> = stage_required
.iter()
.map(|names| {
names
.iter()
.map(|n| leviath_tools::canonical_tool_name(n).to_string())
.collect()
})
.collect();
let model_label = stages
.first()
.map(|s| format!("{}/{}", s.provider_name, s.model));
let seeds = if enforce_seeds {
let policy = SeedCommandPolicy::new(
config.security.allow_seed_commands && !args.no_seed_commands,
std::time::Duration::from_secs(config.limits.script_shell_timeout_secs),
sandbox.clone(),
);
resolve_seeds(&blueprint, args, &args.workdir, &policy)?
} else {
HashMap::new()
};
let region_scripts = resolve_region_scripts(&blueprint, &args.blueprint_path)?;
let outcome_flags = leviath_runtime::persistence::RunOutcomeFlags::for_blueprint(&blueprint);
let entity = spawn_agent_seeded(
world,
args.run_id.clone(),
blueprint,
&seeds,
stages,
leviath_core::config::PromptHints {
batch_tool: config.batch_tool_hint,
shell: config.shell_hint,
},
config.nudge.clone(),
region_scripts,
)?;
let metadata = RunMetadata {
run_id: args.run_id.clone(),
agent_name: agent_name.clone(),
agent_path: args.blueprint_path.clone(),
task: args.task.clone(),
model: model_label,
workdir: args.workdir.clone(),
num_stages,
started_at: now_secs,
parent_run_id: args.parent_run_id.clone(),
metadata: args.metadata.clone(),
callback_url: args.callback_url.clone(),
callback_secret: args.callback_secret.clone(),
title: None,
unattended: args.yolo,
read_paths: read_path_counts,
};
{
let mut entity_mut = world.entity_mut(entity);
entity_mut.insert((
metadata,
TokenTotals::default(),
PersistWatermark::default(),
outcome_flags,
));
(config.title.enabled && !args.task.is_empty() && args.parent_run_id.is_none())
.then_some(leviath_runtime::title::PendingTitle)
.into_iter()
.for_each(|marker| {
entity_mut.insert(marker);
});
args.yolo
.then_some(leviath_runtime::components::InteractionAutoApprove)
.into_iter()
.for_each(|marker| {
entity_mut.insert(marker);
});
compaction.into_iter().for_each(|cc| {
entity_mut.insert(CompactionSettings(cc));
});
tool_sensitivities.into_iter().for_each(|sensitivities| {
let mut gate = leviath_runtime::TaintGate::new(security.clone());
gate.apply_mcp_overrides(&mcp_overrides);
entity_mut.insert((
gate,
leviath_runtime::pipeline::ToolSensitivities(sensitivities),
));
if args.yolo {
entity_mut.insert(leviath_runtime::components::GateAutoApprove);
}
entity_mut
.get_mut::<leviath_runtime::components::ContextWindow>()
.into_iter()
.for_each(|mut window| window.enable_taint_tracking());
});
}
let mut launch_overrides: HashMap<String, crate::config::ToolPolicy> = HashMap::new();
if args.yolo {
launch_overrides.insert("*".to_string(), crate::config::ToolPolicy::Allow);
}
for tool in &args.allow {
launch_overrides.insert(tool.clone(), crate::config::ToolPolicy::Allow);
}
let subagent = SubAgentHandle {
sender: subagent_tx,
parent_run_id: args.run_id.clone(),
workdir: args.workdir.clone(),
max_depth: max_child_depth,
no_seed_commands: args.no_seed_commands,
unattended: args.yolo,
};
let entry_stage_perms = stage_perms_by_index
.get(entry_index)
.cloned()
.unwrap_or_default();
let effective_script_perms = crate::daemon::script_host::effective_script_permissions(
&config.tool_script_permissions,
&content,
);
let agent_scoped_perms = config.permissions_for_agent(&agent_name);
let script_allow = crate::daemon::script_host::resolve_script_permissions(
&effective_script_perms,
&|builtin| {
crate::tools::resolve_policy(
builtin,
true,
&launch_overrides,
&entry_stage_perms,
&agent_perms,
&agent_scoped_perms,
)
},
);
let script_host: Arc<dyn leviath_scripting::ScriptHost> = Arc::new(
crate::daemon::script_host::DaemonScriptHost::new(
script_allow,
std::path::PathBuf::from(&args.workdir),
)
.with_shell(
sandbox.clone(),
std::time::Duration::from_secs(config.limits.script_shell_timeout_secs),
)
.with_local_network(config.security.allow_local_network)
.with_env_allowlist(config.security.allow_env_vars.clone()),
);
let dynamic = dynamic_tools.then(|| {
world
.entity_mut(entity)
.insert(leviath_runtime::pipeline::DynamicTools);
Arc::new(crate::daemon::tool_service::DynamicToolCtx {
scan_dirs: script_scan_dirs(&args.blueprint_path, workdir_tools_dir),
reserved_names: reserved_tool_names(&builtin_names, mcp_tool_defs),
static_defs: static_tool_defs,
stage_available,
stage_required,
unattended: args.yolo,
dirty: Arc::new(std::sync::atomic::AtomicBool::new(false)),
})
});
let state = build_tool_state(
builtins,
builtin_names,
shared_mcp,
config,
hub,
&args.run_id,
&entry_stage,
entry_index,
stage_perms_by_index,
stage_required_by_index,
agent_perms,
&agent_name,
launch_overrides,
Some(subagent),
sandbox,
script_tools,
script_tool_names,
script_host,
dynamic,
args.yolo,
);
tool_service.register(entity, state);
Ok(entity)
}
#[cfg(test)]
mod tests {
use super::*;
use leviath_core::blueprint::ModelConfig;
use leviath_runtime::ProviderRegistry;
use leviath_runtime::world::PipelineWorld;
fn sub_tx() -> UnboundedSender<SubAgentOp> {
tokio::sync::mpsc::unbounded_channel().0
}
#[test]
fn log_blueprint_lint_warns_about_findings_and_skips_notes() {
crate::test_support::with_tracing(|| {});
let home = tempfile::tempdir().unwrap();
temp_env::with_var("LEVIATH_HOME", Some(home.path().to_str().unwrap()), || {
let manifest = r#"
[agent]
name = "noisy"
version = "0.1.0"
[stages.main]
model = { models = [{ provider = "anthropic", model = "claude-sonnet-5" }] }
available_tools = ["ask_user_text"]
[read_paths]
allow = ["~/.leviath/runs"]
[context.regions]
system = { kind = "pinned", max_tokens = 1000 }
"#;
let bp = leviath_core::manifest::parse_manifest(manifest).unwrap();
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("agent.leviath");
std::fs::write(&path, manifest).unwrap();
let env = crate::lint::LintEnv::offline(dir.path());
let findings = crate::lint::lint_manifest(manifest, &bp, &env);
assert!(
findings
.iter()
.any(|f| f.severity == crate::lint::LintSeverity::Note),
"the fixture needs a note for the skip arm to run"
);
assert!(
findings
.iter()
.any(|f| f.severity == crate::lint::LintSeverity::Warning),
"the fixture needs a warning for the log arm to run"
);
log_blueprint_lint(manifest, &bp, &path.to_string_lossy());
});
}
#[test]
fn log_blueprint_lint_is_silent_for_a_clean_blueprint() {
crate::test_support::with_tracing(|| {});
let home = tempfile::tempdir().unwrap();
temp_env::with_var("LEVIATH_HOME", Some(home.path().to_str().unwrap()), || {
let manifest = r#"
[agent]
name = "quiet"
version = "0.1.0"
[stages.main]
mode = "autonomous"
model = { models = [{ provider = "anthropic", model = "claude-sonnet-5" }] }
max_iterations = 5
[context.regions]
system = { kind = "pinned", max_tokens = 1000 }
"#;
let bp = leviath_core::manifest::parse_manifest(manifest).unwrap();
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("agent.leviath");
std::fs::write(&path, manifest).unwrap();
let env = crate::lint::LintEnv::offline(dir.path());
assert!(crate::lint::lint_manifest(manifest, &bp, &env).is_empty());
log_blueprint_lint(manifest, &bp, &path.to_string_lossy());
});
}
#[test]
fn fallback_order_parses_provider_slash_model_and_drops_junk() {
crate::test_support::with_tracing(|| {
let parsed = parse_fallback_order(&[
"openrouter/deepseek/deepseek-v4-flash".to_string(),
"anthropic/claude-sonnet-5".to_string(),
"anthropic".to_string(),
"/no-provider".to_string(),
"no-model/".to_string(),
String::new(),
]);
assert_eq!(
parsed
.iter()
.map(|e| (e.provider.as_str(), e.model.as_str()))
.collect::<Vec<_>>(),
vec![
("openrouter", "deepseek/deepseek-v4-flash"),
("anthropic", "claude-sonnet-5"),
]
);
});
}
#[test]
fn model_defaults_carries_the_fallback_chain_from_config() {
let mut config = Config {
default_provider: "openrouter".to_string(),
default_model: Some("deepseek".to_string()),
..Default::default()
};
config.providers.fallback_order = vec!["anthropic/claude-sonnet-5".to_string()];
let defaults = model_defaults(&config);
assert_eq!(defaults.provider, "openrouter");
assert_eq!(defaults.model.as_deref(), Some("deepseek"));
assert_eq!(defaults.fallback_order.len(), 1);
assert_eq!(defaults.fallback_order[0].provider, "anthropic");
}
#[test]
fn discover_script_tools_registers_and_drops_collisions() {
crate::test_support::with_tracing(|| {});
let home = tempfile::tempdir().unwrap();
temp_env::with_var("LEVIATH_HOME", Some(home.path().to_str().unwrap()), || {
let agent_dir = tempfile::tempdir().unwrap();
let tools = agent_dir.path().join("tools");
std::fs::create_dir(&tools).unwrap();
std::fs::write(tools.join("echo.rhai"), "// @tool echo\nparams.x").unwrap();
std::fs::write(tools.join("read_file.rhai"), "// @tool read_file\n1").unwrap();
std::fs::write(tools.join("mcp_tool.rhai"), "// @tool mcp_tool\n1").unwrap();
std::fs::write(tools.join("bad.rhai"), "no tool directive\nlet").unwrap();
std::fs::write(
tools.join("needs_gpu.rhai"),
"// @tool needs_gpu\n// @requires gpu\n1",
)
.unwrap();
std::fs::write(
tools.join("net_tool.rhai"),
"// @tool net_tool\n// @requires network\n1",
)
.unwrap();
let blueprint = agent_dir.path().join("agent.leviath");
let builtins: HashSet<String> = ["read_file".to_string()].into_iter().collect();
let mcp = vec![leviath_providers::Tool {
name: "mcp_tool".to_string(),
description: String::new(),
parameters: serde_json::json!({}),
}];
let (set, names, defs) =
discover_script_tools(blueprint.to_str().unwrap(), &builtins, &mcp, None);
assert!(set.contains("echo") && set.contains("read_file"));
assert!(names.contains("echo"));
assert!(!names.contains("read_file"));
assert!(!names.contains("mcp_tool"));
assert!(!names.contains("needs_gpu"), "unsatisfiable cap dropped");
assert!(names.contains("net_tool"), "satisfiable cap kept");
let mut def_names: Vec<&str> = defs.iter().map(|d| d.name.as_str()).collect();
def_names.sort_unstable();
assert_eq!(def_names, vec!["echo", "net_tool"]);
});
}
#[test]
fn script_cap_maps_known_and_unknown_names() {
use leviath_tools::ToolCapability::*;
assert_eq!(script_cap("network"), Some(Network));
assert_eq!(script_cap("http"), Some(Network));
assert_eq!(script_cap("shell"), Some(ProcessSpawn));
assert_eq!(script_cap("process_spawn"), Some(ProcessSpawn));
assert_eq!(script_cap("filesystem"), Some(FileSystem));
assert_eq!(script_cap("fs"), Some(FileSystem));
assert_eq!(script_cap("gpu"), None);
}
#[test]
fn platform_satisfies_caps_gates_on_support() {
use leviath_tools::{PlatformCapabilities, ToolCapability};
let mobile = PlatformCapabilities::mobile();
assert!(platform_satisfies_caps(&mobile, &[]));
assert!(platform_satisfies_caps(&mobile, &["network".to_string()]));
assert!(!platform_satisfies_caps(&mobile, &["shell".to_string()]));
let desktop = PlatformCapabilities::from_capabilities([
ToolCapability::Network,
ToolCapability::FileSystem,
ToolCapability::ProcessSpawn,
]);
assert!(!platform_satisfies_caps(&desktop, &["mystery".to_string()]));
}
#[test]
fn discover_script_tools_empty_when_no_tools_dir() {
let home = tempfile::tempdir().unwrap();
temp_env::with_var("LEVIATH_HOME", Some(home.path().to_str().unwrap()), || {
let agent_dir = tempfile::tempdir().unwrap();
let blueprint = agent_dir.path().join("agent.leviath");
let (set, names, defs) =
discover_script_tools(blueprint.to_str().unwrap(), &HashSet::new(), &[], None);
assert!(set.is_empty() && names.is_empty() && defs.is_empty());
});
}
#[test]
fn discover_script_tools_handles_pathless_blueprint() {
let home = tempfile::tempdir().unwrap();
temp_env::with_var("LEVIATH_HOME", Some(home.path().to_str().unwrap()), || {
let (set, _n, _d) = discover_script_tools("", &HashSet::new(), &[], None);
assert!(set.is_empty());
});
}
use leviath_core::blueprint::ModelEntry;
fn model_cfg(models: Vec<(&str, &str)>) -> ModelConfig {
ModelConfig {
models: models
.into_iter()
.map(|(p, m)| ModelEntry {
provider: p.to_string(),
model: m.to_string(),
})
.collect(),
allow_user_default: true,
parameters: HashMap::new(),
request_timeout_secs: None,
}
}
fn registry_with(providers: &[&str]) -> ProviderRegistry {
let mut r = ProviderRegistry::new();
for p in providers {
r.register(p.to_string(), Arc::new(FakeProvider));
}
r
}
struct FakeProvider;
#[async_trait::async_trait]
impl leviath_providers::Provider for FakeProvider {
async fn infer(
&self,
_r: leviath_providers::InferenceRequest,
) -> leviath_providers::Result<leviath_providers::InferenceResponse> {
Err(leviath_providers::ProviderError::Other(
"test provider".to_string(),
))
}
async fn count_tokens(&self, _t: &str, _m: &str) -> usize {
1
}
fn max_context_tokens(&self, _m: &str) -> usize {
1000
}
fn name(&self) -> &str {
"fake"
}
fn capabilities(&self, _m: &str) -> leviath_providers::ModelCapabilities {
leviath_providers::ModelCapabilities::default()
}
}
use leviath_providers::Provider;
use leviath_runtime::components::AgentStatus;
use leviath_runtime::inference_pool::InferencePoolConfig;
use tokio::runtime::Handle;
fn coder_manifest() -> String {
crate::test_support::inline_coder_manifest()
}
fn test_world() -> (PipelineWorld, Arc<CliToolService>) {
let cli = Arc::new(CliToolService::new());
let world = PipelineWorld::new(
registry_with(&["anthropic", "openai", "ollama"]),
cli.clone(),
InferencePoolConfig::new(),
1,
None,
Handle::current(),
);
(world, cli)
}
fn spawn_args(path: &str) -> SpawnArgs {
SpawnArgs {
run_id: "run-x".to_string(),
blueprint_path: path.to_string(),
task: "do the thing".to_string(),
regions: HashMap::new(),
model: None,
workdir: std::env::temp_dir().to_string_lossy().to_string(),
metadata: HashMap::new(),
callback_url: None,
callback_secret: None,
yolo: false,
no_seed_commands: false,
allow: Vec::new(),
max_depth: None,
parent_run_id: None,
}
}
fn custom_region_manifest() -> &'static str {
"[agent]\nname = \"cr\"\nversion = \"0.1.0\"\ndescription = \"d\"\n\n\
[context.regions.brain]\nkind = \"custom\"\nscript = \"hooks/brain.rhai\"\nmax_tokens = 4000\n\n\
[stages.main]\nmodel = { provider = \"anthropic\", model = \"m\" }\n\n\
[stages.main.context.regions.stage_view]\nkind = \"custom\"\nscript = \"hooks/stage.rhai\"\nmax_tokens = 2000\n"
}
#[test]
fn resolve_region_scripts_empty_without_custom_regions() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("agent.leviath");
let bp = leviath_core::manifest::parse_manifest(
"[agent]\nname = \"plain\"\nversion = \"0.1.0\"\ndescription = \"d\"\n\n\
[stages.main]\nmodel = { provider = \"anthropic\", model = \"m\" }\n",
)
.unwrap();
let scripts = resolve_region_scripts(&bp, &manifest.to_string_lossy()).unwrap();
assert!(scripts.is_empty());
}
#[test]
fn resolve_region_scripts_collects_global_and_per_stage_layouts() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("agent.leviath");
std::fs::create_dir(dir.path().join("hooks")).unwrap();
std::fs::write(
dir.path().join("hooks/brain.rhai"),
"fn render(ctx) { \"b\" }",
)
.unwrap();
std::fs::write(
dir.path().join("hooks/stage.rhai"),
"fn render(ctx) { \"s\" }",
)
.unwrap();
let bp = leviath_core::manifest::parse_manifest(custom_region_manifest()).unwrap();
let scripts = resolve_region_scripts(&bp, &manifest.to_string_lossy()).unwrap();
assert_eq!(scripts.len(), 2);
assert!(scripts.contains_key("hooks/brain.rhai"));
assert!(scripts.contains_key("hooks/stage.rhai"));
}
#[test]
fn resolve_region_scripts_reads_a_shared_path_once() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("agent.leviath");
std::fs::create_dir(dir.path().join("hooks")).unwrap();
std::fs::write(
dir.path().join("hooks/shared.rhai"),
"fn render(ctx) { \"x\" }",
)
.unwrap();
let bp = leviath_core::manifest::parse_manifest(
"[agent]\nname = \"cr\"\nversion = \"0.1.0\"\ndescription = \"d\"\n\n\
[context.regions.a]\nkind = \"custom\"\nscript = \"hooks/shared.rhai\"\nmax_tokens = 2000\n\n\
[context.regions.b]\nkind = \"custom\"\nscript = \"hooks/shared.rhai\"\nmax_tokens = 2000\n\n\
[stages.main]\nmodel = { provider = \"anthropic\", model = \"m\" }\n",
)
.unwrap();
let scripts = resolve_region_scripts(&bp, &manifest.to_string_lossy()).unwrap();
assert_eq!(scripts.len(), 1);
}
#[test]
fn resolve_region_scripts_missing_file_is_a_hard_error() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("agent.leviath");
let bp = leviath_core::manifest::parse_manifest(custom_region_manifest()).unwrap();
let err = resolve_region_scripts(&bp, &manifest.to_string_lossy()).unwrap_err();
assert!(err.contains("region 'brain'"), "{err}");
assert!(err.contains("hooks/brain.rhai"), "{err}");
}
#[test]
fn resolve_region_scripts_uncompilable_script_is_a_hard_error() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("agent.leviath");
std::fs::create_dir(dir.path().join("hooks")).unwrap();
std::fs::write(dir.path().join("hooks/brain.rhai"), "fn render(ctx) {").unwrap();
std::fs::write(
dir.path().join("hooks/stage.rhai"),
"fn render(ctx) { \"s\" }",
)
.unwrap();
let bp = leviath_core::manifest::parse_manifest(custom_region_manifest()).unwrap();
let err = resolve_region_scripts(&bp, &manifest.to_string_lossy()).unwrap_err();
assert!(err.contains("failed to compile"), "{err}");
assert!(err.contains("region 'brain'"), "{err}");
}
#[tokio::test]
async fn build_agent_fails_fast_on_a_broken_custom_region_script() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("agent.leviath");
std::fs::write(&manifest, custom_region_manifest()).unwrap();
let (mut world, cli) = test_world();
let hub = InteractionHub::new();
let mcp = Arc::new(Mutex::new(leviath_mcp::ToolExecutor::new()));
let args = spawn_args(&manifest.to_string_lossy());
let err = build_agent(
world.world_mut(),
cli.as_ref(),
&Config::default(),
mcp,
&[],
&hub,
&args,
100,
sub_tx(),
)
.unwrap_err();
assert!(err.contains("region 'brain'"), "got: {err}");
assert!(err.contains("hooks/brain.rhai"), "got: {err}");
}
#[tokio::test]
async fn build_agent_rejects_a_workdir_that_is_missing_or_not_a_directory() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("agent.leviath");
std::fs::write(
&manifest,
"[agent]\nname = \"w\"\nversion = \"0.1.0\"\ndescription = \"d\"\n\n\
[stages.main]\nmodel = { provider = \"anthropic\", model = \"m\" }\n",
)
.unwrap();
let not_a_dir = dir.path().join("a-file");
std::fs::write(¬_a_dir, "x").unwrap();
for workdir in [
dir.path()
.join("does-not-exist")
.to_string_lossy()
.to_string(),
not_a_dir.to_string_lossy().to_string(),
] {
let (mut world, cli) = test_world();
let hub = InteractionHub::new();
let mcp = Arc::new(Mutex::new(leviath_mcp::ToolExecutor::new()));
let mut args = spawn_args(&manifest.to_string_lossy());
args.workdir = workdir.clone();
let err = build_agent(
world.world_mut(),
cli.as_ref(),
&Config::default(),
mcp,
&[],
&hub,
&args,
100,
sub_tx(),
)
.unwrap_err();
assert!(err.contains("workspace"), "got: {err}");
assert!(err.contains(&workdir), "got: {err}");
}
}
#[tokio::test]
async fn build_agent_attaches_taint_gate_when_security_enabled() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("agent.leviath");
std::fs::write(
&manifest,
"[agent]\nname = \"sec\"\nversion = \"0.1.0\"\ndescription = \"d\"\n\n\
[security]\ntaint_tracking = true\n\n\
[stages.main]\nmodel = { provider = \"anthropic\", model = \"m\" }\n",
)
.unwrap();
let (mut world, cli) = test_world();
let hub = InteractionHub::new();
let mcp = Arc::new(Mutex::new(leviath_mcp::ToolExecutor::new()));
let entity = build_agent(
world.world_mut(),
cli.as_ref(),
&Config::default(),
mcp,
&[],
&hub,
&spawn_args(&manifest.to_string_lossy()),
100,
sub_tx(),
)
.expect("spawn succeeds");
assert!(
world
.world()
.get::<leviath_runtime::TaintGate>(entity)
.is_some()
);
assert!(
world
.world()
.get::<leviath_runtime::pipeline::ToolSensitivities>(entity)
.is_some()
);
assert!(
world
.world()
.get::<leviath_runtime::components::ContextWindow>(entity)
.unwrap()
.overall_taint()
.is_some()
);
assert!(
world
.world()
.get::<leviath_runtime::components::GateAutoApprove>(entity)
.is_none()
);
}
#[tokio::test]
async fn build_agent_marks_root_runs_for_titling_but_not_subagents() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("agent.leviath");
std::fs::write(
&manifest,
"[agent]\nname = \"titler\"\nversion = \"0.1.0\"\ndescription = \"d\"\n\n\
[stages.main]\nmodel = { provider = \"anthropic\", model = \"m\" }\n",
)
.unwrap();
let (mut world, cli) = test_world();
let hub = InteractionHub::new();
let root = build_agent(
world.world_mut(),
cli.as_ref(),
&Config::default(),
Arc::new(Mutex::new(leviath_mcp::ToolExecutor::new())),
&[],
&hub,
&spawn_args(&manifest.to_string_lossy()),
100,
sub_tx(),
)
.expect("spawn succeeds");
assert!(
world
.world()
.get::<leviath_runtime::title::PendingTitle>(root)
.is_some()
);
let mut child_args = spawn_args(&manifest.to_string_lossy());
child_args.run_id = "run-child".to_string();
child_args.parent_run_id = Some("run-x".to_string());
let child = build_agent(
world.world_mut(),
cli.as_ref(),
&Config::default(),
Arc::new(Mutex::new(leviath_mcp::ToolExecutor::new())),
&[],
&hub,
&child_args,
100,
sub_tx(),
)
.expect("spawn succeeds");
assert!(
world
.world()
.get::<leviath_runtime::title::PendingTitle>(child)
.is_none()
);
let config = Config {
title: leviath_core::config::TitleConfig {
enabled: false,
provider: None,
model: None,
},
..Config::default()
};
let mut off_args = spawn_args(&manifest.to_string_lossy());
off_args.run_id = "run-off".to_string();
let off = build_agent(
world.world_mut(),
cli.as_ref(),
&config,
Arc::new(Mutex::new(leviath_mcp::ToolExecutor::new())),
&[],
&hub,
&off_args,
100,
sub_tx(),
)
.expect("spawn succeeds");
assert!(
world
.world()
.get::<leviath_runtime::title::PendingTitle>(off)
.is_none()
);
}
#[tokio::test]
async fn build_agent_applies_policy_mcp_overrides_to_the_gate() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("agent.leviath");
std::fs::write(
&manifest,
"[agent]\nname = \"sec-ov\"\nversion = \"0.1.0\"\ndescription = \"d\"\n\n\
[security]\ntaint_tracking = true\n\n\
[stages.main]\nmodel = { provider = \"anthropic\", model = \"m\" }\n",
)
.unwrap();
let (mut world, cli) = test_world();
world
.world_mut()
.insert_resource(leviath_runtime::pipeline::PolicyGate(
leviath_core::PolicyConfig {
allowlist: Vec::new(),
mcp_overrides: HashMap::from([(
"notes.share".to_string(),
leviath_core::policy::McpToolOverride {
sensitivity: None,
direction: Some("outbound".to_string()),
clearance: Some(leviath_core::TaintLevel::Private),
},
)]),
},
));
let hub = InteractionHub::new();
let mcp = Arc::new(Mutex::new(leviath_mcp::ToolExecutor::new()));
let entity = build_agent(
world.world_mut(),
cli.as_ref(),
&Config::default(),
mcp,
&[],
&hub,
&spawn_args(&manifest.to_string_lossy()),
100,
sub_tx(),
)
.expect("spawn succeeds");
let gate = world
.world()
.get::<leviath_runtime::TaintGate>(entity)
.expect("gate attached");
let classification = gate.tool_classification("notes.share");
assert_eq!(
classification.direction,
leviath_core::taint::ToolDirection::Outbound
);
assert_eq!(classification.clearance, leviath_core::TaintLevel::Private);
}
#[tokio::test]
async fn build_agent_errors_when_required_caller_region_missing() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("agent.leviath");
std::fs::write(
&manifest,
"[agent]\nname = \"needs\"\nversion = \"0.1.0\"\ndescription = \"d\"\n\n\
[stages.main]\nmodel = { provider = \"anthropic\", model = \"m\" }\n\n\
[context.regions]\n\
spec = { kind = \"pinned\", max_tokens = 2000, seed = \"input\", required = true }\n\
conversation = { kind = \"sliding_window\", max_items = 20, max_tokens = 10000 }\n",
)
.unwrap();
let (mut world, cli) = test_world();
let hub = InteractionHub::new();
let mcp = Arc::new(Mutex::new(leviath_mcp::ToolExecutor::new()));
let err = build_agent(
world.world_mut(),
cli.as_ref(),
&Config::default(),
mcp,
&[],
&hub,
&spawn_args(&manifest.to_string_lossy()),
100,
sub_tx(),
)
.unwrap_err();
assert!(err.contains("spec"), "got: {err}");
}
#[tokio::test]
async fn build_agent_attaches_sandbox_when_configured() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("agent.leviath");
std::fs::write(
&manifest,
"[agent]\nname = \"sb\"\nversion = \"0.1.0\"\ndescription = \"d\"\n\n\
[sandbox]\nkind = \"namespace\"\non_unavailable = \"warn\"\n\n\
[stages.main]\nmodel = { provider = \"anthropic\", model = \"m\" }\n",
)
.unwrap();
let (mut world, cli) = test_world();
let hub = InteractionHub::new();
let mcp = Arc::new(Mutex::new(leviath_mcp::ToolExecutor::new()));
let entity = build_agent(
world.world_mut(),
cli.as_ref(),
&Config::default(),
mcp,
&[],
&hub,
&spawn_args(&manifest.to_string_lossy()),
100,
sub_tx(),
)
.expect("spawn succeeds");
let state = cli.take(entity).expect("state registered");
assert!(state.sandbox.is_some(), "sandbox manager attached");
}
#[tokio::test]
async fn build_agent_errors_when_sandbox_runtime_unavailable() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("agent.leviath");
std::fs::write(
&manifest,
"[agent]\nname = \"sb\"\nversion = \"0.1.0\"\ndescription = \"d\"\n\n\
[sandbox]\nkind = \"container\"\nimage = \"x\"\nengine = \"leviath-no-such-engine\"\n\n\
[stages.main]\nmodel = { provider = \"anthropic\", model = \"m\" }\n",
)
.unwrap();
let (mut world, cli) = test_world();
let hub = InteractionHub::new();
let mcp = Arc::new(Mutex::new(leviath_mcp::ToolExecutor::new()));
let err = build_agent(
world.world_mut(),
cli.as_ref(),
&Config::default(),
mcp,
&[],
&hub,
&spawn_args(&manifest.to_string_lossy()),
100,
sub_tx(),
)
.expect_err("a nonexistent engine can't start the container");
assert!(err.contains("sandbox unavailable"), "got: {err}");
}
#[tokio::test]
async fn build_agent_yolo_attaches_gate_auto_approve_when_taint_on() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("agent.leviath");
std::fs::write(
&manifest,
"[agent]\nname = \"sec\"\nversion = \"0.1.0\"\ndescription = \"d\"\n\n\
[security]\ntaint_tracking = true\n\n\
[stages.main]\nmodel = { provider = \"anthropic\", model = \"m\" }\n",
)
.unwrap();
let (mut world, cli) = test_world();
let hub = InteractionHub::new();
let mcp = Arc::new(Mutex::new(leviath_mcp::ToolExecutor::new()));
let mut args = spawn_args(&manifest.to_string_lossy());
args.yolo = true;
let entity = build_agent(
world.world_mut(),
cli.as_ref(),
&Config::default(),
mcp,
&[],
&hub,
&args,
100,
sub_tx(),
)
.expect("spawn succeeds");
assert!(
world
.world()
.get::<leviath_runtime::components::GateAutoApprove>(entity)
.is_some()
);
assert!(
world
.world()
.get::<leviath_runtime::components::InteractionAutoApprove>(entity)
.is_some()
);
assert!(cli.take(entity).expect("tool state registered").unattended);
assert!(
world
.world()
.get::<RunMetadata>(entity)
.expect("run metadata attached")
.unattended
);
}
#[tokio::test]
async fn build_agent_yolo_leaves_the_run_active_and_unattended() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("agent.leviath");
std::fs::write(
&manifest,
"[agent]\nname = \"a\"\nversion = \"0.1.0\"\ndescription = \"d\"\n\n\
[stages.main]\nmodel = { provider = \"anthropic\", model = \"m\" }\n",
)
.unwrap();
let (mut world, cli) = test_world();
let mut args = spawn_args(&manifest.to_string_lossy());
args.yolo = true;
let entity = build_agent(
world.world_mut(),
cli.as_ref(),
&Config::default(),
Arc::new(Mutex::new(leviath_mcp::ToolExecutor::new())),
&[],
&InteractionHub::new(),
&args,
100,
sub_tx(),
)
.expect("spawn succeeds");
assert_eq!(world.agent_status(entity), Some(AgentStatus::Active));
let meta = world
.world()
.get::<RunMetadata>(entity)
.expect("run metadata attached");
assert!(meta.unattended);
}
#[tokio::test]
async fn build_agent_carries_required_tools_into_the_tool_state() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("agent.leviath");
std::fs::write(
&manifest,
"[agent]\nname = \"asks\"\nversion = \"0.1.0\"\ndescription = \"d\"\n\n\
[stages.main]\nmodel = { provider = \"anthropic\", model = \"m\" }\n\
available_tools = [\"read_file\", \"ask_user_text\"]\n\
required_tools = [\"ask_user_text\"]\n",
)
.unwrap();
let (mut world, cli) = test_world();
let mut args = spawn_args(&manifest.to_string_lossy());
args.yolo = true;
let entity = build_agent(
world.world_mut(),
cli.as_ref(),
&Config::default(),
Arc::new(Mutex::new(leviath_mcp::ToolExecutor::new())),
&[],
&InteractionHub::new(),
&args,
100,
sub_tx(),
)
.expect("spawn succeeds");
let state = cli.take(entity).expect("tool state registered");
assert!(
state
.stage_required
.lock()
.unwrap()
.contains("ask_user_text")
);
assert_eq!(state.stage_required_by_index.len(), 1);
}
#[tokio::test]
async fn build_agent_without_yolo_keeps_prompts_interactive() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("agent.leviath");
std::fs::write(
&manifest,
"[agent]\nname = \"plain\"\nversion = \"0.1.0\"\ndescription = \"d\"\n\n\
[stages.main]\nmodel = { provider = \"anthropic\", model = \"m\" }\n",
)
.unwrap();
let (mut world, cli) = test_world();
let hub = InteractionHub::new();
let mcp = Arc::new(Mutex::new(leviath_mcp::ToolExecutor::new()));
let entity = build_agent(
world.world_mut(),
cli.as_ref(),
&Config::default(),
mcp,
&[],
&hub,
&spawn_args(&manifest.to_string_lossy()),
100,
sub_tx(),
)
.expect("spawn succeeds");
assert!(
world
.world()
.get::<leviath_runtime::components::InteractionAutoApprove>(entity)
.is_none()
);
assert!(!cli.take(entity).expect("tool state registered").unattended);
}
#[tokio::test]
async fn build_agent_no_security_block_leaves_taint_off_by_default() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("agent.leviath");
std::fs::write(
&manifest,
"[agent]\nname = \"plain\"\nversion = \"0.1.0\"\ndescription = \"d\"\n\n\
[stages.main]\nmodel = { provider = \"anthropic\", model = \"m\" }\n",
)
.unwrap();
let (mut world, cli) = test_world();
let hub = InteractionHub::new();
let mcp = Arc::new(Mutex::new(leviath_mcp::ToolExecutor::new()));
let entity = build_agent(
world.world_mut(),
cli.as_ref(),
&Config::default(), mcp,
&[],
&hub,
&spawn_args(&manifest.to_string_lossy()),
100,
sub_tx(),
)
.expect("spawn succeeds");
assert!(
world
.world()
.get::<leviath_runtime::TaintGate>(entity)
.is_none(),
"no [security] block + global off ⇒ no taint gate"
);
}
async fn spawned_no_output_tools(manifest_body: &str) -> bool {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("agent.leviath");
std::fs::write(&manifest, manifest_body).unwrap();
let (mut world, cli) = test_world();
let hub = InteractionHub::new();
let mcp = Arc::new(Mutex::new(leviath_mcp::ToolExecutor::new()));
let entity = build_agent(
world.world_mut(),
cli.as_ref(),
&Config::default(),
mcp,
&[],
&hub,
&spawn_args(&manifest.to_string_lossy()),
100,
sub_tx(),
)
.expect("spawn succeeds");
world
.world()
.get::<leviath_runtime::persistence::RunOutcomeFlags>(entity)
.expect("build_agent attaches run outcome flags")
.0
.no_output_tools
}
#[tokio::test]
async fn build_agent_records_whether_the_blueprint_can_write_at_all() {
assert!(!spawned_no_output_tools(&coder_manifest()).await);
assert!(
spawned_no_output_tools(
"[agent]\nname = \"router\"\nversion = \"0.1.0\"\ndescription = \"d\"\n\n\
[stages.triage]\nmodel = { provider = \"anthropic\", model = \"m\" }\n\
available_tools = [\"read_file\", \"spawn_agent\"]\n",
)
.await
);
}
#[tokio::test]
async fn build_agent_spawns_registers_and_wires_tools() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("agent.leviath");
std::fs::write(&manifest, coder_manifest()).unwrap();
let (mut world, cli) = test_world();
let hub = InteractionHub::new();
let mcp = Arc::new(Mutex::new(leviath_mcp::ToolExecutor::new()));
let entity = build_agent(
world.world_mut(),
cli.as_ref(),
&Config::default(),
mcp,
&[],
&hub,
&spawn_args(&manifest.to_string_lossy()),
100,
sub_tx(),
)
.expect("spawn succeeds");
assert_eq!(world.agent_status(entity), Some(AgentStatus::Active));
let md = world
.world()
.get::<RunMetadata>(entity)
.expect("run metadata");
assert_eq!(md.run_id, "run-x");
assert_eq!(md.agent_name, "coder");
let out = leviath_runtime::pipeline::ToolService::exec_for(
cli.as_ref(),
entity,
vec![leviath_providers::ToolCall {
id: "c1".to_string(),
name: "list_dir".to_string(),
arguments: serde_json::json!({"path": "."}),
thought_signature: None,
}],
leviath_runtime::pipeline::noop_progress(),
)()
.await;
assert_eq!(out[0].0, "c1");
assert!(!out[0].1.contains("no tool state"));
}
#[tokio::test]
async fn build_agent_tags_dynamic_tools_agent() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("agent.leviath");
std::fs::write(
&manifest,
coder_manifest().replace("[agent]", "[agent]\ndynamic_tools = true"),
)
.unwrap();
let (mut world, cli) = test_world();
let hub = InteractionHub::new();
let mcp = Arc::new(Mutex::new(leviath_mcp::ToolExecutor::new()));
let entity = build_agent(
world.world_mut(),
cli.as_ref(),
&Config::default(),
mcp,
&[],
&hub,
&spawn_args(&manifest.to_string_lossy()),
100,
sub_tx(),
)
.expect("spawn succeeds");
assert!(
world
.world()
.get::<leviath_runtime::pipeline::DynamicTools>(entity)
.is_some(),
"dynamic_tools agent must carry the DynamicTools marker"
);
assert!(
leviath_runtime::pipeline::ToolService::refresh_tools(cli.as_ref(), entity, 0)
.is_some()
);
}
#[tokio::test]
async fn build_agent_applies_yolo_allow_and_max_depth() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("agent.leviath");
std::fs::write(&manifest, coder_manifest()).unwrap();
let (mut world, cli) = test_world();
let hub = InteractionHub::new();
let mcp = Arc::new(Mutex::new(leviath_mcp::ToolExecutor::new()));
let config = Config {
tool_permissions: HashMap::from([(
"read_file".to_string(),
crate::config::ToolPolicy::Deny,
)]),
..Default::default()
};
let mut args = spawn_args(&manifest.to_string_lossy());
args.yolo = true;
args.allow = vec!["read_file".to_string()];
args.max_depth = Some(7);
let entity = build_agent(
world.world_mut(),
cli.as_ref(),
&config,
mcp,
&[],
&hub,
&args,
100,
sub_tx(),
)
.expect("spawn succeeds");
assert_eq!(world.agent_status(entity), Some(AgentStatus::Active));
let out = leviath_runtime::pipeline::ToolService::exec_for(
cli.as_ref(),
entity,
vec![leviath_providers::ToolCall {
id: "c1".to_string(),
name: "read_file".to_string(),
arguments: serde_json::json!({"path": "/no/such/file"}),
thought_signature: None,
}],
leviath_runtime::pipeline::noop_progress(),
)()
.await;
let result = out[0].1.clone();
assert!(
result.contains("[denied]"),
"a configured deny must survive --yolo, got: {result}"
);
let out = leviath_runtime::pipeline::ToolService::exec_for(
cli.as_ref(),
entity,
vec![leviath_providers::ToolCall {
id: "c2".to_string(),
name: "list_dir".to_string(),
arguments: serde_json::json!({"path": "."}),
thought_signature: None,
}],
leviath_runtime::pipeline::noop_progress(),
)()
.await;
let result = out[0].1.clone();
assert!(
!result.contains("[denied]"),
"--yolo must still waive approval where nothing denies, got: {result}"
);
}
#[tokio::test]
async fn build_agent_honors_agent_level_tool_permissions() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("agent.leviath");
std::fs::write(
&manifest,
"[agent]\nname = \"perm\"\nversion = \"0.1.0\"\ndescription = \"d\"\n\n\
[tool_permissions]\nread_file = \"deny\"\n\n\
[stages.main]\nmodel = { provider = \"anthropic\", model = \"m\" }\n",
)
.unwrap();
let (mut world, cli) = test_world();
let hub = InteractionHub::new();
let mcp = Arc::new(Mutex::new(leviath_mcp::ToolExecutor::new()));
let entity = build_agent(
world.world_mut(),
cli.as_ref(),
&Config::default(),
mcp,
&[],
&hub,
&spawn_args(&manifest.to_string_lossy()),
100,
sub_tx(),
)
.expect("spawn succeeds");
let out = leviath_runtime::pipeline::ToolService::exec_for(
cli.as_ref(),
entity,
vec![leviath_providers::ToolCall {
id: "c1".to_string(),
name: "read_file".to_string(),
arguments: serde_json::json!({"path": "/no/such/file"}),
thought_signature: None,
}],
leviath_runtime::pipeline::noop_progress(),
)()
.await;
assert!(
out[0].1.contains("[denied]"),
"agent-level deny should block read_file"
);
}
#[tokio::test]
async fn build_agent_script_host_honors_agent_level_grants() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("agent.leviath");
std::fs::write(
&manifest,
"[agent]\nname = \"scriptperm\"\nversion = \"0.1.0\"\ndescription = \"d\"\n\n\
[stages.main]\nmodel = { provider = \"anthropic\", model = \"m\" }\n",
)
.unwrap();
let mut config = Config::default();
config.agent_tool_permissions.insert(
"scriptperm".to_string(),
HashMap::from([("write_file".to_string(), crate::config::ToolPolicy::Allow)]),
);
let (mut world, cli) = test_world();
let hub = InteractionHub::new();
let mcp = Arc::new(Mutex::new(leviath_mcp::ToolExecutor::new()));
let mut args = spawn_args(&manifest.to_string_lossy());
args.workdir = dir.path().to_string_lossy().to_string();
let entity = build_agent(
world.world_mut(),
cli.as_ref(),
&config,
mcp,
&[],
&hub,
&args,
100,
sub_tx(),
)
.expect("spawn succeeds");
let state = cli.take(entity).expect("tool state registered at spawn");
state
.script_host
.write_file("granted.txt", "ok")
.expect("agent-level write_file grant must reach the script host");
assert_eq!(
std::fs::read_to_string(dir.path().join("granted.txt")).unwrap(),
"ok"
);
}
#[tokio::test]
async fn build_agent_applies_default_max_iterations_only_when_stage_omits_it() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("agent.leviath");
std::fs::write(
&manifest,
"[agent]\nname = \"iters\"\nversion = \"0.1.0\"\ndescription = \"d\"\n\n\
[stages.main]\nmodel = { provider = \"anthropic\", model = \"m\" }\n\n\
[stages.capped]\nmax_iterations = 3\n\
model = { provider = \"anthropic\", model = \"m\" }\n",
)
.unwrap();
let (mut world, cli) = test_world();
let hub = InteractionHub::new();
let mcp = Arc::new(Mutex::new(leviath_mcp::ToolExecutor::new()));
let config = Config {
limits: crate::config::LimitsConfig {
default_max_iterations: Some(42),
..Default::default()
},
..Default::default()
};
let entity = build_agent(
world.world_mut(),
cli.as_ref(),
&config,
mcp,
&[],
&hub,
&spawn_args(&manifest.to_string_lossy()),
100,
sub_tx(),
)
.expect("spawn succeeds");
let bp = world
.world()
.get::<leviath_runtime::pipeline::AgentBlueprint>(entity)
.expect("blueprint");
let by_name = |n: &str| {
bp.0.stages
.iter()
.find(|s| s.name == n)
.unwrap()
.max_iterations
};
assert_eq!(by_name("main"), Some(42));
assert_eq!(by_name("capped"), Some(3));
}
#[tokio::test]
async fn build_agent_leaves_max_iterations_unset_when_config_default_is_none() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("agent.leviath");
std::fs::write(
&manifest,
"[agent]\nname = \"nolimit\"\nversion = \"0.1.0\"\ndescription = \"d\"\n\n\
[stages.main]\nmodel = { provider = \"anthropic\", model = \"m\" }\n",
)
.unwrap();
let (mut world, cli) = test_world();
let hub = InteractionHub::new();
let mcp = Arc::new(Mutex::new(leviath_mcp::ToolExecutor::new()));
let config = Config {
limits: crate::config::LimitsConfig {
default_max_iterations: None,
..Default::default()
},
..Default::default()
};
let entity = build_agent(
world.world_mut(),
cli.as_ref(),
&config,
mcp,
&[],
&hub,
&spawn_args(&manifest.to_string_lossy()),
100,
sub_tx(),
)
.expect("spawn succeeds");
let bp = world
.world()
.get::<leviath_runtime::pipeline::AgentBlueprint>(entity)
.expect("blueprint");
assert_eq!(bp.0.stages[0].max_iterations, None);
}
#[tokio::test]
async fn fake_provider_methods_are_exercised() {
let p = FakeProvider;
assert_eq!(p.name(), "fake");
assert_eq!(p.count_tokens("t", "m").await, 1);
assert_eq!(p.max_context_tokens("m"), 1000);
let _ = p.capabilities("m");
assert!(
p.infer(leviath_providers::InferenceRequest {
system: vec![],
messages: vec![],
model: "m".to_string(),
max_tokens: 1,
temperature: 0.0,
tools: vec![],
extra: serde_json::Value::Null,
request_timeout_secs: None,
})
.await
.is_err()
);
}
use std::path::Path;
fn blueprint_declaring(read_paths: &[&str]) -> Blueprint {
let stage = leviath_core::Stage::new("s".to_string(), model_cfg(vec![("anthropic", "m")]));
let layout = leviath_core::layout::ContextLayout::new(vec![], 1000);
let mut bp = Blueprint::new("cto".to_string(), "d".to_string(), vec![stage], layout);
if !read_paths.is_empty() {
bp.read_paths = Some(leviath_core::ReadPathsConfig {
allow: read_paths.iter().map(|s| s.to_string()).collect(),
});
}
bp
}
#[test]
fn read_path_grant_counts_are_recorded_for_a_declaring_blueprint() {
let bp = blueprint_declaring(&["/data/runs", "/data/docs"]);
let mut config = Config::default();
config.security.read_paths = vec!["/data/runs".to_string()];
let counts = read_path_grant_counts(&bp, &config, Path::new("/w")).expect("declares paths");
assert_eq!(counts.declared, 2);
assert_eq!(counts.granted, 1);
assert!(
read_path_grant_counts(&blueprint_declaring(&[]), &config, Path::new("/w")).is_none()
);
let mut broken = Config::default();
broken.security.read_paths = vec!["regex:relative/.*".to_string()];
assert!(read_path_grant_counts(&bp, &broken, Path::new("/w")).is_none());
}
#[test]
fn read_path_policy_is_inactive_without_declarations() {
let bp = blueprint_declaring(&[]);
let (policy, warning) =
build_read_path_policy(&bp, &Config::default(), Path::new("/w")).unwrap();
assert!(!policy.is_active());
assert!(warning.is_none());
let mut bp = blueprint_declaring(&[]);
bp.read_paths = Some(leviath_core::ReadPathsConfig { allow: vec![] });
let (policy, warning) =
build_read_path_policy(&bp, &Config::default(), Path::new("/w")).unwrap();
assert!(!policy.is_active());
assert!(warning.is_none());
}
#[test]
fn read_path_policy_warns_when_nothing_grants() {
let bp = blueprint_declaring(&["/data/runs", "glob:/data/docs/**"]);
let (policy, warning) =
build_read_path_policy(&bp, &Config::default(), Path::new("/w")).unwrap();
assert!(policy.is_active());
assert!(!policy.allow_blueprint);
assert!(policy.grants.is_empty());
let warning = warning.expect("ungranted declarations must warn");
assert!(warning.contains("allow_blueprint_read_paths"), "{warning}");
assert!(warning.contains("[agent_read_paths.cto]"), "{warning}");
assert!(warning.contains("\"/data/runs\""), "{warning}");
assert!(warning.contains("\"glob:/data/docs/**\""), "{warning}");
}
#[test]
fn read_path_policy_is_quiet_when_granted() {
let bp = blueprint_declaring(&["/data/runs"]);
let mut config = Config::default();
config.agent_read_paths.insert(
"cto".to_string(),
crate::config::ReadPathGrants {
allow: vec!["/data/runs".to_string()],
},
);
let (policy, warning) = build_read_path_policy(&bp, &config, Path::new("/w")).unwrap();
assert!(policy.is_active());
assert!(!policy.grants.is_empty());
assert!(warning.is_none());
}
#[test]
fn read_path_policy_is_quiet_under_the_override() {
let bp = blueprint_declaring(&["/data/runs"]);
let mut config = Config::default();
config.security.allow_blueprint_read_paths = true;
let (policy, warning) = build_read_path_policy(&bp, &config, Path::new("/w")).unwrap();
assert!(policy.allow_blueprint);
assert!(warning.is_none());
}
#[test]
fn read_path_policy_rejects_bad_entries_loudly() {
let bp = blueprint_declaring(&["glob:["]);
let err = build_read_path_policy(&bp, &Config::default(), Path::new("/w")).unwrap_err();
assert!(err.contains("agent 'cto' [read_paths]"), "{err}");
let bp = blueprint_declaring(&["/data/runs"]);
let mut config = Config::default();
config.security.read_paths = vec!["regex:(".to_string()];
let err = build_read_path_policy(&bp, &config, Path::new("/w")).unwrap_err();
assert!(err.contains("config.toml"), "{err}");
}
#[test]
fn read_sensitivities_bump_only_the_read_tools_when_granted() {
use leviath_core::TaintLevel;
let base = || {
HashMap::from([
("read_file".to_string(), TaintLevel::Internal),
("list_dir".to_string(), TaintLevel::Public),
("write_file".to_string(), TaintLevel::Internal),
])
};
let mut map = base();
bump_read_sensitivities(&mut map, true);
assert_eq!(map.get("read_file"), Some(&TaintLevel::Private));
assert_eq!(map.get("list_dir"), Some(&TaintLevel::Private));
assert_eq!(map.get("write_file"), Some(&TaintLevel::Internal));
assert!(!map.contains_key("read_files"));
let mut map = base();
bump_read_sensitivities(&mut map, false);
assert_eq!(map, base(), "no grant, no change");
}
#[tokio::test]
async fn build_agent_read_error() {
let (mut world, cli) = test_world();
let hub = InteractionHub::new();
let mcp = Arc::new(Mutex::new(leviath_mcp::ToolExecutor::new()));
let err = build_agent(
world.world_mut(),
cli.as_ref(),
&Config::default(),
mcp,
&[],
&hub,
&spawn_args("/no/such/manifest.leviath"),
100,
sub_tx(),
)
.unwrap_err();
assert!(err.contains("read manifest"));
}
const OVERSIZED_MANIFEST: &str = r#"
[agent]
name = "tiny"
version = "0.1.0"
description = "d"
entry_stage = "main"
[context.regions]
task = { kind = "pinned", max_tokens = 20 }
[stages.main]
mode = "autonomous"
model = { models = [{ provider = "anthropic", model = "m" }] }
description = "d"
available_tools = []
system_prompt = "SYSTEM_PROMPT_PLACEHOLDER"
"#;
#[tokio::test]
async fn build_agent_propagates_spawn_error() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("tiny.leviath");
let content = OVERSIZED_MANIFEST.replace("SYSTEM_PROMPT_PLACEHOLDER", &"x ".repeat(5000));
std::fs::write(&manifest, content).unwrap();
let (mut world, cli) = test_world();
let hub = InteractionHub::new();
let mcp = Arc::new(Mutex::new(leviath_mcp::ToolExecutor::new()));
let result = build_agent(
world.world_mut(),
cli.as_ref(),
&Config::default(),
mcp,
&[],
&hub,
&spawn_args(&manifest.to_string_lossy()),
100,
sub_tx(),
);
assert!(result.is_err(), "expected spawn error, got {result:?}");
}
#[tokio::test]
async fn build_agent_refuses_a_manifest_with_no_usable_provider() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("ghostly.leviath");
std::fs::write(
&manifest,
r#"
[agent]
name = "ghostly"
version = "0.1.0"
description = "d"
entry_stage = "main"
[context.regions]
task = { kind = "pinned", max_tokens = 4000 }
[stages.main]
mode = "autonomous"
model = { models = [{ provider = "ghost", model = "m" }], allow_user_default = false }
description = "d"
available_tools = []
"#,
)
.unwrap();
let (mut world, cli) = test_world();
let hub = InteractionHub::new();
let mcp = Arc::new(Mutex::new(leviath_mcp::ToolExecutor::new()));
let err = build_agent(
world.world_mut(),
cli.as_ref(),
&Config::default(),
mcp,
&[],
&hub,
&spawn_args(&manifest.to_string_lossy()),
100,
sub_tx(),
)
.unwrap_err();
assert!(err.contains("main"), "names the stage: {err}");
assert!(err.contains("ghost"), "names what it tried: {err}");
}
#[tokio::test]
async fn build_agent_invalid_blueprint() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("bad.leviath");
std::fs::write(
&manifest,
r#"
[agent]
name = "bad"
version = "0.1.0"
description = "d"
entry_stage = "ghost"
[context.regions]
task = { kind = "pinned", max_tokens = 4000 }
[stages.main]
mode = "autonomous"
model = { models = [{ provider = "anthropic", model = "m" }] }
description = "d"
available_tools = []
"#,
)
.unwrap();
let (mut world, cli) = test_world();
let hub = InteractionHub::new();
let mcp = Arc::new(Mutex::new(leviath_mcp::ToolExecutor::new()));
let err = build_agent(
world.world_mut(),
cli.as_ref(),
&Config::default(),
mcp,
&[],
&hub,
&spawn_args(&manifest.to_string_lossy()),
100,
sub_tx(),
)
.unwrap_err();
assert!(err.contains("invalid blueprint"));
}
#[tokio::test]
async fn build_agent_without_entry_stage_and_with_compaction() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("mini.leviath");
std::fs::write(
&manifest,
r#"
[agent]
name = "mini"
version = "0.1.0"
description = "d"
[compaction]
provider = "anthropic"
model = "claude-x"
[context.regions]
task = { kind = "pinned", max_tokens = 4000 }
[stages.main]
mode = "autonomous"
model = { models = [{ provider = "anthropic", model = "m" }] }
description = "d"
available_tools = []
system_prompt = "be brief"
"#,
)
.unwrap();
let (mut world, cli) = test_world();
let hub = InteractionHub::new();
let mcp = Arc::new(Mutex::new(leviath_mcp::ToolExecutor::new()));
let entity = build_agent(
world.world_mut(),
cli.as_ref(),
&Config::default(),
mcp,
&[],
&hub,
&spawn_args(&manifest.to_string_lossy()),
100,
sub_tx(),
)
.expect("spawn succeeds");
assert_eq!(world.agent_status(entity), Some(AgentStatus::Active));
assert!(world.world().get::<CompactionSettings>(entity).is_some());
}
fn write_read_paths_manifest(dir: &std::path::Path, allow: &str) -> std::path::PathBuf {
let manifest = dir.join("reader.leviath");
std::fs::write(
&manifest,
format!(
r#"
[agent]
name = "reader"
version = "0.1.0"
description = "d"
[read_paths]
allow = [{allow}]
[context.regions]
task = {{ kind = "pinned", max_tokens = 4000 }}
[stages.main]
mode = "autonomous"
model = {{ models = [{{ provider = "anthropic", model = "m" }}] }}
description = "d"
available_tools = []
system_prompt = "be brief"
"#
),
)
.unwrap();
manifest
}
#[tokio::test]
async fn build_agent_wires_granted_read_paths() {
let dir = tempfile::tempdir().unwrap();
let manifest = write_read_paths_manifest(dir.path(), "\"/tmp\"");
let (mut world, cli) = test_world();
let hub = InteractionHub::new();
let mcp = Arc::new(Mutex::new(leviath_mcp::ToolExecutor::new()));
let mut config = Config::default();
config.security.allow_blueprint_read_paths = true;
config.taint_tracking = true;
let entity = build_agent(
world.world_mut(),
cli.as_ref(),
&config,
mcp,
&[],
&hub,
&spawn_args(&manifest.to_string_lossy()),
100,
sub_tx(),
)
.expect("spawn succeeds");
assert_eq!(world.agent_status(entity), Some(AgentStatus::Active));
}
#[tokio::test]
async fn build_agent_wires_ungranted_read_paths() {
let dir = tempfile::tempdir().unwrap();
let manifest = write_read_paths_manifest(dir.path(), "\"/tmp\"");
let (mut world, cli) = test_world();
let hub = InteractionHub::new();
let mcp = Arc::new(Mutex::new(leviath_mcp::ToolExecutor::new()));
let entity = build_agent(
world.world_mut(),
cli.as_ref(),
&Config::default(),
mcp,
&[],
&hub,
&spawn_args(&manifest.to_string_lossy()),
100,
sub_tx(),
)
.expect("spawn succeeds even when nothing grants the declaration");
assert_eq!(world.agent_status(entity), Some(AgentStatus::Active));
}
#[tokio::test]
async fn build_agent_rejects_a_malformed_config_grant() {
let dir = tempfile::tempdir().unwrap();
let manifest = write_read_paths_manifest(dir.path(), "\"/tmp\"");
let (mut world, cli) = test_world();
let hub = InteractionHub::new();
let mcp = Arc::new(Mutex::new(leviath_mcp::ToolExecutor::new()));
let mut config = Config::default();
config.security.read_paths = vec!["glob:[".to_string()];
let err = build_agent(
world.world_mut(),
cli.as_ref(),
&config,
mcp,
&[],
&hub,
&spawn_args(&manifest.to_string_lossy()),
100,
sub_tx(),
)
.expect_err("a broken config grant must fail the spawn");
assert!(err.contains("config.toml"), "{err}");
}
#[tokio::test]
async fn build_agent_parse_error() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("bad.leviath");
std::fs::write(&manifest, "this is not valid toml : : :").unwrap();
let (mut world, cli) = test_world();
let hub = InteractionHub::new();
let mcp = Arc::new(Mutex::new(leviath_mcp::ToolExecutor::new()));
let err = build_agent(
world.world_mut(),
cli.as_ref(),
&Config::default(),
mcp,
&[],
&hub,
&spawn_args(&manifest.to_string_lossy()),
100,
sub_tx(),
)
.unwrap_err();
assert!(err.contains("parse manifest"));
}
fn bp(regions_toml: &str) -> Blueprint {
let toml = format!(
r#"
[agent]
name = "seedy"
[stages.main]
mode = "autonomous"
[stages.main.model]
provider = "anthropic"
model = "claude-sonnet-5"
[context.regions]
{regions_toml}
conversation = {{ kind = "sliding_window", max_items = 20, max_tokens = 10000 }}
"#
);
leviath_core::manifest::parse_manifest(&toml).unwrap()
}
fn args_with(task: &str, regions: HashMap<String, String>, workdir: &str) -> SpawnArgs {
SpawnArgs {
run_id: "r".to_string(),
blueprint_path: "/bp".to_string(),
task: task.to_string(),
regions,
model: None,
workdir: workdir.to_string(),
metadata: HashMap::new(),
callback_url: None,
callback_secret: None,
yolo: false,
no_seed_commands: false,
allow: Vec::new(),
max_depth: None,
parent_run_id: None,
}
}
fn seed_policy() -> SeedCommandPolicy {
SeedCommandPolicy::disabled()
}
fn stub_policy(result: Result<String, String>) -> SeedCommandPolicy {
SeedCommandPolicy {
allowed: true,
timeout: std::time::Duration::from_secs(1),
runner: std::sync::Arc::new(move |_, _, _| result.clone()),
}
}
#[test]
fn resolve_seeds_fills_task_and_caller_input() {
let bp = bp(
r#"task = { kind = "pinned", max_tokens = 4000, seed = "task_input" }
criteria = { kind = "pinned", max_tokens = 2000, seed = "input" }"#,
);
let args = args_with(
"build it",
HashMap::from([("criteria".to_string(), "be safe".to_string())]),
"/tmp",
);
let seeds = resolve_seeds(&bp, &args, "/tmp", &seed_policy()).unwrap();
assert_eq!(seeds.get("task").map(String::as_str), Some("build it"));
assert_eq!(seeds.get("criteria").map(String::as_str), Some("be safe"));
}
#[test]
fn resolve_seeds_required_caller_input_missing_is_error() {
let bp =
bp(r#"spec = { kind = "pinned", max_tokens = 2000, seed = "input", required = true }"#);
let args = args_with("t", HashMap::new(), "/tmp");
let err = resolve_seeds(&bp, &args, "/tmp", &seed_policy()).unwrap_err();
assert!(err.contains("spec"), "got: {err}");
}
#[test]
fn resolve_seeds_optional_caller_input_missing_is_omitted() {
let bp = bp(r#"notes = { kind = "pinned", max_tokens = 2000, seed = "input" }"#);
let args = args_with("t", HashMap::new(), "/tmp");
let seeds = resolve_seeds(&bp, &args, "/tmp", &seed_policy()).unwrap();
assert!(!seeds.contains_key("notes"));
}
#[test]
fn resolve_seeds_literal_and_files() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("a.txt"), "alpha").unwrap();
std::fs::write(dir.path().join("b.txt"), "beta").unwrap();
let bp = bp(
r#"lit = { kind = "pinned", max_tokens = 500, seed = { literal = "hello" } }
docs = { kind = "pinned", max_tokens = 2000, seed = { files = ["a.txt", "b.txt"] } }"#,
);
let args = args_with("t", HashMap::new(), &dir.path().to_string_lossy());
let seeds =
resolve_seeds(&bp, &args, &dir.path().to_string_lossy(), &seed_policy()).unwrap();
assert_eq!(seeds.get("lit").map(String::as_str), Some("hello"));
let docs = seeds.get("docs").unwrap();
assert!(docs.contains("alpha") && docs.contains("beta"));
}
#[test]
fn resolve_seeds_glob_concatenates_matches() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir(dir.path().join("specs")).unwrap();
std::fs::write(dir.path().join("specs/one.md"), "spec one").unwrap();
std::fs::write(dir.path().join("specs/two.md"), "spec two").unwrap();
let bp =
bp(r#"specs = { kind = "pinned", max_tokens = 4000, seed = { glob = "specs/*.md" } }"#);
let wd = dir.path().to_string_lossy().to_string();
let args = args_with("t", HashMap::new(), &wd);
let seeds = resolve_seeds(&bp, &args, &wd, &seed_policy()).unwrap();
let specs = seeds.get("specs").unwrap();
assert!(specs.contains("spec one") && specs.contains("spec two"));
}
#[test]
fn resolve_seeds_rhai_runs_script() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("init.rhai"),
r#""seeded: " + input["task"]"#,
)
.unwrap();
let bp = bp(
r#"scripted = { kind = "pinned", max_tokens = 500, seed = { rhai = "init.rhai" } }"#,
);
let wd = dir.path().to_string_lossy().to_string();
let args = args_with("hello", HashMap::new(), &wd);
let seeds = resolve_seeds(&bp, &args, &wd, &seed_policy()).unwrap();
assert_eq!(
seeds.get("scripted").map(String::as_str),
Some("seeded: hello")
);
}
#[test]
fn resolve_seeds_files_required_missing_errors_optional_skips() {
let dir = tempfile::tempdir().unwrap();
let wd = dir.path().to_string_lossy().to_string();
let req = bp(
r#"docs = { kind = "pinned", max_tokens = 2000, seed = { files = ["missing.txt"] }, required = true }"#,
);
let args = args_with("t", HashMap::new(), &wd);
let err = resolve_seeds(&req, &args, &wd, &seed_policy()).unwrap_err();
assert!(err.contains("missing.txt"), "got: {err}");
let opt = bp(
r#"docs = { kind = "pinned", max_tokens = 2000, seed = { files = ["missing.txt"] } }"#,
);
let seeds = resolve_seeds(&opt, &args, &wd, &seed_policy()).unwrap();
assert!(!seeds.contains_key("docs"));
}
#[test]
fn resolve_seeds_glob_no_match_required_errors_optional_skips() {
let dir = tempfile::tempdir().unwrap();
let wd = dir.path().to_string_lossy().to_string();
let args = args_with("t", HashMap::new(), &wd);
let req = bp(
r#"specs = { kind = "pinned", max_tokens = 2000, seed = { glob = "none/*.md" }, required = true }"#,
);
let err = resolve_seeds(&req, &args, &wd, &seed_policy()).unwrap_err();
assert!(err.contains("matched no files"), "got: {err}");
let opt =
bp(r#"specs = { kind = "pinned", max_tokens = 2000, seed = { glob = "none/*.md" } }"#);
let seeds = resolve_seeds(&opt, &args, &wd, &seed_policy()).unwrap();
assert!(!seeds.contains_key("specs"));
}
#[test]
fn resolve_seeds_bad_glob_pattern_errors() {
let dir = tempfile::tempdir().unwrap();
let wd = dir.path().to_string_lossy().to_string();
let bp = bp(r#"specs = { kind = "pinned", max_tokens = 2000, seed = { glob = "[" } }"#);
let args = args_with("t", HashMap::new(), &wd);
let err = resolve_seeds(&bp, &args, &wd, &seed_policy()).unwrap_err();
assert!(err.contains("bad glob"), "got: {err}");
}
#[test]
fn resolve_seeds_rhai_script_error() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("boom.rhai"), "undefined_func()").unwrap();
let wd = dir.path().to_string_lossy().to_string();
let bp = bp(
r#"scripted = { kind = "pinned", max_tokens = 500, seed = { rhai = "boom.rhai" } }"#,
);
let args = args_with("t", HashMap::new(), &wd);
let err = resolve_seeds(&bp, &args, &wd, &seed_policy()).unwrap_err();
assert!(err.contains("rhai seed failed"), "got: {err}");
}
fn command_bp(required: bool) -> leviath_core::Blueprint {
let req = if required { ", required = true" } else { "" };
bp(&format!(
r#"facts = {{ kind = "pinned", max_tokens = 500, seed = {{ command = "scan-repo" }}{req} }}"#
))
}
#[test]
fn resolve_seeds_command_stores_output() {
let bp = command_bp(false);
let args = args_with("t", HashMap::new(), "/tmp");
let seeds = resolve_seeds(
&bp,
&args,
"/tmp",
&stub_policy(Ok("src/lib.rs\nsrc/main.rs".to_string())),
)
.unwrap();
assert_eq!(
seeds.get("facts").map(String::as_str),
Some("src/lib.rs\nsrc/main.rs")
);
}
#[test]
fn resolve_seeds_command_receives_the_workdir_and_command() {
let bp = command_bp(false);
let args = args_with("t", HashMap::new(), "/work");
let policy = SeedCommandPolicy {
allowed: true,
timeout: std::time::Duration::from_secs(9),
runner: std::sync::Arc::new(|command, workdir, timeout| {
Ok(format!(
"{command}@{}#{}",
workdir.display(),
timeout.as_secs()
))
}),
};
let seeds = resolve_seeds(&bp, &args, "/work", &policy).unwrap();
assert_eq!(
seeds.get("facts").map(String::as_str),
Some("scan-repo@/work#9")
);
}
#[test]
fn resolve_seeds_command_failure_is_skipped_when_optional() {
let bp = command_bp(false);
let args = args_with("t", HashMap::new(), "/tmp");
let seeds = resolve_seeds(
&bp,
&args,
"/tmp",
&stub_policy(Err("timed out".to_string())),
)
.unwrap();
assert!(
!seeds.contains_key("facts"),
"an optional command seed must not sink the spawn"
);
}
#[test]
fn resolve_seeds_command_failure_errors_when_required() {
let bp = command_bp(true);
let args = args_with("t", HashMap::new(), "/tmp");
let err =
resolve_seeds(&bp, &args, "/tmp", &stub_policy(Err("boom".to_string()))).unwrap_err();
assert!(err.contains("scan-repo"), "got: {err}");
assert!(err.contains("boom"), "got: {err}");
}
#[test]
fn resolve_seeds_command_empty_output_is_skipped_when_optional() {
let bp = command_bp(false);
let args = args_with("t", HashMap::new(), "/tmp");
let seeds =
resolve_seeds(&bp, &args, "/tmp", &stub_policy(Ok(" \n".to_string()))).unwrap();
assert!(!seeds.contains_key("facts"));
}
#[test]
fn resolve_seeds_command_empty_output_errors_when_required() {
let bp = command_bp(true);
let args = args_with("t", HashMap::new(), "/tmp");
let err = resolve_seeds(&bp, &args, "/tmp", &stub_policy(Ok(String::new()))).unwrap_err();
assert!(err.contains("returned empty"), "got: {err}");
}
#[test]
fn resolve_seeds_command_skipped_when_disabled() {
let bp = command_bp(false);
let args = args_with("t", HashMap::new(), "/tmp");
let mut policy = stub_policy(Ok("SHOULD NOT BE USED".to_string()));
policy.allowed = false;
let seeds = resolve_seeds(&bp, &args, "/tmp", &policy).unwrap();
assert!(!seeds.contains_key("facts"));
}
#[test]
fn resolve_seeds_required_command_errors_when_disabled() {
let bp = command_bp(true);
let args = args_with("t", HashMap::new(), "/tmp");
let err = resolve_seeds(&bp, &args, "/tmp", &SeedCommandPolicy::disabled()).unwrap_err();
assert!(err.contains("allow_seed_commands"), "got: {err}");
}
#[test]
fn resolve_seeds_glob_matching_directory_required_errors() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir(dir.path().join("subdir")).unwrap();
let wd = dir.path().to_string_lossy().to_string();
let bp = bp(
r#"specs = { kind = "pinned", max_tokens = 2000, seed = { glob = "sub*" }, required = true }"#,
);
let args = args_with("t", HashMap::new(), &wd);
let err = resolve_seeds(&bp, &args, &wd, &seed_policy()).unwrap_err();
assert!(err.contains("read seed file"), "got: {err}");
}
#[test]
fn resolve_seeds_rhai_read_error() {
let dir = tempfile::tempdir().unwrap();
let wd = dir.path().to_string_lossy().to_string();
let bp = bp(
r#"scripted = { kind = "pinned", max_tokens = 500, seed = { rhai = "nope.rhai" } }"#,
);
let args = args_with("t", HashMap::new(), &wd);
let err = resolve_seeds(&bp, &args, &wd, &seed_policy()).unwrap_err();
assert!(err.contains("read rhai seed"), "got: {err}");
}
#[test]
fn resolve_seeds_rhai_empty_required_errors_optional_skips() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("empty.rhai"), r#""""#).unwrap();
let wd = dir.path().to_string_lossy().to_string();
let args = args_with("t", HashMap::new(), &wd);
let req = bp(
r#"scripted = { kind = "pinned", max_tokens = 500, seed = { rhai = "empty.rhai" }, required = true }"#,
);
let err = resolve_seeds(&req, &args, &wd, &seed_policy()).unwrap_err();
assert!(err.contains("returned empty"), "got: {err}");
let opt = bp(
r#"scripted = { kind = "pinned", max_tokens = 500, seed = { rhai = "empty.rhai" } }"#,
);
let seeds = resolve_seeds(&opt, &args, &wd, &seed_policy()).unwrap();
assert!(!seeds.contains_key("scripted"));
}
#[test]
fn resolve_seeds_tolerates_unknown_caller_region() {
let bp = bp(r#"task = { kind = "pinned", max_tokens = 4000, seed = "task_input" }"#);
let args = args_with(
"t",
HashMap::from([("ghost".to_string(), "x".to_string())]),
"/tmp",
);
let seeds = resolve_seeds(&bp, &args, "/tmp", &seed_policy()).unwrap();
assert_eq!(seeds.get("task").map(String::as_str), Some("t"));
assert!(!seeds.contains_key("ghost"));
}
}