agentfense 0.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,333 @@
1
+ """Exception classes for the Sandbox SDK.
2
+
3
+ This module defines a hierarchy of exceptions that provide meaningful
4
+ error messages and context for SDK operations, replacing raw gRPC exceptions.
5
+ """
6
+
7
+ from typing import Optional
8
+
9
+
10
+ class SandboxError(Exception):
11
+ """Base exception for all Sandbox SDK errors.
12
+
13
+ All SDK-specific exceptions inherit from this class, making it easy
14
+ to catch any SDK error with a single except clause.
15
+
16
+ Attributes:
17
+ message: Human-readable error description.
18
+ details: Optional additional error context.
19
+ grpc_code: Original gRPC status code if applicable.
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ message: str,
25
+ details: Optional[str] = None,
26
+ grpc_code: Optional[int] = None,
27
+ ):
28
+ self.message = message
29
+ self.details = details
30
+ self.grpc_code = grpc_code
31
+ super().__init__(self._format_message())
32
+
33
+ def _format_message(self) -> str:
34
+ msg = self.message
35
+ if self.details:
36
+ msg += f" Details: {self.details}"
37
+ return msg
38
+
39
+
40
+ class ConnectionError(SandboxError):
41
+ """Failed to connect to the sandbox service.
42
+
43
+ Raised when the SDK cannot establish a connection to the gRPC server.
44
+ This could be due to network issues, server being down, or incorrect endpoint.
45
+ """
46
+
47
+ def __init__(
48
+ self,
49
+ endpoint: str,
50
+ details: Optional[str] = None,
51
+ grpc_code: Optional[int] = None,
52
+ ):
53
+ self.endpoint = endpoint
54
+ super().__init__(
55
+ f"Failed to connect to sandbox service at '{endpoint}'",
56
+ details=details,
57
+ grpc_code=grpc_code,
58
+ )
59
+
60
+
61
+ class SandboxNotFoundError(SandboxError):
62
+ """The specified sandbox does not exist.
63
+
64
+ Raised when attempting to access a sandbox that doesn't exist or has been deleted.
65
+ """
66
+
67
+ def __init__(self, sandbox_id: str, details: Optional[str] = None):
68
+ self.sandbox_id = sandbox_id
69
+ super().__init__(
70
+ f"Sandbox '{sandbox_id}' not found",
71
+ details=details,
72
+ )
73
+
74
+
75
+ class SandboxNotRunningError(SandboxError):
76
+ """The sandbox is not in a running state.
77
+
78
+ Raised when attempting to execute commands or create sessions
79
+ on a sandbox that hasn't been started or has been stopped.
80
+ """
81
+
82
+ def __init__(
83
+ self,
84
+ sandbox_id: str,
85
+ current_status: str,
86
+ details: Optional[str] = None,
87
+ ):
88
+ self.sandbox_id = sandbox_id
89
+ self.current_status = current_status
90
+ super().__init__(
91
+ f"Sandbox '{sandbox_id}' is not running (current status: {current_status})",
92
+ details=details,
93
+ )
94
+
95
+
96
+ class CodebaseError(SandboxError):
97
+ """Error related to codebase operations.
98
+
99
+ Raised for errors during codebase creation, file upload/download, etc.
100
+ """
101
+ pass
102
+
103
+
104
+ class CodebaseNotFoundError(CodebaseError):
105
+ """The specified codebase does not exist.
106
+
107
+ Raised when attempting to access a codebase that doesn't exist or has been deleted.
108
+ """
109
+
110
+ def __init__(self, codebase_id: str, details: Optional[str] = None):
111
+ self.codebase_id = codebase_id
112
+ super().__init__(
113
+ f"Codebase '{codebase_id}' not found",
114
+ details=details,
115
+ )
116
+
117
+
118
+ class FileNotFoundError(CodebaseError):
119
+ """The specified file does not exist in the codebase.
120
+
121
+ Raised when attempting to download or access a file that doesn't exist.
122
+ """
123
+
124
+ def __init__(
125
+ self,
126
+ codebase_id: str,
127
+ file_path: str,
128
+ details: Optional[str] = None,
129
+ ):
130
+ self.codebase_id = codebase_id
131
+ self.file_path = file_path
132
+ super().__init__(
133
+ f"File '{file_path}' not found in codebase '{codebase_id}'",
134
+ details=details,
135
+ )
136
+
137
+
138
+ class CommandTimeoutError(SandboxError):
139
+ """Command execution timed out.
140
+
141
+ Raised when a command takes longer than the specified timeout duration.
142
+ The command may still be running in the sandbox.
143
+ """
144
+
145
+ def __init__(
146
+ self,
147
+ command: str,
148
+ timeout_seconds: float,
149
+ details: Optional[str] = None,
150
+ ):
151
+ self.command = command
152
+ self.timeout_seconds = timeout_seconds
153
+ super().__init__(
154
+ f"Command timed out after {timeout_seconds}s: {command[:100]}...",
155
+ details=details,
156
+ )
157
+
158
+
159
+ class CommandExecutionError(SandboxError):
160
+ """Command execution failed.
161
+
162
+ Raised when a command exits with a non-zero status code and
163
+ raise_on_error is enabled.
164
+ """
165
+
166
+ def __init__(
167
+ self,
168
+ command: str,
169
+ exit_code: int,
170
+ stdout: str = "",
171
+ stderr: str = "",
172
+ ):
173
+ self.command = command
174
+ self.exit_code = exit_code
175
+ self.stdout = stdout
176
+ self.stderr = stderr
177
+
178
+ # Build error message
179
+ details = None
180
+ if stderr:
181
+ details = stderr[:500]
182
+ elif stdout:
183
+ details = stdout[:500]
184
+
185
+ super().__init__(
186
+ f"Command failed with exit code {exit_code}: {command[:100]}",
187
+ details=details,
188
+ )
189
+
190
+
191
+ class PermissionDeniedError(SandboxError):
192
+ """Permission denied for the requested operation.
193
+
194
+ Raised when the sandbox's permission rules prevent access to a file or operation.
195
+ """
196
+
197
+ def __init__(
198
+ self,
199
+ operation: str,
200
+ path: Optional[str] = None,
201
+ details: Optional[str] = None,
202
+ ):
203
+ self.operation = operation
204
+ self.path = path
205
+
206
+ if path:
207
+ message = f"Permission denied for {operation} on '{path}'"
208
+ else:
209
+ message = f"Permission denied for {operation}"
210
+
211
+ super().__init__(message, details=details)
212
+
213
+
214
+ class SessionError(SandboxError):
215
+ """Error related to session operations."""
216
+ pass
217
+
218
+
219
+ class SessionNotFoundError(SessionError):
220
+ """The specified session does not exist.
221
+
222
+ Raised when attempting to access a session that doesn't exist or has been closed.
223
+ """
224
+
225
+ def __init__(self, session_id: str, details: Optional[str] = None):
226
+ self.session_id = session_id
227
+ super().__init__(
228
+ f"Session '{session_id}' not found",
229
+ details=details,
230
+ )
231
+
232
+
233
+ class SessionClosedError(SessionError):
234
+ """The session has been closed.
235
+
236
+ Raised when attempting to execute commands in a closed session.
237
+ """
238
+
239
+ def __init__(self, session_id: str, details: Optional[str] = None):
240
+ self.session_id = session_id
241
+ super().__init__(
242
+ f"Session '{session_id}' is closed",
243
+ details=details,
244
+ )
245
+
246
+
247
+ class ResourceLimitExceededError(SandboxError):
248
+ """Resource limit exceeded.
249
+
250
+ Raised when a sandbox exceeds its allocated resources (memory, CPU, etc.).
251
+ """
252
+
253
+ def __init__(
254
+ self,
255
+ resource_type: str,
256
+ limit: str,
257
+ details: Optional[str] = None,
258
+ ):
259
+ self.resource_type = resource_type
260
+ self.limit = limit
261
+ super().__init__(
262
+ f"Resource limit exceeded: {resource_type} limit is {limit}",
263
+ details=details,
264
+ )
265
+
266
+
267
+ class InvalidConfigurationError(SandboxError):
268
+ """Invalid configuration provided.
269
+
270
+ Raised when the provided configuration is invalid or inconsistent.
271
+ """
272
+ pass
273
+
274
+
275
+ class UploadError(CodebaseError):
276
+ """File upload failed.
277
+
278
+ Raised when uploading a file to a codebase fails.
279
+ """
280
+
281
+ def __init__(
282
+ self,
283
+ file_path: str,
284
+ codebase_id: str,
285
+ details: Optional[str] = None,
286
+ ):
287
+ self.file_path = file_path
288
+ self.codebase_id = codebase_id
289
+ super().__init__(
290
+ f"Failed to upload '{file_path}' to codebase '{codebase_id}'",
291
+ details=details,
292
+ )
293
+
294
+
295
+ def from_grpc_error(error, context: Optional[str] = None) -> SandboxError:
296
+ """Convert a gRPC error to an appropriate SandboxError.
297
+
298
+ Args:
299
+ error: The gRPC RpcError exception.
300
+ context: Optional context about the operation that failed.
301
+
302
+ Returns:
303
+ An appropriate SandboxError subclass.
304
+ """
305
+ import grpc
306
+
307
+ code = error.code() if hasattr(error, 'code') else None
308
+ details = error.details() if hasattr(error, 'details') else str(error)
309
+
310
+ if context:
311
+ details = f"{context}. {details}"
312
+
313
+ grpc_code = code.value if code else None
314
+
315
+ # Map gRPC codes to SDK exceptions
316
+ if code == grpc.StatusCode.NOT_FOUND:
317
+ return SandboxError(f"Resource not found", details=details, grpc_code=grpc_code)
318
+ elif code == grpc.StatusCode.PERMISSION_DENIED:
319
+ return PermissionDeniedError("operation", details=details)
320
+ elif code == grpc.StatusCode.DEADLINE_EXCEEDED:
321
+ return CommandTimeoutError("unknown", 0, details=details)
322
+ elif code == grpc.StatusCode.UNAVAILABLE:
323
+ return ConnectionError("unknown", details=details, grpc_code=grpc_code)
324
+ elif code == grpc.StatusCode.INVALID_ARGUMENT:
325
+ return InvalidConfigurationError(details or "Invalid argument")
326
+ elif code == grpc.StatusCode.RESOURCE_EXHAUSTED:
327
+ return ResourceLimitExceededError("unknown", "unknown", details=details)
328
+ else:
329
+ return SandboxError(
330
+ f"Operation failed",
331
+ details=details,
332
+ grpc_code=grpc_code,
333
+ )
agentfense/presets.py ADDED
@@ -0,0 +1,192 @@
1
+ """Preset permission templates for common use cases.
2
+
3
+ This module provides pre-defined permission configurations that cover
4
+ common scenarios when working with AI agents and sandboxed environments.
5
+ """
6
+
7
+ from typing import Dict, List, Optional, Union
8
+
9
+ from .types import Permission, PatternType, PermissionRule
10
+
11
+
12
+ # Public constants for preset names (better autocomplete than raw strings)
13
+ PRESET_AGENT_SAFE = "agent-safe"
14
+ PRESET_READ_ONLY = "read-only"
15
+ PRESET_FULL_ACCESS = "full-access"
16
+ PRESET_DEVELOPMENT = "development"
17
+ PRESET_VIEW_ONLY = "view-only"
18
+
19
+
20
+ # Pre-defined permission templates
21
+ PRESETS: Dict[str, List[Dict[str, str]]] = {
22
+ # Safe preset for AI agents: read most files, write to output, hide secrets
23
+ PRESET_AGENT_SAFE: [
24
+ {"pattern": "**/*", "permission": "read", "priority": "0"},
25
+ {"pattern": "/output/**", "permission": "write", "priority": "10"},
26
+ {"pattern": "/tmp/**", "permission": "write", "priority": "10"},
27
+ {"pattern": "**/.env*", "permission": "none", "priority": "100"},
28
+ {"pattern": "**/secrets/**", "permission": "none", "priority": "100"},
29
+ {"pattern": "**/*.key", "permission": "none", "priority": "100"},
30
+ {"pattern": "**/*.pem", "permission": "none", "priority": "100"},
31
+ {"pattern": "**/credentials*", "permission": "none", "priority": "100"},
32
+ {"pattern": "**/.git/**", "permission": "none", "priority": "50"},
33
+ ],
34
+ # Read-only access to all files
35
+ PRESET_READ_ONLY: [
36
+ {"pattern": "**/*", "permission": "read", "priority": "0"},
37
+ ],
38
+ # Full write access to all files
39
+ PRESET_FULL_ACCESS: [
40
+ {"pattern": "**/*", "permission": "write", "priority": "0"},
41
+ ],
42
+ # Development preset: full access except secrets
43
+ PRESET_DEVELOPMENT: [
44
+ {"pattern": "**/*", "permission": "write", "priority": "0"},
45
+ {"pattern": "**/.env*", "permission": "none", "priority": "100"},
46
+ {"pattern": "**/secrets/**", "permission": "none", "priority": "100"},
47
+ {"pattern": "**/*.key", "permission": "none", "priority": "100"},
48
+ {"pattern": "**/*.pem", "permission": "none", "priority": "100"},
49
+ ],
50
+ # View-only: can see file names but not read content
51
+ PRESET_VIEW_ONLY: [
52
+ {"pattern": "**/*", "permission": "view", "priority": "0"},
53
+ ],
54
+ }
55
+
56
+
57
+ def get_preset(name: str) -> List[PermissionRule]:
58
+ """Get a preset permission configuration by name.
59
+
60
+ Args:
61
+ name: The name of the preset (e.g., "agent-safe", "read-only").
62
+
63
+ Returns:
64
+ List of PermissionRule objects for the preset.
65
+
66
+ Raises:
67
+ ValueError: If the preset name is not found.
68
+
69
+ Example:
70
+ >>> rules = get_preset("agent-safe")
71
+ >>> for rule in rules:
72
+ ... print(f"{rule.pattern}: {rule.permission.value}")
73
+ """
74
+ if name not in PRESETS:
75
+ available = ", ".join(sorted(PRESETS.keys()))
76
+ raise ValueError(f"Unknown preset '{name}'. Available presets: {available}")
77
+
78
+ return [
79
+ PermissionRule(
80
+ pattern=rule["pattern"],
81
+ permission=Permission(rule["permission"]),
82
+ type=PatternType(rule.get("type", "glob")),
83
+ priority=int(rule.get("priority", 0)),
84
+ )
85
+ for rule in PRESETS[name]
86
+ ]
87
+
88
+
89
+ def get_preset_dicts(name: str) -> List[Dict[str, str]]:
90
+ """Get a preset as a list of dictionaries (for direct API use).
91
+
92
+ Args:
93
+ name: The name of the preset.
94
+
95
+ Returns:
96
+ List of permission rule dictionaries.
97
+
98
+ Raises:
99
+ ValueError: If the preset name is not found.
100
+ """
101
+ if name not in PRESETS:
102
+ available = ", ".join(sorted(PRESETS.keys()))
103
+ raise ValueError(f"Unknown preset '{name}'. Available presets: {available}")
104
+
105
+ return PRESETS[name].copy()
106
+
107
+
108
+ def extend_preset(
109
+ base: str,
110
+ additions: Optional[List[Union[PermissionRule, Dict]]] = None,
111
+ overrides: Optional[List[Union[PermissionRule, Dict]]] = None,
112
+ ) -> List[PermissionRule]:
113
+ """Extend a preset with additional or overriding rules.
114
+
115
+ Args:
116
+ base: The name of the base preset to extend.
117
+ additions: Additional rules to append (lower priority than base).
118
+ overrides: Rules to add with higher priority (override base rules).
119
+
120
+ Returns:
121
+ Combined list of PermissionRule objects.
122
+
123
+ Example:
124
+ >>> # Allow write access to /workspace in addition to agent-safe rules
125
+ >>> rules = extend_preset(
126
+ ... "agent-safe",
127
+ ... additions=[{"pattern": "/workspace/**", "permission": "write"}]
128
+ ... )
129
+ """
130
+ rules = get_preset(base)
131
+
132
+ def to_rule(item: Union[PermissionRule, Dict]) -> PermissionRule:
133
+ if isinstance(item, PermissionRule):
134
+ return item
135
+ return PermissionRule(
136
+ pattern=item["pattern"],
137
+ permission=Permission(item["permission"]),
138
+ type=PatternType(item.get("type", "glob")),
139
+ priority=int(item.get("priority", 0)),
140
+ )
141
+
142
+ if additions:
143
+ for item in additions:
144
+ rules.append(to_rule(item))
145
+
146
+ if overrides:
147
+ # Add overrides with boosted priority
148
+ max_priority = max(r.priority for r in rules) if rules else 0
149
+ for item in overrides:
150
+ rule = to_rule(item)
151
+ # Ensure override has higher priority
152
+ if rule.priority <= max_priority:
153
+ rule = PermissionRule(
154
+ pattern=rule.pattern,
155
+ permission=rule.permission,
156
+ type=rule.type,
157
+ priority=max_priority + 100,
158
+ )
159
+ rules.append(rule)
160
+
161
+ return rules
162
+
163
+
164
+ def list_presets() -> List[str]:
165
+ """List all available preset names.
166
+
167
+ Returns:
168
+ Sorted list of preset names.
169
+ """
170
+ return sorted(PRESETS.keys())
171
+
172
+
173
+ def register_preset(name: str, rules: List[Dict[str, str]]) -> None:
174
+ """Register a custom preset.
175
+
176
+ Args:
177
+ name: The name for the new preset.
178
+ rules: List of permission rule dictionaries.
179
+
180
+ Raises:
181
+ ValueError: If a preset with the name already exists.
182
+
183
+ Example:
184
+ >>> register_preset("my-custom", [
185
+ ... {"pattern": "**/*", "permission": "read"},
186
+ ... {"pattern": "/src/**", "permission": "write"},
187
+ ... ])
188
+ """
189
+ if name in PRESETS:
190
+ raise ValueError(f"Preset '{name}' already exists. Use a different name.")
191
+
192
+ PRESETS[name] = rules