| 1 | //! Pluggable sandbox backend abstraction. |
| 2 | //! |
| 3 | //! External sandbox backends route shell command execution to a remote service |
| 4 | //! (e.g. Alibaba OpenSandbox) instead of spawning a local process. This is |
| 5 | //! complementary to the OS-level sandbox module (Seatbelt / Landlock / Windows) |
| 6 | //! — the external backend *replaces* local execution entirely when configured. |
| 7 | |
| 8 | use std::collections::HashMap; |
| 9 | |
| 10 | use anyhow::Result; |
| 11 | use async_trait::async_trait; |
| 12 | |
| 13 | /// Output from a sandbox backend execution. |
| 14 | #[derive(Debug, Clone)] |
| 15 | pub struct SandboxOutput { |
| 16 | /// Standard output from the command. |
| 17 | pub stdout: String, |
| 18 | /// Standard error from the command. |
| 19 | pub stderr: String, |
| 20 | /// Exit code (0 for success). |
| 21 | pub exit_code: i32, |
| 22 | } |
| 23 | |
| 24 | /// The kind of external sandbox backend. |
| 25 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 26 | pub enum SandboxKind { |
| 27 | /// No external sandbox — execute commands locally. |
| 28 | None, |
| 29 | /// Alibaba OpenSandbox remote execution. |
| 30 | OpenSandbox, |
| 31 | } |
| 32 | |
| 33 | impl SandboxKind { |
| 34 | /// Parse a sandbox backend name from config (case-insensitive). |
| 35 | #[must_use] |
| 36 | pub fn parse(value: &str) -> Option<Self> { |
| 37 | match value.trim().to_ascii_lowercase().as_str() { |
| 38 | "none" | "" => Some(Self::None), |
| 39 | "opensandbox" | "open-sandbox" | "open_sandbox" => Some(Self::OpenSandbox), |
| 40 | _ => None, |
| 41 | } |
| 42 | } |
| 43 | |
| 44 | /// Human-readable label. |
| 45 | #[must_use] |
| 46 | pub fn as_str(self) -> &'static str { |
| 47 | match self { |
| 48 | Self::None => "none", |
| 49 | Self::OpenSandbox => "opensandbox", |
| 50 | } |
| 51 | } |
| 52 | } |
| 53 | |
| 54 | /// Abstract interface for an external sandbox backend. |
| 55 | /// |
| 56 | /// Implementations send commands to a remote execution environment and return |
| 57 | /// structured output. The trait is `Send + Sync` so it can be stored in an |
| 58 | /// `Arc` and shared across async tasks. |
| 59 | #[async_trait] |
| 60 | pub trait SandboxBackend: Send + Sync { |
| 61 | /// Execute a shell command and return its output. |
| 62 | /// |
| 63 | /// `cmd` is the full shell command string (e.g. `"ls -la"`). |
| 64 | /// `env` contains additional environment variables to set. |
| 65 | async fn exec(&self, cmd: &str, env: &HashMap<String, String>) -> Result<SandboxOutput>; |
| 66 | } |
| 67 | |
| 68 | use crate::config::Config; |
| 69 | |
| 70 | /// Create the configured sandbox backend from config. |
| 71 | /// |
| 72 | /// Returns `None` when no external sandbox backend is configured (i.e. the |
| 73 | /// `sandbox_backend` key is absent, empty, or `"none"`). When `"opensandbox"` |
| 74 | /// is set, constructs an [`OpenSandboxBackend`] using `sandbox_url` and |
| 75 | /// `sandbox_api_key`. |
| 76 | pub fn create_backend(config: &Config) -> Result<Option<Box<dyn SandboxBackend>>> { |
| 77 | let kind = config |
| 78 | .sandbox_backend |
| 79 | .as_deref() |
| 80 | .and_then(SandboxKind::parse) |
| 81 | .unwrap_or(SandboxKind::None); |
| 82 | |
| 83 | match kind { |
| 84 | SandboxKind::None => Ok(None), |
| 85 | SandboxKind::OpenSandbox => { |
| 86 | let base_url = config |
| 87 | .sandbox_url |
| 88 | .clone() |
| 89 | .unwrap_or_else(|| "http://localhost:8080".to_string()); |
| 90 | let api_key = config.sandbox_api_key.clone(); |
| 91 | let backend = super::opensandbox::OpenSandboxBackend::new(base_url, api_key, 30)?; |
| 92 | Ok(Some(Box::new(backend))) |
| 93 | } |
| 94 | } |
| 95 | } |
| 96 |