featurebench 0.2.2__tar.gz → 0.2.3__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 (136) hide show
  1. {featurebench-0.2.2 → featurebench-0.2.3}/PKG-INFO +3 -3
  2. {featurebench-0.2.2 → featurebench-0.2.3}/README.md +2 -2
  3. {featurebench-0.2.2 → featurebench-0.2.3}/config_example.toml +2 -1
  4. {featurebench-0.2.2 → featurebench-0.2.3}/docs/config.md +3 -2
  5. {featurebench-0.2.2 → featurebench-0.2.3}/docs/infer_cli_arg.md +7 -0
  6. featurebench-0.2.3/featurebench/environment_fixes/__init__.py +13 -0
  7. featurebench-0.2.3/featurebench/environment_fixes/base.py +24 -0
  8. featurebench-0.2.3/featurebench/environment_fixes/mlflow.py +41 -0
  9. featurebench-0.2.3/featurebench/environment_fixes/registry.py +43 -0
  10. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/harness/runtime.py +26 -12
  11. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/infer/agents/base.py +14 -2
  12. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/infer/agents/mini_swe_agent.py +104 -22
  13. featurebench-0.2.3/featurebench/infer/environments/__init__.py +1 -0
  14. featurebench-0.2.3/featurebench/infer/environments/mini_swe.py +185 -0
  15. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/infer/models.py +8 -1
  16. featurebench-0.2.3/featurebench/infer/network.py +473 -0
  17. featurebench-0.2.3/featurebench/infer/proxy_bridge.py +57 -0
  18. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/infer/run_infer.py +141 -6
  19. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/infer/runtime.py +138 -5
  20. {featurebench-0.2.2 → featurebench-0.2.3}/pyproject.toml +1 -1
  21. {featurebench-0.2.2 → featurebench-0.2.3}/tests/test_dataset_version.py +3 -0
  22. featurebench-0.2.3/tests/test_environment_fixes.py +52 -0
  23. featurebench-0.2.3/tests/test_infer_network.py +150 -0
  24. featurebench-0.2.3/tests/test_infer_runtime.py +163 -0
  25. featurebench-0.2.3/tests/test_mini_swe_agent.py +213 -0
  26. {featurebench-0.2.2 → featurebench-0.2.3}/uv.lock +43 -43
  27. {featurebench-0.2.2 → featurebench-0.2.3}/.gitignore +0 -0
  28. {featurebench-0.2.2 → featurebench-0.2.3}/.python-version +0 -0
  29. {featurebench-0.2.2 → featurebench-0.2.3}/LICENSE +0 -0
  30. {featurebench-0.2.2 → featurebench-0.2.3}/docs/harness_cli_arg.md +0 -0
  31. {featurebench-0.2.2 → featurebench-0.2.3}/docs/pics/logo.png +0 -0
  32. {featurebench-0.2.2 → featurebench-0.2.3}/docs/pics/workflow.png +0 -0
  33. {featurebench-0.2.2 → featurebench-0.2.3}/docs/pipeline.md +0 -0
  34. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/__init__.py +0 -0
  35. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/classification/__init__.py +0 -0
  36. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/classification/code_classifier.py +0 -0
  37. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/classification/file_analyzer.py +0 -0
  38. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/classification/llm_top_classifier.py +0 -0
  39. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/classification/p2p_chooser.py +0 -0
  40. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/cli.py +0 -0
  41. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/conversion/__init__.py +0 -0
  42. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/conversion/case_converter.py +0 -0
  43. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/conversion/eval_code.py +0 -0
  44. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/conversion/llm_prompt_generator.py +0 -0
  45. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/conversion/llm_testfile_adjuster.py +0 -0
  46. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/conversion/render_task.py +0 -0
  47. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/docker/__init__.py +0 -0
  48. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/docker/dynamic_tracer.py +0 -0
  49. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/docker/image_manager.py +0 -0
  50. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/docker/test_runner.py +0 -0
  51. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/docker/test_scanner.py +0 -0
  52. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/harness/__init__.py +0 -0
  53. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/harness/constants.py +0 -0
  54. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/harness/container.py +0 -0
  55. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/harness/grading.py +0 -0
  56. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/harness/report.py +0 -0
  57. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/harness/review_codes.py +0 -0
  58. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/harness/run_evaluation.py +0 -0
  59. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/harness/test_parsers.py +0 -0
  60. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/harness/utils.py +0 -0
  61. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/infer/__init__.py +0 -0
  62. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/infer/agents/__init__.py +0 -0
  63. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/infer/agents/claude_code.py +0 -0
  64. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/infer/agents/codex.py +0 -0
  65. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/infer/agents/gemini_cli.py +0 -0
  66. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/infer/agents/openhands.py +0 -0
  67. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/infer/config.py +0 -0
  68. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/infer/container.py +0 -0
  69. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/infer/gpu_scheduler.py +0 -0
  70. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/infer/output.py +0 -0
  71. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/infer/render_infer_log.py +0 -0
  72. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/llm/__init__.py +0 -0
  73. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/llm/llm_caller.py +0 -0
  74. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/llm/llm_exceptions.py +0 -0
  75. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/mask/__init__.py +0 -0
  76. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/mask/mask_generator.py +0 -0
  77. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/mask/mask_validator.py +0 -0
  78. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/mask/signature_extractor.py +0 -0
  79. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/pipeline.py +0 -0
  80. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/post_verify/__init__.py +0 -0
  81. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/post_verify/f2p_validator.py +0 -0
  82. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/post_verify/level2_validator.py +0 -0
  83. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/post_verify/p2p_validator.py +0 -0
  84. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/resources/constants/fast_images.txt +0 -0
  85. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/resources/constants/full_images.txt +0 -0
  86. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/resources/constants/lite_images.txt +0 -0
  87. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/resources/constants/python_example.py +0 -0
  88. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/resources/dockerfiles/python310.py +0 -0
  89. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/resources/dockerfiles/python310_cu118_torch212.py +0 -0
  90. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/resources/dockerfiles/python310_cu121_torch251.py +0 -0
  91. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/resources/dockerfiles/python310_cu121_torch28.py +0 -0
  92. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/resources/dockerfiles/python311.py +0 -0
  93. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/resources/dockerfiles/python311_cu121_torch28.py +0 -0
  94. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/resources/dockerfiles/python312.py +0 -0
  95. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/resources/dockerfiles/python312_cu121_torch251.py +0 -0
  96. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/resources/dockerfiles/python312_cu121_torch26.py +0 -0
  97. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/resources/dockerfiles/python312_cu121_torch28.py +0 -0
  98. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/resources/dockerfiles/python312_torch251.py +0 -0
  99. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/resources/dockerfiles/python39.py +0 -0
  100. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/resources/scripts/dynamic_script/autograd_backward_patch.py +0 -0
  101. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/resources/scripts/dynamic_script/dynamic_script.py +0 -0
  102. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/resources/scripts/dynamic_script/import_hook_patch.py +0 -0
  103. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/resources/scripts/dynamic_script/joblib_patch.py +0 -0
  104. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/resources/scripts/dynamic_script/sitecustomize.py +0 -0
  105. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/resources/scripts/scanner_script.py +0 -0
  106. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/resources/templates/appendix.md.j2 +0 -0
  107. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/resources/templates/black_links.txt.j2 +0 -0
  108. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/resources/templates/config_template.yaml +0 -0
  109. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/resources/templates/level_1.j2 +0 -0
  110. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/resources/templates/level_2.j2 +0 -0
  111. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/resources/templates/llm_prompt_templates.yaml +0 -0
  112. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/scripts/cal_eval_outputs.py +0 -0
  113. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/scripts/post_filter_cases.py +0 -0
  114. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/scripts/pull_images.py +0 -0
  115. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/utils/__init__.py +0 -0
  116. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/utils/command_utils.py +0 -0
  117. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/utils/config.py +0 -0
  118. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/utils/docker_images.py +0 -0
  119. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/utils/logger.py +0 -0
  120. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/utils/parser/__init__.py +0 -0
  121. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/utils/parser/pytest_parser.py +0 -0
  122. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/utils/repo_manager.py +0 -0
  123. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/utils/storage.py +0 -0
  124. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/utils/utils.py +0 -0
  125. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/verified/__init__.py +0 -0
  126. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/verified/post_additions.py +0 -0
  127. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/verified/prompts/add_prompt_lv1.txt +0 -0
  128. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/verified/prompts/add_prompt_lv2.txt +0 -0
  129. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/verified/prompts/prompt_template_lv1.txt +0 -0
  130. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/verified/prompts/prompt_template_lv2.txt +0 -0
  131. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/verified/run_verification.py +0 -0
  132. {featurebench-0.2.2 → featurebench-0.2.3}/featurebench/verified/runner.py +0 -0
  133. {featurebench-0.2.2 → featurebench-0.2.3}/tests/test_eval_data_container_cleanup.py +0 -0
  134. {featurebench-0.2.2 → featurebench-0.2.3}/tests/test_infer_container_cleanup.py +0 -0
  135. {featurebench-0.2.2 → featurebench-0.2.3}/tests/test_openhands_session_cache.py +0 -0
  136. {featurebench-0.2.2 → featurebench-0.2.3}/tests/test_pull_images.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: featurebench
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: FeatureBench Pipeline - A test-driven data generation pipeline for building and evaluating feature-level coding benchmarks
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -52,7 +52,7 @@ It provides a unified CLI to run inference, evaluation, and dataset generation.
52
52
  <!-- LEADERBOARD_START -->
53
53
  ## 🏆 Leaderboard
54
54
 
55
- [Full interactive leaderboard](https://libercoders.github.io/FeatureBench/leaderboard/) with tabs, filters, and sorting.
55
+ [Full interactive leaderboard](https://libercoders.github.io/FeatureBench/leaderboard/) with tabs, filters, and sorting (Dataset V1.0).
56
56
 
57
57
  <details open>
58
58
  <summary><b>Lite split results, ranked by %PASSED</b></summary>
@@ -159,4 +159,4 @@ If you found FeatureBench useful, please cite us as:
159
159
 
160
160
  ## 📧 Contact
161
161
 
162
- If you have any questions, feel free to contact [zhouqixing@stu.cqu.edu.cn](mailto:zhouqixing@stu.cqu.edu.cn) or [zjcheng2022@gmail.com](mailto:zjcheng2022@gmail.com).
162
+ If you have any questions, feel free to contact [qixing.zhou@smail.nju.edu.cn](mailto:qixing.zhou@smail.nju.edu.cn) or [zjcheng2022@gmail.com](mailto:zjcheng2022@gmail.com).
@@ -28,7 +28,7 @@ It provides a unified CLI to run inference, evaluation, and dataset generation.
28
28
  <!-- LEADERBOARD_START -->
29
29
  ## 🏆 Leaderboard
30
30
 
31
- [Full interactive leaderboard](https://libercoders.github.io/FeatureBench/leaderboard/) with tabs, filters, and sorting.
31
+ [Full interactive leaderboard](https://libercoders.github.io/FeatureBench/leaderboard/) with tabs, filters, and sorting (Dataset V1.0).
32
32
 
33
33
  <details open>
34
34
  <summary><b>Lite split results, ranked by %PASSED</b></summary>
@@ -135,4 +135,4 @@ If you found FeatureBench useful, please cite us as:
135
135
 
136
136
  ## 📧 Contact
137
137
 
138
- If you have any questions, feel free to contact [zhouqixing@stu.cqu.edu.cn](mailto:zhouqixing@stu.cqu.edu.cn) or [zjcheng2022@gmail.com](mailto:zjcheng2022@gmail.com).
138
+ If you have any questions, feel free to contact [qixing.zhou@smail.nju.edu.cn](mailto:qixing.zhou@smail.nju.edu.cn) or [zjcheng2022@gmail.com](mailto:zjcheng2022@gmail.com).
@@ -52,7 +52,8 @@ base_url = "http://localhost:8080/v1" # vLLM server URL
52
52
  # Optional: Host cache directory for agent downloads; will be mounted to /download inside containers
53
53
  # Set to empty to disable cache mount (each container will re-download/install)
54
54
  # Use an absolute path here
55
- download_cache_dir="/path/to/FeatureBench/download_cache"
55
+ # Optional shared install cache. Leave unset for leakage-resistant evaluations.
56
+ # download_cache_dir="/path/to/FeatureBench/download_cache"
56
57
 
57
58
  # OpenHands Agent Configuration
58
59
  [infer_config.openhands]
@@ -68,10 +68,11 @@ How infer reads `config.toml`:
68
68
 
69
69
  ```toml
70
70
  [infer]
71
- download_cache_dir = "/abs/path/to/FeatureBench/download_cache"
71
+ # Optional shared install cache. Leave unset for leakage-resistant evaluations.
72
+ # download_cache_dir = "/abs/path/to/FeatureBench/download_cache"
72
73
  ```
73
74
 
74
- - `download_cache_dir`: host-side cache directory that will be mounted into the container at `/download`.
75
+ - `download_cache_dir`: optional host-side cache directory mounted into the container at `/download`. It is disabled by default because the agent can read shared cache contents during inference.
75
76
  - Purpose: speed up agent installs/downloads (e.g. npm cache).
76
77
  - Empty means no mount (each container downloads on its own; usually slower).
77
78
 
@@ -160,6 +160,13 @@ flags can override metadata (see the argument list below).
160
160
  Default: no override (OpenHands default applies).
161
161
  Resume mode: ignored (uses metadata).
162
162
 
163
+ ### mini-swe-agent Only
164
+
165
+ - `--cost-limit`
166
+ Per-task cost limit in USD, passed to mini-swe-agent.
167
+ Set to `0` to disable the limit. If omitted, mini-swe-agent's upstream default applies.
168
+ Resume mode: ignored (uses metadata).
169
+
163
170
  ## Section 4: Output Directory Structure
164
171
 
165
172
  ```
@@ -0,0 +1,13 @@
1
+ """Repository-specific fixes for leaked or duplicated task-environment artifacts.
2
+
3
+ Add a fix by implementing :class:`EnvironmentFix`, decorating it with
4
+ ``register_environment_fix``, and importing its module below. Each plugin is
5
+ matched by repository and is shared by inference and Oracle evaluation.
6
+ """
7
+
8
+ from featurebench.environment_fixes.registry import apply_environment_fixes
9
+
10
+ # Import built-in plugins so they register themselves.
11
+ from featurebench.environment_fixes import mlflow as _mlflow # noqa: F401, E402
12
+
13
+ __all__ = ["apply_environment_fixes"]
@@ -0,0 +1,24 @@
1
+ """Interfaces for repository-specific task-environment fixes."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from collections.abc import Callable
5
+ import logging
6
+
7
+
8
+ CommandRunner = Callable[[str], tuple[int, str | bytes]]
9
+
10
+
11
+ class EnvironmentFix(ABC):
12
+ """A targeted cleanup applied before the task repository becomes the Git baseline."""
13
+
14
+ name: str
15
+ repositories: frozenset[str]
16
+
17
+ def applies_to(self, repository: str) -> bool:
18
+ """Return whether this fix applies to a canonical ``owner/repo`` name."""
19
+ normalized = repository.strip().casefold()
20
+ return normalized in {repo.casefold() for repo in self.repositories}
21
+
22
+ @abstractmethod
23
+ def apply(self, run_command: CommandRunner, logger: logging.Logger) -> bool:
24
+ """Apply and verify the fix, returning ``False`` on failure."""
@@ -0,0 +1,41 @@
1
+ """MLflow-specific task-environment cleanup."""
2
+
3
+ import logging
4
+
5
+ from featurebench.environment_fixes.base import CommandRunner, EnvironmentFix
6
+ from featurebench.environment_fixes.registry import register_environment_fix
7
+
8
+
9
+ @register_environment_fix
10
+ class MlflowParallelSourcesFix(EnvironmentFix):
11
+ """Remove unmasked MLflow package copies shipped in the monorepo's ``libs`` tree."""
12
+
13
+ name = "mlflow_parallel_sources"
14
+ repositories = frozenset({"mlflow/mlflow"})
15
+ paths = (
16
+ "/testbed/libs/skinny/mlflow",
17
+ "/testbed/libs/tracing/mlflow",
18
+ )
19
+
20
+ def apply(self, run_command: CommandRunner, logger: logging.Logger) -> bool:
21
+ paths = " ".join(self.paths)
22
+ command = f"""
23
+ paths=({paths})
24
+ for path in "${{paths[@]}}"; do
25
+ if [ -e "$path" ] || [ -L "$path" ]; then
26
+ rm -rf -- "$path" || exit 1
27
+ echo "Removed MLflow parallel source: $path"
28
+ fi
29
+ if [ -e "$path" ] || [ -L "$path" ]; then
30
+ echo "MLflow parallel source still exists: $path" >&2
31
+ exit 1
32
+ fi
33
+ done
34
+ """
35
+ exit_code, output = run_command(command)
36
+ if exit_code != 0:
37
+ if isinstance(output, bytes):
38
+ output = output.decode("utf-8", errors="replace")
39
+ logger.error("Failed to remove MLflow parallel sources: %s", output)
40
+ return False
41
+ return True
@@ -0,0 +1,43 @@
1
+ """Registry and execution helpers for task-environment fixes."""
2
+
3
+ import logging
4
+ from typing import TypeVar
5
+
6
+ from featurebench.environment_fixes.base import CommandRunner, EnvironmentFix
7
+
8
+
9
+ _FIXES: list[EnvironmentFix] = []
10
+ _EnvironmentFixType = TypeVar("_EnvironmentFixType", bound=type[EnvironmentFix])
11
+
12
+
13
+ def register_environment_fix(fix_type: _EnvironmentFixType) -> _EnvironmentFixType:
14
+ """Register an environment-fix class at import time."""
15
+ fix = fix_type()
16
+ if any(existing.name == fix.name for existing in _FIXES):
17
+ raise ValueError(f"Duplicate environment fix name: {fix.name}")
18
+ _FIXES.append(fix)
19
+ return fix_type
20
+
21
+
22
+ def apply_environment_fixes(
23
+ repository: str,
24
+ run_command: CommandRunner,
25
+ logger: logging.Logger,
26
+ ) -> bool:
27
+ """Apply every registered fix matching ``repository`` in registration order."""
28
+ matching_fixes = [fix for fix in _FIXES if fix.applies_to(repository)]
29
+ if not matching_fixes:
30
+ return True
31
+
32
+ for fix in matching_fixes:
33
+ logger.info("Applying environment fix %s for %s", fix.name, repository)
34
+ try:
35
+ if not fix.apply(run_command, logger):
36
+ logger.error("Environment fix %s failed for %s", fix.name, repository)
37
+ return False
38
+ except Exception:
39
+ logger.exception("Environment fix %s raised for %s", fix.name, repository)
40
+ return False
41
+ logger.info("Environment fix %s completed for %s", fix.name, repository)
42
+
43
+ return True
@@ -13,6 +13,7 @@ from typing import Any
13
13
  import pandas as pd
14
14
  from docker.models.containers import Container
15
15
 
16
+ from featurebench.environment_fixes import apply_environment_fixes
16
17
  from featurebench.harness.constants import (
17
18
  APPLY_PATCH_FAIL,
18
19
  APPLY_PATCH_PASS,
@@ -55,9 +56,11 @@ def run_instance_level1(
55
56
  1. Activate conda environment
56
57
  2. Restore project from /root/my_repo/
57
58
  3. Apply patch (for masking) and directly delete F2P test files
58
- 4. Reinitialize git
59
- 5. Apply agent's patch
60
- 6. Delete any generated F2P files, restore F2P test, and run tests
59
+ 4. Apply repository-specific environment fixes
60
+ 5. Reinitialize git
61
+ 6. Apply agent's patch
62
+ 7. Delete any generated F2P files and restore the F2P test
63
+ 8. Run tests
61
64
 
62
65
  Args:
63
66
  instance: Instance data (from HuggingFace)
@@ -177,8 +180,20 @@ def run_instance_level1(
177
180
  else:
178
181
  logger.warning("No FAIL_TO_PASS tests found in instance, skipping test file deletion")
179
182
 
180
- # Step 3: Reinitialize git repository
181
- logger.info("Step 3: Reinitializing git repository")
183
+ # Step 3: Apply the same repository-specific cleanup used during
184
+ # inference. Running it before Git initialization keeps the cleanup out
185
+ # of the candidate patch while making Oracle validation representative.
186
+ logger.info("Step 3: Applying repository-specific environment fixes")
187
+ if not apply_environment_fixes(
188
+ str(repo_name),
189
+ lambda command: exec_run_with_timeout(container, command, timeout=120),
190
+ logger,
191
+ ):
192
+ results["error"] = "Repository-specific environment fix failed"
193
+ return results
194
+
195
+ # Step 4: Reinitialize git repository
196
+ logger.info("Step 4: Reinitializing git repository")
182
197
  git_cmds = [
183
198
  "cd /testbed && rm -rf .git",
184
199
  "cd /testbed && git init",
@@ -194,8 +209,8 @@ def run_instance_level1(
194
209
  logger.warning(f"Git command failed: {cmd}")
195
210
  logger.warning(f"Output: {output.decode(UTF8, errors='replace')}")
196
211
 
197
- # Step 4: Apply agent's patch
198
- logger.info("Step 4: Applying agent patch")
212
+ # Step 5: Apply agent's patch
213
+ logger.info("Step 5: Applying agent patch")
199
214
  patch_content = pred[KEY_PREDICTION]
200
215
 
201
216
  if not patch_content or patch_content.strip() == "":
@@ -231,8 +246,8 @@ def run_instance_level1(
231
246
  logger.info(f"{APPLY_PATCH_PASS}")
232
247
  results["patch_applied"] = True
233
248
 
234
- # Step 5: Prepare for testing - delete generated F2P files and restore F2P test
235
- logger.info("Step 5: Preparing for testing - cleaning up and restoring F2P test")
249
+ # Step 6: Prepare for testing - delete generated F2P files and restore F2P test
250
+ logger.info("Step 6: Preparing for testing - cleaning up and restoring F2P test")
236
251
 
237
252
  fail_to_pass = instance.get('FAIL_TO_PASS', [])
238
253
  if not fail_to_pass:
@@ -268,8 +283,8 @@ def run_instance_level1(
268
283
  else:
269
284
  logger.warning("No test_patch available to restore F2P test file")
270
285
 
271
- # Step 6: Run tests
272
- logger.info("Step 6: Running tests")
286
+ # Step 7: Run tests
287
+ logger.info("Step 7: Running tests")
273
288
 
274
289
  # Get test configuration from repo_settings
275
290
  repo_settings = parse_repo_settings(instance)
@@ -589,4 +604,3 @@ def run_instance_level2(
589
604
  logger.error(traceback.format_exc())
590
605
  results["error"] = str(e)
591
606
  return results
592
-
@@ -41,6 +41,10 @@ class BaseAgent(ABC):
41
41
  self.env_vars = env_vars or {}
42
42
  self.logger = logger or logging.getLogger(__name__)
43
43
  self._kwargs = kwargs
44
+ # Agent adapters may populate this with their native terminal status.
45
+ # It deliberately remains optional because not every supported agent
46
+ # exposes a structured status artifact.
47
+ self.agent_exit_status: Optional[str] = None
44
48
 
45
49
  @property
46
50
  @abstractmethod
@@ -354,6 +358,7 @@ class BaseAgent(ABC):
354
358
  Returns:
355
359
  True if agent completed successfully
356
360
  """
361
+ self.agent_exit_status = None
357
362
  self.logger.info(f"Running {self.name} agent...")
358
363
 
359
364
  with open(log_file, "a", encoding="utf-8") as f:
@@ -385,16 +390,23 @@ class BaseAgent(ABC):
385
390
  log_file=log_file,
386
391
  timeout=timeout
387
392
  )
388
-
393
+
389
394
  success_run = exit_code == 0
395
+ timed_out = not success_run and self._infer_log_has_timeout_marker(log_file)
390
396
  success_post_run = self.post_run_hook(container, log_file)
391
397
 
398
+ # A host-side timeout may kill the agent before it can persist a
399
+ # native terminal status. Preserve the actual reason for stopping
400
+ # instead of reporting an unknown/generic failure.
401
+ if timed_out:
402
+ self.agent_exit_status = "TimeExceeded"
403
+
392
404
  if success_run and success_post_run:
393
405
  self.logger.info(f"{self.name} agent completed successfully")
394
406
  return True
395
407
 
396
408
  if not success_run:
397
- if self._force_timeout_enabled() and self._infer_log_has_timeout_marker(log_file):
409
+ if self._force_timeout_enabled() and timed_out:
398
410
  post_run_state = "passed" if success_post_run else "failed"
399
411
  self.logger.warning(
400
412
  f"{self.name} run hit timeout marker (post-run hook {post_run_state}); "
@@ -4,6 +4,7 @@ mini-swe-agent implementation.
4
4
  This agent runs the external `mini` CLI in the container.
5
5
  """
6
6
 
7
+ import json
7
8
  import shlex
8
9
  from pathlib import Path
9
10
  from typing import Dict, List, Optional
@@ -76,7 +77,7 @@ set -e
76
77
  echo "Installing mini-swe-agent..."
77
78
 
78
79
  apt-get update
79
- apt-get install -y python3 python3-pip python3-venv
80
+ apt-get install -y passwd python3 python3-pip python3-venv util-linux
80
81
 
81
82
  CACHE_ROOT="${{AGENT_DOWNLOAD_CACHE:-/download}}"
82
83
  mkdir -p "$CACHE_ROOT" "$CACHE_ROOT/pip"
@@ -98,29 +99,72 @@ echo "mini-swe-agent installation complete"
98
99
  self.cm.exec_command(container, "mkdir -p /agent-logs", log_file=log_file)
99
100
  return True
100
101
 
101
- def post_run_hook(self, container, log_file) -> bool:
102
- """Persist mini-swe-agent CLI output (best-effort)."""
103
- log_dir = Path(log_file).parent
102
+ def pre_run_setup(self, container, instance, log_file: Path) -> bool:
103
+ """Isolate agent-issued shell commands from controller dependencies."""
104
+ controller_dir = "/opt/featurebench-controller"
105
+ environment_source = (
106
+ Path(__file__).parent.parent / "environments" / "mini_swe.py"
107
+ )
108
+ environment_target = f"{controller_dir}/featurebench_mini_swe_environment.py"
109
+
104
110
  try:
105
- self.cm.copy_from_container(
111
+ exit_code, output = self.cm.exec_command(
106
112
  container,
107
- "/agent-logs/mini_swe_agent_output.log",
108
- log_dir / "mini_swe_agent_output.log",
113
+ "set -e\n"
114
+ "getent group fbagent >/dev/null || groupadd --system fbagent\n"
115
+ "id -u fbagent >/dev/null 2>&1 || "
116
+ "useradd --system --gid fbagent --home-dir /home/fbagent "
117
+ "--create-home --shell /bin/bash fbagent\n"
118
+ f"install -d -o root -g root -m 700 {controller_dir} "
119
+ "/agent-logs\n",
120
+ log_file=log_file,
109
121
  )
110
- except Exception:
111
- pass
112
- try:
113
- self.cm.copy_from_container(
122
+ if exit_code != 0:
123
+ raise RuntimeError(
124
+ f"Failed to create restricted mini-swe-agent user: {output}"
125
+ )
126
+
127
+ self.cm.copy_to_container(
114
128
  container,
115
- "/root/.config/mini-swe-agent/last_mini_run.traj.json",
116
- log_dir / "traj.json",
129
+ environment_source,
130
+ environment_target,
117
131
  )
118
- except Exception:
119
- pass
132
+ exit_code, output = self.cm.exec_command(
133
+ container,
134
+ "set -e\n"
135
+ f"chown root:root {environment_target}\n"
136
+ f"chmod 600 {environment_target}\n"
137
+ "chown -R fbagent:fbagent /testbed\n"
138
+ "chmod 700 /opt/mini-swe-agent-venv\n"
139
+ f"chmod 700 {controller_dir} /installed-agent /agent-logs\n"
140
+ "setpriv --reuid=fbagent --regid=fbagent --init-groups "
141
+ "--no-new-privs /bin/bash -c "
142
+ "'test -r /testbed && test -w /testbed && "
143
+ "test ! -r /opt/mini-swe-agent-venv/pyvenv.cfg && "
144
+ "test ! -r /installed-agent/setup-env.sh'",
145
+ log_file=log_file,
146
+ )
147
+ if exit_code != 0:
148
+ raise RuntimeError(
149
+ f"Failed to enforce mini-swe-agent filesystem isolation: {output}"
150
+ )
151
+ except Exception as e:
152
+ self.logger.error(f"Failed to prepare mini-swe-agent isolation: {e}")
153
+ # Isolation is a security boundary. Do not let the runner continue
154
+ # with root-level tool execution if setup fails.
155
+ raise
156
+
157
+ self.logger.info(
158
+ "mini-swe-agent tool commands will run as restricted user fbagent"
159
+ )
120
160
  return True
121
161
 
122
- def failure_hook(self, container, log_file: Path) -> None:
123
- """Collect mini-swe-agent output on failures (best-effort)."""
162
+ def post_run_hook(self, container, log_file) -> bool:
163
+ """Persist artifacts and accept only an explicit agent submission."""
164
+ return self._collect_run_artifacts(container, log_file)
165
+
166
+ def _collect_run_artifacts(self, container, log_file: Path) -> bool:
167
+ """Copy run artifacts and validate mini-swe-agent's terminal status."""
124
168
  log_dir = Path(log_file).parent
125
169
  try:
126
170
  self.cm.copy_from_container(
@@ -128,29 +172,67 @@ echo "mini-swe-agent installation complete"
128
172
  "/agent-logs/mini_swe_agent_output.log",
129
173
  log_dir / "mini_swe_agent_output.log",
130
174
  )
131
- except Exception:
132
- pass
175
+ except Exception as e:
176
+ self.logger.warning(f"Failed to collect mini-swe-agent output: {e}")
177
+
178
+ trajectory_path = log_dir / "traj.json"
133
179
  try:
134
180
  self.cm.copy_from_container(
135
181
  container,
136
182
  "/root/.config/mini-swe-agent/last_mini_run.traj.json",
137
- log_dir / "traj.json",
183
+ trajectory_path,
138
184
  )
139
- except Exception:
140
- pass
185
+ except Exception as e:
186
+ self.logger.error(f"Failed to collect mini-swe-agent trajectory: {e}")
187
+ return False
188
+
189
+ try:
190
+ trajectory = json.loads(trajectory_path.read_text(encoding="utf-8"))
191
+ exit_status = trajectory.get("info", {}).get("exit_status")
192
+ except (OSError, UnicodeError, json.JSONDecodeError, AttributeError) as e:
193
+ self.logger.error(f"Failed to read mini-swe-agent exit status: {e}")
194
+ return False
195
+
196
+ if not isinstance(exit_status, str) or not exit_status.strip():
197
+ self.logger.error("mini-swe-agent trajectory has no exit_status")
198
+ return False
199
+
200
+ self.agent_exit_status = exit_status.strip()
201
+ if self.agent_exit_status != "Submitted":
202
+ self.logger.warning(
203
+ f"mini-swe-agent exited without submission: {self.agent_exit_status}"
204
+ )
205
+ return False
206
+
207
+ self.logger.info("mini-swe-agent exit status: Submitted")
208
+ return True
209
+
210
+ def failure_hook(self, container, log_file: Path) -> None:
211
+ """Collect mini-swe-agent output on failures (best-effort)."""
212
+ trajectory_path = Path(log_file).parent / "traj.json"
213
+ if not trajectory_path.exists():
214
+ self._collect_run_artifacts(container, log_file)
141
215
 
142
216
  def get_run_command(self, instruction: str) -> str:
143
217
  """Get the command to run mini-swe-agent."""
144
218
  escaped_instruction = shlex.quote(instruction)
145
219
  model_name = self._get_model_name()
220
+ cost_limit = self._kwargs.get("cost_limit")
221
+ cost_limit_arg = ""
222
+ if cost_limit is not None:
223
+ cost_limit_arg = f"--cost-limit {shlex.quote(str(cost_limit))} "
146
224
  return (
147
225
  "set -o pipefail; "
148
226
  f'"$MINI_SWE_AGENT_PYTHON" -I - -m {shlex.quote(model_name)} -t {escaped_instruction} -y --exit-immediately '
227
+ f"{cost_limit_arg}"
228
+ "--environment-class "
229
+ "featurebench_mini_swe_environment.RestrictedLocalEnvironment "
149
230
  "<<'PY' | tee /agent-logs/mini_swe_agent_output.log\n"
150
231
  "import runpy\n"
151
232
  "import sys\n"
152
233
  "\n"
153
234
  "sys.path = [p for p in sys.path if p and not p.startswith('/testbed')]\n"
235
+ "sys.path.insert(0, '/opt/featurebench-controller')\n"
154
236
  "sys.argv = ['minisweagent.run.mini', *sys.argv[1:]]\n"
155
237
  "runpy.run_module('minisweagent.run.mini', run_name='__main__')\n"
156
238
  "PY"
@@ -0,0 +1 @@
1
+ """Runtime environments injected into external inference agents."""