use std::path::Path;
use hatchling_broker::AGENT_RUN_SUBCOMMAND;
use hatchling_broker::skill_run::SKILL_CELL_SUBCOMMAND;
use hatchling_isolation::PROGRAM;
pub const USAGE: &str = "\
hatchling: personal AI agents in kernel-isolated cells
Usage:
hatchling broker [subcommand] serve agents, or run one operator subcommand
hatchling agent [run \"task\"] run the agent (the broker starts it inside a cell)
hatchling --help print this help
hatchling --version print the version
Broker subcommands (run instead of serving):
migrate-vault, rekey-vault <new-key-file>, vault-keyring <action>,
redactions <action>, vault-audit
The broker is configured by HATCHLING_* environment variables; see the
README's quickstart. Set HATCHLING_AGENT_BIN to this binary's path to run
the agent from it too.";
#[derive(Debug, PartialEq, Eq)]
pub enum Route<'a> {
Broker(&'a [String]),
Agent(&'a [String]),
Help,
Version,
Usage,
}
#[must_use]
pub fn route<'a>(argv0: &str, args: &'a [String]) -> Route<'a> {
if Path::new(argv0).file_name() == Path::new(PROGRAM).file_name() {
return Route::Agent(args);
}
let rest = args.get(1..).unwrap_or_default();
match args.first().map(String::as_str) {
Some("broker") => Route::Broker(rest),
Some("agent") => Route::Agent(rest),
Some(SKILL_CELL_SUBCOMMAND | AGENT_RUN_SUBCOMMAND) => Route::Broker(args),
Some("--help" | "-h" | "help") => Route::Help,
Some("--version" | "-V") => Route::Version,
Some(_) | None => Route::Usage,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn args(list: &[&str]) -> Vec<String> {
list.iter().map(|arg| (*arg).to_owned()).collect()
}
fn tail(given: &[String]) -> &[String] {
given.get(1..).unwrap_or_default()
}
#[test]
fn subcommands_route_to_their_command_line_without_the_subcommand() {
let given = args(&["broker", "rekey-vault", "/new.key"]);
assert_eq!(route("hatchling", &given), Route::Broker(tail(&given)));
let given = args(&["agent", "run", "Say hi."]);
assert_eq!(
route("/usr/local/bin/hatchling", &given),
Route::Agent(tail(&given))
);
let given = args(&["broker"]);
assert_eq!(route("hatchling", &given), Route::Broker(&[]));
}
#[test]
fn the_brokers_spawner_helpers_reach_the_broker_with_their_subcommand() {
for helper in [SKILL_CELL_SUBCOMMAND, AGENT_RUN_SUBCOMMAND] {
let given = args(&[helper, "/run/spec"]);
assert_eq!(
route("/usr/local/bin/hatchling", &given),
Route::Broker(&given)
);
}
}
#[test]
fn started_as_the_cell_program_it_is_the_agent_with_every_argument() {
let given = args(&["run", "Say hi."]);
assert_eq!(route(PROGRAM, &given), Route::Agent(&given));
assert_eq!(route(PROGRAM, &[]), Route::Agent(&[]));
let given = args(&["broker"]);
assert_eq!(route(PROGRAM, &given), Route::Agent(&given));
}
#[test]
fn help_and_version_have_their_short_and_long_forms() {
for flag in ["--help", "-h", "help"] {
assert_eq!(route("hatchling", &args(&[flag])), Route::Help);
}
for flag in ["--version", "-V"] {
assert_eq!(route("hatchling", &args(&[flag])), Route::Version);
}
}
#[test]
fn nothing_or_an_unknown_subcommand_is_a_usage_error() {
assert_eq!(route("hatchling", &[]), Route::Usage);
assert_eq!(route("hatchling", &args(&["serve"])), Route::Usage);
assert_eq!(route("hatchling", &args(&["migrate-vault"])), Route::Usage);
}
#[test]
fn a_name_that_only_ends_in_agent_is_not_the_cell_program() {
assert_eq!(
route("/usr/bin/hatchling-agent", &args(&["broker"])),
Route::Broker(&[])
);
assert_eq!(route("secret-agent", &[]), Route::Usage);
}
}