1include!(concat!(env!("OUT_DIR"), "/bundled_agents.rs"));
15
16use std::path::Path;
17
18#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum AgentAction {
22 Install,
24 Update { from: String },
26 UpToDate,
28}
29
30impl AgentAction {
31 pub fn is_change(&self) -> bool {
34 !matches!(self, Self::UpToDate)
35 }
36
37 pub fn label(&self, to: &str) -> String {
39 match self {
40 Self::Install => format!("install {to}"),
41 Self::Update { from } => format!("update {from} → {to}"),
42 Self::UpToDate => "up to date".to_string(),
43 }
44 }
45}
46
47pub fn installed_version(agents_dir: &Path, name: &str) -> Option<String> {
55 let manifest = std::fs::read_to_string(agents_dir.join(name).join("agent.leviath")).ok()?;
56 leviath_core::manifest::parse_manifest(&manifest)
57 .ok()
58 .map(|bp| bp.version)
59}
60
61pub fn plan_agent_actions(agents_dir: &Path) -> Vec<(&'static BundledAgent, AgentAction)> {
71 BUNDLED_AGENTS
72 .iter()
73 .map(|agent| {
74 let action = match installed_version(agents_dir, agent.name) {
75 None => AgentAction::Install,
76 Some(v) if v == agent.version => AgentAction::UpToDate,
77 Some(from) => AgentAction::Update { from },
78 };
79 (agent, action)
80 })
81 .collect()
82}
83
84pub fn install_bundled(agent: &BundledAgent, agents_dir: &Path) -> anyhow::Result<()> {
92 let dest = agents_dir.join(agent.name);
93 if dest.exists() {
94 std::fs::remove_dir_all(&dest)?;
95 }
96 for (rel, contents) in agent.files {
97 let parent = match rel.rsplit_once('/') {
103 Some((dir, _)) => dest.join(dir),
104 None => dest.clone(),
105 };
106 std::fs::create_dir_all(&parent)?;
107 std::fs::write(dest.join(rel), contents)?;
108 }
109 Ok(())
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115
116 #[test]
120 fn every_bundled_agent_has_a_name_version_and_manifest() {
121 assert!(
122 !BUNDLED_AGENTS.is_empty(),
123 "the binary shipped with no blueprints -- build.rs found no agents/ directory"
124 );
125 for agent in BUNDLED_AGENTS {
126 assert!(!agent.name.is_empty(), "a bundled agent has an empty name");
127 assert!(
128 !agent.version.is_empty(),
129 "bundled agent {} has an empty version",
130 agent.name
131 );
132 assert!(
133 agent.files.iter().any(|(rel, _)| *rel == "agent.leviath"),
134 "bundled agent {} has no agent.leviath",
135 agent.name
136 );
137 for (rel, contents) in agent.files {
138 assert!(
139 !rel.is_empty(),
140 "bundled agent {} has an empty path",
141 agent.name
142 );
143 assert!(
144 !contents.is_empty(),
145 "bundled agent {} has an empty file {rel}",
146 agent.name
147 );
148 }
149 }
150 }
151
152 #[test]
166 fn a_tool_script_shared_by_several_agents_is_identical_in_all_of_them() {
167 use std::collections::HashMap;
168
169 let mut first_seen: HashMap<&str, (&str, &str)> = HashMap::new();
171 for agent in BUNDLED_AGENTS {
172 for (rel, contents) in agent.files {
173 let Some(filename) = rel.strip_prefix("tools/") else {
174 continue;
175 };
176 match first_seen.get(filename) {
177 Some((other, expected)) => assert!(
178 expected == contents,
179 "tools/{filename} differs between bundled agents {other} and {} - \
180 a change to one copy was not applied to the others",
181 agent.name
182 ),
183 None => {
184 first_seen.insert(filename, (agent.name, contents));
185 }
186 }
187 }
188 }
189 assert!(
192 !first_seen.is_empty(),
193 "no bundled agent ships a tools/ script - this invariant is not being tested"
194 );
195 }
196
197 #[test]
198 fn every_bundled_manifest_parses_and_agrees_with_its_recorded_version() {
199 for agent in BUNDLED_AGENTS {
202 let manifest = agent
203 .files
204 .iter()
205 .find(|(rel, _)| *rel == "agent.leviath")
206 .map(|(_, c)| *c)
207 .expect("checked above");
208 let parsed = leviath_core::manifest::parse_manifest(manifest);
214 assert!(
215 parsed.is_ok(),
216 "bundled agent {} does not parse",
217 agent.name
218 );
219 let blueprint = parsed.expect("asserted Ok just above");
220 assert_eq!(blueprint.version, agent.version);
221 assert_eq!(blueprint.name, agent.name);
222 }
223 }
224
225 const SETUP_PROVIDERS: &[&str] = &["anthropic", "openai", "google", "openrouter", "ollama"];
228
229 const BLUEPRINT_SCHEMA: &str = include_str!("../../../docs/schema/blueprint.schema.json");
235
236 fn schema_problems(
242 validator: &jsonschema::Validator,
243 value: &serde_json::Value,
244 ) -> Vec<String> {
245 validator
246 .iter_errors(value)
247 .map(|e| format!("{}: {e}", e.instance_path()))
248 .collect()
249 }
250
251 fn toml_to_json(value: &toml::Value) -> serde_json::Value {
253 match value {
254 toml::Value::String(s) => serde_json::Value::String(s.clone()),
255 toml::Value::Integer(i) => serde_json::Value::from(*i),
256 toml::Value::Float(f) => serde_json::Value::from(*f),
257 toml::Value::Boolean(b) => serde_json::Value::Bool(*b),
258 toml::Value::Datetime(d) => serde_json::Value::String(d.to_string()),
262 toml::Value::Array(items) => {
263 serde_json::Value::Array(items.iter().map(toml_to_json).collect())
264 }
265 toml::Value::Table(table) => serde_json::Value::Object(
266 table
267 .iter()
268 .map(|(k, v)| (k.clone(), toml_to_json(v)))
269 .collect(),
270 ),
271 }
272 }
273
274 #[test]
275 fn toml_converts_to_json_for_every_value_kind() {
276 let source = concat!(
281 "s = \"text\"\n",
282 "i = 7\n",
283 "f = 0.5\n",
284 "b = true\n",
285 "d = 1979-05-27T07:32:00Z\n",
286 "a = [1, \"two\"]\n",
287 "[t]\n",
288 "nested = 1\n"
289 );
290 let parsed: toml::Value = toml::from_str(source).expect("valid TOML");
291 let json = toml_to_json(&parsed);
292 assert_eq!(json["s"], serde_json::json!("text"));
293 assert_eq!(json["i"], serde_json::json!(7));
294 assert_eq!(json["f"], serde_json::json!(0.5));
295 assert_eq!(json["b"], serde_json::json!(true));
296 assert!(json["d"].is_string());
298 assert_eq!(json["a"], serde_json::json!([1, "two"]));
299 assert_eq!(json["t"]["nested"], serde_json::json!(1));
300 }
301
302 #[test]
303 fn every_bundled_blueprint_validates_against_the_published_schema() {
304 let schema: serde_json::Value =
309 serde_json::from_str(BLUEPRINT_SCHEMA).expect("the schema is valid JSON");
310 let validator = jsonschema::validator_for(&schema).expect("the schema compiles");
311
312 for agent in BUNDLED_AGENTS {
313 let manifest = agent
314 .files
315 .iter()
316 .find(|(rel, _)| *rel == "agent.leviath")
317 .map(|(_, c)| *c)
318 .expect("every bundled agent has a manifest");
319 let parsed: toml::Value = toml::from_str(manifest).expect("the manifest is valid TOML");
320 let json = toml_to_json(&parsed);
321
322 assert_eq!(
323 schema_problems(&validator, &json),
324 Vec::<String>::new(),
325 "{} does not match blueprint.schema.json",
326 agent.name
327 );
328 }
329 }
330
331 #[test]
332 fn the_blueprint_schema_rejects_what_the_parser_rejects() {
333 let schema: serde_json::Value =
337 serde_json::from_str(BLUEPRINT_SCHEMA).expect("the schema is valid JSON");
338 let validator = jsonschema::validator_for(&schema).expect("the schema compiles");
339 let rejects = |manifest: &str| {
343 let parsed: toml::Value = toml::from_str(manifest).expect("valid TOML");
344 !schema_problems(&validator, &toml_to_json(&parsed)).is_empty()
345 };
346
347 assert!(
348 rejects("[stages.main]\nmode = \"autonomous\"\n"),
349 "no [agent]"
350 );
351 assert!(
352 rejects("[agent]\nname = \"a\"\n\n[context.regions]\nx = { kind = \"nonsense\" }\n"),
353 "unknown region kind"
354 );
355 assert!(
356 rejects(
357 "[agent]\nname = \"a\"\n\n[stages.main.transitions.other]\ncondition = \"whenever\"\n"
358 ),
359 "unknown transition condition"
360 );
361 assert!(
362 rejects("[agent]\nname = \"a\"\n\n[stages.main]\nmax_iteratoins = 5\n"),
363 "a typo'd stage key"
364 );
365 assert!(
366 rejects("[agent]\nname = \"a\"\n\n[tool_permissions]\nshell = \"maybe\"\n"),
367 "an invalid tool policy"
368 );
369 assert!(!rejects("[agent]\nname = \"a\"\n"), "a minimal manifest");
372 }
373
374 #[test]
375 fn every_bundled_stage_offers_every_provider_setup_can_configure() {
376 for agent in BUNDLED_AGENTS {
383 let manifest = agent
384 .files
385 .iter()
386 .find(|(rel, _)| *rel == "agent.leviath")
387 .map(|(_, c)| *c)
388 .expect("every bundled agent has a manifest");
389 let blueprint =
390 leviath_core::manifest::parse_manifest(manifest).expect("manifest parses");
391
392 for stage in &blueprint.stages {
393 let stage_name = &stage.name;
394 let listed: Vec<&str> = stage
395 .model
396 .models
397 .iter()
398 .map(|entry| entry.provider.as_str())
399 .collect();
400 for provider in SETUP_PROVIDERS {
401 assert!(
402 listed.contains(provider),
403 "{}/{} omits provider {}",
404 agent.name,
405 stage_name,
406 provider
407 );
408 }
409 assert_eq!(
413 listed.last().copied(),
414 Some("ollama"),
415 "{}/{} must list ollama last",
416 agent.name,
417 stage_name
418 );
419 }
420 }
421 }
422
423 fn lint_env_for(agent: &BundledAgent) -> crate::lint::LintEnv {
430 let mut known_tools: std::collections::HashSet<String> = leviath_tools::BuiltinTools::new(
431 leviath_tools::ToolContext::new(std::path::PathBuf::from(".")),
432 )
433 .names()
434 .into_iter()
435 .collect();
436 known_tools.extend(leviath_tools::BuiltinTools::subagent_tool_names());
437 known_tools.extend(
438 agent
439 .files
440 .iter()
441 .filter_map(|(rel, _)| rel.strip_prefix("tools/"))
442 .filter_map(|f| f.strip_suffix(".rhai"))
443 .map(str::to_string),
444 );
445 crate::lint::LintEnv {
446 known_tools,
447 known_models: crate::commands::models::closed_catalog_models(),
448 available_providers: None,
449 read_paths: None,
450 }
451 }
452
453 #[test]
465 fn no_bundled_agent_has_a_lint_error() {
466 for agent in BUNDLED_AGENTS {
467 let manifest = agent
468 .files
469 .iter()
470 .find(|(rel, _)| *rel == "agent.leviath")
471 .map(|(_, c)| *c)
472 .expect("every bundled agent has a manifest");
473 let parsed = leviath_core::manifest::parse_manifest(manifest);
474 assert!(
475 parsed.is_ok(),
476 "bundled agent {} does not parse",
477 agent.name
478 );
479 let blueprint = parsed.expect("asserted Ok just above");
480 let rendered: Vec<(bool, String)> =
487 crate::lint::lint_manifest(manifest, &blueprint, &lint_env_for(agent))
488 .iter()
489 .map(|f| (f.is_error(), format!("{} [{}]", f.one_line(), f.code)))
490 .collect();
491 let error_count = rendered.iter().filter(|(is_error, _)| *is_error).count();
492 assert_eq!(
493 error_count, 0,
494 "bundled agent {} has lint errors, among {rendered:?}",
495 agent.name
496 );
497 }
498 }
499
500 #[test]
503 fn the_lint_invariant_catches_a_typo_and_an_orphan_permission() {
504 let manifest = r#"
505[agent]
506name = "x"
507version = "0.1.0"
508description = "x"
509
510[stages.only]
511mode = "autonomous"
512model = { provider = "anthropic", model = "claude-sonnet-5" }
513max_iterations = 5
514available_tools = ["read_file", "raed_file"]
515
516[stages.only.tool_permissions]
517write_file = "allow"
518"#;
519 let bp = leviath_core::manifest::parse_manifest(manifest)
520 .expect("the fixture parses; it is the lint that should object");
521 let env = lint_env_for(&BundledAgent {
523 name: "x",
524 version: "0.1.0",
525 files: &[],
526 });
527 let codes: Vec<&str> = crate::lint::lint_manifest(manifest, &bp, &env)
528 .iter()
529 .filter(|f| f.is_error())
530 .map(|f| f.code)
531 .collect();
532 assert_eq!(codes, ["unknown-tool", "orphan-stage-permission"]);
533 }
534
535 #[test]
536 fn bundled_agent_names_are_unique() {
537 let mut names: Vec<&str> = BUNDLED_AGENTS.iter().map(|a| a.name).collect();
538 names.sort_unstable();
539 let count = names.len();
540 names.dedup();
541 assert_eq!(count, names.len(), "duplicate bundled agent names");
542 }
543
544 #[test]
547 fn installed_version_reads_a_manifest() {
548 let dir = tempfile::tempdir().unwrap();
549 let agent = &BUNDLED_AGENTS[0];
550 install_bundled(agent, dir.path()).unwrap();
551
552 assert_eq!(
553 installed_version(dir.path(), agent.name).as_deref(),
554 Some(agent.version)
555 );
556 }
557
558 #[test]
559 fn installed_version_is_none_when_nothing_is_installed() {
560 let dir = tempfile::tempdir().unwrap();
561 assert!(installed_version(dir.path(), "not-installed").is_none());
562 }
563
564 #[test]
565 fn installed_version_is_none_for_an_unparseable_manifest() {
566 let dir = tempfile::tempdir().unwrap();
569 std::fs::create_dir_all(dir.path().join("broken")).unwrap();
570 std::fs::write(
571 dir.path().join("broken/agent.leviath"),
572 "not valid toml {{{",
573 )
574 .unwrap();
575
576 assert!(installed_version(dir.path(), "broken").is_none());
577 }
578
579 #[test]
582 fn plan_offers_to_install_everything_into_an_empty_dir() {
583 let dir = tempfile::tempdir().unwrap();
584
585 let plan = plan_agent_actions(dir.path());
586
587 assert_eq!(plan.len(), BUNDLED_AGENTS.len());
588 for (agent, action) in &plan {
589 assert_eq!(*action, AgentAction::Install);
590 assert!(action.is_change());
591 assert_eq!(
592 action.label(agent.version),
593 format!("install {}", agent.version)
594 );
595 }
596 }
597
598 #[test]
599 fn plan_reports_up_to_date_after_installing() {
600 let dir = tempfile::tempdir().unwrap();
601 for agent in BUNDLED_AGENTS {
602 install_bundled(agent, dir.path()).unwrap();
603 }
604
605 let plan = plan_agent_actions(dir.path());
606
607 for (agent, action) in &plan {
608 assert_eq!(*action, AgentAction::UpToDate, "{}", agent.name);
609 assert!(!action.is_change());
610 assert_eq!(action.label(agent.version), "up to date");
611 }
612 }
613
614 #[test]
615 fn plan_reports_an_update_when_the_installed_version_differs() {
616 let dir = tempfile::tempdir().unwrap();
617 let agent = &BUNDLED_AGENTS[0];
618 install_bundled(agent, dir.path()).unwrap();
619 let manifest_path = dir.path().join(agent.name).join("agent.leviath");
621 let manifest = std::fs::read_to_string(&manifest_path).unwrap();
622 let bumped = manifest.replacen(
623 &format!("version = \"{}\"", agent.version),
624 "version = \"9.9.9\"",
625 1,
626 );
627 std::fs::write(&manifest_path, bumped).unwrap();
628
629 let plan = plan_agent_actions(dir.path());
630 let (_, action) = plan
631 .iter()
632 .find(|(a, _)| a.name == agent.name)
633 .expect("the bundled agent is in the plan");
634
635 assert_eq!(
636 *action,
637 AgentAction::Update {
638 from: "9.9.9".to_string()
639 }
640 );
641 assert!(action.is_change());
642 assert_eq!(
643 action.label(agent.version),
644 format!("update 9.9.9 → {}", agent.version)
645 );
646 }
647
648 #[test]
651 fn install_writes_every_file_including_nested_ones() {
652 let dir = tempfile::tempdir().unwrap();
653 for agent in BUNDLED_AGENTS {
658 install_bundled(agent, dir.path()).unwrap();
659 for (rel, contents) in agent.files {
660 let written = std::fs::read_to_string(dir.path().join(agent.name).join(rel));
661 assert!(written.is_ok(), "{}/{rel} was not written", agent.name);
662 assert_eq!(written.expect("asserted Ok just above"), *contents);
663 }
664 }
665 assert!(
666 BUNDLED_AGENTS
667 .iter()
668 .any(|a| a.files.iter().any(|(rel, _)| rel.contains('/'))),
669 "no bundled blueprint has a nested file, so install's mkdir path is untested"
670 );
671 }
672
673 #[test]
674 fn install_replaces_an_existing_tree_and_drops_stale_files() {
675 let dir = tempfile::tempdir().unwrap();
676 let agent = &BUNDLED_AGENTS[0];
677 install_bundled(agent, dir.path()).unwrap();
678 let stale = dir
679 .path()
680 .join(agent.name)
681 .join("stale-from-an-older-version");
682 std::fs::write(&stale, "leftover").unwrap();
683
684 install_bundled(agent, dir.path()).unwrap();
685
686 assert!(
687 !stale.exists(),
688 "a reinstall must not leave files from the previous version behind"
689 );
690 assert!(dir.path().join(agent.name).join("agent.leviath").exists());
691 }
692
693 #[test]
694 fn install_surfaces_a_directory_creation_failure() {
695 let dir = tempfile::tempdir().unwrap();
698 let blocked = dir.path().join("not-a-dir");
699 std::fs::write(&blocked, "").unwrap();
700
701 let result = install_bundled(&BUNDLED_AGENTS[0], &blocked);
702
703 assert!(result.is_err());
704 }
705
706 #[test]
707 fn install_surfaces_a_file_write_failure() {
708 let agent = BundledAgent {
715 name: "collides-with-its-own-directory",
716 version: "0.0.1",
717 files: &[("tools/a.rhai", "nested first"), ("tools", "then the dir")],
718 };
719 let dir = tempfile::tempdir().unwrap();
720
721 let result = install_bundled(&agent, dir.path());
722
723 assert!(result.is_err());
724 }
725
726 #[test]
727 fn install_surfaces_a_remove_failure() {
728 let dir = tempfile::tempdir().unwrap();
731 let agent = &BUNDLED_AGENTS[0];
732 std::fs::write(dir.path().join(agent.name), "").unwrap();
733
734 let result = install_bundled(agent, dir.path());
735
736 assert!(result.is_err());
737 }
738}