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.
- control_tower/__init__.py +3 -0
- control_tower/cli/__init__.py +3 -0
- control_tower/cli/base.py +215 -0
- control_tower/cli/cloud.py +132 -0
- control_tower/cli/completions/completion_callbacks.py +125 -0
- control_tower/cli/completions/completions_build.py +59 -0
- control_tower/cli/completions/completions_hooks.py +209 -0
- control_tower/cli/ct_cli_group.py +28 -0
- control_tower/cli/extras.py +89 -0
- control_tower/cli/login.py +77 -0
- control_tower/cli/main.py +44 -0
- control_tower/cli/mcp_meta.py +30 -0
- control_tower/cli/task_sets.py +133 -0
- control_tower/ct_runs/__init__.py +0 -0
- control_tower/ct_runs/claude_host.py +26 -0
- control_tower/ct_runs/dashboard.py +265 -0
- control_tower/ct_runs/dashboard_cli.py +19 -0
- control_tower/ct_runs/env_description/__init__.py +0 -0
- control_tower/ct_runs/env_description/audit/README.md +80 -0
- control_tower/ct_runs/env_description/audit/__init__.py +0 -0
- control_tower/ct_runs/env_description/audit/cli.py +81 -0
- control_tower/ct_runs/env_description/audit/models.py +63 -0
- control_tower/ct_runs/env_description/audit/prompts/system.md +68 -0
- control_tower/ct_runs/env_description/audit/prompts/user.md +30 -0
- control_tower/ct_runs/env_description/audit/task.py +278 -0
- control_tower/ct_runs/env_description/generate/README.md +75 -0
- control_tower/ct_runs/env_description/generate/cli.py +146 -0
- control_tower/ct_runs/env_description/generate/prompts/system.md +62 -0
- control_tower/ct_runs/env_description/generate/prompts/user.md +21 -0
- control_tower/ct_runs/env_description/generate/task.py +361 -0
- control_tower/ct_runs/env_description/shared/concurrency.py +72 -0
- control_tower/ct_runs/env_description/shared/spec.md +79 -0
- control_tower/ct_runs/env_description/shared/spec_loader.py +24 -0
- control_tower/ct_runs/integration_test_quality/README.md +59 -0
- control_tower/ct_runs/integration_test_quality/cli.py +115 -0
- control_tower/ct_runs/integration_test_quality/prompts/analyze.md +44 -0
- control_tower/ct_runs/integration_test_quality/prompts/check_tasks.md +29 -0
- control_tower/ct_runs/integration_test_quality/prompts/coverage.md +27 -0
- control_tower/ct_runs/integration_test_quality/prompts/fix.md +14 -0
- control_tower/ct_runs/integration_test_quality/task.py +160 -0
- control_tower/ct_runs/pairing_analysis/README.md +30 -0
- control_tower/ct_runs/pairing_analysis/cli.py +82 -0
- control_tower/ct_runs/pairing_analysis/prompts/turn1.md +59 -0
- control_tower/ct_runs/pairing_analysis/prompts/turn2.md +43 -0
- control_tower/ct_runs/pairing_analysis/task.py +390 -0
- control_tower/ct_runs/prompts.py +28 -0
- control_tower/ct_runs/run_cli.py +16 -0
- control_tower/ct_runs/scorer_idempotency/base.py +216 -0
- control_tower/ct_runs/scorer_idempotency/checks/_json.py +31 -0
- control_tower/ct_runs/scorer_idempotency/checks/chroma.py +285 -0
- control_tower/ct_runs/scorer_idempotency/checks/docker.py +366 -0
- control_tower/ct_runs/scorer_idempotency/checks/elasticsearch.py +293 -0
- control_tower/ct_runs/scorer_idempotency/checks/filesystem.py +155 -0
- control_tower/ct_runs/scorer_idempotency/checks/generated_manifest.json +13 -0
- control_tower/ct_runs/scorer_idempotency/checks/influxdb.py +549 -0
- control_tower/ct_runs/scorer_idempotency/checks/listener.py +82 -0
- control_tower/ct_runs/scorer_idempotency/checks/minio.py +360 -0
- control_tower/ct_runs/scorer_idempotency/checks/mongo.py +338 -0
- control_tower/ct_runs/scorer_idempotency/checks/openldap.py +241 -0
- control_tower/ct_runs/scorer_idempotency/checks/postgres.py +317 -0
- control_tower/ct_runs/scorer_idempotency/checks/process.py +134 -0
- control_tower/ct_runs/scorer_idempotency/checks/rabbitmq.py +369 -0
- control_tower/ct_runs/scorer_idempotency/checks/redis.py +291 -0
- control_tower/ct_runs/scorer_idempotency/checks/sqlite.py +309 -0
- control_tower/ct_runs/scorer_idempotency/checks/sshd.py +270 -0
- control_tower/ct_runs/scorer_idempotency/checks/vault.py +376 -0
- control_tower/ct_runs/scorer_idempotency/cli.py +560 -0
- control_tower/ct_runs/scorer_idempotency/compose_analyzer.py +182 -0
- control_tower/ct_runs/scorer_idempotency/detection.py +236 -0
- control_tower/ct_runs/scorer_idempotency/dockerfile_analyzer.py +377 -0
- control_tower/ct_runs/scorer_idempotency/llm_generator.py +448 -0
- control_tower/ct_runs/scorer_idempotency/runner.py +445 -0
- control_tower/ct_runs/scorer_idempotency/runtime_prober.py +378 -0
- control_tower/ct_runs/storage.py +74 -0
- control_tower/ct_runs/task_analysis/compare_side_tasks/README.md +25 -0
- control_tower/ct_runs/task_analysis/compare_side_tasks/cli.py +25 -0
- control_tower/ct_runs/task_analysis/compare_side_tasks/task.py +126 -0
- control_tower/ct_runs/task_analysis/doc_audit/README.md +77 -0
- control_tower/ct_runs/task_analysis/doc_audit/audits/base.py +129 -0
- control_tower/ct_runs/task_analysis/doc_audit/audits/goal.py +95 -0
- control_tower/ct_runs/task_analysis/doc_audit/audits/sd.py +103 -0
- control_tower/ct_runs/task_analysis/doc_audit/cli.py +207 -0
- control_tower/ct_runs/task_analysis/doc_audit/heuristics/base.py +53 -0
- control_tower/ct_runs/task_analysis/doc_audit/heuristics/goal.py +215 -0
- control_tower/ct_runs/task_analysis/doc_audit/heuristics/sd.py +222 -0
- control_tower/ct_runs/task_analysis/doc_audit/prompts/goal/system.md +109 -0
- control_tower/ct_runs/task_analysis/doc_audit/prompts/goal/user.md +21 -0
- control_tower/ct_runs/task_analysis/doc_audit/prompts/sd/system.md +311 -0
- control_tower/ct_runs/task_analysis/doc_audit/prompts/sd/user.md +26 -0
- control_tower/ct_runs/task_analysis/doc_audit/sd_audit_labels.json +98 -0
- control_tower/ct_runs/task_analysis/doc_audit/task.py +227 -0
- control_tower/ct_runs/task_analysis/failure_type_audit/cli.py +138 -0
- control_tower/ct_runs/task_analysis/failure_type_audit/prompts/system.md +9 -0
- control_tower/ct_runs/task_analysis/failure_type_audit/prompts/user.md +21 -0
- control_tower/ct_runs/task_analysis/failure_type_audit/task.py +227 -0
- control_tower/ct_runs/task_analysis/goal_completeness/README.md +56 -0
- control_tower/ct_runs/task_analysis/goal_completeness/cli.py +125 -0
- control_tower/ct_runs/task_analysis/goal_completeness/prompts/system.md +46 -0
- control_tower/ct_runs/task_analysis/goal_completeness/prompts/user.md +60 -0
- control_tower/ct_runs/task_analysis/goal_completeness/task.py +358 -0
- control_tower/ct_runs/task_analysis/goal_completeness/viewer.py +142 -0
- control_tower/ct_runs/task_analysis/reward_hacking/README.md +46 -0
- control_tower/ct_runs/task_analysis/reward_hacking/cli.py +115 -0
- control_tower/ct_runs/task_analysis/reward_hacking/prompts/system.md +71 -0
- control_tower/ct_runs/task_analysis/reward_hacking/prompts/user.md +62 -0
- control_tower/ct_runs/task_analysis/reward_hacking/task.py +388 -0
- control_tower/ct_runs/task_analysis/reward_hacking/viewer.py +122 -0
- control_tower/ct_runs/task_analysis/rt_alignment/README.md +45 -0
- control_tower/ct_runs/task_analysis/rt_alignment/cli.py +119 -0
- control_tower/ct_runs/task_analysis/rt_alignment/prompts/system.md +50 -0
- control_tower/ct_runs/task_analysis/rt_alignment/prompts/user.md +57 -0
- control_tower/ct_runs/task_analysis/rt_alignment/task.py +387 -0
- control_tower/ct_runs/task_analysis/rt_alignment/viewer.py +128 -0
- control_tower/ct_runs/task_analysis/side_task_review/README.md +47 -0
- control_tower/ct_runs/task_analysis/side_task_review/cli.py +95 -0
- control_tower/ct_runs/task_analysis/side_task_review/prompts/review.md +29 -0
- control_tower/ct_runs/task_analysis/side_task_review/task.py +101 -0
- control_tower/ct_runs/task_analysis/validate_cover/README.md +32 -0
- control_tower/ct_runs/task_analysis/validate_cover/cli.py +97 -0
- control_tower/ct_runs/task_analysis/validate_cover/task.py +94 -0
- control_tower/ct_runs/task_analysis/validate_cover/validate_cover_prompt.md +35 -0
- control_tower/ct_runs/timing_report/__init__.py +17 -0
- control_tower/ct_runs/timing_report/cli.py +71 -0
- control_tower/ct_runs/timing_report/report.py +327 -0
- control_tower/ct_runs/traj_analysis/submit_guessability/README.md +49 -0
- control_tower/ct_runs/traj_analysis/submit_guessability/cli.py +125 -0
- control_tower/ct_runs/traj_analysis/submit_guessability/prompts/system.md +29 -0
- control_tower/ct_runs/traj_analysis/submit_guessability/prompts/user.md +72 -0
- control_tower/ct_runs/traj_analysis/submit_guessability/task.py +402 -0
- control_tower/ct_runs/traj_analysis/submit_guessability/viewer.py +159 -0
- control_tower/ct_runs/traj_analysis/submit_guessability_verified/README.md +52 -0
- control_tower/ct_runs/traj_analysis/submit_guessability_verified/cli.py +143 -0
- control_tower/ct_runs/traj_analysis/submit_guessability_verified/prompts/system.md +36 -0
- control_tower/ct_runs/traj_analysis/submit_guessability_verified/prompts/user.md +63 -0
- control_tower/ct_runs/traj_analysis/submit_guessability_verified/task.py +762 -0
- control_tower/ct_runs/traj_analysis/submit_guessability_verified/viewer.py +186 -0
- control_tower/datasets/__init__.py +0 -0
- control_tower/datasets/check.py +88 -0
- control_tower/datasets/cli.py +532 -0
- control_tower/datasets/external.py +648 -0
- control_tower/datasets/importers/__init__.py +42 -0
- control_tower/datasets/importers/eval_log.py +570 -0
- control_tower/datasets/importers/monitoring_bench.py +643 -0
- control_tower/datasets/importers/mythos_5_incident.py +243 -0
- control_tower/datasets/importers/shade_arena.py +590 -0
- control_tower/datasets/importers/sleight_bench.py +489 -0
- control_tower/datasets/model.py +45 -0
- control_tower/defaults.py +16 -0
- control_tower/environments/build.py +204 -0
- control_tower/environments/env_args.py +128 -0
- control_tower/environments/environment.py +130 -0
- control_tower/environments/internet_simulator/.dockerignore +2 -0
- control_tower/environments/internet_simulator/Dockerfile +91 -0
- control_tower/environments/internet_simulator/__init__.py +0 -0
- control_tower/environments/internet_simulator/__main__.py +4 -0
- control_tower/environments/internet_simulator/compose.yml +35 -0
- control_tower/environments/internet_simulator/constants.py +43 -0
- control_tower/environments/internet_simulator/core/assets.py +276 -0
- control_tower/environments/internet_simulator/core/generate_ca.py +68 -0
- control_tower/environments/internet_simulator/core/logs.py +241 -0
- control_tower/environments/internet_simulator/core/network_mapping.py +24 -0
- control_tower/environments/internet_simulator/core/tls.py +124 -0
- control_tower/environments/internet_simulator/core/trajectory.py +17 -0
- control_tower/environments/internet_simulator/core/webhook.py +213 -0
- control_tower/environments/internet_simulator/env_values.py +17 -0
- control_tower/environments/internet_simulator/malware/malware.c +143 -0
- control_tower/environments/internet_simulator/modules/forward_proxy.py +425 -0
- control_tower/environments/internet_simulator/modules/resolver.py +88 -0
- control_tower/environments/internet_simulator/modules/response_rules.py +192 -0
- control_tower/environments/internet_simulator/modules/shell_sessions.py +206 -0
- control_tower/environments/internet_simulator/modules/upstream_proxy.py +256 -0
- control_tower/environments/internet_simulator/modules/upstream_specs.py +200 -0
- control_tower/environments/internet_simulator/personas.py +150 -0
- control_tower/environments/internet_simulator/requirements.txt +4 -0
- control_tower/environments/internet_simulator/resolve_override.c +75 -0
- control_tower/environments/internet_simulator/runtimes/__init__.py +0 -0
- control_tower/environments/internet_simulator/runtimes/docker.py +2154 -0
- control_tower/environments/internet_simulator/runtimes/hawk.py +173 -0
- control_tower/environments/internet_simulator/runtimes/participants.py +30 -0
- control_tower/environments/internet_simulator/runtimes/provisioning.py +44 -0
- control_tower/environments/internet_simulator/server.py +600 -0
- control_tower/environments/list_utils.py +562 -0
- control_tower/environments/registry.py +302 -0
- control_tower/environments/remote/__init__.py +0 -0
- control_tower/environments/remote/json_model.py +82 -0
- control_tower/environments/remote/process.py +238 -0
- control_tower/environments/remote/remote.py +463 -0
- control_tower/environments/remote/sample.py +289 -0
- control_tower/environments/requirements.py +174 -0
- control_tower/environments/setting_runtime.py +173 -0
- control_tower/environments/settings.py +1222 -0
- control_tower/environments/settings.yml +24 -0
- control_tower/environments/settings_cli.py +642 -0
- control_tower/environments/source_utils.py +304 -0
- control_tower/environments/validate.py +141 -0
- control_tower/eval/README.md +38 -0
- control_tower/eval/__init__.py +11 -0
- control_tower/eval/blue.py +537 -0
- control_tower/eval/cli.py +563 -0
- control_tower/eval/dataset.py +810 -0
- control_tower/eval/dispatch.py +514 -0
- control_tower/eval/game_view.py +28 -0
- control_tower/eval/harness.py +151 -0
- control_tower/eval/history.py +224 -0
- control_tower/eval/metadata.py +43 -0
- control_tower/eval/monitor_cli.py +143 -0
- control_tower/eval/monitor_task.py +156 -0
- control_tower/eval/monitoring/__init__.py +5 -0
- control_tower/eval/monitoring/monitor.py +301 -0
- control_tower/eval/monitoring/posthoc.py +248 -0
- control_tower/eval/options.py +122 -0
- control_tower/eval/protocols_cli.py +140 -0
- control_tower/eval/red.py +328 -0
- control_tower/eval/registry.py +59 -0
- control_tower/eval/run.py +409 -0
- control_tower/eval/solver.py +167 -0
- control_tower/eval/task.py +54 -0
- control_tower/eval/task_messages.py +55 -0
- control_tower/eval/task_scorer_validation.py +90 -0
- control_tower/eval/task_selection.py +284 -0
- control_tower/eval/white.py +71 -0
- control_tower/eval_logs/__init__.py +0 -0
- control_tower/eval_logs/api_upload.py +216 -0
- control_tower/eval_logs/convert.py +1173 -0
- control_tower/eval_logs/io.py +211 -0
- control_tower/eval_logs/merge.py +93 -0
- control_tower/eval_logs/monitor_scores.py +13 -0
- control_tower/eval_logs/run_metadata.py +15 -0
- control_tower/eval_logs/s3_upload.py +167 -0
- control_tower/eval_logs/seat.py +85 -0
- control_tower/eval_logs/source.py +51 -0
- control_tower/eval_logs/utils.py +85 -0
- control_tower/fleet/__init__.py +1 -0
- control_tower/fleet/ami.py +62 -0
- control_tower/fleet/aws_identity.py +9 -0
- control_tower/fleet/bootstrap.sh +395 -0
- control_tower/fleet/controller.py +2359 -0
- control_tower/fleet/job.py +371 -0
- control_tower/fleet/run_job.py +253 -0
- control_tower/fleet/worker.py +1094 -0
- control_tower/fleet/worker_cli.py +68 -0
- control_tower/hawk/__init__.py +11 -0
- control_tower/hawk/config.py +92 -0
- control_tower/hawk/download.py +93 -0
- control_tower/hawk/middleman.py +56 -0
- control_tower/hawk/model_closure.py +33 -0
- control_tower/hawk/preflight.py +161 -0
- control_tower/hawk/pull_envs.py +95 -0
- control_tower/hawk/runner.py +1038 -0
- control_tower/hawk/sandbox.py +472 -0
- control_tower/hawk/yaml_generator.py +215 -0
- control_tower/inspect_tasks.py +271 -0
- control_tower/live/__init__.py +0 -0
- control_tower/live/agents.py +717 -0
- control_tower/live/assets/dev_container_extensions.json +3 -0
- control_tower/live/assets/pyrightconfig.json +4 -0
- control_tower/live/assets/settings.json +3 -0
- control_tower/live/cli.py +1045 -0
- control_tower/live/compose.py +272 -0
- control_tower/live/context/print_context.py +98 -0
- control_tower/live/context/task_getters.py +78 -0
- control_tower/live/context/traj_dir.py +326 -0
- control_tower/live/daemon.py +545 -0
- control_tower/live/integration_tests.py +96 -0
- control_tower/live/remote_sandbox.py +154 -0
- control_tower/live/rpc.py +55 -0
- control_tower/live/sandbox_ops.py +129 -0
- control_tower/live/score.py +366 -0
- control_tower/live/session.py +733 -0
- control_tower/live/setup.py +299 -0
- control_tower/live/task_state.py +139 -0
- control_tower/live/test.py +10 -0
- control_tower/live/traj/__init__.py +0 -0
- control_tower/live/traj/gen_traj.py +345 -0
- control_tower/live/traj/git_status.py +33 -0
- control_tower/live/traj/git_traj.py +748 -0
- control_tower/live/traj/git_traj_helpers/FORMAT.md +25 -0
- control_tower/live/traj/git_traj_helpers/call_insert.py +15 -0
- control_tower/live/traj/git_traj_helpers/call_str_replace.py +20 -0
- control_tower/live/traj/macros.py +117 -0
- control_tower/live/traj/solution.py +265 -0
- control_tower/live/traj/templ/add.py +14 -0
- control_tower/live/traj/templ/add_tool_call.py +98 -0
- control_tower/live/traj/templ/bash.py +128 -0
- control_tower/live/traj/templ/git_utils.py +93 -0
- control_tower/live/traj/templ/input_utils.py +145 -0
- control_tower/live/traj/templ/template_from.py +118 -0
- control_tower/live/traj/templ/tool_call_builder.py +119 -0
- control_tower/live/traj/tool_call_utils.py +289 -0
- control_tower/live/traj/traj.py +316 -0
- control_tower/live/tui/__init__.py +0 -0
- control_tower/live/tui/popup.py +293 -0
- control_tower/live/tui/tui.py +1237 -0
- control_tower/live/tui/tui.tcss +32 -0
- control_tower/live/usage/collect_testers.py +130 -0
- control_tower/live/usage/commands.py +313 -0
- control_tower/live/usage/full_env_context/container_script.py +230 -0
- control_tower/live/usage/full_env_context/full_env_context.py +68 -0
- control_tower/live/usage/migrate_traj.py +83 -0
- control_tower/live/usage/patch.py +64 -0
- control_tower/live/usage/test_all.py +1349 -0
- control_tower/live/usage/upload_helpers.py +31 -0
- control_tower/metrics/__init__.py +0 -0
- control_tower/metrics/audit.py +353 -0
- control_tower/metrics/control.py +110 -0
- control_tower/models/__init__.py +11 -0
- control_tower/models/aliases.py +59 -0
- control_tower/models/cache_scopes.py +26 -0
- control_tower/models/inspect_rates.py +32 -0
- control_tower/models/model_spec.py +284 -0
- control_tower/models/openrouter_provider.py +65 -0
- control_tower/models/pricing.py +168 -0
- control_tower/models/rates.py +290 -0
- control_tower/models/registry_hooks.py +166 -0
- control_tower/models/role_model.py +55 -0
- control_tower/models/usage_hooks.py +14 -0
- control_tower/monitors/__init__.py +36 -0
- control_tower/monitors/checklist.py +96 -0
- control_tower/monitors/jev.py +198 -0
- control_tower/monitors/jev_questions.yml +32 -0
- control_tower/monitors/monitor.py +466 -0
- control_tower/monitors/presets.yml +28 -0
- control_tower/monitors/prompts/action-no-goals.md +75 -0
- control_tower/monitors/prompts/action.md +83 -0
- control_tower/monitors/prompts/goal-analysis.md +77 -0
- control_tower/monitors/prompts/goals.md +82 -0
- control_tower/monitors/prompts/no-history.md +42 -0
- control_tower/monitors/prompts/orthogonal.md +107 -0
- control_tower/monitors/prompts/quick-no-goals.md +62 -0
- control_tower/monitors/prompts/quick.md +68 -0
- control_tower/monitors/prompts/simple.md +21 -0
- control_tower/monitors/prompts/strong-goals.md +104 -0
- control_tower/monitors/prompts/strong.md +95 -0
- control_tower/monitors/prompts/trajectory.md +18 -0
- control_tower/monitors/registry.py +19 -0
- control_tower/monitors/single_goal.py +96 -0
- control_tower/monitors/steps/checklist.md +48 -0
- control_tower/monitors/steps/single_goal.md +80 -0
- control_tower/monitors/steps/single_goal_analysis.md +76 -0
- control_tower/monitors/steps/three_scores.md +112 -0
- control_tower/monitors/three_scores.py +69 -0
- control_tower/policies/__init__.py +1 -0
- control_tower/policies/attack.py +199 -0
- control_tower/policies/honest.py +28 -0
- control_tower/policies/obvious_attack.py +69 -0
- control_tower/policies/prompts/attack.md +7 -0
- control_tower/policies/prompts/attack_planning.md +22 -0
- control_tower/policies/replay.py +227 -0
- control_tower/policies/scripted.py +123 -0
- control_tower/protocols/__init__.py +1 -0
- control_tower/protocols/block_all.py +43 -0
- control_tower/protocols/block_every_other.py +44 -0
- control_tower/protocols/defer_to_trusted.py +152 -0
- control_tower/protocols/null_blue.py +33 -0
- control_tower/protocols/prompts/trusted_editing.md +9 -0
- control_tower/protocols/resample.py +83 -0
- control_tower/protocols/trusted_editing.py +167 -0
- control_tower/protocols/trusted_monitoring.py +116 -0
- control_tower/protocols/watcher.py +341 -0
- control_tower/py.typed +0 -0
- control_tower/runs/README.md +85 -0
- control_tower/runs/convert.py +102 -0
- control_tower/runs/convert_eval_log_to_run.py +91 -0
- control_tower/runs/io.py +189 -0
- control_tower/runs/models.py +123 -0
- control_tower/runs/producer.py +4 -0
- control_tower/runs/runs_cli.py +679 -0
- control_tower/sandbox/__init__.py +6 -0
- control_tower/sandbox/_alpine_rootfs.py +230 -0
- control_tower/sandbox/_ssh.py +426 -0
- control_tower/sandbox/cli.py +301 -0
- control_tower/sandbox/constants.py +33 -0
- control_tower/sandbox/docker.py +1024 -0
- control_tower/sandbox/docker_in_vm.py +517 -0
- control_tower/sandbox/firecracker.py +413 -0
- control_tower/sandbox/inspect_tools.py +39 -0
- control_tower/sandbox/intranet.py +226 -0
- control_tower/sandbox/intranet_guard/Dockerfile +4 -0
- control_tower/sandbox/intranet_guard/firewall.sh +110 -0
- control_tower/sandbox/qemu.py +292 -0
- control_tower/sandbox/replay/__init__.py +22 -0
- control_tower/sandbox/replay/environment.py +435 -0
- control_tower/sandbox/replay/score.py +37 -0
- control_tower/sandbox/shared_containers.py +325 -0
- control_tower/sandbox/simulated/__init__.py +20 -0
- control_tower/sandbox/simulated/actions.py +90 -0
- control_tower/sandbox/simulated/compose.py +166 -0
- control_tower/sandbox/simulated/environment.py +182 -0
- control_tower/sandbox/simulated/judge.py +159 -0
- control_tower/sandbox/simulated/prompts/judge.md +31 -0
- control_tower/sandbox/simulated/prompts/simulator.md +74 -0
- control_tower/sandbox/simulated/prompts/simulator_no_exec.md +22 -0
- control_tower/sandbox/simulated/scratch/compose.yaml +42 -0
- control_tower/sandbox/simulated/session.py +156 -0
- control_tower/scanners.py +123 -0
- control_tower/task/__init__.py +0 -0
- control_tower/task/collection.py +64 -0
- control_tower/task/final_diff.py +88 -0
- control_tower/task/sample_metadata.py +35 -0
- control_tower/task/scorers.py +351 -0
- control_tower/task/select_utils.py +106 -0
- control_tower/task/selectors.py +49 -0
- control_tower/task/setup.py +96 -0
- control_tower/task/space.py +21 -0
- control_tower/task/strat_sets/model_registry_strats.txt +11 -0
- control_tower/task/task.py +456 -0
- control_tower/task/task_sets/__init__.py +176 -0
- control_tower/task/traj_dir.py +39 -0
- control_tower/task/workspace_baseline.py +581 -0
- control_tower/tools/__init__.py +0 -0
- control_tower/tools/answer.py +53 -0
- control_tower/tools/bash_tools.py +37 -0
- control_tower/tools/declarations.py +23 -0
- control_tower/tools/diff_display_text_editor.py +66 -0
- control_tower/tools/registry.py +37 -0
- control_tower/tools/split_text_editor_tools.py +125 -0
- control_tower/tools/submit.py +35 -0
- control_tower/tools/task_tools.py +237 -0
- control_tower/tools/tool_calls.py +46 -0
- control_tower/tools/tool_registry.py +371 -0
- control_tower/trajectories/README.md +76 -0
- control_tower/trajectories/__init__.py +0 -0
- control_tower/trajectories/action.py +122 -0
- control_tower/trajectories/api.py +615 -0
- control_tower/trajectories/eval_trace_metadata.py +15 -0
- control_tower/trajectories/extended_thinking.py +56 -0
- control_tower/trajectories/format_history.py +119 -0
- control_tower/trajectories/from_eval_log.py +323 -0
- control_tower/trajectories/io.py +205 -0
- control_tower/trajectories/models.py +25 -0
- control_tower/trajectories/mongo_docs.py +392 -0
- control_tower/trajectories/monitor_score.py +159 -0
- control_tower/trajectories/sample.py +328 -0
- control_tower/trajectories/task_scores.py +108 -0
- control_tower/trajectories/text_editor_output.py +69 -0
- control_tower/trajectories/timing_utils.py +419 -0
- control_tower/trajectories/tool_calls.py +40 -0
- control_tower/trajectories/utils.py +74 -0
- control_tower/trajectories/viewer_api.py +578 -0
- control_tower/trajectories/viewer_auth.py +50 -0
- control_tower/trajectories/workspace_diff.py +16 -0
- control_tower/utils/__init__.py +0 -0
- control_tower/utils/cli_args.py +20 -0
- control_tower/utils/direct_api_generator.py +151 -0
- control_tower/utils/docs_utils.py +15 -0
- control_tower/utils/env.py +163 -0
- control_tower/utils/file_patterns.py +92 -0
- control_tower/utils/fs.py +137 -0
- control_tower/utils/function_registry/__init__.py +0 -0
- control_tower/utils/function_registry/arg_type.py +41 -0
- control_tower/utils/function_registry/base_model.py +63 -0
- control_tower/utils/function_registry/custom_handlers.py +99 -0
- control_tower/utils/function_registry/display.py +137 -0
- control_tower/utils/function_registry/function_info.py +97 -0
- control_tower/utils/function_registry/naming.py +5 -0
- control_tower/utils/function_registry/registry.py +419 -0
- control_tower/utils/function_registry/selection.py +68 -0
- control_tower/utils/function_registry/signature_info.py +78 -0
- control_tower/utils/function_registry/validation.py +97 -0
- control_tower/utils/imports.py +35 -0
- control_tower/utils/paths.py +98 -0
- control_tower/utils/sanitize.py +74 -0
- control_tower/utils/serde.py +151 -0
- control_tower/utils/suggestions.py +25 -0
- control_tower/utils/time.py +18 -0
- control_tower/utils/transcript.py +20 -0
- control_tower/utils/versioning.py +383 -0
- control_tower-6.0.0rc3.dist-info/METADATA +88 -0
- control_tower-6.0.0rc3.dist-info/RECORD +473 -0
- control_tower-6.0.0rc3.dist-info/WHEEL +5 -0
- control_tower-6.0.0rc3.dist-info/entry_points.txt +5 -0
- control_tower-6.0.0rc3.dist-info/licenses/LICENSE +21 -0
- control_tower-6.0.0rc3.dist-info/top_level.txt +1 -0
|
@@ -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
|