agentenv-framework-protocol 0.1.466__tar.gz → 0.1.468__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.466 → agentenv_framework_protocol-0.1.468}/PKG-INFO +26 -1
  2. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/README.md +25 -0
  3. agentenv_framework_protocol-0.1.468/examples/website/notes/.gitignore +2 -0
  4. agentenv_framework_protocol-0.1.468/examples/website/notes/Dockerfile +21 -0
  5. agentenv_framework_protocol-0.1.468/examples/website/notes/Dockerfile.dockerignore +6 -0
  6. agentenv_framework_protocol-0.1.468/examples/website/notes/README.md +87 -0
  7. agentenv_framework_protocol-0.1.468/examples/website/notes/data/seed.json +15 -0
  8. agentenv_framework_protocol-0.1.468/examples/website/notes/env.toml +3 -0
  9. agentenv_framework_protocol-0.1.468/examples/website/notes/site.py +208 -0
  10. agentenv_framework_protocol-0.1.468/examples/website/notes/web/index.html +37 -0
  11. agentenv_framework_protocol-0.1.468/examples/website/notes/web/package-lock.json +1020 -0
  12. agentenv_framework_protocol-0.1.468/examples/website/notes/web/package.json +14 -0
  13. agentenv_framework_protocol-0.1.468/examples/website/notes/web/src/main.js +252 -0
  14. agentenv_framework_protocol-0.1.468/examples/website/notes/web/src/style.css +57 -0
  15. agentenv_framework_protocol-0.1.468/examples/website/notes/web/vite.config.js +10 -0
  16. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/pyproject.toml +1 -1
  17. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/computer/app_runtime.py +14 -4
  18. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/computer/apps.py +7 -4
  19. agentenv_framework_protocol-0.1.468/src/agentenv_protocol/website_browser/__init__.py +23 -0
  20. agentenv_framework_protocol-0.1.468/src/agentenv_protocol/website_browser/control.py +649 -0
  21. agentenv_framework_protocol-0.1.468/src/agentenv_protocol/website_browser/devtools.py +121 -0
  22. agentenv_framework_protocol-0.1.468/src/agentenv_protocol/website_browser/environment.py +490 -0
  23. agentenv_framework_protocol-0.1.468/src/agentenv_protocol/website_browser/shapes.py +161 -0
  24. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/tests/test_browser_cdp.py +1 -1
  25. agentenv_framework_protocol-0.1.468/tests/test_browser_state.py +314 -0
  26. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/tests/test_computer_app_server.py +19 -0
  27. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/tests/test_website_browser.py +2 -2
  28. agentenv_framework_protocol-0.1.468/tests/test_website_notes_example.py +278 -0
  29. agentenv_framework_protocol-0.1.466/src/agentenv_protocol/website_browser.py +0 -984
  30. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/.gitignore +0 -0
  31. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/LICENSE +0 -0
  32. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/NOTICE +0 -0
  33. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/THIRD_PARTY_NOTICES.md +0 -0
  34. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/examples/__init__.py +0 -0
  35. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/examples/advanced_agent.py +0 -0
  36. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/examples/basic_agent.py +0 -0
  37. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/examples/custom_extensions_agent.py +0 -0
  38. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/examples/multimodal_agent.py +0 -0
  39. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/examples/reference_model.py +0 -0
  40. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/examples/streaming_agent.py +0 -0
  41. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/__init__.py +0 -0
  42. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/a2a_agent/__init__.py +0 -0
  43. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/a2a_agent/_trajectory_log.py +0 -0
  44. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/a2a_agent/_triggers.py +0 -0
  45. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/a2a_agent/extensions.py +0 -0
  46. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/a2a_agent/framework.py +0 -0
  47. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/a2a_agent/peer_agents.py +0 -0
  48. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/a2a_agent/proxy.py +0 -0
  49. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/a2a_agent/registry.py +0 -0
  50. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/a2a_agent/staging.py +0 -0
  51. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/a2a_agent/tasks/__init__.py +0 -0
  52. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/a2a_agent/tasks/v1.py +0 -0
  53. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/agent_env_environment.py +0 -0
  54. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/browser_cdp.py +0 -0
  55. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/client.py +0 -0
  56. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/computer/__init__.py +0 -0
  57. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/computer/_declare.py +0 -0
  58. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/computer/_linux.py +0 -0
  59. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/computer/_load.py +0 -0
  60. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/computer/_macos.py +0 -0
  61. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/computer/_with_fds.py +0 -0
  62. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/computer/app.py +0 -0
  63. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/computer/app_data.py +0 -0
  64. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/computer/card.py +0 -0
  65. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/computer/checks.py +0 -0
  66. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/computer/conformance/__init__.py +0 -0
  67. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/computer/conformance/__main__.py +0 -0
  68. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/computer/conformance/checks.py +0 -0
  69. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/computer/conformance/client.py +0 -0
  70. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/computer/conformance/fixture.py +0 -0
  71. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/computer/conformance/mp4.py +0 -0
  72. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/computer/conformance/page_fixture.py +0 -0
  73. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/computer/conformance/report.py +0 -0
  74. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/computer/dataplane.py +0 -0
  75. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/computer/driver.py +0 -0
  76. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/computer/errors.py +0 -0
  77. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/computer/extensions.py +0 -0
  78. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/computer/frames.py +0 -0
  79. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/computer/keys.py +0 -0
  80. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/computer/launch.py +0 -0
  81. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/computer/manifest.py +0 -0
  82. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/computer/operations.py +0 -0
  83. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/computer/platform.py +0 -0
  84. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/computer/queue.py +0 -0
  85. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/computer/recording.py +0 -0
  86. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/computer/results.py +0 -0
  87. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/computer/run_install_step.py +0 -0
  88. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/computer/runtime.py +0 -0
  89. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/computer/seed.py +0 -0
  90. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/computer/serve.py +0 -0
  91. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/computer/serve_app.py +0 -0
  92. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/computer/session.py +0 -0
  93. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/computer/settings.py +0 -0
  94. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/computer/skill.py +0 -0
  95. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/computer/spaces.py +0 -0
  96. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/computer/svc.py +0 -0
  97. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/computer/tools.py +0 -0
  98. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/errors.py +0 -0
  99. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/manifest.py +0 -0
  100. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/preflight.py +0 -0
  101. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/tool_access.py +0 -0
  102. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/tool_client.py +0 -0
  103. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/tool_context.py +0 -0
  104. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/transfers.py +0 -0
  105. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/types.py +0 -0
  106. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/website/__init__.py +0 -0
  107. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/website/clock.py +0 -0
  108. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/website/conformance.py +0 -0
  109. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/website/environment.py +0 -0
  110. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/website/errors.py +0 -0
  111. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/website/readiness.py +0 -0
  112. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/website/routes.py +0 -0
  113. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/src/agentenv_protocol/website/session.py +0 -0
  114. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/tests/computer_fake.py +0 -0
  115. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/tests/probe_fake.py +0 -0
  116. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/tests/test_a2a_agent.py +0 -0
  117. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/tests/test_a2a_agent_examples.py +0 -0
  118. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/tests/test_a2a_cdp_config.py +0 -0
  119. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/tests/test_a2a_object_transfers.py +0 -0
  120. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/tests/test_a2a_peer_agents.py +0 -0
  121. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/tests/test_a2a_proxy.py +0 -0
  122. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/tests/test_a2a_staging.py +0 -0
  123. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/tests/test_a2a_trajectory_log.py +0 -0
  124. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/tests/test_agent_env_environment.py +0 -0
  125. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/tests/test_client.py +0 -0
  126. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/tests/test_computer_app_data.py +0 -0
  127. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/tests/test_computer_conformance.py +0 -0
  128. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/tests/test_computer_conformance_checks.py +0 -0
  129. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/tests/test_computer_conformance_page.py +0 -0
  130. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/tests/test_computer_conformance_recording.py +0 -0
  131. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/tests/test_computer_dataplane.py +0 -0
  132. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/tests/test_computer_declare.py +0 -0
  133. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/tests/test_computer_driver.py +0 -0
  134. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/tests/test_computer_extensions.py +0 -0
  135. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/tests/test_computer_frames.py +0 -0
  136. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/tests/test_computer_macos.py +0 -0
  137. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/tests/test_computer_page.py +0 -0
  138. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/tests/test_computer_queue.py +0 -0
  139. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/tests/test_computer_recording.py +0 -0
  140. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/tests/test_computer_seed.py +0 -0
  141. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/tests/test_computer_serve.py +0 -0
  142. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/tests/test_computer_spaces.py +0 -0
  143. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/tests/test_computer_svc_forward.py +0 -0
  144. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/tests/test_control_token.py +0 -0
  145. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/tests/test_data_objects.py +0 -0
  146. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/tests/test_errors.py +0 -0
  147. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/tests/test_extension_methods.py +0 -0
  148. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/tests/test_intake.py +0 -0
  149. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/tests/test_manifest.py +0 -0
  150. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/tests/test_preflight.py +0 -0
  151. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/tests/test_tool_access.py +0 -0
  152. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/tests/test_tool_annotations.py +0 -0
  153. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/tests/test_tool_client.py +0 -0
  154. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/tests/test_tool_context.py +0 -0
  155. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/tests/test_tool_context_fastmcp.py +0 -0
  156. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/tests/test_tool_context_injection.py +0 -0
  157. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/tests/test_website_api.py +0 -0
  158. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/tests/test_website_conformance.py +0 -0
  159. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/tests/test_website_environment.py +0 -0
  160. {agentenv_framework_protocol-0.1.466 → agentenv_framework_protocol-0.1.468}/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.466
3
+ Version: 0.1.468
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
@@ -969,6 +969,31 @@ are the profile's template. The base class adds:
969
969
  - `files/choose {paths}`, files in `~/Uploads` for the file chooser open on it, which headless, and so
970
970
  alone, it intercepts;
971
971
  - `page/state`, the active tab's id, URL and title and the dialog or file chooser open on it.
972
+ - `cookies/get {urls?}`, `cookies/set {cookies}` and `cookies/delete {name?, domain?, path?}`, on its default
973
+ context, each cookie as Playwright's `storageState` has it, CDP's `Network.Cookie` fields (`name`, `value`,
974
+ `domain`, `path`, `expires`, `httpOnly`, `secure`, `sameSite`), so a storage state exported from Playwright loads
975
+ as is, and a partitioned cookie's `partitionKey` (`{topLevelSite, hasCrossSiteAncestor}`), without which it would
976
+ be another cookie. `get` returns the cookies a request to any of `urls` would send, as Playwright picks them, else
977
+ every one; `set` takes a `url`, or a `domain` and a `path` (`/` by default), and may carry `max_age` in place of
978
+ `expires`: seconds from when it's set, on the computer's clock, which the browser's cookies are judged by. It sets
979
+ them all at once, so a cookie the browser refuses leaves none set. `delete` removes every cookie with all the
980
+ fields it's given, every cookie with none, partitioned or not, and says how many. A cookie refused names its
981
+ fields, never its value;
982
+ - `local_storage/get {origins?}`, `local_storage/set {origins}` and `local_storage/clear {origins?}`, each origin
983
+ as `storageState` has it, `{origin, localStorage: [{name, value}]}`. CDP reaches an origin's local storage only
984
+ through a page of it, so each goes through a hidden page agentenv sends to the origin and serves itself, past any
985
+ service worker of the origin: the site gets no request and runs no script. The origins answered are as the browser
986
+ names them (`https://xn--bcher-kva.de` for `https://bücher.de`). With no `origins`, every origin a page or a
987
+ same-site frame loaded since its server started, and those agentenv set. A cross-site iframe's local storage is
988
+ partitioned under the site that embeds it, and isn't read, as Playwright's `storageState` doesn't read it either.
989
+ Agents' CDP connections see each hidden page come and go as a target of type `other`;
990
+ - `downloads/list` and `downloads/get {guid, write_namespace?}`, a download and, completed, its file: uploaded
991
+ under the `write_namespace` grant when there is one, else inline, as `data/get` returns files. Inline, the whole
992
+ file is read into memory and sent base64 in one answer, so a large one belongs under a grant.
993
+
994
+ The record's entries are the models in `agentenv_protocol.website_browser` (`OpenTab`, with the tab that opened
995
+ it as `opener`; `ClosedTab`; `Download`; `Dialog`; `LaunchEntry`; `BrowserRecord`), every field present and null
996
+ until it's known.
972
997
  - `cdp {user}`, `user`'s CDP address once the browser is ready, `http_url` to discover from and
973
998
  `ws_url` to connect to, both at the address the call reached it at with a token agentenv mints in
974
999
  the path: the same one each time it's asked for. Through the driver they're under
@@ -907,6 +907,31 @@ are the profile's template. The base class adds:
907
907
  - `files/choose {paths}`, files in `~/Uploads` for the file chooser open on it, which headless, and so
908
908
  alone, it intercepts;
909
909
  - `page/state`, the active tab's id, URL and title and the dialog or file chooser open on it.
910
+ - `cookies/get {urls?}`, `cookies/set {cookies}` and `cookies/delete {name?, domain?, path?}`, on its default
911
+ context, each cookie as Playwright's `storageState` has it, CDP's `Network.Cookie` fields (`name`, `value`,
912
+ `domain`, `path`, `expires`, `httpOnly`, `secure`, `sameSite`), so a storage state exported from Playwright loads
913
+ as is, and a partitioned cookie's `partitionKey` (`{topLevelSite, hasCrossSiteAncestor}`), without which it would
914
+ be another cookie. `get` returns the cookies a request to any of `urls` would send, as Playwright picks them, else
915
+ every one; `set` takes a `url`, or a `domain` and a `path` (`/` by default), and may carry `max_age` in place of
916
+ `expires`: seconds from when it's set, on the computer's clock, which the browser's cookies are judged by. It sets
917
+ them all at once, so a cookie the browser refuses leaves none set. `delete` removes every cookie with all the
918
+ fields it's given, every cookie with none, partitioned or not, and says how many. A cookie refused names its
919
+ fields, never its value;
920
+ - `local_storage/get {origins?}`, `local_storage/set {origins}` and `local_storage/clear {origins?}`, each origin
921
+ as `storageState` has it, `{origin, localStorage: [{name, value}]}`. CDP reaches an origin's local storage only
922
+ through a page of it, so each goes through a hidden page agentenv sends to the origin and serves itself, past any
923
+ service worker of the origin: the site gets no request and runs no script. The origins answered are as the browser
924
+ names them (`https://xn--bcher-kva.de` for `https://bücher.de`). With no `origins`, every origin a page or a
925
+ same-site frame loaded since its server started, and those agentenv set. A cross-site iframe's local storage is
926
+ partitioned under the site that embeds it, and isn't read, as Playwright's `storageState` doesn't read it either.
927
+ Agents' CDP connections see each hidden page come and go as a target of type `other`;
928
+ - `downloads/list` and `downloads/get {guid, write_namespace?}`, a download and, completed, its file: uploaded
929
+ under the `write_namespace` grant when there is one, else inline, as `data/get` returns files. Inline, the whole
930
+ file is read into memory and sent base64 in one answer, so a large one belongs under a grant.
931
+
932
+ The record's entries are the models in `agentenv_protocol.website_browser` (`OpenTab`, with the tab that opened
933
+ it as `opener`; `ClosedTab`; `Download`; `Dialog`; `LaunchEntry`; `BrowserRecord`), every field present and null
934
+ until it's known.
910
935
  - `cdp {user}`, `user`'s CDP address once the browser is ready, `http_url` to discover from and
911
936
  `ws_url` to connect to, both at the address the call reached it at with a token agentenv mints in
912
937
  the path: the same one each time it's asked for. Through the driver they're under
@@ -0,0 +1,2 @@
1
+ web/node_modules/
2
+ web/dist/
@@ -0,0 +1,21 @@
1
+ FROM mirror.gcr.io/library/node:22-slim@sha256:4b150b48b4116e2a22f2c39cc27025fbf4f153e2078fd3b14d159fdb5c7279da AS web
2
+ WORKDIR /web
3
+ COPY web/package.json web/package-lock.json ./
4
+ RUN npm ci --no-audit --no-fund
5
+ COPY web/ ./
6
+ RUN npm run build
7
+
8
+ FROM mirror.gcr.io/library/python:3.12-slim@sha256:423ed6ab25b1921a477529254bfeeabf5855151dc2c3141699a1bfc852199fbf
9
+ WORKDIR /app
10
+ RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/*
11
+ RUN pip install --no-cache-dir "agentenv-framework-protocol>=0.1.466" "uvicorn>=0.30"
12
+ COPY site.py /app/site.py
13
+ COPY data /app/data
14
+ COPY --from=web /web/dist /app/web/dist
15
+ ENV PORT=8000
16
+ EXPOSE 8000
17
+ RUN useradd --create-home --uid 10001 appuser
18
+ USER appuser
19
+ HEALTHCHECK --interval=10s --timeout=3s --start-period=5s --retries=3 \
20
+ CMD python -c "import os, urllib.request; urllib.request.urlopen('http://127.0.0.1:%s/.well-known/agent-env.json' % os.environ.get('PORT', '8000'), timeout=2)"
21
+ CMD ["python", "site.py"]
@@ -0,0 +1,6 @@
1
+ **/node_modules
2
+ **/dist
3
+ **/__pycache__
4
+ **/.pytest_cache
5
+ **/.venv
6
+ .git
@@ -0,0 +1,87 @@
1
+ # Notes: a reference website/v1 site
2
+
3
+ Notes is a small notes app built on `agentenv_protocol.website.WebsiteEnvironment`. It is the reference site for the
4
+ `urn:agentenv:website/v1` server contract. Each user keeps their own notes and can create, edit, mark done and delete
5
+ them. The page shows the universe's time.
6
+
7
+ | File | What it is |
8
+ | --- | --- |
9
+ | `site.py` | The server: the `Notes` WebsiteEnvironment, its `@api` routes, and `serve()` on `PORT` (8000). |
10
+ | `data/seed.json` | The seed data: two users (`ann`, `ben`), their notes, and `current_user`, the acting user. |
11
+ | `web/` | A minimal Vite app (`index.html`, `src/main.js`, `src/style.css`). Its assets (`base: "./"`) and API calls are relative to the page, so it works at `/` and under the gateway's `/website/notes/` alike; opened at a path without its final slash, such as `/website/notes` or `/website/notes/index.html`, the page first reloads itself at its folder's address. |
12
+ | `Dockerfile` | The server image, built with this folder as its context: Node builds `web/`, then Python installs the released protocol (`agentenv-framework-protocol`) with uvicorn. It installs `curl`, which the gateway's compose health check runs against the card. |
13
+ | `env.toml` | The website env as a bundle entry. |
14
+
15
+ ## What it exercises
16
+
17
+ - **Site declaration.** `@website(domains=["notes.test"], static="web/dist")`. The card advertises website/v1 with
18
+ the domains and every route by name.
19
+ - **Routes.** `list_users`, `sign_in`, `list_notes` (with an optional `?done=`), `create_note` (201), `get_note`,
20
+ `edit_note`, `toggle_note` and `delete_note`. Errors are raised as `NotFound`, `BadRequest` and `Unauthorized`.
21
+ - **Roles.** `all_notes` (`GET /api/admin/notes`) is declared with `roles=["admin"]`. It answers only a caller that
22
+ sends `AgentEnv-Role: admin` with `AgentEnv-Role-Key` equal to the container's `WEBSITE_ROLE_KEY`; any other caller
23
+ gets 403. The gateway passes on whatever role a browser sends, so without the key a page could claim `admin`; with no
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.
28
+ - **State.** The site keeps its users and notes itself, starting from `data/seed.json`, and serves them through its
29
+ 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
+ exports them, and `data/reset` empties them. `data/get` is how agentenv reads what a task left the site with.
31
+ - **Time.** Notes are stamped with `self.now()`, which follows the gateway's clock once `clock/v1`'s `sync_time` names
32
+ it. The frontend reads `X-Universe-Now` from every API response into `universeNow()` and shows it in the header.
33
+ It uses this helper rather than `clock_patch=True`, so the page works the same against a server written without the
34
+ SDK.
35
+ - **Readiness.** website/v1's `ready`, through the card, says not ready (`@ready_check seeded`) until the site has
36
+ users, for example after a `data/reset` and before the next `data/add`. The site serves no health path.
37
+
38
+ ## Run it
39
+
40
+ From this folder:
41
+
42
+ ```bash
43
+ cd web && npm ci && npm run build && cd ..
44
+ PYTHONPATH=<repo>/packages/agentenv-protocol/src python site.py
45
+ ```
46
+
47
+ Open http://localhost:8000. To work on the frontend, run `npm run dev` in `web/` while `site.py` runs; Vite proxies
48
+ `/api` to port 8000.
49
+
50
+ ## Test it
51
+
52
+ ```bash
53
+ python -m pytest packages/agentenv-protocol/tests/test_website_notes_example.py
54
+ ```
55
+
56
+ The test imports `site.py`, runs the conformance checker, and checks sign-in, per-user notes, the admin-only route,
57
+ clock-synced timestamps, and the data plane's reset and seed load. It serves a stand-in page, so it needs no Vite
58
+ build.
59
+
60
+ ## Build and verify the image
61
+
62
+ The build context is this folder; the image installs the released protocol, so it runs the SDK as published, while
63
+ the test above runs this repo's.
64
+
65
+ ```bash
66
+ docker build -t notes-site .
67
+ 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
69
+ curl -s http://localhost:18080/api/notes
70
+ ```
71
+
72
+ Use `--clock-host` so the container can reach the checker's clock. The clock check leaves the server following a
73
+ clock that is gone once the check ends: later timestamps start from 2031 until the server is synced again, so restart
74
+ the container before using it.
75
+
76
+ ## Register it
77
+
78
+ In a bundle, `env.toml` declares the env: copied to a bundle's `envs/notes/`, `agent-env bundle register <bundle>`
79
+ builds the image with that folder as its context. Its keys are `type`, `environment_name` and `backend_image`; domains
80
+ are not one of them, as they come from the card. Without a bundle:
81
+
82
+ ```bash
83
+ agent-env env website put --id notes-site --server-dockerfile Dockerfile --server-docker-context .
84
+ ```
85
+
86
+ Run it from this folder. `environment_name` comes from the card (`@environment_card(name="notes")`). The stored env
87
+ has a server image and no frontend image. Pass `--skip-validation` to register without deploying.
@@ -0,0 +1,15 @@
1
+ {
2
+ "current_user": "ann",
3
+ "users": [
4
+ {"id": "ann", "name": "Ann Lee"},
5
+ {"id": "ben", "name": "Ben Ortiz"}
6
+ ],
7
+ "notes": [
8
+ {"id": 1, "owner": "ann", "title": "Buy milk", "body": "Oat, two cartons.", "done": false,
9
+ "created_at": "2026-01-05T09:00:00.000Z", "updated_at": "2026-01-05T09:00:00.000Z"},
10
+ {"id": 2, "owner": "ann", "title": "Book dentist", "body": "", "done": true,
11
+ "created_at": "2026-01-04T15:30:00.000Z", "updated_at": "2026-01-06T08:10:00.000Z"},
12
+ {"id": 3, "owner": "ben", "title": "Draft the quarterly report", "body": "Due Friday.", "done": false,
13
+ "created_at": "2026-01-03T11:00:00.000Z", "updated_at": "2026-01-03T11:00:00.000Z"}
14
+ ]
15
+ }
@@ -0,0 +1,3 @@
1
+ type = "website"
2
+ environment_name = "notes"
3
+ backend_image = { dockerfile = "Dockerfile" }
@@ -0,0 +1,208 @@
1
+ """Notes: the reference site of ``urn:agentenv:website/v1``, a small notes app on WebsiteEnvironment. Each user keeps
2
+ notes they create, edit, mark done and delete; a browser with no session is signed in as the acting user, the seed's
3
+ ``current_user``, and can sign in as another seeded user. The site keeps its users and notes itself, behind one lock
4
+ since plain ``def`` handlers run in worker threads, starting from ``data/seed.json``, and serves them through the data plane (``data/get``, ``data/reset``, ``data/add``), as any
5
+ AgentEnvEnvironment does; it stamps each note with the universe's time. ``admin`` callers, as the gateway's
6
+ ``AgentEnv-Role`` names them, can list every user's notes.
7
+
8
+ Run it with ``python site.py`` after building ``web/`` into ``web/dist``; it serves on ``PORT`` (8000)."""
9
+ from __future__ import annotations
10
+
11
+ import base64
12
+ import copy
13
+ import json
14
+ import threading
15
+ from pathlib import Path
16
+ from typing import Optional
17
+ from urllib.parse import unquote, urlparse
18
+
19
+ from pydantic import BaseModel
20
+
21
+ from agentenv_protocol import DataPart, FilePart, TextPart, add_data, environment_card, get_data, reset_data
22
+ from agentenv_protocol.website import (
23
+ ApiContext,
24
+ BadRequest,
25
+ NotFound,
26
+ Unauthorized,
27
+ WebsiteEnvironment,
28
+ api,
29
+ ready_check,
30
+ website,
31
+ )
32
+ from agentenv_protocol.website.clock import iso
33
+
34
+ NOTES = "notes"
35
+ USERS = "users"
36
+ SEED = Path(__file__).resolve().parent / "data" / "seed.json"
37
+
38
+
39
+ class NewNote(BaseModel):
40
+ title: str
41
+ body: str = ""
42
+
43
+
44
+ class NoteEdit(BaseModel):
45
+ title: Optional[str] = None
46
+ body: Optional[str] = None
47
+ done: Optional[bool] = None
48
+
49
+
50
+ class SignIn(BaseModel):
51
+ user: str
52
+
53
+
54
+ @environment_card(name="notes")
55
+ @website(domains=["notes.test"], static="web/dist")
56
+ class Notes(WebsiteEnvironment):
57
+ def __init__(self) -> None:
58
+ super().__init__()
59
+ self._lock = threading.Lock()
60
+ self.data: dict = json.loads(SEED.read_text(encoding="utf-8"))
61
+
62
+ def acting_user(self) -> Optional[str]:
63
+ with self._lock:
64
+ return self.data.get("current_user")
65
+
66
+ @ready_check
67
+ def seeded(self) -> bool:
68
+ """The site is usable once it knows who may sign in."""
69
+ with self._lock:
70
+ return bool(self.data.get(USERS))
71
+
72
+ @reset_data
73
+ def reset(self) -> None:
74
+ with self._lock:
75
+ self.data = {}
76
+
77
+ @add_data
78
+ def add(self, parts: list) -> None:
79
+ """Each part's JSON, its collections' records added by id and its other fields set, as the seed is."""
80
+ loaded = [_part_data(part) for part in parts]
81
+ with self._lock:
82
+ for data in loaded:
83
+ for key, value in data.items():
84
+ if isinstance(value, list):
85
+ kept = {record["id"]: record for record in self.data.get(key, [])}
86
+ kept.update({record["id"]: copy.deepcopy(record) for record in value})
87
+ self.data[key] = list(kept.values())
88
+ else:
89
+ self.data[key] = copy.deepcopy(value)
90
+
91
+ @get_data
92
+ def export(self) -> list:
93
+ with self._lock:
94
+ return [DataPart(data=copy.deepcopy(self.data))]
95
+
96
+ @api.get("/api/users", name="list_users")
97
+ def list_users(self) -> list:
98
+ """The users a browser can sign in as."""
99
+ with self._lock:
100
+ return copy.deepcopy(self.data.get(USERS, []))
101
+
102
+ @api.post("/api/sign-in", name="sign_in")
103
+ def sign_in(self, body: SignIn, ctx: ApiContext) -> dict:
104
+ """Sign the browser in as one of the site's users."""
105
+ with self._lock:
106
+ known = any(user["id"] == body.user for user in self.data.get(USERS, []))
107
+ if not known:
108
+ raise NotFound(f"no user {body.user!r}")
109
+ ctx.sign_in(body.user)
110
+ return {"user": ctx.user}
111
+
112
+ @api.get("/api/notes", name="list_notes")
113
+ def list_notes(self, ctx: ApiContext, done: Optional[bool] = None) -> list:
114
+ """The signed-in user's notes, newest first; with ``done``, only those done or not."""
115
+ user = self._user(ctx)
116
+ with self._lock:
117
+ mine = [copy.deepcopy(n) for n in self.data.get(NOTES, [])
118
+ if n["owner"] == user and (done is None or n["done"] == done)]
119
+ return sorted(mine, key=lambda note: note["created_at"], reverse=True)
120
+
121
+ @api.post("/api/notes", name="create_note", status=201)
122
+ def create_note(self, note: NewNote, ctx: ApiContext) -> dict:
123
+ """Add a note for the signed-in user, not done, created and updated now."""
124
+ owner, title, now = self._user(ctx), _title(note.title), iso(self.now())
125
+ with self._lock:
126
+ notes = self.data.setdefault(NOTES, [])
127
+ created = {"id": max((n["id"] for n in notes), default=0) + 1, "owner": owner, "title": title,
128
+ "body": note.body, "done": False, "created_at": now, "updated_at": now}
129
+ notes.append(created)
130
+ return copy.deepcopy(created)
131
+
132
+ @api.get("/api/notes/{note_id:int}", name="get_note")
133
+ def get_note(self, note_id: int, ctx: ApiContext) -> dict:
134
+ with self._lock:
135
+ return copy.deepcopy(self._note(note_id, ctx))
136
+
137
+ @api.patch("/api/notes/{note_id:int}", name="edit_note")
138
+ def edit_note(self, note_id: int, edit: NoteEdit, ctx: ApiContext) -> dict:
139
+ """Change a note's title, body or done, and its ``updated_at``."""
140
+ fields = edit.model_dump(exclude_none=True)
141
+ if "title" in fields:
142
+ fields["title"] = _title(fields["title"])
143
+ now = iso(self.now())
144
+ with self._lock:
145
+ note = self._note(note_id, ctx)
146
+ note.update(fields, updated_at=now)
147
+ return copy.deepcopy(note)
148
+
149
+ @api.post("/api/notes/{note_id:int}/toggle", name="toggle_note")
150
+ def toggle_note(self, note_id: int, ctx: ApiContext) -> dict:
151
+ """Mark a note done, or not done when it was."""
152
+ now = iso(self.now())
153
+ with self._lock:
154
+ note = self._note(note_id, ctx)
155
+ note.update(done=not note["done"], updated_at=now)
156
+ return copy.deepcopy(note)
157
+
158
+ @api.delete("/api/notes/{note_id:int}", name="delete_note")
159
+ def delete_note(self, note_id: int, ctx: ApiContext) -> dict:
160
+ with self._lock:
161
+ note = self._note(note_id, ctx)
162
+ self.data[NOTES] = [n for n in self.data[NOTES] if n is not note]
163
+ return {"deleted": note_id}
164
+
165
+ @api.get("/api/admin/notes", name="all_notes", roles=["admin"])
166
+ def all_notes(self) -> list:
167
+ """Every user's notes, for an ``admin`` caller only."""
168
+ with self._lock:
169
+ return copy.deepcopy(self.data.get(NOTES, []))
170
+
171
+ def _user(self, ctx: ApiContext) -> str:
172
+ if ctx.user is None:
173
+ raise Unauthorized("sign in first")
174
+ return ctx.user
175
+
176
+ def _note(self, note_id: int, ctx: ApiContext) -> dict:
177
+ """The signed-in user's note ``note_id``, read under the lock; another user's is as missing as one that
178
+ doesn't exist."""
179
+ note = next((n for n in self.data.get(NOTES, []) if n["id"] == note_id), None)
180
+ if note is None or note["owner"] != self._user(ctx):
181
+ raise NotFound(f"no note {note_id}")
182
+ return note
183
+
184
+
185
+ def _part_data(part) -> dict:
186
+ """A part's JSON: a data part's data, a text part's text, or a file part's bytes or ``file://`` file."""
187
+ if isinstance(part, DataPart):
188
+ return part.data
189
+ if isinstance(part, TextPart):
190
+ return json.loads(part.text)
191
+ if isinstance(part, FilePart):
192
+ if getattr(part.file, "bytes", None) is not None:
193
+ return json.loads(base64.b64decode(part.file.bytes))
194
+ uri = urlparse(part.file.uri)
195
+ if uri.scheme == "file":
196
+ return json.loads(Path(unquote(uri.path)).read_text(encoding="utf-8"))
197
+ raise ValueError(f"Notes reads a file part by its bytes or a file:// URI, not {part.file.uri!r}")
198
+ raise ValueError(f"Notes can't read a {type(part).__name__}")
199
+
200
+
201
+ def _title(title: str) -> str:
202
+ if not title.strip():
203
+ raise BadRequest("a note needs a title", code="empty_title")
204
+ return title.strip()
205
+
206
+
207
+ if __name__ == "__main__":
208
+ Notes().serve()
@@ -0,0 +1,37 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Notes</title>
7
+ <script>
8
+ if (!location.pathname.endsWith("/")) { const url = new URL(location.href); url.pathname = url.pathname.replace(/\/index\.html$/, "") + "/"; location.replace(url.href); }
9
+ </script>
10
+ </head>
11
+ <body>
12
+ <header class="bar">
13
+ <h1>Notes</h1>
14
+ <div class="who">
15
+ <label>Signed in as
16
+ <select id="user" aria-label="Signed-in user"></select>
17
+ </label>
18
+ <time id="universe-now" title="The universe's time, from X-Universe-Now"></time>
19
+ </div>
20
+ </header>
21
+ <main>
22
+ <form id="new-note" class="card">
23
+ <input id="new-title" name="title" placeholder="New note title" autocomplete="off" required />
24
+ <textarea id="new-body" name="body" placeholder="Details (optional)" rows="2"></textarea>
25
+ <button type="submit">Add note</button>
26
+ </form>
27
+ <nav class="filters" aria-label="Filter notes">
28
+ <button data-filter="all" class="active">All</button>
29
+ <button data-filter="open">Open</button>
30
+ <button data-filter="done">Done</button>
31
+ </nav>
32
+ <p id="error" role="alert" hidden></p>
33
+ <ul id="notes"></ul>
34
+ </main>
35
+ <script type="module" src="/src/main.js"></script>
36
+ </body>
37
+ </html>