Skip to main content

adk_code/
executor.rs

1//! Async executor trait and shared request validation helpers.
2//!
3//! [`CodeExecutor`] is the backend interface that all execution backends implement.
4//! The module also provides [`validate_policy`] and [`validate_request`] helpers
5//! that enforce fail-closed semantics: if a backend cannot enforce a requested
6//! sandbox control, execution is rejected before user code runs.
7//!
8//! # Example
9//!
10//! ```rust
11//! use adk_code::{
12//!     BackendCapabilities, ExecutionIsolation, SandboxPolicy, validate_policy,
13//! };
14//!
15//! let caps = BackendCapabilities {
16//!     isolation: ExecutionIsolation::ContainerEphemeral,
17//!     enforce_network_policy: true,
18//!     enforce_filesystem_policy: true,
19//!     enforce_environment_policy: true,
20//!     enforce_timeout: true,
21//!     supports_structured_output: true,
22//!     supports_process_execution: false,
23//!     supports_persistent_workspace: false,
24//!     supports_interactive_sessions: false,
25//! };
26//!
27//! let policy = SandboxPolicy::strict_rust();
28//! assert!(validate_policy(&caps, &policy).is_ok());
29//! ```
30
31use async_trait::async_trait;
32
33use crate::{
34    BackendCapabilities, EnvironmentPolicy, ExecutionError, ExecutionLanguage, ExecutionPayload,
35    ExecutionRequest, ExecutionResult, FilesystemPolicy, GuestModuleFormat, NetworkPolicy,
36    SandboxPolicy,
37};
38
39/// Async trait for code execution backends.
40///
41/// Backends may optionally implement lifecycle methods ([`start`](Self::start),
42/// [`stop`](Self::stop), [`restart`](Self::restart)) for persistent execution
43/// environments like containers. The default implementations are no-ops, so
44/// simple backends (e.g., host-local `rustc`) work without lifecycle management.
45///
46/// Backends that support persistent environments should override these methods
47/// and report `supports_persistent_workspace: true` in their capabilities.
48#[async_trait]
49pub trait CodeExecutor: Send + Sync {
50    /// Human-readable backend name.
51    fn name(&self) -> &str;
52    /// The capabilities this backend can enforce.
53    fn capabilities(&self) -> BackendCapabilities;
54    /// Whether this backend supports the given language.
55    fn supports_language(&self, lang: &ExecutionLanguage) -> bool;
56    /// Execute a request and return a structured result.
57    async fn execute(&self, request: ExecutionRequest) -> Result<ExecutionResult, ExecutionError>;
58
59    /// Start the execution environment (e.g., create and start a container).
60    ///
61    /// For persistent backends, this creates the underlying environment and
62    /// makes it ready for [`execute`](Self::execute) calls. Calling `execute`
63    /// on a started backend reuses the same environment.
64    ///
65    /// The default implementation is a no-op for backends that don't need
66    /// lifecycle management (e.g., host-local compilation).
67    async fn start(&self) -> Result<(), ExecutionError> {
68        Ok(())
69    }
70
71    /// Stop the execution environment and release resources.
72    ///
73    /// For persistent backends, this stops and removes the underlying
74    /// environment (e.g., stops and removes a Docker container). After
75    /// `stop`, the backend can be restarted with [`start`](Self::start).
76    ///
77    /// The default implementation is a no-op.
78    async fn stop(&self) -> Result<(), ExecutionError> {
79        Ok(())
80    }
81
82    /// Restart the execution environment.
83    ///
84    /// Equivalent to [`stop`](Self::stop) followed by [`start`](Self::start),
85    /// but backends may implement this more efficiently (e.g., `docker restart`).
86    ///
87    /// The default implementation calls `stop` then `start`.
88    async fn restart(&self) -> Result<(), ExecutionError> {
89        self.stop().await?;
90        self.start().await
91    }
92
93    /// Whether the execution environment is currently running.
94    ///
95    /// Returns `true` if [`start`](Self::start) has been called and
96    /// [`stop`](Self::stop) has not. For backends without lifecycle
97    /// management, this always returns `true`.
98    async fn is_running(&self) -> bool {
99        true
100    }
101
102    /// A prose snippet describing this backend's execution environment and
103    /// built-in capabilities, suitable for appending to an LLM-facing tool
104    /// description.
105    ///
106    /// `None` (the default) means the backend has nothing to add. Backends
107    /// whose behavior depends on construction-time configuration (granted
108    /// filesystem roots, environment variables, registered host functions,
109    /// state persistence semantics) should override this so tools composed
110    /// over `Arc<dyn CodeExecutor>` can surface an accurate environment
111    /// description to the model without downcasting.
112    fn prompt_snippet(&self) -> Option<String> {
113        None
114    }
115}
116
117/// Validates that the backend can enforce the requested sandbox policy.
118///
119/// Returns `Err(ExecutionError::UnsupportedPolicy(...))` if any requested
120/// control cannot be enforced by the backend. This implements fail-closed
121/// semantics: execution is rejected before user code runs.
122///
123/// # Checks
124///
125/// - Network policy: if disabled, backend must be able to enforce it
126/// - Filesystem policy: if any access is requested, backend must enforce it
127/// - Environment policy: backend must always enforce it — both
128///   [`EnvironmentPolicy::None`] and [`EnvironmentPolicy::AllowList`] restrict
129///   what the code sees, and a backend that cannot enforce either passes the
130///   host environment through
131/// - Timeout: backend must always be able to enforce timeouts
132///
133/// # Errors
134///
135/// Returns [`ExecutionError::UnsupportedPolicy`] naming the first control the
136/// backend cannot enforce.
137pub fn validate_policy(
138    capabilities: &BackendCapabilities,
139    policy: &SandboxPolicy,
140) -> Result<(), ExecutionError> {
141    if matches!(policy.network, NetworkPolicy::Disabled) && !capabilities.enforce_network_policy {
142        return Err(ExecutionError::UnsupportedPolicy(
143            "backend cannot enforce network restrictions".to_string(),
144        ));
145    }
146    if !matches!(policy.filesystem, FilesystemPolicy::None)
147        && !capabilities.enforce_filesystem_policy
148    {
149        return Err(ExecutionError::UnsupportedPolicy(
150            "backend cannot enforce filesystem restrictions".to_string(),
151        ));
152    }
153    if !capabilities.enforce_environment_policy {
154        let requested = match &policy.environment {
155            EnvironmentPolicy::None => "no environment variables",
156            EnvironmentPolicy::AllowList(_) => "an environment allowlist",
157        };
158        return Err(ExecutionError::UnsupportedPolicy(format!(
159            "backend cannot enforce environment variable restrictions: the policy requests \
160             {requested}, but executed code would inherit the host environment"
161        )));
162    }
163    if !capabilities.enforce_timeout {
164        return Err(ExecutionError::UnsupportedPolicy(
165            "backend cannot enforce execution timeouts".to_string(),
166        ));
167    }
168    Ok(())
169}
170
171/// Validates a full execution request against a backend's capabilities.
172///
173/// Checks that:
174/// 1. The backend supports the requested language
175/// 2. The payload type matches the language (e.g., `GuestModule` only for Wasm)
176/// 3. The sandbox policy is enforceable by the backend
177///
178/// Call this before [`CodeExecutor::execute`] for clear, early errors.
179///
180/// # Example
181///
182/// ```rust
183/// use adk_code::{
184///     BackendCapabilities, ExecutionIsolation, ExecutionLanguage,
185///     ExecutionPayload, ExecutionRequest, SandboxPolicy,
186///     validate_request,
187/// };
188///
189/// let caps = BackendCapabilities {
190///     isolation: ExecutionIsolation::ContainerEphemeral,
191///     enforce_network_policy: true,
192///     enforce_filesystem_policy: true,
193///     enforce_environment_policy: true,
194///     enforce_timeout: true,
195///     supports_structured_output: true,
196///     supports_process_execution: false,
197///     supports_persistent_workspace: false,
198///     supports_interactive_sessions: false,
199/// };
200///
201/// let request = ExecutionRequest {
202///     language: ExecutionLanguage::Rust,
203///     payload: ExecutionPayload::Source {
204///         code: "fn run(input: serde_json::Value) -> serde_json::Value { input }".to_string(),
205///     },
206///     argv: vec![],
207///     stdin: None,
208///     input: None,
209///     sandbox: SandboxPolicy::strict_rust(),
210///     identity: None,
211/// };
212///
213/// let supported = [ExecutionLanguage::Rust];
214/// assert!(validate_request(&caps, &supported, &request).is_ok());
215/// ```
216pub fn validate_request(
217    capabilities: &BackendCapabilities,
218    supported_languages: &[ExecutionLanguage],
219    request: &ExecutionRequest,
220) -> Result<(), ExecutionError> {
221    // 1. Language support check
222    if !supported_languages.contains(&request.language) {
223        return Err(ExecutionError::UnsupportedLanguage(format!("{}", request.language)));
224    }
225
226    // 2. Payload-language compatibility check
227    match (&request.language, &request.payload) {
228        // GuestModule payloads are only valid for Wasm
229        (lang, ExecutionPayload::GuestModule { format, .. }) => match format {
230            GuestModuleFormat::Wasm if *lang != ExecutionLanguage::Wasm => {
231                return Err(ExecutionError::InvalidRequest(format!(
232                    "GuestModule(Wasm) payload requires Wasm language, got {lang}"
233                )));
234            }
235            _ => {}
236        },
237        // Wasm language requires a GuestModule payload
238        (ExecutionLanguage::Wasm, ExecutionPayload::Source { .. }) => {
239            return Err(ExecutionError::InvalidRequest(
240                "Wasm language requires a GuestModule payload, not Source".to_string(),
241            ));
242        }
243        _ => {}
244    }
245
246    // 3. Policy enforcement check
247    validate_policy(capabilities, &request.sandbox)?;
248
249    Ok(())
250}