use std::collections::{HashMap, HashSet};
use std::path::Path;
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;
mod policy;
pub(crate) use policy::*;
mod scripts;
pub(crate) use scripts::*;
mod seeds;
use seeds::*;
mod tool_state;
use tool_state::*;
#[derive(Clone)]
pub struct SpawnDeps<'a> {
pub tool_service: &'a CliToolService,
pub config: &'a Config,
pub shared_mcp: Arc<Mutex<leviath_mcp::ToolExecutor>>,
pub mcp_tool_defs: &'a [Tool],
pub hub: &'a InteractionHub,
pub now_secs: i64,
pub subagent_tx: UnboundedSender<SubAgentOp>,
}
pub fn build_agent(
world: &mut World,
deps: SpawnDeps<'_>,
args: &SpawnArgs,
) -> Result<Entity, String> {
build_agent_inner(world, deps, args, true)
}
pub fn build_agent_for_reload(
world: &mut World,
deps: SpawnDeps<'_>,
args: &SpawnArgs,
) -> Result<Entity, String> {
build_agent_inner(world, deps, args, false)
}
fn check_spawn_request(args: &SpawnArgs) -> Result<(), String> {
if !leviath_core::is_safe_path_component(&args.run_id) {
return Err(format!(
"run id '{}' is not a usable directory name",
args.run_id
));
}
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
));
}
Ok(())
}
fn load_blueprint(
args: &SpawnArgs,
config: &crate::config::Config,
) -> Result<(String, leviath_core::Blueprint), String> {
let content = std::fs::read_to_string(&args.blueprint_path)
.map_err(|e| format!("read manifest '{}': {e}", args.blueprint_path))?;
let path = Path::new(&args.blueprint_path);
let stale = || {
crate::bundled::stale_install_suffix(
path,
crate::bundled::real_agents_dir_opt().as_deref(),
". ",
)
};
let mut blueprint = leviath_core::manifest::parse_manifest(&content)
.map_err(|e| format!("parse manifest: {e}{}", stale()))?;
blueprint
.validate()
.map_err(|e| format!("invalid blueprint: {e}{}", stale()))?;
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(_) => {}
}
}
}
Ok((content, blueprint))
}
struct RunRecordParts {
agent_name: String,
model_label: Option<String>,
num_stages: usize,
read_path_counts: Option<leviath_core::run_meta::ReadPathGrantCounts>,
output_validators:
HashMap<String, std::sync::Arc<leviath_scripting::output_validator::OutputValidator>>,
outcome_flags: leviath_runtime::persistence::RunOutcomeFlags,
compaction: Option<leviath_core::CompactionConfig>,
tool_sensitivities: Option<HashMap<String, leviath_core::TaintLevel>>,
security: leviath_core::taint::SecurityConfig,
mcp_overrides: std::collections::HashMap<String, leviath_core::policy::McpToolOverride>,
}
fn attach_run_record(
world: &mut World,
entity: Entity,
args: &SpawnArgs,
deps: &SpawnDeps<'_>,
parts: RunRecordParts,
) {
let metadata = RunMetadata {
run_id: args.run_id.clone(),
agent_name: parts.agent_name,
agent_path: args.blueprint_path.clone(),
task: args.task.clone(),
model: parts.model_label,
workdir: args.workdir.clone(),
num_stages: parts.num_stages,
started_at: deps.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: parts.read_path_counts,
output_request: args.output.clone(),
};
{
let mut entity_mut = world.entity_mut(entity);
if !parts.output_validators.is_empty() {
entity_mut.insert(leviath_runtime::components::OutputValidators(
parts.output_validators,
));
}
entity_mut.insert((
metadata,
TokenTotals::default(),
PersistWatermark::default(),
parts.outcome_flags,
));
(deps.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);
});
parts.compaction.into_iter().for_each(|cc| {
entity_mut.insert(CompactionSettings(cc));
});
parts
.tool_sensitivities
.into_iter()
.for_each(|sensitivities| {
let mut gate = leviath_runtime::TaintGate::new(parts.security.clone());
gate.apply_mcp_overrides(&parts.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());
});
}
}
struct TaintSetup {
security: leviath_core::taint::SecurityConfig,
mcp_overrides: std::collections::HashMap<String, leviath_core::policy::McpToolOverride>,
tool_sensitivities: Option<HashMap<String, leviath_core::TaintLevel>>,
}
fn resolve_taint_setup(
world: &World,
blueprint: &leviath_core::Blueprint,
config: &crate::config::Config,
all_tool_defs: &[leviath_providers::Tool],
read_paths_granted: bool,
) -> TaintSetup {
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
});
TaintSetup {
security,
mcp_overrides,
tool_sensitivities,
}
}
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}");
}
}
fn build_agent_inner(
world: &mut World,
deps: SpawnDeps<'_>,
args: &SpawnArgs,
enforce_seeds: bool,
) -> Result<Entity, String> {
check_spawn_request(args)?;
let (content, blueprint) = load_blueprint(args, deps.config)?;
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(
deps.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, deps.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 seed_read_paths = read_path_policy.clone();
let read_path_counts =
read_path_grant_counts(&blueprint, deps.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)
.with_shell_env(shell_env_policy(deps.config));
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(deps.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,
deps.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(deps.config),
registry,
&all_tool_defs,
args.yolo,
args.output.as_ref(),
)?
};
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 TaintSetup {
security,
mcp_overrides,
tool_sensitivities,
} = resolve_taint_setup(
world,
&blueprint,
deps.config,
&all_tool_defs,
read_paths_granted,
);
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(
deps.config.security.allow_seed_commands && !args.no_seed_commands,
std::time::Duration::from_secs(deps.config.limits.script_shell_timeout_secs),
Arc::new(
deps.config
.safe_keys_for_agent(&agent_name, blueprint.safe_commands.as_ref())
.into_keys()
.collect(),
),
sandbox.clone(),
shell_env_policy(deps.config),
);
resolve_seeds(&blueprint, args, &args.workdir, &policy, &seed_read_paths)?
} else {
HashMap::new()
};
let region_scripts = resolve_region_scripts(&blueprint, &args.blueprint_path)?;
let stage_hooks = resolve_stage_hook_scripts(&blueprint, &args.blueprint_path)?;
let output_validators = resolve_output_validators(&blueprint, &args.blueprint_path)?;
let outcome_flags = leviath_runtime::persistence::RunOutcomeFlags::for_blueprint(&blueprint);
let blueprint_safe = blueprint.safe_commands.clone();
let entity = spawn_agent_seeded(
world,
leviath_runtime::pipeline::SeededSpawn {
agent_id: args.run_id.clone(),
blueprint,
seeds,
stages,
global_hints: leviath_core::config::PromptHints {
batch_tool: deps.config.batch_tool_hint,
shell: deps.config.shell_hint,
},
global_nudge: deps.config.nudge.clone(),
region_scripts,
},
)?;
if !stage_hooks.is_empty() {
world
.entity_mut(entity)
.insert(leviath_runtime::components::StageHookScripts(stage_hooks));
}
attach_run_record(
world,
entity,
args,
&deps,
RunRecordParts {
agent_name: agent_name.clone(),
model_label,
num_stages,
read_path_counts,
output_validators,
outcome_flags,
compaction,
tool_sensitivities,
security: security.clone(),
mcp_overrides,
},
);
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: deps.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(
&deps.config.tool_script_permissions,
&content,
);
let agent_scoped_perms = deps.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,
deps.config.security.allow_blueprint_permissions,
)
},
);
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(deps.config.limits.script_shell_timeout_secs),
shell_env_policy(deps.config),
)
.with_local_network(deps.config.security.allow_local_network)
.with_env_allowlist(deps.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, deps.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(ToolStateParts {
builtins,
builtin_names,
mcp: deps.shared_mcp,
config: deps.config,
hub: deps.hub,
run_id: &args.run_id,
entry_stage: &entry_stage,
entry_index,
stage_perms_by_index,
stage_required_by_index,
agent_perms,
agent_name: &agent_name,
launch_overrides,
subagent: Some(subagent),
sandbox,
script_tools,
script_tool_names,
script_host,
dynamic,
unattended: args.yolo,
blueprint_safe: blueprint_safe.as_ref(),
});
deps.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: String::new(),
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,
output: 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"
}
fn validator_blueprint(agent_script: Option<&str>, stage_script: Option<&str>) -> Blueprint {
let mut bp = leviath_core::manifest::parse_manifest(
"[agent]\nname = \"v\"\nversion = \"0.1.0\"\ndescription = \"d\"\n\n\
[stages.main]\nmodel = { provider = \"anthropic\", model = \"m\" }\n",
)
.unwrap();
let spec = |script: &str| leviath_core::output::OutputSpec {
validator: Some(script.to_string()),
..leviath_core::output::OutputSpec::default()
};
bp.output = agent_script.map(spec);
bp.stages[0].output = stage_script.map(spec);
bp
}
#[test]
fn resolve_output_validators_compiles_each_distinct_script_once() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("agent.leviath");
std::fs::create_dir(dir.path().join("validators")).unwrap();
std::fs::write(
dir.path().join("validators/shape.rhai"),
"fn validate(content) { () }",
)
.unwrap();
let bp = validator_blueprint(Some("validators/shape.rhai"), Some("validators/shape.rhai"));
let compiled =
resolve_output_validators(&bp, &manifest.to_string_lossy()).expect("it compiles");
assert_eq!(compiled.len(), 1);
assert!(compiled.contains_key("validators/shape.rhai"));
}
#[test]
fn resolve_output_validators_is_empty_without_any() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("agent.leviath");
let bp = validator_blueprint(None, None);
assert!(
resolve_output_validators(&bp, &manifest.to_string_lossy())
.unwrap()
.is_empty()
);
let mut shaped = validator_blueprint(None, None);
shaped.stages[0].output = Some(leviath_core::output::OutputSpec {
format: Some("a2ui".to_string()),
..leviath_core::output::OutputSpec::default()
});
assert!(
resolve_output_validators(&shaped, &manifest.to_string_lossy())
.unwrap()
.is_empty()
);
}
#[test]
fn resolve_output_validators_reports_a_missing_script() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("agent.leviath");
let bp = validator_blueprint(None, Some("validators/gone.rhai"));
let err = resolve_output_validators(&bp, &manifest.to_string_lossy())
.expect_err("a script that is not there");
assert!(err.contains("cannot read output validator"), "{err}");
assert!(err.contains("gone.rhai"), "{err}");
}
#[test]
fn resolve_output_validators_reports_one_that_does_not_compile() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("agent.leviath");
std::fs::write(dir.path().join("broken.rhai"), "fn validate(a, b) { () }").unwrap();
let bp = validator_blueprint(None, Some("broken.rhai"));
let err =
resolve_output_validators(&bp, &manifest.to_string_lossy()).expect_err("wrong arity");
assert!(err.contains("failed to compile"), "{err}");
}
fn hooked_manifest(hooks: &str) -> leviath_core::Blueprint {
leviath_core::manifest::parse_manifest(&format!(
"[agent]\nname = \"h\"\nversion = \"0.1.0\"\ndescription = \"d\"\n\n\
[stages.main]\nmodel = {{ provider = \"anthropic\", model = \"m\" }}\n{hooks}"
))
.expect("the fixture manifest parses")
}
#[test]
fn stage_hooks_are_empty_when_no_stage_declares_one() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("agent.leviath");
let bp = hooked_manifest("");
let got = resolve_stage_hook_scripts(&bp, &manifest.to_string_lossy()).unwrap();
assert!(got.is_empty());
}
#[test]
fn a_declared_hook_is_compiled_and_keyed_by_its_path() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("agent.leviath");
std::fs::write(dir.path().join("h.rhai"), "fn on_stage_enter(ctx) { () }").unwrap();
let bp = hooked_manifest("[stages.main.hooks]\non_stage_enter = \"h.rhai\"\n");
let got = resolve_stage_hook_scripts(&bp, &manifest.to_string_lossy()).unwrap();
assert_eq!(got.len(), 1);
assert!(got["h.rhai"].defines("on_stage_enter"));
}
#[test]
fn one_file_backing_two_hooks_is_compiled_once() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("agent.leviath");
std::fs::write(
dir.path().join("h.rhai"),
"fn on_stage_enter(ctx) { () } fn on_stage_exit(ctx) { () }",
)
.unwrap();
let bp = hooked_manifest(
"[stages.main.hooks]\non_stage_enter = \"h.rhai\"\non_stage_exit = \"h.rhai\"\n",
);
let got = resolve_stage_hook_scripts(&bp, &manifest.to_string_lossy()).unwrap();
assert_eq!(got.len(), 1, "one entry, not one per hook");
assert!(got["h.rhai"].defines("on_stage_enter"));
assert!(got["h.rhai"].defines("on_stage_exit"));
}
#[test]
fn a_missing_hook_script_fails_the_spawn() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("agent.leviath");
let bp = hooked_manifest("[stages.main.hooks]\non_stage_enter = \"gone.rhai\"\n");
let err = resolve_stage_hook_scripts(&bp, &manifest.to_string_lossy())
.expect_err("a missing script is a spawn error");
assert!(err.contains("cannot read stage hook script"), "{err}");
}
#[test]
fn a_hook_script_that_does_not_compile_fails_the_spawn() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("agent.leviath");
std::fs::write(dir.path().join("h.rhai"), "fn on_stage_enter(ctx) {").unwrap();
let bp = hooked_manifest("[stages.main.hooks]\non_stage_enter = \"h.rhai\"\n");
let err = resolve_stage_hook_scripts(&bp, &manifest.to_string_lossy())
.expect_err("a broken script is a spawn error");
assert!(err.contains("failed to compile"), "{err}");
}
#[test]
fn a_file_that_lacks_the_hook_it_was_named_for_fails_the_spawn() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("agent.leviath");
std::fs::write(dir.path().join("h.rhai"), "fn on_stage_exit(ctx) { () }").unwrap();
let bp = hooked_manifest("[stages.main.hooks]\non_stage_enter = \"h.rhai\"\n");
let err = resolve_stage_hook_scripts(&bp, &manifest.to_string_lossy())
.expect_err("a file missing its named hook is a spawn error");
assert!(err.contains("defines no"), "{err}");
}
#[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(),
SpawnDeps {
tool_service: cli.as_ref(),
config: &Config::default(),
shared_mcp: mcp,
mcp_tool_defs: &[],
hub: &hub,
now_secs: 100,
subagent_tx: sub_tx(),
},
&args,
)
.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_run_id_that_is_not_a_directory_name() {
for bad in ["../escape", "a/b", "..", ".", ""] {
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("/nonexistent/agent.leviath");
args.run_id = bad.to_string();
let err = build_agent(
world.world_mut(),
SpawnDeps {
tool_service: cli.as_ref(),
config: &Config::default(),
shared_mcp: mcp,
mcp_tool_defs: &[],
hub: &hub,
now_secs: 100,
subagent_tx: sub_tx(),
},
&args,
)
.unwrap_err();
assert!(
err.contains("run id"),
"run id {bad:?} names a directory and must be refused, 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(),
SpawnDeps {
tool_service: cli.as_ref(),
config: &Config::default(),
shared_mcp: mcp,
mcp_tool_defs: &[],
hub: &hub,
now_secs: 100,
subagent_tx: sub_tx(),
},
&args,
)
.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(),
SpawnDeps {
tool_service: cli.as_ref(),
config: &Config::default(),
shared_mcp: mcp,
mcp_tool_defs: &[],
hub: &hub,
now_secs: 100,
subagent_tx: sub_tx(),
},
&spawn_args(&manifest.to_string_lossy()),
)
.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\n\
[context.regions]\ntask = { kind = \"pinned\", max_tokens = 1000 }\n",
)
.unwrap();
let (mut world, cli) = test_world();
let hub = InteractionHub::new();
let root = build_agent(
world.world_mut(),
SpawnDeps {
tool_service: cli.as_ref(),
config: &Config::default(),
shared_mcp: Arc::new(Mutex::new(leviath_mcp::ToolExecutor::new())),
mcp_tool_defs: &[],
hub: &hub,
now_secs: 100,
subagent_tx: sub_tx(),
},
&SpawnArgs {
task: "title me".to_string(),
..spawn_args(&manifest.to_string_lossy())
},
)
.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(),
SpawnDeps {
tool_service: cli.as_ref(),
config: &Config::default(),
shared_mcp: Arc::new(Mutex::new(leviath_mcp::ToolExecutor::new())),
mcp_tool_defs: &[],
hub: &hub,
now_secs: 100,
subagent_tx: sub_tx(),
},
&child_args,
)
.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(),
SpawnDeps {
tool_service: cli.as_ref(),
config: &config,
shared_mcp: Arc::new(Mutex::new(leviath_mcp::ToolExecutor::new())),
mcp_tool_defs: &[],
hub: &hub,
now_secs: 100,
subagent_tx: sub_tx(),
},
&off_args,
)
.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(),
SpawnDeps {
tool_service: cli.as_ref(),
config: &Config::default(),
shared_mcp: mcp,
mcp_tool_defs: &[],
hub: &hub,
now_secs: 100,
subagent_tx: sub_tx(),
},
&spawn_args(&manifest.to_string_lossy()),
)
.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(),
SpawnDeps {
tool_service: cli.as_ref(),
config: &Config::default(),
shared_mcp: mcp,
mcp_tool_defs: &[],
hub: &hub,
now_secs: 100,
subagent_tx: sub_tx(),
},
&spawn_args(&manifest.to_string_lossy()),
)
.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(),
SpawnDeps {
tool_service: cli.as_ref(),
config: &Config::default(),
shared_mcp: mcp,
mcp_tool_defs: &[],
hub: &hub,
now_secs: 100,
subagent_tx: sub_tx(),
},
&spawn_args(&manifest.to_string_lossy()),
)
.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(),
SpawnDeps {
tool_service: cli.as_ref(),
config: &Config::default(),
shared_mcp: mcp,
mcp_tool_defs: &[],
hub: &hub,
now_secs: 100,
subagent_tx: sub_tx(),
},
&spawn_args(&manifest.to_string_lossy()),
)
.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(),
SpawnDeps {
tool_service: cli.as_ref(),
config: &Config::default(),
shared_mcp: mcp,
mcp_tool_defs: &[],
hub: &hub,
now_secs: 100,
subagent_tx: sub_tx(),
},
&args,
)
.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(),
SpawnDeps {
tool_service: cli.as_ref(),
config: &Config::default(),
shared_mcp: Arc::new(Mutex::new(leviath_mcp::ToolExecutor::new())),
mcp_tool_defs: &[],
hub: &InteractionHub::new(),
now_secs: 100,
subagent_tx: sub_tx(),
},
&args,
)
.expect("spawn succeeds");
assert_eq!(
world.agent_status(world.own_agent(entity)),
Some(AgentStatus::Active)
);
let meta = world
.world()
.get::<RunMetadata>(entity)
.expect("run metadata attached");
assert!(meta.unattended);
}
#[tokio::test]
async fn build_agent_refuses_a_validator_that_does_not_compile() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("agent.leviath");
std::fs::write(dir.path().join("shape.rhai"), "fn validate(a, b) { () }").unwrap();
std::fs::write(
&manifest,
"[agent]\nname = \"v\"\nversion = \"0.1.0\"\ndescription = \"d\"\n\n\
[stages.main]\nmodel = { provider = \"anthropic\", model = \"m\" }\n\
available_tools = [\"submit_output\"]\n\n\
[stages.main.output]\nformat = \"a2ui\"\nvalidator = \"shape.rhai\"\n",
)
.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(),
SpawnDeps {
tool_service: cli.as_ref(),
config: &Config::default(),
shared_mcp: mcp,
mcp_tool_defs: &[],
hub: &hub,
now_secs: 100,
subagent_tx: sub_tx(),
},
&args,
)
.unwrap_err();
assert!(err.contains("failed to compile"), "got: {err}");
assert!(err.contains("exactly one parameter"), "and says why: {err}");
}
#[tokio::test]
async fn build_agent_carries_output_validators_onto_the_entity() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("agent.leviath");
std::fs::write(dir.path().join("shape.rhai"), "fn validate(content) { () }").unwrap();
std::fs::write(
&manifest,
"[agent]\nname = \"v\"\nversion = \"0.1.0\"\ndescription = \"d\"\n\n\
[stages.main]\nmodel = { provider = \"anthropic\", model = \"m\" }\n\
available_tools = [\"submit_output\"]\n\n\
[stages.main.output]\nformat = \"a2ui\"\nvalidator = \"shape.rhai\"\n",
)
.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 entity = build_agent(
world.world_mut(),
SpawnDeps {
tool_service: cli.as_ref(),
config: &Config::default(),
shared_mcp: mcp,
mcp_tool_defs: &[],
hub: &hub,
now_secs: 100,
subagent_tx: sub_tx(),
},
&args,
)
.expect("spawns");
let validators = world
.world()
.get::<leviath_runtime::components::OutputValidators>(entity)
.expect("the compiled validator reaches the entity");
assert!(validators.0.contains_key("shape.rhai"));
}
#[tokio::test]
async fn build_agent_carries_no_validators_when_none_are_named() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("agent.leviath");
std::fs::write(
&manifest,
"[agent]\nname = \"v\"\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 args = spawn_args(&manifest.to_string_lossy());
let entity = build_agent(
world.world_mut(),
SpawnDeps {
tool_service: cli.as_ref(),
config: &Config::default(),
shared_mcp: mcp,
mcp_tool_defs: &[],
hub: &hub,
now_secs: 100,
subagent_tx: sub_tx(),
},
&args,
)
.expect("spawns");
assert!(
world
.world()
.get::<leviath_runtime::components::OutputValidators>(entity)
.is_none()
);
}
#[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(),
SpawnDeps {
tool_service: cli.as_ref(),
config: &Config::default(),
shared_mcp: Arc::new(Mutex::new(leviath_mcp::ToolExecutor::new())),
mcp_tool_defs: &[],
hub: &InteractionHub::new(),
now_secs: 100,
subagent_tx: sub_tx(),
},
&args,
)
.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(),
SpawnDeps {
tool_service: cli.as_ref(),
config: &Config::default(),
shared_mcp: mcp,
mcp_tool_defs: &[],
hub: &hub,
now_secs: 100,
subagent_tx: sub_tx(),
},
&spawn_args(&manifest.to_string_lossy()),
)
.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(),
SpawnDeps {
tool_service: cli.as_ref(),
config: &Config::default(),
shared_mcp: mcp,
mcp_tool_defs: &[],
hub: &hub,
now_secs: 100,
subagent_tx: sub_tx(),
},
&spawn_args(&manifest.to_string_lossy()),
)
.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(),
SpawnDeps {
tool_service: cli.as_ref(),
config: &Config::default(),
shared_mcp: mcp,
mcp_tool_defs: &[],
hub: &hub,
now_secs: 100,
subagent_tx: sub_tx(),
},
&spawn_args(&manifest.to_string_lossy()),
)
.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(),
SpawnDeps {
tool_service: cli.as_ref(),
config: &Config::default(),
shared_mcp: mcp,
mcp_tool_defs: &[],
hub: &hub,
now_secs: 100,
subagent_tx: sub_tx(),
},
&spawn_args(&manifest.to_string_lossy()),
)
.expect("spawn succeeds");
assert_eq!(
world.agent_status(world.own_agent(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(),
SpawnDeps {
tool_service: cli.as_ref(),
config: &Config::default(),
shared_mcp: mcp,
mcp_tool_defs: &[],
hub: &hub,
now_secs: 100,
subagent_tx: sub_tx(),
},
&spawn_args(&manifest.to_string_lossy()),
)
.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(),
SpawnDeps {
tool_service: cli.as_ref(),
config: &config,
shared_mcp: mcp,
mcp_tool_defs: &[],
hub: &hub,
now_secs: 100,
subagent_tx: sub_tx(),
},
&args,
)
.expect("spawn succeeds");
assert_eq!(
world.agent_status(world.own_agent(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(),
SpawnDeps {
tool_service: cli.as_ref(),
config: &Config::default(),
shared_mcp: mcp,
mcp_tool_defs: &[],
hub: &hub,
now_secs: 100,
subagent_tx: sub_tx(),
},
&spawn_args(&manifest.to_string_lossy()),
)
.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(),
SpawnDeps {
tool_service: cli.as_ref(),
config: &config,
shared_mcp: mcp,
mcp_tool_defs: &[],
hub: &hub,
now_secs: 100,
subagent_tx: sub_tx(),
},
&args,
)
.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(),
SpawnDeps {
tool_service: cli.as_ref(),
config: &config,
shared_mcp: mcp,
mcp_tool_defs: &[],
hub: &hub,
now_secs: 100,
subagent_tx: sub_tx(),
},
&spawn_args(&manifest.to_string_lossy()),
)
.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(),
SpawnDeps {
tool_service: cli.as_ref(),
config: &config,
shared_mcp: mcp,
mcp_tool_defs: &[],
hub: &hub,
now_secs: 100,
subagent_tx: sub_tx(),
},
&spawn_args(&manifest.to_string_lossy()),
)
.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(),
SpawnDeps {
tool_service: cli.as_ref(),
config: &Config::default(),
shared_mcp: mcp,
mcp_tool_defs: &[],
hub: &hub,
now_secs: 100,
subagent_tx: sub_tx(),
},
&spawn_args("/no/such/manifest.leviath"),
)
.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(),
SpawnDeps {
tool_service: cli.as_ref(),
config: &Config::default(),
shared_mcp: mcp,
mcp_tool_defs: &[],
hub: &hub,
now_secs: 100,
subagent_tx: sub_tx(),
},
&spawn_args(&manifest.to_string_lossy()),
);
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(),
SpawnDeps {
tool_service: cli.as_ref(),
config: &Config::default(),
shared_mcp: mcp,
mcp_tool_defs: &[],
hub: &hub,
now_secs: 100,
subagent_tx: sub_tx(),
},
&spawn_args(&manifest.to_string_lossy()),
)
.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(),
SpawnDeps {
tool_service: cli.as_ref(),
config: &Config::default(),
shared_mcp: mcp,
mcp_tool_defs: &[],
hub: &hub,
now_secs: 100,
subagent_tx: sub_tx(),
},
&spawn_args(&manifest.to_string_lossy()),
)
.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(),
SpawnDeps {
tool_service: cli.as_ref(),
config: &Config::default(),
shared_mcp: mcp,
mcp_tool_defs: &[],
hub: &hub,
now_secs: 100,
subagent_tx: sub_tx(),
},
&spawn_args(&manifest.to_string_lossy()),
)
.expect("spawn succeeds");
assert_eq!(
world.agent_status(world.own_agent(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_attaches_declared_stage_hooks() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("h.rhai"), "fn on_stage_enter(ctx) { () }").unwrap();
let manifest = dir.path().join("agent.leviath");
std::fs::write(
&manifest,
"[agent]\nname = \"h\"\nversion = \"0.1.0\"\ndescription = \"d\"\n\n\
[stages.main]\nmodel = { provider = \"anthropic\", model = \"m\" }\n\
[stages.main.hooks]\non_stage_enter = \"h.rhai\"\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(),
SpawnDeps {
tool_service: cli.as_ref(),
config: &Config::default(),
shared_mcp: mcp,
mcp_tool_defs: &[],
hub: &hub,
now_secs: 100,
subagent_tx: sub_tx(),
},
&spawn_args(&manifest.to_string_lossy()),
)
.expect("spawn succeeds");
let scripts = world
.world_mut()
.get::<leviath_runtime::components::StageHookScripts>(entity)
.expect("the hook script is attached");
assert!(scripts.0.contains_key("h.rhai"));
}
#[tokio::test]
async fn build_agent_refuses_a_broken_stage_hook() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("h.rhai"), "fn on_stage_enter(ctx) {").unwrap();
let manifest = dir.path().join("agent.leviath");
std::fs::write(
&manifest,
"[agent]\nname = \"h\"\nversion = \"0.1.0\"\ndescription = \"d\"\n\n\
[stages.main]\nmodel = { provider = \"anthropic\", model = \"m\" }\n\
[stages.main.hooks]\non_stage_enter = \"h.rhai\"\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(),
SpawnDeps {
tool_service: cli.as_ref(),
config: &Config::default(),
shared_mcp: mcp,
mcp_tool_defs: &[],
hub: &hub,
now_secs: 100,
subagent_tx: sub_tx(),
},
&spawn_args(&manifest.to_string_lossy()),
)
.expect_err("a broken hook script must fail the spawn");
assert!(err.contains("failed to compile"), "{err}");
}
#[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(),
SpawnDeps {
tool_service: cli.as_ref(),
config: &config,
shared_mcp: mcp,
mcp_tool_defs: &[],
hub: &hub,
now_secs: 100,
subagent_tx: sub_tx(),
},
&spawn_args(&manifest.to_string_lossy()),
)
.expect("spawn succeeds");
assert_eq!(
world.agent_status(world.own_agent(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(),
SpawnDeps {
tool_service: cli.as_ref(),
config: &Config::default(),
shared_mcp: mcp,
mcp_tool_defs: &[],
hub: &hub,
now_secs: 100,
subagent_tx: sub_tx(),
},
&spawn_args(&manifest.to_string_lossy()),
)
.expect("spawn succeeds even when nothing grants the declaration");
assert_eq!(
world.agent_status(world.own_agent(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(),
SpawnDeps {
tool_service: cli.as_ref(),
config: &config,
shared_mcp: mcp,
mcp_tool_defs: &[],
hub: &hub,
now_secs: 100,
subagent_tx: sub_tx(),
},
&spawn_args(&manifest.to_string_lossy()),
)
.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(),
SpawnDeps {
tool_service: cli.as_ref(),
config: &Config::default(),
shared_mcp: mcp,
mcp_tool_defs: &[],
hub: &hub,
now_secs: 100,
subagent_tx: sub_tx(),
},
&spawn_args(&manifest.to_string_lossy()),
)
.unwrap_err();
assert!(err.contains("parse manifest"));
}
fn bp(regions_toml: &str) -> Blueprint {
let implicit_task = match regions_toml.contains("task") {
true => "",
false => "task = { kind = \"pinned\", max_tokens = 1000 }",
};
let toml = format!(
r#"
[agent]
name = "seedy"
[stages.main]
mode = "autonomous"
[stages.main.model]
provider = "anthropic"
model = "claude-sonnet-5"
[context.regions]
{regions_toml}
{implicit_task}
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,
output: None,
}
}
fn seed_policy() -> SeedCommandPolicy {
SeedCommandPolicy::disabled()
}
fn seed_safe_keys() -> std::sync::Arc<std::collections::HashSet<String>> {
std::sync::Arc::new(
["shell:scan-repo".to_string()]
.into_iter()
.collect::<std::collections::HashSet<_>>(),
)
}
fn no_read_paths() -> leviath_core::ReadPathPolicy {
leviath_core::ReadPathPolicy {
agent: "a".to_string(),
blueprint: Default::default(),
grants: Default::default(),
allow_blueprint: false,
}
}
fn stub_policy(result: Result<String, String>) -> SeedCommandPolicy {
SeedCommandPolicy {
allowed: true,
timeout: std::time::Duration::from_secs(1),
safe_keys: seed_safe_keys(),
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(), &no_read_paths()).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(), &no_read_paths()).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(), &no_read_paths()).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(),
&no_read_paths(),
)
.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(), &no_read_paths()).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(), &no_read_paths()).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(), &no_read_paths()).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(), &no_read_paths()).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(), &no_read_paths()).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(), &no_read_paths()).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(), &no_read_paths()).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(), &no_read_paths()).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())),
&no_read_paths(),
)
.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),
safe_keys: seed_safe_keys(),
runner: std::sync::Arc::new(|command, workdir, timeout| {
Ok(format!(
"{command}@{}#{}",
workdir.display(),
timeout.as_secs()
))
}),
};
let seeds = resolve_seeds(&bp, &args, "/work", &policy, &no_read_paths()).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())),
&no_read_paths(),
)
.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())),
&no_read_paths(),
)
.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())),
&no_read_paths(),
)
.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())),
&no_read_paths(),
)
.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, &no_read_paths()).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(),
&no_read_paths(),
)
.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(), &no_read_paths()).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(), &no_read_paths()).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(), &no_read_paths()).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(), &no_read_paths()).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(), &no_read_paths()).unwrap();
assert_eq!(seeds.get("task").map(String::as_str), Some("t"));
assert!(!seeds.contains_key("ghost"));
}
fn workdir_with_a_neighbour() -> (tempfile::TempDir, String) {
let root = tempfile::tempdir().expect("tempdir");
let work = root.path().join("work");
std::fs::create_dir_all(&work).expect("dirs");
std::fs::write(root.path().join("config.toml"), "api_key = \"sk-SECRET\"").expect("write");
let wd = work.to_string_lossy().to_string();
(root, wd)
}
#[test]
fn a_seed_file_outside_the_workdir_is_refused() {
let (_root, wd) = workdir_with_a_neighbour();
let bp = bp(
r#"notes = { kind = "pinned", max_tokens = 2000, seed = { files = ["../config.toml"] } }"#,
);
let args = args_with("t", HashMap::new(), &wd);
let err = resolve_seeds(&bp, &args, &wd, &seed_policy(), &no_read_paths()).unwrap_err();
assert!(err.contains("outside the working directory"), "{err}");
assert!(err.contains("read_paths"), "{err}");
}
#[test]
fn a_seed_file_inside_the_workdir_still_seeds() {
let (root, wd) = workdir_with_a_neighbour();
std::fs::write(root.path().join("work").join("notes.md"), "hello").expect("write");
let bp = bp(
r#"notes = { kind = "pinned", max_tokens = 2000, seed = { files = ["notes.md"] } }"#,
);
let args = args_with("t", HashMap::new(), &wd);
let seeds = resolve_seeds(&bp, &args, &wd, &seed_policy(), &no_read_paths()).unwrap();
assert!(seeds.get("notes").is_some_and(|s| s.contains("hello")));
}
#[test]
fn a_glob_that_matches_outside_the_workdir_is_refused() {
let (_root, wd) = workdir_with_a_neighbour();
let bp =
bp(r#"notes = { kind = "pinned", max_tokens = 2000, seed = { glob = "../*.toml" } }"#);
let args = args_with("t", HashMap::new(), &wd);
let err = resolve_seeds(&bp, &args, &wd, &seed_policy(), &no_read_paths()).unwrap_err();
assert!(err.contains("outside the working directory"), "{err}");
}
#[test]
fn a_granted_read_path_lets_a_seed_file_out() {
let (root, wd) = workdir_with_a_neighbour();
let outside = root.path().to_string_lossy().to_string();
let mut policy = no_read_paths();
policy.blueprint = leviath_core::ReadPathSet::compile(
std::slice::from_ref(&outside),
std::path::Path::new(&wd),
None,
cfg!(windows),
)
.expect("declaration compiles");
policy.allow_blueprint = true;
let bp = bp(
r#"notes = { kind = "pinned", max_tokens = 2000, seed = { files = ["../config.toml"] } }"#,
);
let args = args_with("t", HashMap::new(), &wd);
let seeds = resolve_seeds(&bp, &args, &wd, &seed_policy(), &policy).unwrap();
assert!(seeds.get("notes").is_some_and(|s| s.contains("sk-SECRET")));
}
#[test]
fn a_hook_script_outside_the_blueprint_directory_is_refused() {
let root = tempfile::tempdir().expect("tempdir");
let bp_dir = root.path().join("agents").join("evil");
std::fs::create_dir_all(&bp_dir).expect("dirs");
std::fs::write(root.path().join("outside.txt"), "NOT RHAI").expect("write");
let mut stage = leviath_core::Stage::new(
"main".to_string(),
leviath_core::blueprint::ModelConfig::new("p".to_string(), "m".to_string()),
);
stage.hooks.on_stage_enter = Some("../../outside.txt".to_string());
let blueprint = leviath_core::Blueprint::new(
"evil".to_string(),
"d".to_string(),
vec![stage],
leviath_core::layout::ContextLayout::new(vec![], 1000),
);
let bp_path = bp_dir.join("agent.leviath");
let err = resolve_stage_hook_scripts(&blueprint, bp_path.to_str().expect("utf8"))
.expect_err("an escaping script path is refused");
assert!(err.contains("outside the blueprint's directory"), "{err}");
assert!(!err.contains("failed to compile"), "{err}");
}
#[test]
fn a_declared_but_ungranted_read_path_does_not_let_a_seed_file_out() {
let (root, wd) = workdir_with_a_neighbour();
let outside = root.path().to_string_lossy().to_string();
let mut policy = no_read_paths();
policy.blueprint = leviath_core::ReadPathSet::compile(
std::slice::from_ref(&outside),
std::path::Path::new(&wd),
None,
cfg!(windows),
)
.expect("declaration compiles");
let bp = bp(
r#"notes = { kind = "pinned", max_tokens = 2000, seed = { files = ["../config.toml"] } }"#,
);
let args = args_with("t", HashMap::new(), &wd);
let err = resolve_seeds(&bp, &args, &wd, &seed_policy(), &policy).unwrap_err();
assert!(err.contains("outside the working directory"), "{err}");
}
#[test]
fn a_rhai_seed_script_outside_the_workdir_is_refused() {
let (_root, wd) = workdir_with_a_neighbour();
let bp = bp(
r#"notes = { kind = "pinned", max_tokens = 2000, seed = { rhai = "../config.toml" } }"#,
);
let args = args_with("t", HashMap::new(), &wd);
let err = resolve_seeds(&bp, &args, &wd, &seed_policy(), &no_read_paths()).unwrap_err();
assert!(err.contains("outside the working directory"), "{err}");
}
#[test]
fn a_custom_region_script_outside_the_blueprint_directory_is_refused() {
let root = tempfile::tempdir().expect("tempdir");
let bp_dir = root.path().join("agents").join("evil");
std::fs::create_dir_all(&bp_dir).expect("dirs");
std::fs::write(root.path().join("outside.txt"), "NOT RHAI").expect("write");
let blueprint =
bp(r#"notes = { kind = "custom", script = "../../outside.txt", max_tokens = 2000 }"#);
let bp_path = bp_dir.join("agent.leviath");
let err = resolve_region_scripts(&blueprint, bp_path.to_str().expect("utf8"))
.expect_err("an escaping script path is refused");
assert!(err.contains("outside the blueprint's directory"), "{err}");
}
#[test]
fn an_output_validator_outside_the_blueprint_directory_is_refused() {
let root = tempfile::tempdir().expect("tempdir");
let bp_dir = root.path().join("agents").join("evil");
std::fs::create_dir_all(&bp_dir).expect("dirs");
std::fs::write(root.path().join("outside.txt"), "NOT RHAI").expect("write");
let mut blueprint = leviath_core::Blueprint::new(
"evil".to_string(),
"d".to_string(),
vec![],
leviath_core::layout::ContextLayout::new(vec![], 1000),
);
blueprint.output = Some(leviath_core::output::OutputSpec {
validator: Some("../../outside.txt".to_string()),
..Default::default()
});
let bp_path = bp_dir.join("agent.leviath");
let err = resolve_output_validators(&blueprint, bp_path.to_str().expect("utf8"))
.expect_err("an escaping validator path is refused");
assert!(err.contains("outside the blueprint's directory"), "{err}");
}
#[test]
fn a_hook_script_beside_the_blueprint_still_loads() {
let root = tempfile::tempdir().expect("tempdir");
let bp_dir = root.path().join("agents").join("good");
std::fs::create_dir_all(&bp_dir).expect("dirs");
std::fs::write(bp_dir.join("h.rhai"), "fn on_stage_enter(ctx) { () }").expect("write");
let mut stage = leviath_core::Stage::new(
"main".to_string(),
leviath_core::blueprint::ModelConfig::new("p".to_string(), "m".to_string()),
);
stage.hooks.on_stage_enter = Some("h.rhai".to_string());
let blueprint = leviath_core::Blueprint::new(
"good".to_string(),
"d".to_string(),
vec![stage],
leviath_core::layout::ContextLayout::new(vec![], 1000),
);
let bp_path = bp_dir.join("agent.leviath");
let scripts = resolve_stage_hook_scripts(&blueprint, bp_path.to_str().expect("utf8"))
.expect("a script beside the blueprint loads");
assert!(scripts.contains_key("h.rhai"));
}
fn bp_taking_no_task(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()
}
#[test]
fn a_task_the_blueprint_cannot_hold_is_refused() {
let bp = bp_taking_no_task(r#"notes = { kind = "pinned", max_tokens = 100 }"#);
let args = args_with("do the thing", HashMap::new(), "/w");
let err = resolve_seeds(&bp, &args, "/w", &seed_policy(), &no_read_paths())
.expect_err("a task with nowhere to go should be refused");
assert!(err.contains("declares no region to put it in"), "{err}");
assert!(err.contains("takes no caller input at all"), "{err}");
}
#[test]
fn the_refusal_names_the_input_the_agent_does_take() {
let bp =
bp_taking_no_task(r#"diff = { kind = "pinned", max_tokens = 100, seed = "diff" }"#);
let args = args_with("do the thing", HashMap::new(), "/w");
let err =
resolve_seeds(&bp, &args, "/w", &seed_policy(), &no_read_paths()).expect_err("refused");
assert!(err.contains("it takes: diff"), "{err}");
}
#[test]
fn an_agent_driven_by_named_regions_still_spawns_with_no_task() {
let bp =
bp_taking_no_task(r#"diff = { kind = "pinned", max_tokens = 100, seed = "diff" }"#);
let mut regions = HashMap::new();
regions.insert("diff".to_string(), "a patch".to_string());
let args = args_with("", regions, "/w");
let seeds = resolve_seeds(&bp, &args, "/w", &seed_policy(), &no_read_paths())
.expect("no task was supplied, so there is nothing to refuse");
assert_eq!(seeds.get("diff").map(String::as_str), Some("a patch"));
}
#[test]
fn a_whitespace_only_task_is_not_treated_as_a_task() {
let bp = bp_taking_no_task(r#"notes = { kind = "pinned", max_tokens = 100 }"#);
let args = args_with(" \n ", HashMap::new(), "/w");
resolve_seeds(&bp, &args, "/w", &seed_policy(), &no_read_paths())
.expect("blank is the same as absent");
}
#[test]
fn every_bundled_agent_that_documents_a_task_accepts_one() {
for agent in crate::bundled::BUNDLED_AGENTS {
let name = agent.name;
let (_, content) = agent
.files
.iter()
.find(|(rel, _)| *rel == "agent.leviath")
.expect("every bundled agent ships an agent.leviath");
let bp = leviath_core::manifest::parse_manifest(content)
.expect("every bundled agent's manifest parses");
assert!(
!content.contains("--task") || bp.accepts_task(),
"{name} tells the user to pass --task but declares no region to hold one"
);
}
}
}