use-computer 0.0.26__tar.gz → 0.0.27__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 (50) hide show
  1. {use_computer-0.0.26 → use_computer-0.0.27}/PKG-INFO +49 -3
  2. {use_computer-0.0.26 → use_computer-0.0.27}/README.md +48 -2
  3. {use_computer-0.0.26 → use_computer-0.0.27}/pyproject.toml +5 -2
  4. use_computer-0.0.27/use_computer/accessibility.py +3 -0
  5. {use_computer-0.0.26 → use_computer-0.0.27}/use_computer/client.py +37 -17
  6. use_computer-0.0.27/use_computer/display.py +3 -0
  7. use_computer-0.0.27/use_computer/recording.py +3 -0
  8. use_computer-0.0.27/use_computer/resources/__init__.py +17 -0
  9. {use_computer-0.0.26 → use_computer-0.0.27}/use_computer/sandbox.py +10 -4
  10. use_computer-0.0.27/use_computer/screenshot.py +3 -0
  11. {use_computer-0.0.26 → use_computer-0.0.27}/use_computer/tasks/__init__.py +55 -10
  12. use_computer-0.0.27/use_computer/tasks/templates/pre_command.ps1 +1 -0
  13. use_computer-0.0.27/use_computer/tasks/templates/test_ubuntu.sh +20 -0
  14. use_computer-0.0.27/use_computer/tasks/templates/test_ubuntu_check.sh +10 -0
  15. use_computer-0.0.27/use_computer/tasks/templates/test_ubuntu_nograder.sh +6 -0
  16. use_computer-0.0.27/use_computer/tasks/templates/test_windows.sh +20 -0
  17. use_computer-0.0.27/use_computer/tasks/templates/test_windows_check.sh +10 -0
  18. use_computer-0.0.27/use_computer/tasks/templates/test_windows_nograder.sh +6 -0
  19. {use_computer-0.0.26 → use_computer-0.0.27}/.env.example +0 -0
  20. {use_computer-0.0.26 → use_computer-0.0.27}/.gitignore +0 -0
  21. {use_computer-0.0.26 → use_computer-0.0.27}/.pre-commit-config.yaml +0 -0
  22. {use_computer-0.0.26 → use_computer-0.0.27}/use_computer/__init__.py +0 -0
  23. {use_computer-0.0.26 → use_computer-0.0.27}/use_computer/ax_transpile.py +0 -0
  24. {use_computer-0.0.26 → use_computer-0.0.27}/use_computer/errors.py +0 -0
  25. {use_computer-0.0.26 → use_computer-0.0.27}/use_computer/ios/__init__.py +0 -0
  26. {use_computer-0.0.26 → use_computer-0.0.27}/use_computer/ios/apps.py +0 -0
  27. {use_computer-0.0.26 → use_computer-0.0.27}/use_computer/ios/environment.py +0 -0
  28. {use_computer-0.0.26 → use_computer-0.0.27}/use_computer/ios/input.py +0 -0
  29. {use_computer-0.0.26 → use_computer-0.0.27}/use_computer/macos/__init__.py +0 -0
  30. {use_computer-0.0.26 → use_computer-0.0.27}/use_computer/macos/keyboard.py +0 -0
  31. {use_computer-0.0.26 → use_computer-0.0.27}/use_computer/macos/mouse.py +0 -0
  32. {use_computer-0.0.26 → use_computer-0.0.27}/use_computer/models.py +0 -0
  33. {use_computer-0.0.26 → use_computer-0.0.27}/use_computer/parsers.py +0 -0
  34. {use_computer-0.0.26 → use_computer-0.0.27}/use_computer/py.typed +0 -0
  35. {use_computer-0.0.26/use_computer → use_computer-0.0.27/use_computer/resources}/accessibility.py +0 -0
  36. {use_computer-0.0.26/use_computer → use_computer-0.0.27/use_computer/resources}/display.py +0 -0
  37. {use_computer-0.0.26/use_computer → use_computer-0.0.27/use_computer/resources}/recording.py +0 -0
  38. {use_computer-0.0.26/use_computer → use_computer-0.0.27/use_computer/resources}/screenshot.py +0 -0
  39. {use_computer-0.0.26 → use_computer-0.0.27}/use_computer/retry.py +0 -0
  40. {use_computer-0.0.26 → use_computer-0.0.27}/use_computer/simulators.py +0 -0
  41. {use_computer-0.0.26 → use_computer-0.0.27}/use_computer/tasks/templates/pre_command.sh +0 -0
  42. {use_computer-0.0.26 → use_computer-0.0.27}/use_computer/tasks/templates/task.toml +0 -0
  43. {use_computer-0.0.26 → use_computer-0.0.27}/use_computer/tasks/templates/test_ios.sh +0 -0
  44. {use_computer-0.0.26 → use_computer-0.0.27}/use_computer/tasks/templates/test_ios_nograder.sh +0 -0
  45. {use_computer-0.0.26 → use_computer-0.0.27}/use_computer/tasks/templates/test_macos.sh +0 -0
  46. {use_computer-0.0.26 → use_computer-0.0.27}/use_computer/tasks/templates/test_macos_check.sh +0 -0
  47. {use_computer-0.0.26 → use_computer-0.0.27}/use_computer/tasks/templates/test_macos_nograder.sh +0 -0
  48. {use_computer-0.0.26 → use_computer-0.0.27}/use_computer/windows/__init__.py +0 -0
  49. {use_computer-0.0.26 → use_computer-0.0.27}/use_computer/windows/shell.py +0 -0
  50. {use_computer-0.0.26 → use_computer-0.0.27}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: use-computer
3
- Version: 0.0.26
3
+ Version: 0.0.27
4
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
@@ -60,18 +60,62 @@ recording/file surface as macOS; exec runs in-guest via PowerShell/cmd
60
60
  (`win.run` / `win.shell`) instead of SSH. `AsyncWindowsSandbox` mirrors it.
61
61
  See [docs.use.computer/docs/windows](https://docs.use.computer/docs/windows).
62
62
 
63
+ Selectable Windows images are exposed by `Computer().platforms()["windows"]`.
64
+ Prefer `version="windows-11"` plus `resources={"cpus": 4, "memory_mb": 4096,
65
+ "disk_gb": 40}` to select a resource profile. Explicit image IDs such as
66
+ `windows-11-4c4g40g` still work. The image metadata includes CPU/RAM/disk and
67
+ display size.
68
+
63
69
  ### Ubuntu (Beta)
64
70
 
65
71
  ```python
66
72
  with Computer().create(type="ubuntu", version="ubuntu-24.04") as ubuntu:
67
73
  print(ubuntu.run("uname -a").stdout) # bash exec (no SSH)
74
+ print(ubuntu.display.get_info())
68
75
  ubuntu.keyboard.type("hello")
69
76
  ubuntu.screenshot.take_full_screen()
70
77
  ```
71
78
 
72
79
  Ubuntu sandboxes are **Beta** (admin-only). They use the same KVM/noVNC backend
73
- as Windows; pass `version="ubuntu-24.04"` to select a specific image. Exec
74
- defaults to bash and the macOS accessibility tree is not available yet.
80
+ as Windows; pass `version="ubuntu-24.04"` plus `resources` to select CPU/RAM/disk.
81
+
82
+ Selectable Ubuntu images are exposed by `Computer().platforms()["ubuntu"]`.
83
+ Current examples include `ubuntu-24.04`, `ubuntu-24.04-4c4g80g`,
84
+ `ubuntu-24.04-4c4g40g`, and `ubuntu-24.04-2c4g40g`.
85
+
86
+ ```python
87
+ client = Computer()
88
+ platforms = client.platforms()
89
+ for image in platforms["ubuntu"]["images"]:
90
+ print(image["version"], image["resources"], image["display"])
91
+
92
+ with client.create(
93
+ type="ubuntu",
94
+ version="ubuntu-24.04",
95
+ resources={"cpus": 2, "memory_mb": 4096, "disk_gb": 40},
96
+ ) as ubuntu:
97
+ print(ubuntu.display.get_info())
98
+ ```
99
+
100
+ ### Snapshots (Beta)
101
+
102
+ Windows and Ubuntu snapshots preserve disk + RAM state, including open apps and
103
+ running processes. Seed a VM once, snapshot it, then create new sandboxes from
104
+ that snapshot version.
105
+
106
+ ```python
107
+ client = Computer()
108
+
109
+ with client.create(type="ubuntu", version="ubuntu-24.04") as ubuntu:
110
+ ubuntu.run("google-chrome-stable --no-first-run https://example.com >/tmp/chrome.log 2>&1 &")
111
+ snapshot = ubuntu.snapshot("chrome-seeded-ubuntu")
112
+
113
+ with client.create(type="ubuntu", snapshot=snapshot.version) as seeded:
114
+ print(seeded.run("pgrep -af 'chrome|chromium' | head").stdout)
115
+ ```
116
+
117
+ Use `client.snapshots("ubuntu")` or `client.snapshots("windows")` to list saved
118
+ snapshot versions.
75
119
 
76
120
  Full DSL reference: [docs.use.computer/docs/sdk](https://docs.use.computer/docs/sdk)
77
121
 
@@ -107,6 +151,8 @@ visionOS yet. Use it for read-only flows for now.
107
151
  | [`examples/_6_hello_tvos.py`](examples/_6_hello_tvos.py) | tvOS: pick TV family + drive the Apple Remote |
108
152
  | [`examples/_7_hello_windows.py`](examples/_7_hello_windows.py) | create Windows → run PowerShell → screenshot |
109
153
  | [`examples/_8_hello_ubuntu.py`](examples/_8_hello_ubuntu.py) | create Ubuntu → run bash → screenshot |
154
+ | [`examples/_9_seeding.py`](examples/_9_seeding.py) | typed setup: files, hosts, open URLs/apps |
155
+ | [`examples/_10_snapshots.py`](examples/_10_snapshots.py) | snapshot seeded Ubuntu/Windows desktop state |
110
156
 
111
157
  For agent loops and evals: [use-computer-cookbook](https://github.com/josancamon19/use-computer-cookbook).
112
158
 
@@ -38,18 +38,62 @@ recording/file surface as macOS; exec runs in-guest via PowerShell/cmd
38
38
  (`win.run` / `win.shell`) instead of SSH. `AsyncWindowsSandbox` mirrors it.
39
39
  See [docs.use.computer/docs/windows](https://docs.use.computer/docs/windows).
40
40
 
41
+ Selectable Windows images are exposed by `Computer().platforms()["windows"]`.
42
+ Prefer `version="windows-11"` plus `resources={"cpus": 4, "memory_mb": 4096,
43
+ "disk_gb": 40}` to select a resource profile. Explicit image IDs such as
44
+ `windows-11-4c4g40g` still work. The image metadata includes CPU/RAM/disk and
45
+ display size.
46
+
41
47
  ### Ubuntu (Beta)
42
48
 
43
49
  ```python
44
50
  with Computer().create(type="ubuntu", version="ubuntu-24.04") as ubuntu:
45
51
  print(ubuntu.run("uname -a").stdout) # bash exec (no SSH)
52
+ print(ubuntu.display.get_info())
46
53
  ubuntu.keyboard.type("hello")
47
54
  ubuntu.screenshot.take_full_screen()
48
55
  ```
49
56
 
50
57
  Ubuntu sandboxes are **Beta** (admin-only). They use the same KVM/noVNC backend
51
- as Windows; pass `version="ubuntu-24.04"` to select a specific image. Exec
52
- defaults to bash and the macOS accessibility tree is not available yet.
58
+ as Windows; pass `version="ubuntu-24.04"` plus `resources` to select CPU/RAM/disk.
59
+
60
+ Selectable Ubuntu images are exposed by `Computer().platforms()["ubuntu"]`.
61
+ Current examples include `ubuntu-24.04`, `ubuntu-24.04-4c4g80g`,
62
+ `ubuntu-24.04-4c4g40g`, and `ubuntu-24.04-2c4g40g`.
63
+
64
+ ```python
65
+ client = Computer()
66
+ platforms = client.platforms()
67
+ for image in platforms["ubuntu"]["images"]:
68
+ print(image["version"], image["resources"], image["display"])
69
+
70
+ with client.create(
71
+ type="ubuntu",
72
+ version="ubuntu-24.04",
73
+ resources={"cpus": 2, "memory_mb": 4096, "disk_gb": 40},
74
+ ) as ubuntu:
75
+ print(ubuntu.display.get_info())
76
+ ```
77
+
78
+ ### Snapshots (Beta)
79
+
80
+ Windows and Ubuntu snapshots preserve disk + RAM state, including open apps and
81
+ running processes. Seed a VM once, snapshot it, then create new sandboxes from
82
+ that snapshot version.
83
+
84
+ ```python
85
+ client = Computer()
86
+
87
+ with client.create(type="ubuntu", version="ubuntu-24.04") as ubuntu:
88
+ ubuntu.run("google-chrome-stable --no-first-run https://example.com >/tmp/chrome.log 2>&1 &")
89
+ snapshot = ubuntu.snapshot("chrome-seeded-ubuntu")
90
+
91
+ with client.create(type="ubuntu", snapshot=snapshot.version) as seeded:
92
+ print(seeded.run("pgrep -af 'chrome|chromium' | head").stdout)
93
+ ```
94
+
95
+ Use `client.snapshots("ubuntu")` or `client.snapshots("windows")` to list saved
96
+ snapshot versions.
53
97
 
54
98
  Full DSL reference: [docs.use.computer/docs/sdk](https://docs.use.computer/docs/sdk)
55
99
 
@@ -85,6 +129,8 @@ visionOS yet. Use it for read-only flows for now.
85
129
  | [`examples/_6_hello_tvos.py`](examples/_6_hello_tvos.py) | tvOS: pick TV family + drive the Apple Remote |
86
130
  | [`examples/_7_hello_windows.py`](examples/_7_hello_windows.py) | create Windows → run PowerShell → screenshot |
87
131
  | [`examples/_8_hello_ubuntu.py`](examples/_8_hello_ubuntu.py) | create Ubuntu → run bash → screenshot |
132
+ | [`examples/_9_seeding.py`](examples/_9_seeding.py) | typed setup: files, hosts, open URLs/apps |
133
+ | [`examples/_10_snapshots.py`](examples/_10_snapshots.py) | snapshot seeded Ubuntu/Windows desktop state |
88
134
 
89
135
  For agent loops and evals: [use-computer-cookbook](https://github.com/josancamon19/use-computer-cookbook).
90
136
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "use-computer"
7
- version = "0.0.26"
7
+ version = "0.0.27"
8
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"
@@ -48,9 +48,12 @@ exclude = [
48
48
  "/.venv",
49
49
  "/dist",
50
50
  "/results",
51
- "/use_computer/test_*.py",
51
+ "/tests",
52
52
  ]
53
53
 
54
+ [tool.pytest.ini_options]
55
+ testpaths = ["tests"]
56
+
54
57
  [tool.ruff]
55
58
  target-version = "py310"
56
59
  line-length = 100
@@ -0,0 +1,3 @@
1
+ from use_computer.resources.accessibility import Accessibility, AsyncAccessibility
2
+
3
+ __all__ = ["Accessibility", "AsyncAccessibility"]
@@ -41,6 +41,12 @@ def _resolve_base_url(base_url: str | None) -> str:
41
41
  return (base_url or os.getenv("USE_COMPUTER_BASE_URL") or DEFAULT_BASE_URL).rstrip("/")
42
42
 
43
43
 
44
+ def _absolute_url(base_url: str, value: str) -> str:
45
+ if not value or value.startswith(("http://", "https://")):
46
+ return value
47
+ return f"{base_url}{'' if value.startswith('/') else '/'}{value}"
48
+
49
+
44
50
  def _normalize_sandbox_type(type: SandboxTypeInput) -> SandboxType:
45
51
  if isinstance(type, SandboxType):
46
52
  return type
@@ -146,6 +152,7 @@ class Computer:
146
152
  type: Literal["windows", SandboxType.WINDOWS],
147
153
  version: str = ...,
148
154
  snapshot: str = ...,
155
+ resources: dict[str, int] | None = ...,
149
156
  host: str = ...,
150
157
  ) -> WindowsSandbox: ...
151
158
 
@@ -156,6 +163,7 @@ class Computer:
156
163
  type: Literal["ubuntu", SandboxType.UBUNTU],
157
164
  version: str = ...,
158
165
  snapshot: str = ...,
166
+ resources: dict[str, int] | None = ...,
159
167
  host: str = ...,
160
168
  ) -> UbuntuSandbox: ...
161
169
 
@@ -169,6 +177,7 @@ class Computer:
169
177
  runtime: str = "",
170
178
  version: str = "",
171
179
  snapshot: str = "",
180
+ resources: dict[str, int] | None = None,
172
181
  ) -> MacOSSandbox | IOSSandbox | WindowsSandbox | UbuntuSandbox:
173
182
  """Create a new sandbox.
174
183
 
@@ -181,6 +190,8 @@ class Computer:
181
190
  version: KVM desktop image version, for example "windows-11",
182
191
  "windows-10", or "ubuntu-24.04".
183
192
  snapshot: Windur snapshot version returned by sandbox.snapshot().
193
+ resources: KVM CPU/RAM/disk request used to select the baked image
194
+ profile, for example {"cpus": 4, "memory_mb": 4096, "disk_gb": 40}.
184
195
  """
185
196
  sandbox_type = _normalize_sandbox_type(type)
186
197
  body: dict = {"type": sandbox_type.value}
@@ -193,6 +204,8 @@ class Computer:
193
204
  body["snapshot"] = snapshot
194
205
  elif version:
195
206
  body["version"] = version
207
+ if resources:
208
+ body["resources"] = resources
196
209
  if sandbox_type == SandboxType.IOS:
197
210
  if family and (not device_type or not runtime):
198
211
  choice = select_simulator(self.platforms(), family)
@@ -214,7 +227,7 @@ class Computer:
214
227
  return WindowsSandbox(
215
228
  sandbox_id=sid,
216
229
  http=self._http,
217
- vnc_url=f"{self._base_url}{data.get('vnc_url', '')}",
230
+ vnc_url=_absolute_url(self._base_url, data.get("vnc_url", "")),
218
231
  host=data.get("host", ""),
219
232
  version=data.get("version", ""),
220
233
  )
@@ -222,17 +235,17 @@ class Computer:
222
235
  return UbuntuSandbox(
223
236
  sandbox_id=sid,
224
237
  http=self._http,
225
- vnc_url=f"{self._base_url}{data.get('vnc_url', '')}",
238
+ vnc_url=_absolute_url(self._base_url, data.get("vnc_url", "")),
226
239
  host=data.get("host", ""),
227
240
  version=data.get("version", ""),
228
241
  )
229
242
  return MacOSSandbox(
230
243
  sandbox_id=sid,
231
244
  http=self._http,
232
- vnc_url=f"{self._base_url}{data.get('vnc_url', '')}",
245
+ vnc_url=_absolute_url(self._base_url, data.get("vnc_url", "")),
233
246
  vm_ip=data.get("vm_ip", ""),
234
247
  host=data.get("host", ""),
235
- ssh_url=f"{self._base_url}{data.get('ssh_url', '')}",
248
+ ssh_url=_absolute_url(self._base_url, data.get("ssh_url", "")),
236
249
  )
237
250
 
238
251
  def get(self, sandbox_id: str) -> Sandbox:
@@ -247,7 +260,7 @@ class Computer:
247
260
  return WindowsSandbox(
248
261
  sandbox_id=data["sandbox_id"],
249
262
  http=self._http,
250
- vnc_url=f"{self._base_url}{data.get('vnc_url', '')}",
263
+ vnc_url=_absolute_url(self._base_url, data.get("vnc_url", "")),
251
264
  host=data.get("host", ""),
252
265
  version=data.get("version", ""),
253
266
  )
@@ -255,15 +268,15 @@ class Computer:
255
268
  return UbuntuSandbox(
256
269
  sandbox_id=data["sandbox_id"],
257
270
  http=self._http,
258
- vnc_url=f"{self._base_url}{data.get('vnc_url', '')}",
271
+ vnc_url=_absolute_url(self._base_url, data.get("vnc_url", "")),
259
272
  host=data.get("host", ""),
260
273
  version=data.get("version", ""),
261
274
  )
262
275
  return MacOSSandbox(
263
276
  sandbox_id=data["sandbox_id"],
264
277
  http=self._http,
265
- vnc_url=f"{self._base_url}{data.get('vnc_url', '')}",
266
- ssh_url=f"{self._base_url}{data.get('ssh_url', '')}",
278
+ vnc_url=_absolute_url(self._base_url, data.get("vnc_url", "")),
279
+ ssh_url=_absolute_url(self._base_url, data.get("ssh_url", "")),
267
280
  )
268
281
 
269
282
  def adhoc_run(
@@ -281,7 +294,7 @@ class Computer:
281
294
 
282
295
  Args:
283
296
  instruction: What the agent should do.
284
- platform: "macos" or "ios".
297
+ platform: "macos", "ios", "windows", or "ubuntu".
285
298
  model: Model to use.
286
299
  max_steps: Max agent steps.
287
300
  files: Optional list of {remote_path, content_b64} to pre-upload.
@@ -381,6 +394,7 @@ class AsyncComputer:
381
394
  type: Literal["windows", SandboxType.WINDOWS],
382
395
  version: str = ...,
383
396
  snapshot: str = ...,
397
+ resources: dict[str, int] | None = ...,
384
398
  host: str = ...,
385
399
  ) -> AsyncWindowsSandbox: ...
386
400
 
@@ -391,6 +405,7 @@ class AsyncComputer:
391
405
  type: Literal["ubuntu", SandboxType.UBUNTU],
392
406
  version: str = ...,
393
407
  snapshot: str = ...,
408
+ resources: dict[str, int] | None = ...,
394
409
  host: str = ...,
395
410
  ) -> AsyncUbuntuSandbox: ...
396
411
 
@@ -404,6 +419,7 @@ class AsyncComputer:
404
419
  runtime: str = "",
405
420
  version: str = "",
406
421
  snapshot: str = "",
422
+ resources: dict[str, int] | None = None,
407
423
  ) -> AsyncMacOSSandbox | AsyncIOSSandbox | AsyncWindowsSandbox | AsyncUbuntuSandbox:
408
424
  """Create a new sandbox.
409
425
 
@@ -416,6 +432,8 @@ class AsyncComputer:
416
432
  version: KVM desktop image version, for example "windows-11",
417
433
  "windows-10", or "ubuntu-24.04".
418
434
  snapshot: Windur snapshot version returned by sandbox.snapshot().
435
+ resources: KVM CPU/RAM/disk request used to select the baked image
436
+ profile, for example {"cpus": 4, "memory_mb": 4096, "disk_gb": 40}.
419
437
  """
420
438
  sandbox_type = _normalize_sandbox_type(type)
421
439
  body: dict = {"type": sandbox_type.value}
@@ -428,6 +446,8 @@ class AsyncComputer:
428
446
  body["snapshot"] = snapshot
429
447
  elif version:
430
448
  body["version"] = version
449
+ if resources:
450
+ body["resources"] = resources
431
451
  if sandbox_type == SandboxType.IOS:
432
452
  if family and (not device_type or not runtime):
433
453
  choice = select_simulator(await self.platforms(), family)
@@ -449,7 +469,7 @@ class AsyncComputer:
449
469
  return AsyncWindowsSandbox(
450
470
  sandbox_id=sid,
451
471
  http=self._http,
452
- vnc_url=f"{self._base_url}{data.get('vnc_url', '')}",
472
+ vnc_url=_absolute_url(self._base_url, data.get("vnc_url", "")),
453
473
  host=data.get("host", ""),
454
474
  version=data.get("version", ""),
455
475
  )
@@ -457,15 +477,15 @@ class AsyncComputer:
457
477
  return AsyncUbuntuSandbox(
458
478
  sandbox_id=sid,
459
479
  http=self._http,
460
- vnc_url=f"{self._base_url}{data.get('vnc_url', '')}",
480
+ vnc_url=_absolute_url(self._base_url, data.get("vnc_url", "")),
461
481
  host=data.get("host", ""),
462
482
  version=data.get("version", ""),
463
483
  )
464
484
  return AsyncMacOSSandbox(
465
485
  sandbox_id=sid,
466
486
  http=self._http,
467
- vnc_url=f"{self._base_url}{data.get('vnc_url', '')}",
468
- ssh_url=f"{self._base_url}{data.get('ssh_url', '')}",
487
+ vnc_url=_absolute_url(self._base_url, data.get("vnc_url", "")),
488
+ ssh_url=_absolute_url(self._base_url, data.get("ssh_url", "")),
469
489
  vm_ip=data.get("vm_ip", ""),
470
490
  host=data.get("host", ""),
471
491
  )
@@ -482,7 +502,7 @@ class AsyncComputer:
482
502
  return AsyncWindowsSandbox(
483
503
  sandbox_id=data["sandbox_id"],
484
504
  http=self._http,
485
- vnc_url=f"{self._base_url}{data.get('vnc_url', '')}",
505
+ vnc_url=_absolute_url(self._base_url, data.get("vnc_url", "")),
486
506
  host=data.get("host", ""),
487
507
  version=data.get("version", ""),
488
508
  )
@@ -490,15 +510,15 @@ class AsyncComputer:
490
510
  return AsyncUbuntuSandbox(
491
511
  sandbox_id=data["sandbox_id"],
492
512
  http=self._http,
493
- vnc_url=f"{self._base_url}{data.get('vnc_url', '')}",
513
+ vnc_url=_absolute_url(self._base_url, data.get("vnc_url", "")),
494
514
  host=data.get("host", ""),
495
515
  version=data.get("version", ""),
496
516
  )
497
517
  return AsyncMacOSSandbox(
498
518
  sandbox_id=data["sandbox_id"],
499
519
  http=self._http,
500
- vnc_url=f"{self._base_url}{data.get('vnc_url', '')}",
501
- ssh_url=f"{self._base_url}{data.get('ssh_url', '')}",
520
+ vnc_url=_absolute_url(self._base_url, data.get("vnc_url", "")),
521
+ ssh_url=_absolute_url(self._base_url, data.get("ssh_url", "")),
502
522
  )
503
523
 
504
524
  async def adhoc_run(
@@ -0,0 +1,3 @@
1
+ from use_computer.resources.display import AsyncDisplay, Display
2
+
3
+ __all__ = ["AsyncDisplay", "Display"]
@@ -0,0 +1,3 @@
1
+ from use_computer.resources.recording import AsyncRecording, Recording
2
+
3
+ __all__ = ["AsyncRecording", "Recording"]
@@ -0,0 +1,17 @@
1
+ """Shared sandbox resource clients."""
2
+
3
+ from use_computer.resources.accessibility import Accessibility, AsyncAccessibility
4
+ from use_computer.resources.display import AsyncDisplay, Display
5
+ from use_computer.resources.recording import AsyncRecording, Recording
6
+ from use_computer.resources.screenshot import AsyncScreenshot, Screenshot
7
+
8
+ __all__ = [
9
+ "Accessibility",
10
+ "AsyncAccessibility",
11
+ "AsyncDisplay",
12
+ "AsyncRecording",
13
+ "AsyncScreenshot",
14
+ "Display",
15
+ "Recording",
16
+ "Screenshot",
17
+ ]
@@ -11,8 +11,6 @@ from pathlib import Path
11
11
 
12
12
  import httpx
13
13
 
14
- from use_computer.accessibility import Accessibility, AsyncAccessibility
15
- from use_computer.display import AsyncDisplay, Display
16
14
  from use_computer.errors import PlatformNotSupportedError
17
15
  from use_computer.ios.apps import Apps, AsyncApps
18
16
  from use_computer.ios.environment import AsyncEnvironment, Environment
@@ -20,8 +18,16 @@ from use_computer.ios.input import AsyncInput, Input
20
18
  from use_computer.macos.keyboard import AsyncKeyboard, Keyboard
21
19
  from use_computer.macos.mouse import AsyncMouse, Mouse
22
20
  from use_computer.models import ActResult, ExecResult
23
- from use_computer.recording import AsyncRecording, Recording
24
- from use_computer.screenshot import AsyncScreenshot, Screenshot
21
+ from use_computer.resources import (
22
+ Accessibility,
23
+ AsyncAccessibility,
24
+ AsyncDisplay,
25
+ AsyncRecording,
26
+ AsyncScreenshot,
27
+ Display,
28
+ Recording,
29
+ Screenshot,
30
+ )
25
31
  from use_computer.windows.shell import AsyncShell, Shell, ShellKind
26
32
 
27
33
 
@@ -0,0 +1,3 @@
1
+ from use_computer.resources.screenshot import AsyncScreenshot, Screenshot
2
+
3
+ __all__ = ["AsyncScreenshot", "Screenshot"]
@@ -87,8 +87,13 @@ class TasksClient:
87
87
  def __init__(self, http: httpx.Client):
88
88
  self._http = http
89
89
 
90
- def list(self, limit: int = 50, offset: int = 0) -> builtins.list[TaskSummary]:
91
- resp = self._http.get("/admin/tasks", params={"limit": limit, "offset": offset})
90
+ def list(
91
+ self, limit: int = 50, offset: int = 0, platform: str | None = None
92
+ ) -> builtins.list[TaskSummary]:
93
+ params: dict[str, int | str] = {"limit": limit, "offset": offset}
94
+ if platform:
95
+ params["platform"] = platform
96
+ resp = self._http.get("/admin/tasks", params=params)
92
97
  resp.raise_for_status()
93
98
  return [
94
99
  TaskSummary(
@@ -149,9 +154,13 @@ class TasksClient:
149
154
 
150
155
 
151
156
  def _build_test_sh(task: Task) -> str:
152
- """iOS = JSON checker DSL POSTed to /grade. macOS = bash with perl-alarm wrap."""
157
+ """Build verifier script for the task's platform."""
153
158
  if task.platform == "ios":
154
159
  return _build_test_sh_ios(task)
160
+ if task.platform == "windows":
161
+ return _build_test_sh_windows(task)
162
+ if task.platform == "ubuntu":
163
+ return _build_test_sh_ubuntu(task)
155
164
  return _build_test_sh_macos(task)
156
165
 
157
166
 
@@ -175,6 +184,24 @@ def _build_test_sh_macos(task: Task) -> str:
175
184
  return out
176
185
 
177
186
 
187
+ def _build_test_sh_windows(task: Task) -> str:
188
+ grader = (task.grader or "").strip()
189
+ if not grader:
190
+ return _tpl("test_windows_nograder.sh")
191
+ check_tpl = _tpl("test_windows_check.sh")
192
+ checks = _render(check_tpl, N="1", CMD=grader.replace("'", "'\\''"))
193
+ return _render(_tpl("test_windows.sh"), CHECKS=checks)
194
+
195
+
196
+ def _build_test_sh_ubuntu(task: Task) -> str:
197
+ grader = (task.grader or "").strip()
198
+ if not grader:
199
+ return _tpl("test_ubuntu_nograder.sh")
200
+ check_tpl = _tpl("test_ubuntu_check.sh")
201
+ checks = _render(check_tpl, N="1", CMD=grader.replace("'", "'\\''"))
202
+ return _render(_tpl("test_ubuntu.sh"), CHECKS=checks)
203
+
204
+
178
205
  def _build_pre_command_sh(task: Task) -> str | None:
179
206
  # `defaults delete` fails on fresh VMs (key never set). Tasks recording such
180
207
  # commands likely rely on the cleanup; skip the whole task instead of
@@ -189,6 +216,19 @@ def _build_pre_command_sh(task: Task) -> str | None:
189
216
  return _render(_tpl("pre_command.sh"), COMMANDS="\n".join(task.setup_commands))
190
217
 
191
218
 
219
+ def _build_pre_command(task: Task) -> tuple[str, str] | None:
220
+ if task.platform == "windows":
221
+ if not task.setup_commands:
222
+ return None
223
+ return "pre_command.ps1", _render(
224
+ _tpl("pre_command.ps1"), COMMANDS="\n".join(task.setup_commands)
225
+ )
226
+ pre = _build_pre_command_sh(task)
227
+ if not pre:
228
+ return None
229
+ return "pre_command.sh", pre
230
+
231
+
192
232
  def task_to_harbor(
193
233
  task: Task,
194
234
  output_root: Path,
@@ -206,7 +246,7 @@ def task_to_harbor(
206
246
  tests/
207
247
  test.sh
208
248
  setup/
209
- pre_command.sh
249
+ pre_command.sh | pre_command.ps1
210
250
  files/
211
251
  manifest.json
212
252
  <local_name>...
@@ -225,7 +265,7 @@ def task_to_harbor(
225
265
 
226
266
  # Build pre_command early so we fail before creating any dirs if all setup
227
267
  # commands got stripped (raises ValueError).
228
- pre_cmd = _build_pre_command_sh(task)
268
+ pre_cmd = _build_pre_command(task)
229
269
 
230
270
  if task_dir.exists():
231
271
  shutil.rmtree(task_dir)
@@ -277,13 +317,15 @@ def task_to_harbor(
277
317
  json.dumps({"steps": actions}, indent=2) + "\n", encoding="utf-8"
278
318
  )
279
319
 
280
- # tests/setup/pre_command.sh — skip directory entirely if no setup commands
320
+ # tests/setup/pre_command.* — skip directory entirely if no setup commands
281
321
  setup_dir = tests_dir / "setup"
282
322
  if pre_cmd:
323
+ pre_cmd_name, pre_cmd_text = pre_cmd
283
324
  setup_dir.mkdir(exist_ok=True)
284
- pre_cmd_path = setup_dir / "pre_command.sh"
285
- pre_cmd_path.write_text(pre_cmd, encoding="utf-8")
286
- pre_cmd_path.chmod(0o755)
325
+ pre_cmd_path = setup_dir / pre_cmd_name
326
+ pre_cmd_path.write_text(pre_cmd_text, encoding="utf-8")
327
+ if pre_cmd_name.endswith(".sh"):
328
+ pre_cmd_path.chmod(0o755)
287
329
 
288
330
  # tests/setup/files/<local_name> + manifest.json — files captured at
289
331
  # collect time. The runner's setup.py reads manifest.json and uploads
@@ -315,7 +357,10 @@ def task_to_harbor(
315
357
  logger.warning("collected file missing on gateway: %s", local_name)
316
358
  continue
317
359
  (files_dir / local_name).write_bytes(data)
318
- manifest.append({"remote_path": remote_path, "local_name": local_name})
360
+ entry = {"remote_path": remote_path, "local_name": local_name}
361
+ if task.platform in ("windows", "ubuntu"):
362
+ entry["vm_path"] = remote_path
363
+ manifest.append(entry)
319
364
  logger.info("exported file %s → %s (%dB)", local_name, remote_path, len(data))
320
365
  if manifest:
321
366
  (files_dir / "manifest.json").write_text(
@@ -0,0 +1,20 @@
1
+ #!/bin/bash
2
+ # Ubuntu test.sh runs on the runner host. Use the gateway SDK/API to inspect
3
+ # the live sandbox; bash state setup runs separately in pre_command.sh.
4
+ REWARD="${HARBOR_REWARD_FILE:-/logs/verifier/reward.txt}"
5
+ GRADER_LOG="${HARBOR_GRADER_LOG:-/logs/verifier/grader.log}"
6
+ : "${GATEWAY_URL:?GATEWAY_URL is required}"
7
+ : "${SANDBOX_ID:?SANDBOX_ID is required}"
8
+ : "${USE_COMPUTER_API_KEY:?USE_COMPUTER_API_KEY is required}"
9
+ export MMINI_API_KEY="${MMINI_API_KEY:-$USE_COMPUTER_API_KEY}"
10
+ mkdir -p "$(dirname "$GRADER_LOG")" 2>/dev/null || true
11
+
12
+ write_reward() {
13
+ mkdir -p "$(dirname "$REWARD")" 2>/dev/null || true
14
+ echo "$1" > "$REWARD" 2>/dev/null || true
15
+ echo "REWARD=$1"
16
+ }
17
+
18
+ {{CHECKS}}
19
+ write_reward 0
20
+ echo "Score: 0"
@@ -0,0 +1,10 @@
1
+ # Check {{N}}
2
+ _r=$(mktemp)
3
+ bash -c '{{CMD}}' > "$_r" 2>>"$GRADER_LOG"
4
+ if grep -qi "true" "$_r" 2>/dev/null; then
5
+ rm -f "$_r"
6
+ write_reward 1
7
+ echo "Score: 1"
8
+ exit 0
9
+ fi
10
+ rm -f "$_r"
@@ -0,0 +1,6 @@
1
+ #!/bin/bash
2
+ REWARD="${HARBOR_REWARD_FILE:-/logs/verifier/reward.txt}"
3
+ mkdir -p "$(dirname "$REWARD")" 2>/dev/null || true
4
+ echo "0" > "$REWARD" 2>/dev/null || true
5
+ echo "REWARD=0"
6
+ echo "Score: 0 (no grader)"
@@ -0,0 +1,20 @@
1
+ #!/bin/bash
2
+ # Windows test.sh runs on the runner host. Use the gateway SDK/API to inspect
3
+ # the live sandbox; PowerShell state setup runs separately in pre_command.ps1.
4
+ REWARD="${HARBOR_REWARD_FILE:-/logs/verifier/reward.txt}"
5
+ GRADER_LOG="${HARBOR_GRADER_LOG:-/logs/verifier/grader.log}"
6
+ : "${GATEWAY_URL:?GATEWAY_URL is required}"
7
+ : "${SANDBOX_ID:?SANDBOX_ID is required}"
8
+ : "${USE_COMPUTER_API_KEY:?USE_COMPUTER_API_KEY is required}"
9
+ export MMINI_API_KEY="${MMINI_API_KEY:-$USE_COMPUTER_API_KEY}"
10
+ mkdir -p "$(dirname "$GRADER_LOG")" 2>/dev/null || true
11
+
12
+ write_reward() {
13
+ mkdir -p "$(dirname "$REWARD")" 2>/dev/null || true
14
+ echo "$1" > "$REWARD" 2>/dev/null || true
15
+ echo "REWARD=$1"
16
+ }
17
+
18
+ {{CHECKS}}
19
+ write_reward 0
20
+ echo "Score: 0"
@@ -0,0 +1,10 @@
1
+ # Check {{N}}
2
+ _r=$(mktemp)
3
+ bash -c '{{CMD}}' > "$_r" 2>>"$GRADER_LOG"
4
+ if grep -qi "true" "$_r" 2>/dev/null; then
5
+ rm -f "$_r"
6
+ write_reward 1
7
+ echo "Score: 1"
8
+ exit 0
9
+ fi
10
+ rm -f "$_r"
@@ -0,0 +1,6 @@
1
+ #!/bin/bash
2
+ REWARD="${HARBOR_REWARD_FILE:-/logs/verifier/reward.txt}"
3
+ mkdir -p "$(dirname "$REWARD")" 2>/dev/null || true
4
+ echo "0" > "$REWARD" 2>/dev/null || true
5
+ echo "REWARD=0"
6
+ echo "Score: 0 (no grader)"
File without changes
File without changes