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: if any variables are exposed, backend must enforce it
128/// - Timeout: backend must always be able to enforce timeouts
129pub fn validate_policy(
130 capabilities: &BackendCapabilities,
131 policy: &SandboxPolicy,
132) -> Result<(), ExecutionError> {
133 if matches!(policy.network, NetworkPolicy::Disabled) && !capabilities.enforce_network_policy {
134 return Err(ExecutionError::UnsupportedPolicy(
135 "backend cannot enforce network restrictions".to_string(),
136 ));
137 }
138 if !matches!(policy.filesystem, FilesystemPolicy::None)
139 && !capabilities.enforce_filesystem_policy
140 {
141 return Err(ExecutionError::UnsupportedPolicy(
142 "backend cannot enforce filesystem restrictions".to_string(),
143 ));
144 }
145 if !matches!(policy.environment, EnvironmentPolicy::None)
146 && !capabilities.enforce_environment_policy
147 {
148 return Err(ExecutionError::UnsupportedPolicy(
149 "backend cannot enforce environment variable restrictions".to_string(),
150 ));
151 }
152 if !capabilities.enforce_timeout {
153 return Err(ExecutionError::UnsupportedPolicy(
154 "backend cannot enforce execution timeouts".to_string(),
155 ));
156 }
157 Ok(())
158}
159
160/// Validates a full execution request against a backend's capabilities.
161///
162/// Checks that:
163/// 1. The backend supports the requested language
164/// 2. The payload type matches the language (e.g., `GuestModule` only for Wasm)
165/// 3. The sandbox policy is enforceable by the backend
166///
167/// Call this before [`CodeExecutor::execute`] for clear, early errors.
168///
169/// # Example
170///
171/// ```rust
172/// use adk_code::{
173/// BackendCapabilities, ExecutionIsolation, ExecutionLanguage,
174/// ExecutionPayload, ExecutionRequest, SandboxPolicy,
175/// validate_request,
176/// };
177///
178/// let caps = BackendCapabilities {
179/// isolation: ExecutionIsolation::ContainerEphemeral,
180/// enforce_network_policy: true,
181/// enforce_filesystem_policy: true,
182/// enforce_environment_policy: true,
183/// enforce_timeout: true,
184/// supports_structured_output: true,
185/// supports_process_execution: false,
186/// supports_persistent_workspace: false,
187/// supports_interactive_sessions: false,
188/// };
189///
190/// let request = ExecutionRequest {
191/// language: ExecutionLanguage::Rust,
192/// payload: ExecutionPayload::Source {
193/// code: "fn run(input: serde_json::Value) -> serde_json::Value { input }".to_string(),
194/// },
195/// argv: vec![],
196/// stdin: None,
197/// input: None,
198/// sandbox: SandboxPolicy::strict_rust(),
199/// identity: None,
200/// };
201///
202/// let supported = [ExecutionLanguage::Rust];
203/// assert!(validate_request(&caps, &supported, &request).is_ok());
204/// ```
205pub fn validate_request(
206 capabilities: &BackendCapabilities,
207 supported_languages: &[ExecutionLanguage],
208 request: &ExecutionRequest,
209) -> Result<(), ExecutionError> {
210 // 1. Language support check
211 if !supported_languages.contains(&request.language) {
212 return Err(ExecutionError::UnsupportedLanguage(format!("{}", request.language)));
213 }
214
215 // 2. Payload-language compatibility check
216 match (&request.language, &request.payload) {
217 // GuestModule payloads are only valid for Wasm
218 (lang, ExecutionPayload::GuestModule { format, .. }) => match format {
219 GuestModuleFormat::Wasm if *lang != ExecutionLanguage::Wasm => {
220 return Err(ExecutionError::InvalidRequest(format!(
221 "GuestModule(Wasm) payload requires Wasm language, got {lang}"
222 )));
223 }
224 _ => {}
225 },
226 // Wasm language requires a GuestModule payload
227 (ExecutionLanguage::Wasm, ExecutionPayload::Source { .. }) => {
228 return Err(ExecutionError::InvalidRequest(
229 "Wasm language requires a GuestModule payload, not Source".to_string(),
230 ));
231 }
232 _ => {}
233 }
234
235 // 3. Policy enforcement check
236 validate_policy(capabilities, &request.sandbox)?;
237
238 Ok(())
239}