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.
- examples/export_tasks.py +16 -5
- examples/export_tasks_filtered.py +245 -0
- examples/fetch_tasks.py +230 -0
- examples/import_tasks.py +140 -8
- examples/iterate_verifiers.py +725 -0
- fleet/__init__.py +128 -5
- fleet/_async/__init__.py +27 -3
- fleet/_async/base.py +24 -9
- fleet/_async/client.py +938 -41
- fleet/_async/env/client.py +60 -3
- fleet/_async/instance/client.py +52 -7
- fleet/_async/models.py +15 -0
- fleet/_async/resources/api.py +200 -0
- fleet/_async/resources/sqlite.py +1801 -46
- fleet/_async/tasks.py +122 -25
- fleet/_async/verifiers/bundler.py +22 -21
- fleet/_async/verifiers/verifier.py +25 -19
- fleet/agent/__init__.py +32 -0
- fleet/agent/gemini_cua/Dockerfile +45 -0
- fleet/agent/gemini_cua/__init__.py +10 -0
- fleet/agent/gemini_cua/agent.py +759 -0
- fleet/agent/gemini_cua/mcp/main.py +108 -0
- fleet/agent/gemini_cua/mcp_server/__init__.py +5 -0
- fleet/agent/gemini_cua/mcp_server/main.py +105 -0
- fleet/agent/gemini_cua/mcp_server/tools.py +178 -0
- fleet/agent/gemini_cua/requirements.txt +5 -0
- fleet/agent/gemini_cua/start.sh +30 -0
- fleet/agent/orchestrator.py +854 -0
- fleet/agent/types.py +49 -0
- fleet/agent/utils.py +34 -0
- fleet/base.py +34 -9
- fleet/cli.py +1061 -0
- fleet/client.py +1060 -48
- fleet/config.py +1 -1
- fleet/env/__init__.py +16 -0
- fleet/env/client.py +60 -3
- fleet/eval/__init__.py +15 -0
- fleet/eval/uploader.py +231 -0
- fleet/exceptions.py +8 -0
- fleet/instance/client.py +53 -8
- fleet/instance/models.py +1 -0
- fleet/models.py +303 -0
- fleet/proxy/__init__.py +25 -0
- fleet/proxy/proxy.py +453 -0
- fleet/proxy/whitelist.py +244 -0
- fleet/resources/api.py +200 -0
- fleet/resources/sqlite.py +1845 -46
- fleet/tasks.py +113 -20
- fleet/utils/__init__.py +7 -0
- fleet/utils/http_logging.py +178 -0
- fleet/utils/logging.py +13 -0
- fleet/utils/playwright.py +440 -0
- fleet/verifiers/bundler.py +22 -21
- fleet/verifiers/db.py +985 -1
- fleet/verifiers/decorator.py +1 -1
- fleet/verifiers/verifier.py +25 -19
- {fleet_python-0.2.66b2.dist-info → fleet_python-0.2.105.dist-info}/METADATA +28 -1
- fleet_python-0.2.105.dist-info/RECORD +115 -0
- {fleet_python-0.2.66b2.dist-info → fleet_python-0.2.105.dist-info}/WHEEL +1 -1
- fleet_python-0.2.105.dist-info/entry_points.txt +2 -0
- tests/test_app_method.py +85 -0
- tests/test_expect_exactly.py +4148 -0
- tests/test_expect_only.py +2593 -0
- tests/test_instance_dispatch.py +607 -0
- tests/test_sqlite_resource_dual_mode.py +263 -0
- tests/test_sqlite_shared_memory_behavior.py +117 -0
- fleet_python-0.2.66b2.dist-info/RECORD +0 -81
- tests/test_verifier_security.py +0 -427
- {fleet_python-0.2.66b2.dist-info → fleet_python-0.2.105.dist-info}/licenses/LICENSE +0 -0
- {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,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,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
|