Skip to main content

agent_first_http/cli/cmd/
ui.rs

1//! `afhttp ui` subcommand. Prints the ops panel URL for an endpoint.
2
3use clap::Args as ClapArgs;
4use serde::Serialize;
5
6use crate::cli::output;
7use crate::sdk::endpoint::Endpoint;
8use crate::sdk::Client;
9use crate::shared::error::Error;
10
11#[derive(ClapArgs, Debug)]
12pub struct Args {
13    /// CDP endpoint of the running host.
14    #[arg(long = "endpoint-url")]
15    pub endpoint: String,
16    /// Bearer token, if the host requires one (appended to the panel URLs).
17    #[arg(long = "token-secret")]
18    pub token: Option<String>,
19}
20
21#[derive(Serialize)]
22struct UiResult {
23    screencast_url: String,
24    display_url: String,
25    recommended_url: String,
26    recommended_url_kind: String,
27    display_provider: Option<String>,
28    #[serde(skip_serializing_if = "Vec::is_empty")]
29    warnings: Vec<String>,
30}
31
32pub async fn run(args: Args) -> Result<(), Error> {
33    let result = build_result(&args.endpoint, args.token.as_deref()).await?;
34    output::emit("ui", &result)
35}
36
37async fn build_result(endpoint: &str, token: Option<&str>) -> Result<UiResult, Error> {
38    let mut result = build_urls(endpoint, token)?;
39    let mut client = Client::connect(endpoint)?;
40    if let Some(token) = token {
41        client = client.with_token(token);
42    }
43    match client.capabilities().await {
44        Ok(caps) => {
45            if caps.ops_panel.display {
46                result.recommended_url = result.display_url.clone();
47                result.recommended_url_kind = "display_url".into();
48                result.display_provider = caps.ops_panel.display_provider;
49            } else if caps.backend.family == "camoufox" || !caps.ops_panel.screencast {
50                result.warnings.push(
51                    "screencast takeover is unavailable or limited for this backend; for hard sites start the host with `--takeover display --display-provider kasmvnc` and use display_url. This is not a captcha bypass.".into(),
52                );
53            }
54        }
55        Err(e) => {
56            result.warnings.push(format!(
57                "could not read host capabilities; defaulting recommended_url to screencast_url: {}",
58                e.detail
59            ));
60        }
61    }
62    Ok(result)
63}
64
65fn build_urls(endpoint: &str, token: Option<&str>) -> Result<UiResult, Error> {
66    let endpoint = Endpoint::parse(endpoint)?;
67    let base = endpoint.http_base();
68    let mut screencast_url = url::Url::parse(&format!("{base}/ops/screencast")).map_err(|e| {
69        crate::shared::error::Error::new(
70            crate::shared::error::ErrorCode::InvalidEndpoint,
71            format!("ui screencast URL from endpoint {base:?}: {e}"),
72        )
73    })?;
74    let mut display_url = url::Url::parse(&format!("{base}/ops/display")).map_err(|e| {
75        crate::shared::error::Error::new(
76            crate::shared::error::ErrorCode::InvalidEndpoint,
77            format!("ui display URL from endpoint {base:?}: {e}"),
78        )
79    })?;
80    if let Some(token) = token {
81        screencast_url
82            .query_pairs_mut()
83            .append_pair("token_secret", token);
84        display_url
85            .query_pairs_mut()
86            .append_pair("token_secret", token);
87    }
88    let screencast_url = screencast_url.to_string();
89    let display_url = display_url.to_string();
90    Ok(UiResult {
91        recommended_url: screencast_url.clone(),
92        recommended_url_kind: "screencast_url".into(),
93        screencast_url,
94        display_url,
95        display_provider: None,
96        warnings: Vec::new(),
97    })
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn ui_result_has_no_legacy_url_aliases() {
106        let value = serde_json::to_value(UiResult {
107            screencast_url: "http://localhost:9222/ops/screencast".into(),
108            display_url: "http://localhost:9222/ops/display".into(),
109            recommended_url: "http://localhost:9222/ops/screencast".into(),
110            recommended_url_kind: "screencast_url".into(),
111            display_provider: None,
112            warnings: Vec::new(),
113        })
114        .unwrap();
115        assert!(value.get("url").is_none());
116        assert!(value.get("panel_url").is_none());
117        assert!(value.get("screencast_url").is_some());
118        assert!(value.get("display_url").is_some());
119        assert!(value.get("recommended_url").is_some());
120        assert!(value.get("display_provider").is_some());
121    }
122
123    #[test]
124    fn ui_token_query_is_percent_encoded() {
125        let result = build_urls("http://localhost:9222", Some("a+b&c%20")).unwrap();
126        assert_eq!(
127            result.screencast_url,
128            "http://localhost:9222/ops/screencast?token_secret=a%2Bb%26c%2520"
129        );
130        assert_eq!(
131            result.display_url,
132            "http://localhost:9222/ops/display?token_secret=a%2Bb%26c%2520"
133        );
134    }
135}