The hawk-values contract

Every env that runs on hawk (ct run eval --hawk) carries a hand-authored hawk-values.yaml in codebase/, alongside its Dockerfile and compose.yml:

<env-repo>/
  codebase/
    Dockerfile          # shared with local-docker evals
    compose.yml         # drives local-docker evals
    hawk-values.yaml    # hand-authored K8sSandboxEnvironmentValues; drives the hawk path
  main_tasks/
  side_tasks/

This file is the contract between env authors and CT. CT performs exactly two substitutions on it at runner-task-start; everything else is passed verbatim to the hawk K8s sandbox chart (inspect_k8s_sandbox).

Required header

Every hawk-values.yaml starts with:

# hawk-values for <env_id> — see docs/hawk-values-contract.md for the contract. # CT substitutes ${CT_IMAGE_REGISTRY_PREFIX} and ${HAWK_ENV_TAG} at submission time.

Image tags

Each env's images are tagged with that env's commit SHA: the operator publishes at the SHA of the env's .settings/ checkout (see "Env-image registry" in ops/docs/hawk-deployment.md), and ct run eval --hawk resolves ${HAWK_ENV_TAG} to the same SHA, so the two match by construction. Setting HAWK_ENV_TAG in .env pins every env in a run to that one tag. The shared internet-simulator image is tagged latest.

Substitution rules

PlaceholderResolved fromWhen
${CT_IMAGE_REGISTRY_PREFIX}User's .env (passed via eval-set YAML to runner pod)Runner-task-start, before K8sSandboxEnvironmentConfig(values=Path(...)) is constructed
${HAWK_ENV_TAG}The env's commit SHA (see Image tags); HAWK_ENV_TAG in .env overridesSame

Every other ${...} token is passed through verbatim. CT writes the substituted file as a sibling: hawk-values.rewritten.yaml. The sandbox spec points at the rewritten file. The original stays on disk for debugging.

Image reference convention

  • CT-built env images: ${CT_IMAGE_REGISTRY_PREFIX}/env-<envname>:${HAWK_ENV_TAG}
  • Internet-simulator: ${CT_IMAGE_REGISTRY_PREFIX}/env-internet-simulator:latest
  • Third-party images (postgres, redis, nginx, etc.): literal pull strings, no substitution. Must be public-DockerHub-pullable (kubelet anonymous pull) OR also in the operator's registry.

Worked example: clinical_trial

Local-docker shape (codebase/compose.yml):

services: default: image: linuxarena/env-clinical_trial:${ENV_IMAGE_TAG_CLINICAL_TRIAL:-latest} build: ... depends_on: postgres: condition: service_healthy volumes: - ./init/db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro postgres: image: postgres:16.12 environment: POSTGRES_DB: vaccine_trial POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres

K8s shape (hawk-values.yaml):

# hawk-values for clinical_trial — see docs/hawk-values-contract.md for the contract. # CT substitutes ${CT_IMAGE_REGISTRY_PREFIX} and ${HAWK_ENV_TAG} at submission time. services: default: image: ${CT_IMAGE_REGISTRY_PREFIX}/env-clinical_trial:${HAWK_ENV_TAG} command: - /bin/bash - -c - /app/restart.sh & exec tail -f /dev/null readinessProbe: exec: command: [sh, -c, "pg_isready -h postgres -U postgres"] initialDelaySeconds: 5 periodSeconds: 2 postgres: image: postgres:16.12 env: - { name: POSTGRES_DB, value: vaccine_trial } - { name: POSTGRES_USER, value: postgres } - { name: POSTGRES_PASSWORD, value: postgres } - { name: INIT_SQL_B64, value: <base64-encoded-init.sql> } command: - /bin/sh - -c - printf %s "$INIT_SQL_B64" | base64 -d > /docker-entrypoint-initdb.d/init.sql && exec docker-entrypoint.sh postgres readinessProbe: exec: command: [sh, -c, "pg_isready -U postgres"]

Both files coexist in codebase/. compose.yml drives local-docker evals; hawk-values.yaml drives the hawk path.

Never declare fixed-name additionalResources. Every sample in an eval-set is its own Helm release in a shared namespace, so a resource with a hardcoded metadata.name (a ConfigMap, say) is fought over by every concurrent release — the second sample errors with a Helm ownership conflict (invalid ownership metadata; ... release-name must equal). The chart runs additionalResources through tpl, so {{ .Release.Name }}-scoped names render fine — but the services.*.volumes entries that would reference such a resource are rendered verbatim (no templating), so a release-scoped ConfigMap cannot be mounted. Ship file content some other way: bake it into the env image, or carry it as a base64 env var written out by the service command (as INIT_SQL_B64 above). The only safe additionalResources are ones whose names CT itself uniquifies per sample (the Docker Hub pull secret).

Internet-simulator pattern

Envs that need the internet simulator declare it as a sibling service in their own values. No CT auto-injection:

services: default: # ... env vars to talk to IS, agent-side startup wrapping ... env: - { name: IS_BASE_URL, value: "http://internet-simulator" } - { name: LB_PERSONA_HOSTS, value: "filebin.net packages-cdn.com" } command: - /bin/bash - -c - | # Wait for IS, write /etc/hosts overrides, register personas, then run env startup until curl -sf "$IS_BASE_URL/health"; do sleep 1; done IS_IP=$(getent hosts internet-simulator | awk '{print $1}') for h in $LB_PERSONA_HOSTS; do echo "$IS_IP $h" >> /etc/hosts; done curl -sfX POST "$IS_BASE_URL/personas" -H 'Content-Type: application/json' -d '...' /app/restart.sh & exec tail -f /dev/null internet-simulator: image: ${CT_IMAGE_REGISTRY_PREFIX}/env-internet-simulator:latest securityContext: capabilities: add: [SYS_ADMIN] readinessProbe: exec: command: [sh, -c, "curl -sf http://localhost/health"] initialDelaySeconds: 2 periodSeconds: 1

This is one IS pod per cell — i.e., one IS pod per sample. No pooling across cells.

Validation

Two layers, both required:

  1. Local helm template (every env author runs locally before merging; env-repo CI runs the same):

    helm template test-release <chart-path> \ --set CT_IMAGE_REGISTRY_PREFIX=example.invalid \ --set HAWK_ENV_TAG=test \ -f hawk-values.yaml

    Exits 0. Catches typos, indent bugs, dangling placeholders. Chart path: bundled with inspect_k8s_sandbox at .venv/lib/python*/site-packages/k8s_sandbox/resources/helm/agent-env/.

  2. End-to-end smoke (after the env's images are published):

    ct run eval --hawk -e <env> -t <one-cheap-main-task> \ --untrusted-policy honest --untrusted-policy-arg model=anthropic/claude-sonnet-4-6 \ --protocol untrusted-agent --protocol-arg max_steps=10 \ --sample-time-limit 600

    .eval file lands locally with status success. This is the definition of done for a new env.