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 a resolved CLI invocation.
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 takeover_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    On { provider: TakeoverProviderKind },
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub enum TakeoverProviderKind {
73    KasmVnc,
74}
75
76impl TakeoverProviderKind {
77    pub fn as_str(self) -> &'static str {
78        match self {
79            Self::KasmVnc => "kasmvnc",
80        }
81    }
82}
83
84impl std::str::FromStr for TakeoverProviderKind {
85    type Err = String;
86
87    fn from_str(s: &str) -> Result<Self, Self::Err> {
88        Ok(match s {
89            "kasmvnc" => Self::KasmVnc,
90            other => return Err(format!("unknown {other:?}; expected kasmvnc")),
91        })
92    }
93}
94
95#[derive(Debug, Clone, PartialEq, Eq, Default)]
96pub enum BrowserChoice {
97    #[default]
98    Auto,
99    Chromium,
100    Chrome,
101    FingerprintChromium,
102    Edge,
103    Brave,
104    Lightpanda,
105    /// Camoufox (Firefox stealth fork) driven via the foxbridge
106    /// CDP→Juggler proxy. The host spawns foxbridge which in turn
107    /// spawns camoufox; the agent sees a chromium-style WS endpoint.
108    Camoufox,
109}
110
111impl std::str::FromStr for BrowserChoice {
112    type Err = String;
113
114    fn from_str(s: &str) -> Result<Self, Self::Err> {
115        Ok(match s {
116            "auto" => Self::Auto,
117            "chromium" => Self::Chromium,
118            "chrome" => Self::Chrome,
119            "fingerprint_chromium" | "fingerprint-chromium" => Self::FingerprintChromium,
120            "edge" => Self::Edge,
121            "brave" => Self::Brave,
122            "lightpanda" => Self::Lightpanda,
123            "camoufox" => Self::Camoufox,
124            other => return Err(format!("unknown {other:?}")),
125        })
126    }
127}
128
129impl BrowserChoice {
130    /// Stable backend-scope key used for persistent profile directories.
131    #[must_use]
132    pub fn profile_backend_key(&self) -> &'static str {
133        match self {
134            Self::Auto | Self::Chromium => "chromium",
135            Self::Chrome => "chrome",
136            Self::FingerprintChromium => "fingerprint-chromium",
137            Self::Edge => "edge",
138            Self::Brave => "brave",
139            Self::Lightpanda => "lightpanda",
140            Self::Camoufox => "camoufox",
141        }
142    }
143}
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
146pub enum HealthPublic {
147    Off,
148    Minimal,
149}
150
151pub fn install_rustls_provider() {
152    let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
153}
154
155/// Run the host until SIGTERM/SIGINT. Launches the backend browser, builds
156/// the listener state around it, and serves until the shutdown signal.
157pub async fn run(args: HostArgs) -> Result<(), Error> {
158    install_rustls_provider();
159    let state = AppState::launch(&args).await?;
160    state.serve(&args.listen).await
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    #[test]
168    fn browser_choice_default_is_auto() {
169        assert_eq!(BrowserChoice::default(), BrowserChoice::Auto);
170    }
171
172    #[test]
173    fn browser_choice_parses_every_variant_and_aliases() {
174        assert_eq!(
175            "auto".parse::<BrowserChoice>().unwrap(),
176            BrowserChoice::Auto
177        );
178        assert_eq!(
179            "chromium".parse::<BrowserChoice>().unwrap(),
180            BrowserChoice::Chromium
181        );
182        assert_eq!(
183            "chrome".parse::<BrowserChoice>().unwrap(),
184            BrowserChoice::Chrome
185        );
186        // chrome-headless-shell was removed as a backend: same engine and CDP
187        // surface as chromium, which the image always installs, so it added a
188        // version pin and an x86-only asymmetry for no capability of its own.
189        // Both spellings must now be rejected rather than silently aliased.
190        assert!("chrome-headless-shell".parse::<BrowserChoice>().is_err());
191        assert!("chrome_shell".parse::<BrowserChoice>().is_err());
192        // Both spellings of the fingerprint backend.
193        assert_eq!(
194            "fingerprint_chromium".parse::<BrowserChoice>().unwrap(),
195            BrowserChoice::FingerprintChromium
196        );
197        assert_eq!(
198            "fingerprint-chromium".parse::<BrowserChoice>().unwrap(),
199            BrowserChoice::FingerprintChromium
200        );
201        assert_eq!(
202            "edge".parse::<BrowserChoice>().unwrap(),
203            BrowserChoice::Edge
204        );
205        assert_eq!(
206            "brave".parse::<BrowserChoice>().unwrap(),
207            BrowserChoice::Brave
208        );
209        assert_eq!(
210            "lightpanda".parse::<BrowserChoice>().unwrap(),
211            BrowserChoice::Lightpanda
212        );
213        assert_eq!(
214            "camoufox".parse::<BrowserChoice>().unwrap(),
215            BrowserChoice::Camoufox
216        );
217    }
218
219    #[test]
220    fn browser_choice_rejects_unknown() {
221        let err = "netscape".parse::<BrowserChoice>().unwrap_err();
222        assert!(err.contains("netscape"), "error was {err:?}");
223    }
224}