1use std::path::Path;
20
21use hatchling_broker::AGENT_RUN_SUBCOMMAND;
22use hatchling_broker::skill_run::SKILL_CELL_SUBCOMMAND;
23use hatchling_isolation::PROGRAM;
24
25pub 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#[derive(Debug, PartialEq, Eq)]
45pub enum Route<'a> {
46 Broker(&'a [String]),
48 Agent(&'a [String]),
50 Help,
52 Version,
54 Usage,
56}
57
58#[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 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 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}