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.
Files changed (318) hide show
  1. {hud-0.6.13.dev2 → hud-0.6.14}/PKG-INFO +3 -2
  2. {hud-0.6.13.dev2 → hud-0.6.14}/hud/__init__.py +4 -0
  3. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/claude/sdk/agent.py +73 -54
  4. hud-0.6.14/hud/agents/claude/sdk/computer_mcp.py +282 -0
  5. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/claude/tools/computer.py +11 -3
  6. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/openai/tools/coding.py +13 -37
  7. hud-0.6.14/hud/agents/tests/test_claude_sdk_agent.py +658 -0
  8. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/tests/test_provider_native_tools.py +151 -51
  9. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/tests/test_tool_agent.py +45 -0
  10. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/tool_agent.py +25 -3
  11. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/tools/rfb.py +34 -5
  12. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/tools/ssh.py +35 -3
  13. {hud-0.6.13.dev2 → hud-0.6.14}/hud/capabilities/base.py +5 -1
  14. hud-0.6.14/hud/capabilities/ssh.py +278 -0
  15. hud-0.6.14/hud/capabilities/tests/test_ssh.py +372 -0
  16. {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/__init__.py +28 -23
  17. {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/deploy.py +119 -73
  18. {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/eval.py +18 -5
  19. hud-0.6.14/hud/cli/qa.py +193 -0
  20. {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/sync.py +5 -2
  21. {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/tests/test_deploy.py +136 -86
  22. {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/tests/test_eval_config.py +29 -2
  23. hud-0.6.14/hud/cli/tests/test_qa.py +171 -0
  24. hud-0.6.14/hud/cli/tests/test_sync_export.py +58 -0
  25. hud-0.6.14/hud/cli/tests/test_usage.py +134 -0
  26. {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/utils/context.py +1 -1
  27. {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/utils/tests/test_context.py +1 -1
  28. hud-0.6.14/hud/cli/utils/usage.py +179 -0
  29. {hud-0.6.13.dev2 → hud-0.6.14}/hud/clients/client.py +24 -2
  30. {hud-0.6.13.dev2 → hud-0.6.14}/hud/clients/tests/test_connect.py +118 -0
  31. {hud-0.6.13.dev2 → hud-0.6.14}/hud/conftest.py +1 -0
  32. {hud-0.6.13.dev2 → hud-0.6.14}/hud/environment/egress.py +42 -23
  33. {hud-0.6.13.dev2 → hud-0.6.14}/hud/environment/env.py +5 -6
  34. {hud-0.6.13.dev2 → hud-0.6.14}/hud/environment/namespace.py +136 -44
  35. {hud-0.6.13.dev2 → hud-0.6.14}/hud/environment/tests/conftest.py +5 -5
  36. {hud-0.6.13.dev2 → hud-0.6.14}/hud/environment/tests/test_capability_backing.py +55 -34
  37. {hud-0.6.13.dev2 → hud-0.6.14}/hud/environment/tests/test_sessions.py +22 -8
  38. {hud-0.6.13.dev2 → hud-0.6.14}/hud/environment/tests/test_workspace.py +851 -25
  39. {hud-0.6.13.dev2 → hud-0.6.14}/hud/environment/workspace.py +330 -117
  40. {hud-0.6.13.dev2 → hud-0.6.14}/hud/eval/__init__.py +4 -0
  41. {hud-0.6.13.dev2 → hud-0.6.14}/hud/eval/docker-seccomp.json +11 -1
  42. {hud-0.6.13.dev2 → hud-0.6.14}/hud/eval/run.py +154 -41
  43. hud-0.6.14/hud/eval/runtime/__init__.py +40 -0
  44. hud-0.6.14/hud/eval/runtime/compose.py +598 -0
  45. hud-0.6.14/hud/eval/runtime/core.py +302 -0
  46. hud-0.6.14/hud/eval/runtime/daytona.py +299 -0
  47. hud-0.6.14/hud/eval/runtime/docker.py +390 -0
  48. hud-0.6.14/hud/eval/runtime/hosted.py +208 -0
  49. hud-0.6.14/hud/eval/runtime/hud.py +246 -0
  50. hud-0.6.14/hud/eval/runtime/local.py +217 -0
  51. hud-0.6.14/hud/eval/runtime/modal.py +372 -0
  52. {hud-0.6.13.dev2 → hud-0.6.14}/hud/eval/sync.py +11 -27
  53. {hud-0.6.13.dev2 → hud-0.6.14}/hud/eval/task.py +50 -41
  54. {hud-0.6.13.dev2 → hud-0.6.14}/hud/eval/taskset.py +72 -81
  55. {hud-0.6.13.dev2 → hud-0.6.14}/hud/eval/tests/test_docker_provider.py +814 -67
  56. {hud-0.6.13.dev2 → hud-0.6.14}/hud/eval/tests/test_hosted.py +304 -38
  57. hud-0.6.14/hud/eval/tests/test_local_runtime.py +273 -0
  58. {hud-0.6.13.dev2 → hud-0.6.14}/hud/eval/tests/test_rollout.py +403 -47
  59. {hud-0.6.13.dev2 → hud-0.6.14}/hud/eval/tests/test_shared.py +72 -6
  60. {hud-0.6.13.dev2 → hud-0.6.14}/hud/eval/tests/test_sync.py +45 -5
  61. {hud-0.6.13.dev2 → hud-0.6.14}/hud/eval/tests/test_task.py +161 -15
  62. {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/Dockerfile +5 -3
  63. hud-0.6.14/hud/integrations/harbor/__init__.py +13 -0
  64. hud-0.6.14/hud/integrations/harbor/adapt.py +1148 -0
  65. {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/env.py +438 -176
  66. {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/install.sh +4 -1
  67. hud-0.6.14/hud/integrations/harbor/tests/tasks/sidecar-reachability/environment/docker-compose.yaml +11 -0
  68. hud-0.6.14/hud/integrations/harbor/tests/tasks/sidecar-reachability/solution/solve.sh +39 -0
  69. hud-0.6.14/hud/integrations/harbor/tests/tasks/sidecar-reachability/task.toml +30 -0
  70. {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/sidecar-reachability/tests/Dockerfile +4 -2
  71. hud-0.6.14/hud/integrations/harbor/tests/tasks/sidecar-reachability/tests/test.sh +32 -0
  72. {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/verifier-lifecycle/tests/test.sh +4 -0
  73. hud-0.6.14/hud/integrations/harbor/tests/test_contract.py +1329 -0
  74. hud-0.6.14/hud/integrations/harbor/tests/test_integration.py +681 -0
  75. {hud-0.6.13.dev2 → hud-0.6.14}/hud/settings.py +12 -36
  76. {hud-0.6.13.dev2 → hud-0.6.14}/hud/utils/platform.py +1 -1
  77. {hud-0.6.13.dev2 → hud-0.6.14}/hud/utils/tests/test_platform.py +13 -0
  78. {hud-0.6.13.dev2 → hud-0.6.14}/hud/version.py +1 -1
  79. {hud-0.6.13.dev2 → hud-0.6.14}/pyproject.toml +1 -0
  80. hud-0.6.13.dev2/hud/agents/claude/sdk/computer_mcp.py +0 -147
  81. hud-0.6.13.dev2/hud/agents/tests/test_claude_sdk_agent.py +0 -241
  82. hud-0.6.13.dev2/hud/capabilities/ssh.py +0 -135
  83. hud-0.6.13.dev2/hud/cli/tests/test_sync_export.py +0 -27
  84. hud-0.6.13.dev2/hud/eval/_runtime_protocols.py +0 -212
  85. hud-0.6.13.dev2/hud/eval/compose.py +0 -172
  86. hud-0.6.13.dev2/hud/eval/runtime.py +0 -1667
  87. hud-0.6.13.dev2/hud/eval/tests/test_local_runtime.py +0 -440
  88. hud-0.6.13.dev2/hud/integrations/harbor/__init__.py +0 -12
  89. hud-0.6.13.dev2/hud/integrations/harbor/adapt.py +0 -734
  90. hud-0.6.13.dev2/hud/integrations/harbor/tests/tasks/sidecar-reachability/environment/compose.yaml +0 -11
  91. hud-0.6.13.dev2/hud/integrations/harbor/tests/tasks/sidecar-reachability/solution/solve.sh +0 -26
  92. hud-0.6.13.dev2/hud/integrations/harbor/tests/tasks/sidecar-reachability/task.toml +0 -26
  93. hud-0.6.13.dev2/hud/integrations/harbor/tests/tasks/sidecar-reachability/tests/test.sh +0 -19
  94. hud-0.6.13.dev2/hud/integrations/harbor/tests/test_contract.py +0 -896
  95. hud-0.6.13.dev2/hud/integrations/harbor/tests/test_integration.py +0 -339
  96. {hud-0.6.13.dev2 → hud-0.6.14}/.gitignore +0 -0
  97. {hud-0.6.13.dev2 → hud-0.6.14}/LICENSE +0 -0
  98. {hud-0.6.13.dev2 → hud-0.6.14}/README.md +0 -0
  99. {hud-0.6.13.dev2 → hud-0.6.14}/hud/__main__.py +0 -0
  100. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/__init__.py +0 -0
  101. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/base.py +0 -0
  102. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/browser_use/__init__.py +0 -0
  103. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/browser_use/agent.py +0 -0
  104. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/claude/__init__.py +0 -0
  105. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/claude/agent.py +0 -0
  106. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/claude/sdk/__init__.py +0 -0
  107. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/claude/tools/__init__.py +0 -0
  108. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/claude/tools/base.py +0 -0
  109. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/claude/tools/coding.py +0 -0
  110. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/claude/tools/hosted.py +0 -0
  111. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/claude/tools/mcp_proxy.py +0 -0
  112. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/claude/tools/settings.py +0 -0
  113. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/claude/tools/tests/__init__.py +0 -0
  114. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/claude/tools/tests/test_computer.py +0 -0
  115. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/gemini/__init__.py +0 -0
  116. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/gemini/agent.py +0 -0
  117. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/gemini/settings.py +0 -0
  118. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/gemini/tools/__init__.py +0 -0
  119. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/gemini/tools/base.py +0 -0
  120. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/gemini/tools/coding.py +0 -0
  121. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/gemini/tools/computer.py +0 -0
  122. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/gemini/tools/filesystem.py +0 -0
  123. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/gemini/tools/hosted.py +0 -0
  124. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/gemini/tools/mcp_proxy.py +0 -0
  125. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/gemini/tools/tests/__init__.py +0 -0
  126. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/gemini/tools/tests/test_computer.py +0 -0
  127. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/misc/__init__.py +0 -0
  128. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/misc/response_automation.py +0 -0
  129. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/openai/__init__.py +0 -0
  130. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/openai/agent.py +0 -0
  131. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/openai/tools/__init__.py +0 -0
  132. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/openai/tools/apply_patch.py +0 -0
  133. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/openai/tools/base.py +0 -0
  134. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/openai/tools/computer.py +0 -0
  135. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/openai/tools/hosted.py +0 -0
  136. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/openai/tools/mcp_proxy.py +0 -0
  137. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/openai/tools/strict_schema.py +0 -0
  138. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/openai/tools/tests/__init__.py +0 -0
  139. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/openai/tools/tests/test_computer.py +0 -0
  140. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/openai/tools/tests/test_strict_schema.py +0 -0
  141. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/openai_compatible/__init__.py +0 -0
  142. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/openai_compatible/agent.py +0 -0
  143. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/openai_compatible/tools/__init__.py +0 -0
  144. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/openai_compatible/tools/base.py +0 -0
  145. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/openai_compatible/tools/filesystem.py +0 -0
  146. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/openai_compatible/tools/mcp_proxy.py +0 -0
  147. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/robot/__init__.py +0 -0
  148. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/robot/adapter.py +0 -0
  149. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/robot/agent.py +0 -0
  150. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/robot/batching.py +0 -0
  151. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/robot/dataset.py +0 -0
  152. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/robot/model.py +0 -0
  153. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/tests/__init__.py +0 -0
  154. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/tests/test_apply_patch.py +0 -0
  155. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/tests/test_base.py +0 -0
  156. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/tests/test_claude_agent.py +0 -0
  157. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/tests/test_gemini_agent.py +0 -0
  158. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/tests/test_openai_agent.py +0 -0
  159. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/tests/test_openai_compatible_agent.py +0 -0
  160. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/tests/test_robot_dataset_writer.py +0 -0
  161. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/tests/test_trace.py +0 -0
  162. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/tools/__init__.py +0 -0
  163. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/tools/base.py +0 -0
  164. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/tools/hosted.py +0 -0
  165. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/tools/mcp.py +0 -0
  166. {hud-0.6.13.dev2 → hud-0.6.14}/hud/agents/types.py +0 -0
  167. {hud-0.6.13.dev2 → hud-0.6.14}/hud/capabilities/__init__.py +0 -0
  168. {hud-0.6.13.dev2 → hud-0.6.14}/hud/capabilities/cdp.py +0 -0
  169. {hud-0.6.13.dev2 → hud-0.6.14}/hud/capabilities/mcp.py +0 -0
  170. {hud-0.6.13.dev2 → hud-0.6.14}/hud/capabilities/rfb.py +0 -0
  171. {hud-0.6.13.dev2 → hud-0.6.14}/hud/capabilities/robot.py +0 -0
  172. {hud-0.6.13.dev2 → hud-0.6.14}/hud/capabilities/tests/__init__.py +0 -0
  173. {hud-0.6.13.dev2 → hud-0.6.14}/hud/capabilities/tests/test_rfb.py +0 -0
  174. {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/__main__.py +0 -0
  175. {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/cancel.py +0 -0
  176. {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/client.py +0 -0
  177. {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/init.py +0 -0
  178. {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/jobs.py +0 -0
  179. {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/login.py +0 -0
  180. {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/models.py +0 -0
  181. {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/presets.py +0 -0
  182. {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/serve.py +0 -0
  183. {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/task.py +0 -0
  184. {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/templates.py +0 -0
  185. {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/tests/__init__.py +0 -0
  186. {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/tests/test_cli_init.py +0 -0
  187. {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/tests/test_cli_main.py +0 -0
  188. {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/tests/test_cli_more_wrappers.py +0 -0
  189. {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/tests/test_eval_bedrock.py +0 -0
  190. {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/tests/test_init.py +0 -0
  191. {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/tests/test_main_module.py +0 -0
  192. {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/trace.py +0 -0
  193. {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/utils/__init__.py +0 -0
  194. {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/utils/api.py +0 -0
  195. {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/utils/build_display.py +0 -0
  196. {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/utils/build_logs.py +0 -0
  197. {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/utils/config.py +0 -0
  198. {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/utils/display.py +0 -0
  199. {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/utils/jobs.py +0 -0
  200. {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/utils/registry.py +0 -0
  201. {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/utils/source.py +0 -0
  202. {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/utils/tasks.py +0 -0
  203. {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/utils/tests/__init__.py +0 -0
  204. {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/utils/tests/test_build_display.py +0 -0
  205. {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/utils/tests/test_config.py +0 -0
  206. {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/utils/tests/test_registry.py +0 -0
  207. {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/utils/tests/test_source.py +0 -0
  208. {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/utils/tests/test_tasks.py +0 -0
  209. {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/utils/tests/test_version_check.py +0 -0
  210. {hud-0.6.13.dev2 → hud-0.6.14}/hud/cli/utils/version_check.py +0 -0
  211. {hud-0.6.13.dev2 → hud-0.6.14}/hud/clients/__init__.py +0 -0
  212. {hud-0.6.13.dev2 → hud-0.6.14}/hud/clients/tests/__init__.py +0 -0
  213. {hud-0.6.13.dev2 → hud-0.6.14}/hud/environment/__init__.py +0 -0
  214. {hud-0.6.13.dev2 → hud-0.6.14}/hud/environment/file_tracker.py +0 -0
  215. {hud-0.6.13.dev2 → hud-0.6.14}/hud/environment/robot/__init__.py +0 -0
  216. {hud-0.6.13.dev2 → hud-0.6.14}/hud/environment/robot/bridge.py +0 -0
  217. {hud-0.6.13.dev2 → hud-0.6.14}/hud/environment/robot/endpoint.py +0 -0
  218. {hud-0.6.13.dev2 → hud-0.6.14}/hud/environment/robot/gym.py +0 -0
  219. {hud-0.6.13.dev2 → hud-0.6.14}/hud/environment/server.py +0 -0
  220. {hud-0.6.13.dev2 → hud-0.6.14}/hud/environment/tests/__init__.py +0 -0
  221. {hud-0.6.13.dev2 → hud-0.6.14}/hud/environment/tests/test_file_tracker.py +0 -0
  222. {hud-0.6.13.dev2 → hud-0.6.14}/hud/environment/tests/test_file_tracking.py +0 -0
  223. {hud-0.6.13.dev2 → hud-0.6.14}/hud/environment/tests/test_loader.py +0 -0
  224. {hud-0.6.13.dev2 → hud-0.6.14}/hud/environment/tests/test_manifest.py +0 -0
  225. {hud-0.6.13.dev2 → hud-0.6.14}/hud/environment/tests/test_robot_regressions.py +0 -0
  226. {hud-0.6.13.dev2 → hud-0.6.14}/hud/environment/tests/test_server.py +0 -0
  227. {hud-0.6.13.dev2 → hud-0.6.14}/hud/environment/tests/test_tunnel.py +0 -0
  228. {hud-0.6.13.dev2 → hud-0.6.14}/hud/environment/utils.py +0 -0
  229. {hud-0.6.13.dev2 → hud-0.6.14}/hud/eval/chat.py +0 -0
  230. {hud-0.6.13.dev2 → hud-0.6.14}/hud/eval/file_tracking.py +0 -0
  231. {hud-0.6.13.dev2 → hud-0.6.14}/hud/eval/job.py +0 -0
  232. {hud-0.6.13.dev2 → hud-0.6.14}/hud/eval/tests/__init__.py +0 -0
  233. {hud-0.6.13.dev2 → hud-0.6.14}/hud/eval/tests/test_chat.py +0 -0
  234. {hud-0.6.13.dev2 → hud-0.6.14}/hud/eval/tests/test_file_tracking_observer.py +0 -0
  235. {hud-0.6.13.dev2 → hud-0.6.14}/hud/eval/tests/test_job.py +0 -0
  236. {hud-0.6.13.dev2 → hud-0.6.14}/hud/graders/__init__.py +0 -0
  237. {hud-0.6.13.dev2 → hud-0.6.14}/hud/graders/base.py +0 -0
  238. {hud-0.6.13.dev2 → hud-0.6.14}/hud/graders/bash.py +0 -0
  239. {hud-0.6.13.dev2 → hud-0.6.14}/hud/graders/combine.py +0 -0
  240. {hud-0.6.13.dev2 → hud-0.6.14}/hud/graders/judge.py +0 -0
  241. {hud-0.6.13.dev2 → hud-0.6.14}/hud/graders/results.py +0 -0
  242. {hud-0.6.13.dev2 → hud-0.6.14}/hud/graders/text.py +0 -0
  243. {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/__init__.py +0 -0
  244. {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/export.py +0 -0
  245. {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/__init__.py +0 -0
  246. {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/conftest.py +0 -0
  247. {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/agent-lifecycle/environment/Dockerfile +0 -0
  248. {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/agent-lifecycle/instruction.md +0 -0
  249. {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/agent-lifecycle/solution/solve.sh +0 -0
  250. {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/agent-lifecycle/task.toml +0 -0
  251. {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/agent-lifecycle/tests/test.sh +0 -0
  252. {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/hello-mcp/environment/Dockerfile +0 -0
  253. /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
  254. {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/hello-mcp/environment/mcp-server/Dockerfile +0 -0
  255. {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/hello-mcp/environment/mcp-server/__init__.py +0 -0
  256. {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/hello-mcp/environment/mcp-server/server.py +0 -0
  257. {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/hello-mcp/instruction.md +0 -0
  258. {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/hello-mcp/solution/solve.sh +0 -0
  259. {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/hello-mcp/task.toml +0 -0
  260. {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/hello-mcp/tests/test.sh +0 -0
  261. {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/phase-boundary/environment/Dockerfile +0 -0
  262. {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/phase-boundary/environment/entrypoint.sh +0 -0
  263. {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/phase-boundary/instruction.md +0 -0
  264. {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/phase-boundary/solution/solve.sh +0 -0
  265. {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/phase-boundary/task.toml +0 -0
  266. {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/phase-boundary/tests/test.sh +0 -0
  267. {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/sidecar-reachability/environment/Dockerfile +0 -0
  268. {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/sidecar-reachability/instruction.md +0 -0
  269. {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/verifier-lifecycle/environment/Dockerfile +0 -0
  270. {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/verifier-lifecycle/environment/entrypoint.sh +0 -0
  271. {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/verifier-lifecycle/instruction.md +0 -0
  272. {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/verifier-lifecycle/solution/solve.sh +0 -0
  273. {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/tasks/verifier-lifecycle/task.toml +0 -0
  274. {hud-0.6.13.dev2 → hud-0.6.14}/hud/integrations/harbor/tests/test_harbor.py +0 -0
  275. {hud-0.6.13.dev2 → hud-0.6.14}/hud/patches/__init__.py +0 -0
  276. {hud-0.6.13.dev2 → hud-0.6.14}/hud/patches/mcp_patches.py +0 -0
  277. {hud-0.6.13.dev2 → hud-0.6.14}/hud/patches/tests/__init__.py +0 -0
  278. {hud-0.6.13.dev2 → hud-0.6.14}/hud/patches/tests/test_warnings.py +0 -0
  279. {hud-0.6.13.dev2 → hud-0.6.14}/hud/patches/warnings.py +0 -0
  280. {hud-0.6.13.dev2 → hud-0.6.14}/hud/py.typed +0 -0
  281. {hud-0.6.13.dev2 → hud-0.6.14}/hud/telemetry/__init__.py +0 -0
  282. {hud-0.6.13.dev2 → hud-0.6.14}/hud/telemetry/context.py +0 -0
  283. {hud-0.6.13.dev2 → hud-0.6.14}/hud/telemetry/exporter.py +0 -0
  284. {hud-0.6.13.dev2 → hud-0.6.14}/hud/telemetry/instrument.py +0 -0
  285. {hud-0.6.13.dev2 → hud-0.6.14}/hud/telemetry/robot/__init__.py +0 -0
  286. {hud-0.6.13.dev2 → hud-0.6.14}/hud/telemetry/robot/recorder.py +0 -0
  287. {hud-0.6.13.dev2 → hud-0.6.14}/hud/telemetry/robot/video.py +0 -0
  288. {hud-0.6.13.dev2 → hud-0.6.14}/hud/telemetry/span.py +0 -0
  289. {hud-0.6.13.dev2 → hud-0.6.14}/hud/telemetry/tests/__init__.py +0 -0
  290. {hud-0.6.13.dev2 → hud-0.6.14}/hud/telemetry/tests/test_exporter.py +0 -0
  291. {hud-0.6.13.dev2 → hud-0.6.14}/hud/telemetry/tests/test_instrument.py +0 -0
  292. {hud-0.6.13.dev2 → hud-0.6.14}/hud/train/__init__.py +0 -0
  293. {hud-0.6.13.dev2 → hud-0.6.14}/hud/train/base.py +0 -0
  294. {hud-0.6.13.dev2 → hud-0.6.14}/hud/train/client.py +0 -0
  295. {hud-0.6.13.dev2 → hud-0.6.14}/hud/train/tests/__init__.py +0 -0
  296. {hud-0.6.13.dev2 → hud-0.6.14}/hud/train/tests/test_client.py +0 -0
  297. {hud-0.6.13.dev2 → hud-0.6.14}/hud/train/types.py +0 -0
  298. {hud-0.6.13.dev2 → hud-0.6.14}/hud/types.py +0 -0
  299. {hud-0.6.13.dev2 → hud-0.6.14}/hud/utils/__init__.py +0 -0
  300. {hud-0.6.13.dev2 → hud-0.6.14}/hud/utils/docker.py +0 -0
  301. {hud-0.6.13.dev2 → hud-0.6.14}/hud/utils/exceptions.py +0 -0
  302. {hud-0.6.13.dev2 → hud-0.6.14}/hud/utils/gateway.py +0 -0
  303. {hud-0.6.13.dev2 → hud-0.6.14}/hud/utils/hints.py +0 -0
  304. {hud-0.6.13.dev2 → hud-0.6.14}/hud/utils/hud_console.py +0 -0
  305. {hud-0.6.13.dev2 → hud-0.6.14}/hud/utils/modules.py +0 -0
  306. {hud-0.6.13.dev2 → hud-0.6.14}/hud/utils/naming.py +0 -0
  307. {hud-0.6.13.dev2 → hud-0.6.14}/hud/utils/process.py +0 -0
  308. {hud-0.6.13.dev2 → hud-0.6.14}/hud/utils/requests.py +0 -0
  309. {hud-0.6.13.dev2 → hud-0.6.14}/hud/utils/serialization.py +0 -0
  310. {hud-0.6.13.dev2 → hud-0.6.14}/hud/utils/tests/__init__.py +0 -0
  311. {hud-0.6.13.dev2 → hud-0.6.14}/hud/utils/tests/test_exceptions.py +0 -0
  312. {hud-0.6.13.dev2 → hud-0.6.14}/hud/utils/tests/test_gateway.py +0 -0
  313. {hud-0.6.13.dev2 → hud-0.6.14}/hud/utils/tests/test_hints.py +0 -0
  314. {hud-0.6.13.dev2 → hud-0.6.14}/hud/utils/tests/test_hud_console.py +0 -0
  315. {hud-0.6.13.dev2 → hud-0.6.14}/hud/utils/tests/test_process.py +0 -0
  316. {hud-0.6.13.dev2 → hud-0.6.14}/hud/utils/tests/test_requests.py +0 -0
  317. {hud-0.6.13.dev2 → hud-0.6.14}/hud/utils/tests/test_serialization.py +0 -0
  318. {hud-0.6.13.dev2 → hud-0.6.14}/hud/utils/time.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: hud
3
- Version: 0.6.13.dev2
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 RFBClient, SSHClient
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 and RFB are
74
- opened live off the run (we
75
- drive them); MCP servers are read as raw bindings and written into the CLI's
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
- self._mcp_servers = {}
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
- self._ssh = cast("SSHClient", await run.client.open("ssh"))
96
- self._shell = self._ssh.capability.params.get("shell", "bash")
97
-
98
- for cap in bindings:
99
- family = cap.protocol.split("/", 1)[0]
100
- if family == "mcp":
101
- token = cap.params.get("auth_token")
102
- transport = "http" if cap.params["transport"] == "streamable-http" else "sse"
103
- server_config: dict[str, Any] = {"type": transport, "url": cap.url}
104
- if token:
105
- server_config["headers"] = {"Authorization": f"Bearer {token}"}
106
- self._mcp_servers[cap.name] = server_config
107
- elif family == "rfb":
108
- from hud.agents.claude.sdk.computer_mcp import serve_computer_mcp
109
-
110
- rfb = cast("RFBClient", await run.client.open("rfb"))
111
- port = await serve_computer_mcp(rfb, self.config.screenshot_encoding)
112
- self._mcp_servers["computer-use"] = {
113
- "type": "http",
114
- "url": f"http://127.0.0.1:{port}/mcp",
115
- }
116
-
117
- await self._exec(
118
- run,
119
- prompt=run.prompt_text,
120
- max_steps=self.config.max_steps,
121
- system_prompt=self.config.system_prompt,
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
- assert self._ssh is not None
147
+ mcp_config_path = await self._write_mcp_config(ssh, mcp_servers)
133
148
 
134
- mcp_config_path = await self._write_mcp_config()
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(self._shell, run_cmd)
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 self._ssh.write_text(invocation.script_name, invocation.script_body)
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 self._ssh.conn.run(full_cmd, check=False)
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("exit=%s stdout=%d stderr=%d", completed.exit_status, len(stdout), len(stderr))
174
+ logger.info("returncode=%s stdout=%d stderr=%d", returncode, len(stdout), len(stderr))
160
175
 
161
- if completed.exit_status != 0 and not stdout.strip():
162
- error = stderr or f"claude CLI exited with status {completed.exit_status}"
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({"exit_status": completed.exit_status, "stderr": stderr})
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(self) -> str | None:
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 self._mcp_servers or self._ssh is None:
216
+ if not mcp_servers:
198
217
  return None
199
- mcp_json = json.dumps({"mcpServers": self._mcp_servers}, indent=2)
218
+ mcp_json = json.dumps({"mcpServers": mcp_servers}, indent=2)
200
219
  path = ".hud_mcp_config.json"
201
- await self._ssh.write_text(path, mcp_json)
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 = self._shell in WINDOWS_SHELLS
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
- keys = _split_keys(arguments.get("text"))
226
- if not keys:
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
- await self.press_keys(keys, count=min(count, 100))
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
- env_arguments: dict[str, Any] = {}
77
+ timeout_seconds: float | None = None
79
78
  timeout_ms = arguments.get("timeout_ms")
80
79
  if isinstance(timeout_ms, int):
81
- env_arguments["timeout_seconds"] = timeout_ms / 1000.0
80
+ timeout_seconds = timeout_ms / 1000.0
82
81
 
83
82
  for command in command_list:
84
- if env_arguments.get("timeout_seconds"):
85
- full_cmd = f"timeout {int(env_arguments['timeout_seconds'])} {command}"
86
- else:
87
- full_cmd = command
88
- completed = await self.client.conn.run(full_cmd, check=False)
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.exit_status if completed.exit_status is not None else 1
92
- stdout, stderr = _bound_output(stdout, stderr, max_output_length)
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,