Skip to main content

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}