evalrx 0.1.2__py3-none-any.whl

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 (339) hide show
  1. evalrx/__init__.py +139 -0
  2. evalrx/agent_assets/__init__.py +2 -0
  3. evalrx/agent_assets/skills/README.md +28 -0
  4. evalrx/agent_assets/skills/eval-chart-style/SKILL.md +172 -0
  5. evalrx/agent_assets/skills/evalrx-report-ui/SKILL.md +116 -0
  6. evalrx/agent_assets/skills/nature-figure/LICENSE +201 -0
  7. evalrx/agent_assets/skills/nature-figure/README.md +412 -0
  8. evalrx/agent_assets/skills/nature-figure/SKILL.md +60 -0
  9. evalrx/agent_assets/skills/nature-figure/manifest.yaml +59 -0
  10. evalrx/agent_assets/skills/nature-figure/references/api.md +436 -0
  11. evalrx/agent_assets/skills/nature-figure/references/backend-selection.md +100 -0
  12. evalrx/agent_assets/skills/nature-figure/references/chart-types.md +281 -0
  13. evalrx/agent_assets/skills/nature-figure/references/common-patterns.md +350 -0
  14. evalrx/agent_assets/skills/nature-figure/references/demos.md +65 -0
  15. evalrx/agent_assets/skills/nature-figure/references/design-theory.md +439 -0
  16. evalrx/agent_assets/skills/nature-figure/references/figure-contract.md +93 -0
  17. evalrx/agent_assets/skills/nature-figure/references/figure-legend-conventions.md +71 -0
  18. evalrx/agent_assets/skills/nature-figure/references/nature-2026-observations.md +112 -0
  19. evalrx/agent_assets/skills/nature-figure/references/qa-contract.md +119 -0
  20. evalrx/agent_assets/skills/nature-figure/references/r-template-index.md +66 -0
  21. evalrx/agent_assets/skills/nature-figure/references/r-workflow.md +161 -0
  22. evalrx/agent_assets/skills/nature-figure/references/tutorials.md +251 -0
  23. evalrx/agent_assets/skills/nature-figure/static/core/contract.md +29 -0
  24. evalrx/agent_assets/skills/nature-figure/static/core/stance.md +37 -0
  25. evalrx/agent_assets/skills/nature-figure/static/fragments/backend/python.md +37 -0
  26. evalrx/agent_assets/skills/nature-figure/static/fragments/backend/r.md +44 -0
  27. evalrx/agent_assets/skills/outcome-driver-analysis/SKILL.md +213 -0
  28. evalrx/agent_assets/skills/outcome-driver-analysis/assets/analysis_report_template.md +53 -0
  29. evalrx/agent_assets/skills/outcome-driver-analysis/references/model_selection.md +72 -0
  30. evalrx/agent_assets/skills/outcome-driver-analysis/scripts/explanatory_var_eda.R +130 -0
  31. evalrx/agent_assets/skills/outcome-driver-analysis/scripts/explanatory_var_eda.py +150 -0
  32. evalrx/agent_assets/skills/outcome-driver-analysis/scripts/fit_outcome_model.R +181 -0
  33. evalrx/agent_assets/skills/outcome-driver-analysis/scripts/fit_outcome_model.py +186 -0
  34. evalrx/agent_assets/skills/outcome-driver-analysis/scripts/univariate_eda.R +149 -0
  35. evalrx/agent_assets/skills/outcome-driver-analysis/scripts/univariate_eda.py +177 -0
  36. evalrx/agent_assets/skills.py +27 -0
  37. evalrx/agent_runtime/__init__.py +78 -0
  38. evalrx/agent_runtime/_docker_runner.py +89 -0
  39. evalrx/agent_runtime/cli_runtime.py +103 -0
  40. evalrx/agent_runtime/cli_transcript.py +138 -0
  41. evalrx/agent_runtime/cli_types.py +68 -0
  42. evalrx/agent_runtime/codegen/__init__.py +5 -0
  43. evalrx/agent_runtime/codegen/runner.py +94 -0
  44. evalrx/agent_runtime/experiment_harness.py +117 -0
  45. evalrx/agent_runtime/factory.py +102 -0
  46. evalrx/agent_runtime/json_shape.py +44 -0
  47. evalrx/agent_runtime/judges/__init__.py +28 -0
  48. evalrx/agent_runtime/judges/agy.py +179 -0
  49. evalrx/agent_runtime/judges/autodetect.py +135 -0
  50. evalrx/agent_runtime/judges/claude.py +159 -0
  51. evalrx/agent_runtime/judges/codex.py +120 -0
  52. evalrx/agent_runtime/providers/__init__.py +21 -0
  53. evalrx/agent_runtime/providers/antigravity.py +31 -0
  54. evalrx/agent_runtime/providers/base.py +145 -0
  55. evalrx/agent_runtime/providers/claude_code.py +49 -0
  56. evalrx/agent_runtime/providers/codex.py +37 -0
  57. evalrx/agent_runtime/providers/gemini_cli.py +26 -0
  58. evalrx/agent_runtime/providers/kimi_cli.py +27 -0
  59. evalrx/agent_runtime/providers/opencode.py +27 -0
  60. evalrx/agent_runtime/providers/registry.py +58 -0
  61. evalrx/agent_runtime/sandbox.py +517 -0
  62. evalrx/agent_runtime/skill_audit.py +143 -0
  63. evalrx/agent_runtime/skills/__init__.py +19 -0
  64. evalrx/agent_runtime/skills/installer.py +68 -0
  65. evalrx/agent_runtime/skills/prompt_policy.py +86 -0
  66. evalrx/agent_runtime/skills/resolver.py +19 -0
  67. evalrx/analysis/__init__.py +132 -0
  68. evalrx/analysis/adjudicate.py +154 -0
  69. evalrx/analysis/analysis_module.py +361 -0
  70. evalrx/analysis/api.py +171 -0
  71. evalrx/analysis/case_studio.py +651 -0
  72. evalrx/analysis/cli.py +114 -0
  73. evalrx/analysis/dashboard.py +350 -0
  74. evalrx/analysis/eval_case_matrix.py +118 -0
  75. evalrx/analysis/eval_viz_theme.py +833 -0
  76. evalrx/analysis/explore_run.py +333 -0
  77. evalrx/analysis/explorer.py +1276 -0
  78. evalrx/analysis/failure_modes.py +607 -0
  79. evalrx/analysis/fused_pipeline.py +489 -0
  80. evalrx/analysis/holdout.py +300 -0
  81. evalrx/analysis/hypothesis_agent.py +230 -0
  82. evalrx/analysis/narration.py +177 -0
  83. evalrx/analysis/operationalize.py +442 -0
  84. evalrx/analysis/plain_language.py +42 -0
  85. evalrx/analysis/planner.py +283 -0
  86. evalrx/analysis/probe_search.py +203 -0
  87. evalrx/analysis/profile.py +268 -0
  88. evalrx/analysis/prompts/__init__.py +0 -0
  89. evalrx/analysis/prompts/explorer.py +417 -0
  90. evalrx/analysis/prompts/failure_modes.py +33 -0
  91. evalrx/analysis/prompts/holdout.py +27 -0
  92. evalrx/analysis/prompts/hypothesis_agent.py +78 -0
  93. evalrx/analysis/prompts/run_codebase.py +47 -0
  94. evalrx/analysis/prompts/stats_agent.py +72 -0
  95. evalrx/analysis/prompts/stats_tool_generator.py +43 -0
  96. evalrx/analysis/result_marker.py +47 -0
  97. evalrx/analysis/run_codebase.py +242 -0
  98. evalrx/analysis/run_view.py +205 -0
  99. evalrx/analysis/stage_views.py +93 -0
  100. evalrx/analysis/stats_agent.py +944 -0
  101. evalrx/analysis/stats_tool_agent.py +261 -0
  102. evalrx/analysis/stats_tool_generator.py +415 -0
  103. evalrx/analysis/stats_tools.py +1153 -0
  104. evalrx/analysis/trajectory_records.py +193 -0
  105. evalrx/analysis/workbench.py +431 -0
  106. evalrx/analyzers/__init__.py +42 -0
  107. evalrx/analyzers/agent/__init__.py +25 -0
  108. evalrx/analyzers/agent/counterfactual.py +84 -0
  109. evalrx/analyzers/agent/first_error_judge.py +96 -0
  110. evalrx/analyzers/agent/ignored_obs.py +81 -0
  111. evalrx/analyzers/agent/loop_detect.py +79 -0
  112. evalrx/analyzers/agent/reliability.py +165 -0
  113. evalrx/analyzers/agent/tool_shap.py +225 -0
  114. evalrx/analyzers/agent/trajectory_rubric.py +168 -0
  115. evalrx/analyzers/attention/__init__.py +19 -0
  116. evalrx/analyzers/attention/relative_attn.py +610 -0
  117. evalrx/analyzers/attention/rollout.py +73 -0
  118. evalrx/analyzers/attention/sink.py +56 -0
  119. evalrx/analyzers/attention/summary.py +190 -0
  120. evalrx/analyzers/attribution/__init__.py +6 -0
  121. evalrx/analyzers/attribution/generic_attn.py +31 -0
  122. evalrx/analyzers/attribution/gradcam.py +30 -0
  123. evalrx/analyzers/base.py +12 -0
  124. evalrx/analyzers/geometry/__init__.py +6 -0
  125. evalrx/analyzers/geometry/cka.py +70 -0
  126. evalrx/analyzers/geometry/linear_probe.py +157 -0
  127. evalrx/analyzers/hallucination/__init__.py +9 -0
  128. evalrx/analyzers/hallucination/chair.py +78 -0
  129. evalrx/analyzers/hallucination/opera.py +29 -0
  130. evalrx/analyzers/hallucination/pope.py +119 -0
  131. evalrx/analyzers/hallucination/selfcheck.py +155 -0
  132. evalrx/analyzers/hallucination/vcd.py +29 -0
  133. evalrx/analyzers/lens/__init__.py +7 -0
  134. evalrx/analyzers/lens/layer_contrast.py +133 -0
  135. evalrx/analyzers/lens/logit_lens.py +138 -0
  136. evalrx/analyzers/lens/tuned_lens.py +30 -0
  137. evalrx/analyzers/patching/__init__.py +5 -0
  138. evalrx/analyzers/patching/causal_trace.py +30 -0
  139. evalrx/analyzers/perturbation/__init__.py +23 -0
  140. evalrx/analyzers/perturbation/_shapley.py +54 -0
  141. evalrx/analyzers/perturbation/context_shap.py +174 -0
  142. evalrx/analyzers/perturbation/cot_faithfulness.py +239 -0
  143. evalrx/analyzers/perturbation/format_sensitivity.py +237 -0
  144. evalrx/analyzers/perturbation/mm_shap.py +146 -0
  145. evalrx/analyzers/perturbation/modality_ablation.py +196 -0
  146. evalrx/analyzers/perturbation/perturbation_battery.py +274 -0
  147. evalrx/analyzers/perturbation/prompt_contrast.py +265 -0
  148. evalrx/analyzers/perturbation/rise.py +94 -0
  149. evalrx/analyzers/perturbation/vl_shap.py +102 -0
  150. evalrx/analyzers/reasoning/__init__.py +33 -0
  151. evalrx/analyzers/reasoning/_text.py +328 -0
  152. evalrx/analyzers/reasoning/answer_extraction_audit.py +327 -0
  153. evalrx/analyzers/reasoning/arith_audit.py +226 -0
  154. evalrx/analyzers/reasoning/contamination.py +214 -0
  155. evalrx/analyzers/reasoning/knowledge_split.py +253 -0
  156. evalrx/analyzers/reasoning/self_repair.py +246 -0
  157. evalrx/analyzers/reasoning/step_rollout_value.py +216 -0
  158. evalrx/analyzers/reasoning/termination_audit.py +258 -0
  159. evalrx/analyzers/uncertainty/__init__.py +18 -0
  160. evalrx/analyzers/uncertainty/calibration.py +174 -0
  161. evalrx/analyzers/uncertainty/coverage_gap.py +199 -0
  162. evalrx/analyzers/uncertainty/entropy.py +90 -0
  163. evalrx/analyzers/uncertainty/logprob_entropy.py +69 -0
  164. evalrx/analyzers/uncertainty/self_consistency.py +204 -0
  165. evalrx/analyzers/uncertainty/verbalized_conf.py +64 -0
  166. evalrx/cli.py +411 -0
  167. evalrx/config.py +77 -0
  168. evalrx/contract/__init__.py +179 -0
  169. evalrx/contract/common.py +452 -0
  170. evalrx/contract/emit.py +948 -0
  171. evalrx/contract/export.py +237 -0
  172. evalrx/contract/m1.py +325 -0
  173. evalrx/contract/m2.py +317 -0
  174. evalrx/contract/m3.py +165 -0
  175. evalrx/contract/m4.py +130 -0
  176. evalrx/contract/m5.py +292 -0
  177. evalrx/contract/methodology.py +76 -0
  178. evalrx/contract/pre_m1.py +58 -0
  179. evalrx/contract/typescript.py +140 -0
  180. evalrx/core/__init__.py +85 -0
  181. evalrx/core/analyzer.py +174 -0
  182. evalrx/core/capability.py +54 -0
  183. evalrx/core/case.py +443 -0
  184. evalrx/core/experiment.py +106 -0
  185. evalrx/core/model.py +198 -0
  186. evalrx/core/pipeline.py +42 -0
  187. evalrx/core/registry.py +142 -0
  188. evalrx/core/result.py +64 -0
  189. evalrx/core/spec.py +173 -0
  190. evalrx/core/tokentype.py +165 -0
  191. evalrx/core/tool.py +92 -0
  192. evalrx/datasets/__init__.py +41 -0
  193. evalrx/datasets/base.py +68 -0
  194. evalrx/datasets/gui_os.py +52 -0
  195. evalrx/datasets/llm_qa.py +57 -0
  196. evalrx/datasets/pure_qa.py +12 -0
  197. evalrx/datasets/vlm_qa.py +695 -0
  198. evalrx/datasets/web_search_qa.py +52 -0
  199. evalrx/eval_agent/__init__.py +341 -0
  200. evalrx/eval_agent/_tools.py +81 -0
  201. evalrx/eval_agent/ab_runner.py +50 -0
  202. evalrx/eval_agent/agentic/__init__.py +43 -0
  203. evalrx/eval_agent/agentic/actions.py +216 -0
  204. evalrx/eval_agent/agentic/board.py +107 -0
  205. evalrx/eval_agent/agentic/loop.py +190 -0
  206. evalrx/eval_agent/agentic/tools.py +538 -0
  207. evalrx/eval_agent/checkpoint.py +57 -0
  208. evalrx/eval_agent/cli_agent.py +59 -0
  209. evalrx/eval_agent/cli_skills.py +5 -0
  210. evalrx/eval_agent/evolution.py +396 -0
  211. evalrx/eval_agent/git_manager.py +215 -0
  212. evalrx/eval_agent/hypothesis.py +172 -0
  213. evalrx/eval_agent/label_quarantine.py +209 -0
  214. evalrx/eval_agent/legacy.py +530 -0
  215. evalrx/eval_agent/log_schema.py +497 -0
  216. evalrx/eval_agent/loop.py +2159 -0
  217. evalrx/eval_agent/loop_reports.py +116 -0
  218. evalrx/eval_agent/model_instrumentation.py +282 -0
  219. evalrx/eval_agent/narration.py +193 -0
  220. evalrx/eval_agent/nl_runner.py +460 -0
  221. evalrx/eval_agent/orchestrator.py +61 -0
  222. evalrx/eval_agent/preregister.py +93 -0
  223. evalrx/eval_agent/prompts/__init__.py +1 -0
  224. evalrx/eval_agent/prompts/agentic.py +46 -0
  225. evalrx/eval_agent/prompts/case_discovery.py +25 -0
  226. evalrx/eval_agent/prompts/diagnosis.py +125 -0
  227. evalrx/eval_agent/prompts/experiment_writer.py +265 -0
  228. evalrx/eval_agent/prompts/explore_step.py +37 -0
  229. evalrx/eval_agent/prompts/fix_agent.py +257 -0
  230. evalrx/eval_agent/prompts/hypothesis_tester.py +15 -0
  231. evalrx/eval_agent/prompts/nl_runner.py +38 -0
  232. evalrx/eval_agent/prompts/probe_agent.py +25 -0
  233. evalrx/eval_agent/prompts/probe_candidate_generator.py +14 -0
  234. evalrx/eval_agent/prompts/probe_generator.py +35 -0
  235. evalrx/eval_agent/prompts/whitebox_probe_generator.py +38 -0
  236. evalrx/eval_agent/report.py +58 -0
  237. evalrx/eval_agent/run_context.py +354 -0
  238. evalrx/eval_agent/run_log.schema.json +1215 -0
  239. evalrx/eval_agent/run_logger_v2.py +1764 -0
  240. evalrx/eval_agent/run_metadata.py +208 -0
  241. evalrx/eval_agent/stages/__init__.py +56 -0
  242. evalrx/eval_agent/stages/case_discovery.py +293 -0
  243. evalrx/eval_agent/stages/diagnosis.py +1017 -0
  244. evalrx/eval_agent/stages/experiment_writer.py +1634 -0
  245. evalrx/eval_agent/stages/fix_agent.py +3916 -0
  246. evalrx/eval_agent/stages/fix_internals.py +499 -0
  247. evalrx/eval_agent/stages/fix_pipeline.py +725 -0
  248. evalrx/eval_agent/stages/fix_tiers.py +187 -0
  249. evalrx/eval_agent/stages/fix_tools.py +1034 -0
  250. evalrx/eval_agent/stages/hypothesis_tester.py +1014 -0
  251. evalrx/eval_agent/stages/probe.py +439 -0
  252. evalrx/eval_agent/stages/probe_agent.py +1079 -0
  253. evalrx/eval_agent/stages/probe_candidate_generator.py +128 -0
  254. evalrx/eval_agent/stages/probe_generator.py +326 -0
  255. evalrx/eval_agent/stages/probe_search_agent.py +106 -0
  256. evalrx/eval_agent/stages/protocol.py +112 -0
  257. evalrx/eval_agent/stages/repair_catalog.py +273 -0
  258. evalrx/eval_agent/stages/surgery.py +524 -0
  259. evalrx/eval_agent/stages/whitebox_probe_generator.py +351 -0
  260. evalrx/eval_agent/store.py +231 -0
  261. evalrx/logging_utils.py +112 -0
  262. evalrx/models/__init__.py +161 -0
  263. evalrx/models/_discover.py +101 -0
  264. evalrx/models/agent.py +380 -0
  265. evalrx/models/backends/__init__.py +58 -0
  266. evalrx/models/backends/api.py +169 -0
  267. evalrx/models/backends/base.py +57 -0
  268. evalrx/models/backends/gemini_compat.py +579 -0
  269. evalrx/models/backends/hf_local.py +2074 -0
  270. evalrx/models/backends/openai_compat.py +301 -0
  271. evalrx/models/backends/vllm_offline.py +116 -0
  272. evalrx/models/base.py +24 -0
  273. evalrx/models/blackbox/__init__.py +4 -0
  274. evalrx/models/blackbox/agent.py +31 -0
  275. evalrx/models/blackbox/base.py +29 -0
  276. evalrx/models/blackbox/gemini.py +279 -0
  277. evalrx/models/blackbox/llm_api.py +17 -0
  278. evalrx/models/blackbox/vlm_api.py +17 -0
  279. evalrx/models/compose.py +66 -0
  280. evalrx/models/inference.py +88 -0
  281. evalrx/models/paper_methods/__init__.py +8 -0
  282. evalrx/models/paper_methods/aad.py +53 -0
  283. evalrx/models/paper_methods/ifcd.py +204 -0
  284. evalrx/models/paper_methods/pai.py +164 -0
  285. evalrx/models/paper_methods/tcd.py +202 -0
  286. evalrx/models/paper_methods/vcd.py +45 -0
  287. evalrx/models/paper_methods/vicrop.py +137 -0
  288. evalrx/models/toolcodec.py +143 -0
  289. evalrx/models/tools/__init__.py +20 -0
  290. evalrx/models/tools/perception.py +300 -0
  291. evalrx/models/tools/visual.py +174 -0
  292. evalrx/models/whitebox/__init__.py +26 -0
  293. evalrx/models/whitebox/agent.py +31 -0
  294. evalrx/models/whitebox/base.py +24 -0
  295. evalrx/models/whitebox/qwen.py +61 -0
  296. evalrx/models/whitebox/qwen2_5_omni.py +29 -0
  297. evalrx/models/whitebox/qwen2_audio.py +25 -0
  298. evalrx/models/whitebox/qwen_omni.py +53 -0
  299. evalrx/models/whitebox/qwen_vl.py +62 -0
  300. evalrx/observability/__init__.py +21 -0
  301. evalrx/observability/envelope.py +122 -0
  302. evalrx/observability/outbox.py +111 -0
  303. evalrx/observability/tracer.py +882 -0
  304. evalrx/reporting/__init__.py +28 -0
  305. evalrx/reporting/case_study.py +947 -0
  306. evalrx/reporting/compiler.py +587 -0
  307. evalrx/reporting/dynamic.py +1882 -0
  308. evalrx/reporting/html_report.py +2225 -0
  309. evalrx/reporting/langfuse_exporter.py +38 -0
  310. evalrx/reporting/langfuse_source.py +155 -0
  311. evalrx/reporting/model.py +151 -0
  312. evalrx/reporting/run_events.py +184 -0
  313. evalrx/reporting/server.py +557 -0
  314. evalrx/reporting/stages.py +58 -0
  315. evalrx/reporting/static_export.py +142 -0
  316. evalrx/reporting/web_dist/index.html +146 -0
  317. evalrx/specs.py +727 -0
  318. evalrx/stats/__init__.py +47 -0
  319. evalrx/stats/api.py +192 -0
  320. evalrx/stats/bootstrap.py +86 -0
  321. evalrx/stats/ebh.py +27 -0
  322. evalrx/stats/evalue.py +98 -0
  323. evalrx/stats/friedman.py +138 -0
  324. evalrx/stats/mcnemar.py +40 -0
  325. evalrx/stats/multiplicity.py +159 -0
  326. evalrx/stats/subset_sampling.py +55 -0
  327. evalrx/term_links.py +43 -0
  328. evalrx/viz/__init__.py +7 -0
  329. evalrx/viz/labels.py +77 -0
  330. evalrx/viz/prompts.py +39 -0
  331. evalrx/viz/renderer.py +590 -0
  332. evalrx/viz/schema.py +36 -0
  333. evalrx/viz/style.py +134 -0
  334. evalrx-0.1.2.dist-info/METADATA +532 -0
  335. evalrx-0.1.2.dist-info/RECORD +339 -0
  336. evalrx-0.1.2.dist-info/WHEEL +5 -0
  337. evalrx-0.1.2.dist-info/entry_points.txt +3 -0
  338. evalrx-0.1.2.dist-info/licenses/LICENSE +121 -0
  339. evalrx-0.1.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,517 @@
1
+ """Experiment sandbox — safe subprocess execution + ARC-compatible metric parsing.
2
+
3
+ Mirrors researchclaw/experiment/sandbox.py.
4
+
5
+ Metric output formats understood by ``parse_metrics``:
6
+
7
+ Plain: ``metric: 0.85``
8
+ Condition: ``condition=arithmetic metric: 0.42``
9
+ Summary: ``SUMMARY condition=X metric=Y mean=M std=S``
10
+
11
+ The diagnostic script printed by ``ExperimentWriter`` uses these same formats,
12
+ so ``SandboxResult.metrics`` is immediately ready for the surgery agent to
13
+ interpret.
14
+
15
+ Enhanced over the original with:
16
+ - ``SandboxProtocol`` for pluggable backends
17
+ - ``run_project()`` for multi-file experiment projects
18
+ - Path traversal protection (validate_entry_point / resolved)
19
+ - Numbered project dirs (``_project_{counter}``) with thread-safe counter
20
+ - Immutable harness injection (``experiment_harness.py``)
21
+ - Cleanup policy: delete scripts only on success
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import logging
27
+ import math
28
+ import os
29
+ import re
30
+ import shutil
31
+ import subprocess
32
+ import sys
33
+ import tempfile
34
+ import threading
35
+ import time
36
+ from dataclasses import dataclass, field
37
+ from pathlib import Path, PurePosixPath, PureWindowsPath
38
+ from typing import Callable, Protocol
39
+
40
+ logger = logging.getLogger(__name__)
41
+
42
+ # ---------------------------------------------------------------------------
43
+ # Entry-point validation (path traversal protection)
44
+ # ---------------------------------------------------------------------------
45
+
46
+
47
+ def validate_entry_point(entry_point: str) -> str | None:
48
+ """Validate *entry_point* syntax without filesystem access.
49
+
50
+ Returns an error message if invalid, ``None`` if valid.
51
+ Call this **before** copying files to fail fast on obviously bad input.
52
+ """
53
+ if not entry_point or not entry_point.strip():
54
+ return "Entry point is empty"
55
+ ep = Path(entry_point)
56
+ posix_ep = PurePosixPath(entry_point)
57
+ windows_ep = PureWindowsPath(entry_point)
58
+ if ep.is_absolute() or posix_ep.is_absolute() or windows_ep.is_absolute():
59
+ return f"Entry point must be a relative path, got: {entry_point}"
60
+ if ".." in ep.parts:
61
+ return f"Entry point must not contain '..': {entry_point}"
62
+ return None
63
+
64
+
65
+ def validate_entry_point_resolved(staging: Path, entry_point: str) -> str | None:
66
+ """Validate that *entry_point* resolves inside *staging*.
67
+
68
+ Returns an error message if invalid, ``None`` if valid.
69
+ Call this **after** copying files so that symlinks are resolved against
70
+ the real staging contents.
71
+ """
72
+ resolved = (staging / entry_point).resolve()
73
+ staging_resolved = staging.resolve()
74
+ if not resolved.is_relative_to(staging_resolved):
75
+ return f"Entry point escapes staging directory: {entry_point}"
76
+ return None
77
+
78
+
79
+ # ---------------------------------------------------------------------------
80
+ # Metric parsing (ARC-compatible formats)
81
+ # ---------------------------------------------------------------------------
82
+
83
+ _FLOAT_RE = r"[+-]?\d+\.?\d*(?:[eE][+-]?\d+)?"
84
+
85
+ # Plain: "metric: 0.85"
86
+ _PLAIN_RE = re.compile(rf"^(\w[\w./]*)\s*:\s*({_FLOAT_RE})\s*$")
87
+
88
+ # Condition: "condition=X metric: 0.42"
89
+ _CONDITION_RE = re.compile(rf"^condition=(\S+)\s+(\w[\w./]*)\s*:\s*({_FLOAT_RE})\s*$")
90
+
91
+ # Summary: "SUMMARY condition=X metric=Y mean=M std=S"
92
+ _SUMMARY_RE = re.compile(
93
+ rf"^SUMMARY\s+condition=(\S+)\s+metric=(\S+)\s+mean=({_FLOAT_RE})\s+std=({_FLOAT_RE})"
94
+ )
95
+
96
+
97
+ def parse_metrics(stdout: str) -> dict[str, float]:
98
+ """Extract ``name: float`` pairs from *stdout*.
99
+
100
+ Matching priority: SUMMARY lines first, then condition-prefixed, then plain.
101
+ Non-finite values are silently dropped.
102
+ """
103
+ metrics: dict[str, float] = {}
104
+
105
+ for line in stdout.splitlines():
106
+ stripped = line.strip()
107
+
108
+ # SUMMARY condition=X metric=Y mean=M std=S
109
+ m = _SUMMARY_RE.match(stripped)
110
+ if m:
111
+ cond, metric, mean_s, std_s = m.groups()
112
+ try:
113
+ mean_v, std_v = float(mean_s), float(std_s)
114
+ except ValueError:
115
+ continue
116
+ if math.isfinite(mean_v):
117
+ metrics[f"{cond}/{metric}"] = mean_v
118
+ metrics[f"{cond}/{metric}_mean"] = mean_v
119
+ metrics[f"{cond}/{metric}_std"] = std_v
120
+ metrics[metric] = mean_v
121
+ continue
122
+
123
+ # condition=X metric: value
124
+ m = _CONDITION_RE.match(stripped)
125
+ if m:
126
+ cond, name, val_s = m.groups()
127
+ try:
128
+ val = float(val_s)
129
+ except ValueError:
130
+ continue
131
+ if math.isfinite(val):
132
+ metrics[f"{cond}/{name}"] = val
133
+ metrics[name] = val
134
+ continue
135
+
136
+ # metric: value
137
+ m = _PLAIN_RE.match(stripped)
138
+ if m:
139
+ name, val_s = m.groups()
140
+ try:
141
+ val = float(val_s)
142
+ except ValueError:
143
+ continue
144
+ if math.isfinite(val):
145
+ metrics[name] = val
146
+
147
+ return metrics
148
+
149
+
150
+ # ---------------------------------------------------------------------------
151
+ # Result + Protocol
152
+ # ---------------------------------------------------------------------------
153
+
154
+
155
+ @dataclass
156
+ class SandboxResult:
157
+ """Output of one :class:`ExperimentSandbox` run.
158
+
159
+ Attributes:
160
+ returncode: Process exit code (0 = success, −1 = internal/timeout).
161
+ stdout: Captured standard output.
162
+ stderr: Captured standard error.
163
+ elapsed_sec: Wall-clock seconds.
164
+ metrics: Parsed ``{name: float}`` from *stdout*.
165
+ timed_out: ``True`` when the process was killed for exceeding the budget.
166
+ """
167
+
168
+ returncode: int
169
+ stdout: str
170
+ stderr: str
171
+ elapsed_sec: float
172
+ metrics: dict[str, float] = field(default_factory=dict)
173
+ timed_out: bool = False
174
+
175
+ @property
176
+ def ok(self) -> bool:
177
+ return self.returncode == 0 and not self.timed_out
178
+
179
+
180
+ class SandboxProtocol(Protocol):
181
+ """Structural type for sandbox backends (subprocess, Docker, SSH, …)."""
182
+
183
+ def run(self, code: str, *, timeout_sec: int = 60) -> SandboxResult: ...
184
+
185
+ def run_project(
186
+ self,
187
+ project_dir: Path,
188
+ *,
189
+ entry_point: str = "main.py",
190
+ timeout_sec: int = 60,
191
+ args: list[str] | None = None,
192
+ env_overrides: dict[str, str] | None = None,
193
+ ) -> SandboxResult: ...
194
+
195
+
196
+ # ---------------------------------------------------------------------------
197
+ # ExperimentSandbox
198
+ # ---------------------------------------------------------------------------
199
+
200
+
201
+ def _make_resource_preexec(
202
+ max_memory_bytes: int | None,
203
+ max_cpu_seconds: int | None,
204
+ ) -> "Callable[[], None] | None":
205
+ """Return a ``preexec_fn`` that sets resource limits on Linux, or ``None``.
206
+
207
+ Uses ``resource.setrlimit`` which is only available on Unix. The function
208
+ is constructed at call time and captures the limit values in its closure so
209
+ each subprocess gets its own independent limits.
210
+
211
+ Memory is capped via ``RLIMIT_AS`` (virtual address space). CPU time is
212
+ capped via ``RLIMIT_CPU`` (user+system seconds). Hitting the CPU limit
213
+ sends SIGXCPU to the process; the subprocess.run *timeout* handles wall-clock
214
+ kills independently, so both limits are in effect simultaneously.
215
+ """
216
+ import sys
217
+ if sys.platform == "win32":
218
+ return None
219
+ try:
220
+ import resource as _resource
221
+ except ImportError:
222
+ return None
223
+
224
+ def _preexec() -> None:
225
+ if max_memory_bytes is not None:
226
+ try:
227
+ _resource.setrlimit(
228
+ _resource.RLIMIT_AS,
229
+ (max_memory_bytes, max_memory_bytes),
230
+ )
231
+ except (ValueError, _resource.error):
232
+ pass
233
+ if max_cpu_seconds is not None:
234
+ try:
235
+ _resource.setrlimit(
236
+ _resource.RLIMIT_CPU,
237
+ (max_cpu_seconds, max_cpu_seconds),
238
+ )
239
+ except (ValueError, _resource.error):
240
+ pass
241
+
242
+ return _preexec
243
+
244
+
245
+ class ExperimentSandbox:
246
+ """Run Python code in a subprocess and parse metrics from stdout.
247
+
248
+ Mirrors ``researchclaw.experiment.sandbox.ExperimentSandbox``.
249
+
250
+ Args:
251
+ workdir: Directory where scripts are written. A temporary
252
+ directory is created when ``None``.
253
+ python_path: Python executable to use. Defaults to ``sys.executable``.
254
+ max_memory_bytes: Virtual-address-space cap applied via ``RLIMIT_AS``
255
+ before the child process starts (Linux/macOS only).
256
+ ``None`` means no memory limit.
257
+ max_cpu_seconds: CPU-time cap applied via ``RLIMIT_CPU`` (Linux/macOS).
258
+ ``None`` means no CPU limit. This complements the
259
+ wall-clock *timeout_sec* argument to ``run()``.
260
+ cleanup: When ``True`` (default), a successful run's script/
261
+ project is deleted (see ``_should_cleanup``) — the
262
+ original behaviour for an ephemeral *workdir*. Pass
263
+ ``False`` when *workdir* is a durable, self-contained
264
+ location (e.g. one fix/M5 trial's ``workspace/``)
265
+ whose code should stay on disk even on success.
266
+ """
267
+
268
+ def __init__(
269
+ self,
270
+ workdir: Path | str | None = None,
271
+ *,
272
+ python_path: str | None = None,
273
+ max_memory_bytes: int | None = None,
274
+ max_cpu_seconds: int | None = None,
275
+ cleanup: bool = True,
276
+ ) -> None:
277
+ if workdir is None:
278
+ workdir = Path(tempfile.mkdtemp(prefix="evalrx_sandbox_"))
279
+ # Resolved to absolute: _run_script passes this same path as both the
280
+ # subprocess cwd and (via script_path) part of the command argv — a
281
+ # relative workdir makes the child resolve the script path a second
282
+ # time relative to its new cwd, doubling it and raising
283
+ # FileNotFoundError instead of running the script.
284
+ self.workdir = Path(workdir).resolve()
285
+ self.workdir.mkdir(parents=True, exist_ok=True)
286
+ self._python_path: str = python_path or sys.executable
287
+ self._run_counter: int = 0
288
+ self._counter_lock = threading.Lock()
289
+ self._preexec_fn = _make_resource_preexec(max_memory_bytes, max_cpu_seconds)
290
+ self._cleanup = cleanup
291
+
292
+ # ------------------------------------------------------------------
293
+ # Public interface
294
+ # ------------------------------------------------------------------
295
+
296
+ def run(self, code: str, *, timeout_sec: int = 60) -> SandboxResult:
297
+ """Write *code* to a numbered script file and execute it.
298
+
299
+ Args:
300
+ code: Python source to execute.
301
+ timeout_sec: Hard wall-clock limit; the process is killed on breach.
302
+
303
+ Returns:
304
+ :class:`SandboxResult` with parsed metrics.
305
+ """
306
+ with self._counter_lock:
307
+ self._run_counter += 1
308
+ counter = self._run_counter
309
+ script_path = self.workdir / f"exp_{counter:04d}.py"
310
+ script_path.write_text(code, encoding="utf-8")
311
+
312
+ result = self._run_script(script_path, timeout_sec=timeout_sec)
313
+
314
+ if self._should_cleanup(result):
315
+ self._cleanup_path(script_path)
316
+
317
+ return result
318
+
319
+ def run_project(
320
+ self,
321
+ project_dir: Path,
322
+ *,
323
+ entry_point: str = "main.py",
324
+ timeout_sec: int = 60,
325
+ args: list[str] | None = None,
326
+ env_overrides: dict[str, str] | None = None,
327
+ ) -> SandboxResult:
328
+ """Copy a multi-file project into the sandbox and execute *entry_point*.
329
+
330
+ Security:
331
+ - *entry_point* is validated for path traversal before and after copy.
332
+ - The project cannot overwrite the injected ``experiment_harness.py``.
333
+
334
+ Args:
335
+ project_dir: Source directory containing project files.
336
+ entry_point: Relative path to the script to execute (default ``main.py``).
337
+ timeout_sec: Hard wall-clock limit.
338
+ args: Extra command-line arguments passed to the script.
339
+ env_overrides: Additional environment variables for the subprocess.
340
+
341
+ Returns:
342
+ :class:`SandboxResult` with parsed metrics.
343
+ """
344
+ # Pre-copy syntax validation — fail fast on obviously bad input
345
+ err = validate_entry_point(entry_point)
346
+ if err:
347
+ return SandboxResult(returncode=-1, stdout="", stderr=err,
348
+ elapsed_sec=0.0, metrics={})
349
+
350
+ with self._counter_lock:
351
+ self._run_counter += 1
352
+ counter = self._run_counter
353
+ sandbox_project = self.workdir / f"_project_{counter}"
354
+ if sandbox_project.exists():
355
+ shutil.rmtree(sandbox_project)
356
+ sandbox_project.mkdir(parents=True, exist_ok=True)
357
+
358
+ # Inject immutable experiment harness first
359
+ self._inject_harness(sandbox_project)
360
+
361
+ # Copy project files (harness cannot be overwritten)
362
+ for src in Path(project_dir).iterdir():
363
+ if src.is_file():
364
+ dest = sandbox_project / src.name
365
+ if dest.name == "experiment_harness.py":
366
+ logger.warning(
367
+ "Project contains experiment_harness.py — skipping (immutable)"
368
+ )
369
+ continue
370
+ dest.write_bytes(src.read_bytes())
371
+ elif src.is_dir() and not src.name.startswith("."):
372
+ shutil.copytree(src, sandbox_project / src.name, dirs_exist_ok=True)
373
+
374
+ # Post-copy resolve check — catches symlink-based escapes
375
+ err = validate_entry_point_resolved(sandbox_project, entry_point)
376
+ if err:
377
+ return SandboxResult(returncode=-1, stdout="", stderr=err,
378
+ elapsed_sec=0.0, metrics={})
379
+
380
+ entry = sandbox_project / entry_point
381
+ if not entry.exists():
382
+ return SandboxResult(
383
+ returncode=-1, stdout="",
384
+ stderr=f"Entry point '{entry_point}' not found in project",
385
+ elapsed_sec=0.0, metrics={},
386
+ )
387
+
388
+ result = self._run_script(
389
+ entry,
390
+ timeout_sec=timeout_sec,
391
+ cwd=sandbox_project,
392
+ args=args,
393
+ env_overrides=env_overrides,
394
+ )
395
+
396
+ if self._should_cleanup(result):
397
+ self._cleanup_path(sandbox_project)
398
+
399
+ return result
400
+
401
+ # ------------------------------------------------------------------
402
+ # Internals
403
+ # ------------------------------------------------------------------
404
+
405
+ def _run_script(
406
+ self,
407
+ script_path: Path,
408
+ *,
409
+ timeout_sec: int,
410
+ cwd: Path | None = None,
411
+ args: list[str] | None = None,
412
+ env_overrides: dict[str, str] | None = None,
413
+ ) -> SandboxResult:
414
+ env = {**os.environ, "PYTHONUNBUFFERED": "1"}
415
+ if env_overrides:
416
+ env.update(env_overrides)
417
+ command = self._build_command(script_path, args=args)
418
+ run_cwd = str(cwd or self.workdir)
419
+ start = time.monotonic()
420
+ try:
421
+ completed = subprocess.run(
422
+ command,
423
+ capture_output=True,
424
+ text=True,
425
+ encoding="utf-8",
426
+ errors="replace",
427
+ timeout=timeout_sec,
428
+ cwd=run_cwd,
429
+ env=env,
430
+ check=False,
431
+ preexec_fn=self._preexec_fn,
432
+ )
433
+ return self._result_from_completed(completed, elapsed_sec=time.monotonic() - start)
434
+ except subprocess.TimeoutExpired as exc:
435
+ return self._result_from_timeout(
436
+ exc, timeout_sec=timeout_sec, elapsed_sec=time.monotonic() - start
437
+ )
438
+ except Exception as exc: # noqa: BLE001
439
+ return self._result_from_exception(exc, elapsed_sec=time.monotonic() - start)
440
+
441
+ def _build_command(
442
+ self, script_path: Path, *, args: list[str] | None = None
443
+ ) -> list[str]:
444
+ python = self._python_path
445
+ python_path = Path(python)
446
+ if not python_path.is_absolute() and python != "python":
447
+ python_path = Path.cwd() / python_path
448
+ command = [str(python_path), "-u", str(script_path)]
449
+ if args:
450
+ command.extend(args)
451
+ return command
452
+
453
+ @staticmethod
454
+ def _inject_harness(target_dir: Path) -> None:
455
+ """Copy the immutable experiment harness into *target_dir*."""
456
+ harness_src = Path(__file__).parent / "experiment_harness.py"
457
+ if harness_src.exists():
458
+ dest = target_dir / "experiment_harness.py"
459
+ dest.write_text(harness_src.read_text(encoding="utf-8"), encoding="utf-8")
460
+ logger.debug("Injected experiment harness into %s", target_dir)
461
+ else:
462
+ logger.warning("Harness template not found at %s", harness_src)
463
+
464
+ @staticmethod
465
+ def _result_from_completed(
466
+ completed: subprocess.CompletedProcess[str], *, elapsed_sec: float
467
+ ) -> SandboxResult:
468
+ stdout = completed.stdout or ""
469
+ stderr = completed.stderr or ""
470
+ return SandboxResult(
471
+ returncode=completed.returncode,
472
+ stdout=stdout,
473
+ stderr=stderr,
474
+ elapsed_sec=elapsed_sec,
475
+ metrics=parse_metrics(stdout),
476
+ )
477
+
478
+ @staticmethod
479
+ def _result_from_timeout(
480
+ exc: subprocess.TimeoutExpired,
481
+ *,
482
+ timeout_sec: int,
483
+ elapsed_sec: float,
484
+ ) -> SandboxResult:
485
+ stdout = (exc.stdout or b"").decode("utf-8", errors="replace") if isinstance(exc.stdout, bytes) else (exc.stdout or "")
486
+ stderr = (exc.stderr or b"").decode("utf-8", errors="replace") if isinstance(exc.stderr, bytes) else (exc.stderr or "")
487
+ return SandboxResult(
488
+ returncode=-1,
489
+ stdout=stdout,
490
+ stderr=f"[TIMEOUT after {timeout_sec}s]\n{stderr}",
491
+ elapsed_sec=elapsed_sec,
492
+ metrics=parse_metrics(stdout),
493
+ timed_out=True,
494
+ )
495
+
496
+ @staticmethod
497
+ def _result_from_exception(exc: Exception, *, elapsed_sec: float) -> SandboxResult:
498
+ return SandboxResult(
499
+ returncode=-1,
500
+ stdout="",
501
+ stderr=f"[SANDBOX ERROR] {exc}",
502
+ elapsed_sec=elapsed_sec,
503
+ metrics={},
504
+ )
505
+
506
+ def _should_cleanup(self, result: SandboxResult) -> bool:
507
+ return self._cleanup and result.returncode == 0 and not result.timed_out
508
+
509
+ @staticmethod
510
+ def _cleanup_path(path: Path) -> None:
511
+ try:
512
+ if path.is_dir():
513
+ shutil.rmtree(path)
514
+ else:
515
+ path.unlink(missing_ok=True)
516
+ except Exception: # noqa: BLE001
517
+ logger.warning("Failed to clean up sandbox path: %s", path)
@@ -0,0 +1,143 @@
1
+ """Provider-neutral, evidence-backed auditing for configured Agent Skills.
2
+
3
+ The audit deliberately distinguishes a skill being requested or installed from
4
+ the provider trace proving it was invoked/read. A provider without a machine
5
+ readable trace is reported as ``not_observable`` rather than guessed from the
6
+ assistant's prose.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ AUDIT_SCHEMA_VERSION = 1
16
+
17
+
18
+ def _skill_names(paths: list[str] | tuple[str, ...]) -> list[str]:
19
+ return list(dict.fromkeys(Path(path).name for path in paths if Path(path).name))
20
+
21
+
22
+ def _json_lines(stdout: str) -> list[dict[str, Any]]:
23
+ rows: list[dict[str, Any]] = []
24
+ for line in stdout.splitlines():
25
+ try:
26
+ item = json.loads(line)
27
+ except (TypeError, ValueError):
28
+ continue
29
+ if isinstance(item, dict):
30
+ rows.append(item)
31
+ return rows
32
+
33
+
34
+ def _claude_evidence(stdout: str, requested: list[str]) -> list[dict[str, str]]:
35
+ evidence: list[dict[str, str]] = []
36
+ requested_set = set(requested)
37
+ for event in _json_lines(stdout):
38
+ if event.get("type") != "assistant":
39
+ continue
40
+ content = (event.get("message") or {}).get("content") or []
41
+ for block in content:
42
+ if not isinstance(block, dict) or block.get("type") != "tool_use":
43
+ continue
44
+ if block.get("name") != "Skill":
45
+ continue
46
+ inp = block.get("input") or {}
47
+ raw_name = str(inp.get("skill") or "").strip().lstrip("/")
48
+ if raw_name in requested_set:
49
+ evidence.append({
50
+ "skill": raw_name,
51
+ "kind": "native_skill_invocation",
52
+ "detail": "Claude Code Skill tool invocation",
53
+ })
54
+ return evidence
55
+
56
+
57
+ def _codex_evidence(stdout: str, requested: list[str]) -> list[dict[str, str]]:
58
+ """Find conservative evidence that Codex read a vendored skill guide.
59
+
60
+ ``codex exec --json`` exposes shell command events, but Codex has no
61
+ ``Skill`` tool. Merely listing a directory is not evidence of use, so only
62
+ commands that name a skill's ``SKILL.md`` (or a file within its directory)
63
+ are admitted, excluding ``rg --files`` discovery commands.
64
+ """
65
+ evidence: list[dict[str, str]] = []
66
+ for event in _json_lines(stdout):
67
+ item = event.get("item") if isinstance(event.get("item"), dict) else {}
68
+ if item.get("type") != "command_execution":
69
+ continue
70
+ command = str(item.get("command") or "")
71
+ if "rg --files" in command:
72
+ continue
73
+ for name in requested:
74
+ marker = f".claude/skills/{name}/"
75
+ if marker in command:
76
+ evidence.append({
77
+ "skill": name,
78
+ "kind": "vendored_skill_file_read",
79
+ "detail": "Codex command referenced a file in the vendored skill directory",
80
+ })
81
+ return evidence
82
+
83
+
84
+ def build_agent_audit(
85
+ *,
86
+ provider: str,
87
+ skills: list[str] | tuple[str, ...],
88
+ workdir: Path,
89
+ stdout: str,
90
+ stderr: str,
91
+ returncode: int,
92
+ timed_out: bool,
93
+ elapsed_sec: float,
94
+ files: dict[str, str],
95
+ error: str | None,
96
+ ) -> dict[str, Any]:
97
+ """Build one durable audit record for a provider invocation."""
98
+ requested = _skill_names(skills)
99
+ installed = [
100
+ name for name in requested
101
+ if (workdir / ".claude" / "skills" / name / "SKILL.md").is_file()
102
+ ]
103
+ if provider == "claude_code":
104
+ evidence = _claude_evidence(stdout, requested)
105
+ observation = "native_skill_events"
106
+ elif provider == "codex":
107
+ evidence = _codex_evidence(stdout, requested)
108
+ observation = "json_command_events"
109
+ elif provider == "antigravity":
110
+ evidence = []
111
+ observation = "not_observable"
112
+ else:
113
+ evidence = []
114
+ observation = "not_observable"
115
+
116
+ invoked = list(dict.fromkeys(item["skill"] for item in evidence))
117
+ if timed_out:
118
+ status = "timed_out"
119
+ elif error:
120
+ status = "failed"
121
+ elif files:
122
+ status = "completed"
123
+ else:
124
+ status = "completed_without_code"
125
+
126
+ return {
127
+ "schema_version": AUDIT_SCHEMA_VERSION,
128
+ "provider": provider,
129
+ "skills_requested": requested,
130
+ "skills_installed": installed,
131
+ "skills_invoked": invoked,
132
+ "skill_observability": observation,
133
+ "evidence": evidence,
134
+ "execution": {
135
+ "status": status,
136
+ "returncode": returncode,
137
+ "timed_out": timed_out,
138
+ "elapsed_sec": round(elapsed_sec, 3),
139
+ "produced_files": sorted(files),
140
+ "error": error or "",
141
+ "stderr": stderr[:4000],
142
+ },
143
+ }
@@ -0,0 +1,19 @@
1
+ """Skill resolution, installation, and prompt policy helpers."""
2
+
3
+ from evalrx.agent_runtime.skills.installer import CodexSkillInstaller, SkillInstaller
4
+ from evalrx.agent_runtime.skills.prompt_policy import fences_hint, skills_hint
5
+ from evalrx.agent_runtime.skills.resolver import (
6
+ SKILL_BACKENDS,
7
+ bundled_skill_paths,
8
+ resolve_skill_paths,
9
+ )
10
+
11
+ __all__ = [
12
+ "CodexSkillInstaller",
13
+ "SKILL_BACKENDS",
14
+ "SkillInstaller",
15
+ "bundled_skill_paths",
16
+ "fences_hint",
17
+ "resolve_skill_paths",
18
+ "skills_hint",
19
+ ]