agentenv-framework-protocol 0.1.448__tar.gz → 0.1.451__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.448 → agentenv_framework_protocol-0.1.451}/PKG-INFO +13 -2
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/README.md +10 -1
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/pyproject.toml +5 -3
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/agent_env_environment.py +13 -4
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/computer/apps.py +1 -10
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/computer/conformance/fixture.py +57 -12
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/computer/driver.py +21 -13
- agentenv_framework_protocol-0.1.451/src/agentenv_protocol/computer/svc.py +182 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/website_browser.py +3 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/tests/test_computer_conformance.py +26 -1
- agentenv_framework_protocol-0.1.451/tests/test_computer_svc_forward.py +227 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/tests/test_website_browser.py +2 -1
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/.gitignore +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/LICENSE +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/NOTICE +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/THIRD_PARTY_NOTICES.md +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/examples/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/examples/advanced_agent.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/examples/basic_agent.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/examples/custom_extensions_agent.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/examples/multimodal_agent.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/examples/reference_model.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/examples/streaming_agent.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/a2a_agent/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/a2a_agent/_trajectory_log.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/a2a_agent/_triggers.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/a2a_agent/extensions.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/a2a_agent/framework.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/a2a_agent/peer_agents.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/a2a_agent/proxy.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/a2a_agent/registry.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/a2a_agent/staging.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/a2a_agent/tasks/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/a2a_agent/tasks/v1.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/client.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/computer/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/computer/_declare.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/computer/_linux.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/computer/_load.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/computer/_with_fds.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/computer/app.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/computer/app_data.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/computer/app_runtime.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/computer/card.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/computer/checks.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/computer/conformance/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/computer/conformance/__main__.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/computer/conformance/checks.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/computer/conformance/client.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/computer/conformance/mp4.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/computer/conformance/page_fixture.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/computer/conformance/report.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/computer/dataplane.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/computer/errors.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/computer/extensions.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/computer/frames.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/computer/keys.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/computer/launch.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/computer/manifest.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/computer/operations.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/computer/platform.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/computer/queue.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/computer/recording.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/computer/results.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/computer/run_install_step.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/computer/runtime.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/computer/seed.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/computer/serve.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/computer/serve_app.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/computer/session.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/computer/settings.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/computer/skill.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/computer/spaces.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/computer/tools.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/errors.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/manifest.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/preflight.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/tool_access.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/tool_client.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/tool_context.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/transfers.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/src/agentenv_protocol/types.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/tests/computer_fake.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/tests/probe_fake.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/tests/test_a2a_agent.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/tests/test_a2a_agent_examples.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/tests/test_a2a_cdp_config.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/tests/test_a2a_object_transfers.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/tests/test_a2a_peer_agents.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/tests/test_a2a_proxy.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/tests/test_a2a_staging.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/tests/test_a2a_trajectory_log.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/tests/test_agent_env_environment.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/tests/test_client.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/tests/test_computer_app_data.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/tests/test_computer_app_server.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/tests/test_computer_conformance_checks.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/tests/test_computer_conformance_page.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/tests/test_computer_conformance_recording.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/tests/test_computer_dataplane.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/tests/test_computer_declare.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/tests/test_computer_driver.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/tests/test_computer_extensions.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/tests/test_computer_frames.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/tests/test_computer_page.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/tests/test_computer_queue.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/tests/test_computer_recording.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/tests/test_computer_seed.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/tests/test_computer_serve.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/tests/test_computer_spaces.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/tests/test_control_token.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/tests/test_data_objects.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/tests/test_errors.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/tests/test_extension_methods.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/tests/test_intake.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/tests/test_manifest.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/tests/test_preflight.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/tests/test_tool_access.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/tests/test_tool_annotations.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/tests/test_tool_client.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/tests/test_tool_context.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/tests/test_tool_context_fastmcp.py +0 -0
- {agentenv_framework_protocol-0.1.448 → agentenv_framework_protocol-0.1.451}/tests/test_tool_context_injection.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.451
|
|
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
|
|
@@ -42,6 +42,7 @@ Requires-Dist: av>=17; extra == 'computer'
|
|
|
42
42
|
Requires-Dist: mcp<2,>=1.25; extra == 'computer'
|
|
43
43
|
Requires-Dist: pillow>=10.4; extra == 'computer'
|
|
44
44
|
Requires-Dist: uvicorn>=0.30; extra == 'computer'
|
|
45
|
+
Requires-Dist: websockets>=13; extra == 'computer'
|
|
45
46
|
Provides-Extra: dev
|
|
46
47
|
Requires-Dist: a2a-sdk[http-server]<0.4,>=0.3.26; extra == 'dev'
|
|
47
48
|
Requires-Dist: av>=17; extra == 'dev'
|
|
@@ -52,6 +53,7 @@ Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
|
52
53
|
Requires-Dist: pytest>=9.0; extra == 'dev'
|
|
53
54
|
Requires-Dist: python-multipart>=0.0.13; extra == 'dev'
|
|
54
55
|
Requires-Dist: regex>=2023.12.25; extra == 'dev'
|
|
56
|
+
Requires-Dist: websockets>=13; extra == 'dev'
|
|
55
57
|
Description-Content-Type: text/markdown
|
|
56
58
|
|
|
57
59
|
# agentenv-framework-protocol
|
|
@@ -826,7 +828,16 @@ launches one at a time, runs a load and a reset with the app stopped and relaunc
|
|
|
826
828
|
follows a failed load with a reset, and fails a call that runs past `data_timeout_s` (300 s); a sync
|
|
827
829
|
hook runs in a thread, which the reset and relaunch after it wait for. The driver fronts its apps:
|
|
828
830
|
its card lists each one's card as a child environment, rooted at `/svc/<app>/`, where it forwards
|
|
829
|
-
requests to the app's server
|
|
831
|
+
requests to the app's server, taking the control token:
|
|
832
|
+
|
|
833
|
+
- every method, its body and the answer's streamed both ways, so a large answer isn't held whole;
|
|
834
|
+
- headers passed on but the hop-by-hop ones, those a `Connection` header names and the control token's
|
|
835
|
+
`Authorization`; `Host` as the caller sent it, and in place of any address the caller forwarded,
|
|
836
|
+
the one it reached the driver at: `X-Forwarded-Host`, `X-Forwarded-Proto` and `X-Forwarded-Prefix`
|
|
837
|
+
(`/svc/<app>`), as the gateway sets them;
|
|
838
|
+
- WebSockets, relayed to the app's server both ways with the subprotocol it picks until either side
|
|
839
|
+
closes, the other then closed with its code; refused before they open without the token, or with no
|
|
840
|
+
such app or one not answering. The `computer` extra brings `websockets` for this.
|
|
830
841
|
|
|
831
842
|
`MailApp.declaration()` returns what the class declares, an `AppDeclaration`, which a `computer_app`
|
|
832
843
|
env stores (`browser: true` for a website browser), and `python -m agentenv_protocol.computer._declare <folder> <file> <class>` prints it as
|
|
@@ -770,7 +770,16 @@ launches one at a time, runs a load and a reset with the app stopped and relaunc
|
|
|
770
770
|
follows a failed load with a reset, and fails a call that runs past `data_timeout_s` (300 s); a sync
|
|
771
771
|
hook runs in a thread, which the reset and relaunch after it wait for. The driver fronts its apps:
|
|
772
772
|
its card lists each one's card as a child environment, rooted at `/svc/<app>/`, where it forwards
|
|
773
|
-
requests to the app's server
|
|
773
|
+
requests to the app's server, taking the control token:
|
|
774
|
+
|
|
775
|
+
- every method, its body and the answer's streamed both ways, so a large answer isn't held whole;
|
|
776
|
+
- headers passed on but the hop-by-hop ones, those a `Connection` header names and the control token's
|
|
777
|
+
`Authorization`; `Host` as the caller sent it, and in place of any address the caller forwarded,
|
|
778
|
+
the one it reached the driver at: `X-Forwarded-Host`, `X-Forwarded-Proto` and `X-Forwarded-Prefix`
|
|
779
|
+
(`/svc/<app>`), as the gateway sets them;
|
|
780
|
+
- WebSockets, relayed to the app's server both ways with the subprotocol it picks until either side
|
|
781
|
+
closes, the other then closed with its code; refused before they open without the token, or with no
|
|
782
|
+
such app or one not answering. The `computer` extra brings `websockets` for this.
|
|
774
783
|
|
|
775
784
|
`MailApp.declaration()` returns what the class declares, an `AppDeclaration`, which a `computer_app`
|
|
776
785
|
env stores (`browser: true` for a website browser), and `python -m agentenv_protocol.computer._declare <folder> <file> <class>` prints it as
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "agentenv-framework-protocol"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.451"
|
|
4
4
|
description = "Open data-plane protocol and server SDK for agent environments."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -41,13 +41,14 @@ app = [
|
|
|
41
41
|
"mcp>=1.25,<2",
|
|
42
42
|
"uvicorn>=0.30",
|
|
43
43
|
]
|
|
44
|
-
# A computer driver: ComputerDriverEnvironment serves its tools over mcp's FastMCP, crops and encodes frames,
|
|
45
|
-
#
|
|
44
|
+
# A computer driver: ComputerDriverEnvironment serves its tools over mcp's FastMCP, crops and encodes frames, records
|
|
45
|
+
# the display as H.264 with PyAV (computer-recording/v1), and relays WebSockets to its apps on /svc/<app>/.
|
|
46
46
|
computer = [
|
|
47
47
|
"av>=17",
|
|
48
48
|
"mcp>=1.25,<2",
|
|
49
49
|
"Pillow>=10.4",
|
|
50
50
|
"uvicorn>=0.30",
|
|
51
|
+
"websockets>=13",
|
|
51
52
|
]
|
|
52
53
|
dev = [
|
|
53
54
|
"a2a-sdk[http-server]>=0.3.26,<0.4",
|
|
@@ -59,6 +60,7 @@ dev = [
|
|
|
59
60
|
"fastmcp>=3.4,<4", # the standalone-fastmcp mount path
|
|
60
61
|
"Pillow>=10.4", # the computer extra's frame crops and encodings
|
|
61
62
|
"av>=17", # the computer extra's encoder, and the tests' decoder
|
|
63
|
+
"websockets>=13", # the computer extra's /svc/<app>/ relay
|
|
62
64
|
]
|
|
63
65
|
[project.urls]
|
|
64
66
|
Homepage = "https://www.agentenvframework.com"
|
|
@@ -1546,18 +1546,27 @@ def _error_response(error: AgentEnvError) -> Response:
|
|
|
1546
1546
|
return _json_response(error_body(**error.fields()), error.status)
|
|
1547
1547
|
|
|
1548
1548
|
|
|
1549
|
+
def control_check() -> Callable[[Any], bool]:
|
|
1550
|
+
"""Whether a call's headers carry the env's control token as their bearer, with ``CONTROL_TOKEN_ENV`` set; without
|
|
1551
|
+
it, any do."""
|
|
1552
|
+
token = os.environ.get(CONTROL_TOKEN_ENV)
|
|
1553
|
+
if not token:
|
|
1554
|
+
return lambda headers: True
|
|
1555
|
+
expected = f"Bearer {token}".encode()
|
|
1556
|
+
return lambda headers: hmac.compare_digest(headers.get("authorization", "").encode(), expected)
|
|
1557
|
+
|
|
1558
|
+
|
|
1549
1559
|
def control_guard() -> Callable[[Callable], Callable]:
|
|
1550
1560
|
"""What wraps a framework-only route, everything an env serves but its MCP endpoint and its card: with
|
|
1551
1561
|
``CONTROL_TOKEN_ENV`` set, a call that doesn't carry that token as its bearer is refused before the route runs;
|
|
1552
1562
|
without it, nothing."""
|
|
1553
|
-
|
|
1554
|
-
if not token:
|
|
1563
|
+
if not os.environ.get(CONTROL_TOKEN_ENV):
|
|
1555
1564
|
return lambda handler: handler
|
|
1556
|
-
|
|
1565
|
+
allowed = control_check()
|
|
1557
1566
|
|
|
1558
1567
|
def guard(handler: Callable) -> Callable:
|
|
1559
1568
|
async def guarded(request: Request) -> Response:
|
|
1560
|
-
if not
|
|
1569
|
+
if not allowed(request.headers):
|
|
1561
1570
|
return _error_response(AgentEnvError(
|
|
1562
1571
|
"unauthorized", "this route is agent-env's own: it takes the env's control token", status=401))
|
|
1563
1572
|
return await handler(request)
|
|
@@ -3,7 +3,7 @@ Each app's own server answers on its folder's socket (``serve_app``): its card s
|
|
|
3
3
|
paths it owns, and its ``computer-app/v1`` ``ready`` whether it's ready. Every app's server must answer before the
|
|
4
4
|
computer is ready; an app that starts with the computer must also be ready, and a ``demand`` app is checked once
|
|
5
5
|
something launches it. The driver lists each app as a child environment named for its folder, its card's paths under
|
|
6
|
-
``/svc/<name>/``,
|
|
6
|
+
``/svc/<name>/``, where it forwards each request to the app's server (``svc``)."""
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import asyncio
|
|
@@ -91,15 +91,6 @@ async def owned_paths(folders: list[Path]) -> list[str]:
|
|
|
91
91
|
for path in client.extension_params(card, COMPUTER_APP).get("paths") or []]
|
|
92
92
|
|
|
93
93
|
|
|
94
|
-
async def forward(folder: Path, method: str, path: str, query: str, body: bytes,
|
|
95
|
-
content_type: Optional[str]) -> httpx.Response:
|
|
96
|
-
"""The app's server's answer to the request, sent on as it came; it takes as long as the app does."""
|
|
97
|
-
async with httpx.AsyncClient(transport=httpx.AsyncHTTPTransport(uds=str(folder / SOCKET)), base_url="http://app",
|
|
98
|
-
timeout=None) as http:
|
|
99
|
-
return await http.request(method, f"/{path}" + (f"?{query}" if query else ""), content=body,
|
|
100
|
-
headers={"content-type": content_type} if content_type else None)
|
|
101
|
-
|
|
102
|
-
|
|
103
94
|
def endpoint(card: dict, method: str) -> str:
|
|
104
95
|
"""The path of a ``computer-app/v1`` method, as the app's card advertises it."""
|
|
105
96
|
return client.find_extension_method(card, COMPUTER_APP, method)["endpoint"]
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
"""The
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
"""The fixture the conformance probe stages with data/add and launches with computer-exec: a Tk window, the Fixture
|
|
2
|
+
Desk, whose widgets' screen boxes are written to <state-dir>/layout.json and every effect that reaches it, with
|
|
3
|
+
whatever lands in <state-dir>/inbox.txt, to <state-dir>/state.json, so the probe can aim at it and read back what
|
|
4
|
+
arrived. It records the same effects under X11, macOS's Aqua and Windows, whose Tk names keys, binds select-all and
|
|
5
|
+
reports the wheel differently. Run as ``python3 fixture.py <state-dir>``; it needs a 1280x800 screen or larger."""
|
|
5
6
|
from __future__ import annotations
|
|
6
7
|
|
|
8
|
+
import ctypes
|
|
7
9
|
import json
|
|
8
10
|
import os
|
|
9
11
|
import sys
|
|
@@ -19,6 +21,14 @@ MARKERS = ((11, 11, "#ff0000"), (12, 11, "#00ff00"), (11, 12, "#0000ff"), (12, 1
|
|
|
19
21
|
(20, 37, "#00ffff"))
|
|
20
22
|
CLIPBOARD = "agentenv-probe clipboard"
|
|
21
23
|
MIN_SIZE = (1280, 800)
|
|
24
|
+
#: X11's keysyms for the keys Aqua's Tk names by their character.
|
|
25
|
+
KEYSYMS = {",": "comma", " ": "space"}
|
|
26
|
+
#: A wheel notch's <MouseWheel> delta where Tk scales the wheel: Windows', and Tk 9's everywhere.
|
|
27
|
+
NOTCH_DELTA = 120
|
|
28
|
+
#: Tk 9 packs a <TouchpadScroll>'s distances into its delta: y, a signed 16-bit number, in the low bits, x above.
|
|
29
|
+
Y_BITS = 16
|
|
30
|
+
#: The pixels of a scrolled line, macOS's for a scroll posted in lines.
|
|
31
|
+
LINE_PX = 10
|
|
22
32
|
|
|
23
33
|
|
|
24
34
|
def main(state_dir: Path) -> None:
|
|
@@ -32,7 +42,7 @@ def main(state_dir: Path) -> None:
|
|
|
32
42
|
state: dict = {"name": "", "notes": "", "keys": [], "saved_count": 0, "subscribed": False, "selected": None,
|
|
33
43
|
"scroll_top": 0.0, "cells": {}, "right_clicks": 0, "double_clicks": 0, "drags": [],
|
|
34
44
|
"wheel": {"up": 0, "down": 0, "left": 0, "right": 0}, "clipboard": None, "loaded": None,
|
|
35
|
-
"screen": [width, height]}
|
|
45
|
+
"screen": [width, height], "windowing": root.tk.call("tk", "windowingsystem")}
|
|
36
46
|
|
|
37
47
|
def save() -> None:
|
|
38
48
|
part = state_dir / ".state.json.part"
|
|
@@ -52,12 +62,20 @@ def main(state_dir: Path) -> None:
|
|
|
52
62
|
|
|
53
63
|
def key(event) -> None:
|
|
54
64
|
state["name"] = name.get()
|
|
55
|
-
state["keys"] = (state["keys"] + [event.keysym])[-100:]
|
|
65
|
+
state["keys"] = (state["keys"] + [KEYSYMS.get(event.keysym, event.keysym)])[-100:]
|
|
56
66
|
state["clipboard"] = _clipboard(root)
|
|
57
67
|
save()
|
|
58
68
|
|
|
59
69
|
name.bind("<KeyRelease>", key)
|
|
60
70
|
|
|
71
|
+
def select_all(event) -> str:
|
|
72
|
+
name.selection_range(0, "end")
|
|
73
|
+
name.icursor("end")
|
|
74
|
+
return "break"
|
|
75
|
+
|
|
76
|
+
# Tk binds Ctrl+/ to select-all on X11 alone; Aqua's is Cmd+A.
|
|
77
|
+
name.bind("<Control-slash>", select_all)
|
|
78
|
+
|
|
61
79
|
def on_save() -> None:
|
|
62
80
|
state["saved_count"] += 1
|
|
63
81
|
save()
|
|
@@ -157,14 +175,25 @@ def main(state_dir: Path) -> None:
|
|
|
157
175
|
wheel = tk.Canvas(root, width=200, height=120, bg="#e6f0ff", highlightthickness=0)
|
|
158
176
|
wheel.place(x=840, y=500)
|
|
159
177
|
|
|
160
|
-
def wheeled(
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
state["wheel"][("left" if horizontal else "up") if button == 4 else ("right" if horizontal else "down")] += 1
|
|
178
|
+
def wheeled(horizontal: bool, steps: int) -> None:
|
|
179
|
+
way = ("left" if horizontal else "up") if steps > 0 else ("right" if horizontal else "down")
|
|
180
|
+
state["wheel"][way] += abs(steps)
|
|
164
181
|
save()
|
|
165
182
|
|
|
166
|
-
|
|
167
|
-
|
|
183
|
+
def scrolled(delta: int) -> None:
|
|
184
|
+
if steps := touchpad_steps(delta):
|
|
185
|
+
wheeled(*steps)
|
|
186
|
+
|
|
187
|
+
# Tk 8.6 on X11 reports the wheel as buttons 4 and 5, Aqua, Windows and Tk 9 as <MouseWheel>, a horizontal scroll
|
|
188
|
+
# (X's buttons 6 and 7) as a vertical one with Shift held. Tk 9 reports a scroll in pixels, a trackpad's or one a
|
|
189
|
+
# driver posts as Aqua's do, as <TouchpadScroll>.
|
|
190
|
+
if root.tk.call("tk", "windowingsystem") == "x11" and tk.TkVersion < 8.7:
|
|
191
|
+
wheel.bind("<Button-4>", lambda e: wheeled(bool(e.state & 1), 1))
|
|
192
|
+
wheel.bind("<Button-5>", lambda e: wheeled(bool(e.state & 1), -1))
|
|
193
|
+
else:
|
|
194
|
+
wheel.bind("<MouseWheel>", lambda e: wheeled(bool(e.state & 1), wheel_steps(e.delta)))
|
|
195
|
+
if tk.TkVersion >= 8.7:
|
|
196
|
+
wheel.bind("<TouchpadScroll>", lambda e: scrolled(e.delta))
|
|
168
197
|
|
|
169
198
|
field = tk.Canvas(root, width=48, height=48, bg="#000000", highlightthickness=0, bd=0)
|
|
170
199
|
field.place(x=1080, y=500)
|
|
@@ -198,6 +227,22 @@ def main(state_dir: Path) -> None:
|
|
|
198
227
|
root.mainloop()
|
|
199
228
|
|
|
200
229
|
|
|
230
|
+
def wheel_steps(delta: int) -> int:
|
|
231
|
+
"""The wheel steps a <MouseWheel> event's ``delta`` holds, up positive: whole notches of 120 where Tk scales them
|
|
232
|
+
so (Windows, and Tk 9 everywhere), else one a unit (Aqua's Tk 8.6)."""
|
|
233
|
+
return int(delta / NOTCH_DELTA) if abs(delta) >= NOTCH_DELTA else delta
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def touchpad_steps(delta: int) -> tuple[bool, int] | None:
|
|
237
|
+
"""The steps a <TouchpadScroll> event's ``delta`` holds, as horizontal or not and up or left positive, or None
|
|
238
|
+
when it moves nothing. A step is a line, LINE_PX, and macOS may coalesce several into one event."""
|
|
239
|
+
dx, dy = delta >> Y_BITS, ctypes.c_int16(delta).value
|
|
240
|
+
if not (dx or dy):
|
|
241
|
+
return None
|
|
242
|
+
distance = dy or dx
|
|
243
|
+
return not dy, (1 if distance > 0 else -1) * max(1, round(abs(distance) / LINE_PX))
|
|
244
|
+
|
|
245
|
+
|
|
201
246
|
def _clipboard(root: tk.Tk) -> str | None:
|
|
202
247
|
try:
|
|
203
248
|
return root.clipboard_get()
|
|
@@ -4,21 +4,22 @@ computer's apps as child environments, each reached at ``/svc/<name>/`` through
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
6
|
import asyncio
|
|
7
|
-
from
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Callable, ClassVar, Literal, Optional
|
|
8
9
|
|
|
9
|
-
import httpx
|
|
10
10
|
from pydantic import BaseModel, ConfigDict
|
|
11
11
|
from starlette.requests import Request
|
|
12
12
|
from starlette.responses import Response
|
|
13
|
+
from starlette.websockets import WebSocket
|
|
13
14
|
|
|
14
15
|
from ..agent_env_environment import (_CARD_CONFIG_ATTR, AgentEnvEnvironment, AgentEnvFastMCPApplication,
|
|
15
16
|
_error_response, control_guard, environment_card)
|
|
16
17
|
from ..tool_context import ToolContext
|
|
17
18
|
from ..types import EnvironmentCard, EnvironmentExtension, EnvironmentTool
|
|
18
|
-
from . import _linux, apps, card
|
|
19
|
+
from . import _linux, apps, card, svc
|
|
19
20
|
from .checks import check_features, check_names
|
|
20
21
|
from .dataplane import DataPlane
|
|
21
|
-
from .errors import
|
|
22
|
+
from .errors import TargetUnavailable
|
|
22
23
|
from .extensions import Extensions
|
|
23
24
|
from .operations import ReadyCheck, check_class, default_for
|
|
24
25
|
from .platform import Platform, detect
|
|
@@ -138,21 +139,28 @@ class ComputerDriverEnvironment(Tools, Extensions, DataPlane, AgentEnvEnvironmen
|
|
|
138
139
|
return await apps.children(self._computer.apps) if self._computer.apps else None
|
|
139
140
|
|
|
140
141
|
def _computer_front_apps(self, app: Any) -> Any:
|
|
141
|
-
app.custom_route("/svc/{name}/{path:path}", methods=
|
|
142
|
+
app.custom_route("/svc/{name}/{path:path}", methods=svc.METHODS)(control_guard()(self._computer_forward))
|
|
143
|
+
svc.websocket_route(app, "/svc/{name}/{path:path}", self._computer_relay)
|
|
142
144
|
return app
|
|
143
145
|
|
|
146
|
+
def _computer_app_folder(self, name: str) -> Optional[Path]:
|
|
147
|
+
return next((folder for folder in self._computer.apps if folder.name == name), None)
|
|
148
|
+
|
|
144
149
|
async def _computer_forward(self, request: Request) -> Response:
|
|
145
150
|
"""A request under ``/svc/<name>/``, sent on to the app named so: its own server answers it."""
|
|
146
151
|
name = request.path_params["name"]
|
|
147
|
-
|
|
148
|
-
if folder is None:
|
|
152
|
+
if (folder := self._computer_app_folder(name)) is None:
|
|
149
153
|
return _error_response(TargetUnavailable(f"this computer has no app named {name!r}"))
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
154
|
+
return await svc.forward(folder, name, request)
|
|
155
|
+
|
|
156
|
+
async def _computer_relay(self, websocket: WebSocket) -> None:
|
|
157
|
+
"""A WebSocket under ``/svc/<name>/``, relayed both ways to the app named so; refused, before it opens, with no
|
|
158
|
+
such app."""
|
|
159
|
+
name = websocket.path_params["name"]
|
|
160
|
+
if (folder := self._computer_app_folder(name)) is None:
|
|
161
|
+
await websocket.close(code=1008)
|
|
162
|
+
return
|
|
163
|
+
await svc.relay(folder, name, websocket)
|
|
156
164
|
|
|
157
165
|
# The card: what the driver's operations support.
|
|
158
166
|
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""The driver's ``/svc/<name>/`` route to each app on its computer: a request sent on to the app's server with its body
|
|
2
|
+
and its answer streamed, and a WebSocket relayed to it both ways. It needs the ``computer`` extra's ``websockets``, so
|
|
3
|
+
only the driver imports it; an app's venv, with the ``app`` extra, imports ``apps`` and never this."""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import contextlib
|
|
8
|
+
import re
|
|
9
|
+
from collections.abc import AsyncIterator, Iterable
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Callable
|
|
12
|
+
from urllib.parse import quote, unquote
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
import websockets
|
|
16
|
+
from starlette.background import BackgroundTask
|
|
17
|
+
from starlette.requests import Request
|
|
18
|
+
from starlette.responses import Response, StreamingResponse
|
|
19
|
+
from starlette.routing import WebSocketRoute
|
|
20
|
+
from starlette.websockets import WebSocket, WebSocketDisconnect
|
|
21
|
+
from websockets.asyncio.client import ClientConnection, unix_connect
|
|
22
|
+
|
|
23
|
+
from ..agent_env_environment import _error_response, control_check
|
|
24
|
+
from .app import SOCKET
|
|
25
|
+
from .errors import DeliveryFailed
|
|
26
|
+
|
|
27
|
+
#: The methods the route forwards.
|
|
28
|
+
METHODS = ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]
|
|
29
|
+
#: The headers that belong to one connection, which a proxy doesn't pass on.
|
|
30
|
+
HOP_HEADERS = frozenset({"connection", "keep-alive", "proxy-authenticate", "proxy-authorization", "proxy-connection",
|
|
31
|
+
"te", "trailer", "transfer-encoding", "upgrade"})
|
|
32
|
+
#: The address a request reached the driver at, which the driver sets for the app in place of any the caller sent.
|
|
33
|
+
FORWARDED = ("x-forwarded-host", "x-forwarded-proto", "x-forwarded-prefix")
|
|
34
|
+
#: A WebSocket close code that means the connection ended without one, which a close can't send on.
|
|
35
|
+
UNSENDABLE_CLOSES = (1005, 1006, 1015)
|
|
36
|
+
#: A Host header that names a host, and its port or not, and nothing else.
|
|
37
|
+
HOST = re.compile(r"(?:[A-Za-z0-9.-]+|\[[0-9A-Fa-f:.]+\])(?::[0-9]{1,5})?")
|
|
38
|
+
#: What a path's segments may hold unescaped, besides letters, digits and ``_.-~``.
|
|
39
|
+
PATH_SAFE = "/:@!$&'()*+,;="
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def passed_on(headers: Iterable[tuple[str, str]], *dropped: str) -> list[tuple[str, str]]:
|
|
43
|
+
"""``headers`` as a proxy passes them on: without the hop-by-hop ones, those a ``Connection`` header names, and
|
|
44
|
+
``dropped``."""
|
|
45
|
+
headers = list(headers)
|
|
46
|
+
named = {token.strip().lower() for name, value in headers if name.lower() == "connection"
|
|
47
|
+
for token in value.split(",")}
|
|
48
|
+
skipped = HOP_HEADERS | named | set(dropped)
|
|
49
|
+
return [(name, value) for name, value in headers if name.lower() not in skipped]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def to_app(headers: Iterable[tuple[str, str]], host: str, scheme: str, prefix: str) -> list[tuple[str, str]]:
|
|
53
|
+
"""A caller's headers as the app gets them: passed on without the control token's ``Authorization`` or any address
|
|
54
|
+
the caller forwarded, and with the one it reached the driver at, its ``host``, ``scheme`` and the app's
|
|
55
|
+
``prefix``."""
|
|
56
|
+
return passed_on(headers, "authorization", *FORWARDED) + [
|
|
57
|
+
("x-forwarded-host", host), ("x-forwarded-proto", scheme), ("x-forwarded-prefix", prefix)]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def target(connection: Request | WebSocket) -> str:
|
|
61
|
+
"""The path and query a request under ``/svc/<name>/`` asks its app for, as it came: still percent-encoded, so an
|
|
62
|
+
escaped ``?``, ``#`` or ``/`` stays one. The raw path's segments past any the driver is mounted under must be
|
|
63
|
+
``svc`` and the app's name, else the routed path is escaped again."""
|
|
64
|
+
mounted = len([segment for segment in connection.scope.get("root_path", "").split("/") if segment])
|
|
65
|
+
parts = (connection.scope.get("raw_path") or b"").decode("latin-1").split("/")[1 + mounted:]
|
|
66
|
+
if len(parts) > 2 and [unquote(part) for part in parts[:2]] == ["svc", connection.path_params["name"]]:
|
|
67
|
+
tail = "/".join(parts[2:])
|
|
68
|
+
else:
|
|
69
|
+
tail = quote(connection.path_params["path"], safe=PATH_SAFE)
|
|
70
|
+
query = connection.scope.get("query_string", b"").decode("latin-1")
|
|
71
|
+
return f"/{tail}" + (f"?{query}" if query else "")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _bytes(headers: Iterable[tuple[str, str]]) -> list[tuple[bytes, bytes]]:
|
|
75
|
+
"""Headers decoded as Latin-1, as Starlette and the helpers here hold them, as the bytes they came as."""
|
|
76
|
+
return [(name.encode("latin-1"), value.encode("latin-1")) for name, value in headers]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def websocket_route(app: Any, path: str, endpoint: Callable) -> None:
|
|
80
|
+
"""``endpoint`` served at ``path`` as a WebSocket route of ``app``, an mcp or a standalone fastmcp FastMCP, which
|
|
81
|
+
have no API for one."""
|
|
82
|
+
for routes in ("_custom_starlette_routes", "_additional_http_routes"):
|
|
83
|
+
if isinstance(getattr(app, routes, None), list):
|
|
84
|
+
getattr(app, routes).append(WebSocketRoute(path, endpoint))
|
|
85
|
+
return
|
|
86
|
+
raise TypeError(f"a driver's app takes WebSocket routes on an mcp or a fastmcp FastMCP, not a {type(app).__name__}")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
async def forward(folder: Path, name: str, request: Request) -> Response:
|
|
90
|
+
"""``request`` sent on to the app ``name``'s server in ``folder`` with the caller's headers and none of the
|
|
91
|
+
client's own, its body and its answer streamed; it takes as long as the app does."""
|
|
92
|
+
headers = to_app(request.headers.items(), request.headers.get("host", ""), request.url.scheme, f"/svc/{name}")
|
|
93
|
+
has_body = request.headers.get("content-length", "0") != "0" or "transfer-encoding" in request.headers
|
|
94
|
+
http = httpx.AsyncClient(transport=httpx.AsyncHTTPTransport(uds=str(folder / SOCKET)), base_url="http://app",
|
|
95
|
+
timeout=None)
|
|
96
|
+
http.headers.clear()
|
|
97
|
+
try:
|
|
98
|
+
answer = await http.send(http.build_request(request.method, target(request), headers=_bytes(headers),
|
|
99
|
+
content=request.stream() if has_body else None), stream=True)
|
|
100
|
+
except (httpx.HTTPError, OSError) as e:
|
|
101
|
+
await http.aclose()
|
|
102
|
+
return _error_response(DeliveryFailed(f"app {name}'s server isn't answering: {type(e).__name__}: {e}"))
|
|
103
|
+
except BaseException:
|
|
104
|
+
await http.aclose()
|
|
105
|
+
raise
|
|
106
|
+
|
|
107
|
+
async def close() -> None:
|
|
108
|
+
await answer.aclose()
|
|
109
|
+
await http.aclose()
|
|
110
|
+
|
|
111
|
+
async def streamed() -> AsyncIterator[bytes]:
|
|
112
|
+
try:
|
|
113
|
+
async for chunk in answer.aiter_raw():
|
|
114
|
+
yield chunk
|
|
115
|
+
finally:
|
|
116
|
+
await close()
|
|
117
|
+
|
|
118
|
+
response = StreamingResponse(streamed(), status_code=answer.status_code, background=BackgroundTask(close))
|
|
119
|
+
response.raw_headers = _bytes(passed_on((name.decode("latin-1"), value.decode("latin-1"))
|
|
120
|
+
for name, value in answer.headers.raw))
|
|
121
|
+
return response
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
async def relay(folder: Path, name: str, websocket: WebSocket) -> None:
|
|
125
|
+
"""``websocket`` relayed both ways to the app ``name``'s server in ``folder`` until either side closes; refused,
|
|
126
|
+
before it opens, without the control token, with a Host that isn't a host and port, or with the app's server not
|
|
127
|
+
answering."""
|
|
128
|
+
host = websocket.headers.get("host", "")
|
|
129
|
+
if not control_check()(websocket.headers) or (host and not HOST.fullmatch(host)):
|
|
130
|
+
await websocket.close(code=1008)
|
|
131
|
+
return
|
|
132
|
+
headers = [(key, _utf8(value)) for key, value in to_app(
|
|
133
|
+
websocket.headers.items(), host, "https" if websocket.url.scheme == "wss" else "http", f"/svc/{name}")
|
|
134
|
+
if key.lower() != "host" and not key.lower().startswith("sec-websocket-")]
|
|
135
|
+
offered = [p.strip() for p in websocket.headers.get("sec-websocket-protocol", "").split(",") if p.strip()]
|
|
136
|
+
try:
|
|
137
|
+
upstream = await unix_connect(str(folder / SOCKET), f"ws://{host or 'app'}{target(websocket)}",
|
|
138
|
+
additional_headers=headers, subprotocols=offered or None, max_size=None,
|
|
139
|
+
compression=None, ping_interval=None, user_agent_header=None)
|
|
140
|
+
except (OSError, ValueError, asyncio.TimeoutError, websockets.WebSocketException):
|
|
141
|
+
await websocket.close(code=1011)
|
|
142
|
+
return
|
|
143
|
+
async with upstream:
|
|
144
|
+
await websocket.accept(subprotocol=upstream.subprotocol)
|
|
145
|
+
await _both_ways(websocket, upstream)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _utf8(value: str) -> str:
|
|
149
|
+
"""A header value decoded as Latin-1 as the string ``websockets`` sends as the same bytes, which it encodes as
|
|
150
|
+
UTF-8; bytes that aren't UTF-8 become replacement characters."""
|
|
151
|
+
return value.encode("latin-1").decode("utf-8", "replace")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
async def _both_ways(client: WebSocket, app: ClientConnection) -> None:
|
|
155
|
+
"""Each message from either side sent to the other until one side closes, then the other closed with its code."""
|
|
156
|
+
async def inbound() -> None:
|
|
157
|
+
while True:
|
|
158
|
+
message = await client.receive()
|
|
159
|
+
if message["type"] == "websocket.disconnect":
|
|
160
|
+
code = message.get("code") or 1000
|
|
161
|
+
await app.close(1000 if code in UNSENDABLE_CLOSES else code)
|
|
162
|
+
return
|
|
163
|
+
await app.send(message["text"] if message.get("text") is not None else message["bytes"])
|
|
164
|
+
|
|
165
|
+
async def outbound() -> None:
|
|
166
|
+
with contextlib.suppress(websockets.ConnectionClosed):
|
|
167
|
+
async for message in app:
|
|
168
|
+
if isinstance(message, str):
|
|
169
|
+
await client.send_text(message)
|
|
170
|
+
else:
|
|
171
|
+
await client.send_bytes(message)
|
|
172
|
+
code = app.close_code or 1000
|
|
173
|
+
with contextlib.suppress(RuntimeError, WebSocketDisconnect):
|
|
174
|
+
await client.close(1000 if code in UNSENDABLE_CLOSES else code)
|
|
175
|
+
|
|
176
|
+
tasks = [asyncio.ensure_future(inbound()), asyncio.ensure_future(outbound())]
|
|
177
|
+
try:
|
|
178
|
+
await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
|
|
179
|
+
finally:
|
|
180
|
+
for task in tasks:
|
|
181
|
+
task.cancel()
|
|
182
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
@@ -430,7 +430,10 @@ class BrowserControl:
|
|
|
430
430
|
return {**({"png": png} if png is not None else {"blank": True}), "page": self._state_of(target)}
|
|
431
431
|
|
|
432
432
|
async def _capture(self, session: str) -> Optional[str]:
|
|
433
|
+
"""The tab's page as it shows now; with a dialog open on it, which keeps it from painting, the last it took."""
|
|
433
434
|
target = self._sessions[session]
|
|
435
|
+
if session in self._open_dialogs:
|
|
436
|
+
return self._shots.get(target)
|
|
434
437
|
navigated = self.tabs.get(target, {}).get("url")
|
|
435
438
|
try:
|
|
436
439
|
shot = await self.devtools.call("Page.captureScreenshot", {"format": "png", "optimizeForSpeed": True},
|
|
@@ -4,8 +4,10 @@ full needs gets those checks unverified; a driver broken one way fails the check
|
|
|
4
4
|
fronts that exports is asked for its export once; the report's verdicts and the CLI."""
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
+
import importlib
|
|
7
8
|
import json
|
|
8
|
-
|
|
9
|
+
import sys
|
|
10
|
+
from types import ModuleType, SimpleNamespace
|
|
9
11
|
|
|
10
12
|
import pytest
|
|
11
13
|
|
|
@@ -284,6 +286,29 @@ async def test_a_fixture_that_doesnt_start_leaves_full_unverified_and_is_cleaned
|
|
|
284
286
|
assert (["kill", "4243"] in probed.commands) is (driver is SilentFixture)
|
|
285
287
|
|
|
286
288
|
|
|
289
|
+
@pytest.mark.parametrize("delta, steps", [(1, 1), (-3, -3), (120, 1), (-360, -3), (240, 2)],
|
|
290
|
+
ids=["aqua-up", "aqua-down-3", "scaled-up", "scaled-down-3", "scaled-up-2"])
|
|
291
|
+
def test_the_fixture_counts_a_wheel_events_steps_whether_tk_scales_them_by_120_or_not(delta, steps, monkeypatch):
|
|
292
|
+
"""Aqua's Tk 8.6 reports a step as a delta of 1; Windows, and Tk 9 everywhere, as 120."""
|
|
293
|
+
assert _fixture(monkeypatch).wheel_steps(delta) == steps
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
@pytest.mark.parametrize("delta, steps", [(10, (False, 1)), (65526, (False, -1)), (65516, (False, -2)),
|
|
297
|
+
(655360, (True, 1)), (-655360, (True, -1)), (0, None)],
|
|
298
|
+
ids=["up", "down", "down-2-coalesced", "left", "right", "still"])
|
|
299
|
+
def test_the_fixture_counts_a_scroll_tk_9_reports_in_pixels_by_the_lines_it_moves(delta, steps, monkeypatch):
|
|
300
|
+
"""The deltas Tk 9 on macOS gave cua-driver's scrolls: 10 pixels a line, two lines at once where macOS coalesced
|
|
301
|
+
them."""
|
|
302
|
+
assert _fixture(monkeypatch).touchpad_steps(delta) == steps
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _fixture(monkeypatch):
|
|
306
|
+
"""The fixture module, which runs in the computer, read with Tk stood in for, as this Python may have none."""
|
|
307
|
+
monkeypatch.setitem(sys.modules, "tkinter", ModuleType("tkinter"))
|
|
308
|
+
monkeypatch.delitem(sys.modules, "agentenv_protocol.computer.conformance.fixture", raising=False)
|
|
309
|
+
return importlib.import_module("agentenv_protocol.computer.conformance.fixture")
|
|
310
|
+
|
|
311
|
+
|
|
287
312
|
def test_a_refusal_must_carry_the_code_and_applied_no():
|
|
288
313
|
def result(error: bool, **data) -> CallToolResult:
|
|
289
314
|
return CallToolResult(content=[TextContent(type="text", text="x")], structuredContent=data, isError=error)
|