rustyclaw_core/runtime/traits.rs
1//! Runtime adapter trait for platform abstraction.
2//!
3//! This module defines the [`RuntimeAdapter`] trait which abstracts platform
4//! differences for agent execution. Implementations allow RustyClaw to run on
5//! different environments (native, Docker, serverless) with appropriate
6//! capability detection.
7//!
8//! Adapted from ZeroClaw (MIT OR Apache-2.0 licensed).
9
10use std::path::{Path, PathBuf};
11
12/// Runtime adapter that abstracts platform differences for the agent.
13///
14/// Implement this trait to port the agent to a new execution environment.
15/// The adapter declares platform capabilities (shell access, filesystem,
16/// long-running processes) and provides platform-specific implementations
17/// for operations like spawning shell commands. The orchestration loop
18/// queries these capabilities to adapt its behavior—for example, disabling
19/// tool execution on runtimes without shell access.
20///
21/// Implementations must be `Send + Sync` because the adapter is shared
22/// across async tasks on the Tokio runtime.
23pub trait RuntimeAdapter: Send + Sync {
24 /// Return the human-readable name of this runtime environment.
25 ///
26 /// Used in logs and diagnostics (e.g., `"native"`, `"docker"`,
27 /// `"cloudflare-workers"`).
28 fn name(&self) -> &str;
29
30 /// Report whether this runtime supports shell command execution.
31 ///
32 /// When `false`, the agent disables shell-based tools. Serverless and
33 /// edge runtimes typically return `false`.
34 fn has_shell_access(&self) -> bool;
35
36 /// Report whether this runtime supports filesystem read/write.
37 ///
38 /// When `false`, the agent disables file-based tools and falls back to
39 /// in-memory storage.
40 fn has_filesystem_access(&self) -> bool;
41
42 /// Return the base directory for persistent storage on this runtime.
43 ///
44 /// Memory backends, logs, and other artifacts are stored under this path.
45 /// Implementations should return a platform-appropriate writable directory.
46 fn storage_path(&self) -> PathBuf;
47
48 /// Report whether this runtime supports long-running background processes.
49 ///
50 /// When `true`, the agent may start the gateway server, heartbeat loop,
51 /// and other persistent tasks. Serverless runtimes with short execution
52 /// limits should return `false`.
53 fn supports_long_running(&self) -> bool;
54
55 /// Return the maximum memory budget in bytes for this runtime.
56 ///
57 /// A value of `0` (the default) indicates no limit. Constrained
58 /// environments (embedded, serverless) should return their actual
59 /// memory ceiling so the agent can adapt buffer sizes and caching.
60 fn memory_budget(&self) -> u64 {
61 0
62 }
63
64 /// Build a shell command process configured for this runtime.
65 ///
66 /// Constructs a [`tokio::process::Command`] that will execute `command`
67 /// with `workspace_dir` as the working directory. Implementations may
68 /// prepend sandbox wrappers, set environment variables, or redirect
69 /// I/O as appropriate for the platform.
70 ///
71 /// # Errors
72 ///
73 /// Returns an error if the runtime does not support shell access or if
74 /// the command cannot be constructed (e.g., missing shell binary).
75 fn build_shell_command(
76 &self,
77 command: &str,
78 workspace_dir: &Path,
79 ) -> anyhow::Result<tokio::process::Command>;
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85
86 struct DummyRuntime;
87
88 impl RuntimeAdapter for DummyRuntime {
89 fn name(&self) -> &str {
90 "dummy-runtime"
91 }
92
93 fn has_shell_access(&self) -> bool {
94 true
95 }
96
97 fn has_filesystem_access(&self) -> bool {
98 true
99 }
100
101 fn storage_path(&self) -> PathBuf {
102 PathBuf::from("/tmp/dummy-runtime")
103 }
104
105 fn supports_long_running(&self) -> bool {
106 true
107 }
108
109 fn build_shell_command(
110 &self,
111 command: &str,
112 workspace_dir: &Path,
113 ) -> anyhow::Result<tokio::process::Command> {
114 let mut cmd = tokio::process::Command::new("sh");
115 cmd.arg("-c").arg(command);
116 cmd.current_dir(workspace_dir);
117 Ok(cmd)
118 }
119 }
120
121 #[test]
122 fn default_memory_budget_is_zero() {
123 let runtime = DummyRuntime;
124 assert_eq!(runtime.memory_budget(), 0);
125 }
126
127 #[test]
128 fn runtime_reports_capabilities() {
129 let runtime = DummyRuntime;
130
131 assert_eq!(runtime.name(), "dummy-runtime");
132 assert!(runtime.has_shell_access());
133 assert!(runtime.has_filesystem_access());
134 assert!(runtime.supports_long_running());
135 assert_eq!(runtime.storage_path(), PathBuf::from("/tmp/dummy-runtime"));
136 }
137
138 #[tokio::test]
139 async fn build_shell_command_executes() {
140 let runtime = DummyRuntime;
141 let mut cmd = runtime
142 .build_shell_command("echo hello-runtime", Path::new("."))
143 .unwrap();
144
145 let output = cmd.output().await.unwrap();
146 let stdout = String::from_utf8_lossy(&output.stdout);
147
148 assert!(output.status.success());
149 assert!(stdout.contains("hello-runtime"));
150 }
151}