control-tower 6.0.0rc3__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 (473) hide show
  1. control_tower/__init__.py +3 -0
  2. control_tower/cli/__init__.py +3 -0
  3. control_tower/cli/base.py +215 -0
  4. control_tower/cli/cloud.py +132 -0
  5. control_tower/cli/completions/completion_callbacks.py +125 -0
  6. control_tower/cli/completions/completions_build.py +59 -0
  7. control_tower/cli/completions/completions_hooks.py +209 -0
  8. control_tower/cli/ct_cli_group.py +28 -0
  9. control_tower/cli/extras.py +89 -0
  10. control_tower/cli/login.py +77 -0
  11. control_tower/cli/main.py +44 -0
  12. control_tower/cli/mcp_meta.py +30 -0
  13. control_tower/cli/task_sets.py +133 -0
  14. control_tower/ct_runs/__init__.py +0 -0
  15. control_tower/ct_runs/claude_host.py +26 -0
  16. control_tower/ct_runs/dashboard.py +265 -0
  17. control_tower/ct_runs/dashboard_cli.py +19 -0
  18. control_tower/ct_runs/env_description/__init__.py +0 -0
  19. control_tower/ct_runs/env_description/audit/README.md +80 -0
  20. control_tower/ct_runs/env_description/audit/__init__.py +0 -0
  21. control_tower/ct_runs/env_description/audit/cli.py +81 -0
  22. control_tower/ct_runs/env_description/audit/models.py +63 -0
  23. control_tower/ct_runs/env_description/audit/prompts/system.md +68 -0
  24. control_tower/ct_runs/env_description/audit/prompts/user.md +30 -0
  25. control_tower/ct_runs/env_description/audit/task.py +278 -0
  26. control_tower/ct_runs/env_description/generate/README.md +75 -0
  27. control_tower/ct_runs/env_description/generate/cli.py +146 -0
  28. control_tower/ct_runs/env_description/generate/prompts/system.md +62 -0
  29. control_tower/ct_runs/env_description/generate/prompts/user.md +21 -0
  30. control_tower/ct_runs/env_description/generate/task.py +361 -0
  31. control_tower/ct_runs/env_description/shared/concurrency.py +72 -0
  32. control_tower/ct_runs/env_description/shared/spec.md +79 -0
  33. control_tower/ct_runs/env_description/shared/spec_loader.py +24 -0
  34. control_tower/ct_runs/integration_test_quality/README.md +59 -0
  35. control_tower/ct_runs/integration_test_quality/cli.py +115 -0
  36. control_tower/ct_runs/integration_test_quality/prompts/analyze.md +44 -0
  37. control_tower/ct_runs/integration_test_quality/prompts/check_tasks.md +29 -0
  38. control_tower/ct_runs/integration_test_quality/prompts/coverage.md +27 -0
  39. control_tower/ct_runs/integration_test_quality/prompts/fix.md +14 -0
  40. control_tower/ct_runs/integration_test_quality/task.py +160 -0
  41. control_tower/ct_runs/pairing_analysis/README.md +30 -0
  42. control_tower/ct_runs/pairing_analysis/cli.py +82 -0
  43. control_tower/ct_runs/pairing_analysis/prompts/turn1.md +59 -0
  44. control_tower/ct_runs/pairing_analysis/prompts/turn2.md +43 -0
  45. control_tower/ct_runs/pairing_analysis/task.py +390 -0
  46. control_tower/ct_runs/prompts.py +28 -0
  47. control_tower/ct_runs/run_cli.py +16 -0
  48. control_tower/ct_runs/scorer_idempotency/base.py +216 -0
  49. control_tower/ct_runs/scorer_idempotency/checks/_json.py +31 -0
  50. control_tower/ct_runs/scorer_idempotency/checks/chroma.py +285 -0
  51. control_tower/ct_runs/scorer_idempotency/checks/docker.py +366 -0
  52. control_tower/ct_runs/scorer_idempotency/checks/elasticsearch.py +293 -0
  53. control_tower/ct_runs/scorer_idempotency/checks/filesystem.py +155 -0
  54. control_tower/ct_runs/scorer_idempotency/checks/generated_manifest.json +13 -0
  55. control_tower/ct_runs/scorer_idempotency/checks/influxdb.py +549 -0
  56. control_tower/ct_runs/scorer_idempotency/checks/listener.py +82 -0
  57. control_tower/ct_runs/scorer_idempotency/checks/minio.py +360 -0
  58. control_tower/ct_runs/scorer_idempotency/checks/mongo.py +338 -0
  59. control_tower/ct_runs/scorer_idempotency/checks/openldap.py +241 -0
  60. control_tower/ct_runs/scorer_idempotency/checks/postgres.py +317 -0
  61. control_tower/ct_runs/scorer_idempotency/checks/process.py +134 -0
  62. control_tower/ct_runs/scorer_idempotency/checks/rabbitmq.py +369 -0
  63. control_tower/ct_runs/scorer_idempotency/checks/redis.py +291 -0
  64. control_tower/ct_runs/scorer_idempotency/checks/sqlite.py +309 -0
  65. control_tower/ct_runs/scorer_idempotency/checks/sshd.py +270 -0
  66. control_tower/ct_runs/scorer_idempotency/checks/vault.py +376 -0
  67. control_tower/ct_runs/scorer_idempotency/cli.py +560 -0
  68. control_tower/ct_runs/scorer_idempotency/compose_analyzer.py +182 -0
  69. control_tower/ct_runs/scorer_idempotency/detection.py +236 -0
  70. control_tower/ct_runs/scorer_idempotency/dockerfile_analyzer.py +377 -0
  71. control_tower/ct_runs/scorer_idempotency/llm_generator.py +448 -0
  72. control_tower/ct_runs/scorer_idempotency/runner.py +445 -0
  73. control_tower/ct_runs/scorer_idempotency/runtime_prober.py +378 -0
  74. control_tower/ct_runs/storage.py +74 -0
  75. control_tower/ct_runs/task_analysis/compare_side_tasks/README.md +25 -0
  76. control_tower/ct_runs/task_analysis/compare_side_tasks/cli.py +25 -0
  77. control_tower/ct_runs/task_analysis/compare_side_tasks/task.py +126 -0
  78. control_tower/ct_runs/task_analysis/doc_audit/README.md +77 -0
  79. control_tower/ct_runs/task_analysis/doc_audit/audits/base.py +129 -0
  80. control_tower/ct_runs/task_analysis/doc_audit/audits/goal.py +95 -0
  81. control_tower/ct_runs/task_analysis/doc_audit/audits/sd.py +103 -0
  82. control_tower/ct_runs/task_analysis/doc_audit/cli.py +207 -0
  83. control_tower/ct_runs/task_analysis/doc_audit/heuristics/base.py +53 -0
  84. control_tower/ct_runs/task_analysis/doc_audit/heuristics/goal.py +215 -0
  85. control_tower/ct_runs/task_analysis/doc_audit/heuristics/sd.py +222 -0
  86. control_tower/ct_runs/task_analysis/doc_audit/prompts/goal/system.md +109 -0
  87. control_tower/ct_runs/task_analysis/doc_audit/prompts/goal/user.md +21 -0
  88. control_tower/ct_runs/task_analysis/doc_audit/prompts/sd/system.md +311 -0
  89. control_tower/ct_runs/task_analysis/doc_audit/prompts/sd/user.md +26 -0
  90. control_tower/ct_runs/task_analysis/doc_audit/sd_audit_labels.json +98 -0
  91. control_tower/ct_runs/task_analysis/doc_audit/task.py +227 -0
  92. control_tower/ct_runs/task_analysis/failure_type_audit/cli.py +138 -0
  93. control_tower/ct_runs/task_analysis/failure_type_audit/prompts/system.md +9 -0
  94. control_tower/ct_runs/task_analysis/failure_type_audit/prompts/user.md +21 -0
  95. control_tower/ct_runs/task_analysis/failure_type_audit/task.py +227 -0
  96. control_tower/ct_runs/task_analysis/goal_completeness/README.md +56 -0
  97. control_tower/ct_runs/task_analysis/goal_completeness/cli.py +125 -0
  98. control_tower/ct_runs/task_analysis/goal_completeness/prompts/system.md +46 -0
  99. control_tower/ct_runs/task_analysis/goal_completeness/prompts/user.md +60 -0
  100. control_tower/ct_runs/task_analysis/goal_completeness/task.py +358 -0
  101. control_tower/ct_runs/task_analysis/goal_completeness/viewer.py +142 -0
  102. control_tower/ct_runs/task_analysis/reward_hacking/README.md +46 -0
  103. control_tower/ct_runs/task_analysis/reward_hacking/cli.py +115 -0
  104. control_tower/ct_runs/task_analysis/reward_hacking/prompts/system.md +71 -0
  105. control_tower/ct_runs/task_analysis/reward_hacking/prompts/user.md +62 -0
  106. control_tower/ct_runs/task_analysis/reward_hacking/task.py +388 -0
  107. control_tower/ct_runs/task_analysis/reward_hacking/viewer.py +122 -0
  108. control_tower/ct_runs/task_analysis/rt_alignment/README.md +45 -0
  109. control_tower/ct_runs/task_analysis/rt_alignment/cli.py +119 -0
  110. control_tower/ct_runs/task_analysis/rt_alignment/prompts/system.md +50 -0
  111. control_tower/ct_runs/task_analysis/rt_alignment/prompts/user.md +57 -0
  112. control_tower/ct_runs/task_analysis/rt_alignment/task.py +387 -0
  113. control_tower/ct_runs/task_analysis/rt_alignment/viewer.py +128 -0
  114. control_tower/ct_runs/task_analysis/side_task_review/README.md +47 -0
  115. control_tower/ct_runs/task_analysis/side_task_review/cli.py +95 -0
  116. control_tower/ct_runs/task_analysis/side_task_review/prompts/review.md +29 -0
  117. control_tower/ct_runs/task_analysis/side_task_review/task.py +101 -0
  118. control_tower/ct_runs/task_analysis/validate_cover/README.md +32 -0
  119. control_tower/ct_runs/task_analysis/validate_cover/cli.py +97 -0
  120. control_tower/ct_runs/task_analysis/validate_cover/task.py +94 -0
  121. control_tower/ct_runs/task_analysis/validate_cover/validate_cover_prompt.md +35 -0
  122. control_tower/ct_runs/timing_report/__init__.py +17 -0
  123. control_tower/ct_runs/timing_report/cli.py +71 -0
  124. control_tower/ct_runs/timing_report/report.py +327 -0
  125. control_tower/ct_runs/traj_analysis/submit_guessability/README.md +49 -0
  126. control_tower/ct_runs/traj_analysis/submit_guessability/cli.py +125 -0
  127. control_tower/ct_runs/traj_analysis/submit_guessability/prompts/system.md +29 -0
  128. control_tower/ct_runs/traj_analysis/submit_guessability/prompts/user.md +72 -0
  129. control_tower/ct_runs/traj_analysis/submit_guessability/task.py +402 -0
  130. control_tower/ct_runs/traj_analysis/submit_guessability/viewer.py +159 -0
  131. control_tower/ct_runs/traj_analysis/submit_guessability_verified/README.md +52 -0
  132. control_tower/ct_runs/traj_analysis/submit_guessability_verified/cli.py +143 -0
  133. control_tower/ct_runs/traj_analysis/submit_guessability_verified/prompts/system.md +36 -0
  134. control_tower/ct_runs/traj_analysis/submit_guessability_verified/prompts/user.md +63 -0
  135. control_tower/ct_runs/traj_analysis/submit_guessability_verified/task.py +762 -0
  136. control_tower/ct_runs/traj_analysis/submit_guessability_verified/viewer.py +186 -0
  137. control_tower/datasets/__init__.py +0 -0
  138. control_tower/datasets/check.py +88 -0
  139. control_tower/datasets/cli.py +532 -0
  140. control_tower/datasets/external.py +648 -0
  141. control_tower/datasets/importers/__init__.py +42 -0
  142. control_tower/datasets/importers/eval_log.py +570 -0
  143. control_tower/datasets/importers/monitoring_bench.py +643 -0
  144. control_tower/datasets/importers/mythos_5_incident.py +243 -0
  145. control_tower/datasets/importers/shade_arena.py +590 -0
  146. control_tower/datasets/importers/sleight_bench.py +489 -0
  147. control_tower/datasets/model.py +45 -0
  148. control_tower/defaults.py +16 -0
  149. control_tower/environments/build.py +204 -0
  150. control_tower/environments/env_args.py +128 -0
  151. control_tower/environments/environment.py +130 -0
  152. control_tower/environments/internet_simulator/.dockerignore +2 -0
  153. control_tower/environments/internet_simulator/Dockerfile +91 -0
  154. control_tower/environments/internet_simulator/__init__.py +0 -0
  155. control_tower/environments/internet_simulator/__main__.py +4 -0
  156. control_tower/environments/internet_simulator/compose.yml +35 -0
  157. control_tower/environments/internet_simulator/constants.py +43 -0
  158. control_tower/environments/internet_simulator/core/assets.py +276 -0
  159. control_tower/environments/internet_simulator/core/generate_ca.py +68 -0
  160. control_tower/environments/internet_simulator/core/logs.py +241 -0
  161. control_tower/environments/internet_simulator/core/network_mapping.py +24 -0
  162. control_tower/environments/internet_simulator/core/tls.py +124 -0
  163. control_tower/environments/internet_simulator/core/trajectory.py +17 -0
  164. control_tower/environments/internet_simulator/core/webhook.py +213 -0
  165. control_tower/environments/internet_simulator/env_values.py +17 -0
  166. control_tower/environments/internet_simulator/malware/malware.c +143 -0
  167. control_tower/environments/internet_simulator/modules/forward_proxy.py +425 -0
  168. control_tower/environments/internet_simulator/modules/resolver.py +88 -0
  169. control_tower/environments/internet_simulator/modules/response_rules.py +192 -0
  170. control_tower/environments/internet_simulator/modules/shell_sessions.py +206 -0
  171. control_tower/environments/internet_simulator/modules/upstream_proxy.py +256 -0
  172. control_tower/environments/internet_simulator/modules/upstream_specs.py +200 -0
  173. control_tower/environments/internet_simulator/personas.py +150 -0
  174. control_tower/environments/internet_simulator/requirements.txt +4 -0
  175. control_tower/environments/internet_simulator/resolve_override.c +75 -0
  176. control_tower/environments/internet_simulator/runtimes/__init__.py +0 -0
  177. control_tower/environments/internet_simulator/runtimes/docker.py +2154 -0
  178. control_tower/environments/internet_simulator/runtimes/hawk.py +173 -0
  179. control_tower/environments/internet_simulator/runtimes/participants.py +30 -0
  180. control_tower/environments/internet_simulator/runtimes/provisioning.py +44 -0
  181. control_tower/environments/internet_simulator/server.py +600 -0
  182. control_tower/environments/list_utils.py +562 -0
  183. control_tower/environments/registry.py +302 -0
  184. control_tower/environments/remote/__init__.py +0 -0
  185. control_tower/environments/remote/json_model.py +82 -0
  186. control_tower/environments/remote/process.py +238 -0
  187. control_tower/environments/remote/remote.py +463 -0
  188. control_tower/environments/remote/sample.py +289 -0
  189. control_tower/environments/requirements.py +174 -0
  190. control_tower/environments/setting_runtime.py +173 -0
  191. control_tower/environments/settings.py +1222 -0
  192. control_tower/environments/settings.yml +24 -0
  193. control_tower/environments/settings_cli.py +642 -0
  194. control_tower/environments/source_utils.py +304 -0
  195. control_tower/environments/validate.py +141 -0
  196. control_tower/eval/README.md +38 -0
  197. control_tower/eval/__init__.py +11 -0
  198. control_tower/eval/blue.py +537 -0
  199. control_tower/eval/cli.py +563 -0
  200. control_tower/eval/dataset.py +810 -0
  201. control_tower/eval/dispatch.py +514 -0
  202. control_tower/eval/game_view.py +28 -0
  203. control_tower/eval/harness.py +151 -0
  204. control_tower/eval/history.py +224 -0
  205. control_tower/eval/metadata.py +43 -0
  206. control_tower/eval/monitor_cli.py +143 -0
  207. control_tower/eval/monitor_task.py +156 -0
  208. control_tower/eval/monitoring/__init__.py +5 -0
  209. control_tower/eval/monitoring/monitor.py +301 -0
  210. control_tower/eval/monitoring/posthoc.py +248 -0
  211. control_tower/eval/options.py +122 -0
  212. control_tower/eval/protocols_cli.py +140 -0
  213. control_tower/eval/red.py +328 -0
  214. control_tower/eval/registry.py +59 -0
  215. control_tower/eval/run.py +409 -0
  216. control_tower/eval/solver.py +167 -0
  217. control_tower/eval/task.py +54 -0
  218. control_tower/eval/task_messages.py +55 -0
  219. control_tower/eval/task_scorer_validation.py +90 -0
  220. control_tower/eval/task_selection.py +284 -0
  221. control_tower/eval/white.py +71 -0
  222. control_tower/eval_logs/__init__.py +0 -0
  223. control_tower/eval_logs/api_upload.py +216 -0
  224. control_tower/eval_logs/convert.py +1173 -0
  225. control_tower/eval_logs/io.py +211 -0
  226. control_tower/eval_logs/merge.py +93 -0
  227. control_tower/eval_logs/monitor_scores.py +13 -0
  228. control_tower/eval_logs/run_metadata.py +15 -0
  229. control_tower/eval_logs/s3_upload.py +167 -0
  230. control_tower/eval_logs/seat.py +85 -0
  231. control_tower/eval_logs/source.py +51 -0
  232. control_tower/eval_logs/utils.py +85 -0
  233. control_tower/fleet/__init__.py +1 -0
  234. control_tower/fleet/ami.py +62 -0
  235. control_tower/fleet/aws_identity.py +9 -0
  236. control_tower/fleet/bootstrap.sh +395 -0
  237. control_tower/fleet/controller.py +2359 -0
  238. control_tower/fleet/job.py +371 -0
  239. control_tower/fleet/run_job.py +253 -0
  240. control_tower/fleet/worker.py +1094 -0
  241. control_tower/fleet/worker_cli.py +68 -0
  242. control_tower/hawk/__init__.py +11 -0
  243. control_tower/hawk/config.py +92 -0
  244. control_tower/hawk/download.py +93 -0
  245. control_tower/hawk/middleman.py +56 -0
  246. control_tower/hawk/model_closure.py +33 -0
  247. control_tower/hawk/preflight.py +161 -0
  248. control_tower/hawk/pull_envs.py +95 -0
  249. control_tower/hawk/runner.py +1038 -0
  250. control_tower/hawk/sandbox.py +472 -0
  251. control_tower/hawk/yaml_generator.py +215 -0
  252. control_tower/inspect_tasks.py +271 -0
  253. control_tower/live/__init__.py +0 -0
  254. control_tower/live/agents.py +717 -0
  255. control_tower/live/assets/dev_container_extensions.json +3 -0
  256. control_tower/live/assets/pyrightconfig.json +4 -0
  257. control_tower/live/assets/settings.json +3 -0
  258. control_tower/live/cli.py +1045 -0
  259. control_tower/live/compose.py +272 -0
  260. control_tower/live/context/print_context.py +98 -0
  261. control_tower/live/context/task_getters.py +78 -0
  262. control_tower/live/context/traj_dir.py +326 -0
  263. control_tower/live/daemon.py +545 -0
  264. control_tower/live/integration_tests.py +96 -0
  265. control_tower/live/remote_sandbox.py +154 -0
  266. control_tower/live/rpc.py +55 -0
  267. control_tower/live/sandbox_ops.py +129 -0
  268. control_tower/live/score.py +366 -0
  269. control_tower/live/session.py +733 -0
  270. control_tower/live/setup.py +299 -0
  271. control_tower/live/task_state.py +139 -0
  272. control_tower/live/test.py +10 -0
  273. control_tower/live/traj/__init__.py +0 -0
  274. control_tower/live/traj/gen_traj.py +345 -0
  275. control_tower/live/traj/git_status.py +33 -0
  276. control_tower/live/traj/git_traj.py +748 -0
  277. control_tower/live/traj/git_traj_helpers/FORMAT.md +25 -0
  278. control_tower/live/traj/git_traj_helpers/call_insert.py +15 -0
  279. control_tower/live/traj/git_traj_helpers/call_str_replace.py +20 -0
  280. control_tower/live/traj/macros.py +117 -0
  281. control_tower/live/traj/solution.py +265 -0
  282. control_tower/live/traj/templ/add.py +14 -0
  283. control_tower/live/traj/templ/add_tool_call.py +98 -0
  284. control_tower/live/traj/templ/bash.py +128 -0
  285. control_tower/live/traj/templ/git_utils.py +93 -0
  286. control_tower/live/traj/templ/input_utils.py +145 -0
  287. control_tower/live/traj/templ/template_from.py +118 -0
  288. control_tower/live/traj/templ/tool_call_builder.py +119 -0
  289. control_tower/live/traj/tool_call_utils.py +289 -0
  290. control_tower/live/traj/traj.py +316 -0
  291. control_tower/live/tui/__init__.py +0 -0
  292. control_tower/live/tui/popup.py +293 -0
  293. control_tower/live/tui/tui.py +1237 -0
  294. control_tower/live/tui/tui.tcss +32 -0
  295. control_tower/live/usage/collect_testers.py +130 -0
  296. control_tower/live/usage/commands.py +313 -0
  297. control_tower/live/usage/full_env_context/container_script.py +230 -0
  298. control_tower/live/usage/full_env_context/full_env_context.py +68 -0
  299. control_tower/live/usage/migrate_traj.py +83 -0
  300. control_tower/live/usage/patch.py +64 -0
  301. control_tower/live/usage/test_all.py +1349 -0
  302. control_tower/live/usage/upload_helpers.py +31 -0
  303. control_tower/metrics/__init__.py +0 -0
  304. control_tower/metrics/audit.py +353 -0
  305. control_tower/metrics/control.py +110 -0
  306. control_tower/models/__init__.py +11 -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/model_spec.py +284 -0
  311. control_tower/models/openrouter_provider.py +65 -0
  312. control_tower/models/pricing.py +168 -0
  313. control_tower/models/rates.py +290 -0
  314. control_tower/models/registry_hooks.py +166 -0
  315. control_tower/models/role_model.py +55 -0
  316. control_tower/models/usage_hooks.py +14 -0
  317. control_tower/monitors/__init__.py +36 -0
  318. control_tower/monitors/checklist.py +96 -0
  319. control_tower/monitors/jev.py +198 -0
  320. control_tower/monitors/jev_questions.yml +32 -0
  321. control_tower/monitors/monitor.py +466 -0
  322. control_tower/monitors/presets.yml +28 -0
  323. control_tower/monitors/prompts/action-no-goals.md +75 -0
  324. control_tower/monitors/prompts/action.md +83 -0
  325. control_tower/monitors/prompts/goal-analysis.md +77 -0
  326. control_tower/monitors/prompts/goals.md +82 -0
  327. control_tower/monitors/prompts/no-history.md +42 -0
  328. control_tower/monitors/prompts/orthogonal.md +107 -0
  329. control_tower/monitors/prompts/quick-no-goals.md +62 -0
  330. control_tower/monitors/prompts/quick.md +68 -0
  331. control_tower/monitors/prompts/simple.md +21 -0
  332. control_tower/monitors/prompts/strong-goals.md +104 -0
  333. control_tower/monitors/prompts/strong.md +95 -0
  334. control_tower/monitors/prompts/trajectory.md +18 -0
  335. control_tower/monitors/registry.py +19 -0
  336. control_tower/monitors/single_goal.py +96 -0
  337. control_tower/monitors/steps/checklist.md +48 -0
  338. control_tower/monitors/steps/single_goal.md +80 -0
  339. control_tower/monitors/steps/single_goal_analysis.md +76 -0
  340. control_tower/monitors/steps/three_scores.md +112 -0
  341. control_tower/monitors/three_scores.py +69 -0
  342. control_tower/policies/__init__.py +1 -0
  343. control_tower/policies/attack.py +199 -0
  344. control_tower/policies/honest.py +28 -0
  345. control_tower/policies/obvious_attack.py +69 -0
  346. control_tower/policies/prompts/attack.md +7 -0
  347. control_tower/policies/prompts/attack_planning.md +22 -0
  348. control_tower/policies/replay.py +227 -0
  349. control_tower/policies/scripted.py +123 -0
  350. control_tower/protocols/__init__.py +1 -0
  351. control_tower/protocols/block_all.py +43 -0
  352. control_tower/protocols/block_every_other.py +44 -0
  353. control_tower/protocols/defer_to_trusted.py +152 -0
  354. control_tower/protocols/null_blue.py +33 -0
  355. control_tower/protocols/prompts/trusted_editing.md +9 -0
  356. control_tower/protocols/resample.py +83 -0
  357. control_tower/protocols/trusted_editing.py +167 -0
  358. control_tower/protocols/trusted_monitoring.py +116 -0
  359. control_tower/protocols/watcher.py +341 -0
  360. control_tower/py.typed +0 -0
  361. control_tower/runs/README.md +85 -0
  362. control_tower/runs/convert.py +102 -0
  363. control_tower/runs/convert_eval_log_to_run.py +91 -0
  364. control_tower/runs/io.py +189 -0
  365. control_tower/runs/models.py +123 -0
  366. control_tower/runs/producer.py +4 -0
  367. control_tower/runs/runs_cli.py +679 -0
  368. control_tower/sandbox/__init__.py +6 -0
  369. control_tower/sandbox/_alpine_rootfs.py +230 -0
  370. control_tower/sandbox/_ssh.py +426 -0
  371. control_tower/sandbox/cli.py +301 -0
  372. control_tower/sandbox/constants.py +33 -0
  373. control_tower/sandbox/docker.py +1024 -0
  374. control_tower/sandbox/docker_in_vm.py +517 -0
  375. control_tower/sandbox/firecracker.py +413 -0
  376. control_tower/sandbox/inspect_tools.py +39 -0
  377. control_tower/sandbox/intranet.py +226 -0
  378. control_tower/sandbox/intranet_guard/Dockerfile +4 -0
  379. control_tower/sandbox/intranet_guard/firewall.sh +110 -0
  380. control_tower/sandbox/qemu.py +292 -0
  381. control_tower/sandbox/replay/__init__.py +22 -0
  382. control_tower/sandbox/replay/environment.py +435 -0
  383. control_tower/sandbox/replay/score.py +37 -0
  384. control_tower/sandbox/shared_containers.py +325 -0
  385. control_tower/sandbox/simulated/__init__.py +20 -0
  386. control_tower/sandbox/simulated/actions.py +90 -0
  387. control_tower/sandbox/simulated/compose.py +166 -0
  388. control_tower/sandbox/simulated/environment.py +182 -0
  389. control_tower/sandbox/simulated/judge.py +159 -0
  390. control_tower/sandbox/simulated/prompts/judge.md +31 -0
  391. control_tower/sandbox/simulated/prompts/simulator.md +74 -0
  392. control_tower/sandbox/simulated/prompts/simulator_no_exec.md +22 -0
  393. control_tower/sandbox/simulated/scratch/compose.yaml +42 -0
  394. control_tower/sandbox/simulated/session.py +156 -0
  395. control_tower/scanners.py +123 -0
  396. control_tower/task/__init__.py +0 -0
  397. control_tower/task/collection.py +64 -0
  398. control_tower/task/final_diff.py +88 -0
  399. control_tower/task/sample_metadata.py +35 -0
  400. control_tower/task/scorers.py +351 -0
  401. control_tower/task/select_utils.py +106 -0
  402. control_tower/task/selectors.py +49 -0
  403. control_tower/task/setup.py +96 -0
  404. control_tower/task/space.py +21 -0
  405. control_tower/task/strat_sets/model_registry_strats.txt +11 -0
  406. control_tower/task/task.py +456 -0
  407. control_tower/task/task_sets/__init__.py +176 -0
  408. control_tower/task/traj_dir.py +39 -0
  409. control_tower/task/workspace_baseline.py +581 -0
  410. control_tower/tools/__init__.py +0 -0
  411. control_tower/tools/answer.py +53 -0
  412. control_tower/tools/bash_tools.py +37 -0
  413. control_tower/tools/declarations.py +23 -0
  414. control_tower/tools/diff_display_text_editor.py +66 -0
  415. control_tower/tools/registry.py +37 -0
  416. control_tower/tools/split_text_editor_tools.py +125 -0
  417. control_tower/tools/submit.py +35 -0
  418. control_tower/tools/task_tools.py +237 -0
  419. control_tower/tools/tool_calls.py +46 -0
  420. control_tower/tools/tool_registry.py +371 -0
  421. control_tower/trajectories/README.md +76 -0
  422. control_tower/trajectories/__init__.py +0 -0
  423. control_tower/trajectories/action.py +122 -0
  424. control_tower/trajectories/api.py +615 -0
  425. control_tower/trajectories/eval_trace_metadata.py +15 -0
  426. control_tower/trajectories/extended_thinking.py +56 -0
  427. control_tower/trajectories/format_history.py +119 -0
  428. control_tower/trajectories/from_eval_log.py +323 -0
  429. control_tower/trajectories/io.py +205 -0
  430. control_tower/trajectories/models.py +25 -0
  431. control_tower/trajectories/mongo_docs.py +392 -0
  432. control_tower/trajectories/monitor_score.py +159 -0
  433. control_tower/trajectories/sample.py +328 -0
  434. control_tower/trajectories/task_scores.py +108 -0
  435. control_tower/trajectories/text_editor_output.py +69 -0
  436. control_tower/trajectories/timing_utils.py +419 -0
  437. control_tower/trajectories/tool_calls.py +40 -0
  438. control_tower/trajectories/utils.py +74 -0
  439. control_tower/trajectories/viewer_api.py +578 -0
  440. control_tower/trajectories/viewer_auth.py +50 -0
  441. control_tower/trajectories/workspace_diff.py +16 -0
  442. control_tower/utils/__init__.py +0 -0
  443. control_tower/utils/cli_args.py +20 -0
  444. control_tower/utils/direct_api_generator.py +151 -0
  445. control_tower/utils/docs_utils.py +15 -0
  446. control_tower/utils/env.py +163 -0
  447. control_tower/utils/file_patterns.py +92 -0
  448. control_tower/utils/fs.py +137 -0
  449. control_tower/utils/function_registry/__init__.py +0 -0
  450. control_tower/utils/function_registry/arg_type.py +41 -0
  451. control_tower/utils/function_registry/base_model.py +63 -0
  452. control_tower/utils/function_registry/custom_handlers.py +99 -0
  453. control_tower/utils/function_registry/display.py +137 -0
  454. control_tower/utils/function_registry/function_info.py +97 -0
  455. control_tower/utils/function_registry/naming.py +5 -0
  456. control_tower/utils/function_registry/registry.py +419 -0
  457. control_tower/utils/function_registry/selection.py +68 -0
  458. control_tower/utils/function_registry/signature_info.py +78 -0
  459. control_tower/utils/function_registry/validation.py +97 -0
  460. control_tower/utils/imports.py +35 -0
  461. control_tower/utils/paths.py +98 -0
  462. control_tower/utils/sanitize.py +74 -0
  463. control_tower/utils/serde.py +151 -0
  464. control_tower/utils/suggestions.py +25 -0
  465. control_tower/utils/time.py +18 -0
  466. control_tower/utils/transcript.py +20 -0
  467. control_tower/utils/versioning.py +383 -0
  468. control_tower-6.0.0rc3.dist-info/METADATA +88 -0
  469. control_tower-6.0.0rc3.dist-info/RECORD +473 -0
  470. control_tower-6.0.0rc3.dist-info/WHEEL +5 -0
  471. control_tower-6.0.0rc3.dist-info/entry_points.txt +5 -0
  472. control_tower-6.0.0rc3.dist-info/licenses/LICENSE +21 -0
  473. control_tower-6.0.0rc3.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,3 @@
1
+ from control_tower.utils.env import ensure_dotenv
2
+
3
+ ensure_dotenv()
@@ -0,0 +1,215 @@
1
+ import functools
2
+ import json
3
+ import re
4
+ import sys
5
+ import termios
6
+ import tty
7
+ from collections.abc import Callable
8
+ from pathlib import Path
9
+ from typing import Any, cast
10
+
11
+ import click
12
+
13
+ from control_tower.cli.completions.completion_callbacks import (
14
+ completion_env_names,
15
+ completion_main_task_names,
16
+ completion_side_task_names,
17
+ )
18
+ from ct_setting_sdk.protocol.errors import ExpectedError
19
+
20
+
21
+ def getch() -> str:
22
+ """Read a single character from stdin without requiring Enter.
23
+
24
+ Handles arrow keys by returning "left" or "right" strings.
25
+ """
26
+ fd = sys.stdin.fileno()
27
+ old_settings = termios.tcgetattr(fd)
28
+ try:
29
+ tty.setraw(fd)
30
+ ch = sys.stdin.read(1)
31
+ if ch == "\x1b":
32
+ ch2 = sys.stdin.read(1)
33
+ if ch2 == "[":
34
+ ch3 = sys.stdin.read(1)
35
+ if ch3 == "D":
36
+ return "left"
37
+ elif ch3 == "C":
38
+ return "right"
39
+ return ch
40
+ finally:
41
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
42
+
43
+
44
+ def clear_screen() -> None:
45
+ """Clear the terminal screen."""
46
+ click.echo("\033[2J\033[H", nl=False)
47
+
48
+
49
+ def word_wrap(text: str, width: int) -> list[str]:
50
+ """Wrap text to specified width, preserving line breaks."""
51
+ result = []
52
+ for paragraph in text.split("\n"):
53
+ if not paragraph.strip():
54
+ result.append("")
55
+ continue
56
+ words = paragraph.split()
57
+ current_line = ""
58
+ for word in words:
59
+ if len(current_line) + len(word) + 1 > width:
60
+ if current_line:
61
+ result.append(current_line)
62
+ current_line = word
63
+ else:
64
+ current_line += (" " if current_line else "") + word
65
+ if current_line:
66
+ result.append(current_line)
67
+ return result
68
+
69
+
70
+ def load_jsonl(path: Path) -> list[dict[str, Any]]:
71
+ """Load all non-empty lines from a JSONL file as dicts."""
72
+ results = []
73
+ with open(path) as f:
74
+ for line in f:
75
+ line = line.strip()
76
+ if line:
77
+ results.append(json.loads(line))
78
+ return results
79
+
80
+
81
+ def select_audit_run(runs_dir: Path, title: str = "Select audit run") -> Path | None:
82
+ """Interactive picker for audit run directories. Returns None if no runs or user cancels."""
83
+ from pick import Option, pick
84
+
85
+ if not runs_dir.exists():
86
+ click.secho(f"No runs found in {runs_dir}/", fg="yellow")
87
+ return None
88
+
89
+ runs = sorted(
90
+ [d for d in runs_dir.iterdir() if d.is_dir()],
91
+ key=lambda p: p.stat().st_ctime,
92
+ reverse=True,
93
+ )
94
+
95
+ if not runs:
96
+ click.secho(f"No runs found in {runs_dir}/", fg="yellow")
97
+ return None
98
+
99
+ options: list[Option] = []
100
+ for run in runs:
101
+ results_file = run / "results.jsonl"
102
+ count = 0
103
+ if results_file.exists():
104
+ count = sum(1 for _ in results_file.open())
105
+ label = f"{run.name} ({count} results)"
106
+ options.append(Option(label, run))
107
+
108
+ selected, _ = pick(
109
+ options,
110
+ title=title,
111
+ indicator="=>",
112
+ default_index=0,
113
+ )
114
+
115
+ return selected.value if isinstance(selected, Option) else Path(str(selected))
116
+
117
+
118
+ def _build_param_map(click_params: list[click.Parameter]) -> dict[str, str]:
119
+ """Build mapping from Python param names to CLI representations."""
120
+ param_map: dict[str, str] = {}
121
+
122
+ for param in click_params:
123
+ if not param.name:
124
+ continue
125
+
126
+ param_name = param.name
127
+ if not isinstance(param_name, str):
128
+ continue
129
+
130
+ if isinstance(param, click.Option):
131
+ if param.opts:
132
+ cli_name = cast(str, max(param.opts, key=len))
133
+ param_map[param_name] = cli_name
134
+ elif isinstance(param, click.Argument):
135
+ param_map[param_name] = param_name.upper()
136
+
137
+ return param_map
138
+
139
+
140
+ def _transform_error_message(message: str, click_params: list[click.Parameter]) -> str:
141
+ """Transform parameter names in error message to CLI equivalents."""
142
+ param_map = _build_param_map(click_params)
143
+
144
+ for py_name, cli_name in param_map.items():
145
+ message = re.sub(rf"`{re.escape(py_name)}`", cli_name, message)
146
+
147
+ if "_" in py_name:
148
+ message = re.sub(rf"\b{re.escape(py_name)}\b", cli_name, message)
149
+
150
+ return message
151
+
152
+
153
+ def handle_user_errors(f: Callable[..., object]) -> Callable[..., object]:
154
+ """Decorator that converts ExpectedError exceptions to click.ClickException.
155
+
156
+ Apply this decorator to Click commands to ensure that ExpectedError exceptions
157
+ are displayed cleanly without a stack trace, while unexpected errors still
158
+ show full tracebacks for debugging.
159
+
160
+ Also transforms parameter names in error messages to their CLI equivalents:
161
+ - `param` (backtick-wrapped) -> --param
162
+ - param_name (with underscores) -> --param-name
163
+
164
+ Adds a --traceback flag to show full stack traces for expected errors.
165
+ """
166
+
167
+ @functools.wraps(f)
168
+ def wrapper(*args: object, traceback: bool = False, **kwargs: object) -> object:
169
+ if traceback:
170
+ return f(*args, **kwargs)
171
+
172
+ try:
173
+ return f(*args, **kwargs)
174
+ except ExpectedError as e:
175
+ message = str(e)
176
+
177
+ ctx = click.get_current_context(silent=True)
178
+ if ctx and ctx.command:
179
+ message = _transform_error_message(message, ctx.command.params)
180
+
181
+ raise click.ClickException(message) from None
182
+
183
+ return click.option(
184
+ "--traceback",
185
+ is_flag=True,
186
+ hidden=True,
187
+ help="Show full traceback for expected errors (for debugging)",
188
+ )(wrapper)
189
+
190
+
191
+ def task_override_options[F: Callable[..., object]](func: F) -> F:
192
+ """Add -e/--env, -t/--main-task and -s/--side-task options with shell completion."""
193
+ func = click.option(
194
+ "-s",
195
+ "--side-task",
196
+ type=str,
197
+ shell_complete=completion_side_task_names,
198
+ help="Side task name",
199
+ )(func)
200
+ func = click.option(
201
+ "-t",
202
+ "--main-task",
203
+ type=str,
204
+ shell_complete=completion_main_task_names,
205
+ help="Main task name",
206
+ )(func)
207
+ func = click.option(
208
+ "-e",
209
+ "--env",
210
+ "environment",
211
+ type=str,
212
+ shell_complete=completion_env_names,
213
+ help="Environment name",
214
+ )(func)
215
+ return func
@@ -0,0 +1,132 @@
1
+ """CLI commands for AWS cloud operations."""
2
+
3
+ import click
4
+
5
+ from control_tower.cli.mcp_meta import CTGroup
6
+
7
+
8
+ @click.group(cls=CTGroup)
9
+ def cloud_cli() -> None:
10
+ """AWS cloud operations for distributed evaluation."""
11
+
12
+
13
+ @cloud_cli.command("status")
14
+ @click.option(
15
+ "--region",
16
+ type=str,
17
+ default="us-east-1",
18
+ help="AWS region to check",
19
+ )
20
+ @click.option(
21
+ "--all-users",
22
+ is_flag=True,
23
+ help="Show resources from all users (default: only yours)",
24
+ )
25
+ def status_cmd(region: str, all_users: bool) -> None:
26
+ """Show running fleet resources (EC2 instances, SQS queues).
27
+
28
+ By default shows only resources launched by the calling AWS identity
29
+ (IAM username or SSO session name).
30
+ Use --all-users to see everything tagged ManagedBy=linuxbench.
31
+ """
32
+ from datetime import UTC, datetime
33
+
34
+ import boto3
35
+
36
+ from control_tower.fleet.aws_identity import caller_identity_name
37
+ from control_tower.utils.env import EnvVar
38
+
39
+ env = EnvVar()
40
+ if env.aws_profile and env.aws_access_key_id:
41
+ click.echo(
42
+ "WARNING: both AWS_PROFILE and AWS_ACCESS_KEY_ID are set — static keys take "
43
+ "precedence, so everything below reflects the static-key identity.\n"
44
+ )
45
+ ec2 = boto3.client("ec2", region_name=region)
46
+ sqs = boto3.client("sqs", region_name=region)
47
+
48
+ username = caller_identity_name(region)
49
+ click.echo(f"AWS identity: {username}")
50
+ click.echo(f"Region: {region}\n")
51
+
52
+ filters = [
53
+ {"Name": "tag:ManagedBy", "Values": ["linuxbench"]},
54
+ {"Name": "instance-state-name", "Values": ["running", "pending"]},
55
+ ]
56
+ if not all_users:
57
+ filters.append({"Name": "tag:LaunchedBy", "Values": [username]})
58
+
59
+ resp = ec2.describe_instances(Filters=filters)
60
+ instances = [inst for res in resp["Reservations"] for inst in res["Instances"]]
61
+
62
+ click.echo(f"EC2 Instances: {len(instances)}")
63
+ by_fleet: dict[str, list] = {}
64
+ for inst in instances:
65
+ tags = {t["Key"]: t["Value"] for t in inst.get("Tags", [])}
66
+ fleet_id = tags.get("FleetId", "unknown")
67
+ by_fleet.setdefault(fleet_id, []).append((inst, tags))
68
+
69
+ if instances:
70
+ for fleet_id, fleet_instances in sorted(by_fleet.items()):
71
+ first_inst, first_tags = fleet_instances[0]
72
+ launch_time = first_inst["LaunchTime"]
73
+ age = datetime.now(UTC) - launch_time
74
+ hours = age.total_seconds() / 3600
75
+ owner = first_tags.get("LaunchedBy", "unknown")
76
+ itype = first_inst["InstanceType"]
77
+ click.echo(
78
+ f" Fleet {fleet_id[:8]}: {len(fleet_instances)} x {itype} "
79
+ f"({hours:.1f}h) owner={owner}"
80
+ )
81
+ for inst, _tags in fleet_instances:
82
+ click.echo(
83
+ f" {inst['InstanceId']} "
84
+ f"{inst.get('PublicIpAddress', 'no-ip')} "
85
+ f"state={inst['State']['Name']}"
86
+ )
87
+ click.echo()
88
+
89
+ # SQS list_queues doesn't support tag-based filtering; show all lb-fleet-*
90
+ # queues but annotate which belong to the user's EC2 fleets.
91
+ # None means the listing failed, which is not the same as finding no queues.
92
+ queue_urls: list[str] | None = None
93
+ try:
94
+ queue_resp = sqs.list_queues(QueueNamePrefix="lb-fleet-")
95
+ except Exception as exc:
96
+ click.echo(f"SQS Queues: could not be listed: {exc}", err=True)
97
+ else:
98
+ queue_urls = queue_resp.get("QueueUrls", [])
99
+ own_fleet_ids = set(by_fleet.keys())
100
+
101
+ click.echo(f"SQS Queues: {len(queue_urls)}")
102
+ seen_fleets: set[str] = set()
103
+ for url in sorted(queue_urls):
104
+ name = url.rsplit("/", 1)[-1]
105
+ parts = name.split("-") # lb-fleet-{fleet_id}-{suffix}
106
+ if len(parts) >= 4:
107
+ fleet_id = parts[2]
108
+ if fleet_id not in seen_fleets:
109
+ seen_fleets.add(fleet_id)
110
+ fleet_queues = [u for u in queue_urls if f"-{fleet_id}-" in u]
111
+ owner_mark = " (yours)" if fleet_id in own_fleet_ids else ""
112
+ click.echo(
113
+ f" Fleet {fleet_id[:8]}: {len(fleet_queues)} queues{owner_mark}"
114
+ )
115
+ click.echo()
116
+
117
+ if not instances and queue_urls == []:
118
+ click.echo("No active fleet resources found.")
119
+ elif instances:
120
+ own_ids = [
121
+ inst["InstanceId"]
122
+ for inst in instances
123
+ if any(
124
+ t["Key"] == "LaunchedBy" and t["Value"] == username
125
+ for t in inst.get("Tags", [])
126
+ )
127
+ ]
128
+ if own_ids:
129
+ click.echo("To terminate your instances:")
130
+ click.echo(
131
+ f" aws ec2 terminate-instances --instance-ids {' '.join(own_ids)}"
132
+ )
@@ -0,0 +1,125 @@
1
+ """Click shell completion for environment / main-task / side-task values (reads ``~/.cache/ct/completions-data.json``)."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ import click
8
+ from click.shell_completion import CompletionItem
9
+
10
+ _CACHE_DIR_NAME = "ct"
11
+ _COMPLETION_JSON = "completions-data.json"
12
+
13
+ _completions_cache: tuple[float, dict[str, Any]] | None = None
14
+
15
+
16
+ def _completion_json_path() -> Path:
17
+ return Path.home() / ".cache" / _CACHE_DIR_NAME / _COMPLETION_JSON
18
+
19
+
20
+ def invalidate_completion_payload_cache() -> None:
21
+ global _completions_cache
22
+ _completions_cache = None
23
+
24
+
25
+ def load_completion_payload() -> dict[str, Any]:
26
+ global _completions_cache
27
+ path = _completion_json_path()
28
+ if not path.is_file():
29
+ return {}
30
+ try:
31
+ mtime = path.stat().st_mtime
32
+ except OSError:
33
+ return {}
34
+ if _completions_cache is not None and _completions_cache[0] == mtime:
35
+ return _completions_cache[1]
36
+ raw = path.read_text(encoding="utf-8")
37
+ lines = raw.splitlines()
38
+ if lines and lines[0].startswith("#"):
39
+ raw = "\n".join(lines[1:]).lstrip()
40
+ try:
41
+ data = json.loads(raw)
42
+ except json.JSONDecodeError:
43
+ data = {}
44
+ if not isinstance(data, dict):
45
+ data = {}
46
+ _completions_cache = (mtime, data)
47
+ return data
48
+
49
+
50
+ def _env_ids_from_context(ctx: click.Context | None) -> list[str]:
51
+ if ctx is None:
52
+ return []
53
+ keys = ("environment", "environments", "env_name", "only_envs", "env")
54
+ collected: list[str] = []
55
+ c: click.Context | None = ctx
56
+ while c is not None:
57
+ params = c.params
58
+ for key in keys:
59
+ if key not in params:
60
+ continue
61
+ v = params[key]
62
+ if v is None:
63
+ continue
64
+ if isinstance(v, list | tuple):
65
+ for x in v:
66
+ if x is not None and not isinstance(x, Path):
67
+ collected.append(str(x))
68
+ elif not isinstance(v, Path):
69
+ collected.append(str(v))
70
+ c = c.parent
71
+ seen: set[str] = set()
72
+ out: list[str] = []
73
+ for x in collected:
74
+ if x not in seen:
75
+ seen.add(x)
76
+ out.append(x)
77
+ return out
78
+
79
+
80
+ def _prefix_match(candidates: list[str], incomplete: str) -> list[str]:
81
+ if not incomplete:
82
+ return candidates
83
+ return [c for c in candidates if c.startswith(incomplete)]
84
+
85
+
86
+ def completion_env_names(
87
+ ctx: click.Context, param: click.Parameter, incomplete: str
88
+ ) -> list[CompletionItem | str]:
89
+ payload = load_completion_payload()
90
+ envs = payload.get("envs", [])
91
+ return [CompletionItem(e) for e in _prefix_match(envs, incomplete)]
92
+
93
+
94
+ def completion_main_task_names(
95
+ ctx: click.Context, param: click.Parameter, incomplete: str
96
+ ) -> list[CompletionItem | str]:
97
+ payload = load_completion_payload()
98
+ envs = _env_ids_from_context(ctx)
99
+ if envs:
100
+ by_env: dict[str, list[str]] = payload.get("main_tasks_by_env", {})
101
+ cands: list[str] = []
102
+ for name, tasks in by_env.items():
103
+ if any(e in (name, name.rpartition("/")[2]) for e in envs):
104
+ cands.extend(tasks)
105
+ cands = list(dict.fromkeys(cands))
106
+ else:
107
+ cands = list(payload.get("main_tasks", []))
108
+ return [CompletionItem(t) for t in _prefix_match(cands, incomplete)]
109
+
110
+
111
+ def completion_side_task_names(
112
+ ctx: click.Context, param: click.Parameter, incomplete: str
113
+ ) -> list[CompletionItem | str]:
114
+ payload = load_completion_payload()
115
+ envs = _env_ids_from_context(ctx)
116
+ if envs:
117
+ by_env: dict[str, list[str]] = payload.get("side_tasks_by_env", {})
118
+ cands: list[str] = []
119
+ for name, tasks in by_env.items():
120
+ if any(e in (name, name.rpartition("/")[2]) for e in envs):
121
+ cands.extend(tasks)
122
+ cands = list(dict.fromkeys(cands))
123
+ else:
124
+ cands = list(payload.get("side_tasks", []))
125
+ return [CompletionItem(t) for t in _prefix_match(cands, incomplete)]
@@ -0,0 +1,59 @@
1
+ """Static completion data for ``ct`` (written under ``~/.cache/ct``)."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from control_tower.cli.completions.completion_callbacks import (
8
+ invalidate_completion_payload_cache,
9
+ )
10
+
11
+ COMPLETION_GENERATED_LINE = "# Generated by LinuxArena."
12
+ COMPLETION_DATA_JSON = "completions-data.json"
13
+
14
+
15
+ def default_completion_data_dir() -> Path:
16
+ return Path.home() / ".cache" / "ct"
17
+
18
+
19
+ def scan_environments() -> dict[str, Any]:
20
+ """Return env and task ids for shell completion.
21
+
22
+ Ids come from ``task_space()`` rather than from task folders, so an env
23
+ that builds its tasks in code completes the same as a folder-backed one.
24
+ """
25
+ from control_tower.environments.registry import environments, map_by_package
26
+
27
+ spaces = map_by_package(environments.all(), lambda _, env: env.task_space())
28
+ main_tasks_by_env = {env_id: sorted(s.mains()) for env_id, s in spaces.items()}
29
+ side_tasks_by_env = {env_id: sorted(s.sides()) for env_id, s in spaces.items()}
30
+
31
+ return {
32
+ "envs": sorted(main_tasks_by_env),
33
+ "main_tasks": sorted({t for ts in main_tasks_by_env.values() for t in ts}),
34
+ "main_tasks_by_env": {
35
+ k: main_tasks_by_env[k] for k in sorted(main_tasks_by_env)
36
+ },
37
+ "side_tasks": sorted({t for ts in side_tasks_by_env.values() for t in ts}),
38
+ "side_tasks_by_env": {
39
+ k: side_tasks_by_env[k] for k in sorted(side_tasks_by_env)
40
+ },
41
+ }
42
+
43
+
44
+ def build_completion_data(*, data_dir: Path | None = None) -> dict[str, Any]:
45
+ """Write ``completions-data.json`` with env/task lists for Click shell completion."""
46
+ payload = scan_environments()
47
+ data_dir = data_dir or default_completion_data_dir()
48
+ data_dir.mkdir(parents=True, exist_ok=True)
49
+
50
+ out_path = data_dir / COMPLETION_DATA_JSON
51
+ text = json.dumps(payload, indent=2, sort_keys=True)
52
+ out_path.write_text(
53
+ "\n".join([COMPLETION_GENERATED_LINE, text]) + "\n",
54
+ encoding="utf-8",
55
+ )
56
+
57
+ invalidate_completion_payload_cache()
58
+
59
+ return payload