agentenv-framework-protocol 0.1.459__tar.gz → 0.1.462__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.
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/PKG-INFO +4 -1
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/pyproject.toml +6 -1
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/__init__.py +5 -2
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/_declare.py +3 -1
- agentenv_framework_protocol-0.1.462/src/agentenv_protocol/computer/_macos.py +433 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/checks.py +3 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/driver.py +7 -3
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/settings.py +5 -3
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/website/environment.py +24 -7
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/website/routes.py +17 -4
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_computer_declare.py +10 -2
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_computer_driver.py +9 -0
- agentenv_framework_protocol-0.1.462/tests/test_computer_macos.py +214 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_website_api.py +13 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_website_environment.py +20 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/.gitignore +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/LICENSE +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/NOTICE +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/README.md +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/THIRD_PARTY_NOTICES.md +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/examples/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/examples/advanced_agent.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/examples/basic_agent.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/examples/custom_extensions_agent.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/examples/multimodal_agent.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/examples/reference_model.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/examples/streaming_agent.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/a2a_agent/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/a2a_agent/_trajectory_log.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/a2a_agent/_triggers.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/a2a_agent/extensions.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/a2a_agent/framework.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/a2a_agent/peer_agents.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/a2a_agent/proxy.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/a2a_agent/registry.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/a2a_agent/staging.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/a2a_agent/tasks/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/a2a_agent/tasks/v1.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/agent_env_environment.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/browser_cdp.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/client.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/_linux.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/_load.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/_with_fds.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/app.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/app_data.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/app_runtime.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/apps.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/card.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/conformance/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/conformance/__main__.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/conformance/checks.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/conformance/client.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/conformance/fixture.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/conformance/mp4.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/conformance/page_fixture.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/conformance/report.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/dataplane.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/errors.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/extensions.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/frames.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/keys.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/launch.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/manifest.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/operations.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/platform.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/queue.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/recording.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/results.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/run_install_step.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/runtime.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/seed.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/serve.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/serve_app.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/session.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/skill.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/spaces.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/svc.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/tools.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/errors.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/manifest.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/preflight.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/tool_access.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/tool_client.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/tool_context.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/transfers.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/types.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/website/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/website/clock.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/website/conformance.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/website/errors.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/website/readiness.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/website/session.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/website_browser.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/computer_fake.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/probe_fake.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_a2a_agent.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_a2a_agent_examples.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_a2a_cdp_config.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_a2a_object_transfers.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_a2a_peer_agents.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_a2a_proxy.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_a2a_staging.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_a2a_trajectory_log.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_agent_env_environment.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_browser_cdp.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_client.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_computer_app_data.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_computer_app_server.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_computer_conformance.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_computer_conformance_checks.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_computer_conformance_page.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_computer_conformance_recording.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_computer_dataplane.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_computer_extensions.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_computer_frames.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_computer_page.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_computer_queue.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_computer_recording.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_computer_seed.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_computer_serve.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_computer_spaces.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_computer_svc_forward.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_control_token.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_data_objects.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_errors.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_extension_methods.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_intake.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_manifest.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_preflight.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_tool_access.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_tool_annotations.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_tool_client.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_tool_context.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_tool_context_fastmcp.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_tool_context_injection.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_website_browser.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_website_conformance.py +0 -0
- {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_website_readiness.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: agentenv-framework-protocol
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.462
|
|
4
4
|
Summary: Open data-plane protocol and server SDK for agent environments.
|
|
5
5
|
Project-URL: Homepage, https://www.agentenvframework.com
|
|
6
6
|
Project-URL: Documentation, https://www.agentenvframework.com/docs
|
|
@@ -55,6 +55,9 @@ Requires-Dist: pytest>=9.0; extra == 'dev'
|
|
|
55
55
|
Requires-Dist: python-multipart>=0.0.13; extra == 'dev'
|
|
56
56
|
Requires-Dist: regex>=2023.12.25; extra == 'dev'
|
|
57
57
|
Requires-Dist: websockets>=13; extra == 'dev'
|
|
58
|
+
Provides-Extra: macos
|
|
59
|
+
Requires-Dist: pyobjc-framework-quartz>=11; (sys_platform == 'darwin') and extra == 'macos'
|
|
60
|
+
Requires-Dist: pyobjc-framework-screencapturekit>=11; (sys_platform == 'darwin') and extra == 'macos'
|
|
58
61
|
Description-Content-Type: text/markdown
|
|
59
62
|
|
|
60
63
|
# agentenv-framework-protocol
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "agentenv-framework-protocol"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.462"
|
|
4
4
|
description = "Open data-plane protocol and server SDK for agent environments."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -51,6 +51,11 @@ computer = [
|
|
|
51
51
|
"uvicorn>=0.30",
|
|
52
52
|
"websockets>=13",
|
|
53
53
|
]
|
|
54
|
+
# A macOS driver on MacOSDriver: ScreenCaptureKit captures the display and Quartz events send its input, through PyObjC.
|
|
55
|
+
macos = [
|
|
56
|
+
"pyobjc-framework-Quartz>=11; sys_platform == 'darwin'",
|
|
57
|
+
"pyobjc-framework-ScreenCaptureKit>=11; sys_platform == 'darwin'",
|
|
58
|
+
]
|
|
54
59
|
dev = [
|
|
55
60
|
"a2a-sdk[http-server]>=0.3.26,<0.4",
|
|
56
61
|
"python-multipart>=0.0.13",
|
|
@@ -97,14 +97,17 @@ if TYPE_CHECKING:
|
|
|
97
97
|
COMPUTER_SPACES,
|
|
98
98
|
)
|
|
99
99
|
from .driver import DEFAULT_IDENTITY, ComputerDriverEnvironment, DriverDeclaration, computer_driver
|
|
100
|
+
from ._macos import MacOSDriver
|
|
100
101
|
from .recording import RecordedFrame, RecordedSegment, RecordHints, Recording, frames_in_thread
|
|
101
102
|
|
|
102
|
-
# The names whose modules need the computer extra (mcp, av and Pillow), by module
|
|
103
|
+
# The names whose modules need the computer extra (mcp, av and Pillow), by module; MacOSDriver's, the macos extra too,
|
|
104
|
+
# on first use of the Mac.
|
|
103
105
|
_ON_USE = {
|
|
104
106
|
"driver": ("ComputerDriverEnvironment", "computer_driver", "DEFAULT_IDENTITY", "DriverDeclaration"),
|
|
105
107
|
"card": ("COMPUTER", "COMPUTER_READY", "COMPUTER_DISPLAY", "COMPUTER_SPACES", "COMPUTER_POINTER",
|
|
106
108
|
"COMPUTER_KEYBOARD", "COMPUTER_PAGE", "COMPUTER_EXEC", "COMPUTER_RECORDING"),
|
|
107
109
|
"recording": ("Recording", "RecordHints", "RecordedFrame", "RecordedSegment", "frames_in_thread"),
|
|
110
|
+
"_macos": ("MacOSDriver",),
|
|
108
111
|
}
|
|
109
112
|
|
|
110
113
|
|
|
@@ -116,7 +119,7 @@ def __getattr__(name: str) -> Any:
|
|
|
116
119
|
|
|
117
120
|
|
|
118
121
|
__all__ = [
|
|
119
|
-
"ComputerDriverEnvironment", "computer_driver", "DEFAULT_IDENTITY", "DriverDeclaration",
|
|
122
|
+
"ComputerDriverEnvironment", "computer_driver", "DEFAULT_IDENTITY", "DriverDeclaration", "MacOSDriver",
|
|
120
123
|
"ComputerAppEnvironment", "computer_app", "install", "linux", "install_step", "ready_check", "Linux", "Service",
|
|
121
124
|
"Launch", "Ready", "Packages", "AptSource", "Download", "RunScript",
|
|
122
125
|
"AppDeclaration", "AppOs",
|
|
@@ -20,6 +20,7 @@ from .app import AppDeclaration, ComputerAppEnvironment
|
|
|
20
20
|
from .card import COMPUTER
|
|
21
21
|
from .driver import ComputerDriverEnvironment, DriverDeclaration, DriverPlatform
|
|
22
22
|
from .platform import supported
|
|
23
|
+
from .settings import ComputerSettings
|
|
23
24
|
|
|
24
25
|
#: The env type that stores each kind of declaration, which the printed JSON carries as ``type``.
|
|
25
26
|
_ENV_TYPES = {AppDeclaration: "computer_app", DriverDeclaration: "computer_driver"}
|
|
@@ -82,7 +83,8 @@ def declare(folder: Path, file: str, name: str) -> AppDeclaration | DriverDeclar
|
|
|
82
83
|
if not platforms:
|
|
83
84
|
raise ValueError(f"{name} declares no platform: name them with @computer_driver(supports=[...])")
|
|
84
85
|
cards = [(platform, cls.declaration(platform)) for platform in platforms]
|
|
85
|
-
|
|
86
|
+
own = cls.Settings.model_json_schema() if cls.Settings is not ComputerSettings else None
|
|
87
|
+
return DriverDeclaration(name=cards[0][1].name, supports=list(cls.supports), settings=own, platforms=[
|
|
86
88
|
DriverPlatform(platform=platform, provides=[e.uri for e in card.capabilities.extensions],
|
|
87
89
|
tools=[t.name for t in card.capabilities.tools],
|
|
88
90
|
features=next({k: v for k, v in e.params.items() if k not in ("endpoint", "methods")}
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
"""computer/v1 on macOS for a driver whose software serves tools of its own, as agent-desktop's and macos-harness's do:
|
|
2
|
+
ScreenCaptureKit captures the display and Quartz events, posted where the hardware's are, send the pointer and the
|
|
3
|
+
keyboard, from the driver's process in the login session. That process needs Accessibility, Post Event and Screen
|
|
4
|
+
Recording, which its layer grants the Python it runs on (``[install.macos]``'s ``grants``); ``computer-ready`` names
|
|
5
|
+
any it lacks. A driver subclasses ``MacOSDriver``, declares itself with ``computer_driver``, and serves its own tools
|
|
6
|
+
beside these. Needs the ``macos`` extra: PyObjC's Quartz and ScreenCaptureKit."""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import ctypes
|
|
11
|
+
import math
|
|
12
|
+
import threading
|
|
13
|
+
import time
|
|
14
|
+
from contextlib import contextmanager
|
|
15
|
+
from typing import Any, Callable, Iterator
|
|
16
|
+
|
|
17
|
+
from .driver import ComputerDriverEnvironment
|
|
18
|
+
from .errors import DeliveryFailed, DeviceUnavailable, Unsupported
|
|
19
|
+
from .keys import PRINTABLE_KEYS
|
|
20
|
+
from .operations import (Display, Image, ReadyCheck, capture, click, display, drag, key_down, key_up, pointer_down,
|
|
21
|
+
pointer_move, pointer_position, pointer_up, press, scroll, type_text)
|
|
22
|
+
from .platform import Platform
|
|
23
|
+
from .settings import ComputerSettings
|
|
24
|
+
|
|
25
|
+
#: Each W3C modifier key value: its virtual key code (kVK_*) and the Quartz event flag it holds; Meta and Super are
|
|
26
|
+
#: Command.
|
|
27
|
+
MODIFIERS = {"Shift": (56, 0x20000), "Control": (59, 0x40000), "Alt": (58, 0x80000), "Meta": (55, 0x100000),
|
|
28
|
+
"Super": (55, 0x100000)}
|
|
29
|
+
SHIFT = "Shift"
|
|
30
|
+
#: Named W3C key values and their virtual key codes.
|
|
31
|
+
NAMED = {
|
|
32
|
+
"Enter": 36, "Tab": 48, " ": 49, "Backspace": 51, "Escape": 53, "Delete": 117, "Help": 114, "Home": 115,
|
|
33
|
+
"End": 119, "PageUp": 116, "PageDown": 121, "ArrowLeft": 123, "ArrowRight": 124, "ArrowDown": 125,
|
|
34
|
+
"ArrowUp": 126, "CapsLock": 57,
|
|
35
|
+
"F1": 122, "F2": 120, "F3": 99, "F4": 118, "F5": 96, "F6": 97, "F7": 98, "F8": 100, "F9": 101, "F10": 109,
|
|
36
|
+
"F11": 103, "F12": 111, "F13": 105, "F14": 107, "F15": 113, "F16": 106, "F17": 64, "F18": 79, "F19": 80,
|
|
37
|
+
"F20": 90,
|
|
38
|
+
}
|
|
39
|
+
#: The virtual key code of each key on a US keyboard, by the character it types unshifted.
|
|
40
|
+
US = {
|
|
41
|
+
**dict(zip("asdfhgzxcvb", (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11))),
|
|
42
|
+
**dict(zip("qwertyuiop", (12, 13, 14, 15, 17, 16, 32, 34, 31, 35))),
|
|
43
|
+
**dict(zip("1234567890", (18, 19, 20, 21, 23, 22, 26, 28, 25, 29))),
|
|
44
|
+
**dict(zip("lkjnm", (37, 40, 38, 45, 46))),
|
|
45
|
+
"=": 24, "-": 27, "]": 30, "[": 33, "'": 39, ";": 41, "\\": 42, ",": 43, "/": 44, ".": 47, "`": 50,
|
|
46
|
+
}
|
|
47
|
+
#: Each character a US keyboard types with Shift, and the key's unshifted character.
|
|
48
|
+
SHIFTED = {**{c.upper(): c for c in "abcdefghijklmnopqrstuvwxyz"},
|
|
49
|
+
**dict(zip('!@#$%^&*()_+{}|:"<>?~', "1234567890-=[]\\;',./`"))}
|
|
50
|
+
#: How typing sends a space, a tab or a newline.
|
|
51
|
+
TYPED = {" ": " ", "\t": "Tab", "\n": "Enter"}
|
|
52
|
+
#: The key code a character off the keyboard is typed on, carrying the character itself.
|
|
53
|
+
UNMAPPED = 0
|
|
54
|
+
#: Quartz's event types for each button: down, up and dragged; and the pointer moved with no button down.
|
|
55
|
+
BUTTONS = {"left": (0, 1, 2, 6), "right": (1, 3, 4, 7), "middle": (2, 25, 26, 27)}
|
|
56
|
+
MOVED = 5
|
|
57
|
+
#: How long the pointer rests where a click or a drag presses before the press: an app such as Tk reads where the
|
|
58
|
+
#: pointer is when it handles the press, and a button the pointer hasn't yet entered loses the click.
|
|
59
|
+
SETTLE_S = 0.2
|
|
60
|
+
#: How long a click holds its button down, as a hand does: a press released at once is lost now and then.
|
|
61
|
+
CLICK_HOLD_S = 0.03
|
|
62
|
+
#: The pause between the presses of a double or triple click.
|
|
63
|
+
CLICK_GAP_S = 0.04
|
|
64
|
+
#: How far apart a drag's pointer moves are, in pixels.
|
|
65
|
+
DRAG_STEP_PX = 20
|
|
66
|
+
#: The pause between a scroll's wheel notches, as a wheel turned by hand has: notches posted back to back reach an app
|
|
67
|
+
#: as fewer.
|
|
68
|
+
SCROLL_GAP_S = 0.05
|
|
69
|
+
#: How long a posted move may take to become where macOS says the pointer is, and how often that's looked at: a press
|
|
70
|
+
#: at the pointer right after a move otherwise lands where it was.
|
|
71
|
+
MOVE_SETTLE_S, MOVE_POLL_S = 0.2, 0.01
|
|
72
|
+
#: How long ScreenCaptureKit may take to answer.
|
|
73
|
+
CAPTURE_TIMEOUT_S = 10.0
|
|
74
|
+
#: The TCC permissions the driver's process needs, and how each is named to a person.
|
|
75
|
+
PERMISSIONS = {"accessibility": "Accessibility", "post_event": "Post Event", "screen_capture": "Screen Recording"}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class MacOSDriver(ComputerDriverEnvironment):
|
|
79
|
+
"""The computer/v1 operations, the pointer and keyboard groups among them, on macOS. Input goes out one call at a
|
|
80
|
+
time; keys and buttons held by ``@key_down`` and ``@pointer_down`` stay held, as their flags on every event, until
|
|
81
|
+
released."""
|
|
82
|
+
|
|
83
|
+
buttons = ("left", "right", "middle")
|
|
84
|
+
max_click_count = 3
|
|
85
|
+
max_drag_points = 50
|
|
86
|
+
keys = (*MODIFIERS, *NAMED, *PRINTABLE_KEYS)
|
|
87
|
+
|
|
88
|
+
def __init__(self, settings: ComputerSettings | None = None, platform: Platform | None = None) -> None:
|
|
89
|
+
super().__init__(settings, platform)
|
|
90
|
+
self._mac: Quartz | None = None
|
|
91
|
+
self._mac_input = threading.Lock()
|
|
92
|
+
#: The keys ``@key_down`` holds, each with its key code, and the buttons ``@pointer_down`` holds.
|
|
93
|
+
self._mac_keys: dict[str, int] = {}
|
|
94
|
+
self._mac_buttons: list[str] = []
|
|
95
|
+
|
|
96
|
+
@display
|
|
97
|
+
async def screen(self) -> Display:
|
|
98
|
+
width, height, scale = await asyncio.to_thread(self._mac_backend().display)
|
|
99
|
+
return Display(width, height, scale)
|
|
100
|
+
|
|
101
|
+
@capture
|
|
102
|
+
async def grab(self) -> Image:
|
|
103
|
+
width, height, data = await asyncio.to_thread(self._mac_backend().capture)
|
|
104
|
+
return Image(width, height, data, "BGRA")
|
|
105
|
+
|
|
106
|
+
async def ready_checks(self) -> list[ReadyCheck]:
|
|
107
|
+
missing = await asyncio.to_thread(self._mac_backend().missing)
|
|
108
|
+
return [ReadyCheck("macOS permissions", not missing, "Accessibility, Post Event and Screen Recording granted"
|
|
109
|
+
if not missing else f"macOS hasn't granted this driver {', '.join(missing)}, which its "
|
|
110
|
+
"layer grants the Python it runs on")]
|
|
111
|
+
|
|
112
|
+
@click
|
|
113
|
+
async def click_at(self, x, y, button, count, modifiers) -> None:
|
|
114
|
+
number, down, up, _ = BUTTONS[button]
|
|
115
|
+
|
|
116
|
+
def act(mac):
|
|
117
|
+
with self._mac_holding(mac, modifiers) as flags:
|
|
118
|
+
mac.mouse(MOVED, x, y, 0, 1, flags)
|
|
119
|
+
time.sleep(SETTLE_S)
|
|
120
|
+
for n in range(1, count + 1):
|
|
121
|
+
mac.mouse(down, x, y, number, n, flags)
|
|
122
|
+
time.sleep(CLICK_HOLD_S)
|
|
123
|
+
mac.mouse(up, x, y, number, n, flags)
|
|
124
|
+
if n < count:
|
|
125
|
+
time.sleep(CLICK_GAP_S)
|
|
126
|
+
await self._mac_send(act)
|
|
127
|
+
|
|
128
|
+
@drag
|
|
129
|
+
async def drag_along(self, path, button, duration_ms, modifiers) -> None:
|
|
130
|
+
number, down, up, dragged = BUTTONS[button]
|
|
131
|
+
steps = _steps(path)
|
|
132
|
+
pause = duration_ms / 1000 / len(steps)
|
|
133
|
+
|
|
134
|
+
def act(mac):
|
|
135
|
+
with self._mac_holding(mac, modifiers) as flags:
|
|
136
|
+
mac.mouse(MOVED, *path[0], 0, 1, flags)
|
|
137
|
+
time.sleep(SETTLE_S)
|
|
138
|
+
mac.mouse(down, *path[0], number, 1, flags)
|
|
139
|
+
try:
|
|
140
|
+
for x, y in steps:
|
|
141
|
+
time.sleep(pause)
|
|
142
|
+
mac.mouse(dragged, x, y, number, 1, flags)
|
|
143
|
+
finally:
|
|
144
|
+
mac.mouse(up, *path[-1], number, 1, flags)
|
|
145
|
+
await self._mac_send(act)
|
|
146
|
+
|
|
147
|
+
@scroll
|
|
148
|
+
async def scroll_at(self, x, y, direction, amount, modifiers) -> None:
|
|
149
|
+
lines, across = {"up": (1, 0), "down": (-1, 0), "left": (0, 1), "right": (0, -1)}[direction]
|
|
150
|
+
|
|
151
|
+
def act(mac):
|
|
152
|
+
with self._mac_holding(mac, modifiers) as flags:
|
|
153
|
+
mac.mouse(MOVED, x, y, 0, 1, flags)
|
|
154
|
+
for notch in range(amount):
|
|
155
|
+
if notch:
|
|
156
|
+
time.sleep(SCROLL_GAP_S)
|
|
157
|
+
mac.wheel(x, y, lines, across, flags)
|
|
158
|
+
await self._mac_send(act)
|
|
159
|
+
|
|
160
|
+
@type_text
|
|
161
|
+
async def type_in(self, text) -> None:
|
|
162
|
+
hold, delay = self.settings.tap_hold_ms / 1000, self.settings.type_delay_ms / 1000
|
|
163
|
+
for character in text:
|
|
164
|
+
await self._mac_send(lambda mac, character=character: self._mac_type(mac, character, hold))
|
|
165
|
+
await asyncio.sleep(delay)
|
|
166
|
+
|
|
167
|
+
@press
|
|
168
|
+
async def press_keys(self, keys) -> None:
|
|
169
|
+
*held, key = keys
|
|
170
|
+
code = _code(key)
|
|
171
|
+
hold = self.settings.tap_hold_ms / 1000
|
|
172
|
+
if key in SHIFTED and SHIFT not in held:
|
|
173
|
+
held.append(SHIFT)
|
|
174
|
+
|
|
175
|
+
def act(mac):
|
|
176
|
+
with self._mac_holding(mac, held) as flags:
|
|
177
|
+
if key in MODIFIERS:
|
|
178
|
+
with self._mac_holding(mac, [key]):
|
|
179
|
+
time.sleep(hold)
|
|
180
|
+
return
|
|
181
|
+
mac.key(code, True, flags)
|
|
182
|
+
time.sleep(hold)
|
|
183
|
+
mac.key(code, False, flags)
|
|
184
|
+
await self._mac_send(act)
|
|
185
|
+
|
|
186
|
+
@key_down
|
|
187
|
+
async def keys_down(self, keys) -> None:
|
|
188
|
+
codes = [_code(key) for key in keys]
|
|
189
|
+
|
|
190
|
+
def act(mac):
|
|
191
|
+
for key, code in zip(keys, codes):
|
|
192
|
+
if key not in self._mac_keys:
|
|
193
|
+
self._mac_keys[key] = code
|
|
194
|
+
mac.key(code, True, self._mac_flags())
|
|
195
|
+
await self._mac_send(act)
|
|
196
|
+
|
|
197
|
+
@key_up
|
|
198
|
+
async def keys_up(self, keys) -> None:
|
|
199
|
+
def act(mac):
|
|
200
|
+
for key in keys:
|
|
201
|
+
code = self._mac_keys.pop(key, None)
|
|
202
|
+
mac.key(_code(key) if code is None else code, False, self._mac_flags())
|
|
203
|
+
await self._mac_send(act)
|
|
204
|
+
|
|
205
|
+
@pointer_move
|
|
206
|
+
async def move(self, x, y) -> None:
|
|
207
|
+
def act(mac):
|
|
208
|
+
if self._mac_buttons:
|
|
209
|
+
number, _, _, dragged = BUTTONS[self._mac_buttons[0]]
|
|
210
|
+
mac.mouse(dragged, x, y, number, 1, self._mac_flags())
|
|
211
|
+
else:
|
|
212
|
+
mac.mouse(MOVED, x, y, 0, 1, self._mac_flags())
|
|
213
|
+
deadline = time.monotonic() + MOVE_SETTLE_S
|
|
214
|
+
while max(abs(a - b) for a, b in zip(mac.location(), (x, y))) >= 1 and time.monotonic() < deadline:
|
|
215
|
+
time.sleep(MOVE_POLL_S)
|
|
216
|
+
await self._mac_send(act)
|
|
217
|
+
|
|
218
|
+
@pointer_down
|
|
219
|
+
async def down(self, button) -> None:
|
|
220
|
+
def act(mac):
|
|
221
|
+
number, down, _, _ = BUTTONS[button]
|
|
222
|
+
mac.mouse(down, *mac.location(), number, 1, self._mac_flags())
|
|
223
|
+
if button not in self._mac_buttons:
|
|
224
|
+
self._mac_buttons.append(button)
|
|
225
|
+
await self._mac_send(act)
|
|
226
|
+
|
|
227
|
+
@pointer_up
|
|
228
|
+
async def up(self, button) -> None:
|
|
229
|
+
def act(mac):
|
|
230
|
+
number, _, up, _ = BUTTONS[button]
|
|
231
|
+
mac.mouse(up, *mac.location(), number, 1, self._mac_flags())
|
|
232
|
+
if button in self._mac_buttons:
|
|
233
|
+
self._mac_buttons.remove(button)
|
|
234
|
+
await self._mac_send(act)
|
|
235
|
+
|
|
236
|
+
@pointer_position
|
|
237
|
+
async def where(self) -> tuple[int, int]:
|
|
238
|
+
x, y = await asyncio.to_thread(self._mac_backend().location)
|
|
239
|
+
return round(x), round(y)
|
|
240
|
+
|
|
241
|
+
def _mac_type(self, mac: Any, character: str, hold: float) -> None:
|
|
242
|
+
"""One character: its key, with Shift around it when it needs Shift and no key holds Shift; or, off the
|
|
243
|
+
keyboard, a key carrying the character itself."""
|
|
244
|
+
if character in TYPED:
|
|
245
|
+
code, shifted, carried = NAMED[TYPED[character]], False, None
|
|
246
|
+
elif character in US or character in SHIFTED:
|
|
247
|
+
code, shifted, carried = US[SHIFTED.get(character, character)], character in SHIFTED, None
|
|
248
|
+
else:
|
|
249
|
+
code, shifted, carried = UNMAPPED, False, character
|
|
250
|
+
with self._mac_holding(mac, [SHIFT] if shifted and SHIFT not in self._mac_keys else []) as flags:
|
|
251
|
+
mac.key(code, True, flags, carried)
|
|
252
|
+
time.sleep(hold)
|
|
253
|
+
mac.key(code, False, flags, carried)
|
|
254
|
+
|
|
255
|
+
@contextmanager
|
|
256
|
+
def _mac_holding(self, mac: Any, modifiers: list[str]) -> Iterator[int]:
|
|
257
|
+
"""The modifier keys pressed in order and released in reverse, as a keyboard sends them; yields the flags every
|
|
258
|
+
event between carries, those of the keys ``@key_down`` holds too."""
|
|
259
|
+
pressed: list[str] = []
|
|
260
|
+
try:
|
|
261
|
+
for modifier in modifiers:
|
|
262
|
+
if modifier not in MODIFIERS:
|
|
263
|
+
raise Unsupported(f"{modifier!r} isn't a modifier key, so it can't be held with another")
|
|
264
|
+
if modifier in self._mac_keys or modifier in pressed:
|
|
265
|
+
continue
|
|
266
|
+
pressed.append(modifier)
|
|
267
|
+
mac.key(MODIFIERS[modifier][0], True, self._mac_flags(pressed))
|
|
268
|
+
yield self._mac_flags(pressed)
|
|
269
|
+
finally:
|
|
270
|
+
for modifier in reversed(pressed):
|
|
271
|
+
pressed.remove(modifier)
|
|
272
|
+
mac.key(MODIFIERS[modifier][0], False, self._mac_flags(pressed))
|
|
273
|
+
|
|
274
|
+
def _mac_flags(self, pressed: list[str] = ()) -> int:
|
|
275
|
+
flags = 0
|
|
276
|
+
for key in (*self._mac_keys, *pressed):
|
|
277
|
+
if key in MODIFIERS:
|
|
278
|
+
flags |= MODIFIERS[key][1]
|
|
279
|
+
return flags
|
|
280
|
+
|
|
281
|
+
async def _mac_send(self, act: Callable[[Any], None]) -> None:
|
|
282
|
+
"""``act`` on the Mac, one input at a time. Quartz posts an event or raises, and a raise mid-input may have
|
|
283
|
+
sent some of it."""
|
|
284
|
+
def run():
|
|
285
|
+
with self._mac_input:
|
|
286
|
+
act(self._mac_backend())
|
|
287
|
+
try:
|
|
288
|
+
await asyncio.to_thread(run)
|
|
289
|
+
except (Unsupported, DeviceUnavailable):
|
|
290
|
+
raise
|
|
291
|
+
except Exception as e:
|
|
292
|
+
raise DeliveryFailed(f"Quartz failed while sending input: {type(e).__name__}: {e}") from None
|
|
293
|
+
|
|
294
|
+
def _mac_backend(self) -> Quartz:
|
|
295
|
+
if self._mac is None:
|
|
296
|
+
self._mac = Quartz()
|
|
297
|
+
return self._mac
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _code(key: str) -> int:
|
|
301
|
+
"""The virtual key code of a key value this driver presses: a modifier, a named key, or a character on a US
|
|
302
|
+
keyboard, its unshifted key for one it types with Shift."""
|
|
303
|
+
if key in MODIFIERS:
|
|
304
|
+
return MODIFIERS[key][0]
|
|
305
|
+
if key in NAMED:
|
|
306
|
+
return NAMED[key]
|
|
307
|
+
if key in US or key in SHIFTED:
|
|
308
|
+
return US[SHIFTED.get(key, key)]
|
|
309
|
+
raise Unsupported(f"no key on a US keyboard presses {key!r}")
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _steps(path: list) -> list[tuple[float, float]]:
|
|
313
|
+
"""The points a drag moves through: each of ``path`` after its first, with points about ``DRAG_STEP_PX`` apart
|
|
314
|
+
between them."""
|
|
315
|
+
steps = []
|
|
316
|
+
for (x0, y0), (x1, y1) in zip(path, path[1:]):
|
|
317
|
+
count = max(1, math.ceil(math.hypot(x1 - x0, y1 - y0) / DRAG_STEP_PX))
|
|
318
|
+
steps += [(x0 + (x1 - x0) * i / count, y0 + (y1 - y0) * i / count) for i in range(1, count + 1)]
|
|
319
|
+
return steps or [tuple(path[-1])]
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
class Quartz:
|
|
323
|
+
"""The Mac through PyObjC: ScreenCaptureKit for the display, Quartz events for input, and TCC's answers for this
|
|
324
|
+
process, read through ctypes."""
|
|
325
|
+
|
|
326
|
+
def __init__(self) -> None:
|
|
327
|
+
try:
|
|
328
|
+
import Quartz as quartz
|
|
329
|
+
import ScreenCaptureKit
|
|
330
|
+
except ImportError as e:
|
|
331
|
+
raise DeviceUnavailable(f"PyObjC's Quartz and ScreenCaptureKit aren't installed: {e}") from None
|
|
332
|
+
self.q, self.sck = quartz, ScreenCaptureKit
|
|
333
|
+
self._shot: tuple | None = None
|
|
334
|
+
services = ctypes.cdll.LoadLibrary("/System/Library/Frameworks/ApplicationServices.framework/ApplicationServices")
|
|
335
|
+
services.AXIsProcessTrusted.restype = ctypes.c_bool
|
|
336
|
+
graphics = ctypes.cdll.LoadLibrary("/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics")
|
|
337
|
+
graphics.CGPreflightPostEventAccess.restype = ctypes.c_bool
|
|
338
|
+
graphics.CGPreflightScreenCaptureAccess.restype = ctypes.c_bool
|
|
339
|
+
self._granted = {"accessibility": services.AXIsProcessTrusted,
|
|
340
|
+
"post_event": graphics.CGPreflightPostEventAccess,
|
|
341
|
+
"screen_capture": graphics.CGPreflightScreenCaptureAccess}
|
|
342
|
+
|
|
343
|
+
def missing(self) -> list[str]:
|
|
344
|
+
return [PERMISSIONS[name] for name, granted in self._granted.items() if not granted()]
|
|
345
|
+
|
|
346
|
+
def display(self) -> tuple[int, int, float]:
|
|
347
|
+
q = self.q
|
|
348
|
+
main = q.CGMainDisplayID()
|
|
349
|
+
bounds = q.CGDisplayBounds(main)
|
|
350
|
+
width, height = int(bounds.size.width), int(bounds.size.height)
|
|
351
|
+
pixels = q.CGDisplayModeGetPixelWidth(q.CGDisplayCopyDisplayMode(main))
|
|
352
|
+
return width, height, pixels / width if width and pixels else 1.0
|
|
353
|
+
|
|
354
|
+
def capture(self) -> tuple[int, int, bytes]:
|
|
355
|
+
"""The main display at its native resolution, as BGRA rows. Its filter is made once a display mode, and again
|
|
356
|
+
when a capture fails, as one does after the mode changes."""
|
|
357
|
+
try:
|
|
358
|
+
image = self._screenshot()
|
|
359
|
+
except DeviceUnavailable:
|
|
360
|
+
self._shot = None
|
|
361
|
+
image = self._screenshot()
|
|
362
|
+
q = self.q
|
|
363
|
+
width, height, row = q.CGImageGetWidth(image), q.CGImageGetHeight(image), q.CGImageGetBytesPerRow(image)
|
|
364
|
+
data = bytes(q.CGDataProviderCopyData(q.CGImageGetDataProvider(image)))
|
|
365
|
+
if row != width * 4:
|
|
366
|
+
data = b"".join(data[y * row:y * row + width * 4] for y in range(height))
|
|
367
|
+
return width, height, data
|
|
368
|
+
|
|
369
|
+
def _screenshot(self) -> Any:
|
|
370
|
+
return self._wait(lambda done: self.sck.SCScreenshotManager
|
|
371
|
+
.captureImageWithFilter_configuration_completionHandler_(*self._filter(), done))
|
|
372
|
+
|
|
373
|
+
def _filter(self) -> tuple:
|
|
374
|
+
q, sck = self.q, self.sck
|
|
375
|
+
main = q.CGMainDisplayID()
|
|
376
|
+
mode = q.CGDisplayCopyDisplayMode(main)
|
|
377
|
+
size = (q.CGDisplayModeGetPixelWidth(mode), q.CGDisplayModeGetPixelHeight(mode))
|
|
378
|
+
if self._shot is None or self._shot[0] != size:
|
|
379
|
+
content = self._wait(sck.SCShareableContent.getShareableContentWithCompletionHandler_)
|
|
380
|
+
screen = next((d for d in content.displays() if d.displayID() == main), None)
|
|
381
|
+
if screen is None:
|
|
382
|
+
raise DeviceUnavailable("ScreenCaptureKit lists no main display")
|
|
383
|
+
config = sck.SCStreamConfiguration.alloc().init()
|
|
384
|
+
config.setWidth_(size[0])
|
|
385
|
+
config.setHeight_(size[1])
|
|
386
|
+
config.setShowsCursor_(False)
|
|
387
|
+
self._shot = (size, sck.SCContentFilter.alloc().initWithDisplay_excludingWindows_(screen, []), config)
|
|
388
|
+
return self._shot[1:]
|
|
389
|
+
|
|
390
|
+
@staticmethod
|
|
391
|
+
def _wait(start: Callable[[Callable], None]) -> Any:
|
|
392
|
+
"""What ScreenCaptureKit hands the completion handler ``start`` passes it: its result, or its error raised."""
|
|
393
|
+
done, answer = threading.Event(), {}
|
|
394
|
+
|
|
395
|
+
def handler(result, error):
|
|
396
|
+
answer.update(result=result, error=error)
|
|
397
|
+
done.set()
|
|
398
|
+
|
|
399
|
+
start(handler)
|
|
400
|
+
if not done.wait(CAPTURE_TIMEOUT_S):
|
|
401
|
+
raise DeviceUnavailable(f"ScreenCaptureKit didn't answer within {CAPTURE_TIMEOUT_S:.0f} s")
|
|
402
|
+
if answer["error"] is not None:
|
|
403
|
+
raise DeviceUnavailable(f"ScreenCaptureKit: {answer['error'].localizedDescription()}")
|
|
404
|
+
return answer["result"]
|
|
405
|
+
|
|
406
|
+
def mouse(self, kind: int, x: float, y: float, button: int, count: int, flags: int) -> None:
|
|
407
|
+
q = self.q
|
|
408
|
+
event = q.CGEventCreateMouseEvent(None, kind, (x, y), button)
|
|
409
|
+
q.CGEventSetIntegerValueField(event, q.kCGMouseEventClickState, count)
|
|
410
|
+
self._post(event, flags)
|
|
411
|
+
|
|
412
|
+
def wheel(self, x: float, y: float, lines: int, across: int, flags: int) -> None:
|
|
413
|
+
q = self.q
|
|
414
|
+
event = q.CGEventCreateScrollWheelEvent(None, q.kCGScrollEventUnitLine, 2, lines, across)
|
|
415
|
+
q.CGEventSetLocation(event, (x, y))
|
|
416
|
+
self._post(event, flags)
|
|
417
|
+
|
|
418
|
+
def key(self, code: int, down: bool, flags: int, character: str | None = None) -> None:
|
|
419
|
+
event = self.q.CGEventCreateKeyboardEvent(None, code, down)
|
|
420
|
+
if character is not None:
|
|
421
|
+
self.q.CGEventKeyboardSetUnicodeString(event, len(character.encode("utf-16-le")) // 2, character)
|
|
422
|
+
self._post(event, flags)
|
|
423
|
+
|
|
424
|
+
def location(self) -> tuple[float, float]:
|
|
425
|
+
point = self.q.CGEventGetLocation(self.q.CGEventCreate(None))
|
|
426
|
+
return point.x, point.y
|
|
427
|
+
|
|
428
|
+
def _post(self, event: Any, flags: int) -> None:
|
|
429
|
+
if event is None:
|
|
430
|
+
raise DeliveryFailed("Quartz made no event: this process may lack Post Event")
|
|
431
|
+
self.q.CGEventSetFlags(event, flags)
|
|
432
|
+
self.q.CGEventPost(self.q.kCGHIDEventTap, event)
|
|
433
|
+
|
|
@@ -6,6 +6,7 @@ from ..errors import AgentEnvError
|
|
|
6
6
|
from .keys import canonical
|
|
7
7
|
from .operations import declared
|
|
8
8
|
from .recording import check_recording
|
|
9
|
+
from .settings import ComputerSettings
|
|
9
10
|
|
|
10
11
|
BUTTONS = ("left", "right", "middle", "back", "forward")
|
|
11
12
|
|
|
@@ -37,6 +38,8 @@ def check_features(cls: type) -> None:
|
|
|
37
38
|
problems.append("keys are W3C key values in their W3C case")
|
|
38
39
|
except AgentEnvError as e:
|
|
39
40
|
problems.append(f"keys: {e.message}")
|
|
41
|
+
if not (isinstance(cls.Settings, type) and issubclass(cls.Settings, ComputerSettings)):
|
|
42
|
+
problems.append(f"Settings is ComputerSettings or a subclass of it, got {cls.Settings!r}")
|
|
40
43
|
if cls.screen_kind not in ("desktop", "page"):
|
|
41
44
|
problems.append(f"screen_kind is desktop or page, got {cls.screen_kind!r}")
|
|
42
45
|
if ("set_display" in declared(cls)) != bool(cls.display_modes):
|
|
@@ -48,13 +48,15 @@ class DriverPlatform(BaseModel):
|
|
|
48
48
|
|
|
49
49
|
class DriverDeclaration(BaseModel):
|
|
50
50
|
"""What a ComputerDriverEnvironment class declares, as a ``computer_driver`` env stores it: its card's ``name``, the
|
|
51
|
-
platforms it ``supports``, as predicates,
|
|
51
|
+
platforms it ``supports``, as predicates, what it serves on each of them, and ``settings``, the JSON schema of its
|
|
52
|
+
``Settings`` when it adds to the base ones."""
|
|
52
53
|
|
|
53
54
|
model_config = ConfigDict(extra="forbid")
|
|
54
55
|
|
|
55
56
|
name: str
|
|
56
57
|
supports: list[dict[str, Any]]
|
|
57
58
|
platforms: list[DriverPlatform]
|
|
59
|
+
settings: Optional[dict[str, Any]] = None
|
|
58
60
|
|
|
59
61
|
|
|
60
62
|
class ComputerDriverEnvironment(Tools, Extensions, DataPlane, AgentEnvEnvironment):
|
|
@@ -77,6 +79,8 @@ class ComputerDriverEnvironment(Tools, Extensions, DataPlane, AgentEnvEnvironmen
|
|
|
77
79
|
layouts: ClassVar[tuple[str, ...]] = ("us",)
|
|
78
80
|
#: The platforms the driver supports, as the driver env declares them: ``[{"os": {...}, "arch": [...]}]``.
|
|
79
81
|
supports: ClassVar[list] = []
|
|
82
|
+
#: The settings a deploy gives the driver, read as it starts: ComputerSettings, or a subclass adding its own.
|
|
83
|
+
Settings: ClassVar[type[ComputerSettings]] = ComputerSettings
|
|
80
84
|
#: computer-recording/v1, from ``@record`` if the driver has one, else by polling ``@capture``; ``None`` turns
|
|
81
85
|
#: it off.
|
|
82
86
|
recording: ClassVar[Recording | None] = Recording()
|
|
@@ -96,7 +100,7 @@ class ComputerDriverEnvironment(Tools, Extensions, DataPlane, AgentEnvEnvironmen
|
|
|
96
100
|
check_features(cls)
|
|
97
101
|
|
|
98
102
|
def __init__(self, settings: ComputerSettings | None = None, platform: Platform | None = None) -> None:
|
|
99
|
-
self.settings = settings if settings is not None else load_settings()
|
|
103
|
+
self.settings = settings if settings is not None else load_settings(model=type(self).Settings)
|
|
100
104
|
self.platform = platform if platform is not None else detect()
|
|
101
105
|
self._computer = ComputerRuntime(self)
|
|
102
106
|
#: The routes each app's card opens to agents, by app, read once it answers.
|
|
@@ -109,7 +113,7 @@ class ComputerDriverEnvironment(Tools, Extensions, DataPlane, AgentEnvEnvironmen
|
|
|
109
113
|
running its ``__init__``: what a ``computer_driver`` env records for each platform it supports. The card
|
|
110
114
|
hooks may read the class and the base class's state, not what a subclass's ``__init__`` sets."""
|
|
111
115
|
driver = cls.__new__(cls)
|
|
112
|
-
ComputerDriverEnvironment.__init__(driver, settings=
|
|
116
|
+
ComputerDriverEnvironment.__init__(driver, settings=cls.Settings(), platform=platform)
|
|
113
117
|
config = dict(getattr(cls, _CARD_CONFIG_ATTR, None) or {})
|
|
114
118
|
card = EnvironmentCard(**{**config, "name": config.get("name") or cls.__name__})
|
|
115
119
|
return AgentEnvFastMCPApplication(card, driver).environment_card
|
|
@@ -30,11 +30,13 @@ class ComputerSettings(BaseModel):
|
|
|
30
30
|
exec_timeout_s: float = Field(default=60.0, gt=0, le=3600)
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
def load_settings(environ: dict | None = None) -> ComputerSettings:
|
|
33
|
+
def load_settings(environ: dict | None = None, model: type[ComputerSettings] = ComputerSettings) -> ComputerSettings:
|
|
34
|
+
"""The settings in ``environ`` (this process's environment by default), read as ``model``: a driver's ``Settings``,
|
|
35
|
+
the base ones and those it adds."""
|
|
34
36
|
raw = (os.environ if environ is None else environ).get(SETTINGS_ENV)
|
|
35
37
|
if not raw:
|
|
36
|
-
return
|
|
38
|
+
return model()
|
|
37
39
|
try:
|
|
38
|
-
return
|
|
40
|
+
return model.model_validate(json.loads(raw))
|
|
39
41
|
except (ValueError, ValidationError) as e:
|
|
40
42
|
raise ValueError(f"{SETTINGS_ENV} is not valid computer settings: {e}") from None
|