diff --git a/crates/codegen/xai-grok-workspace/src/file_system/mod.rs b/crates/codegen/xai-grok-workspace/src/file_system/mod.rs index 50e02fe2..fecb6645 100644 --- a/crates/codegen/xai-grok-workspace/src/file_system/mod.rs +++ b/crates/codegen/xai-grok-workspace/src/file_system/mod.rs @@ -34,6 +34,9 @@ pub use fs::{AsyncFileSystem, AsyncFsWrapper, FsError, bytes_to_string}; mod local_fs; pub use local_fs::LocalFs; +mod runtime_fs; +pub use runtime_fs::RuntimeFs; + mod mock_fs; pub use mock_fs::MockFs; diff --git a/crates/codegen/xai-grok-workspace/src/file_system/runtime_fs.rs b/crates/codegen/xai-grok-workspace/src/file_system/runtime_fs.rs new file mode 100644 index 00000000..d51d03d1 --- /dev/null +++ b/crates/codegen/xai-grok-workspace/src/file_system/runtime_fs.rs @@ -0,0 +1,79 @@ +//! Workspace filesystem adapter backed by the Simplicio Runtime. +//! +//! Project content must not be read, written, or deleted through the host +//! filesystem. The tools crate owns the Runtime/Agent handshake; this thin +//! adapter lets workspace session state use the same fail-closed boundary. + +use std::path::{Path, PathBuf}; + +use crate::file_system::{AsyncFileSystem, FsError}; +use xai_grok_tools::computer::types::AsyncFileSystem as ToolAsyncFileSystem; +use xai_grok_tools::computer::local::SimplicioRuntimeFs; + +/// Workspace filesystem whose project effects are delegated to Runtime. +pub struct RuntimeFs { + root: PathBuf, + runtime: SimplicioRuntimeFs, +} + +impl RuntimeFs { + pub fn new(root: PathBuf) -> Self { + Self { + runtime: SimplicioRuntimeFs::new(root.clone()), + root, + } + } + + fn map_error(error: xai_grok_tools::computer::types::ComputerError) -> FsError { + FsError::Other(format!("Simplicio Runtime denied workspace operation: {error}")) + } + + fn is_not_found(error: &xai_grok_tools::computer::types::ComputerError) -> bool { + let message = error.to_string().to_ascii_lowercase(); + message.contains("not found") || message.contains("resource_not_found") + } +} + +#[async_trait::async_trait] +impl AsyncFileSystem for RuntimeFs { + fn root(&self) -> &Path { + &self.root + } + + async fn exists(&self, path: &Path) -> Result { + match self.runtime.stat_workspace(path).await { + Ok(_) => Ok(true), + Err(error) if Self::is_not_found(&error) => Ok(false), + Err(error) => Err(Self::map_error(error)), + } + } + + async fn read_file(&self, path: &Path) -> Result, FsError> { + self.runtime + .read_file(path) + .await + .map_err(Self::map_error) + } + + async fn try_read_file(&self, path: &Path) -> Result>, FsError> { + match self.runtime.read_file(path).await { + Ok(bytes) => Ok(Some(bytes)), + Err(error) if Self::is_not_found(&error) => Ok(None), + Err(error) => Err(Self::map_error(error)), + } + } + + async fn write_file(&self, path: &Path, data: &[u8]) -> Result<(), FsError> { + self.runtime + .write_file(path, data) + .await + .map_err(Self::map_error) + } + + async fn delete_file(&self, path: &Path) -> Result<(), FsError> { + self.runtime + .delete_file(path) + .await + .map_err(Self::map_error) + } +} diff --git a/crates/codegen/xai-grok-workspace/src/session/mod.rs b/crates/codegen/xai-grok-workspace/src/session/mod.rs index 4e9ac2c7..efbbec19 100644 --- a/crates/codegen/xai-grok-workspace/src/session/mod.rs +++ b/crates/codegen/xai-grok-workspace/src/session/mod.rs @@ -7,7 +7,7 @@ pub(crate) mod swap_policy; pub mod tool_config; use crate::capability::CapabilityMode; use crate::config::{MemoryConfig, SessionContextFactory}; -use crate::file_system::{AsyncFsWrapper, LocalFs}; +use crate::file_system::{AsyncFsWrapper, LocalFs, RuntimeFs}; use crate::hub::{HubConfig, HubHandle}; use crate::session::file_state::FileStateTracker; use parking_lot::RwLock; @@ -165,7 +165,14 @@ impl WorkspaceSession { Some((handle, rx)) => (Some(handle), Some(rx)), None => (None, None), }; - let async_fs = AsyncFsWrapper::new(Arc::new(LocalFs::new(cwd.clone()))); + // Unit tests use LocalFs fixtures because they do not start the + // independent Agent host. Every production session uses RuntimeFs; + // project content therefore fails closed when Agent/Runtime is absent. + let async_fs = if cfg!(test) { + AsyncFsWrapper::new(Arc::new(LocalFs::new(cwd.clone()))) + } else { + AsyncFsWrapper::new(Arc::new(RuntimeFs::new(cwd.clone()))) + }; let file_state_tracker = Arc::new(FileStateTracker::new()); let checkpoint_store = crate::session::checkpoint_store::CheckpointStore::new(&cwd, &session_id);