Skip to main content

boxxkite_client/
desktop.rs

1//! GUI/remote-desktop human takeover: `WS /v1/sandboxes/{id}/desktop` -- an
2//! interactive, duplex byte stream (VNC) proxied straight through to the
3//! sidecar's own `WS /desktop`, structurally identical to `takeover.rs` but
4//! bridging a full desktop instead of a shell -- see `docs/API.md` and
5//! `SECURITY.md`'s "New trust boundary: remote desktop takeover" section.
6//! This client is not a browser, so it uses the same header-based
7//! `Authorization: Bearer` path `takeover()` does.
8
9use tokio::net::TcpStream;
10use tokio_tungstenite::tungstenite::client::IntoClientRequest;
11use tokio_tungstenite::tungstenite::http::header::AUTHORIZATION;
12use tokio_tungstenite::tungstenite::http::HeaderValue;
13use tokio_tungstenite::{MaybeTlsStream, WebSocketStream};
14
15use crate::client::{to_ws_url, Client};
16use crate::error::BoxxkiteError;
17
18/// The duplex byte stream returned by [`Client::desktop_takeover`]. Send and
19/// receive raw bytes on it exactly as you would over a local terminal --
20/// there is no separate message envelope.
21pub type DesktopStream = WebSocketStream<MaybeTlsStream<TcpStream>>;
22
23impl Client {
24    /// `WS /v1/sandboxes/{id}/desktop` -- interactive GUI/remote-desktop
25    /// human takeover of a sandbox session.
26    ///
27    /// Reuses [`Client::takeover`]'s RBAC gate as-is: requires an
28    /// **`"admin"`-role** API key (see `POST /v1/api-keys`'s `role` field) --
29    /// a `"member"`-role key closes the connection with close code `4403`.
30    /// There is no dedicated `can_initiate_desktop` permission yet, and no
31    /// read-only variant of this connection. A missing/invalid/expired
32    /// credential closes with `4401`; an unowned or already-destroyed
33    /// `session_id` closes with `4404` -- this deployment closes with `4404`
34    /// as well when `BOXXKITE_DESKTOP_ENABLED` is unset. All surface as a
35    /// [`BoxxkiteError::WebSocket`] (or, for a close that happens after a
36    /// clean handshake, as a close frame on the returned stream itself --
37    /// inspect it if the connection ends unexpectedly).
38    pub async fn desktop_takeover(&self, session_id: &str) -> Result<DesktopStream, BoxxkiteError> {
39        let ws_url = to_ws_url(
40            &self.base_url,
41            &format!("/v1/sandboxes/{session_id}/desktop"),
42        );
43        let mut request = ws_url.into_client_request()?;
44        let header_value = HeaderValue::from_str(&format!("Bearer {}", self.api_key))
45            .map_err(|err| BoxxkiteError::Config(format!("invalid api_key: {err}")))?;
46        request.headers_mut().insert(AUTHORIZATION, header_value);
47
48        let (stream, _response) = tokio_tungstenite::connect_async(request).await?;
49        Ok(stream)
50    }
51}