fleet-python 0.2.128__tar.gz → 0.2.130__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 (127) hide show
  1. {fleet_python-0.2.128/fleet_python.egg-info → fleet_python-0.2.130}/PKG-INFO +1 -1
  2. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/__init__.py +7 -1
  3. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/_async/__init__.py +1 -1
  4. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/_async/base.py +1 -1
  5. fleet_python-0.2.130/fleet/_async/browser.py +191 -0
  6. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/_async/client.py +100 -5
  7. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/_async/models.py +5 -0
  8. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/_async/tasks.py +36 -6
  9. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/_async/verifiers/verifier.py +11 -1
  10. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/base.py +1 -1
  11. fleet_python-0.2.130/fleet/browser.py +216 -0
  12. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/client.py +105 -5
  13. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/models.py +5 -0
  14. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/tasks.py +36 -6
  15. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/verifiers/verifier.py +11 -1
  16. {fleet_python-0.2.128 → fleet_python-0.2.130/fleet_python.egg-info}/PKG-INFO +1 -1
  17. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet_python.egg-info/SOURCES.txt +3 -0
  18. {fleet_python-0.2.128 → fleet_python-0.2.130}/pyproject.toml +1 -1
  19. fleet_python-0.2.130/tests/test_verifier_cost_team.py +198 -0
  20. {fleet_python-0.2.128 → fleet_python-0.2.130}/LICENSE +0 -0
  21. {fleet_python-0.2.128 → fleet_python-0.2.130}/README.md +0 -0
  22. {fleet_python-0.2.128 → fleet_python-0.2.130}/examples/diff_example.py +0 -0
  23. {fleet_python-0.2.128 → fleet_python-0.2.130}/examples/dsl_example.py +0 -0
  24. {fleet_python-0.2.128 → fleet_python-0.2.130}/examples/example.py +0 -0
  25. {fleet_python-0.2.128 → fleet_python-0.2.130}/examples/exampleResume.py +0 -0
  26. {fleet_python-0.2.128 → fleet_python-0.2.130}/examples/example_account.py +0 -0
  27. {fleet_python-0.2.128 → fleet_python-0.2.130}/examples/example_action_log.py +0 -0
  28. {fleet_python-0.2.128 → fleet_python-0.2.130}/examples/example_client.py +0 -0
  29. {fleet_python-0.2.128 → fleet_python-0.2.130}/examples/example_mcp_anthropic.py +0 -0
  30. {fleet_python-0.2.128 → fleet_python-0.2.130}/examples/example_mcp_openai.py +0 -0
  31. {fleet_python-0.2.128 → fleet_python-0.2.130}/examples/example_sync.py +0 -0
  32. {fleet_python-0.2.128 → fleet_python-0.2.130}/examples/example_task.py +0 -0
  33. {fleet_python-0.2.128 → fleet_python-0.2.130}/examples/example_tasks.py +0 -0
  34. {fleet_python-0.2.128 → fleet_python-0.2.130}/examples/example_verifier.py +0 -0
  35. {fleet_python-0.2.128 → fleet_python-0.2.130}/examples/export_tasks.py +0 -0
  36. {fleet_python-0.2.128 → fleet_python-0.2.130}/examples/export_tasks_filtered.py +0 -0
  37. {fleet_python-0.2.128 → fleet_python-0.2.130}/examples/fetch_tasks.py +0 -0
  38. {fleet_python-0.2.128 → fleet_python-0.2.130}/examples/gemini_example.py +0 -0
  39. {fleet_python-0.2.128 → fleet_python-0.2.130}/examples/import_tasks.py +0 -0
  40. {fleet_python-0.2.128 → fleet_python-0.2.130}/examples/iterate_verifiers.py +0 -0
  41. {fleet_python-0.2.128 → fleet_python-0.2.130}/examples/json_tasks_example.py +0 -0
  42. {fleet_python-0.2.128 → fleet_python-0.2.130}/examples/nova_act_example.py +0 -0
  43. {fleet_python-0.2.128 → fleet_python-0.2.130}/examples/openai_example.py +0 -0
  44. {fleet_python-0.2.128 → fleet_python-0.2.130}/examples/openai_simple_example.py +0 -0
  45. {fleet_python-0.2.128 → fleet_python-0.2.130}/examples/query_builder_example.py +0 -0
  46. {fleet_python-0.2.128 → fleet_python-0.2.130}/examples/quickstart.py +0 -0
  47. {fleet_python-0.2.128 → fleet_python-0.2.130}/examples/test_cdp_logging.py +0 -0
  48. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/_async/env/__init__.py +0 -0
  49. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/_async/env/client.py +0 -0
  50. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/_async/exceptions.py +0 -0
  51. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/_async/global_client.py +0 -0
  52. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/_async/instance/__init__.py +0 -0
  53. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/_async/instance/base.py +0 -0
  54. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/_async/instance/client.py +0 -0
  55. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/_async/judge.py +0 -0
  56. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/_async/resources/__init__.py +0 -0
  57. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/_async/resources/api.py +0 -0
  58. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/_async/resources/base.py +0 -0
  59. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/_async/resources/browser.py +0 -0
  60. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/_async/resources/filesystem.py +0 -0
  61. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/_async/resources/mcp.py +0 -0
  62. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/_async/resources/sqlite.py +0 -0
  63. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/_async/verifiers/__init__.py +0 -0
  64. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/_async/verifiers/bundler.py +0 -0
  65. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/agent/__init__.py +0 -0
  66. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/agent/gemini_cua/Dockerfile +0 -0
  67. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/agent/gemini_cua/__init__.py +0 -0
  68. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/agent/gemini_cua/agent.py +0 -0
  69. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/agent/gemini_cua/mcp/main.py +0 -0
  70. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/agent/gemini_cua/mcp_server/__init__.py +0 -0
  71. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/agent/gemini_cua/mcp_server/main.py +0 -0
  72. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/agent/gemini_cua/mcp_server/tools.py +0 -0
  73. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/agent/gemini_cua/requirements.txt +0 -0
  74. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/agent/gemini_cua/start.sh +0 -0
  75. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/agent/orchestrator.py +0 -0
  76. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/agent/types.py +0 -0
  77. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/agent/utils.py +0 -0
  78. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/cli.py +0 -0
  79. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/config.py +0 -0
  80. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/env/__init__.py +0 -0
  81. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/env/client.py +0 -0
  82. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/eval/__init__.py +0 -0
  83. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/eval/uploader.py +0 -0
  84. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/exceptions.py +0 -0
  85. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/global_client.py +0 -0
  86. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/instance/__init__.py +0 -0
  87. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/instance/base.py +0 -0
  88. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/instance/client.py +0 -0
  89. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/instance/models.py +0 -0
  90. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/judge.py +0 -0
  91. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/proxy/__init__.py +0 -0
  92. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/proxy/proxy.py +0 -0
  93. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/proxy/whitelist.py +0 -0
  94. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/resources/__init__.py +0 -0
  95. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/resources/api.py +0 -0
  96. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/resources/base.py +0 -0
  97. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/resources/browser.py +0 -0
  98. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/resources/filesystem.py +0 -0
  99. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/resources/mcp.py +0 -0
  100. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/resources/sqlite.py +0 -0
  101. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/types.py +0 -0
  102. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/utils/__init__.py +0 -0
  103. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/utils/http_logging.py +0 -0
  104. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/utils/logging.py +0 -0
  105. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/utils/playwright.py +0 -0
  106. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/verifiers/__init__.py +0 -0
  107. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/verifiers/bundler.py +0 -0
  108. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/verifiers/code.py +0 -0
  109. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/verifiers/db.py +0 -0
  110. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/verifiers/decorator.py +0 -0
  111. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/verifiers/parse.py +0 -0
  112. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet/verifiers/sql_differ.py +0 -0
  113. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet_python.egg-info/dependency_links.txt +0 -0
  114. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet_python.egg-info/entry_points.txt +0 -0
  115. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet_python.egg-info/requires.txt +0 -0
  116. {fleet_python-0.2.128 → fleet_python-0.2.130}/fleet_python.egg-info/top_level.txt +0 -0
  117. {fleet_python-0.2.128 → fleet_python-0.2.130}/scripts/fix_sync_imports.py +0 -0
  118. {fleet_python-0.2.128 → fleet_python-0.2.130}/scripts/unasync.py +0 -0
  119. {fleet_python-0.2.128 → fleet_python-0.2.130}/setup.cfg +0 -0
  120. {fleet_python-0.2.128 → fleet_python-0.2.130}/tests/__init__.py +0 -0
  121. {fleet_python-0.2.128 → fleet_python-0.2.130}/tests/test_app_method.py +0 -0
  122. {fleet_python-0.2.128 → fleet_python-0.2.130}/tests/test_expect_exactly.py +0 -0
  123. {fleet_python-0.2.128 → fleet_python-0.2.130}/tests/test_expect_only.py +0 -0
  124. {fleet_python-0.2.128 → fleet_python-0.2.130}/tests/test_instance_dispatch.py +0 -0
  125. {fleet_python-0.2.128 → fleet_python-0.2.130}/tests/test_sqlite_resource_dual_mode.py +0 -0
  126. {fleet_python-0.2.128 → fleet_python-0.2.130}/tests/test_sqlite_shared_memory_behavior.py +0 -0
  127. {fleet_python-0.2.128 → fleet_python-0.2.130}/tests/test_verifier_from_string.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fleet-python
3
- Version: 0.2.128
3
+ Version: 0.2.130
4
4
  Summary: Python SDK for Fleet environments
5
5
  Author-email: Fleet AI <nic@fleet.so>
6
6
  License: Apache-2.0
@@ -26,6 +26,8 @@ from .exceptions import (
26
26
  )
27
27
  from .client import Fleet, SyncEnv, Session
28
28
  from ._async.client import AsyncFleet, AsyncEnv, AsyncSession
29
+ from .browser import BrowserLease, host_from_url
30
+ from ._async.browser import AsyncBrowserLease
29
31
  from .models import InstanceResponse, Environment, Run
30
32
  from .instance.models import Resource, ResetResponse
31
33
 
@@ -76,7 +78,7 @@ from . import env
76
78
  from . import global_client as _global_client
77
79
  from ._async import global_client as _async_global_client
78
80
 
79
- __version__ = "0.2.128"
81
+ __version__ = "0.2.130"
80
82
 
81
83
  __all__ = [
82
84
  # Core classes
@@ -84,6 +86,10 @@ __all__ = [
84
86
  "SyncEnv",
85
87
  "AsyncFleet",
86
88
  "AsyncEnv",
89
+ # Browser lease (orchestrator-managed /v1/browser)
90
+ "BrowserLease",
91
+ "AsyncBrowserLease",
92
+ "host_from_url",
87
93
  # Models
88
94
  "InstanceResponse",
89
95
  "SyncEnv",
@@ -44,7 +44,7 @@ from ..types import VerifierFunction
44
44
  from .. import env
45
45
  from . import global_client as _async_global_client
46
46
 
47
- __version__ = "0.2.128"
47
+ __version__ = "0.2.130"
48
48
 
49
49
  __all__ = [
50
50
  # Core classes
@@ -26,7 +26,7 @@ from .exceptions import (
26
26
  try:
27
27
  from .. import __version__
28
28
  except ImportError:
29
- __version__ = "0.2.128"
29
+ __version__ = "0.2.130"
30
30
 
31
31
  logger = logging.getLogger(__name__)
32
32
 
@@ -0,0 +1,191 @@
1
+ # Copyright 2025 Fleet AI
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ """Async mirror of ``fleet.browser``.
10
+
11
+ See :mod:`fleet.browser` for design notes. Kept as a separate file so the
12
+ async surface stays cleanly importable without dragging the sync wrapper
13
+ through any asyncio shims.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import asyncio
19
+ from typing import Any, Dict, List, Optional, TYPE_CHECKING
20
+
21
+ from ..browser import host_from_url # re-export, identical logic
22
+
23
+ if TYPE_CHECKING:
24
+ from .base import AsyncWrapper
25
+
26
+
27
+ _BROWSER_PATH = "/v1/browser"
28
+
29
+
30
+ def _extra_headers(
31
+ jwt_token: Optional[str] = None, team_id: Optional[str] = None
32
+ ) -> Optional[Dict[str, str]]:
33
+ headers: Dict[str, str] = {}
34
+ if jwt_token:
35
+ headers["X-JWT-Token"] = jwt_token
36
+ if team_id:
37
+ headers["X-Team-ID"] = team_id
38
+ return headers or None
39
+
40
+
41
+ class AsyncBrowserLease:
42
+ """Async counterpart of :class:`fleet.browser.BrowserLease`."""
43
+
44
+ def __init__(self, client: "AsyncWrapper", data: Dict[str, Any]):
45
+ self._client = client
46
+ self._raw: Dict[str, Any] = dict(data)
47
+ self._jwt_token: Optional[str] = None
48
+ self._team_id: Optional[str] = None
49
+ self._apply(data)
50
+
51
+ def _apply(self, data: Dict[str, Any]) -> None:
52
+ self.id: str = data.get("id") or data["lease_id"]
53
+ self.lease_id: str = data["lease_id"]
54
+ self.browser_id: Optional[str] = data.get("browser_id")
55
+ self.host_domain: Optional[str] = data.get("host_domain")
56
+ self.mcp_url: Optional[str] = data.get("mcp_url")
57
+ self.cdp_url: Optional[str] = data.get("cdp_url")
58
+ self.stream_url: Optional[str] = data.get("stream_url")
59
+ self.status: Optional[str] = data.get("status")
60
+ self.stream_ready: Optional[bool] = data.get("stream_ready")
61
+ self.allowed_hosts: Optional[List[str]] = data.get("allowed_hosts")
62
+ self.created_timestamp_ms: Optional[int] = data.get("created_timestamp_ms")
63
+ self.expires_timestamp_ms: Optional[int] = data.get("expires_timestamp_ms")
64
+ self.age_duration_ms: Optional[int] = data.get("age_duration_ms")
65
+ self.cluster_name: Optional[str] = data.get("cluster_name")
66
+
67
+ @property
68
+ def raw(self) -> Dict[str, Any]:
69
+ return self._raw
70
+
71
+ async def refresh(self) -> "AsyncBrowserLease":
72
+ response = await self._client.request(
73
+ "GET",
74
+ f"{_BROWSER_PATH}/{self.lease_id}",
75
+ extra_headers=_extra_headers(self._jwt_token, self._team_id),
76
+ )
77
+ self._raw = response.json()
78
+ self._apply(self._raw)
79
+ return self
80
+
81
+ async def wait_until_running(
82
+ self,
83
+ timeout: float = 60.0,
84
+ poll_interval: float = 1.0,
85
+ require_stream_ready: bool = False,
86
+ ) -> "AsyncBrowserLease":
87
+ loop = asyncio.get_event_loop()
88
+ deadline = loop.time() + timeout
89
+ while True:
90
+ await self.refresh()
91
+ running = self.status == "running"
92
+ if running and (not require_stream_ready or self.stream_ready):
93
+ return self
94
+ if loop.time() >= deadline:
95
+ raise TimeoutError(
96
+ f"Browser lease {self.lease_id} not running after "
97
+ f"{timeout}s (status={self.status}, stream_ready={self.stream_ready})"
98
+ )
99
+ await asyncio.sleep(poll_interval)
100
+
101
+ async def mcp_tools(self) -> Dict[str, Any]:
102
+ response = await self._client.request(
103
+ "GET",
104
+ f"{_BROWSER_PATH}/{self.lease_id}/mcp-tools",
105
+ extra_headers=_extra_headers(self._jwt_token, self._team_id),
106
+ )
107
+ return response.json()
108
+
109
+ async def delete(self) -> None:
110
+ from ..exceptions import FleetAPIError
111
+
112
+ try:
113
+ await self._client.request(
114
+ "DELETE",
115
+ f"{_BROWSER_PATH}/{self.lease_id}",
116
+ extra_headers=_extra_headers(self._jwt_token, self._team_id),
117
+ )
118
+ except FleetAPIError as exc:
119
+ status = getattr(exc, "status_code", None)
120
+ if status not in (404, 410):
121
+ raise
122
+
123
+ async def __aenter__(self) -> "AsyncBrowserLease":
124
+ return self
125
+
126
+ async def __aexit__(self, exc_type, exc, tb) -> None:
127
+ await self.delete()
128
+
129
+ def __repr__(self) -> str:
130
+ return (
131
+ f"AsyncBrowserLease(lease_id={self.lease_id!r}, status={self.status!r}, "
132
+ f"cdp_url={self.cdp_url!r})"
133
+ )
134
+
135
+
136
+ async def create_browser(
137
+ client: "AsyncWrapper",
138
+ *,
139
+ ttl_seconds: int = 300,
140
+ lease_id: Optional[str] = None,
141
+ allowed_hosts: Optional[List[str]] = None,
142
+ request_timestamp_ms: Optional[int] = None,
143
+ extra: Optional[Dict[str, Any]] = None,
144
+ jwt_token: Optional[str] = None,
145
+ team_id: Optional[str] = None,
146
+ wait_until_running: bool = False,
147
+ wait_timeout: float = 60.0,
148
+ ) -> AsyncBrowserLease:
149
+ payload: Dict[str, Any] = {"ttl_seconds": ttl_seconds}
150
+ if lease_id is not None:
151
+ payload["lease_id"] = lease_id
152
+ if allowed_hosts is not None:
153
+ payload["allowed_hosts"] = allowed_hosts
154
+ if request_timestamp_ms is not None:
155
+ payload["request_timestamp_ms"] = request_timestamp_ms
156
+ if extra:
157
+ payload.update(extra)
158
+
159
+ response = await client.request(
160
+ "POST",
161
+ _BROWSER_PATH,
162
+ json=payload,
163
+ extra_headers=_extra_headers(jwt_token, team_id),
164
+ )
165
+ lease = AsyncBrowserLease(client, response.json())
166
+ lease._jwt_token = jwt_token
167
+ lease._team_id = team_id
168
+ if wait_until_running:
169
+ await lease.wait_until_running(timeout=wait_timeout)
170
+ return lease
171
+
172
+
173
+ async def get_browser(
174
+ client: "AsyncWrapper",
175
+ lease_id: str,
176
+ *,
177
+ jwt_token: Optional[str] = None,
178
+ team_id: Optional[str] = None,
179
+ ) -> AsyncBrowserLease:
180
+ response = await client.request(
181
+ "GET",
182
+ f"{_BROWSER_PATH}/{lease_id}",
183
+ extra_headers=_extra_headers(jwt_token, team_id),
184
+ )
185
+ lease = AsyncBrowserLease(client, response.json())
186
+ lease._jwt_token = jwt_token
187
+ lease._team_id = team_id
188
+ return lease
189
+
190
+
191
+ __all__ = ["AsyncBrowserLease", "create_browser", "get_browser", "host_from_url"]
@@ -171,10 +171,15 @@ from .instance.base import default_httpx_client
171
171
  from .instance.client import ValidatorType
172
172
  from .resources.base import Resource
173
173
  from .resources.sqlite import AsyncSQLiteResource
174
- from .resources.browser import AsyncBrowserResource
175
174
  from .resources.filesystem import AsyncFilesystemResource
176
175
  from .resources.mcp import AsyncMCPResource
177
176
  from .resources.api import AsyncAPIResource
177
+ from .browser import (
178
+ AsyncBrowserLease,
179
+ create_browser as _create_browser_lease,
180
+ get_browser as _get_browser_lease,
181
+ host_from_url,
182
+ )
178
183
 
179
184
  logger = logging.getLogger(__name__)
180
185
 
@@ -386,8 +391,51 @@ class AsyncEnv(EnvironmentBase):
386
391
  def db(self, name: str = "current") -> AsyncSQLiteResource:
387
392
  return self.instance.db(name)
388
393
 
389
- def browser(self, name: str = "cdp") -> AsyncBrowserResource:
390
- return self.instance.browser(name)
394
+ async def browser(
395
+ self,
396
+ ttl_seconds: int = 300,
397
+ *,
398
+ lease_id: Optional[str] = None,
399
+ allowed_hosts: Optional[List[str]] = None,
400
+ include_root_host: bool = True,
401
+ wait_until_running: bool = False,
402
+ wait_timeout: float = 60.0,
403
+ extra: Optional[Dict[str, Any]] = None,
404
+ jwt_token: Optional[str] = None,
405
+ team_id: Optional[str] = None,
406
+ ) -> AsyncBrowserLease:
407
+ """Spin up an orchestrator-managed Fleet Browser lease for this env.
408
+
409
+ ``await env.browser()`` posts to ``/v1/browser`` and returns an
410
+ :class:`fleet._async.browser.AsyncBrowserLease` with ``cdp_url`` /
411
+ ``mcp_url`` / ``stream_url`` and a ``mcp_tools()`` accessor. By
412
+ default the host derived from ``self.urls.root`` is prepended to
413
+ ``allowed_hosts`` so the browser can reach the instance — pass
414
+ ``include_root_host=False`` to opt out.
415
+ """
416
+ hosts: Optional[List[str]] = list(allowed_hosts) if allowed_hosts else None
417
+ if include_root_host and self.urls and self.urls.root:
418
+ root_host = host_from_url(self.urls.root)
419
+ if root_host:
420
+ hosts = hosts or []
421
+ if root_host not in hosts:
422
+ hosts.insert(0, root_host)
423
+ return await _create_browser_lease(
424
+ self._load_client,
425
+ ttl_seconds=ttl_seconds,
426
+ lease_id=lease_id,
427
+ allowed_hosts=hosts,
428
+ extra=extra,
429
+ jwt_token=jwt_token,
430
+ team_id=team_id,
431
+ wait_until_running=wait_until_running,
432
+ wait_timeout=wait_timeout,
433
+ )
434
+
435
+ @property
436
+ def root_url(self) -> Optional[str]:
437
+ """Convenience: ``self.urls.root`` if available."""
438
+ return self.urls.root if self.urls else None
391
439
 
392
440
  def fs(self) -> AsyncFilesystemResource:
393
441
  """Get a filesystem diff resource for inspecting file changes."""
@@ -484,6 +532,7 @@ class AsyncEnv(EnvironmentBase):
484
532
  verifier_runtime_version: Optional[str] = None,
485
533
  async_: bool = False,
486
534
  poll_interval: float = 5.0,
535
+ cost_team_id: Optional[str] = None,
487
536
  ) -> VerifiersExecuteResponse:
488
537
  return await _execute_verifier_remote(
489
538
  self._load_client,
@@ -499,6 +548,7 @@ class AsyncEnv(EnvironmentBase):
499
548
  verifier_runtime_version,
500
549
  async_=async_,
501
550
  poll_interval=poll_interval,
551
+ cost_team_id=cost_team_id,
502
552
  )
503
553
 
504
554
  def __getstate__(self):
@@ -605,7 +655,9 @@ class AsyncFleet:
605
655
  )
606
656
 
607
657
  instance = AsyncEnv(client=self.client, **response.json())
608
- await instance.instance.load()
658
+ # Resources are loaded lazily on first `db()`/`browser()`/`resources()` access
659
+ # via `_load_resources()`, so we don't preload here. Eagerly loading would
660
+ # fail-fast with a 502 while the container is still warming up.
609
661
  return instance
610
662
 
611
663
  async def make_for_task(self, task: Task) -> AsyncEnv:
@@ -657,7 +709,7 @@ class AsyncFleet:
657
709
  else:
658
710
  response = await self.client.request("GET", f"/v1/env/instances/{instance_id}")
659
711
  instance = AsyncEnv(client=self.client, **response.json())
660
- await instance.instance.load()
712
+ # Resources load lazily on first `db()`/`browser()`/`resources()` access.
661
713
  return instance
662
714
 
663
715
  def _create_url_instance(self, base_url: str) -> AsyncEnv:
@@ -798,6 +850,45 @@ class AsyncFleet:
798
850
  self.client, bundle_data, args, kwargs, timeout
799
851
  )
800
852
 
853
+ async def create_browser(
854
+ self,
855
+ ttl_seconds: int = 300,
856
+ *,
857
+ lease_id: Optional[str] = None,
858
+ allowed_hosts: Optional[List[str]] = None,
859
+ request_timestamp_ms: Optional[int] = None,
860
+ extra: Optional[Dict[str, Any]] = None,
861
+ jwt_token: Optional[str] = None,
862
+ team_id: Optional[str] = None,
863
+ wait_until_running: bool = False,
864
+ wait_timeout: float = 60.0,
865
+ ) -> AsyncBrowserLease:
866
+ """Create a Fleet Browser lease (``POST /v1/browser``)."""
867
+ return await _create_browser_lease(
868
+ self.client,
869
+ ttl_seconds=ttl_seconds,
870
+ lease_id=lease_id,
871
+ allowed_hosts=allowed_hosts,
872
+ request_timestamp_ms=request_timestamp_ms,
873
+ extra=extra,
874
+ jwt_token=jwt_token,
875
+ team_id=team_id,
876
+ wait_until_running=wait_until_running,
877
+ wait_timeout=wait_timeout,
878
+ )
879
+
880
+ async def get_browser(
881
+ self,
882
+ lease_id: str,
883
+ *,
884
+ jwt_token: Optional[str] = None,
885
+ team_id: Optional[str] = None,
886
+ ) -> AsyncBrowserLease:
887
+ """Inspect an existing browser lease (``GET /v1/browser/{lease_id}``)."""
888
+ return await _get_browser_lease(
889
+ self.client, lease_id, jwt_token=jwt_token, team_id=team_id
890
+ )
891
+
801
892
  async def delete(self, instance_id: str) -> InstanceResponse:
802
893
  return await _delete_instance(self.client, instance_id)
803
894
 
@@ -1731,6 +1822,7 @@ async def _execute_verifier_remote(
1731
1822
  verifier_runtime_version: Optional[str] = None,
1732
1823
  async_: bool = False,
1733
1824
  poll_interval: float = 5.0,
1825
+ cost_team_id: Optional[str] = None,
1734
1826
  ) -> VerifiersExecuteResponse:
1735
1827
  # Pickle args and kwargs together
1736
1828
  # The first arg should be None as a placeholder for env
@@ -1758,6 +1850,9 @@ async def _execute_verifier_remote(
1758
1850
  if verifier_runtime_version:
1759
1851
  request_data["verifier_runtime_version"] = verifier_runtime_version
1760
1852
 
1853
+ if cost_team_id is not None:
1854
+ request_data["cost_team_id"] = cost_team_id
1855
+
1761
1856
  # Async submit-and-poll path. When async_ is False the behavior below is
1762
1857
  # identical to the original synchronous request.
1763
1858
  if async_:
@@ -51,6 +51,7 @@ class Instance(BaseModel):
51
51
  team_id: str = Field(..., title="Team Id")
52
52
  region: str = Field(..., title="Region")
53
53
  env_variables: Optional[Dict[str, Any]] = Field(None, title="Env Variables")
54
+ multi_env_list: Optional[List[str]] = Field(None, title="Multi Env List")
54
55
 
55
56
 
56
57
  class InstanceRequest(BaseModel):
@@ -221,6 +222,9 @@ class VerifiersCheckResponse(BaseModel):
221
222
 
222
223
 
223
224
  class VerifiersExecuteRequest(BaseModel):
225
+ cost_team_id: Optional[str] = Field(
226
+ None, description="Team to attribute verifier costs to", title="Cost Team Id"
227
+ )
224
228
  key: Optional[str] = Field(
225
229
  None, description="Key of the verifier artifact", title="Key"
226
230
  )
@@ -373,6 +377,7 @@ class InstanceResponse(BaseModel):
373
377
  data_version: Optional[str] = Field(None, title="Data Version")
374
378
  urls: Optional[InstanceURLs] = Field(None, title="Urls")
375
379
  health: Optional[bool] = Field(None, title="Health")
380
+ multi_env_list: Optional[List[str]] = Field(None, title="Multi Env List")
376
381
 
377
382
 
378
383
  class AccountResponse(BaseModel):
@@ -85,6 +85,7 @@ class Task(BaseModel):
85
85
  self,
86
86
  env,
87
87
  *args,
88
+ cost_team_id: Optional[str] = None,
88
89
  async_: bool = False,
89
90
  poll_interval: float = 5.0,
90
91
  **kwargs,
@@ -108,7 +109,12 @@ class Task(BaseModel):
108
109
  import inspect
109
110
 
110
111
  result = self.verifier.remote(
111
- env, *args, async_=async_, poll_interval=poll_interval, **kwargs
112
+ env,
113
+ *args,
114
+ cost_team_id=cost_team_id,
115
+ async_=async_,
116
+ poll_interval=poll_interval,
117
+ **kwargs,
112
118
  )
113
119
 
114
120
  # If the result is a coroutine, we need to run it
@@ -130,7 +136,12 @@ class Task(BaseModel):
130
136
  raise ValueError("No verifier function found for this task")
131
137
 
132
138
  async def verify_async(
133
- self, *args, async_: bool = False, poll_interval: float = 5.0, **kwargs
139
+ self,
140
+ *args,
141
+ cost_team_id: Optional[str] = None,
142
+ async_: bool = False,
143
+ poll_interval: float = 5.0,
144
+ **kwargs,
134
145
  ) -> float:
135
146
  """Verify the task using the verifier function (async version).
136
147
 
@@ -148,7 +159,11 @@ class Task(BaseModel):
148
159
 
149
160
  if self.verifier:
150
161
  result = self.verifier.remote(
151
- *args, async_=async_, poll_interval=poll_interval, **kwargs
162
+ *args,
163
+ cost_team_id=cost_team_id,
164
+ async_=async_,
165
+ poll_interval=poll_interval,
166
+ **kwargs,
152
167
  )
153
168
  # If it's a coroutine, await it
154
169
  import inspect
@@ -161,7 +176,12 @@ class Task(BaseModel):
161
176
  raise ValueError("No verifier function found for this task")
162
177
 
163
178
  async def verify_detailed_async(
164
- self, *args, async_: bool = False, poll_interval: float = 5.0, **kwargs
179
+ self,
180
+ *args,
181
+ cost_team_id: Optional[str] = None,
182
+ async_: bool = False,
183
+ poll_interval: float = 5.0,
184
+ **kwargs,
165
185
  ) -> "VerifiersExecuteResponse":
166
186
  """Verify the task and return the full execute response model.
167
187
 
@@ -179,7 +199,11 @@ class Task(BaseModel):
179
199
 
180
200
  if self.verifier:
181
201
  result = self.verifier.remote_with_response(
182
- *args, async_=async_, poll_interval=poll_interval, **kwargs
202
+ *args,
203
+ cost_team_id=cost_team_id,
204
+ async_=async_,
205
+ poll_interval=poll_interval,
206
+ **kwargs,
183
207
  )
184
208
  # If it's a coroutine, await it
185
209
  import inspect
@@ -195,6 +219,7 @@ class Task(BaseModel):
195
219
  self,
196
220
  env,
197
221
  *args,
222
+ cost_team_id: Optional[str] = None,
198
223
  async_: bool = False,
199
224
  poll_interval: float = 5.0,
200
225
  **kwargs,
@@ -219,7 +244,12 @@ class Task(BaseModel):
219
244
 
220
245
  # Check if verifier has remote_with_response method (for decorated verifiers)
221
246
  result = self.verifier.remote_with_response(
222
- env, *args, async_=async_, poll_interval=poll_interval, **kwargs
247
+ env,
248
+ *args,
249
+ cost_team_id=cost_team_id,
250
+ async_=async_,
251
+ poll_interval=poll_interval,
252
+ **kwargs,
223
253
  )
224
254
 
225
255
  # If the result is a coroutine, we need to run it
@@ -158,6 +158,7 @@ class AsyncVerifierFunction:
158
158
  self,
159
159
  env: AsyncEnv,
160
160
  *args,
161
+ cost_team_id: Optional[str] = None,
161
162
  async_: bool = False,
162
163
  poll_interval: float = 5.0,
163
164
  **kwargs,
@@ -171,7 +172,12 @@ class AsyncVerifierFunction:
171
172
  request blocks until the verifier finishes).
172
173
  """
173
174
  response = await self.remote_with_response(
174
- env, *args, async_=async_, poll_interval=poll_interval, **kwargs
175
+ env,
176
+ *args,
177
+ cost_team_id=cost_team_id,
178
+ async_=async_,
179
+ poll_interval=poll_interval,
180
+ **kwargs,
175
181
  )
176
182
 
177
183
  # Handle response
@@ -247,6 +253,7 @@ Remote traceback:
247
253
  self,
248
254
  env: "AsyncEnv",
249
255
  *args,
256
+ cost_team_id: Optional[str] = None,
250
257
  async_: bool = False,
251
258
  poll_interval: float = 5.0,
252
259
  **kwargs,
@@ -281,6 +288,7 @@ Remote traceback:
281
288
  kwargs=kwargs,
282
289
  needs_upload=True,
283
290
  verifier_runtime_version=self.verifier_runtime_version,
291
+ cost_team_id=cost_team_id,
284
292
  async_=async_,
285
293
  poll_interval=poll_interval,
286
294
  )
@@ -300,6 +308,7 @@ Remote traceback:
300
308
  kwargs=kwargs,
301
309
  needs_upload=False,
302
310
  verifier_runtime_version=self.verifier_runtime_version,
311
+ cost_team_id=cost_team_id,
303
312
  async_=async_,
304
313
  poll_interval=poll_interval,
305
314
  )
@@ -323,6 +332,7 @@ Remote traceback:
323
332
  kwargs=kwargs,
324
333
  needs_upload=True,
325
334
  verifier_runtime_version=self.verifier_runtime_version,
335
+ cost_team_id=cost_team_id,
326
336
  async_=async_,
327
337
  poll_interval=poll_interval,
328
338
  )
@@ -27,7 +27,7 @@ from .exceptions import (
27
27
  try:
28
28
  from . import __version__
29
29
  except ImportError:
30
- __version__ = "0.2.128"
30
+ __version__ = "0.2.130"
31
31
 
32
32
  logger = logging.getLogger(__name__)
33
33