Skip to main content

leviath_cli/
bundled.rs

1//! The agent blueprints shipped inside the `lev` binary, and the planner that
2//! decides what to do with them.
3//!
4//! Embedding is what makes the ten blueprints under the workspace's `agents/`
5//! directory reachable outside a git checkout: `lev add` takes a local path,
6//! and an `agents/` directory next to the executable is a layout no real
7//! install has, so without the bundle a user who downloads a release binary
8//! gets a working runtime and zero agents to run on it.
9//!
10//! `build.rs` embeds every file of every blueprint via `include_str!` (23
11//! files, ~170 KB of text) and generates the [`BUNDLED_AGENTS`] table included
12//! below. `lev setup` offers to install them; `lev list` reports them.
13
14include!(concat!(env!("OUT_DIR"), "/bundled_agents.rs"));
15
16use std::path::Path;
17
18/// What `lev setup` should do with one bundled blueprint, given what is
19/// currently installed.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum AgentAction {
22    /// Not installed.
23    Install,
24    /// Installed at a different version.
25    Update {
26        /// The version currently on disk, so the offer can say what it replaces.
27        from: String,
28    },
29    /// Installed at the bundled version, but the files on disk differ from the
30    /// bundled ones.
31    Modified,
32    /// Installed at the bundled version, byte for byte.
33    UpToDate,
34}
35
36impl AgentAction {
37    /// Whether applying this action would change anything on disk.
38    pub fn is_change(&self) -> bool {
39        !matches!(self, Self::UpToDate)
40    }
41
42    /// Whether the wizard should pre-check this row.
43    ///
44    /// Not the same question as [`Self::is_change`], and the difference is the
45    /// point of [`Self::Modified`]: reinstalling over a tree the user edited
46    /// destroys their work, and `install_bundled` removes the destination
47    /// first, so it destroys files they added too. Offered, never assumed.
48    pub fn preselect(&self) -> bool {
49        matches!(self, Self::Install | Self::Update { .. })
50    }
51
52    /// Short label for the wizard's blueprint list.
53    pub fn label(&self, to: &str) -> String {
54        match self {
55            Self::Install => format!("install {to}"),
56            Self::Update { from } => format!("update {from} → {to}"),
57            Self::Modified => format!("{to}, edited locally - reinstall overwrites"),
58            Self::UpToDate => "up to date".to_string(),
59        }
60    }
61}
62
63/// The installed version of `name` under `agents_dir`, if a readable manifest
64/// is there.
65///
66/// Deliberately lenient: a blueprint directory whose manifest is missing or
67/// unparseable reads as *not installed*, so the wizard offers a clean reinstall
68/// instead of refusing to plan. An unreadable manifest is exactly the state a
69/// half-finished copy leaves behind.
70pub fn installed_version(agents_dir: &Path, name: &str) -> Option<String> {
71    let manifest = std::fs::read_to_string(agents_dir.join(name).join("agent.leviath")).ok()?;
72    leviath_core::manifest::parse_manifest(&manifest)
73        .ok()
74        .map(|bp| bp.version)
75}
76
77/// Whether the installed copy of `agent` is byte-identical to the bundled one.
78///
79/// [`install_bundled`] removes the destination first, so a tree it wrote has
80/// exactly the bundle's files with exactly the bundle's bytes. Any difference -
81/// an edited manifest, a tool script the user added, one they deleted - means
82/// what is on disk is not what shipped.
83///
84/// An IO error reads as *differing*, which is the safe direction: the caller
85/// uses this to decide whether overwriting is safe, and a directory it cannot
86/// read is not one to clobber unasked.
87fn matches_bundled(agent: &BundledAgent, agents_dir: &Path) -> bool {
88    let dest = agents_dir.join(agent.name);
89    for (rel, contents) in agent.files {
90        match std::fs::read_to_string(dest.join(rel)) {
91            Ok(on_disk) if on_disk == *contents => {}
92            _ => return false,
93        }
94    }
95    // Every declared file was found and matched, so equal counts means the two
96    // sets are equal - which is what catches a file the user added.
97    installed_file_count(&dest) == agent.files.len()
98}
99
100/// How many files are under `dir`, recursively.
101///
102/// An entry that cannot be read counts as one file rather than aborting the
103/// walk. The only caller is asking whether the tree is exactly the bundled one,
104/// and something on disk it cannot read is already an answer of "no".
105fn installed_file_count(dir: &Path) -> usize {
106    let Ok(entries) = std::fs::read_dir(dir) else {
107        return 0;
108    };
109    entries
110        .map(|entry| match entry.map(|e| e.path()) {
111            Ok(path) if path.is_dir() => installed_file_count(&path),
112            _ => 1,
113        })
114        .sum()
115}
116
117/// Decide what to do with every bundled blueprint.
118///
119/// Version comparison is plain string inequality, not semver ordering: this
120/// crate has no semver dependency, and both versions are shown to the user
121/// anyway, so a downgrade and an upgrade both surface as an offered update they
122/// can decline.
123///
124/// A blueprint at the bundled version is only up to date if its files are the
125/// bundled files. Comparing versions alone meant a blueprint edited without a
126/// version bump read as current forever - and so did a stale install whose
127/// version happened to match, which is how an install could sit on an old
128/// checkpoint policy while believing itself current. Nothing is hashed and
129/// nothing is stored: the bundled bytes are in the binary, so the files
130/// themselves are the comparison.
131pub fn plan_agent_actions(agents_dir: &Path) -> Vec<(&'static BundledAgent, AgentAction)> {
132    BUNDLED_AGENTS
133        .iter()
134        .map(|agent| {
135            let action = match installed_version(agents_dir, agent.name) {
136                None => AgentAction::Install,
137                Some(v) if v != agent.version => AgentAction::Update { from: v },
138                Some(_) if matches_bundled(agent, agents_dir) => AgentAction::UpToDate,
139                Some(_) => AgentAction::Modified,
140            };
141            (agent, action)
142        })
143        .collect()
144}
145
146/// A note for a run about to start on an installed bundled blueprint that this
147/// binary ships a different version of.
148///
149/// `lev setup` is the only thing that has ever said this, and only when asked.
150/// Nothing said it at the moment it mattered, so an install could sit versions
151/// behind indefinitely - which is exactly how a run kept using an old
152/// checkpoint policy while the fix had shipped.
153///
154/// Deliberately narrow. It fires only for a manifest that *is* the installed
155/// copy, under `agents_dir/<name>/`, so a blueprint of the user's own that
156/// happens to share a name with a bundled one is never nagged about.
157pub fn stale_install_note(
158    manifest_path: &Path,
159    blueprint: &leviath_core::Blueprint,
160    agents_dir: Option<&Path>,
161) -> Option<String> {
162    let installed = agents_dir?.join(&blueprint.name);
163    if !manifest_path.starts_with(&installed) {
164        return None;
165    }
166    let bundled = BUNDLED_AGENTS.iter().find(|a| a.name == blueprint.name)?;
167    if bundled.version == blueprint.version {
168        return None;
169    }
170    Some(format!(
171        "note: '{}' is installed at {}, and this build ships {}. \
172         Run `lev setup` to update it.",
173        blueprint.name, blueprint.version, bundled.version
174    ))
175}
176
177/// Write one bundled blueprint into `<agents_dir>/<name>/`, replacing whatever
178/// is there.
179///
180/// The existing tree is removed first rather than merged over: a stale file
181/// from an older version of the blueprint (a tool script that was dropped, say)
182/// would otherwise survive forever and keep being loaded. This mirrors what
183/// `lev add`'s directory install already does.
184pub fn install_bundled(agent: &BundledAgent, agents_dir: &Path) -> anyhow::Result<()> {
185    let dest = agents_dir.join(agent.name);
186    if dest.exists() {
187        std::fs::remove_dir_all(&dest)?;
188    }
189    for (rel, contents) in agent.files {
190        // Derive the parent from the *relative* path rather than calling
191        // `path.parent()`. `dest.join(rel)` always has a parent, so the `None`
192        // arm of `parent()` would be unreachable code pretending to be a
193        // handled case; splitting `rel` gives two arms that both actually
194        // happen - nested (`tools/web_fetch.rhai`) and flat (`agent.leviath`).
195        let parent = match rel.rsplit_once('/') {
196            Some((dir, _)) => dest.join(dir),
197            None => dest.clone(),
198        };
199        std::fs::create_dir_all(&parent)?;
200        std::fs::write(dest.join(rel), contents)?;
201    }
202    Ok(())
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208
209    /// Every assertion here is an invariant over *all* discovered blueprints.
210    /// Naming individual agents would turn adding or renaming one into a test
211    /// edit, and would stop testing the property the moment the list drifted.
212    #[test]
213    fn every_bundled_agent_has_a_name_version_and_manifest() {
214        assert!(
215            !BUNDLED_AGENTS.is_empty(),
216            "the binary shipped with no blueprints -- build.rs found no agents/ directory"
217        );
218        for agent in BUNDLED_AGENTS {
219            assert!(!agent.name.is_empty(), "a bundled agent has an empty name");
220            assert!(
221                !agent.version.is_empty(),
222                "bundled agent {} has an empty version",
223                agent.name
224            );
225            assert!(
226                agent.files.iter().any(|(rel, _)| *rel == "agent.leviath"),
227                "bundled agent {} has no agent.leviath",
228                agent.name
229            );
230            for (rel, contents) in agent.files {
231                assert!(
232                    !rel.is_empty(),
233                    "bundled agent {} has an empty path",
234                    agent.name
235                );
236                assert!(
237                    !contents.is_empty(),
238                    "bundled agent {} has an empty file {rel}",
239                    agent.name
240                );
241            }
242        }
243    }
244
245    /// A tool script shipped under the same filename by more than one agent
246    /// must be byte-identical everywhere.
247    ///
248    /// Each agent directory is self-contained - that is what lets `lev add
249    /// <dir>` and `lev pack` work - so `web_fetch.rhai` and `web_search.rhai`
250    /// exist as five copies each rather than one shared file. That is fine
251    /// until one copy is fixed and the others are not: these scripts are the
252    /// agents' network surface, so a hardening change applied to one of five is
253    /// four agents still carrying the unfixed behaviour, with nothing to say so.
254    ///
255    /// This turns that silent drift into a test failure. Deliberately keyed on
256    /// filename over *all* discovered agents rather than naming the five, so it
257    /// keeps holding as agents are added or renamed.
258    #[test]
259    fn a_tool_script_shared_by_several_agents_is_identical_in_all_of_them() {
260        use std::collections::HashMap;
261
262        // filename -> (first agent that shipped it, its contents)
263        let mut first_seen: HashMap<&str, (&str, &str)> = HashMap::new();
264        for agent in BUNDLED_AGENTS {
265            for (rel, contents) in agent.files {
266                let Some(filename) = rel.strip_prefix("tools/") else {
267                    continue;
268                };
269                match first_seen.get(filename) {
270                    Some((other, expected)) => assert!(
271                        expected == contents,
272                        "tools/{filename} differs between bundled agents {other} and {} - \
273                         a change to one copy was not applied to the others",
274                        agent.name
275                    ),
276                    None => {
277                        first_seen.insert(filename, (agent.name, contents));
278                    }
279                }
280            }
281        }
282        // Guard against a vacuous pass: if the scan found no tool scripts at
283        // all, the loop above asserts nothing.
284        assert!(
285            !first_seen.is_empty(),
286            "no bundled agent ships a tools/ script - this invariant is not being tested"
287        );
288    }
289
290    #[test]
291    fn every_bundled_manifest_parses_and_agrees_with_its_recorded_version() {
292        // The recorded version drives install/update planning, so a build.rs
293        // scan that disagreed with the manifest would make the wizard lie.
294        for agent in BUNDLED_AGENTS {
295            let manifest = agent
296                .files
297                .iter()
298                .find(|(rel, _)| *rel == "agent.leviath")
299                .map(|(_, c)| *c)
300                .expect("checked above");
301            // `.expect`, not `.unwrap_or_else(|e| panic!(...))`: the closure in
302            // the latter is a function that never runs on a passing test, which
303            // reads to llvm-cov as an uncovered region. For the same reason the
304            // message is a literal - a *call* in an `assert!`'s format args is
305            // also a region that only the failing path reaches.
306            let parsed = leviath_core::manifest::parse_manifest(manifest);
307            assert!(
308                parsed.is_ok(),
309                "bundled agent {} does not parse",
310                agent.name
311            );
312            let blueprint = parsed.expect("asserted Ok just above");
313            assert_eq!(blueprint.version, agent.version);
314            assert_eq!(blueprint.name, agent.name);
315        }
316    }
317
318    /// Every bundled agent ends in a stage that hands something back, and
319    /// nothing upstream can end the run before reaching it.
320    ///
321    /// The second half is the part that fails quietly. `allow_complete` on any
322    /// earlier stage offers the model a "DONE" it can pick instead of routing
323    /// onward - and it is appended even to a stage's custom `transition_prompt`,
324    /// so a blueprint can offer an exit its own prompt never mentions. A run
325    /// that takes it finishes with no answer, looking exactly like success.
326    /// That happened to `writing-assistant` while this was being written.
327    ///
328    /// Asserted over whatever is bundled rather than a hard-coded list, so a
329    /// new agent is held to it the day it lands.
330    #[test]
331    fn every_bundled_agent_ends_by_handing_something_back() {
332        for agent in BUNDLED_AGENTS {
333            let manifest = agent
334                .files
335                .iter()
336                .find(|(rel, _)| *rel == "agent.leviath")
337                .map(|(_, c)| *c)
338                .expect("checked above");
339            let blueprint = leviath_core::manifest::parse_manifest(manifest)
340                .expect("checked by every_bundled_manifest_parses");
341
342            let outputs: Vec<&leviath_core::Stage> = blueprint
343                .stages
344                .iter()
345                .filter(|s| s.mode == leviath_core::blueprint::StageMode::Output)
346                .collect();
347            assert!(
348                !outputs.is_empty(),
349                "bundled agent {} has no output stage, so a run of it hands back nothing",
350                agent.name
351            );
352
353            for stage in &outputs {
354                // The mode is meant to imply all three; a stage where it did
355                // not would advertise a tool it is not required to call.
356                assert!(stage.require_output, "{} output stage", agent.name);
357                assert!(
358                    stage
359                        .available_tools
360                        .iter()
361                        .any(|t| t == leviath_core::blueprint::SUBMIT_OUTPUT_TOOL),
362                    "{} output stage cannot submit",
363                    agent.name
364                );
365                // A stage whose job is to report has no business writing files.
366                assert!(
367                    !stage.available_tools.iter().any(|t| {
368                        leviath_core::blueprint::MODIFYING_TOOLS
369                            .contains(&leviath_tools::canonical_tool_name(t))
370                    }),
371                    "{} output stage can modify files",
372                    agent.name
373                );
374            }
375
376            for stage in &blueprint.stages {
377                assert!(
378                    !stage.allow_complete
379                        || stage.mode == leviath_core::blueprint::StageMode::Output,
380                    "bundled agent {}: stage '{}' may end the run, skipping the output stage",
381                    agent.name,
382                    stage.name
383                );
384            }
385        }
386    }
387
388    /// Every provider `lev setup` can configure. Claude Code is a transport
389    /// rather than a provider a stage names, so it is not in this list.
390    const SETUP_PROVIDERS: &[&str] = &["anthropic", "openai", "google", "openrouter", "ollama"];
391
392    /// The published JSON Schema for `agent.leviath`.
393    ///
394    /// Compiled into the test so it cannot drift from the file that ships:
395    /// this is the same text served at
396    /// `https://leviath.dev/docs/<channel>/blueprint.schema.json`.
397    const BLUEPRINT_SCHEMA: &str = include_str!("../../../docs/schema/blueprint.schema.json");
398
399    /// Every way `value` fails `validator`, as readable lines.
400    ///
401    /// Shared by the positive and negative tests so the formatting closure runs
402    /// against real errors. Called only from the passing path of each, because
403    /// a call inside an `assert!` message is a region only failure reaches.
404    fn schema_problems(
405        validator: &jsonschema::Validator,
406        value: &serde_json::Value,
407    ) -> Vec<String> {
408        validator
409            .iter_errors(value)
410            .map(|e| format!("{}: {e}", e.instance_path()))
411            .collect()
412    }
413
414    /// Convert parsed TOML to JSON so a JSON Schema can be applied to it.
415    fn toml_to_json(value: &toml::Value) -> serde_json::Value {
416        match value {
417            toml::Value::String(s) => serde_json::Value::String(s.clone()),
418            toml::Value::Integer(i) => serde_json::Value::from(*i),
419            toml::Value::Float(f) => serde_json::Value::from(*f),
420            toml::Value::Boolean(b) => serde_json::Value::Bool(*b),
421            // A TOML datetime has no JSON counterpart; the blueprint format has
422            // no datetime-valued key, so rendering it as its own text is enough
423            // for the schema to reject it wherever it appears.
424            toml::Value::Datetime(d) => serde_json::Value::String(d.to_string()),
425            toml::Value::Array(items) => {
426                serde_json::Value::Array(items.iter().map(toml_to_json).collect())
427            }
428            toml::Value::Table(table) => serde_json::Value::Object(
429                table
430                    .iter()
431                    .map(|(k, v)| (k.clone(), toml_to_json(v)))
432                    .collect(),
433            ),
434        }
435    }
436
437    #[test]
438    fn toml_converts_to_json_for_every_value_kind() {
439        // Every arm, because a kind converted wrongly would be validated
440        // against the wrong JSON type and the schema would pass or fail for the
441        // wrong reason. `temperature` is a real float-valued blueprint key, so
442        // that arm is not hypothetical.
443        let source = concat!(
444            "s = \"text\"\n",
445            "i = 7\n",
446            "f = 0.5\n",
447            "b = true\n",
448            "d = 1979-05-27T07:32:00Z\n",
449            "a = [1, \"two\"]\n",
450            "[t]\n",
451            "nested = 1\n"
452        );
453        let parsed: toml::Value = toml::from_str(source).expect("valid TOML");
454        let json = toml_to_json(&parsed);
455        assert_eq!(json["s"], serde_json::json!("text"));
456        assert_eq!(json["i"], serde_json::json!(7));
457        assert_eq!(json["f"], serde_json::json!(0.5));
458        assert_eq!(json["b"], serde_json::json!(true));
459        // No JSON counterpart for a datetime, so it becomes its own text.
460        assert!(json["d"].is_string());
461        assert_eq!(json["a"], serde_json::json!([1, "two"]));
462        assert_eq!(json["t"]["nested"], serde_json::json!(1));
463    }
464
465    #[test]
466    fn every_bundled_blueprint_validates_against_the_published_schema() {
467        // The schema is the only machine-readable description of this format,
468        // and an agent authoring a blueprint will write against it. Nothing but
469        // this test keeps it honest: the parser is a hand-rolled toml::Value
470        // walker, so there is no derive to generate it from.
471        let schema: serde_json::Value =
472            serde_json::from_str(BLUEPRINT_SCHEMA).expect("the schema is valid JSON");
473        let validator = jsonschema::validator_for(&schema).expect("the schema compiles");
474
475        for agent in BUNDLED_AGENTS {
476            let manifest = agent
477                .files
478                .iter()
479                .find(|(rel, _)| *rel == "agent.leviath")
480                .map(|(_, c)| *c)
481                .expect("every bundled agent has a manifest");
482            let parsed: toml::Value = toml::from_str(manifest).expect("the manifest is valid TOML");
483            let json = toml_to_json(&parsed);
484
485            assert_eq!(
486                schema_problems(&validator, &json),
487                Vec::<String>::new(),
488                "{} does not match blueprint.schema.json",
489                agent.name
490            );
491        }
492    }
493
494    #[test]
495    fn the_blueprint_schema_rejects_what_the_parser_rejects() {
496        // A schema that accepts everything would pass the test above over any
497        // input at all. These are the mistakes it exists to catch before a run
498        // is ever spawned.
499        let schema: serde_json::Value =
500            serde_json::from_str(BLUEPRINT_SCHEMA).expect("the schema is valid JSON");
501        let validator = jsonschema::validator_for(&schema).expect("the schema compiles");
502        // Goes through `schema_problems` rather than `is_valid`, so the same
503        // error-formatting path the positive test uses is actually exercised
504        // by something that produces errors.
505        let rejects = |manifest: &str| {
506            let parsed: toml::Value = toml::from_str(manifest).expect("valid TOML");
507            !schema_problems(&validator, &toml_to_json(&parsed)).is_empty()
508        };
509
510        assert!(
511            rejects("[stages.main]\nmode = \"autonomous\"\n"),
512            "no [agent]"
513        );
514        assert!(
515            rejects("[agent]\nname = \"a\"\n\n[context.regions]\nx = { kind = \"nonsense\" }\n"),
516            "unknown region kind"
517        );
518        assert!(
519            rejects(
520                "[agent]\nname = \"a\"\n\n[stages.main.transitions.other]\ncondition = \"whenever\"\n"
521            ),
522            "unknown transition condition"
523        );
524        assert!(
525            rejects("[agent]\nname = \"a\"\n\n[stages.main]\nmax_iteratoins = 5\n"),
526            "a typo'd stage key"
527        );
528        assert!(
529            rejects("[agent]\nname = \"a\"\n\n[tool_permissions]\nshell = \"maybe\"\n"),
530            "an invalid tool policy"
531        );
532        // And the minimum the parser accepts still passes, so the rules above
533        // are not rejecting everything.
534        assert!(!rejects("[agent]\nname = \"a\"\n"), "a minimal manifest");
535    }
536
537    #[test]
538    fn every_bundled_stage_offers_every_provider_setup_can_configure() {
539        // Getting Started promises that one provider is all you need. That is
540        // only true if each stage lists them all: a stage naming a subset fails
541        // at spawn on a machine holding a key for a provider it left out.
542        //
543        // Discovered from BUNDLED_AGENTS rather than enumerated, so a new
544        // blueprint is covered the day it lands.
545        for agent in BUNDLED_AGENTS {
546            let manifest = agent
547                .files
548                .iter()
549                .find(|(rel, _)| *rel == "agent.leviath")
550                .map(|(_, c)| *c)
551                .expect("every bundled agent has a manifest");
552            let blueprint =
553                leviath_core::manifest::parse_manifest(manifest).expect("manifest parses");
554
555            for stage in &blueprint.stages {
556                let stage_name = &stage.name;
557                let listed: Vec<&str> = stage
558                    .model
559                    .models
560                    .iter()
561                    .map(|entry| entry.provider.as_str())
562                    .collect();
563                for provider in SETUP_PROVIDERS {
564                    assert!(
565                        listed.contains(provider),
566                        "{}/{} omits provider {}",
567                        agent.name,
568                        stage_name,
569                        provider
570                    );
571                }
572                // Ollama needs no API key, so it registers on every machine. Any
573                // position but last makes it beat a provider the user actually
574                // configured, and the run then dies on its first inference.
575                assert_eq!(
576                    listed.last().copied(),
577                    Some("ollama"),
578                    "{}/{} must list ollama last",
579                    agent.name,
580                    stage_name
581                );
582            }
583        }
584    }
585
586    /// The lint env for a bundled agent: the built-ins, the sub-agent tools,
587    /// and the agent's own `tools/<name>.rhai`, each of which defines `<name>`.
588    ///
589    /// Built by hand rather than through `LintEnv::offline`, which discovers
590    /// script tools by reading a directory: a bundled agent's files are
591    /// compiled into the binary and there is no directory to read.
592    fn lint_env_for(agent: &BundledAgent) -> crate::lint::LintEnv {
593        let mut known_tools: std::collections::HashSet<String> = leviath_tools::BuiltinTools::new(
594            leviath_tools::ToolContext::new(std::path::PathBuf::from(".")),
595        )
596        .names()
597        .into_iter()
598        .collect();
599        known_tools.extend(leviath_tools::BuiltinTools::subagent_tool_names());
600        known_tools.extend(
601            agent
602                .files
603                .iter()
604                .filter_map(|(rel, _)| rel.strip_prefix("tools/"))
605                .filter_map(|f| f.strip_suffix(".rhai"))
606                .map(str::to_string),
607        );
608        crate::lint::LintEnv {
609            known_tools,
610            known_models: crate::commands::models::closed_catalog_models(),
611            available_providers: None,
612            read_paths: None,
613            safe_commands_granted: None,
614        }
615    }
616
617    /// No bundled agent ships a blueprint the linter calls broken.
618    ///
619    /// The errors this catches are the ones that are invisible on inspection: a
620    /// tool name matching nothing is silently dropped from what the stage
621    /// advertises, so the model is told the tool does not exist and the stage
622    /// cannot do its job. A permission for a tool the stage never granted is the
623    /// same drift from the other side, reading as a grant and not being one.
624    ///
625    /// Asserted by running the shipped linter rather than by a parallel copy of
626    /// its rules, and over all discovered agents rather than a list of names -
627    /// either would stop testing the property the moment it drifted.
628    #[test]
629    fn no_bundled_agent_has_a_lint_error() {
630        for agent in BUNDLED_AGENTS {
631            let manifest = agent
632                .files
633                .iter()
634                .find(|(rel, _)| *rel == "agent.leviath")
635                .map(|(_, c)| *c)
636                .expect("every bundled agent has a manifest");
637            let parsed = leviath_core::manifest::parse_manifest(manifest);
638            assert!(
639                parsed.is_ok(),
640                "bundled agent {} does not parse",
641                agent.name
642            );
643            let blueprint = parsed.expect("asserted Ok just above");
644            // Every finding is rendered up front, and the errors are then
645            // *counted* rather than collected. Any per-error work - a `.map`
646            // that formats, a `.collect` into a list of messages - sits in a
647            // closure that only runs when the test is about to fail, which
648            // llvm-cov reads as an uncovered region for as long as the
649            // invariant holds. Counting has no such body.
650            let rendered: Vec<(bool, String)> =
651                crate::lint::lint_manifest(manifest, &blueprint, &lint_env_for(agent))
652                    .iter()
653                    .map(|f| (f.is_error(), format!("{} [{}]", f.one_line(), f.code)))
654                    .collect();
655            let error_count = rendered.iter().filter(|(is_error, _)| *is_error).count();
656            assert_eq!(
657                error_count, 0,
658                "bundled agent {} has lint errors, among {rendered:?}",
659                agent.name
660            );
661        }
662    }
663
664    /// The invariant above can actually fail - a check over shipped data that
665    /// happens to pass says nothing about whether it would catch drift.
666    #[test]
667    fn the_lint_invariant_catches_a_typo_and_an_orphan_permission() {
668        let manifest = r#"
669[agent]
670name = "x"
671version = "0.1.0"
672description = "x"
673
674[stages.only]
675mode = "autonomous"
676model = { provider = "anthropic", model = "claude-sonnet-5" }
677max_iterations = 5
678available_tools = ["read_file", "raed_file"]
679
680[stages.only.tool_permissions]
681write_file = "allow"
682"#;
683        let bp = leviath_core::manifest::parse_manifest(manifest)
684            .expect("the fixture parses; it is the lint that should object");
685        // Reuse the same env shape a real bundled agent gets, minus any scripts.
686        let env = lint_env_for(&BundledAgent {
687            name: "x",
688            version: "0.1.0",
689            files: &[],
690        });
691        let codes: Vec<&str> = crate::lint::lint_manifest(manifest, &bp, &env)
692            .iter()
693            .filter(|f| f.is_error())
694            .map(|f| f.code)
695            .collect();
696        assert_eq!(codes, ["unknown-tool", "orphan-stage-permission"]);
697    }
698
699    #[test]
700    fn bundled_agent_names_are_unique() {
701        let mut names: Vec<&str> = BUNDLED_AGENTS.iter().map(|a| a.name).collect();
702        names.sort_unstable();
703        let count = names.len();
704        names.dedup();
705        assert_eq!(count, names.len(), "duplicate bundled agent names");
706    }
707
708    // ─── installed_version ──────────────────────────────────────────────────
709
710    #[test]
711    fn installed_version_reads_a_manifest() {
712        let dir = tempfile::tempdir().unwrap();
713        let agent = &BUNDLED_AGENTS[0];
714        install_bundled(agent, dir.path()).unwrap();
715
716        assert_eq!(
717            installed_version(dir.path(), agent.name).as_deref(),
718            Some(agent.version)
719        );
720    }
721
722    #[test]
723    fn installed_version_is_none_when_nothing_is_installed() {
724        let dir = tempfile::tempdir().unwrap();
725        assert!(installed_version(dir.path(), "not-installed").is_none());
726    }
727
728    #[test]
729    fn installed_version_is_none_for_an_unparseable_manifest() {
730        // A half-written install must read as "not installed" so the wizard
731        // offers a clean reinstall rather than refusing to plan.
732        let dir = tempfile::tempdir().unwrap();
733        std::fs::create_dir_all(dir.path().join("broken")).unwrap();
734        std::fs::write(
735            dir.path().join("broken/agent.leviath"),
736            "not valid toml {{{",
737        )
738        .unwrap();
739
740        assert!(installed_version(dir.path(), "broken").is_none());
741    }
742
743    // ─── plan_agent_actions ─────────────────────────────────────────────────
744
745    #[test]
746    fn plan_offers_to_install_everything_into_an_empty_dir() {
747        let dir = tempfile::tempdir().unwrap();
748
749        let plan = plan_agent_actions(dir.path());
750
751        assert_eq!(plan.len(), BUNDLED_AGENTS.len());
752        for (agent, action) in &plan {
753            assert_eq!(*action, AgentAction::Install);
754            assert!(action.is_change());
755            assert_eq!(
756                action.label(agent.version),
757                format!("install {}", agent.version)
758            );
759        }
760    }
761
762    #[test]
763    fn plan_reports_up_to_date_after_installing() {
764        let dir = tempfile::tempdir().unwrap();
765        for agent in BUNDLED_AGENTS {
766            install_bundled(agent, dir.path()).unwrap();
767        }
768
769        let plan = plan_agent_actions(dir.path());
770
771        for (agent, action) in &plan {
772            assert_eq!(*action, AgentAction::UpToDate, "{}", agent.name);
773            assert!(!action.is_change());
774            assert_eq!(action.label(agent.version), "up to date");
775        }
776    }
777
778    #[test]
779    fn plan_reports_an_update_when_the_installed_version_differs() {
780        let dir = tempfile::tempdir().unwrap();
781        let agent = &BUNDLED_AGENTS[0];
782        install_bundled(agent, dir.path()).unwrap();
783        // Rewrite the installed manifest at a different version.
784        let manifest_path = dir.path().join(agent.name).join("agent.leviath");
785        let manifest = std::fs::read_to_string(&manifest_path).unwrap();
786        let bumped = manifest.replacen(
787            &format!("version = \"{}\"", agent.version),
788            "version = \"9.9.9\"",
789            1,
790        );
791        std::fs::write(&manifest_path, bumped).unwrap();
792
793        let plan = plan_agent_actions(dir.path());
794        let (_, action) = plan
795            .iter()
796            .find(|(a, _)| a.name == agent.name)
797            .expect("the bundled agent is in the plan");
798
799        assert_eq!(
800            *action,
801            AgentAction::Update {
802                from: "9.9.9".to_string()
803            }
804        );
805        assert!(action.is_change());
806        assert_eq!(
807            action.label(agent.version),
808            format!("update 9.9.9 → {}", agent.version)
809        );
810    }
811
812    /// The limitation this closes: comparing versions alone meant a blueprint
813    /// edited without a version bump read as current forever, so the user was
814    /// never told their copy had drifted from the one that shipped.
815    #[test]
816    fn plan_reports_an_edited_install_as_modified() {
817        let dir = tempfile::tempdir().unwrap();
818        let agent = &BUNDLED_AGENTS[0];
819        install_bundled(agent, dir.path()).unwrap();
820        let manifest_path = dir.path().join(agent.name).join("agent.leviath");
821        let manifest = std::fs::read_to_string(&manifest_path).unwrap();
822        std::fs::write(&manifest_path, manifest + "\n# a local edit\n").unwrap();
823
824        let action = action_for(&plan_agent_actions(dir.path()), agent.name);
825        assert_eq!(action, AgentAction::Modified);
826        // It would change disk, so the wizard offers it - but never unasked,
827        // because reinstalling destroys the edit.
828        assert!(action.is_change());
829        assert!(!action.preselect());
830        let label = action.label(agent.version);
831        assert!(label.contains("edited locally"), "{label}");
832    }
833
834    /// `install_bundled` removes the destination first, so a file the user
835    /// added is destroyed by a reinstall too - which makes it exactly as
836    /// important to notice as an edited one.
837    #[test]
838    fn a_file_the_user_added_or_removed_counts_as_modified() {
839        let agent = &BUNDLED_AGENTS[0];
840
841        let added = tempfile::tempdir().unwrap();
842        install_bundled(agent, added.path()).unwrap();
843        std::fs::write(added.path().join(agent.name).join("notes.md"), "mine").unwrap();
844        assert_eq!(
845            action_for(&plan_agent_actions(added.path()), agent.name),
846            AgentAction::Modified
847        );
848
849        // A file deleted from a blueprint that ships more than the manifest.
850        // The manifest still parses at the bundled version, so only the file
851        // comparison can catch this.
852        let multi = BUNDLED_AGENTS
853            .iter()
854            .find(|a| a.files.len() > 1)
855            .expect("some bundled blueprint ships more than its manifest");
856        let removed = tempfile::tempdir().unwrap();
857        install_bundled(multi, removed.path()).unwrap();
858        let extra = multi
859            .files
860            .iter()
861            .map(|(rel, _)| *rel)
862            .find(|rel| *rel != "agent.leviath")
863            .expect("a file other than the manifest");
864        std::fs::remove_file(removed.path().join(multi.name).join(extra)).unwrap();
865        assert_eq!(
866            action_for(&plan_agent_actions(removed.path()), multi.name),
867            AgentAction::Modified
868        );
869    }
870
871    /// A directory that cannot be walked reads as differing, which is the safe
872    /// direction: this decides whether overwriting is safe.
873    #[test]
874    fn an_unreadable_tree_is_not_up_to_date() {
875        assert_eq!(installed_file_count(Path::new("/no/such/dir")), 0);
876        let dir = tempfile::tempdir().unwrap();
877        assert!(!matches_bundled(&BUNDLED_AGENTS[0], dir.path()));
878    }
879
880    #[test]
881    fn installed_file_count_walks_nested_directories() {
882        let dir = tempfile::tempdir().unwrap();
883        std::fs::create_dir_all(dir.path().join("a/b")).unwrap();
884        std::fs::write(dir.path().join("top.txt"), "x").unwrap();
885        std::fs::write(dir.path().join("a/mid.txt"), "x").unwrap();
886        std::fs::write(dir.path().join("a/b/leaf.txt"), "x").unwrap();
887        assert_eq!(installed_file_count(dir.path()), 3);
888    }
889
890    fn action_for(plan: &[(&'static BundledAgent, AgentAction)], name: &str) -> AgentAction {
891        plan.iter()
892            .find(|(a, _)| a.name == name)
893            .expect("the bundled agent is in the plan")
894            .1
895            .clone()
896    }
897
898    // ─── stale_install_note ─────────────────────────────────────────────────
899
900    /// The case that prompted this: an install sitting versions behind, with
901    /// nothing saying so at the moment it mattered.
902    #[test]
903    fn a_stale_install_is_named_when_the_run_starts() {
904        let dir = tempfile::tempdir().unwrap();
905        let agent = &BUNDLED_AGENTS[0];
906        install_bundled(agent, dir.path()).unwrap();
907        let manifest = dir.path().join(agent.name).join("agent.leviath");
908        let mut blueprint =
909            leviath_core::manifest::parse_manifest(&std::fs::read_to_string(&manifest).unwrap())
910                .unwrap();
911
912        // At the bundled version there is nothing to say.
913        assert_eq!(
914            stale_install_note(&manifest, &blueprint, Some(dir.path())),
915            None
916        );
917
918        blueprint.version = "0.0.1".to_string();
919        let note = stale_install_note(&manifest, &blueprint, Some(dir.path()))
920            .expect("a behind install is named");
921        assert!(note.contains("0.0.1"), "{note}");
922        assert!(note.contains(agent.version), "{note}");
923        assert!(note.contains("lev setup"), "{note}");
924    }
925
926    /// Deliberately narrow: a blueprint of the user's own that happens to share
927    /// a name with a bundled one is never nagged about, and neither is one this
928    /// build does not ship.
929    #[test]
930    fn a_blueprint_that_is_not_the_installed_copy_is_left_alone() {
931        let dir = tempfile::tempdir().unwrap();
932        let agent = &BUNDLED_AGENTS[0];
933        install_bundled(agent, dir.path()).unwrap();
934        let manifest = dir.path().join(agent.name).join("agent.leviath");
935        let mut blueprint =
936            leviath_core::manifest::parse_manifest(&std::fs::read_to_string(&manifest).unwrap())
937                .unwrap();
938        blueprint.version = "0.0.1".to_string();
939
940        // Somewhere else on disk, under the same name.
941        let elsewhere = tempfile::tempdir().unwrap();
942        let copy = elsewhere.path().join(agent.name).join("agent.leviath");
943        assert_eq!(
944            stale_install_note(&copy, &blueprint, Some(dir.path())),
945            None,
946            "not the installed copy"
947        );
948
949        // No agents dir resolves at all.
950        assert_eq!(stale_install_note(&manifest, &blueprint, None), None);
951
952        // A name this build ships nothing for.
953        blueprint.name = "not-a-bundled-agent".to_string();
954        assert_eq!(
955            stale_install_note(
956                &dir.path().join("not-a-bundled-agent").join("agent.leviath"),
957                &blueprint,
958                Some(dir.path())
959            ),
960            None
961        );
962    }
963
964    // ─── install_bundled ────────────────────────────────────────────────────
965
966    #[test]
967    fn install_writes_every_file_including_nested_ones() {
968        let dir = tempfile::tempdir().unwrap();
969        // Pick a blueprint that actually has a nested `tools/` file, so the
970        // create_dir_all arm is exercised by a real shipped layout rather than
971        // a fixture. If none ships nested files any more, the flat arm below
972        // still covers the rest.
973        for agent in BUNDLED_AGENTS {
974            install_bundled(agent, dir.path()).unwrap();
975            for (rel, contents) in agent.files {
976                let written = std::fs::read_to_string(dir.path().join(agent.name).join(rel));
977                assert!(written.is_ok(), "{}/{rel} was not written", agent.name);
978                assert_eq!(written.expect("asserted Ok just above"), *contents);
979            }
980        }
981        assert!(
982            BUNDLED_AGENTS
983                .iter()
984                .any(|a| a.files.iter().any(|(rel, _)| rel.contains('/'))),
985            "no bundled blueprint has a nested file, so install's mkdir path is untested"
986        );
987    }
988
989    #[test]
990    fn install_replaces_an_existing_tree_and_drops_stale_files() {
991        let dir = tempfile::tempdir().unwrap();
992        let agent = &BUNDLED_AGENTS[0];
993        install_bundled(agent, dir.path()).unwrap();
994        let stale = dir
995            .path()
996            .join(agent.name)
997            .join("stale-from-an-older-version");
998        std::fs::write(&stale, "leftover").unwrap();
999
1000        install_bundled(agent, dir.path()).unwrap();
1001
1002        assert!(
1003            !stale.exists(),
1004            "a reinstall must not leave files from the previous version behind"
1005        );
1006        assert!(dir.path().join(agent.name).join("agent.leviath").exists());
1007    }
1008
1009    #[test]
1010    fn install_surfaces_a_directory_creation_failure() {
1011        // `agents_dir` is itself a file, so creating the blueprint directory
1012        // under it fails.
1013        let dir = tempfile::tempdir().unwrap();
1014        let blocked = dir.path().join("not-a-dir");
1015        std::fs::write(&blocked, "").unwrap();
1016
1017        let result = install_bundled(&BUNDLED_AGENTS[0], &blocked);
1018
1019        assert!(result.is_err());
1020    }
1021
1022    #[test]
1023    fn install_surfaces_a_file_write_failure() {
1024        // Isolating the `write` error from the `create_dir_all` error needs a
1025        // layout where the directory step succeeds and only the write fails.
1026        // A synthetic blueprint whose second entry names a path the first entry
1027        // already created as a *directory* does exactly that: `create_dir_all`
1028        // sees an existing dir and returns Ok, then the write hits EISDIR.
1029        // No shipped blueprint has that shape, hence the hand-built one.
1030        let agent = BundledAgent {
1031            name: "collides-with-its-own-directory",
1032            version: "0.0.1",
1033            files: &[("tools/a.rhai", "nested first"), ("tools", "then the dir")],
1034        };
1035        let dir = tempfile::tempdir().unwrap();
1036
1037        let result = install_bundled(&agent, dir.path());
1038
1039        assert!(result.is_err());
1040    }
1041
1042    #[test]
1043    fn install_surfaces_a_remove_failure() {
1044        // The destination exists but is a *file*, so `remove_dir_all` fails
1045        // rather than the write.
1046        let dir = tempfile::tempdir().unwrap();
1047        let agent = &BUNDLED_AGENTS[0];
1048        std::fs::write(dir.path().join(agent.name), "").unwrap();
1049
1050        let result = install_bundled(agent, dir.path());
1051
1052        assert!(result.is_err());
1053    }
1054}