Skip to main content

rucora_core/agent/
runtime_adapter.rs

1//! RuntimeAdapter(运行时适配器)抽象
2//!
3//! 本模块提供跨平台运行时抽象,使相同的 Agent 代码可以在不同平台运行:
4//! - Native(原生操作系统)
5//! - Docker(容器环境)
6//! - WASM(WebAssembly)
7//! - Serverless(无服务器函数)
8//!
9//! 参考实现: zeroclaw `RuntimeAdapter` trait
10
11use std::path::{Path, PathBuf};
12use std::process::Stdio;
13
14/// 运行时平台类型
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum RuntimePlatform {
17    /// 原生操作系统
18    Native,
19    /// Docker 容器
20    Docker,
21    /// WebAssembly
22    Wasm,
23    /// 无服务器函数(如 AWS Lambda, Azure Functions)
24    Serverless,
25    /// 嵌入式设备
26    Embedded,
27    /// 浏览器环境
28    Browser,
29}
30
31impl std::fmt::Display for RuntimePlatform {
32    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33        match self {
34            RuntimePlatform::Native => write!(f, "native"),
35            RuntimePlatform::Docker => write!(f, "docker"),
36            RuntimePlatform::Wasm => write!(f, "wasm"),
37            RuntimePlatform::Serverless => write!(f, "serverless"),
38            RuntimePlatform::Embedded => write!(f, "embedded"),
39            RuntimePlatform::Browser => write!(f, "browser"),
40        }
41    }
42}
43
44/// 运行时能力声明
45///
46/// 描述当前运行时支持的功能
47#[derive(Debug, Clone, Copy, Default)]
48pub struct RuntimeCapabilities {
49    /// 是否有 shell 访问权限
50    pub has_shell_access: bool,
51    /// 是否有文件系统访问权限
52    pub has_filesystem_access: bool,
53    /// 是否支持网络访问
54    pub has_network_access: bool,
55    /// 是否支持长时间运行任务
56    pub supports_long_running: bool,
57    /// 是否支持多线程
58    pub supports_multithreading: bool,
59    /// 是否支持动态加载
60    pub supports_dynamic_loading: bool,
61    /// 最大文件大小限制(字节,0 表示无限制)
62    pub max_file_size: u64,
63    /// 最大内存限制(字节,0 表示无限制)
64    pub max_memory: u64,
65}
66
67impl RuntimeCapabilities {
68    /// 创建完整能力的运行时
69    pub fn full() -> Self {
70        Self {
71            has_shell_access: true,
72            has_filesystem_access: true,
73            has_network_access: true,
74            supports_long_running: true,
75            supports_multithreading: true,
76            supports_dynamic_loading: true,
77            max_file_size: 0,
78            max_memory: 0,
79        }
80    }
81
82    /// 创建受限能力的运行时(如 WASM)
83    pub fn restricted() -> Self {
84        Self {
85            has_shell_access: false,
86            has_filesystem_access: false,
87            has_network_access: false,
88            supports_long_running: false,
89            supports_multithreading: false,
90            supports_dynamic_loading: false,
91            max_file_size: 10 * 1024 * 1024, // 10MB
92            max_memory: 128 * 1024 * 1024,   // 128MB
93        }
94    }
95
96    /// 创建容器环境的运行时
97    pub fn container() -> Self {
98        Self {
99            has_shell_access: true,
100            has_filesystem_access: true,
101            has_network_access: true,
102            supports_long_running: true,
103            supports_multithreading: true,
104            supports_dynamic_loading: false, // 通常容器中不鼓励动态加载
105            max_file_size: 100 * 1024 * 1024, // 100MB
106            max_memory: 512 * 1024 * 1024,   // 512MB
107        }
108    }
109}
110
111/// 运行时适配器 trait
112///
113/// 抽象不同运行时的平台能力,使 Agent 代码可以跨平台运行。
114#[async_trait::async_trait]
115pub trait RuntimeAdapter: Send + Sync {
116    /// 获取运行时名称
117    fn name(&self) -> &str;
118
119    /// 获取运行时平台类型
120    fn platform(&self) -> RuntimePlatform;
121
122    /// 获取运行时能力
123    fn capabilities(&self) -> RuntimeCapabilities;
124
125    /// 获取存储路径
126    ///
127    /// 返回运行时允许持久化存储的路径
128    fn storage_path(&self) -> PathBuf;
129
130    /// 获取临时目录路径
131    fn temp_path(&self) -> PathBuf;
132
133    /// 获取内存预算(字节,0 表示无限制)
134    fn memory_budget(&self) -> u64 {
135        self.capabilities().max_memory
136    }
137
138    /// 检查是否有 shell 访问权限
139    fn has_shell_access(&self) -> bool {
140        self.capabilities().has_shell_access
141    }
142
143    /// 检查是否有文件系统访问权限
144    fn has_filesystem_access(&self) -> bool {
145        self.capabilities().has_filesystem_access
146    }
147
148    /// 检查是否支持网络访问
149    fn has_network_access(&self) -> bool {
150        self.capabilities().has_network_access
151    }
152
153    /// 检查是否支持长时间运行
154    fn supports_long_running(&self) -> bool {
155        self.capabilities().supports_long_running
156    }
157
158    /// 构建 shell 命令
159    ///
160    /// 根据运行时环境构建适当的命令执行器
161    ///
162    /// # Errors
163    ///
164    /// 当运行时不支持 shell 访问时返回 [`RuntimeError`]。
165    fn build_shell_command(
166        &self,
167        command: &str,
168        working_dir: Option<&Path>,
169    ) -> Result<tokio::process::Command, RuntimeError>;
170
171    /// 读取文件
172    ///
173    /// 根据运行时限制读取文件内容
174    async fn read_file(&self, path: &Path) -> Result<String, RuntimeError>;
175
176    /// 写入文件
177    ///
178    /// 根据运行时限制写入文件内容
179    async fn write_file(&self, path: &Path, content: &str) -> Result<(), RuntimeError>;
180
181    /// 检查文件是否存在
182    async fn file_exists(&self, path: &Path) -> bool;
183
184    /// 获取文件大小
185    async fn file_size(&self, path: &Path) -> Result<u64, RuntimeError>;
186
187    /// 列出目录内容
188    async fn list_directory(&self, path: &Path) -> Result<Vec<tokio::fs::DirEntry>, RuntimeError>;
189
190    /// 创建目录
191    async fn create_directory(&self, path: &Path) -> Result<(), RuntimeError>;
192
193    /// 执行 shell 命令
194    ///
195    /// 在支持 shell 的运行时中执行命令
196    async fn execute_shell(
197        &self,
198        command: &str,
199        working_dir: Option<&Path>,
200        timeout_secs: Option<u64>,
201    ) -> Result<ShellResult, RuntimeError>;
202
203    /// 获取环境变量
204    fn get_env(&self, key: &str) -> Option<String>;
205
206    /// 设置环境变量(如果运行时支持)
207    ///
208    /// # Errors
209    ///
210    /// 当运行时不支持设置环境变量时返回 [`RuntimeError`]。
211    fn set_env(&self, key: &str, value: &str) -> Result<(), RuntimeError>;
212
213    /// 记录日志
214    fn log(&self, level: LogLevel, message: &str);
215
216    /// 获取当前时间戳
217    fn current_timestamp(&self) -> u64 {
218        std::time::SystemTime::now()
219            .duration_since(std::time::UNIX_EPOCH)
220            .map_or(0, |d| d.as_secs())
221    }
222}
223
224/// 运行时错误
225#[derive(Debug, Clone)]
226pub enum RuntimeError {
227    /// 操作不被支持
228    NotSupported(String),
229    /// 权限不足
230    PermissionDenied(String),
231    /// 资源限制
232    ResourceLimit(String),
233    /// IO 错误
234    IoError(String),
235    /// 超时
236    Timeout(String),
237    /// 其他错误
238    Other(String),
239}
240
241impl std::fmt::Display for RuntimeError {
242    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
243        match self {
244            RuntimeError::NotSupported(msg) => write!(f, "操作不被支持: {msg}"),
245            RuntimeError::PermissionDenied(msg) => write!(f, "权限不足: {msg}"),
246            RuntimeError::ResourceLimit(msg) => write!(f, "资源限制: {msg}"),
247            RuntimeError::IoError(msg) => write!(f, "IO 错误: {msg}"),
248            RuntimeError::Timeout(msg) => write!(f, "超时: {msg}"),
249            RuntimeError::Other(msg) => write!(f, "错误: {msg}"),
250        }
251    }
252}
253
254impl std::error::Error for RuntimeError {}
255
256/// 日志级别
257#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
258pub enum LogLevel {
259    Trace,
260    Debug,
261    Info,
262    Warn,
263    Error,
264}
265
266/// Shell 执行结果
267#[derive(Debug, Clone)]
268pub struct ShellResult {
269    /// 退出码
270    pub exit_code: i32,
271    /// 标准输出
272    pub stdout: String,
273    /// 标准错误
274    pub stderr: String,
275    /// 执行时长(毫秒)
276    pub duration_ms: u64,
277}
278
279/// 原生运行时适配器
280pub struct NativeRuntimeAdapter {
281    name: String,
282    storage_path: PathBuf,
283    capabilities: RuntimeCapabilities,
284}
285
286impl NativeRuntimeAdapter {
287    /// 创建新的原生运行时适配器
288    pub fn new() -> Self {
289        // 使用标准目录或当前目录作为存储路径
290        let storage_path = PathBuf::from("./.rucora");
291
292        Self {
293            name: "native".to_string(),
294            storage_path,
295            capabilities: RuntimeCapabilities::full(),
296        }
297    }
298
299    /// 设置自定义存储路径
300    pub fn with_storage_path(mut self, path: impl AsRef<Path>) -> Self {
301        self.storage_path = path.as_ref().to_path_buf();
302        self
303    }
304
305    /// 设置自定义能力
306    pub fn with_capabilities(mut self, capabilities: RuntimeCapabilities) -> Self {
307        self.capabilities = capabilities;
308        self
309    }
310}
311
312impl Default for NativeRuntimeAdapter {
313    fn default() -> Self {
314        Self::new()
315    }
316}
317
318#[async_trait::async_trait]
319impl RuntimeAdapter for NativeRuntimeAdapter {
320    fn name(&self) -> &str {
321        &self.name
322    }
323
324    fn platform(&self) -> RuntimePlatform {
325        RuntimePlatform::Native
326    }
327
328    fn capabilities(&self) -> RuntimeCapabilities {
329        self.capabilities
330    }
331
332    fn storage_path(&self) -> PathBuf {
333        self.storage_path.clone()
334    }
335
336    fn temp_path(&self) -> PathBuf {
337        std::env::temp_dir().join("rucora")
338    }
339
340    fn build_shell_command(
341        &self,
342        command: &str,
343        working_dir: Option<&Path>,
344    ) -> Result<tokio::process::Command, RuntimeError> {
345        if !self.has_shell_access() {
346            return Err(RuntimeError::NotSupported(
347                "当前运行时不支持 shell 访问".to_string(),
348            ));
349        }
350
351        #[cfg(target_os = "windows")]
352        let mut cmd = {
353            let mut c = tokio::process::Command::new("cmd");
354            c.arg("/C").arg(command);
355            c
356        };
357
358        #[cfg(not(target_os = "windows"))]
359        let mut cmd = {
360            let mut c = tokio::process::Command::new("sh");
361            c.arg("-c").arg(command);
362            c
363        };
364
365        if let Some(dir) = working_dir {
366            cmd.current_dir(dir);
367        }
368
369        Ok(cmd)
370    }
371
372    async fn read_file(&self, path: &Path) -> Result<String, RuntimeError> {
373        if !self.has_filesystem_access() {
374            return Err(RuntimeError::NotSupported(
375                "当前运行时不支持文件系统访问".to_string(),
376            ));
377        }
378
379        // 检查文件大小限制
380        if let Ok(metadata) = tokio::fs::metadata(path).await {
381            let size = metadata.len();
382            let max_size = self.capabilities.max_file_size;
383            if max_size > 0 && size > max_size {
384                return Err(RuntimeError::ResourceLimit(format!(
385                    "文件大小 {size} 超过限制 {max_size}"
386                )));
387            }
388        }
389
390        tokio::fs::read_to_string(path)
391            .await
392            .map_err(|e| RuntimeError::IoError(e.to_string()))
393    }
394
395    async fn write_file(&self, path: &Path, content: &str) -> Result<(), RuntimeError> {
396        if !self.has_filesystem_access() {
397            return Err(RuntimeError::NotSupported(
398                "当前运行时不支持文件系统访问".to_string(),
399            ));
400        }
401
402        // 检查内容大小限制
403        let content_size = content.len() as u64;
404        let max_size = self.capabilities.max_file_size;
405        if max_size > 0 && content_size > max_size {
406            return Err(RuntimeError::ResourceLimit(format!(
407                "内容大小 {content_size} 超过限制 {max_size}"
408            )));
409        }
410
411        // 确保父目录存在
412        if let Some(parent) = path.parent() {
413            tokio::fs::create_dir_all(parent)
414                .await
415                .map_err(|e| RuntimeError::IoError(e.to_string()))?;
416        }
417
418        tokio::fs::write(path, content)
419            .await
420            .map_err(|e| RuntimeError::IoError(e.to_string()))
421    }
422
423    async fn file_exists(&self, path: &Path) -> bool {
424        if !self.has_filesystem_access() {
425            return false;
426        }
427        tokio::fs::metadata(path).await.is_ok()
428    }
429
430    async fn file_size(&self, path: &Path) -> Result<u64, RuntimeError> {
431        if !self.has_filesystem_access() {
432            return Err(RuntimeError::NotSupported(
433                "当前运行时不支持文件系统访问".to_string(),
434            ));
435        }
436
437        tokio::fs::metadata(path)
438            .await
439            .map(|m| m.len())
440            .map_err(|e| RuntimeError::IoError(e.to_string()))
441    }
442
443    async fn list_directory(&self, path: &Path) -> Result<Vec<tokio::fs::DirEntry>, RuntimeError> {
444        if !self.has_filesystem_access() {
445            return Err(RuntimeError::NotSupported(
446                "当前运行时不支持文件系统访问".to_string(),
447            ));
448        }
449
450        let mut entries = Vec::new();
451        let mut dir = tokio::fs::read_dir(path)
452            .await
453            .map_err(|e| RuntimeError::IoError(e.to_string()))?;
454
455        while let Some(entry) = dir
456            .next_entry()
457            .await
458            .map_err(|e| RuntimeError::IoError(e.to_string()))?
459        {
460            entries.push(entry);
461        }
462
463        Ok(entries)
464    }
465
466    async fn create_directory(&self, path: &Path) -> Result<(), RuntimeError> {
467        if !self.has_filesystem_access() {
468            return Err(RuntimeError::NotSupported(
469                "当前运行时不支持文件系统访问".to_string(),
470            ));
471        }
472
473        tokio::fs::create_dir_all(path)
474            .await
475            .map_err(|e| RuntimeError::IoError(e.to_string()))
476    }
477
478    async fn execute_shell(
479        &self,
480        command: &str,
481        working_dir: Option<&Path>,
482        timeout_secs: Option<u64>,
483    ) -> Result<ShellResult, RuntimeError> {
484        if !self.has_shell_access() {
485            return Err(RuntimeError::NotSupported(
486                "当前运行时不支持 shell 访问".to_string(),
487            ));
488        }
489
490        let mut cmd = self.build_shell_command(command, working_dir)?;
491
492        cmd.stdout(Stdio::piped());
493        cmd.stderr(Stdio::piped());
494
495        let start = std::time::Instant::now();
496
497        let output = if let Some(timeout) = timeout_secs {
498            tokio::time::timeout(tokio::time::Duration::from_secs(timeout), cmd.output())
499                .await
500                .map_err(|_| RuntimeError::Timeout("命令执行超时".to_string()))?
501                .map_err(|e| RuntimeError::IoError(e.to_string()))?
502        } else {
503            cmd.output()
504                .await
505                .map_err(|e| RuntimeError::IoError(e.to_string()))?
506        };
507
508        let duration_ms = start.elapsed().as_millis() as u64;
509
510        Ok(ShellResult {
511            exit_code: output.status.code().unwrap_or(-1),
512            stdout: String::from_utf8_lossy(&output.stdout).to_string(),
513            stderr: String::from_utf8_lossy(&output.stderr).to_string(),
514            duration_ms,
515        })
516    }
517
518    fn get_env(&self, key: &str) -> Option<String> {
519        std::env::var(key).ok()
520    }
521
522    fn set_env(&self, key: &str, value: &str) -> Result<(), RuntimeError> {
523        // SAFETY: 我们信任调用者不会传入无效的键或值
524        unsafe {
525            std::env::set_var(key, value);
526        }
527        Ok(())
528    }
529
530    #[allow(clippy::cognitive_complexity)]
531    fn log(&self, level: LogLevel, message: &str) {
532        match level {
533            LogLevel::Trace => tracing::trace!("{}", message),
534            LogLevel::Debug => tracing::debug!("{}", message),
535            LogLevel::Info => tracing::info!("{}", message),
536            LogLevel::Warn => tracing::warn!("{}", message),
537            LogLevel::Error => tracing::error!("{}", message),
538        }
539    }
540}
541
542/// 受限运行时适配器(用于 WASM 等受限环境)
543pub struct RestrictedRuntimeAdapter {
544    name: String,
545    storage_path: PathBuf,
546}
547
548impl RestrictedRuntimeAdapter {
549    /// 创建新的受限运行时适配器
550    pub fn new() -> Self {
551        Self {
552            name: "restricted".to_string(),
553            storage_path: PathBuf::from("/tmp/rucora"),
554        }
555    }
556}
557
558impl Default for RestrictedRuntimeAdapter {
559    fn default() -> Self {
560        Self::new()
561    }
562}
563
564#[async_trait::async_trait]
565impl RuntimeAdapter for RestrictedRuntimeAdapter {
566    fn name(&self) -> &str {
567        &self.name
568    }
569
570    fn platform(&self) -> RuntimePlatform {
571        RuntimePlatform::Wasm
572    }
573
574    fn capabilities(&self) -> RuntimeCapabilities {
575        RuntimeCapabilities::restricted()
576    }
577
578    fn storage_path(&self) -> PathBuf {
579        self.storage_path.clone()
580    }
581
582    fn temp_path(&self) -> PathBuf {
583        PathBuf::from("/tmp")
584    }
585
586    fn build_shell_command(
587        &self,
588        _command: &str,
589        _working_dir: Option<&Path>,
590    ) -> Result<tokio::process::Command, RuntimeError> {
591        Err(RuntimeError::NotSupported(
592            "受限运行时不支持 shell 命令".to_string(),
593        ))
594    }
595
596    async fn read_file(&self, path: &Path) -> Result<String, RuntimeError> {
597        // 在受限环境中,只允许读取特定路径
598        if !path.starts_with(&self.storage_path) {
599            return Err(RuntimeError::PermissionDenied(
600                "只能访问存储目录内的文件".to_string(),
601            ));
602        }
603
604        tokio::fs::read_to_string(path)
605            .await
606            .map_err(|e| RuntimeError::IoError(e.to_string()))
607    }
608
609    async fn write_file(&self, path: &Path, content: &str) -> Result<(), RuntimeError> {
610        if !path.starts_with(&self.storage_path) {
611            return Err(RuntimeError::PermissionDenied(
612                "只能写入存储目录内的文件".to_string(),
613            ));
614        }
615
616        tokio::fs::write(path, content)
617            .await
618            .map_err(|e| RuntimeError::IoError(e.to_string()))
619    }
620
621    async fn file_exists(&self, path: &Path) -> bool {
622        tokio::fs::metadata(path).await.is_ok()
623    }
624
625    async fn file_size(&self, path: &Path) -> Result<u64, RuntimeError> {
626        tokio::fs::metadata(path)
627            .await
628            .map(|m| m.len())
629            .map_err(|e| RuntimeError::IoError(e.to_string()))
630    }
631
632    async fn list_directory(&self, _path: &Path) -> Result<Vec<tokio::fs::DirEntry>, RuntimeError> {
633        Err(RuntimeError::NotSupported(
634            "受限运行时不支持目录列表".to_string(),
635        ))
636    }
637
638    async fn create_directory(&self, path: &Path) -> Result<(), RuntimeError> {
639        if !path.starts_with(&self.storage_path) {
640            return Err(RuntimeError::PermissionDenied(
641                "只能在存储目录内创建目录".to_string(),
642            ));
643        }
644
645        tokio::fs::create_dir_all(path)
646            .await
647            .map_err(|e| RuntimeError::IoError(e.to_string()))
648    }
649
650    async fn execute_shell(
651        &self,
652        _command: &str,
653        _working_dir: Option<&Path>,
654        _timeout_secs: Option<u64>,
655    ) -> Result<ShellResult, RuntimeError> {
656        Err(RuntimeError::NotSupported(
657            "受限运行时不支持 shell 命令".to_string(),
658        ))
659    }
660
661    fn get_env(&self, key: &str) -> Option<String> {
662        // 受限环境中只允许访问特定环境变量
663        if key.starts_with("rucora_") {
664            std::env::var(key).ok()
665        } else {
666            None
667        }
668    }
669
670    fn set_env(&self, _key: &str, _value: &str) -> Result<(), RuntimeError> {
671        Err(RuntimeError::NotSupported(
672            "受限运行时不支持设置环境变量".to_string(),
673        ))
674    }
675
676    fn log(&self, level: LogLevel, message: &str) {
677        // 受限环境中使用 console.log 或类似机制
678        eprintln!("[{level:?}] {message}");
679    }
680}
681
682#[cfg(test)]
683mod tests {
684    use super::*;
685
686    #[test]
687    fn test_runtime_platform_display() {
688        assert_eq!(RuntimePlatform::Native.to_string(), "native");
689        assert_eq!(RuntimePlatform::Docker.to_string(), "docker");
690        assert_eq!(RuntimePlatform::Wasm.to_string(), "wasm");
691    }
692
693    #[test]
694    fn test_runtime_capabilities() {
695        let full = RuntimeCapabilities::full();
696        assert!(full.has_shell_access);
697        assert!(full.has_filesystem_access);
698        assert!(full.supports_long_running);
699
700        let restricted = RuntimeCapabilities::restricted();
701        assert!(!restricted.has_shell_access);
702        assert!(!restricted.has_filesystem_access);
703        assert!(!restricted.supports_long_running);
704
705        let container = RuntimeCapabilities::container();
706        assert!(container.has_shell_access);
707        assert!(!container.supports_dynamic_loading);
708    }
709
710    #[test]
711    fn test_runtime_error_display() {
712        let err = RuntimeError::NotSupported("test".to_string());
713        assert!(err.to_string().contains("不被支持"));
714
715        let err = RuntimeError::PermissionDenied("test".to_string());
716        assert!(err.to_string().contains("权限不足"));
717    }
718
719    #[tokio::test]
720    async fn test_native_runtime_adapter() {
721        let adapter = NativeRuntimeAdapter::new();
722
723        assert_eq!(adapter.name(), "native");
724        assert!(adapter.has_shell_access());
725        assert!(adapter.has_filesystem_access());
726        assert!(adapter.supports_long_running());
727
728        // 测试文件操作
729        let test_path = adapter.temp_path().join("test.txt");
730        adapter.write_file(&test_path, "hello").await.unwrap();
731
732        assert!(adapter.file_exists(&test_path).await);
733        assert_eq!(adapter.file_size(&test_path).await.unwrap(), 5);
734
735        let content = adapter.read_file(&test_path).await.unwrap();
736        assert_eq!(content, "hello");
737
738        // 清理
739        tokio::fs::remove_file(&test_path).await.ok();
740    }
741
742    #[tokio::test]
743    async fn test_restricted_runtime_adapter() {
744        let adapter = RestrictedRuntimeAdapter::new();
745
746        assert_eq!(adapter.name(), "restricted");
747        assert!(!adapter.has_shell_access());
748        assert!(!adapter.has_filesystem_access());
749
750        // 尝试执行 shell 命令应该失败
751        let result = adapter.execute_shell("echo hello", None, None).await;
752        assert!(matches!(result, Err(RuntimeError::NotSupported(_))));
753    }
754}