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