agentenv-framework-protocol 0.1.471__tar.gz → 0.1.473__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 (163) hide show
  1. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/PKG-INFO +37 -5
  2. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/README.md +36 -4
  3. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/pyproject.toml +1 -1
  4. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/browser_cdp.py +5 -0
  5. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/computer/app.py +25 -1
  6. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/computer/app_data.py +46 -14
  7. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/computer/app_runtime.py +11 -0
  8. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/computer/apps.py +14 -1
  9. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/computer/dataplane.py +2 -2
  10. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/computer/driver.py +12 -2
  11. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/computer/session.py +20 -3
  12. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/computer/svc.py +8 -4
  13. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/website_browser/control.py +49 -5
  14. agentenv_framework_protocol-0.1.473/src/agentenv_protocol/website_browser/data.py +125 -0
  15. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/website_browser/environment.py +109 -6
  16. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/website_browser/shapes.py +34 -0
  17. agentenv_framework_protocol-0.1.473/tests/test_browser_data.py +313 -0
  18. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/tests/test_computer_app_data.py +137 -5
  19. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/tests/test_computer_app_server.py +26 -0
  20. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/tests/test_computer_conformance.py +17 -9
  21. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/tests/test_computer_dataplane.py +3 -3
  22. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/tests/test_computer_svc_forward.py +33 -2
  23. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/tests/test_website_browser.py +45 -1
  24. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/.gitignore +0 -0
  25. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/LICENSE +0 -0
  26. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/NOTICE +0 -0
  27. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/THIRD_PARTY_NOTICES.md +0 -0
  28. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/examples/__init__.py +0 -0
  29. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/examples/advanced_agent.py +0 -0
  30. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/examples/basic_agent.py +0 -0
  31. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/examples/custom_extensions_agent.py +0 -0
  32. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/examples/multimodal_agent.py +0 -0
  33. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/examples/reference_model.py +0 -0
  34. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/examples/streaming_agent.py +0 -0
  35. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/examples/website/notes/.gitignore +0 -0
  36. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/examples/website/notes/Dockerfile +0 -0
  37. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/examples/website/notes/Dockerfile.dockerignore +0 -0
  38. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/examples/website/notes/README.md +0 -0
  39. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/examples/website/notes/data/seed.json +0 -0
  40. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/examples/website/notes/env.toml +0 -0
  41. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/examples/website/notes/site.py +0 -0
  42. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/examples/website/notes/web/index.html +0 -0
  43. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/examples/website/notes/web/package-lock.json +0 -0
  44. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/examples/website/notes/web/package.json +0 -0
  45. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/examples/website/notes/web/src/main.js +0 -0
  46. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/examples/website/notes/web/src/style.css +0 -0
  47. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/examples/website/notes/web/vite.config.js +0 -0
  48. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/__init__.py +0 -0
  49. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/a2a_agent/__init__.py +0 -0
  50. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/a2a_agent/_trajectory_log.py +0 -0
  51. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/a2a_agent/_triggers.py +0 -0
  52. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/a2a_agent/extensions.py +0 -0
  53. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/a2a_agent/framework.py +0 -0
  54. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/a2a_agent/peer_agents.py +0 -0
  55. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/a2a_agent/proxy.py +0 -0
  56. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/a2a_agent/registry.py +0 -0
  57. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/a2a_agent/staging.py +0 -0
  58. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/a2a_agent/tasks/__init__.py +0 -0
  59. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/a2a_agent/tasks/v1.py +0 -0
  60. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/agent_env_environment.py +0 -0
  61. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/client.py +0 -0
  62. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/computer/__init__.py +0 -0
  63. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/computer/_declare.py +0 -0
  64. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/computer/_linux.py +0 -0
  65. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/computer/_load.py +0 -0
  66. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/computer/_macos.py +0 -0
  67. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/computer/_with_fds.py +0 -0
  68. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/computer/_x11.py +0 -0
  69. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/computer/card.py +0 -0
  70. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/computer/checks.py +0 -0
  71. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/computer/conformance/__init__.py +0 -0
  72. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/computer/conformance/__main__.py +0 -0
  73. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/computer/conformance/checks.py +0 -0
  74. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/computer/conformance/client.py +0 -0
  75. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/computer/conformance/fixture.py +0 -0
  76. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/computer/conformance/mp4.py +0 -0
  77. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/computer/conformance/page_fixture.py +0 -0
  78. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/computer/conformance/report.py +0 -0
  79. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/computer/errors.py +0 -0
  80. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/computer/extensions.py +0 -0
  81. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/computer/frames.py +0 -0
  82. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/computer/keys.py +0 -0
  83. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/computer/launch.py +0 -0
  84. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/computer/manifest.py +0 -0
  85. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/computer/operations.py +0 -0
  86. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/computer/platform.py +0 -0
  87. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/computer/queue.py +0 -0
  88. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/computer/recording.py +0 -0
  89. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/computer/results.py +0 -0
  90. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/computer/run_install_step.py +0 -0
  91. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/computer/runtime.py +0 -0
  92. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/computer/seed.py +0 -0
  93. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/computer/serve.py +0 -0
  94. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/computer/serve_app.py +0 -0
  95. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/computer/settings.py +0 -0
  96. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/computer/skill.py +0 -0
  97. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/computer/spaces.py +0 -0
  98. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/computer/tools.py +0 -0
  99. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/errors.py +0 -0
  100. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/manifest.py +0 -0
  101. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/preflight.py +0 -0
  102. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/tool_access.py +0 -0
  103. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/tool_client.py +0 -0
  104. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/tool_context.py +0 -0
  105. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/transfers.py +0 -0
  106. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/types.py +0 -0
  107. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/website/__init__.py +0 -0
  108. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/website/clock.py +0 -0
  109. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/website/conformance.py +0 -0
  110. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/website/environment.py +0 -0
  111. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/website/errors.py +0 -0
  112. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/website/readiness.py +0 -0
  113. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/website/routes.py +0 -0
  114. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/website/session.py +0 -0
  115. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/website_browser/__init__.py +0 -0
  116. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/src/agentenv_protocol/website_browser/devtools.py +0 -0
  117. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/tests/computer_fake.py +0 -0
  118. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/tests/probe_fake.py +0 -0
  119. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/tests/test_a2a_agent.py +0 -0
  120. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/tests/test_a2a_agent_examples.py +0 -0
  121. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/tests/test_a2a_cdp_config.py +0 -0
  122. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/tests/test_a2a_object_transfers.py +0 -0
  123. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/tests/test_a2a_peer_agents.py +0 -0
  124. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/tests/test_a2a_proxy.py +0 -0
  125. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/tests/test_a2a_staging.py +0 -0
  126. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/tests/test_a2a_trajectory_log.py +0 -0
  127. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/tests/test_agent_env_environment.py +0 -0
  128. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/tests/test_browser_cdp.py +0 -0
  129. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/tests/test_browser_state.py +0 -0
  130. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/tests/test_client.py +0 -0
  131. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/tests/test_computer_conformance_checks.py +0 -0
  132. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/tests/test_computer_conformance_page.py +0 -0
  133. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/tests/test_computer_conformance_recording.py +0 -0
  134. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/tests/test_computer_declare.py +0 -0
  135. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/tests/test_computer_driver.py +0 -0
  136. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/tests/test_computer_extensions.py +0 -0
  137. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/tests/test_computer_frames.py +0 -0
  138. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/tests/test_computer_macos.py +0 -0
  139. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/tests/test_computer_page.py +0 -0
  140. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/tests/test_computer_queue.py +0 -0
  141. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/tests/test_computer_recording.py +0 -0
  142. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/tests/test_computer_seed.py +0 -0
  143. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/tests/test_computer_serve.py +0 -0
  144. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/tests/test_computer_spaces.py +0 -0
  145. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/tests/test_control_token.py +0 -0
  146. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/tests/test_data_objects.py +0 -0
  147. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/tests/test_errors.py +0 -0
  148. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/tests/test_extension_methods.py +0 -0
  149. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/tests/test_intake.py +0 -0
  150. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/tests/test_manifest.py +0 -0
  151. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/tests/test_preflight.py +0 -0
  152. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/tests/test_tool_access.py +0 -0
  153. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/tests/test_tool_annotations.py +0 -0
  154. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/tests/test_tool_client.py +0 -0
  155. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/tests/test_tool_context.py +0 -0
  156. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/tests/test_tool_context_fastmcp.py +0 -0
  157. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/tests/test_tool_context_injection.py +0 -0
  158. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/tests/test_website_api.py +0 -0
  159. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/tests/test_website_conformance.py +0 -0
  160. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/tests/test_website_environment.py +0 -0
  161. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/tests/test_website_mcp.py +0 -0
  162. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/tests/test_website_notes_example.py +0 -0
  163. {agentenv_framework_protocol-0.1.471 → agentenv_framework_protocol-0.1.473}/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.471
3
+ Version: 0.1.473
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
@@ -894,13 +894,19 @@ in the session (`agentenv_protocol.computer.session`) is Linux's.
894
894
 
895
895
  The server serves the app's data plane and its skill. With `paths` alone, `data/add` writes the files
896
896
  it's given under them and refuses any other path, or one a link leads out of them; `data/get`
897
- returns the files there, a file tree; and `data/reset` puts them back as the server found them when
897
+ returns the files there, a file tree, copied with every process of the app paused (SIGSTOP) so they
898
+ are of one moment, and sent from the copy once it runs again; and `data/reset` puts them back as the server found them when
898
899
  it first started on this boot, which is the app as installed. Hooks serve what they mark instead.
899
900
  A hook's `data/add` parts come as they were sent, inline or as a read grant, and
900
901
  `part_bytes(part)` returns a part's bytes either way. Either way the base class takes data calls and
901
902
  launches one at a time, runs a load and a reset with the app stopped and relaunches it after,
902
903
  follows a failed load with a reset, and fails a call that runs past `data_timeout_s` (300 s); a sync
903
- hook runs in a thread, which the reset and relaunch after it wait for. The driver fronts its apps:
904
+ hook runs in a thread, which the reset and relaunch after it wait for. An app whose hooks serve an
905
+ operation on the running app names it in `data_live` (a website browser's load sets cookies over its
906
+ pipe), and the base class doesn't stop it for that one; a reset after such a load that failed still
907
+ stops and relaunches it. `_resets_failed_add()` says whether a failed load is followed by a reset at
908
+ all, always by default; and `data_keeps_paths` has the server keep the app's paths as installed for a
909
+ reset hook of its own, which `app_data.reset(paths)` puts back, as the default reset does. The driver fronts its apps:
904
910
  its card lists each one's card as a child environment, rooted at `/svc/<app>/`, where it forwards
905
911
  requests to the app's server, taking the control token:
906
912
 
@@ -918,6 +924,11 @@ agents reach this way without the control token: the driver reads them from the
918
924
  (`computer-app/v1`'s `agent_routes`) and lets a call under one through, while the app checks it
919
925
  itself. A path with a `.` or `..` segment, escaped or not, never counts as under one.
920
926
 
927
+ Once an agent acts in the computer, by any tool call on the driver's `/mcp` or a call on a route an
928
+ app opens to agents, the driver writes `.agentenv/agent-acted` in each app's folder, and
929
+ `agent_acted()` reads it from the app's own: a fresh computer starts without it, and an app that resets
930
+ removes its own with `forget_agent_acted()`. agentenv's own calls, with the control token, don't count.
931
+
921
932
  `MailApp.declaration()` returns what the class declares, an `AppDeclaration`, which a `computer_app`
922
933
  env stores (`browser: true` for a website browser), and `python -m agentenv_protocol.computer._declare <folder> <file> <class>` prints it as
923
934
  JSON, or a driver's `DriverDeclaration`, without the class's own dependencies installed.
@@ -989,8 +1000,9 @@ are the profile's template. The base class adds:
989
1000
  `Input.dispatchKeyEvent`, `Input.insertText`), sent to it in order. Also framework-only, for page
990
1001
  drivers and agentenv's own steps alike:
991
1002
  - `navigate {url}` and `history {direction}` (`back`, `forward` or `reload`), in the active tab;
992
- - `tabs/list`, `tabs/open {url?}`, `tabs/activate {tab}` and `tabs/close {tab}`, by the browser's own tab
993
- ids. The active tab is the one a dialog is open on, else the visible one whose page has focus (a hidden
1003
+ - `tabs/list`, `tabs/open {url?, background?}`, `tabs/activate {tab}` and `tabs/close {tab}`, by the browser's
1004
+ own tab ids. A tab opened with `background` opens behind the active tab, which stays active; otherwise it comes
1005
+ to the front. The active tab is the one a dialog is open on, else the visible one whose page has focus (a hidden
994
1006
  tab's page that had input still says it has focus), else the one agentenv last brought to the front.
995
1007
  Headless, a tab opened at a URL opens blank and goes there once it's held to the window's size;
996
1008
  - `dialog/answer {accept, text?}`, the agent's answer to the dialog open on the active tab;
@@ -1019,6 +1031,26 @@ are the profile's template. The base class adds:
1019
1031
  under the `write_namespace` grant when there is one, else inline, as `data/get` returns files. Inline, the whole
1020
1032
  file is read into memory and sent base64 in one answer, so a large one belongs under a grant.
1021
1033
 
1034
+ The browser's data plane is a file tree, every file by its path under the session user's home:
1035
+ - `data/get` returns its state as `browser-state.json` (`BrowserState`: `cookies` and `local_storage`
1036
+ as above, `tabs`, `closed_tabs`, `downloads` and `clock`, `real` with its `now`); its profile's files,
1037
+ each database with its journal or log, copied in one pass with the browser paused once its state is
1038
+ read, so the files are of one moment, without caches (`Cache`, `Code Cache`,
1039
+ `GPUCache`, the shader and Dawn caches, `CacheStorage`, `ScriptCache`, `Crashpad`) or locks
1040
+ (`Singleton*`); and its finished downloads.
1041
+ - `data/add` applies such a state while the browser runs, from `browser-state.json` or a data part of
1042
+ its shape (`StateToLoad`: `cookies`, `local_storage` and `tabs`; what it records of the past is left),
1043
+ after files written by their path: profile files restore the profile to exactly them, with the
1044
+ browser stopped and relaunched first, and files under `~/Downloads` and `~/Uploads` land there; any
1045
+ other path is refused before anything is applied. A load's `tabs` open in place of the open ones,
1046
+ the active one in front; with none, a load that changes cookies or local storage sends the start tabs
1047
+ back to their `start_urls`, so a page loaded before it shows it. Once an agent has acted, a load
1048
+ can't change the profile or the tabs, the start tabs stay as they are, and a load that fails isn't
1049
+ reset: its error says what it had applied.
1050
+ - `data/reset` ends every CDP address, puts the profile back as installed, empties `~/Downloads` and
1051
+ `~/Uploads`, starts its record over and forgets that an agent acted; its data plane relaunches it at
1052
+ its start tabs.
1053
+
1022
1054
  The record's entries are the models in `agentenv_protocol.website_browser` (`OpenTab`, with the tab that opened
1023
1055
  it as `opener`; `ClosedTab`; `Download`; `Dialog`; `LaunchEntry`; `BrowserRecord`), every field present and null
1024
1056
  until it's known.
@@ -829,13 +829,19 @@ in the session (`agentenv_protocol.computer.session`) is Linux's.
829
829
 
830
830
  The server serves the app's data plane and its skill. With `paths` alone, `data/add` writes the files
831
831
  it's given under them and refuses any other path, or one a link leads out of them; `data/get`
832
- returns the files there, a file tree; and `data/reset` puts them back as the server found them when
832
+ returns the files there, a file tree, copied with every process of the app paused (SIGSTOP) so they
833
+ are of one moment, and sent from the copy once it runs again; and `data/reset` puts them back as the server found them when
833
834
  it first started on this boot, which is the app as installed. Hooks serve what they mark instead.
834
835
  A hook's `data/add` parts come as they were sent, inline or as a read grant, and
835
836
  `part_bytes(part)` returns a part's bytes either way. Either way the base class takes data calls and
836
837
  launches one at a time, runs a load and a reset with the app stopped and relaunches it after,
837
838
  follows a failed load with a reset, and fails a call that runs past `data_timeout_s` (300 s); a sync
838
- hook runs in a thread, which the reset and relaunch after it wait for. The driver fronts its apps:
839
+ hook runs in a thread, which the reset and relaunch after it wait for. An app whose hooks serve an
840
+ operation on the running app names it in `data_live` (a website browser's load sets cookies over its
841
+ pipe), and the base class doesn't stop it for that one; a reset after such a load that failed still
842
+ stops and relaunches it. `_resets_failed_add()` says whether a failed load is followed by a reset at
843
+ all, always by default; and `data_keeps_paths` has the server keep the app's paths as installed for a
844
+ reset hook of its own, which `app_data.reset(paths)` puts back, as the default reset does. The driver fronts its apps:
839
845
  its card lists each one's card as a child environment, rooted at `/svc/<app>/`, where it forwards
840
846
  requests to the app's server, taking the control token:
841
847
 
@@ -853,6 +859,11 @@ agents reach this way without the control token: the driver reads them from the
853
859
  (`computer-app/v1`'s `agent_routes`) and lets a call under one through, while the app checks it
854
860
  itself. A path with a `.` or `..` segment, escaped or not, never counts as under one.
855
861
 
862
+ Once an agent acts in the computer, by any tool call on the driver's `/mcp` or a call on a route an
863
+ app opens to agents, the driver writes `.agentenv/agent-acted` in each app's folder, and
864
+ `agent_acted()` reads it from the app's own: a fresh computer starts without it, and an app that resets
865
+ removes its own with `forget_agent_acted()`. agentenv's own calls, with the control token, don't count.
866
+
856
867
  `MailApp.declaration()` returns what the class declares, an `AppDeclaration`, which a `computer_app`
857
868
  env stores (`browser: true` for a website browser), and `python -m agentenv_protocol.computer._declare <folder> <file> <class>` prints it as
858
869
  JSON, or a driver's `DriverDeclaration`, without the class's own dependencies installed.
@@ -924,8 +935,9 @@ are the profile's template. The base class adds:
924
935
  `Input.dispatchKeyEvent`, `Input.insertText`), sent to it in order. Also framework-only, for page
925
936
  drivers and agentenv's own steps alike:
926
937
  - `navigate {url}` and `history {direction}` (`back`, `forward` or `reload`), in the active tab;
927
- - `tabs/list`, `tabs/open {url?}`, `tabs/activate {tab}` and `tabs/close {tab}`, by the browser's own tab
928
- ids. The active tab is the one a dialog is open on, else the visible one whose page has focus (a hidden
938
+ - `tabs/list`, `tabs/open {url?, background?}`, `tabs/activate {tab}` and `tabs/close {tab}`, by the browser's
939
+ own tab ids. A tab opened with `background` opens behind the active tab, which stays active; otherwise it comes
940
+ to the front. The active tab is the one a dialog is open on, else the visible one whose page has focus (a hidden
929
941
  tab's page that had input still says it has focus), else the one agentenv last brought to the front.
930
942
  Headless, a tab opened at a URL opens blank and goes there once it's held to the window's size;
931
943
  - `dialog/answer {accept, text?}`, the agent's answer to the dialog open on the active tab;
@@ -954,6 +966,26 @@ are the profile's template. The base class adds:
954
966
  under the `write_namespace` grant when there is one, else inline, as `data/get` returns files. Inline, the whole
955
967
  file is read into memory and sent base64 in one answer, so a large one belongs under a grant.
956
968
 
969
+ The browser's data plane is a file tree, every file by its path under the session user's home:
970
+ - `data/get` returns its state as `browser-state.json` (`BrowserState`: `cookies` and `local_storage`
971
+ as above, `tabs`, `closed_tabs`, `downloads` and `clock`, `real` with its `now`); its profile's files,
972
+ each database with its journal or log, copied in one pass with the browser paused once its state is
973
+ read, so the files are of one moment, without caches (`Cache`, `Code Cache`,
974
+ `GPUCache`, the shader and Dawn caches, `CacheStorage`, `ScriptCache`, `Crashpad`) or locks
975
+ (`Singleton*`); and its finished downloads.
976
+ - `data/add` applies such a state while the browser runs, from `browser-state.json` or a data part of
977
+ its shape (`StateToLoad`: `cookies`, `local_storage` and `tabs`; what it records of the past is left),
978
+ after files written by their path: profile files restore the profile to exactly them, with the
979
+ browser stopped and relaunched first, and files under `~/Downloads` and `~/Uploads` land there; any
980
+ other path is refused before anything is applied. A load's `tabs` open in place of the open ones,
981
+ the active one in front; with none, a load that changes cookies or local storage sends the start tabs
982
+ back to their `start_urls`, so a page loaded before it shows it. Once an agent has acted, a load
983
+ can't change the profile or the tabs, the start tabs stay as they are, and a load that fails isn't
984
+ reset: its error says what it had applied.
985
+ - `data/reset` ends every CDP address, puts the profile back as installed, empties `~/Downloads` and
986
+ `~/Uploads`, starts its record over and forgets that an agent acted; its data plane relaunches it at
987
+ its start tabs.
988
+
957
989
  The record's entries are the models in `agentenv_protocol.website_browser` (`OpenTab`, with the tab that opened
958
990
  it as `opener`; `ClosedTab`; `Download`; `Dialog`; `LaunchEntry`; `BrowserRecord`), every field present and null
959
991
  until it's known.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agentenv-framework-protocol"
3
- version = "0.1.471"
3
+ version = "0.1.473"
4
4
  description = "Open data-plane protocol and server SDK for agent environments."
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -111,6 +111,11 @@ class Tokens:
111
111
  self._save()
112
112
  return self._minted[user]
113
113
 
114
+ def revoke_all(self) -> None:
115
+ """Every address minted so far ended, as a reset ends them."""
116
+ self._minted, self._known = {}, {}
117
+ self._save()
118
+
114
119
  def address(self, token: str) -> Optional[Address]:
115
120
  """The address ``token`` was minted for, or None for one that wasn't."""
116
121
  known = self._known.get(_hash(token))
@@ -43,6 +43,9 @@ WINDOW_KEYS = frozenset({"class", "instance", "title"})
43
43
  COMPUTER_APP = "urn:agentenv:computer-app/v1"
44
44
  #: Where an app's own server listens, in the app's folder, and where it says why it last failed to start.
45
45
  SOCKET = Path(".agentenv/server.sock")
46
+ #: Written in an app's folder by its computer's driver once an agent acts there, by a tool call or on a route the app
47
+ #: opens to agents: a fresh computer starts without it, and an app that resets removes its own.
48
+ AGENT_ACTED = Path(".agentenv/agent-acted")
46
49
  FAILURE = Path(".agentenv/server.failure")
47
50
  #: The values of an app's build settings its env fixes in its computer's image, in the app's folder.
48
51
  BUILD_SETTINGS = Path(".agentenv/build_settings.json")
@@ -359,6 +362,17 @@ def _named(os_types: Union[str, list[str], None]) -> Optional[list[str]]:
359
362
  return None if os_types is None else [os_types] if isinstance(os_types, str) else list(os_types)
360
363
 
361
364
 
365
+ def agent_acted() -> bool:
366
+ """Whether an agent has acted in this app's computer since the computer started or the app last reset, as its
367
+ driver notes; read from the app's own folder."""
368
+ return AGENT_ACTED.exists()
369
+
370
+
371
+ def forget_agent_acted() -> None:
372
+ """Start over, as an app that has reset: no agent has acted since."""
373
+ AGENT_ACTED.unlink(missing_ok=True)
374
+
375
+
362
376
  class ComputerAppEnvironment(AgentEnvEnvironment):
363
377
  """Subclass it, declare it with ``@computer_app`` and an OS decorator for each OS (``@linux``), and set ``skill`` as
364
378
  a class attribute; a deploy's settings arrive as ``self.settings``, those its env fixes at build as
@@ -383,6 +397,11 @@ class ComputerAppEnvironment(AgentEnvEnvironment):
383
397
  skill: ClassVar[Optional[str]] = None
384
398
  #: How long one of its data calls may take, a hook or its paths' default, before it fails.
385
399
  data_timeout_s: ClassVar[float] = 300.0
400
+ #: The data operations its hooks serve on the running app, which its data plane neither stops it for nor
401
+ #: relaunches it after; and whether its server keeps its paths as installed for a reset hook of its own to put
402
+ #: back (``app_data.reset``), as it does for the default reset.
403
+ data_live: ClassVar[frozenset[str]] = frozenset()
404
+ data_keeps_paths: ClassVar[bool] = False
386
405
  #: Path prefixes of its own server's routes, each ending in ``/``, that agents reach through the driver without the
387
406
  #: computer's control token; the app checks a call to one itself.
388
407
  agent_routes: ClassVar[tuple[str, ...]] = ()
@@ -426,7 +445,8 @@ class ComputerAppEnvironment(AgentEnvEnvironment):
426
445
  launch_running=self._launch_running())
427
446
  self._computer_app_data = AppDataPlane(
428
447
  self._computer_app, {op: getattr(self, name) for op, name in cls._data_hooks.items()}, self.paths,
429
- cls.data_timeout_s)
448
+ cls.data_timeout_s, live=cls.data_live, keeps_paths=cls.data_keeps_paths,
449
+ resets_failed_add=self._resets_failed_add)
430
450
 
431
451
  def _launch_for(self, launch: Launch) -> Launch:
432
452
  """The launch its server uses, from the one declared for its OS, whose ``ready`` timeout it keeps: as
@@ -449,6 +469,10 @@ class ComputerAppEnvironment(AgentEnvEnvironment):
449
469
  """What a launch does while it runs instead of starting a second copy: nothing, so another copy starts."""
450
470
  return None
451
471
 
472
+ def _resets_failed_add(self) -> bool:
473
+ """Whether a load that fails is followed by a reset, so the app isn't left half loaded: always."""
474
+ return True
475
+
452
476
  @classmethod
453
477
  def settings_on(cls, os_type: Optional[str]) -> type[BaseModel]:
454
478
  """Its ``Settings`` on ``os_type``, with the fields its OS adds."""
@@ -5,10 +5,12 @@ boot, which is the app as installed, since every boot starts from the computer's
5
5
  from __future__ import annotations
6
6
 
7
7
  import asyncio
8
+ import contextlib
8
9
  import functools
9
10
  import inspect
10
11
  import logging
11
12
  import shutil
13
+ import tempfile
12
14
  from collections.abc import Callable
13
15
  from pathlib import Path, PurePosixPath
14
16
  from typing import TYPE_CHECKING, Any, Optional
@@ -16,7 +18,7 @@ from typing import TYPE_CHECKING, Any, Optional
16
18
  from ..agent_env_environment import OP_ADD_DATA, OP_GET_DATA, OP_RESET_DATA
17
19
  from ..transfers import WriteNamespaceGrant
18
20
  from ..types import METHOD_ADD, METHOD_RESET, FilePart, uploaded_object_path
19
- from .dataplane import file_parts, part_bytes
21
+ from .dataplane import data_files, file_parts, part_bytes
20
22
 
21
23
  if TYPE_CHECKING:
22
24
  from .app_runtime import AppRuntime
@@ -31,21 +33,28 @@ INITIAL = Path(".agentenv/initial")
31
33
 
32
34
  class AppDataPlane:
33
35
  """An app's data plane, served around its contract: one data call or launch at a time; a load and a reset stop the
34
- running app first and relaunch it after, and a load that fails is followed by a reset. A call fails once it runs
36
+ running app first and relaunch it after, but for those the app serves live, and a load that fails is followed by a
37
+ reset, when the app says so. A call fails once it runs
35
38
  past ``timeout_s`` and is cancelled: what awaits stops at once, but a thread can't be stopped, so a sync hook's or
36
39
  a default's thread is waited for, as are the reset and relaunch after it and the next call."""
37
40
 
38
- def __init__(self, runtime: AppRuntime, hooks: dict[str, Callable], paths: list[str], timeout_s: float) -> None:
41
+ def __init__(self, runtime: AppRuntime, hooks: dict[str, Callable], paths: list[str], timeout_s: float, *,
42
+ live: frozenset[str] = frozenset(), keeps_paths: bool = False,
43
+ resets_failed_add: Callable[[], bool] = lambda: True) -> None:
39
44
  self.runtime = runtime
40
45
  self.hooks = hooks
41
46
  self.paths = paths
42
47
  self.timeout_s = timeout_s
48
+ #: The operations served on the running app; whether a hook's reset needs the paths as installed; and whether
49
+ #: a load that fails is followed by a reset now.
50
+ self.live, self.keeps_paths, self.resets_failed_add = live, keeps_paths, resets_failed_add
43
51
  self.turn = asyncio.Lock()
44
52
  self.changing: set[asyncio.Task] = set()
45
53
 
46
54
  def keep_initial(self) -> None:
47
- """Keep the app's paths as installed, before anything changes them, when they serve its data."""
48
- if self.paths and not self.hooks:
55
+ """Keep the app's paths as installed, before anything changes them, when they serve its data or its own reset
56
+ puts them back."""
57
+ if self.paths and (not self.hooks or self.keeps_paths):
49
58
  keep_initial(self.paths)
50
59
 
51
60
  async def call(self, op: str, *args: Any) -> Any:
@@ -71,14 +80,20 @@ class AppDataPlane:
71
80
 
72
81
  async def _change(self, op: str, served: Callable, args: tuple, answer: asyncio.Future) -> None:
73
82
  """``op`` between the app's stop and its relaunch, answered on ``answer``: as it ends, or once it runs past
74
- ``timeout_s``."""
83
+ ``timeout_s``. A live one runs on the running app, which a reset after it fails stops and relaunches; an app
84
+ it ran on is running again after it fails, though the op stopped it."""
75
85
  try:
76
- running = op != OP_GET_DATA and await self.runtime.stop_app()
86
+ running = op != OP_GET_DATA and op not in self.live and await self.runtime.stop_app()
87
+ ran = op in self.live and self.runtime.running()
77
88
  try:
78
89
  result = await self._within(op, served, args, answer)
79
90
  except Exception:
80
- if op == OP_ADD_DATA:
91
+ if op == OP_ADD_DATA and self.resets_failed_add():
92
+ if op in self.live:
93
+ running = await self.runtime.stop_app() or ran
81
94
  await self._undo(answer)
95
+ elif op in self.live:
96
+ running = ran and not self.runtime.running()
82
97
  raise
83
98
  finally:
84
99
  if running:
@@ -116,7 +131,7 @@ class AppDataPlane:
116
131
  if not self.paths:
117
132
  return None
118
133
  return {OP_ADD_DATA: functools.partial(add, paths=self.paths),
119
- OP_GET_DATA: functools.partial(get, self.paths),
134
+ OP_GET_DATA: functools.partial(get, self.paths, self.runtime.paused),
120
135
  OP_RESET_DATA: functools.partial(reset, self.paths)}[op]
121
136
 
122
137
  async def _within(self, op: str, served: Callable, args: tuple, answer: asyncio.Future) -> Any:
@@ -159,12 +174,18 @@ async def add(parts: list, paths: list[str]) -> None:
159
174
  if not any(PurePosixPath(path).is_relative_to(owned) for owned in paths):
160
175
  raise ValueError(f"{path!r} is under none of this app's paths: {', '.join(paths)}")
161
176
  files[path] = await part_bytes(part)
162
- await in_thread(_write, Path.home(), files, paths)
177
+ await in_thread(write_within, Path.home(), files, paths)
163
178
 
164
179
 
165
- async def get(paths: list[str], write_namespace: Optional[WriteNamespaceGrant]) -> list:
166
- """A part for each file under ``paths``."""
167
- return await file_parts(Path.home(), paths, write_namespace)
180
+ async def get(paths: list[str], paused: Callable[[], contextlib.AbstractContextManager],
181
+ write_namespace: Optional[WriteNamespaceGrant]) -> list:
182
+ """A part for each file under ``paths``, copied with the app ``paused``, so they are of one moment, and sent from
183
+ the copy once it runs again."""
184
+ INITIAL.parent.mkdir(parents=True, exist_ok=True)
185
+ with tempfile.TemporaryDirectory(dir=INITIAL.parent) as staging:
186
+ with paused():
187
+ await in_thread(_stage, Path.home(), paths, Path(staging))
188
+ return await file_parts(Path(staging), paths, write_namespace)
168
189
 
169
190
 
170
191
  async def reset(paths: list[str]) -> None:
@@ -191,7 +212,8 @@ async def in_thread(fn: Callable, *args: Any) -> Any:
191
212
  raise
192
213
 
193
214
 
194
- def _write(home: Path, files: dict[str, bytes], paths: list[str]) -> None:
215
+ def write_within(home: Path, files: dict[str, bytes], paths: list[str]) -> None:
216
+ """Each file at its path under the home, none written when one leads out of ``paths`` through a link."""
195
217
  owned = [(home / path).resolve() for path in paths]
196
218
  if linked := [path for path in files if not any((home / path).resolve().is_relative_to(o) for o in owned)]:
197
219
  raise ValueError(f"{', '.join(map(repr, linked))} lead out of this app's paths through a link")
@@ -211,6 +233,16 @@ def _restore(home: Path, paths: list[str]) -> None:
211
233
  _copy(INITIAL / path, target)
212
234
 
213
235
 
236
+ def _stage(home: Path, paths: list[str], staging: Path) -> None:
237
+ """A copy in ``staging`` of each file an export of ``paths`` sends, by its path: a link's target, inside the home, as
238
+ a file, and no socket or pipe."""
239
+ for relative, source in data_files(home, paths):
240
+ target = staging / relative
241
+ target.parent.mkdir(parents=True, exist_ok=True)
242
+ with contextlib.suppress(FileNotFoundError):
243
+ shutil.copy2(source, target)
244
+
245
+
214
246
  def _copy(source: Path, target: Path) -> None:
215
247
  if source.is_dir() and not source.is_symlink():
216
248
  shutil.copytree(source, target, symlinks=True)
@@ -4,6 +4,7 @@ of the boot, stops the app it launched, and says whether the app is ready, its c
4
4
  from __future__ import annotations
5
5
 
6
6
  import asyncio
7
+ import contextlib
7
8
  import inspect
8
9
  import json
9
10
  import os
@@ -133,6 +134,16 @@ class AppRuntime:
133
134
  await session.stop(pid, STOP_TIMEOUT_S)
134
135
  return True
135
136
 
137
+ def running(self) -> bool:
138
+ """Whether the app this server launched last runs."""
139
+ return _running() is not None
140
+
141
+ def paused(self) -> contextlib.AbstractContextManager:
142
+ """The app this server launched last, every process of it, paused while the block runs, so a copy of its files
143
+ taken meanwhile is of one moment."""
144
+ pid = _running()
145
+ return session.paused(pid) if pid is not None else contextlib.nullcontext()
146
+
136
147
  def exited(self) -> bool:
137
148
  """Log that the app this server launched last ended on its own, with its exit code or the signal that ended
138
149
  it, unless the server stopped it; whether it did."""
@@ -10,6 +10,7 @@ from __future__ import annotations
10
10
  import asyncio
11
11
  import copy
12
12
  import json
13
+ from datetime import datetime, timezone
13
14
  from pathlib import Path
14
15
  from typing import Optional
15
16
 
@@ -17,7 +18,7 @@ import httpx
17
18
 
18
19
  from .. import client
19
20
  from ..types import RPC_PATH, WELL_KNOWN_PATH, EnvironmentCard
20
- from .app import COMPUTER_APP, FAILURE, SOCKET
21
+ from .app import AGENT_ACTED, COMPUTER_APP, FAILURE, SOCKET
21
22
  from .app_runtime import CHECK_TIMEOUT_S, LAUNCHES, AppReadiness, boot_launch_held
22
23
  from .operations import ReadyCheck
23
24
 
@@ -151,3 +152,15 @@ def _unanswered(folder: Path) -> str:
151
152
  def _client(socket: Path) -> httpx.AsyncClient:
152
153
  return httpx.AsyncClient(transport=httpx.AsyncHTTPTransport(uds=str(socket)), base_url="http://app",
153
154
  timeout=ANSWER_TIMEOUT_S)
155
+
156
+
157
+ def note_agent_acted(folders: list[Path]) -> None:
158
+ """Note in each app's folder that an agent has acted in the computer, where it isn't noted yet."""
159
+ for folder in folders:
160
+ marker = folder / AGENT_ACTED
161
+ if not marker.exists() and marker.parent.is_dir():
162
+ marker.write_text(_now())
163
+
164
+
165
+ def _now() -> str:
166
+ return datetime.now(timezone.utc).isoformat()
@@ -66,7 +66,7 @@ async def file_parts(home: Path, paths: list[str], write_namespace: Optional[Wri
66
66
  """A part for each file under ``paths`` of ``home`` but under none of ``leaving_out``, naming its path relative to
67
67
  ``home``: uploaded under ``write_namespace`` when there is one, else inline. A file a running app removes before it's
68
68
  read is left out."""
69
- files = [(relative, source) for relative, source in _data_files(home, paths)
69
+ files = [(relative, source) for relative, source in data_files(home, paths)
70
70
  if not any(PurePosixPath(relative).is_relative_to(owned) for owned in leaving_out)]
71
71
  parts = []
72
72
  if write_namespace is not None:
@@ -125,7 +125,7 @@ async def _get_url(file: FileWithUri) -> bytes:
125
125
  return bytes(data)
126
126
 
127
127
 
128
- def _data_files(home: Path, paths: list[str]) -> list[tuple[str, Path]]:
128
+ def data_files(home: Path, paths: list[str]) -> list[tuple[str, Path]]:
129
129
  """Every file under ``paths``, by its path relative to the home; a path not there yet is skipped."""
130
130
  root = home.resolve()
131
131
  found: dict[str, Path] = {}
@@ -118,6 +118,12 @@ class ComputerDriverEnvironment(Tools, Extensions, DataPlane, AgentEnvEnvironmen
118
118
  card = EnvironmentCard(**{**config, "name": config.get("name") or cls.__name__})
119
119
  return AgentEnvFastMCPApplication(card, driver).environment_card
120
120
 
121
+ def on_tool_call(self, context: ToolContext) -> Any:
122
+ """Note that an agent has acted, for the apps whose loads change what's on screen only before one has: a
123
+ subclass that has its own calls this one too."""
124
+ apps.note_agent_acted(self._computer.apps)
125
+ return None
126
+
121
127
  def has(self, capability: str) -> bool:
122
128
  """Whether an operation, or a whole group (``pointer``, ``keyboard``), works on this platform."""
123
129
  return self._computer.has(capability)
@@ -177,7 +183,10 @@ class ComputerDriverEnvironment(Tools, Extensions, DataPlane, AgentEnvEnvironmen
177
183
  if not await self._computer_allows(folder, request):
178
184
  return _error_response(AgentEnvError(
179
185
  "unauthorized", "this route is agent-env's own: it takes the env's control token", status=401))
180
- return await svc.forward(folder, name, request)
186
+ answer = await svc.forward(folder, name, request)
187
+ if answer.status_code < 400 and not control_check()(request.headers):
188
+ apps.note_agent_acted(self._computer.apps)
189
+ return answer
181
190
 
182
191
  async def _computer_relay(self, websocket: WebSocket) -> None:
183
192
  """A WebSocket under ``/svc/<name>/``, relayed both ways to the app named so; refused, before it opens, with no
@@ -186,7 +195,8 @@ class ComputerDriverEnvironment(Tools, Extensions, DataPlane, AgentEnvEnvironmen
186
195
  if (folder := self._computer_app_folder(name)) is None or not await self._computer_allows(folder, websocket):
187
196
  await websocket.close(code=1008)
188
197
  return
189
- await svc.relay(folder, name, websocket)
198
+ agent = not control_check()(websocket.headers)
199
+ await svc.relay(folder, name, websocket, (lambda: apps.note_agent_acted(self._computer.apps)) if agent else None)
190
200
 
191
201
  # The card: what the driver's operations support.
192
202
 
@@ -9,7 +9,7 @@ import re
9
9
  import signal
10
10
  import sys
11
11
  from pathlib import Path
12
- from typing import Optional
12
+ from typing import Iterator, Optional
13
13
 
14
14
  #: How often a starting session is looked for.
15
15
  POLL_S = 0.2
@@ -68,13 +68,15 @@ async def returncode(pid: int, timeout_s: float) -> Optional[int]:
68
68
 
69
69
 
70
70
  async def stop(pid: int, timeout_s: float) -> None:
71
- """End the session ``launch`` started as ``pid``, every process in it: asked to exit, then killed after
72
- ``timeout_s``. No one asks how it exited once it's stopped."""
71
+ """End the session ``launch`` started as ``pid``, every process in it: asked to exit, a paused one continued so it
72
+ can, then killed after ``timeout_s``. No one asks how it exited once it's stopped."""
73
73
  _exits.pop(pid, None)
74
74
  try:
75
75
  os.killpg(pid, signal.SIGTERM)
76
76
  except ProcessLookupError:
77
77
  return
78
+ with contextlib.suppress(ProcessLookupError, PermissionError): # macOS refuses a group of zombies alone
79
+ os.killpg(pid, signal.SIGCONT)
78
80
  try:
79
81
  await asyncio.wait_for(_exited(pid), timeout_s)
80
82
  except TimeoutError:
@@ -82,6 +84,21 @@ async def stop(pid: int, timeout_s: float) -> None:
82
84
  os.killpg(pid, signal.SIGKILL)
83
85
 
84
86
 
87
+ @contextlib.contextmanager
88
+ def paused(pid: int) -> Iterator[None]:
89
+ """Every process of the session ``launch`` started as ``pid`` stopped while the block runs, and continued after."""
90
+ try:
91
+ os.killpg(pid, signal.SIGSTOP)
92
+ except (ProcessLookupError, PermissionError):
93
+ yield
94
+ return
95
+ try:
96
+ yield
97
+ finally:
98
+ with contextlib.suppress(ProcessLookupError, PermissionError):
99
+ os.killpg(pid, signal.SIGCONT)
100
+
101
+
85
102
  def alive(pid: int) -> bool:
86
103
  """Whether a process of the session ``launch`` started as ``pid`` is running, its first or one it started."""
87
104
  try:
@@ -7,8 +7,9 @@ import asyncio
7
7
  import contextlib
8
8
  import logging
9
9
  import re
10
- from collections.abc import AsyncIterator, Iterable
10
+ from collections.abc import AsyncIterator, Callable, Iterable
11
11
  from pathlib import Path
12
+ from typing import Optional
12
13
  from urllib.parse import quote, unquote
13
14
 
14
15
  import httpx
@@ -148,9 +149,10 @@ async def forward(folder: Path, name: str, request: Request) -> Response:
148
149
  return response
149
150
 
150
151
 
151
- async def relay(folder: Path, name: str, websocket: WebSocket) -> None:
152
- """``websocket`` relayed both ways to the app ``name``'s server in ``folder`` until either side closes; refused,
153
- before it opens, with a Host that isn't a host and port, or with the app's server not answering."""
152
+ async def relay(folder: Path, name: str, websocket: WebSocket, opened: Optional[Callable[[], None]] = None) -> None:
153
+ """``websocket`` relayed both ways to the app ``name``'s server in ``folder`` until either side closes, ``opened``
154
+ called once the app has taken it; refused, before it opens, with a Host that isn't a host and port, or with the app's
155
+ server not answering."""
154
156
  host = websocket.headers.get("host", "")
155
157
  if host and not HOST.fullmatch(host):
156
158
  await websocket.close(code=1008)
@@ -168,6 +170,8 @@ async def relay(folder: Path, name: str, websocket: WebSocket) -> None:
168
170
  return
169
171
  async with upstream:
170
172
  await websocket.accept(subprotocol=upstream.subprotocol)
173
+ if opened is not None:
174
+ opened()
171
175
  await _both_ways(websocket, upstream)
172
176
 
173
177
 
@@ -120,19 +120,63 @@ class BrowserControl:
120
120
  active = await self.active_tab()
121
121
  return [OpenTab(**tab, active=tab["id"] == active).model_dump() for tab in self.tabs.values()]
122
122
 
123
- async def open_tab(self, url: Optional[str] = None) -> str:
124
- """A new tab at ``url``, else blank, brought to the front; its id. Headless, it opens blank and goes to ``url``
125
- once it's held to the window's size, so its page loads at that size."""
123
+ async def open_tab(self, url: Optional[str] = None, background: bool = False) -> str:
124
+ """A new tab at ``url``, else blank, brought to the front, or with ``background`` behind the active tab, which
125
+ stays active, brought back only if the new tab took its place; its id. Headless, it opens blank and goes to
126
+ ``url`` once it's held to the window's size, so its page loads at that size."""
127
+ active = await self.active_tab() if background else None
126
128
  held = self.viewport is not None and url
127
129
  start = "about:blank" if held else url or "about:blank"
128
- target = (await self.devtools.call("Target.createTarget", {"url": start}))["targetId"]
130
+ target = (await self.devtools.call("Target.createTarget", {"url": start, "background": background}))["targetId"]
129
131
  if held:
130
132
  await asyncio.wait_for(self._pinned.setdefault(target, asyncio.Event()).wait(), PIN_TIMEOUT_S)
131
133
  session = next(s for s, t in self._sessions.items() if t == target)
132
134
  await self.devtools.call("Page.navigate", {"url": url}, session_id=session)
133
- await self._bring_to_front(target)
135
+ if not background:
136
+ await self._bring_to_front(target)
137
+ elif active is not None and active in self.tabs and await self.active_tab() == target:
138
+ await self._bring_to_front(active)
134
139
  return target
135
140
 
141
+ async def reopen_start_tabs(self, urls: list[str]) -> None:
142
+ """The start tabs at their ``urls`` again, in place, the first active: each tab open, in opened order, goes to
143
+ its URL, and any the browser lacks opens. A page already loaded doesn't read cookies set after it, so a tab
144
+ a load signed in shows it only once it loads again."""
145
+ tabs = list(self.tabs)
146
+ for index, url in enumerate(urls):
147
+ if index < len(tabs):
148
+ await self._navigate_tab(tabs[index], url)
149
+ else:
150
+ tabs.append(await self.open_tab(url))
151
+ if tabs:
152
+ await self._bring_to_front(tabs[0])
153
+
154
+ async def replace_tabs(self, tabs: list) -> None:
155
+ """``tabs``, each with its ``url`` and whether it's ``active``, opened in order in place of the tabs open now,
156
+ the active one, else the first, in front. The new ones open before the old close, so the window stays."""
157
+ before = list(self.tabs)
158
+ opened = [await self.open_tab(tab.url) for tab in tabs]
159
+ for target in before:
160
+ await self.close_tab(target)
161
+ if opened:
162
+ await self._bring_to_front(next((target for target, tab in zip(opened, tabs) if tab.active), opened[0]))
163
+
164
+ async def _navigate_tab(self, target: str, url: str) -> None:
165
+ session = next((s for s, t in self._sessions.items() if t == target), None)
166
+ if session is None:
167
+ raise DeviceUnavailable(f"the browser's tab {target} has no session to navigate it")
168
+ answer = await self.devtools.call("Page.navigate", {"url": url}, session_id=session)
169
+ if error := answer.get("errorText"):
170
+ raise InvalidArgument(f"the browser couldn't open {url}: {error}")
171
+
172
+ def forget(self) -> None:
173
+ """A record that starts now, as a reset leaves the browser: no tabs closed, downloads or dialogs before it, and
174
+ no origin seen."""
175
+ self.since = _now()
176
+ self.closed_tabs, self.dialog_log = [], []
177
+ self.downloads.clear()
178
+ self.origins.clear()
179
+
136
180
  async def activate(self, target: str) -> None:
137
181
  """The open tab ``target`` brought to the front, the active one."""
138
182
  self._open(target)