Development Setup
main is the active integration branch and is not stable, so use it for development only. Releases are published to PyPI and as GitHub release tags after checks pass.
Clone the repository and set up the development environment:
git clone git@github.com:linuxarena/control-tower.git cd control-tower make init
make init runs uv sync --locked --dev --all-extras, creates .env from .env.example, activates the repo git hooks, and offers to install shell tab completions and an alias ct='uv run ct'.
Edit .env to add at least one LLM API key, such as ANTHROPIC_API_KEY or OPENAI_API_KEY.
Generate the model rate table so eval costs are priced rather than unknown:
uv run python scripts/generate_model_rates.py
The file lands at ~/.cache/control-tower/model_rates.json. Override the path with CONTROL_TOWER_MODEL_RATES in .env if needed. CONTROL_TOWER_PRICING=api prices from the deployment's GET /api/pricing instead, and CONTROL_TOWER_PRICING=off loads no table at all: model calls still run and still bill, but every one is recorded as unknown cost (see Cost Tracking).
Re-running the script merges into that file rather than replacing it: published rates are refreshed, new models are added, and a model the source has stopped publishing keeps the rate it last had (the script reports how many). To price a model the source does not carry, add the row by hand and mark it "manual": true; the script leaves those rows alone. The script refuses to write a table that would not load back, so it reports a typo in a hand-added row.
{ "model": "our-internal-model", "manual": true, "pricing": {"input_per_million": 3.0, "output_per_million": 15.0, "cached_input_per_million": 0.3} }
Optional dependencies
The default install covers running evals and monitors. Heavier features ship as extras, e.g. uv sync --extra viz:
| Extra | Enables |
|---|---|
viz | ct game-metrics plots and the action-monitor web UI (matplotlib, plotly, seaborn, flask-cors) |
docent | ct docent and Docent uploads |
hf | Hugging Face dataset import (pyarrow) |
providers | Google and xAI model SDKs (Anthropic, OpenAI, and OpenRouter work without this) |
vagrant | Vagrant sandbox used by the docker_sandbox_breakout setting |
Pulling Settings
Settings are directories under .settings/ that group one or more environments—realistic software systems that agents are evaluated in (e.g., a Flask API, a Clojure job scheduler, a C++ network monitor). The .settings/ directory will be empty after a fresh clone. Each setting is one repo under the linuxarena GitHub org, and decides for itself which environments it contains; you can also create your own. To pull official ones:
ct settings pull # every declared setting + recorded repos and links ct settings pull linuxarena # one setting ct settings pull linuxarena@<ref> # pin it per-machine
Check installed environments and their status:
ct settings status
ct settings pull clones public repos anonymously with git clone and falls back to gh repo clone for private ones, so the GitHub CLI (gh) must be installed and authenticated only for private repos. The registry lists only repositories anyone can clone, so a plain pull works without credentials. A setting outside the registry, such as a private one, is pulled by repository: ct settings pull linuxarena/ct-linuxarena-private clones it at its repo name and records it in .settings.local.yml, so every later plain pull updates it too. A setting that fetches environments of its own declares that step as [tool.control_tower] install in its pyproject.toml, and the pull runs it after cloning.
LinuxArena is three settings: linuxarena is the open benchmark suite, linuxarena-extras holds open environments outside the suite, and linuxarena/ct-linuxarena-private holds the held-out environments.
Tracked settings are defined in src/control_tower/environments/settings.yml, one repo per setting. Per-machine pins and links live in .settings.local.yml at the repo root (pins: / links:), written by the CLI and hand-editable.
ct settings pull skips a setting whose working tree has uncommitted changes or untracked files; commit, stash, or ct settings remove it to resume updates. The environments inside a setting are managed by the setting's install step, which runs on every pull; LinuxArena's sync.py leaves a dirty environment clone alone and reports it.
ct settings pull --global pulls into a machine-level store at ~/.cache/control-tower/settings (honoring XDG_CACHE_HOME) instead of this checkout's .settings/. A checkout with no .settings/ of its own resolves environments from that store, so new checkouts and git worktrees of Control Tower or any consumer repo need no pull of their own while the store is current. A local .settings/ takes precedence over the store, and CONTROL_TOWER_SETTINGS_DIR overrides both. Every ct settings command resolves the same way, so in a checkout without .settings/ a plain ct settings pull updates the store; create an empty .settings/ first to give a checkout its own copies.
ct settings pull <setting>@<ref> pulls at that tag, branch, or SHA and records the pin for this machine; later pulls keep the setting there until you release it:
ct settings pull linuxarena@<ref> # pin a setting ct settings pull linuxarena@default # release, back to the declared ref
Team-wide pins are ref: edits in settings.yml, reviewed in a pull request (PR). To also pin the prebuilt Docker image, set ENV_IMAGE_TAG_<ENV> in .env.
For a local environment with no GitHub repo, ct settings pull ./my_env symlinks the directory into .settings/ and records the link, so it survives future pulls. The source directory is never modified. ct settings remove my_env unlinks it.
Local Services
A single Docker Compose file spins up support services such as MongoDB, the web viewer, and the inspect logs viewer:
docker compose up -d --build
See Web Viewer, Inspect Logs Viewer, and Docent for service ports, credentials, and environment variables.
Shell Tab Completion
make init offers to install tab completion. To run the completion installer on its own:
uv run python scripts/setup_completions.py
This installs a marked block in ~/.zshrc and/or your bash rc file that loads completions in new shells.
To uninstall, run:
uv run python scripts/setup_completions.py --uninstall
Agent Skills
Domain skills (running-evals, monitoring, making-*, safety-curves, trajectories, control-concepts, validate-straj) live in .agents/skills/ and are symlinked into .claude/skills/.
The dev-workflow skills (pr, babysit, ship, deliver, ct-box) ship as the control-tower-dev plugin from the control-tower-plugins marketplace; this repo's committed .claude/settings.json installs it when you trust the repo and auto-updates it at startup. Other plugins are opt-in: /plugin install control-research@control-tower-plugins. For anything beyond that (developing, publishing, or adding plugins), see that repo's README.
Codex discovers .agents/skills/ natively and consumes the same marketplace, once per machine: codex plugin marketplace add linuxarena/control-tower-plugins, then codex plugin add control-tower-dev@control-tower-plugins. Codex does not auto-update marketplace snapshots, so refresh with codex plugin marketplace upgrade control-tower-plugins.
Editing control-tower from a downstream project
To iterate on control-tower while working in a downstream repo (basharena, research, etc.), from inside the downstream project root:
uv add --editable /path/to/control-tower
To switch back when done:
uv add 'control-tower @ git+https://github.com/linuxarena/control-tower.git@main'