hud 0.6.19.dev1__tar.gz → 0.6.19.dev2__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.19.dev1 → hud-0.6.19.dev2}/PKG-INFO +1 -1
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/claude/agent.py +18 -11
- hud-0.6.19.dev2/hud/agents/claude/images.py +65 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/claude/tools/coding.py +1 -1
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/openai_compatible/tools/filesystem.py +4 -3
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/tests/test_claude_agent.py +117 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/tests/test_provider_native_tools.py +55 -2
- hud-0.6.19.dev2/hud/agents/tools/file_view.py +38 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/tools/ssh.py +9 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/capabilities/ssh.py +7 -5
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/version.py +1 -1
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/.gitignore +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/LICENSE +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/README.md +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/__init__.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/__main__.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/__init__.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/base.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/browser_use/__init__.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/browser_use/agent.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/claude/__init__.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/claude/sdk/__init__.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/claude/sdk/agent.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/claude/sdk/computer_mcp.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/claude/sdk/events.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/claude/tools/__init__.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/claude/tools/base.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/claude/tools/computer.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/claude/tools/hosted.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/claude/tools/mcp_proxy.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/claude/tools/settings.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/claude/tools/tests/__init__.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/claude/tools/tests/test_computer.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/cli.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/codex/__init__.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/codex/agent.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/gemini/__init__.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/gemini/agent.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/gemini/settings.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/gemini/tools/__init__.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/gemini/tools/base.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/gemini/tools/coding.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/gemini/tools/computer.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/gemini/tools/filesystem.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/gemini/tools/hosted.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/gemini/tools/mcp_proxy.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/gemini/tools/tests/__init__.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/gemini/tools/tests/test_computer.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/misc/__init__.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/misc/response_automation.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/openai/__init__.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/openai/agent.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/openai/tools/__init__.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/openai/tools/apply_patch.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/openai/tools/base.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/openai/tools/coding.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/openai/tools/computer.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/openai/tools/hosted.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/openai/tools/mcp_proxy.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/openai/tools/strict_schema.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/openai/tools/tests/__init__.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/openai/tools/tests/test_computer.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/openai/tools/tests/test_strict_schema.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/openai_compatible/__init__.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/openai_compatible/agent.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/openai_compatible/tools/__init__.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/openai_compatible/tools/base.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/openai_compatible/tools/mcp_proxy.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/robot/__init__.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/robot/adapter.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/robot/agent.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/robot/batching.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/robot/dataset.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/robot/model.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/tests/__init__.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/tests/cli_fakes.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/tests/test_apply_patch.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/tests/test_base.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/tests/test_claude_cli_agent.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/tests/test_codex_cli_agent.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/tests/test_gemini_agent.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/tests/test_openai_agent.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/tests/test_openai_compatible_agent.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/tests/test_robot_dataset_writer.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/tests/test_tool_agent.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/tests/test_trace.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/tool_agent.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/tools/__init__.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/tools/base.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/tools/hosted.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/tools/mcp.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/tools/rfb.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/types.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/capabilities/__init__.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/capabilities/base.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/capabilities/cdp.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/capabilities/connection.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/capabilities/mcp.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/capabilities/rfb.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/capabilities/robot.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/capabilities/tests/__init__.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/capabilities/tests/test_rfb.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/capabilities/tests/test_ssh.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/cli/__init__.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/cli/__main__.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/cli/deploy.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/cli/eval.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/cli/init.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/cli/jobs.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/cli/models.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/cli/project.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/cli/qa.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/cli/serve.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/cli/sync.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/cli/task.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/cli/tests/__init__.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/cli/tests/test_cli.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/cli/tests/test_deploy.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/cli/tests/test_eval.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/cli/tests/test_init.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/cli/tests/test_jobs.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/cli/tests/test_main.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/cli/tests/test_project.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/cli/tests/test_qa.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/cli/tests/test_sync.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/cli/tests/test_task.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/cli/tests/test_trace.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/cli/trace.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/clients/__init__.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/clients/client.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/clients/tests/__init__.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/clients/tests/test_connect.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/conftest.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/__init__.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/arguments.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/egress.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/env.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/file_tracker.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/namespace.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/process_guard.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/robot/__init__.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/robot/bridge.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/robot/endpoint.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/robot/gym.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/server.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/tests/__init__.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/tests/conftest.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/tests/test_arguments.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/tests/test_capability_backing.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/tests/test_file_tracker.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/tests/test_file_tracking.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/tests/test_loader.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/tests/test_manifest.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/tests/test_platform.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/tests/test_process_connection.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/tests/test_robot_regressions.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/tests/test_server.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/tests/test_sessions.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/tests/test_tunnel.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/tests/test_workspace.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/utils.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/workspace.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/__init__.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/chat.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/docker-seccomp.json +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/file_tracking.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/job.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/run.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/runtime/__init__.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/runtime/compose.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/runtime/core.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/runtime/daytona.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/runtime/docker.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/runtime/hosted.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/runtime/hud.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/runtime/local.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/runtime/modal.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/sync.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/task.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/taskset.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/tests/__init__.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/tests/test_chat.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/tests/test_docker_provider.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/tests/test_file_tracking_observer.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/tests/test_hosted.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/tests/test_job.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/tests/test_local_runtime.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/tests/test_rollout.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/tests/test_shared.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/tests/test_sync.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/tests/test_task.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/graders/__init__.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/graders/base.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/graders/bash.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/graders/combine.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/graders/judge.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/graders/results.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/graders/text.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/__init__.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/Dockerfile +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/__init__.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/adapt.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/env.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/export.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/install.sh +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/__init__.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/conftest.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/agent-lifecycle/environment/Dockerfile +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/agent-lifecycle/instruction.md +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/agent-lifecycle/solution/solve.sh +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/agent-lifecycle/task.toml +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/agent-lifecycle/tests/test.sh +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/hello-mcp/environment/Dockerfile +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/hello-mcp/environment/docker-compose.yaml +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/hello-mcp/environment/mcp-server/Dockerfile +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/hello-mcp/environment/mcp-server/__init__.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/hello-mcp/environment/mcp-server/server.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/hello-mcp/instruction.md +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/hello-mcp/solution/solve.sh +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/hello-mcp/task.toml +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/hello-mcp/tests/test.sh +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/phase-boundary/environment/Dockerfile +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/phase-boundary/environment/entrypoint.sh +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/phase-boundary/instruction.md +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/phase-boundary/solution/solve.sh +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/phase-boundary/task.toml +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/phase-boundary/tests/test.sh +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/sidecar-reachability/environment/Dockerfile +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/sidecar-reachability/environment/docker-compose.yaml +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/sidecar-reachability/instruction.md +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/sidecar-reachability/solution/solve.sh +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/sidecar-reachability/task.toml +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/sidecar-reachability/tests/Dockerfile +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/sidecar-reachability/tests/test.sh +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/verifier-lifecycle/environment/Dockerfile +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/verifier-lifecycle/environment/entrypoint.sh +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/verifier-lifecycle/instruction.md +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/verifier-lifecycle/solution/solve.sh +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/verifier-lifecycle/task.toml +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/verifier-lifecycle/tests/test.sh +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/test_contract.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/test_harbor.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/test_integration.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/patches/__init__.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/patches/mcp_patches.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/patches/tests/__init__.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/patches/tests/test_warnings.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/patches/warnings.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/py.typed +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/settings.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/telemetry/__init__.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/telemetry/context.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/telemetry/exporter.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/telemetry/instrument.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/telemetry/robot/__init__.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/telemetry/robot/recorder.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/telemetry/robot/video.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/telemetry/span.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/telemetry/tests/__init__.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/telemetry/tests/test_exporter.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/telemetry/tests/test_instrument.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/train/__init__.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/train/base.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/train/client.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/train/tests/__init__.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/train/tests/test_client.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/train/types.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/types.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/utils/__init__.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/utils/docker.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/utils/exceptions.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/utils/gateway.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/utils/hints.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/utils/hud_console.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/utils/modules.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/utils/naming.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/utils/platform.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/utils/process.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/utils/requests.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/utils/serialization.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/utils/tests/__init__.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/utils/tests/test_exceptions.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/utils/tests/test_gateway.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/utils/tests/test_hints.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/utils/tests/test_hud_console.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/utils/tests/test_naming.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/utils/tests/test_platform.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/utils/tests/test_process.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/utils/tests/test_requests.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/utils/tests/test_serialization.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/utils/time.py +0 -0
- {hud-0.6.19.dev1 → hud-0.6.19.dev2}/pyproject.toml +0 -0
|
@@ -8,7 +8,7 @@ import json
|
|
|
8
8
|
import logging
|
|
9
9
|
import math
|
|
10
10
|
from random import SystemRandom
|
|
11
|
-
from typing import TYPE_CHECKING,
|
|
11
|
+
from typing import TYPE_CHECKING, cast
|
|
12
12
|
|
|
13
13
|
import httpx
|
|
14
14
|
import httpx2
|
|
@@ -23,7 +23,6 @@ from anthropic import (
|
|
|
23
23
|
)
|
|
24
24
|
from anthropic.types import CacheControlEphemeralParam
|
|
25
25
|
from anthropic.types.beta import (
|
|
26
|
-
BetaBase64ImageSourceParam,
|
|
27
26
|
BetaBase64PDFSourceParam,
|
|
28
27
|
BetaImageBlockParam,
|
|
29
28
|
BetaMessage,
|
|
@@ -35,12 +34,14 @@ from anthropic.types.beta import (
|
|
|
35
34
|
BetaToolResultBlockParam,
|
|
36
35
|
BetaToolUnionParam,
|
|
37
36
|
)
|
|
37
|
+
from PIL import Image
|
|
38
38
|
|
|
39
39
|
from hud.agents.tool_agent import RunState, ToolAgent
|
|
40
40
|
from hud.agents.types import AgentStep, Citation, ClaudeConfig, Usage
|
|
41
41
|
from hud.types import MCPToolCall, MCPToolResult
|
|
42
42
|
from hud.utils import gateway
|
|
43
43
|
|
|
44
|
+
from .images import image_source
|
|
44
45
|
from .tools.coding import ClaudeBashTool, ClaudeTextEditorTool
|
|
45
46
|
from .tools.computer import ClaudeComputerTool
|
|
46
47
|
from .tools.mcp_proxy import ClaudeMCPProxyTool
|
|
@@ -51,7 +52,6 @@ if TYPE_CHECKING:
|
|
|
51
52
|
|
|
52
53
|
logger = logging.getLogger(__name__)
|
|
53
54
|
|
|
54
|
-
ClaudeImageMediaType = Literal["image/jpeg", "image/png", "image/gif", "image/webp"]
|
|
55
55
|
ClaudeToolResultContent = BetaTextBlockParam | BetaImageBlockParam | BetaRequestDocumentBlockParam
|
|
56
56
|
|
|
57
57
|
_RETRYABLE_STREAM_ERROR_TYPES = frozenset(
|
|
@@ -140,6 +140,7 @@ class ClaudeAgent(ToolAgent[BetaMessageParam, ClaudeConfig]):
|
|
|
140
140
|
return None
|
|
141
141
|
|
|
142
142
|
result_content = result.content
|
|
143
|
+
image_error = False
|
|
143
144
|
if result.isError:
|
|
144
145
|
error_msg = next(
|
|
145
146
|
(c.text for c in result.content if isinstance(c, mcp_types.TextContent)),
|
|
@@ -168,14 +169,19 @@ class ClaudeAgent(ToolAgent[BetaMessageParam, ClaudeConfig]):
|
|
|
168
169
|
citations={"enabled": True},
|
|
169
170
|
)
|
|
170
171
|
case mcp_types.ImageContent():
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
172
|
+
try:
|
|
173
|
+
block = BetaImageBlockParam(
|
|
174
|
+
type="image",
|
|
175
|
+
source=image_source(
|
|
176
|
+
content,
|
|
177
|
+
preserve_dimensions=isinstance(
|
|
178
|
+
state.tools.get(call.name), ClaudeComputerTool
|
|
179
|
+
),
|
|
180
|
+
),
|
|
181
|
+
)
|
|
182
|
+
except (ValueError, OSError, Image.DecompressionBombError) as exc:
|
|
183
|
+
image_error = True
|
|
184
|
+
block = BetaTextBlockParam(type="text", text=f"Cannot view image: {exc}")
|
|
179
185
|
case mcp_types.EmbeddedResource(
|
|
180
186
|
resource=mcp_types.BlobResourceContents(mimeType="application/pdf") as resource,
|
|
181
187
|
):
|
|
@@ -207,6 +213,7 @@ class ClaudeAgent(ToolAgent[BetaMessageParam, ClaudeConfig]):
|
|
|
207
213
|
type="tool_result",
|
|
208
214
|
tool_use_id=tool_use_id,
|
|
209
215
|
content=claude_blocks,
|
|
216
|
+
is_error=bool(result.isError) or image_error,
|
|
210
217
|
),
|
|
211
218
|
],
|
|
212
219
|
)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Image input preparation for Anthropic Messages."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import math
|
|
7
|
+
from io import BytesIO
|
|
8
|
+
from typing import TYPE_CHECKING, Literal, cast
|
|
9
|
+
|
|
10
|
+
from anthropic.types.beta import BetaBase64ImageSourceParam
|
|
11
|
+
from PIL import Image, ImageOps
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from mcp.types import ImageContent
|
|
15
|
+
|
|
16
|
+
ClaudeImageMediaType = Literal["image/jpeg", "image/png", "image/gif", "image/webp"]
|
|
17
|
+
MAX_SOURCE_IMAGE_PIXELS = 16_000_000
|
|
18
|
+
MAX_IMAGE_EDGE = 1568
|
|
19
|
+
MAX_IMAGE_PIXELS = 1_150_000
|
|
20
|
+
MAX_IMAGE_BASE64_BYTES = 5_000_000
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def image_source(
|
|
24
|
+
content: ImageContent, *, preserve_dimensions: bool = False
|
|
25
|
+
) -> BetaBase64ImageSourceParam:
|
|
26
|
+
"""Prepare a tool image, preserving computer-tool coordinate space when requested."""
|
|
27
|
+
if preserve_dimensions:
|
|
28
|
+
return BetaBase64ImageSourceParam(
|
|
29
|
+
type="base64",
|
|
30
|
+
media_type=cast("ClaudeImageMediaType", content.mimeType),
|
|
31
|
+
data=content.data,
|
|
32
|
+
)
|
|
33
|
+
with Image.open(BytesIO(base64.b64decode(content.data, validate=True))) as image:
|
|
34
|
+
if image.width * image.height > MAX_SOURCE_IMAGE_PIXELS:
|
|
35
|
+
raise ValueError(
|
|
36
|
+
f"Image exceeds the {MAX_SOURCE_IMAGE_PIXELS:,}-pixel source limit. "
|
|
37
|
+
"Resize or crop it before viewing."
|
|
38
|
+
)
|
|
39
|
+
return _encode_image(image)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _encode_image(image: Image.Image) -> BetaBase64ImageSourceParam:
|
|
43
|
+
scale = min(
|
|
44
|
+
1.0,
|
|
45
|
+
MAX_IMAGE_EDGE / max(image.size),
|
|
46
|
+
math.sqrt(MAX_IMAGE_PIXELS / (image.width * image.height)),
|
|
47
|
+
)
|
|
48
|
+
image.thumbnail(
|
|
49
|
+
(max(1, int(image.width * scale)), max(1, int(image.height * scale))),
|
|
50
|
+
Image.Resampling.LANCZOS,
|
|
51
|
+
)
|
|
52
|
+
image = ImageOps.exif_transpose(image)
|
|
53
|
+
image = image.convert("RGBA" if image.has_transparency_data else "RGB")
|
|
54
|
+
buffer = BytesIO()
|
|
55
|
+
image.save(buffer, format="PNG", optimize=True)
|
|
56
|
+
data = base64.b64encode(buffer.getvalue()).decode("ascii")
|
|
57
|
+
mime_type: ClaudeImageMediaType = "image/png"
|
|
58
|
+
if len(data) > MAX_IMAGE_BASE64_BYTES:
|
|
59
|
+
background = Image.new("RGB", image.size, "white")
|
|
60
|
+
background.paste(image, mask=image.getchannel("A") if image.mode == "RGBA" else None)
|
|
61
|
+
buffer = BytesIO()
|
|
62
|
+
background.save(buffer, format="JPEG", quality=85, optimize=True)
|
|
63
|
+
data = base64.b64encode(buffer.getvalue()).decode("ascii")
|
|
64
|
+
mime_type = "image/jpeg"
|
|
65
|
+
return BetaBase64ImageSourceParam(type="base64", media_type=mime_type, data=data)
|
|
@@ -171,7 +171,7 @@ class ClaudeTextEditorTool(SSHTool):
|
|
|
171
171
|
|
|
172
172
|
match command:
|
|
173
173
|
case "view":
|
|
174
|
-
return await self.
|
|
174
|
+
return await self.file_view(path)
|
|
175
175
|
case "create":
|
|
176
176
|
content = arguments.get("file_text", "")
|
|
177
177
|
return await self.file_write(path, str(content))
|
|
@@ -39,7 +39,8 @@ class _FilesystemTool(SSHTool):
|
|
|
39
39
|
class ReadTool(_FilesystemTool):
|
|
40
40
|
name = "read"
|
|
41
41
|
description = (
|
|
42
|
-
"Reads
|
|
42
|
+
"Reads text, images, or directories from the workspace. "
|
|
43
|
+
"Use offset and limit for text pagination."
|
|
43
44
|
)
|
|
44
45
|
parameters: ClassVar[dict[str, Any]] = {
|
|
45
46
|
"type": "object",
|
|
@@ -70,8 +71,8 @@ class ReadTool(_FilesystemTool):
|
|
|
70
71
|
limit = _positive_int(arguments.get("limit"), default=DEFAULT_READ_LIMIT, name="limit")
|
|
71
72
|
if not (await self.bash(f"test -d {shlex.quote(path)}")).isError:
|
|
72
73
|
return await self._read_directory(path, offset=offset, limit=limit)
|
|
73
|
-
result = await self.
|
|
74
|
-
if result.isError:
|
|
74
|
+
result = await self.file_view(path)
|
|
75
|
+
if result.isError or any(isinstance(c, mcp_types.ImageContent) for c in result.content):
|
|
75
76
|
return result
|
|
76
77
|
text = result_text(result)
|
|
77
78
|
lines = text.splitlines()
|
|
@@ -4,6 +4,11 @@ plus the pure ``_citation`` / ``_cache_last_user_block`` helpers.
|
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
+
import base64
|
|
8
|
+
import random
|
|
9
|
+
import struct
|
|
10
|
+
import zlib
|
|
11
|
+
from io import BytesIO
|
|
7
12
|
from types import SimpleNamespace
|
|
8
13
|
from typing import Any, cast
|
|
9
14
|
from unittest.mock import AsyncMock, Mock, call
|
|
@@ -12,8 +17,14 @@ import httpx
|
|
|
12
17
|
import httpx2
|
|
13
18
|
import pytest
|
|
14
19
|
from anthropic import APIStatusError
|
|
20
|
+
from mcp.types import ImageContent
|
|
21
|
+
from PIL import Image
|
|
15
22
|
|
|
16
23
|
from hud.agents.claude.agent import ClaudeAgent
|
|
24
|
+
from hud.agents.claude.tools.computer import ClaudeComputerTool
|
|
25
|
+
from hud.agents.tool_agent import RunState
|
|
26
|
+
from hud.capabilities import RFBClient
|
|
27
|
+
from hud.types import MCPToolCall, MCPToolResult
|
|
17
28
|
|
|
18
29
|
|
|
19
30
|
class FakeStream:
|
|
@@ -344,3 +355,109 @@ def test_cache_last_user_block_marks_content() -> None:
|
|
|
344
355
|
content = cast("list[Any]", out[-1]["content"])
|
|
345
356
|
block = cast("dict[str, Any]", content[0])
|
|
346
357
|
assert block.get("cache_control") == {"type": "ephemeral"}
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _image_result(data: bytes, *, computer: bool = False) -> Any:
|
|
361
|
+
agent = _agent()
|
|
362
|
+
state = RunState()
|
|
363
|
+
if computer:
|
|
364
|
+
spec = ClaudeComputerTool.default_spec("claude")
|
|
365
|
+
assert spec is not None
|
|
366
|
+
tool = ClaudeComputerTool(spec=spec, client=Mock(spec=RFBClient))
|
|
367
|
+
state.tools[tool.provider_name] = tool
|
|
368
|
+
name = tool.provider_name
|
|
369
|
+
else:
|
|
370
|
+
name = "read_file"
|
|
371
|
+
content = ImageContent(type="image", mimeType="image/png", data=base64.b64encode(data).decode())
|
|
372
|
+
result = MCPToolResult(content=[content])
|
|
373
|
+
original = result.model_dump()
|
|
374
|
+
message = agent._format_result(
|
|
375
|
+
MCPToolCall(id="image-call", name=name, arguments={}), result, state
|
|
376
|
+
)
|
|
377
|
+
assert result.model_dump() == original
|
|
378
|
+
assert isinstance(message, dict)
|
|
379
|
+
return next(iter(message["content"]))
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
@pytest.mark.parametrize("size", [(4000, 2000), (2000, 4000), (2000, 2000), (32, 24)])
|
|
383
|
+
def test_tool_images_fit_claude_limits_without_upscaling(size: tuple[int, int]) -> None:
|
|
384
|
+
buffer = BytesIO()
|
|
385
|
+
Image.new("RGB", size, "white").save(buffer, format="PNG")
|
|
386
|
+
result = _image_result(buffer.getvalue())
|
|
387
|
+
assert not result["is_error"]
|
|
388
|
+
source = result["content"][0]["source"]
|
|
389
|
+
assert len(source["data"]) <= 5_000_000
|
|
390
|
+
with Image.open(BytesIO(base64.b64decode(source["data"]))) as image:
|
|
391
|
+
assert max(image.size) <= 1568
|
|
392
|
+
assert image.width * image.height <= 1_150_000
|
|
393
|
+
assert image.width <= size[0] and image.height <= size[1]
|
|
394
|
+
assert image.width / image.height == pytest.approx(size[0] / size[1], rel=0.005)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def test_claude_compresses_noisy_transparent_tool_images() -> None:
|
|
398
|
+
size = (1072, 1072)
|
|
399
|
+
image = Image.frombytes("RGBA", size, random.Random(0).randbytes(size[0] * size[1] * 4))
|
|
400
|
+
buffer = BytesIO()
|
|
401
|
+
image.save(buffer, format="PNG")
|
|
402
|
+
assert len(base64.b64encode(buffer.getvalue())) > 5_000_000
|
|
403
|
+
result = _image_result(buffer.getvalue())
|
|
404
|
+
assert not result["is_error"]
|
|
405
|
+
source = result["content"][0]["source"]
|
|
406
|
+
assert source["media_type"] == "image/jpeg"
|
|
407
|
+
assert len(source["data"]) <= 5_000_000
|
|
408
|
+
with Image.open(BytesIO(base64.b64decode(source["data"]))) as viewed:
|
|
409
|
+
assert viewed.size == size
|
|
410
|
+
assert viewed.mode == "RGB"
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def test_claude_applies_image_orientation() -> None:
|
|
414
|
+
image = Image.new("RGB", (40, 20), "red")
|
|
415
|
+
exif = Image.Exif()
|
|
416
|
+
exif[274] = 6
|
|
417
|
+
buffer = BytesIO()
|
|
418
|
+
image.save(buffer, format="JPEG", exif=exif)
|
|
419
|
+
source = _image_result(buffer.getvalue())["content"][0]["source"]
|
|
420
|
+
with Image.open(BytesIO(base64.b64decode(source["data"]))) as viewed:
|
|
421
|
+
assert viewed.size == (20, 40)
|
|
422
|
+
assert viewed.getexif().get(274, 1) == 1
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def test_claude_preserves_small_png_transparency() -> None:
|
|
426
|
+
buffer = BytesIO()
|
|
427
|
+
Image.new("RGBA", (32, 24), (255, 0, 0, 128)).save(buffer, format="PNG")
|
|
428
|
+
source = _image_result(buffer.getvalue())["content"][0]["source"]
|
|
429
|
+
assert source["media_type"] == "image/png"
|
|
430
|
+
with Image.open(BytesIO(base64.b64decode(source["data"]))) as viewed:
|
|
431
|
+
assert viewed.getpixel((0, 0)) == (255, 0, 0, 128)
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
@pytest.mark.filterwarnings("ignore:Image size.*:PIL.Image.DecompressionBombWarning")
|
|
435
|
+
@pytest.mark.parametrize("size", [(4001, 4000), (10000, 10000)])
|
|
436
|
+
def test_claude_rejects_oversized_source_before_decoding(size: tuple[int, int]) -> None:
|
|
437
|
+
buffer = BytesIO()
|
|
438
|
+
Image.new("RGB", (1, 1)).save(buffer, format="PNG")
|
|
439
|
+
data = bytearray(buffer.getvalue())
|
|
440
|
+
data[16:24] = struct.pack(">II", *size)
|
|
441
|
+
data[29:33] = struct.pack(">I", zlib.crc32(data[12:29]))
|
|
442
|
+
result = _image_result(bytes(data))
|
|
443
|
+
assert result["is_error"]
|
|
444
|
+
assert result["content"][0]["type"] == "text"
|
|
445
|
+
assert "16,000,000-pixel source limit" in result["content"][0]["text"]
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def test_claude_returns_invalid_image_as_tool_error() -> None:
|
|
449
|
+
result = _image_result(b"invalid image")
|
|
450
|
+
assert result["is_error"]
|
|
451
|
+
assert result["content"][0]["type"] == "text"
|
|
452
|
+
assert "Cannot view image" in result["content"][0]["text"]
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def test_claude_preserves_computer_screenshot_coordinate_space() -> None:
|
|
456
|
+
buffer = BytesIO()
|
|
457
|
+
Image.new("RGB", (2000, 1000), "white").save(buffer, format="PNG")
|
|
458
|
+
data = buffer.getvalue()
|
|
459
|
+
result = _image_result(data, computer=True)
|
|
460
|
+
assert not result["is_error"]
|
|
461
|
+
source = result["content"][0]["source"]
|
|
462
|
+
assert source["media_type"] == "image/png"
|
|
463
|
+
assert base64.b64decode(source["data"]) == data
|
|
@@ -7,13 +7,16 @@ client and assert the command translation + result shape, fully offline.
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
+
import base64
|
|
10
11
|
import shlex
|
|
12
|
+
from io import BytesIO
|
|
11
13
|
from types import SimpleNamespace
|
|
12
14
|
from typing import Any, cast
|
|
13
15
|
|
|
14
16
|
import asyncssh
|
|
15
17
|
import mcp.types as mcp_types
|
|
16
18
|
import pytest
|
|
19
|
+
from PIL import Image
|
|
17
20
|
|
|
18
21
|
from hud.agents.claude.tools.coding import ClaudeBashTool, ClaudeTextEditorTool
|
|
19
22
|
from hud.agents.gemini.tools.coding import GeminiEditTool, GeminiShellTool
|
|
@@ -29,7 +32,7 @@ from hud.types import MCPToolCall
|
|
|
29
32
|
|
|
30
33
|
|
|
31
34
|
class _Completed:
|
|
32
|
-
def __init__(self, *, stdout: str = "", stderr: str = "", exit_status: int = 0) -> None:
|
|
35
|
+
def __init__(self, *, stdout: str | bytes = "", stderr: str = "", exit_status: int = 0) -> None:
|
|
33
36
|
self.stdout = stdout
|
|
34
37
|
self.stderr = stderr
|
|
35
38
|
self.exit_status = exit_status
|
|
@@ -71,7 +74,8 @@ class _Conn:
|
|
|
71
74
|
return _Completed(
|
|
72
75
|
stderr=f"cat: {parts[2]}: No such file or directory", exit_status=1
|
|
73
76
|
)
|
|
74
|
-
|
|
77
|
+
data = self._store[parts[2]]
|
|
78
|
+
return _Completed(stdout=data if encoding is None else data.decode(encoding))
|
|
75
79
|
if len(parts) == 3 and parts[:2] == ["cat", ">"]:
|
|
76
80
|
assert input is not None
|
|
77
81
|
self._store[parts[2]] = input.encode()
|
|
@@ -645,3 +649,52 @@ async def test_reading_a_missing_file_is_a_tool_error_not_a_raised_traceback() -
|
|
|
645
649
|
|
|
646
650
|
assert result.isError is True
|
|
647
651
|
assert "No such file or directory" in result_text(result)
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
@pytest.mark.parametrize("image_format", ["PNG", "JPEG", "GIF", "WEBP"])
|
|
655
|
+
@pytest.mark.parametrize("tool_class", [ClaudeTextEditorTool, ReadTool])
|
|
656
|
+
async def test_file_view_returns_images_as_image_content(
|
|
657
|
+
image_format: str, tool_class: type[ClaudeTextEditorTool] | type[ReadTool]
|
|
658
|
+
) -> None:
|
|
659
|
+
buffer = BytesIO()
|
|
660
|
+
Image.new("RGB", (32, 24), "red").save(buffer, format=image_format)
|
|
661
|
+
data = buffer.getvalue()
|
|
662
|
+
ssh = _FakeSSH(files={"/image with no extension": data})
|
|
663
|
+
tool = tool_class(spec=tool_class.default_spec("claude"), client=ssh)
|
|
664
|
+
|
|
665
|
+
result = await tool.execute(
|
|
666
|
+
{
|
|
667
|
+
"command": "view",
|
|
668
|
+
"path": "/image with no extension",
|
|
669
|
+
"filePath": "/image with no extension",
|
|
670
|
+
}
|
|
671
|
+
)
|
|
672
|
+
|
|
673
|
+
assert not result.isError
|
|
674
|
+
assert len(result.content) == 1
|
|
675
|
+
content = result.content[0]
|
|
676
|
+
assert isinstance(content, mcp_types.ImageContent)
|
|
677
|
+
assert content.mimeType == Image.MIME[image_format]
|
|
678
|
+
assert base64.b64decode(content.data) == data
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
@pytest.mark.parametrize("data", [b"\x00binary", b"\xff\xfeinvalid"])
|
|
682
|
+
async def test_file_view_rejects_non_image_binary(data: bytes) -> None:
|
|
683
|
+
ssh = _FakeSSH(files={"/binary": data})
|
|
684
|
+
tool = ClaudeTextEditorTool(spec=ClaudeTextEditorTool.default_spec("claude"), client=ssh)
|
|
685
|
+
|
|
686
|
+
result = await tool.execute({"command": "view", "path": "/binary"})
|
|
687
|
+
|
|
688
|
+
assert result.isError
|
|
689
|
+
assert "expected UTF-8 text or an image" in result_text(result)
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
async def test_file_view_preserves_unicode_text() -> None:
|
|
693
|
+
text = "Hello, 世界\n"
|
|
694
|
+
ssh = _FakeSSH(files={"/text.png": text.encode()})
|
|
695
|
+
tool = ClaudeTextEditorTool(spec=ClaudeTextEditorTool.default_spec("claude"), client=ssh)
|
|
696
|
+
|
|
697
|
+
result = await tool.execute({"command": "view", "path": "/text.png"})
|
|
698
|
+
|
|
699
|
+
assert not result.isError
|
|
700
|
+
assert result_text(result) == text
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Typed file content for agent tools."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
|
|
7
|
+
from mcp.types import ImageContent
|
|
8
|
+
|
|
9
|
+
from hud.agents.tools.base import tool_err, tool_ok
|
|
10
|
+
from hud.types import MCPToolResult
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def view_file(data: bytes) -> MCPToolResult:
|
|
14
|
+
"""Identify supported images by signature and preserve their encoded bytes."""
|
|
15
|
+
mime_type = None
|
|
16
|
+
if data.startswith(b"\x89PNG\r\n\x1a\n"):
|
|
17
|
+
mime_type = "image/png"
|
|
18
|
+
elif data.startswith(b"\xff\xd8\xff"):
|
|
19
|
+
mime_type = "image/jpeg"
|
|
20
|
+
elif data.startswith((b"GIF87a", b"GIF89a")):
|
|
21
|
+
mime_type = "image/gif"
|
|
22
|
+
elif data.startswith(b"RIFF") and data[8:12] == b"WEBP":
|
|
23
|
+
mime_type = "image/webp"
|
|
24
|
+
if mime_type is not None:
|
|
25
|
+
return MCPToolResult(
|
|
26
|
+
content=[
|
|
27
|
+
ImageContent(
|
|
28
|
+
type="image", mimeType=mime_type, data=base64.b64encode(data).decode("ascii")
|
|
29
|
+
)
|
|
30
|
+
]
|
|
31
|
+
)
|
|
32
|
+
try:
|
|
33
|
+
text = data.decode("utf-8")
|
|
34
|
+
except UnicodeDecodeError:
|
|
35
|
+
return tool_err("Cannot view this file: expected UTF-8 text or an image.")
|
|
36
|
+
if "\x00" in text:
|
|
37
|
+
return tool_err("Cannot view this file: expected UTF-8 text or an image.")
|
|
38
|
+
return tool_ok(text)
|
|
@@ -11,6 +11,7 @@ import asyncssh
|
|
|
11
11
|
import mcp.types as mcp_types
|
|
12
12
|
|
|
13
13
|
from hud.agents.tools.base import AgentTool, tool_err, tool_ok
|
|
14
|
+
from hud.agents.tools.file_view import view_file
|
|
14
15
|
from hud.capabilities import SSHClient
|
|
15
16
|
from hud.types import MCPToolResult
|
|
16
17
|
|
|
@@ -59,6 +60,14 @@ class SSHTool(AgentTool[SSHClient]):
|
|
|
59
60
|
except asyncssh.ProcessError as e:
|
|
60
61
|
return tool_err(_remote_error(e))
|
|
61
62
|
|
|
63
|
+
async def file_view(self, path: str) -> MCPToolResult:
|
|
64
|
+
"""Read a file as text or image content through SSH exec."""
|
|
65
|
+
try:
|
|
66
|
+
data = await self.client.read_bytes(path)
|
|
67
|
+
except asyncssh.ProcessError as e:
|
|
68
|
+
return tool_err(_remote_error(e))
|
|
69
|
+
return view_file(data)
|
|
70
|
+
|
|
62
71
|
async def file_write(self, path: str, content: str) -> MCPToolResult:
|
|
63
72
|
"""Write a text file through SSH exec."""
|
|
64
73
|
try:
|
|
@@ -7,7 +7,7 @@ import base64
|
|
|
7
7
|
import contextlib
|
|
8
8
|
import json
|
|
9
9
|
import shlex
|
|
10
|
-
from typing import TYPE_CHECKING, Any, ClassVar, Self
|
|
10
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Self, cast
|
|
11
11
|
from urllib.parse import urlsplit
|
|
12
12
|
|
|
13
13
|
import asyncssh
|
|
@@ -204,20 +204,22 @@ class SSHClient(CapabilityClient):
|
|
|
204
204
|
|
|
205
205
|
async def read_text(self, path: str, *, timeout_s: float | None = None) -> str:
|
|
206
206
|
"""Read a UTF-8 text file through the exec channel."""
|
|
207
|
+
return _decode(await self.read_bytes(path, timeout_s=timeout_s))
|
|
208
|
+
|
|
209
|
+
async def read_bytes(self, path: str, *, timeout_s: float | None = None) -> bytes:
|
|
210
|
+
"""Read a file through the exec channel without decoding it."""
|
|
207
211
|
if self._is_windows:
|
|
208
212
|
quoted = _powershell_quote(path)
|
|
209
213
|
script = f"[Convert]::ToBase64String([IO.File]::ReadAllBytes({quoted}))"
|
|
210
214
|
result = await self.run(_powershell(script), check=True, timeout=timeout_s)
|
|
211
|
-
return base64.b64decode(_stdout(result))
|
|
212
|
-
# encoding=None transports raw bytes: a strict connection-level UTF-8
|
|
213
|
-
# decode would raise on files with invalid UTF-8 instead of replacing.
|
|
215
|
+
return base64.b64decode(_stdout(result))
|
|
214
216
|
result = await self.run(
|
|
215
217
|
f"cat -- {shlex.quote(path)}",
|
|
216
218
|
check=True,
|
|
217
219
|
encoding=None,
|
|
218
220
|
timeout=timeout_s,
|
|
219
221
|
)
|
|
220
|
-
return
|
|
222
|
+
return cast("bytes", result.stdout)
|
|
221
223
|
|
|
222
224
|
async def write_text(
|
|
223
225
|
self,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|