Skip to main content

hatchling_runtime/
lib.rs

1//! `hatchling-runtime`: the `hatchling` command.
2//!
3//! One binary for the whole runtime: `hatchling broker` runs the broker
4//! and `hatchling agent` runs the agent, each with exactly the command
5//! line of `hatchling-broker` and `hatchling-agent`. Install it with
6//! `cargo install hatchling-runtime`.
7//!
8//! Two invocations don't name a subcommand, because the broker makes
9//! them itself:
10//! - The broker starts its spawner helpers (`skill-cell`, `agent-run`)
11//!   by re-running its own executable, which is `hatchling` here.
12//! - A cell runs its program as `/agent`, so `hatchling` started under
13//!   that name is the agent. Point `HATCHLING_AGENT_BIN` at `hatchling`
14//!   and the one binary serves both sides.
15//!
16//! The routing is [`route`], a pure function, so it's tested without
17//! starting anything.
18
19use std::path::Path;
20
21use hatchling_broker::AGENT_RUN_SUBCOMMAND;
22use hatchling_broker::skill_run::SKILL_CELL_SUBCOMMAND;
23use hatchling_isolation::PROGRAM;
24
25/// Usage, printed for `--help` on stdout and for a usage error on stderr.
26pub const USAGE: &str = "\
27hatchling: personal AI agents in kernel-isolated cells
28
29Usage:
30  hatchling broker [subcommand]   serve agents, or run one operator subcommand
31  hatchling agent [run \"task\"]    run the agent (the broker starts it inside a cell)
32  hatchling --help                print this help
33  hatchling --version             print the version
34
35Broker subcommands (run instead of serving):
36  migrate-vault, rekey-vault <new-key-file>, vault-keyring <action>,
37  redactions <action>, vault-audit
38
39The broker is configured by HATCHLING_* environment variables; see the
40README's quickstart. Set HATCHLING_AGENT_BIN to this binary's path to run
41the agent from it too.";
42
43/// What one invocation of `hatchling` runs.
44#[derive(Debug, PartialEq, Eq)]
45pub enum Route<'a> {
46    /// The broker's command line, with these arguments.
47    Broker(&'a [String]),
48    /// The agent's command line, with these arguments.
49    Agent(&'a [String]),
50    /// `--help`: usage on stdout, exit 0.
51    Help,
52    /// `--version`: the version on stdout, exit 0.
53    Version,
54    /// No subcommand, or an unknown one: usage on stderr, exit 64.
55    Usage,
56}
57
58/// Routes one invocation: `argv0` is the name it was started under, and
59/// `args` the arguments after it.
60#[must_use]
61pub fn route<'a>(argv0: &str, args: &'a [String]) -> Route<'a> {
62    if Path::new(argv0).file_name() == Path::new(PROGRAM).file_name() {
63        return Route::Agent(args);
64    }
65    let rest = args.get(1..).unwrap_or_default();
66    match args.first().map(String::as_str) {
67        Some("broker") => Route::Broker(rest),
68        Some("agent") => Route::Agent(rest),
69        Some(SKILL_CELL_SUBCOMMAND | AGENT_RUN_SUBCOMMAND) => Route::Broker(args),
70        Some("--help" | "-h" | "help") => Route::Help,
71        Some("--version" | "-V") => Route::Version,
72        Some(_) | None => Route::Usage,
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    fn args(list: &[&str]) -> Vec<String> {
81        list.iter().map(|arg| (*arg).to_owned()).collect()
82    }
83
84    /// Everything after the subcommand.
85    fn tail(given: &[String]) -> &[String] {
86        given.get(1..).unwrap_or_default()
87    }
88
89    #[test]
90    fn subcommands_route_to_their_command_line_without_the_subcommand() {
91        let given = args(&["broker", "rekey-vault", "/new.key"]);
92        assert_eq!(route("hatchling", &given), Route::Broker(tail(&given)));
93        let given = args(&["agent", "run", "Say hi."]);
94        assert_eq!(
95            route("/usr/local/bin/hatchling", &given),
96            Route::Agent(tail(&given))
97        );
98        let given = args(&["broker"]);
99        assert_eq!(route("hatchling", &given), Route::Broker(&[]));
100    }
101
102    #[test]
103    fn the_brokers_spawner_helpers_reach_the_broker_with_their_subcommand() {
104        for helper in [SKILL_CELL_SUBCOMMAND, AGENT_RUN_SUBCOMMAND] {
105            let given = args(&[helper, "/run/spec"]);
106            assert_eq!(
107                route("/usr/local/bin/hatchling", &given),
108                Route::Broker(&given)
109            );
110        }
111    }
112
113    #[test]
114    fn started_as_the_cell_program_it_is_the_agent_with_every_argument() {
115        let given = args(&["run", "Say hi."]);
116        assert_eq!(route(PROGRAM, &given), Route::Agent(&given));
117        assert_eq!(route(PROGRAM, &[]), Route::Agent(&[]));
118        // Even an argument that names a subcommand stays the agent's.
119        let given = args(&["broker"]);
120        assert_eq!(route(PROGRAM, &given), Route::Agent(&given));
121    }
122
123    #[test]
124    fn help_and_version_have_their_short_and_long_forms() {
125        for flag in ["--help", "-h", "help"] {
126            assert_eq!(route("hatchling", &args(&[flag])), Route::Help);
127        }
128        for flag in ["--version", "-V"] {
129            assert_eq!(route("hatchling", &args(&[flag])), Route::Version);
130        }
131    }
132
133    #[test]
134    fn nothing_or_an_unknown_subcommand_is_a_usage_error() {
135        assert_eq!(route("hatchling", &[]), Route::Usage);
136        assert_eq!(route("hatchling", &args(&["serve"])), Route::Usage);
137        assert_eq!(route("hatchling", &args(&["migrate-vault"])), Route::Usage);
138    }
139
140    #[test]
141    fn a_name_that_only_ends_in_agent_is_not_the_cell_program() {
142        assert_eq!(
143            route("/usr/bin/hatchling-agent", &args(&["broker"])),
144            Route::Broker(&[])
145        );
146        assert_eq!(route("secret-agent", &[]), Route::Usage);
147    }
148}