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.
Files changed (38) hide show
  1. {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/PKG-INFO +1 -1
  2. {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/opencomputer/__init__.py +5 -1
  3. opencomputer_sdk-0.6.2/opencomputer/mounts.py +124 -0
  4. {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/opencomputer/sandbox.py +99 -0
  5. {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/pyproject.toml +1 -1
  6. {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/.gitignore +0 -0
  7. {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/README.md +0 -0
  8. {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/examples/run_all_tests.py +0 -0
  9. {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/examples/stream_demo.py +0 -0
  10. {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/examples/test_commands.py +0 -0
  11. {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/examples/test_concurrent.py +0 -0
  12. {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/examples/test_declarative_images.py +0 -0
  13. {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/examples/test_default_template.py +0 -0
  14. {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/examples/test_disk_size.py +0 -0
  15. {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/examples/test_domain_tls.py +0 -0
  16. {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/examples/test_environment.py +0 -0
  17. {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/examples/test_exec.py +0 -0
  18. {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/examples/test_file_ops.py +0 -0
  19. {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/examples/test_large_files.py +0 -0
  20. {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/examples/test_multi_template.py +0 -0
  21. {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/examples/test_python_sdk.py +0 -0
  22. {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/examples/test_reconnect.py +0 -0
  23. {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/examples/test_secret_store_fork.py +0 -0
  24. {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/examples/test_secretstore.py +0 -0
  25. {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/examples/test_shell.py +0 -0
  26. {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/examples/test_timeout.py +0 -0
  27. {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/opencomputer/agent.py +0 -0
  28. {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/opencomputer/commands.py +0 -0
  29. {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/opencomputer/exec.py +0 -0
  30. {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/opencomputer/filesystem.py +0 -0
  31. {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/opencomputer/image.py +0 -0
  32. {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/opencomputer/project.py +0 -0
  33. {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/opencomputer/pty.py +0 -0
  34. {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/opencomputer/shell.py +0 -0
  35. {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/opencomputer/snapshot.py +0 -0
  36. {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/opencomputer/sse.py +0 -0
  37. {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.2}/opencomputer/template.py +0 -0
  38. {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.0
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
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "opencomputer-sdk"
7
- version = "0.6.0"
7
+ version = "0.6.2"
8
8
  description = "Python SDK for OpenComputer - cloud sandbox platform"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"