ya-agent-environment 0.74.0__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.
- ya_agent_environment/__init__.py +85 -0
- ya_agent_environment/environment.py +382 -0
- ya_agent_environment/exceptions.py +69 -0
- ya_agent_environment/file_operator.py +942 -0
- ya_agent_environment/protocols.py +316 -0
- ya_agent_environment/resources.py +516 -0
- ya_agent_environment/shell.py +1067 -0
- ya_agent_environment/types.py +27 -0
- ya_agent_environment/utils.py +131 -0
- ya_agent_environment-0.74.0.dist-info/METADATA +47 -0
- ya_agent_environment-0.74.0.dist-info/RECORD +13 -0
- ya_agent_environment-0.74.0.dist-info/WHEEL +4 -0
- ya_agent_environment-0.74.0.dist-info/licenses/LICENSE +28 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Environment abstractions for file operations and shell execution.
|
|
2
|
+
|
|
3
|
+
This module provides Protocol-based interfaces and implementations for
|
|
4
|
+
environment operations, allowing different backends (local, remote, S3, SSH, etc.)
|
|
5
|
+
to be used interchangeably.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from ya_agent_environment.environment import Environment
|
|
9
|
+
from ya_agent_environment.exceptions import (
|
|
10
|
+
EnvironmentError as EnvironmentError,
|
|
11
|
+
)
|
|
12
|
+
from ya_agent_environment.exceptions import (
|
|
13
|
+
EnvironmentNotEnteredError,
|
|
14
|
+
FileOperationError,
|
|
15
|
+
PathNotAllowedError,
|
|
16
|
+
ShellExecutionError,
|
|
17
|
+
ShellTimeoutError,
|
|
18
|
+
)
|
|
19
|
+
from ya_agent_environment.file_operator import (
|
|
20
|
+
DEFAULT_INSTRUCTIONS_MAX_DEPTH,
|
|
21
|
+
DEFAULT_INSTRUCTIONS_SKIP_DIRS,
|
|
22
|
+
FileOperator,
|
|
23
|
+
LocalTmpFileOperator,
|
|
24
|
+
)
|
|
25
|
+
from ya_agent_environment.protocols import (
|
|
26
|
+
DEFAULT_CHUNK_SIZE,
|
|
27
|
+
InstructableResource,
|
|
28
|
+
Resource,
|
|
29
|
+
ResumableResource,
|
|
30
|
+
TmpFileOperator,
|
|
31
|
+
)
|
|
32
|
+
from ya_agent_environment.resources import (
|
|
33
|
+
BaseResource,
|
|
34
|
+
ResourceEntry,
|
|
35
|
+
ResourceFactory,
|
|
36
|
+
ResourceRegistry,
|
|
37
|
+
ResourceRegistryState,
|
|
38
|
+
)
|
|
39
|
+
from ya_agent_environment.shell import (
|
|
40
|
+
BackgroundProcess,
|
|
41
|
+
CompletedProcess,
|
|
42
|
+
DeferredShell,
|
|
43
|
+
ExecutionHandle,
|
|
44
|
+
OutputBuffer,
|
|
45
|
+
ReadyState,
|
|
46
|
+
Shell,
|
|
47
|
+
StdinAdapter,
|
|
48
|
+
)
|
|
49
|
+
from ya_agent_environment.types import FileStat, TruncatedResult
|
|
50
|
+
from ya_agent_environment.utils import generate_filetree
|
|
51
|
+
|
|
52
|
+
__all__ = [
|
|
53
|
+
"DEFAULT_CHUNK_SIZE",
|
|
54
|
+
"DEFAULT_INSTRUCTIONS_MAX_DEPTH",
|
|
55
|
+
"DEFAULT_INSTRUCTIONS_SKIP_DIRS",
|
|
56
|
+
"BackgroundProcess",
|
|
57
|
+
"BaseResource",
|
|
58
|
+
"CompletedProcess",
|
|
59
|
+
"DeferredShell",
|
|
60
|
+
"Environment",
|
|
61
|
+
"EnvironmentError",
|
|
62
|
+
"EnvironmentNotEnteredError",
|
|
63
|
+
"ExecutionHandle",
|
|
64
|
+
"FileOperationError",
|
|
65
|
+
"FileOperator",
|
|
66
|
+
"FileStat",
|
|
67
|
+
"InstructableResource",
|
|
68
|
+
"LocalTmpFileOperator",
|
|
69
|
+
"OutputBuffer",
|
|
70
|
+
"PathNotAllowedError",
|
|
71
|
+
"ReadyState",
|
|
72
|
+
"Resource",
|
|
73
|
+
"ResourceEntry",
|
|
74
|
+
"ResourceFactory",
|
|
75
|
+
"ResourceRegistry",
|
|
76
|
+
"ResourceRegistryState",
|
|
77
|
+
"ResumableResource",
|
|
78
|
+
"Shell",
|
|
79
|
+
"ShellExecutionError",
|
|
80
|
+
"ShellTimeoutError",
|
|
81
|
+
"StdinAdapter",
|
|
82
|
+
"TmpFileOperator",
|
|
83
|
+
"TruncatedResult",
|
|
84
|
+
"generate_filetree",
|
|
85
|
+
]
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
"""Environment abstraction for environment module.
|
|
2
|
+
|
|
3
|
+
This module provides an abstract base class for environment context managers
|
|
4
|
+
that manage the lifecycle of shared resources.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from contextlib import AbstractAsyncContextManager
|
|
10
|
+
from typing import Any, Self
|
|
11
|
+
|
|
12
|
+
from ya_agent_environment.exceptions import EnvironmentNotEnteredError
|
|
13
|
+
from ya_agent_environment.file_operator import FileOperator
|
|
14
|
+
from ya_agent_environment.resources import ResourceFactory, ResourceRegistry, ResourceRegistryState
|
|
15
|
+
from ya_agent_environment.shell import Shell
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Environment(ABC):
|
|
19
|
+
"""Abstract base class for environment context manager.
|
|
20
|
+
|
|
21
|
+
Environment manages the lifecycle of shared resources (file_operator, shell, resources)
|
|
22
|
+
that can be reused across multiple AgentContext sessions.
|
|
23
|
+
|
|
24
|
+
Subclasses should:
|
|
25
|
+
- Call super().__init__() to initialize the resource registry
|
|
26
|
+
- Implement _setup() to create file_operator, shell, and any custom resources
|
|
27
|
+
- Implement _teardown() to clean up environment-specific resources
|
|
28
|
+
- Optionally populate self._toolsets in _setup() to provide environment-specific tools
|
|
29
|
+
- NOT override __aenter__ or __aexit__ (use _setup/_teardown instead)
|
|
30
|
+
|
|
31
|
+
The base class handles:
|
|
32
|
+
- Calling _setup() in __aenter__
|
|
33
|
+
- Binding resource registry to environment (so factories can access infrastructure)
|
|
34
|
+
- Calling resources.restore_all() after _setup() for resumable resources
|
|
35
|
+
- Calling _teardown() then resources.close_all() in __aexit__
|
|
36
|
+
|
|
37
|
+
Resource Factory Pattern:
|
|
38
|
+
Factories receive the Environment instance, allowing access to infrastructure:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
async def create_browser(env: Environment) -> BrowserSession:
|
|
42
|
+
return BrowserSession(
|
|
43
|
+
file_operator=env.file_operator, # Access file system
|
|
44
|
+
shell=env.shell, # Execute commands
|
|
45
|
+
tmp_dir=env.tmp_dir, # Temporary storage
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
async with LocalEnvironment() as env:
|
|
49
|
+
env.resources.register_factory("browser", create_browser)
|
|
50
|
+
browser = await env.resources.get_or_create("browser")
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Resumable Resources:
|
|
54
|
+
Environment supports resource state persistence via ResourceRegistry.
|
|
55
|
+
Resources implementing ResumableResource can have their state exported
|
|
56
|
+
and restored across process restarts.
|
|
57
|
+
|
|
58
|
+
Example:
|
|
59
|
+
# First run
|
|
60
|
+
async with LocalEnvironment() as env:
|
|
61
|
+
env.resources.register_factory("browser", create_browser)
|
|
62
|
+
browser = await env.resources.get_or_create("browser")
|
|
63
|
+
# ... use browser ...
|
|
64
|
+
state = env.export_resource_state()
|
|
65
|
+
save_state(state)
|
|
66
|
+
|
|
67
|
+
# Subsequent run
|
|
68
|
+
state = load_state()
|
|
69
|
+
async with LocalEnvironment(
|
|
70
|
+
resource_state=state,
|
|
71
|
+
resource_factories={"browser": create_browser},
|
|
72
|
+
) as env:
|
|
73
|
+
# Browser automatically restored with previous state
|
|
74
|
+
browser = env.resources.get("browser")
|
|
75
|
+
|
|
76
|
+
Example:
|
|
77
|
+
Using AsyncExitStack (recommended for dependent contexts):
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from contextlib import AsyncExitStack
|
|
81
|
+
|
|
82
|
+
async with AsyncExitStack() as stack:
|
|
83
|
+
env = await stack.enter_async_context(
|
|
84
|
+
LocalEnvironment(allowed_paths=[Path("/workspace")])
|
|
85
|
+
)
|
|
86
|
+
ctx = await stack.enter_async_context(
|
|
87
|
+
AgentContext(env=env)
|
|
88
|
+
)
|
|
89
|
+
# Get combined toolsets from environment and resources
|
|
90
|
+
toolsets = env.get_toolsets()
|
|
91
|
+
agent = Agent(..., toolsets=[*core_toolsets, *toolsets])
|
|
92
|
+
...
|
|
93
|
+
# Resources cleaned up when stack exits
|
|
94
|
+
```
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
def __init__(
|
|
98
|
+
self,
|
|
99
|
+
resource_state: ResourceRegistryState | None = None,
|
|
100
|
+
resource_factories: dict[str, ResourceFactory] | None = None,
|
|
101
|
+
) -> None:
|
|
102
|
+
"""Initialize the environment.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
resource_state: Optional state to restore resources from.
|
|
106
|
+
Resources will be restored when entering the context.
|
|
107
|
+
resource_factories: Optional dictionary of resource factories.
|
|
108
|
+
Required for any resources in resource_state.
|
|
109
|
+
"""
|
|
110
|
+
self._resources = ResourceRegistry(
|
|
111
|
+
state=resource_state,
|
|
112
|
+
factories=resource_factories,
|
|
113
|
+
)
|
|
114
|
+
self._file_operator: FileOperator | None = None
|
|
115
|
+
self._shell: Shell | None = None
|
|
116
|
+
self._toolsets: list[Any] = []
|
|
117
|
+
self._entered: bool = False
|
|
118
|
+
self._enter_lock: asyncio.Lock = asyncio.Lock()
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def entered(self) -> bool:
|
|
122
|
+
"""Whether the environment has been entered via async context manager."""
|
|
123
|
+
return self._entered
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def file_operator(self) -> FileOperator | None:
|
|
127
|
+
"""Return the file operator, or None if not configured.
|
|
128
|
+
|
|
129
|
+
Raises:
|
|
130
|
+
EnvironmentNotEnteredError: If environment has not been entered.
|
|
131
|
+
|
|
132
|
+
Returns None when the subclass chooses not to provide a file operator.
|
|
133
|
+
"""
|
|
134
|
+
if not self._entered:
|
|
135
|
+
raise EnvironmentNotEnteredError("file_operator")
|
|
136
|
+
return self._file_operator
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def shell(self) -> Shell | None:
|
|
140
|
+
"""Return the shell, or None if not configured.
|
|
141
|
+
|
|
142
|
+
Raises:
|
|
143
|
+
EnvironmentNotEnteredError: If environment has not been entered.
|
|
144
|
+
|
|
145
|
+
Returns None when the subclass chooses not to provide a shell.
|
|
146
|
+
"""
|
|
147
|
+
if not self._entered:
|
|
148
|
+
raise EnvironmentNotEnteredError("shell")
|
|
149
|
+
return self._shell
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def resources(self) -> ResourceRegistry:
|
|
153
|
+
"""Return the resource registry for runtime resources.
|
|
154
|
+
|
|
155
|
+
Resources can be accessed by AgentContext and tools.
|
|
156
|
+
"""
|
|
157
|
+
return self._resources
|
|
158
|
+
|
|
159
|
+
def get_toolsets(self) -> list[Any]:
|
|
160
|
+
"""Return combined toolsets from environment and all resources.
|
|
161
|
+
|
|
162
|
+
Collects toolsets from:
|
|
163
|
+
1. Environment-level toolsets (self._toolsets, set in _setup())
|
|
164
|
+
2. All registered resources via ResourceRegistry.get_toolsets()
|
|
165
|
+
|
|
166
|
+
This is the recommended way to get all available toolsets.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Combined list of toolsets from environment and resources.
|
|
170
|
+
|
|
171
|
+
Example:
|
|
172
|
+
```python
|
|
173
|
+
async with MyEnvironment() as env:
|
|
174
|
+
toolsets = env.get_toolsets()
|
|
175
|
+
agent = Agent(..., toolsets=[*core_toolsets, *toolsets])
|
|
176
|
+
```
|
|
177
|
+
"""
|
|
178
|
+
toolsets = list(self._toolsets)
|
|
179
|
+
toolsets.extend(self._resources.get_toolsets())
|
|
180
|
+
return toolsets
|
|
181
|
+
|
|
182
|
+
# --- Chaining API for resource factories and state ---
|
|
183
|
+
|
|
184
|
+
def with_resource_factory(self, key: str, factory: ResourceFactory) -> "Self":
|
|
185
|
+
"""Register a resource factory. Can be chained.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
key: Unique identifier for the resource.
|
|
189
|
+
factory: Async callable that creates the resource.
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
Self for method chaining.
|
|
193
|
+
|
|
194
|
+
Example:
|
|
195
|
+
env = (LocalEnvironment()
|
|
196
|
+
.with_resource_factory("browser", create_browser)
|
|
197
|
+
.with_resource_factory("db", create_db_pool))
|
|
198
|
+
"""
|
|
199
|
+
self._resources.register_factory(key, factory)
|
|
200
|
+
return self
|
|
201
|
+
|
|
202
|
+
def with_resource_state(self, state: ResourceRegistryState | None) -> "Self":
|
|
203
|
+
"""Set resource state to restore on enter. Can be chained.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
state: State to restore from, or None to clear pending state.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
Self for method chaining.
|
|
210
|
+
|
|
211
|
+
Example:
|
|
212
|
+
state = ResourceRegistryState.model_validate_json(saved_json)
|
|
213
|
+
env = (LocalEnvironment()
|
|
214
|
+
.with_resource_factory("browser", create_browser)
|
|
215
|
+
.with_resource_state(state))
|
|
216
|
+
"""
|
|
217
|
+
if state is not None:
|
|
218
|
+
self._resources._pending_state = state
|
|
219
|
+
return self
|
|
220
|
+
|
|
221
|
+
# --- Export method ---
|
|
222
|
+
|
|
223
|
+
async def export_resource_state(self) -> ResourceRegistryState:
|
|
224
|
+
"""Export resource registry state for serialization.
|
|
225
|
+
|
|
226
|
+
Only resources implementing ResumableResource will be included.
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
ResourceRegistryState that can be serialized to JSON.
|
|
230
|
+
|
|
231
|
+
Example:
|
|
232
|
+
state = await env.export_resource_state()
|
|
233
|
+
Path("state.json").write_text(state.model_dump_json())
|
|
234
|
+
"""
|
|
235
|
+
return await self._resources.export_state()
|
|
236
|
+
|
|
237
|
+
# --- Subclass hooks ---
|
|
238
|
+
|
|
239
|
+
@abstractmethod
|
|
240
|
+
async def _setup(self) -> None:
|
|
241
|
+
"""Initialize environment resources.
|
|
242
|
+
|
|
243
|
+
Subclasses must implement this to:
|
|
244
|
+
- Optionally create and assign self._file_operator
|
|
245
|
+
- Optionally create and assign self._shell
|
|
246
|
+
- Optionally register custom resources via self._resources.set()
|
|
247
|
+
|
|
248
|
+
Both file_operator and shell may be left as None if the environment
|
|
249
|
+
does not need them. Tools should check availability before use.
|
|
250
|
+
|
|
251
|
+
This is called by __aenter__.
|
|
252
|
+
"""
|
|
253
|
+
...
|
|
254
|
+
|
|
255
|
+
@abstractmethod
|
|
256
|
+
async def _teardown(self) -> None:
|
|
257
|
+
"""Clean up environment-specific resources.
|
|
258
|
+
|
|
259
|
+
Subclasses must implement this to:
|
|
260
|
+
- Clean up tmp_dir, containers, connections, etc.
|
|
261
|
+
- Set self._file_operator = None
|
|
262
|
+
- Set self._shell = None
|
|
263
|
+
|
|
264
|
+
Note: self._resources.close_all() is called automatically after _teardown().
|
|
265
|
+
This is called by __aexit__.
|
|
266
|
+
"""
|
|
267
|
+
...
|
|
268
|
+
|
|
269
|
+
# --- Workspace isolation ---
|
|
270
|
+
|
|
271
|
+
def fork(self) -> AbstractAsyncContextManager["Self"]:
|
|
272
|
+
"""Create an isolated copy of this environment.
|
|
273
|
+
|
|
274
|
+
Returns an async context manager that yields a new Environment instance
|
|
275
|
+
with its own file_operator and shell pointing to an isolated workspace
|
|
276
|
+
directory. The forked environment starts with an empty resource registry.
|
|
277
|
+
|
|
278
|
+
Subclasses that support workspace isolation should override this method.
|
|
279
|
+
The mechanism for creating the isolated directory (e.g., git worktree,
|
|
280
|
+
file copy) is an implementation detail of each subclass.
|
|
281
|
+
|
|
282
|
+
Yields:
|
|
283
|
+
A new Environment instance with isolated workspace.
|
|
284
|
+
|
|
285
|
+
Raises:
|
|
286
|
+
NotImplementedError: If the subclass does not support forking.
|
|
287
|
+
|
|
288
|
+
Example::
|
|
289
|
+
|
|
290
|
+
async with env.fork() as forked_env:
|
|
291
|
+
# forked_env has its own workspace
|
|
292
|
+
await forked_env.shell.execute("make test")
|
|
293
|
+
# Isolated workspace is cleaned up automatically
|
|
294
|
+
"""
|
|
295
|
+
raise NotImplementedError(
|
|
296
|
+
f"{self.__class__.__name__} does not support fork(). "
|
|
297
|
+
"Subclasses must override this method to provide workspace isolation."
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
# --- Fixed lifecycle management ---
|
|
301
|
+
|
|
302
|
+
async def __aenter__(self) -> "Self":
|
|
303
|
+
"""Enter context and setup resources.
|
|
304
|
+
|
|
305
|
+
This method:
|
|
306
|
+
1. Calls _setup() to initialize file_operator, shell, etc.
|
|
307
|
+
2. Binds the resource registry to this environment
|
|
308
|
+
3. Calls resources.restore_all() to restore pending resources
|
|
309
|
+
|
|
310
|
+
Raises:
|
|
311
|
+
RuntimeError: If the environment has already been entered.
|
|
312
|
+
KeyError: If pending state references a resource without factory.
|
|
313
|
+
"""
|
|
314
|
+
async with self._enter_lock:
|
|
315
|
+
if self._entered:
|
|
316
|
+
raise RuntimeError(
|
|
317
|
+
f"{self.__class__.__name__} has already been entered. "
|
|
318
|
+
"Each Environment instance can only be entered once at a time."
|
|
319
|
+
)
|
|
320
|
+
self._entered = True
|
|
321
|
+
await self._setup()
|
|
322
|
+
|
|
323
|
+
# Bind resource registry to this environment so factories can access infrastructure
|
|
324
|
+
self._resources.bind(self)
|
|
325
|
+
|
|
326
|
+
# Restore resources from pending state (if any)
|
|
327
|
+
await self._resources.restore_all()
|
|
328
|
+
|
|
329
|
+
return self
|
|
330
|
+
|
|
331
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
332
|
+
"""Exit context and cleanup resources."""
|
|
333
|
+
try:
|
|
334
|
+
await self._teardown()
|
|
335
|
+
finally:
|
|
336
|
+
# Close file_operator and shell, then close all registered resources
|
|
337
|
+
if self._file_operator is not None:
|
|
338
|
+
await self._file_operator.close()
|
|
339
|
+
if self._shell is not None:
|
|
340
|
+
await self._shell.close()
|
|
341
|
+
await self._resources.close_all()
|
|
342
|
+
async with self._enter_lock:
|
|
343
|
+
self._entered = False
|
|
344
|
+
|
|
345
|
+
async def get_context_instructions(self) -> str:
|
|
346
|
+
"""Return combined context instructions from file_operator, shell, and resources.
|
|
347
|
+
|
|
348
|
+
Subclasses can override this to provide additional environment-specific
|
|
349
|
+
instructions. The default implementation combines file_operator, shell,
|
|
350
|
+
and resources instructions.
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
Combined XML-formatted instructions string wrapped in <environment-context> tags.
|
|
354
|
+
|
|
355
|
+
Raises:
|
|
356
|
+
EnvironmentNotEnteredError: If environment has not been entered yet.
|
|
357
|
+
"""
|
|
358
|
+
if not self._entered:
|
|
359
|
+
raise EnvironmentNotEnteredError("environment")
|
|
360
|
+
|
|
361
|
+
parts: list[str] = []
|
|
362
|
+
|
|
363
|
+
if self._file_operator is not None:
|
|
364
|
+
file_instructions = await self._file_operator.get_context_instructions()
|
|
365
|
+
if file_instructions:
|
|
366
|
+
parts.append(file_instructions)
|
|
367
|
+
|
|
368
|
+
if self._shell is not None:
|
|
369
|
+
shell_instructions = await self._shell.get_context_instructions()
|
|
370
|
+
if shell_instructions:
|
|
371
|
+
parts.append(shell_instructions)
|
|
372
|
+
|
|
373
|
+
# Collect resource instructions
|
|
374
|
+
resource_instructions = await self._resources.get_context_instructions()
|
|
375
|
+
if resource_instructions:
|
|
376
|
+
parts.append(resource_instructions)
|
|
377
|
+
|
|
378
|
+
if not parts:
|
|
379
|
+
return ""
|
|
380
|
+
|
|
381
|
+
content = "\n\n".join(parts)
|
|
382
|
+
return f"<environment-context>\n{content}\n</environment-context>"
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Environment-related exceptions."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class EnvironmentError(Exception): # noqa: A001
|
|
5
|
+
"""Base exception for environment operations."""
|
|
6
|
+
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PathNotAllowedError(EnvironmentError):
|
|
11
|
+
"""Raised when a path is outside allowed directories."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, path: str, allowed_paths: list[str] | None = None):
|
|
14
|
+
self.path = path
|
|
15
|
+
self.allowed_paths = allowed_paths or []
|
|
16
|
+
msg = f"Path '{path}' is not within allowed directories"
|
|
17
|
+
if self.allowed_paths:
|
|
18
|
+
msg += f": {', '.join(self.allowed_paths)}"
|
|
19
|
+
super().__init__(msg)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class FileOperationError(EnvironmentError):
|
|
23
|
+
"""Raised when a file operation fails."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, operation: str, path: str, reason: str | None = None):
|
|
26
|
+
self.operation = operation
|
|
27
|
+
self.path = path
|
|
28
|
+
self.reason = reason
|
|
29
|
+
msg = f"Failed to {operation} '{path}'"
|
|
30
|
+
if reason:
|
|
31
|
+
msg += f": {reason}"
|
|
32
|
+
super().__init__(msg)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ShellExecutionError(EnvironmentError):
|
|
36
|
+
"""Raised when shell command execution fails."""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
command: str,
|
|
41
|
+
exit_code: int | None = None,
|
|
42
|
+
stderr: str | None = None,
|
|
43
|
+
):
|
|
44
|
+
self.command = command
|
|
45
|
+
self.exit_code = exit_code
|
|
46
|
+
self.stderr = stderr
|
|
47
|
+
msg = f"Command failed: {command}"
|
|
48
|
+
if exit_code is not None:
|
|
49
|
+
msg += f" (exit code: {exit_code})"
|
|
50
|
+
if stderr:
|
|
51
|
+
msg += f"\n{stderr}"
|
|
52
|
+
super().__init__(msg)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ShellTimeoutError(ShellExecutionError):
|
|
56
|
+
"""Raised when shell command times out."""
|
|
57
|
+
|
|
58
|
+
def __init__(self, command: str, timeout: float):
|
|
59
|
+
self.timeout = timeout
|
|
60
|
+
super().__init__(command)
|
|
61
|
+
self.args = (f"Command timed out after {timeout}s: {command}",)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class EnvironmentNotEnteredError(EnvironmentError):
|
|
65
|
+
"""Raised when accessing resources before entering the environment context."""
|
|
66
|
+
|
|
67
|
+
def __init__(self, resource: str):
|
|
68
|
+
self.resource = resource
|
|
69
|
+
super().__init__(f"Environment not entered. Use 'async with environment:' before accessing {resource}.")
|