fleet-python 0.2.125__tar.gz → 0.2.127__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 (126) hide show
  1. {fleet_python-0.2.125/fleet_python.egg-info → fleet_python-0.2.127}/PKG-INFO +1 -1
  2. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/__init__.py +7 -1
  3. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/_async/__init__.py +1 -1
  4. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/_async/base.py +1 -1
  5. fleet_python-0.2.127/fleet/_async/browser.py +191 -0
  6. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/_async/client.py +94 -5
  7. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/_async/models.py +2 -0
  8. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/base.py +1 -1
  9. fleet_python-0.2.127/fleet/browser.py +216 -0
  10. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/client.py +99 -5
  11. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/models.py +2 -0
  12. {fleet_python-0.2.125 → fleet_python-0.2.127/fleet_python.egg-info}/PKG-INFO +1 -1
  13. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet_python.egg-info/SOURCES.txt +2 -0
  14. {fleet_python-0.2.125 → fleet_python-0.2.127}/pyproject.toml +1 -1
  15. {fleet_python-0.2.125 → fleet_python-0.2.127}/LICENSE +0 -0
  16. {fleet_python-0.2.125 → fleet_python-0.2.127}/README.md +0 -0
  17. {fleet_python-0.2.125 → fleet_python-0.2.127}/examples/diff_example.py +0 -0
  18. {fleet_python-0.2.125 → fleet_python-0.2.127}/examples/dsl_example.py +0 -0
  19. {fleet_python-0.2.125 → fleet_python-0.2.127}/examples/example.py +0 -0
  20. {fleet_python-0.2.125 → fleet_python-0.2.127}/examples/exampleResume.py +0 -0
  21. {fleet_python-0.2.125 → fleet_python-0.2.127}/examples/example_account.py +0 -0
  22. {fleet_python-0.2.125 → fleet_python-0.2.127}/examples/example_action_log.py +0 -0
  23. {fleet_python-0.2.125 → fleet_python-0.2.127}/examples/example_client.py +0 -0
  24. {fleet_python-0.2.125 → fleet_python-0.2.127}/examples/example_mcp_anthropic.py +0 -0
  25. {fleet_python-0.2.125 → fleet_python-0.2.127}/examples/example_mcp_openai.py +0 -0
  26. {fleet_python-0.2.125 → fleet_python-0.2.127}/examples/example_sync.py +0 -0
  27. {fleet_python-0.2.125 → fleet_python-0.2.127}/examples/example_task.py +0 -0
  28. {fleet_python-0.2.125 → fleet_python-0.2.127}/examples/example_tasks.py +0 -0
  29. {fleet_python-0.2.125 → fleet_python-0.2.127}/examples/example_verifier.py +0 -0
  30. {fleet_python-0.2.125 → fleet_python-0.2.127}/examples/export_tasks.py +0 -0
  31. {fleet_python-0.2.125 → fleet_python-0.2.127}/examples/export_tasks_filtered.py +0 -0
  32. {fleet_python-0.2.125 → fleet_python-0.2.127}/examples/fetch_tasks.py +0 -0
  33. {fleet_python-0.2.125 → fleet_python-0.2.127}/examples/gemini_example.py +0 -0
  34. {fleet_python-0.2.125 → fleet_python-0.2.127}/examples/import_tasks.py +0 -0
  35. {fleet_python-0.2.125 → fleet_python-0.2.127}/examples/iterate_verifiers.py +0 -0
  36. {fleet_python-0.2.125 → fleet_python-0.2.127}/examples/json_tasks_example.py +0 -0
  37. {fleet_python-0.2.125 → fleet_python-0.2.127}/examples/nova_act_example.py +0 -0
  38. {fleet_python-0.2.125 → fleet_python-0.2.127}/examples/openai_example.py +0 -0
  39. {fleet_python-0.2.125 → fleet_python-0.2.127}/examples/openai_simple_example.py +0 -0
  40. {fleet_python-0.2.125 → fleet_python-0.2.127}/examples/query_builder_example.py +0 -0
  41. {fleet_python-0.2.125 → fleet_python-0.2.127}/examples/quickstart.py +0 -0
  42. {fleet_python-0.2.125 → fleet_python-0.2.127}/examples/test_cdp_logging.py +0 -0
  43. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/_async/env/__init__.py +0 -0
  44. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/_async/env/client.py +0 -0
  45. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/_async/exceptions.py +0 -0
  46. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/_async/global_client.py +0 -0
  47. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/_async/instance/__init__.py +0 -0
  48. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/_async/instance/base.py +0 -0
  49. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/_async/instance/client.py +0 -0
  50. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/_async/judge.py +0 -0
  51. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/_async/resources/__init__.py +0 -0
  52. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/_async/resources/api.py +0 -0
  53. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/_async/resources/base.py +0 -0
  54. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/_async/resources/browser.py +0 -0
  55. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/_async/resources/filesystem.py +0 -0
  56. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/_async/resources/mcp.py +0 -0
  57. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/_async/resources/sqlite.py +0 -0
  58. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/_async/tasks.py +0 -0
  59. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/_async/verifiers/__init__.py +0 -0
  60. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/_async/verifiers/bundler.py +0 -0
  61. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/_async/verifiers/verifier.py +0 -0
  62. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/agent/__init__.py +0 -0
  63. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/agent/gemini_cua/Dockerfile +0 -0
  64. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/agent/gemini_cua/__init__.py +0 -0
  65. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/agent/gemini_cua/agent.py +0 -0
  66. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/agent/gemini_cua/mcp/main.py +0 -0
  67. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/agent/gemini_cua/mcp_server/__init__.py +0 -0
  68. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/agent/gemini_cua/mcp_server/main.py +0 -0
  69. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/agent/gemini_cua/mcp_server/tools.py +0 -0
  70. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/agent/gemini_cua/requirements.txt +0 -0
  71. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/agent/gemini_cua/start.sh +0 -0
  72. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/agent/orchestrator.py +0 -0
  73. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/agent/types.py +0 -0
  74. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/agent/utils.py +0 -0
  75. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/cli.py +0 -0
  76. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/config.py +0 -0
  77. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/env/__init__.py +0 -0
  78. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/env/client.py +0 -0
  79. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/eval/__init__.py +0 -0
  80. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/eval/uploader.py +0 -0
  81. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/exceptions.py +0 -0
  82. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/global_client.py +0 -0
  83. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/instance/__init__.py +0 -0
  84. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/instance/base.py +0 -0
  85. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/instance/client.py +0 -0
  86. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/instance/models.py +0 -0
  87. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/judge.py +0 -0
  88. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/proxy/__init__.py +0 -0
  89. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/proxy/proxy.py +0 -0
  90. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/proxy/whitelist.py +0 -0
  91. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/resources/__init__.py +0 -0
  92. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/resources/api.py +0 -0
  93. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/resources/base.py +0 -0
  94. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/resources/browser.py +0 -0
  95. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/resources/filesystem.py +0 -0
  96. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/resources/mcp.py +0 -0
  97. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/resources/sqlite.py +0 -0
  98. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/tasks.py +0 -0
  99. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/types.py +0 -0
  100. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/utils/__init__.py +0 -0
  101. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/utils/http_logging.py +0 -0
  102. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/utils/logging.py +0 -0
  103. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/utils/playwright.py +0 -0
  104. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/verifiers/__init__.py +0 -0
  105. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/verifiers/bundler.py +0 -0
  106. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/verifiers/code.py +0 -0
  107. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/verifiers/db.py +0 -0
  108. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/verifiers/decorator.py +0 -0
  109. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/verifiers/parse.py +0 -0
  110. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/verifiers/sql_differ.py +0 -0
  111. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet/verifiers/verifier.py +0 -0
  112. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet_python.egg-info/dependency_links.txt +0 -0
  113. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet_python.egg-info/entry_points.txt +0 -0
  114. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet_python.egg-info/requires.txt +0 -0
  115. {fleet_python-0.2.125 → fleet_python-0.2.127}/fleet_python.egg-info/top_level.txt +0 -0
  116. {fleet_python-0.2.125 → fleet_python-0.2.127}/scripts/fix_sync_imports.py +0 -0
  117. {fleet_python-0.2.125 → fleet_python-0.2.127}/scripts/unasync.py +0 -0
  118. {fleet_python-0.2.125 → fleet_python-0.2.127}/setup.cfg +0 -0
  119. {fleet_python-0.2.125 → fleet_python-0.2.127}/tests/__init__.py +0 -0
  120. {fleet_python-0.2.125 → fleet_python-0.2.127}/tests/test_app_method.py +0 -0
  121. {fleet_python-0.2.125 → fleet_python-0.2.127}/tests/test_expect_exactly.py +0 -0
  122. {fleet_python-0.2.125 → fleet_python-0.2.127}/tests/test_expect_only.py +0 -0
  123. {fleet_python-0.2.125 → fleet_python-0.2.127}/tests/test_instance_dispatch.py +0 -0
  124. {fleet_python-0.2.125 → fleet_python-0.2.127}/tests/test_sqlite_resource_dual_mode.py +0 -0
  125. {fleet_python-0.2.125 → fleet_python-0.2.127}/tests/test_sqlite_shared_memory_behavior.py +0 -0
  126. {fleet_python-0.2.125 → fleet_python-0.2.127}/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.125
3
+ Version: 0.2.127
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.125"
81
+ __version__ = "0.2.127"
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.125"
47
+ __version__ = "0.2.127"
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.125"
29
+ __version__ = "0.2.127"
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."""
@@ -601,7 +649,9 @@ class AsyncFleet:
601
649
  )
602
650
 
603
651
  instance = AsyncEnv(client=self.client, **response.json())
604
- await instance.instance.load()
652
+ # Resources are loaded lazily on first `db()`/`browser()`/`resources()` access
653
+ # via `_load_resources()`, so we don't preload here. Eagerly loading would
654
+ # fail-fast with a 502 while the container is still warming up.
605
655
  return instance
606
656
 
607
657
  async def make_for_task(self, task: Task) -> AsyncEnv:
@@ -653,7 +703,7 @@ class AsyncFleet:
653
703
  else:
654
704
  response = await self.client.request("GET", f"/v1/env/instances/{instance_id}")
655
705
  instance = AsyncEnv(client=self.client, **response.json())
656
- await instance.instance.load()
706
+ # Resources load lazily on first `db()`/`browser()`/`resources()` access.
657
707
  return instance
658
708
 
659
709
  def _create_url_instance(self, base_url: str) -> AsyncEnv:
@@ -794,6 +844,45 @@ class AsyncFleet:
794
844
  self.client, bundle_data, args, kwargs, timeout
795
845
  )
796
846
 
847
+ async def create_browser(
848
+ self,
849
+ ttl_seconds: int = 300,
850
+ *,
851
+ lease_id: Optional[str] = None,
852
+ allowed_hosts: Optional[List[str]] = None,
853
+ request_timestamp_ms: Optional[int] = None,
854
+ extra: Optional[Dict[str, Any]] = None,
855
+ jwt_token: Optional[str] = None,
856
+ team_id: Optional[str] = None,
857
+ wait_until_running: bool = False,
858
+ wait_timeout: float = 60.0,
859
+ ) -> AsyncBrowserLease:
860
+ """Create a Fleet Browser lease (``POST /v1/browser``)."""
861
+ return await _create_browser_lease(
862
+ self.client,
863
+ ttl_seconds=ttl_seconds,
864
+ lease_id=lease_id,
865
+ allowed_hosts=allowed_hosts,
866
+ request_timestamp_ms=request_timestamp_ms,
867
+ extra=extra,
868
+ jwt_token=jwt_token,
869
+ team_id=team_id,
870
+ wait_until_running=wait_until_running,
871
+ wait_timeout=wait_timeout,
872
+ )
873
+
874
+ async def get_browser(
875
+ self,
876
+ lease_id: str,
877
+ *,
878
+ jwt_token: Optional[str] = None,
879
+ team_id: Optional[str] = None,
880
+ ) -> AsyncBrowserLease:
881
+ """Inspect an existing browser lease (``GET /v1/browser/{lease_id}``)."""
882
+ return await _get_browser_lease(
883
+ self.client, lease_id, jwt_token=jwt_token, team_id=team_id
884
+ )
885
+
797
886
  async def delete(self, instance_id: str) -> InstanceResponse:
798
887
  return await _delete_instance(self.client, instance_id)
799
888
 
@@ -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):
@@ -357,6 +358,7 @@ class InstanceResponse(BaseModel):
357
358
  data_version: Optional[str] = Field(None, title="Data Version")
358
359
  urls: Optional[InstanceURLs] = Field(None, title="Urls")
359
360
  health: Optional[bool] = Field(None, title="Health")
361
+ multi_env_list: Optional[List[str]] = Field(None, title="Multi Env List")
360
362
 
361
363
 
362
364
  class AccountResponse(BaseModel):
@@ -27,7 +27,7 @@ from .exceptions import (
27
27
  try:
28
28
  from . import __version__
29
29
  except ImportError:
30
- __version__ = "0.2.125"
30
+ __version__ = "0.2.127"
31
31
 
32
32
  logger = logging.getLogger(__name__)
33
33
 
@@ -0,0 +1,216 @@
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
+ """Fleet Browser API — orchestrator-managed remote browser leases.
10
+
11
+ Thin wrapper around the `/v1/browser` HTTP surface documented at
12
+ `https://orchestrator.fleetai.com/docs`. A ``BrowserLease`` exposes the
13
+ ``cdp_url`` / ``mcp_url`` / ``stream_url`` returned by the create call and
14
+ lets you poll, list MCP tools, and release the lease.
15
+
16
+ This is intentionally independent of ``fleet.resources.browser`` (the
17
+ in-instance CDP resource); they solve different problems.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import time
23
+ from typing import Any, Dict, List, Optional, TYPE_CHECKING
24
+ from urllib.parse import urlparse
25
+
26
+ if TYPE_CHECKING:
27
+ from .base import SyncWrapper
28
+
29
+
30
+ _BROWSER_PATH = "/v1/browser"
31
+
32
+
33
+ def _extra_headers(
34
+ jwt_token: Optional[str] = None, team_id: Optional[str] = None
35
+ ) -> Optional[Dict[str, str]]:
36
+ headers: Dict[str, str] = {}
37
+ if jwt_token:
38
+ headers["X-JWT-Token"] = jwt_token
39
+ if team_id:
40
+ headers["X-Team-ID"] = team_id
41
+ return headers or None
42
+
43
+
44
+ def host_from_url(url: str) -> str:
45
+ """Return the bare hostname for a URL (no scheme, no port, no path).
46
+
47
+ Useful when populating ``allowed_hosts`` from ``env.urls.root``.
48
+ """
49
+ parsed = urlparse(url if "://" in url else f"https://{url}")
50
+ return parsed.hostname or url
51
+
52
+
53
+ class BrowserLease:
54
+ """A lease created via ``POST /v1/browser``.
55
+
56
+ Attribute names mirror the JSON response. Methods that hit the API
57
+ reuse the same :class:`SyncWrapper` that created the lease so auth /
58
+ base URL / retries stay consistent.
59
+ """
60
+
61
+ def __init__(self, client: "SyncWrapper", data: Dict[str, Any]):
62
+ self._client = client
63
+ self._raw: Dict[str, Any] = dict(data)
64
+ self._jwt_token: Optional[str] = None
65
+ self._team_id: Optional[str] = None
66
+ self._apply(data)
67
+
68
+ def _apply(self, data: Dict[str, Any]) -> None:
69
+ self.id: str = data.get("id") or data["lease_id"]
70
+ self.lease_id: str = data["lease_id"]
71
+ self.browser_id: Optional[str] = data.get("browser_id")
72
+ self.host_domain: Optional[str] = data.get("host_domain")
73
+ self.mcp_url: Optional[str] = data.get("mcp_url")
74
+ self.cdp_url: Optional[str] = data.get("cdp_url")
75
+ self.stream_url: Optional[str] = data.get("stream_url")
76
+ self.status: Optional[str] = data.get("status")
77
+ self.stream_ready: Optional[bool] = data.get("stream_ready")
78
+ self.allowed_hosts: Optional[List[str]] = data.get("allowed_hosts")
79
+ self.created_timestamp_ms: Optional[int] = data.get("created_timestamp_ms")
80
+ self.expires_timestamp_ms: Optional[int] = data.get("expires_timestamp_ms")
81
+ self.age_duration_ms: Optional[int] = data.get("age_duration_ms")
82
+ self.cluster_name: Optional[str] = data.get("cluster_name")
83
+
84
+ @property
85
+ def raw(self) -> Dict[str, Any]:
86
+ """Last server response payload (for fields not surfaced as attrs)."""
87
+ return self._raw
88
+
89
+ def refresh(self) -> "BrowserLease":
90
+ """Re-fetch the lease (``GET /v1/browser/{lease_id}``)."""
91
+ response = self._client.request(
92
+ "GET",
93
+ f"{_BROWSER_PATH}/{self.lease_id}",
94
+ extra_headers=_extra_headers(self._jwt_token, self._team_id),
95
+ )
96
+ self._raw = response.json()
97
+ self._apply(self._raw)
98
+ return self
99
+
100
+ def wait_until_running(
101
+ self,
102
+ timeout: float = 60.0,
103
+ poll_interval: float = 1.0,
104
+ require_stream_ready: bool = False,
105
+ ) -> "BrowserLease":
106
+ """Poll until ``status == 'running'`` (or stream_ready, if requested)."""
107
+ deadline = time.monotonic() + timeout
108
+ while True:
109
+ self.refresh()
110
+ running = self.status == "running"
111
+ if running and (not require_stream_ready or self.stream_ready):
112
+ return self
113
+ if time.monotonic() >= deadline:
114
+ raise TimeoutError(
115
+ f"Browser lease {self.lease_id} not running after "
116
+ f"{timeout}s (status={self.status}, stream_ready={self.stream_ready})"
117
+ )
118
+ time.sleep(poll_interval)
119
+
120
+ def mcp_tools(self) -> Dict[str, Any]:
121
+ """List MCP tools the browser exposes."""
122
+ response = self._client.request(
123
+ "GET",
124
+ f"{_BROWSER_PATH}/{self.lease_id}/mcp-tools",
125
+ extra_headers=_extra_headers(self._jwt_token, self._team_id),
126
+ )
127
+ return response.json()
128
+
129
+ def delete(self) -> None:
130
+ """Release the lease. Idempotent — ignores 404 if already gone."""
131
+ from .exceptions import FleetAPIError
132
+
133
+ try:
134
+ self._client.request(
135
+ "DELETE",
136
+ f"{_BROWSER_PATH}/{self.lease_id}",
137
+ extra_headers=_extra_headers(self._jwt_token, self._team_id),
138
+ )
139
+ except FleetAPIError as exc:
140
+ status = getattr(exc, "status_code", None)
141
+ if status not in (404, 410):
142
+ raise
143
+
144
+ # Context manager so `with client.create_browser(...) as br: ...` cleans up.
145
+ def __enter__(self) -> "BrowserLease":
146
+ return self
147
+
148
+ def __exit__(self, exc_type, exc, tb) -> None:
149
+ self.delete()
150
+
151
+ def __repr__(self) -> str:
152
+ return (
153
+ f"BrowserLease(lease_id={self.lease_id!r}, status={self.status!r}, "
154
+ f"cdp_url={self.cdp_url!r})"
155
+ )
156
+
157
+
158
+ def create_browser(
159
+ client: "SyncWrapper",
160
+ *,
161
+ ttl_seconds: int = 300,
162
+ lease_id: Optional[str] = None,
163
+ allowed_hosts: Optional[List[str]] = None,
164
+ request_timestamp_ms: Optional[int] = None,
165
+ extra: Optional[Dict[str, Any]] = None,
166
+ jwt_token: Optional[str] = None,
167
+ team_id: Optional[str] = None,
168
+ wait_until_running: bool = False,
169
+ wait_timeout: float = 60.0,
170
+ ) -> BrowserLease:
171
+ """POST ``/v1/browser`` and return a :class:`BrowserLease`.
172
+
173
+ ``extra`` is merged into the request body so callers can pass
174
+ fields that aren't first-class kwargs yet — keeps this freeform.
175
+ """
176
+ payload: Dict[str, Any] = {"ttl_seconds": ttl_seconds}
177
+ if lease_id is not None:
178
+ payload["lease_id"] = lease_id
179
+ if allowed_hosts is not None:
180
+ payload["allowed_hosts"] = allowed_hosts
181
+ if request_timestamp_ms is not None:
182
+ payload["request_timestamp_ms"] = request_timestamp_ms
183
+ if extra:
184
+ payload.update(extra)
185
+
186
+ response = client.request(
187
+ "POST",
188
+ _BROWSER_PATH,
189
+ json=payload,
190
+ extra_headers=_extra_headers(jwt_token, team_id),
191
+ )
192
+ lease = BrowserLease(client, response.json())
193
+ lease._jwt_token = jwt_token
194
+ lease._team_id = team_id
195
+ if wait_until_running:
196
+ lease.wait_until_running(timeout=wait_timeout)
197
+ return lease
198
+
199
+
200
+ def get_browser(
201
+ client: "SyncWrapper",
202
+ lease_id: str,
203
+ *,
204
+ jwt_token: Optional[str] = None,
205
+ team_id: Optional[str] = None,
206
+ ) -> BrowserLease:
207
+ """Inspect an existing lease (``GET /v1/browser/{lease_id}``)."""
208
+ response = client.request(
209
+ "GET",
210
+ f"{_BROWSER_PATH}/{lease_id}",
211
+ extra_headers=_extra_headers(jwt_token, team_id),
212
+ )
213
+ lease = BrowserLease(client, response.json())
214
+ lease._jwt_token = jwt_token
215
+ lease._team_id = team_id
216
+ return lease