1mod agent;
2mod agent_credentials;
3mod api;
4mod app;
5mod apparmor;
6mod cli;
7pub mod client;
8mod command;
9mod config;
10mod egress_probe;
11mod invitation;
12mod locks;
13mod managed;
14mod metadata;
15mod platform;
16mod principal_grants;
17mod redeploy_version;
18mod release;
19mod ssh_service;
20mod system_user;
21mod tunnel_operation;
22pub mod ui;
23mod wireguard_endpoint;
24mod wireguard_keys;
25
26use std::sync::Arc;
27
28use anyhow::{Result, bail};
29use clap::Parser;
30
31use crate::cli::{AgentCommands, Cli, Commands};
32
33pub fn run_cli() -> capulus::CliTermination {
34 let Cli {
35 api_base,
36 namespace,
37 ui: ui_options,
38 command,
39 } = Cli::parse();
40 if matches!(command, Commands::Agent(_)) && (api_base.is_some() || namespace.is_some()) {
41 return capulus::CliTermination::without_ui(Err(anyhow::anyhow!(
42 "agent commands use the enrolled context in their configuration; --api-base and --namespace apply to CLI operations"
43 )));
44 }
45 let ui_configuration = ui_options.options();
46 match command {
47 Commands::Agent(agent) => match agent.command {
48 AgentCommands::DirectSsh => {
49 if let Err(error) = ui::init(ui_configuration) {
50 return capulus::CliTermination::without_ui(Err(error));
51 }
52 capulus::CliTermination::with_ui(ui::current(), app::run_direct_ssh())
53 }
54 command => capulus::CliTermination::without_ui(run_agent(command).map(|()| 0)),
55 },
56 command => {
57 if let Err(error) = ui::init(ui_configuration) {
58 return capulus::CliTermination::without_ui(Err(error));
59 }
60 capulus::CliTermination::with_ui(
61 ui::current(),
62 app::run(Cli {
63 api_base,
64 namespace,
65 ui: ui_options,
66 command,
67 }),
68 )
69 }
70 }
71}
72
73fn run_agent(command: AgentCommands) -> Result<()> {
74 match command {
75 AgentCommands::PrepareIdentity { inbound_ssh } => {
76 require_agent_root()?;
77 crate::platform::detect()?;
78 wireguard_keys::Keypair::ensure(
79 std::path::Path::new("/etc/aegis/wireguard/wg-aegis.key"),
80 std::path::Path::new("/etc/aegis/wireguard/wg-aegis.pub"),
81 )?;
82 if inbound_ssh {
83 let key = std::path::Path::new("/etc/ssh/ssh_host_ed25519_key");
84 if !key.exists() {
85 command::require_success(
86 "create SSH host identity",
87 std::process::Command::new("/usr/bin/ssh-keygen")
88 .args(["-q", "-t", "ed25519", "-N", "", "-f"])
89 .arg(key),
90 )?;
91 }
92 }
93 Ok(())
94 }
95 #[cfg(target_os = "macos")]
96 AgentCommands::WireguardWorker { interface } => {
97 require_agent_root()?;
98 agent::macos::wireguard_worker(&interface)
99 }
100 AgentCommands::Serve(args) => {
101 require_agent_root()?;
102 let status = agent::run(&args)?;
103 if status == 0 {
104 Ok(())
105 } else {
106 bail!("aegis-agent exited with status {status}")
107 }
108 }
109 AgentCommands::DirectSsh => {
110 unreachable!("the direct SSH endpoint is dispatched with the interactive UI")
111 }
112 AgentCommands::Lifecycle(command) => {
113 let product = Arc::new(managed::product()?);
114 let health_product = Arc::clone(&product);
115 command.run(product, move || {
116 app::application_agent_info(&health_product)
117 })
118 }
119 AgentCommands::EgressProbeWorker => {
120 require_agent_root()?;
121 egress_probe::run_worker().map_err(|error| {
124 let detail = format!("{error:#}");
125 error.context(detail)
126 })
127 }
128 }
129}
130
131fn require_agent_root() -> Result<()> {
132 if rustix::process::geteuid().is_root() {
133 Ok(())
134 } else {
135 bail!("aegis agent operations must run as root")
136 }
137}