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    ChromeShell,
102    FingerprintChromium,
103    Edge,
104    Brave,
105    Lightpanda,
106    /// Camoufox (Firefox stealth fork) driven via the foxbridge
107    /// CDP→Juggler proxy. The host spawns foxbridge which in turn
108    /// spawns camoufox; the agent sees a chromium-style WS endpoint.
109    Camoufox,
110}
111
112impl std::str::FromStr for BrowserChoice {
113    type Err = String;
114
115    fn from_str(s: &str) -> Result<Self, Self::Err> {
116        Ok(match s {
117            "auto" => Self::Auto,
118            "chromium" => Self::Chromium,
119            "chrome" => Self::Chrome,
120            "chrome_shell" | "chrome-headless-shell" => Self::ChromeShell,
121            "fingerprint_chromium" | "fingerprint-chromium" => Self::FingerprintChromium,
122            "edge" => Self::Edge,
123            "brave" => Self::Brave,
124            "lightpanda" => Self::Lightpanda,
125            "camoufox" => Self::Camoufox,
126            other => return Err(format!("unknown {other:?}")),
127        })
128    }
129}
130
131impl BrowserChoice {
132    /// Stable backend-scope key used for persistent profile directories.
133    #[must_use]
134    pub fn profile_backend_key(&self) -> &'static str {
135        match self {
136            Self::Auto | Self::Chromium => "chromium",
137            Self::Chrome => "chrome",
138            Self::ChromeShell => "chrome-headless-shell",
139            Self::FingerprintChromium => "fingerprint-chromium",
140            Self::Edge => "edge",
141            Self::Brave => "brave",
142            Self::Lightpanda => "lightpanda",
143            Self::Camoufox => "camoufox",
144        }
145    }
146}
147
148#[derive(Debug, Clone, Copy, PartialEq, Eq)]
149pub enum HealthPublic {
150    Off,
151    Minimal,
152}
153
154pub fn install_rustls_provider() {
155    let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
156}
157
158/// Run the host until SIGTERM/SIGINT. Launches the backend browser, builds
159/// the listener state around it, and serves until the shutdown signal.
160pub async fn run(args: HostArgs) -> Result<(), Error> {
161    install_rustls_provider();
162    let state = AppState::launch(&args).await?;
163    state.serve(&args.listen).await
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    #[test]
171    fn browser_choice_default_is_auto() {
172        assert_eq!(BrowserChoice::default(), BrowserChoice::Auto);
173    }
174
175    #[test]
176    fn browser_choice_parses_every_variant_and_aliases() {
177        assert_eq!(
178            "auto".parse::<BrowserChoice>().unwrap(),
179            BrowserChoice::Auto
180        );
181        assert_eq!(
182            "chromium".parse::<BrowserChoice>().unwrap(),
183            BrowserChoice::Chromium
184        );
185        assert_eq!(
186            "chrome".parse::<BrowserChoice>().unwrap(),
187            BrowserChoice::Chrome
188        );
189        // Both spellings of the headless-shell and fingerprint backends.
190        assert_eq!(
191            "chrome_shell".parse::<BrowserChoice>().unwrap(),
192            BrowserChoice::ChromeShell
193        );
194        assert_eq!(
195            "chrome-headless-shell".parse::<BrowserChoice>().unwrap(),
196            BrowserChoice::ChromeShell
197        );
198        assert_eq!(
199            "fingerprint_chromium".parse::<BrowserChoice>().unwrap(),
200            BrowserChoice::FingerprintChromium
201        );
202        assert_eq!(
203            "fingerprint-chromium".parse::<BrowserChoice>().unwrap(),
204            BrowserChoice::FingerprintChromium
205        );
206        assert_eq!(
207            "edge".parse::<BrowserChoice>().unwrap(),
208            BrowserChoice::Edge
209        );
210        assert_eq!(
211            "brave".parse::<BrowserChoice>().unwrap(),
212            BrowserChoice::Brave
213        );
214        assert_eq!(
215            "lightpanda".parse::<BrowserChoice>().unwrap(),
216            BrowserChoice::Lightpanda
217        );
218        assert_eq!(
219            "camoufox".parse::<BrowserChoice>().unwrap(),
220            BrowserChoice::Camoufox
221        );
222    }
223
224    #[test]
225    fn browser_choice_rejects_unknown() {
226        let err = "netscape".parse::<BrowserChoice>().unwrap_err();
227        assert!(err.contains("netscape"), "error was {err:?}");
228    }
229}