use super::*;
pub(super) fn seed_path_within(
base: &std::path::Path,
declared: &std::path::Path,
read_paths: &leviath_core::ReadPathPolicy,
) -> Result<std::path::PathBuf, String> {
if leviath_core::resolves_within(declared, base) {
return Ok(declared.to_path_buf());
}
let refusal = || {
format!(
"seed path '{}' resolves outside the working directory ({}); grant it with \
[read_paths] in the blueprint and your config, or move it inside",
declared.display(),
base.display()
)
};
if !read_paths.is_active() {
return Err(refusal());
}
leviath_core::canonicalize_for_match(declared)
.filter(|c| {
matches!(
read_paths.decide(c),
leviath_core::ReadPathDecision::Allowed
)
})
.ok_or_else(refusal)
}
pub(super) fn resolve_seeds(
blueprint: &Blueprint,
args: &SpawnArgs,
workdir: &str,
commands: &SeedCommandPolicy,
read_paths: &leviath_core::ReadPathPolicy,
) -> Result<HashMap<String, String>, String> {
use leviath_core::layout::RegionSeed;
let mut caller: HashMap<String, String> = HashMap::new();
caller.insert("task".to_string(), args.task.clone());
for (k, v) in &args.regions {
caller.insert(k.clone(), v.clone());
}
if !args.task.trim().is_empty() && !blueprint.accepts_task() {
return Err(blueprint.task_refusal());
}
let base = std::path::Path::new(workdir);
let mut seeds: HashMap<String, String> = HashMap::new();
for region in &blueprint.context_layout.regions {
let Some(seed) = ®ion.seed else { continue };
match seed {
RegionSeed::CallerInput { name } => {
let value = caller.get(name).map(|s| s.as_str()).unwrap_or("");
if value.trim().is_empty() {
if region.required {
return Err(region.required_message.clone().unwrap_or_else(|| {
format!(
"required region '{}' was not provided; supply it via \
--{name} <text|@file> (CLI), a ---region:{name}--- block \
(ACP), or the API `regions` field",
region.name
)
}));
}
continue;
}
seeds.insert(region.name.clone(), value.to_string());
}
RegionSeed::Literal { text } => {
seeds.insert(region.name.clone(), text.clone());
}
RegionSeed::Files { paths } => {
let resolved = paths
.iter()
.map(|p| seed_path_within(base, &base.join(p), read_paths))
.collect::<Result<Vec<_>, _>>()
.map_err(|e| format!("region '{}': {e}", region.name))?;
let content = read_and_concat(®ion.name, resolved.into_iter(), region.required)?;
if let Some(content) = content {
seeds.insert(region.name.clone(), content);
}
}
RegionSeed::Glob { pattern } => {
let full = base.join(pattern);
let full = full.to_string_lossy();
let matches = glob::glob(&full)
.map_err(|e| format!("region '{}': bad glob '{pattern}': {e}", region.name))?;
let paths = matches
.filter_map(|m| m.ok())
.map(|p| seed_path_within(base, &p, read_paths))
.collect::<Result<Vec<_>, _>>()
.map_err(|e| format!("region '{}': {e}", region.name))?;
let content = read_and_concat(®ion.name, paths.into_iter(), region.required)?;
match content {
Some(content) => {
seeds.insert(region.name.clone(), content);
}
None if region.required => {
return Err(format!(
"required region '{}': glob '{pattern}' matched no files",
region.name
));
}
None => {}
}
}
RegionSeed::Rhai { script } => {
let path = seed_path_within(base, &base.join(script), read_paths)
.map_err(|e| format!("region '{}': {e}", region.name))?;
let src = std::fs::read_to_string(&path).map_err(|e| {
format!(
"region '{}': read rhai seed '{}': {e}",
region.name,
path.display()
)
})?;
let mut input = rhai::Map::new();
input.insert("task".into(), rhai::Dynamic::from(args.task.clone()));
input.insert("workdir".into(), rhai::Dynamic::from(workdir.to_string()));
let out = leviath_scripting::ScriptEngine::new()
.transform(&src, input)
.map_err(|e| format!("region '{}': rhai seed failed: {e}", region.name))?;
if !out.trim().is_empty() {
seeds.insert(region.name.clone(), out);
} else if region.required {
return Err(format!(
"required region '{}': rhai seed '{script}' returned empty",
region.name
));
}
}
RegionSeed::Command { command } => {
if !commands.allowed {
if region.required {
return Err(format!(
"required region '{}': command seeds are disabled \
(`[security] allow_seed_commands = false` or --no-seed-commands)",
region.name
));
}
tracing::warn!(
region = %region.name,
"command seed skipped: command seeds are disabled"
);
continue;
}
match commands.run(command, base) {
Ok(out) if !out.trim().is_empty() => {
seeds.insert(region.name.clone(), out);
}
Ok(_) => {
if region.required {
return Err(format!(
"required region '{}': command seed '{command}' returned empty",
region.name
));
}
tracing::warn!(
region = %region.name,
command = %command,
"command seed returned no output; region left empty"
);
}
Err(e) => {
if region.required {
return Err(format!(
"required region '{}': command seed '{command}' failed: {e}",
region.name
));
}
tracing::warn!(
region = %region.name,
command = %command,
error = %e,
"command seed failed; region left empty"
);
}
}
}
}
}
Ok(seeds)
}
pub(super) fn read_and_concat(
region: &str,
paths: impl Iterator<Item = std::path::PathBuf>,
required: bool,
) -> Result<Option<String>, String> {
let mut parts: Vec<String> = Vec::new();
for path in paths {
match std::fs::read_to_string(&path) {
Ok(text) => parts.push(format!("--- {} ---\n{}", path.display(), text)),
Err(e) => {
if required {
return Err(format!(
"region '{region}': read seed file '{}': {e}",
path.display()
));
}
}
}
}
Ok((!parts.is_empty()).then(|| parts.join("\n\n")))
}
pub(super) fn build_read_path_policy(
blueprint: &leviath_core::Blueprint,
config: &crate::config::Config,
workdir: &std::path::Path,
) -> Result<(leviath_core::ReadPathPolicy, Option<String>), String> {
let Some(rp) = blueprint
.read_paths
.as_ref()
.filter(|rp| !rp.allow.is_empty())
else {
return Ok((leviath_core::ReadPathPolicy::inactive(), None));
};
let home = leviath_core::home_dir();
let declared =
leviath_core::ReadPathSet::compile(&rp.allow, workdir, home.as_deref(), cfg!(windows))
.map_err(|e| format!("agent '{}' [read_paths]: {e}", blueprint.name))?;
let grant_entries = config.read_path_grants_for_agent(&blueprint.name);
let grants =
leviath_core::ReadPathSet::compile(&grant_entries, workdir, home.as_deref(), cfg!(windows))
.map_err(|e| format!("read_paths grant in your config.toml: {e}"))?;
let allow_blueprint = config.security.allow_blueprint_read_paths;
let warning = (!allow_blueprint && grants.is_empty()).then(|| {
let entries = rp
.allow
.iter()
.map(|e| format!("\"{e}\""))
.collect::<Vec<_>>()
.join(", ");
format!(
"agent '{name}' declares [read_paths] but nothing grants them; reads outside \
the workdir will be refused. To grant them, add to your config.toml either:\n\
[security]\nallow_blueprint_read_paths = true\n\
or the specific paths:\n[agent_read_paths.{name}]\nallow = [{entries}]",
name = blueprint.name,
)
});
Ok((
leviath_core::ReadPathPolicy {
agent: blueprint.name.clone(),
blueprint: declared,
grants,
allow_blueprint,
},
warning,
))
}
pub(super) fn read_path_grant_counts(
blueprint: &leviath_core::Blueprint,
config: &crate::config::Config,
workdir: &std::path::Path,
) -> Option<leviath_core::run_meta::ReadPathGrantCounts> {
let report = crate::read_path_report::build(blueprint, config, workdir)?.ok()?;
Some(leviath_core::run_meta::ReadPathGrantCounts {
declared: report.declared(),
granted: report.granted(),
})
}
pub(super) fn bump_read_sensitivities(
map: &mut HashMap<String, leviath_core::TaintLevel>,
read_paths_granted: bool,
) {
if !read_paths_granted {
return;
}
for tool in ["read_file", "read_files", "list_dir"] {
if let Some(level) = map.get_mut(tool) {
*level = (*level).max(leviath_core::TaintLevel::Private);
}
}
}