verifiers 0.3.2.dev138__tar.gz → 0.3.2.dev139__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 (220) hide show
  1. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/PKG-INFO +1 -1
  2. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/runtimes/base.py +5 -1
  3. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/runtimes/container.py +135 -64
  4. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/runtimes/docker/__init__.py +80 -7
  5. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/runtimes/modal.py +12 -0
  6. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/runtimes/prime.py +3 -0
  7. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/runtimes/subprocess.py +3 -0
  8. verifiers-0.3.2.dev139/verifiers/v1/tasksets/harbor/compose.py +328 -0
  9. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/tasksets/harbor/env.py +59 -7
  10. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/tasksets/harbor/taskset.py +78 -29
  11. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/utils/artifacts.py +3 -2
  12. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/.gitignore +0 -0
  13. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/LICENSE +0 -0
  14. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/README.md +0 -0
  15. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/pyproject.toml +0 -0
  16. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/tests/v1/conftest.py +0 -0
  17. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/tests/v1/fixtures/counter_tool_v1.py +0 -0
  18. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/tests/v1/fixtures/duet_v1.py +0 -0
  19. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/tests/v1/fixtures/echo_acp_resume_v1.py +0 -0
  20. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/tests/v1/fixtures/echo_agentic_v1.py +0 -0
  21. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/tests/v1/fixtures/echo_tool_v1.py +0 -0
  22. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/tests/v1/fixtures/echo_user_sim_v1.py +0 -0
  23. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/tests/v1/fixtures/echo_v1.py +0 -0
  24. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/tests/v1/fixtures/pyproject.toml +0 -0
  25. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/tests/v1/fixtures/tool_response_image_v1.py +0 -0
  26. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/tests/v1/test_color_codeword_tasks.py +0 -0
  27. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/tests/v1/test_configs.py +0 -0
  28. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/tests/v1/test_e2e.py +0 -0
  29. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/tests/v1/test_envs.py +0 -0
  30. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/tests/v1/test_graph.py +0 -0
  31. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/tests/v1/test_judges.py +0 -0
  32. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/tests/v1/test_platform_run.py +0 -0
  33. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/tests/v1/test_scoring.py +0 -0
  34. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/tests/v1/test_serve_delta.py +0 -0
  35. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/tests/v1/test_taskset.py +0 -0
  36. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/tests/v1/test_trace.py +0 -0
  37. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/__init__.py +0 -0
  38. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/__init__.py +0 -0
  39. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/acp/__init__.py +0 -0
  40. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/acp/runner.py +0 -0
  41. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/agent.py +0 -0
  42. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/cli/__init__.py +0 -0
  43. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/cli/dashboard/__init__.py +0 -0
  44. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/cli/dashboard/base.py +0 -0
  45. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/cli/dashboard/eval.py +0 -0
  46. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/cli/dashboard/replay.py +0 -0
  47. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/cli/dashboard/validate.py +0 -0
  48. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/cli/debug.py +0 -0
  49. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/cli/eval/__init__.py +0 -0
  50. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/cli/eval/hint.py +0 -0
  51. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/cli/eval/main.py +0 -0
  52. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/cli/eval/resume.py +0 -0
  53. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/cli/eval/runner.py +0 -0
  54. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/cli/gepa.py +0 -0
  55. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/cli/init.py +0 -0
  56. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/cli/output.py +0 -0
  57. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/cli/replay.py +0 -0
  58. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/cli/resolve.py +0 -0
  59. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/cli/resume.py +0 -0
  60. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/cli/validate.py +0 -0
  61. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/clients/__init__.py +0 -0
  62. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/clients/base.py +0 -0
  63. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/clients/client.py +0 -0
  64. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/clients/eval.py +0 -0
  65. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/clients/train.py +0 -0
  66. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/configs/__init__.py +0 -0
  67. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/configs/agent.py +0 -0
  68. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/configs/cli/__init__.py +0 -0
  69. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/configs/cli/debug.py +0 -0
  70. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/configs/cli/env.py +0 -0
  71. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/configs/cli/eval.py +0 -0
  72. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/configs/cli/init.py +0 -0
  73. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/configs/cli/replay.py +0 -0
  74. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/configs/cli/validate.py +0 -0
  75. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/configs/client.py +0 -0
  76. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/configs/env.py +0 -0
  77. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/configs/harness.py +0 -0
  78. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/configs/judge.py +0 -0
  79. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/configs/retries.py +0 -0
  80. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/configs/runtime.py +0 -0
  81. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/configs/serve.py +0 -0
  82. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/configs/task.py +0 -0
  83. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/configs/taskset.py +0 -0
  84. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/dialects/__init__.py +0 -0
  85. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/dialects/anthropic.py +0 -0
  86. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/dialects/base.py +0 -0
  87. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/dialects/chat.py +0 -0
  88. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/dialects/responses.py +0 -0
  89. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/env.py +0 -0
  90. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/envs/__init__.py +0 -0
  91. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/envs/agentic_judge/__init__.py +0 -0
  92. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/envs/agentic_judge/env.py +0 -0
  93. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/envs/best_of_n/__init__.py +0 -0
  94. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/envs/best_of_n/env.py +0 -0
  95. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/envs/isolated_verifier/__init__.py +0 -0
  96. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/envs/isolated_verifier/env.py +0 -0
  97. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/envs/shared_agentic_judge/__init__.py +0 -0
  98. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/envs/single_agent/__init__.py +0 -0
  99. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/envs/single_agent/env.py +0 -0
  100. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/envs/user_sim/__init__.py +0 -0
  101. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/envs/user_sim/env.py +0 -0
  102. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/episode.py +0 -0
  103. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/errors.py +0 -0
  104. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/gepa/__init__.py +0 -0
  105. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/gepa/adapter.py +0 -0
  106. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/gepa/config.py +0 -0
  107. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/gepa/dataset.py +0 -0
  108. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/gepa/reflection.py +0 -0
  109. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/gepa/runner.py +0 -0
  110. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/graph.py +0 -0
  111. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/harness.py +0 -0
  112. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/harnesses/__init__.py +0 -0
  113. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/harnesses/bash/__init__.py +0 -0
  114. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/harnesses/bash/harness.py +0 -0
  115. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/harnesses/browser_use/__init__.py +0 -0
  116. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/harnesses/browser_use/harness.py +0 -0
  117. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/harnesses/browser_use/program.py +0 -0
  118. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/harnesses/claude_code/__init__.py +0 -0
  119. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/harnesses/claude_code/harness.py +0 -0
  120. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/harnesses/codex/__init__.py +0 -0
  121. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/harnesses/codex/gate.mjs +0 -0
  122. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/harnesses/codex/harness.py +0 -0
  123. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/harnesses/hermes_agent/__init__.py +0 -0
  124. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/harnesses/hermes_agent/harness.py +0 -0
  125. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/harnesses/hermes_agent/program.py +0 -0
  126. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/harnesses/kimi_code/__init__.py +0 -0
  127. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/harnesses/kimi_code/harness.py +0 -0
  128. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/harnesses/mini_swe_agent/__init__.py +0 -0
  129. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/harnesses/mini_swe_agent/harness.py +0 -0
  130. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/harnesses/mini_swe_agent/program.py +0 -0
  131. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/harnesses/node.py +0 -0
  132. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/harnesses/null/__init__.py +0 -0
  133. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/harnesses/null/harness.py +0 -0
  134. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/harnesses/openclaw/__init__.py +0 -0
  135. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/harnesses/openclaw/harness.py +0 -0
  136. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/harnesses/pi/__init__.py +0 -0
  137. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/harnesses/pi/gate.mjs +0 -0
  138. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/harnesses/pi/harness.py +0 -0
  139. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/harnesses/prime_agent/__init__.py +0 -0
  140. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/harnesses/prime_agent/harness.py +0 -0
  141. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/harnesses/rlm/__init__.py +0 -0
  142. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/harnesses/rlm/harness.py +0 -0
  143. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/harnesses/terminus_2/__init__.py +0 -0
  144. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/harnesses/terminus_2/harness.py +0 -0
  145. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/harnesses/terminus_2/program.py +0 -0
  146. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/harnesses/utils/__init__.py +0 -0
  147. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/harnesses/utils/compaction.py +0 -0
  148. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/harnesses/utils/core.py +0 -0
  149. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/harnesses/utils/install.py +0 -0
  150. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/harnesses/utils/launch.py +0 -0
  151. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/harnesses/utils/mcp.py +0 -0
  152. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/interception/__init__.py +0 -0
  153. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/interception/base.py +0 -0
  154. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/interception/pool.py +0 -0
  155. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/interception/server.py +0 -0
  156. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/interception/tunnel/__init__.py +0 -0
  157. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/interception/tunnel/base.py +0 -0
  158. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/interception/tunnel/custom.py +0 -0
  159. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/interception/tunnel/prime.py +0 -0
  160. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/judge.py +0 -0
  161. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/judges/__init__.py +0 -0
  162. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/judges/reference.py +0 -0
  163. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/judges/reference.txt +0 -0
  164. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/judges/rubric.py +0 -0
  165. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/judges/rubric.txt +0 -0
  166. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/mcp/__init__.py +0 -0
  167. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/mcp/launch.py +0 -0
  168. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/mcp/server.py +0 -0
  169. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/mcp/toolset.py +0 -0
  170. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/rollout.py +0 -0
  171. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/runtimes/__init__.py +0 -0
  172. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/runtimes/apptainer.py +0 -0
  173. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/runtimes/docker/egress.py +0 -0
  174. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/runtimes/limiters.py +0 -0
  175. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/semantic.py +0 -0
  176. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/serve/__init__.py +0 -0
  177. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/serve/client.py +0 -0
  178. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/serve/delta.py +0 -0
  179. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/serve/encoding.py +0 -0
  180. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/serve/pool.py +0 -0
  181. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/serve/server.py +0 -0
  182. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/serve/types.py +0 -0
  183. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/session.py +0 -0
  184. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/state.py +0 -0
  185. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/task.py +0 -0
  186. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/taskset.py +0 -0
  187. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/tasksets/__init__.py +0 -0
  188. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/tasksets/harbor/__init__.py +0 -0
  189. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/tasksets/harbor/toolset.py +0 -0
  190. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/tasksets/nemo_gym/__init__.py +0 -0
  191. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/tasksets/nemo_gym/response.py +0 -0
  192. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/tasksets/nemo_gym/server.py +0 -0
  193. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/tasksets/nemo_gym/taskset.py +0 -0
  194. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/tasksets/nemo_gym/toolset.py +0 -0
  195. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/tasksets/openenv/__init__.py +0 -0
  196. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/tasksets/openenv/taskset.py +0 -0
  197. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/tasksets/textarena/__init__.py +0 -0
  198. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/tasksets/textarena/taskset.py +0 -0
  199. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/trace.py +0 -0
  200. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/types.py +0 -0
  201. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/utils/__init__.py +0 -0
  202. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/utils/aio.py +0 -0
  203. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/utils/compile.py +0 -0
  204. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/utils/decorators.py +0 -0
  205. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/utils/format.py +0 -0
  206. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/utils/generic.py +0 -0
  207. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/utils/git.py +0 -0
  208. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/utils/image.py +0 -0
  209. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/utils/interrupt.py +0 -0
  210. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/utils/loaders.py +0 -0
  211. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/utils/logging.py +0 -0
  212. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/utils/memory.py +0 -0
  213. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/utils/paths.py +0 -0
  214. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/utils/platform.py +0 -0
  215. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/utils/prime.py +0 -0
  216. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/utils/retries.py +0 -0
  217. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/utils/scope.py +0 -0
  218. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/utils/score.py +0 -0
  219. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/utils/trace_store.py +0 -0
  220. {verifiers-0.3.2.dev138 → verifiers-0.3.2.dev139}/verifiers/v1/utils/version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: verifiers
3
- Version: 0.3.2.dev138
3
+ Version: 0.3.2.dev139
4
4
  Summary: Verifiers: Environments for LLM Reinforcement Learning
5
5
  Project-URL: Homepage, https://github.com/primeintellect-ai/verifiers
6
6
  Project-URL: Documentation, https://github.com/primeintellect-ai/verifiers
@@ -72,6 +72,10 @@ class RuntimeProcess(ABC):
72
72
  async def wait(self) -> int:
73
73
  pass
74
74
 
75
+ @abstractmethod
76
+ async def poll(self) -> int | None:
77
+ """Return the underlying process's exit code without waiting for it to exit."""
78
+
75
79
  @abstractmethod
76
80
  async def terminate(self) -> None:
77
81
  pass
@@ -134,7 +138,7 @@ class BaseRuntimeInfo(BaseConfig):
134
138
  class Runtime(ABC):
135
139
  __slots__ = ("env",)
136
140
 
137
- is_local: ClassVar[bool] = True
141
+ is_local: bool = True
138
142
  """Whether this runtime exchanges host-local URLs without a public tunnel. True for
139
143
  subprocess and the local container runtimes; remote runtimes override to False and
140
144
  use a host `Tunnel` inward plus `expose` outward."""
@@ -1,4 +1,4 @@
1
- """Shared machinery for local container runtimes driven through a CLI `exec`."""
1
+ """Container operations through a CLI on the local machine or an owned runtime."""
2
2
 
3
3
  import asyncio
4
4
  import contextlib
@@ -7,14 +7,19 @@ import shlex
7
7
  import signal
8
8
  import uuid
9
9
  from pathlib import PurePosixPath
10
+ from typing import TYPE_CHECKING
10
11
 
11
12
  from pydantic_config import BaseConfig
12
13
 
13
14
  from verifiers.v1.errors import SandboxError
14
15
  from verifiers.v1.runtimes.base import ProgramResult, Runtime, RuntimeProcess
15
- from verifiers.v1.runtimes.subprocess import read_stream
16
+ from verifiers.v1.runtimes.subprocess import SubprocessProcess
16
17
  from verifiers.v1.utils.aio import run_shielded
17
18
 
19
+ if TYPE_CHECKING:
20
+ from verifiers.v1.runtimes.modal import ModalConfig
21
+ from verifiers.v1.runtimes.prime import PrimeConfig
22
+
18
23
 
19
24
  class ContainerConfig(BaseConfig):
20
25
  image: str = "python:3.11-slim"
@@ -37,11 +42,12 @@ class ContainerConfig(BaseConfig):
37
42
 
38
43
 
39
44
  async def _communicate(
40
- *argv: str, input: bytes | None = None
45
+ *argv: str, input: bytes | None = None, env: dict[str, str] | None = None
41
46
  ) -> tuple[int, bytes, bytes]:
42
- """Run a host command to completion; a cancelled await kills it first."""
47
+ """Run a host command; explicit env replaces inheritance, cancellation kills it."""
43
48
  proc = await asyncio.create_subprocess_exec(
44
49
  *argv,
50
+ env=env,
45
51
  stdin=asyncio.subprocess.PIPE
46
52
  if input is not None
47
53
  else asyncio.subprocess.DEVNULL,
@@ -59,8 +65,10 @@ async def _communicate(
59
65
  return proc.returncode or 0, stdout, stderr
60
66
 
61
67
 
62
- async def cli(*argv: str, input: bytes | None = None) -> ProgramResult:
63
- code, stdout, stderr = await _communicate(*argv, input=input)
68
+ async def cli(
69
+ *argv: str, input: bytes | None = None, env: dict[str, str] | None = None
70
+ ) -> ProgramResult:
71
+ code, stdout, stderr = await _communicate(*argv, input=input, env=env)
64
72
  return ProgramResult(
65
73
  code, stdout.decode(errors="replace"), stderr.decode(errors="replace")
66
74
  )
@@ -68,35 +76,38 @@ async def cli(*argv: str, input: bytes | None = None) -> ProgramResult:
68
76
 
69
77
  class ContainerProcess(RuntimeProcess):
70
78
  """A process attached through the CLI client. The client does not forward signals,
71
- so `exec` (the argv prefix that runs a command inside the container) delivers them."""
79
+ so the container runtime delivers them directly to the inner process group."""
72
80
 
73
81
  def __init__(
74
82
  self,
75
- process: asyncio.subprocess.Process,
76
- exec: list[str],
83
+ process: RuntimeProcess,
84
+ runtime: "ContainerRuntime",
77
85
  pid: int,
78
86
  pidfile: str,
79
87
  ) -> None:
80
88
  self._process = process
81
- self._exec = exec
89
+ self._runtime = runtime
82
90
  self._pid = pid
83
91
  self._pidfile = pidfile
84
- assert process.stdin is not None
85
- assert process.stdout is not None
86
- assert process.stderr is not None
87
- self._stdin = process.stdin
88
- self.stdout = read_stream(process.stdout)
89
- self.stderr = read_stream(process.stderr)
92
+ self.stdout, self.stderr = process.stdout, process.stderr
90
93
 
91
94
  async def write(self, data: bytes) -> None:
92
- self._stdin.write(data)
93
- await self._stdin.drain()
95
+ await self._process.write(data)
94
96
 
95
97
  async def wait(self) -> int:
96
98
  try:
97
99
  return await self._process.wait()
98
100
  finally:
99
- await run_shielded(cli(*self._exec, "rm", "-f", self._pidfile))
101
+ # Removing bookkeeping files must not replace the process result.
102
+ with contextlib.suppress(Exception):
103
+ await run_shielded(
104
+ self._runtime._run_host(
105
+ *self._runtime._exec({}), "rm", "-f", self._pidfile
106
+ )
107
+ )
108
+
109
+ async def poll(self) -> int | None:
110
+ return await self._process.poll()
100
111
 
101
112
  async def terminate(self) -> None:
102
113
  await self._signal("TERM")
@@ -105,27 +116,27 @@ class ContainerProcess(RuntimeProcess):
105
116
  await self._signal("KILL")
106
117
 
107
118
  async def _signal(self, signal: str) -> None:
108
- if self._process.returncode is not None:
119
+ if await self.poll() is not None:
109
120
  return
110
- result = await cli(
111
- *self._exec,
121
+ result = await self._runtime._run_host(
122
+ *self._runtime._exec({}),
112
123
  "sh",
113
124
  "-c",
114
- 'kill -"$1" "-$2" 2>/dev/null || kill -"$1" "$2"',
125
+ 'kill -"$1" "-$2" 2>/dev/null || kill -"$1" "$2" 2>/dev/null',
115
126
  "vf-signal",
116
127
  signal,
117
128
  str(self._pid),
118
129
  )
119
- if result.exit_code != 0 and self._process.returncode is None:
130
+ if result.exit_code != 0 and await self.poll() is None:
120
131
  raise SandboxError(
121
132
  f"container process signal failed: {result.stderr.strip()}"
122
133
  )
123
134
 
124
135
 
125
136
  async def _abort_process_startup(
126
- proc: asyncio.subprocess.Process, exec: list[str], pidfile: str
137
+ proc: RuntimeProcess, runtime: "ContainerRuntime", pidfile: str
127
138
  ) -> str:
128
- """Kill a partially opened container process and reap its local CLI client."""
139
+ """Kill a partially opened container process and reap its CLI client."""
129
140
  # The target normally writes its PID immediately, but cancellation can win
130
141
  # that race. Wait briefly for the file before signalling the process group.
131
142
  cleanup = (
@@ -137,23 +148,64 @@ async def _abort_process_startup(
137
148
  )
138
149
  try:
139
150
  with contextlib.suppress(Exception):
151
+ # An exited CLI client may have left its inner process running.
140
152
  await asyncio.wait_for(
141
- cli(*exec, "sh", "-c", cleanup, "vf-process-cleanup", pidfile),
142
- timeout=2,
153
+ runtime._run_host(
154
+ *runtime._exec({}),
155
+ "sh",
156
+ "-c",
157
+ cleanup,
158
+ "vf-process-cleanup",
159
+ pidfile,
160
+ ),
161
+ timeout=5,
143
162
  )
144
163
  finally:
145
- if proc.returncode is None:
146
- with contextlib.suppress(ProcessLookupError):
147
- proc.kill()
148
- _, stderr = await proc.communicate()
164
+ with contextlib.suppress(Exception):
165
+ await asyncio.wait_for(proc.kill(), 5)
166
+ stderr = b""
167
+ with contextlib.suppress(Exception):
168
+ async with asyncio.timeout(5):
169
+ async for _ in proc.stdout:
170
+ pass
171
+ stderr = b"".join([chunk async for chunk in proc.stderr])
172
+ await proc.wait()
149
173
  return stderr.decode(errors="replace").strip()
150
174
 
151
175
 
152
176
  class ContainerRuntime(Runtime):
153
- """A local container reached through its CLI: every operation is an `exec` into it.
177
+ """A container reached through its CLI: every operation is an `exec` into it.
154
178
  Subclasses provision the container (`start` / `cleanup`) and describe the exec."""
155
179
 
156
- config: ContainerConfig
180
+ config: "ContainerConfig | PrimeConfig | ModalConfig"
181
+ _host: Runtime | None = None
182
+
183
+ async def _run_host(self, *argv: str) -> ProgramResult:
184
+ if self._host is None:
185
+ return await cli(*argv)
186
+ return await self._host.run(list(argv), {})
187
+
188
+ async def _communicate_host(
189
+ self, *argv: str, input: bytes | None = None
190
+ ) -> tuple[int, bytes, bytes]:
191
+ if self._host is None:
192
+ return await _communicate(*argv, input=input)
193
+ # Stage bytes through the provider filesystem, never its text command logs.
194
+ temporary = f"/tmp/vf-io-{uuid.uuid4().hex}"
195
+ command = f"{shlex.join(argv)} > {temporary}.out"
196
+ try:
197
+ if input is not None:
198
+ await self._host.write(f"{temporary}.in", input)
199
+ command += f" < {temporary}.in"
200
+ result = await self._run_host("sh", "-c", command)
201
+ data = await self._host.read(f"{temporary}.out")
202
+ return result.exit_code, data, result.stderr.encode()
203
+ finally:
204
+ # Temporary-file cleanup is best-effort after command completion/failure.
205
+ with contextlib.suppress(Exception):
206
+ await run_shielded(
207
+ self._run_host("rm", "-f", f"{temporary}.in", f"{temporary}.out")
208
+ )
157
209
 
158
210
  def _exec(self, env: dict[str, str], *, stdin: bool = False) -> list[str]:
159
211
  """Host argv that runs a command inside the container, in the workdir, with
@@ -161,7 +213,7 @@ class ContainerRuntime(Runtime):
161
213
  raise NotImplementedError
162
214
 
163
215
  async def run(self, argv: list[str], env: dict[str, str]) -> ProgramResult:
164
- return await cli(*self._exec(self.process_env(env)), *argv)
216
+ return await self._run_host(*self._exec(self.process_env(env)), *argv)
165
217
 
166
218
  async def open_process(
167
219
  self, argv: list[str], env: dict[str, str]
@@ -177,8 +229,7 @@ class ContainerRuntime(Runtime):
177
229
  'vf-process "$@"; '
178
230
  'fi; echo $$ > "$1"; shift; exec "$@"'
179
231
  )
180
- control = self._exec({})
181
- proc = await asyncio.create_subprocess_exec(
232
+ command = [
182
233
  *self._exec(self.process_env(env), stdin=True),
183
234
  "sh",
184
235
  "-c",
@@ -186,33 +237,47 @@ class ContainerRuntime(Runtime):
186
237
  "vf-process",
187
238
  pidfile,
188
239
  *argv,
189
- stdin=asyncio.subprocess.PIPE,
190
- stdout=asyncio.subprocess.PIPE,
191
- stderr=asyncio.subprocess.PIPE,
192
- )
240
+ ]
241
+ if self._host is None:
242
+ proc = SubprocessProcess(
243
+ await asyncio.create_subprocess_exec(
244
+ *command,
245
+ stdin=asyncio.subprocess.PIPE,
246
+ stdout=asyncio.subprocess.PIPE,
247
+ stderr=asyncio.subprocess.PIPE,
248
+ start_new_session=True,
249
+ )
250
+ )
251
+ else:
252
+ proc = await self._host.open_process(command, {})
193
253
  loop = asyncio.get_running_loop()
194
- deadline = loop.time() + 5
195
- exited = False
254
+ deadline = loop.time() + (30 if self._host is not None else 5)
196
255
  try:
197
- while True:
198
- ready = await cli(*control, "cat", pidfile)
199
- if ready.exit_code == 0 and ready.stdout.strip().isdigit():
200
- return ContainerProcess(
201
- proc, control, int(ready.stdout.strip()), pidfile
202
- )
203
- # A target that already exited still left its pidfile: poll once more.
204
- if exited or loop.time() >= deadline:
205
- break
206
- exited = proc.returncode is not None
207
- await asyncio.sleep(0.05)
256
+ async with asyncio.timeout_at(deadline):
257
+ while True:
258
+ # An exited client gets one final PID read: short commands may
259
+ # have written their PID and finished before the first poll.
260
+ returncode = await proc.poll()
261
+ ready = await self._run_host(*self._exec({}), "cat", pidfile)
262
+ if ready.exit_code == 0 and ready.stdout.strip().isdigit():
263
+ return ContainerProcess(
264
+ proc, self, int(ready.stdout.strip()), pidfile
265
+ )
266
+ if returncode is not None:
267
+ break
268
+ await asyncio.sleep(0.05)
269
+ except TimeoutError as error:
270
+ stderr = await run_shielded(_abort_process_startup(proc, self, pidfile))
271
+ raise SandboxError(
272
+ f"container live process failed to start: {stderr or 'PID unavailable'}"
273
+ ) from error
208
274
  except BaseException:
209
- await run_shielded(_abort_process_startup(proc, control, pidfile))
275
+ await run_shielded(_abort_process_startup(proc, self, pidfile))
210
276
  raise
211
-
212
- stderr = await run_shielded(_abort_process_startup(proc, control, pidfile))
213
- detail = stderr or ready.stderr.strip()
277
+ stderr = await run_shielded(_abort_process_startup(proc, self, pidfile))
214
278
  raise SandboxError(
215
- f"container live process failed to start: {detail or 'PID unavailable'}"
279
+ f"container client exited with code {returncode} before reporting its PID: "
280
+ f"{stderr or ready.stderr.strip()}"
216
281
  )
217
282
 
218
283
  async def run_background(
@@ -221,7 +286,9 @@ class ContainerRuntime(Runtime):
221
286
  # Backgrounded inside the container, so it outlives this exec and lives until
222
287
  # the container is removed in stop().
223
288
  script = f"{shlex.join(argv)} > {shlex.quote(log)} 2>&1 < /dev/null &"
224
- result = await cli(*self._exec(self.process_env(env)), "sh", "-c", script)
289
+ result = await self._run_host(
290
+ *self._exec(self.process_env(env)), "sh", "-c", script
291
+ )
225
292
  if result.exit_code != 0:
226
293
  raise SandboxError(
227
294
  f"container background process failed: {result.stderr.strip()}"
@@ -229,7 +296,9 @@ class ContainerRuntime(Runtime):
229
296
 
230
297
  async def _read(self, path: str, max_bytes: int | None = None) -> bytes:
231
298
  argv = ["cat"] if max_bytes is None else ["head", "-c", str(max_bytes)]
232
- code, data, stderr = await _communicate(*self._exec({}), *argv, "--", path)
299
+ code, data, stderr = await self._communicate_host(
300
+ *self._exec({}), *argv, "--", path
301
+ )
233
302
  if code != 0:
234
303
  raise SandboxError(
235
304
  f"read {path!r}: {stderr.decode(errors='replace').strip()}"
@@ -238,12 +307,14 @@ class ContainerRuntime(Runtime):
238
307
 
239
308
  async def write(self, path: str, data: bytes) -> None:
240
309
  parent = shlex.quote(str(PurePosixPath(path).parent))
241
- result = await cli(
310
+ code, _, stderr = await self._communicate_host(
242
311
  *self._exec({}, stdin=True),
243
312
  "sh",
244
313
  "-c",
245
314
  f"mkdir -p {parent} && cat > {shlex.quote(path)}",
246
315
  input=data,
247
316
  )
248
- if result.exit_code != 0:
249
- raise SandboxError(f"write {path!r}: {result.stderr.strip()}")
317
+ if code != 0:
318
+ raise SandboxError(
319
+ f"write {path!r}: {stderr.decode(errors='replace').strip()}"
320
+ )
@@ -12,7 +12,8 @@ import subprocess
12
12
  import sys
13
13
  import tempfile
14
14
  import uuid
15
- from typing import ClassVar, Literal
15
+ from collections.abc import AsyncIterator
16
+ from typing import TYPE_CHECKING, ClassVar, Literal
16
17
  from urllib.parse import urlsplit
17
18
 
18
19
  from verifiers.v1.configs.runtime import NetworkPolicyConfig
@@ -20,6 +21,7 @@ from verifiers.v1.errors import SandboxError
20
21
  from verifiers.v1.runtimes.base import (
21
22
  SERVICE_PORT,
22
23
  BaseRuntimeInfo,
24
+ Runtime,
23
25
  parse_gpu,
24
26
  )
25
27
  from verifiers.v1.runtimes.container import ContainerConfig, ContainerRuntime, cli
@@ -32,6 +34,10 @@ from verifiers.v1.utils.scope import run_scope
32
34
 
33
35
  logger = logging.getLogger(__name__)
34
36
 
37
+ if TYPE_CHECKING:
38
+ from verifiers.v1.runtimes.modal import ModalConfig
39
+ from verifiers.v1.runtimes.prime import PrimeConfig
40
+
35
41
 
36
42
  class DockerConfig(ContainerConfig, NetworkPolicyConfig):
37
43
  type: Literal["docker"] = "docker"
@@ -69,17 +75,77 @@ class DockerRuntime(ContainerRuntime):
69
75
  info_cls: ClassVar[type[BaseRuntimeInfo]] = DockerRuntimeInfo
70
76
 
71
77
  def __init__(
72
- self, config: DockerConfig | PodmanConfig, name: str | None = None
78
+ self,
79
+ config: "DockerConfig | PodmanConfig | PrimeConfig | ModalConfig",
80
+ name: str | None = None,
81
+ *,
82
+ host: Runtime | None = None,
73
83
  ) -> None:
74
84
  super().__init__(name)
75
85
  self.config = config.model_copy(update={"workdir": config.workdir or "/app"})
76
- self.info = self.info_cls(**self.config.model_dump())
86
+ self._host = host
87
+ self.is_local = host is None or host.is_local
88
+ self.info = (
89
+ host.info.model_copy(
90
+ update={"image": self.config.image, "workdir": self.config.workdir}
91
+ )
92
+ if host
93
+ else self.info_cls(**self.config.model_dump())
94
+ )
77
95
  self._container: str | None = None # our `--name` (used for exec/rm)
78
96
  self._service_url: str | None = None
79
97
  self._proxy: EgressProxy | None = None
80
98
  self._image_env: dict[str, str] = {}
81
99
  self._stopped = False
82
100
  self._cut = False
101
+ # Helper containers carry the run label, including for borrowed containers.
102
+ self._label_args = ["--label", f"verifiers.run={run_scope()}"]
103
+
104
+ @classmethod
105
+ @contextlib.asynccontextmanager
106
+ async def attach(
107
+ cls,
108
+ config: "DockerConfig | PrimeConfig | ModalConfig",
109
+ container: str,
110
+ *,
111
+ service_url: str | None = None,
112
+ host: Runtime | None = None,
113
+ ) -> AsyncIterator["DockerRuntime"]:
114
+ """Borrow an existing container; its caller owns creation and removal."""
115
+ runtime = cls(config, host=host)
116
+ runtime._container = container
117
+ runtime.info.borrowed = True # cleanup never removes a borrowed container
118
+ try:
119
+ code, data, stderr = await runtime._communicate_host(
120
+ runtime.engine, "inspect", "--format", "{{json .Config}}", container
121
+ )
122
+ if code:
123
+ raise SandboxError(
124
+ f"Container inspection failed: {stderr.decode(errors='replace')}"
125
+ )
126
+ info = json.loads(data)
127
+ runtime._image_env = dict(
128
+ entry.split("=", 1) for entry in info["Env"] or []
129
+ )
130
+ runtime.config = config.model_copy(
131
+ update={"image": info["Image"], "workdir": info["WorkingDir"] or "/"}
132
+ )
133
+ if host is None:
134
+ runtime.info.id = container
135
+ runtime.info.image = runtime.config.image
136
+ runtime.info.workdir = runtime.config.workdir
137
+ runtime._service_url = service_url
138
+ if host is None and service_url is not None:
139
+ runtime._proxy = EgressProxy(NetworkPolicy(NetworkPolicyConfig(), []))
140
+ if sys.platform == "linux":
141
+ await runtime._proxy.start(
142
+ listener=await runtime._container_listener()
143
+ )
144
+ else:
145
+ await runtime._proxy.start("127.0.0.1")
146
+ yield runtime
147
+ finally:
148
+ await runtime.stop()
83
149
 
84
150
  @property
85
151
  def published_port(self) -> int:
@@ -162,7 +228,6 @@ class DockerRuntime(ContainerRuntime):
162
228
  for key, value in self.env.items()
163
229
  for arg in ("--env", f"{key}={value}")
164
230
  ]
165
- self._label_args = ["--label", f"verifiers.run={run_scope()}"]
166
231
  run = await cli(
167
232
  self.engine,
168
233
  "run",
@@ -262,6 +327,8 @@ class DockerRuntime(ContainerRuntime):
262
327
  )
263
328
 
264
329
  def host_url(self, url: str) -> str:
330
+ if self._host is not None:
331
+ return self._host.host_url(url)
265
332
  parts = urlsplit(url)
266
333
  if not is_loopback_host(parts.hostname or ""):
267
334
  return url
@@ -270,6 +337,8 @@ class DockerRuntime(ContainerRuntime):
270
337
  return self._proxy.callback_url(url, host)
271
338
 
272
339
  async def expose(self, port: int) -> str:
340
+ if self._host is not None:
341
+ return await self._host.expose(port)
273
342
  if port != SERVICE_PORT or self._service_url is None:
274
343
  raise SandboxError(
275
344
  f"{self.engine} publishes only port {SERVICE_PORT}, not {port}"
@@ -340,6 +409,9 @@ class DockerRuntime(ContainerRuntime):
340
409
 
341
410
  async def prepare_execution(self, routes: list[str] | None) -> None:
342
411
  """Allow the declared framework routes, then leave the proxy as the only way out."""
412
+ if self._host is not None:
413
+ await self._host.prepare_execution(routes)
414
+ return
343
415
  if not self.network_restricted:
344
416
  return
345
417
  assert self._proxy is not None
@@ -352,6 +424,7 @@ class DockerRuntime(ContainerRuntime):
352
424
  urlsplit(url)._replace(path="", query="", fragment="").geturl()
353
425
  for url in routes
354
426
  ]
427
+ assert isinstance(self.config, NetworkPolicyConfig)
355
428
  self._proxy.policy = NetworkPolicy(self.config, framework)
356
429
  if self._cut:
357
430
  return
@@ -465,20 +538,20 @@ class DockerRuntime(ContainerRuntime):
465
538
  self, argv: list[str], env: dict[str, str], log: str
466
539
  ) -> None:
467
540
  # A setup server outlives the network cut and needs the initially open proxy.
468
- if self.network_restricted:
541
+ if self.network_restricted and self._proxy is not None:
469
542
  env = {**env, **self._proxy_env()}
470
543
  # The engine owns the background server as a detached exec process.
471
544
  command = self._exec(self.process_env(env))
472
545
  command.insert(2, "--detach")
473
546
  script = f"exec {shlex.join(argv)} > {shlex.quote(log)} 2>&1 < /dev/null"
474
- result = await cli(*command, "sh", "-c", script)
547
+ result = await self._run_host(*command, "sh", "-c", script)
475
548
  if result.exit_code != 0:
476
549
  raise SandboxError(
477
550
  f"{self.engine} background process failed: {result.stderr.strip()}"
478
551
  )
479
552
 
480
553
  def cleanup(self) -> None:
481
- if self._container is None or self._stopped:
554
+ if self.info.borrowed or self._container is None or self._stopped:
482
555
  return
483
556
  self._stopped = (
484
557
  True # idempotency guard; keep `_container` so the name still shows
@@ -75,6 +75,8 @@ def _egress_domain(rule: str, *, framework: bool = False) -> str | None:
75
75
 
76
76
  class ModalConfig(NetworkPolicyConfig):
77
77
  type: Literal["modal"] = "modal"
78
+ vm: bool = False
79
+ """Use a VM sandbox for workloads requiring a Docker daemon."""
78
80
  image: str = "python:3.11-slim"
79
81
  workdir: str | None = None
80
82
  """Working directory override; None uses the task's workdir, or /app."""
@@ -113,6 +115,12 @@ class ModalConfig(NetworkPolicyConfig):
113
115
  _egress_domain(rule)
114
116
  return self
115
117
 
118
+ @model_validator(mode="after")
119
+ def _validate_vm(self) -> "ModalConfig":
120
+ if self.vm and self.gpu:
121
+ raise ValueError("Modal VM sandboxes support CPU workloads only")
122
+ return self
123
+
116
124
 
117
125
  class ModalRuntimeInfo(ModalConfig, BaseRuntimeInfo):
118
126
  pass
@@ -134,6 +142,9 @@ class ModalProcess(RuntimeProcess):
134
142
  async def wait(self) -> int:
135
143
  return await self._process.wait.aio()
136
144
 
145
+ async def poll(self) -> int | None:
146
+ return await self._process.poll.aio()
147
+
137
148
  async def terminate(self) -> None:
138
149
  await self._signal("TERM")
139
150
 
@@ -233,6 +244,7 @@ class ModalRuntime(Runtime):
233
244
  ),
234
245
  timeout=24 * 60 * 60, # Maximum lifetime of any sandbox.
235
246
  encrypted_ports=[SERVICE_PORT],
247
+ experimental_options={"vm_runtime": True} if self.config.vm else {},
236
248
  )
237
249
 
238
250
  async def prepare_execution(self, routes: list[str] | None) -> None:
@@ -133,6 +133,9 @@ class PrimeProcess(RuntimeProcess):
133
133
  async def wait(self) -> int:
134
134
  return await self._process.wait()
135
135
 
136
+ async def poll(self) -> int | None:
137
+ return self._process.returncode
138
+
136
139
  async def terminate(self) -> None:
137
140
  await self._process.terminate()
138
141
 
@@ -70,6 +70,9 @@ class SubprocessProcess(RuntimeProcess):
70
70
  async def wait(self) -> int:
71
71
  return await self._process.wait()
72
72
 
73
+ async def poll(self) -> int | None:
74
+ return self._process.returncode
75
+
73
76
  async def terminate(self) -> None:
74
77
  signal_process(self._process, signal.SIGTERM)
75
78