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.
- agentfense/__init__.py +191 -0
- agentfense/_async/__init__.py +21 -0
- agentfense/_async/client.py +679 -0
- agentfense/_async/sandbox.py +667 -0
- agentfense/_gen/__init__.py +0 -0
- agentfense/_gen/codebase_pb2.py +78 -0
- agentfense/_gen/codebase_pb2.pyi +141 -0
- agentfense/_gen/codebase_pb2_grpc.py +366 -0
- agentfense/_gen/common_pb2.py +47 -0
- agentfense/_gen/common_pb2.pyi +68 -0
- agentfense/_gen/common_pb2_grpc.py +24 -0
- agentfense/_gen/sandbox_pb2.py +123 -0
- agentfense/_gen/sandbox_pb2.pyi +255 -0
- agentfense/_gen/sandbox_pb2_grpc.py +678 -0
- agentfense/_shared.py +238 -0
- agentfense/client.py +751 -0
- agentfense/exceptions.py +333 -0
- agentfense/presets.py +192 -0
- agentfense/sandbox.py +672 -0
- agentfense/types.py +256 -0
- agentfense/utils.py +286 -0
- agentfense-0.2.1.dist-info/METADATA +378 -0
- agentfense-0.2.1.dist-info/RECORD +25 -0
- agentfense-0.2.1.dist-info/WHEEL +5 -0
- agentfense-0.2.1.dist-info/top_level.txt +1 -0
agentfense/exceptions.py
ADDED
|
@@ -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
|