hud 0.6.13.dev2__tar.gz → 0.6.14__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-0.6.13.dev2 → hud-0.6.14}/PKG-INFO +3 -2
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/__init__.py +4 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/claude/sdk/agent.py +73 -54
- hud-0.6.14/hud/agents/claude/sdk/computer_mcp.py +282 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/claude/tools/computer.py +11 -3
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/openai/tools/coding.py +13 -37
- hud-0.6.14/hud/agents/tests/test_claude_sdk_agent.py +658 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/tests/test_provider_native_tools.py +151 -51
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/tests/test_tool_agent.py +45 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/tool_agent.py +25 -3
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/tools/rfb.py +34 -5
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/tools/ssh.py +35 -3
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/capabilities/base.py +5 -1
- hud-0.6.14/hud/capabilities/ssh.py +278 -0
- hud-0.6.14/hud/capabilities/tests/test_ssh.py +372 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/__init__.py +28 -23
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/deploy.py +119 -73
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/eval.py +18 -5
- hud-0.6.14/hud/cli/qa.py +193 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/sync.py +5 -2
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/tests/test_deploy.py +136 -86
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/tests/test_eval_config.py +29 -2
- hud-0.6.14/hud/cli/tests/test_qa.py +171 -0
- hud-0.6.14/hud/cli/tests/test_sync_export.py +58 -0
- hud-0.6.14/hud/cli/tests/test_usage.py +134 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/utils/context.py +1 -1
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/utils/tests/test_context.py +1 -1
- hud-0.6.14/hud/cli/utils/usage.py +179 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/clients/client.py +24 -2
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/clients/tests/test_connect.py +118 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/conftest.py +1 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/environment/egress.py +42 -23
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/environment/env.py +5 -6
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/environment/namespace.py +136 -44
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/environment/tests/conftest.py +5 -5
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/environment/tests/test_capability_backing.py +55 -34
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/environment/tests/test_sessions.py +22 -8
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/environment/tests/test_workspace.py +851 -25
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/environment/workspace.py +330 -117
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/eval/__init__.py +4 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/eval/docker-seccomp.json +11 -1
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/eval/run.py +154 -41
- hud-0.6.14/hud/eval/runtime/__init__.py +40 -0
- hud-0.6.14/hud/eval/runtime/compose.py +598 -0
- hud-0.6.14/hud/eval/runtime/core.py +302 -0
- hud-0.6.14/hud/eval/runtime/daytona.py +299 -0
- hud-0.6.14/hud/eval/runtime/docker.py +390 -0
- hud-0.6.14/hud/eval/runtime/hosted.py +208 -0
- hud-0.6.14/hud/eval/runtime/hud.py +246 -0
- hud-0.6.14/hud/eval/runtime/local.py +217 -0
- hud-0.6.14/hud/eval/runtime/modal.py +372 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/eval/sync.py +11 -27
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/eval/task.py +50 -41
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/eval/taskset.py +72 -81
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/eval/tests/test_docker_provider.py +814 -67
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/eval/tests/test_hosted.py +304 -38
- hud-0.6.14/hud/eval/tests/test_local_runtime.py +273 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/eval/tests/test_rollout.py +403 -47
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/eval/tests/test_shared.py +72 -6
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/eval/tests/test_sync.py +45 -5
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/eval/tests/test_task.py +161 -15
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/Dockerfile +5 -3
- hud-0.6.14/hud/integrations/harbor/__init__.py +13 -0
- hud-0.6.14/hud/integrations/harbor/adapt.py +1148 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/env.py +438 -176
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/install.sh +4 -1
- hud-0.6.14/hud/integrations/harbor/tests/tasks/sidecar-reachability/environment/docker-compose.yaml +11 -0
- hud-0.6.14/hud/integrations/harbor/tests/tasks/sidecar-reachability/solution/solve.sh +39 -0
- hud-0.6.14/hud/integrations/harbor/tests/tasks/sidecar-reachability/task.toml +30 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/sidecar-reachability/tests/Dockerfile +4 -2
- hud-0.6.14/hud/integrations/harbor/tests/tasks/sidecar-reachability/tests/test.sh +32 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/verifier-lifecycle/tests/test.sh +4 -0
- hud-0.6.14/hud/integrations/harbor/tests/test_contract.py +1329 -0
- hud-0.6.14/hud/integrations/harbor/tests/test_integration.py +681 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/settings.py +12 -36
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/utils/platform.py +1 -1
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/utils/tests/test_platform.py +13 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/version.py +1 -1
- {hud-0.6.13.dev2 → hud-0.6.14}/pyproject.toml +1 -0
- hud-0.6.13.dev2/hud/agents/claude/sdk/computer_mcp.py +0 -147
- hud-0.6.13.dev2/hud/agents/tests/test_claude_sdk_agent.py +0 -241
- hud-0.6.13.dev2/hud/capabilities/ssh.py +0 -135
- hud-0.6.13.dev2/hud/cli/tests/test_sync_export.py +0 -27
- hud-0.6.13.dev2/hud/eval/_runtime_protocols.py +0 -212
- hud-0.6.13.dev2/hud/eval/compose.py +0 -172
- hud-0.6.13.dev2/hud/eval/runtime.py +0 -1667
- hud-0.6.13.dev2/hud/eval/tests/test_local_runtime.py +0 -440
- hud-0.6.13.dev2/hud/integrations/harbor/__init__.py +0 -12
- hud-0.6.13.dev2/hud/integrations/harbor/adapt.py +0 -734
- hud-0.6.13.dev2/hud/integrations/harbor/tests/tasks/sidecar-reachability/environment/compose.yaml +0 -11
- hud-0.6.13.dev2/hud/integrations/harbor/tests/tasks/sidecar-reachability/solution/solve.sh +0 -26
- hud-0.6.13.dev2/hud/integrations/harbor/tests/tasks/sidecar-reachability/task.toml +0 -26
- hud-0.6.13.dev2/hud/integrations/harbor/tests/tasks/sidecar-reachability/tests/test.sh +0 -19
- hud-0.6.13.dev2/hud/integrations/harbor/tests/test_contract.py +0 -896
- hud-0.6.13.dev2/hud/integrations/harbor/tests/test_integration.py +0 -339
- {hud-0.6.13.dev2 → hud-0.6.14}/.gitignore +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/LICENSE +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/README.md +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/__main__.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/__init__.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/base.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/browser_use/__init__.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/browser_use/agent.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/claude/__init__.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/claude/agent.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/claude/sdk/__init__.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/claude/tools/__init__.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/claude/tools/base.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/claude/tools/coding.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/claude/tools/hosted.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/claude/tools/mcp_proxy.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/claude/tools/settings.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/claude/tools/tests/__init__.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/claude/tools/tests/test_computer.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/gemini/__init__.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/gemini/agent.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/gemini/settings.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/gemini/tools/__init__.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/gemini/tools/base.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/gemini/tools/coding.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/gemini/tools/computer.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/gemini/tools/filesystem.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/gemini/tools/hosted.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/gemini/tools/mcp_proxy.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/gemini/tools/tests/__init__.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/gemini/tools/tests/test_computer.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/misc/__init__.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/misc/response_automation.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/openai/__init__.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/openai/agent.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/openai/tools/__init__.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/openai/tools/apply_patch.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/openai/tools/base.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/openai/tools/computer.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/openai/tools/hosted.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/openai/tools/mcp_proxy.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/openai/tools/strict_schema.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/openai/tools/tests/__init__.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/openai/tools/tests/test_computer.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/openai/tools/tests/test_strict_schema.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/openai_compatible/__init__.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/openai_compatible/agent.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/openai_compatible/tools/__init__.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/openai_compatible/tools/base.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/openai_compatible/tools/filesystem.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/openai_compatible/tools/mcp_proxy.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/robot/__init__.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/robot/adapter.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/robot/agent.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/robot/batching.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/robot/dataset.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/robot/model.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/tests/__init__.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/tests/test_apply_patch.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/tests/test_base.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/tests/test_claude_agent.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/tests/test_gemini_agent.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/tests/test_openai_agent.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/tests/test_openai_compatible_agent.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/tests/test_robot_dataset_writer.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/tests/test_trace.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/tools/__init__.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/tools/base.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/tools/hosted.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/tools/mcp.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/types.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/capabilities/__init__.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/capabilities/cdp.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/capabilities/mcp.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/capabilities/rfb.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/capabilities/robot.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/capabilities/tests/__init__.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/capabilities/tests/test_rfb.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/__main__.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/cancel.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/client.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/init.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/jobs.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/login.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/models.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/presets.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/serve.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/task.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/templates.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/tests/__init__.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/tests/test_cli_init.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/tests/test_cli_main.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/tests/test_cli_more_wrappers.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/tests/test_eval_bedrock.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/tests/test_init.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/tests/test_main_module.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/trace.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/utils/__init__.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/utils/api.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/utils/build_display.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/utils/build_logs.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/utils/config.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/utils/display.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/utils/jobs.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/utils/registry.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/utils/source.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/utils/tasks.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/utils/tests/__init__.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/utils/tests/test_build_display.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/utils/tests/test_config.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/utils/tests/test_registry.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/utils/tests/test_source.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/utils/tests/test_tasks.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/utils/tests/test_version_check.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/utils/version_check.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/clients/__init__.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/clients/tests/__init__.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/environment/__init__.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/environment/file_tracker.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/environment/robot/__init__.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/environment/robot/bridge.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/environment/robot/endpoint.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/environment/robot/gym.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/environment/server.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/environment/tests/__init__.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/environment/tests/test_file_tracker.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/environment/tests/test_file_tracking.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/environment/tests/test_loader.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/environment/tests/test_manifest.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/environment/tests/test_robot_regressions.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/environment/tests/test_server.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/environment/tests/test_tunnel.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/environment/utils.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/eval/chat.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/eval/file_tracking.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/eval/job.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/eval/tests/__init__.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/eval/tests/test_chat.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/eval/tests/test_file_tracking_observer.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/eval/tests/test_job.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/graders/__init__.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/graders/base.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/graders/bash.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/graders/combine.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/graders/judge.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/graders/results.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/graders/text.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/__init__.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/export.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/__init__.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/conftest.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/agent-lifecycle/environment/Dockerfile +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/agent-lifecycle/instruction.md +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/agent-lifecycle/solution/solve.sh +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/agent-lifecycle/task.toml +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/agent-lifecycle/tests/test.sh +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/hello-mcp/environment/Dockerfile +0 -0
- /hud-0.6.13.dev2/hud/integrations/harbor/tests/tasks/hello-mcp/environment/compose.yaml → /hud-0.6.14/hud/integrations/harbor/tests/tasks/hello-mcp/environment/docker-compose.yaml +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/hello-mcp/environment/mcp-server/Dockerfile +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/hello-mcp/environment/mcp-server/__init__.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/hello-mcp/environment/mcp-server/server.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/hello-mcp/instruction.md +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/hello-mcp/solution/solve.sh +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/hello-mcp/task.toml +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/hello-mcp/tests/test.sh +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/phase-boundary/environment/Dockerfile +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/phase-boundary/environment/entrypoint.sh +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/phase-boundary/instruction.md +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/phase-boundary/solution/solve.sh +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/phase-boundary/task.toml +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/phase-boundary/tests/test.sh +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/sidecar-reachability/environment/Dockerfile +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/sidecar-reachability/instruction.md +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/verifier-lifecycle/environment/Dockerfile +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/verifier-lifecycle/environment/entrypoint.sh +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/verifier-lifecycle/instruction.md +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/verifier-lifecycle/solution/solve.sh +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/verifier-lifecycle/task.toml +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/test_harbor.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/patches/__init__.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/patches/mcp_patches.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/patches/tests/__init__.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/patches/tests/test_warnings.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/patches/warnings.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/py.typed +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/telemetry/__init__.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/telemetry/context.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/telemetry/exporter.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/telemetry/instrument.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/telemetry/robot/__init__.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/telemetry/robot/recorder.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/telemetry/robot/video.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/telemetry/span.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/telemetry/tests/__init__.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/telemetry/tests/test_exporter.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/telemetry/tests/test_instrument.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/train/__init__.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/train/base.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/train/client.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/train/tests/__init__.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/train/tests/test_client.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/train/types.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/types.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/utils/__init__.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/utils/docker.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/utils/exceptions.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/utils/gateway.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/utils/hints.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/utils/hud_console.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/utils/modules.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/utils/naming.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/utils/process.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/utils/requests.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/utils/serialization.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/utils/tests/__init__.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/utils/tests/test_exceptions.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/utils/tests/test_gateway.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/utils/tests/test_hints.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/utils/tests/test_hud_console.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/utils/tests/test_process.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/utils/tests/test_requests.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/utils/tests/test_serialization.py +0 -0
- {hud-0.6.13.dev2 → hud-0.6.14}/hud/utils/time.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: hud
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.14
|
|
4
4
|
Summary: SDK for the HUD platform.
|
|
5
5
|
Project-URL: Homepage, https://github.com/hud-evals/hud-python
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/hud-evals/hud-python/issues
|
|
@@ -47,6 +47,7 @@ Requires-Dist: pillow>=11.0.0
|
|
|
47
47
|
Requires-Dist: prompt-toolkit==3.0.51
|
|
48
48
|
Requires-Dist: pydantic-settings<3,>=2.2
|
|
49
49
|
Requires-Dist: pydantic<3,>=2.10
|
|
50
|
+
Requires-Dist: python-dotenv<2,>=1.0
|
|
50
51
|
Requires-Dist: questionary==2.1.0
|
|
51
52
|
Requires-Dist: rich>=13.0.0
|
|
52
53
|
Requires-Dist: typer>=0.9.0
|
|
@@ -11,6 +11,7 @@ from .clients import connect
|
|
|
11
11
|
from .environment import Environment
|
|
12
12
|
from .eval import (
|
|
13
13
|
Chat,
|
|
14
|
+
ComposeProject,
|
|
14
15
|
DockerRuntime,
|
|
15
16
|
Grade,
|
|
16
17
|
HostedRuntime,
|
|
@@ -23,6 +24,7 @@ from .eval import (
|
|
|
23
24
|
RuntimeGPU,
|
|
24
25
|
RuntimeLimits,
|
|
25
26
|
RuntimeResources,
|
|
27
|
+
RuntimeTPU,
|
|
26
28
|
SubprocessRuntime,
|
|
27
29
|
SyncPlan,
|
|
28
30
|
Task,
|
|
@@ -35,6 +37,7 @@ from .version import __version__
|
|
|
35
37
|
|
|
36
38
|
__all__ = [
|
|
37
39
|
"Chat",
|
|
40
|
+
"ComposeProject",
|
|
38
41
|
"DockerRuntime",
|
|
39
42
|
"Environment",
|
|
40
43
|
"Grade",
|
|
@@ -48,6 +51,7 @@ __all__ = [
|
|
|
48
51
|
"RuntimeGPU",
|
|
49
52
|
"RuntimeLimits",
|
|
50
53
|
"RuntimeResources",
|
|
54
|
+
"RuntimeTPU",
|
|
51
55
|
"SubprocessRuntime",
|
|
52
56
|
"SyncPlan",
|
|
53
57
|
"Task",
|
|
@@ -13,6 +13,7 @@ from __future__ import annotations
|
|
|
13
13
|
import json
|
|
14
14
|
import logging
|
|
15
15
|
import shlex
|
|
16
|
+
from contextlib import AsyncExitStack
|
|
16
17
|
from dataclasses import dataclass
|
|
17
18
|
from typing import TYPE_CHECKING, Any, cast
|
|
18
19
|
|
|
@@ -22,7 +23,7 @@ from hud.settings import settings
|
|
|
22
23
|
from hud.types import Step
|
|
23
24
|
|
|
24
25
|
if TYPE_CHECKING:
|
|
25
|
-
from hud.capabilities import
|
|
26
|
+
from hud.capabilities import SSHClient
|
|
26
27
|
from hud.eval.run import Run
|
|
27
28
|
|
|
28
29
|
logger = logging.getLogger(__name__)
|
|
@@ -70,98 +71,112 @@ def build_remote_invocation(shell: str, run_cmd: str) -> RemoteInvocation:
|
|
|
70
71
|
class ClaudeSDKAgent(Agent):
|
|
71
72
|
"""Runs ``claude`` CLI over SSH inside the env workspace.
|
|
72
73
|
|
|
73
|
-
Stateless w.r.t. the env: driven by ``await agent(run)``. SSH
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
MCP config (the CLI connects to them itself).
|
|
74
|
+
Stateless w.r.t. the env: driven by ``await agent(run)``. SSH is opened
|
|
75
|
+
live off the run. Environment MCP bindings are used directly; computer MCP
|
|
76
|
+
servers are bridged over the run's SSH connection.
|
|
77
77
|
"""
|
|
78
78
|
|
|
79
79
|
config: ClaudeSDKConfig
|
|
80
80
|
|
|
81
81
|
def __init__(self, config: ClaudeSDKConfig | None = None) -> None:
|
|
82
82
|
self.config = config or ClaudeSDKConfig()
|
|
83
|
-
self._ssh: SSHClient | None = None
|
|
84
|
-
self._mcp_servers: dict[str, dict[str, Any]] = {}
|
|
85
|
-
self._shell = "bash"
|
|
86
83
|
|
|
87
84
|
async def __call__(self, run: Run) -> None:
|
|
88
|
-
|
|
85
|
+
mcp_servers: dict[str, dict[str, Any]] = {}
|
|
89
86
|
manifest = run.client.manifest
|
|
90
87
|
bindings = manifest.bindings if manifest is not None else []
|
|
91
88
|
families = {c.protocol.split("/", 1)[0] for c in bindings}
|
|
92
89
|
|
|
93
90
|
if "ssh" not in families:
|
|
94
91
|
raise RuntimeError("ClaudeSDKAgent requires an SSH capability")
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
for cap in bindings
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
server_config[
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
92
|
+
ssh = cast("SSHClient", await run.client.open("ssh"))
|
|
93
|
+
shell = ssh.capability.params.get("shell", "bash")
|
|
94
|
+
|
|
95
|
+
rfb_bindings = [cap for cap in bindings if cap.protocol.split("/", 1)[0] == "rfb"]
|
|
96
|
+
async with AsyncExitStack() as resources:
|
|
97
|
+
for cap in bindings:
|
|
98
|
+
family = cap.protocol.split("/", 1)[0]
|
|
99
|
+
if family == "mcp":
|
|
100
|
+
token = cap.params.get("auth_token")
|
|
101
|
+
transport = "http" if cap.params["transport"] == "streamable-http" else "sse"
|
|
102
|
+
server_config: dict[str, Any] = {"type": transport, "url": cap.url}
|
|
103
|
+
if token:
|
|
104
|
+
server_config["headers"] = {"Authorization": f"Bearer {token}"}
|
|
105
|
+
if cap.name in mcp_servers:
|
|
106
|
+
raise RuntimeError(f"duplicate MCP server name {cap.name!r}")
|
|
107
|
+
mcp_servers[cap.name] = server_config
|
|
108
|
+
elif family == "rfb":
|
|
109
|
+
from hud.agents.claude.sdk.computer_mcp import bridge_computer_mcp
|
|
110
|
+
|
|
111
|
+
server_name = (
|
|
112
|
+
"computer-use" if len(rfb_bindings) == 1 else f"computer-use-{cap.name}"
|
|
113
|
+
)
|
|
114
|
+
if server_name in mcp_servers:
|
|
115
|
+
raise RuntimeError(f"duplicate MCP server name {server_name!r}")
|
|
116
|
+
routed = run.client.binding(cap.name)
|
|
117
|
+
mcp_servers[server_name] = await resources.enter_async_context(
|
|
118
|
+
bridge_computer_mcp(
|
|
119
|
+
ssh,
|
|
120
|
+
routed,
|
|
121
|
+
self.config.screenshot_encoding,
|
|
122
|
+
shell=shell,
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
await self._exec(
|
|
127
|
+
run,
|
|
128
|
+
ssh=ssh,
|
|
129
|
+
shell=shell,
|
|
130
|
+
mcp_servers=mcp_servers,
|
|
131
|
+
prompt=run.prompt_text,
|
|
132
|
+
max_steps=self.config.max_steps,
|
|
133
|
+
system_prompt=self.config.system_prompt,
|
|
134
|
+
)
|
|
123
135
|
|
|
124
136
|
async def _exec(
|
|
125
137
|
self,
|
|
126
138
|
run: Run,
|
|
127
139
|
*,
|
|
140
|
+
ssh: SSHClient,
|
|
141
|
+
shell: str,
|
|
142
|
+
mcp_servers: dict[str, dict[str, Any]],
|
|
128
143
|
prompt: str,
|
|
129
144
|
max_steps: int = -1,
|
|
130
145
|
system_prompt: str | None = None,
|
|
131
146
|
) -> None:
|
|
132
|
-
|
|
147
|
+
mcp_config_path = await self._write_mcp_config(ssh, mcp_servers)
|
|
133
148
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
await self._ssh.write_text(".hud_prompt.txt", prompt)
|
|
149
|
+
await ssh.write_text(".hud_prompt.txt", prompt)
|
|
137
150
|
|
|
138
151
|
run_cmd = self._build_cli_command(
|
|
152
|
+
shell=shell,
|
|
139
153
|
prompt=prompt,
|
|
140
154
|
max_steps=max_steps,
|
|
141
155
|
system_prompt=system_prompt,
|
|
142
156
|
mcp_config_path=mcp_config_path,
|
|
143
157
|
)
|
|
144
158
|
|
|
145
|
-
invocation = build_remote_invocation(
|
|
159
|
+
invocation = build_remote_invocation(shell, run_cmd)
|
|
146
160
|
if invocation.script_name is not None:
|
|
147
161
|
assert invocation.script_body is not None
|
|
148
162
|
# cmd.exe mangles inline quotes, so the command rides a batch file.
|
|
149
|
-
await
|
|
163
|
+
await ssh.write_text(invocation.script_name, invocation.script_body)
|
|
150
164
|
|
|
151
165
|
full_cmd = invocation.command
|
|
152
166
|
logger.info("SSH exec claude CLI (%d chars)", len(full_cmd))
|
|
153
167
|
logger.info("Full command: %s", full_cmd)
|
|
154
168
|
|
|
155
|
-
completed = await
|
|
169
|
+
completed = await ssh.run(full_cmd, check=False)
|
|
156
170
|
stdout = completed.stdout if isinstance(completed.stdout, str) else ""
|
|
157
171
|
stderr = completed.stderr if isinstance(completed.stderr, str) else ""
|
|
172
|
+
returncode = completed.returncode
|
|
158
173
|
|
|
159
|
-
logger.info("
|
|
174
|
+
logger.info("returncode=%s stdout=%d stderr=%d", returncode, len(stdout), len(stderr))
|
|
160
175
|
|
|
161
|
-
if
|
|
162
|
-
error = stderr or f"claude CLI exited with
|
|
176
|
+
if returncode != 0 and not stdout.strip():
|
|
177
|
+
error = stderr or f"claude CLI exited with return code {returncode}"
|
|
163
178
|
run.trace.status = "error"
|
|
164
|
-
run.trace.extra.update({"
|
|
179
|
+
run.trace.extra.update({"returncode": returncode, "stderr": stderr})
|
|
165
180
|
run.record(Step(source="system", error=error))
|
|
166
181
|
return
|
|
167
182
|
|
|
@@ -192,27 +207,31 @@ class ClaudeSDKAgent(Agent):
|
|
|
192
207
|
|
|
193
208
|
return env
|
|
194
209
|
|
|
195
|
-
async def _write_mcp_config(
|
|
210
|
+
async def _write_mcp_config(
|
|
211
|
+
self,
|
|
212
|
+
ssh: SSHClient,
|
|
213
|
+
mcp_servers: dict[str, dict[str, Any]],
|
|
214
|
+
) -> str | None:
|
|
196
215
|
"""Write MCP config into the workspace and return its path."""
|
|
197
|
-
if not
|
|
216
|
+
if not mcp_servers:
|
|
198
217
|
return None
|
|
199
|
-
mcp_json = json.dumps({"mcpServers":
|
|
218
|
+
mcp_json = json.dumps({"mcpServers": mcp_servers}, indent=2)
|
|
200
219
|
path = ".hud_mcp_config.json"
|
|
201
|
-
await
|
|
220
|
+
await ssh.write_text(path, mcp_json)
|
|
202
221
|
logger.info("Wrote MCP config")
|
|
203
222
|
return path
|
|
204
223
|
|
|
205
224
|
def _build_cli_command(
|
|
206
225
|
self,
|
|
207
226
|
*,
|
|
227
|
+
shell: str,
|
|
208
228
|
prompt: str,
|
|
209
229
|
max_steps: int,
|
|
210
230
|
system_prompt: str | None,
|
|
211
231
|
mcp_config_path: str | None = None,
|
|
212
232
|
) -> str:
|
|
213
233
|
env_vars = self._build_env_vars()
|
|
214
|
-
is_win =
|
|
215
|
-
self._win_redirect = False
|
|
234
|
+
is_win = shell in WINDOWS_SHELLS
|
|
216
235
|
|
|
217
236
|
# Raw args list (no shell quoting) — used directly for Windows Python launcher.
|
|
218
237
|
base_args: list[str] = [
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"""MCP server that exposes computer-use over VNC.
|
|
2
|
+
|
|
3
|
+
Single tool ``computer`` backed by ``ClaudeComputerTool`` / ``RFBTool``.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import contextlib
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
import secrets
|
|
14
|
+
import shlex
|
|
15
|
+
import sys
|
|
16
|
+
from contextlib import asynccontextmanager
|
|
17
|
+
from pathlib import PurePosixPath
|
|
18
|
+
from typing import TYPE_CHECKING, Any
|
|
19
|
+
|
|
20
|
+
import asyncssh
|
|
21
|
+
import fastmcp
|
|
22
|
+
from pydantic import TypeAdapter
|
|
23
|
+
|
|
24
|
+
from hud.capabilities import Capability
|
|
25
|
+
from hud.capabilities.rfb import RFBClient, ScreenshotEncoding, WebPScreenshotEncoding
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from collections.abc import AsyncIterator, Mapping
|
|
29
|
+
|
|
30
|
+
from hud.capabilities import SSHClient
|
|
31
|
+
|
|
32
|
+
_DEFAULT_SCREENSHOT_ENCODING = WebPScreenshotEncoding()
|
|
33
|
+
RFB_CAPABILITY_ENV = "HUD_RFB_CAPABILITY"
|
|
34
|
+
SCREENSHOT_ENCODING_ENV = "HUD_SCREENSHOT_ENCODING"
|
|
35
|
+
_PROCESS_CLOSE_TIMEOUT_S = 5.0
|
|
36
|
+
_BRIDGE_READY_TIMEOUT_S = 5.0
|
|
37
|
+
_REMOTE_TMP = PurePosixPath("/") / "tmp"
|
|
38
|
+
|
|
39
|
+
logger = logging.getLogger(__name__)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def create_computer_mcp(
|
|
43
|
+
rfb: RFBClient,
|
|
44
|
+
screenshot_encoding: ScreenshotEncoding = _DEFAULT_SCREENSHOT_ENCODING,
|
|
45
|
+
) -> fastmcp.FastMCP:
|
|
46
|
+
"""Build a FastMCP server with one ``computer`` tool backed by ``rfb``."""
|
|
47
|
+
|
|
48
|
+
mcp = fastmcp.FastMCP("computer-use")
|
|
49
|
+
|
|
50
|
+
@mcp.tool()
|
|
51
|
+
async def computer(
|
|
52
|
+
action: str,
|
|
53
|
+
coordinate: str | None = None,
|
|
54
|
+
text: str | None = None,
|
|
55
|
+
scroll_direction: str | None = None,
|
|
56
|
+
scroll_amount: int | None = None,
|
|
57
|
+
start_coordinate: str | None = None,
|
|
58
|
+
duration: float | None = None,
|
|
59
|
+
repeat: int | None = None,
|
|
60
|
+
region: str | None = None,
|
|
61
|
+
) -> list[Any]:
|
|
62
|
+
"""Control a remote screen — screenshot, click, type, key, scroll, move, drag, wait, zoom.
|
|
63
|
+
|
|
64
|
+
Actions: screenshot, left_click, right_click, middle_click, double_click,
|
|
65
|
+
triple_click, mouse_move, move, type, key, scroll, left_click_drag, drag,
|
|
66
|
+
wait, hold_key, cursor_position, zoom, left_mouse_down, left_mouse_up.
|
|
67
|
+
|
|
68
|
+
Returns the resulting screenshot image so you can see the screen state.
|
|
69
|
+
"""
|
|
70
|
+
import mcp.types as mcp_types
|
|
71
|
+
|
|
72
|
+
from hud.agents.claude.tools.computer import ClaudeComputerTool
|
|
73
|
+
from hud.agents.tools.base import AgentToolSpec
|
|
74
|
+
|
|
75
|
+
arguments: dict[str, Any] = {"action": action}
|
|
76
|
+
if coordinate is not None:
|
|
77
|
+
try:
|
|
78
|
+
arguments["coordinate"] = json.loads(coordinate)
|
|
79
|
+
except json.JSONDecodeError:
|
|
80
|
+
arguments["coordinate"] = coordinate
|
|
81
|
+
if text is not None:
|
|
82
|
+
arguments["text"] = text
|
|
83
|
+
if scroll_direction is not None:
|
|
84
|
+
arguments["scroll_direction"] = scroll_direction
|
|
85
|
+
if scroll_amount is not None:
|
|
86
|
+
arguments["scroll_amount"] = scroll_amount
|
|
87
|
+
if start_coordinate is not None:
|
|
88
|
+
try:
|
|
89
|
+
arguments["start_coordinate"] = json.loads(start_coordinate)
|
|
90
|
+
except json.JSONDecodeError:
|
|
91
|
+
arguments["start_coordinate"] = start_coordinate
|
|
92
|
+
if duration is not None:
|
|
93
|
+
arguments["duration"] = duration
|
|
94
|
+
if repeat is not None:
|
|
95
|
+
arguments["repeat"] = repeat
|
|
96
|
+
if region is not None:
|
|
97
|
+
try:
|
|
98
|
+
arguments["region"] = json.loads(region)
|
|
99
|
+
except json.JSONDecodeError:
|
|
100
|
+
arguments["region"] = region
|
|
101
|
+
|
|
102
|
+
spec = AgentToolSpec(api_type="computer", api_name="computer")
|
|
103
|
+
tool = ClaudeComputerTool(
|
|
104
|
+
spec=spec,
|
|
105
|
+
client=rfb,
|
|
106
|
+
screenshot_encoding=screenshot_encoding,
|
|
107
|
+
)
|
|
108
|
+
result = await tool.execute(arguments)
|
|
109
|
+
|
|
110
|
+
# Return content blocks directly so the CLI/model sees real images.
|
|
111
|
+
blocks: list[Any] = []
|
|
112
|
+
for block in result.content:
|
|
113
|
+
if isinstance(block, mcp_types.ImageContent):
|
|
114
|
+
blocks.append(
|
|
115
|
+
mcp_types.ImageContent(
|
|
116
|
+
type="image",
|
|
117
|
+
data=block.data,
|
|
118
|
+
mimeType=block.mimeType,
|
|
119
|
+
),
|
|
120
|
+
)
|
|
121
|
+
elif isinstance(block, mcp_types.TextContent):
|
|
122
|
+
blocks.append(mcp_types.TextContent(type="text", text=block.text))
|
|
123
|
+
if not blocks:
|
|
124
|
+
blocks.append(mcp_types.TextContent(type="text", text="ok"))
|
|
125
|
+
if result.isError:
|
|
126
|
+
blocks.insert(0, mcp_types.TextContent(type="text", text="ERROR"))
|
|
127
|
+
return blocks
|
|
128
|
+
|
|
129
|
+
return mcp
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _required_env(environ: Mapping[str, str], name: str) -> str:
|
|
133
|
+
try:
|
|
134
|
+
return environ[name]
|
|
135
|
+
except KeyError as exc:
|
|
136
|
+
raise RuntimeError(f"missing required environment variable {name}") from exc
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
async def run_computer_mcp(environ: Mapping[str, str] = os.environ) -> None:
|
|
140
|
+
"""Run computer-use over stdio in a controller-side child process."""
|
|
141
|
+
raw_manifest = json.loads(_required_env(environ, RFB_CAPABILITY_ENV))
|
|
142
|
+
if not isinstance(raw_manifest, dict):
|
|
143
|
+
raise ValueError(f"{RFB_CAPABILITY_ENV} must contain a JSON object")
|
|
144
|
+
capability = Capability.from_manifest(raw_manifest)
|
|
145
|
+
if capability.protocol.split("/", 1)[0] != "rfb":
|
|
146
|
+
raise ValueError(f"{RFB_CAPABILITY_ENV} must describe an RFB capability")
|
|
147
|
+
screenshot_encoding = TypeAdapter(ScreenshotEncoding).validate_json(
|
|
148
|
+
_required_env(environ, SCREENSHOT_ENCODING_ENV)
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
rfb = await RFBClient.connect(capability)
|
|
152
|
+
try:
|
|
153
|
+
await create_computer_mcp(rfb, screenshot_encoding).run_async(
|
|
154
|
+
transport="stdio",
|
|
155
|
+
show_banner=False,
|
|
156
|
+
)
|
|
157
|
+
finally:
|
|
158
|
+
await rfb.close()
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@asynccontextmanager
|
|
162
|
+
async def bridge_computer_mcp(
|
|
163
|
+
ssh: SSHClient,
|
|
164
|
+
capability: Capability,
|
|
165
|
+
screenshot_encoding: ScreenshotEncoding = _DEFAULT_SCREENSHOT_ENCODING,
|
|
166
|
+
*,
|
|
167
|
+
shell: str,
|
|
168
|
+
) -> AsyncIterator[dict[str, Any]]:
|
|
169
|
+
"""Bridge a controller-side computer MCP process into a remote POSIX shell."""
|
|
170
|
+
if shell in {"cmd", "powershell"}:
|
|
171
|
+
raise RuntimeError("ClaudeSDKAgent computer use requires a POSIX workspace")
|
|
172
|
+
|
|
173
|
+
token = secrets.token_hex(16)
|
|
174
|
+
request_path = str(_REMOTE_TMP / f"hud-computer-{token}.request")
|
|
175
|
+
response_path = str(_REMOTE_TMP / f"hud-computer-{token}.response")
|
|
176
|
+
bridge = await ssh.create_process(_bridge_command(request_path, response_path))
|
|
177
|
+
local: asyncio.subprocess.Process | None = None
|
|
178
|
+
tasks: list[asyncio.Task[None]] = []
|
|
179
|
+
try:
|
|
180
|
+
ready = await asyncio.wait_for(bridge.stderr.readline(), _BRIDGE_READY_TIMEOUT_S)
|
|
181
|
+
if ready != b"ready\n":
|
|
182
|
+
detail = ready.decode("utf-8", "replace").strip()
|
|
183
|
+
raise RuntimeError(detail or "computer MCP SSH bridge did not become ready")
|
|
184
|
+
|
|
185
|
+
environ = {
|
|
186
|
+
**os.environ,
|
|
187
|
+
RFB_CAPABILITY_ENV: json.dumps(capability.to_manifest(), separators=(",", ":")),
|
|
188
|
+
SCREENSHOT_ENCODING_ENV: screenshot_encoding.model_dump_json(),
|
|
189
|
+
}
|
|
190
|
+
local = await asyncio.create_subprocess_exec(
|
|
191
|
+
sys.executable,
|
|
192
|
+
"-m",
|
|
193
|
+
"hud.agents.claude.sdk.computer_mcp",
|
|
194
|
+
stdin=asyncio.subprocess.PIPE,
|
|
195
|
+
stdout=asyncio.subprocess.PIPE,
|
|
196
|
+
stderr=asyncio.subprocess.PIPE,
|
|
197
|
+
env=environ,
|
|
198
|
+
)
|
|
199
|
+
assert local.stdin is not None
|
|
200
|
+
assert local.stdout is not None
|
|
201
|
+
assert local.stderr is not None
|
|
202
|
+
tasks = [
|
|
203
|
+
asyncio.create_task(_copy_stream(bridge.stdout, local.stdin)),
|
|
204
|
+
asyncio.create_task(_copy_stream(local.stdout, bridge.stdin)),
|
|
205
|
+
asyncio.create_task(_log_stream(bridge.stderr, "SSH bridge")),
|
|
206
|
+
asyncio.create_task(_log_stream(local.stderr, "computer MCP")),
|
|
207
|
+
]
|
|
208
|
+
yield _relay_config(request_path, response_path)
|
|
209
|
+
finally:
|
|
210
|
+
for task in tasks:
|
|
211
|
+
task.cancel()
|
|
212
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
213
|
+
bridge.stdin.close()
|
|
214
|
+
bridge.channel.close()
|
|
215
|
+
with contextlib.suppress(OSError, TimeoutError, asyncssh.Error):
|
|
216
|
+
await asyncio.wait_for(bridge.wait_closed(), _PROCESS_CLOSE_TIMEOUT_S)
|
|
217
|
+
if local is not None:
|
|
218
|
+
if local.stdin is not None:
|
|
219
|
+
local.stdin.close()
|
|
220
|
+
if local.returncode is None:
|
|
221
|
+
with contextlib.suppress(ProcessLookupError):
|
|
222
|
+
local.terminate()
|
|
223
|
+
try:
|
|
224
|
+
await asyncio.wait_for(local.wait(), _PROCESS_CLOSE_TIMEOUT_S)
|
|
225
|
+
except TimeoutError:
|
|
226
|
+
with contextlib.suppress(ProcessLookupError):
|
|
227
|
+
local.kill()
|
|
228
|
+
with contextlib.suppress(TimeoutError):
|
|
229
|
+
await asyncio.wait_for(local.wait(), _PROCESS_CLOSE_TIMEOUT_S)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _bridge_command(request_path: str, response_path: str) -> str:
|
|
233
|
+
request = shlex.quote(request_path)
|
|
234
|
+
response = shlex.quote(response_path)
|
|
235
|
+
cleanup = shlex.quote(f"rm -f -- {request} {response}")
|
|
236
|
+
return (
|
|
237
|
+
"set -eu; umask 077; "
|
|
238
|
+
f"rm -f -- {request} {response}; mkfifo -- {request} {response}; "
|
|
239
|
+
f"trap {cleanup} EXIT HUP INT TERM; "
|
|
240
|
+
"printf 'ready\\n' >&2; "
|
|
241
|
+
f"cat {request} & reader=$!; cat > {response}; wait $reader"
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _relay_config(request_path: str, response_path: str) -> dict[str, Any]:
|
|
246
|
+
request = shlex.quote(request_path)
|
|
247
|
+
response = shlex.quote(response_path)
|
|
248
|
+
script = f"cat {response} & reader=$!; cat > {request}; wait $reader"
|
|
249
|
+
return {"type": "stdio", "command": "sh", "args": ["-c", script]}
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
async def _copy_stream(
|
|
253
|
+
reader: asyncio.StreamReader | asyncssh.SSHReader[bytes],
|
|
254
|
+
writer: asyncio.StreamWriter | asyncssh.SSHWriter[bytes],
|
|
255
|
+
) -> None:
|
|
256
|
+
try:
|
|
257
|
+
while chunk := await reader.read(65536):
|
|
258
|
+
writer.write(chunk)
|
|
259
|
+
await writer.drain()
|
|
260
|
+
finally:
|
|
261
|
+
writer.close()
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
async def _log_stream(
|
|
265
|
+
reader: asyncio.StreamReader | asyncssh.SSHReader[bytes],
|
|
266
|
+
source: str,
|
|
267
|
+
) -> None:
|
|
268
|
+
while line := await reader.readline():
|
|
269
|
+
logger.warning("%s: %s", source, line.decode("utf-8", "replace").rstrip())
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
if __name__ == "__main__":
|
|
273
|
+
asyncio.run(run_computer_mcp())
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
__all__ = [
|
|
277
|
+
"RFB_CAPABILITY_ENV",
|
|
278
|
+
"SCREENSHOT_ENCODING_ENV",
|
|
279
|
+
"bridge_computer_mcp",
|
|
280
|
+
"create_computer_mcp",
|
|
281
|
+
"run_computer_mcp",
|
|
282
|
+
]
|
|
@@ -222,12 +222,20 @@ class ClaudeComputerTool(RFBTool):
|
|
|
222
222
|
await self.type_text(text)
|
|
223
223
|
|
|
224
224
|
case "key":
|
|
225
|
-
|
|
226
|
-
|
|
225
|
+
text = arguments.get("text")
|
|
226
|
+
# xdotool syntax: space-separated sequential presses, + binds a chord
|
|
227
|
+
chords = (
|
|
228
|
+
[chord for part in text.split() if (chord := _split_keys(part))]
|
|
229
|
+
if isinstance(text, str)
|
|
230
|
+
else []
|
|
231
|
+
)
|
|
232
|
+
if not chords:
|
|
227
233
|
return tool_err("`text` (key chord) is required for key")
|
|
228
234
|
repeat = arguments.get("repeat")
|
|
229
235
|
count = repeat if isinstance(repeat, int) and repeat > 0 else 1
|
|
230
|
-
|
|
236
|
+
for _ in range(min(count, 100)):
|
|
237
|
+
for chord in chords:
|
|
238
|
+
await self.press_keys(chord)
|
|
231
239
|
|
|
232
240
|
case "hold_key":
|
|
233
241
|
keys = _split_keys(arguments.get("text"))
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import shlex
|
|
5
6
|
from typing import Any, cast
|
|
6
7
|
|
|
7
8
|
import mcp.types as mcp_types
|
|
8
9
|
|
|
9
10
|
from hud.agents.tools import SSHTool
|
|
11
|
+
from hud.agents.tools.ssh import MAX_SHELL_OUTPUT_LENGTH, bound_shell_output
|
|
10
12
|
from hud.types import MCPToolResult
|
|
11
13
|
|
|
12
14
|
from .base import OpenAIToolSpec
|
|
@@ -16,9 +18,6 @@ OPENAI_SHELL_SPEC = OpenAIToolSpec(
|
|
|
16
18
|
api_name="shell",
|
|
17
19
|
)
|
|
18
20
|
|
|
19
|
-
MAX_SHELL_OUTPUT_LENGTH = 10 * 1024 * 1024
|
|
20
|
-
TRUNCATION_MARKER = "[truncated]"
|
|
21
|
-
|
|
22
21
|
|
|
23
22
|
class OpenAIShellTool(SSHTool):
|
|
24
23
|
name = "shell"
|
|
@@ -75,21 +74,23 @@ class OpenAIShellTool(SSHTool):
|
|
|
75
74
|
outputs: list[dict[str, Any]] = []
|
|
76
75
|
display_outputs: list[str] = []
|
|
77
76
|
is_error = False
|
|
78
|
-
|
|
77
|
+
timeout_seconds: float | None = None
|
|
79
78
|
timeout_ms = arguments.get("timeout_ms")
|
|
80
79
|
if isinstance(timeout_ms, int):
|
|
81
|
-
|
|
80
|
+
timeout_seconds = timeout_ms / 1000.0
|
|
82
81
|
|
|
83
82
|
for command in command_list:
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
83
|
+
full_cmd = (
|
|
84
|
+
f"timeout {timeout_seconds:g}s bash -lc {shlex.quote(command)}"
|
|
85
|
+
if timeout_seconds
|
|
86
|
+
else command
|
|
87
|
+
)
|
|
88
|
+
completed = await self.client.run(full_cmd, check=False)
|
|
89
89
|
stdout = completed.stdout if isinstance(completed.stdout, str) else ""
|
|
90
90
|
stderr = completed.stderr if isinstance(completed.stderr, str) else ""
|
|
91
|
-
exit_code = completed.
|
|
92
|
-
|
|
91
|
+
exit_code = completed.returncode
|
|
92
|
+
assert exit_code is not None
|
|
93
|
+
stdout, stderr = bound_shell_output(stdout, stderr, max_output_length)
|
|
93
94
|
outputs.append(shell_output(stdout, stderr, exit_code))
|
|
94
95
|
display_outputs.append(stdout + stderr)
|
|
95
96
|
is_error = is_error or bool(exit_code)
|
|
@@ -118,31 +119,6 @@ def _shell_result(
|
|
|
118
119
|
)
|
|
119
120
|
|
|
120
121
|
|
|
121
|
-
def _bound_output(stdout: str, stderr: str, limit: int) -> tuple[str, str]:
|
|
122
|
-
total_length = len(stdout) + len(stderr)
|
|
123
|
-
if total_length <= limit:
|
|
124
|
-
return stdout, stderr
|
|
125
|
-
|
|
126
|
-
marker = TRUNCATION_MARKER[:limit]
|
|
127
|
-
available = limit - len(marker)
|
|
128
|
-
prefix_length = (available + 1) // 2
|
|
129
|
-
suffix_length = available // 2
|
|
130
|
-
|
|
131
|
-
stdout_prefix = stdout[:prefix_length]
|
|
132
|
-
stderr_prefix = stderr[: max(0, prefix_length - len(stdout))]
|
|
133
|
-
stderr_suffix = stderr[-suffix_length:] if suffix_length else ""
|
|
134
|
-
stdout_suffix_length = max(0, suffix_length - len(stderr))
|
|
135
|
-
stdout_suffix = stdout[-stdout_suffix_length:] if stdout_suffix_length else ""
|
|
136
|
-
|
|
137
|
-
if len(stdout_prefix) + len(stdout_suffix) < len(stdout):
|
|
138
|
-
stdout = stdout_prefix + marker + stdout_suffix
|
|
139
|
-
stderr = stderr_prefix + stderr_suffix
|
|
140
|
-
else:
|
|
141
|
-
stdout = stdout_prefix + stdout_suffix
|
|
142
|
-
stderr = stderr_prefix + marker + stderr_suffix
|
|
143
|
-
return stdout, stderr
|
|
144
|
-
|
|
145
|
-
|
|
146
122
|
def shell_output(stdout: str, stderr: str, exit_code: int) -> dict[str, Any]:
|
|
147
123
|
return {
|
|
148
124
|
"stdout": stdout,
|