agentenv-framework-protocol 0.1.468__tar.gz → 0.1.469__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 (160) hide show
  1. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/PKG-INFO +26 -1
  2. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/README.md +25 -0
  3. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/examples/website/notes/README.md +9 -5
  4. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/pyproject.toml +1 -1
  5. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/website/conformance.py +161 -26
  6. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/website/environment.py +30 -6
  7. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/website/routes.py +3 -2
  8. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/website/session.py +27 -3
  9. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/tests/test_website_conformance.py +79 -18
  10. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/tests/test_website_environment.py +44 -6
  11. agentenv_framework_protocol-0.1.469/tests/test_website_mcp.py +234 -0
  12. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/tests/test_website_notes_example.py +6 -4
  13. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/.gitignore +0 -0
  14. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/LICENSE +0 -0
  15. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/NOTICE +0 -0
  16. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/THIRD_PARTY_NOTICES.md +0 -0
  17. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/examples/__init__.py +0 -0
  18. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/examples/advanced_agent.py +0 -0
  19. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/examples/basic_agent.py +0 -0
  20. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/examples/custom_extensions_agent.py +0 -0
  21. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/examples/multimodal_agent.py +0 -0
  22. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/examples/reference_model.py +0 -0
  23. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/examples/streaming_agent.py +0 -0
  24. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/examples/website/notes/.gitignore +0 -0
  25. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/examples/website/notes/Dockerfile +0 -0
  26. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/examples/website/notes/Dockerfile.dockerignore +0 -0
  27. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/examples/website/notes/data/seed.json +0 -0
  28. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/examples/website/notes/env.toml +0 -0
  29. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/examples/website/notes/site.py +0 -0
  30. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/examples/website/notes/web/index.html +0 -0
  31. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/examples/website/notes/web/package-lock.json +0 -0
  32. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/examples/website/notes/web/package.json +0 -0
  33. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/examples/website/notes/web/src/main.js +0 -0
  34. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/examples/website/notes/web/src/style.css +0 -0
  35. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/examples/website/notes/web/vite.config.js +0 -0
  36. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/__init__.py +0 -0
  37. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/a2a_agent/__init__.py +0 -0
  38. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/a2a_agent/_trajectory_log.py +0 -0
  39. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/a2a_agent/_triggers.py +0 -0
  40. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/a2a_agent/extensions.py +0 -0
  41. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/a2a_agent/framework.py +0 -0
  42. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/a2a_agent/peer_agents.py +0 -0
  43. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/a2a_agent/proxy.py +0 -0
  44. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/a2a_agent/registry.py +0 -0
  45. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/a2a_agent/staging.py +0 -0
  46. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/a2a_agent/tasks/__init__.py +0 -0
  47. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/a2a_agent/tasks/v1.py +0 -0
  48. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/agent_env_environment.py +0 -0
  49. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/browser_cdp.py +0 -0
  50. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/client.py +0 -0
  51. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/__init__.py +0 -0
  52. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/_declare.py +0 -0
  53. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/_linux.py +0 -0
  54. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/_load.py +0 -0
  55. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/_macos.py +0 -0
  56. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/_with_fds.py +0 -0
  57. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/app.py +0 -0
  58. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/app_data.py +0 -0
  59. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/app_runtime.py +0 -0
  60. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/apps.py +0 -0
  61. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/card.py +0 -0
  62. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/checks.py +0 -0
  63. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/conformance/__init__.py +0 -0
  64. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/conformance/__main__.py +0 -0
  65. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/conformance/checks.py +0 -0
  66. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/conformance/client.py +0 -0
  67. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/conformance/fixture.py +0 -0
  68. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/conformance/mp4.py +0 -0
  69. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/conformance/page_fixture.py +0 -0
  70. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/conformance/report.py +0 -0
  71. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/dataplane.py +0 -0
  72. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/driver.py +0 -0
  73. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/errors.py +0 -0
  74. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/extensions.py +0 -0
  75. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/frames.py +0 -0
  76. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/keys.py +0 -0
  77. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/launch.py +0 -0
  78. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/manifest.py +0 -0
  79. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/operations.py +0 -0
  80. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/platform.py +0 -0
  81. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/queue.py +0 -0
  82. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/recording.py +0 -0
  83. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/results.py +0 -0
  84. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/run_install_step.py +0 -0
  85. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/runtime.py +0 -0
  86. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/seed.py +0 -0
  87. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/serve.py +0 -0
  88. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/serve_app.py +0 -0
  89. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/session.py +0 -0
  90. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/settings.py +0 -0
  91. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/skill.py +0 -0
  92. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/spaces.py +0 -0
  93. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/svc.py +0 -0
  94. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/tools.py +0 -0
  95. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/errors.py +0 -0
  96. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/manifest.py +0 -0
  97. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/preflight.py +0 -0
  98. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/tool_access.py +0 -0
  99. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/tool_client.py +0 -0
  100. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/tool_context.py +0 -0
  101. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/transfers.py +0 -0
  102. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/types.py +0 -0
  103. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/website/__init__.py +0 -0
  104. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/website/clock.py +0 -0
  105. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/website/errors.py +0 -0
  106. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/website/readiness.py +0 -0
  107. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/website_browser/__init__.py +0 -0
  108. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/website_browser/control.py +0 -0
  109. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/website_browser/devtools.py +0 -0
  110. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/website_browser/environment.py +0 -0
  111. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/website_browser/shapes.py +0 -0
  112. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/tests/computer_fake.py +0 -0
  113. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/tests/probe_fake.py +0 -0
  114. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/tests/test_a2a_agent.py +0 -0
  115. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/tests/test_a2a_agent_examples.py +0 -0
  116. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/tests/test_a2a_cdp_config.py +0 -0
  117. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/tests/test_a2a_object_transfers.py +0 -0
  118. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/tests/test_a2a_peer_agents.py +0 -0
  119. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/tests/test_a2a_proxy.py +0 -0
  120. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/tests/test_a2a_staging.py +0 -0
  121. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/tests/test_a2a_trajectory_log.py +0 -0
  122. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/tests/test_agent_env_environment.py +0 -0
  123. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/tests/test_browser_cdp.py +0 -0
  124. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/tests/test_browser_state.py +0 -0
  125. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/tests/test_client.py +0 -0
  126. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/tests/test_computer_app_data.py +0 -0
  127. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/tests/test_computer_app_server.py +0 -0
  128. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/tests/test_computer_conformance.py +0 -0
  129. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/tests/test_computer_conformance_checks.py +0 -0
  130. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/tests/test_computer_conformance_page.py +0 -0
  131. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/tests/test_computer_conformance_recording.py +0 -0
  132. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/tests/test_computer_dataplane.py +0 -0
  133. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/tests/test_computer_declare.py +0 -0
  134. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/tests/test_computer_driver.py +0 -0
  135. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/tests/test_computer_extensions.py +0 -0
  136. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/tests/test_computer_frames.py +0 -0
  137. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/tests/test_computer_macos.py +0 -0
  138. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/tests/test_computer_page.py +0 -0
  139. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/tests/test_computer_queue.py +0 -0
  140. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/tests/test_computer_recording.py +0 -0
  141. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/tests/test_computer_seed.py +0 -0
  142. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/tests/test_computer_serve.py +0 -0
  143. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/tests/test_computer_spaces.py +0 -0
  144. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/tests/test_computer_svc_forward.py +0 -0
  145. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/tests/test_control_token.py +0 -0
  146. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/tests/test_data_objects.py +0 -0
  147. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/tests/test_errors.py +0 -0
  148. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/tests/test_extension_methods.py +0 -0
  149. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/tests/test_intake.py +0 -0
  150. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/tests/test_manifest.py +0 -0
  151. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/tests/test_preflight.py +0 -0
  152. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/tests/test_tool_access.py +0 -0
  153. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/tests/test_tool_annotations.py +0 -0
  154. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/tests/test_tool_client.py +0 -0
  155. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/tests/test_tool_context.py +0 -0
  156. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/tests/test_tool_context_fastmcp.py +0 -0
  157. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/tests/test_tool_context_injection.py +0 -0
  158. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/tests/test_website_api.py +0 -0
  159. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/tests/test_website_browser.py +0 -0
  160. {agentenv_framework_protocol-0.1.468 → agentenv_framework_protocol-0.1.469}/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.468
3
+ Version: 0.1.469
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
@@ -524,6 +524,31 @@ print(env.ready, env.problems) # False ['shop: seeded']
524
524
  asked, or answers no `WebsiteReadiness`, is a problem itself. A card or child without `ready`, an MCP server's or an
525
525
  older site's, isn't counted, so an env with no site serving it is ready with no problems.
526
526
 
527
+ ## Websites with tools
528
+
529
+ A website env may also be an MCP server, so an agent can act on a site through tools as well as its pages. Give a
530
+ `WebsiteEnvironment` `@tool` (or `@resource`) methods, as an MCP env has; they need the `mcp` package. The site then
531
+ serves them at `/mcp` (streamable HTTP) beside its pages, `@api` routes and data plane, and its card declares that
532
+ endpoint as its `mcp` interface (`additionalInterfaces`) and lists the tools under `capabilities.tools`. A site without
533
+ tools serves no `/mcp` and declares no interface; `serves_mcp()` says which a site is.
534
+
535
+ ```python
536
+ @environment_card(name="shop")
537
+ @website(domains=["shop.example"], static="dist")
538
+ class Shop(WebsiteEnvironment):
539
+ @tool()
540
+ def place_order(self, item: str, ctx: ToolContext) -> str:
541
+ """Place an order for item, as the caller."""
542
+ ...
543
+ ```
544
+
545
+ A tool runs in the site's process, so it changes what its pages show and what `data/get` returns, and it reads the
546
+ universe's time (`self.now()`) as a page's request does. The gateway reads each site's card when it starts, and serves
547
+ the tools of a site whose card declares an `mcp` interface as it does an MCP env's: listed to agents with the rest,
548
+ called with the caller's role in `ToolContext`. The site's `/mcp` is one of the paths its card advertises, which its
549
+ browsing route (`/website/<name>/`) never reaches. `python -m agentenv_protocol.website.conformance` checks a declared
550
+ endpoint (`mcp`): it answers `initialize` and lists the tools the card does.
551
+
527
552
  ## Computer drivers
528
553
 
529
554
  A computer driver is what runs inside a computer an agent uses through screenshots and input. Write
@@ -462,6 +462,31 @@ print(env.ready, env.problems) # False ['shop: seeded']
462
462
  asked, or answers no `WebsiteReadiness`, is a problem itself. A card or child without `ready`, an MCP server's or an
463
463
  older site's, isn't counted, so an env with no site serving it is ready with no problems.
464
464
 
465
+ ## Websites with tools
466
+
467
+ A website env may also be an MCP server, so an agent can act on a site through tools as well as its pages. Give a
468
+ `WebsiteEnvironment` `@tool` (or `@resource`) methods, as an MCP env has; they need the `mcp` package. The site then
469
+ serves them at `/mcp` (streamable HTTP) beside its pages, `@api` routes and data plane, and its card declares that
470
+ endpoint as its `mcp` interface (`additionalInterfaces`) and lists the tools under `capabilities.tools`. A site without
471
+ tools serves no `/mcp` and declares no interface; `serves_mcp()` says which a site is.
472
+
473
+ ```python
474
+ @environment_card(name="shop")
475
+ @website(domains=["shop.example"], static="dist")
476
+ class Shop(WebsiteEnvironment):
477
+ @tool()
478
+ def place_order(self, item: str, ctx: ToolContext) -> str:
479
+ """Place an order for item, as the caller."""
480
+ ...
481
+ ```
482
+
483
+ A tool runs in the site's process, so it changes what its pages show and what `data/get` returns, and it reads the
484
+ universe's time (`self.now()`) as a page's request does. The gateway reads each site's card when it starts, and serves
485
+ the tools of a site whose card declares an `mcp` interface as it does an MCP env's: listed to agents with the rest,
486
+ called with the caller's role in `ToolContext`. The site's `/mcp` is one of the paths its card advertises, which its
487
+ browsing route (`/website/<name>/`) never reaches. `python -m agentenv_protocol.website.conformance` checks a declared
488
+ endpoint (`mcp`): it answers `initialize` and lists the tools the card does.
489
+
465
490
  ## Computer drivers
466
491
 
467
492
  A computer driver is what runs inside a computer an agent uses through screenshots and input. Write
@@ -22,9 +22,11 @@ them. The page shows the universe's time.
22
22
  sends `AgentEnv-Role: admin` with `AgentEnv-Role-Key` equal to the container's `WEBSITE_ROLE_KEY`; any other caller
23
23
  gets 403. The gateway passes on whatever role a browser sends, so without the key a page could claim `admin`; with no
24
24
  `WEBSITE_ROLE_KEY` set, every caller is `default` and the route is closed.
25
- - **Users.** `ApiContext.user` is the user in the signed session cookie. With no cookie, it is the acting user (the
26
- data's `current_user`, which `acting_user()` returns), and `GET /api/session` signs that user in. `POST /api/sign-in` switches to another seeded
27
- user. A user's notes are invisible to every other user, and another user's note answers 404.
25
+ - **Users.** `ApiContext.user` is the user in the signed session cookie, `agentenv_session_notes` (the site's own, so
26
+ it doesn't collide with another site's under the same gateway host; the card names it). With no cookie, it is the
27
+ acting user (the data's `current_user`, which `acting_user()` returns), and `GET /api/session` signs that user in.
28
+ `POST /api/sign-in` switches to another seeded user. A user's notes are invisible to every other user, and another
29
+ user's note answers 404.
28
30
  - **State.** The site keeps its users and notes itself, starting from `data/seed.json`, and serves them through its
29
31
  data hooks: `data/add` adds a part's records by id (a data part, or a file part by bytes or `file://`), `data/get`
30
32
  exports them, and `data/reset` empties them. `data/get` is how agentenv reads what a task left the site with.
@@ -65,11 +67,13 @@ the test above runs this repo's.
65
67
  ```bash
66
68
  docker build -t notes-site .
67
69
  docker run -d --name notes-site -p 18080:8000 notes-site
68
- python -m agentenv_protocol.website.conformance http://localhost:18080 --clock-host host.docker.internal
70
+ python -m agentenv_protocol.website.conformance http://localhost:18080 --clock-host host.docker.internal --strict
69
71
  curl -s http://localhost:18080/api/notes
70
72
  ```
71
73
 
72
- Use `--clock-host` so the container can reach the checker's clock. The clock check leaves the server following a
74
+ Use `--clock-host` so the container can reach the checker's clock. `--strict` fails on a recommended check's warning
75
+ too (the card, `X-Universe-Now`, the clock, forwarded headers, cookies); without it only the contract's required checks
76
+ fail the run, and Notes passes every check either way. The clock check leaves the server following a
73
77
  clock that is gone once the check ends: later timestamps start from 2031 until the server is synced again, so restart
74
78
  the container before using it.
75
79
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agentenv-framework-protocol"
3
- version = "0.1.468"
3
+ version = "0.1.469"
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,13 +1,21 @@
1
- """Check a running site's server against ``urn:agentenv:website/v1``, whatever it's written in:
2
- ``python -m agentenv_protocol.website.conformance <url>`` prints each check and exits 1 when one failed (a warning
3
- doesn't), 2 when the server couldn't be reached. Everything goes over HTTP: the card, ``/`` under a made-up host, readiness by the card's
4
- ``website/v1`` ``ready``, whose answer must be a ``WebsiteReadiness`` (by the declared health path, deprecated, for a
5
- card without it), ``X-Universe-Now`` on every answer the other checks got, a data-plane round trip at the card's ``url``
6
- (data/get, data/reset, data/get, which no longer returns the exported data, data/add of what the first data/get
7
- returned, data/get again), ``clock/v1``'s
8
- ``sync_time`` against a clock the checker serves, and the forwarded headers wherever an answer shows them, on ``/`` and
9
- on a GET route with its trailing slash toggled, which a server may redirect, and the cookies those answers set:
10
- none tied to a domain, and a warning for one whose lifetime is an absolute ``Expires`` with no ``Max-Age``.
1
+ """Check a running site's server against ``urn:agentenv:website/v1``, whatever it's written in: ``python -m
2
+ agentenv_protocol.website.conformance <url>`` prints each check and exits 1 when a required one failed (or, with
3
+ ``--strict``, when a recommended one warned), 2 when the server couldn't be reached. Everything goes over HTTP.
4
+
5
+ The required checks are the contract, what agentenv, the gateway and a browser rely on of a website env's server: a
6
+ card declaring the site's domains, readiness and routes; ``/`` serving HTML under a made-up host; the site ready, by
7
+ the card's ``website/v1`` ``ready``, whose answer must be a ``WebsiteReadiness`` (by the declared health path,
8
+ deprecated, for a card without it); the data plane's and card's paths not the site's pages; and the data plane the
9
+ card advertises, whose round trip at the card's ``url`` (data/get, data/reset, data/get, which no longer returns the
10
+ exported data, data/add of what the first data/get returned, data/get again) returns what it was given; and, for a
11
+ card declaring an ``mcp`` interface, a site that is an MCP server too, that endpoint answering ``initialize`` and
12
+ listing, every page of ``tools/list`` read, no tool its card doesn't (streamable HTTP, ``MCP_PROTOCOL_VERSION``); a
13
+ card's tool it doesn't list is warned, since a hidden-by-default tool is listed to no caller until one enables it.
14
+ A failed one is ``failed``. The recommended checks are what an SDK site does and the gateway uses when a server offers it:
15
+ ``X-Universe-Now`` on every answer the other checks got, ``clock/v1``'s ``sync_time`` against a clock the checker serves, the forwarded headers
16
+ wherever an answer shows them (on ``/`` and on a GET route with its trailing slash toggled, which a server may
17
+ redirect), and the cookies those answers set: none tied to a domain, the session path's the one the card names, and
18
+ none whose lifetime is an absolute ``Expires`` with no ``Max-Age``. A failed one is ``warned``.
11
19
 
12
20
  Every request goes to the server under check: a card's path or URL naming another scheme, host or port fails the check
13
21
  that would have used it, with nothing sent there. The round trip leaves the server's data as it found it, adding the
@@ -33,25 +41,35 @@ import httpx
33
41
  from pydantic import ValidationError
34
42
 
35
43
  from ..client import find_extension, find_extension_method
36
- from ..types import METHOD_ADD, METHOD_GET, METHOD_RESET, RPC_PATH, WELL_KNOWN_PATH
44
+ from ..types import MCP_TRANSPORT, METHOD_ADD, METHOD_GET, METHOD_RESET, RPC_PATH, WELL_KNOWN_PATH
37
45
  from .clock import CLOCK_EXTENSION_URI, UNIVERSE_NOW_HEADER, iso, parse_time
38
46
  from .readiness import DEFAULT_HEALTH_PATH, WEBSITE_EXTENSION_URI, WEBSITE_READY_OP, WebsiteReadiness
39
47
 
40
48
  Status = Literal["passed", "failed", "skipped", "warned"]
41
49
  CLOCK_START = datetime.datetime(2031, 5, 6, 7, 8, 9, tzinfo=datetime.timezone.utc)
42
50
  CLOCK_SLACK_S = 120
51
+ MCP_PROTOCOL_VERSION = "2025-06-18"
52
+ MCP_LIST_PAGES = 100
43
53
 
44
54
 
45
55
  @dataclasses.dataclass(frozen=True)
46
56
  class Result:
57
+ """One check's outcome; ``required`` for a check of the contract, whose failure is ``failed``, and not for a
58
+ recommended one, whose failure is ``warned``."""
59
+
47
60
  name: str
48
61
  status: Status
49
62
  detail: str = ""
63
+ required: bool = True
50
64
 
51
65
  @property
52
66
  def passed(self) -> bool:
53
67
  return self.status != "failed"
54
68
 
69
+ def fails(self, strict: bool = False) -> bool:
70
+ """Whether the result fails the run: a failed check, or with ``strict`` a warned one too."""
71
+ return self.status == "failed" or (strict and self.status == "warned")
72
+
55
73
 
56
74
  def check(base_url: str, *, client: httpx.Client | None = None, clock_host: str = "127.0.0.1",
57
75
  timeout: float = 30) -> list[Result]:
@@ -92,19 +110,27 @@ class _Checker:
92
110
  card = response.json() if response.status_code == 200 else {}
93
111
  except ValueError:
94
112
  card = {}
113
+ card = card if isinstance(card, dict) else {}
95
114
  site = (find_extension(card, WEBSITE_EXTENSION_URI) or {}).get("params") or {}
96
115
  health = site.get("health") if isinstance(site.get("health"), str) else DEFAULT_HEALTH_PATH
97
- steps = (("any_host", self._any_host), ("health", lambda: self._health(card, health)),
98
- ("universe_now", self._universe_now), ("data_plane", lambda: self._data_plane(card)),
99
- ("clock", lambda: self._clock(card)), ("forwarded", lambda: self._forwarded(site)),
100
- ("cookies", lambda: self._cookies(site)))
101
- outcomes = {name: _outcome(step) for name, step in steps if name != "universe_now"}
116
+ # (name, required, check): a recommended check's failure is warned.
117
+ steps = (("card", True, lambda: self._card(response, card, site)), ("any_host", True, self._any_host),
118
+ ("health", True, lambda: self._health(card, health)), ("universe_now", False, self._universe_now),
119
+ ("reserved_paths", True, self._reserved_paths), ("data_plane", True, lambda: self._data_plane(card)),
120
+ ("mcp", True, lambda: self._mcp(card)),
121
+ ("clock", False, lambda: self._clock(card)), ("forwarded", False, lambda: self._forwarded(site)),
122
+ ("cookies", False, lambda: self._cookies(site)))
123
+ outcomes = {name: _outcome(step) for name, _required, step in steps if name != "universe_now"}
102
124
  outcomes["universe_now"] = _outcome(self._universe_now)
103
- return [self._card(response, card, site), *(Result(name, *outcomes[name]) for name, _step in steps)]
125
+ results = []
126
+ for name, required, _step in steps:
127
+ status, detail = outcomes[name]
128
+ results.append(Result(name, "warned" if status == "failed" and not required else status, detail, required))
129
+ return results
104
130
 
105
- def _card(self, response: httpx.Response, card: dict, site: dict) -> Result:
131
+ def _card(self, response: httpx.Response, card: dict, site: dict) -> tuple[Status, str]:
106
132
  if response.status_code != 200:
107
- return Result("card", "failed", f"{WELL_KNOWN_PATH} answered {response.status_code}")
133
+ return "failed", f"{WELL_KNOWN_PATH} answered {response.status_code}"
108
134
  problems = []
109
135
  if not site:
110
136
  problems.append(f"declares no {WEBSITE_EXTENSION_URI} with params")
@@ -117,7 +143,7 @@ class _Checker:
117
143
  if not isinstance(routes, list) or not all(isinstance(r, dict) and {"method", "path", "name"} <= set(r)
118
144
  for r in routes):
119
145
  problems.append("its params' routes aren't a list of {method, path, name}")
120
- return Result("card", "failed" if problems else "passed", "; ".join(problems))
146
+ return ("failed" if problems else "passed"), "; ".join(problems)
121
147
 
122
148
  def _any_host(self) -> tuple[Status, str]:
123
149
  host = f"conformance-{uuid.uuid4().hex[:8]}.invalid"
@@ -159,6 +185,15 @@ class _Checker:
159
185
  parse_time(response.headers[UNIVERSE_NOW_HEADER])
160
186
  return "passed", f"on all {len(self.responses)} answers"
161
187
 
188
+ def _reserved_paths(self) -> tuple[Status, str]:
189
+ paths = [RPC_PATH, WELL_KNOWN_PATH, f"{RPC_PATH}/conformance-{uuid.uuid4().hex[:8]}", f"{RPC_PATH}/",
190
+ f"{WELL_KNOWN_PATH}/"]
191
+ for path in paths:
192
+ response = self.get(path)
193
+ if response.is_success and response.headers.get("content-type", "").startswith("text/html"):
194
+ return "failed", f"{path} answered a page, but the gateway keeps {RPC_PATH} and {WELL_KNOWN_PATH}"
195
+ return "passed", f"no page at {', '.join(paths)}"
196
+
162
197
  def _rpc(self, method: str, params: dict | None = None) -> dict:
163
198
  response = self._seen(self.client.post(self.rpc_url, json={"jsonrpc": "2.0", "id": method,
164
199
  "method": method, "params": params or {}}))
@@ -171,7 +206,8 @@ class _Checker:
171
206
 
172
207
  def _data_plane(self, card: dict) -> tuple[Status, str]:
173
208
  operations = (card.get("capabilities") or {}).get("operations")
174
- missing = [m for m in (METHOD_GET, METHOD_RESET, METHOD_ADD) if operations is not None and m not in operations]
209
+ operations = operations if isinstance(operations, list) else []
210
+ missing = [m for m in (METHOD_GET, METHOD_RESET, METHOD_ADD) if m not in operations]
175
211
  if missing:
176
212
  return "failed", f"the card's operations lack {', '.join(missing)}"
177
213
  url = card.get("url")
@@ -198,6 +234,55 @@ class _Checker:
198
234
  return "failed", "data/get after data/reset and data/add of its own export returned other data"
199
235
  return "passed", f"{len(exported)} part(s) round-tripped through reset and add"
200
236
 
237
+ def _mcp(self, card: dict) -> tuple[Status, str]:
238
+ """The MCP endpoint the card declares answers ``initialize`` and lists no tool the card doesn't, reading up to
239
+ ``MCP_LIST_PAGES`` pages of ``tools/list``; its answers needn't carry ``X-Universe-Now``, which the MCP
240
+ transport doesn't promise."""
241
+ interfaces = [i for i in card.get("additionalInterfaces") or [] if isinstance(i, dict)
242
+ and i.get("transport") == MCP_TRANSPORT]
243
+ if not interfaces:
244
+ return "skipped", "the card declares no mcp interface"
245
+ path = interfaces[0].get("url")
246
+ if not isinstance(path, str) or not path:
247
+ return "failed", f"the card's mcp interface has no url: {path!r}"
248
+ url = self._url(path)
249
+ headers = {"Accept": "application/json, text/event-stream"}
250
+ first = self.client.post(url, headers=headers, json={
251
+ "jsonrpc": "2.0", "id": "initialize", "method": "initialize",
252
+ "params": {"protocolVersion": MCP_PROTOCOL_VERSION, "capabilities": {},
253
+ "clientInfo": {"name": "agentenv-website-conformance", "version": "1"}}})
254
+ server = _mcp_result(first, "initialize", "initialize").get("serverInfo") or {}
255
+ if session := first.headers.get("mcp-session-id"):
256
+ headers["Mcp-Session-Id"] = session
257
+ try:
258
+ self.client.post(url, headers=headers, json={"jsonrpc": "2.0", "method": "notifications/initialized"})
259
+ served: list = []
260
+ cursor = None
261
+ for _page in range(MCP_LIST_PAGES):
262
+ listed = _mcp_result(self.client.post(url, headers=headers, json={
263
+ "jsonrpc": "2.0", "id": "tools/list", "method": "tools/list",
264
+ **({"params": {"cursor": cursor}} if cursor else {})}), "tools/list", "tools/list")
265
+ if not isinstance(listed, dict) or not isinstance(listed.get("tools"), list):
266
+ return "failed", f"tools/list at {path} answered no tools list: {str(listed)[:200]}"
267
+ served += [t.get("name") for t in listed["tools"] if isinstance(t, dict)]
268
+ cursor = listed.get("nextCursor")
269
+ if not cursor:
270
+ break
271
+ else:
272
+ return "failed", f"tools/list at {path} still had a next page after {MCP_LIST_PAGES}"
273
+ finally:
274
+ if session:
275
+ with contextlib.suppress(httpx.HTTPError):
276
+ self.client.delete(url, headers=headers)
277
+ advertised = {t.get("name") for t in (card.get("capabilities") or {}).get("tools") or [] if isinstance(t, dict)}
278
+ if unadvertised := sorted(set(served) - advertised):
279
+ return "failed", f"tools/list at {path} named {unadvertised}, which the card doesn't list"
280
+ name = server.get("name") or "the server"
281
+ if unlisted := sorted(advertised - set(served)):
282
+ return "warned", (f"{name} at {path} lists {len(served)} of the card's tools, not {unlisted}: hidden by "
283
+ "default, or missing")
284
+ return "passed", f"{name} at {path} lists the card's {len(served)} tool(s)"
285
+
201
286
  def _clock(self, card: dict) -> tuple[Status, str]:
202
287
  if find_extension(card, CLOCK_EXTENSION_URI) is None:
203
288
  return "failed", f"the card declares no {CLOCK_EXTENSION_URI}"
@@ -238,14 +323,19 @@ class _Checker:
238
323
  return "passed", f"no answer to {', '.join(paths)} named the internal Host"
239
324
 
240
325
  def _cookies(self, site: dict) -> tuple[Status, str]:
326
+ session = []
241
327
  if isinstance(site.get("session_path"), str):
242
- self.get(site["session_path"])
328
+ session = self.get(site["session_path"]).headers.get_list("set-cookie")
243
329
  cookies = [c for r in self.responses for c in r.headers.get_list("set-cookie")]
244
330
  if not cookies:
245
331
  return "skipped", "no answer set a cookie"
246
332
  scoped = sorted({c.split("=", 1)[0] for c in cookies if "domain" in _attributes(c)})
247
333
  if scoped:
248
334
  return "failed", f"cookies tied to a domain: {', '.join(scoped)}"
335
+ named = site.get("session_cookie")
336
+ if isinstance(named, str) and session and named not in {c.split("=", 1)[0].strip() for c in session}:
337
+ return "failed", (f"{site['session_path']} set {', '.join(c.split('=', 1)[0] for c in session)}, not the "
338
+ f"card's {named}")
249
339
  absolute = sorted({c.split("=", 1)[0] for c in cookies if "expires" in _attributes(c) and "max-age" not in _attributes(c)})
250
340
  if absolute:
251
341
  return "warned", (f"cookies with an absolute Expires and no Max-Age, which a browser on another clock "
@@ -275,6 +365,48 @@ def _outcome(step: Callable[[], tuple[Status, str]]) -> tuple[Status, str]:
275
365
  return "failed", f"{type(e).__name__}: {e}"
276
366
 
277
367
 
368
+ def _mcp_result(response: httpx.Response, method: str, request_id: str) -> dict:
369
+ """The result of the JSON-RPC reply to request ``request_id``: the answer's JSON, or the reply among an event
370
+ stream's messages, each event's data lines joined, as ``ToolSession`` reads them."""
371
+ if response.headers.get("content-type", "").startswith("text/event-stream"):
372
+ messages = []
373
+ for data in _sse_data(response.text):
374
+ try:
375
+ messages.append(json.loads(data))
376
+ except ValueError:
377
+ raise RuntimeError(f"{method} answered an event that isn't JSON: {data[:200]!r}") from None
378
+ else:
379
+ try:
380
+ messages = [json.loads(response.text)]
381
+ except ValueError:
382
+ raise RuntimeError(f"{method} answered {response.status_code} with no JSON-RPC answer: "
383
+ f"{response.text[:200]!r}") from None
384
+ body = next((m for m in messages if isinstance(m, dict) and m.get("id") == request_id
385
+ and ("result" in m or "error" in m)), None)
386
+ if body is None or "result" not in body:
387
+ raise RuntimeError(f"{method} answered {response.status_code}: {str(body or messages)[:200]}")
388
+ return body["result"] or {}
389
+
390
+
391
+ def _sse_data(text: str) -> Iterator[str]:
392
+ """The data of each ``message`` event in a server-sent event stream, its data lines joined; an event the stream
393
+ ends inside is dropped, as are events with no data."""
394
+ event, data = "message", []
395
+ for line in text.splitlines():
396
+ if not line:
397
+ payload = "\n".join(data)
398
+ if payload.strip() and event == "message":
399
+ yield payload
400
+ event, data = "message", []
401
+ continue
402
+ field, _, value = line.partition(":")
403
+ value = value[1:] if value.startswith(" ") else value
404
+ if field == "data":
405
+ data.append(value)
406
+ elif field == "event":
407
+ event = value
408
+
409
+
278
410
  def _field_error(error: dict) -> str:
279
411
  """One of a ``ValidationError``'s errors as a person reads it: where, then what."""
280
412
  where = ".".join(map(str, error.get("loc") or ())) or "the answer"
@@ -341,6 +473,7 @@ def main(argv: list[str] | None = None) -> int:
341
473
  parser.add_argument("url", help="where the server answers, such as http://localhost:8000")
342
474
  parser.add_argument("--clock-host", default="127.0.0.1", help="the address the server reaches this checker at")
343
475
  parser.add_argument("--timeout", type=float, default=30, help="seconds for each request")
476
+ parser.add_argument("--strict", action="store_true", help="fail on a recommended check's warning too")
344
477
  args = parser.parse_args(argv)
345
478
  try:
346
479
  results = check(args.url, clock_host=args.clock_host, timeout=args.timeout)
@@ -348,10 +481,12 @@ def main(argv: list[str] | None = None) -> int:
348
481
  print(f"couldn't reach {args.url}: {type(e).__name__}: {e}", file=sys.stderr)
349
482
  return 2
350
483
  for result in results:
351
- print(f"{_MARK[result.status]} {result.name}{': ' + result.detail if result.detail else ''}")
352
- failed = sum(not r.passed for r in results)
353
- print(f"{args.url}: {len(results) - failed} passed, warned or skipped, {failed} failed")
354
- return 1 if failed else 0
484
+ kind = "required" if result.required else "recommended"
485
+ print(f"{_MARK[result.status]} {result.name} ({kind}){': ' + result.detail if result.detail else ''}")
486
+ counts = {status: sum(r.status == status for r in results) for status in _MARK}
487
+ print(f"{args.url}: {counts['passed']} passed, {counts['failed']} failed, {counts['warned']} warned, "
488
+ f"{counts['skipped']} skipped{' (strict)' if args.strict else ''}")
489
+ return 1 if any(r.fails(args.strict) for r in results) else 0
355
490
 
356
491
 
357
492
  if __name__ == "__main__":
@@ -8,6 +8,10 @@ usually its schema of the env's database (``database_url``), and serves it throu
8
8
  (``@reset_data``, ``@add_data``, ``@get_data``) as any AgentEnvEnvironment does: ``data/get`` is how agentenv reads
9
9
  what a task left a site with.
10
10
 
11
+ A site may also be an MCP server, as an MCP env is: its ``@tool`` and ``@resource`` methods are served at ``/mcp``
12
+ (streamable HTTP, of the ``mcp`` package), which its card declares as its ``mcp`` interface and lists the tools of, so
13
+ an agent can act on the site through tools beside its pages. A site with none serves no ``/mcp``.
14
+
11
15
  One route is built in, replaced by a site's own ``@api`` route at its very path and answering before one whose path
12
16
  parameters would also match it: ``/api/session``, which a frontend calls to learn who is signed in, signing the acting
13
17
  user in when nobody is (GET), or to sign out (DELETE)."""
@@ -32,9 +36,12 @@ from starlette.routing import Route
32
36
 
33
37
  from ..agent_env_environment import (
34
38
  AgentEnvEnvironment,
39
+ _RESOURCE_ATTR,
40
+ _TOOL_ATTR,
35
41
  AgentEnvStarletteApplication,
36
42
  _marked_members,
37
43
  _maybe_await,
44
+ create_fastmcp_app,
38
45
  extension,
39
46
  )
40
47
  from .clock import CLOCK_EXTENSION_URI, UNIVERSE_NOW_HEADER, ClockUnavailable, UniverseClock, clock_patch_script, iso
@@ -42,7 +49,7 @@ from .errors import ApiError, NotFound
42
49
  from .readiness import SITE_READY_TIMEOUT_S, WEBSITE_EXTENSION_URI, WEBSITE_READY_OP, WebsiteReadiness
43
50
  from .routes import (ApiContext, ApiRoute, endpoint, error_response, forwarded_prefix, openapi, openapi_path, request_role,
44
51
  reserved_path, routes_of)
45
- from .session import SESSION_COOKIE, SessionSigner
52
+ from .session import SessionSigner, session_cookie_name
46
53
 
47
54
  SESSION_PATH = "/api/session"
48
55
  CHECK_TIMEOUT_S = 10.0
@@ -114,7 +121,7 @@ class WebsiteEnvironment(AgentEnvEnvironment):
114
121
 
115
122
  def __init__(self) -> None:
116
123
  self.clock = UniverseClock()
117
- self._signer = SessionSigner()
124
+ self._signer = SessionSigner(cookie_name=session_cookie_name(self._build_card().name))
118
125
  self._started = time.monotonic()
119
126
  self._checking: dict[str, asyncio.Future] = {}
120
127
 
@@ -126,6 +133,11 @@ class WebsiteEnvironment(AgentEnvEnvironment):
126
133
  def routes(cls) -> list[ApiRoute]:
127
134
  return routes_of(cls)
128
135
 
136
+ @property
137
+ def session_cookie(self) -> str:
138
+ """The name of the site's session cookie, ``agentenv_session_<environment_name>``."""
139
+ return self._session_signer().cookie_name
140
+
129
141
  @property
130
142
  def database_url(self) -> str | None:
131
143
  """Where the site keeps its task state: its own schema of the env's database."""
@@ -148,7 +160,7 @@ class WebsiteEnvironment(AgentEnvEnvironment):
148
160
 
149
161
  def context(self, request: Request) -> ApiContext:
150
162
  """The ``ApiContext`` of ``request``, for a mounted ``app``'s routes as for ``@api`` ones."""
151
- session = self._session_signer().decode(request.cookies.get(SESSION_COOKIE))
163
+ session = self._session_signer().read(request.cookies)
152
164
  user = session["user"] if session is not None else self.acting_user()
153
165
  return ApiContext(request, user, request_role(request), session, self._session_signer())
154
166
 
@@ -159,8 +171,15 @@ class WebsiteEnvironment(AgentEnvEnvironment):
159
171
  api_routes = self.routes()
160
172
  taken = {openapi_path(r.path) for r in api_routes}
161
173
  root = self._site_path(declaration.static) if declaration.static is not None else None
162
- asgi = Starlette(middleware=[Middleware(_Forwarded), Middleware(_Universe, env=self)])
163
- AgentEnvStarletteApplication(self._build_card(), self).add_routes_to_app(asgi)
174
+ middleware = [Middleware(_Forwarded), Middleware(_Universe, env=self)]
175
+ if self.serves_mcp():
176
+ self.mcp = create_fastmcp_app(self, card=self._build_card())
177
+ served = self.mcp.streamable_http_app()
178
+ asgi = Starlette(middleware=middleware, lifespan=served.router.lifespan_context)
179
+ asgi.router.routes.extend(served.routes)
180
+ else:
181
+ asgi = Starlette(middleware=middleware)
182
+ AgentEnvStarletteApplication(self._build_card(), self).add_routes_to_app(asgi)
164
183
  if SESSION_PATH not in taken:
165
184
  asgi.router.routes.append(Route(SESSION_PATH, self._session, methods=["GET", "DELETE"]))
166
185
  asgi.router.routes.extend(Route(r.path, _unreserved(endpoint(getattr(self, r.fn.__name__), r, self.context)),
@@ -170,6 +189,11 @@ class WebsiteEnvironment(AgentEnvEnvironment):
170
189
  self._mounted = True
171
190
  return asgi
172
191
 
192
+ def serves_mcp(self) -> bool:
193
+ """Whether the site is an MCP server too: it has ``@tool`` or ``@resource`` methods, or forwards tools."""
194
+ return any(True for _ in _marked_members(self, _TOOL_ATTR)) or any(
195
+ True for _ in _marked_members(self, _RESOURCE_ATTR)) or bool(self.forwarded_tools())
196
+
173
197
  def mount(self, app: Any) -> Any:
174
198
  raise TypeError(f"{type(self).__name__} builds its own app with create_app(); set app = ... to serve more")
175
199
 
@@ -186,7 +210,7 @@ class WebsiteEnvironment(AgentEnvEnvironment):
186
210
  if uri == WEBSITE_EXTENSION_URI:
187
211
  declaration = self.declaration()
188
212
  return {"domains": list(declaration.domains),
189
- "routes": [r.advertised() for r in self.routes()], "session_cookie": SESSION_COOKIE,
213
+ "routes": [r.advertised() for r in self.routes()], "session_cookie": self.session_cookie,
190
214
  "session_path": SESSION_PATH}
191
215
  return super().extension_params(uri)
192
216
 
@@ -28,7 +28,7 @@ from ..agent_env_environment import _coerce_scalar, _maybe_await, _schema_for_an
28
28
  from ..tool_context import DEFAULT_ROLE, caller_from_request
29
29
  from ..types import RPC_PATH, WELL_KNOWN_PATH
30
30
  from .errors import ApiError, BadRequest, Forbidden
31
- from .session import SESSION_COOKIE, SESSION_TTL_S, SessionSigner
31
+ from .session import SESSION_TTL_S, SessionSigner
32
32
 
33
33
  logger = logging.getLogger(__name__)
34
34
 
@@ -154,7 +154,8 @@ class ApiContext:
154
154
 
155
155
  def apply(self, response: Response) -> Response:
156
156
  for value in self._set_cookie:
157
- response.set_cookie(SESSION_COOKIE, value, max_age=SESSION_TTL_S, path="/", httponly=True, samesite="lax")
157
+ response.set_cookie(self._signer.cookie_name, value, max_age=SESSION_TTL_S, path="/", httponly=True,
158
+ samesite="lax")
158
159
  return response
159
160
 
160
161
 
@@ -2,7 +2,8 @@
2
2
  under ``WEBSITE_SESSION_SECRET``, which a deploy sets for each site so its sessions outlive its process, or a secret
3
3
  of the process's own when that isn't set. The cookie names no domain, so it holds under whatever host the site is
4
4
  reached at, and its lifetime is a ``Max-Age``, never an absolute ``Expires``, so the browser's clock and the site's
5
- needn't agree."""
5
+ needn't agree. Its name is the site's own, ``agentenv_session_<name>``, so two sites a browser reaches under one gateway
6
+ host (``/website/a/``, ``/website/b/``) keep a session each."""
6
7
  from __future__ import annotations
7
8
 
8
9
  import base64
@@ -11,6 +12,7 @@ import hashlib
11
12
  import hmac
12
13
  import json
13
14
  import os
15
+ import re
14
16
  import secrets
15
17
  import time
16
18
 
@@ -18,12 +20,30 @@ SESSION_COOKIE = "agentenv_session"
18
20
  SESSION_SECRET_ENV = "WEBSITE_SESSION_SECRET"
19
21
  SESSION_TTL_S = 7 * 24 * 3600
20
22
  SECRET_BYTES = 32
23
+ _NOT_TOKEN = re.compile(r"[^A-Za-z0-9_.-]")
24
+
25
+
26
+ def _cookie_suffix(environment_name: str) -> str:
27
+ """The part of a site's cookie names that is its own: the name itself when a cookie name can hold each of its
28
+ characters, else the name with each such character made ``_`` and ``~`` and a hash of the whole name appended. No
29
+ name kept as itself holds a ``~``, so two distinct names never make one suffix."""
30
+ if not _NOT_TOKEN.search(environment_name):
31
+ return environment_name
32
+ digest = hashlib.sha256(environment_name.encode("utf-8", "surrogatepass")).hexdigest()[:12]
33
+ return f"{_NOT_TOKEN.sub('_', environment_name)}~{digest}"
34
+
35
+
36
+ def session_cookie_name(environment_name: str | None) -> str:
37
+ """The session cookie of the site named ``environment_name``: ``agentenv_session_<name>``, a name a cookie name
38
+ can't hold spelled as ``_cookie_suffix`` spells it; the shared ``agentenv_session`` for a site with no name."""
39
+ return f"{SESSION_COOKIE}_{_cookie_suffix(environment_name)}" if environment_name else SESSION_COOKIE
21
40
 
22
41
 
23
42
  class SessionSigner:
24
- def __init__(self, secret: str | bytes | None = None) -> None:
43
+ def __init__(self, secret: str | bytes | None = None, cookie_name: str = SESSION_COOKIE) -> None:
25
44
  secret = secret if secret is not None else os.environ.get(SESSION_SECRET_ENV)
26
45
  self._key = (secret.encode() if isinstance(secret, str) else secret) or secrets.token_bytes(SECRET_BYTES)
46
+ self.cookie_name = cookie_name
27
47
 
28
48
  def encode(self, user: str | None, ttl_s: int = SESSION_TTL_S) -> str:
29
49
  """The cookie value for ``user`` signed in, or signed out when None, good for ``ttl_s`` seconds."""
@@ -45,10 +65,14 @@ class SessionSigner:
45
65
  return None
46
66
  return {"user": data.get("u")}
47
67
 
68
+ def read(self, cookies: dict) -> dict | None:
69
+ """The session of a request's ``cookies``, from the site's own cookie."""
70
+ return self.decode(cookies.get(self.cookie_name))
71
+
48
72
  def cookie(self, user: str | None, ttl_s: int = SESSION_TTL_S) -> dict:
49
73
  """The cookie a browser adds to be signed in as ``user``, in browser/v1's shape with ``max_age`` for its
50
74
  lifetime; its domain is left to whoever adds it."""
51
- return {"name": SESSION_COOKIE, "value": self.encode(user, ttl_s), "path": "/", "max_age": ttl_s,
75
+ return {"name": self.cookie_name, "value": self.encode(user, ttl_s), "path": "/", "max_age": ttl_s,
52
76
  "httpOnly": True, "sameSite": "Lax"}
53
77
 
54
78
  def _sign(self, payload: str) -> str: