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.
Files changed (292) hide show
  1. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/PKG-INFO +1 -1
  2. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/claude/agent.py +18 -11
  3. hud-0.6.19.dev2/hud/agents/claude/images.py +65 -0
  4. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/claude/tools/coding.py +1 -1
  5. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/openai_compatible/tools/filesystem.py +4 -3
  6. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/tests/test_claude_agent.py +117 -0
  7. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/tests/test_provider_native_tools.py +55 -2
  8. hud-0.6.19.dev2/hud/agents/tools/file_view.py +38 -0
  9. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/tools/ssh.py +9 -0
  10. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/capabilities/ssh.py +7 -5
  11. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/version.py +1 -1
  12. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/.gitignore +0 -0
  13. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/LICENSE +0 -0
  14. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/README.md +0 -0
  15. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/__init__.py +0 -0
  16. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/__main__.py +0 -0
  17. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/__init__.py +0 -0
  18. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/base.py +0 -0
  19. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/browser_use/__init__.py +0 -0
  20. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/browser_use/agent.py +0 -0
  21. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/claude/__init__.py +0 -0
  22. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/claude/sdk/__init__.py +0 -0
  23. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/claude/sdk/agent.py +0 -0
  24. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/claude/sdk/computer_mcp.py +0 -0
  25. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/claude/sdk/events.py +0 -0
  26. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/claude/tools/__init__.py +0 -0
  27. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/claude/tools/base.py +0 -0
  28. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/claude/tools/computer.py +0 -0
  29. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/claude/tools/hosted.py +0 -0
  30. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/claude/tools/mcp_proxy.py +0 -0
  31. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/claude/tools/settings.py +0 -0
  32. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/claude/tools/tests/__init__.py +0 -0
  33. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/claude/tools/tests/test_computer.py +0 -0
  34. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/cli.py +0 -0
  35. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/codex/__init__.py +0 -0
  36. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/codex/agent.py +0 -0
  37. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/gemini/__init__.py +0 -0
  38. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/gemini/agent.py +0 -0
  39. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/gemini/settings.py +0 -0
  40. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/gemini/tools/__init__.py +0 -0
  41. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/gemini/tools/base.py +0 -0
  42. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/gemini/tools/coding.py +0 -0
  43. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/gemini/tools/computer.py +0 -0
  44. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/gemini/tools/filesystem.py +0 -0
  45. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/gemini/tools/hosted.py +0 -0
  46. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/gemini/tools/mcp_proxy.py +0 -0
  47. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/gemini/tools/tests/__init__.py +0 -0
  48. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/gemini/tools/tests/test_computer.py +0 -0
  49. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/misc/__init__.py +0 -0
  50. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/misc/response_automation.py +0 -0
  51. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/openai/__init__.py +0 -0
  52. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/openai/agent.py +0 -0
  53. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/openai/tools/__init__.py +0 -0
  54. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/openai/tools/apply_patch.py +0 -0
  55. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/openai/tools/base.py +0 -0
  56. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/openai/tools/coding.py +0 -0
  57. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/openai/tools/computer.py +0 -0
  58. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/openai/tools/hosted.py +0 -0
  59. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/openai/tools/mcp_proxy.py +0 -0
  60. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/openai/tools/strict_schema.py +0 -0
  61. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/openai/tools/tests/__init__.py +0 -0
  62. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/openai/tools/tests/test_computer.py +0 -0
  63. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/openai/tools/tests/test_strict_schema.py +0 -0
  64. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/openai_compatible/__init__.py +0 -0
  65. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/openai_compatible/agent.py +0 -0
  66. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/openai_compatible/tools/__init__.py +0 -0
  67. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/openai_compatible/tools/base.py +0 -0
  68. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/openai_compatible/tools/mcp_proxy.py +0 -0
  69. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/robot/__init__.py +0 -0
  70. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/robot/adapter.py +0 -0
  71. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/robot/agent.py +0 -0
  72. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/robot/batching.py +0 -0
  73. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/robot/dataset.py +0 -0
  74. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/robot/model.py +0 -0
  75. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/tests/__init__.py +0 -0
  76. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/tests/cli_fakes.py +0 -0
  77. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/tests/test_apply_patch.py +0 -0
  78. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/tests/test_base.py +0 -0
  79. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/tests/test_claude_cli_agent.py +0 -0
  80. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/tests/test_codex_cli_agent.py +0 -0
  81. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/tests/test_gemini_agent.py +0 -0
  82. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/tests/test_openai_agent.py +0 -0
  83. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/tests/test_openai_compatible_agent.py +0 -0
  84. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/tests/test_robot_dataset_writer.py +0 -0
  85. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/tests/test_tool_agent.py +0 -0
  86. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/tests/test_trace.py +0 -0
  87. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/tool_agent.py +0 -0
  88. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/tools/__init__.py +0 -0
  89. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/tools/base.py +0 -0
  90. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/tools/hosted.py +0 -0
  91. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/tools/mcp.py +0 -0
  92. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/tools/rfb.py +0 -0
  93. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/agents/types.py +0 -0
  94. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/capabilities/__init__.py +0 -0
  95. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/capabilities/base.py +0 -0
  96. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/capabilities/cdp.py +0 -0
  97. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/capabilities/connection.py +0 -0
  98. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/capabilities/mcp.py +0 -0
  99. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/capabilities/rfb.py +0 -0
  100. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/capabilities/robot.py +0 -0
  101. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/capabilities/tests/__init__.py +0 -0
  102. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/capabilities/tests/test_rfb.py +0 -0
  103. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/capabilities/tests/test_ssh.py +0 -0
  104. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/cli/__init__.py +0 -0
  105. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/cli/__main__.py +0 -0
  106. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/cli/deploy.py +0 -0
  107. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/cli/eval.py +0 -0
  108. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/cli/init.py +0 -0
  109. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/cli/jobs.py +0 -0
  110. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/cli/models.py +0 -0
  111. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/cli/project.py +0 -0
  112. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/cli/qa.py +0 -0
  113. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/cli/serve.py +0 -0
  114. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/cli/sync.py +0 -0
  115. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/cli/task.py +0 -0
  116. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/cli/tests/__init__.py +0 -0
  117. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/cli/tests/test_cli.py +0 -0
  118. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/cli/tests/test_deploy.py +0 -0
  119. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/cli/tests/test_eval.py +0 -0
  120. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/cli/tests/test_init.py +0 -0
  121. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/cli/tests/test_jobs.py +0 -0
  122. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/cli/tests/test_main.py +0 -0
  123. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/cli/tests/test_project.py +0 -0
  124. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/cli/tests/test_qa.py +0 -0
  125. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/cli/tests/test_sync.py +0 -0
  126. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/cli/tests/test_task.py +0 -0
  127. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/cli/tests/test_trace.py +0 -0
  128. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/cli/trace.py +0 -0
  129. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/clients/__init__.py +0 -0
  130. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/clients/client.py +0 -0
  131. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/clients/tests/__init__.py +0 -0
  132. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/clients/tests/test_connect.py +0 -0
  133. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/conftest.py +0 -0
  134. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/__init__.py +0 -0
  135. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/arguments.py +0 -0
  136. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/egress.py +0 -0
  137. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/env.py +0 -0
  138. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/file_tracker.py +0 -0
  139. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/namespace.py +0 -0
  140. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/process_guard.py +0 -0
  141. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/robot/__init__.py +0 -0
  142. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/robot/bridge.py +0 -0
  143. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/robot/endpoint.py +0 -0
  144. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/robot/gym.py +0 -0
  145. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/server.py +0 -0
  146. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/tests/__init__.py +0 -0
  147. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/tests/conftest.py +0 -0
  148. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/tests/test_arguments.py +0 -0
  149. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/tests/test_capability_backing.py +0 -0
  150. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/tests/test_file_tracker.py +0 -0
  151. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/tests/test_file_tracking.py +0 -0
  152. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/tests/test_loader.py +0 -0
  153. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/tests/test_manifest.py +0 -0
  154. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/tests/test_platform.py +0 -0
  155. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/tests/test_process_connection.py +0 -0
  156. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/tests/test_robot_regressions.py +0 -0
  157. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/tests/test_server.py +0 -0
  158. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/tests/test_sessions.py +0 -0
  159. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/tests/test_tunnel.py +0 -0
  160. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/tests/test_workspace.py +0 -0
  161. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/utils.py +0 -0
  162. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/environment/workspace.py +0 -0
  163. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/__init__.py +0 -0
  164. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/chat.py +0 -0
  165. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/docker-seccomp.json +0 -0
  166. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/file_tracking.py +0 -0
  167. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/job.py +0 -0
  168. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/run.py +0 -0
  169. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/runtime/__init__.py +0 -0
  170. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/runtime/compose.py +0 -0
  171. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/runtime/core.py +0 -0
  172. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/runtime/daytona.py +0 -0
  173. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/runtime/docker.py +0 -0
  174. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/runtime/hosted.py +0 -0
  175. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/runtime/hud.py +0 -0
  176. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/runtime/local.py +0 -0
  177. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/runtime/modal.py +0 -0
  178. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/sync.py +0 -0
  179. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/task.py +0 -0
  180. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/taskset.py +0 -0
  181. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/tests/__init__.py +0 -0
  182. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/tests/test_chat.py +0 -0
  183. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/tests/test_docker_provider.py +0 -0
  184. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/tests/test_file_tracking_observer.py +0 -0
  185. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/tests/test_hosted.py +0 -0
  186. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/tests/test_job.py +0 -0
  187. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/tests/test_local_runtime.py +0 -0
  188. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/tests/test_rollout.py +0 -0
  189. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/tests/test_shared.py +0 -0
  190. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/tests/test_sync.py +0 -0
  191. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/eval/tests/test_task.py +0 -0
  192. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/graders/__init__.py +0 -0
  193. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/graders/base.py +0 -0
  194. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/graders/bash.py +0 -0
  195. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/graders/combine.py +0 -0
  196. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/graders/judge.py +0 -0
  197. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/graders/results.py +0 -0
  198. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/graders/text.py +0 -0
  199. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/__init__.py +0 -0
  200. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/Dockerfile +0 -0
  201. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/__init__.py +0 -0
  202. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/adapt.py +0 -0
  203. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/env.py +0 -0
  204. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/export.py +0 -0
  205. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/install.sh +0 -0
  206. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/__init__.py +0 -0
  207. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/conftest.py +0 -0
  208. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/agent-lifecycle/environment/Dockerfile +0 -0
  209. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/agent-lifecycle/instruction.md +0 -0
  210. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/agent-lifecycle/solution/solve.sh +0 -0
  211. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/agent-lifecycle/task.toml +0 -0
  212. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/agent-lifecycle/tests/test.sh +0 -0
  213. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/hello-mcp/environment/Dockerfile +0 -0
  214. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/hello-mcp/environment/docker-compose.yaml +0 -0
  215. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/hello-mcp/environment/mcp-server/Dockerfile +0 -0
  216. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/hello-mcp/environment/mcp-server/__init__.py +0 -0
  217. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/hello-mcp/environment/mcp-server/server.py +0 -0
  218. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/hello-mcp/instruction.md +0 -0
  219. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/hello-mcp/solution/solve.sh +0 -0
  220. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/hello-mcp/task.toml +0 -0
  221. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/hello-mcp/tests/test.sh +0 -0
  222. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/phase-boundary/environment/Dockerfile +0 -0
  223. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/phase-boundary/environment/entrypoint.sh +0 -0
  224. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/phase-boundary/instruction.md +0 -0
  225. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/phase-boundary/solution/solve.sh +0 -0
  226. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/phase-boundary/task.toml +0 -0
  227. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/phase-boundary/tests/test.sh +0 -0
  228. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/sidecar-reachability/environment/Dockerfile +0 -0
  229. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/sidecar-reachability/environment/docker-compose.yaml +0 -0
  230. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/sidecar-reachability/instruction.md +0 -0
  231. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/sidecar-reachability/solution/solve.sh +0 -0
  232. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/sidecar-reachability/task.toml +0 -0
  233. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/sidecar-reachability/tests/Dockerfile +0 -0
  234. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/sidecar-reachability/tests/test.sh +0 -0
  235. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/verifier-lifecycle/environment/Dockerfile +0 -0
  236. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/verifier-lifecycle/environment/entrypoint.sh +0 -0
  237. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/verifier-lifecycle/instruction.md +0 -0
  238. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/verifier-lifecycle/solution/solve.sh +0 -0
  239. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/verifier-lifecycle/task.toml +0 -0
  240. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/tasks/verifier-lifecycle/tests/test.sh +0 -0
  241. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/test_contract.py +0 -0
  242. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/test_harbor.py +0 -0
  243. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/integrations/harbor/tests/test_integration.py +0 -0
  244. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/patches/__init__.py +0 -0
  245. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/patches/mcp_patches.py +0 -0
  246. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/patches/tests/__init__.py +0 -0
  247. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/patches/tests/test_warnings.py +0 -0
  248. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/patches/warnings.py +0 -0
  249. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/py.typed +0 -0
  250. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/settings.py +0 -0
  251. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/telemetry/__init__.py +0 -0
  252. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/telemetry/context.py +0 -0
  253. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/telemetry/exporter.py +0 -0
  254. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/telemetry/instrument.py +0 -0
  255. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/telemetry/robot/__init__.py +0 -0
  256. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/telemetry/robot/recorder.py +0 -0
  257. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/telemetry/robot/video.py +0 -0
  258. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/telemetry/span.py +0 -0
  259. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/telemetry/tests/__init__.py +0 -0
  260. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/telemetry/tests/test_exporter.py +0 -0
  261. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/telemetry/tests/test_instrument.py +0 -0
  262. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/train/__init__.py +0 -0
  263. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/train/base.py +0 -0
  264. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/train/client.py +0 -0
  265. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/train/tests/__init__.py +0 -0
  266. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/train/tests/test_client.py +0 -0
  267. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/train/types.py +0 -0
  268. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/types.py +0 -0
  269. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/utils/__init__.py +0 -0
  270. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/utils/docker.py +0 -0
  271. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/utils/exceptions.py +0 -0
  272. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/utils/gateway.py +0 -0
  273. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/utils/hints.py +0 -0
  274. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/utils/hud_console.py +0 -0
  275. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/utils/modules.py +0 -0
  276. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/utils/naming.py +0 -0
  277. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/utils/platform.py +0 -0
  278. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/utils/process.py +0 -0
  279. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/utils/requests.py +0 -0
  280. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/utils/serialization.py +0 -0
  281. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/utils/tests/__init__.py +0 -0
  282. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/utils/tests/test_exceptions.py +0 -0
  283. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/utils/tests/test_gateway.py +0 -0
  284. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/utils/tests/test_hints.py +0 -0
  285. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/utils/tests/test_hud_console.py +0 -0
  286. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/utils/tests/test_naming.py +0 -0
  287. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/utils/tests/test_platform.py +0 -0
  288. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/utils/tests/test_process.py +0 -0
  289. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/utils/tests/test_requests.py +0 -0
  290. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/utils/tests/test_serialization.py +0 -0
  291. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/hud/utils/time.py +0 -0
  292. {hud-0.6.19.dev1 → hud-0.6.19.dev2}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: hud
3
- Version: 0.6.19.dev1
3
+ Version: 0.6.19.dev2
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
@@ -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, Literal, cast
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
- block = BetaImageBlockParam(
172
- type="image",
173
- source=BetaBase64ImageSourceParam(
174
- type="base64",
175
- media_type=cast("ClaudeImageMediaType", content.mimeType),
176
- data=content.data,
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.file_read(path)
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 a file or directory from the workspace. Use offset and limit for pagination."
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.file_read(path)
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
- return _Completed(stdout=self._store[parts[2]].decode())
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)).decode("utf-8", errors="replace")
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 _decode(result.stdout)
222
+ return cast("bytes", result.stdout)
221
223
 
222
224
  async def write_text(
223
225
  self,
@@ -4,4 +4,4 @@ Version information for the HUD SDK.
4
4
 
5
5
  from __future__ import annotations
6
6
 
7
- __version__ = "0.6.19.dev1"
7
+ __version__ = "0.6.19.dev2"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes