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
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
"""Async Sandbox SDK Client for interacting with the Sandbox service.
|
|
2
|
+
|
|
3
|
+
This module provides an asynchronous version of SandboxClient using grpc.aio.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from datetime import timedelta
|
|
7
|
+
from functools import wraps
|
|
8
|
+
from typing import AsyncIterator, Callable, Dict, List, Optional, TypeVar, Union
|
|
9
|
+
|
|
10
|
+
import grpc
|
|
11
|
+
import grpc.aio
|
|
12
|
+
from google.protobuf.duration_pb2 import Duration
|
|
13
|
+
|
|
14
|
+
from .._gen import sandbox_pb2, sandbox_pb2_grpc
|
|
15
|
+
from .._gen import codebase_pb2, codebase_pb2_grpc
|
|
16
|
+
from .._gen import common_pb2
|
|
17
|
+
from ..types import (
|
|
18
|
+
Codebase,
|
|
19
|
+
ExecResult,
|
|
20
|
+
FileInfo,
|
|
21
|
+
Permission,
|
|
22
|
+
PatternType,
|
|
23
|
+
PermissionRule,
|
|
24
|
+
ResourceLimits,
|
|
25
|
+
RuntimeType,
|
|
26
|
+
Sandbox,
|
|
27
|
+
SandboxStatus,
|
|
28
|
+
Session,
|
|
29
|
+
SessionStatus,
|
|
30
|
+
UploadResult,
|
|
31
|
+
)
|
|
32
|
+
from ..exceptions import (
|
|
33
|
+
SandboxError,
|
|
34
|
+
SandboxNotFoundError,
|
|
35
|
+
CodebaseNotFoundError,
|
|
36
|
+
CommandTimeoutError,
|
|
37
|
+
ConnectionError,
|
|
38
|
+
PermissionDeniedError,
|
|
39
|
+
SessionNotFoundError,
|
|
40
|
+
from_grpc_error,
|
|
41
|
+
)
|
|
42
|
+
from .._shared import (
|
|
43
|
+
permission_to_proto,
|
|
44
|
+
pattern_type_to_proto,
|
|
45
|
+
runtime_type_to_proto,
|
|
46
|
+
resource_limits_to_proto,
|
|
47
|
+
duration_to_timedelta,
|
|
48
|
+
proto_to_sandbox,
|
|
49
|
+
proto_to_codebase,
|
|
50
|
+
proto_to_file_info,
|
|
51
|
+
proto_to_session,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Type variable for decorator
|
|
55
|
+
T = TypeVar("T")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _handle_grpc_errors_async(context: str = "") -> Callable:
|
|
59
|
+
"""Decorator to convert gRPC errors to SDK exceptions (async version).
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
context: Description of the operation for error messages.
|
|
63
|
+
"""
|
|
64
|
+
def decorator(func: Callable[..., T]) -> Callable[..., T]:
|
|
65
|
+
@wraps(func)
|
|
66
|
+
async def wrapper(*args, **kwargs) -> T:
|
|
67
|
+
try:
|
|
68
|
+
return await func(*args, **kwargs)
|
|
69
|
+
except grpc.aio.AioRpcError as e:
|
|
70
|
+
raise from_grpc_error(e, context)
|
|
71
|
+
return wrapper
|
|
72
|
+
return decorator
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class AsyncSessionWrapper:
|
|
76
|
+
"""Async wrapper for a shell session with context manager support.
|
|
77
|
+
|
|
78
|
+
A session maintains a persistent shell process that preserves
|
|
79
|
+
working directory, environment variables, and background processes.
|
|
80
|
+
|
|
81
|
+
Example:
|
|
82
|
+
>>> async with sandbox.session() as session:
|
|
83
|
+
... await session.exec("cd /workspace")
|
|
84
|
+
... await session.exec("npm install")
|
|
85
|
+
... result = await session.exec("npm test")
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
def __init__(self, client: "AsyncSandboxClient", session: Session):
|
|
89
|
+
"""Initialize the AsyncSessionWrapper.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
client: The AsyncSandboxClient instance.
|
|
93
|
+
session: The Session object.
|
|
94
|
+
"""
|
|
95
|
+
self._client = client
|
|
96
|
+
self._session = session
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def id(self) -> str:
|
|
100
|
+
"""Get the session ID."""
|
|
101
|
+
return self._session.id
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def sandbox_id(self) -> str:
|
|
105
|
+
"""Get the sandbox ID."""
|
|
106
|
+
return self._session.sandbox_id
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def status(self) -> SessionStatus:
|
|
110
|
+
"""Get the session status."""
|
|
111
|
+
return self._session.status
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def shell(self) -> str:
|
|
115
|
+
"""Get the shell binary path."""
|
|
116
|
+
return self._session.shell
|
|
117
|
+
|
|
118
|
+
async def exec(
|
|
119
|
+
self,
|
|
120
|
+
command: str,
|
|
121
|
+
timeout: Optional[timedelta] = None,
|
|
122
|
+
) -> ExecResult:
|
|
123
|
+
"""Execute a command in the session.
|
|
124
|
+
|
|
125
|
+
The command runs in the context of the persistent shell,
|
|
126
|
+
so working directory and environment changes persist.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
command: The command to execute.
|
|
130
|
+
timeout: Optional timeout duration.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
The ExecResult with stdout, stderr, and exit code.
|
|
134
|
+
"""
|
|
135
|
+
return await self._client.session_exec(self.id, command, timeout)
|
|
136
|
+
|
|
137
|
+
async def close(self):
|
|
138
|
+
"""Close the session and clean up all child processes."""
|
|
139
|
+
await self._client.destroy_session(self.id)
|
|
140
|
+
|
|
141
|
+
async def __aenter__(self) -> "AsyncSessionWrapper":
|
|
142
|
+
"""Enter async context manager."""
|
|
143
|
+
return self
|
|
144
|
+
|
|
145
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
146
|
+
"""Exit async context manager, closing the session."""
|
|
147
|
+
await self.close()
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class AsyncSandboxClient:
|
|
151
|
+
"""Async client for interacting with the Sandbox service.
|
|
152
|
+
|
|
153
|
+
Example:
|
|
154
|
+
>>> client = AsyncSandboxClient(endpoint="localhost:9000")
|
|
155
|
+
>>> sandbox = await client.create_sandbox(
|
|
156
|
+
... codebase_id="cb_123",
|
|
157
|
+
... permissions=[
|
|
158
|
+
... {"pattern": "/docs/**", "permission": "write"},
|
|
159
|
+
... {"pattern": "**/*.py", "permission": "read"},
|
|
160
|
+
... ]
|
|
161
|
+
... )
|
|
162
|
+
>>> await client.start_sandbox(sandbox.id)
|
|
163
|
+
>>> result = await client.exec(sandbox.id, command="ls -la /workspace")
|
|
164
|
+
>>> print(result.stdout)
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
def __init__(self, endpoint: str = "localhost:9000", secure: bool = False):
|
|
168
|
+
"""Initialize the AsyncSandboxClient.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
endpoint: The gRPC server endpoint (host:port).
|
|
172
|
+
secure: Whether to use TLS for the connection.
|
|
173
|
+
"""
|
|
174
|
+
if secure:
|
|
175
|
+
self._channel = grpc.aio.secure_channel(endpoint, grpc.ssl_channel_credentials())
|
|
176
|
+
else:
|
|
177
|
+
self._channel = grpc.aio.insecure_channel(endpoint)
|
|
178
|
+
|
|
179
|
+
self._sandbox_stub = sandbox_pb2_grpc.SandboxServiceStub(self._channel)
|
|
180
|
+
self._codebase_stub = codebase_pb2_grpc.CodebaseServiceStub(self._channel)
|
|
181
|
+
|
|
182
|
+
async def close(self):
|
|
183
|
+
"""Close the gRPC channel."""
|
|
184
|
+
await self._channel.close()
|
|
185
|
+
|
|
186
|
+
async def __aenter__(self):
|
|
187
|
+
return self
|
|
188
|
+
|
|
189
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
190
|
+
await self.close()
|
|
191
|
+
|
|
192
|
+
# ============================================
|
|
193
|
+
# Sandbox Operations
|
|
194
|
+
# ============================================
|
|
195
|
+
|
|
196
|
+
@_handle_grpc_errors_async("create sandbox")
|
|
197
|
+
async def create_sandbox(
|
|
198
|
+
self,
|
|
199
|
+
codebase_id: str,
|
|
200
|
+
permissions: Optional[List[Union[PermissionRule, Dict]]] = None,
|
|
201
|
+
labels: Optional[Dict[str, str]] = None,
|
|
202
|
+
expires_in: Optional[timedelta] = None,
|
|
203
|
+
runtime: RuntimeType = RuntimeType.BWRAP,
|
|
204
|
+
image: Optional[str] = None,
|
|
205
|
+
resources: Optional[ResourceLimits] = None,
|
|
206
|
+
) -> Sandbox:
|
|
207
|
+
"""Create a new sandbox.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
codebase_id: The ID of the codebase to use.
|
|
211
|
+
permissions: List of permission rules (PermissionRule or dict).
|
|
212
|
+
labels: Optional labels for the sandbox.
|
|
213
|
+
expires_in: Optional expiration duration.
|
|
214
|
+
runtime: Runtime type (bwrap or docker). Default is bwrap.
|
|
215
|
+
image: Docker image to use (required for docker runtime).
|
|
216
|
+
resources: Resource limits (memory, CPU, processes).
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
The created Sandbox object.
|
|
220
|
+
"""
|
|
221
|
+
# Convert permissions to protobuf
|
|
222
|
+
pb_permissions = []
|
|
223
|
+
if permissions:
|
|
224
|
+
for p in permissions:
|
|
225
|
+
if isinstance(p, PermissionRule):
|
|
226
|
+
pb_permissions.append(common_pb2.PermissionRule(
|
|
227
|
+
pattern=p.pattern,
|
|
228
|
+
permission=permission_to_proto(p.permission),
|
|
229
|
+
type=pattern_type_to_proto(p.type),
|
|
230
|
+
priority=p.priority,
|
|
231
|
+
))
|
|
232
|
+
elif isinstance(p, dict):
|
|
233
|
+
perm = Permission(p.get("permission", "read"))
|
|
234
|
+
ptype = PatternType(p.get("type", "glob"))
|
|
235
|
+
pb_permissions.append(common_pb2.PermissionRule(
|
|
236
|
+
pattern=p["pattern"],
|
|
237
|
+
permission=permission_to_proto(perm),
|
|
238
|
+
type=pattern_type_to_proto(ptype),
|
|
239
|
+
priority=int(p.get("priority", 0)),
|
|
240
|
+
))
|
|
241
|
+
|
|
242
|
+
# Build request
|
|
243
|
+
request = sandbox_pb2.CreateSandboxRequest(
|
|
244
|
+
codebase_id=codebase_id,
|
|
245
|
+
permissions=pb_permissions,
|
|
246
|
+
labels=labels or {},
|
|
247
|
+
runtime=runtime_type_to_proto(runtime),
|
|
248
|
+
image=image or "",
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
if expires_in:
|
|
252
|
+
request.expires_in.CopyFrom(Duration(
|
|
253
|
+
seconds=int(expires_in.total_seconds()),
|
|
254
|
+
nanos=int((expires_in.total_seconds() % 1) * 1e9),
|
|
255
|
+
))
|
|
256
|
+
|
|
257
|
+
if resources:
|
|
258
|
+
pb_resources = resource_limits_to_proto(resources)
|
|
259
|
+
if pb_resources:
|
|
260
|
+
request.resources.CopyFrom(pb_resources)
|
|
261
|
+
|
|
262
|
+
response = await self._sandbox_stub.CreateSandbox(request)
|
|
263
|
+
return proto_to_sandbox(response)
|
|
264
|
+
|
|
265
|
+
@_handle_grpc_errors_async("get sandbox")
|
|
266
|
+
async def get_sandbox(self, sandbox_id: str) -> Sandbox:
|
|
267
|
+
"""Get information about a sandbox.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
sandbox_id: The ID of the sandbox.
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
The Sandbox object.
|
|
274
|
+
"""
|
|
275
|
+
request = sandbox_pb2.GetSandboxRequest(sandbox_id=sandbox_id)
|
|
276
|
+
response = await self._sandbox_stub.GetSandbox(request)
|
|
277
|
+
return proto_to_sandbox(response)
|
|
278
|
+
|
|
279
|
+
@_handle_grpc_errors_async("list sandboxes")
|
|
280
|
+
async def list_sandboxes(self, codebase_id: Optional[str] = None) -> List[Sandbox]:
|
|
281
|
+
"""List all sandboxes.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
codebase_id: Optional filter by codebase ID.
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
List of Sandbox objects.
|
|
288
|
+
"""
|
|
289
|
+
request = sandbox_pb2.ListSandboxesRequest(codebase_id=codebase_id or "")
|
|
290
|
+
response = await self._sandbox_stub.ListSandboxes(request)
|
|
291
|
+
return [proto_to_sandbox(sb) for sb in response.sandboxes]
|
|
292
|
+
|
|
293
|
+
@_handle_grpc_errors_async("start sandbox")
|
|
294
|
+
async def start_sandbox(self, sandbox_id: str) -> Sandbox:
|
|
295
|
+
"""Start a pending sandbox.
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
sandbox_id: The ID of the sandbox to start.
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
The updated Sandbox object.
|
|
302
|
+
"""
|
|
303
|
+
request = sandbox_pb2.StartSandboxRequest(sandbox_id=sandbox_id)
|
|
304
|
+
response = await self._sandbox_stub.StartSandbox(request)
|
|
305
|
+
return proto_to_sandbox(response)
|
|
306
|
+
|
|
307
|
+
@_handle_grpc_errors_async("stop sandbox")
|
|
308
|
+
async def stop_sandbox(self, sandbox_id: str) -> Sandbox:
|
|
309
|
+
"""Stop a running sandbox.
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
sandbox_id: The ID of the sandbox to stop.
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
The updated Sandbox object.
|
|
316
|
+
"""
|
|
317
|
+
request = sandbox_pb2.StopSandboxRequest(sandbox_id=sandbox_id)
|
|
318
|
+
response = await self._sandbox_stub.StopSandbox(request)
|
|
319
|
+
return proto_to_sandbox(response)
|
|
320
|
+
|
|
321
|
+
@_handle_grpc_errors_async("destroy sandbox")
|
|
322
|
+
async def destroy_sandbox(self, sandbox_id: str) -> None:
|
|
323
|
+
"""Destroy a sandbox and release resources.
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
sandbox_id: The ID of the sandbox to destroy.
|
|
327
|
+
"""
|
|
328
|
+
request = sandbox_pb2.DestroySandboxRequest(sandbox_id=sandbox_id)
|
|
329
|
+
await self._sandbox_stub.DestroySandbox(request)
|
|
330
|
+
|
|
331
|
+
@_handle_grpc_errors_async("execute command")
|
|
332
|
+
async def exec(
|
|
333
|
+
self,
|
|
334
|
+
sandbox_id: str,
|
|
335
|
+
command: str,
|
|
336
|
+
stdin: Optional[str] = None,
|
|
337
|
+
env: Optional[Dict[str, str]] = None,
|
|
338
|
+
workdir: Optional[str] = None,
|
|
339
|
+
timeout: Optional[timedelta] = None,
|
|
340
|
+
) -> ExecResult:
|
|
341
|
+
"""Execute a command in a sandbox.
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
sandbox_id: The ID of the sandbox.
|
|
345
|
+
command: The command to execute.
|
|
346
|
+
stdin: Optional stdin input.
|
|
347
|
+
env: Optional environment variables.
|
|
348
|
+
workdir: Optional working directory.
|
|
349
|
+
timeout: Optional timeout duration.
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
The ExecResult with stdout, stderr, and exit code.
|
|
353
|
+
"""
|
|
354
|
+
request = sandbox_pb2.ExecRequest(
|
|
355
|
+
sandbox_id=sandbox_id,
|
|
356
|
+
command=command,
|
|
357
|
+
stdin=stdin or "",
|
|
358
|
+
env=env or {},
|
|
359
|
+
workdir=workdir or "",
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
if timeout:
|
|
363
|
+
request.timeout.CopyFrom(Duration(
|
|
364
|
+
seconds=int(timeout.total_seconds()),
|
|
365
|
+
nanos=int((timeout.total_seconds() % 1) * 1e9),
|
|
366
|
+
))
|
|
367
|
+
|
|
368
|
+
response = await self._sandbox_stub.Exec(request)
|
|
369
|
+
return ExecResult(
|
|
370
|
+
stdout=response.stdout,
|
|
371
|
+
stderr=response.stderr,
|
|
372
|
+
exit_code=response.exit_code,
|
|
373
|
+
duration=duration_to_timedelta(response.duration),
|
|
374
|
+
command=command,
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
async def exec_stream(
|
|
378
|
+
self,
|
|
379
|
+
sandbox_id: str,
|
|
380
|
+
command: str,
|
|
381
|
+
stdin: Optional[str] = None,
|
|
382
|
+
env: Optional[Dict[str, str]] = None,
|
|
383
|
+
workdir: Optional[str] = None,
|
|
384
|
+
timeout: Optional[timedelta] = None,
|
|
385
|
+
) -> AsyncIterator[bytes]:
|
|
386
|
+
"""Execute a command and stream the output.
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
sandbox_id: The ID of the sandbox.
|
|
390
|
+
command: The command to execute.
|
|
391
|
+
stdin: Optional stdin input.
|
|
392
|
+
env: Optional environment variables.
|
|
393
|
+
workdir: Optional working directory.
|
|
394
|
+
timeout: Optional timeout duration.
|
|
395
|
+
|
|
396
|
+
Yields:
|
|
397
|
+
Chunks of output data.
|
|
398
|
+
"""
|
|
399
|
+
request = sandbox_pb2.ExecRequest(
|
|
400
|
+
sandbox_id=sandbox_id,
|
|
401
|
+
command=command,
|
|
402
|
+
stdin=stdin or "",
|
|
403
|
+
env=env or {},
|
|
404
|
+
workdir=workdir or "",
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
if timeout:
|
|
408
|
+
request.timeout.CopyFrom(Duration(
|
|
409
|
+
seconds=int(timeout.total_seconds()),
|
|
410
|
+
nanos=int((timeout.total_seconds() % 1) * 1e9),
|
|
411
|
+
))
|
|
412
|
+
|
|
413
|
+
try:
|
|
414
|
+
async for response in self._sandbox_stub.ExecStream(request):
|
|
415
|
+
yield response.data
|
|
416
|
+
except grpc.aio.AioRpcError as e:
|
|
417
|
+
raise from_grpc_error(e, "execute command (stream)")
|
|
418
|
+
|
|
419
|
+
# ============================================
|
|
420
|
+
# Session Operations
|
|
421
|
+
# ============================================
|
|
422
|
+
|
|
423
|
+
@_handle_grpc_errors_async("create session")
|
|
424
|
+
async def create_session(
|
|
425
|
+
self,
|
|
426
|
+
sandbox_id: str,
|
|
427
|
+
shell: str = "/bin/sh",
|
|
428
|
+
env: Optional[Dict[str, str]] = None,
|
|
429
|
+
) -> AsyncSessionWrapper:
|
|
430
|
+
"""Create a new shell session within a sandbox.
|
|
431
|
+
|
|
432
|
+
Args:
|
|
433
|
+
sandbox_id: The ID of the sandbox.
|
|
434
|
+
shell: The shell binary to use (default: /bin/sh).
|
|
435
|
+
env: Optional initial environment variables.
|
|
436
|
+
|
|
437
|
+
Returns:
|
|
438
|
+
An AsyncSessionWrapper object for the new session.
|
|
439
|
+
"""
|
|
440
|
+
request = sandbox_pb2.CreateSessionRequest(
|
|
441
|
+
sandbox_id=sandbox_id,
|
|
442
|
+
shell=shell,
|
|
443
|
+
env=env or {},
|
|
444
|
+
)
|
|
445
|
+
response = await self._sandbox_stub.CreateSession(request)
|
|
446
|
+
session = proto_to_session(response)
|
|
447
|
+
return AsyncSessionWrapper(self, session)
|
|
448
|
+
|
|
449
|
+
async def session(
|
|
450
|
+
self,
|
|
451
|
+
sandbox_id: str,
|
|
452
|
+
shell: str = "/bin/sh",
|
|
453
|
+
env: Optional[Dict[str, str]] = None,
|
|
454
|
+
) -> AsyncSessionWrapper:
|
|
455
|
+
"""Create a session with context manager support (alias for create_session).
|
|
456
|
+
|
|
457
|
+
Example:
|
|
458
|
+
>>> async with client.session(sandbox_id) as session:
|
|
459
|
+
... await session.exec("cd /workspace")
|
|
460
|
+
... await session.exec("npm install")
|
|
461
|
+
"""
|
|
462
|
+
return await self.create_session(sandbox_id, shell, env)
|
|
463
|
+
|
|
464
|
+
@_handle_grpc_errors_async("get session")
|
|
465
|
+
async def get_session(self, session_id: str) -> Session:
|
|
466
|
+
"""Get information about a session.
|
|
467
|
+
|
|
468
|
+
Args:
|
|
469
|
+
session_id: The ID of the session.
|
|
470
|
+
|
|
471
|
+
Returns:
|
|
472
|
+
The Session object.
|
|
473
|
+
"""
|
|
474
|
+
request = sandbox_pb2.GetSessionRequest(session_id=session_id)
|
|
475
|
+
response = await self._sandbox_stub.GetSession(request)
|
|
476
|
+
return proto_to_session(response)
|
|
477
|
+
|
|
478
|
+
@_handle_grpc_errors_async("list sessions")
|
|
479
|
+
async def list_sessions(self, sandbox_id: str) -> List[Session]:
|
|
480
|
+
"""List all sessions for a sandbox.
|
|
481
|
+
|
|
482
|
+
Args:
|
|
483
|
+
sandbox_id: The ID of the sandbox.
|
|
484
|
+
|
|
485
|
+
Returns:
|
|
486
|
+
List of Session objects.
|
|
487
|
+
"""
|
|
488
|
+
request = sandbox_pb2.ListSessionsRequest(sandbox_id=sandbox_id)
|
|
489
|
+
response = await self._sandbox_stub.ListSessions(request)
|
|
490
|
+
return [proto_to_session(s) for s in response.sessions]
|
|
491
|
+
|
|
492
|
+
@_handle_grpc_errors_async("destroy session")
|
|
493
|
+
async def destroy_session(self, session_id: str) -> None:
|
|
494
|
+
"""Destroy a session and clean up all child processes.
|
|
495
|
+
|
|
496
|
+
Args:
|
|
497
|
+
session_id: The ID of the session to destroy.
|
|
498
|
+
"""
|
|
499
|
+
request = sandbox_pb2.DestroySessionRequest(session_id=session_id)
|
|
500
|
+
await self._sandbox_stub.DestroySession(request)
|
|
501
|
+
|
|
502
|
+
@_handle_grpc_errors_async("session exec")
|
|
503
|
+
async def session_exec(
|
|
504
|
+
self,
|
|
505
|
+
session_id: str,
|
|
506
|
+
command: str,
|
|
507
|
+
timeout: Optional[timedelta] = None,
|
|
508
|
+
) -> ExecResult:
|
|
509
|
+
"""Execute a command within a session (stateful).
|
|
510
|
+
|
|
511
|
+
Args:
|
|
512
|
+
session_id: The ID of the session.
|
|
513
|
+
command: The command to execute.
|
|
514
|
+
timeout: Optional timeout duration.
|
|
515
|
+
|
|
516
|
+
Returns:
|
|
517
|
+
The ExecResult with stdout, stderr, and exit code.
|
|
518
|
+
"""
|
|
519
|
+
request = sandbox_pb2.SessionExecRequest(
|
|
520
|
+
session_id=session_id,
|
|
521
|
+
command=command,
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
if timeout:
|
|
525
|
+
request.timeout.CopyFrom(Duration(
|
|
526
|
+
seconds=int(timeout.total_seconds()),
|
|
527
|
+
nanos=int((timeout.total_seconds() % 1) * 1e9),
|
|
528
|
+
))
|
|
529
|
+
|
|
530
|
+
response = await self._sandbox_stub.SessionExec(request)
|
|
531
|
+
return ExecResult(
|
|
532
|
+
stdout=response.stdout,
|
|
533
|
+
stderr=response.stderr,
|
|
534
|
+
exit_code=response.exit_code,
|
|
535
|
+
duration=duration_to_timedelta(response.duration),
|
|
536
|
+
command=command,
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
# ============================================
|
|
540
|
+
# Codebase Operations
|
|
541
|
+
# ============================================
|
|
542
|
+
|
|
543
|
+
@_handle_grpc_errors_async("create codebase")
|
|
544
|
+
async def create_codebase(self, name: str, owner_id: str) -> Codebase:
|
|
545
|
+
"""Create a new codebase.
|
|
546
|
+
|
|
547
|
+
Args:
|
|
548
|
+
name: The name of the codebase.
|
|
549
|
+
owner_id: The ID of the owner.
|
|
550
|
+
|
|
551
|
+
Returns:
|
|
552
|
+
The created Codebase object.
|
|
553
|
+
"""
|
|
554
|
+
request = codebase_pb2.CreateCodebaseRequest(name=name, owner_id=owner_id)
|
|
555
|
+
response = await self._codebase_stub.CreateCodebase(request)
|
|
556
|
+
return proto_to_codebase(response)
|
|
557
|
+
|
|
558
|
+
@_handle_grpc_errors_async("get codebase")
|
|
559
|
+
async def get_codebase(self, codebase_id: str) -> Codebase:
|
|
560
|
+
"""Get information about a codebase.
|
|
561
|
+
|
|
562
|
+
Args:
|
|
563
|
+
codebase_id: The ID of the codebase.
|
|
564
|
+
|
|
565
|
+
Returns:
|
|
566
|
+
The Codebase object.
|
|
567
|
+
"""
|
|
568
|
+
request = codebase_pb2.GetCodebaseRequest(codebase_id=codebase_id)
|
|
569
|
+
response = await self._codebase_stub.GetCodebase(request)
|
|
570
|
+
return proto_to_codebase(response)
|
|
571
|
+
|
|
572
|
+
@_handle_grpc_errors_async("list codebases")
|
|
573
|
+
async def list_codebases(self, owner_id: Optional[str] = None) -> List[Codebase]:
|
|
574
|
+
"""List all codebases.
|
|
575
|
+
|
|
576
|
+
Args:
|
|
577
|
+
owner_id: Optional filter by owner ID.
|
|
578
|
+
|
|
579
|
+
Returns:
|
|
580
|
+
List of Codebase objects.
|
|
581
|
+
"""
|
|
582
|
+
request = codebase_pb2.ListCodebasesRequest(owner_id=owner_id or "")
|
|
583
|
+
response = await self._codebase_stub.ListCodebases(request)
|
|
584
|
+
return [proto_to_codebase(cb) for cb in response.codebases]
|
|
585
|
+
|
|
586
|
+
@_handle_grpc_errors_async("delete codebase")
|
|
587
|
+
async def delete_codebase(self, codebase_id: str) -> None:
|
|
588
|
+
"""Delete a codebase.
|
|
589
|
+
|
|
590
|
+
Args:
|
|
591
|
+
codebase_id: The ID of the codebase to delete.
|
|
592
|
+
"""
|
|
593
|
+
request = codebase_pb2.DeleteCodebaseRequest(codebase_id=codebase_id)
|
|
594
|
+
await self._codebase_stub.DeleteCodebase(request)
|
|
595
|
+
|
|
596
|
+
@_handle_grpc_errors_async("upload file")
|
|
597
|
+
async def upload_file(
|
|
598
|
+
self,
|
|
599
|
+
codebase_id: str,
|
|
600
|
+
file_path: str,
|
|
601
|
+
content: bytes,
|
|
602
|
+
chunk_size: int = 64 * 1024,
|
|
603
|
+
) -> UploadResult:
|
|
604
|
+
"""Upload a file to a codebase.
|
|
605
|
+
|
|
606
|
+
Args:
|
|
607
|
+
codebase_id: The ID of the codebase.
|
|
608
|
+
file_path: The path where the file should be stored.
|
|
609
|
+
content: The file content as bytes.
|
|
610
|
+
chunk_size: Size of upload chunks (default 64KB).
|
|
611
|
+
|
|
612
|
+
Returns:
|
|
613
|
+
The UploadResult with file info.
|
|
614
|
+
"""
|
|
615
|
+
async def generate_chunks():
|
|
616
|
+
# First send metadata
|
|
617
|
+
yield codebase_pb2.UploadChunk(
|
|
618
|
+
metadata=codebase_pb2.UploadChunk.Metadata(
|
|
619
|
+
codebase_id=codebase_id,
|
|
620
|
+
file_path=file_path,
|
|
621
|
+
total_size=len(content),
|
|
622
|
+
)
|
|
623
|
+
)
|
|
624
|
+
# Then send data chunks
|
|
625
|
+
for i in range(0, len(content), chunk_size):
|
|
626
|
+
yield codebase_pb2.UploadChunk(data=content[i:i + chunk_size])
|
|
627
|
+
|
|
628
|
+
response = await self._codebase_stub.UploadFiles(generate_chunks())
|
|
629
|
+
return UploadResult(
|
|
630
|
+
codebase_id=response.codebase_id,
|
|
631
|
+
file_path=response.file_path,
|
|
632
|
+
size=response.size,
|
|
633
|
+
checksum=response.checksum,
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
@_handle_grpc_errors_async("download file")
|
|
637
|
+
async def download_file(self, codebase_id: str, file_path: str) -> bytes:
|
|
638
|
+
"""Download a file from a codebase.
|
|
639
|
+
|
|
640
|
+
Args:
|
|
641
|
+
codebase_id: The ID of the codebase.
|
|
642
|
+
file_path: The path of the file to download.
|
|
643
|
+
|
|
644
|
+
Returns:
|
|
645
|
+
The file content as bytes.
|
|
646
|
+
"""
|
|
647
|
+
request = codebase_pb2.DownloadFileRequest(
|
|
648
|
+
codebase_id=codebase_id,
|
|
649
|
+
file_path=file_path,
|
|
650
|
+
)
|
|
651
|
+
chunks = []
|
|
652
|
+
async for response in self._codebase_stub.DownloadFile(request):
|
|
653
|
+
chunks.append(response.data)
|
|
654
|
+
return b"".join(chunks)
|
|
655
|
+
|
|
656
|
+
@_handle_grpc_errors_async("list files")
|
|
657
|
+
async def list_files(
|
|
658
|
+
self,
|
|
659
|
+
codebase_id: str,
|
|
660
|
+
path: str = "",
|
|
661
|
+
recursive: bool = False,
|
|
662
|
+
) -> List[FileInfo]:
|
|
663
|
+
"""List files in a codebase directory.
|
|
664
|
+
|
|
665
|
+
Args:
|
|
666
|
+
codebase_id: The ID of the codebase.
|
|
667
|
+
path: The directory path (empty for root).
|
|
668
|
+
recursive: Whether to list recursively.
|
|
669
|
+
|
|
670
|
+
Returns:
|
|
671
|
+
List of FileInfo objects.
|
|
672
|
+
"""
|
|
673
|
+
request = codebase_pb2.ListFilesRequest(
|
|
674
|
+
codebase_id=codebase_id,
|
|
675
|
+
path=path,
|
|
676
|
+
recursive=recursive,
|
|
677
|
+
)
|
|
678
|
+
response = await self._codebase_stub.ListFiles(request)
|
|
679
|
+
return [proto_to_file_info(f) for f in response.files]
|