From 6a582357f4566fcb43d8e2590661ead0cd47ecaa Mon Sep 17 00:00:00 2001 From: Sewer56 Date: Fri, 29 May 2026 21:50:04 +0100 Subject: [PATCH] Changed: Introduce ToolBuildContext to pass workspace root and permissions through tool construction - Add ToolBuildContext struct in reloaded-code-core with canonicalized workspace root and optional permission ruleset - Refactor build_resolver_for_tool to accept &ToolBuildContext instead of separate config and workspace_root parameters - Update reloaded-code-agents resolver and tests to use new context - Update reloaded-code-serdesai runtime to create one context before tool construction loop --- src/reloaded-code-agents/src/path/resolver.rs | 57 +++++++++++-------- src/reloaded-code-core/src/lib.rs | 2 + .../src/tool_context/mod.rs | 51 +++++++++++++++++ .../src/agent_runtime/build.rs | 31 ++++++++-- 4 files changed, 110 insertions(+), 31 deletions(-) create mode 100644 src/reloaded-code-core/src/tool_context/mod.rs diff --git a/src/reloaded-code-agents/src/path/resolver.rs b/src/reloaded-code-agents/src/path/resolver.rs index 870f7db..90ee90e 100644 --- a/src/reloaded-code-agents/src/path/resolver.rs +++ b/src/reloaded-code-agents/src/path/resolver.rs @@ -23,7 +23,7 @@ use reloaded_code_core::path::{ PathResolver, RuleAction, }; use reloaded_code_core::permissions::PermissionAction; -use soft_canonicalize::soft_canonicalize; +use reloaded_code_core::tool_context::ToolBuildContext; use std::path::{Path, PathBuf}; /// Closed enum of resolver types used for file tools. @@ -77,31 +77,25 @@ impl PathResolver for FileToolResolver { /// /// # Arguments /// -/// - `config` - Permission config mapping tool names to [`PermissionRule`]. -/// - `tool_name` - Name of the tool to look up in `config`. -/// - `workspace_root` - Workspace root used for relative-pattern resolution. +/// - `build_context` - [`ToolBuildContext`] containing the canonicalized workspace root +/// and optional permission ruleset. +/// - `config` - Permission config map used to look up tool-specific rules. +/// - `tool_name` - Name of the tool to look up in the context's permission config. /// /// # Returns /// /// The cheapest [`FileToolResolver`] variant satisfying the tool's permission config. /// /// # Errors -/// - Returns [`ToolError::InvalidPath`] when the workspace root does not exist or cannot be canonicalized. /// - Returns [`ToolError::PermissionDenied`] when the tool is disabled by configuration (`deny`). /// - Returns [`ToolError::InvalidPath`] when shell expansion fails (e.g., unresolvable environment variable). /// - Returns [`ToolError::InvalidPattern`] when a glob pattern is syntactically invalid. pub fn build_resolver_for_tool( + build_context: &ToolBuildContext, config: &IndexMap, tool_name: &str, - workspace_root: &Path, ) -> Result { - let workspace_root = soft_canonicalize(workspace_root).map_err(|e| { - ToolError::InvalidPath(format!( - "failed to resolve workspace root '{}': {}", - workspace_root.display(), - e - )) - })?; + let workspace_root = build_context.workspace_root().to_path_buf(); let Some(rule) = config.get(tool_name) else { // Nothing specified: default to workspace only. @@ -188,6 +182,7 @@ fn build_glob_policy( #[cfg(test)] mod tests { use super::*; + use reloaded_code_core::ToolBuildContext; use soft_canonicalize::soft_canonicalize; type TestResult = Result<(), ToolError>; @@ -202,7 +197,8 @@ mod tests { let temp = tempfile::TempDir::new().unwrap(); let config = IndexMap::new(); - let resolver = build_resolver_for_tool(&config, "read", temp.path())?; + let ctx = ToolBuildContext::new(temp.path(), None).unwrap(); + let resolver = build_resolver_for_tool(&ctx, &config, "read")?; let FileToolResolver::Allowed(inner) = &resolver else { panic!("expected Allowed, got {resolver:?}"); @@ -230,7 +226,8 @@ mod tests { PermissionRule::Action(PermissionAction::Allow), ); - let resolver = build_resolver_for_tool(&config, "read", temp.path())?; + let ctx = ToolBuildContext::new(temp.path(), None).unwrap(); + let resolver = build_resolver_for_tool(&ctx, &config, "read")?; let FileToolResolver::Allowed(inner) = &resolver else { panic!("expected Allowed, got {resolver:?}"); @@ -257,7 +254,8 @@ mod tests { let mut config = IndexMap::new(); config.insert("read".to_string(), PermissionRule::Pattern(patterns)); - let resolver = build_resolver_for_tool(&config, "read", temp.path())?; + let ctx = ToolBuildContext::new(temp.path(), None).unwrap(); + let resolver = build_resolver_for_tool(&ctx, &config, "read")?; // Any absolute path is allowed, even outside the workspace. assert!(resolver.is_path_allowed(Path::new("/etc/passwd"))); @@ -279,7 +277,8 @@ mod tests { let mut config = IndexMap::new(); config.insert("read".to_string(), PermissionRule::Pattern(patterns)); - let resolver = build_resolver_for_tool(&config, "read", temp.path())?; + let ctx = ToolBuildContext::new(temp.path(), None).unwrap(); + let resolver = build_resolver_for_tool(&ctx, &config, "read")?; let FileToolResolver::Allowed(inner) = &resolver else { panic!("expected Allowed, got {resolver:?}"); @@ -318,7 +317,8 @@ mod tests { let mut config = IndexMap::new(); config.insert("read".to_string(), PermissionRule::Pattern(patterns)); - let resolver = build_resolver_for_tool(&config, "read", temp.path())?; + let ctx = ToolBuildContext::new(temp.path(), None).unwrap(); + let resolver = build_resolver_for_tool(&ctx, &config, "read")?; assert!( matches!(resolver, FileToolResolver::Glob(_)), @@ -358,7 +358,8 @@ mod tests { let mut config = IndexMap::new(); config.insert("read".to_string(), PermissionRule::Pattern(patterns)); - let resolver = build_resolver_for_tool(&config, "read", temp.path())?; + let ctx = ToolBuildContext::new(temp.path(), None).unwrap(); + let resolver = build_resolver_for_tool(&ctx, &config, "read")?; assert!(matches!(resolver, FileToolResolver::Glob(_))); @@ -395,7 +396,8 @@ mod tests { let mut config = IndexMap::new(); config.insert("read".to_string(), PermissionRule::Pattern(patterns)); - let resolver = build_resolver_for_tool(&config, "read", temp.path())?; + let ctx = ToolBuildContext::new(temp.path(), None).unwrap(); + let resolver = build_resolver_for_tool(&ctx, &config, "read")?; assert!(matches!(resolver, FileToolResolver::Glob(_))); @@ -419,7 +421,8 @@ mod tests { let mut config = IndexMap::new(); config.insert("read".to_string(), PermissionRule::Pattern(patterns)); - let resolver = build_resolver_for_tool(&config, "read", temp.path())?; + let ctx = ToolBuildContext::new(temp.path(), None).unwrap(); + let resolver = build_resolver_for_tool(&ctx, &config, "read")?; assert!( matches!(resolver, FileToolResolver::Glob(_)), @@ -462,7 +465,8 @@ mod tests { let mut config = IndexMap::new(); config.insert("read".to_string(), PermissionRule::Pattern(patterns)); - let resolver = build_resolver_for_tool(&config, "read", temp.path())?; + let ctx = ToolBuildContext::new(temp.path(), None).unwrap(); + let resolver = build_resolver_for_tool(&ctx, &config, "read")?; assert!(matches!(resolver, FileToolResolver::Glob(_))); @@ -486,7 +490,8 @@ mod tests { let mut config = IndexMap::new(); config.insert("read".to_string(), PermissionRule::Pattern(patterns)); - let result = build_resolver_for_tool(&config, "read", temp.path()); + let ctx = ToolBuildContext::new(temp.path(), None).unwrap(); + let result = build_resolver_for_tool(&ctx, &config, "read"); assert!( result.is_err(), "unresolvable shell variable should produce an error" @@ -514,7 +519,8 @@ mod tests { let mut config = IndexMap::new(); config.insert("read".to_string(), PermissionRule::Pattern(patterns)); - let resolver = build_resolver_for_tool(&config, "read", temp.path())?; + let ctx = ToolBuildContext::new(temp.path(), None).unwrap(); + let resolver = build_resolver_for_tool(&ctx, &config, "read")?; let root = soft_canonicalize(temp.path())?; let resolved = resolver.resolve("src/lib.rs")?; @@ -547,7 +553,8 @@ mod tests { let mut config = IndexMap::new(); config.insert("read".to_string(), PermissionRule::Pattern(patterns)); - let resolver = build_resolver_for_tool(&config, "read", temp.path())?; + let ctx = ToolBuildContext::new(temp.path(), None).unwrap(); + let resolver = build_resolver_for_tool(&ctx, &config, "read")?; let workspace_root = soft_canonicalize(temp.path())?; let resolved = resolver.resolve(".")?; diff --git a/src/reloaded-code-core/src/lib.rs b/src/reloaded-code-core/src/lib.rs index 1aa1ed0..32698e0 100644 --- a/src/reloaded-code-core/src/lib.rs +++ b/src/reloaded-code-core/src/lib.rs @@ -17,6 +17,7 @@ pub mod path; pub mod permissions; pub mod permissions_ext; pub mod system_prompt; +pub mod tool_context; pub mod tool_metadata; pub mod tools; pub mod util; @@ -30,6 +31,7 @@ pub use error::{ToolError, ToolResult}; pub use output::ToolOutput; pub use path::{AbsolutePathResolver, AllowedGlobResolver, AllowedPathResolver, PathResolver}; pub use system_prompt::SystemPromptBuilder; +pub use tool_context::ToolBuildContext; pub use workspace::resolve_workspace_root; // Re-export tools (always available, sync or async based on runtime feature) diff --git a/src/reloaded-code-core/src/tool_context/mod.rs b/src/reloaded-code-core/src/tool_context/mod.rs new file mode 100644 index 0000000..dc5432a --- /dev/null +++ b/src/reloaded-code-core/src/tool_context/mod.rs @@ -0,0 +1,51 @@ +//! Tool build context module. +//! +//! Provides `ToolBuildContext` for passing common build-time information +//! when constructing tools. Create one instance before the tool construction +//! loop and pass it to each tool resolver. + +use std::path::{Path, PathBuf}; + +use crate::permissions::Ruleset; +use soft_canonicalize::soft_canonicalize; + +/// Context passed when building any tool. +/// +/// Create one instance before the tool construction loop and pass it to +/// each tool resolver. The context holds the canonicalized workspace root +/// and optional permission ruleset. +/// +/// # Canonicalization +/// +/// `workspace_root` is canonicalized once at construction time using +/// `soft_canonicalize`, enabling fail-fast error detection before the +/// tool loop. +#[derive(Debug, Clone)] +pub struct ToolBuildContext<'a> { + /// Canonicalized root directory of the workspace. + workspace_root: PathBuf, + /// Optional permission ruleset for tool access control. + pub permission: Option<&'a Ruleset>, +} + +impl<'a> ToolBuildContext<'a> { + /// Creates a new `ToolBuildContext` with a canonicalized workspace root. + /// + /// # Errors + /// Returns an error if `workspace_root` cannot be canonicalized. + pub fn new( + workspace_root: impl AsRef, + permission: Option<&'a Ruleset>, + ) -> Result { + Ok(Self { + workspace_root: soft_canonicalize(workspace_root)?, + permission, + }) + } + + /// Returns the canonicalized workspace root. + #[must_use] + pub fn workspace_root(&self) -> &Path { + &self.workspace_root + } +} diff --git a/src/reloaded-code-serdesai/src/agent_runtime/build.rs b/src/reloaded-code-serdesai/src/agent_runtime/build.rs index be705ce..75e3957 100644 --- a/src/reloaded-code-serdesai/src/agent_runtime/build.rs +++ b/src/reloaded-code-serdesai/src/agent_runtime/build.rs @@ -18,6 +18,7 @@ use reloaded_code_agents::{ ToolCatalogEntry, ToolCatalogKind, build_resolver_for_tool, }; use reloaded_code_core::permissions::Ruleset; +use reloaded_code_core::tool_context::ToolBuildContext; use reloaded_code_core::tool_metadata::{ edit as edit_meta, glob as glob_meta, grep as grep_meta, read as read_meta, webfetch as webfetch_meta, write as write_meta, @@ -25,7 +26,7 @@ use reloaded_code_core::tool_metadata::{ use reloaded_code_core::tools::{ GlobSettings, GrepFormattingSettings, GrepSettings, ReadSettings, WebFetchSettings, }; -use reloaded_code_core::{CredentialLookup, models::ModelCatalog}; +use reloaded_code_core::{CredentialLookup, ToolError, models::ModelCatalog}; use serdes_ai::AgentBuilder; use serdes_ai_models::BoxedModel; use std::path::Path; @@ -153,6 +154,17 @@ where } /// Attaches the standard runtime tools and prompt contexts without finalizing the builder. +/// +/// # Errors +/// +/// Returns [`AgentBuildError::UnsupportedToolKind`] when the runtime catalog contains an +/// unrecognized [`ToolCatalogKind`] variant (this function only handles standard tools). +/// +/// Returns [`AgentBuildError::ToolSettingsValidation`] when resolver creation or settings +/// building fails for any tool, including: +/// - [`ToolError::InvalidPath`] if the workspace root cannot be canonicalized +/// - [`ToolError::PermissionDenied`] if a tool is explicitly disabled in the permission config +/// - [`ToolError::InvalidPattern`] if a glob permission pattern is syntactically malformed pub(super) fn attach_standard_tools<'a, C>( mut builder: AgentBuilder<(), String>, prepared: &PreparedBuild<'a>, @@ -177,6 +189,13 @@ where builder = builder.top_p(top_p); } + // Create build context once before the tool construction loop. + let build_context = ToolBuildContext::new(workspace_root, prepared.permission.as_deref()) + .map_err(|e| AgentBuildError::ToolSettingsValidation { + tool: "workspace_root", + source: ToolError::InvalidPath(e.to_string()), + })?; + // Use pre-built permission ruleset from PreparedBuild for non-file tools. let permission = prepared.permission.clone(); let permission_config = &prepared.permission_config; @@ -185,7 +204,7 @@ where match entry.kind { ToolCatalogKind::Read => { let resolver = - build_resolver_for_tool(permission_config, read_meta::NAME, workspace_root) + build_resolver_for_tool(&build_context, permission_config, read_meta::NAME) .with_tool(read_meta::NAME)?; let settings = build_read_settings(&prepared.tool_settings.read)?; builder = @@ -193,19 +212,19 @@ where } ToolCatalogKind::Write => { let resolver = - build_resolver_for_tool(permission_config, write_meta::NAME, workspace_root) + build_resolver_for_tool(&build_context, permission_config, write_meta::NAME) .with_tool(write_meta::NAME)?; builder = builder.tool(prompt_builder.track(WriteTool::new(resolver))); } ToolCatalogKind::Edit => { let resolver = - build_resolver_for_tool(permission_config, edit_meta::NAME, workspace_root) + build_resolver_for_tool(&build_context, permission_config, edit_meta::NAME) .with_tool(edit_meta::NAME)?; builder = builder.tool(prompt_builder.track(EditTool::new(resolver))); } ToolCatalogKind::Glob => { let resolver = - build_resolver_for_tool(permission_config, glob_meta::NAME, workspace_root) + build_resolver_for_tool(&build_context, permission_config, glob_meta::NAME) .with_tool(glob_meta::NAME)?; let settings = build_glob_settings(&prepared.tool_settings.glob)?; builder = @@ -213,7 +232,7 @@ where } ToolCatalogKind::Grep => { let resolver = - build_resolver_for_tool(permission_config, grep_meta::NAME, workspace_root) + build_resolver_for_tool(&build_context, permission_config, grep_meta::NAME) .with_tool(grep_meta::NAME)?; let (search_settings, formatting_settings) = build_grep_settings(&prepared.tool_settings.grep)?;