use-computer 0.0.24__tar.gz → 0.0.26__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. {use_computer-0.0.24 → use_computer-0.0.26}/PKG-INFO +7 -5
  2. {use_computer-0.0.24 → use_computer-0.0.26}/README.md +4 -2
  3. {use_computer-0.0.24 → use_computer-0.0.26}/pyproject.toml +3 -3
  4. {use_computer-0.0.24 → use_computer-0.0.26}/use_computer/__init__.py +4 -0
  5. {use_computer-0.0.24 → use_computer-0.0.26}/use_computer/client.py +39 -7
  6. {use_computer-0.0.24 → use_computer-0.0.26}/use_computer/models.py +7 -1
  7. {use_computer-0.0.24 → use_computer-0.0.26}/use_computer/sandbox.py +139 -4
  8. {use_computer-0.0.24 → use_computer-0.0.26}/use_computer/windows/shell.py +1 -1
  9. {use_computer-0.0.24 → use_computer-0.0.26}/.env.example +0 -0
  10. {use_computer-0.0.24 → use_computer-0.0.26}/.gitignore +0 -0
  11. {use_computer-0.0.24 → use_computer-0.0.26}/.pre-commit-config.yaml +0 -0
  12. {use_computer-0.0.24 → use_computer-0.0.26}/use_computer/accessibility.py +0 -0
  13. {use_computer-0.0.24 → use_computer-0.0.26}/use_computer/ax_transpile.py +0 -0
  14. {use_computer-0.0.24 → use_computer-0.0.26}/use_computer/display.py +0 -0
  15. {use_computer-0.0.24 → use_computer-0.0.26}/use_computer/errors.py +0 -0
  16. {use_computer-0.0.24 → use_computer-0.0.26}/use_computer/ios/__init__.py +0 -0
  17. {use_computer-0.0.24 → use_computer-0.0.26}/use_computer/ios/apps.py +0 -0
  18. {use_computer-0.0.24 → use_computer-0.0.26}/use_computer/ios/environment.py +0 -0
  19. {use_computer-0.0.24 → use_computer-0.0.26}/use_computer/ios/input.py +0 -0
  20. {use_computer-0.0.24 → use_computer-0.0.26}/use_computer/macos/__init__.py +0 -0
  21. {use_computer-0.0.24 → use_computer-0.0.26}/use_computer/macos/keyboard.py +0 -0
  22. {use_computer-0.0.24 → use_computer-0.0.26}/use_computer/macos/mouse.py +0 -0
  23. {use_computer-0.0.24 → use_computer-0.0.26}/use_computer/parsers.py +0 -0
  24. {use_computer-0.0.24 → use_computer-0.0.26}/use_computer/py.typed +0 -0
  25. {use_computer-0.0.24 → use_computer-0.0.26}/use_computer/recording.py +0 -0
  26. {use_computer-0.0.24 → use_computer-0.0.26}/use_computer/retry.py +0 -0
  27. {use_computer-0.0.24 → use_computer-0.0.26}/use_computer/screenshot.py +0 -0
  28. {use_computer-0.0.24 → use_computer-0.0.26}/use_computer/simulators.py +0 -0
  29. {use_computer-0.0.24 → use_computer-0.0.26}/use_computer/tasks/__init__.py +0 -0
  30. {use_computer-0.0.24 → use_computer-0.0.26}/use_computer/tasks/templates/pre_command.sh +0 -0
  31. {use_computer-0.0.24 → use_computer-0.0.26}/use_computer/tasks/templates/task.toml +0 -0
  32. {use_computer-0.0.24 → use_computer-0.0.26}/use_computer/tasks/templates/test_ios.sh +0 -0
  33. {use_computer-0.0.24 → use_computer-0.0.26}/use_computer/tasks/templates/test_ios_nograder.sh +0 -0
  34. {use_computer-0.0.24 → use_computer-0.0.26}/use_computer/tasks/templates/test_macos.sh +0 -0
  35. {use_computer-0.0.24 → use_computer-0.0.26}/use_computer/tasks/templates/test_macos_check.sh +0 -0
  36. {use_computer-0.0.24 → use_computer-0.0.26}/use_computer/tasks/templates/test_macos_nograder.sh +0 -0
  37. {use_computer-0.0.24 → use_computer-0.0.26}/use_computer/windows/__init__.py +0 -0
  38. {use_computer-0.0.24 → use_computer-0.0.26}/uv.lock +0 -0
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: use-computer
3
- Version: 0.0.24
4
- Summary: Python SDK for the use.computer macOS and iOS Computer Use API
3
+ Version: 0.0.26
4
+ Summary: Python SDK for use.computer sandboxes across macOS, iOS simulators, Windows, and Ubuntu
5
5
  Project-URL: Homepage, https://use.computer
6
6
  Project-URL: Documentation, https://api.use.computer/docs
7
7
  Project-URL: Repository, https://github.com/josancamon19/use-computer-python
8
8
  Author: use.computer
9
- Keywords: automation,computer-use,ios,macos,sandbox
9
+ Keywords: automation,computer-use,ios,macos,sandbox,ubuntu,vnc,windows
10
10
  Classifier: Development Status :: 3 - Alpha
11
11
  Classifier: Intended Audience :: Developers
12
12
  Classifier: Operating System :: OS Independent
@@ -22,7 +22,7 @@ Description-Content-Type: text/markdown
22
22
 
23
23
  # use-computer Python SDK
24
24
 
25
- Python client for [use.computer](https://use.computer) — rent dedicated Mac minis with macOS and Apple simulator sandboxes built for computer-use agents.
25
+ Python client for [use.computer](https://use.computer) — rent dedicated VMs across macOS, iOS/visionOS/tvOS simulators, Windows, and Ubuntu built for computer-use agents.
26
26
 
27
27
  ```bash
28
28
  pip install use-computer
@@ -73,7 +73,7 @@ Ubuntu sandboxes are **Beta** (admin-only). They use the same KVM/noVNC backend
73
73
  as Windows; pass `version="ubuntu-24.04"` to select a specific image. Exec
74
74
  defaults to bash and the macOS accessibility tree is not available yet.
75
75
 
76
- Full DSL reference (macOS + simulator): [docs.use.computer/docs/sdk](https://docs.use.computer/docs/sdk)
76
+ Full DSL reference: [docs.use.computer/docs/sdk](https://docs.use.computer/docs/sdk)
77
77
 
78
78
  Simulator sandboxes use `type=SandboxType.IOS` for the SDK route, but
79
79
  `device_type` and `runtime` can target any installed compatible CoreSimulator
@@ -105,6 +105,8 @@ visionOS yet. Use it for read-only flows for now.
105
105
  | [`examples/_4_file_transfer.py`](examples/_4_file_transfer.py) | upload bytes, download a file back |
106
106
  | [`examples/_5_keepalive.py`](examples/_5_keepalive.py) | heartbeat for sessions idle > 2 min |
107
107
  | [`examples/_6_hello_tvos.py`](examples/_6_hello_tvos.py) | tvOS: pick TV family + drive the Apple Remote |
108
+ | [`examples/_7_hello_windows.py`](examples/_7_hello_windows.py) | create Windows → run PowerShell → screenshot |
109
+ | [`examples/_8_hello_ubuntu.py`](examples/_8_hello_ubuntu.py) | create Ubuntu → run bash → screenshot |
108
110
 
109
111
  For agent loops and evals: [use-computer-cookbook](https://github.com/josancamon19/use-computer-cookbook).
110
112
 
@@ -1,6 +1,6 @@
1
1
  # use-computer Python SDK
2
2
 
3
- Python client for [use.computer](https://use.computer) — rent dedicated Mac minis with macOS and Apple simulator sandboxes built for computer-use agents.
3
+ Python client for [use.computer](https://use.computer) — rent dedicated VMs across macOS, iOS/visionOS/tvOS simulators, Windows, and Ubuntu built for computer-use agents.
4
4
 
5
5
  ```bash
6
6
  pip install use-computer
@@ -51,7 +51,7 @@ Ubuntu sandboxes are **Beta** (admin-only). They use the same KVM/noVNC backend
51
51
  as Windows; pass `version="ubuntu-24.04"` to select a specific image. Exec
52
52
  defaults to bash and the macOS accessibility tree is not available yet.
53
53
 
54
- Full DSL reference (macOS + simulator): [docs.use.computer/docs/sdk](https://docs.use.computer/docs/sdk)
54
+ Full DSL reference: [docs.use.computer/docs/sdk](https://docs.use.computer/docs/sdk)
55
55
 
56
56
  Simulator sandboxes use `type=SandboxType.IOS` for the SDK route, but
57
57
  `device_type` and `runtime` can target any installed compatible CoreSimulator
@@ -83,6 +83,8 @@ visionOS yet. Use it for read-only flows for now.
83
83
  | [`examples/_4_file_transfer.py`](examples/_4_file_transfer.py) | upload bytes, download a file back |
84
84
  | [`examples/_5_keepalive.py`](examples/_5_keepalive.py) | heartbeat for sessions idle > 2 min |
85
85
  | [`examples/_6_hello_tvos.py`](examples/_6_hello_tvos.py) | tvOS: pick TV family + drive the Apple Remote |
86
+ | [`examples/_7_hello_windows.py`](examples/_7_hello_windows.py) | create Windows → run PowerShell → screenshot |
87
+ | [`examples/_8_hello_ubuntu.py`](examples/_8_hello_ubuntu.py) | create Ubuntu → run bash → screenshot |
86
88
 
87
89
  For agent loops and evals: [use-computer-cookbook](https://github.com/josancamon19/use-computer-cookbook).
88
90
 
@@ -4,13 +4,13 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "use-computer"
7
- version = "0.0.24"
8
- description = "Python SDK for the use.computer macOS and iOS Computer Use API"
7
+ version = "0.0.26"
8
+ description = "Python SDK for use.computer sandboxes across macOS, iOS simulators, Windows, and Ubuntu"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
11
11
  authors = [{ name = "use.computer" }]
12
12
  dependencies = ["httpx>=0.27"]
13
- keywords = ["computer-use", "macos", "ios", "automation", "sandbox"]
13
+ keywords = ["computer-use", "macos", "ios", "windows", "ubuntu", "automation", "sandbox", "vnc"]
14
14
  classifiers = [
15
15
  "Development Status :: 3 - Alpha",
16
16
  "Intended Audience :: Developers",
@@ -17,10 +17,12 @@ from use_computer.sandbox import (
17
17
  AsyncSandbox,
18
18
  AsyncUbuntuSandbox,
19
19
  AsyncWindowsSandbox,
20
+ HostRedirect,
20
21
  IOSSandbox,
21
22
  MacOSSandbox,
22
23
  Sandbox,
23
24
  SandboxType,
25
+ Snapshot,
24
26
  UbuntuSandbox,
25
27
  WindowsSandbox,
26
28
  )
@@ -51,6 +53,7 @@ __all__ = [
51
53
  "CursorPosition",
52
54
  "DisplayInfo",
53
55
  "ExecResult",
56
+ "HostRedirect",
54
57
  "IOSSandbox",
55
58
  "Key",
56
59
  "MacOSSandbox",
@@ -60,6 +63,7 @@ __all__ = [
60
63
  "RunStatus",
61
64
  "Sandbox",
62
65
  "SandboxType",
66
+ "Snapshot",
63
67
  "SimulatorChoice",
64
68
  "SimulatorFamily",
65
69
  "Task",
@@ -91,7 +91,7 @@ class RunStatus:
91
91
 
92
92
 
93
93
  class Computer:
94
- """use.computer client. Creates and manages macOS and iOS sandboxes."""
94
+ """use.computer client. Creates and manages macOS, iOS, Windows, and Ubuntu sandboxes."""
95
95
 
96
96
  def __init__(self, api_key: str | None = None, base_url: str | None = None):
97
97
  self._base_url = _resolve_base_url(base_url)
@@ -114,6 +114,13 @@ class Computer:
114
114
  resp.raise_for_status()
115
115
  return resp.json()
116
116
 
117
+ def snapshots(self, platform: str = "") -> list[dict[str, Any]]:
118
+ """List Windur desktop snapshots visible to this API key."""
119
+ params = {"platform": platform} if platform else None
120
+ resp = self._http.get("/v1/snapshots", params=params)
121
+ resp.raise_for_status()
122
+ return resp.json().get("snapshots", [])
123
+
117
124
  @overload
118
125
  def create(
119
126
  self,
@@ -138,6 +145,7 @@ class Computer:
138
145
  *,
139
146
  type: Literal["windows", SandboxType.WINDOWS],
140
147
  version: str = ...,
148
+ snapshot: str = ...,
141
149
  host: str = ...,
142
150
  ) -> WindowsSandbox: ...
143
151
 
@@ -147,6 +155,7 @@ class Computer:
147
155
  *,
148
156
  type: Literal["ubuntu", SandboxType.UBUNTU],
149
157
  version: str = ...,
158
+ snapshot: str = ...,
150
159
  host: str = ...,
151
160
  ) -> UbuntuSandbox: ...
152
161
 
@@ -159,24 +168,31 @@ class Computer:
159
168
  device_type: str = "",
160
169
  runtime: str = "",
161
170
  version: str = "",
171
+ snapshot: str = "",
162
172
  ) -> MacOSSandbox | IOSSandbox | WindowsSandbox | UbuntuSandbox:
163
173
  """Create a new sandbox.
164
174
 
165
175
  Args:
166
- type: SandboxType.MACOS (default), SandboxType.IOS, or the raw API string.
176
+ type: SandboxType.MACOS (default), IOS, WINDOWS, UBUNTU, or the raw API string.
167
177
  host: Pin sandbox to a specific host machine (e.g. "mm001").
168
178
  family: Simulator family enum/alias, for example SimulatorFamily.TV.
169
179
  device_type: Simulator device type identifier, for exact pinning.
170
180
  runtime: Simulator runtime identifier, for exact pinning.
171
181
  version: KVM desktop image version, for example "windows-11",
172
182
  "windows-10", or "ubuntu-24.04".
183
+ snapshot: Windur snapshot version returned by sandbox.snapshot().
173
184
  """
174
185
  sandbox_type = _normalize_sandbox_type(type)
175
186
  body: dict = {"type": sandbox_type.value}
176
187
  if host:
177
188
  body["host"] = host
178
- if sandbox_type in (SandboxType.WINDOWS, SandboxType.UBUNTU) and version:
179
- body["version"] = version
189
+ if sandbox_type in (SandboxType.WINDOWS, SandboxType.UBUNTU):
190
+ if version and snapshot:
191
+ raise ValueError("version and snapshot are mutually exclusive")
192
+ if snapshot:
193
+ body["snapshot"] = snapshot
194
+ elif version:
195
+ body["version"] = version
180
196
  if sandbox_type == SandboxType.IOS:
181
197
  if family and (not device_type or not runtime):
182
198
  choice = select_simulator(self.platforms(), family)
@@ -333,6 +349,13 @@ class AsyncComputer:
333
349
  resp.raise_for_status()
334
350
  return resp.json()
335
351
 
352
+ async def snapshots(self, platform: str = "") -> list[dict[str, Any]]:
353
+ """List Windur desktop snapshots visible to this API key."""
354
+ params = {"platform": platform} if platform else None
355
+ resp = await self._http.get("/v1/snapshots", params=params)
356
+ resp.raise_for_status()
357
+ return resp.json().get("snapshots", [])
358
+
336
359
  @overload
337
360
  async def create(
338
361
  self,
@@ -357,6 +380,7 @@ class AsyncComputer:
357
380
  *,
358
381
  type: Literal["windows", SandboxType.WINDOWS],
359
382
  version: str = ...,
383
+ snapshot: str = ...,
360
384
  host: str = ...,
361
385
  ) -> AsyncWindowsSandbox: ...
362
386
 
@@ -366,6 +390,7 @@ class AsyncComputer:
366
390
  *,
367
391
  type: Literal["ubuntu", SandboxType.UBUNTU],
368
392
  version: str = ...,
393
+ snapshot: str = ...,
369
394
  host: str = ...,
370
395
  ) -> AsyncUbuntuSandbox: ...
371
396
 
@@ -378,24 +403,31 @@ class AsyncComputer:
378
403
  device_type: str = "",
379
404
  runtime: str = "",
380
405
  version: str = "",
406
+ snapshot: str = "",
381
407
  ) -> AsyncMacOSSandbox | AsyncIOSSandbox | AsyncWindowsSandbox | AsyncUbuntuSandbox:
382
408
  """Create a new sandbox.
383
409
 
384
410
  Args:
385
- type: SandboxType.MACOS (default), SandboxType.IOS, or the raw API string.
411
+ type: SandboxType.MACOS (default), IOS, WINDOWS, UBUNTU, or the raw API string.
386
412
  host: Pin sandbox to a specific host machine (e.g. "mm001").
387
413
  family: Simulator family enum/alias, for example SimulatorFamily.TV.
388
414
  device_type: Simulator device type identifier, for exact pinning.
389
415
  runtime: Simulator runtime identifier, for exact pinning.
390
416
  version: KVM desktop image version, for example "windows-11",
391
417
  "windows-10", or "ubuntu-24.04".
418
+ snapshot: Windur snapshot version returned by sandbox.snapshot().
392
419
  """
393
420
  sandbox_type = _normalize_sandbox_type(type)
394
421
  body: dict = {"type": sandbox_type.value}
395
422
  if host:
396
423
  body["host"] = host
397
- if sandbox_type in (SandboxType.WINDOWS, SandboxType.UBUNTU) and version:
398
- body["version"] = version
424
+ if sandbox_type in (SandboxType.WINDOWS, SandboxType.UBUNTU):
425
+ if version and snapshot:
426
+ raise ValueError("version and snapshot are mutually exclusive")
427
+ if snapshot:
428
+ body["snapshot"] = snapshot
429
+ elif version:
430
+ body["version"] = version
399
431
  if sandbox_type == SandboxType.IOS:
400
432
  if family and (not device_type or not runtime):
401
433
  choice = select_simulator(await self.platforms(), family)
@@ -88,12 +88,18 @@ class RecordingInfo:
88
88
  class ExecResult:
89
89
  return_code: int
90
90
  stdout: str
91
+ stderr: str = ""
92
+
93
+ @property
94
+ def exit_code(self) -> int:
95
+ return self.return_code
91
96
 
92
97
  @classmethod
93
98
  def from_dict(cls, d: dict) -> ExecResult:
94
99
  return cls(
95
- return_code=d.get("return_code", 0),
100
+ return_code=d.get("return_code", d.get("exit_code", d.get("returncode", 0))),
96
101
  stdout=d.get("stdout", ""),
102
+ stderr=d.get("stderr", ""),
97
103
  )
98
104
 
99
105
 
@@ -1,9 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import base64
3
4
  import io
4
5
  import tarfile
5
6
  import threading
6
7
  import urllib.request
8
+ from dataclasses import dataclass
7
9
  from enum import Enum
8
10
  from pathlib import Path
9
11
 
@@ -30,6 +32,15 @@ class SandboxType(str, Enum):
30
32
  UBUNTU = "ubuntu"
31
33
 
32
34
 
35
+ @dataclass
36
+ class Snapshot:
37
+ """Windur-backed desktop snapshot."""
38
+
39
+ name: str
40
+ version: str
41
+ platform: str
42
+
43
+
33
44
  # ---------------------------------------------------------------------------
34
45
  # Keepalive helper — shared by sync and async sandboxes
35
46
  # ---------------------------------------------------------------------------
@@ -134,6 +145,26 @@ class Sandbox:
134
145
  resp.raise_for_status()
135
146
  local_path.write_bytes(resp.content)
136
147
 
148
+ def snapshot(self, name: str, *, version: str = "", timeout: int = 900) -> Snapshot:
149
+ """Capture a Windur desktop snapshot and return its image version."""
150
+ if self.type not in (SandboxType.WINDOWS, SandboxType.UBUNTU):
151
+ raise PlatformNotSupportedError(
152
+ action="snapshot",
153
+ sandbox_type=self.type.value,
154
+ hint="snapshots are supported for Windows and Ubuntu sandboxes",
155
+ )
156
+ body = {"name": name}
157
+ if version:
158
+ body["version"] = version
159
+ resp = self._http.post(f"{self._prefix}/snapshots", json=body, timeout=timeout)
160
+ resp.raise_for_status()
161
+ data = resp.json()
162
+ return Snapshot(
163
+ name=data.get("name", name),
164
+ version=data.get("version", version),
165
+ platform=data.get("platform", self.type.value),
166
+ )
167
+
137
168
  def start_keepalive(self, interval: float = 30.0) -> None:
138
169
  """Start a background thread that pings the gateway every `interval` seconds."""
139
170
  self._keepalive_stop, self._keepalive_thread = _start_keepalive_thread(
@@ -256,6 +287,90 @@ class IOSSandbox(Sandbox):
256
287
  self.environment = Environment(http, self._prefix)
257
288
 
258
289
 
290
+ @dataclass
291
+ class HostRedirect:
292
+ """Point a domain at the loopback host (the /etc/hosts redirect) so a browser
293
+ hitting it lands on a locally-running app. If ``port`` is set, :80 and :443
294
+ are also forwarded to 127.0.0.1:port, so e.g. ``https://mail.google.com``
295
+ reaches a local server on that port."""
296
+
297
+ host: str
298
+ ip: str = "127.0.0.1"
299
+ port: int | None = None
300
+
301
+
302
+ def _build_setup_spec(
303
+ hosts: list[HostRedirect] | None,
304
+ files: dict[str, bytes | str] | None,
305
+ script: str | None,
306
+ open_urls: list[str] | None,
307
+ open_apps: list[str] | None,
308
+ ) -> dict:
309
+ spec: dict = {}
310
+ if hosts:
311
+ spec["hosts"] = [
312
+ {"host": h.host, "ip": h.ip, **({"port": h.port} if h.port else {})} for h in hosts
313
+ ]
314
+ if files:
315
+ spec["files"] = [
316
+ {
317
+ "path": path,
318
+ "content_b64": base64.b64encode(
319
+ data if isinstance(data, bytes) else data.encode()
320
+ ).decode(),
321
+ }
322
+ for path, data in files.items()
323
+ ]
324
+ if script:
325
+ spec["script"] = script
326
+ if open_urls:
327
+ spec["open_urls"] = list(open_urls)
328
+ if open_apps:
329
+ spec["open_apps"] = list(open_apps)
330
+ return spec
331
+
332
+
333
+ class _Seeding:
334
+ """Typed seeding for KVM desktops (windows/ubuntu), applied in order:
335
+ host redirects → files → script → open_urls → open_apps."""
336
+
337
+ _http: httpx.Client
338
+ _prefix: str
339
+
340
+ def setup(
341
+ self,
342
+ *,
343
+ hosts: list[HostRedirect] | None = None,
344
+ files: dict[str, bytes | str] | None = None,
345
+ script: str | None = None,
346
+ open_urls: list[str] | None = None,
347
+ open_apps: list[str] | None = None,
348
+ ) -> None:
349
+ spec = _build_setup_spec(hosts, files, script, open_urls, open_apps)
350
+ resp = self._http.post(f"{self._prefix}/setup", json=spec, timeout=1200)
351
+ resp.raise_for_status()
352
+
353
+
354
+ class _AsyncSeeding:
355
+ """Async counterpart of _Seeding."""
356
+
357
+ _http: httpx.AsyncClient
358
+ _prefix: str
359
+
360
+ async def setup(
361
+ self,
362
+ *,
363
+ hosts: list[HostRedirect] | None = None,
364
+ files: dict[str, bytes | str] | None = None,
365
+ script: str | None = None,
366
+ open_urls: list[str] | None = None,
367
+ open_apps: list[str] | None = None,
368
+ ) -> None:
369
+ spec = _build_setup_spec(hosts, files, script, open_urls, open_apps)
370
+ resp = await self._http.post(f"{self._prefix}/setup", json=spec, timeout=1200)
371
+ resp.raise_for_status()
372
+
373
+
259
374
  class _NoAxeAccessibility:
260
375
  """Mixin for desktop guests without the macOS axe-schema tree."""
261
376
 
@@ -275,7 +390,7 @@ class _NoAxeAccessibility:
275
390
  pass # discard the base's axe helper — Windows exposes .ui_tree() instead
276
391
 
277
392
 
278
- class WindowsSandbox(_NoAxeAccessibility, Sandbox):
393
+ class WindowsSandbox(_Seeding, _NoAxeAccessibility, Sandbox):
279
394
  """Windows VM sandbox (Beta) — mouse, keyboard, PowerShell/cmd exec, VNC.
280
395
 
281
396
  Beta. Differs from macOS: no SSH (exec runs through the in-guest agent via
@@ -333,7 +448,7 @@ class WindowsSandbox(_NoAxeAccessibility, Sandbox):
333
448
  return resp.json()
334
449
 
335
450
 
336
- class UbuntuSandbox(_NoAxeAccessibility, Sandbox):
451
+ class UbuntuSandbox(_Seeding, _NoAxeAccessibility, Sandbox):
337
452
  """Ubuntu desktop VM sandbox — mouse, keyboard, bash exec, VNC."""
338
453
 
339
454
  _accessibility_sandbox_type = "ubuntu"
@@ -431,6 +546,26 @@ class AsyncSandbox:
431
546
  resp.raise_for_status()
432
547
  local_path.write_bytes(resp.content)
433
548
 
549
+ async def snapshot(self, name: str, *, version: str = "", timeout: int = 900) -> Snapshot:
550
+ """Capture a Windur desktop snapshot and return its image version."""
551
+ if self.type not in (SandboxType.WINDOWS, SandboxType.UBUNTU):
552
+ raise PlatformNotSupportedError(
553
+ action="snapshot",
554
+ sandbox_type=self.type.value,
555
+ hint="snapshots are supported for Windows and Ubuntu sandboxes",
556
+ )
557
+ body = {"name": name}
558
+ if version:
559
+ body["version"] = version
560
+ resp = await self._http.post(f"{self._prefix}/snapshots", json=body, timeout=timeout)
561
+ resp.raise_for_status()
562
+ data = resp.json()
563
+ return Snapshot(
564
+ name=data.get("name", name),
565
+ version=data.get("version", version),
566
+ platform=data.get("platform", self.type.value),
567
+ )
568
+
434
569
  async def start_keepalive(self, interval: float = 30.0) -> None:
435
570
  """Start a background OS thread that pings the gateway every `interval` seconds.
436
571
 
@@ -549,7 +684,7 @@ class AsyncIOSSandbox(AsyncSandbox):
549
684
  self.environment = AsyncEnvironment(http, self._prefix)
550
685
 
551
686
 
552
- class AsyncWindowsSandbox(_NoAxeAccessibility, AsyncSandbox):
687
+ class AsyncWindowsSandbox(_AsyncSeeding, _NoAxeAccessibility, AsyncSandbox):
553
688
  """Async Windows VM sandbox — see WindowsSandbox."""
554
689
 
555
690
  _accessibility_sandbox_type = "windows"
@@ -605,7 +740,7 @@ class AsyncWindowsSandbox(_NoAxeAccessibility, AsyncSandbox):
605
740
  return resp.json()
606
741
 
607
742
 
608
- class AsyncUbuntuSandbox(_NoAxeAccessibility, AsyncSandbox):
743
+ class AsyncUbuntuSandbox(_AsyncSeeding, _NoAxeAccessibility, AsyncSandbox):
609
744
  """Async Ubuntu desktop VM sandbox — see UbuntuSandbox."""
610
745
 
611
746
  _accessibility_sandbox_type = "ubuntu"
@@ -10,7 +10,7 @@ ShellKind = Literal["bash", "sh", "powershell", "cmd"]
10
10
 
11
11
 
12
12
  class Shell:
13
- """Run commands inside a Windows sandbox via the in-guest agent (no SSH)."""
13
+ """Run commands inside a KVM desktop sandbox via the in-guest agent."""
14
14
 
15
15
  def __init__(self, http: httpx.Client, prefix: str, default_shell: ShellKind = "powershell"):
16
16
  self._http = http
File without changes
File without changes