fleet-python 0.2.83__py3-none-any.whl → 0.2.85__py3-none-any.whl

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.
fleet/__init__.py CHANGED
@@ -73,7 +73,7 @@ from . import env
73
73
  from . import global_client as _global_client
74
74
  from ._async import global_client as _async_global_client
75
75
 
76
- __version__ = "0.2.83"
76
+ __version__ = "0.2.85"
77
77
 
78
78
  __all__ = [
79
79
  # Core classes
fleet/_async/__init__.py CHANGED
@@ -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.83"
47
+ __version__ = "0.2.85"
48
48
 
49
49
  __all__ = [
50
50
  # Core classes
fleet/_async/base.py CHANGED
@@ -26,7 +26,7 @@ from .exceptions import (
26
26
  try:
27
27
  from .. import __version__
28
28
  except ImportError:
29
- __version__ = "0.2.83"
29
+ __version__ = "0.2.85"
30
30
 
31
31
  logger = logging.getLogger(__name__)
32
32
 
@@ -0,0 +1,44 @@
1
+ # MCP Server - Browser control in Docker with optional VNC
2
+ FROM python:3.11-slim
3
+
4
+ # Install dependencies for Chromium and VNC
5
+ RUN apt-get update && apt-get install -y --no-install-recommends \
6
+ # Chromium dependencies
7
+ wget fonts-liberation libasound2 libatk-bridge2.0-0 libatk1.0-0 \
8
+ libatspi2.0-0 libcups2 libdbus-1-3 libdrm2 libgbm1 libgtk-3-0 \
9
+ libnspr4 libnss3 libxcomposite1 libxdamage1 libxfixes3 libxkbcommon0 \
10
+ libxrandr2 xdg-utils \
11
+ # VNC and display for headful mode
12
+ xvfb x11vnc fluxbox \
13
+ # noVNC for web-based viewing
14
+ novnc websockify \
15
+ # Utilities
16
+ procps net-tools \
17
+ && rm -rf /var/lib/apt/lists/*
18
+
19
+ WORKDIR /app
20
+
21
+ # Install Python deps
22
+ COPY requirements.txt .
23
+ RUN pip install --no-cache-dir -r requirements.txt && playwright install chromium
24
+
25
+ # Copy server files (all from same directory)
26
+ COPY playwright_utils.py .
27
+ COPY mcp_server.py .
28
+ COPY start.sh .
29
+ RUN chmod +x start.sh
30
+
31
+ # Environment
32
+ ENV PORT=8765 \
33
+ SCREEN_WIDTH=1366 \
34
+ SCREEN_HEIGHT=768 \
35
+ HEADLESS=true \
36
+ VNC_PORT=5900 \
37
+ NOVNC_PORT=6080 \
38
+ DISPLAY=:99
39
+
40
+ # Expose ports: MCP server, VNC, noVNC
41
+ EXPOSE 8765 5900 6080
42
+
43
+ # Start script handles display setup
44
+ CMD ["./start.sh"]
@@ -25,6 +25,7 @@ from mcp import ClientSession
25
25
  from mcp.client.streamable_http import streamable_http_client
26
26
  from google import genai
27
27
  from google.genai import types
28
+ import fleet
28
29
  from fleet.utils.logging import log_verbose, VERBOSE
29
30
 
30
31
  # Whitelist hooks for auto-detecting model endpoints (optional)
@@ -136,20 +137,36 @@ class MCP:
136
137
  result = await self._session.call_tool(name, args or {})
137
138
  duration_ms = int((time.time() - start_time) * 1000)
138
139
 
140
+ # Debug: log raw MCP result structure
141
+ log_verbose(f" MCP result.content ({len(result.content)} items):")
142
+ for i, item in enumerate(result.content):
143
+ log_verbose(f" [{i}] type={type(item).__name__}, attrs={dir(item)[:10]}...")
144
+ if hasattr(item, "type"):
145
+ log_verbose(f" .type = {repr(item.type)}")
146
+ if hasattr(item, "data"):
147
+ data_preview = str(item.data)[:50] if item.data else "None"
148
+ log_verbose(f" .data = {data_preview}...")
149
+
150
+ # Helper to get attribute or dict key
151
+ def _get(item, key, default=None):
152
+ if isinstance(item, dict):
153
+ return item.get(key, default)
154
+ return getattr(item, key, default)
155
+
139
156
  # Convert MCP result to dict format expected by agent
140
157
  content = []
141
158
  for item in result.content:
142
- if hasattr(item, "type"):
143
- if item.type == "image":
144
- content.append({
145
- "type": "image",
146
- "data": item.data[:100] + "..." if len(item.data) > 100 else item.data, # Truncate for logging
147
- "mimeType": getattr(item, "mimeType", "image/png"),
148
- })
149
- elif item.type == "text":
150
- content.append({"type": "text", "text": item.text})
159
+ item_type = _get(item, "type")
160
+ if item_type == "image":
161
+ content.append({
162
+ "type": "image",
163
+ "data": _get(item, "data", ""),
164
+ "mimeType": _get(item, "mimeType", "image/png"),
165
+ })
166
+ elif item_type == "text":
167
+ content.append({"type": "text", "text": _get(item, "text", "")})
151
168
 
152
- # Log the call
169
+ # Log the call (just types, not data)
153
170
  self._log({
154
171
  "type": "mcp_call",
155
172
  "tool": name,
@@ -158,20 +175,7 @@ class MCP:
158
175
  "response_content_types": [c.get("type") for c in content],
159
176
  "is_error": result.isError if hasattr(result, "isError") else False,
160
177
  })
161
-
162
- # Return full content (not truncated)
163
- full_content = []
164
- for item in result.content:
165
- if hasattr(item, "type"):
166
- if item.type == "image":
167
- full_content.append({
168
- "type": "image",
169
- "data": item.data,
170
- "mimeType": getattr(item, "mimeType", "image/png"),
171
- })
172
- elif item.type == "text":
173
- full_content.append({"type": "text", "text": item.text})
174
- return {"content": full_content, "isError": result.isError if hasattr(result, "isError") else False}
178
+ return {"content": content, "isError": result.isError if hasattr(result, "isError") else False}
175
179
 
176
180
  def get_tools(self) -> List[Dict]:
177
181
  """Return the list of tools from the server."""
@@ -201,12 +205,13 @@ def get_image_data(result: Dict) -> Optional[str]:
201
205
  class GeminiAgent:
202
206
  """Gemini Computer Use Agent."""
203
207
 
204
- def __init__(self, mcp: MCP, model: str):
208
+ def __init__(self, mcp: MCP, model: str, session=None):
205
209
  self.mcp = mcp
206
210
  # Strip provider prefix if present
207
211
  self.model = model.split("/")[-1] if "/" in model else model
208
212
  self.client = get_gemini_client()
209
213
  self.transcript: List[Dict] = []
214
+ self.session = session # Fleet session for live logging
210
215
 
211
216
  async def _execute_tool(self, name: str, args: Dict) -> Dict:
212
217
  return await self.mcp.call(name, args)
@@ -251,8 +256,13 @@ STRICT RULES:
251
256
  max_output_tokens=4096,
252
257
  system_instruction=system_prompt,
253
258
  tools=[types.Tool(function_declarations=gemini_tools)],
259
+ thinking_config=types.ThinkingConfig(include_thoughts=True),
254
260
  )
255
261
 
262
+ # Set config on session for logging (if session exists)
263
+ if self.session:
264
+ self.session.config = config
265
+
256
266
  history: List[types.Content] = []
257
267
 
258
268
  user_prompt = f"""###User instruction: {prompt}"""
@@ -292,6 +302,15 @@ STRICT RULES:
292
302
  log_verbose(f" Candidate: {candidate}")
293
303
  continue
294
304
 
305
+ # Log to Fleet session (live)
306
+ if self.session:
307
+ try:
308
+ await self.session.log(history, response)
309
+ if step == 1 and self.session.session_id:
310
+ print(f"Session: https://fleetai.com/dashboard/sessions/{self.session.session_id}")
311
+ except Exception as e:
312
+ log_verbose(f" [WARN] Session log failed: {e}")
313
+
295
314
  # Log all parts for debugging
296
315
  log_verbose(f"\n Response parts ({len(candidate.content.parts)}):")
297
316
  for i, part in enumerate(candidate.content.parts):
@@ -415,6 +434,8 @@ async def main():
415
434
  "url": os.environ.get("FLEET_MCP_URL", "http://localhost:8765"),
416
435
  "prompt": os.environ.get("FLEET_TASK_PROMPT", ""),
417
436
  "task_key": os.environ.get("FLEET_TASK_KEY", ""),
437
+ "job_id": os.environ.get("FLEET_JOB_ID"),
438
+ "instance_id": os.environ.get("FLEET_INSTANCE_ID"),
418
439
  "model": os.environ.get("FLEET_MODEL", "gemini-2.5-pro"),
419
440
  "max_steps": int(os.environ.get("FLEET_MAX_STEPS", "100")),
420
441
  }
@@ -430,10 +451,24 @@ async def main():
430
451
  print(json.dumps(result))
431
452
  return result
432
453
 
454
+ # Create Fleet session for live logging
455
+ session = None
456
+ if os.environ.get("FLEET_API_KEY"):
457
+ session = fleet.session_async(
458
+ job_id=config["job_id"],
459
+ model=config["model"],
460
+ task_key=config["task_key"],
461
+ instance_id=config["instance_id"],
462
+ )
463
+
433
464
  async with MCP(config["url"]) as mcp:
434
- agent = GeminiAgent(mcp, config["model"])
465
+ agent = GeminiAgent(mcp, config["model"], session=session)
435
466
  result = await agent.run(config["prompt"], config["max_steps"])
436
467
  result["task_key"] = config["task_key"]
468
+ # Include session_id in result so orchestrator can complete it after verification
469
+ if session and session.session_id:
470
+ result["session_id"] = session.session_id
471
+
437
472
  print(json.dumps(result))
438
473
  return result
439
474
 
@@ -18,6 +18,7 @@ from contextlib import asynccontextmanager
18
18
  from typing import Optional
19
19
 
20
20
  from mcp.server.fastmcp import FastMCP
21
+ from mcp.types import ImageContent, TextContent
21
22
  from starlette.requests import Request
22
23
  from starlette.responses import JSONResponse
23
24
 
@@ -227,9 +228,10 @@ def _dy(y: int) -> int:
227
228
 
228
229
 
229
230
  def _screenshot_response(img: bytes) -> list:
231
+ """Return screenshot as proper MCP content types."""
230
232
  return [
231
- {"type": "image", "data": base64.b64encode(img).decode(), "mimeType": "image/png"},
232
- {"type": "text", "text": f"URL: {computer.current_url}"}
233
+ ImageContent(type="image", data=base64.b64encode(img).decode(), mimeType="image/png"),
234
+ TextContent(type="text", text=f"URL: {computer.current_url}"),
233
235
  ]
234
236
 
235
237
 
@@ -0,0 +1,440 @@
1
+ """Playwright browser control utilities.
2
+
3
+ Provides PlaywrightComputer class for browser automation with:
4
+ - Mouse actions (click, move, drag, scroll)
5
+ - Keyboard actions (type, key combinations)
6
+ - Screenshot capture
7
+ - Normalized coordinate support (0-1000 range)
8
+
9
+ Key mapping follows the action spec convention for cross-platform compatibility.
10
+ """
11
+
12
+ import asyncio
13
+ import logging
14
+ from typing import List, Optional, Tuple
15
+
16
+ from playwright.async_api import async_playwright, Page, Browser, BrowserContext
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ # =============================================================================
22
+ # Key Mapping - Action spec keys to Playwright keys
23
+ # =============================================================================
24
+
25
+ PLAYWRIGHT_KEY_MAP = {
26
+ # Common keys
27
+ "enter": "Enter", "return": "Enter", "tab": "Tab",
28
+ "escape": "Escape", "esc": "Escape", "space": " ",
29
+ "backspace": "Backspace", "delete": "Delete", "insert": "Insert",
30
+
31
+ # Modifiers
32
+ "alt": "Alt", "alt_left": "Alt", "alt_right": "Alt",
33
+ "control": "Control", "control_left": "Control", "control_right": "Control",
34
+ "ctrl": "Control", "ctrl_left": "Control", "ctrl_right": "Control",
35
+ "shift": "Shift", "shift_left": "Shift", "shift_right": "Shift",
36
+ "caps_lock": "CapsLock", "capslock": "CapsLock",
37
+ "meta": "Meta", "meta_left": "Meta", "meta_right": "Meta",
38
+ "command": "Meta", "cmd": "Meta", "super": "Meta", "win": "Meta", "windows": "Meta",
39
+ "num_lock": "NumLock", "numlock": "NumLock",
40
+ "scroll_lock": "ScrollLock", "scrolllock": "ScrollLock",
41
+
42
+ # Navigation
43
+ "arrow_down": "ArrowDown", "arrow_up": "ArrowUp",
44
+ "arrow_left": "ArrowLeft", "arrow_right": "ArrowRight",
45
+ "down": "ArrowDown", "up": "ArrowUp", "left": "ArrowLeft", "right": "ArrowRight",
46
+ "end": "End", "home": "Home",
47
+ "page_down": "PageDown", "pagedown": "PageDown",
48
+ "page_up": "PageUp", "pageup": "PageUp",
49
+
50
+ # Function keys
51
+ **{f"f{i}": f"F{i}" for i in range(1, 21)},
52
+
53
+ # Symbols
54
+ "backquote": "`", "grave": "`", "tilde": "`",
55
+ "backslash": "\\", "bracket_left": "[", "bracketleft": "[",
56
+ "bracket_right": "]", "bracketright": "]",
57
+ "comma": ",", "double_quote": '"', "doublequote": '"',
58
+ "equal": "=", "equals": "=", "minus": "-", "dash": "-",
59
+ "period": ".", "dot": ".", "quote": "'", "apostrophe": "'",
60
+ "semicolon": ";", "slash": "/", "forward_slash": "/", "forwardslash": "/",
61
+
62
+ # Numpad
63
+ **{f"numpad_{i}": f"Numpad{i}" for i in range(10)},
64
+ **{f"numpad{i}": f"Numpad{i}" for i in range(10)},
65
+ "numpad_add": "NumpadAdd", "numpadadd": "NumpadAdd",
66
+ "numpad_subtract": "NumpadSubtract", "numpadsubtract": "NumpadSubtract",
67
+ "numpad_multiply": "NumpadMultiply", "numpadmultiply": "NumpadMultiply",
68
+ "numpad_divide": "NumpadDivide", "numpaddivide": "NumpadDivide",
69
+ "numpad_decimal": "NumpadDecimal", "numpaddecimal": "NumpadDecimal",
70
+ "numpad_enter": "NumpadEnter", "numpadenter": "NumpadEnter",
71
+
72
+ # Media
73
+ "audio_volume_mute": "AudioVolumeMute",
74
+ "audio_volume_down": "AudioVolumeDown",
75
+ "audio_volume_up": "AudioVolumeUp",
76
+ "media_track_next": "MediaTrackNext",
77
+ "media_track_previous": "MediaTrackPrevious",
78
+ "media_stop": "MediaStop",
79
+ "media_play_pause": "MediaPlayPause",
80
+
81
+ # Other
82
+ "print_screen": "PrintScreen", "printscreen": "PrintScreen",
83
+ "pause": "Pause", "context_menu": "ContextMenu", "contextmenu": "ContextMenu",
84
+ "help": "Help",
85
+ }
86
+
87
+ MODIFIER_KEYS = {
88
+ "Alt", "Control", "Shift", "Meta",
89
+ "alt", "alt_left", "alt_right",
90
+ "control", "control_left", "control_right", "ctrl", "ctrl_left", "ctrl_right",
91
+ "shift", "shift_left", "shift_right",
92
+ "meta", "meta_left", "meta_right", "command", "cmd", "super", "win", "windows",
93
+ }
94
+
95
+ # Key specification for tool docstrings
96
+ KEY_SPEC = (
97
+ "Key specification: * Common: enter, tab, escape, space, backspace, delete "
98
+ "* Modifiers: alt_left, control_left, control_right, shift_left, caps_lock, meta "
99
+ "* Navigation: arrow_down, arrow_right, end, home, page_down "
100
+ "* Function: f1 to f12 "
101
+ "* Alphanumeric: key_a to key_z, digit_0 to digit_9 "
102
+ "* Symbols: backquote, backslash, bracket_left, bracket_right, comma, double_quote, "
103
+ "equal, minus, period, quote, semicolon, slash "
104
+ "* Numpad: numpad_0 to numpad_9, numpad_add, numpad_divide, numpad_enter, numpad_multiply"
105
+ )
106
+
107
+
108
+ def map_key(key: str) -> str:
109
+ """Map action spec key name to Playwright key name.
110
+
111
+ Args:
112
+ key: Key name in action spec format (e.g., "key_a", "control_left")
113
+
114
+ Returns:
115
+ Playwright key name (e.g., "a", "Control")
116
+ """
117
+ k = key.lower().strip()
118
+ if k in PLAYWRIGHT_KEY_MAP:
119
+ return PLAYWRIGHT_KEY_MAP[k]
120
+ if k.startswith("key_") and len(k) == 5:
121
+ return k[4].lower()
122
+ if k.startswith("digit_") and len(k) == 7:
123
+ return k[6]
124
+ if len(key) == 1:
125
+ return key
126
+ return key
127
+
128
+
129
+ def is_modifier(key: str) -> bool:
130
+ """Check if a key is a modifier key.
131
+
132
+ Args:
133
+ key: Key name to check
134
+
135
+ Returns:
136
+ True if the key is a modifier (Alt, Control, Shift, Meta)
137
+ """
138
+ return key.lower().strip() in MODIFIER_KEYS or map_key(key) in {"Alt", "Control", "Shift", "Meta"}
139
+
140
+
141
+ # =============================================================================
142
+ # PlaywrightComputer - Browser control
143
+ # =============================================================================
144
+
145
+ class PlaywrightComputer:
146
+ """Browser control via Playwright.
147
+
148
+ Provides a high-level interface for browser automation:
149
+ - Mouse actions with optional visual highlighting
150
+ - Keyboard input with proper modifier handling
151
+ - Screenshot capture
152
+ - Automatic page load waiting
153
+
154
+ Args:
155
+ screen_size: Tuple of (width, height) for viewport
156
+ initial_url: URL to navigate to on start
157
+ headless: Run browser without visible window
158
+ highlight_mouse: Show visual indicator for mouse actions (useful for debugging)
159
+
160
+ Example:
161
+ computer = PlaywrightComputer(
162
+ screen_size=(1366, 768),
163
+ initial_url="https://example.com",
164
+ headless=False,
165
+ highlight_mouse=True,
166
+ )
167
+ await computer.start()
168
+ await computer.mouse_click(683, 384) # Click center
169
+ screenshot = await computer.screenshot()
170
+ await computer.stop()
171
+ """
172
+
173
+ def __init__(
174
+ self,
175
+ screen_size: Tuple[int, int],
176
+ initial_url: str,
177
+ headless: bool = True,
178
+ highlight_mouse: bool = False,
179
+ ):
180
+ self._screen_size = screen_size
181
+ self._initial_url = initial_url
182
+ self._headless = headless
183
+ self._highlight_mouse = highlight_mouse
184
+ self._playwright = None
185
+ self._browser: Optional[Browser] = None
186
+ self._context: Optional[BrowserContext] = None
187
+ self._page: Optional[Page] = None
188
+
189
+ @property
190
+ def width(self) -> int:
191
+ """Viewport width in pixels."""
192
+ return self._screen_size[0]
193
+
194
+ @property
195
+ def height(self) -> int:
196
+ """Viewport height in pixels."""
197
+ return self._screen_size[1]
198
+
199
+ @property
200
+ def current_url(self) -> str:
201
+ """Current page URL."""
202
+ return self._page.url if self._page else ""
203
+
204
+ async def _handle_new_page(self, new_page: Page):
205
+ """Handle new tab by redirecting to current page."""
206
+ new_url = new_page.url
207
+ await new_page.close()
208
+ await self._page.goto(new_url)
209
+
210
+ async def start(self):
211
+ """Start the browser and navigate to initial URL."""
212
+ logger.info(f"Starting browser (headless={self._headless})...")
213
+ self._playwright = await async_playwright().start()
214
+ self._browser = await self._playwright.chromium.launch(
215
+ headless=self._headless,
216
+ args=[
217
+ "--no-sandbox",
218
+ "--disable-extensions",
219
+ "--disable-file-system",
220
+ "--disable-plugins",
221
+ "--disable-dev-shm-usage",
222
+ "--disable-background-networking",
223
+ "--disable-default-apps",
224
+ "--disable-sync",
225
+ ],
226
+ )
227
+ self._context = await self._browser.new_context(
228
+ viewport={"width": self._screen_size[0], "height": self._screen_size[1]}
229
+ )
230
+ self._page = await self._context.new_page()
231
+ self._context.on("page", self._handle_new_page)
232
+ await self._page.goto(self._initial_url)
233
+ await self._page.wait_for_load_state()
234
+ logger.info(f"Browser ready: {self._initial_url}")
235
+
236
+ async def stop(self):
237
+ """Stop the browser and clean up resources."""
238
+ if self._context:
239
+ await self._context.close()
240
+ if self._browser:
241
+ try:
242
+ await self._browser.close()
243
+ except Exception:
244
+ pass
245
+ if self._playwright:
246
+ await self._playwright.stop()
247
+ logger.info("Browser stopped")
248
+
249
+ async def screenshot(self) -> bytes:
250
+ """Take a screenshot of the current viewport.
251
+
252
+ Returns:
253
+ PNG image data as bytes
254
+ """
255
+ await self._page.wait_for_load_state()
256
+ await asyncio.sleep(0.5)
257
+ return await self._page.screenshot(type="png", full_page=False)
258
+
259
+ async def _highlight(self, x: int, y: int):
260
+ """Show visual highlight at mouse position (for debugging)."""
261
+ if not self._highlight_mouse:
262
+ return
263
+ await self._page.evaluate(f"""
264
+ () => {{
265
+ const div = document.createElement('div');
266
+ div.style.cssText = 'position:fixed;width:20px;height:20px;border-radius:50%;border:4px solid red;pointer-events:none;z-index:9999;left:{x-10}px;top:{y-10}px;';
267
+ document.body.appendChild(div);
268
+ setTimeout(() => div.remove(), 2000);
269
+ }}
270
+ """)
271
+ await asyncio.sleep(1)
272
+
273
+ # -------------------------------------------------------------------------
274
+ # Mouse actions
275
+ # -------------------------------------------------------------------------
276
+
277
+ async def mouse_click(self, x: int, y: int, button: str = "left", repeats: int = 1) -> None:
278
+ """Click at position.
279
+
280
+ Args:
281
+ x: X coordinate in pixels
282
+ y: Y coordinate in pixels
283
+ button: Mouse button ('left', 'middle', 'right')
284
+ repeats: Number of clicks (2 for double-click)
285
+ """
286
+ await self._highlight(x, y)
287
+ for _ in range(repeats):
288
+ await self._page.mouse.click(x, y, button=button)
289
+ await self._page.wait_for_load_state()
290
+
291
+ async def mouse_move(self, x: int, y: int) -> None:
292
+ """Move mouse to position.
293
+
294
+ Args:
295
+ x: X coordinate in pixels
296
+ y: Y coordinate in pixels
297
+ """
298
+ await self._highlight(x, y)
299
+ await self._page.mouse.move(x, y)
300
+ await self._page.wait_for_load_state()
301
+
302
+ async def mouse_down(self, button: str = "left") -> None:
303
+ """Press mouse button down.
304
+
305
+ Args:
306
+ button: Mouse button ('left', 'middle', 'right')
307
+ """
308
+ await self._page.mouse.down(button=button)
309
+
310
+ async def mouse_up(self, button: str = "left") -> None:
311
+ """Release mouse button.
312
+
313
+ Args:
314
+ button: Mouse button ('left', 'middle', 'right')
315
+ """
316
+ await self._page.mouse.up(button=button)
317
+ await self._page.wait_for_load_state()
318
+
319
+ async def mouse_scroll(self, dx: int, dy: int) -> None:
320
+ """Scroll the mouse wheel.
321
+
322
+ Args:
323
+ dx: Horizontal scroll amount in pixels
324
+ dy: Vertical scroll amount in pixels
325
+ """
326
+ await self._page.mouse.wheel(dx, dy)
327
+ await self._page.wait_for_load_state()
328
+
329
+ async def mouse_drag(
330
+ self,
331
+ x_start: int,
332
+ y_start: int,
333
+ x_end: int,
334
+ y_end: int,
335
+ button: str = "left",
336
+ ) -> None:
337
+ """Drag from one position to another.
338
+
339
+ Args:
340
+ x_start: Starting X coordinate
341
+ y_start: Starting Y coordinate
342
+ x_end: Ending X coordinate
343
+ y_end: Ending Y coordinate
344
+ button: Mouse button to hold during drag
345
+ """
346
+ await self._highlight(x_start, y_start)
347
+ await self._page.mouse.move(x_start, y_start)
348
+ await self._page.mouse.down(button=button)
349
+ await self._highlight(x_end, y_end)
350
+ await self._page.mouse.move(x_end, y_end)
351
+ await self._page.mouse.up(button=button)
352
+ await self._page.wait_for_load_state()
353
+
354
+ # -------------------------------------------------------------------------
355
+ # Keyboard actions
356
+ # -------------------------------------------------------------------------
357
+
358
+ async def type_text(self, text: str, press_enter: bool = False) -> None:
359
+ """Type text using the keyboard.
360
+
361
+ Args:
362
+ text: Text to type
363
+ press_enter: Whether to press Enter after typing
364
+ """
365
+ await self._page.keyboard.type(text)
366
+ await self._page.wait_for_load_state()
367
+ if press_enter:
368
+ await self._page.keyboard.press("Enter")
369
+ await self._page.wait_for_load_state()
370
+
371
+ async def key_combination(self, keys: List[str]) -> None:
372
+ """Press a key combination (e.g., Ctrl+C).
373
+
374
+ Handles modifiers properly - holds them down while pressing other keys.
375
+
376
+ Args:
377
+ keys: List of keys to press together
378
+ """
379
+ if not keys:
380
+ return
381
+
382
+ modifiers = [map_key(k) for k in keys if is_modifier(k)]
383
+ regular = [map_key(k) for k in keys if not is_modifier(k)]
384
+
385
+ # Press modifiers down
386
+ for mod in modifiers:
387
+ await self._page.keyboard.down(mod)
388
+
389
+ # Press regular keys
390
+ for key in regular:
391
+ await self._page.keyboard.press(key)
392
+
393
+ # If only modifiers, brief pause
394
+ if not regular and modifiers:
395
+ await asyncio.sleep(0.05)
396
+
397
+ # Release modifiers
398
+ for mod in reversed(modifiers):
399
+ await self._page.keyboard.up(mod)
400
+
401
+ await self._page.wait_for_load_state()
402
+
403
+ async def key_down(self, key: str) -> None:
404
+ """Press a key down (without releasing).
405
+
406
+ Args:
407
+ key: Key to press down
408
+ """
409
+ await self._page.keyboard.down(map_key(key))
410
+
411
+ async def key_up(self, key: str) -> None:
412
+ """Release a key.
413
+
414
+ Args:
415
+ key: Key to release
416
+ """
417
+ await self._page.keyboard.up(map_key(key))
418
+ await self._page.wait_for_load_state()
419
+
420
+ # -------------------------------------------------------------------------
421
+ # Utilities
422
+ # -------------------------------------------------------------------------
423
+
424
+ async def wait(self, seconds: int) -> None:
425
+ """Wait for a number of seconds.
426
+
427
+ Args:
428
+ seconds: Number of seconds to wait
429
+ """
430
+ await asyncio.sleep(seconds)
431
+
432
+ async def goto(self, url: str) -> None:
433
+ """Navigate to a URL.
434
+
435
+ Args:
436
+ url: URL to navigate to
437
+ """
438
+ await self._page.goto(url)
439
+ await self._page.wait_for_load_state()
440
+
@@ -0,0 +1,4 @@
1
+ playwright>=1.40.0
2
+ mcp[cli]>=1.2.0
3
+ uvicorn>=0.30.0
4
+ starlette>=0.38.0
@@ -0,0 +1,31 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ # Start virtual display if not headless
5
+ if [ "$HEADLESS" != "true" ]; then
6
+ echo "Starting Xvfb virtual display..."
7
+ Xvfb :99 -screen 0 ${SCREEN_WIDTH}x${SCREEN_HEIGHT}x24 &
8
+ sleep 1
9
+
10
+ echo "Starting fluxbox window manager..."
11
+ fluxbox &
12
+ sleep 1
13
+
14
+ echo "Starting VNC server on port $VNC_PORT..."
15
+ x11vnc -display :99 -forever -shared -rfbport $VNC_PORT -nopw &
16
+ sleep 1
17
+
18
+ echo "Starting noVNC on port $NOVNC_PORT..."
19
+ websockify --web=/usr/share/novnc/ $NOVNC_PORT localhost:$VNC_PORT &
20
+ sleep 1
21
+
22
+ echo ""
23
+ echo "=========================================="
24
+ echo " Browser visible at: http://localhost:$NOVNC_PORT/vnc.html"
25
+ echo "=========================================="
26
+ echo ""
27
+ fi
28
+
29
+ # Start the MCP server
30
+ exec python mcp_server.py
31
+
@@ -21,15 +21,14 @@ import asyncio
21
21
  import json
22
22
  import logging
23
23
  import os
24
- import subprocess
25
24
  import time
25
+ from datetime import datetime
26
26
  from pathlib import Path
27
27
  from typing import Dict, List, Optional, Tuple
28
28
 
29
+ import fleet
29
30
  from .utils import get_agent_path
30
31
  from .types import AgentConfig, AgentResult, TaskResult
31
- from fleet.proxy import ProxyManager
32
- from fleet.eval import TrafficUploader
33
32
 
34
33
  logger = logging.getLogger(__name__)
35
34
 
@@ -45,11 +44,6 @@ class AgentOrchestrator:
45
44
  self._docker_image: Optional[str] = None
46
45
  # Track available ports (recycled when tasks complete)
47
46
  self._available_ports: List[Tuple[int, int]] = []
48
- # MITM proxy for traffic capture
49
- self._proxy: Optional[ProxyManager] = None
50
- self._proxy_env: Dict[str, str] = {}
51
- # Traffic uploader (tails proxy log, ships to backend)
52
- self._uploader: Optional[TrafficUploader] = None
53
47
 
54
48
  async def _get_next_ports(self) -> Tuple[int, int]:
55
49
  """Get next available MCP port and VNC port."""
@@ -75,38 +69,18 @@ class AgentOrchestrator:
75
69
  from rich.console import Console
76
70
  from rich.live import Live
77
71
  from rich.spinner import Spinner
78
- import uuid
79
72
 
80
73
  console = Console()
81
74
 
82
- # Generate job ID for this run
83
- self._job_id = f"eval_{uuid.uuid4().hex[:12]}"
84
- console.print(f"Eval job: {self._job_id}")
75
+ # Create job via Fleet API
76
+ job_name = f"eval-{self.config.agent}-{datetime.now().strftime('%Y%m%d_%H%M%S')}"
77
+ self._job_id = await fleet.job_async(name=job_name)
78
+ console.print(f"Job: https://fleetai.com/dashboard/jobs/{self._job_id}")
85
79
 
86
80
  # Create log directory: ~/.fleet/logs/{job_id}/
87
81
  self._log_dir = Path.home() / ".fleet" / "logs" / self._job_id
88
82
  self._log_dir.mkdir(parents=True, exist_ok=True)
89
83
 
90
- # Start MITM proxy for traffic capture
91
- self._proxy = ProxyManager()
92
- try:
93
- self._proxy_env = await self._proxy.start()
94
- console.print(f"Proxy started, logging to: {self._proxy.log_path}")
95
-
96
- # Start traffic uploader (tails proxy log, ships raw to backend)
97
- self._uploader = TrafficUploader(
98
- job_id=self._job_id,
99
- log_file=self._proxy.log_path,
100
- whitelist=None, # No filter - upload everything
101
- )
102
- await self._uploader.start()
103
- except Exception as e:
104
- console.print(f"[yellow]⚠[/yellow] Proxy failed to start: {e}")
105
- console.print("[dim] Proxy requires aiohttp: pip install aiohttp[/dim]")
106
- self._proxy = None
107
- self._proxy_env = {}
108
- self._uploader = None
109
-
110
84
  # Load tasks with spinner
111
85
  with Live(Spinner("dots", text=f"Loading tasks from {self.config.project_key}..."), console=console, transient=True):
112
86
  if self.config.task_keys:
@@ -168,16 +142,6 @@ class AgentOrchestrator:
168
142
  else:
169
143
  final.append(r)
170
144
 
171
- # Stop uploader first (flushes remaining entries)
172
- if self._uploader:
173
- await self._uploader.stop()
174
- stats = self._uploader.stats
175
- console.print(f"Traffic: {stats['read']} read, {stats['uploaded']} uploaded")
176
-
177
- # Stop proxy
178
- if self._proxy:
179
- await self._proxy.stop()
180
-
181
145
  # Show logs location
182
146
  if hasattr(self, '_log_dir') and self._log_dir.exists():
183
147
  session_logs = list(self._log_dir.glob("*.jsonl"))
@@ -198,17 +162,12 @@ class AgentOrchestrator:
198
162
 
199
163
  image_name = f"fleet-cua-{agent_path.name}"
200
164
 
201
- # Use fleet SDK root as build context (so Dockerfile can access fleet/utils)
202
- # agent_path is like: .../fleet-sdk/fleet/agent/gemini_cua
203
- # We want: .../fleet-sdk
204
- fleet_root = agent_path.parent.parent.parent
205
-
165
+ # Build context is the agent directory (all files are self-contained)
206
166
  with Live(Spinner("dots", text=f"Building Docker image {image_name}..."), console=console, transient=True):
207
167
  proc = await asyncio.create_subprocess_exec(
208
168
  "docker", "build",
209
169
  "-t", image_name,
210
- "-f", str(dockerfile),
211
- str(fleet_root), # Build context is repo root
170
+ str(agent_path), # Build context is agent directory
212
171
  stdout=asyncio.subprocess.PIPE,
213
172
  stderr=asyncio.subprocess.PIPE,
214
173
  )
@@ -280,12 +239,14 @@ class AgentOrchestrator:
280
239
  port=port,
281
240
  task_prompt=task_prompt,
282
241
  task_key=task_key,
242
+ instance_id=env.instance_id,
283
243
  )
284
244
  logger.debug(f"[{short_key}] Agent done: completed={agent_result.completed}")
285
245
 
286
246
  # 4. Run verification
287
247
  verification_success = None
288
248
  verification_score = None
249
+ verifier_execution_id = None
289
250
 
290
251
  if agent_result.completed and task.verifier:
291
252
  logger.info(f"[{task_key}] Running verification...")
@@ -295,12 +256,27 @@ class AgentOrchestrator:
295
256
  final_answer=agent_result.final_answer,
296
257
  )
297
258
  verification_success = v.success
259
+ verifier_execution_id = v.execution_id
298
260
  # Score is in v.result (the verifier function's return value)
299
261
  verification_score = v.result if isinstance(v.result, (int, float)) else None
300
262
  logger.info(f"[{task_key}] Verification: {verification_success}")
301
263
  except Exception as e:
302
264
  logger.error(f"[{task_key}] Verification error: {e}")
303
265
 
266
+ # 5. Complete/fail session (session was created by agent, we just complete it)
267
+ session_id = getattr(agent_result, 'session_id', None)
268
+ if session_id:
269
+ try:
270
+ # Create session object to complete it
271
+ session = fleet.session_async(session_id=session_id)
272
+ if verification_success:
273
+ await session.complete(verifier_execution_id=verifier_execution_id)
274
+ else:
275
+ await session.fail(verifier_execution_id=verifier_execution_id)
276
+ logger.info(f"[{task_key}] Session: https://fleetai.com/dashboard/sessions/{session_id}")
277
+ except Exception as e:
278
+ logger.error(f"[{task_key}] Session complete error: {e}")
279
+
304
280
  return TaskResult(
305
281
  task_key=task_key,
306
282
  task_prompt=task_prompt,
@@ -414,6 +390,7 @@ class AgentOrchestrator:
414
390
  port: int,
415
391
  task_prompt: str,
416
392
  task_key: str,
393
+ instance_id: Optional[str] = None,
417
394
  ) -> AgentResult:
418
395
  """Run agent process."""
419
396
  agent_path = get_agent_path(self.config.agent)
@@ -431,6 +408,7 @@ class AgentOrchestrator:
431
408
  "FLEET_JOB_ID": self._job_id,
432
409
  "FLEET_TASK_PROMPT": task_prompt,
433
410
  "FLEET_TASK_KEY": task_key,
411
+ "FLEET_INSTANCE_ID": instance_id or "",
434
412
  "FLEET_MODEL": self.config.model,
435
413
  "FLEET_MAX_STEPS": str(self.config.max_steps),
436
414
  "FLEET_SCREEN_WIDTH": str(self.config.screen_width),
@@ -438,8 +416,6 @@ class AgentOrchestrator:
438
416
  "FLEET_VERBOSE": "true" if self.config.verbose else "false",
439
417
  })
440
418
  env.update(self.config.api_keys)
441
- # Add proxy env vars for traffic capture
442
- env.update(self._proxy_env)
443
419
 
444
420
  proc = await asyncio.create_subprocess_exec(
445
421
  "python", str(agent_script),
@@ -494,6 +470,7 @@ class AgentOrchestrator:
494
470
  steps_taken=result_json.get("steps_taken", 0),
495
471
  execution_time_ms=result_json.get("execution_time_ms", 0),
496
472
  transcript=result_json.get("transcript", []),
473
+ session_id=result_json.get("session_id"),
497
474
  )
498
475
 
499
476
  # Include stderr in error message
fleet/agent/types.py CHANGED
@@ -33,6 +33,7 @@ class AgentResult(BaseModel):
33
33
  steps_taken: int = 0
34
34
  execution_time_ms: int = 0
35
35
  transcript: List[Dict[str, Any]] = Field(default_factory=list)
36
+ session_id: Optional[str] = None # Fleet session ID for completion
36
37
 
37
38
 
38
39
  class TaskResult(BaseModel):
fleet/base.py CHANGED
@@ -27,7 +27,7 @@ from .exceptions import (
27
27
  try:
28
28
  from . import __version__
29
29
  except ImportError:
30
- __version__ = "0.2.83"
30
+ __version__ = "0.2.85"
31
31
 
32
32
  logger = logging.getLogger(__name__)
33
33
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fleet-python
3
- Version: 0.2.83
3
+ Version: 0.2.85
4
4
  Summary: Python SDK for Fleet environments
5
5
  Author-email: Fleet AI <nic@fleet.so>
6
6
  License: Apache-2.0
@@ -23,8 +23,8 @@ examples/openai_simple_example.py,sha256=HmiufucrAZne7tHq9uoEsDWlEhjNC265bQAyIGB
23
23
  examples/query_builder_example.py,sha256=-cOMfWGNifYfYEt_Ds73XpwATZvFDL6F4KTkVxdMjzg,3951
24
24
  examples/quickstart.py,sha256=1VT39IRRhemsJgxi0O0gprdpcw7HB4pYO97GAYagIcg,3788
25
25
  examples/test_cdp_logging.py,sha256=AkCwQCgOTQEI8w3v0knWK_4eXMph7L9x07wj9yIYM10,2836
26
- fleet/__init__.py,sha256=HoKk1ZMCG5h9tFo0Fg0Akpcnc5UyoMe-8gTr4cWBcFA,7683
27
- fleet/base.py,sha256=0ekxdFfuhjP6UGQTfoOBffuU_qfKASyJj2KkeBA7BCw,10065
26
+ fleet/__init__.py,sha256=COtmFIP-72TCZB5y0jwNy_PbHuaXY3Sem7UREJbi554,7683
27
+ fleet/base.py,sha256=Lo1tHYRlGuXYNA547HTz-psjuawHbOFxJAI05JHQYgk,10065
28
28
  fleet/cli.py,sha256=kXtinWio3hniLq8CyBCwmy6YYgX3yovx1HcqPO2ERdc,36025
29
29
  fleet/client.py,sha256=SuNf5IcU8UNDlH-kIe-RlyJ32fBlbxpELBy-43xrrvE,67043
30
30
  fleet/config.py,sha256=n_wh9Sahu3gGE7nHJ7kqNFUH1qDiBtF4bgZq9MvIBMU,319
@@ -33,8 +33,8 @@ fleet/global_client.py,sha256=frrDAFNM2ywN0JHLtlm9qbE1dQpnQJsavJpb7xSR_bU,1072
33
33
  fleet/models.py,sha256=Q-bGdXBazExADtvKTpMOYo77poizXol4wtGLH3tlj08,25111
34
34
  fleet/tasks.py,sha256=8pEzXmgC7RslqsMC_0s6shhr_t2WGIRpTRqo-MAQjdg,20778
35
35
  fleet/types.py,sha256=L4Y82xICf1tzyCLqhLYUgEoaIIS5h9T05TyFNHSWs3s,652
36
- fleet/_async/__init__.py,sha256=Y8dtXVAM-VC-wkmX8NRnUSUeytO0S0mfHTbYpZpdQIU,9115
37
- fleet/_async/base.py,sha256=mdFxKJGTY_r3WnyxgRD-8WBtLusu457wWHweFsBHCd0,9630
36
+ fleet/_async/__init__.py,sha256=DdZeU7jd0L-li1bynFCnkh0037Wxl623w1sUWoXdz68,9115
37
+ fleet/_async/base.py,sha256=X73TI7gLUXbks6Abe9FnoCivRMJqGdPEMjg5QHpqfzI,9630
38
38
  fleet/_async/client.py,sha256=8UhY61h1M6iSb1WpncmHScLEe-zTTDCABDG11apaKGU,63451
39
39
  fleet/_async/exceptions.py,sha256=fUmPwWhnT8SR97lYsRq0kLHQHKtSh2eJS0VQ2caSzEI,5055
40
40
  fleet/_async/global_client.py,sha256=4WskpLHbsDEgWW7hXMD09W-brkp4euy8w2ZJ88594rQ,1103
@@ -54,12 +54,16 @@ fleet/_async/verifiers/__init__.py,sha256=1WTlCNq4tIFbbXaQu5Bf2WppZq0A8suhtZbxMT
54
54
  fleet/_async/verifiers/bundler.py,sha256=9aWWXFsovBPcndE06IATn5jaeli5fRORAYeenF9heN0,26264
55
55
  fleet/_async/verifiers/verifier.py,sha256=iSa-rO-E1R3IQTFS9Z7jbQvQVtsDkilITQP9IIQU2JA,14556
56
56
  fleet/agent/__init__.py,sha256=BuiElLoL_OTq_tmEMD85Zc1x3x9iUerYM7TxLpD22aI,737
57
- fleet/agent/orchestrator.py,sha256=HRbeBgXgX9XgJXNboFGjWkhmoZc8ZRJNesCXvzS9mXY,20375
58
- fleet/agent/types.py,sha256=Ms884KhHdbYOMTwf8uWaBWe3FAkKU_w5YAt5g0Yfky4,1404
57
+ fleet/agent/orchestrator.py,sha256=cUJIrrfanbmqgVcZB6-KNngOT4Mr_vmNGbLylBH9wmY,19777
58
+ fleet/agent/types.py,sha256=v9EpttwyOBDwwMo0lABQmIvT3xlRQqfqLkHXJjsOmrw,1476
59
59
  fleet/agent/utils.py,sha256=VNhyIFwTKizl00ccqXdhh6s3DYZxPy55PMXgYhTLNX4,1171
60
+ fleet/agent/gemini_cua/Dockerfile,sha256=uirE7L7JzmHpCTHTrIAsPg-LTdhlr78ajqqBJlrfTB4,1209
60
61
  fleet/agent/gemini_cua/__init__.py,sha256=EqjnPPhWWUI3Gk5FrWKfajVYPn_nlO6tTl_jPdeHSXU,205
61
- fleet/agent/gemini_cua/agent.py,sha256=D-DpAa3DqDDGQiLc2wIiq6CC2ElmPL9wWhwMb8pWP-4,18263
62
- fleet/agent/gemini_cua/mcp_server.py,sha256=_MvYPDi8wE3w3AzMqG03ab0nskTI7QKTurzf2CdPiD8,7480
62
+ fleet/agent/gemini_cua/agent.py,sha256=vm8s0B6yg3gyeMmRn9lI1pBOIChUBBPtO6vtZFQBgQk,19844
63
+ fleet/agent/gemini_cua/mcp_server.py,sha256=_-LBmmnLp7krXrmGVHtX9ofFtRGUoeNhrLg5eshl3oI,7594
64
+ fleet/agent/gemini_cua/playwright_utils.py,sha256=UZrlJJC5z3deQfvSAo03g-ajNkN52b333gBjqvQ6Qa0,15546
65
+ fleet/agent/gemini_cua/requirements.txt,sha256=WJR0rfBCyDzeQg1ivLpKclVdpeFIlbaGZIErlGEr-IE,69
66
+ fleet/agent/gemini_cua/start.sh,sha256=sw05agVHRcFSc1LRgBHWYMI62zu2HDFkH7bvrTUvw7c,827
63
67
  fleet/env/__init__.py,sha256=BVPZ4AYTznL6AYNrVmjr1yLF16qBcT1U56jWwF7AJ5o,964
64
68
  fleet/env/client.py,sha256=N9oF2hGSNmfVacTnyQhIEocoN3BA5fMa-arbpVeNi-E,3221
65
69
  fleet/eval/__init__.py,sha256=ng9V-KTsdW-bOOyF7UUGvABA9x0npeuYzMdS3MUd4nQ,389
@@ -88,7 +92,7 @@ fleet/verifiers/decorator.py,sha256=RuTjjDijbicNfMSjA7HcTpKueEki5dzNOdTuHS7UoZs,
88
92
  fleet/verifiers/parse.py,sha256=qz9AfJrTbjlg-LU-lE8Ciqi7Yt2a8-cs17FdpjTLhMk,8550
89
93
  fleet/verifiers/sql_differ.py,sha256=TqTLWyK3uOyLbitT6HYzYEzuSFC39wcyhgk3rcm__k8,6525
90
94
  fleet/verifiers/verifier.py,sha256=iqGevW7dSd0J5RdRQjpu-zioy_FYAXnzMfkuB3-QmO0,14601
91
- fleet_python-0.2.83.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
95
+ fleet_python-0.2.85.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
92
96
  scripts/fix_sync_imports.py,sha256=X9fWLTpiPGkSHsjyQUDepOJkxOqw1DPj7nd8wFlFqLQ,8368
93
97
  scripts/unasync.py,sha256=vWVQxRWX8SRZO5cmzEhpvnG_REhCWXpidIGIpWmEcvI,696
94
98
  tests/__init__.py,sha256=Re1SdyxH8NfyL1kjhi7SQkGP1mYeWB-D6UALqdIMd8I,35
@@ -98,8 +102,8 @@ tests/test_instance_dispatch.py,sha256=CvU4C3LBIqsYZdEsEFfontGjyxAZfVYyXnGwxyIvX
98
102
  tests/test_sqlite_resource_dual_mode.py,sha256=Mh8jBd-xsIGDYFsOACKKK_5DXMUYlFFS7W-jaY6AjG4,8734
99
103
  tests/test_sqlite_shared_memory_behavior.py,sha256=fKx_1BmLS3b8x-9pMgjMycpnaHWY8P-2ZuXEspx6Sbw,4082
100
104
  tests/test_verifier_from_string.py,sha256=Lxi3TpFHFb-hG4-UhLKZJkqo84ax9YJY8G6beO-1erM,13581
101
- fleet_python-0.2.83.dist-info/METADATA,sha256=SOgVOcZ9dToWUp5_Huk5aSvWhnhlRjEHEDbRdJ5qpF0,4166
102
- fleet_python-0.2.83.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
103
- fleet_python-0.2.83.dist-info/entry_points.txt,sha256=qKIQ326cHR5WyCd16QnrW-1DpcT0YyxVRDb3IlTyzTA,39
104
- fleet_python-0.2.83.dist-info/top_level.txt,sha256=qb1zIbtEktyhRFZdqVytwg54l64qtoZL0wjHB4bUg3c,29
105
- fleet_python-0.2.83.dist-info/RECORD,,
105
+ fleet_python-0.2.85.dist-info/METADATA,sha256=CuG2c-jrKzWdgrw6n0jL1XwGsryKpafXW9jNEpbFqXA,4166
106
+ fleet_python-0.2.85.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
107
+ fleet_python-0.2.85.dist-info/entry_points.txt,sha256=qKIQ326cHR5WyCd16QnrW-1DpcT0YyxVRDb3IlTyzTA,39
108
+ fleet_python-0.2.85.dist-info/top_level.txt,sha256=qb1zIbtEktyhRFZdqVytwg54l64qtoZL0wjHB4bUg3c,29
109
+ fleet_python-0.2.85.dist-info/RECORD,,