Skip to main content

boxxkite_client/
takeover.rs

1//! Human takeover: `WS /v1/sandboxes/{id}/takeover` -- an interactive,
2//! duplex byte stream proxied straight through to the sandbox's PTY, no
3//! message envelope. Mirrors `sdk-python`'s `takeover()` (which
4//! authenticates the WebSocket upgrade with a normal `Authorization: Bearer`
5//! header, unlike the JS SDK/dashboard, which cannot set a custom header on
6//! a browser WebSocket and must mint a short-lived takeover token first --
7//! see `docs/API.md` and `SECURITY.md`'s "Human takeover" section). This
8//! client is not a browser, so it uses the same header-based path as
9//! `sdk-python`.
10
11use tokio::net::TcpStream;
12use tokio_tungstenite::tungstenite::client::IntoClientRequest;
13use tokio_tungstenite::tungstenite::http::header::AUTHORIZATION;
14use tokio_tungstenite::tungstenite::http::HeaderValue;
15use tokio_tungstenite::{MaybeTlsStream, WebSocketStream};
16
17use crate::client::{to_ws_url, Client};
18use crate::error::BoxxkiteError;
19
20/// The duplex byte stream returned by [`Client::takeover`]. Send and
21/// receive raw bytes on it exactly as you would over a local terminal --
22/// there is no separate message envelope.
23pub type TakeoverStream = WebSocketStream<MaybeTlsStream<TcpStream>>;
24
25impl Client {
26    /// `WS /v1/sandboxes/{id}/takeover` -- interactive human takeover of a
27    /// sandbox session's shell.
28    ///
29    /// Requires an **`"admin"`-role** API key (see `POST /v1/api-keys`'s
30    /// `role` field) -- a `"member"`-role key closes the connection with
31    /// close code `4403`. A missing/invalid/expired credential closes with
32    /// `4401`; an unowned or already-destroyed `session_id` closes with
33    /// `4404`. All three surface as a [`BoxxkiteError::WebSocket`] (or, for a
34    /// close that happens after a clean handshake, as a close frame on the
35    /// returned stream itself -- inspect it if the connection ends
36    /// unexpectedly).
37    pub async fn takeover(&self, session_id: &str) -> Result<TakeoverStream, BoxxkiteError> {
38        let ws_url = to_ws_url(
39            &self.base_url,
40            &format!("/v1/sandboxes/{session_id}/takeover"),
41        );
42        let mut request = ws_url.into_client_request()?;
43        let header_value = HeaderValue::from_str(&format!("Bearer {}", self.api_key))
44            .map_err(|err| BoxxkiteError::Config(format!("invalid api_key: {err}")))?;
45        request.headers_mut().insert(AUTHORIZATION, header_value);
46
47        let (stream, _response) = tokio_tungstenite::connect_async(request).await?;
48        Ok(stream)
49    }
50}