Skip to main content

agent_first_http/host/
bootstrap.rs

1//! `afhttp host` startup: parse args, install rustls provider, install
2//! signal handlers, build state, bind the listener, and block until
3//! shutdown.
4
5use std::path::PathBuf;
6
7use crate::host::listener::AppState;
8use crate::shared::error::Error;
9
10/// Parsed command-line arguments for `afhttp host`. The CLI layer in
11/// `cli::cmd::host` builds this struct from `clap`.
12#[derive(Debug, Clone)]
13pub struct HostArgs {
14    pub listen: String,
15    /// The single browsing identity bound to this host.
16    pub profile: ProfileChoice,
17    pub display: DisplayMode,
18    pub takeover: Takeover,
19    /// Display-provider image quality as a percentage (0-100). Higher is
20    /// crisper but uses more bandwidth. Current KasmVNC provider maps this to
21    /// its 0-9 quality tiers.
22    pub display_quality: u8,
23    pub browser: BrowserChoice,
24    pub browser_bin: Option<PathBuf>,
25    pub token: Option<String>,
26    pub ops_enabled: bool,
27    pub health_enabled: bool,
28    pub health_public: HealthPublic,
29    /// Explicit environment variables to propagate into the backend
30    /// subprocess. The host never silently forwards the parent process's
31    /// `HTTP_PROXY`, `XDG_*`, `BROWSER`, etc. — agents that genuinely
32    /// need an env var inside the browser pass it here.
33    pub engine_envs: Vec<(String, String)>,
34    /// Raw command-line arguments appended to the backend subprocess
35    /// after the host's curated defaults. Used for backend-specific
36    /// surfaces the host does not model first-class (e.g.
37    /// `--fingerprint-brand=Chrome` for fingerprint-chromium). Chromium
38    /// honors last-wins for duplicate flags, so an explicit entry here
39    /// overrides any default the host applied.
40    pub browser_args: Vec<String>,
41    /// Explicit upstream HTTP/HTTPS proxy for all browser traffic in
42    /// this host instance. Per the isolation invariant
43    /// (`design.md` "Browsing environments are isolated"), the host
44    /// never honors ambient `HTTP_PROXY`/`HTTPS_PROXY` — this flag is
45    /// the ONLY way to route browser traffic through a proxy.
46    /// Format: `http://user:pass@host:port` or `socks5://host:port`.
47    pub proxy: Option<String>,
48    /// Maximum number of recent requests to keep in the ring. `0` (default)
49    /// disables the `/recent-requests` endpoint entirely.
50    pub recent_requests_cap: usize,
51}
52
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub enum ProfileChoice {
55    Ephemeral,
56    Persistent(String),
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum DisplayMode {
61    Headless,
62    Headful,
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub enum Takeover {
67    Off,
68    Screencast,
69    Display { provider: DisplayProvider },
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub enum DisplayProvider {
74    KasmVnc,
75}
76
77impl DisplayProvider {
78    pub fn as_str(self) -> &'static str {
79        match self {
80            Self::KasmVnc => "kasmvnc",
81        }
82    }
83}
84
85impl std::str::FromStr for DisplayProvider {
86    type Err = String;
87
88    fn from_str(s: &str) -> Result<Self, Self::Err> {
89        Ok(match s {
90            "kasmvnc" => Self::KasmVnc,
91            other => return Err(format!("unknown {other:?}; expected kasmvnc")),
92        })
93    }
94}
95
96#[derive(Debug, Clone, PartialEq, Eq, Default)]
97pub enum BrowserChoice {
98    #[default]
99    Auto,
100    Chromium,
101    Chrome,
102    ChromeShell,
103    FingerprintChromium,
104    Edge,
105    Brave,
106    Lightpanda,
107    /// Camoufox (Firefox stealth fork) driven via the foxbridge
108    /// CDP→Juggler proxy. The host spawns foxbridge which in turn
109    /// spawns camoufox; the agent sees a chromium-style WS endpoint.
110    Camoufox,
111}
112
113impl std::str::FromStr for BrowserChoice {
114    type Err = String;
115
116    fn from_str(s: &str) -> Result<Self, Self::Err> {
117        Ok(match s {
118            "auto" => Self::Auto,
119            "chromium" => Self::Chromium,
120            "chrome" => Self::Chrome,
121            "chrome_shell" | "chrome-headless-shell" => Self::ChromeShell,
122            "fingerprint_chromium" | "fingerprint-chromium" => Self::FingerprintChromium,
123            "edge" => Self::Edge,
124            "brave" => Self::Brave,
125            "lightpanda" => Self::Lightpanda,
126            "camoufox" => Self::Camoufox,
127            other => return Err(format!("unknown {other:?}")),
128        })
129    }
130}
131
132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133pub enum HealthPublic {
134    Off,
135    Minimal,
136}
137
138pub fn install_rustls_provider() {
139    let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
140}
141
142/// Run the host until SIGTERM/SIGINT. Launches the backend browser, builds
143/// the listener state around it, and serves until the shutdown signal.
144pub async fn run(args: HostArgs) -> Result<(), Error> {
145    install_rustls_provider();
146    let state = AppState::launch(&args).await?;
147    state.serve(&args.listen).await
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn browser_choice_default_is_auto() {
156        assert_eq!(BrowserChoice::default(), BrowserChoice::Auto);
157    }
158
159    #[test]
160    fn browser_choice_parses_every_variant_and_aliases() {
161        assert_eq!(
162            "auto".parse::<BrowserChoice>().unwrap(),
163            BrowserChoice::Auto
164        );
165        assert_eq!(
166            "chromium".parse::<BrowserChoice>().unwrap(),
167            BrowserChoice::Chromium
168        );
169        assert_eq!(
170            "chrome".parse::<BrowserChoice>().unwrap(),
171            BrowserChoice::Chrome
172        );
173        // Both spellings of the headless-shell and fingerprint backends.
174        assert_eq!(
175            "chrome_shell".parse::<BrowserChoice>().unwrap(),
176            BrowserChoice::ChromeShell
177        );
178        assert_eq!(
179            "chrome-headless-shell".parse::<BrowserChoice>().unwrap(),
180            BrowserChoice::ChromeShell
181        );
182        assert_eq!(
183            "fingerprint_chromium".parse::<BrowserChoice>().unwrap(),
184            BrowserChoice::FingerprintChromium
185        );
186        assert_eq!(
187            "fingerprint-chromium".parse::<BrowserChoice>().unwrap(),
188            BrowserChoice::FingerprintChromium
189        );
190        assert_eq!(
191            "edge".parse::<BrowserChoice>().unwrap(),
192            BrowserChoice::Edge
193        );
194        assert_eq!(
195            "brave".parse::<BrowserChoice>().unwrap(),
196            BrowserChoice::Brave
197        );
198        assert_eq!(
199            "lightpanda".parse::<BrowserChoice>().unwrap(),
200            BrowserChoice::Lightpanda
201        );
202        assert_eq!(
203            "camoufox".parse::<BrowserChoice>().unwrap(),
204            BrowserChoice::Camoufox
205        );
206    }
207
208    #[test]
209    fn browser_choice_rejects_unknown() {
210        let err = "netscape".parse::<BrowserChoice>().unwrap_err();
211        assert!(err.contains("netscape"), "error was {err:?}");
212    }
213}