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.
Files changed (140) hide show
  1. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/PKG-INFO +4 -1
  2. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/pyproject.toml +6 -1
  3. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/__init__.py +5 -2
  4. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/_declare.py +3 -1
  5. agentenv_framework_protocol-0.1.462/src/agentenv_protocol/computer/_macos.py +433 -0
  6. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/checks.py +3 -0
  7. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/driver.py +7 -3
  8. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/settings.py +5 -3
  9. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/website/environment.py +24 -7
  10. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/website/routes.py +17 -4
  11. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_computer_declare.py +10 -2
  12. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_computer_driver.py +9 -0
  13. agentenv_framework_protocol-0.1.462/tests/test_computer_macos.py +214 -0
  14. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_website_api.py +13 -0
  15. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_website_environment.py +20 -0
  16. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/.gitignore +0 -0
  17. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/LICENSE +0 -0
  18. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/NOTICE +0 -0
  19. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/README.md +0 -0
  20. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/THIRD_PARTY_NOTICES.md +0 -0
  21. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/examples/__init__.py +0 -0
  22. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/examples/advanced_agent.py +0 -0
  23. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/examples/basic_agent.py +0 -0
  24. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/examples/custom_extensions_agent.py +0 -0
  25. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/examples/multimodal_agent.py +0 -0
  26. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/examples/reference_model.py +0 -0
  27. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/examples/streaming_agent.py +0 -0
  28. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/__init__.py +0 -0
  29. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/a2a_agent/__init__.py +0 -0
  30. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/a2a_agent/_trajectory_log.py +0 -0
  31. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/a2a_agent/_triggers.py +0 -0
  32. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/a2a_agent/extensions.py +0 -0
  33. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/a2a_agent/framework.py +0 -0
  34. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/a2a_agent/peer_agents.py +0 -0
  35. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/a2a_agent/proxy.py +0 -0
  36. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/a2a_agent/registry.py +0 -0
  37. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/a2a_agent/staging.py +0 -0
  38. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/a2a_agent/tasks/__init__.py +0 -0
  39. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/a2a_agent/tasks/v1.py +0 -0
  40. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/agent_env_environment.py +0 -0
  41. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/browser_cdp.py +0 -0
  42. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/client.py +0 -0
  43. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/_linux.py +0 -0
  44. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/_load.py +0 -0
  45. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/_with_fds.py +0 -0
  46. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/app.py +0 -0
  47. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/app_data.py +0 -0
  48. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/app_runtime.py +0 -0
  49. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/apps.py +0 -0
  50. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/card.py +0 -0
  51. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/conformance/__init__.py +0 -0
  52. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/conformance/__main__.py +0 -0
  53. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/conformance/checks.py +0 -0
  54. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/conformance/client.py +0 -0
  55. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/conformance/fixture.py +0 -0
  56. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/conformance/mp4.py +0 -0
  57. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/conformance/page_fixture.py +0 -0
  58. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/conformance/report.py +0 -0
  59. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/dataplane.py +0 -0
  60. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/errors.py +0 -0
  61. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/extensions.py +0 -0
  62. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/frames.py +0 -0
  63. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/keys.py +0 -0
  64. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/launch.py +0 -0
  65. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/manifest.py +0 -0
  66. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/operations.py +0 -0
  67. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/platform.py +0 -0
  68. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/queue.py +0 -0
  69. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/recording.py +0 -0
  70. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/results.py +0 -0
  71. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/run_install_step.py +0 -0
  72. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/runtime.py +0 -0
  73. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/seed.py +0 -0
  74. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/serve.py +0 -0
  75. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/serve_app.py +0 -0
  76. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/session.py +0 -0
  77. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/skill.py +0 -0
  78. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/spaces.py +0 -0
  79. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/svc.py +0 -0
  80. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/computer/tools.py +0 -0
  81. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/errors.py +0 -0
  82. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/manifest.py +0 -0
  83. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/preflight.py +0 -0
  84. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/tool_access.py +0 -0
  85. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/tool_client.py +0 -0
  86. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/tool_context.py +0 -0
  87. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/transfers.py +0 -0
  88. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/types.py +0 -0
  89. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/website/__init__.py +0 -0
  90. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/website/clock.py +0 -0
  91. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/website/conformance.py +0 -0
  92. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/website/errors.py +0 -0
  93. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/website/readiness.py +0 -0
  94. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/website/session.py +0 -0
  95. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/src/agentenv_protocol/website_browser.py +0 -0
  96. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/computer_fake.py +0 -0
  97. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/probe_fake.py +0 -0
  98. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_a2a_agent.py +0 -0
  99. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_a2a_agent_examples.py +0 -0
  100. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_a2a_cdp_config.py +0 -0
  101. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_a2a_object_transfers.py +0 -0
  102. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_a2a_peer_agents.py +0 -0
  103. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_a2a_proxy.py +0 -0
  104. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_a2a_staging.py +0 -0
  105. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_a2a_trajectory_log.py +0 -0
  106. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_agent_env_environment.py +0 -0
  107. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_browser_cdp.py +0 -0
  108. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_client.py +0 -0
  109. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_computer_app_data.py +0 -0
  110. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_computer_app_server.py +0 -0
  111. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_computer_conformance.py +0 -0
  112. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_computer_conformance_checks.py +0 -0
  113. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_computer_conformance_page.py +0 -0
  114. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_computer_conformance_recording.py +0 -0
  115. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_computer_dataplane.py +0 -0
  116. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_computer_extensions.py +0 -0
  117. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_computer_frames.py +0 -0
  118. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_computer_page.py +0 -0
  119. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_computer_queue.py +0 -0
  120. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_computer_recording.py +0 -0
  121. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_computer_seed.py +0 -0
  122. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_computer_serve.py +0 -0
  123. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_computer_spaces.py +0 -0
  124. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_computer_svc_forward.py +0 -0
  125. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_control_token.py +0 -0
  126. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_data_objects.py +0 -0
  127. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_errors.py +0 -0
  128. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_extension_methods.py +0 -0
  129. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_intake.py +0 -0
  130. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_manifest.py +0 -0
  131. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_preflight.py +0 -0
  132. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_tool_access.py +0 -0
  133. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_tool_annotations.py +0 -0
  134. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_tool_client.py +0 -0
  135. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_tool_context.py +0 -0
  136. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_tool_context_fastmcp.py +0 -0
  137. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_tool_context_injection.py +0 -0
  138. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_website_browser.py +0 -0
  139. {agentenv_framework_protocol-0.1.459 → agentenv_framework_protocol-0.1.462}/tests/test_website_conformance.py +0 -0
  140. {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.459
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.459"
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
- return DriverDeclaration(name=cards[0][1].name, supports=list(cls.supports), platforms=[
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, and what it serves on each of them."""
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=ComputerSettings(), platform=platform)
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 ComputerSettings()
38
+ return model()
37
39
  try:
38
- return ComputerSettings.model_validate(json.loads(raw))
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