control-tower 6.0.0__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 (446) hide show
  1. control_tower/__init__.py +3 -0
  2. control_tower/backends/__init__.py +31 -0
  3. control_tower/backends/fleet/__init__.py +1 -0
  4. control_tower/backends/fleet/ami.py +45 -0
  5. control_tower/backends/fleet/aws_identity.py +9 -0
  6. control_tower/backends/fleet/bootstrap.sh +349 -0
  7. control_tower/backends/fleet/config.py +40 -0
  8. control_tower/backends/fleet/controller.py +2058 -0
  9. control_tower/backends/fleet/job.py +325 -0
  10. control_tower/backends/fleet/run.py +399 -0
  11. control_tower/backends/fleet/run_job.py +270 -0
  12. control_tower/backends/fleet/worker.py +1045 -0
  13. control_tower/backends/fleet/worker_cli.py +68 -0
  14. control_tower/backends/hawk/__init__.py +7 -0
  15. control_tower/backends/hawk/config.py +114 -0
  16. control_tower/backends/hawk/download.py +93 -0
  17. control_tower/backends/hawk/middleman.py +64 -0
  18. control_tower/backends/hawk/model_closure.py +39 -0
  19. control_tower/backends/hawk/preflight.py +142 -0
  20. control_tower/backends/hawk/pull_envs.py +160 -0
  21. control_tower/backends/hawk/runner.py +1050 -0
  22. control_tower/backends/hawk/sandbox.py +561 -0
  23. control_tower/backends/hawk/yaml_generator.py +206 -0
  24. control_tower/backends/local.py +223 -0
  25. control_tower/cli/__init__.py +3 -0
  26. control_tower/cli/base.py +207 -0
  27. control_tower/cli/cloud.py +132 -0
  28. control_tower/cli/completions/completion_callbacks.py +125 -0
  29. control_tower/cli/completions/completions_build.py +59 -0
  30. control_tower/cli/completions/completions_hooks.py +209 -0
  31. control_tower/cli/extras.py +89 -0
  32. control_tower/cli/login.py +77 -0
  33. control_tower/cli/main.py +51 -0
  34. control_tower/cli/mcp_meta.py +30 -0
  35. control_tower/cli/protocols.py +140 -0
  36. control_tower/cli/run.py +893 -0
  37. control_tower/cli/task_sets.py +123 -0
  38. control_tower/datasets/__init__.py +0 -0
  39. control_tower/datasets/check.py +88 -0
  40. control_tower/datasets/cli.py +599 -0
  41. control_tower/datasets/external.py +675 -0
  42. control_tower/datasets/importers/__init__.py +47 -0
  43. control_tower/datasets/importers/eval_log.py +596 -0
  44. control_tower/datasets/importers/monitoring_bench.py +766 -0
  45. control_tower/datasets/importers/mythos_5_incident.py +241 -0
  46. control_tower/datasets/importers/sessions.py +159 -0
  47. control_tower/datasets/importers/shade_arena.py +588 -0
  48. control_tower/datasets/importers/sleight_bench.py +487 -0
  49. control_tower/datasets/model.py +45 -0
  50. control_tower/environments/build.py +191 -0
  51. control_tower/environments/env_args.py +98 -0
  52. control_tower/environments/environment.py +92 -0
  53. control_tower/environments/internet_simulator/.dockerignore +3 -0
  54. control_tower/environments/internet_simulator/Dockerfile +89 -0
  55. control_tower/environments/internet_simulator/__init__.py +0 -0
  56. control_tower/environments/internet_simulator/__main__.py +4 -0
  57. control_tower/environments/internet_simulator/compose.yml +31 -0
  58. control_tower/environments/internet_simulator/constants.py +53 -0
  59. control_tower/environments/internet_simulator/core/assets.py +304 -0
  60. control_tower/environments/internet_simulator/core/exposed_services.py +71 -0
  61. control_tower/environments/internet_simulator/core/generate_ca.py +68 -0
  62. control_tower/environments/internet_simulator/core/logs.py +212 -0
  63. control_tower/environments/internet_simulator/core/network_mapping.py +63 -0
  64. control_tower/environments/internet_simulator/core/tls.py +124 -0
  65. control_tower/environments/internet_simulator/core/trajectory.py +17 -0
  66. control_tower/environments/internet_simulator/core/webhook.py +195 -0
  67. control_tower/environments/internet_simulator/malware/malware.c +143 -0
  68. control_tower/environments/internet_simulator/modules/forward_proxy.py +431 -0
  69. control_tower/environments/internet_simulator/modules/provider_policy.py +390 -0
  70. control_tower/environments/internet_simulator/modules/response_rules.py +193 -0
  71. control_tower/environments/internet_simulator/modules/shell_sessions.py +206 -0
  72. control_tower/environments/internet_simulator/modules/upstream_proxy.py +289 -0
  73. control_tower/environments/internet_simulator/modules/upstream_specs.py +221 -0
  74. control_tower/environments/internet_simulator/personas.py +150 -0
  75. control_tower/environments/internet_simulator/requirements.txt +3 -0
  76. control_tower/environments/internet_simulator/resolve_override.c +75 -0
  77. control_tower/environments/internet_simulator/runtimes/__init__.py +0 -0
  78. control_tower/environments/internet_simulator/runtimes/docker.py +630 -0
  79. control_tower/environments/internet_simulator/runtimes/hawk.py +193 -0
  80. control_tower/environments/internet_simulator/runtimes/provisioning.py +39 -0
  81. control_tower/environments/internet_simulator/server.py +660 -0
  82. control_tower/environments/internet_simulator/source_hash.py +78 -0
  83. control_tower/environments/list_utils.py +482 -0
  84. control_tower/environments/registry.py +275 -0
  85. control_tower/environments/remote/__init__.py +0 -0
  86. control_tower/environments/remote/process.py +248 -0
  87. control_tower/environments/remote/remote.py +407 -0
  88. control_tower/environments/remote/sample.py +244 -0
  89. control_tower/environments/setting_runtime.py +176 -0
  90. control_tower/environments/settings.py +1170 -0
  91. control_tower/environments/settings.yml +26 -0
  92. control_tower/environments/settings_cli.py +610 -0
  93. control_tower/environments/validate.py +116 -0
  94. control_tower/eval/README.md +38 -0
  95. control_tower/eval/__init__.py +1 -0
  96. control_tower/eval/game/__init__.py +11 -0
  97. control_tower/eval/game/blue.py +570 -0
  98. control_tower/eval/game/harness.py +146 -0
  99. control_tower/eval/game/history.py +221 -0
  100. control_tower/eval/game/messages.py +75 -0
  101. control_tower/eval/game/red.py +346 -0
  102. control_tower/eval/game/registry.py +29 -0
  103. control_tower/eval/game/solver.py +214 -0
  104. control_tower/eval/game/view.py +114 -0
  105. control_tower/eval/game/white.py +37 -0
  106. control_tower/eval/launch/__init__.py +1 -0
  107. control_tower/eval/launch/assemble.py +416 -0
  108. control_tower/eval/launch/dataset.py +611 -0
  109. control_tower/eval/launch/dispatch.py +142 -0
  110. control_tower/eval/launch/inspect_tasks.py +248 -0
  111. control_tower/eval/launch/metadata.py +39 -0
  112. control_tower/eval/launch/naming.py +37 -0
  113. control_tower/eval/launch/options.py +40 -0
  114. control_tower/eval/launch/preflight.py +90 -0
  115. control_tower/eval/launch/selection.py +151 -0
  116. control_tower/eval/log/README.md +76 -0
  117. control_tower/eval/log/__init__.py +0 -0
  118. control_tower/eval/log/action.py +72 -0
  119. control_tower/eval/log/blue_calls.py +93 -0
  120. control_tower/eval/log/convert.py +1773 -0
  121. control_tower/eval/log/eval_trace_metadata.py +15 -0
  122. control_tower/eval/log/extended_thinking.py +56 -0
  123. control_tower/eval/log/format_history.py +35 -0
  124. control_tower/eval/log/from_eval_log.py +141 -0
  125. control_tower/eval/log/merge.py +87 -0
  126. control_tower/eval/log/monitor_scores.py +112 -0
  127. control_tower/eval/log/sample.py +403 -0
  128. control_tower/eval/log/task_args.py +23 -0
  129. control_tower/eval/log/task_scores.py +110 -0
  130. control_tower/eval/log/tool_calls.py +40 -0
  131. control_tower/eval/log/translation.py +342 -0
  132. control_tower/eval/log/utils.py +85 -0
  133. control_tower/eval/log/workspace_diff.py +16 -0
  134. control_tower/eval/monitoring/__init__.py +1 -0
  135. control_tower/eval/monitoring/posthoc.py +339 -0
  136. control_tower/eval/monitoring/scorer.py +409 -0
  137. control_tower/extras/__init__.py +0 -0
  138. control_tower/extras/claude_host.py +26 -0
  139. control_tower/extras/dashboard.py +269 -0
  140. control_tower/extras/dashboard_cli.py +19 -0
  141. control_tower/extras/env_description/__init__.py +0 -0
  142. control_tower/extras/env_description/audit/README.md +80 -0
  143. control_tower/extras/env_description/audit/__init__.py +0 -0
  144. control_tower/extras/env_description/audit/cli.py +81 -0
  145. control_tower/extras/env_description/audit/models.py +63 -0
  146. control_tower/extras/env_description/audit/prompts/system.md +68 -0
  147. control_tower/extras/env_description/audit/prompts/user.md +30 -0
  148. control_tower/extras/env_description/audit/task.py +278 -0
  149. control_tower/extras/env_description/generate/README.md +75 -0
  150. control_tower/extras/env_description/generate/cli.py +146 -0
  151. control_tower/extras/env_description/generate/prompts/system.md +62 -0
  152. control_tower/extras/env_description/generate/prompts/user.md +21 -0
  153. control_tower/extras/env_description/generate/task.py +359 -0
  154. control_tower/extras/env_description/shared/concurrency.py +72 -0
  155. control_tower/extras/env_description/shared/spec.md +79 -0
  156. control_tower/extras/env_description/shared/spec_loader.py +24 -0
  157. control_tower/extras/env_source.py +230 -0
  158. control_tower/extras/integration_test_quality/README.md +59 -0
  159. control_tower/extras/integration_test_quality/cli.py +115 -0
  160. control_tower/extras/integration_test_quality/prompts/analyze.md +44 -0
  161. control_tower/extras/integration_test_quality/prompts/check_tasks.md +29 -0
  162. control_tower/extras/integration_test_quality/prompts/coverage.md +27 -0
  163. control_tower/extras/integration_test_quality/prompts/fix.md +14 -0
  164. control_tower/extras/integration_test_quality/task.py +179 -0
  165. control_tower/extras/pairing_analysis/README.md +30 -0
  166. control_tower/extras/pairing_analysis/cli.py +82 -0
  167. control_tower/extras/pairing_analysis/prompts/pairing.md +43 -0
  168. control_tower/extras/pairing_analysis/task.py +367 -0
  169. control_tower/extras/prompts.py +28 -0
  170. control_tower/extras/scorer_idempotency/base.py +216 -0
  171. control_tower/extras/scorer_idempotency/checks/_json.py +31 -0
  172. control_tower/extras/scorer_idempotency/checks/chroma.py +285 -0
  173. control_tower/extras/scorer_idempotency/checks/docker.py +366 -0
  174. control_tower/extras/scorer_idempotency/checks/elasticsearch.py +293 -0
  175. control_tower/extras/scorer_idempotency/checks/filesystem.py +155 -0
  176. control_tower/extras/scorer_idempotency/checks/generated_manifest.json +13 -0
  177. control_tower/extras/scorer_idempotency/checks/influxdb.py +549 -0
  178. control_tower/extras/scorer_idempotency/checks/listener.py +81 -0
  179. control_tower/extras/scorer_idempotency/checks/minio.py +360 -0
  180. control_tower/extras/scorer_idempotency/checks/mongo.py +338 -0
  181. control_tower/extras/scorer_idempotency/checks/openldap.py +241 -0
  182. control_tower/extras/scorer_idempotency/checks/postgres.py +317 -0
  183. control_tower/extras/scorer_idempotency/checks/process.py +134 -0
  184. control_tower/extras/scorer_idempotency/checks/rabbitmq.py +369 -0
  185. control_tower/extras/scorer_idempotency/checks/redis.py +291 -0
  186. control_tower/extras/scorer_idempotency/checks/sqlite.py +309 -0
  187. control_tower/extras/scorer_idempotency/checks/sshd.py +270 -0
  188. control_tower/extras/scorer_idempotency/checks/vault.py +376 -0
  189. control_tower/extras/scorer_idempotency/cli.py +575 -0
  190. control_tower/extras/scorer_idempotency/compose_analyzer.py +182 -0
  191. control_tower/extras/scorer_idempotency/detection.py +236 -0
  192. control_tower/extras/scorer_idempotency/dockerfile_analyzer.py +377 -0
  193. control_tower/extras/scorer_idempotency/llm_generator.py +448 -0
  194. control_tower/extras/scorer_idempotency/runner.py +449 -0
  195. control_tower/extras/scorer_idempotency/runtime_prober.py +378 -0
  196. control_tower/extras/side_task_folder.py +115 -0
  197. control_tower/extras/storage.py +74 -0
  198. control_tower/extras/task_analysis/compare_side_tasks/README.md +25 -0
  199. control_tower/extras/task_analysis/compare_side_tasks/cli.py +25 -0
  200. control_tower/extras/task_analysis/compare_side_tasks/task.py +127 -0
  201. control_tower/extras/task_analysis/doc_audit/README.md +77 -0
  202. control_tower/extras/task_analysis/doc_audit/audits/base.py +129 -0
  203. control_tower/extras/task_analysis/doc_audit/audits/goal.py +92 -0
  204. control_tower/extras/task_analysis/doc_audit/audits/sd.py +98 -0
  205. control_tower/extras/task_analysis/doc_audit/cli.py +185 -0
  206. control_tower/extras/task_analysis/doc_audit/heuristics/base.py +53 -0
  207. control_tower/extras/task_analysis/doc_audit/heuristics/goal.py +215 -0
  208. control_tower/extras/task_analysis/doc_audit/heuristics/sd.py +222 -0
  209. control_tower/extras/task_analysis/doc_audit/prompts/goal/system.md +109 -0
  210. control_tower/extras/task_analysis/doc_audit/prompts/goal/user.md +21 -0
  211. control_tower/extras/task_analysis/doc_audit/prompts/sd/system.md +311 -0
  212. control_tower/extras/task_analysis/doc_audit/prompts/sd/user.md +26 -0
  213. control_tower/extras/task_analysis/doc_audit/sd_audit_labels.json +98 -0
  214. control_tower/extras/task_analysis/doc_audit/task.py +200 -0
  215. control_tower/extras/task_analysis/failure_type_audit/cli.py +125 -0
  216. control_tower/extras/task_analysis/failure_type_audit/prompts/system.md +9 -0
  217. control_tower/extras/task_analysis/failure_type_audit/prompts/user.md +23 -0
  218. control_tower/extras/task_analysis/failure_type_audit/task.py +190 -0
  219. control_tower/extras/task_analysis/goal_completeness/README.md +56 -0
  220. control_tower/extras/task_analysis/goal_completeness/cli.py +125 -0
  221. control_tower/extras/task_analysis/goal_completeness/prompts/system.md +44 -0
  222. control_tower/extras/task_analysis/goal_completeness/prompts/user.md +50 -0
  223. control_tower/extras/task_analysis/goal_completeness/task.py +320 -0
  224. control_tower/extras/task_analysis/goal_completeness/viewer.py +142 -0
  225. control_tower/extras/task_analysis/reward_hacking/README.md +46 -0
  226. control_tower/extras/task_analysis/reward_hacking/cli.py +115 -0
  227. control_tower/extras/task_analysis/reward_hacking/prompts/system.md +71 -0
  228. control_tower/extras/task_analysis/reward_hacking/prompts/user.md +62 -0
  229. control_tower/extras/task_analysis/reward_hacking/task.py +360 -0
  230. control_tower/extras/task_analysis/reward_hacking/viewer.py +122 -0
  231. control_tower/extras/task_analysis/rt_alignment/README.md +45 -0
  232. control_tower/extras/task_analysis/rt_alignment/cli.py +119 -0
  233. control_tower/extras/task_analysis/rt_alignment/prompts/system.md +50 -0
  234. control_tower/extras/task_analysis/rt_alignment/prompts/user.md +57 -0
  235. control_tower/extras/task_analysis/rt_alignment/task.py +357 -0
  236. control_tower/extras/task_analysis/rt_alignment/viewer.py +124 -0
  237. control_tower/extras/task_analysis/side_task_review/README.md +43 -0
  238. control_tower/extras/task_analysis/side_task_review/cli.py +87 -0
  239. control_tower/extras/task_analysis/side_task_review/prompts/review.md +30 -0
  240. control_tower/extras/task_analysis/side_task_review/task.py +98 -0
  241. control_tower/extras/task_analysis/validate_cover/README.md +32 -0
  242. control_tower/extras/task_analysis/validate_cover/cli.py +97 -0
  243. control_tower/extras/task_analysis/validate_cover/container_script.py +230 -0
  244. control_tower/extras/task_analysis/validate_cover/full_env_context.py +66 -0
  245. control_tower/extras/task_analysis/validate_cover/task.py +97 -0
  246. control_tower/extras/task_analysis/validate_cover/validate_cover_prompt.md +35 -0
  247. control_tower/extras/timing_report/__init__.py +17 -0
  248. control_tower/extras/timing_report/cli.py +71 -0
  249. control_tower/extras/timing_report/report.py +324 -0
  250. control_tower/extras/traj_analysis/submit_guessability/README.md +49 -0
  251. control_tower/extras/traj_analysis/submit_guessability/cli.py +125 -0
  252. control_tower/extras/traj_analysis/submit_guessability/prompts/system.md +29 -0
  253. control_tower/extras/traj_analysis/submit_guessability/prompts/user.md +70 -0
  254. control_tower/extras/traj_analysis/submit_guessability/task.py +351 -0
  255. control_tower/extras/traj_analysis/submit_guessability/viewer.py +159 -0
  256. control_tower/extras/traj_analysis/submit_guessability_verified/README.md +52 -0
  257. control_tower/extras/traj_analysis/submit_guessability_verified/cli.py +143 -0
  258. control_tower/extras/traj_analysis/submit_guessability_verified/prompts/system.md +36 -0
  259. control_tower/extras/traj_analysis/submit_guessability_verified/prompts/user.md +61 -0
  260. control_tower/extras/traj_analysis/submit_guessability_verified/task.py +673 -0
  261. control_tower/extras/traj_analysis/submit_guessability_verified/viewer.py +186 -0
  262. control_tower/live/__init__.py +0 -0
  263. control_tower/live/agents.py +117 -0
  264. control_tower/live/assets/dev_container_extensions.json +3 -0
  265. control_tower/live/assets/live.gitignore +53 -0
  266. control_tower/live/assets/pyrightconfig.json +4 -0
  267. control_tower/live/assets/settings.json +3 -0
  268. control_tower/live/cli.py +1133 -0
  269. control_tower/live/compose.py +117 -0
  270. control_tower/live/context/print_context.py +98 -0
  271. control_tower/live/context/task_getters.py +65 -0
  272. control_tower/live/context/traj_dir.py +334 -0
  273. control_tower/live/integration_tests.py +128 -0
  274. control_tower/live/sandbox_ops.py +134 -0
  275. control_tower/live/score.py +85 -0
  276. control_tower/live/session.py +513 -0
  277. control_tower/live/setup.py +294 -0
  278. control_tower/live/task_state.py +145 -0
  279. control_tower/live/traj/__init__.py +0 -0
  280. control_tower/live/traj/gen_traj.py +319 -0
  281. control_tower/live/traj/git_status.py +33 -0
  282. control_tower/live/traj/git_traj.py +735 -0
  283. control_tower/live/traj/git_traj_helpers/FORMAT.md +25 -0
  284. control_tower/live/traj/git_traj_helpers/call_insert.py +15 -0
  285. control_tower/live/traj/git_traj_helpers/call_str_replace.py +20 -0
  286. control_tower/live/traj/macros.py +100 -0
  287. control_tower/live/traj/migrate_traj.py +80 -0
  288. control_tower/live/traj/patch.py +62 -0
  289. control_tower/live/traj/solution.py +242 -0
  290. control_tower/live/traj/templ/add.py +14 -0
  291. control_tower/live/traj/templ/add_tool_call.py +85 -0
  292. control_tower/live/traj/templ/bash.py +127 -0
  293. control_tower/live/traj/templ/git_utils.py +93 -0
  294. control_tower/live/traj/templ/input_utils.py +145 -0
  295. control_tower/live/traj/templ/template_from.py +118 -0
  296. control_tower/live/traj/templ/tool_call_builder.py +119 -0
  297. control_tower/live/traj/tool_call_utils.py +283 -0
  298. control_tower/live/traj/traj.py +294 -0
  299. control_tower/live/tui/__init__.py +0 -0
  300. control_tower/live/tui/popup.py +293 -0
  301. control_tower/live/tui/tui.py +1237 -0
  302. control_tower/live/tui/tui.tcss +32 -0
  303. control_tower/metrics/__init__.py +0 -0
  304. control_tower/metrics/audit.py +357 -0
  305. control_tower/metrics/control.py +120 -0
  306. control_tower/models/__init__.py +3 -0
  307. control_tower/models/aliases.py +59 -0
  308. control_tower/models/cache_scopes.py +26 -0
  309. control_tower/models/inspect_rates.py +32 -0
  310. control_tower/models/openrouter_provider.py +65 -0
  311. control_tower/models/pricing.py +183 -0
  312. control_tower/models/rates.py +240 -0
  313. control_tower/models/role_model.py +179 -0
  314. control_tower/models/usage_hooks.py +14 -0
  315. control_tower/monitors/__init__.py +40 -0
  316. control_tower/monitors/_monitor.py +722 -0
  317. control_tower/monitors/checklist.py +95 -0
  318. control_tower/monitors/jev.py +217 -0
  319. control_tower/monitors/jev_questions.yml +32 -0
  320. control_tower/monitors/presets.yml +29 -0
  321. control_tower/monitors/prompts/action-goals.md +78 -0
  322. control_tower/monitors/prompts/action.md +70 -0
  323. control_tower/monitors/prompts/goal-analysis.md +72 -0
  324. control_tower/monitors/prompts/goals.md +76 -0
  325. control_tower/monitors/prompts/no-history.md +37 -0
  326. control_tower/monitors/prompts/off-task.md +86 -0
  327. control_tower/monitors/prompts/quick-goals.md +65 -0
  328. control_tower/monitors/prompts/quick.md +57 -0
  329. control_tower/monitors/prompts/simple.md +19 -0
  330. control_tower/monitors/prompts/strong-goals.md +87 -0
  331. control_tower/monitors/prompts/strong.md +79 -0
  332. control_tower/monitors/prompts/trajectory.md +16 -0
  333. control_tower/monitors/registry.py +18 -0
  334. control_tower/monitors/single_goal.py +67 -0
  335. control_tower/monitors/steps/checklist.md +38 -0
  336. control_tower/monitors/steps/single_goal.md +75 -0
  337. control_tower/monitors/steps/single_goal_analysis.md +71 -0
  338. control_tower/monitors/steps/three_scores.md +107 -0
  339. control_tower/monitors/three_scores.py +68 -0
  340. control_tower/policies/__init__.py +1 -0
  341. control_tower/policies/attack.py +185 -0
  342. control_tower/policies/honest.py +23 -0
  343. control_tower/policies/obvious_attack.py +67 -0
  344. control_tower/policies/prompts/attack.md +7 -0
  345. control_tower/policies/prompts/attack_planning.md +22 -0
  346. control_tower/policies/replay.py +201 -0
  347. control_tower/policies/scripted.py +115 -0
  348. control_tower/protocols/__init__.py +1 -0
  349. control_tower/protocols/defer_to_trusted.py +141 -0
  350. control_tower/protocols/null_blue.py +27 -0
  351. control_tower/protocols/prompts/trusted_editing.md +9 -0
  352. control_tower/protocols/resample.py +191 -0
  353. control_tower/protocols/trusted_editing.py +143 -0
  354. control_tower/protocols/trusted_monitoring.py +90 -0
  355. control_tower/protocols/watcher.py +352 -0
  356. control_tower/py.typed +0 -0
  357. control_tower/runs/README.md +93 -0
  358. control_tower/runs/__init__.py +0 -0
  359. control_tower/runs/convert.py +100 -0
  360. control_tower/runs/convert_eval_log_to_run.py +92 -0
  361. control_tower/runs/eval_log_io.py +200 -0
  362. control_tower/runs/io.py +190 -0
  363. control_tower/runs/models.py +64 -0
  364. control_tower/runs/mongo_docs.py +359 -0
  365. control_tower/runs/runs_cli.py +735 -0
  366. control_tower/runs/s3_upload.py +167 -0
  367. control_tower/runs/source.py +24 -0
  368. control_tower/runs/timing_utils.py +149 -0
  369. control_tower/runs/viewer/__init__.py +0 -0
  370. control_tower/runs/viewer/api.py +464 -0
  371. control_tower/runs/viewer/auth.py +50 -0
  372. control_tower/runs/viewer/io.py +204 -0
  373. control_tower/runs/viewer/models.py +25 -0
  374. control_tower/runs/viewer/upload.py +216 -0
  375. control_tower/runs/viewer/urls.py +501 -0
  376. control_tower/sandbox/__init__.py +6 -0
  377. control_tower/sandbox/_alpine_rootfs.py +230 -0
  378. control_tower/sandbox/_ssh.py +471 -0
  379. control_tower/sandbox/cli.py +236 -0
  380. control_tower/sandbox/constants.py +21 -0
  381. control_tower/sandbox/docker.py +685 -0
  382. control_tower/sandbox/docker_in_vm.py +527 -0
  383. control_tower/sandbox/firecracker.py +363 -0
  384. control_tower/sandbox/inspect_tools.py +46 -0
  385. control_tower/sandbox/qemu.py +251 -0
  386. control_tower/sandbox/replay/__init__.py +23 -0
  387. control_tower/sandbox/replay/environment.py +435 -0
  388. control_tower/sandbox/replay/score.py +37 -0
  389. control_tower/sandbox/simulated/__init__.py +25 -0
  390. control_tower/sandbox/simulated/actions.py +90 -0
  391. control_tower/sandbox/simulated/compose.py +192 -0
  392. control_tower/sandbox/simulated/environment.py +190 -0
  393. control_tower/sandbox/simulated/judge.py +158 -0
  394. control_tower/sandbox/simulated/prompts/judge.md +31 -0
  395. control_tower/sandbox/simulated/prompts/simulator.md +74 -0
  396. control_tower/sandbox/simulated/prompts/simulator_no_exec.md +22 -0
  397. control_tower/sandbox/simulated/scratch/Dockerfile +11 -0
  398. control_tower/sandbox/simulated/scratch/compose.yaml +42 -0
  399. control_tower/sandbox/simulated/session.py +155 -0
  400. control_tower/scanners.py +130 -0
  401. control_tower/task/__init__.py +0 -0
  402. control_tower/task/final_diff.py +88 -0
  403. control_tower/task/sample_metadata.py +46 -0
  404. control_tower/task/scorers.py +327 -0
  405. control_tower/task/select_utils.py +106 -0
  406. control_tower/task/selectors.py +49 -0
  407. control_tower/task/setup.py +115 -0
  408. control_tower/task/space.py +45 -0
  409. control_tower/task/task.py +230 -0
  410. control_tower/task/task_sets/__init__.py +203 -0
  411. control_tower/task/traj_dir.py +39 -0
  412. control_tower/task/workspace_baseline.py +581 -0
  413. control_tower/tools/__init__.py +0 -0
  414. control_tower/tools/answer.py +45 -0
  415. control_tower/tools/bash_tools.py +37 -0
  416. control_tower/tools/declarations.py +23 -0
  417. control_tower/tools/diff_display_text_editor.py +66 -0
  418. control_tower/tools/registry.py +34 -0
  419. control_tower/tools/split_text_editor_tools.py +125 -0
  420. control_tower/tools/task_tools.py +204 -0
  421. control_tower/tools/text_editor_output.py +53 -0
  422. control_tower/tools/tool_calls.py +44 -0
  423. control_tower/tools/tool_registry.py +363 -0
  424. control_tower/utils/__init__.py +0 -0
  425. control_tower/utils/cli_args.py +20 -0
  426. control_tower/utils/direct_api_generator.py +124 -0
  427. control_tower/utils/docs_utils.py +18 -0
  428. control_tower/utils/env.py +191 -0
  429. control_tower/utils/file_patterns.py +89 -0
  430. control_tower/utils/fs.py +137 -0
  431. control_tower/utils/function_registry/__init__.py +0 -0
  432. control_tower/utils/function_registry/base_model.py +30 -0
  433. control_tower/utils/function_registry/registry.py +535 -0
  434. control_tower/utils/function_registry/selection.py +68 -0
  435. control_tower/utils/paths.py +91 -0
  436. control_tower/utils/sanitize.py +29 -0
  437. control_tower/utils/suggestions.py +25 -0
  438. control_tower/utils/time.py +8 -0
  439. control_tower/utils/transcript.py +20 -0
  440. control_tower/utils/versioning.py +327 -0
  441. control_tower-6.0.0.dist-info/METADATA +91 -0
  442. control_tower-6.0.0.dist-info/RECORD +446 -0
  443. control_tower-6.0.0.dist-info/WHEEL +5 -0
  444. control_tower-6.0.0.dist-info/entry_points.txt +5 -0
  445. control_tower-6.0.0.dist-info/licenses/LICENSE +21 -0
  446. control_tower-6.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,3 @@
1
+ from importlib.metadata import version
2
+
3
+ __version__ = version("control-tower")
@@ -0,0 +1,31 @@
1
+ """Where an eval runs: in this process, on the EC2 fleet, or on Hawk."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ if TYPE_CHECKING:
7
+ from inspect_ai.log import EvalLog
8
+
9
+ from control_tower.eval.launch.options import EvalCliInputs
10
+
11
+
12
+ class Backend(ABC):
13
+ """Where `run_task` runs an eval, given its completed task args."""
14
+
15
+ def resolvable(self, name: str) -> bool:
16
+ """Whether the task set, policy, protocol or monitor ``name`` is looked up before the run; the rest are looked
17
+ up where the task is built."""
18
+ return True
19
+
20
+ @abstractmethod
21
+ def run(
22
+ self,
23
+ task_name: str,
24
+ task_args: dict[str, Any],
25
+ eval_inputs: "EvalCliInputs",
26
+ *,
27
+ upload: bool,
28
+ metadata: dict[str, Any],
29
+ ) -> "EvalLog | None":
30
+ """Run ``task_name`` with ``task_args``, ``metadata`` joining its eval metadata, and upload the log when
31
+ ``upload``; return it, or None when it stays where it ran."""
@@ -0,0 +1 @@
1
+ """Fleet mode: distributed evaluation on EC2 workers."""
@@ -0,0 +1,45 @@
1
+ """Worker AMI resolution for fleet mode: the pre-baked AMI named in SSM ``/linuxbench/worker-ami-id``."""
2
+
3
+ import boto3
4
+ import botocore.exceptions
5
+
6
+ SSM_AMI_PARAM = "/linuxbench/worker-ami-id"
7
+
8
+
9
+ def get_worker_ami_id(region: str = "us-east-1", requested: str | None = None) -> str:
10
+ """Return the AMI to use for fleet workers, refusing one that cannot boot now.
11
+
12
+ ``requested`` is an AMI id, or the SSM parameter naming one; the default is ``SSM_AMI_PARAM``.
13
+ """
14
+ source = requested or SSM_AMI_PARAM
15
+ ami_id = source
16
+ if source.startswith("/"):
17
+ ssm = boto3.client("ssm", region_name=region)
18
+ try:
19
+ ami_id = ssm.get_parameter(Name=source)["Parameter"]["Value"]
20
+ except ssm.exceptions.ParameterNotFound as e:
21
+ raise RuntimeError(
22
+ f"No worker AMI: SSM parameter {source} does not exist in {region}. "
23
+ f"Build one with scripts/build_worker_ami.sh --region {region}."
24
+ ) from e
25
+ label = ami_id if ami_id == source else f"{ami_id} (from {source})"
26
+ ec2 = boto3.client("ec2", region_name=region)
27
+ try:
28
+ images = ec2.describe_images(ImageIds=[ami_id])["Images"]
29
+ except botocore.exceptions.ClientError as e:
30
+ if not e.response["Error"]["Code"].startswith("InvalidAMIID"):
31
+ raise
32
+ raise RuntimeError(
33
+ f"Worker AMI {label} not found in {region}: "
34
+ f"{e.response['Error']['Message']}"
35
+ ) from e
36
+ if not images:
37
+ raise RuntimeError(f"Worker AMI {label} not found in {region}.")
38
+ image = images[0]
39
+ if image["State"] != "available":
40
+ reason = image.get("StateReason", {}).get("Message", "")
41
+ raise RuntimeError(
42
+ f"Worker AMI {label} in {region} is {image['State']}"
43
+ f"{f' ({reason})' if reason else ''}, not available; no workers launched."
44
+ )
45
+ return ami_id
@@ -0,0 +1,9 @@
1
+ """AWS caller identity lookup for resource tagging."""
2
+
3
+ import boto3
4
+
5
+
6
+ def caller_identity_name(region: str) -> str:
7
+ """Get the caller's identity name (IAM username or SSO session name) for resource tagging."""
8
+ sts = boto3.client("sts", region_name=region)
9
+ return sts.get_caller_identity()["Arn"].rsplit("/", 1)[-1]
@@ -0,0 +1,349 @@
1
+ #!/bin/bash
2
+ # Fleet worker bootstrap script — runs as EC2 user-data (root).
3
+ #
4
+ # Template variables (replaced by controller at launch time):
5
+ # {s3_bucket} — S3 bucket for fleet results and code artifact
6
+ # {s3_prefix} — S3 prefix for this fleet run (fleet_id/)
7
+ # {code_s3_key} — S3 key of the code tarball
8
+ # {rates_s3_key} — S3 key of the driver's rate table (empty when pricing is off)
9
+ # {worker_setup_s3_key} — S3 key of the run's worker setup script (empty when none)
10
+ # {region} — AWS region
11
+ # {work_queue_url} — SQS work queue URL
12
+ # {done_queue_url} — SQS done queue URL
13
+ # {concurrency} — Concurrent eval tasks per worker
14
+ # {idle_timeout} — Seconds idle before worker self-terminates
15
+ # {fleet_secrets_prefix} — SSM path holding this fleet's per-instance
16
+ # SecureStrings (JSON of API keys, GH_TOKEN, Docker
17
+ # auth, env-required vars), one per instance id
18
+ # {setting_archives} — space-separated `<s3 key>=<dir>` pairs: the controller's
19
+ # setting checkouts, one archive of each repo's files and
20
+ # one of its shallow .git, each extracted into
21
+ # /opt/control-tower/<dir>
22
+ # {env_secret_packages} — space-separated setting packages whose `<setting>.env` text
23
+ # sits in "<instance secrets param>.env.<package>"
24
+ # {docker_org_name_ssm_param} — shared SSM param for Docker org name
25
+ # {docker_org_pat_ssm_param} — shared SSM param for Docker PAT
26
+ #
27
+ # What the pre-baked AMI provides (see scripts/build_worker_ami.sh):
28
+ # - Docker + compose plugin, with the address pool below already in daemon.json
29
+ # - NVIDIA driver + container toolkit, which a GPU worker needs to pass its GPU check
30
+ # - uv installed for both ubuntu and root users
31
+ # - Pre-pulled Docker images for all environments and shared services
32
+ # - Python wheel cache at /opt/uv-cache
33
+
34
+ set -euo pipefail
35
+ export HOME=/root
36
+ exec > /var/log/lb-worker-bootstrap.log 2>&1
37
+
38
+ # Upload bootstrap log to S3 on exit (success or failure) for debugging
39
+ IMDS_TOKEN=$(curl -sX PUT http://169.254.169.254/latest/api/token -H "X-aws-ec2-metadata-token-ttl-seconds: 21600" || true)
40
+ INSTANCE_ID=$(curl -s -H "X-aws-ec2-metadata-token: $IMDS_TOKEN" http://169.254.169.254/latest/meta-data/instance-id || echo "unknown")
41
+ on_exit() {{
42
+ local rc=$?
43
+ aws s3 cp /var/log/lb-worker-bootstrap.log \
44
+ 's3://{s3_bucket}/{s3_prefix}logs/'$INSTANCE_ID'-bootstrap.log' \
45
+ --region {region} || true
46
+ if [ "$rc" -ne 0 ]; then
47
+ detail=$(tail -n 5 /var/log/lb-worker-bootstrap.log | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))')
48
+ aws sqs send-message --queue-url '{done_queue_url}' --region {region} \
49
+ --message-body '{{"type": "bootstrap_failed", "instance_id": "'"$INSTANCE_ID"'", "detail": '"$detail"'}}' || true
50
+ shutdown -h now
51
+ fi
52
+ }}
53
+ trap on_exit EXIT
54
+
55
+ SECONDS=0
56
+ echo "=== Control Tower Fleet Worker Bootstrap ==="
57
+ echo "Instance: $INSTANCE_ID"
58
+ echo "Started at $(date)"
59
+
60
+ resolve_ssm_param() {{
61
+ local param_name="$1"
62
+ local with_decryption="${{2:-false}}"
63
+ if [ -z "$param_name" ]; then
64
+ return 0
65
+ fi
66
+ if [ "$with_decryption" = "true" ]; then
67
+ aws ssm get-parameter \
68
+ --name "$param_name" \
69
+ --with-decryption \
70
+ --query 'Parameter.Value' \
71
+ --output text \
72
+ --region {region} || true
73
+ else
74
+ aws ssm get-parameter \
75
+ --name "$param_name" \
76
+ --query 'Parameter.Value' \
77
+ --output text \
78
+ --region {region} || true
79
+ fi
80
+ }}
81
+
82
+ configure_docker_auth() {{
83
+ local org_name="${{DOCKERHUB_USERNAME:-}}"
84
+ local org_pat="${{DOCKERHUB_TOKEN:-}}"
85
+ local org_name_param='{docker_org_name_ssm_param}'
86
+ local org_pat_param='{docker_org_pat_ssm_param}'
87
+
88
+ if [ -z "$org_name" ]; then
89
+ org_name=$(resolve_ssm_param "$org_name_param")
90
+ fi
91
+ if [ -z "$org_pat" ]; then
92
+ org_pat=$(resolve_ssm_param "$org_pat_param" true)
93
+ fi
94
+
95
+ if [ -z "$org_name" ] && [ -z "$org_pat" ]; then
96
+ echo "FATAL: No Docker registry auth configured (need DOCKERHUB_USERNAME/DOCKERHUB_TOKEN in the launching shell or the shared SSM params)"
97
+ exit 1
98
+ fi
99
+ if [ -z "$org_name" ] || [ -z "$org_pat" ]; then
100
+ echo "FATAL: Incomplete Docker registry auth configuration"
101
+ exit 1
102
+ fi
103
+
104
+ if ! printf '%s\n' "$org_pat" | docker login -u "$org_name" --password-stdin >/dev/null; then
105
+ echo "FATAL: Docker registry login failed for $org_name"
106
+ exit 1
107
+ fi
108
+ unset org_pat
109
+ echo " [+${{SECONDS}}s] Docker registry login configured for $org_name"
110
+ }}
111
+
112
+ # --- inotify limit ------------------------------------------------------
113
+ # The kernel's 128 inotify instances per user are shared by every container; file-watching dev servers exhaust them at fleet concurrency.
114
+ sysctl -w fs.inotify.max_user_instances=8192 >/dev/null
115
+ echo " [+${{SECONDS}}s] inotify max_user_instances=$(cat /proc/sys/fs/inotify/max_user_instances)"
116
+
117
+ # --- Docker daemon config: the AMI's address pool -----------------------
118
+ # Docker's ~30-network default pool exhausts at concurrency; 10.224/11 as /24s gives 8192 networks clear of the VPC's 172.31/16.
119
+ if ! python3 -c '
120
+ import json, sys
121
+ config = json.load(open("/etc/docker/daemon.json"))
122
+ sys.exit(config.get("default-address-pools") != [{{"base": "10.224.0.0/11", "size": 24}}])
123
+ '; then
124
+ echo "FATAL: /etc/docker/daemon.json lacks the 10.224.0.0/11 /24 default address pool. Fleet workers boot only from the worker AMI built by scripts/build_worker_ami.sh, which bakes it."
125
+ exit 1
126
+ fi
127
+
128
+
129
+ # --- Docker -----------------------------------------------------------
130
+ docker version --format json >/dev/null 2>&1 || {{ echo "FATAL: Docker is not running. Fleet workers boot only from the worker AMI built by scripts/build_worker_ami.sh."; exit 1; }}
131
+ echo " [+${{SECONDS}}s] Docker ready"
132
+
133
+ # --- Secrets (per-instance SSM SecureString; user-data carries the path only) ---
134
+ # The controller writes this parameter right after RunInstances and tags it
135
+ # with this instance's ARN; the instance role reads only parameters tagged
136
+ # with its own ARN, so the fetch is retried until the write and tag land.
137
+ if [ "$INSTANCE_ID" = unknown ]; then
138
+ echo "FATAL: IMDS did not return an instance id, so this instance's secrets parameter cannot be located"
139
+ exit 1
140
+ fi
141
+ FLEET_SECRETS_PARAM='{fleet_secrets_prefix}'/"$INSTANCE_ID"
142
+ SECRETS_DEADLINE=$((SECONDS + 900))
143
+ SECRETS_NEXT_LOG=$SECONDS
144
+ until secrets=$(AWS_RETRY_MODE=standard AWS_MAX_ATTEMPTS=10 aws ssm get-parameter \
145
+ --name "$FLEET_SECRETS_PARAM" --with-decryption \
146
+ --query 'Parameter.Value' --output text --region {region} 2>/tmp/secrets-fetch.err); do
147
+ if [ "$SECONDS" -ge "$SECRETS_DEADLINE" ]; then
148
+ echo "FATAL: cannot read $FLEET_SECRETS_PARAM after 15 minutes: $(cat /tmp/secrets-fetch.err)"
149
+ echo " (does the instance role allow ssm:GetParameter where the InstanceArn tag is this instance, and did the controller tag the parameter?)"
150
+ exit 1
151
+ fi
152
+ if [ "$SECONDS" -ge "$SECRETS_NEXT_LOG" ]; then
153
+ echo " [+${{SECONDS}}s] waiting for $FLEET_SECRETS_PARAM: $(tail -n 1 /tmp/secrets-fetch.err)"
154
+ SECRETS_NEXT_LOG=$((SECONDS + 60))
155
+ fi
156
+ sleep 5
157
+ done
158
+ exports=$(printf '%s' "$secrets" | python3 -c '
159
+ import json, shlex, sys
160
+ for name, value in json.load(sys.stdin).items():
161
+ print("export " + name + "=" + shlex.quote(value))
162
+ ')
163
+ eval "$exports"
164
+ unset secrets exports
165
+ echo " [+${{SECONDS}}s] Secrets fetched from SSM"
166
+
167
+ # --- Docker registry auth (early — fail fast before expensive steps) ----
168
+ configure_docker_auth
169
+ unset DOCKERHUB_TOKEN DOCKERHUB_USERNAME
170
+
171
+ PCI_VENDORS=$(cat /sys/bus/pci/devices/*/vendor)
172
+ if grep -qx 0x10de <<< "$PCI_VENDORS"; then
173
+ docker run --rm --gpus all ubuntu:22.04 nvidia-smi -L
174
+ fi
175
+
176
+ export PATH="/root/.local/bin:$PATH"
177
+
178
+ # --- Code download ----------------------------------------------------
179
+ rm -rf /opt/control-tower && mkdir -p /opt/control-tower
180
+ cd /opt/control-tower
181
+ aws s3 cp 's3://{s3_bucket}/{code_s3_key}' code.tar.gz --region {region}
182
+ tar xzf code.tar.gz
183
+ rm code.tar.gz
184
+ echo " [+${{SECONDS}}s] Code downloaded and extracted"
185
+
186
+ # --- SSH known hosts (needed for git+ssh deps in uv sync) ----------------
187
+ mkdir -p ~/.ssh
188
+ ssh-keyscan github.com >> ~/.ssh/known_hosts 2>/dev/null
189
+ chmod 600 ~/.ssh/known_hosts
190
+ echo " [+${{SECONDS}}s] GitHub SSH host key added to known_hosts"
191
+
192
+ # --- GitHub credentials (needed BEFORE uv sync for private git+https deps) ---
193
+ # uv shells out to plain git, which ignores GH_TOKEN — only gh reads it
194
+ # natively. A credential helper scoped to https://github.com supplies it
195
+ # when git asks; deliberately NOT a url.insteadOf rewrite, which would
196
+ # mutate every GitHub URL on the worker.
197
+ git config --global credential.'https://github.com'.helper \
198
+ '!f() {{ echo "username=x-access-token"; echo "password=${{GH_TOKEN}}"; }}; f'
199
+ echo " [+${{SECONDS}}s] GitHub HTTPS credential helper configured"
200
+
201
+ # --- Python deps -------------------------------------------------------
202
+ export UV_CACHE_DIR=/opt/uv-cache
203
+ export UV_PYTHON_INSTALL_DIR=/opt/uv-python
204
+ # git archive strips .git, so setuptools-scm can't detect version. Fake it.
205
+ export SETUPTOOLS_SCM_PRETEND_VERSION=0.0.0
206
+ if [ -f uv.lock ]; then
207
+ uv sync --frozen
208
+ else
209
+ echo " No uv.lock in code artifact; resolving dependencies from pyproject.toml"
210
+ uv sync
211
+ fi
212
+ echo " [+${{SECONDS}}s] Python deps installed"
213
+ # Defeat AMI shadowing: a pre-baked control_tower elsewhere in the worker
214
+ # Python's sys.path can win priority over the bundle's editable install,
215
+ # so the worker silently runs OLDER code even though uv sync reports success.
216
+ # (Verified via sentinel string round-trip: stale install wins by default.)
217
+ # Three defense layers:
218
+ # 1. Re-install editable into the venv explicitly.
219
+ # 2. Purge any other control_tower sitting in the venv's site-packages.
220
+ # 3. Prepend /opt/control-tower/src to PYTHONPATH so the bundle is the FIRST
221
+ # sys.path entry no matter what.
222
+ uv pip install --no-deps -e .
223
+ VENV_SITE=$(uv run python -c "import sysconfig; print(sysconfig.get_paths()['purelib'])")
224
+ echo " [+${{SECONDS}}s] venv site-packages = $VENV_SITE"
225
+ # Remove any non-editable control_tower copy — but only when the bundle
226
+ # ships its own (out-of-tree consumers rely on the pip-installed one).
227
+ if [ -d /opt/control-tower/src/control_tower ] && [ -d "$VENV_SITE/control_tower" ] && [ ! -L "$VENV_SITE/control_tower" ]; then
228
+ echo " [+${{SECONDS}}s] Removing non-editable control_tower from $VENV_SITE"
229
+ rm -rf "$VENV_SITE/control_tower"
230
+ fi
231
+ # On PYTHONPATH, src/control_tower.egg-info shadows the venv's dist-info, and Inspect then registers tasks, scorers and metrics without the control_tower/ prefix.
232
+ rm -rf /opt/control-tower/src/control_tower.egg-info
233
+ export PYTHONPATH="/opt/control-tower/src${{PYTHONPATH:+:$PYTHONPATH}}"
234
+ echo " [+${{SECONDS}}s] PYTHONPATH=$PYTHONPATH"
235
+ # Verify resolution
236
+ RESOLVED=$(uv run python -c "import control_tower; print(control_tower.__file__)")
237
+ echo " [+${{SECONDS}}s] control_tower resolves to: $RESOLVED"
238
+
239
+ export CT_FLEET_WORKER_USER='{worker_user}'
240
+ if [ "$CT_FLEET_WORKER_USER" != root ]; then
241
+ worker_home="$(getent passwd "$CT_FLEET_WORKER_USER" | cut -d: -f6)"
242
+ install -d -o "$CT_FLEET_WORKER_USER" -m 700 "$worker_home/.docker"
243
+ install -o "$CT_FLEET_WORKER_USER" -m 600 /root/.docker/config.json "$worker_home/.docker/config.json"
244
+ usermod -aG docker "$CT_FLEET_WORKER_USER"
245
+ runuser -u "$CT_FLEET_WORKER_USER" -- env HOME="$worker_home" docker info >/dev/null || {{ echo "FATAL: $CT_FLEET_WORKER_USER cannot reach the Docker daemon"; exit 1; }}
246
+ fi
247
+
248
+ # --- Settings: the controller's checkouts of each shipped repo ---
249
+ SETTING_ARCHIVES='{setting_archives}'
250
+ for archive in $SETTING_ARCHIVES; do
251
+ mkdir -p "/opt/control-tower/${{archive#*=}}"
252
+ aws s3 cp "s3://{s3_bucket}/${{archive%%=*}}" - --region {region} \
253
+ | tar xz -C "/opt/control-tower/${{archive#*=}}"
254
+ done
255
+ echo " [+${{SECONDS}}s] Settings extracted: $(ls .settings 2>/dev/null | tr '\n' ' ')"
256
+
257
+ # --- Setting values: each shipped setting's `<setting>.env`, beside its checkout as on the controller ---
258
+ ENV_SECRET_PACKAGES='{env_secret_packages}'
259
+ for package in $ENV_SECRET_PACKAGES; do
260
+ attempt=0
261
+ until package_values=$(AWS_RETRY_MODE=standard AWS_MAX_ATTEMPTS=10 aws ssm get-parameter \
262
+ --name "$FLEET_SECRETS_PARAM.env.$package" --with-decryption \
263
+ --query 'Parameter.Value' --output text --region {region} 2>/tmp/secrets-fetch.err); do
264
+ attempt=$((attempt + 1))
265
+ if [ "$attempt" -ge 5 ]; then
266
+ echo "FATAL: cannot read $FLEET_SECRETS_PARAM.env.$package: $(cat /tmp/secrets-fetch.err)"
267
+ exit 1
268
+ fi
269
+ sleep $((attempt * 5))
270
+ done
271
+ install -o "$CT_FLEET_WORKER_USER" -m 600 /dev/null ".settings/$package.env"
272
+ printf '%s\n' "$package_values" > ".settings/$package.env"
273
+ done
274
+ unset package_values
275
+
276
+ # --- Setting package venvs: the env processes CT spawns run from them ---
277
+ for package_dir in .settings/*/; do
278
+ [ -f "$package_dir/pyproject.toml" ] || continue
279
+ sync_flags=()
280
+ [ -f "$package_dir/uv.lock" ] && sync_flags=(--frozen)
281
+ if ! uv sync --project "$package_dir" --no-dev "${{sync_flags[@]}}" 2>&1; then
282
+ echo " [+${{SECONDS}}s] FATAL: uv sync failed for setting package $package_dir."
283
+ echo " Workers cannot start its env process without its venv; check the"
284
+ echo " package's pyproject.toml and uv.lock as shipped."
285
+ exit 1
286
+ fi
287
+ echo " [+${{SECONDS}}s] Setting package venv ready: $package_dir"
288
+ done
289
+
290
+ # --- Setting plugins: each shipped setting's ct_plugin/ joins CT's venv, as `ct settings pull` does ---
291
+ for plugin_dir in .settings/*/ct_plugin/; do
292
+ [ -f "$plugin_dir/pyproject.toml" ] || continue
293
+ if ! uv pip install --python .venv/bin/python -e "$plugin_dir" 2>&1; then
294
+ echo " [+${{SECONDS}}s] FATAL: installing setting plugin $plugin_dir failed."
295
+ exit 1
296
+ fi
297
+ echo " [+${{SECONDS}}s] Setting plugin installed: $plugin_dir"
298
+ done
299
+
300
+ # --- Worker setup (--ec2-arg worker_setup): e.g. an external sandbox's host runtime ---
301
+ # It sees the venv at /opt/control-tower/.venv; /opt/control-tower-worker-setup.env is sourced after it.
302
+ WORKER_SETUP_S3_KEY='{worker_setup_s3_key}'
303
+ if [ -n "$WORKER_SETUP_S3_KEY" ]; then
304
+ aws s3 cp "s3://{s3_bucket}/$WORKER_SETUP_S3_KEY" /opt/control-tower-worker-setup.sh --region {region}
305
+ echo " [+${{SECONDS}}s] Running worker setup..."
306
+ if ! bash -e /opt/control-tower-worker-setup.sh; then
307
+ echo " [+${{SECONDS}}s] FATAL: worker setup failed"
308
+ exit 1
309
+ fi
310
+ if [ -f /opt/control-tower-worker-setup.env ]; then
311
+ # shellcheck disable=SC1091
312
+ source /opt/control-tower-worker-setup.env
313
+ fi
314
+ echo " [+${{SECONDS}}s] Worker setup finished"
315
+ fi
316
+
317
+
318
+ # --- Model rates: the driver's table, so workers price as the driver does ---
319
+ RATES_S3_KEY='{rates_s3_key}'
320
+ if [ -n "$RATES_S3_KEY" ]; then
321
+ aws s3 cp "s3://{s3_bucket}/$RATES_S3_KEY" /opt/control-tower/model_rates.json --region {region}
322
+ export CONTROL_TOWER_MODEL_RATES=/opt/control-tower/model_rates.json
323
+ else
324
+ export CONTROL_TOWER_PRICING=off
325
+ fi
326
+
327
+ # --- Worker -------------------------------------------------------------
328
+ echo "=== Starting worker at +${{SECONDS}}s ==="
329
+ export CONTROL_TOWER_IMAGES=pull
330
+ export INSPECT_LOG_LEVEL='{inspect_log_level}'
331
+ if [ "$CT_FLEET_WORKER_USER" != root ]; then
332
+ mkdir -p /opt/uv-python
333
+ chown -R "$CT_FLEET_WORKER_USER": /opt/control-tower /opt/uv-cache /opt/uv-python
334
+ if [ ! "$(command -v uv)" -ef /usr/local/bin/uv ]; then
335
+ install -m 755 "$(command -v uv)" /usr/local/bin/uv
336
+ fi
337
+ echo "$CT_FLEET_WORKER_USER ALL=(ALL) NOPASSWD: /usr/sbin/shutdown" > /etc/sudoers.d/fleet-worker
338
+ chmod 440 /etc/sudoers.d/fleet-worker
339
+ fi
340
+ runuser -u "$CT_FLEET_WORKER_USER" -- env HOME="$(getent passwd "$CT_FLEET_WORKER_USER" | cut -d: -f6)" PATH="$PATH" \
341
+ uv run python -m control_tower.backends.fleet.worker_cli \
342
+ --queue-url '{work_queue_url}' \
343
+ --done-queue-url '{done_queue_url}' \
344
+ --s3-bucket '{s3_bucket}' \
345
+ --concurrency {concurrency} \
346
+ --idle-timeout {idle_timeout} \
347
+ --region {region}
348
+
349
+ echo "=== Worker finished at $(date) ==="
@@ -0,0 +1,40 @@
1
+ """Fleet run options."""
2
+
3
+ from typing import ClassVar
4
+
5
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
6
+
7
+
8
+ class Ec2Config(BaseModel):
9
+ """The fleet ``--ec2`` launches.
10
+
11
+ Every field is settable as ``--ec2-arg key=value``, so a new knob is a field here and never a
12
+ new CLI flag.
13
+ """
14
+
15
+ model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid")
16
+
17
+ instance_type: str = "c7i.4xlarge"
18
+ instance_type_fallbacks: list[str] = []
19
+ region: str = "us-east-1"
20
+ max_workers: int = 24
21
+ # Workers start at 10 and adapt within [1, max] from observed rate-limit pressure.
22
+ concurrency_per_worker: int = Field(default=32, ge=1)
23
+ # Print the cost estimate and exit without launching.
24
+ estimate: bool = False
25
+ # Skip the confirmation wait.
26
+ auto_confirm: bool = False
27
+ # Force a fresh fleet, skipping auto-resume.
28
+ new_fleet: bool = False
29
+ spot: bool = False
30
+ ami: str | None = None
31
+ worker_user: str = "root"
32
+ # Local script each worker runs as root before taking jobs, e.g. to install an external sandbox's host side.
33
+ worker_setup: str | None = None
34
+
35
+ @field_validator("instance_type_fallbacks", mode="before")
36
+ @classmethod
37
+ def _split_instance_type_fallbacks(cls, value: object) -> object:
38
+ if isinstance(value, str):
39
+ return [t.strip() for t in value.split(",") if t.strip()]
40
+ return value