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