use-computer 0.0.44__tar.gz → 0.0.45__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 (89) hide show
  1. {use_computer-0.0.44 → use_computer-0.0.45}/PKG-INFO +31 -17
  2. {use_computer-0.0.44 → use_computer-0.0.45}/README.md +29 -15
  3. {use_computer-0.0.44 → use_computer-0.0.45}/pyproject.toml +1 -1
  4. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/__init__.py +2 -0
  5. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/agents/base/agent.py +4 -4
  6. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/agents/base/task_setup.py +64 -1
  7. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/agents/ios/agent.py +3 -4
  8. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/core/__init__.py +2 -0
  9. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/core/client.py +21 -19
  10. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/core/errors.py +31 -2
  11. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/core/sandbox.py +12 -7
  12. {use_computer-0.0.44 → use_computer-0.0.45}/uv.lock +306 -306
  13. {use_computer-0.0.44 → use_computer-0.0.45}/.env.example +0 -0
  14. {use_computer-0.0.44 → use_computer-0.0.45}/.gitignore +0 -0
  15. {use_computer-0.0.44 → use_computer-0.0.45}/.pre-commit-config.yaml +0 -0
  16. {use_computer-0.0.44 → use_computer-0.0.45}/scripts/smoke_wheel.py +0 -0
  17. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/agents/__init__.py +0 -0
  18. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/agents/base/__init__.py +0 -0
  19. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/agents/base/actions.py +0 -0
  20. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/agents/base/artifacts.py +0 -0
  21. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/agents/base/desktop.py +0 -0
  22. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/agents/base/final_state.py +0 -0
  23. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/agents/base/prompts.py +0 -0
  24. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/agents/base/recordings.py +0 -0
  25. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/agents/base/sandbox.py +0 -0
  26. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/agents/base/screenshots.py +0 -0
  27. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/agents/base/trajectory.py +0 -0
  28. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/agents/core.py +0 -0
  29. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/agents/debug/README.md +0 -0
  30. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/agents/debug/__init__.py +0 -0
  31. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/agents/debug/actions.py +0 -0
  32. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/agents/debug/agent.py +0 -0
  33. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/agents/debug/models.py +0 -0
  34. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/agents/ios/__init__.py +0 -0
  35. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/agents/ios/tools.py +0 -0
  36. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/agents/prompts/anthropic.txt +0 -0
  37. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/agents/prompts/gemini.txt +0 -0
  38. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/agents/prompts/ios.txt +0 -0
  39. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/agents/prompts/openai.txt +0 -0
  40. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/agents/prompts/osworld_pyautogui.txt +0 -0
  41. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/agents/prompts/pyautogui.txt +0 -0
  42. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/agents/providers/__init__.py +0 -0
  43. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/agents/providers/anthropic.py +0 -0
  44. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/agents/providers/gemini.py +0 -0
  45. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/agents/providers/generic.py +0 -0
  46. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/agents/providers/openai.py +0 -0
  47. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/agents/providers/osworld.py +0 -0
  48. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/agents/providers/tinker_backend.py +0 -0
  49. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/agents/types.py +0 -0
  50. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/automation/__init__.py +0 -0
  51. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/automation/ax_transpile.py +0 -0
  52. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/automation/osworld.py +0 -0
  53. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/automation/parsers.py +0 -0
  54. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/core/models.py +0 -0
  55. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/core/retry.py +0 -0
  56. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/harbor/__init__.py +0 -0
  57. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/harbor/agents.py +0 -0
  58. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/ios/__init__.py +0 -0
  59. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/ios/apps.py +0 -0
  60. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/ios/environment.py +0 -0
  61. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/ios/input.py +0 -0
  62. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/ios/simulators.py +0 -0
  63. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/macos/__init__.py +0 -0
  64. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/macos/keyboard.py +0 -0
  65. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/macos/mouse.py +0 -0
  66. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/py.typed +0 -0
  67. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/resources/__init__.py +0 -0
  68. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/resources/accessibility.py +0 -0
  69. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/resources/display.py +0 -0
  70. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/resources/recording.py +0 -0
  71. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/resources/screenshot.py +0 -0
  72. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/setup_files.py +0 -0
  73. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/tasks/__init__.py +0 -0
  74. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/tasks/templates/pre_command.ps1 +0 -0
  75. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/tasks/templates/pre_command.sh +0 -0
  76. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/tasks/templates/task.toml +0 -0
  77. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/tasks/templates/test_ios.sh +0 -0
  78. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/tasks/templates/test_ios_nograder.sh +0 -0
  79. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/tasks/templates/test_macos.sh +0 -0
  80. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/tasks/templates/test_macos_check.sh +0 -0
  81. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/tasks/templates/test_macos_nograder.sh +0 -0
  82. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/tasks/templates/test_ubuntu.sh +0 -0
  83. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/tasks/templates/test_ubuntu_check.sh +0 -0
  84. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/tasks/templates/test_ubuntu_nograder.sh +0 -0
  85. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/tasks/templates/test_windows.sh +0 -0
  86. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/tasks/templates/test_windows_check.sh +0 -0
  87. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/tasks/templates/test_windows_nograder.sh +0 -0
  88. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/windows/__init__.py +0 -0
  89. {use_computer-0.0.44 → use_computer-0.0.45}/use_computer/windows/shell.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: use-computer
3
- Version: 0.0.44
3
+ Version: 0.0.45
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
@@ -99,14 +99,19 @@ with Computer().create(type="macos", reservation_id="...") as mac:
99
99
  print(mac.exec_ssh("uname -a").stdout)
100
100
  ```
101
101
 
102
+ Each reserved Mac Mini supports up to 2 parallel macOS VMs. Check
103
+ `Computer().platforms(reservation_id=...)["macos"]["capacity"]` for the selected
104
+ reservation's `max` and `used` counts. iOS capacity is reported separately under
105
+ `["ios"]["capacity"]`.
106
+
102
107
  ### Windows (Beta)
103
108
 
104
109
  ```python
105
110
  with Computer().create(type="windows") as win:
106
- print(win.run("$env:COMPUTERNAME").stdout) # PowerShell exec (no SSH)
111
+ print(win.run("$env:COMPUTERNAME").stdout) # PowerShell exec (no SSH)
107
112
  win.keyboard.type("hello")
108
113
  win.screenshot.take_full_screen()
109
- win.ui_tree() # native Windows UIAutomation tree
114
+ win.ui_tree() # native Windows UIAutomation tree
110
115
  ```
111
116
 
112
117
  Windows sandboxes are **Beta** (admin-only). Same mouse/keyboard/screenshot/
@@ -132,6 +137,10 @@ with Computer().create(type="ubuntu", version="ubuntu-24.04") as ubuntu:
132
137
 
133
138
  Ubuntu sandboxes are **Beta** (admin-only). They use the same KVM/noVNC backend
134
139
  as Windows; pass `version="ubuntu-24.04"` plus `resources` to select CPU/RAM/disk.
140
+ Ubuntu and Windows share a per-account KVM concurrency limit. The default is 8
141
+ running desktops; when the limit is hit, `create()` raises
142
+ `SandboxLimitReachedError` with `limit` and `running` fields from the gateway's
143
+ HTTP 429 response.
135
144
 
136
145
  Selectable Ubuntu images are exposed by `Computer().platforms()["ubuntu"]`.
137
146
  Current examples include `ubuntu-24.04`, `ubuntu-24.04-4c4g80g`,
@@ -153,27 +162,32 @@ with client.create(
153
162
 
154
163
  ### Snapshots (Beta)
155
164
 
156
- Windows and Ubuntu snapshots preserve disk + RAM state, including open apps and
157
- running processes. Seed a VM once, snapshot it, then create new sandboxes from
158
- that snapshot version.
165
+ macOS, Windows, and Ubuntu snapshots let you seed a VM once, snapshot it, then
166
+ create new sandboxes from that snapshot version. macOS snapshots preserve disk
167
+ state. Windows and Ubuntu snapshots preserve disk + RAM state, including open
168
+ apps and running processes.
169
+
170
+ macOS snapshot launch is slower because the gateway stores a portable disk-delta
171
+ artifact and transfers it to the Mac Mini that claims the new VM. Windows and
172
+ Ubuntu snapshots restore disk + RAM on their desktop host.
159
173
 
160
174
  ```python
161
175
  client = Computer()
162
176
 
163
177
  # Configure the desktop by hand over VNC, then snapshot it.
164
- with client.create(type="ubuntu", version="ubuntu-24.04") as ubuntu:
165
- print("Set up the desktop here:", ubuntu.vnc_url)
166
- # install apps, sign into accounts, pre-open the windows your agent starts from
178
+ with client.create(type="macos") as mac:
179
+ print("Set up the desktop here:", mac.vnc_url)
180
+ # install apps, sign into accounts, create files your agent starts from
167
181
  input("Press Enter once the desktop is ready to snapshot...")
168
- snapshot = ubuntu.snapshot("chrome-seeded-ubuntu")
182
+ snapshot = mac.snapshot("chrome-seeded-macos")
169
183
 
170
- # New sandboxes boot from that exact state, no setup needed.
171
- with client.create(type="ubuntu", snapshot=snapshot.version) as seeded:
172
- print(seeded.vnc_url) # apps, logins, and open windows restored
184
+ # New sandboxes boot from that saved state, no setup needed.
185
+ with client.create(type="macos", snapshot=snapshot.version) as seeded:
186
+ print(seeded.vnc_url) # installed apps, files, and logins restored
173
187
  ```
174
188
 
175
- Use `client.snapshots("ubuntu")` or `client.snapshots("windows")` to list saved
176
- snapshot versions.
189
+ Use `client.snapshots("macos")`, `client.snapshots("ubuntu")`, or
190
+ `client.snapshots("windows")` to list saved snapshot versions.
177
191
 
178
192
  Full DSL reference: [docs.use.computer/docs/sdk](https://docs.use.computer/docs/sdk)
179
193
 
@@ -212,7 +226,7 @@ visionOS yet. Use it for read-only flows for now.
212
226
  | [`examples/_7_hello_windows.py`](examples/_7_hello_windows.py) | create Windows → run PowerShell → screenshot |
213
227
  | [`examples/_8_hello_ubuntu.py`](examples/_8_hello_ubuntu.py) | create Ubuntu → run bash → screenshot |
214
228
  | [`examples/_9_seeding.py`](examples/_9_seeding.py) | typed setup: files, hosts, open URLs/apps |
215
- | [`examples/_10_snapshots.py`](examples/_10_snapshots.py) | snapshot seeded Ubuntu/Windows desktop state |
229
+ | [`examples/_10_snapshots.py`](examples/_10_snapshots.py) | snapshot seeded macOS/Ubuntu/Windows state |
216
230
 
217
231
  For agent loops and evals: [use-computer-cookbook](https://github.com/josancamon19/use-computer-cookbook). Harbor job YAMLs should use Harbor's `type: use-computer` environment plus `use_computer.harbor.agents:*` agent wrappers.
218
232
 
@@ -65,14 +65,19 @@ with Computer().create(type="macos", reservation_id="...") as mac:
65
65
  print(mac.exec_ssh("uname -a").stdout)
66
66
  ```
67
67
 
68
+ Each reserved Mac Mini supports up to 2 parallel macOS VMs. Check
69
+ `Computer().platforms(reservation_id=...)["macos"]["capacity"]` for the selected
70
+ reservation's `max` and `used` counts. iOS capacity is reported separately under
71
+ `["ios"]["capacity"]`.
72
+
68
73
  ### Windows (Beta)
69
74
 
70
75
  ```python
71
76
  with Computer().create(type="windows") as win:
72
- print(win.run("$env:COMPUTERNAME").stdout) # PowerShell exec (no SSH)
77
+ print(win.run("$env:COMPUTERNAME").stdout) # PowerShell exec (no SSH)
73
78
  win.keyboard.type("hello")
74
79
  win.screenshot.take_full_screen()
75
- win.ui_tree() # native Windows UIAutomation tree
80
+ win.ui_tree() # native Windows UIAutomation tree
76
81
  ```
77
82
 
78
83
  Windows sandboxes are **Beta** (admin-only). Same mouse/keyboard/screenshot/
@@ -98,6 +103,10 @@ with Computer().create(type="ubuntu", version="ubuntu-24.04") as ubuntu:
98
103
 
99
104
  Ubuntu sandboxes are **Beta** (admin-only). They use the same KVM/noVNC backend
100
105
  as Windows; pass `version="ubuntu-24.04"` plus `resources` to select CPU/RAM/disk.
106
+ Ubuntu and Windows share a per-account KVM concurrency limit. The default is 8
107
+ running desktops; when the limit is hit, `create()` raises
108
+ `SandboxLimitReachedError` with `limit` and `running` fields from the gateway's
109
+ HTTP 429 response.
101
110
 
102
111
  Selectable Ubuntu images are exposed by `Computer().platforms()["ubuntu"]`.
103
112
  Current examples include `ubuntu-24.04`, `ubuntu-24.04-4c4g80g`,
@@ -119,27 +128,32 @@ with client.create(
119
128
 
120
129
  ### Snapshots (Beta)
121
130
 
122
- Windows and Ubuntu snapshots preserve disk + RAM state, including open apps and
123
- running processes. Seed a VM once, snapshot it, then create new sandboxes from
124
- that snapshot version.
131
+ macOS, Windows, and Ubuntu snapshots let you seed a VM once, snapshot it, then
132
+ create new sandboxes from that snapshot version. macOS snapshots preserve disk
133
+ state. Windows and Ubuntu snapshots preserve disk + RAM state, including open
134
+ apps and running processes.
135
+
136
+ macOS snapshot launch is slower because the gateway stores a portable disk-delta
137
+ artifact and transfers it to the Mac Mini that claims the new VM. Windows and
138
+ Ubuntu snapshots restore disk + RAM on their desktop host.
125
139
 
126
140
  ```python
127
141
  client = Computer()
128
142
 
129
143
  # Configure the desktop by hand over VNC, then snapshot it.
130
- with client.create(type="ubuntu", version="ubuntu-24.04") as ubuntu:
131
- print("Set up the desktop here:", ubuntu.vnc_url)
132
- # install apps, sign into accounts, pre-open the windows your agent starts from
144
+ with client.create(type="macos") as mac:
145
+ print("Set up the desktop here:", mac.vnc_url)
146
+ # install apps, sign into accounts, create files your agent starts from
133
147
  input("Press Enter once the desktop is ready to snapshot...")
134
- snapshot = ubuntu.snapshot("chrome-seeded-ubuntu")
148
+ snapshot = mac.snapshot("chrome-seeded-macos")
135
149
 
136
- # New sandboxes boot from that exact state, no setup needed.
137
- with client.create(type="ubuntu", snapshot=snapshot.version) as seeded:
138
- print(seeded.vnc_url) # apps, logins, and open windows restored
150
+ # New sandboxes boot from that saved state, no setup needed.
151
+ with client.create(type="macos", snapshot=snapshot.version) as seeded:
152
+ print(seeded.vnc_url) # installed apps, files, and logins restored
139
153
  ```
140
154
 
141
- Use `client.snapshots("ubuntu")` or `client.snapshots("windows")` to list saved
142
- snapshot versions.
155
+ Use `client.snapshots("macos")`, `client.snapshots("ubuntu")`, or
156
+ `client.snapshots("windows")` to list saved snapshot versions.
143
157
 
144
158
  Full DSL reference: [docs.use.computer/docs/sdk](https://docs.use.computer/docs/sdk)
145
159
 
@@ -178,7 +192,7 @@ visionOS yet. Use it for read-only flows for now.
178
192
  | [`examples/_7_hello_windows.py`](examples/_7_hello_windows.py) | create Windows → run PowerShell → screenshot |
179
193
  | [`examples/_8_hello_ubuntu.py`](examples/_8_hello_ubuntu.py) | create Ubuntu → run bash → screenshot |
180
194
  | [`examples/_9_seeding.py`](examples/_9_seeding.py) | typed setup: files, hosts, open URLs/apps |
181
- | [`examples/_10_snapshots.py`](examples/_10_snapshots.py) | snapshot seeded Ubuntu/Windows desktop state |
195
+ | [`examples/_10_snapshots.py`](examples/_10_snapshots.py) | snapshot seeded macOS/Ubuntu/Windows state |
182
196
 
183
197
  For agent loops and evals: [use-computer-cookbook](https://github.com/josancamon19/use-computer-cookbook). Harbor job YAMLs should use Harbor's `type: use-computer` environment plus `use_computer.harbor.agents:*` agent wrappers.
184
198
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "use-computer"
7
- version = "0.0.44"
7
+ version = "0.0.45"
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"
@@ -5,6 +5,7 @@ from use_computer.core.errors import (
5
5
  BillingRequiredError,
6
6
  PlatformNotSupportedError,
7
7
  ReservationRequiredError,
8
+ SandboxLimitReachedError,
8
9
  UseComputerError,
9
10
  )
10
11
  from use_computer.core.models import (
@@ -73,6 +74,7 @@ __all__ = [
73
74
  "RemoteButton",
74
75
  "RunStatus",
75
76
  "Sandbox",
77
+ "SandboxLimitReachedError",
76
78
  "SandboxType",
77
79
  "Snapshot",
78
80
  "SimulatorChoice",
@@ -16,7 +16,7 @@ from use_computer.agents.base.desktop import (
16
16
  from use_computer.agents.base.final_state import FinalStateMixin
17
17
  from use_computer.agents.base.recordings import RecordingMixin
18
18
  from use_computer.agents.base.sandbox import ComputerSandbox
19
- from use_computer.agents.base.task_setup import run_task_setup
19
+ from use_computer.agents.base.task_setup import run_task_setup, task_dir_from_env
20
20
  from use_computer.agents.base.trajectory import write_trajectory
21
21
  from use_computer.agents.core import (
22
22
  AgentRunOptions,
@@ -31,13 +31,13 @@ from use_computer.core.sandbox import AsyncSandbox
31
31
 
32
32
  def _task_dir_from_env(environment: Any) -> Path:
33
33
  """Pull the task dir from the environment."""
34
- p = getattr(environment, "_task_dir", None) or getattr(environment, "task_dir", None)
34
+ p = task_dir_from_env(environment)
35
35
  if p is None:
36
36
  raise RuntimeError(
37
- "environment exposes neither _task_dir nor task_dir; "
37
+ "environment exposes neither task_dir nor task environment_dir; "
38
38
  "agent can't locate the task directory"
39
39
  )
40
- return Path(p).resolve()
40
+ return p
41
41
 
42
42
 
43
43
  class BaseCUAAgent(RecordingMixin, FinalStateMixin, ArtifactCaptureMixin):
@@ -3,10 +3,13 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
+ import importlib.util
7
+ import inspect
6
8
  import json
7
9
  import logging
8
10
  import re
9
11
  import shlex
12
+ import sys
10
13
  from pathlib import Path, PureWindowsPath
11
14
  from typing import Any
12
15
 
@@ -35,7 +38,11 @@ async def run_task_setup(environment: Any, logger: logging.Logger) -> None:
35
38
  platform = _platform_from_env(environment)
36
39
 
37
40
  _load_in_process_config(environment, setup_dir)
41
+ await _run_task_local_setup(environment, setup_dir, logger)
42
+
38
43
  if platform is Platform.IOS:
44
+ if setup_dir.exists():
45
+ logger.info("CUA task setup complete")
39
46
  return
40
47
 
41
48
  await _upload_setup_files(environment, setup_dir, platform, logger)
@@ -46,8 +53,25 @@ async def run_task_setup(environment: Any, logger: logging.Logger) -> None:
46
53
 
47
54
 
48
55
  def _task_dir_from_env(environment: Any) -> Path | None:
56
+ return task_dir_from_env(environment)
57
+
58
+
59
+ def task_dir_from_env(environment: Any) -> Path | None:
49
60
  raw = getattr(environment, "_task_dir", None) or getattr(environment, "task_dir", None)
50
- return Path(raw).resolve() if raw else None
61
+ if raw:
62
+ return Path(raw).resolve()
63
+
64
+ environment_dir = getattr(environment, "environment_dir", None)
65
+ if environment_dir:
66
+ path = Path(environment_dir).resolve()
67
+ if path.name == "environment" and _looks_like_task_dir(path.parent):
68
+ return path.parent
69
+
70
+ return None
71
+
72
+
73
+ def _looks_like_task_dir(path: Path) -> bool:
74
+ return (path / "task.toml").exists() and (path / "instruction.md").exists()
51
75
 
52
76
 
53
77
  def _platform_from_env(environment: Any) -> Platform:
@@ -143,6 +167,45 @@ async def _run_pre_command(
143
167
  await _run_macos_pre_command(environment, setup_dir, logger)
144
168
 
145
169
 
170
+ async def _run_task_local_setup(
171
+ environment: Any,
172
+ setup_dir: Path,
173
+ logger: logging.Logger,
174
+ ) -> None:
175
+ setup_py = setup_dir / "setup.py"
176
+ if not setup_py.exists():
177
+ return
178
+
179
+ logger.info("running setup.py")
180
+ spec = importlib.util.spec_from_file_location(
181
+ f"_harbor_task_setup_{abs(hash(setup_py.resolve()))}",
182
+ setup_py,
183
+ )
184
+ if spec is None or spec.loader is None:
185
+ raise RuntimeError(f"could not load task setup script: {setup_py}")
186
+
187
+ module = importlib.util.module_from_spec(spec)
188
+ setup_path = str(setup_dir)
189
+ sys.path.insert(0, setup_path)
190
+ try:
191
+ spec.loader.exec_module(module)
192
+ setup_task = getattr(module, "setup_task", None)
193
+ if not callable(setup_task):
194
+ raise RuntimeError(f"task setup script lacks setup_task(): {setup_py}")
195
+
196
+ result = setup_task(environment, logger, setup_dir)
197
+ if inspect.isawaitable(result):
198
+ await result
199
+ finally:
200
+ if sys.path and sys.path[0] == setup_path:
201
+ sys.path.pop(0)
202
+ else:
203
+ try:
204
+ sys.path.remove(setup_path)
205
+ except ValueError:
206
+ pass
207
+
208
+
146
209
  async def _run_windows_pre_command(
147
210
  environment: Any,
148
211
  setup_dir: Path,
@@ -32,6 +32,7 @@ from use_computer.agents.base import (
32
32
  load_prompt,
33
33
  scale_screenshot_for_model,
34
34
  )
35
+ from use_computer.agents.base.task_setup import task_dir_from_env
35
36
  from use_computer.agents.ios.tools import (
36
37
  _build_ios_system_prompt,
37
38
  _coerce_coord_args,
@@ -69,12 +70,10 @@ class IOSAgent(BaseCUAAgent):
69
70
 
70
71
  self.sandbox = sandbox
71
72
  if self.task_dir is None:
72
- task_dir = getattr(environment, "_task_dir", None) or getattr(
73
- environment, "task_dir", None
74
- )
73
+ task_dir = task_dir_from_env(environment)
75
74
  if task_dir is None:
76
75
  raise RuntimeError(
77
- "environment exposes neither _task_dir nor task_dir; "
76
+ "environment exposes neither task_dir nor task environment_dir; "
78
77
  "agent can't locate the task directory"
79
78
  )
80
79
  self.task_dir = task_dir
@@ -5,6 +5,7 @@ from use_computer.core.errors import (
5
5
  BillingRequiredError,
6
6
  PlatformNotSupportedError,
7
7
  ReservationRequiredError,
8
+ SandboxLimitReachedError,
8
9
  UseComputerError,
9
10
  )
10
11
  from use_computer.core.models import (
@@ -56,6 +57,7 @@ __all__ = [
56
57
  "ReservationRequiredError",
57
58
  "RunStatus",
58
59
  "Sandbox",
60
+ "SandboxLimitReachedError",
59
61
  "SandboxType",
60
62
  "Snapshot",
61
63
  "UbuntuSandbox",
@@ -4,6 +4,7 @@ import asyncio
4
4
  import os
5
5
  import time
6
6
  from dataclasses import dataclass, field
7
+ from importlib.metadata import PackageNotFoundError, version
7
8
  from typing import Any, Literal, overload
8
9
 
9
10
  import httpx
@@ -27,6 +28,11 @@ from use_computer.ios.simulators import SimulatorFamily, select_simulator
27
28
  from use_computer.tasks import TasksClient
28
29
 
29
30
  DEFAULT_BASE_URL = "https://api.use.computer"
31
+ SDK_VERSION = "0.0.0"
32
+ try:
33
+ SDK_VERSION = version("use-computer")
34
+ except PackageNotFoundError:
35
+ pass
30
36
 
31
37
  SandboxTypeInput = SandboxType | Literal["macos", "ios", "windows", "ubuntu"]
32
38
 
@@ -42,7 +48,7 @@ def _resolve_base_url(base_url: str | None) -> str:
42
48
 
43
49
 
44
50
  def _resolve_headers(api_key: str | None, host_header: str | None = None) -> dict[str, str]:
45
- headers: dict[str, str] = {}
51
+ headers: dict[str, str] = {"X-Use-Computer-SDK": f"python/{SDK_VERSION}"}
46
52
  if api_key:
47
53
  headers["Authorization"] = f"Bearer {api_key}"
48
54
  host = host_header if host_header is not None else os.getenv("USE_COMPUTER_HOST_HEADER")
@@ -172,7 +178,7 @@ class Computer:
172
178
  return resp.json()
173
179
 
174
180
  def snapshots(self, platform: str = "") -> list[dict[str, Any]]:
175
- """List Windur desktop snapshots visible to this API key."""
181
+ """List desktop snapshots visible to this API key."""
176
182
  params = {"platform": platform} if platform else None
177
183
  resp = self._http.get("/v1/snapshots", params=params)
178
184
  resp.raise_for_status()
@@ -195,6 +201,7 @@ class Computer:
195
201
  type: Literal["macos", SandboxType.MACOS] = ...,
196
202
  reservation_id: str = ...,
197
203
  host: str = ...,
204
+ snapshot: str = ...,
198
205
  ephemeral: bool = ...,
199
206
  ) -> MacOSSandbox: ...
200
207
 
@@ -260,7 +267,7 @@ class Computer:
260
267
  runtime: Simulator runtime identifier, for exact pinning.
261
268
  version: KVM desktop image version, for example "windows-11",
262
269
  "windows-10", "ubuntu-24.04", or "osworld".
263
- snapshot: Windur snapshot version returned by sandbox.snapshot().
270
+ snapshot: Snapshot version returned by sandbox.snapshot().
264
271
  resources: KVM CPU/RAM/disk request used to select the baked image
265
272
  profile, for example {"cpus": 4, "memory_mb": 4096, "disk_gb": 40}.
266
273
  ephemeral: If true, delete the sandbox after the idle timeout.
@@ -280,6 +287,8 @@ class Computer:
280
287
  body["version"] = version
281
288
  if resources:
282
289
  body["resources"] = resources
290
+ elif sandbox_type == SandboxType.MACOS and snapshot:
291
+ body["snapshot"] = snapshot
283
292
  if sandbox_type == SandboxType.IOS:
284
293
  if host:
285
294
  body["mini_ip"] = host
@@ -378,17 +387,7 @@ class Computer:
378
387
  poll: bool = True,
379
388
  poll_interval: float = 3.0,
380
389
  ) -> RunStatus:
381
- """Run an ad-hoc task: spin up a sandbox and drive a model against a free-form instruction.
382
-
383
- Args:
384
- instruction: What the agent should do.
385
- platform: "macos", "ios", "windows", or "ubuntu".
386
- model: Model to use.
387
- max_steps: Max agent steps.
388
- files: Optional list of {remote_path, content_b64} to pre-upload.
389
- poll: If True, block until the run finishes.
390
- poll_interval: Seconds between status polls.
391
- """
390
+ """Run an ad-hoc task (requires admin base URL and key)."""
392
391
  body: dict[str, Any] = {
393
392
  "instruction": instruction,
394
393
  "platform": platform,
@@ -405,7 +404,7 @@ class Computer:
405
404
  return self._poll_run(status.id, poll_interval)
406
405
 
407
406
  def get_run(self, run_id: str) -> RunStatus:
408
- """Get current status of a model run."""
407
+ """Get current status of a model run (requires admin base URL and key)."""
409
408
  resp = self._http.get(f"/admin/tasks/model-runs/{run_id}")
410
409
  resp.raise_for_status()
411
410
  return RunStatus._from_dict(resp.json())
@@ -454,7 +453,7 @@ class AsyncComputer:
454
453
  return resp.json()
455
454
 
456
455
  async def snapshots(self, platform: str = "") -> list[dict[str, Any]]:
457
- """List Windur desktop snapshots visible to this API key."""
456
+ """List desktop snapshots visible to this API key."""
458
457
  params = {"platform": platform} if platform else None
459
458
  resp = await self._http.get("/v1/snapshots", params=params)
460
459
  resp.raise_for_status()
@@ -477,6 +476,7 @@ class AsyncComputer:
477
476
  type: Literal["macos", SandboxType.MACOS] = ...,
478
477
  reservation_id: str = ...,
479
478
  host: str = ...,
479
+ snapshot: str = ...,
480
480
  ephemeral: bool = ...,
481
481
  ) -> AsyncMacOSSandbox: ...
482
482
 
@@ -542,7 +542,7 @@ class AsyncComputer:
542
542
  runtime: Simulator runtime identifier, for exact pinning.
543
543
  version: KVM desktop image version, for example "windows-11",
544
544
  "windows-10", "ubuntu-24.04", or "osworld".
545
- snapshot: Windur snapshot version returned by sandbox.snapshot().
545
+ snapshot: Snapshot version returned by sandbox.snapshot().
546
546
  resources: KVM CPU/RAM/disk request used to select the baked image
547
547
  profile, for example {"cpus": 4, "memory_mb": 4096, "disk_gb": 40}.
548
548
  ephemeral: If true, delete the sandbox after the idle timeout.
@@ -562,6 +562,8 @@ class AsyncComputer:
562
562
  body["version"] = version
563
563
  if resources:
564
564
  body["resources"] = resources
565
+ elif sandbox_type == SandboxType.MACOS and snapshot:
566
+ body["snapshot"] = snapshot
565
567
  if sandbox_type == SandboxType.IOS:
566
568
  if host:
567
569
  body["mini_ip"] = host
@@ -662,7 +664,7 @@ class AsyncComputer:
662
664
  poll: bool = True,
663
665
  poll_interval: float = 3.0,
664
666
  ) -> RunStatus:
665
- """Run an ad-hoc task and drive a model against a free-form instruction."""
667
+ """Run an ad-hoc task (requires admin base URL and key)."""
666
668
  body: dict[str, Any] = {
667
669
  "instruction": instruction,
668
670
  "platform": platform,
@@ -679,7 +681,7 @@ class AsyncComputer:
679
681
  return await self._poll_run(status.id, poll_interval)
680
682
 
681
683
  async def get_run(self, run_id: str) -> RunStatus:
682
- """Get current status of a model run."""
684
+ """Get current status of a model run (requires admin base URL and key)."""
683
685
  resp = await self._http.get(f"/admin/tasks/model-runs/{run_id}")
684
686
  resp.raise_for_status()
685
687
  return RunStatus._from_dict(resp.json())
@@ -50,6 +50,26 @@ class ReservationRequiredError(UseComputerError):
50
50
  super().__init__(message or "macOS requires an active Mac Mini reservation")
51
51
 
52
52
 
53
+ class SandboxLimitReachedError(UseComputerError):
54
+ """Raised when the account has too many running Ubuntu/Windows sandboxes."""
55
+
56
+ def __init__(
57
+ self,
58
+ message: str,
59
+ *,
60
+ limit: int | None = None,
61
+ running: int | None = None,
62
+ response: httpx.Response | None = None,
63
+ ):
64
+ self.message = message
65
+ self.limit = limit
66
+ self.running = running
67
+ self.response = response
68
+ super().__init__(
69
+ message or "Ubuntu/Windows sandbox limit reached. Delete one sandbox to launch another."
70
+ )
71
+
72
+
53
73
  def _typed_error_from_response(resp: httpx.Response) -> UseComputerError | None:
54
74
  try:
55
75
  data = resp.json()
@@ -70,6 +90,15 @@ def _typed_error_from_response(resp: httpx.Response) -> UseComputerError | None:
70
90
  return BillingRequiredError(message=message, response=resp)
71
91
  if resp.status_code == 409 and error_code == "macos_reservation_required":
72
92
  return ReservationRequiredError(message=message, response=resp)
93
+ if resp.status_code == 429 and error_code == "sandbox_limit_reached":
94
+ limit = data.get("limit")
95
+ running = data.get("running")
96
+ return SandboxLimitReachedError(
97
+ message=message,
98
+ limit=limit if isinstance(limit, int) else None,
99
+ running=running if isinstance(running, int) else None,
100
+ response=resp,
101
+ )
73
102
  return None
74
103
 
75
104
 
@@ -80,7 +109,7 @@ def _raise_if_unsupported(resp: httpx.Response) -> None:
80
109
  only touch the body for statuses we own — anything else is left
81
110
  untouched for raise_for_status() to handle normally.
82
111
  """
83
- if resp.status_code not in (402, 409, 501):
112
+ if resp.status_code not in (402, 409, 429, 501):
84
113
  return
85
114
  try:
86
115
  resp.read()
@@ -92,7 +121,7 @@ def _raise_if_unsupported(resp: httpx.Response) -> None:
92
121
 
93
122
 
94
123
  async def _araise_if_unsupported(resp: httpx.Response) -> None:
95
- if resp.status_code not in (402, 409, 501):
124
+ if resp.status_code not in (402, 409, 429, 501):
96
125
  return
97
126
  try:
98
127
  await resp.aread()
@@ -4,6 +4,7 @@ import base64
4
4
  import io
5
5
  import tarfile
6
6
  import threading
7
+ import urllib.error
7
8
  import urllib.request
8
9
  from dataclasses import dataclass
9
10
  from enum import Enum
@@ -41,7 +42,7 @@ class SandboxType(str, Enum):
41
42
 
42
43
  @dataclass
43
44
  class Snapshot:
44
- """Windur-backed desktop snapshot."""
45
+ """Desktop snapshot."""
45
46
 
46
47
  name: str
47
48
  version: str
@@ -83,6 +84,10 @@ def _start_keepalive_thread(
83
84
  try:
84
85
  req = urllib.request.Request(keepalive_url, method="POST", headers=headers)
85
86
  urllib.request.urlopen(req, timeout=10).close()
87
+ except urllib.error.HTTPError as exc:
88
+ if exc.code in (404, 410):
89
+ stop.set()
90
+ break
86
91
  except Exception:
87
92
  pass
88
93
 
@@ -167,12 +172,12 @@ class Sandbox:
167
172
  local_path.write_bytes(resp.content)
168
173
 
169
174
  def snapshot(self, name: str, *, version: str = "", timeout: int = 900) -> Snapshot:
170
- """Capture a Windur desktop snapshot and return its image version."""
171
- if self.type not in (SandboxType.WINDOWS, SandboxType.UBUNTU):
175
+ """Capture a desktop snapshot and return its version."""
176
+ if self.type not in (SandboxType.MACOS, SandboxType.WINDOWS, SandboxType.UBUNTU):
172
177
  raise PlatformNotSupportedError(
173
178
  action="snapshot",
174
179
  sandbox_type=self.type.value,
175
- hint="snapshots are supported for Windows and Ubuntu sandboxes",
180
+ hint="snapshots are supported for macOS, Windows, and Ubuntu sandboxes",
176
181
  )
177
182
  body = {"name": name}
178
183
  if version:
@@ -601,12 +606,12 @@ class AsyncSandbox:
601
606
  local_path.write_bytes(resp.content)
602
607
 
603
608
  async def snapshot(self, name: str, *, version: str = "", timeout: int = 900) -> Snapshot:
604
- """Capture a Windur desktop snapshot and return its image version."""
605
- if self.type not in (SandboxType.WINDOWS, SandboxType.UBUNTU):
609
+ """Capture a desktop snapshot and return its version."""
610
+ if self.type not in (SandboxType.MACOS, SandboxType.WINDOWS, SandboxType.UBUNTU):
606
611
  raise PlatformNotSupportedError(
607
612
  action="snapshot",
608
613
  sandbox_type=self.type.value,
609
- hint="snapshots are supported for Windows and Ubuntu sandboxes",
614
+ hint="snapshots are supported for macOS, Windows, and Ubuntu sandboxes",
610
615
  )
611
616
  body = {"name": name}
612
617
  if version: