agentenv-framework-protocol 0.1.455__tar.gz → 0.1.458__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 (126) hide show
  1. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/PKG-INFO +1 -1
  2. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/pyproject.toml +1 -1
  3. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/computer/_linux.py +3 -2
  4. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/computer/app.py +9 -0
  5. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/computer/app_runtime.py +4 -2
  6. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/computer/conformance/checks.py +3 -1
  7. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/computer/driver.py +3 -3
  8. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/computer/platform.py +6 -4
  9. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/computer/recording.py +23 -3
  10. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/tests/probe_fake.py +6 -5
  11. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/tests/test_computer_app_server.py +11 -0
  12. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/tests/test_computer_conformance.py +1 -1
  13. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/tests/test_computer_declare.py +5 -2
  14. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/tests/test_computer_driver.py +9 -5
  15. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/tests/test_computer_recording.py +21 -0
  16. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/.gitignore +0 -0
  17. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/LICENSE +0 -0
  18. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/NOTICE +0 -0
  19. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/README.md +0 -0
  20. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/THIRD_PARTY_NOTICES.md +0 -0
  21. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/examples/__init__.py +0 -0
  22. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/examples/advanced_agent.py +0 -0
  23. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/examples/basic_agent.py +0 -0
  24. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/examples/custom_extensions_agent.py +0 -0
  25. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/examples/multimodal_agent.py +0 -0
  26. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/examples/reference_model.py +0 -0
  27. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/examples/streaming_agent.py +0 -0
  28. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/__init__.py +0 -0
  29. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/a2a_agent/__init__.py +0 -0
  30. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/a2a_agent/_trajectory_log.py +0 -0
  31. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/a2a_agent/_triggers.py +0 -0
  32. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/a2a_agent/extensions.py +0 -0
  33. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/a2a_agent/framework.py +0 -0
  34. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/a2a_agent/peer_agents.py +0 -0
  35. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/a2a_agent/proxy.py +0 -0
  36. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/a2a_agent/registry.py +0 -0
  37. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/a2a_agent/staging.py +0 -0
  38. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/a2a_agent/tasks/__init__.py +0 -0
  39. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/a2a_agent/tasks/v1.py +0 -0
  40. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/agent_env_environment.py +0 -0
  41. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/browser_cdp.py +0 -0
  42. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/client.py +0 -0
  43. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/computer/__init__.py +0 -0
  44. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/computer/_declare.py +0 -0
  45. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/computer/_load.py +0 -0
  46. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/computer/_with_fds.py +0 -0
  47. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/computer/app_data.py +0 -0
  48. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/computer/apps.py +0 -0
  49. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/computer/card.py +0 -0
  50. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/computer/checks.py +0 -0
  51. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/computer/conformance/__init__.py +0 -0
  52. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/computer/conformance/__main__.py +0 -0
  53. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/computer/conformance/client.py +0 -0
  54. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/computer/conformance/fixture.py +0 -0
  55. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/computer/conformance/mp4.py +0 -0
  56. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/computer/conformance/page_fixture.py +0 -0
  57. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/computer/conformance/report.py +0 -0
  58. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/computer/dataplane.py +0 -0
  59. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/computer/errors.py +0 -0
  60. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/computer/extensions.py +0 -0
  61. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/computer/frames.py +0 -0
  62. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/computer/keys.py +0 -0
  63. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/computer/launch.py +0 -0
  64. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/computer/manifest.py +0 -0
  65. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/computer/operations.py +0 -0
  66. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/computer/queue.py +0 -0
  67. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/computer/results.py +0 -0
  68. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/computer/run_install_step.py +0 -0
  69. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/computer/runtime.py +0 -0
  70. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/computer/seed.py +0 -0
  71. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/computer/serve.py +0 -0
  72. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/computer/serve_app.py +0 -0
  73. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/computer/session.py +0 -0
  74. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/computer/settings.py +0 -0
  75. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/computer/skill.py +0 -0
  76. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/computer/spaces.py +0 -0
  77. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/computer/svc.py +0 -0
  78. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/computer/tools.py +0 -0
  79. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/errors.py +0 -0
  80. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/manifest.py +0 -0
  81. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/preflight.py +0 -0
  82. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/tool_access.py +0 -0
  83. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/tool_client.py +0 -0
  84. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/tool_context.py +0 -0
  85. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/transfers.py +0 -0
  86. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/types.py +0 -0
  87. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/src/agentenv_protocol/website_browser.py +0 -0
  88. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/tests/computer_fake.py +0 -0
  89. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/tests/test_a2a_agent.py +0 -0
  90. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/tests/test_a2a_agent_examples.py +0 -0
  91. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/tests/test_a2a_cdp_config.py +0 -0
  92. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/tests/test_a2a_object_transfers.py +0 -0
  93. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/tests/test_a2a_peer_agents.py +0 -0
  94. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/tests/test_a2a_proxy.py +0 -0
  95. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/tests/test_a2a_staging.py +0 -0
  96. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/tests/test_a2a_trajectory_log.py +0 -0
  97. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/tests/test_agent_env_environment.py +0 -0
  98. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/tests/test_browser_cdp.py +0 -0
  99. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/tests/test_client.py +0 -0
  100. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/tests/test_computer_app_data.py +0 -0
  101. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/tests/test_computer_conformance_checks.py +0 -0
  102. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/tests/test_computer_conformance_page.py +0 -0
  103. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/tests/test_computer_conformance_recording.py +0 -0
  104. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/tests/test_computer_dataplane.py +0 -0
  105. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/tests/test_computer_extensions.py +0 -0
  106. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/tests/test_computer_frames.py +0 -0
  107. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/tests/test_computer_page.py +0 -0
  108. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/tests/test_computer_queue.py +0 -0
  109. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/tests/test_computer_seed.py +0 -0
  110. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/tests/test_computer_serve.py +0 -0
  111. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/tests/test_computer_spaces.py +0 -0
  112. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/tests/test_computer_svc_forward.py +0 -0
  113. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/tests/test_control_token.py +0 -0
  114. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/tests/test_data_objects.py +0 -0
  115. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/tests/test_errors.py +0 -0
  116. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/tests/test_extension_methods.py +0 -0
  117. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/tests/test_intake.py +0 -0
  118. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/tests/test_manifest.py +0 -0
  119. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/tests/test_preflight.py +0 -0
  120. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/tests/test_tool_access.py +0 -0
  121. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/tests/test_tool_annotations.py +0 -0
  122. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/tests/test_tool_client.py +0 -0
  123. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/tests/test_tool_context.py +0 -0
  124. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/tests/test_tool_context_fastmcp.py +0 -0
  125. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/tests/test_tool_context_injection.py +0 -0
  126. {agentenv_framework_protocol-0.1.455 → agentenv_framework_protocol-0.1.458}/tests/test_website_browser.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agentenv-framework-protocol
3
- Version: 0.1.455
3
+ Version: 0.1.458
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agentenv-framework-protocol"
3
- version = "0.1.455"
3
+ version = "0.1.458"
4
4
  description = "Open data-plane protocol and server SDK for agent environments."
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -1,5 +1,6 @@
1
- """The base class's Linux defaults for the OS operations. The driver runs as the session user inside the desktop
2
- session, so both act directly: a command inherits the session's environment, a file lands as that user."""
1
+ """The base class's defaults for the OS operations on Linux and macOS, which use POSIX alone. The driver runs as the
2
+ session user inside the desktop session, so both act directly: a command inherits the session's environment, a file
3
+ lands as that user."""
3
4
  from __future__ import annotations
4
5
 
5
6
  import asyncio
@@ -251,6 +251,15 @@ class AppDeclaration(BaseModel):
251
251
  ready_checks: list[AppReadyCheck]
252
252
  browser: Optional[bool] = None
253
253
 
254
+ @model_validator(mode="after")
255
+ def _no_window_wait_on_macos(self) -> AppDeclaration:
256
+ """A window's readiness is read from X11's windows, which macOS hasn't."""
257
+ launch = self.os["macos"].launch if "macos" in self.os else None
258
+ if launch is not None and launch.ready is not None and launch.ready.window:
259
+ raise ValueError("on macos an app's launch can't wait on its window, which is read from X11; wait on a "
260
+ "port, a command or a @ready_check")
261
+ return self
262
+
254
263
  @model_validator(mode="before")
255
264
  @classmethod
256
265
  def _stored_before_each_os(cls, data: Any) -> Any:
@@ -9,6 +9,7 @@ import json
9
9
  import os
10
10
  import shlex
11
11
  import signal
12
+ import sys
12
13
  import time
13
14
  from collections.abc import Awaitable, Callable
14
15
  from datetime import datetime, timezone
@@ -22,8 +23,9 @@ from . import session
22
23
  if TYPE_CHECKING:
23
24
  from .app import Launch
24
25
 
25
- #: Where an app's server marks that it launched the app since the computer booted, at boot or on demand.
26
- LAUNCHED = Path("/run/agentenv/apps")
26
+ #: Where an app's server marks that it launched the app since the computer booted, at boot or on demand: a folder a
27
+ #: boot clears, under /tmp on macOS, which has no /run and lets the session user write nothing at its root.
28
+ LAUNCHED = Path("/tmp/agentenv/apps" if sys.platform == "darwin" else "/run/agentenv/apps")
27
29
  #: How long one readiness check may take; an app's checks run at once.
28
30
  CHECK_TIMEOUT_S = 10.0
29
31
  #: Where an app's server keeps the pid of the app it launched last, in the app's folder, so a server that restarts can
@@ -48,6 +48,8 @@ MINIMUM_TOOLS = ("computer_screenshot", "computer_click", "computer_drag", "comp
48
48
  DATA_PLANE = (INTAKE_EXTENSION_URI, DATA_OBJECTS_EXTENSION_URI)
49
49
  #: Where the fixture is staged and keeps its state, relative to the session user's home.
50
50
  STAGE = ".agentenv-probe"
51
+ #: How the probe reads a staged file's owner: through the python3 the fixture runs on, the same on every OS.
52
+ OWNER = ("python3", "-c", "import os, sys; print(os.stat(sys.argv[1]).st_uid)")
51
53
  #: Where a page driver's computer_upload takes files from, relative to the session user's home.
52
54
  UPLOADS = "Uploads"
53
55
  #: What the probe answers the page fixture's prompt with.
@@ -605,7 +607,7 @@ class Probe:
605
607
  async def _security(self) -> None:
606
608
  async def one_user() -> str | None:
607
609
  uid = (await self.driver.run("id", "-u"))["stdout"].strip()
608
- owner = (await self.driver.run("stat", "-c", "%u", f"{STAGE}/{self.fixture}"))["stdout"].strip()
610
+ owner = (await self.driver.run(*OWNER, f"{STAGE}/{self.fixture}"))["stdout"].strip()
609
611
  if uid == "0":
610
612
  return "commands run as root"
611
613
  return None if owner == uid else f"data/add wrote as uid {owner}, commands run as uid {uid}"
@@ -205,13 +205,13 @@ class ComputerDriverEnvironment(Tools, Extensions, DataPlane, AgentEnvEnvironmen
205
205
  if self._computer is None:
206
206
  raise TypeError(f"{type(self).__qualname__}.__init__ must call super().__init__()")
207
207
 
208
- # The Linux defaults for the OS operations.
208
+ # The defaults for the OS operations on Linux and macOS, POSIX alike.
209
209
 
210
- @default_for("run_in_session", os="linux")
210
+ @default_for("run_in_session", os=["linux", "macos"])
211
211
  async def _computer_linux_run_in_session(self, argv: list, cwd: str | None, env: dict | None, timeout_s: float):
212
212
  return await _linux.run_in_session(argv, cwd, env, timeout_s, output_bytes=self.settings.exec_output_bytes)
213
213
 
214
- @default_for("write_files", os="linux")
214
+ @default_for("write_files", os=["linux", "macos"])
215
215
  async def _computer_linux_write_files(self, files: dict) -> None:
216
216
  await asyncio.to_thread(_linux.write_files, files)
217
217
 
@@ -27,13 +27,15 @@ class Platform:
27
27
 
28
28
 
29
29
  def detect(environ: dict | None = None, os_release: Path = Path("/etc/os-release")) -> Platform:
30
- """The running platform: the OS, ``/etc/os-release``'s ID and VERSION_ID on Linux, the machine, and the
31
- display from ``XDG_SESSION_TYPE``, ``none`` for a computer with no display, else ``DISPLAY`` (x11) or
32
- ``WAYLAND_DISPLAY`` (wayland)."""
30
+ """The running platform: the OS, ``/etc/os-release``'s ID and VERSION_ID on Linux and the macOS version on macOS,
31
+ the machine, and the display: on Linux from ``XDG_SESSION_TYPE``, ``none`` for a computer with no display, else
32
+ ``DISPLAY`` (x11) or ``WAYLAND_DISPLAY`` (wayland); on macOS Aqua."""
33
33
  environ = os.environ if environ is None else environ
34
34
  name = _OS.get(sys.platform, sys.platform)
35
35
  release = _os_release(os_release) if name == "linux" else {}
36
- display = None
36
+ if name == "macos":
37
+ release = {"VERSION_ID": _platform.mac_ver()[0] or None}
38
+ display = "aqua" if name == "macos" else None
37
39
  if name == "linux":
38
40
  session = (environ.get("XDG_SESSION_TYPE") or "").strip().lower()
39
41
  if session in ("x11", "wayland", "none"):
@@ -52,6 +52,8 @@ FAILING_S = 60.0
52
52
  GAP_S = 2.0
53
53
  #: How long a stream that stopped or failed waits before it is started again.
54
54
  RETRY_S = 1.0
55
+ #: How long a recording's start waits for its first frame, so a recording a start answered for is recording.
56
+ FIRST_FRAME_S = 5.0
55
57
  #: How often free disk is checked while frames arrive.
56
58
  DISK_CHECK_S = 5.0
57
59
  KEEP_ENDED = 64
@@ -127,8 +129,9 @@ def recording_params(recording: Recording, streams: bool) -> dict:
127
129
 
128
130
 
129
131
  class Recordings:
130
- """The recordings of one driver: started by request id, open ones counted against ``max_concurrent``, ended
131
- ones kept with their manifests so a repeated ``end`` answers the same."""
132
+ """The recordings of one driver: started by request id, each start answering once its recording is recording, open
133
+ ones counted against ``max_concurrent``, ended ones kept with their manifests so a repeated ``end`` answers the
134
+ same."""
132
135
 
133
136
  def __init__(self, runtime: ComputerRuntime) -> None:
134
137
  self.runtime = runtime
@@ -144,7 +147,9 @@ class Recordings:
144
147
  raise InvalidArgument("request_id is a non-empty string")
145
148
  async with self._lock:
146
149
  if request_id in self.started:
147
- return self.started[request_id]
150
+ answer = self.started[request_id]
151
+ await self._live(self.running.get(answer["recording_id"]))
152
+ return answer
148
153
  hints, grant = self._hints(features, scope, window, fps, max_edge, cursor, upload)
149
154
  stopped = [rid for rid, r in self.running.items() if r.done]
150
155
  for rid in stopped[:max(0, len(stopped) - KEEP_ENDED)]: # stopped by itself and never ended
@@ -164,8 +169,16 @@ class Recordings:
164
169
  self.started[request_id] = answer
165
170
  while len(self.started) > KEEP_STARTED:
166
171
  self.started.popitem(last=False)
172
+ await self._live(recording)
167
173
  return answer
168
174
 
175
+ @staticmethod
176
+ async def _live(recording: Optional[_Recording]) -> None:
177
+ """Wait up to FIRST_FRAME_S for ``recording``, one still running, to have its first frame."""
178
+ if recording is not None:
179
+ with contextlib.suppress(TimeoutError):
180
+ await asyncio.wait_for(recording.live.wait(), FIRST_FRAME_S)
181
+
169
182
  async def end(self, recording_id: str, upload: Optional[dict], reason: str) -> dict:
170
183
  if reason not in REASONS:
171
184
  raise InvalidArgument(f"reason is one of {list(REASONS)}, got {reason!r}")
@@ -219,6 +232,7 @@ class _Recording:
219
232
  self.ended_by: Optional[str] = None
220
233
  self.error: Optional[str] = None
221
234
  self.done = False
235
+ self.live = asyncio.Event()
222
236
  self.task: Optional[asyncio.Task] = None
223
237
  self.timer: Optional[asyncio.TimerHandle] = None
224
238
  self.released: Optional[asyncio.Future] = None
@@ -228,7 +242,11 @@ class _Recording:
228
242
  self._last_disk_check = 0.0
229
243
 
230
244
  async def run(self) -> None:
245
+ """Frames or segments from the source, recorded until the recording ends; ``live`` once it's recording: a
246
+ stream from its start, the fallback's polling from its first frame, which a busy computer is slow to take."""
231
247
  failing_since: Optional[float] = None
248
+ if self.streams:
249
+ self.live.set()
232
250
  try:
233
251
  while True:
234
252
  try:
@@ -255,6 +273,7 @@ class _Recording:
255
273
  await self._close()
256
274
  finally:
257
275
  self.done = True
276
+ self.live.set()
258
277
  self.released = asyncio.ensure_future(self._release())
259
278
 
260
279
  def expire(self) -> None:
@@ -315,6 +334,7 @@ class _Recording:
315
334
  path = self.hints.directory / f"{len(self.segments):04d}.mp4"
316
335
  self.encoder = await self._in_thread(_Encoder, path, frame.width, frame.height, self.hints.fps, at)
317
336
  await self._in_thread(self.encoder.add, frame, at)
337
+ self.live.set()
318
338
  self._check_disk()
319
339
 
320
340
  async def _add_segment(self, item: RecordedSegment) -> None:
@@ -39,7 +39,7 @@ from agentenv_protocol.computer import (
39
39
  upload,
40
40
  )
41
41
  from agentenv_protocol.computer import _linux as linux
42
- from agentenv_protocol.computer.conformance.checks import STAGE
42
+ from agentenv_protocol.computer.conformance.checks import OWNER, STAGE
43
43
  from agentenv_protocol.computer.conformance.page_fixture import QUESTION, TITLE, TWO, TWO_TITLE
44
44
 
45
45
  CLIPBOARD = "agentenv-probe clipboard"
@@ -56,6 +56,7 @@ DESK_URL = "http://127.0.0.1:4343/"
56
56
  TITLES = {DESK_URL: TITLE, DESK_URL + TWO.lstrip("/"): TWO_TITLE}
57
57
 
58
58
 
59
+
59
60
  class FakeDesk:
60
61
  """What the Fixture Desk would record of the input the fake screen gets."""
61
62
 
@@ -202,7 +203,7 @@ class FixtureDriver(FakeDriver):
202
203
  if f"python3 {STAGE}/fixture.py" in text:
203
204
  self.desk.start()
204
205
  return ExecResult(0, b"4242\n")
205
- if argv in (["id", "-u"], ["stat", "-c", "%u", f"{STAGE}/fixture.py"]):
206
+ if argv in (["id", "-u"], [*OWNER, f"{STAGE}/fixture.py"]):
206
207
  return ExecResult(0, b"1000\n")
207
208
  if argv[:1] in (["kill"], ["tail"]) or f"rm -rf {STAGE}/state" in text:
208
209
  return ExecResult(0)
@@ -320,10 +321,10 @@ class PageFixtureDriver(FixtureDriver):
320
321
  return ExecResult(0, b"4343") if self.desk.started else ExecResult(1)
321
322
  if argv[:1] == ["cat"] and argv[1].endswith("layout.json") and self.desk.started:
322
323
  return ExecResult(0, json.dumps(PAGE_LAYOUT).encode())
323
- if argv == ["stat", "-c", "%u", f"{STAGE}/page_fixture.py"]:
324
+ if argv == [*OWNER, f"{STAGE}/page_fixture.py"]:
324
325
  return ExecResult(0, b"1000\n")
325
- if argv == ["stat", "-c", "%u", f"{STAGE}/fixture.py"]:
326
- return ExecResult(1, b"", b"stat: cannot statx: No such file or directory")
326
+ if argv == [*OWNER, f"{STAGE}/fixture.py"]:
327
+ return ExecResult(1, b"", b"FileNotFoundError: [Errno 2] No such file or directory")
327
328
  return super()._fixture(argv)
328
329
 
329
330
 
@@ -6,6 +6,7 @@ from __future__ import annotations
6
6
 
7
7
  import asyncio
8
8
  import contextlib
9
+ import importlib.util
9
10
  import os
10
11
  import shlex
11
12
  import signal
@@ -109,6 +110,16 @@ async def test_an_apps_server_is_ready_once_it_launched_the_app_and_each_check_p
109
110
  assert after["ready"] is True and after["problems"] == []
110
111
 
111
112
 
113
+
114
+ @pytest.mark.parametrize("platform, folder", [("linux", "/run/agentenv/apps"), ("darwin", "/tmp/agentenv/apps")])
115
+ def test_a_launch_is_marked_in_a_folder_a_boot_clears_and_the_session_user_writes(monkeypatch, platform, folder):
116
+ monkeypatch.setattr(sys, "platform", platform)
117
+ spec = importlib.util.spec_from_file_location(f"agentenv_protocol.computer._app_runtime_on_{platform}",
118
+ app_runtime.__file__)
119
+ loaded = importlib.util.module_from_spec(spec)
120
+ spec.loader.exec_module(loaded)
121
+ assert loaded.LAUNCHED == Path(folder)
122
+
112
123
  @pytest.mark.asyncio
113
124
  async def test_checks_that_fail_hang_or_raise_say_so_at_once_without_holding_up_the_server(desktop, monkeypatch):
114
125
  monkeypatch.setattr(app_runtime, "CHECK_TIMEOUT_S", 1.0)
@@ -167,7 +167,7 @@ async def test_each_app_a_driver_fronts_that_exports_is_asked_for_its_export_onc
167
167
 
168
168
  @pytest.mark.asyncio
169
169
  async def test_a_driver_without_exec_or_data_add_passes_basic_and_leaves_full_unverified():
170
- report = await _probe(MinimalDriver(FakeScreen(640, 400), platform=Platform(os="macos")))
170
+ report = await _probe(MinimalDriver(FakeScreen(640, 400), platform=Platform(os="windows")))
171
171
  assert report.passed, _failed(report)
172
172
  unverified = [c for c in report.checks if c.status == "unverified"]
173
173
  assert {c.check for c in unverified} == {"P3", "P4", "P5", "P6", "P7", "P9"}
@@ -264,6 +264,9 @@ def test_an_app_declaring_its_skill_twice_is_refused(folder):
264
264
  "Changed.fonts runs on 'windows', which Changed declares no install for; it declares freebsd, linux"),
265
265
  ('@ready_check(os="macos")\n def window_shown(self): return True',
266
266
  "Changed.window_shown runs on 'macos', which Changed declares no install for"),
267
+ ('@install("macos", FreeBSD(), launch=Launch(command=["mail"], window={"class": "Mail"}, '
268
+ 'ready=Ready(window=True)))',
269
+ "on macos an app's launch can't wait on its window, which is read from X11"),
267
270
  ('class BuildSettings(BaseModel): key: SecretStr = SecretStr("")',
268
271
  "Changed.BuildSettings holds a secret at properties.key, which its computer's image would keep; declare it in "
269
272
  "Settings"),
@@ -273,9 +276,9 @@ def test_an_app_declaring_its_skill_twice_is_refused(folder):
273
276
  "Changed.BuildSettings holds a secret at $defs.Settings.properties.password, which"),
274
277
  ], ids=["no-wait", "no-window", "window-key", "download-sha", "script-escape", "os-skill-twice", "os-build-secret",
275
278
  "step-os",
276
- "check-os", "build-secret", "build-secrets-listed", "build-secret-nested"])
279
+ "check-os", "macos-window-wait", "build-secret", "build-secrets-listed", "build-secret-nested"])
277
280
  def test_an_app_whose_declaration_cant_be_checked_is_refused_saying_why(folder, capsys, change, problem):
278
- decorator, body = (change, "pass") if change.startswith("@linux") else ("", change)
281
+ decorator, body = (change, "pass") if change.startswith(("@linux", "@install(")) else ("", change)
279
282
  (folder / "app.py").write_text(APP.format(supports=LINUX, sha='a' * 64) + f"""
280
283
 
281
284
  @computer_app(supports={LINUX})
@@ -45,6 +45,8 @@ from agentenv_protocol.computer.settings import load_settings # noqa: E402
45
45
  from agentenv_protocol.computer.tools import _modifiers # noqa: E402
46
46
 
47
47
  MACOS = Platform(os="macos", arch="arm64")
48
+ #: A platform the base class has no OS defaults for.
49
+ WINDOWS = Platform(os="windows", arch="amd64")
48
50
 
49
51
 
50
52
  def _required(namespace: dict) -> dict:
@@ -130,7 +132,8 @@ def test_set_display_without_modes_is_refused():
130
132
  def test_a_driver_method_replaces_the_linux_default():
131
133
  Driver = _define({"run": run_in_session(lambda self, *a: None)})
132
134
  assert declared(Driver)["run_in_session"] == [("run", {})]
133
- assert declared(MinimalDriver)["run_in_session"] == [("_computer_linux_run_in_session", {"os": frozenset({"linux"})})]
135
+ assert declared(MinimalDriver)["run_in_session"] == [("_computer_linux_run_in_session",
136
+ {"os": frozenset({"linux", "macos"})})]
134
137
 
135
138
 
136
139
  def test_the_platform_is_detected(tmp_path, monkeypatch):
@@ -143,7 +146,8 @@ def test_the_platform_is_detected(tmp_path, monkeypatch):
143
146
  assert detect({"WAYLAND_DISPLAY": "wayland-0"}, release).display == "wayland"
144
147
  assert detect({}, tmp_path / "missing").as_dict() == {"os": "linux", "arch": "arm64"}
145
148
  monkeypatch.setattr("agentenv_protocol.computer.platform.sys.platform", "darwin")
146
- assert detect({"DISPLAY": ":0"}, release) == Platform("macos", None, None, "arm64", None)
149
+ monkeypatch.setattr("agentenv_protocol.computer.platform._platform.mac_ver", lambda: ("26.5.2", ("", "", ""), ""))
150
+ assert detect({"DISPLAY": ":0"}, release) == Platform("macos", None, "26.5.2", "arm64", "aqua")
147
151
 
148
152
 
149
153
  def test_settings_come_from_one_json_value():
@@ -222,7 +226,7 @@ async def test_a_driver_whose_software_ships_a_skill_leads_with_it_and_the_compu
222
226
 
223
227
  @pytest.mark.asyncio
224
228
  async def test_a_minimal_driver_declares_only_what_it_has():
225
- async with http_client(MinimalDriver(platform=MACOS)) as http:
229
+ async with http_client(MinimalDriver(platform=WINDOWS)) as http:
226
230
  card = (await http.get("/.well-known/agent-env.json")).json()
227
231
  display_set = await http.post("/agentenv/ext/computer-display/set", json={"width": 1, "height": 1})
228
232
  exec_run = await http.post("/agentenv/ext/computer-exec/run", json={"argv": ["true"], "request_id": "r"})
@@ -449,14 +453,14 @@ def test_a_driver_class_declares_what_it_serves_on_each_platform(monkeypatch):
449
453
  raise AssertionError("declaration builds the card without the subclass's __init__")
450
454
 
451
455
  linux = Declared.declaration(LINUX)
452
- elsewhere = MinimalDriver.declaration(Platform(os="macos"))
456
+ elsewhere = MinimalDriver.declaration(Platform(os="windows"))
453
457
  uris = lambda card: {e.uri for e in card.capabilities.extensions} # noqa: E731
454
458
  assert linux.name == "declared" and len(linux.capabilities.tools) == 14
455
459
  assert {"urn:agentenv:computer-pointer/v1", "urn:agentenv:computer-exec/v1"} <= uris(linux)
456
460
  assert elsewhere.name == "MinimalDriver" and len(elsewhere.capabilities.tools) == 7
457
461
  assert "urn:agentenv:computer-exec/v1" not in uris(elsewhere)
458
462
  computer = next(e for e in elsewhere.capabilities.extensions if e.uri == "urn:agentenv:computer/v1")
459
- assert computer.params["platform"] == {"os": "macos"}
463
+ assert computer.params["platform"] == {"os": "windows"}
460
464
 
461
465
 
462
466
  def test_a_supports_entry_names_each_platform_it_covers():
@@ -176,6 +176,27 @@ async def test_the_fallback_polls_capture_and_draws_the_pointer(home):
176
176
  assert _at(manifest["started_at"]) <= _at(manifest["ended_at"])
177
177
 
178
178
 
179
+ @pytest.mark.asyncio
180
+ async def test_a_start_answers_once_its_recording_has_its_first_frame_however_slow_the_capture(home):
181
+ driver = Recorded()
182
+ driver.slow["capture"] = 0.5
183
+ async with http_client(driver) as http:
184
+ began = time.monotonic()
185
+ started = (await http.post(START, json={"request_id": "r", "fps": 10})).json()
186
+ took = time.monotonic() - began
187
+ recording = driver._computer.recordings.running[started["recording_id"]]
188
+ assert took >= 0.5 and recording.encoder is not None and recording.encoder.frames >= 1
189
+ await http.post(END, json={"recording_id": started["recording_id"]})
190
+
191
+ recordings = driver._computer.recordings
192
+ with pytest.raises(TimeoutError):
193
+ await asyncio.wait_for(recordings.start("cancelled", "display", None, 10, None, True, None), 0.1)
194
+ retried = await recordings.start("cancelled", "display", None, 10, None, True, None)
195
+ running = [r for r in recordings.running.values() if not r.done]
196
+ assert [r.id for r in running] == [retried["recording_id"]] and running[0].encoder.frames >= 1
197
+ await http.post(END, json={"recording_id": retried["recording_id"]})
198
+
199
+
179
200
  @pytest.mark.asyncio
180
201
  async def test_a_driver_without_the_pointer_group_records_without_a_cursor(home):
181
202
  _, manifest = await _record(MinimalDriver(), 0.4)