1use std::path::Path;
23
24use hatchling_broker::AGENT_RUN_SUBCOMMAND;
25use hatchling_broker::skill_run::SKILL_CELL_SUBCOMMAND;
26use hatchling_isolation::PROGRAM;
27
28pub const USAGE: &str = "\
30hatchling: personal AI agents in kernel-isolated cells
31
32Usage:
33 hatchling broker [subcommand] serve agents, or run one operator subcommand
34 hatchling agent [run \"task\"] run the agent (the broker starts it inside a cell)
35 hatchling --help print this help
36 hatchling --version print the version
37
38Broker subcommands (run instead of serving):
39 migrate-vault, rekey-vault <new-key-file>, vault-keyring <action>,
40 redactions <action>, vault-audit
41
42The broker is configured by HATCHLING_* environment variables; see the
43README's quickstart. Set HATCHLING_AGENT_BIN to this binary's path to run
44the agent from it too.";
45
46#[must_use]
48pub fn help() -> String {
49 format!(
50 "{USAGE}\n\n{}\n{}",
51 hatchling_broker::cli::help("hatchling broker"),
52 hatchling_agent::cli::help("hatchling agent")
53 )
54}
55
56#[derive(Debug, PartialEq, Eq)]
58pub enum Route<'a> {
59 Broker(&'a [String]),
61 Agent(&'a [String]),
63 Help,
65 Version,
67 Usage,
69}
70
71#[must_use]
74pub fn route<'a>(argv0: &str, args: &'a [String]) -> Route<'a> {
75 if Path::new(argv0).file_name() == Path::new(PROGRAM).file_name() {
76 return Route::Agent(args);
77 }
78 let rest = args.get(1..).unwrap_or_default();
79 match args.first().map(String::as_str) {
80 Some("broker") => Route::Broker(rest),
81 Some("agent") => Route::Agent(rest),
82 Some(SKILL_CELL_SUBCOMMAND | AGENT_RUN_SUBCOMMAND) => Route::Broker(args),
83 Some("--help" | "-h" | "help") if args.len() == 1 => Route::Help,
84 Some("--version" | "-V") if args.len() == 1 => Route::Version,
85 Some(_) | None => Route::Usage,
86 }
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92
93 fn args(list: &[&str]) -> Vec<String> {
94 list.iter().map(|arg| (*arg).to_owned()).collect()
95 }
96
97 fn tail(given: &[String]) -> &[String] {
99 given.get(1..).unwrap_or_default()
100 }
101
102 #[test]
103 fn subcommands_route_to_their_command_line_without_the_subcommand() {
104 let given = args(&["broker", "rekey-vault", "/new.key"]);
105 assert_eq!(route("hatchling", &given), Route::Broker(tail(&given)));
106 let given = args(&["agent", "run", "Say hi."]);
107 assert_eq!(
108 route("/usr/local/bin/hatchling", &given),
109 Route::Agent(tail(&given))
110 );
111 let given = args(&["broker"]);
112 assert_eq!(route("hatchling", &given), Route::Broker(&[]));
113 }
114
115 #[test]
116 fn the_brokers_spawner_helpers_reach_the_broker_with_their_subcommand() {
117 for helper in [SKILL_CELL_SUBCOMMAND, AGENT_RUN_SUBCOMMAND] {
118 let given = args(&[helper, "/run/spec"]);
119 assert_eq!(
120 route("/usr/local/bin/hatchling", &given),
121 Route::Broker(&given)
122 );
123 }
124 }
125
126 #[test]
127 fn started_as_the_cell_program_it_is_the_agent_with_every_argument() {
128 let given = args(&["run", "Say hi."]);
129 assert_eq!(route(PROGRAM, &given), Route::Agent(&given));
130 assert_eq!(route(PROGRAM, &[]), Route::Agent(&[]));
131 let given = args(&["broker"]);
133 assert_eq!(route(PROGRAM, &given), Route::Agent(&given));
134 }
135
136 #[test]
137 fn help_and_version_have_their_short_and_long_forms() {
138 for flag in ["--help", "-h", "help"] {
139 assert_eq!(route("hatchling", &args(&[flag])), Route::Help);
140 }
141 for flag in ["--version", "-V"] {
142 assert_eq!(route("hatchling", &args(&[flag])), Route::Version);
143 }
144 for flag in ["--help", "-h", "help", "--version", "-V"] {
145 assert_eq!(route("hatchling", &args(&[flag, "extra"])), Route::Usage);
146 }
147 let text = help();
148 assert!(text.contains("Usage: hatchling broker"));
149 assert!(text.contains("Usage: hatchling agent"));
150 assert!(text.contains("HATCHLING_CELL_CGROUP"));
151 assert!(text.contains("HATCHLING_MODEL_ENDPOINT"));
152 }
153
154 #[test]
155 fn nothing_or_an_unknown_subcommand_is_a_usage_error() {
156 assert_eq!(route("hatchling", &[]), Route::Usage);
157 assert_eq!(route("hatchling", &args(&["serve"])), Route::Usage);
158 assert_eq!(route("hatchling", &args(&["migrate-vault"])), Route::Usage);
159 }
160
161 #[test]
162 fn a_name_that_only_ends_in_agent_is_not_the_cell_program() {
163 assert_eq!(
164 route("/usr/bin/hatchling-agent", &args(&["broker"])),
165 Route::Broker(&[])
166 );
167 assert_eq!(route("secret-agent", &[]), Route::Usage);
168 }
169}