Skip to main content

agent_first_http/cli/cmd/
panel.rs

1//! `afhttp panel` subcommand. Prints a short-lived takeover URL for an endpoint.
2
3use serde::Serialize;
4
5use crate::cli::connect::Connection;
6use crate::cli::output;
7use crate::shared::error::Error;
8
9#[derive(Debug)]
10pub struct Args {
11    pub connection: Connection,
12}
13
14#[derive(Serialize)]
15struct PanelResult {
16    takeover_url_secret: String,
17    takeover_url_expires_at_rfc3339: String,
18    takeover_url_ttl_s: u64,
19    takeover_url_scope: String,
20    provider: Option<String>,
21    #[serde(skip_serializing_if = "Vec::is_empty")]
22    warnings: Vec<String>,
23}
24
25pub async fn run(args: Args) -> Result<(), Error> {
26    let result = build_result(&args.connection).await?;
27    output::emit_revealing_takeover("panel", &result)
28}
29
30async fn build_result(connection: &Connection) -> Result<PanelResult, Error> {
31    let client = connection.client().await?;
32    let handoff = client.takeover_handoff(None, None).await?;
33    let mut warnings = Vec::new();
34    let mut provider = None;
35    match client.capabilities().await {
36        Ok(caps) => {
37            if caps.takeover.supported {
38                provider = caps.takeover.provider;
39            } else {
40                warnings.push(
41                    "this host has no takeover panel; build a takeover-ready host with `afhttp container install` and reconnect. This is not a captcha bypass.".into(),
42                );
43            }
44        }
45        Err(e) => {
46            warnings.push(format!(
47                "could not read host capabilities; returning the takeover_url_secret anyway: {}",
48                e.detail
49            ));
50        }
51    }
52    Ok(PanelResult {
53        takeover_url_secret: handoff.takeover_url_secret,
54        takeover_url_expires_at_rfc3339: handoff.takeover_url_expires_at_rfc3339,
55        takeover_url_ttl_s: handoff.takeover_url_ttl_s,
56        takeover_url_scope: handoff.takeover_url_scope,
57        provider,
58        warnings,
59    })
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65
66    #[test]
67    fn panel_result_exposes_takeover_url_secret_only() {
68        let value = serde_json::to_value(PanelResult {
69            takeover_url_secret: "http://localhost:9222/takeover/panel?handoff_secret=h".into(),
70            takeover_url_expires_at_rfc3339: "2026-06-11T00:00:00Z".into(),
71            takeover_url_ttl_s: 900,
72            takeover_url_scope: "takeover".into(),
73            provider: Some("kasmvnc".into()),
74            warnings: Vec::new(),
75        })
76        .unwrap();
77        assert!(value.get("url").is_none());
78        assert!(value.get("display_url").is_none());
79        assert!(value.get("panel_url").is_none());
80        assert!(value.get("token_secret").is_none());
81        assert!(
82            value["takeover_url_secret"]
83                .as_str()
84                .unwrap()
85                .contains("handoff_secret=")
86        );
87        assert!(value.get("provider").is_some());
88    }
89}