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