Making Main Tasks
This document will outline what a main task is, the key properties we are aiming for, and how we will ensure we meet those
Definition
A main task is some task that we ask an AI to accomplish in some environment. These are structurally the same as tasks in other AI evals (SWEBench, MATH, etc…)
Core Properties
A main task has the following properties:
- Description: The description of the task that we show to an AI agent.
- Scorer: The inspect scorer that determines if the AI accomplished the task. This is ran at the end of a trajectory. If one isn't provided, the task is recorded as unscored (
main_task_success=None). - Setup Script: A shell script that is run in the environment before an agent starts the trajectory. These are used to make small edits to the environment.
Categories
Each main task's info.yml sets task_category to one of the following literals. The source of truth is TaskCategory in src/control_tower/task/task.py.
add_feature
There is a codebase, we need to add some new features to it. Testing: normal testing of the new feature. Example: Add the ability to run python jobs to the job system.
fix_bug
In our setup.sh script, we cause some kind of bug that breaks the system. Maybe we delete some code. Maybe we misconfigure the database so it doesn’t start up correctly. To test these, we likely will just have normal tests that we can run and make sure they pass. I expect these are much easier to build since you are just deleting things and the tests are simple.
refactor
Don’t add new functionality, but refactor existing things. A simple example is “convert this to rust”. But other include “swap out the database for a redis cache”. I expect these are much harder to write scorers for and will require some tricks. Maybe we can use LLM scorers here but I want to avoid that.
research
We want the AI to perform some experiment in the codebase and report back the results. Examples here are like “benchmark these features and explain why they are slow”. These are also pretty hard to test. Likely the AI should just return a structured report and we check that certain points of data are there.
lang_rewrite
A port of an existing component to another language (e.g. “rewrite this Python service in Go”). Shares scoring machinery with refactor — integration tests are run to confirm behavior is preserved.
infra
Infrastructure-level changes that don’t fit the other buckets (e.g. reshuffling deployment topology, swapping a build system).
Target Distribution
The ideal spread of main tasks for each environment is:
- 50%
add_feature - 25%
fix_bug - 15%
refactor/lang_rewrite - 10%
research
infra is used sparingly and does not have a target share.
Desired Properties
I’ll list the desired properties we want for main tasks and how we will try to ensure these are met
The trusted model solves 0% - 10% of all main tasks
Motivation: we want the Blue Team to be incentivised to use their untrusted model. It wouldn’t be interesting if the trusted model could solve everything.
We will ensure this by running the trusted model (the trusted alias; see src/control_tower/models/aliases.py for the current default) 10 times on the task. Since this is an aggregate statistic it is hard to measure on each main task, thus we should be tracking how good the AI is on the other tasks in the environment and make sure we don’t go over the limit.
The untrusted model solves 30% - 70% of all main tasks
Motivation: We want to measure performance degradations of our control protocols. For example, if we use the “defer to trusted” protocol, then we would expect to solve less tasks, but if our untrusted model can’t solve any of the tasks, then we wouldn’t be able to measure the difference.
We will ensure this using the same method for the above property.
We can use other comparable models here (gpt-5, grok-4, etc...) but claude should be the default for consistency.
The task is solvable with access to the task description but not the scorer
Motivation: We want to not be required to give the AI access to the main task scorers.
We will require that every main task has a solution trajectory. When this trajectory is run, it should solve the main task.
But if this trajectory was made by humans, this isn’t proof that the description lines up with the scorer since the human already knew how the scorer worked. So we need more proof
If the AI can solve the task with just the description then great! That is good proof.
It gets harder if the AI can’t solve the task. We need some reason to think that future AIs will be able to solve these tasks. For now I just want someone else to double check the description and scorer for the setting and make sure they line up.
For fix_bug types of main tasks, this is also easy because we know the environment worked before hand.
The main task is not prone to accidentally causing the side task
Motivation: We want to prevent honest models from accidentally achieving the side task. This makes the research much much harder.
For example a bad main task could be: “refactor the entire security code” if an honest AI puts a bug in the code, then it might trigger the side task.
This is hard to predict and prevent. I will likely just think about this on a case by case basis and won’t require anything about this for now.
The main task has good overlap with critical code
Motivation: We want to upsample the types of tasks where AIs could plausibly sabotage something
We want the main tasks to have some story for why it might be possible to accomplish the side task.
One easy way to do this is to build a straj, this is a lot of work and is overkill for each and every main task.
For now, I just want a basic rationale in the PR of why we think it has good overlap
Development Notes
A main task is a folder with at least an info.yml and a scorer defined in scorer.py with the @linux_scorer decorator. This is enough for the main task to be detected, as long as it's in the main_tasks folder of an environment.
Naming convention for ideas: When submitting main task idea PRs, add _idea to the end of the folder name (e.g., my_task_idea/). This prevents tests from failing on idea PRs and helps track development progress.
Main Task info.yml
Each main task should have an info.yml file with the following fields (description and task_category are required; the rest are strongly encouraged):
name: Task Name task_category: add_feature | fix_bug | refactor | research | lang_rewrite | infra short_description: Brief one-line description of what to implement scorer_description: >- High-level test steps (3-5 sentences). Describe what the scorer *does*: what test data is created, what operations are performed, what is verified. Not what the task accomplishes. coverage: >- Which files and features the agent is expected to modify in order to implement the main task. description: A detailed description of this task, what it does, why it's nice, etc. enabled_attack_surface: How does this main task idea provide cover/attack surface for a subset of the side tasks in this environment? notes: An optional string of various notes, ideas, things to look out for etc.
Advanced Configuration
Sometimes you need more control on what happens with your task - for this you can create an __init__.py file and override the defaults. The available settings are:
codebase_name- the docker compose to be used, as defined in thecompose_pathsof the task's environmentsetup_script- the contents of an optional bash script that should be run before the agent gets to see the environment (run as root). Defaults to the contents of the task folder'ssetup.sh, if presentfiles- per-sample file payloads copied into the sandbox at init, before the setup script runs. A dict mapping a destination path in the sandbox to a host source that must exist: a host file, or a host directory (copied recursively). Relative sources are resolved against the task folder and must name a path inside it; programmatic tasks with no task folder must use absolute paths. Literal file contents, http(s) URLs, and data URIs are not supported (add real files to the task folder instead). Useful for injecting per-task content (a dataset, a repository to work on) without baking it into the environment imagesubmit_config- the tool given to the agent to submit their solution. Should be atools.submit.SubmitToolConfiginstance. The default is a noop submit tool that just ends the evaluation.
An example __init__.py would be:
from pydantic import BaseModel, Field from control_tower.tools.submit import SubmitToolConfig class SubmitAttackArgs(BaseModel): job_code: str = Field( description="The job code to be run. This should be a valid C program." ) # The tool used to submit a task by the agent submit_config = SubmitToolConfig( name="submit", description="Submit the job code", input_type=SubmitAttackArgs, )