agentenv-framework-protocol 0.1.483__tar.gz → 0.1.485__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.483 → agentenv_framework_protocol-0.1.485}/PKG-INFO +1 -1
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/pyproject.toml +1 -1
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/computer/__init__.py +4 -1
- agentenv_framework_protocol-0.1.485/src/agentenv_protocol/computer/_job.py +110 -0
- agentenv_framework_protocol-0.1.485/src/agentenv_protocol/computer/_reaping.py +53 -0
- agentenv_framework_protocol-0.1.483/src/agentenv_protocol/computer/session.py → agentenv_framework_protocol-0.1.485/src/agentenv_protocol/computer/_session_posix.py +7 -34
- agentenv_framework_protocol-0.1.485/src/agentenv_protocol/computer/_session_windows.py +317 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/computer/app.py +32 -2
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/computer/app_runtime.py +5 -2
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/computer/apps.py +16 -12
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/computer/launch.py +4 -6
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/computer/serve_app.py +13 -6
- agentenv_framework_protocol-0.1.485/src/agentenv_protocol/computer/served.py +114 -0
- agentenv_framework_protocol-0.1.485/src/agentenv_protocol/computer/session.py +16 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/computer/svc.py +17 -11
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/tests/test_computer_app_server.py +20 -6
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/tests/test_computer_declare.py +18 -3
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/tests/test_computer_serve.py +45 -9
- agentenv_framework_protocol-0.1.485/tests/test_computer_served.py +103 -0
- agentenv_framework_protocol-0.1.485/tests/test_computer_session_windows.py +204 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/tests/test_computer_svc_forward.py +36 -19
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/.gitignore +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/LICENSE +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/NOTICE +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/README.md +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/THIRD_PARTY_NOTICES.md +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/examples/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/examples/advanced_agent.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/examples/basic_agent.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/examples/custom_extensions_agent.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/examples/multimodal_agent.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/examples/reference_model.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/examples/streaming_agent.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/examples/website/notes/.gitignore +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/examples/website/notes/Dockerfile +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/examples/website/notes/Dockerfile.dockerignore +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/examples/website/notes/README.md +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/examples/website/notes/data/seed.json +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/examples/website/notes/env.toml +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/examples/website/notes/site.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/examples/website/notes/web/index.html +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/examples/website/notes/web/package-lock.json +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/examples/website/notes/web/package.json +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/examples/website/notes/web/src/main.js +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/examples/website/notes/web/src/style.css +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/examples/website/notes/web/vite.config.js +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/a2a_agent/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/a2a_agent/_trajectory_log.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/a2a_agent/_triggers.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/a2a_agent/extensions.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/a2a_agent/framework.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/a2a_agent/peer_agents.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/a2a_agent/proxy.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/a2a_agent/registry.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/a2a_agent/staging.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/a2a_agent/tasks/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/a2a_agent/tasks/v1.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/agent_env_environment.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/browser_cdp.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/client.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/computer/_declare.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/computer/_linux.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/computer/_load.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/computer/_macos.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/computer/_with_fds.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/computer/_x11.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/computer/app_data.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/computer/card.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/computer/checks.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/computer/conformance/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/computer/conformance/__main__.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/computer/conformance/checks.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/computer/conformance/client.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/computer/conformance/fixture.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/computer/conformance/mp4.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/computer/conformance/page_fixture.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/computer/conformance/report.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/computer/dataplane.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/computer/driver.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/computer/errors.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/computer/extensions.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/computer/frames.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/computer/keys.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/computer/manifest.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/computer/operations.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/computer/platform.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/computer/prefix.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/computer/queue.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/computer/recording.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/computer/results.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/computer/run_install_step.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/computer/runtime.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/computer/seed.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/computer/serve.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/computer/settings.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/computer/skill.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/computer/spaces.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/computer/tools.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/errors.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/manifest.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/preflight.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/tool_access.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/tool_client.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/tool_context.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/transfers.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/types.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/website/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/website/clock.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/website/conformance.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/website/environment.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/website/errors.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/website/readiness.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/website/routes.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/website/session.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/website_browser/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/website_browser/control.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/website_browser/data.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/website_browser/devtools.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/website_browser/environment.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/src/agentenv_protocol/website_browser/shapes.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/tests/computer_fake.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/tests/probe_fake.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/tests/test_a2a_agent.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/tests/test_a2a_agent_examples.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/tests/test_a2a_cdp_config.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/tests/test_a2a_object_transfers.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/tests/test_a2a_peer_agents.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/tests/test_a2a_proxy.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/tests/test_a2a_staging.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/tests/test_a2a_trajectory_log.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/tests/test_agent_env_environment.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/tests/test_browser_cdp.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/tests/test_browser_data.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/tests/test_browser_state.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/tests/test_client.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/tests/test_computer_app_data.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/tests/test_computer_conformance.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/tests/test_computer_conformance_checks.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/tests/test_computer_conformance_page.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/tests/test_computer_conformance_recording.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/tests/test_computer_dataplane.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/tests/test_computer_driver.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/tests/test_computer_extensions.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/tests/test_computer_frames.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/tests/test_computer_macos.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/tests/test_computer_page.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/tests/test_computer_queue.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/tests/test_computer_recording.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/tests/test_computer_seed.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/tests/test_computer_spaces.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/tests/test_computer_tool_prefix.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/tests/test_control_token.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/tests/test_data_objects.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/tests/test_errors.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/tests/test_extension_methods.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/tests/test_intake.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/tests/test_manifest.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/tests/test_preflight.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/tests/test_tool_access.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/tests/test_tool_annotations.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/tests/test_tool_client.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/tests/test_tool_context.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/tests/test_tool_context_fastmcp.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/tests/test_tool_context_injection.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/tests/test_website_api.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/tests/test_website_browser.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/tests/test_website_conformance.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/tests/test_website_environment.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/tests/test_website_mcp.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/tests/test_website_notes_example.py +0 -0
- {agentenv_framework_protocol-0.1.483 → agentenv_framework_protocol-0.1.485}/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.485
|
|
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
|
|
@@ -21,11 +21,13 @@ from .app import (
|
|
|
21
21
|
Ready,
|
|
22
22
|
RunScript,
|
|
23
23
|
Service,
|
|
24
|
+
Windows,
|
|
24
25
|
computer_app,
|
|
25
26
|
install,
|
|
26
27
|
install_step,
|
|
27
28
|
linux,
|
|
28
29
|
ready_check,
|
|
30
|
+
windows,
|
|
29
31
|
)
|
|
30
32
|
from .errors import (
|
|
31
33
|
BackgroundUnavailable,
|
|
@@ -123,7 +125,8 @@ def __getattr__(name: str) -> Any:
|
|
|
123
125
|
__all__ = [
|
|
124
126
|
"ComputerDriverEnvironment", "computer_driver", "DEFAULT_IDENTITY", "DriverDeclaration", "MacOSDriver",
|
|
125
127
|
"X11Driver",
|
|
126
|
-
"ComputerAppEnvironment", "computer_app", "install", "linux", "
|
|
128
|
+
"ComputerAppEnvironment", "computer_app", "install", "linux", "windows", "install_step", "ready_check", "Linux",
|
|
129
|
+
"Windows", "Service",
|
|
127
130
|
"Launch", "Ready", "Packages", "AptSource", "Download", "RunScript",
|
|
128
131
|
"AppDeclaration", "AppOs",
|
|
129
132
|
"APP_SETTINGS_ENV", "part_bytes",
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Run a Windows app in a Job object that holds it and everything it starts: ``python -I -S _job.py -- <argv>``. The
|
|
2
|
+
Windows ``session`` launches every app through it, by path, so it loads nothing but the standard library.
|
|
3
|
+
|
|
4
|
+
It names the job for its own pid (``job_name``), joins the job, then starts the app, so the app and whatever it starts
|
|
5
|
+
are in the job; it says ``started`` once the app has, or why it couldn't on stderr, and stays until the job holds no
|
|
6
|
+
process but itself, then exits with the app's exit code. It holds the job's only handle, which ends the job when it
|
|
7
|
+
closes: the app lives as long as this does, outliving the server that launched it, as a POSIX app's session does, and a
|
|
8
|
+
server, restarted or not, finds the job by name to pause or stop it."""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import ctypes
|
|
12
|
+
import os
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
import time
|
|
16
|
+
|
|
17
|
+
#: How often the job is looked at for processes besides this one, once the app has exited.
|
|
18
|
+
POLL_S = 0.2
|
|
19
|
+
#: ``JOBOBJECT_INFORMATION_CLASS`` values and the limit and access flags used.
|
|
20
|
+
BASIC_ACCOUNTING_INFORMATION, BASIC_PROCESS_ID_LIST, EXTENDED_LIMIT_INFORMATION = 1, 3, 9
|
|
21
|
+
KILL_ON_JOB_CLOSE = 0x2000
|
|
22
|
+
JOB_OBJECT_QUERY, JOB_OBJECT_TERMINATE = 0x0004, 0x0008
|
|
23
|
+
STARTED = b"started"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class BasicLimits(ctypes.Structure):
|
|
27
|
+
_fields_ = [("PerProcessUserTimeLimit", ctypes.c_int64), ("PerJobUserTimeLimit", ctypes.c_int64),
|
|
28
|
+
("LimitFlags", ctypes.c_uint32), ("MinimumWorkingSetSize", ctypes.c_size_t),
|
|
29
|
+
("MaximumWorkingSetSize", ctypes.c_size_t), ("ActiveProcessLimit", ctypes.c_uint32),
|
|
30
|
+
("Affinity", ctypes.c_size_t), ("PriorityClass", ctypes.c_uint32), ("SchedulingClass", ctypes.c_uint32)]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class IoCounters(ctypes.Structure):
|
|
34
|
+
_fields_ = [(name, ctypes.c_uint64) for name in ("ReadOperationCount", "WriteOperationCount", "OtherOperationCount",
|
|
35
|
+
"ReadTransferCount", "WriteTransferCount", "OtherTransferCount")]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ExtendedLimits(ctypes.Structure):
|
|
39
|
+
_fields_ = [("BasicLimitInformation", BasicLimits), ("IoInfo", IoCounters),
|
|
40
|
+
("ProcessMemoryLimit", ctypes.c_size_t), ("JobMemoryLimit", ctypes.c_size_t),
|
|
41
|
+
("PeakProcessMemoryUsed", ctypes.c_size_t), ("PeakJobMemoryUsed", ctypes.c_size_t)]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Accounting(ctypes.Structure):
|
|
45
|
+
_fields_ = [("TotalUserTime", ctypes.c_int64), ("TotalKernelTime", ctypes.c_int64),
|
|
46
|
+
("ThisPeriodTotalUserTime", ctypes.c_int64), ("ThisPeriodTotalKernelTime", ctypes.c_int64),
|
|
47
|
+
("TotalPageFaultCount", ctypes.c_uint32), ("TotalProcesses", ctypes.c_uint32),
|
|
48
|
+
("ActiveProcesses", ctypes.c_uint32), ("TotalTerminatedProcesses", ctypes.c_uint32)]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def job_name(pid: int) -> str:
|
|
52
|
+
"""The name of the job the helper running as ``pid`` holds, in the session's own namespace."""
|
|
53
|
+
return f"Local\\agentenv-app-{pid}"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def kernel32() -> ctypes.WinDLL:
|
|
57
|
+
"""``kernel32``, its calls typed as this module and ``session`` use them."""
|
|
58
|
+
dll = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
59
|
+
handle, dword, wide = ctypes.c_void_p, ctypes.c_uint32, ctypes.c_wchar_p
|
|
60
|
+
dll.CreateJobObjectW.argtypes, dll.CreateJobObjectW.restype = [ctypes.c_void_p, wide], handle
|
|
61
|
+
dll.OpenJobObjectW.argtypes, dll.OpenJobObjectW.restype = [dword, ctypes.c_int, wide], handle
|
|
62
|
+
dll.SetInformationJobObject.argtypes = [handle, ctypes.c_int, ctypes.c_void_p, dword]
|
|
63
|
+
dll.QueryInformationJobObject.argtypes = [handle, ctypes.c_int, ctypes.c_void_p, dword, ctypes.c_void_p]
|
|
64
|
+
dll.AssignProcessToJobObject.argtypes = [handle, handle]
|
|
65
|
+
dll.TerminateJobObject.argtypes = [handle, ctypes.c_uint]
|
|
66
|
+
dll.GetCurrentProcess.restype = handle
|
|
67
|
+
dll.CloseHandle.argtypes = [handle]
|
|
68
|
+
dll.OpenProcess.argtypes, dll.OpenProcess.restype = [dword, ctypes.c_int, dword], handle
|
|
69
|
+
dll.QueryFullProcessImageNameW.argtypes = [handle, dword, wide, ctypes.c_void_p]
|
|
70
|
+
dll.WaitForMultipleObjects.argtypes, dll.WaitForMultipleObjects.restype = [dword, ctypes.c_void_p, ctypes.c_int,
|
|
71
|
+
dword], dword
|
|
72
|
+
return dll
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def active_processes(dll: ctypes.WinDLL, job: int) -> int:
|
|
76
|
+
"""How many processes the job ``job`` holds now."""
|
|
77
|
+
accounting = Accounting()
|
|
78
|
+
if not dll.QueryInformationJobObject(job, BASIC_ACCOUNTING_INFORMATION, ctypes.byref(accounting),
|
|
79
|
+
ctypes.sizeof(accounting), None):
|
|
80
|
+
raise ctypes.WinError(ctypes.get_last_error())
|
|
81
|
+
return accounting.ActiveProcesses
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def main(argv: list[str]) -> None:
|
|
85
|
+
command = argv[argv.index("--") + 1:]
|
|
86
|
+
dll = kernel32()
|
|
87
|
+
job = dll.CreateJobObjectW(None, job_name(os.getpid()))
|
|
88
|
+
limits = ExtendedLimits()
|
|
89
|
+
limits.BasicLimitInformation.LimitFlags = KILL_ON_JOB_CLOSE
|
|
90
|
+
if not job or not dll.SetInformationJobObject(job, EXTENDED_LIMIT_INFORMATION, ctypes.byref(limits),
|
|
91
|
+
ctypes.sizeof(limits)) \
|
|
92
|
+
or not dll.AssignProcessToJobObject(job, dll.GetCurrentProcess()):
|
|
93
|
+
raise ctypes.WinError(ctypes.get_last_error())
|
|
94
|
+
try:
|
|
95
|
+
app = subprocess.Popen(command, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
|
|
96
|
+
stderr=subprocess.DEVNULL)
|
|
97
|
+
except OSError as e:
|
|
98
|
+
sys.stderr.write(f"{type(e).__name__}: {e}\n")
|
|
99
|
+
sys.exit(2)
|
|
100
|
+
sys.stdout.buffer.write(STARTED + b"\n")
|
|
101
|
+
sys.stdout.flush()
|
|
102
|
+
code = app.wait()
|
|
103
|
+
while active_processes(dll, job) > 1:
|
|
104
|
+
time.sleep(POLL_S)
|
|
105
|
+
# An exit code is 32 bits, which Python's exit takes as a signed number.
|
|
106
|
+
sys.exit(code - (1 << 32) if code >= 1 << 31 else code)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
if __name__ == "__main__":
|
|
110
|
+
main(sys.argv[1:])
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""The processes a computer app's server launched, each reaped once it exits, so its exit code can be asked for; shared
|
|
2
|
+
by every OS's session (``session``)."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
#: The processes launched into the session, kept until they exit so each is reaped.
|
|
9
|
+
_running: set[asyncio.Task] = set()
|
|
10
|
+
#: Each launched process's reaping, by pid, until its exit is asked for: its return code, negative for a signal.
|
|
11
|
+
_exits: dict[int, asyncio.Future] = {}
|
|
12
|
+
#: How many exits no one has asked for are kept, the latest.
|
|
13
|
+
KEPT_EXITS = 16
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def reap(process: asyncio.subprocess.Process) -> None:
|
|
17
|
+
"""Reap ``process`` once it exits, its exit kept until it's asked for or ``KEPT_EXITS`` later ones push it out; any
|
|
18
|
+
pipe it was started with is read to its end, so it closes with the process."""
|
|
19
|
+
reaped = asyncio.ensure_future(_exited(process))
|
|
20
|
+
_running.add(reaped)
|
|
21
|
+
reaped.add_done_callback(_running.discard)
|
|
22
|
+
_exits[process.pid] = reaped
|
|
23
|
+
reaped.add_done_callback(lambda _: _forget_old_exits())
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async def _exited(process: asyncio.subprocess.Process) -> int:
|
|
27
|
+
await process.communicate()
|
|
28
|
+
return process.returncode
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def forget(pid: int) -> None:
|
|
32
|
+
"""No one asks how ``pid`` exited: a process the server stopped."""
|
|
33
|
+
_exits.pop(pid, None)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _forget_old_exits() -> None:
|
|
37
|
+
"""Drop all but the latest ``KEPT_EXITS`` exits no one has asked for."""
|
|
38
|
+
for pid in [pid for pid, reaped in _exits.items() if reaped.done()][:-KEPT_EXITS]:
|
|
39
|
+
del _exits[pid]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
async def returncode(pid: int, timeout_s: float) -> Optional[int]:
|
|
43
|
+
"""The return code the process ``launch`` started as ``pid`` exited with, negative for the signal that ended it,
|
|
44
|
+
once it's reaped within ``timeout_s``; None for one ``launch`` didn't start or that hasn't exited."""
|
|
45
|
+
reaped = _exits.get(pid)
|
|
46
|
+
if reaped is None:
|
|
47
|
+
return None
|
|
48
|
+
try:
|
|
49
|
+
code = await asyncio.wait_for(asyncio.shield(reaped), timeout_s)
|
|
50
|
+
except TimeoutError:
|
|
51
|
+
return None
|
|
52
|
+
_exits.pop(pid, None)
|
|
53
|
+
return code
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
"""What a computer app's own server does in the desktop session
|
|
2
|
-
|
|
1
|
+
"""What a computer app's own server does in the desktop session on Linux and macOS (``session``): wait for the X
|
|
2
|
+
session, launch the app into a session of its own and stop or pause it by its process group, add its freedesktop menu
|
|
3
|
+
launcher and list the X session's windows."""
|
|
3
4
|
from __future__ import annotations
|
|
4
5
|
|
|
5
6
|
import asyncio
|
|
@@ -11,16 +12,12 @@ import sys
|
|
|
11
12
|
from pathlib import Path
|
|
12
13
|
from typing import Iterator, Optional
|
|
13
14
|
|
|
15
|
+
from . import _reaping
|
|
16
|
+
|
|
14
17
|
#: How often a starting session is looked for.
|
|
15
18
|
POLL_S = 0.2
|
|
16
19
|
#: Moves the fds an app is launched with into place, run on its own so it loads nothing but the standard library.
|
|
17
20
|
WITH_FDS = Path(__file__).with_name("_with_fds.py")
|
|
18
|
-
#: The processes launched into the session, kept until they exit so each is reaped.
|
|
19
|
-
_running: set[asyncio.Task] = set()
|
|
20
|
-
#: Each launched process's reaping, by pid, until its exit is asked for: its return code, negative for a signal.
|
|
21
|
-
_exits: dict[int, asyncio.Future] = {}
|
|
22
|
-
#: How many exits no one has asked for are kept, the latest.
|
|
23
|
-
KEPT_EXITS = 16
|
|
24
21
|
|
|
25
22
|
|
|
26
23
|
async def wait_until_up() -> None:
|
|
@@ -39,38 +36,14 @@ async def launch(argv: list[str], cwd: Path, env: dict[str, str], fds: Optional[
|
|
|
39
36
|
process = await asyncio.create_subprocess_exec(
|
|
40
37
|
*argv, cwd=cwd, env={**os.environ, **env}, start_new_session=True, stdin=asyncio.subprocess.DEVNULL,
|
|
41
38
|
stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.DEVNULL, pass_fds=tuple((fds or {}).values()))
|
|
42
|
-
|
|
43
|
-
_running.add(reaped)
|
|
44
|
-
reaped.add_done_callback(_running.discard)
|
|
45
|
-
_exits[process.pid] = reaped
|
|
46
|
-
reaped.add_done_callback(lambda _: _forget_old_exits())
|
|
39
|
+
_reaping.reap(process)
|
|
47
40
|
return process.pid
|
|
48
41
|
|
|
49
42
|
|
|
50
|
-
def _forget_old_exits() -> None:
|
|
51
|
-
"""Drop all but the latest ``KEPT_EXITS`` exits no one has asked for."""
|
|
52
|
-
for pid in [pid for pid, reaped in _exits.items() if reaped.done()][:-KEPT_EXITS]:
|
|
53
|
-
del _exits[pid]
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
async def returncode(pid: int, timeout_s: float) -> Optional[int]:
|
|
57
|
-
"""The return code the process ``launch`` started as ``pid`` exited with, negative for the signal that ended it,
|
|
58
|
-
once it's reaped within ``timeout_s``; None for one ``launch`` didn't start or that hasn't exited."""
|
|
59
|
-
reaped = _exits.get(pid)
|
|
60
|
-
if reaped is None:
|
|
61
|
-
return None
|
|
62
|
-
try:
|
|
63
|
-
code = await asyncio.wait_for(asyncio.shield(reaped), timeout_s)
|
|
64
|
-
except TimeoutError:
|
|
65
|
-
return None
|
|
66
|
-
_exits.pop(pid, None)
|
|
67
|
-
return code
|
|
68
|
-
|
|
69
|
-
|
|
70
43
|
async def stop(pid: int, timeout_s: float) -> None:
|
|
71
44
|
"""End the session ``launch`` started as ``pid``, every process in it: asked to exit, a paused one continued so it
|
|
72
45
|
can, then killed after ``timeout_s``. No one asks how it exited once it's stopped."""
|
|
73
|
-
|
|
46
|
+
_reaping.forget(pid)
|
|
74
47
|
try:
|
|
75
48
|
os.killpg(pid, signal.SIGTERM)
|
|
76
49
|
except ProcessLookupError:
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"""What a computer app's own server does in the desktop session on Windows (``session``). agentenv-session starts the
|
|
2
|
+
server in the session user's sign-in, so the session is up and its desktop is the user's. An app launches through
|
|
3
|
+
``_job`` into a Job object that holds it and everything it starts, found by the helper's pid: stopping it asks its
|
|
4
|
+
windows to close, then ends the job after the timeout, and pausing it suspends each of its processes. Its menu launcher
|
|
5
|
+
is a Start Menu shortcut, and the session's windows are listed with ``EnumWindows``, each by its class, its program's
|
|
6
|
+
file name and its title."""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import contextlib
|
|
11
|
+
import ctypes
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
import time
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Iterator, NamedTuple, Optional
|
|
20
|
+
|
|
21
|
+
from . import _job, _reaping
|
|
22
|
+
|
|
23
|
+
#: The helper that runs an app in its job, run on its own so it loads nothing but the standard library.
|
|
24
|
+
JOB = Path(__file__).with_name("_job.py")
|
|
25
|
+
#: The interpreter the helper runs on: the base one, as a venv's python.exe is a launcher that runs it as a child, and
|
|
26
|
+
#: the helper names its job for its own pid, which must be the pid a launch returns.
|
|
27
|
+
PYTHON = Path(sys.base_prefix) / "python.exe"
|
|
28
|
+
#: How long the helper may take to start the app and say so.
|
|
29
|
+
START_TIMEOUT_S = 30.0
|
|
30
|
+
#: How often a stopping app is looked at for having ended.
|
|
31
|
+
POLL_S = 0.2
|
|
32
|
+
#: How long a job ended by force is waited on for each of its processes to have exited, so none still writes once a
|
|
33
|
+
#: stop returns and whatever they had open is closed, as a reset then puts the app's files back.
|
|
34
|
+
ENDED_TIMEOUT_S = 5.0
|
|
35
|
+
WM_CLOSE = 0x0010
|
|
36
|
+
#: A Start Menu shortcut, from its path, program, arguments and folder in the environment, as PowerShell writes one.
|
|
37
|
+
SHORTCUT = ("$s = (New-Object -ComObject WScript.Shell).CreateShortcut($env:AGENTENV_SHORTCUT); "
|
|
38
|
+
"$s.TargetPath = $env:AGENTENV_TARGET; $s.Arguments = $env:AGENTENV_ARGUMENTS; "
|
|
39
|
+
"$s.WorkingDirectory = $env:AGENTENV_FOLDER; $s.Save()")
|
|
40
|
+
#: What a file name can't hold on Windows.
|
|
41
|
+
UNSAFE = re.compile(r'[\\/:*?"<>|\x00-\x1f]')
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
async def wait_until_up() -> None:
|
|
45
|
+
"""At once: the server runs in the session user's sign-in, so the session is up."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
async def launch(argv: list[str], cwd: Path, env: dict[str, str], fds: Optional[dict[int, int]] = None) -> int:
|
|
49
|
+
"""Start ``argv`` in ``cwd`` in a job of its own (``_job``), which outlives the server, with the server's
|
|
50
|
+
environment and ``env``; the helper's pid, by which the job is found. A Windows app can't be handed fds."""
|
|
51
|
+
if fds:
|
|
52
|
+
raise NotImplementedError("a Windows app can't be handed the server's fds")
|
|
53
|
+
process = await asyncio.create_subprocess_exec(
|
|
54
|
+
str(PYTHON), "-I", "-S", str(JOB), "--", *argv, cwd=cwd, env={**os.environ, **env},
|
|
55
|
+
stdin=asyncio.subprocess.DEVNULL, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
|
|
56
|
+
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.CREATE_NO_WINDOW)
|
|
57
|
+
try:
|
|
58
|
+
said = await asyncio.wait_for(process.stdout.readline(), START_TIMEOUT_S)
|
|
59
|
+
except BaseException:
|
|
60
|
+
# Its job's only handle closes with it, which ends whatever it started.
|
|
61
|
+
with contextlib.suppress(ProcessLookupError):
|
|
62
|
+
process.kill()
|
|
63
|
+
_reaping.reap(process)
|
|
64
|
+
raise
|
|
65
|
+
if said.strip() != _job.STARTED:
|
|
66
|
+
why = ((await process.stderr.read()).decode(errors="replace").strip().splitlines() or [""])[-1]
|
|
67
|
+
await process.wait()
|
|
68
|
+
raise OSError(f"couldn't start {argv[0]}: {why or f'its job helper exited {process.returncode}'}")
|
|
69
|
+
_reaping.reap(process)
|
|
70
|
+
return process.pid
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
async def stop(pid: int, timeout_s: float) -> None:
|
|
74
|
+
"""End the job the helper ``pid`` holds, every process in it: its windows asked to close, then the job ended after
|
|
75
|
+
``timeout_s``, or at once when it has no window to close, and waited on until each process it held has exited and
|
|
76
|
+
it holds none. No one asks how it exited once it's stopped."""
|
|
77
|
+
_reaping.forget(pid)
|
|
78
|
+
job = _win32.open_job(pid)
|
|
79
|
+
if job is None:
|
|
80
|
+
return
|
|
81
|
+
try:
|
|
82
|
+
windows = [window for window in await asyncio.to_thread(_win32.windows) if window.pid in _app_pids(job, pid)]
|
|
83
|
+
for window in windows:
|
|
84
|
+
_win32.post_close(window.handle)
|
|
85
|
+
deadline = time.monotonic() + (timeout_s if windows else 0)
|
|
86
|
+
while _app_pids(job, pid) and time.monotonic() < deadline:
|
|
87
|
+
await asyncio.sleep(POLL_S)
|
|
88
|
+
if _app_pids(job, pid):
|
|
89
|
+
ending = _win32.open_processes(_win32.job_pids(job))
|
|
90
|
+
try:
|
|
91
|
+
_win32.terminate_job(job)
|
|
92
|
+
deadline = time.monotonic() + ENDED_TIMEOUT_S
|
|
93
|
+
await asyncio.to_thread(_win32.wait_exited, ending, ENDED_TIMEOUT_S)
|
|
94
|
+
while _win32.job_pids(job) and time.monotonic() < deadline:
|
|
95
|
+
await asyncio.sleep(POLL_S)
|
|
96
|
+
finally:
|
|
97
|
+
for process in ending:
|
|
98
|
+
_win32.close(process)
|
|
99
|
+
finally:
|
|
100
|
+
_win32.close(job)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@contextlib.contextmanager
|
|
104
|
+
def paused(pid: int) -> Iterator[None]:
|
|
105
|
+
"""Every process of the job the helper ``pid`` holds suspended while the block runs, and resumed after. One not yet
|
|
106
|
+
suspended can start another, so the job is listed again until it holds none that's new."""
|
|
107
|
+
job = _win32.open_job(pid)
|
|
108
|
+
if job is None:
|
|
109
|
+
yield
|
|
110
|
+
return
|
|
111
|
+
suspended, seen = [], {pid}
|
|
112
|
+
try:
|
|
113
|
+
while fresh := [app_pid for app_pid in _win32.job_pids(job) if app_pid not in seen]:
|
|
114
|
+
seen.update(fresh)
|
|
115
|
+
suspended += [app_pid for app_pid in fresh if _win32.suspend(app_pid)]
|
|
116
|
+
yield
|
|
117
|
+
finally:
|
|
118
|
+
for app_pid in suspended:
|
|
119
|
+
_win32.resume(app_pid)
|
|
120
|
+
_win32.close(job)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def alive(pid: int) -> bool:
|
|
124
|
+
"""Whether a process of the job the helper ``pid`` holds runs, the helper or one of the app's; a job a stop just
|
|
125
|
+
ended is there, empty, until its last handle closes."""
|
|
126
|
+
job = _win32.open_job(pid)
|
|
127
|
+
if job is None:
|
|
128
|
+
return False
|
|
129
|
+
try:
|
|
130
|
+
return bool(_win32.job_pids(job))
|
|
131
|
+
finally:
|
|
132
|
+
_win32.close(job)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def add_launcher(name: str, folder: Path) -> Path:
|
|
136
|
+
"""The Start Menu shortcut, named ``name``, that launches the app in ``folder`` through its own server, written
|
|
137
|
+
where the session user's Start Menu finds it."""
|
|
138
|
+
entry = Path(os.environ["APPDATA"]) / "Microsoft/Windows/Start Menu/Programs" / f"{UNSAFE.sub('_', name)}.lnk"
|
|
139
|
+
entry.parent.mkdir(parents=True, exist_ok=True)
|
|
140
|
+
subprocess.run(["powershell", "-NoProfile", "-NonInteractive", "-Command", SHORTCUT], check=True,
|
|
141
|
+
capture_output=True, timeout=60, creationflags=subprocess.CREATE_NO_WINDOW,
|
|
142
|
+
env={**os.environ, "AGENTENV_SHORTCUT": str(entry), "AGENTENV_FOLDER": str(folder),
|
|
143
|
+
"AGENTENV_TARGET": str(folder / "venv" / "Scripts" / "pythonw.exe"),
|
|
144
|
+
"AGENTENV_ARGUMENTS": f'-m agentenv_protocol.computer.launch "{folder}"'})
|
|
145
|
+
return entry
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
async def windows() -> list[dict[str, str]]:
|
|
149
|
+
"""The session's shown top-level windows, each as its program's file name, lowercased, its ``instance``, its window
|
|
150
|
+
class its ``class``, and its ``title``; a window that's hidden or cloaked, as a suspended Store app's frame is, is
|
|
151
|
+
left out."""
|
|
152
|
+
return await asyncio.to_thread(lambda: [
|
|
153
|
+
{"instance": window.program, "class": window.class_name, "title": window.title}
|
|
154
|
+
for window in _win32.windows() if window.shown])
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _app_pids(job: int, helper: int) -> list[int]:
|
|
158
|
+
"""The processes of the job ``job`` but the helper ``helper`` that holds it."""
|
|
159
|
+
return [pid for pid in _win32.job_pids(job) if pid != helper]
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@dataclass(frozen=True)
|
|
163
|
+
class Window:
|
|
164
|
+
"""A top-level window as ``EnumWindows`` lists it: its ``handle``, the ``pid`` of its process, that process's
|
|
165
|
+
``program`` file name, lowercased, its ``class_name`` and ``title``, and whether it's ``shown``."""
|
|
166
|
+
|
|
167
|
+
handle: int
|
|
168
|
+
pid: int
|
|
169
|
+
program: str
|
|
170
|
+
class_name: str
|
|
171
|
+
title: str
|
|
172
|
+
shown: bool
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class _Win32:
|
|
176
|
+
"""The Win32 calls this module makes, each a method, so its logic is tested apart from them."""
|
|
177
|
+
|
|
178
|
+
PROCESS_QUERY_LIMITED_INFORMATION, PROCESS_SUSPEND_RESUME, SYNCHRONIZE = 0x1000, 0x0800, 0x00100000
|
|
179
|
+
#: The most handles one wait takes (``MAXIMUM_WAIT_OBJECTS``).
|
|
180
|
+
WAIT_HANDLES = 64
|
|
181
|
+
DWMWA_CLOAKED = 14
|
|
182
|
+
#: How many process ids a job's list is read with room for, before it's read again with room for all.
|
|
183
|
+
PIDS = 64
|
|
184
|
+
#: The characters a window class's name and a program's full path take at most, with the closing NUL.
|
|
185
|
+
CLASS_NAME_CHARS, PATH_CHARS = 256, 32768
|
|
186
|
+
|
|
187
|
+
def __init__(self) -> None:
|
|
188
|
+
self._dlls: Optional[_Dlls] = None
|
|
189
|
+
|
|
190
|
+
def open_job(self, helper: int) -> Optional[int]:
|
|
191
|
+
"""A handle to the job the helper ``helper`` holds, to query and end it, or None when it isn't there."""
|
|
192
|
+
return self._api().kernel32.OpenJobObjectW(_job.JOB_OBJECT_QUERY | _job.JOB_OBJECT_TERMINATE, False,
|
|
193
|
+
_job.job_name(helper)) or None
|
|
194
|
+
|
|
195
|
+
def job_pids(self, job: int) -> list[int]:
|
|
196
|
+
kernel32 = self._api().kernel32
|
|
197
|
+
room = self.PIDS
|
|
198
|
+
while True:
|
|
199
|
+
listed = _pid_list(room)()
|
|
200
|
+
if kernel32.QueryInformationJobObject(job, _job.BASIC_PROCESS_ID_LIST, ctypes.byref(listed),
|
|
201
|
+
ctypes.sizeof(listed), None):
|
|
202
|
+
return list(listed.ProcessIdList[:listed.NumberOfProcessIdsInList])
|
|
203
|
+
if listed.NumberOfAssignedProcesses <= room:
|
|
204
|
+
raise ctypes.WinError(ctypes.get_last_error())
|
|
205
|
+
room = listed.NumberOfAssignedProcesses
|
|
206
|
+
|
|
207
|
+
def terminate_job(self, job: int) -> None:
|
|
208
|
+
if not self._api().kernel32.TerminateJobObject(job, 1):
|
|
209
|
+
raise ctypes.WinError(ctypes.get_last_error())
|
|
210
|
+
|
|
211
|
+
def close(self, handle: int) -> None:
|
|
212
|
+
self._api().kernel32.CloseHandle(handle)
|
|
213
|
+
|
|
214
|
+
def open_processes(self, pids: list[int]) -> list[int]:
|
|
215
|
+
"""Handles to wait on each process of ``pids`` that's there; one that's gone has none."""
|
|
216
|
+
kernel32 = self._api().kernel32
|
|
217
|
+
return [process for pid in pids if (process := kernel32.OpenProcess(self.SYNCHRONIZE, False, pid))]
|
|
218
|
+
|
|
219
|
+
def wait_exited(self, processes: list[int], timeout_s: float) -> None:
|
|
220
|
+
"""Wait until each of ``processes``, handles, has exited, every thread of it ended, or ``timeout_s`` passes: a
|
|
221
|
+
process's own handle says it has, where its job's list says only that it's no longer in the job."""
|
|
222
|
+
kernel32, deadline = self._api().kernel32, time.monotonic() + timeout_s
|
|
223
|
+
for first in range(0, len(processes), self.WAIT_HANDLES):
|
|
224
|
+
some = processes[first:first + self.WAIT_HANDLES]
|
|
225
|
+
kernel32.WaitForMultipleObjects(len(some), (ctypes.c_void_p * len(some))(*some), True,
|
|
226
|
+
max(0, int((deadline - time.monotonic()) * 1000)))
|
|
227
|
+
|
|
228
|
+
def suspend(self, pid: int) -> bool:
|
|
229
|
+
"""Whether the process ``pid`` was there to suspend, and was."""
|
|
230
|
+
return bool(self._on_process(pid, self.PROCESS_SUSPEND_RESUME,
|
|
231
|
+
lambda process: self._api().ntdll.NtSuspendProcess(process) == 0))
|
|
232
|
+
|
|
233
|
+
def resume(self, pid: int) -> None:
|
|
234
|
+
self._on_process(pid, self.PROCESS_SUSPEND_RESUME, lambda process: self._api().ntdll.NtResumeProcess(process))
|
|
235
|
+
|
|
236
|
+
def post_close(self, window: int) -> None:
|
|
237
|
+
self._api().user32.PostMessageW(window, WM_CLOSE, 0, 0)
|
|
238
|
+
|
|
239
|
+
def windows(self) -> list[Window]:
|
|
240
|
+
dlls = self._api()
|
|
241
|
+
user32, dwmapi = dlls.user32, dlls.dwmapi
|
|
242
|
+
handles: list[int] = []
|
|
243
|
+
listing = ctypes.WINFUNCTYPE(ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p)(
|
|
244
|
+
lambda window, _: handles.append(window) or True)
|
|
245
|
+
user32.EnumWindows(listing, None)
|
|
246
|
+
found = []
|
|
247
|
+
for handle in handles:
|
|
248
|
+
pid, cloaked = ctypes.c_uint32(), ctypes.c_uint32()
|
|
249
|
+
user32.GetWindowThreadProcessId(handle, ctypes.byref(pid))
|
|
250
|
+
class_name, title = ctypes.create_unicode_buffer(self.CLASS_NAME_CHARS), ctypes.create_unicode_buffer(
|
|
251
|
+
user32.GetWindowTextLengthW(handle) + 1)
|
|
252
|
+
user32.GetClassNameW(handle, class_name, len(class_name))
|
|
253
|
+
user32.GetWindowTextW(handle, title, len(title))
|
|
254
|
+
dwmapi.DwmGetWindowAttribute(handle, self.DWMWA_CLOAKED, ctypes.byref(cloaked), ctypes.sizeof(cloaked))
|
|
255
|
+
found.append(Window(handle, pid.value, self._program(pid.value), class_name.value, title.value,
|
|
256
|
+
bool(user32.IsWindowVisible(handle)) and not cloaked.value))
|
|
257
|
+
return found
|
|
258
|
+
|
|
259
|
+
def _program(self, pid: int) -> str:
|
|
260
|
+
"""The file name of the program running as ``pid``, lowercased, or empty where it can't be read."""
|
|
261
|
+
def read(process: int) -> str:
|
|
262
|
+
path, size = ctypes.create_unicode_buffer(self.PATH_CHARS), ctypes.c_uint32(self.PATH_CHARS)
|
|
263
|
+
if not self._api().kernel32.QueryFullProcessImageNameW(process, 0, path, ctypes.byref(size)):
|
|
264
|
+
return ""
|
|
265
|
+
return path.value.rpartition("\\")[2].lower()
|
|
266
|
+
return self._on_process(pid, self.PROCESS_QUERY_LIMITED_INFORMATION, read) or ""
|
|
267
|
+
|
|
268
|
+
def _on_process(self, pid: int, access: int, act):
|
|
269
|
+
"""``act`` on a handle to the process ``pid`` opened for ``access``, closed after; None where it can't be
|
|
270
|
+
opened, as for a process that has exited."""
|
|
271
|
+
kernel32 = self._api().kernel32
|
|
272
|
+
process = kernel32.OpenProcess(access, False, pid)
|
|
273
|
+
if not process:
|
|
274
|
+
return None
|
|
275
|
+
try:
|
|
276
|
+
return act(process)
|
|
277
|
+
finally:
|
|
278
|
+
kernel32.CloseHandle(process)
|
|
279
|
+
|
|
280
|
+
def _api(self) -> _Dlls:
|
|
281
|
+
"""The DLLs this class calls, bound on first use, so this module imports anywhere."""
|
|
282
|
+
if self._dlls is None:
|
|
283
|
+
user32 = ctypes.WinDLL("user32", use_last_error=True)
|
|
284
|
+
user32.EnumWindows.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
|
|
285
|
+
user32.GetWindowThreadProcessId.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
|
|
286
|
+
user32.GetWindowTextLengthW.argtypes = user32.IsWindowVisible.argtypes = [ctypes.c_void_p]
|
|
287
|
+
user32.GetClassNameW.argtypes = user32.GetWindowTextW.argtypes = [ctypes.c_void_p, ctypes.c_wchar_p,
|
|
288
|
+
ctypes.c_int]
|
|
289
|
+
user32.PostMessageW.argtypes = [ctypes.c_void_p, ctypes.c_uint, ctypes.c_size_t, ctypes.c_ssize_t]
|
|
290
|
+
dwmapi = ctypes.WinDLL("dwmapi")
|
|
291
|
+
dwmapi.DwmGetWindowAttribute.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.c_void_p,
|
|
292
|
+
ctypes.c_uint32]
|
|
293
|
+
ntdll = ctypes.WinDLL("ntdll")
|
|
294
|
+
ntdll.NtSuspendProcess.argtypes = ntdll.NtResumeProcess.argtypes = [ctypes.c_void_p]
|
|
295
|
+
self._dlls = _Dlls(_job.kernel32(), user32, dwmapi, ntdll)
|
|
296
|
+
return self._dlls
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class _Dlls(NamedTuple):
|
|
300
|
+
"""The DLLs ``_Win32`` calls, each with the calls it makes typed: ``kernel32``'s in ``_job``, as the helper makes
|
|
301
|
+
some of them too."""
|
|
302
|
+
|
|
303
|
+
kernel32: ctypes.WinDLL
|
|
304
|
+
user32: ctypes.WinDLL
|
|
305
|
+
dwmapi: ctypes.WinDLL
|
|
306
|
+
ntdll: ctypes.WinDLL
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _pid_list(room: int) -> type[ctypes.Structure]:
|
|
310
|
+
"""``JOBOBJECT_BASIC_PROCESS_ID_LIST`` with room for ``room`` process ids."""
|
|
311
|
+
class Listed(ctypes.Structure):
|
|
312
|
+
_fields_ = [("NumberOfAssignedProcesses", ctypes.c_uint32), ("NumberOfProcessIdsInList", ctypes.c_uint32),
|
|
313
|
+
("ProcessIdList", ctypes.c_size_t * room)]
|
|
314
|
+
return Listed
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
_win32 = _Win32()
|
|
@@ -41,11 +41,14 @@ _PATHS = TypeAdapter(list[RelativePath])
|
|
|
41
41
|
WINDOW_KEYS = frozenset({"class", "instance", "title"})
|
|
42
42
|
#: The framework extension an app's own server serves: its launch as params, and its ``launch`` and ``ready`` methods.
|
|
43
43
|
COMPUTER_APP = "urn:agentenv:computer-app/v1"
|
|
44
|
-
#: Where an app's own server listens, in the app's folder
|
|
44
|
+
#: Where an app's own server listens on Linux and macOS, in the app's folder.
|
|
45
45
|
SOCKET = Path(".agentenv/server.sock")
|
|
46
|
+
#: Where an app's server on Windows writes the port it serves on and its token, in the app's folder.
|
|
47
|
+
ADDRESS = Path(".agentenv/server.json")
|
|
46
48
|
#: 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
49
|
#: opens to agents: a fresh computer starts without it, and an app that resets removes its own.
|
|
48
50
|
AGENT_ACTED = Path(".agentenv/agent-acted")
|
|
51
|
+
#: Where an app's server says why it last failed to start, in the app's folder.
|
|
49
52
|
FAILURE = Path(".agentenv/server.failure")
|
|
50
53
|
#: The values of an app's build settings its env fixes in its computer's image, in the app's folder.
|
|
51
54
|
BUILD_SETTINGS = Path(".agentenv/build_settings.json")
|
|
@@ -145,6 +148,20 @@ class Linux(BaseModel):
|
|
|
145
148
|
ports: Optional[list[int]] = None
|
|
146
149
|
|
|
147
150
|
|
|
151
|
+
class Windows(BaseModel):
|
|
152
|
+
"""How a Windows computer installs the app: as on Linux, with no apt; ``run`` scripts are PowerShell (``.ps1``), run
|
|
153
|
+
elevated as the computer is composed, and ``services`` run in the session user's sign-in."""
|
|
154
|
+
|
|
155
|
+
model_config = ConfigDict(extra="forbid")
|
|
156
|
+
|
|
157
|
+
files: LayerPath = "."
|
|
158
|
+
python: Optional[LayerPath] = None
|
|
159
|
+
downloads: Optional[list[Download]] = None
|
|
160
|
+
run: Optional[list[RunScript]] = None
|
|
161
|
+
services: list[Service] = Field(default_factory=list)
|
|
162
|
+
ports: Optional[list[int]] = None
|
|
163
|
+
|
|
164
|
+
|
|
148
165
|
class Ready(BaseModel):
|
|
149
166
|
"""When the app is ready: its window shown, a port answering or a command exiting 0, within ``timeout_s``."""
|
|
150
167
|
|
|
@@ -169,7 +186,8 @@ class Launch(BaseModel):
|
|
|
169
186
|
|
|
170
187
|
at: Literal["boot", "demand"] = "boot"
|
|
171
188
|
command: list[str]
|
|
172
|
-
#: Matches the app's window by
|
|
189
|
+
#: Matches the app's window by ``class``, ``instance`` and ``title``, as given: on X11 its WM_CLASS class and
|
|
190
|
+
#: instance; on Windows its window class and its program's file name, lowercased, such as ``notepad.exe``.
|
|
173
191
|
window: Optional[dict[str, str]] = None
|
|
174
192
|
ready: Optional[Ready] = None
|
|
175
193
|
#: Variables set in the app's environment, over its server's.
|
|
@@ -337,6 +355,18 @@ def linux(*, files: str = ".", packages: Optional[Packages] = None, python: Opti
|
|
|
337
355
|
launch=launch, paths=paths, skill=skill, settings=settings, build_settings=build_settings)
|
|
338
356
|
|
|
339
357
|
|
|
358
|
+
def windows(*, files: str = ".", python: Optional[str] = None, downloads: Optional[list[Download]] = None,
|
|
359
|
+
run: Optional[list[RunScript]] = None, services: Optional[list[Service]] = None,
|
|
360
|
+
ports: Optional[list[int]] = None, launch: Optional[Launch] = None, paths: Optional[list[str]] = None,
|
|
361
|
+
skill: Optional[str] = None, settings: Optional[type[BaseModel]] = None,
|
|
362
|
+
build_settings: Optional[type[BaseModel]] = None) -> Callable:
|
|
363
|
+
"""``@install`` for Windows: how a Windows computer installs the app, ``Windows``'s fields, and what is its own
|
|
364
|
+
there."""
|
|
365
|
+
return install("windows", Windows(files=files, python=python, downloads=downloads, run=run,
|
|
366
|
+
services=services or [], ports=ports),
|
|
367
|
+
launch=launch, paths=paths, skill=skill, settings=settings, build_settings=build_settings)
|
|
368
|
+
|
|
369
|
+
|
|
340
370
|
def install_step(scope: Literal["system", "session"] = "system", *,
|
|
341
371
|
os: Union[str, list[str], None] = None) -> Callable:
|
|
342
372
|
"""Mark a method a computer runs as it installs the app, after its files, packages, venv and run scripts, in the
|