Skip to main content

agent_first_http/sdk/
capabilities.rs

1//! `/capabilities` client. Shape from `architecture.md §6`.
2
3use std::collections::BTreeMap;
4
5use serde::{Deserialize, Serialize};
6
7use crate::sdk::client::Client;
8use crate::shared::error::{Error, ErrorCode};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct CapabilitiesResponse {
12    pub code: String,
13    pub backend: BackendFamily,
14    pub artifacts: BTreeMap<String, ArtifactSupport>,
15    pub wait_modes: Vec<String>,
16    /// Whether this backend can expose a real-display takeover when the host
17    /// is started with `--takeover display --display-provider kasmvnc`.
18    pub display_takeover: bool,
19    pub ops_panel: OpsPanelSupport,
20    pub profile: ProfileSupport,
21    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
22    pub features: BTreeMap<String, FeatureSupport>,
23    pub limits: BTreeMap<String, serde_json::Value>,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct BackendFamily {
28    pub family: String,
29    pub version: String,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct ArtifactSupport {
34    pub supported: bool,
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub source: Option<String>,
37    #[serde(default, skip_serializing_if = "Vec::is_empty")]
38    pub body_capture: Vec<String>,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct OpsPanelSupport {
43    pub supported: bool,
44    pub screencast: bool,
45    #[serde(default)]
46    pub display: bool,
47    #[serde(default)]
48    pub screencast_url: Option<String>,
49    #[serde(default)]
50    pub display_url: Option<String>,
51    #[serde(default)]
52    pub display_provider: Option<String>,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct ProfileSupport {
57    pub persistent: bool,
58    pub ephemeral: bool,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct FeatureSupport {
63    pub supported: bool,
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub detail: Option<String>,
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub risk: Option<String>,
68}
69
70impl Client {
71    pub async fn capabilities(&self) -> Result<CapabilitiesResponse, Error> {
72        let endpoint = self.effective_endpoint().await?;
73        let base = endpoint.http_base();
74        let url = format!("{base}/capabilities");
75        let mut req = self.http().get(&url);
76        if let Some(token) = self.token() {
77            req = req.bearer_auth(token);
78        }
79        let resp = req
80            .send()
81            .await
82            .map_err(|e| Error::new(ErrorCode::HostUnreachable, format!("GET {url}: {e}")))?;
83        let status = resp.status();
84        let bytes = resp.bytes().await.map_err(|e| {
85            Error::new(
86                ErrorCode::InternalError,
87                format!("capabilities: read response: {e}"),
88            )
89        })?;
90        if !status.is_success() {
91            if let Ok(err) = serde_json::from_slice::<Error>(&bytes) {
92                return Err(err);
93            }
94            return Err(Error::new(
95                ErrorCode::InternalError,
96                format!(
97                    "capabilities: status {status}; failed to decode error envelope: {}",
98                    String::from_utf8_lossy(&bytes)
99                ),
100            ));
101        }
102        serde_json::from_slice::<CapabilitiesResponse>(&bytes).map_err(|e| {
103            Error::new(
104                ErrorCode::InternalError,
105                format!("capabilities: decode response: {e}"),
106            )
107        })
108    }
109
110    /// Build a raw CDP request. Returns a [`crate::sdk::cdp::CdpBuilder`].
111    pub fn cdp(&self, method: impl Into<String>) -> crate::sdk::cdp::CdpBuilder {
112        crate::sdk::cdp::CdpBuilder::new(self.clone(), method)
113    }
114}