vaultrun-sdk 0.2.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.
sandbox_sdk/__init__.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""VaultRun Python SDK — secure sandbox runtime for AI agents."""
|
|
2
|
+
|
|
3
|
+
from .client import (
|
|
4
|
+
Client,
|
|
5
|
+
Session,
|
|
6
|
+
Run,
|
|
7
|
+
AsyncRunResult,
|
|
8
|
+
File,
|
|
9
|
+
APIKey,
|
|
10
|
+
CreatedKey,
|
|
11
|
+
AuditLog,
|
|
12
|
+
StreamResult,
|
|
13
|
+
Organization,
|
|
14
|
+
OrgMember,
|
|
15
|
+
Snapshot,
|
|
16
|
+
SharedArtifact,
|
|
17
|
+
Image,
|
|
18
|
+
SessionStats,
|
|
19
|
+
PullStatus,
|
|
20
|
+
VaultRunError,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"Client",
|
|
25
|
+
"Session",
|
|
26
|
+
"Run",
|
|
27
|
+
"AsyncRunResult",
|
|
28
|
+
"File",
|
|
29
|
+
"APIKey",
|
|
30
|
+
"CreatedKey",
|
|
31
|
+
"AuditLog",
|
|
32
|
+
"StreamResult",
|
|
33
|
+
"Organization",
|
|
34
|
+
"OrgMember",
|
|
35
|
+
"Snapshot",
|
|
36
|
+
"SharedArtifact",
|
|
37
|
+
"Image",
|
|
38
|
+
"SessionStats",
|
|
39
|
+
"PullStatus",
|
|
40
|
+
"VaultRunError",
|
|
41
|
+
]
|
|
42
|
+
__version__ = "0.1.0"
|
sandbox_sdk/client.py
ADDED
|
@@ -0,0 +1,902 @@
|
|
|
1
|
+
"""VaultRun API client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import hmac
|
|
7
|
+
import io
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import IO, Any, Generator, Iterator, Optional
|
|
14
|
+
|
|
15
|
+
import requests
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class VaultRunError(Exception):
|
|
19
|
+
"""Raised when the VaultRun API returns an error response."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, status_code: int, message: str) -> None:
|
|
22
|
+
self.status_code = status_code
|
|
23
|
+
super().__init__(f"VaultRun API error {status_code}: {message}")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class Session:
|
|
28
|
+
id: str
|
|
29
|
+
image: str
|
|
30
|
+
status: str
|
|
31
|
+
network_enabled: bool
|
|
32
|
+
cpu_limit: float
|
|
33
|
+
memory_limit_mb: int
|
|
34
|
+
timeout_seconds: int
|
|
35
|
+
created_at: str
|
|
36
|
+
labels: dict[str, str] = field(default_factory=dict)
|
|
37
|
+
name: Optional[str] = None
|
|
38
|
+
container_id: Optional[str] = None
|
|
39
|
+
stopped_at: Optional[str] = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class Run:
|
|
44
|
+
id: str
|
|
45
|
+
session_id: str
|
|
46
|
+
command: str
|
|
47
|
+
args: list[str]
|
|
48
|
+
status: str
|
|
49
|
+
timeout_seconds: int
|
|
50
|
+
created_at: str
|
|
51
|
+
exit_code: Optional[int] = None
|
|
52
|
+
stdout: Optional[str] = None
|
|
53
|
+
stderr: Optional[str] = None
|
|
54
|
+
duration_ms: Optional[int] = None
|
|
55
|
+
output_truncated: bool = False
|
|
56
|
+
started_at: Optional[str] = None
|
|
57
|
+
finished_at: Optional[str] = None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class AsyncRunResult:
|
|
62
|
+
"""Returned by run_async — contains the pending run's ID."""
|
|
63
|
+
run_id: str
|
|
64
|
+
status: str
|
|
65
|
+
message: str
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class File:
|
|
70
|
+
id: str
|
|
71
|
+
session_id: str
|
|
72
|
+
path: str
|
|
73
|
+
size_bytes: int
|
|
74
|
+
content_type: str
|
|
75
|
+
created_at: str
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class AuditLog:
|
|
80
|
+
"""A single audit trail entry."""
|
|
81
|
+
id: str
|
|
82
|
+
actor: str
|
|
83
|
+
action: str
|
|
84
|
+
timestamp: str
|
|
85
|
+
session_id: Optional[str] = None
|
|
86
|
+
run_id: Optional[str] = None
|
|
87
|
+
metadata: Optional[dict[str, Any]] = None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass
|
|
91
|
+
class StreamResult:
|
|
92
|
+
"""Returned by :meth:`Client.stream` when the SSE stream closes."""
|
|
93
|
+
run_id: str
|
|
94
|
+
status: str
|
|
95
|
+
exit_code: Optional[int] = None
|
|
96
|
+
duration_ms: Optional[int] = None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass
|
|
100
|
+
class APIKey:
|
|
101
|
+
id: str
|
|
102
|
+
name: str
|
|
103
|
+
prefix: str
|
|
104
|
+
active: bool
|
|
105
|
+
created_at: str
|
|
106
|
+
last_used_at: Optional[str] = None
|
|
107
|
+
expires_at: Optional[str] = None
|
|
108
|
+
org_id: Optional[str] = None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass
|
|
112
|
+
class CreatedKey(APIKey):
|
|
113
|
+
key: str = ""
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass
|
|
117
|
+
class Organization:
|
|
118
|
+
"""A VaultRun team / org."""
|
|
119
|
+
id: str
|
|
120
|
+
name: str
|
|
121
|
+
slug: str
|
|
122
|
+
created_at: str
|
|
123
|
+
updated_at: str
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclass
|
|
127
|
+
class OrgMember:
|
|
128
|
+
"""An actor's membership in an org with a RBAC role."""
|
|
129
|
+
org_id: str
|
|
130
|
+
principal: str
|
|
131
|
+
role: str # "viewer" | "executor" | "admin"
|
|
132
|
+
created_at: str
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dataclass
|
|
136
|
+
class Snapshot:
|
|
137
|
+
"""A compressed workspace archive."""
|
|
138
|
+
id: str
|
|
139
|
+
session_id: str
|
|
140
|
+
name: str
|
|
141
|
+
created_by: str
|
|
142
|
+
size_bytes: int
|
|
143
|
+
created_at: str
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@dataclass
|
|
147
|
+
class SharedArtifact:
|
|
148
|
+
"""A file promoted to the shared artifact registry."""
|
|
149
|
+
id: str
|
|
150
|
+
name: str
|
|
151
|
+
size_bytes: int
|
|
152
|
+
content_type: str
|
|
153
|
+
created_by: str
|
|
154
|
+
created_at: str
|
|
155
|
+
session_id: Optional[str] = None
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@dataclass
|
|
159
|
+
class Image:
|
|
160
|
+
"""A Docker image available on the VaultRun host."""
|
|
161
|
+
id: str
|
|
162
|
+
tags: list[str]
|
|
163
|
+
size_bytes: int
|
|
164
|
+
created_at: str
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@dataclass
|
|
168
|
+
class SessionStats:
|
|
169
|
+
"""Live resource usage for a running session."""
|
|
170
|
+
session_id: str
|
|
171
|
+
cpu_percent: float
|
|
172
|
+
memory_usage_mb: float
|
|
173
|
+
memory_limit_mb: int
|
|
174
|
+
pids: int
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@dataclass
|
|
178
|
+
class PullStatus:
|
|
179
|
+
"""Result of a Docker image pull operation."""
|
|
180
|
+
image: str
|
|
181
|
+
status: str
|
|
182
|
+
message: str
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class Client:
|
|
186
|
+
"""VaultRun API client.
|
|
187
|
+
|
|
188
|
+
Example::
|
|
189
|
+
|
|
190
|
+
from sandbox_sdk import Client
|
|
191
|
+
|
|
192
|
+
client = Client("http://localhost:8080", api_key="vr_...")
|
|
193
|
+
session = client.create_session(image="python:3.12-slim")
|
|
194
|
+
client.upload_file(session.id, "script.py", open("script.py", "rb"))
|
|
195
|
+
result = client.run(session.id, command="python", args=["script.py"])
|
|
196
|
+
print(result.stdout)
|
|
197
|
+
client.delete_session(session.id)
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
def __init__(
|
|
201
|
+
self,
|
|
202
|
+
base_url: str,
|
|
203
|
+
api_key: str = "",
|
|
204
|
+
timeout: int = 120,
|
|
205
|
+
) -> None:
|
|
206
|
+
self.base_url = base_url.rstrip("/")
|
|
207
|
+
self.api_key = api_key or os.environ.get("VAULTRUN_API_KEY", "")
|
|
208
|
+
self._session = requests.Session()
|
|
209
|
+
self._session.headers["X-API-Key"] = self.api_key
|
|
210
|
+
self._timeout = timeout
|
|
211
|
+
|
|
212
|
+
# --- Sessions ---
|
|
213
|
+
|
|
214
|
+
def create_session(
|
|
215
|
+
self,
|
|
216
|
+
*,
|
|
217
|
+
name: str = "",
|
|
218
|
+
image: str = "python:3.12-slim",
|
|
219
|
+
network_enabled: bool = False,
|
|
220
|
+
cpu_limit: float = 1.0,
|
|
221
|
+
memory_limit_mb: int = 512,
|
|
222
|
+
timeout_seconds: int = 300,
|
|
223
|
+
labels: dict[str, str] | None = None,
|
|
224
|
+
) -> Session:
|
|
225
|
+
"""Create a new sandbox session."""
|
|
226
|
+
body: dict[str, Any] = {
|
|
227
|
+
"image": image,
|
|
228
|
+
"network_enabled": network_enabled,
|
|
229
|
+
"cpu_limit": cpu_limit,
|
|
230
|
+
"memory_limit_mb": memory_limit_mb,
|
|
231
|
+
"timeout_seconds": timeout_seconds,
|
|
232
|
+
}
|
|
233
|
+
if name:
|
|
234
|
+
body["name"] = name
|
|
235
|
+
if labels:
|
|
236
|
+
body["labels"] = labels
|
|
237
|
+
|
|
238
|
+
data = self._post("/api/v1/sessions", body)
|
|
239
|
+
return self._parse_session(data)
|
|
240
|
+
|
|
241
|
+
def get_session(self, session_id: str) -> Session:
|
|
242
|
+
"""Get a session by ID."""
|
|
243
|
+
return self._parse_session(self._get(f"/api/v1/sessions/{session_id}"))
|
|
244
|
+
|
|
245
|
+
def list_sessions(
|
|
246
|
+
self,
|
|
247
|
+
*,
|
|
248
|
+
page: int = 1,
|
|
249
|
+
limit: int = 20,
|
|
250
|
+
label: str = "",
|
|
251
|
+
) -> list[Session]:
|
|
252
|
+
"""List sessions, newest first. Use page/limit to paginate.
|
|
253
|
+
|
|
254
|
+
Pass label="key:value" to filter by a specific label.
|
|
255
|
+
"""
|
|
256
|
+
url = f"/api/v1/sessions?page={page}&limit={limit}"
|
|
257
|
+
if label:
|
|
258
|
+
url += f"&label={label}"
|
|
259
|
+
data = self._get(url)
|
|
260
|
+
return [self._parse_session(s) for s in data.get("sessions", [])]
|
|
261
|
+
|
|
262
|
+
def delete_session(self, session_id: str) -> None:
|
|
263
|
+
"""Delete a session and its container."""
|
|
264
|
+
self._delete(f"/api/v1/sessions/{session_id}")
|
|
265
|
+
|
|
266
|
+
def update_labels(
|
|
267
|
+
self,
|
|
268
|
+
session_id: str,
|
|
269
|
+
labels: dict[str, str],
|
|
270
|
+
) -> dict[str, str]:
|
|
271
|
+
"""Replace the label set on a session. Pass {} to clear all labels."""
|
|
272
|
+
data = self._patch(
|
|
273
|
+
f"/api/v1/sessions/{session_id}/labels",
|
|
274
|
+
{"labels": labels},
|
|
275
|
+
)
|
|
276
|
+
return data.get("labels", {})
|
|
277
|
+
|
|
278
|
+
# --- Command execution ---
|
|
279
|
+
|
|
280
|
+
def run(
|
|
281
|
+
self,
|
|
282
|
+
session_id: str,
|
|
283
|
+
*,
|
|
284
|
+
command: str,
|
|
285
|
+
args: list[str] | None = None,
|
|
286
|
+
env: dict[str, str] | None = None,
|
|
287
|
+
working_dir: str = "",
|
|
288
|
+
timeout_seconds: int = 30,
|
|
289
|
+
) -> Run:
|
|
290
|
+
"""Execute a command inside a session and return the result (blocking)."""
|
|
291
|
+
body: dict[str, Any] = {
|
|
292
|
+
"command": command,
|
|
293
|
+
"args": args or [],
|
|
294
|
+
"timeout_seconds": timeout_seconds,
|
|
295
|
+
}
|
|
296
|
+
if env:
|
|
297
|
+
body["env"] = env
|
|
298
|
+
if working_dir:
|
|
299
|
+
body["working_dir"] = working_dir
|
|
300
|
+
|
|
301
|
+
data = self._post(f"/api/v1/sessions/{session_id}/run", body)
|
|
302
|
+
return self._parse_run(data)
|
|
303
|
+
|
|
304
|
+
def run_async(
|
|
305
|
+
self,
|
|
306
|
+
session_id: str,
|
|
307
|
+
*,
|
|
308
|
+
command: str,
|
|
309
|
+
args: list[str] | None = None,
|
|
310
|
+
env: dict[str, str] | None = None,
|
|
311
|
+
working_dir: str = "",
|
|
312
|
+
timeout_seconds: int = 30,
|
|
313
|
+
callback_url: str = "",
|
|
314
|
+
) -> AsyncRunResult:
|
|
315
|
+
"""Submit a command for non-blocking (async) execution.
|
|
316
|
+
|
|
317
|
+
Returns immediately with the pending run's ID. Poll get_run() to check
|
|
318
|
+
for completion, or supply a callback_url to receive a webhook when done.
|
|
319
|
+
"""
|
|
320
|
+
body: dict[str, Any] = {
|
|
321
|
+
"command": command,
|
|
322
|
+
"args": args or [],
|
|
323
|
+
"timeout_seconds": timeout_seconds,
|
|
324
|
+
}
|
|
325
|
+
if env:
|
|
326
|
+
body["env"] = env
|
|
327
|
+
if working_dir:
|
|
328
|
+
body["working_dir"] = working_dir
|
|
329
|
+
if callback_url:
|
|
330
|
+
body["callback_url"] = callback_url
|
|
331
|
+
|
|
332
|
+
data = self._post(f"/api/v1/sessions/{session_id}/run/async", body)
|
|
333
|
+
return AsyncRunResult(
|
|
334
|
+
run_id=data.get("run_id", ""),
|
|
335
|
+
status=data.get("status", "pending"),
|
|
336
|
+
message=data.get("message", ""),
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
def get_run(self, run_id: str) -> Run:
|
|
340
|
+
"""Get a run by ID."""
|
|
341
|
+
return self._parse_run(self._get(f"/api/v1/runs/{run_id}"))
|
|
342
|
+
|
|
343
|
+
def list_runs(self, session_id: str) -> list[Run]:
|
|
344
|
+
"""List all runs for a session."""
|
|
345
|
+
data = self._get(f"/api/v1/sessions/{session_id}/runs")
|
|
346
|
+
return [self._parse_run(r) for r in data.get("runs", [])]
|
|
347
|
+
|
|
348
|
+
# --- Session stats and logs ---
|
|
349
|
+
|
|
350
|
+
def get_session_stats(self, session_id: str) -> SessionStats:
|
|
351
|
+
"""Return live CPU and memory usage for a running session."""
|
|
352
|
+
data = self._get(f"/api/v1/sessions/{session_id}/stats")
|
|
353
|
+
return SessionStats(
|
|
354
|
+
session_id=session_id,
|
|
355
|
+
cpu_percent=float(data.get("cpu_percent", 0)),
|
|
356
|
+
memory_usage_mb=float(data.get("memory_usage_mb", 0)),
|
|
357
|
+
memory_limit_mb=int(data.get("memory_limit_mb", 0)),
|
|
358
|
+
pids=int(data.get("pids", 0)),
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
def get_session_logs(self, session_id: str, *, tail: int = 100) -> str:
|
|
362
|
+
"""Return the container stdout+stderr stream as a single string.
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
session_id: target session.
|
|
366
|
+
tail: number of lines to return from the end of the log (default 100).
|
|
367
|
+
"""
|
|
368
|
+
data = self._get(f"/api/v1/sessions/{session_id}/logs?tail={tail}")
|
|
369
|
+
return data.get("logs", "")
|
|
370
|
+
|
|
371
|
+
# --- Docker image management ---
|
|
372
|
+
|
|
373
|
+
def list_images(self) -> list[Image]:
|
|
374
|
+
"""List Docker images available on the VaultRun host."""
|
|
375
|
+
data = self._get("/api/v1/images")
|
|
376
|
+
return [
|
|
377
|
+
Image(
|
|
378
|
+
id=img["id"],
|
|
379
|
+
tags=img.get("tags", []),
|
|
380
|
+
size_bytes=img.get("size_bytes", 0),
|
|
381
|
+
created_at=img.get("created_at", ""),
|
|
382
|
+
)
|
|
383
|
+
for img in data.get("images", [])
|
|
384
|
+
]
|
|
385
|
+
|
|
386
|
+
def pull_image(self, image: str) -> PullStatus:
|
|
387
|
+
"""Pull a Docker image on the VaultRun host.
|
|
388
|
+
|
|
389
|
+
This is an async operation on the server side; the response reflects
|
|
390
|
+
whether the pull was accepted, not that it has completed.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
image: Docker image reference, e.g. ``"python:3.12-slim"``.
|
|
394
|
+
"""
|
|
395
|
+
data = self._post("/api/v1/images/pull", {"image": image})
|
|
396
|
+
return PullStatus(
|
|
397
|
+
image=data.get("image", image),
|
|
398
|
+
status=data.get("status", ""),
|
|
399
|
+
message=data.get("message", ""),
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
# --- Files ---
|
|
403
|
+
|
|
404
|
+
def upload_file(
|
|
405
|
+
self,
|
|
406
|
+
session_id: str,
|
|
407
|
+
remote_path: str,
|
|
408
|
+
content: IO[bytes] | bytes | str | Path,
|
|
409
|
+
) -> File:
|
|
410
|
+
"""Upload a file to a session workspace."""
|
|
411
|
+
if isinstance(content, (str, Path)):
|
|
412
|
+
content = open(content, "rb")
|
|
413
|
+
if isinstance(content, bytes):
|
|
414
|
+
content = io.BytesIO(content)
|
|
415
|
+
|
|
416
|
+
files = {"file": (Path(remote_path).name, content)}
|
|
417
|
+
data_fields = {"path": remote_path}
|
|
418
|
+
|
|
419
|
+
resp = self._session.post(
|
|
420
|
+
self.base_url + f"/api/v1/sessions/{session_id}/files",
|
|
421
|
+
files=files,
|
|
422
|
+
data=data_fields,
|
|
423
|
+
timeout=self._timeout,
|
|
424
|
+
)
|
|
425
|
+
self._raise_for_status(resp)
|
|
426
|
+
return self._parse_file(resp.json())
|
|
427
|
+
|
|
428
|
+
def download_file(self, session_id: str, remote_path: str) -> bytes:
|
|
429
|
+
"""Download a single file from a session workspace."""
|
|
430
|
+
clean = remote_path.lstrip("/")
|
|
431
|
+
resp = self._session.get(
|
|
432
|
+
self.base_url + f"/api/v1/sessions/{session_id}/files/{clean}",
|
|
433
|
+
timeout=self._timeout,
|
|
434
|
+
)
|
|
435
|
+
self._raise_for_status(resp)
|
|
436
|
+
return resp.content
|
|
437
|
+
|
|
438
|
+
def download_workspace(self, session_id: str) -> bytes:
|
|
439
|
+
"""Download the entire session workspace as a ZIP archive."""
|
|
440
|
+
resp = self._session.get(
|
|
441
|
+
self.base_url + f"/api/v1/sessions/{session_id}/workspace.zip",
|
|
442
|
+
timeout=0, # no timeout — workspace may be large
|
|
443
|
+
)
|
|
444
|
+
self._raise_for_status(resp)
|
|
445
|
+
return resp.content
|
|
446
|
+
|
|
447
|
+
def list_files(self, session_id: str) -> list[File]:
|
|
448
|
+
"""List files in a session workspace."""
|
|
449
|
+
data = self._get(f"/api/v1/sessions/{session_id}/files")
|
|
450
|
+
return [self._parse_file(f) for f in data.get("files", [])]
|
|
451
|
+
|
|
452
|
+
# --- API key management ---
|
|
453
|
+
|
|
454
|
+
def list_keys(self) -> list[APIKey]:
|
|
455
|
+
"""List all API keys (does not reveal plaintext keys)."""
|
|
456
|
+
data = self._get("/api/v1/keys")
|
|
457
|
+
return [self._parse_api_key(k) for k in data.get("api_keys", [])]
|
|
458
|
+
|
|
459
|
+
def create_key(
|
|
460
|
+
self,
|
|
461
|
+
name: str,
|
|
462
|
+
*,
|
|
463
|
+
expires_at: Optional[datetime] = None,
|
|
464
|
+
) -> CreatedKey:
|
|
465
|
+
"""Create a new API key. The plaintext key is only available in the returned object."""
|
|
466
|
+
body: dict[str, Any] = {"name": name}
|
|
467
|
+
if expires_at is not None:
|
|
468
|
+
if expires_at.tzinfo is None:
|
|
469
|
+
expires_at = expires_at.replace(tzinfo=timezone.utc)
|
|
470
|
+
body["expires_at"] = expires_at.isoformat()
|
|
471
|
+
data = self._post("/api/v1/keys", body)
|
|
472
|
+
return self._parse_created_key(data)
|
|
473
|
+
|
|
474
|
+
def revoke_key(self, key_id: str) -> None:
|
|
475
|
+
"""Revoke an API key by ID."""
|
|
476
|
+
self._delete(f"/api/v1/keys/{key_id}")
|
|
477
|
+
|
|
478
|
+
# --- Audit ---
|
|
479
|
+
|
|
480
|
+
def list_audit_logs(
|
|
481
|
+
self,
|
|
482
|
+
session_id: Optional[str] = None,
|
|
483
|
+
limit: int = 50,
|
|
484
|
+
offset: int = 0,
|
|
485
|
+
) -> list["AuditLog"]:
|
|
486
|
+
"""Return audit log entries for the current actor.
|
|
487
|
+
|
|
488
|
+
Master key holders see all actors' entries; regular keys see only their
|
|
489
|
+
own sessions' entries. Entries are ordered newest-first.
|
|
490
|
+
|
|
491
|
+
Args:
|
|
492
|
+
session_id: restrict results to a single session (optional).
|
|
493
|
+
limit: maximum entries to return (default 50).
|
|
494
|
+
offset: pagination offset.
|
|
495
|
+
|
|
496
|
+
Returns:
|
|
497
|
+
List of :class:`AuditLog` entries.
|
|
498
|
+
"""
|
|
499
|
+
params: list[str] = []
|
|
500
|
+
if session_id:
|
|
501
|
+
params.append(f"session_id={session_id}")
|
|
502
|
+
if limit != 50:
|
|
503
|
+
params.append(f"limit={limit}")
|
|
504
|
+
if offset:
|
|
505
|
+
params.append(f"offset={offset}")
|
|
506
|
+
path = "/api/v1/audit"
|
|
507
|
+
if params:
|
|
508
|
+
path += "?" + "&".join(params)
|
|
509
|
+
data = self._get(path)
|
|
510
|
+
return [self._parse_audit_log(e) for e in data.get("audit_logs", [])]
|
|
511
|
+
|
|
512
|
+
# --- Streaming ---
|
|
513
|
+
|
|
514
|
+
def stream(
|
|
515
|
+
self,
|
|
516
|
+
session_id: str,
|
|
517
|
+
command: str,
|
|
518
|
+
args: Optional[list[str]] = None,
|
|
519
|
+
env: Optional[dict[str, str]] = None,
|
|
520
|
+
working_dir: Optional[str] = None,
|
|
521
|
+
timeout_seconds: int = 30,
|
|
522
|
+
stdout: Optional[IO[str]] = None,
|
|
523
|
+
stderr: Optional[IO[str]] = None,
|
|
524
|
+
) -> "StreamResult":
|
|
525
|
+
"""Execute a command via SSE streaming, writing output chunks as they arrive.
|
|
526
|
+
|
|
527
|
+
Unlike :meth:`run`, which buffers all output and returns when the command
|
|
528
|
+
finishes, ``stream`` writes stdout/stderr to the provided file-like objects
|
|
529
|
+
as each chunk arrives. This is useful for long-running commands where you
|
|
530
|
+
want to observe progress in real time.
|
|
531
|
+
|
|
532
|
+
Args:
|
|
533
|
+
session_id: target session.
|
|
534
|
+
command: executable name (no shell — no metacharacters).
|
|
535
|
+
args: positional arguments.
|
|
536
|
+
env: extra environment variables injected into the exec.
|
|
537
|
+
working_dir: working directory inside the container.
|
|
538
|
+
timeout_seconds: server-side execution timeout.
|
|
539
|
+
stdout: writable text stream for stdout chunks (default: discard).
|
|
540
|
+
stderr: writable text stream for stderr chunks (default: discard).
|
|
541
|
+
|
|
542
|
+
Returns:
|
|
543
|
+
:class:`StreamResult` with the final status, exit code, and duration.
|
|
544
|
+
|
|
545
|
+
Example::
|
|
546
|
+
|
|
547
|
+
import sys
|
|
548
|
+
result = client.stream(
|
|
549
|
+
session.id, "python", args=["script.py"],
|
|
550
|
+
stdout=sys.stdout, stderr=sys.stderr,
|
|
551
|
+
)
|
|
552
|
+
print(f"exit code: {result.exit_code}")
|
|
553
|
+
"""
|
|
554
|
+
body: dict[str, Any] = {
|
|
555
|
+
"command": command,
|
|
556
|
+
"args": args or [],
|
|
557
|
+
"timeout_seconds": timeout_seconds,
|
|
558
|
+
}
|
|
559
|
+
if env:
|
|
560
|
+
body["env"] = env
|
|
561
|
+
if working_dir:
|
|
562
|
+
body["working_dir"] = working_dir
|
|
563
|
+
|
|
564
|
+
resp = self._session.post(
|
|
565
|
+
self.base_url + f"/api/v1/sessions/{session_id}/run/stream",
|
|
566
|
+
json=body,
|
|
567
|
+
headers={"Accept": "text/event-stream"},
|
|
568
|
+
stream=True,
|
|
569
|
+
timeout=None, # caller controls duration via context / timeout_seconds
|
|
570
|
+
)
|
|
571
|
+
self._raise_for_status(resp)
|
|
572
|
+
|
|
573
|
+
final: dict[str, Any] = {}
|
|
574
|
+
for line in resp.iter_lines():
|
|
575
|
+
if not line:
|
|
576
|
+
continue
|
|
577
|
+
if isinstance(line, bytes):
|
|
578
|
+
line = line.decode()
|
|
579
|
+
if not line.startswith("data:"):
|
|
580
|
+
continue
|
|
581
|
+
payload = line[5:].strip()
|
|
582
|
+
if not payload:
|
|
583
|
+
continue
|
|
584
|
+
try:
|
|
585
|
+
event = json.loads(payload)
|
|
586
|
+
except ValueError:
|
|
587
|
+
continue
|
|
588
|
+
etype = event.get("type", "")
|
|
589
|
+
if etype == "stdout" and stdout is not None:
|
|
590
|
+
stdout.write(event.get("data", ""))
|
|
591
|
+
elif etype == "stderr" and stderr is not None:
|
|
592
|
+
stderr.write(event.get("data", ""))
|
|
593
|
+
elif etype == "done":
|
|
594
|
+
final = event
|
|
595
|
+
break
|
|
596
|
+
|
|
597
|
+
return StreamResult(
|
|
598
|
+
run_id=final.get("run_id", ""),
|
|
599
|
+
status=final.get("status", "unknown"),
|
|
600
|
+
exit_code=final.get("exit_code"),
|
|
601
|
+
duration_ms=final.get("duration_ms"),
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
# --- Organizations & RBAC ---
|
|
605
|
+
|
|
606
|
+
def create_org(self, name: str, slug: Optional[str] = None) -> Organization:
|
|
607
|
+
"""Create a new organization. Requires master key.
|
|
608
|
+
|
|
609
|
+
Args:
|
|
610
|
+
name: human-readable org name.
|
|
611
|
+
slug: URL-safe identifier; auto-generated from name if omitted.
|
|
612
|
+
|
|
613
|
+
Returns:
|
|
614
|
+
:class:`Organization`
|
|
615
|
+
"""
|
|
616
|
+
body: dict[str, Any] = {"name": name}
|
|
617
|
+
if slug:
|
|
618
|
+
body["slug"] = slug
|
|
619
|
+
d = self._post("/api/v1/orgs", body)
|
|
620
|
+
return self._parse_org(d)
|
|
621
|
+
|
|
622
|
+
def list_orgs(self) -> list[Organization]:
|
|
623
|
+
"""List all organizations. Requires master key."""
|
|
624
|
+
d = self._get("/api/v1/orgs")
|
|
625
|
+
return [self._parse_org(o) for o in d.get("orgs", [])]
|
|
626
|
+
|
|
627
|
+
def get_org(self, org_id: str) -> Organization:
|
|
628
|
+
"""Fetch a single org by ID. Accessible to org members."""
|
|
629
|
+
d = self._get(f"/api/v1/orgs/{org_id}")
|
|
630
|
+
return self._parse_org(d)
|
|
631
|
+
|
|
632
|
+
def delete_org(self, org_id: str) -> None:
|
|
633
|
+
"""Delete an org and all its members. Requires master key."""
|
|
634
|
+
self._delete(f"/api/v1/orgs/{org_id}")
|
|
635
|
+
|
|
636
|
+
def add_org_member(
|
|
637
|
+
self,
|
|
638
|
+
org_id: str,
|
|
639
|
+
principal: str,
|
|
640
|
+
role: str = "executor",
|
|
641
|
+
) -> OrgMember:
|
|
642
|
+
"""Add or update an org member.
|
|
643
|
+
|
|
644
|
+
Args:
|
|
645
|
+
org_id: target organization ID.
|
|
646
|
+
principal: API key name (the actor string).
|
|
647
|
+
role: one of ``"viewer"``, ``"executor"``, or ``"admin"``.
|
|
648
|
+
|
|
649
|
+
Requires master key or org admin role.
|
|
650
|
+
"""
|
|
651
|
+
body: dict[str, Any] = {"principal": principal, "role": role}
|
|
652
|
+
d = self._post(f"/api/v1/orgs/{org_id}/members", body)
|
|
653
|
+
return self._parse_org_member(d)
|
|
654
|
+
|
|
655
|
+
def list_org_members(self, org_id: str) -> list[OrgMember]:
|
|
656
|
+
"""List all members of an org. Accessible to org members."""
|
|
657
|
+
d = self._get(f"/api/v1/orgs/{org_id}/members")
|
|
658
|
+
return [self._parse_org_member(m) for m in d.get("members", [])]
|
|
659
|
+
|
|
660
|
+
def remove_org_member(self, org_id: str, principal: str) -> None:
|
|
661
|
+
"""Remove a principal from an org. Requires master key or org admin."""
|
|
662
|
+
self._delete(f"/api/v1/orgs/{org_id}/members/{principal}")
|
|
663
|
+
|
|
664
|
+
def list_org_sessions(self, org_id: str) -> list[Session]:
|
|
665
|
+
"""List active sessions that belong to the org.
|
|
666
|
+
|
|
667
|
+
The caller must be an org member; the server filters by role.
|
|
668
|
+
"""
|
|
669
|
+
d = self._get(f"/api/v1/orgs/{org_id}/sessions")
|
|
670
|
+
return [self._parse_session(s) for s in d.get("sessions", [])]
|
|
671
|
+
|
|
672
|
+
# --- Snapshots ---
|
|
673
|
+
|
|
674
|
+
def create_snapshot(self, session_id: str, *, name: str) -> Snapshot:
|
|
675
|
+
"""Create a snapshot archive of a session's workspace."""
|
|
676
|
+
data = self._post(f"/api/v1/sessions/{session_id}/snapshots", {"name": name})
|
|
677
|
+
return Snapshot(**{k: v for k, v in data.items() if k in Snapshot.__dataclass_fields__})
|
|
678
|
+
|
|
679
|
+
def list_snapshots(self, session_id: str) -> list[Snapshot]:
|
|
680
|
+
"""List all snapshots for a session."""
|
|
681
|
+
data = self._get(f"/api/v1/sessions/{session_id}/snapshots")
|
|
682
|
+
return [Snapshot(**{k: v for k, v in s.items() if k in Snapshot.__dataclass_fields__})
|
|
683
|
+
for s in data.get("snapshots", [])]
|
|
684
|
+
|
|
685
|
+
def download_snapshot(self, snapshot_id: str) -> bytes:
|
|
686
|
+
"""Download a snapshot archive as bytes."""
|
|
687
|
+
resp = self._session.get(
|
|
688
|
+
f"{self.base_url}/api/v1/snapshots/{snapshot_id}/download",
|
|
689
|
+
timeout=self._timeout,
|
|
690
|
+
)
|
|
691
|
+
self._raise_for_status(resp)
|
|
692
|
+
return resp.content
|
|
693
|
+
|
|
694
|
+
def delete_snapshot(self, snapshot_id: str) -> None:
|
|
695
|
+
"""Delete a snapshot."""
|
|
696
|
+
self._delete(f"/api/v1/snapshots/{snapshot_id}")
|
|
697
|
+
|
|
698
|
+
# --- Artifacts ---
|
|
699
|
+
|
|
700
|
+
def promote_artifact(
|
|
701
|
+
self,
|
|
702
|
+
session_id: str,
|
|
703
|
+
path: str,
|
|
704
|
+
*,
|
|
705
|
+
name: str = "",
|
|
706
|
+
) -> SharedArtifact:
|
|
707
|
+
"""Promote a workspace file to the shared artifact registry."""
|
|
708
|
+
body: dict[str, Any] = {"path": path}
|
|
709
|
+
if name:
|
|
710
|
+
body["name"] = name
|
|
711
|
+
data = self._post(f"/api/v1/sessions/{session_id}/artifacts", body)
|
|
712
|
+
return SharedArtifact(**{k: v for k, v in data.items() if k in SharedArtifact.__dataclass_fields__})
|
|
713
|
+
|
|
714
|
+
def list_artifacts(self) -> list[SharedArtifact]:
|
|
715
|
+
"""List shared artifacts visible to the caller."""
|
|
716
|
+
data = self._get("/api/v1/artifacts")
|
|
717
|
+
return [SharedArtifact(**{k: v for k, v in a.items() if k in SharedArtifact.__dataclass_fields__})
|
|
718
|
+
for a in data.get("artifacts", [])]
|
|
719
|
+
|
|
720
|
+
def download_artifact(self, artifact_id: str) -> bytes:
|
|
721
|
+
"""Download a shared artifact as bytes."""
|
|
722
|
+
resp = self._session.get(
|
|
723
|
+
f"{self.base_url}/api/v1/artifacts/{artifact_id}/download",
|
|
724
|
+
timeout=self._timeout,
|
|
725
|
+
)
|
|
726
|
+
self._raise_for_status(resp)
|
|
727
|
+
return resp.content
|
|
728
|
+
|
|
729
|
+
def delete_artifact(self, artifact_id: str) -> None:
|
|
730
|
+
"""Delete a shared artifact."""
|
|
731
|
+
self._delete(f"/api/v1/artifacts/{artifact_id}")
|
|
732
|
+
|
|
733
|
+
# --- Webhook signature verification ---
|
|
734
|
+
|
|
735
|
+
@staticmethod
|
|
736
|
+
def verify_webhook_signature(
|
|
737
|
+
payload: bytes,
|
|
738
|
+
signature_header: str,
|
|
739
|
+
secret: str,
|
|
740
|
+
) -> bool:
|
|
741
|
+
"""Verify the X-VaultRun-Signature header on a callback POST.
|
|
742
|
+
|
|
743
|
+
Args:
|
|
744
|
+
payload: raw request body bytes
|
|
745
|
+
signature_header: value of the X-VaultRun-Signature header
|
|
746
|
+
secret: the WEBHOOK_SECRET configured on the server
|
|
747
|
+
|
|
748
|
+
Returns True when the signature is valid, False otherwise.
|
|
749
|
+
|
|
750
|
+
Example (Flask)::
|
|
751
|
+
|
|
752
|
+
@app.route("/webhook", methods=["POST"])
|
|
753
|
+
def webhook():
|
|
754
|
+
if not Client.verify_webhook_signature(
|
|
755
|
+
request.data,
|
|
756
|
+
request.headers.get("X-VaultRun-Signature", ""),
|
|
757
|
+
os.environ["WEBHOOK_SECRET"],
|
|
758
|
+
):
|
|
759
|
+
abort(401)
|
|
760
|
+
...
|
|
761
|
+
"""
|
|
762
|
+
if not signature_header.startswith("sha256="):
|
|
763
|
+
return False
|
|
764
|
+
expected = signature_header[7:]
|
|
765
|
+
mac = hmac.new(secret.encode(), payload, hashlib.sha256)
|
|
766
|
+
computed = mac.hexdigest()
|
|
767
|
+
return hmac.compare_digest(computed, expected)
|
|
768
|
+
|
|
769
|
+
# --- Internal helpers ---
|
|
770
|
+
|
|
771
|
+
def _get(self, path: str) -> dict:
|
|
772
|
+
resp = self._session.get(self.base_url + path, timeout=self._timeout)
|
|
773
|
+
self._raise_for_status(resp)
|
|
774
|
+
return resp.json()
|
|
775
|
+
|
|
776
|
+
def _post(self, path: str, body: dict) -> dict:
|
|
777
|
+
resp = self._session.post(self.base_url + path, json=body, timeout=self._timeout)
|
|
778
|
+
self._raise_for_status(resp)
|
|
779
|
+
return resp.json()
|
|
780
|
+
|
|
781
|
+
def _patch(self, path: str, body: dict) -> dict:
|
|
782
|
+
resp = self._session.patch(self.base_url + path, json=body, timeout=self._timeout)
|
|
783
|
+
self._raise_for_status(resp)
|
|
784
|
+
return resp.json()
|
|
785
|
+
|
|
786
|
+
def _delete(self, path: str) -> None:
|
|
787
|
+
resp = self._session.delete(self.base_url + path, timeout=self._timeout)
|
|
788
|
+
self._raise_for_status(resp)
|
|
789
|
+
|
|
790
|
+
@staticmethod
|
|
791
|
+
def _raise_for_status(resp: requests.Response) -> None:
|
|
792
|
+
if resp.status_code >= 400:
|
|
793
|
+
try:
|
|
794
|
+
msg = resp.json().get("error", resp.text)
|
|
795
|
+
except Exception:
|
|
796
|
+
msg = resp.text
|
|
797
|
+
raise VaultRunError(resp.status_code, msg)
|
|
798
|
+
|
|
799
|
+
@staticmethod
|
|
800
|
+
def _parse_api_key(d: dict) -> APIKey:
|
|
801
|
+
return APIKey(
|
|
802
|
+
id=d["id"],
|
|
803
|
+
name=d["name"],
|
|
804
|
+
prefix=d["prefix"],
|
|
805
|
+
active=d["active"],
|
|
806
|
+
created_at=d["created_at"],
|
|
807
|
+
last_used_at=d.get("last_used_at"),
|
|
808
|
+
expires_at=d.get("expires_at"),
|
|
809
|
+
)
|
|
810
|
+
|
|
811
|
+
@staticmethod
|
|
812
|
+
def _parse_created_key(d: dict) -> CreatedKey:
|
|
813
|
+
return CreatedKey(
|
|
814
|
+
id=d["id"],
|
|
815
|
+
name=d["name"],
|
|
816
|
+
prefix=d["prefix"],
|
|
817
|
+
active=d["active"],
|
|
818
|
+
created_at=d["created_at"],
|
|
819
|
+
last_used_at=d.get("last_used_at"),
|
|
820
|
+
expires_at=d.get("expires_at"),
|
|
821
|
+
key=d["key"],
|
|
822
|
+
)
|
|
823
|
+
|
|
824
|
+
@staticmethod
|
|
825
|
+
def _parse_session(d: dict) -> Session:
|
|
826
|
+
raw_labels = d.get("labels") or {}
|
|
827
|
+
labels = {str(k): str(v) for k, v in raw_labels.items()}
|
|
828
|
+
return Session(
|
|
829
|
+
id=d["id"],
|
|
830
|
+
name=d.get("name"),
|
|
831
|
+
image=d["image"],
|
|
832
|
+
status=d["status"],
|
|
833
|
+
container_id=d.get("container_id"),
|
|
834
|
+
network_enabled=d["network_enabled"],
|
|
835
|
+
cpu_limit=d["cpu_limit"],
|
|
836
|
+
memory_limit_mb=d["memory_limit_mb"],
|
|
837
|
+
timeout_seconds=d["timeout_seconds"],
|
|
838
|
+
labels=labels,
|
|
839
|
+
created_at=d["created_at"],
|
|
840
|
+
stopped_at=d.get("stopped_at"),
|
|
841
|
+
)
|
|
842
|
+
|
|
843
|
+
@staticmethod
|
|
844
|
+
def _parse_run(d: dict) -> Run:
|
|
845
|
+
return Run(
|
|
846
|
+
id=d["id"],
|
|
847
|
+
session_id=d["session_id"],
|
|
848
|
+
command=d["command"],
|
|
849
|
+
args=d.get("args", []),
|
|
850
|
+
status=d["status"],
|
|
851
|
+
exit_code=d.get("exit_code"),
|
|
852
|
+
stdout=d.get("stdout"),
|
|
853
|
+
stderr=d.get("stderr"),
|
|
854
|
+
duration_ms=d.get("duration_ms"),
|
|
855
|
+
output_truncated=d.get("output_truncated", False),
|
|
856
|
+
timeout_seconds=d["timeout_seconds"],
|
|
857
|
+
created_at=d["created_at"],
|
|
858
|
+
started_at=d.get("started_at"),
|
|
859
|
+
finished_at=d.get("finished_at"),
|
|
860
|
+
)
|
|
861
|
+
|
|
862
|
+
@staticmethod
|
|
863
|
+
def _parse_file(d: dict) -> File:
|
|
864
|
+
return File(
|
|
865
|
+
id=d["id"],
|
|
866
|
+
session_id=d["session_id"],
|
|
867
|
+
path=d["path"],
|
|
868
|
+
size_bytes=d["size_bytes"],
|
|
869
|
+
content_type=d["content_type"],
|
|
870
|
+
created_at=d["created_at"],
|
|
871
|
+
)
|
|
872
|
+
|
|
873
|
+
@staticmethod
|
|
874
|
+
def _parse_audit_log(d: dict) -> AuditLog:
|
|
875
|
+
return AuditLog(
|
|
876
|
+
id=d["id"],
|
|
877
|
+
actor=d["actor"],
|
|
878
|
+
action=d["action"],
|
|
879
|
+
timestamp=d["timestamp"],
|
|
880
|
+
session_id=d.get("session_id"),
|
|
881
|
+
run_id=d.get("run_id"),
|
|
882
|
+
metadata=d.get("metadata"),
|
|
883
|
+
)
|
|
884
|
+
|
|
885
|
+
@staticmethod
|
|
886
|
+
def _parse_org(d: dict) -> Organization:
|
|
887
|
+
return Organization(
|
|
888
|
+
id=d["id"],
|
|
889
|
+
name=d["name"],
|
|
890
|
+
slug=d["slug"],
|
|
891
|
+
created_at=d["created_at"],
|
|
892
|
+
updated_at=d["updated_at"],
|
|
893
|
+
)
|
|
894
|
+
|
|
895
|
+
@staticmethod
|
|
896
|
+
def _parse_org_member(d: dict) -> OrgMember:
|
|
897
|
+
return OrgMember(
|
|
898
|
+
org_id=d["org_id"],
|
|
899
|
+
principal=d["principal"],
|
|
900
|
+
role=d["role"],
|
|
901
|
+
created_at=d["created_at"],
|
|
902
|
+
)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vaultrun-sdk
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Python SDK for the VaultRun secure AI agent sandbox runtime
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Project-URL: Homepage, https://vaultrun.dev
|
|
7
|
+
Project-URL: Repository, https://github.com/nickvd7/vaultrun
|
|
8
|
+
Project-URL: Issues, https://github.com/nickvd7/vaultrun/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/nickvd7/vaultrun/blob/main/CHANGELOG.md
|
|
10
|
+
Keywords: ai-agents,sandbox,mcp,docker,security,self-hosted
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Classifier: Topic :: Security
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
Requires-Dist: requests>=2.31.0
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
25
|
+
Requires-Dist: responses>=0.25; extra == "dev"
|
|
26
|
+
|
|
27
|
+
# vaultrun-sdk
|
|
28
|
+
|
|
29
|
+
Python SDK for [VaultRun](https://vaultrun.dev) — the self-hosted secure runtime for AI agents.
|
|
30
|
+
|
|
31
|
+
VaultRun lets AI agents safely execute code, query databases, call cloud APIs, and manage
|
|
32
|
+
files inside isolated Docker sandboxes running on your own infrastructure. This package is
|
|
33
|
+
the typed Python client for the VaultRun REST API.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install vaultrun-sdk
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Quickstart
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from sandbox_sdk import Client
|
|
45
|
+
|
|
46
|
+
client = Client("http://localhost:8080", api_key="vr_...")
|
|
47
|
+
|
|
48
|
+
session = client.create_session(image="python:3.12-slim", memory_limit_mb=256)
|
|
49
|
+
client.upload_file(session.id, "script.py", open("script.py", "rb"))
|
|
50
|
+
|
|
51
|
+
result = client.run(session.id, command="python", args=["script.py"])
|
|
52
|
+
print(result.stdout)
|
|
53
|
+
|
|
54
|
+
client.delete_session(session.id)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Features
|
|
58
|
+
|
|
59
|
+
- Sessions: create, list, get, delete isolated sandbox sessions
|
|
60
|
+
- Runs: execute commands, stream output, fetch run details and logs
|
|
61
|
+
- Files: upload, list, and download workspace files
|
|
62
|
+
- Keys, organizations, audit logs, snapshots, artifacts, images, and session stats
|
|
63
|
+
|
|
64
|
+
## Requirements
|
|
65
|
+
|
|
66
|
+
- Python 3.10+
|
|
67
|
+
- A running [VaultRun](https://github.com/nickvd7/vaultrun) instance and an API key (`vr_...`)
|
|
68
|
+
|
|
69
|
+
## Links
|
|
70
|
+
|
|
71
|
+
- Website: https://vaultrun.dev
|
|
72
|
+
- Source & docs: https://github.com/nickvd7/vaultrun
|
|
73
|
+
- Issues: https://github.com/nickvd7/vaultrun/issues
|
|
74
|
+
|
|
75
|
+
## License
|
|
76
|
+
|
|
77
|
+
Apache 2.0
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
sandbox_sdk/__init__.py,sha256=7QVYUm3uT10HJXTJzmmMC54z4nbkijbWRnG2vNmo4VM,674
|
|
2
|
+
sandbox_sdk/client.py,sha256=CW6uqOWzND8_XXUDfVd1JD-Sibi_FPYE4gockth7VZ0,28417
|
|
3
|
+
vaultrun_sdk-0.2.0.dist-info/METADATA,sha256=YRW4XEEm2neS1qkv2EULeiGBdcoDcZzRzcJFCckxaQc,2492
|
|
4
|
+
vaultrun_sdk-0.2.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
5
|
+
vaultrun_sdk-0.2.0.dist-info/top_level.txt,sha256=ykxgSok0elqp8YNIoo8NP6xBZKrC3bKbjIScaN4-l_o,12
|
|
6
|
+
vaultrun_sdk-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
sandbox_sdk
|