返回 CodeWhale
opensandbox.rs
根目录 / crates / tui / src / sandbox / opensandbox.rs
1 //! Alibaba OpenSandbox backend adapter.
2 //!
3 //! Sends shell commands to an OpenSandbox-compatible HTTP API for remote
4 //! execution. The API endpoint is `POST {base_url}/v1/sandbox/run` with
5 //! JSON body `{"cmd": "...", "env": {...}}` and expects a JSON response
6 //! `{"stdout": "...", "stderr": "...", "exit_code": 0}`.
7
8 use std::collections::HashMap;
9 use std::time::Duration;
10
11 use anyhow::{Context, Result};
12 use async_trait::async_trait;
13 use serde::Deserialize;
14 use serde::Serialize;
15
16 use super::backend::{SandboxBackend, SandboxKind, SandboxOutput};
17
18 /// Request body sent to the OpenSandbox `/v1/sandbox/run` endpoint.
19 #[derive(Debug, Serialize)]
20 struct SandboxRunRequest {
21 /// Full shell command to execute.
22 cmd: String,
23 /// Environment variables to set in the sandbox.
24 env: HashMap<String, String>,
25 }
26
27 /// Response body from the OpenSandbox `/v1/sandbox/run` endpoint.
28 #[derive(Debug, Deserialize)]
29 struct SandboxRunResponse {
30 /// Standard output from the command.
31 stdout: String,
32 /// Standard error from the command.
33 stderr: String,
34 /// Exit code (0 for success).
35 exit_code: i32,
36 }
37
38 /// An OpenSandbox-compatible remote execution backend.
39 ///
40 /// Constructed with a base URL (e.g. `"http://localhost:8080"`), an optional
41 /// API key sent as a `Bearer` token, and a timeout in seconds.
42 pub struct OpenSandboxBackend {
43 base_url: String,
44 api_key: Option<String>,
45 timeout_secs: u64,
46 client: reqwest::Client,
47 }
48
49 impl OpenSandboxBackend {
50 /// Create a new OpenSandbox backend.
51 ///
52 /// `base_url` should be the root of the OpenSandbox API (e.g.
53 /// `"http://localhost:8080"`). `api_key` is optional and sent as
54 /// `Authorization: Bearer <key>` when set. `timeout_secs` controls the
55 /// HTTP request timeout.
56 pub fn new(base_url: String, api_key: Option<String>, timeout_secs: u64) -> Result<Self> {
57 let client = crate::tls::reqwest_client_builder()
58 .timeout(Duration::from_secs(timeout_secs))
59 .build()
60 .context("failed to construct HTTP client for OpenSandbox backend")?;
61
62 Ok(Self {
63 base_url,
64 api_key,
65 timeout_secs,
66 client,
67 })
68 }
69
70 /// Build the full URL for the sandbox run endpoint.
71 fn run_url(&self) -> String {
72 format!("{}/v1/sandbox/run", self.base_url.trim_end_matches('/'))
73 }
74 }
75
76 #[async_trait]
77 impl SandboxBackend for OpenSandboxBackend {
78 fn kind(&self) -> SandboxKind {
79 SandboxKind::OpenSandbox
80 }
81
82 async fn exec(&self, cmd: &str, env: &HashMap<String, String>) -> Result<SandboxOutput> {
83 let request_body = SandboxRunRequest {
84 cmd: cmd.to_string(),
85 env: env.clone(),
86 };
87
88 let mut req = self.client.post(self.run_url()).json(&request_body);
89
90 if let Some(ref api_key) = self.api_key {
91 req = req.bearer_auth(api_key);
92 }
93
94 let response = req
95 .send()
96 .await
97 .context("Failed to reach OpenSandbox endpoint")?;
98
99 let status = response.status();
100 if !status.is_success() {
101 let body = response.text().await.unwrap_or_default();
102 anyhow::bail!("OpenSandbox returned HTTP {}: {}", status.as_u16(), body);
103 }
104
105 let parsed: SandboxRunResponse = response
106 .json()
107 .await
108 .context("Failed to parse OpenSandbox response")?;
109
110 Ok(SandboxOutput {
111 stdout: parsed.stdout,
112 stderr: parsed.stderr,
113 exit_code: parsed.exit_code,
114 })
115 }
116 }
117
117 lines RUST