hud-python 0.5.40__tar.gz → 0.5.41__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.
- {hud_python-0.5.40 → hud_python-0.5.41}/PKG-INFO +1 -1
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/init.py +114 -41
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/environment/environment.py +74 -33
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/environment/tests/test_environment.py +251 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/computer/gemini.py +37 -1
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/computer/tests/test_computer.py +67 -1
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/executors/base.py +29 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/executors/pyautogui.py +13 -20
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/executors/tests/test_pyautogui_executor.py +10 -3
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/executors/xdo.py +8 -4
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/utils/tests/test_version.py +1 -1
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/version.py +1 -1
- {hud_python-0.5.40 → hud_python-0.5.41}/pyproject.toml +1 -1
- {hud_python-0.5.40 → hud_python-0.5.41}/.gitignore +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/LICENSE +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/README.md +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/examples/README.md +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/__init__.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/__main__.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/agents/__init__.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/agents/base.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/agents/claude.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/agents/gateway.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/agents/gemini.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/agents/gemini_cua.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/agents/grounded_openai.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/agents/misc/__init__.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/agents/misc/integration_test_agent.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/agents/misc/response_agent.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/agents/openai.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/agents/openai_chat.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/agents/operator.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/agents/resolver.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/agents/tests/__init__.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/agents/tests/conftest.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/agents/tests/test_base.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/agents/tests/test_base_runtime.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/agents/tests/test_claude.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/agents/tests/test_gemini.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/agents/tests/test_grounded_openai_agent.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/agents/tests/test_integration_test_agent.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/agents/tests/test_openai.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/agents/tests/test_operator.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/agents/tests/test_resolver.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/agents/tests/test_run_eval.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/agents/types.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/__init__.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/__main__.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/analyze.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/build.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/cancel.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/convert/__init__.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/convert/base.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/convert/harbor.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/convert/tests/__init__.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/convert/tests/conftest.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/convert/tests/test_harbor.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/debug.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/deploy.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/dev.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/eval.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/flows/__init__.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/flows/dev.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/flows/init.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/flows/tasks.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/flows/templates.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/flows/tests/__init__.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/flows/tests/test_dev.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/link.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/login.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/models.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/push.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/rl.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/scenario.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/sync.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/tests/__init__.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/tests/test_analysis_utils.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/tests/test_analyze.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/tests/test_analyze_metadata.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/tests/test_analyze_module.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/tests/test_build.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/tests/test_build_failure.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/tests/test_build_module.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/tests/test_cli_init.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/tests/test_cli_main.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/tests/test_cli_more_wrappers.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/tests/test_cli_root.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/tests/test_convert.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/tests/test_debug.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/tests/test_debug_directory_mode.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/tests/test_deploy.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/tests/test_dev.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/tests/test_eval.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/tests/test_eval_bedrock.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/tests/test_init.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/tests/test_lockfile_utils.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/tests/test_main_module.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/tests/test_mcp_server.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/tests/test_push.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/tests/test_push_happy.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/tests/test_push_wrapper.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/tests/test_rl.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/tests/test_scenario.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/tests/test_sync.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/tests/test_utils.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/utils/__init__.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/utils/analysis.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/utils/api.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/utils/args.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/utils/build_display.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/utils/build_logs.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/utils/collect.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/utils/config.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/utils/context.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/utils/docker.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/utils/env_check.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/utils/environment.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/utils/git.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/utils/interactive.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/utils/lockfile.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/utils/logging.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/utils/metadata.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/utils/name_check.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/utils/project_config.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/utils/server.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/utils/source_hash.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/utils/tasks.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/utils/taskset.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/utils/tests/__init__.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/utils/tests/test_collect.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/utils/tests/test_config.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/utils/tests/test_docker.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/utils/tests/test_docker_hints.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/utils/tests/test_env_check.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/utils/tests/test_environment.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/utils/tests/test_git.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/utils/tests/test_interactive_module.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/utils/tests/test_logging_utils.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/utils/tests/test_metadata.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/utils/tests/test_source_hash.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/utils/tests/test_tasks.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/utils/validation.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/utils/version_check.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/cli/utils/viewer.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/datasets/__init__.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/datasets/loader.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/datasets/runner.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/datasets/tests/__init__.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/datasets/tests/test_loader.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/datasets/tests/test_utils.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/datasets/utils.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/environment/__init__.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/environment/connection.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/environment/connectors/__init__.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/environment/connectors/base.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/environment/connectors/local.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/environment/connectors/mcp_config.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/environment/connectors/openai.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/environment/connectors/remote.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/environment/integrations/__init__.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/environment/integrations/adk.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/environment/integrations/anthropic.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/environment/integrations/gemini.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/environment/integrations/langchain.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/environment/integrations/llamaindex.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/environment/integrations/openai.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/environment/mock.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/environment/router.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/environment/scenarios.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/environment/tests/__init__.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/environment/tests/test_connection.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/environment/tests/test_connectors.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/environment/tests/test_integrations.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/environment/tests/test_local_connectors.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/environment/tests/test_scenarios.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/environment/tests/test_session_id.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/environment/tests/test_tools.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/environment/types.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/environment/utils/__init__.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/environment/utils/formats.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/environment/utils/schema.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/environment/utils/tool_wrappers.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/eval/__init__.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/eval/context.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/eval/display.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/eval/instrument.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/eval/manager.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/eval/parallel.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/eval/task.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/eval/tests/__init__.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/eval/tests/test_context.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/eval/tests/test_eval.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/eval/tests/test_manager.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/eval/tests/test_parallel.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/eval/tests/test_task.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/eval/types.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/eval/utils.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/native/__init__.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/native/chat.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/native/graders.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/native/permissions.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/native/skills.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/native/tests/__init__.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/native/tests/test_graders.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/patches/__init__.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/patches/mcp_patches.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/patches/warnings.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/py.typed +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/server/__init__.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/server/context.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/server/helper/__init__.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/server/low_level.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/server/router.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/server/server.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/server/tests/__init__.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/server/tests/test_add_tool.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/server/tests/test_context.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/server/tests/test_mcp_server_handlers.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/server/tests/test_mcp_server_integration.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/server/tests/test_mcp_server_more.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/server/tests/test_prefix_naming.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/server/tests/test_run_wrapper.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/server/tests/test_server_extra.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/server/tests/test_sigterm_runner.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/services/__init__.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/services/chat.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/services/chat_service.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/services/reply_metadata.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/services/tests/__init__.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/services/tests/test_chat.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/services/tests/test_chat_service.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/settings.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/shared/__init__.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/shared/exceptions.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/shared/hints.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/shared/requests.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/shared/tests/__init__.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/shared/tests/test_exceptions.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/shared/tests/test_hints.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/shared/tests/test_requests.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/telemetry/__init__.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/telemetry/exporter.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/telemetry/instrument.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/telemetry/tests/__init__.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/telemetry/tests/test_eval_telemetry.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/telemetry/tests/test_exporter.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/telemetry/tests/test_instrument.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/__init__.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/agent.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/base.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/coding/__init__.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/coding/apply_patch.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/coding/bash.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/coding/edit.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/coding/gemini_edit.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/coding/gemini_shell.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/coding/gemini_write.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/coding/session.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/coding/shell.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/coding/tests/__init__.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/coding/tests/test_apply_patch.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/coding/tests/test_bash.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/coding/tests/test_bash_extended.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/coding/tests/test_bash_integration.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/coding/tests/test_edit.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/coding/tests/test_gemini_tools.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/coding/tests/test_shell.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/coding/utils.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/computer/__init__.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/computer/anthropic.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/computer/glm.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/computer/hud.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/computer/openai.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/computer/qwen.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/computer/settings.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/computer/tests/__init__.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/computer/tests/test_compression.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/computer/tests/test_computer_actions.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/computer/tests/test_glm_computer.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/elicitation.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/executors/__init__.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/executors/tests/__init__.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/executors/tests/test_base_executor.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/filesystem/__init__.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/filesystem/base.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/filesystem/gemini.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/filesystem/gemini_read_many.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/filesystem/glob.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/filesystem/grep.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/filesystem/list.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/filesystem/read.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/filesystem/tests/__init__.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/filesystem/tests/test_glob.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/filesystem/tests/test_grep.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/filesystem/tests/test_list.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/filesystem/tests/test_read.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/filesystem/tests/test_read_many.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/grounding/__init__.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/grounding/config.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/grounding/grounded_tool.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/grounding/grounder.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/grounding/tests/__init__.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/grounding/tests/test_grounded_tool.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/hosted/__init__.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/hosted/base.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/hosted/code_execution.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/hosted/google_search.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/hosted/tool_search.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/hosted/url_context.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/hosted/web_fetch.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/hosted/web_search.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/jupyter.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/memory/__init__.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/memory/base.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/memory/claude.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/memory/gemini.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/memory/session.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/memory/tests/__init__.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/memory/tests/test_claude.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/memory/tests/test_gemini.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/memory/tests/test_session.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/native_types.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/playwright.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/response.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/submit.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/tests/__init__.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/tests/test_agent_tool.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/tests/test_base.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/tests/test_elicitation.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/tests/test_init.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/tests/test_jupyter_tool.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/tests/test_native_tool_e2e.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/tests/test_native_types.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/tests/test_playwright_tool.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/tests/test_response.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/tests/test_submit.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/tests/test_tools.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/tests/test_tools_init.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/tests/test_types.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/tests/test_utils.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/types.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/tools/utils.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/types.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/utils/__init__.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/utils/env.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/utils/hud_console.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/utils/mcp.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/utils/pretty_errors.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/utils/serialization.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/utils/strict_schema.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/utils/tests/__init__.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/utils/tests/test_init.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/utils/tests/test_pretty_errors.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/utils/tests/test_serialization.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/utils/tests/test_tool_shorthand.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/utils/tool_shorthand.py +0 -0
- {hud_python-0.5.40 → hud_python-0.5.41}/hud/utils/types.py +0 -0
|
@@ -12,6 +12,8 @@ import httpx
|
|
|
12
12
|
import questionary
|
|
13
13
|
import typer
|
|
14
14
|
|
|
15
|
+
from hud.cli.utils.api import hud_headers
|
|
16
|
+
from hud.settings import settings
|
|
15
17
|
from hud.utils.hud_console import HUDConsole
|
|
16
18
|
|
|
17
19
|
# Presets mapping to public GitHub repositories under hud-evals org
|
|
@@ -22,6 +24,8 @@ PRESET_MAP: dict[str, str | None] = {
|
|
|
22
24
|
"blank": "hud-blank",
|
|
23
25
|
"deep-research": "hud-deepresearch",
|
|
24
26
|
"browser": "hud-browser",
|
|
27
|
+
"remote-browser": "hud-remote-browser",
|
|
28
|
+
"coding": "coding-template",
|
|
25
29
|
"rubrics": "hud-rubrics",
|
|
26
30
|
"verilog-coding-template": "verilog-coding-template",
|
|
27
31
|
"data-science-template": "data-science-template",
|
|
@@ -86,34 +90,53 @@ def _replace_placeholders(target_dir: Path, env_name: str) -> list[str]:
|
|
|
86
90
|
return modified_files
|
|
87
91
|
|
|
88
92
|
|
|
89
|
-
def
|
|
93
|
+
def _fetch_available_templates() -> tuple[list[dict], list[dict]]:
|
|
94
|
+
"""Fetch available templates from the HUD API.
|
|
95
|
+
|
|
96
|
+
Returns (public_templates, private_templates). Falls back to empty
|
|
97
|
+
private list if the API is unreachable or the user has no API key.
|
|
98
|
+
"""
|
|
99
|
+
if not settings.api_key:
|
|
100
|
+
return [], []
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
with httpx.Client(timeout=10) as client:
|
|
104
|
+
resp = client.get(
|
|
105
|
+
f"{settings.hud_api_url}/templates/available",
|
|
106
|
+
headers=hud_headers(),
|
|
107
|
+
)
|
|
108
|
+
if resp.status_code != 200:
|
|
109
|
+
return [], []
|
|
110
|
+
data = resp.json()
|
|
111
|
+
return data.get("public_templates", []), data.get("private_templates", [])
|
|
112
|
+
except Exception:
|
|
113
|
+
return [], []
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _prompt_for_preset() -> tuple[str, bool] | None:
|
|
90
117
|
"""Ask the user to choose a preset when not provided.
|
|
91
118
|
|
|
92
|
-
Returns None if the user cancels
|
|
119
|
+
Returns (preset_id, is_private) or None if the user cancels.
|
|
93
120
|
"""
|
|
121
|
+
# Fetch private templates from API
|
|
122
|
+
_, private_templates = _fetch_available_templates()
|
|
123
|
+
|
|
94
124
|
try:
|
|
95
|
-
choices = [
|
|
96
|
-
|
|
97
|
-
{"name": "browser", "message": "browser"},
|
|
98
|
-
{"name": "deep-research", "message": "deep-research"},
|
|
99
|
-
{"name": "rubrics", "message": "rubrics"},
|
|
100
|
-
{"name": "verilog-coding-template", "message": "verilog-coding-template"},
|
|
101
|
-
{"name": "data-science-template", "message": "data-science-template"},
|
|
125
|
+
choices = [questionary.Choice(title=key, value=(key, False)) for key in PRESET_MAP] + [
|
|
126
|
+
questionary.Choice(title=t["id"], value=(t["id"], True)) for t in private_templates
|
|
102
127
|
]
|
|
103
|
-
|
|
128
|
+
|
|
104
129
|
selected = questionary.select(
|
|
105
|
-
"Choose a preset",
|
|
130
|
+
"Choose a preset",
|
|
131
|
+
choices=choices,
|
|
106
132
|
).ask()
|
|
107
133
|
if not selected:
|
|
108
134
|
return None # User cancelled
|
|
109
|
-
|
|
110
|
-
if c["message"] == selected:
|
|
111
|
-
return c["name"]
|
|
112
|
-
return "blank"
|
|
135
|
+
return selected
|
|
113
136
|
except KeyboardInterrupt:
|
|
114
137
|
return None # User pressed Ctrl+C
|
|
115
138
|
except Exception:
|
|
116
|
-
return "blank"
|
|
139
|
+
return ("blank", False)
|
|
117
140
|
|
|
118
141
|
|
|
119
142
|
def _download_tarball_repo(
|
|
@@ -142,6 +165,32 @@ def _download_tarball_repo(
|
|
|
142
165
|
tmp_file.write(chunk)
|
|
143
166
|
tmp_path = Path(tmp_file.name)
|
|
144
167
|
|
|
168
|
+
_extract_tarball(tmp_path, dest_dir, files_created)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _download_private_template(template_id: str, dest_dir: Path, files_created: list[str]) -> None:
|
|
172
|
+
"""Download a private template tarball from the HUD API."""
|
|
173
|
+
url = f"{settings.hud_api_url}/templates/private/{template_id}/download"
|
|
174
|
+
|
|
175
|
+
with (
|
|
176
|
+
tempfile.NamedTemporaryFile(delete=False) as tmp_file,
|
|
177
|
+
httpx.Client(timeout=120) as client,
|
|
178
|
+
client.stream("GET", url, headers=hud_headers()) as resp,
|
|
179
|
+
):
|
|
180
|
+
if resp.status_code == 403:
|
|
181
|
+
raise RuntimeError("Access denied: your team does not have access to this template.")
|
|
182
|
+
if resp.status_code != 200:
|
|
183
|
+
raise RuntimeError(f"Failed to download private template (HTTP {resp.status_code})")
|
|
184
|
+
for chunk in resp.iter_bytes():
|
|
185
|
+
if chunk:
|
|
186
|
+
tmp_file.write(chunk)
|
|
187
|
+
tmp_path = Path(tmp_file.name)
|
|
188
|
+
|
|
189
|
+
_extract_tarball(tmp_path, dest_dir, files_created)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _extract_tarball(tmp_path: Path, dest_dir: Path, files_created: list[str]) -> None:
|
|
193
|
+
"""Extract a tarball into dest_dir, stripping the top-level directory."""
|
|
145
194
|
try:
|
|
146
195
|
with tarfile.open(tmp_path, mode="r:gz") as tar:
|
|
147
196
|
members = tar.getmembers()
|
|
@@ -191,15 +240,26 @@ def create_environment(
|
|
|
191
240
|
|
|
192
241
|
hud_console = HUDConsole()
|
|
193
242
|
|
|
243
|
+
is_private = False
|
|
244
|
+
|
|
194
245
|
# Choose preset
|
|
195
246
|
if preset:
|
|
196
|
-
|
|
247
|
+
preset_stripped = preset.strip()
|
|
248
|
+
preset_normalized = preset_stripped.lower()
|
|
249
|
+
# Check if the preset matches a private template (case-insensitive)
|
|
250
|
+
_, private_templates = _fetch_available_templates()
|
|
251
|
+
for t in private_templates:
|
|
252
|
+
if t["id"].lower() == preset_normalized:
|
|
253
|
+
# Preserve the original API ID for case-sensitive downstream use
|
|
254
|
+
preset_normalized = t["id"]
|
|
255
|
+
is_private = True
|
|
256
|
+
break
|
|
197
257
|
else:
|
|
198
258
|
preset_result = _prompt_for_preset()
|
|
199
259
|
if preset_result is None:
|
|
200
260
|
# User cancelled the selection
|
|
201
261
|
raise typer.Exit(0)
|
|
202
|
-
preset_normalized = preset_result
|
|
262
|
+
preset_normalized, is_private = preset_result
|
|
203
263
|
|
|
204
264
|
# If no name is provided, use the preset name as the environment name
|
|
205
265
|
if name is None:
|
|
@@ -209,7 +269,7 @@ def create_environment(
|
|
|
209
269
|
# Always create a new directory based on the name
|
|
210
270
|
target_dir = Path.cwd() / name if directory == "." else Path(directory) / name
|
|
211
271
|
|
|
212
|
-
if preset_normalized not in PRESET_MAP:
|
|
272
|
+
if not is_private and preset_normalized not in PRESET_MAP:
|
|
213
273
|
available = ", ".join(sorted(PRESET_MAP.keys()))
|
|
214
274
|
hud_console.warning(
|
|
215
275
|
f"Unknown preset '{preset_normalized}', defaulting to 'blank' (available: {available})"
|
|
@@ -225,32 +285,45 @@ def create_environment(
|
|
|
225
285
|
else:
|
|
226
286
|
hud_console.warning(f"Overwriting existing files in {target_dir}")
|
|
227
287
|
|
|
228
|
-
# Download preset from GitHub
|
|
229
|
-
repo_name = PRESET_MAP[preset_normalized]
|
|
230
|
-
if repo_name is None:
|
|
231
|
-
hud_console.error("Internal error: preset mapping missing repo name")
|
|
232
|
-
raise typer.Exit(1)
|
|
233
|
-
|
|
234
288
|
hud_console.header(f"Initializing HUD Environment: {name} (preset: {preset_normalized})")
|
|
235
|
-
hud_console.section_title("Downloading template from GitHub")
|
|
236
|
-
source_url = f"https://github.com/{GITHUB_OWNER}/{repo_name}"
|
|
237
|
-
hud_console.info("Source: " + source_url)
|
|
238
|
-
|
|
239
289
|
target_dir.mkdir(parents=True, exist_ok=True)
|
|
240
290
|
|
|
241
291
|
started = time.time()
|
|
242
292
|
files_created_dl: list[str] = []
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
293
|
+
|
|
294
|
+
if is_private:
|
|
295
|
+
hud_console.section_title("Downloading private template from HUD")
|
|
296
|
+
try:
|
|
297
|
+
_download_private_template(
|
|
298
|
+
template_id=preset_normalized,
|
|
299
|
+
dest_dir=target_dir,
|
|
300
|
+
files_created=files_created_dl,
|
|
301
|
+
)
|
|
302
|
+
except Exception as e:
|
|
303
|
+
hud_console.error(f"Failed to download private template '{preset_normalized}': {e}")
|
|
304
|
+
raise typer.Exit(1) from None
|
|
305
|
+
else:
|
|
306
|
+
# Download preset from GitHub
|
|
307
|
+
repo_name = PRESET_MAP[preset_normalized]
|
|
308
|
+
if repo_name is None:
|
|
309
|
+
hud_console.error("Internal error: preset mapping missing repo name")
|
|
310
|
+
raise typer.Exit(1)
|
|
311
|
+
|
|
312
|
+
hud_console.section_title("Downloading template from GitHub")
|
|
313
|
+
source_url = f"https://github.com/{GITHUB_OWNER}/{repo_name}"
|
|
314
|
+
hud_console.info("Source: " + source_url)
|
|
315
|
+
|
|
316
|
+
try:
|
|
317
|
+
_download_tarball_repo(
|
|
318
|
+
owner=GITHUB_OWNER,
|
|
319
|
+
repo=repo_name,
|
|
320
|
+
ref=GITHUB_BRANCH,
|
|
321
|
+
dest_dir=target_dir,
|
|
322
|
+
files_created=files_created_dl,
|
|
323
|
+
)
|
|
324
|
+
except Exception as e:
|
|
325
|
+
hud_console.error(f"Failed to download preset '{preset_normalized}': {e}")
|
|
326
|
+
raise typer.Exit(1) from None
|
|
254
327
|
|
|
255
328
|
duration_ms = int((time.time() - started) * 1000)
|
|
256
329
|
hud_console.success(
|
|
@@ -258,7 +331,7 @@ def create_environment(
|
|
|
258
331
|
)
|
|
259
332
|
|
|
260
333
|
# Replace placeholders in template files (only for blank preset)
|
|
261
|
-
if preset_normalized == "blank":
|
|
334
|
+
if preset_normalized == "blank" and not is_private:
|
|
262
335
|
hud_console.section_title("Customizing template files")
|
|
263
336
|
modified_files = _replace_placeholders(target_dir, name)
|
|
264
337
|
if modified_files:
|
|
@@ -195,6 +195,58 @@ class Environment(
|
|
|
195
195
|
# Core Methods
|
|
196
196
|
# =========================================================================
|
|
197
197
|
|
|
198
|
+
def _filtered_tools_for_session(self, session: Any) -> list[mcp_types.Tool]:
|
|
199
|
+
"""Apply scenario-level tool filtering for a given session.
|
|
200
|
+
|
|
201
|
+
Filters in order:
|
|
202
|
+
1. exclude_sources: remove tools from excluded connections
|
|
203
|
+
2. exclude_tools: remove tools matching fnmatch patterns
|
|
204
|
+
3. allowed_tools: rescue specific tools back from exclusions
|
|
205
|
+
|
|
206
|
+
Does NOT apply agent-level filtering (_agent_include/_agent_exclude).
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
session: The ScenarioSession to filter for, or None (no filtering).
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
List of tools visible under the session's exclusions.
|
|
213
|
+
"""
|
|
214
|
+
import fnmatch
|
|
215
|
+
|
|
216
|
+
tools = self._router.tools
|
|
217
|
+
|
|
218
|
+
if not session:
|
|
219
|
+
return tools
|
|
220
|
+
|
|
221
|
+
excluded_sources = set(session.exclude_sources) if session.exclude_sources else None
|
|
222
|
+
excluded_patterns = session.exclude_tools
|
|
223
|
+
|
|
224
|
+
if excluded_sources or excluded_patterns:
|
|
225
|
+
filtered = []
|
|
226
|
+
for tool in tools:
|
|
227
|
+
if excluded_sources:
|
|
228
|
+
source = self._router._tool_routing.get(tool.name, "")
|
|
229
|
+
if source in excluded_sources:
|
|
230
|
+
continue
|
|
231
|
+
if excluded_patterns and any(
|
|
232
|
+
fnmatch.fnmatch(tool.name, pat) for pat in excluded_patterns
|
|
233
|
+
):
|
|
234
|
+
continue
|
|
235
|
+
filtered.append(tool)
|
|
236
|
+
tools = filtered
|
|
237
|
+
|
|
238
|
+
# Rescue: add back tools matching allowed_tools patterns
|
|
239
|
+
allowed_patterns = session.allowed_tools
|
|
240
|
+
if allowed_patterns:
|
|
241
|
+
visible_names = {t.name for t in tools}
|
|
242
|
+
for tool in self._router.tools:
|
|
243
|
+
if tool.name not in visible_names and any(
|
|
244
|
+
fnmatch.fnmatch(tool.name, pat) for pat in allowed_patterns
|
|
245
|
+
):
|
|
246
|
+
tools.append(tool)
|
|
247
|
+
|
|
248
|
+
return tools
|
|
249
|
+
|
|
198
250
|
def as_tools(self) -> list[mcp_types.Tool]:
|
|
199
251
|
"""Return tools in MCP format (base format).
|
|
200
252
|
|
|
@@ -207,37 +259,7 @@ class Environment(
|
|
|
207
259
|
"""
|
|
208
260
|
import fnmatch
|
|
209
261
|
|
|
210
|
-
tools = self.
|
|
211
|
-
|
|
212
|
-
# Scenario-level exclusion (from @env.scenario(exclude_tools/exclude_sources))
|
|
213
|
-
session = self._active_session
|
|
214
|
-
if session:
|
|
215
|
-
excluded_sources = set(session.exclude_sources) if session.exclude_sources else None
|
|
216
|
-
excluded_patterns = session.exclude_tools
|
|
217
|
-
|
|
218
|
-
if excluded_sources or excluded_patterns:
|
|
219
|
-
filtered = []
|
|
220
|
-
for tool in tools:
|
|
221
|
-
if excluded_sources:
|
|
222
|
-
source = self._router._tool_routing.get(tool.name, "")
|
|
223
|
-
if source in excluded_sources:
|
|
224
|
-
continue
|
|
225
|
-
if excluded_patterns and any(
|
|
226
|
-
fnmatch.fnmatch(tool.name, pat) for pat in excluded_patterns
|
|
227
|
-
):
|
|
228
|
-
continue
|
|
229
|
-
filtered.append(tool)
|
|
230
|
-
tools = filtered
|
|
231
|
-
|
|
232
|
-
# Rescue: add back tools matching allowed_tools patterns
|
|
233
|
-
allowed_patterns = session.allowed_tools
|
|
234
|
-
if allowed_patterns:
|
|
235
|
-
visible_names = {t.name for t in tools}
|
|
236
|
-
for tool in self._router.tools:
|
|
237
|
-
if tool.name not in visible_names and any(
|
|
238
|
-
fnmatch.fnmatch(tool.name, pat) for pat in allowed_patterns
|
|
239
|
-
):
|
|
240
|
-
tools.append(tool)
|
|
262
|
+
tools = self._filtered_tools_for_session(self._active_session)
|
|
241
263
|
|
|
242
264
|
# Apply agent-level filtering (from v4 allowed_tools/disallowed_tools)
|
|
243
265
|
if self._agent_include is not None or self._agent_exclude is not None:
|
|
@@ -628,10 +650,16 @@ class Environment(
|
|
|
628
650
|
return mcp_types.ReadResourceResult(contents=contents)
|
|
629
651
|
|
|
630
652
|
async def _env_list_tools(self) -> list[mcp_types.Tool]:
|
|
631
|
-
"""Return
|
|
653
|
+
"""Return tools filtered by the active scenario session (if any).
|
|
654
|
+
|
|
655
|
+
When an MCP client has an active scenario session (set via get_prompt),
|
|
656
|
+
applies scenario-level tool exclusions so the agent only sees permitted tools.
|
|
657
|
+
"""
|
|
632
658
|
if not self._tool_routing_built:
|
|
633
659
|
await self._build_tool_routing()
|
|
634
|
-
|
|
660
|
+
session_id = _safe_session_id(None)
|
|
661
|
+
session = self._get_session(session_id)
|
|
662
|
+
return self._filtered_tools_for_session(session)
|
|
635
663
|
|
|
636
664
|
async def _env_list_prompts(self) -> list[mcp_types.Prompt]:
|
|
637
665
|
"""Return all prompts including those from connectors."""
|
|
@@ -649,6 +677,19 @@ class Environment(
|
|
|
649
677
|
"""Route tool calls through our router (handles both local and connector tools)."""
|
|
650
678
|
args = dict(arguments or {})
|
|
651
679
|
|
|
680
|
+
# Enforce scenario-level tool exclusions for MCP clients.
|
|
681
|
+
# Internal tools (underscore prefix, e.g. _hud_submit) are always allowed
|
|
682
|
+
# as they are infrastructure tools, not agent-facing.
|
|
683
|
+
if not name.startswith("_"):
|
|
684
|
+
session_id = _safe_session_id(None)
|
|
685
|
+
session = self._get_session(session_id)
|
|
686
|
+
if session:
|
|
687
|
+
if not self._tool_routing_built:
|
|
688
|
+
await self._build_tool_routing()
|
|
689
|
+
allowed_names = {t.name for t in self._filtered_tools_for_session(session)}
|
|
690
|
+
if name not in allowed_names:
|
|
691
|
+
raise ValueError(f"Tool '{name}' is not available in the current scenario.")
|
|
692
|
+
|
|
652
693
|
# Extract trace context propagated via MCP request (meta or arguments)
|
|
653
694
|
trace_id = args.pop("_hud_trace_id", None)
|
|
654
695
|
meta = kwargs.get("_meta") or kwargs.get("meta")
|
|
@@ -740,3 +740,254 @@ class TestEnvironmentToolFiltering:
|
|
|
740
740
|
assert "browser_navigate" in tool_names
|
|
741
741
|
assert "browser_setup" not in tool_names # Excluded by *setup*
|
|
742
742
|
assert "file_read" not in tool_names # Not included by browser_*
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
class TestMCPServerToolExclusion:
|
|
746
|
+
"""Tests that scenario exclude_tools/exclude_sources/allowed_tools
|
|
747
|
+
are enforced on the MCP server path (_env_list_tools, _env_call_tool).
|
|
748
|
+
"""
|
|
749
|
+
|
|
750
|
+
@pytest.mark.asyncio
|
|
751
|
+
async def test_env_list_tools_applies_scenario_filtering(self) -> None:
|
|
752
|
+
"""_env_list_tools resolves the MCP session and applies scenario filtering.
|
|
753
|
+
|
|
754
|
+
The filtering logic itself (exclude_tools, exclude_sources, allowed_tools)
|
|
755
|
+
is tested thoroughly in test_scenarios.py::TestScenarioToolExclusion.
|
|
756
|
+
This test verifies the MCP server path wires up session lookup correctly.
|
|
757
|
+
"""
|
|
758
|
+
from types import SimpleNamespace
|
|
759
|
+
|
|
760
|
+
import mcp.types as mcp_types
|
|
761
|
+
from mcp.server.lowlevel.server import request_ctx
|
|
762
|
+
|
|
763
|
+
from hud.environment import Environment
|
|
764
|
+
from hud.environment.connection import ConnectionConfig, ConnectionType, Connector
|
|
765
|
+
|
|
766
|
+
env = Environment("test-env")
|
|
767
|
+
|
|
768
|
+
@env.tool()
|
|
769
|
+
def browser_navigate(url: str) -> str:
|
|
770
|
+
"""Navigate."""
|
|
771
|
+
return url
|
|
772
|
+
|
|
773
|
+
@env.tool()
|
|
774
|
+
def browser_screenshot() -> str:
|
|
775
|
+
"""Screenshot."""
|
|
776
|
+
return "img"
|
|
777
|
+
|
|
778
|
+
@env.tool()
|
|
779
|
+
def bash(cmd: str) -> str:
|
|
780
|
+
"""Run command."""
|
|
781
|
+
return cmd
|
|
782
|
+
|
|
783
|
+
connector = Connector(
|
|
784
|
+
transport={},
|
|
785
|
+
config=ConnectionConfig(),
|
|
786
|
+
name="remote-hub",
|
|
787
|
+
connection_type=ConnectionType.REMOTE,
|
|
788
|
+
)
|
|
789
|
+
connector._tools_cache = [
|
|
790
|
+
mcp_types.Tool(name="remote_a", inputSchema={"type": "object"}),
|
|
791
|
+
]
|
|
792
|
+
env._connections["remote-hub"] = connector
|
|
793
|
+
|
|
794
|
+
@env.scenario(
|
|
795
|
+
"filtered",
|
|
796
|
+
exclude_tools=["browser_*"],
|
|
797
|
+
exclude_sources=["remote-hub"],
|
|
798
|
+
allowed_tools=["browser_navigate"],
|
|
799
|
+
)
|
|
800
|
+
async def filtered():
|
|
801
|
+
yield "Do it"
|
|
802
|
+
yield 1.0
|
|
803
|
+
|
|
804
|
+
await env._build_routing()
|
|
805
|
+
|
|
806
|
+
req = SimpleNamespace(
|
|
807
|
+
session=SimpleNamespace(),
|
|
808
|
+
request=SimpleNamespace(headers={"mcp-session-id": "test-session"}),
|
|
809
|
+
)
|
|
810
|
+
token = request_ctx.set(req) # type: ignore[arg-type]
|
|
811
|
+
try:
|
|
812
|
+
await env._env_get_prompt("test-env:filtered", {})
|
|
813
|
+
tools = await env._env_list_tools()
|
|
814
|
+
finally:
|
|
815
|
+
request_ctx.reset(token)
|
|
816
|
+
|
|
817
|
+
tool_names = [t.name for t in tools]
|
|
818
|
+
assert "bash" in tool_names
|
|
819
|
+
assert "browser_navigate" in tool_names # Rescued by allowed_tools
|
|
820
|
+
assert "browser_screenshot" not in tool_names # Excluded by pattern
|
|
821
|
+
assert "remote_a" not in tool_names # Excluded by source
|
|
822
|
+
|
|
823
|
+
@pytest.mark.asyncio
|
|
824
|
+
async def test_env_call_tool_rejects_excluded_tool(self) -> None:
|
|
825
|
+
"""_env_call_tool raises ValueError for excluded tools."""
|
|
826
|
+
from types import SimpleNamespace
|
|
827
|
+
|
|
828
|
+
from mcp.server.lowlevel.server import request_ctx
|
|
829
|
+
|
|
830
|
+
from hud.environment import Environment
|
|
831
|
+
|
|
832
|
+
env = Environment("test-env")
|
|
833
|
+
|
|
834
|
+
@env.tool()
|
|
835
|
+
def browser_navigate(url: str) -> str:
|
|
836
|
+
"""Navigate."""
|
|
837
|
+
return url
|
|
838
|
+
|
|
839
|
+
@env.tool()
|
|
840
|
+
def bash(cmd: str) -> str:
|
|
841
|
+
"""Run command."""
|
|
842
|
+
return cmd
|
|
843
|
+
|
|
844
|
+
@env.scenario("headless", exclude_tools=["browser_*"])
|
|
845
|
+
async def headless():
|
|
846
|
+
yield "Do it"
|
|
847
|
+
yield 1.0
|
|
848
|
+
|
|
849
|
+
await env._build_routing()
|
|
850
|
+
|
|
851
|
+
req = SimpleNamespace(
|
|
852
|
+
session=SimpleNamespace(),
|
|
853
|
+
request=SimpleNamespace(headers={"mcp-session-id": "test-session-4"}),
|
|
854
|
+
)
|
|
855
|
+
token = request_ctx.set(req) # type: ignore[arg-type]
|
|
856
|
+
try:
|
|
857
|
+
await env._env_get_prompt("test-env:headless", {})
|
|
858
|
+
with pytest.raises(ValueError, match="not available"):
|
|
859
|
+
await env._env_call_tool("browser_navigate", {"url": "http://example.com"})
|
|
860
|
+
finally:
|
|
861
|
+
request_ctx.reset(token)
|
|
862
|
+
|
|
863
|
+
@pytest.mark.asyncio
|
|
864
|
+
async def test_env_call_tool_allows_non_excluded_tool(self) -> None:
|
|
865
|
+
"""_env_call_tool succeeds for non-excluded tools."""
|
|
866
|
+
from types import SimpleNamespace
|
|
867
|
+
|
|
868
|
+
from mcp.server.lowlevel.server import request_ctx
|
|
869
|
+
|
|
870
|
+
from hud.environment import Environment
|
|
871
|
+
|
|
872
|
+
env = Environment("test-env")
|
|
873
|
+
|
|
874
|
+
@env.tool()
|
|
875
|
+
def browser_navigate(url: str) -> str:
|
|
876
|
+
"""Navigate."""
|
|
877
|
+
return url
|
|
878
|
+
|
|
879
|
+
@env.tool()
|
|
880
|
+
def bash(cmd: str) -> str:
|
|
881
|
+
"""Run command."""
|
|
882
|
+
return cmd
|
|
883
|
+
|
|
884
|
+
@env.scenario("headless", exclude_tools=["browser_*"])
|
|
885
|
+
async def headless():
|
|
886
|
+
yield "Do it"
|
|
887
|
+
yield 1.0
|
|
888
|
+
|
|
889
|
+
await env._build_routing()
|
|
890
|
+
|
|
891
|
+
req = SimpleNamespace(
|
|
892
|
+
session=SimpleNamespace(),
|
|
893
|
+
request=SimpleNamespace(headers={"mcp-session-id": "test-session-5"}, scope={}),
|
|
894
|
+
)
|
|
895
|
+
token = request_ctx.set(req) # type: ignore[arg-type]
|
|
896
|
+
try:
|
|
897
|
+
await env._env_get_prompt("test-env:headless", {})
|
|
898
|
+
# Should not raise - bash is not excluded
|
|
899
|
+
result = await env._env_call_tool("bash", {"cmd": "echo hi"})
|
|
900
|
+
assert result is not None
|
|
901
|
+
finally:
|
|
902
|
+
request_ctx.reset(token)
|
|
903
|
+
|
|
904
|
+
@pytest.mark.asyncio
|
|
905
|
+
async def test_env_call_tool_allows_internal_tools(self) -> None:
|
|
906
|
+
"""_env_call_tool always allows underscore-prefixed internal tools."""
|
|
907
|
+
from types import SimpleNamespace
|
|
908
|
+
|
|
909
|
+
from mcp.server.lowlevel.server import request_ctx
|
|
910
|
+
|
|
911
|
+
from hud.environment import Environment
|
|
912
|
+
|
|
913
|
+
env = Environment("test-env")
|
|
914
|
+
|
|
915
|
+
@env.tool()
|
|
916
|
+
def browser_navigate(url: str) -> str:
|
|
917
|
+
"""Navigate."""
|
|
918
|
+
return url
|
|
919
|
+
|
|
920
|
+
@env.scenario("headless", exclude_tools=["*"])
|
|
921
|
+
async def headless():
|
|
922
|
+
answer = yield "Do it"
|
|
923
|
+
yield 1.0 if answer == "ok" else 0.0
|
|
924
|
+
|
|
925
|
+
await env._build_routing()
|
|
926
|
+
|
|
927
|
+
req = SimpleNamespace(
|
|
928
|
+
session=SimpleNamespace(),
|
|
929
|
+
request=SimpleNamespace(headers={"mcp-session-id": "test-session-6"}, scope={}),
|
|
930
|
+
)
|
|
931
|
+
token = request_ctx.set(req) # type: ignore[arg-type]
|
|
932
|
+
try:
|
|
933
|
+
await env._env_get_prompt("test-env:headless", {})
|
|
934
|
+
# _hud_submit should always work even with exclude_tools=["*"]
|
|
935
|
+
result = await env._env_call_tool(
|
|
936
|
+
"_hud_submit", {"scenario": "headless", "answer": "ok"}
|
|
937
|
+
)
|
|
938
|
+
assert result is not None
|
|
939
|
+
finally:
|
|
940
|
+
request_ctx.reset(token)
|
|
941
|
+
|
|
942
|
+
@pytest.mark.asyncio
|
|
943
|
+
async def test_env_list_tools_no_session_returns_all(self) -> None:
|
|
944
|
+
"""_env_list_tools returns all tools when no scenario session is active."""
|
|
945
|
+
from hud.environment import Environment
|
|
946
|
+
|
|
947
|
+
env = Environment("test-env")
|
|
948
|
+
|
|
949
|
+
@env.tool()
|
|
950
|
+
def browser_navigate(url: str) -> str:
|
|
951
|
+
"""Navigate."""
|
|
952
|
+
return url
|
|
953
|
+
|
|
954
|
+
@env.tool()
|
|
955
|
+
def bash(cmd: str) -> str:
|
|
956
|
+
"""Run command."""
|
|
957
|
+
return cmd
|
|
958
|
+
|
|
959
|
+
@env.scenario("headless", exclude_tools=["browser_*"])
|
|
960
|
+
async def headless():
|
|
961
|
+
yield "Do it"
|
|
962
|
+
yield 1.0
|
|
963
|
+
|
|
964
|
+
await env._build_routing()
|
|
965
|
+
|
|
966
|
+
# No scenario setup, no request_ctx - should return all tools
|
|
967
|
+
tools = await env._env_list_tools()
|
|
968
|
+
tool_names = [t.name for t in tools]
|
|
969
|
+
assert "browser_navigate" in tool_names
|
|
970
|
+
assert "bash" in tool_names
|
|
971
|
+
|
|
972
|
+
@pytest.mark.asyncio
|
|
973
|
+
async def test_env_call_tool_no_session_allows_all(self) -> None:
|
|
974
|
+
"""_env_call_tool allows any tool when no scenario session is active."""
|
|
975
|
+
from hud.environment import Environment
|
|
976
|
+
|
|
977
|
+
env = Environment("test-env")
|
|
978
|
+
|
|
979
|
+
@env.tool()
|
|
980
|
+
def browser_navigate(url: str) -> str:
|
|
981
|
+
"""Navigate."""
|
|
982
|
+
return url
|
|
983
|
+
|
|
984
|
+
@env.scenario("headless", exclude_tools=["browser_*"])
|
|
985
|
+
async def headless():
|
|
986
|
+
yield "Do it"
|
|
987
|
+
yield 1.0
|
|
988
|
+
|
|
989
|
+
await env._build_routing()
|
|
990
|
+
|
|
991
|
+
# No scenario setup - should allow any tool
|
|
992
|
+
result = await env._env_call_tool("browser_navigate", {"url": "http://example.com"})
|
|
993
|
+
assert result is not None
|
|
@@ -22,6 +22,9 @@ if TYPE_CHECKING:
|
|
|
22
22
|
|
|
23
23
|
logger = logging.getLogger(__name__)
|
|
24
24
|
|
|
25
|
+
GEMINI_DRAG_INSET = 25
|
|
26
|
+
DISPLAY_DRAG_INSET_PIXELS = 20
|
|
27
|
+
|
|
25
28
|
SUPPORTED_GEMINI_COMPUTER_USE_MODELS = (
|
|
26
29
|
"gemini-2.5-computer-use-preview-10-2025",
|
|
27
30
|
"gemini-3-flash-preview",
|
|
@@ -168,6 +171,30 @@ class GeminiComputerTool(HudComputerTool):
|
|
|
168
171
|
**kwargs,
|
|
169
172
|
)
|
|
170
173
|
|
|
174
|
+
def _inset_drag_coordinate(self, value: int) -> int:
|
|
175
|
+
"""Keep Gemini normalized drag endpoints away from display edges."""
|
|
176
|
+
if (
|
|
177
|
+
self.coordinate_space is None
|
|
178
|
+
or not isinstance(value, int | float)
|
|
179
|
+
or not 0 <= value <= self.coordinate_space
|
|
180
|
+
):
|
|
181
|
+
return value
|
|
182
|
+
|
|
183
|
+
max_value = max(self.coordinate_space - GEMINI_DRAG_INSET, GEMINI_DRAG_INSET)
|
|
184
|
+
return min(max(value, GEMINI_DRAG_INSET), max_value)
|
|
185
|
+
|
|
186
|
+
def _inset_scaled_drag_path(self, path: list[tuple[int, int]]) -> list[tuple[int, int]]:
|
|
187
|
+
"""Keep scaled drag points inside the display so they do not hit OS/window edges."""
|
|
188
|
+
max_x = max(self.environment_width - 1 - DISPLAY_DRAG_INSET_PIXELS, 0)
|
|
189
|
+
max_y = max(self.environment_height - 1 - DISPLAY_DRAG_INSET_PIXELS, 0)
|
|
190
|
+
return [
|
|
191
|
+
(
|
|
192
|
+
min(max(int(x), DISPLAY_DRAG_INSET_PIXELS), max_x),
|
|
193
|
+
min(max(int(y), DISPLAY_DRAG_INSET_PIXELS), max_y),
|
|
194
|
+
)
|
|
195
|
+
for x, y in path
|
|
196
|
+
]
|
|
197
|
+
|
|
171
198
|
async def __call__(
|
|
172
199
|
self,
|
|
173
200
|
action: str = ACTION_FIELD,
|
|
@@ -381,7 +408,16 @@ class GeminiComputerTool(HudComputerTool):
|
|
|
381
408
|
message="x, y, destination_x, and destination_y are required",
|
|
382
409
|
)
|
|
383
410
|
)
|
|
384
|
-
path = self._scale_path(
|
|
411
|
+
path = self._scale_path(
|
|
412
|
+
[
|
|
413
|
+
(self._inset_drag_coordinate(x), self._inset_drag_coordinate(y)),
|
|
414
|
+
(
|
|
415
|
+
self._inset_drag_coordinate(destination_x),
|
|
416
|
+
self._inset_drag_coordinate(destination_y),
|
|
417
|
+
),
|
|
418
|
+
]
|
|
419
|
+
)
|
|
420
|
+
path = self._inset_scaled_drag_path(path)
|
|
385
421
|
result = await self.executor.drag(path=path)
|
|
386
422
|
return await _finalize(result)
|
|
387
423
|
|