claude_agent/tools/
context.rs

1//! Execution context for tool operations.
2
3use std::collections::HashMap;
4use std::path::Path;
5use std::sync::Arc;
6
7use crate::hooks::{HookContext, HookEvent, HookInput, HookManager};
8use crate::permissions::{PermissionResult, ToolLimits};
9use crate::security::bash::{BashAnalysis, SanitizedEnv};
10use crate::security::fs::SecureFileHandle;
11use crate::security::guard::SecurityGuard;
12use crate::security::path::SafePath;
13use crate::security::sandbox::{DomainCheck, SandboxResult};
14use crate::security::{ResourceLimits, SecurityContext, SecurityError};
15
16#[derive(Clone)]
17pub struct ExecutionContext {
18    security: Arc<SecurityContext>,
19    hooks: Option<HookManager>,
20    session_id: Option<String>,
21}
22
23impl ExecutionContext {
24    pub fn new(security: SecurityContext) -> Self {
25        Self {
26            security: Arc::new(security),
27            hooks: None,
28            session_id: None,
29        }
30    }
31
32    pub fn from_path(root: impl AsRef<Path>) -> Result<Self, SecurityError> {
33        let security = SecurityContext::new(root)?;
34        Ok(Self::new(security))
35    }
36
37    pub fn permissive() -> Self {
38        Self {
39            security: Arc::new(SecurityContext::permissive()),
40            hooks: None,
41            session_id: None,
42        }
43    }
44
45    pub fn with_hooks(mut self, hooks: HookManager, session_id: impl Into<String>) -> Self {
46        self.hooks = Some(hooks);
47        self.session_id = Some(session_id.into());
48        self
49    }
50
51    pub fn session_id(&self) -> Option<&str> {
52        self.session_id.as_deref()
53    }
54
55    pub async fn fire_hook(&self, event: HookEvent, input: HookInput) {
56        if let Some(ref hooks) = self.hooks {
57            let context =
58                HookContext::new(input.session_id.clone()).with_cwd(self.root().to_path_buf());
59            if let Err(e) = hooks.execute(event, input, &context).await {
60                tracing::warn!(error = %e, "Hook execution failed");
61            }
62        }
63    }
64
65    pub fn root(&self) -> &Path {
66        self.security.root()
67    }
68
69    pub fn limits_for(&self, tool_name: &str) -> ToolLimits {
70        self.security
71            .policy
72            .permission
73            .get_limits(tool_name)
74            .cloned()
75            .unwrap_or_default()
76    }
77
78    pub fn resolve(&self, input: &str) -> Result<SafePath, SecurityError> {
79        self.security.fs.resolve(input)
80    }
81
82    pub fn resolve_with_limits(
83        &self,
84        input: &str,
85        limits: &ToolLimits,
86    ) -> Result<SafePath, SecurityError> {
87        self.security.fs.resolve_with_limits(input, limits)
88    }
89
90    pub fn resolve_for(&self, tool_name: &str, path: &str) -> Result<SafePath, SecurityError> {
91        let limits = self.limits_for(tool_name);
92        self.resolve_with_limits(path, &limits)
93    }
94
95    pub fn try_resolve_for(
96        &self,
97        tool_name: &str,
98        path: &str,
99    ) -> Result<SafePath, crate::types::ToolResult> {
100        self.resolve_for(tool_name, path)
101            .map_err(|e| crate::types::ToolResult::error(e.to_string()))
102    }
103
104    pub fn try_resolve_or_root_for(
105        &self,
106        tool_name: &str,
107        path: Option<&str>,
108    ) -> Result<std::path::PathBuf, crate::types::ToolResult> {
109        let limits = self.limits_for(tool_name);
110        self.resolve_or_root(path, &limits)
111            .map_err(|e| crate::types::ToolResult::error(e.to_string()))
112    }
113
114    pub fn resolve_or_root(
115        &self,
116        path: Option<&str>,
117        limits: &ToolLimits,
118    ) -> Result<std::path::PathBuf, SecurityError> {
119        match path {
120            Some(p) => self
121                .resolve_with_limits(p, limits)
122                .map(|sp| sp.as_path().to_path_buf()),
123            None => Ok(self.root().to_path_buf()),
124        }
125    }
126
127    pub fn open_read(&self, input: &str) -> Result<SecureFileHandle, SecurityError> {
128        self.security.fs.open_read(input)
129    }
130
131    pub fn open_write(&self, input: &str) -> Result<SecureFileHandle, SecurityError> {
132        self.security.fs.open_write(input)
133    }
134
135    pub fn is_within(&self, path: &Path) -> bool {
136        self.security.fs.is_within(path)
137    }
138
139    pub fn analyze_bash(&self, command: &str) -> BashAnalysis {
140        self.security.bash.analyze(command)
141    }
142
143    pub fn validate_bash(&self, command: &str) -> Result<BashAnalysis, String> {
144        self.security.bash.validate(command)
145    }
146
147    fn sanitized_env(&self) -> SanitizedEnv {
148        SanitizedEnv::from_current().with_working_dir(self.root())
149    }
150
151    pub fn resource_limits(&self) -> &ResourceLimits {
152        &self.security.limits
153    }
154
155    pub fn check_domain(&self, domain: &str) -> DomainCheck {
156        self.security.network.check(domain)
157    }
158
159    pub fn can_bypass_sandbox(&self) -> bool {
160        self.security.policy.can_bypass_sandbox()
161    }
162
163    pub fn is_sandboxed(&self) -> bool {
164        self.security.is_sandboxed()
165    }
166
167    pub fn should_auto_allow_bash(&self) -> bool {
168        self.security.should_auto_allow_bash()
169    }
170
171    pub fn wrap_command(&self, command: &str) -> SandboxResult<String> {
172        self.security.sandbox.wrap_command(command)
173    }
174
175    pub fn sandbox_env(&self) -> HashMap<String, String> {
176        self.security.sandbox.environment_vars()
177    }
178
179    pub fn sanitized_env_with_sandbox(&self) -> SanitizedEnv {
180        let sandbox_env = self.sandbox_env();
181        self.sanitized_env().with_vars(sandbox_env)
182    }
183
184    pub fn check_permission(&self, tool_name: &str, input: &serde_json::Value) -> PermissionResult {
185        self.security.policy.permission.check(tool_name, input)
186    }
187
188    pub fn validate_security(
189        &self,
190        tool_name: &str,
191        input: &serde_json::Value,
192    ) -> Result<(), String> {
193        SecurityGuard::validate(&self.security, tool_name, input).map_err(|e| e.to_string())
194    }
195}
196
197impl Default for ExecutionContext {
198    fn default() -> Self {
199        let security = SecurityContext::builder()
200            .build()
201            .unwrap_or_else(|_| SecurityContext::permissive());
202        Self::new(security)
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209    use tempfile::tempdir;
210
211    #[test]
212    fn test_execution_context_new() {
213        let dir = tempdir().unwrap();
214        let context = ExecutionContext::from_path(dir.path()).unwrap();
215        assert!(context.is_within(&std::fs::canonicalize(dir.path()).unwrap()));
216    }
217
218    #[test]
219    fn test_permissive_context() {
220        let context = ExecutionContext::permissive();
221        assert!(context.can_bypass_sandbox());
222    }
223
224    #[test]
225    fn test_resolve() {
226        let dir = tempdir().unwrap();
227        let root = std::fs::canonicalize(dir.path()).unwrap();
228        std::fs::write(root.join("test.txt"), "content").unwrap();
229
230        let context = ExecutionContext::from_path(&root).unwrap();
231        let path = context.resolve("test.txt").unwrap();
232        assert_eq!(path.as_path(), root.join("test.txt"));
233    }
234
235    #[test]
236    fn test_path_escape_blocked() {
237        let dir = tempdir().unwrap();
238        let context = ExecutionContext::from_path(dir.path()).unwrap();
239        let result = context.resolve("../../../etc/passwd");
240        assert!(result.is_err());
241    }
242
243    #[test]
244    fn test_analyze_bash() {
245        let context = ExecutionContext::default();
246        let analysis = context.analyze_bash("cat /etc/passwd");
247        assert!(analysis.paths.iter().any(|p| p.path == "/etc/passwd"));
248    }
249}