fleet-python 0.2.66b2__py3-none-any.whl → 0.2.105__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.
Files changed (70) hide show
  1. examples/export_tasks.py +16 -5
  2. examples/export_tasks_filtered.py +245 -0
  3. examples/fetch_tasks.py +230 -0
  4. examples/import_tasks.py +140 -8
  5. examples/iterate_verifiers.py +725 -0
  6. fleet/__init__.py +128 -5
  7. fleet/_async/__init__.py +27 -3
  8. fleet/_async/base.py +24 -9
  9. fleet/_async/client.py +938 -41
  10. fleet/_async/env/client.py +60 -3
  11. fleet/_async/instance/client.py +52 -7
  12. fleet/_async/models.py +15 -0
  13. fleet/_async/resources/api.py +200 -0
  14. fleet/_async/resources/sqlite.py +1801 -46
  15. fleet/_async/tasks.py +122 -25
  16. fleet/_async/verifiers/bundler.py +22 -21
  17. fleet/_async/verifiers/verifier.py +25 -19
  18. fleet/agent/__init__.py +32 -0
  19. fleet/agent/gemini_cua/Dockerfile +45 -0
  20. fleet/agent/gemini_cua/__init__.py +10 -0
  21. fleet/agent/gemini_cua/agent.py +759 -0
  22. fleet/agent/gemini_cua/mcp/main.py +108 -0
  23. fleet/agent/gemini_cua/mcp_server/__init__.py +5 -0
  24. fleet/agent/gemini_cua/mcp_server/main.py +105 -0
  25. fleet/agent/gemini_cua/mcp_server/tools.py +178 -0
  26. fleet/agent/gemini_cua/requirements.txt +5 -0
  27. fleet/agent/gemini_cua/start.sh +30 -0
  28. fleet/agent/orchestrator.py +854 -0
  29. fleet/agent/types.py +49 -0
  30. fleet/agent/utils.py +34 -0
  31. fleet/base.py +34 -9
  32. fleet/cli.py +1061 -0
  33. fleet/client.py +1060 -48
  34. fleet/config.py +1 -1
  35. fleet/env/__init__.py +16 -0
  36. fleet/env/client.py +60 -3
  37. fleet/eval/__init__.py +15 -0
  38. fleet/eval/uploader.py +231 -0
  39. fleet/exceptions.py +8 -0
  40. fleet/instance/client.py +53 -8
  41. fleet/instance/models.py +1 -0
  42. fleet/models.py +303 -0
  43. fleet/proxy/__init__.py +25 -0
  44. fleet/proxy/proxy.py +453 -0
  45. fleet/proxy/whitelist.py +244 -0
  46. fleet/resources/api.py +200 -0
  47. fleet/resources/sqlite.py +1845 -46
  48. fleet/tasks.py +113 -20
  49. fleet/utils/__init__.py +7 -0
  50. fleet/utils/http_logging.py +178 -0
  51. fleet/utils/logging.py +13 -0
  52. fleet/utils/playwright.py +440 -0
  53. fleet/verifiers/bundler.py +22 -21
  54. fleet/verifiers/db.py +985 -1
  55. fleet/verifiers/decorator.py +1 -1
  56. fleet/verifiers/verifier.py +25 -19
  57. {fleet_python-0.2.66b2.dist-info → fleet_python-0.2.105.dist-info}/METADATA +28 -1
  58. fleet_python-0.2.105.dist-info/RECORD +115 -0
  59. {fleet_python-0.2.66b2.dist-info → fleet_python-0.2.105.dist-info}/WHEEL +1 -1
  60. fleet_python-0.2.105.dist-info/entry_points.txt +2 -0
  61. tests/test_app_method.py +85 -0
  62. tests/test_expect_exactly.py +4148 -0
  63. tests/test_expect_only.py +2593 -0
  64. tests/test_instance_dispatch.py +607 -0
  65. tests/test_sqlite_resource_dual_mode.py +263 -0
  66. tests/test_sqlite_shared_memory_behavior.py +117 -0
  67. fleet_python-0.2.66b2.dist-info/RECORD +0 -81
  68. tests/test_verifier_security.py +0 -427
  69. {fleet_python-0.2.66b2.dist-info → fleet_python-0.2.105.dist-info}/licenses/LICENSE +0 -0
  70. {fleet_python-0.2.66b2.dist-info → fleet_python-0.2.105.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ CUA Server - Computer Use Agent MCP Server
4
+
5
+ MCP server with playwright browser control using FastMCP's streamable-http transport.
6
+
7
+ Env vars:
8
+ FLEET_ENV_URL: URL to navigate to
9
+ PORT: Server port (default: 8765)
10
+ SCREEN_WIDTH/HEIGHT: Browser size
11
+ HEADLESS: "true" or "false" (default: true)
12
+ """
13
+
14
+ import logging
15
+ import os
16
+ from contextlib import asynccontextmanager
17
+ from typing import Optional
18
+
19
+ from mcp.server.fastmcp import FastMCP
20
+ from starlette.requests import Request
21
+ from starlette.responses import JSONResponse
22
+
23
+ from fleet.utils.playwright import PlaywrightComputer
24
+
25
+ # Support both module and standalone execution
26
+ try:
27
+ from .tools import register_tools
28
+ except ImportError:
29
+ from tools import register_tools
30
+
31
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s')
32
+ logger = logging.getLogger(__name__)
33
+
34
+
35
+ # =============================================================================
36
+ # Setup
37
+ # =============================================================================
38
+
39
+ computer: Optional[PlaywrightComputer] = None
40
+ PORT = int(os.environ.get("PORT", "8765"))
41
+
42
+
43
+ def get_computer() -> PlaywrightComputer:
44
+ """Get the current computer instance."""
45
+ if computer is None:
46
+ raise RuntimeError("Computer not initialized")
47
+ return computer
48
+
49
+
50
+ @asynccontextmanager
51
+ async def lifespan(app):
52
+ """Initialize browser on startup, cleanup on shutdown."""
53
+ global computer
54
+
55
+ url = os.environ.get("FLEET_ENV_URL", "about:blank")
56
+ width = int(os.environ.get("SCREEN_WIDTH", "1366"))
57
+ height = int(os.environ.get("SCREEN_HEIGHT", "768"))
58
+ headless = os.environ.get("HEADLESS", "true").lower() == "true"
59
+ highlight = os.environ.get("HIGHLIGHT_MOUSE", "false").lower() == "true"
60
+
61
+ logger.info(f"CUA Server: {width}x{height}, headless={headless}, url={url}")
62
+
63
+ computer = PlaywrightComputer(
64
+ screen_size=(width, height),
65
+ initial_url=url,
66
+ headless=headless,
67
+ highlight_mouse=highlight or not headless,
68
+ )
69
+
70
+ try:
71
+ logger.info("Starting Playwright browser...")
72
+ await computer.start()
73
+ logger.info(f"Browser started, navigated to: {computer.current_url}")
74
+ yield
75
+ except Exception as e:
76
+ logger.error(f"Browser startup FAILED: {type(e).__name__}: {e}")
77
+ raise
78
+ finally:
79
+ logger.info("Stopping Playwright browser...")
80
+ try:
81
+ await computer.stop()
82
+ logger.info("Browser stopped")
83
+ except Exception as e:
84
+ logger.error(f"Browser stop error: {type(e).__name__}: {e}")
85
+
86
+
87
+ mcp = FastMCP("cua-server", lifespan=lifespan, host="0.0.0.0", port=PORT)
88
+
89
+ # Register all tools
90
+ register_tools(mcp, get_computer)
91
+
92
+
93
+ # =============================================================================
94
+ # Routes
95
+ # =============================================================================
96
+
97
+ @mcp.custom_route("/health", methods=["GET"])
98
+ async def health_check(request: Request) -> JSONResponse:
99
+ return JSONResponse({"status": "ok", "url": computer.current_url if computer else ""})
100
+
101
+
102
+ # =============================================================================
103
+ # Main
104
+ # =============================================================================
105
+
106
+ if __name__ == "__main__":
107
+ logger.info(f"Starting CUA Server on port {PORT}")
108
+ mcp.run(transport="streamable-http")
@@ -0,0 +1,5 @@
1
+ """MCP server for Gemini CUA agent.
2
+
3
+ This folder is named 'mcp_server' instead of 'mcp' to avoid shadowing the 'mcp' package.
4
+ """
5
+
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ CUA Server - Computer Use Agent MCP Server
4
+
5
+ MCP server with playwright browser control using FastMCP's streamable-http transport.
6
+
7
+ Env vars:
8
+ FLEET_ENV_URL: URL to navigate to
9
+ PORT: Server port (default: 8765)
10
+ SCREEN_WIDTH/HEIGHT: Browser size
11
+ HEADLESS: "true" or "false" (default: true)
12
+ """
13
+
14
+ import logging
15
+ import os
16
+ from contextlib import asynccontextmanager
17
+ from typing import Optional
18
+
19
+ from mcp.server.fastmcp import FastMCP
20
+ from starlette.requests import Request
21
+ from starlette.responses import JSONResponse
22
+
23
+ from fleet.utils.playwright import PlaywrightComputer
24
+
25
+ # Import tools (standalone execution in container)
26
+ from tools import register_tools
27
+
28
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s')
29
+ logger = logging.getLogger(__name__)
30
+
31
+
32
+ # =============================================================================
33
+ # Setup
34
+ # =============================================================================
35
+
36
+ computer: Optional[PlaywrightComputer] = None
37
+ PORT = int(os.environ.get("PORT", "8765"))
38
+
39
+
40
+ def get_computer() -> PlaywrightComputer:
41
+ """Get the current computer instance."""
42
+ if computer is None:
43
+ raise RuntimeError("Computer not initialized")
44
+ return computer
45
+
46
+
47
+ @asynccontextmanager
48
+ async def lifespan(app):
49
+ """Initialize browser on startup, cleanup on shutdown."""
50
+ global computer
51
+
52
+ url = os.environ.get("FLEET_ENV_URL", "about:blank")
53
+ width = int(os.environ.get("SCREEN_WIDTH", "1366"))
54
+ height = int(os.environ.get("SCREEN_HEIGHT", "768"))
55
+ headless = os.environ.get("HEADLESS", "true").lower() == "true"
56
+ highlight = os.environ.get("HIGHLIGHT_MOUSE", "false").lower() == "true"
57
+
58
+ logger.info(f"CUA Server: {width}x{height}, headless={headless}, url={url}")
59
+
60
+ computer = PlaywrightComputer(
61
+ screen_size=(width, height),
62
+ initial_url=url,
63
+ headless=headless,
64
+ highlight_mouse=highlight or not headless,
65
+ )
66
+
67
+ try:
68
+ logger.info("Starting Playwright browser...")
69
+ await computer.start()
70
+ logger.info(f"Browser started, navigated to: {computer.current_url}")
71
+ yield
72
+ except Exception as e:
73
+ logger.error(f"Browser startup FAILED: {type(e).__name__}: {e}")
74
+ raise
75
+ finally:
76
+ logger.info("Stopping Playwright browser...")
77
+ try:
78
+ await computer.stop()
79
+ logger.info("Browser stopped")
80
+ except Exception as e:
81
+ logger.error(f"Browser stop error: {type(e).__name__}: {e}")
82
+
83
+
84
+ mcp = FastMCP("cua-server", lifespan=lifespan, host="0.0.0.0", port=PORT)
85
+
86
+ # Register all tools
87
+ register_tools(mcp, get_computer)
88
+
89
+
90
+ # =============================================================================
91
+ # Routes
92
+ # =============================================================================
93
+
94
+ @mcp.custom_route("/health", methods=["GET"])
95
+ async def health_check(request: Request) -> JSONResponse:
96
+ return JSONResponse({"status": "ok", "url": computer.current_url if computer else ""})
97
+
98
+
99
+ # =============================================================================
100
+ # Main
101
+ # =============================================================================
102
+
103
+ if __name__ == "__main__":
104
+ logger.info(f"Starting CUA Server on port {PORT}")
105
+ mcp.run(transport="streamable-http")
@@ -0,0 +1,178 @@
1
+ """MCP tool definitions for CUA server."""
2
+
3
+ import base64
4
+ import logging
5
+ from typing import Callable
6
+
7
+ from mcp.server.fastmcp import FastMCP
8
+ from mcp.types import ImageContent, TextContent
9
+
10
+ from fleet.utils.playwright import PlaywrightComputer, KEY_SPEC
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def register_tools(mcp: FastMCP, get_computer: Callable[[], PlaywrightComputer]) -> None:
16
+ """Register all CUA tools with the MCP server.
17
+
18
+ Args:
19
+ mcp: FastMCP server instance
20
+ get_computer: Callable that returns the current PlaywrightComputer instance
21
+ """
22
+
23
+ def _dx(x: int) -> int:
24
+ """Denormalize x: [0,1000] -> pixels."""
25
+ return int(x / 1000 * get_computer().width)
26
+
27
+ def _dy(y: int) -> int:
28
+ """Denormalize y: [0,1000] -> pixels."""
29
+ return int(y / 1000 * get_computer().height)
30
+
31
+ def _screenshot_response(img: bytes) -> list:
32
+ """Return screenshot as proper MCP content types."""
33
+ computer = get_computer()
34
+ return [
35
+ ImageContent(type="image", data=base64.b64encode(img).decode(), mimeType="image/png"),
36
+ TextContent(type="text", text=f"URL: {computer.current_url}"),
37
+ ]
38
+
39
+ @mcp.tool()
40
+ async def computer_screenshot() -> list:
41
+ """Takes a screenshot of the computer screen. Use this to see what's on screen."""
42
+ logger.info("computer_screenshot()")
43
+ try:
44
+ result = await get_computer().screenshot()
45
+ logger.info(f"computer_screenshot() -> {len(result)} bytes")
46
+ return _screenshot_response(result)
47
+ except Exception as e:
48
+ logger.error(f"computer_screenshot() FAILED: {type(e).__name__}: {e}")
49
+ raise
50
+
51
+ @mcp.tool()
52
+ async def mouse_click(x: int, y: int, button: str, repeats: int = 1) -> None:
53
+ """Performs a mouse click.
54
+
55
+ Args:
56
+ x: The normalized x coordinate within the [0, 1000] range of the image.
57
+ y: The normalized y coordinate within the [0, 1000] range of the image.
58
+ button: The button to click. Either 'left', 'middle' or 'right'.
59
+ repeats: The number of times to click. Default is 1.
60
+ """
61
+ logger.info(f"mouse_click({x}, {y}, {button}, {repeats})")
62
+ try:
63
+ await get_computer().mouse_click(_dx(x), _dy(y), button, repeats)
64
+ except Exception as e:
65
+ logger.error(f"mouse_click FAILED: {type(e).__name__}: {e}")
66
+ raise
67
+
68
+ @mcp.tool()
69
+ async def mouse_move(x: int, y: int) -> None:
70
+ """Moves the mouse to a new position.
71
+
72
+ Args:
73
+ x: The normalized x coordinate within the [0, 1000] range of the image.
74
+ y: The normalized y coordinate within the [0, 1000] range of the image.
75
+ """
76
+ logger.info(f"mouse_move({x}, {y})")
77
+ await get_computer().mouse_move(_dx(x), _dy(y))
78
+
79
+ @mcp.tool()
80
+ async def mouse_down(button: str) -> None:
81
+ """Keeps a mouse button down.
82
+
83
+ Args:
84
+ button: The button to press down. Either 'left', 'middle' or 'right'.
85
+ """
86
+ logger.info(f"mouse_down({button})")
87
+ await get_computer().mouse_down(button)
88
+
89
+ @mcp.tool()
90
+ async def mouse_up(button: str) -> None:
91
+ """Releases a mouse button after executing a mouse down action.
92
+
93
+ Args:
94
+ button: The button to release. Either 'left', 'middle' or 'right'.
95
+ """
96
+ logger.info(f"mouse_up({button})")
97
+ await get_computer().mouse_up(button)
98
+
99
+ @mcp.tool()
100
+ async def mouse_scroll(dx: int, dy: int) -> None:
101
+ """Uses the mouse to perform a two dimensional scroll.
102
+
103
+ Args:
104
+ dx: The number of pixels to scroll horizontally.
105
+ dy: The number of pixels to scroll vertically.
106
+ """
107
+ logger.info(f"mouse_scroll({dx}, {dy})")
108
+ await get_computer().mouse_scroll(dx, dy)
109
+
110
+ @mcp.tool()
111
+ async def mouse_drag(x_start: int, y_start: int, x_end: int, y_end: int, button: str = "left") -> None:
112
+ """Drag mouse from a point A to a point B.
113
+
114
+ Args:
115
+ x_start: The x coordinate of the starting point normalized within [0, 1000].
116
+ y_start: The y coordinate of the starting point normalized within [0, 1000].
117
+ x_end: The x coordinate of the destination point normalized within [0, 1000].
118
+ y_end: The y coordinate of the destination point normalized within [0, 1000].
119
+ button: The mouse button: left, right, middle. Default is 'left'.
120
+ """
121
+ logger.info(f"mouse_drag({x_start}, {y_start} -> {x_end}, {y_end})")
122
+ await get_computer().mouse_drag(_dx(x_start), _dy(y_start), _dx(x_end), _dy(y_end), button)
123
+
124
+ @mcp.tool()
125
+ async def wait(seconds: int) -> None:
126
+ """Waits for a given number of seconds. Use if the screen is blank or page is loading.
127
+
128
+ Args:
129
+ seconds: The number of seconds to wait.
130
+ """
131
+ logger.info(f"wait({seconds})")
132
+ await get_computer().wait(seconds)
133
+
134
+ @mcp.tool()
135
+ async def type_text(input_text: str, press_enter: bool) -> None:
136
+ """Type text on a keyboard.
137
+
138
+ Args:
139
+ input_text: The input text to type.
140
+ press_enter: Whether to press enter after typing.
141
+ """
142
+ logger.info(f"type_text({input_text[:50]}{'...' if len(input_text) > 50 else ''}, enter={press_enter})")
143
+ try:
144
+ await get_computer().type_text(input_text, press_enter)
145
+ except Exception as e:
146
+ logger.error(f"type_text FAILED: {type(e).__name__}: {e}")
147
+ raise
148
+
149
+ @mcp.tool()
150
+ async def key_combination(keys_to_press: list[str]) -> None:
151
+ f"""Performs a key combination. {KEY_SPEC}
152
+
153
+ Args:
154
+ keys_to_press: The list of keys to press.
155
+ """
156
+ logger.info(f"key_combination({keys_to_press})")
157
+ await get_computer().key_combination(keys_to_press)
158
+
159
+ @mcp.tool()
160
+ async def key_down(key: str) -> None:
161
+ f"""Keeps a keyboard key down. {KEY_SPEC}
162
+
163
+ Args:
164
+ key: The key to press down.
165
+ """
166
+ logger.info(f"key_down({key})")
167
+ await get_computer().key_down(key)
168
+
169
+ @mcp.tool()
170
+ async def key_up(key: str) -> None:
171
+ f"""Releases a keyboard key after executing a key down action. {KEY_SPEC}
172
+
173
+ Args:
174
+ key: The key to press up.
175
+ """
176
+ logger.info(f"key_up({key})")
177
+ await get_computer().key_up(key)
178
+
@@ -0,0 +1,5 @@
1
+ fleet-python
2
+ playwright>=1.40.0
3
+ mcp[cli]>=1.2.0
4
+ uvicorn>=0.30.0
5
+ starlette>=0.38.0
@@ -0,0 +1,30 @@
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 (standalone script, imports from installed fleet-python)
30
+ exec python mcp_server/main.py