agentenv-framework-protocol 0.1.467__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.
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/PKG-INFO +26 -1
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/README.md +25 -0
- agentenv_framework_protocol-0.1.469/examples/website/notes/.gitignore +2 -0
- agentenv_framework_protocol-0.1.469/examples/website/notes/Dockerfile +21 -0
- agentenv_framework_protocol-0.1.469/examples/website/notes/Dockerfile.dockerignore +6 -0
- agentenv_framework_protocol-0.1.469/examples/website/notes/README.md +91 -0
- agentenv_framework_protocol-0.1.469/examples/website/notes/data/seed.json +15 -0
- agentenv_framework_protocol-0.1.469/examples/website/notes/env.toml +3 -0
- agentenv_framework_protocol-0.1.469/examples/website/notes/site.py +208 -0
- agentenv_framework_protocol-0.1.469/examples/website/notes/web/index.html +37 -0
- agentenv_framework_protocol-0.1.469/examples/website/notes/web/package-lock.json +1020 -0
- agentenv_framework_protocol-0.1.469/examples/website/notes/web/package.json +14 -0
- agentenv_framework_protocol-0.1.469/examples/website/notes/web/src/main.js +252 -0
- agentenv_framework_protocol-0.1.469/examples/website/notes/web/src/style.css +57 -0
- agentenv_framework_protocol-0.1.469/examples/website/notes/web/vite.config.js +10 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/pyproject.toml +1 -1
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/website/conformance.py +161 -26
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/website/environment.py +30 -6
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/website/routes.py +3 -2
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/website/session.py +27 -3
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/tests/test_website_conformance.py +79 -18
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/tests/test_website_environment.py +44 -6
- agentenv_framework_protocol-0.1.469/tests/test_website_mcp.py +234 -0
- agentenv_framework_protocol-0.1.469/tests/test_website_notes_example.py +280 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/.gitignore +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/LICENSE +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/NOTICE +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/THIRD_PARTY_NOTICES.md +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/examples/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/examples/advanced_agent.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/examples/basic_agent.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/examples/custom_extensions_agent.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/examples/multimodal_agent.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/examples/reference_model.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/examples/streaming_agent.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/a2a_agent/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/a2a_agent/_trajectory_log.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/a2a_agent/_triggers.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/a2a_agent/extensions.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/a2a_agent/framework.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/a2a_agent/peer_agents.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/a2a_agent/proxy.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/a2a_agent/registry.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/a2a_agent/staging.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/a2a_agent/tasks/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/a2a_agent/tasks/v1.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/agent_env_environment.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/browser_cdp.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/client.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/_declare.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/_linux.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/_load.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/_macos.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/_with_fds.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/app.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/app_data.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/app_runtime.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/apps.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/card.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/checks.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/conformance/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/conformance/__main__.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/conformance/checks.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/conformance/client.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/conformance/fixture.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/conformance/mp4.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/conformance/page_fixture.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/conformance/report.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/dataplane.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/driver.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/errors.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/extensions.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/frames.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/keys.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/launch.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/manifest.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/operations.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/platform.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/queue.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/recording.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/results.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/run_install_step.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/runtime.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/seed.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/serve.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/serve_app.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/session.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/settings.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/skill.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/spaces.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/svc.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/computer/tools.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/errors.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/manifest.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/preflight.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/tool_access.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/tool_client.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/tool_context.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/transfers.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/types.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/website/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/website/clock.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/website/errors.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/website/readiness.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/website_browser/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/website_browser/control.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/website_browser/devtools.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/website_browser/environment.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/src/agentenv_protocol/website_browser/shapes.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/tests/computer_fake.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/tests/probe_fake.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/tests/test_a2a_agent.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/tests/test_a2a_agent_examples.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/tests/test_a2a_cdp_config.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/tests/test_a2a_object_transfers.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/tests/test_a2a_peer_agents.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/tests/test_a2a_proxy.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/tests/test_a2a_staging.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/tests/test_a2a_trajectory_log.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/tests/test_agent_env_environment.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/tests/test_browser_cdp.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/tests/test_browser_state.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/tests/test_client.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/tests/test_computer_app_data.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/tests/test_computer_app_server.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/tests/test_computer_conformance.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/tests/test_computer_conformance_checks.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/tests/test_computer_conformance_page.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/tests/test_computer_conformance_recording.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/tests/test_computer_dataplane.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/tests/test_computer_declare.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/tests/test_computer_driver.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/tests/test_computer_extensions.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/tests/test_computer_frames.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/tests/test_computer_macos.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/tests/test_computer_page.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/tests/test_computer_queue.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/tests/test_computer_recording.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/tests/test_computer_seed.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/tests/test_computer_serve.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/tests/test_computer_spaces.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/tests/test_computer_svc_forward.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/tests/test_control_token.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/tests/test_data_objects.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/tests/test_errors.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/tests/test_extension_methods.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/tests/test_intake.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/tests/test_manifest.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/tests/test_preflight.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/tests/test_tool_access.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/tests/test_tool_annotations.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/tests/test_tool_client.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/tests/test_tool_context.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/tests/test_tool_context_fastmcp.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/tests/test_tool_context_injection.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/tests/test_website_api.py +0 -0
- {agentenv_framework_protocol-0.1.467 → agentenv_framework_protocol-0.1.469}/tests/test_website_browser.py +0 -0
- {agentenv_framework_protocol-0.1.467 → 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.
|
|
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
|
|
@@ -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,91 @@
|
|
|
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, `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.
|
|
30
|
+
- **State.** The site keeps its users and notes itself, starting from `data/seed.json`, and serves them through its
|
|
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`
|
|
32
|
+
exports them, and `data/reset` empties them. `data/get` is how agentenv reads what a task left the site with.
|
|
33
|
+
- **Time.** Notes are stamped with `self.now()`, which follows the gateway's clock once `clock/v1`'s `sync_time` names
|
|
34
|
+
it. The frontend reads `X-Universe-Now` from every API response into `universeNow()` and shows it in the header.
|
|
35
|
+
It uses this helper rather than `clock_patch=True`, so the page works the same against a server written without the
|
|
36
|
+
SDK.
|
|
37
|
+
- **Readiness.** website/v1's `ready`, through the card, says not ready (`@ready_check seeded`) until the site has
|
|
38
|
+
users, for example after a `data/reset` and before the next `data/add`. The site serves no health path.
|
|
39
|
+
|
|
40
|
+
## Run it
|
|
41
|
+
|
|
42
|
+
From this folder:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
cd web && npm ci && npm run build && cd ..
|
|
46
|
+
PYTHONPATH=<repo>/packages/agentenv-protocol/src python site.py
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Open http://localhost:8000. To work on the frontend, run `npm run dev` in `web/` while `site.py` runs; Vite proxies
|
|
50
|
+
`/api` to port 8000.
|
|
51
|
+
|
|
52
|
+
## Test it
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
python -m pytest packages/agentenv-protocol/tests/test_website_notes_example.py
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The test imports `site.py`, runs the conformance checker, and checks sign-in, per-user notes, the admin-only route,
|
|
59
|
+
clock-synced timestamps, and the data plane's reset and seed load. It serves a stand-in page, so it needs no Vite
|
|
60
|
+
build.
|
|
61
|
+
|
|
62
|
+
## Build and verify the image
|
|
63
|
+
|
|
64
|
+
The build context is this folder; the image installs the released protocol, so it runs the SDK as published, while
|
|
65
|
+
the test above runs this repo's.
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
docker build -t notes-site .
|
|
69
|
+
docker run -d --name notes-site -p 18080:8000 notes-site
|
|
70
|
+
python -m agentenv_protocol.website.conformance http://localhost:18080 --clock-host host.docker.internal --strict
|
|
71
|
+
curl -s http://localhost:18080/api/notes
|
|
72
|
+
```
|
|
73
|
+
|
|
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
|
|
77
|
+
clock that is gone once the check ends: later timestamps start from 2031 until the server is synced again, so restart
|
|
78
|
+
the container before using it.
|
|
79
|
+
|
|
80
|
+
## Register it
|
|
81
|
+
|
|
82
|
+
In a bundle, `env.toml` declares the env: copied to a bundle's `envs/notes/`, `agent-env bundle register <bundle>`
|
|
83
|
+
builds the image with that folder as its context. Its keys are `type`, `environment_name` and `backend_image`; domains
|
|
84
|
+
are not one of them, as they come from the card. Without a bundle:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
agent-env env website put --id notes-site --server-dockerfile Dockerfile --server-docker-context .
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Run it from this folder. `environment_name` comes from the card (`@environment_card(name="notes")`). The stored env
|
|
91
|
+
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,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>
|