opencomputer-sdk 0.6.0__tar.gz → 0.6.2__tar.gz
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.
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/PKG-INFO +1 -1
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/opencomputer/__init__.py +5 -1
- opencomputer_sdk-0.6.2/opencomputer/mounts.py +124 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/opencomputer/sandbox.py +99 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/pyproject.toml +1 -1
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/.gitignore +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/README.md +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/examples/run_all_tests.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/examples/stream_demo.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/examples/test_commands.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/examples/test_concurrent.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/examples/test_declarative_images.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/examples/test_default_template.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/examples/test_disk_size.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/examples/test_domain_tls.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/examples/test_environment.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/examples/test_exec.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/examples/test_file_ops.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/examples/test_large_files.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/examples/test_multi_template.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/examples/test_python_sdk.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/examples/test_reconnect.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/examples/test_secret_store_fork.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/examples/test_secretstore.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/examples/test_shell.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/examples/test_timeout.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/opencomputer/agent.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/opencomputer/commands.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/opencomputer/exec.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/opencomputer/filesystem.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/opencomputer/image.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/opencomputer/project.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/opencomputer/pty.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/opencomputer/shell.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/opencomputer/snapshot.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/opencomputer/sse.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/opencomputer/template.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/opencomputer/usage.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: opencomputer-sdk
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.2
|
|
4
4
|
Summary: Python SDK for OpenComputer - cloud sandbox platform
|
|
5
5
|
Project-URL: Homepage, https://github.com/diggerhq/opensandbox
|
|
6
6
|
Project-URL: Repository, https://github.com/diggerhq/opensandbox
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"""OpenComputer Python SDK - cloud sandbox platform."""
|
|
2
2
|
|
|
3
|
-
from opencomputer.sandbox import Sandbox, ScalingLockedError, PlanLimitError
|
|
3
|
+
from opencomputer.sandbox import Sandbox, ScalingLockedError, PlanLimitError, SandboxFamilyLimitError
|
|
4
4
|
from opencomputer.agent import Agent, AgentEvent, AgentSession, AgentSessionInfo
|
|
5
5
|
from opencomputer.filesystem import Filesystem
|
|
6
6
|
from opencomputer.exec import Exec, ProcessResult, ExecSession, ExecSessionInfo
|
|
7
7
|
from opencomputer.image import Image
|
|
8
|
+
from opencomputer.mounts import Mounts, MountInfo
|
|
8
9
|
from opencomputer.pty import Pty, PtySession
|
|
9
10
|
from opencomputer.shell import Shell, ShellBusyError, ShellClosedError
|
|
10
11
|
from opencomputer.template import Template
|
|
@@ -29,6 +30,7 @@ __all__ = [
|
|
|
29
30
|
"Sandbox",
|
|
30
31
|
"ScalingLockedError",
|
|
31
32
|
"PlanLimitError",
|
|
33
|
+
"SandboxFamilyLimitError",
|
|
32
34
|
"Agent",
|
|
33
35
|
"AgentEvent",
|
|
34
36
|
"AgentSession",
|
|
@@ -39,6 +41,8 @@ __all__ = [
|
|
|
39
41
|
"ExecSession",
|
|
40
42
|
"ExecSessionInfo",
|
|
41
43
|
"Image",
|
|
44
|
+
"Mounts",
|
|
45
|
+
"MountInfo",
|
|
42
46
|
"Pty",
|
|
43
47
|
"PtySession",
|
|
44
48
|
"Shell",
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""FUSE-backed remote filesystem mounts inside a sandbox.
|
|
2
|
+
|
|
3
|
+
Mounts use ``rclone mount`` under the hood — one driver covering ~40 backends
|
|
4
|
+
(S3, GCS, Azure Blob, SFTP, WebDAV, Dropbox, etc.). Credentials are passed
|
|
5
|
+
inline, written to a tmpfs file inside the VM (mode 0600), and never persisted
|
|
6
|
+
on the worker. v1 does NOT auto-restore mounts on hibernate/wake — callers
|
|
7
|
+
re-issue ``add(...)`` after a wake if they need the mount back.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Literal
|
|
14
|
+
|
|
15
|
+
import httpx
|
|
16
|
+
|
|
17
|
+
MountBackend = Literal["s3", "gcs", "azureblob", "sftp", "webdav", "dropbox"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class MountInfo:
|
|
22
|
+
"""An active mount as tracked by the worker.
|
|
23
|
+
|
|
24
|
+
``rclone_version`` is the rclone version inside the sandbox captured at
|
|
25
|
+
mount-add time (e.g. ``"v1.65.2"``). rclone is baked into the rootfs, so
|
|
26
|
+
different sandboxes may carry different versions; this lets ops triage
|
|
27
|
+
backend-specific bug reports quickly.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
path: str
|
|
31
|
+
remote: str
|
|
32
|
+
read_only: bool
|
|
33
|
+
backend: str = ""
|
|
34
|
+
rclone_version: str = ""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class Mounts:
|
|
39
|
+
"""Mount remote filesystems via rclone+FUSE inside the sandbox."""
|
|
40
|
+
|
|
41
|
+
_client: httpx.AsyncClient
|
|
42
|
+
_sandbox_id: str
|
|
43
|
+
|
|
44
|
+
async def add(
|
|
45
|
+
self,
|
|
46
|
+
path: str,
|
|
47
|
+
remote: str,
|
|
48
|
+
backend: MountBackend | None = None,
|
|
49
|
+
creds: dict[str, str] | None = None,
|
|
50
|
+
rclone_config: str | None = None,
|
|
51
|
+
read_only: bool = True,
|
|
52
|
+
mount_options: list[str] | None = None,
|
|
53
|
+
) -> MountInfo:
|
|
54
|
+
"""Mount a remote filesystem at ``path`` inside the sandbox.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
path: Absolute path inside the VM where the remote will be mounted.
|
|
58
|
+
remote: rclone remote spec — ``"<name>:<path>"`` (e.g. ``"s3:my-bucket/prefix"``).
|
|
59
|
+
backend: One of ``s3``, ``gcs``, ``azureblob``, ``sftp``, ``webdav``,
|
|
60
|
+
``dropbox``. Determines how ``creds`` are templated into the
|
|
61
|
+
rclone config. Omit when passing ``rclone_config`` directly.
|
|
62
|
+
creds: Backend-specific config keys (rclone field names — e.g. for
|
|
63
|
+
S3: ``access_key_id``, ``secret_access_key``, ``region``).
|
|
64
|
+
rclone_config: Raw rclone config string. Overrides ``backend`` and
|
|
65
|
+
``creds`` — useful for backends not in the typed list or for
|
|
66
|
+
advanced tuning.
|
|
67
|
+
read_only: Default ``True``. Object-store FUSE mounts have
|
|
68
|
+
well-known write footguns; opt in to RW explicitly.
|
|
69
|
+
mount_options: Extra args appended to ``rclone mount`` (e.g.
|
|
70
|
+
``["--dir-cache-time", "1m"]``).
|
|
71
|
+
"""
|
|
72
|
+
body: dict[str, object] = {
|
|
73
|
+
"path": path,
|
|
74
|
+
"remote": remote,
|
|
75
|
+
"readOnly": read_only,
|
|
76
|
+
}
|
|
77
|
+
if backend is not None:
|
|
78
|
+
body["backend"] = backend
|
|
79
|
+
if creds is not None:
|
|
80
|
+
body["creds"] = creds
|
|
81
|
+
if rclone_config is not None:
|
|
82
|
+
body["rcloneConfig"] = rclone_config
|
|
83
|
+
if mount_options is not None:
|
|
84
|
+
body["mountOptions"] = mount_options
|
|
85
|
+
|
|
86
|
+
resp = await self._client.post(
|
|
87
|
+
f"/sandboxes/{self._sandbox_id}/mounts", json=body
|
|
88
|
+
)
|
|
89
|
+
resp.raise_for_status()
|
|
90
|
+
data = resp.json()
|
|
91
|
+
return _mount_from_dict(data)
|
|
92
|
+
|
|
93
|
+
async def list(self) -> list[MountInfo]:
|
|
94
|
+
"""List the mounts this worker is tracking for the sandbox.
|
|
95
|
+
|
|
96
|
+
Returns empty after hibernate/wake — re-issue ``add()`` for any mounts
|
|
97
|
+
you need back.
|
|
98
|
+
"""
|
|
99
|
+
resp = await self._client.get(f"/sandboxes/{self._sandbox_id}/mounts")
|
|
100
|
+
resp.raise_for_status()
|
|
101
|
+
data = resp.json() or []
|
|
102
|
+
return [_mount_from_dict(entry) for entry in data]
|
|
103
|
+
|
|
104
|
+
async def remove(self, path: str) -> None:
|
|
105
|
+
"""Unmount a path previously passed to ``add()``. No-op if not mounted."""
|
|
106
|
+
try:
|
|
107
|
+
resp = await self._client.delete(
|
|
108
|
+
f"/sandboxes/{self._sandbox_id}/mounts", params={"path": path}
|
|
109
|
+
)
|
|
110
|
+
if resp.status_code == 404:
|
|
111
|
+
return
|
|
112
|
+
resp.raise_for_status()
|
|
113
|
+
except httpx.HTTPError:
|
|
114
|
+
raise
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _mount_from_dict(data: dict) -> MountInfo:
|
|
118
|
+
return MountInfo(
|
|
119
|
+
path=data["path"],
|
|
120
|
+
remote=data["remote"],
|
|
121
|
+
backend=data.get("backend", ""),
|
|
122
|
+
read_only=data.get("readOnly", True),
|
|
123
|
+
rclone_version=data.get("rcloneVersion", ""),
|
|
124
|
+
)
|
|
@@ -12,6 +12,7 @@ from opencomputer.agent import Agent
|
|
|
12
12
|
from opencomputer.exec import Exec
|
|
13
13
|
from opencomputer.filesystem import Filesystem
|
|
14
14
|
from opencomputer.image import Image
|
|
15
|
+
from opencomputer.mounts import Mounts
|
|
15
16
|
from opencomputer.pty import Pty
|
|
16
17
|
from opencomputer.sse import parse_sse_stream
|
|
17
18
|
|
|
@@ -33,6 +34,14 @@ class PlanLimitError(Exception):
|
|
|
33
34
|
"""
|
|
34
35
|
|
|
35
36
|
|
|
37
|
+
class SandboxFamilyLimitError(Exception):
|
|
38
|
+
"""Raised when a resource-changing call is blocked by a sandbox family.
|
|
39
|
+
Kept for compatibility with older API responses.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
code = "sandbox_family_scale_disabled"
|
|
43
|
+
|
|
44
|
+
|
|
36
45
|
def _raise_scaling_error(resp: httpx.Response, action: str) -> None:
|
|
37
46
|
"""Inspect a non-OK scaling response and raise the most specific error.
|
|
38
47
|
Falls back to ``raise_for_status`` so callers still see HTTP details for
|
|
@@ -43,6 +52,8 @@ def _raise_scaling_error(resp: httpx.Response, action: str) -> None:
|
|
|
43
52
|
body = {}
|
|
44
53
|
if resp.status_code == 403 and isinstance(body, dict) and body.get("code") == "scaling_locked":
|
|
45
54
|
raise ScalingLockedError(body.get("error", "scaling is locked on this sandbox"))
|
|
55
|
+
if resp.status_code == 403 and isinstance(body, dict) and body.get("code") == "sandbox_family_scale_disabled":
|
|
56
|
+
raise SandboxFamilyLimitError(body.get("error", "sandbox family does not allow scaling"))
|
|
46
57
|
if resp.status_code == 402:
|
|
47
58
|
msg = body.get("error", "plan limit exceeded") if isinstance(body, dict) else "plan limit exceeded"
|
|
48
59
|
raise PlanLimitError(msg)
|
|
@@ -59,6 +70,12 @@ class Sandbox:
|
|
|
59
70
|
sandbox_id: str
|
|
60
71
|
status: str = "running"
|
|
61
72
|
template: str = ""
|
|
73
|
+
#: Plaintext preview-URL bearer token, available immediately after a
|
|
74
|
+
#: ``Sandbox.create(preview_auth=...)`` call. Read it once and store
|
|
75
|
+
#: it somewhere durable — the server will not return it again. After
|
|
76
|
+
#: a successful ``rotate_preview_auth_token()`` this value is replaced
|
|
77
|
+
#: with the new token. Empty string when not enabled or when reconnecting.
|
|
78
|
+
preview_auth_token: str = ""
|
|
62
79
|
_api_url: str = ""
|
|
63
80
|
_api_key: str = ""
|
|
64
81
|
_connect_url: str = ""
|
|
@@ -75,11 +92,15 @@ class Sandbox:
|
|
|
75
92
|
api_url: str | None = None,
|
|
76
93
|
envs: dict[str, str] | None = None,
|
|
77
94
|
metadata: dict[str, str] | None = None,
|
|
95
|
+
burst: bool | None = None,
|
|
96
|
+
sandbox_family: str | None = None,
|
|
78
97
|
disk_mb: int | None = None,
|
|
98
|
+
memory_mb: int | None = None,
|
|
79
99
|
secret_store: str | None = None,
|
|
80
100
|
image: Image | None = None,
|
|
81
101
|
snapshot: str | None = None,
|
|
82
102
|
on_build_log: Callable[[str], None] | None = None,
|
|
103
|
+
preview_auth: dict[str, str] | None = None,
|
|
83
104
|
) -> Sandbox:
|
|
84
105
|
"""Create a new sandbox instance.
|
|
85
106
|
|
|
@@ -90,16 +111,32 @@ class Sandbox:
|
|
|
90
111
|
api_url: API URL (or OPENCOMPUTER_API_URL env var).
|
|
91
112
|
envs: Environment variables to inject. Overrides store secrets.
|
|
92
113
|
metadata: Custom metadata key-value pairs.
|
|
114
|
+
burst: Create a Burst Sandbox. Disk is preserved across
|
|
115
|
+
infrastructure restarts; processes may restart.
|
|
116
|
+
sandbox_family: Internal/legacy placement family. Prefer
|
|
117
|
+
``burst=True`` for public API usage.
|
|
93
118
|
disk_mb: Workspace disk size in MB (default 20480 = 20GB). Any
|
|
94
119
|
additional GB above 20GB is metered at a per-second rate
|
|
95
120
|
comparable to EBS gp3. Closed beta: requests above 20GB
|
|
96
121
|
require the org's ``max_disk_mb`` to be raised. Contact us:
|
|
97
122
|
https://cal.com/team/digger/opencomputer-founder-chat
|
|
123
|
+
memory_mb: Memory in MB. On a snapshot/checkpoint fork the server
|
|
124
|
+
clamps this to [snapshot memory, 16 GB]; the new sandbox's
|
|
125
|
+
``memory_mb`` reflects the effective value.
|
|
98
126
|
secret_store: Secret store name — resolves encrypted secrets
|
|
99
127
|
and egress allowlist.
|
|
100
128
|
image: Declarative Image definition. The server builds and caches it as a checkpoint.
|
|
101
129
|
snapshot: Name of a pre-built snapshot to create the sandbox from.
|
|
102
130
|
on_build_log: Callback for build log streaming when using image/snapshot.
|
|
131
|
+
preview_auth: Require a bearer token on the sandbox's preview URLs.
|
|
132
|
+
When set, every request to ``https://sb-{id}-p{port}.<domain>``
|
|
133
|
+
must include the token in an ``Authorization: Bearer <token>``
|
|
134
|
+
or ``X-OC-Preview-Token`` header. The check happens at the edge
|
|
135
|
+
before traffic reaches the VM. Pass ``{"token": "auto"}`` (or
|
|
136
|
+
omit the key) to have the server generate a 256-bit random
|
|
137
|
+
token; pass an explicit string (>=16 chars) to bring your own.
|
|
138
|
+
The plaintext is returned exactly once and assigned to
|
|
139
|
+
``sandbox.preview_auth_token``.
|
|
103
140
|
"""
|
|
104
141
|
url = api_url or os.environ.get("OPENCOMPUTER_API_URL", "https://app.opencomputer.dev")
|
|
105
142
|
url = url.rstrip("/")
|
|
@@ -130,14 +167,25 @@ class Sandbox:
|
|
|
130
167
|
body["envs"] = envs
|
|
131
168
|
if metadata:
|
|
132
169
|
body["metadata"] = metadata
|
|
170
|
+
if burst is not None:
|
|
171
|
+
body["burst"] = burst
|
|
172
|
+
if sandbox_family:
|
|
173
|
+
body["sandboxFamily"] = sandbox_family
|
|
133
174
|
if disk_mb is not None:
|
|
134
175
|
body["diskMB"] = disk_mb
|
|
176
|
+
if memory_mb is not None:
|
|
177
|
+
body["memoryMB"] = memory_mb
|
|
135
178
|
if secret_store:
|
|
136
179
|
body["secretStore"] = secret_store
|
|
137
180
|
if image is not None:
|
|
138
181
|
body["image"] = image.to_dict()
|
|
139
182
|
if snapshot is not None:
|
|
140
183
|
body["snapshot"] = snapshot
|
|
184
|
+
if preview_auth is not None:
|
|
185
|
+
body["previewAuth"] = {
|
|
186
|
+
"scheme": preview_auth.get("scheme", "bearer"),
|
|
187
|
+
"token": preview_auth.get("token", "auto"),
|
|
188
|
+
}
|
|
141
189
|
|
|
142
190
|
if use_sse:
|
|
143
191
|
data = await cls._create_with_sse(client, body, on_build_log)
|
|
@@ -162,6 +210,7 @@ class Sandbox:
|
|
|
162
210
|
sandbox_id=data["sandboxID"],
|
|
163
211
|
status=data.get("status", "running"),
|
|
164
212
|
template=template,
|
|
213
|
+
preview_auth_token=data.get("previewAuthToken", ""),
|
|
165
214
|
_api_url=url,
|
|
166
215
|
_api_key=key,
|
|
167
216
|
_connect_url=connect_url,
|
|
@@ -270,6 +319,32 @@ class Sandbox:
|
|
|
270
319
|
resp = await self._client.post(f"/sandboxes/{self.sandbox_id}/power-cycle")
|
|
271
320
|
resp.raise_for_status()
|
|
272
321
|
|
|
322
|
+
async def hibernate(self) -> None:
|
|
323
|
+
"""Hibernate the sandbox.
|
|
324
|
+
|
|
325
|
+
Snapshots the running VM (RAM + disk) to storage and frees its worker
|
|
326
|
+
slot. The sandbox keeps its ID, disks, env, and secrets; in-memory
|
|
327
|
+
process state is preserved and restored on :meth:`wake`. Compute
|
|
328
|
+
billing stops while hibernated (only storage is metered).
|
|
329
|
+
"""
|
|
330
|
+
resp = await self._client.post(f"/sandboxes/{self.sandbox_id}/hibernate")
|
|
331
|
+
resp.raise_for_status()
|
|
332
|
+
self.status = "hibernated"
|
|
333
|
+
|
|
334
|
+
async def wake(self, timeout: int | None = None) -> None:
|
|
335
|
+
"""Wake a hibernated sandbox.
|
|
336
|
+
|
|
337
|
+
Restores the VM from its hibernation snapshot on a worker — running
|
|
338
|
+
processes resume where they left off. Optionally set a new idle
|
|
339
|
+
``timeout`` (seconds; ``0`` = persistent, never auto-hibernate).
|
|
340
|
+
"""
|
|
341
|
+
body: dict[str, Any] = {}
|
|
342
|
+
if timeout is not None:
|
|
343
|
+
body["timeout"] = timeout
|
|
344
|
+
resp = await self._client.post(f"/sandboxes/{self.sandbox_id}/wake", json=body)
|
|
345
|
+
resp.raise_for_status()
|
|
346
|
+
self.status = "running"
|
|
347
|
+
|
|
273
348
|
async def is_running(self) -> bool:
|
|
274
349
|
"""Check if the sandbox is still running."""
|
|
275
350
|
try:
|
|
@@ -482,6 +557,11 @@ class Sandbox:
|
|
|
482
557
|
"""Access filesystem operations."""
|
|
483
558
|
return Filesystem(self._ops_client, self.sandbox_id)
|
|
484
559
|
|
|
560
|
+
@property
|
|
561
|
+
def mounts(self) -> Mounts:
|
|
562
|
+
"""Mount remote filesystems (S3, GCS, SFTP, …) via rclone+FUSE."""
|
|
563
|
+
return Mounts(self._ops_client, self.sandbox_id)
|
|
564
|
+
|
|
485
565
|
@property
|
|
486
566
|
def exec(self) -> Exec:
|
|
487
567
|
"""Access session-based command execution."""
|
|
@@ -751,6 +831,25 @@ class Sandbox:
|
|
|
751
831
|
if resp.status_code != 404:
|
|
752
832
|
resp.raise_for_status()
|
|
753
833
|
|
|
834
|
+
async def rotate_preview_auth_token(self) -> str:
|
|
835
|
+
"""Issue a new preview-URL bearer token; invalidate the previous one.
|
|
836
|
+
|
|
837
|
+
The old token stops working immediately — there is no zero-downtime
|
|
838
|
+
dual-token mode in v1, so coordinate the rollover with whoever is
|
|
839
|
+
calling your preview URL. If the sandbox was created without
|
|
840
|
+
``preview_auth``, calling this enables the auth gate from now on.
|
|
841
|
+
|
|
842
|
+
Returns the new plaintext token (also written to
|
|
843
|
+
``self.preview_auth_token``).
|
|
844
|
+
"""
|
|
845
|
+
resp = await self._client.post(
|
|
846
|
+
f"/sandboxes/{self.sandbox_id}/preview/rotate",
|
|
847
|
+
)
|
|
848
|
+
resp.raise_for_status()
|
|
849
|
+
data = resp.json()
|
|
850
|
+
self.preview_auth_token = data["previewAuthToken"]
|
|
851
|
+
return self.preview_auth_token
|
|
852
|
+
|
|
754
853
|
async def create_preview_url(self, port: int, domain: str | None = None, auth_config: dict | None = None) -> dict:
|
|
755
854
|
"""Create a preview URL targeting a specific container port.
|
|
756
855
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|