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 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}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct ProfileSupport {
49    pub persistent: bool,
50    pub ephemeral: bool,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct FeatureSupport {
55    pub supported: bool,
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub detail: Option<String>,
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub risk: Option<String>,
60}
61
62impl Client {
63    pub async fn capabilities(&self) -> Result<CapabilitiesResponse, Error> {
64        let endpoint = self.effective_endpoint().await?;
65        let base = endpoint.http_base();
66        let url = format!("{base}/capabilities");
67        let mut req = self.http().get(&url);
68        if let Some(token) = self.token() {
69            req = req.bearer_auth(token);
70        }
71        let resp = req
72            .send()
73            .await
74            .map_err(|e| Error::new(ErrorCode::HostUnreachable, format!("GET {url}: {e}")))?;
75        let status = resp.status();
76        let bytes = resp.bytes().await.map_err(|e| {
77            Error::new(
78                ErrorCode::InternalError,
79                format!("capabilities: read response: {e}"),
80            )
81        })?;
82        if !status.is_success() {
83            if let Ok(err) = serde_json::from_slice::<Error>(&bytes) {
84                return Err(err);
85            }
86            return Err(Error::new(
87                ErrorCode::InternalError,
88                format!(
89                    "capabilities: status {status}; failed to decode error envelope: {}",
90                    String::from_utf8_lossy(&bytes)
91                ),
92            ));
93        }
94        serde_json::from_slice::<CapabilitiesResponse>(&bytes).map_err(|e| {
95            Error::new(
96                ErrorCode::InternalError,
97                format!("capabilities: decode response: {e}"),
98            )
99        })
100    }
101
102    /// Build a raw CDP request. Returns a [`crate::sdk::cdp::CdpBuilder`].
103    pub fn cdp(&self, method: impl Into<String>) -> crate::sdk::cdp::CdpBuilder {
104        crate::sdk::cdp::CdpBuilder::new(self.clone(), method)
105    }
106}