Cortex uses three independently operated roles:
| Role | Runtime | Responsibility |
|---|---|---|
| master | Docker Compose | public gateway, Proof, challenge containers and durable epoch emission |
| validator | Docker Compose | independent bundle verification, peer roots and Bittensor submission |
| Proof VM host | systemd on a KVM machine | Firecracker topic and experiment VMs |
The gateway exists only on the master. A validator never receives challenge signing keys and never executes miner code. Production runs the VM host on a dedicated machine reachable from the master over a private network and HTTPS.
- Validate the deployment source
- Build and publish the Python image
- Master
- Challenge containers
- Validator
- Proof VM host
- Promotion and rollback
uv run python scripts/check_deploy.py --check-examplesThe contract check requires digest-pinned images, read-only non-root application containers, dropped capabilities, explicit bind addresses, private file mounts, durable state and strict separation between master and validator credentials. It does not start a service.
deploy/Dockerfile has two final targets:
runtimeincludes the chain dependency and thecortexentry point for master and validator;guestexcludes Bittensor and bootsproof-guest-initfor conversion into a Firecracker rootfs.
The base image is an exact Python 3.12 Bookworm digest. On every push to main,
.github/workflows/images.yml waits for CI, builds the runtime once, exercises
it without network access, generates a CycloneDX SBOM, rejects fixable high or
critical vulnerabilities, publishes the tested bytes to GHCR under the source
commit and records GitHub build provenance. Annotated release tags only alias
that existing attested digest; they never rebuild it. The privileged workflow
has no manual entry point. It does not build or publish the unwired Proof guest,
boot init, Firecracker or KVM. Do not deploy a local tag or copy a digest from
another build.
docker build --file deploy/Dockerfile --target runtime \
--build-arg PYTHON_IMAGE='python:3.12-slim-bookworm@sha256:<verified-digest>' \
--tag cortex-python:test .
docker run --rm --network none --read-only --cap-drop ALL cortex-python:test --helpThe guest rootfs and kernel are operator artifacts. Build the Python guest
stage and convert it to ext4 with deploy/guest/bake-rootfs.sh. The builder
normalizes filesystem time and ext4 metadata, verifies the result with
e2fsck, names it from the SHA-256 of the final bytes and never writes a live
pin. Follow the guest rootfs procedure,
install the measured image under /var/lib/proof/images, and put only that
value in host.toml. The repository contains no fabricated rootfs or kernel
digest.
Create deploy/env/master.env from the example and set the primary Bittensor
network plus optional bounded wss:// fallback RPC origins, then set:
CORTEX_IMAGEto the publishedrepository@sha256:<64 hex>;- the master VPC bind address and netuid;
BASE_CHALLENGE_SECRETS_HOST_DIR, the absolute host directory of challenge bearers, andBASE_DOCKER_GID, the group owning/var/run/docker.sock;- the Proof VM URL, CA, exact rootfs digest, signed inference-offer commitment and registered custom IDs when Proof custom topics should open;
- explicit
PROOF_RLM_VM_VCPUS,PROOF_RLM_VM_MEM_MIBandPROOF_RLM_VM_DISK_MIBvalues that fit the VM host ceilings and capacity.
Prepare a private directory, owned so container UID 65532 can read it:
deploy/secrets/master/
gateway.key
proof.key
bounty.key # one <id>.key per trusted container challenge
operator.token
proof-vm.token
proof-vm-ca.pem
Seeds are raw 32 bytes or 64 hexadecimal characters. Bearers are nonempty opaque values. Files are regular,
not hardlinks or symlinks, and mode 0400 or 0600. The Proof VM token and CA are
needed only when PROOF_VM_ORCHESTRATOR_URL is set.
The trust bind defaults to config/ and contains owner.pubkey, both signed
TOML documents and their adjacent .sig files. Verify them before startup with
the offline ceremony.
docker compose --project-directory . --env-file deploy/env/master.env \
-f deploy/compose/role-master.yml --profile master up -dThe state volume contains gateway seals, Proof jobs/topics and the epoch
journal. Each challenge keeps its own state in the cortex-challenge-<id>-data
volume. Back it up as SQLite state, including WAL, through a quiesced copy
or SQLite's backup API. A filesystem copy of only the main database while the
service writes is not a valid backup.
The master role runs a second service, challenge-supervisor, which is the only
Cortex process holding the Docker socket. It reads
deploy/challenges/registry.toml, and for each entry it:
- pulls the channel or pin;
- checks the slug, contract and source labels and the GitHub build provenance;
- runs a secretless canary;
- replaces the container
cortex-challenge-<id>on the privatecortex-challengesnetwork; - rolls back when the new digest does not answer
/version.
The gateway joins that network as cortex-master, polls get_weights once per
completed epoch and proxies public routes under /challenge/<id>/. The full
contract is in docs/CHALLENGES.md.
Per challenge, on the master host:
dir="$BASE_CHALLENGE_SECRETS_HOST_DIR/bounty"
install -d -m 0700 -o 65532 -g 65532 "$dir"
(umask 077; openssl rand -hex 32 >"$dir/internal.token"; openssl rand -hex 32 >"$dir/admin.token")
chown 65532:65532 "$dir"/*.tokenPut the matching leaf seed at deploy/secrets/master/<id>.key (its public key
is the trust-root row) and any challenge settings in the registry env table,
for example BOUNTY_BACKEND_PUBLIC_URL. Challenge-specific secrets, such as
Bounty's session.key, go next to the tokens; see each challenge's operator
guide (Bounty,
OpenType).
A registered id that the trust root does not list runs without emission. Use this
burn-in to check GET /challenge/<id>/version and the logs. Then activate it with
a signed algorithm 3 profile.
A trusted id that is missing, unhealthy or invalid burns its share.
Operator switches:
channel = "edge"followsmain;pin = "sha256:..."freezes a digest;attestation = falseis for local images only;- deleting a registry row stops that container and keeps its volume.
The master re-reads the registry when the file changes, and the supervisor re-reads it every 15 seconds.
Create deploy/env/python-validator.env and set the exact master HTTPS origin,
netuid, independently pinned gateway public key, chain network, wallet name and
hotkey, trust-version minimums, live subnet version_key, VPC peer bind address
and private host paths. Optional fallback RPCs are a JSON list of at most eight
credential-free wss:// URLs.
The wallet directory is mounted read-only at /run/wallets. The validator
identity directory contains:
consensus.key
tls.crt
tls.key
The trust directory additionally contains peers.json, a JSON object mapping
each validator public-key hex string to one SAN-valid HTTPS origin. Do not run
two validators with the same wallet: their commit/reveal or rate-limit state can
conflict.
docker compose --project-directory . --env-file deploy/env/python-validator.env \
-f deploy/compose/role-validator.yml up -dEnable chain submission only after the master returns a current sealed: true
bundle and the validator --verify-only --once preflight
reports validator outcome=verified for the same root/vector from historical
chain state. The unsealed UID0 fallback is a readiness failure, not a weight to
submit.
The host requires Linux, /dev/kvm, Firecracker, jailer, nftables/network
namespace support, a reviewed kernel and one or more measured ext4 images. Install
the locked Cortex wheel in /opt/base/venv, copy
deploy/env/proof-vm-host.toml.example to /etc/proof-vm/host.toml, and fill
every blank pin from exact local bytes or a signed offer.
Private /etc/proof-vm files include the rotating master bearer, TLS private key,
OpenRouter key, signed inference offer and optional knowledge-approval public
key. The TLS certificate must cover every hostname or IP used in the master's
URL. The host binds one explicit address; the example loopback value is not a
production wire.
The [images] table maps each raw 64-hex rootfs digest to its absolute installed
path. [host].kernel_digest binds the kernel. custom_ids is empty by default;
only listed IDs can open custom topics. [egress] permits exact topic-setup
IPv4/port pairs. Experiment VMs receive no network regardless of that list.
For the local KVM smoke, plan for a dedicated host with at least 4 vCPU, 8 GiB RAM
and 80 GiB storage, with working KVM support. This is a sizing starting point,
not evidence that a particular host can boot the guest or run a research topic.
In the local copy of host.toml, set:
[host]
# Keep the other required host fields from the example.
max_topics = 1
max_experiments = 1
[caps]
vcpus = 1
mem_mib = 1024
disk_mib = 16384Both master environment examples request this same per-VM shape. The resource settings apply to the persistent topic VM and each fresh experiment VM, so leave capacity for both at once. Reserve additional space for the OS, installed images and retained failed experiments. Larger topics need explicit capacity planning; the general host template retains its larger ceilings and topic count.
Requests above host ceilings fail rather than being clamped. A restarted master cannot attach to an existing topic VM with a different image or resource shape; changing environment values does not resize it. Resource ranges and omitted-value defaults are in the configuration reference.
Run the local preflight before enabling the host service:
sudo /opt/base/venv/bin/cortex vm-host --config /etc/proof-vm/host.toml --checkThe JSON report exits successfully only when all local prerequisite checks pass.
It requires effective UID 0, matching the service and jailer, and checks that
cp, chown and mkfs.ext4 resolve to safe executable files. Only when topic
egress is configured does it also check ip, nft and sysctl, and verify that
/dev/net/tun opens read/write as a character device. It creates no TAP interface.
It does not create state, boot VMs, execute binaries or contact inference/provider
services. A passing preflight is not live execution evidence.
The separate smoke explicitly starts real Firecracker guests on that host:
sudo /opt/base/venv/bin/python -m cortex.vm.smoke --run-live \
--config /etc/proof-vm/host.toml \
--state-dir /var/lib/proof/smoke-first \
--vcpus 1 --mem-mib 1024 --disk-mib 16384Choose a new private state directory for each run; its parent must already exist.
Keep its absolute path short: generated UNIX socket paths must fit Linux's
107-byte limit. An excessive path length is rejected before any VM is created.
If several images are configured, select the installed rootfs with
--image-digest sha256:<actual-digest>. From a source checkout, the equivalent
entry point is uv run --no-sync python scripts/vm_smoke.py with the same flags.
The smoke reads local artifact and hypervisor settings without reading TLS or
inference credentials. It leaves the running orchestrator configuration untouched.
The smoke checks two separate guest identities and boot IDs, a read-only root,
a separate writable workspace, loopback-only network interfaces and confirmed
teardown. It records private evidence in smoke.json. It does not run inference,
reproduce a scientific result, score a submission or submit chain weights. CI
tests this command with an injected hypervisor; only a successful real host run
provides KVM lifecycle evidence.
After preflight and the separate smoke, install and start the service:
sudo install -m 0644 deploy/systemd/proof-vm-orchestrator.service \
/etc/systemd/system/proof-vm-orchestrator.service
sudo systemctl daemon-reload
sudo systemctl enable --now proof-vm-orchestrator.serviceBefore opening a topic, additionally test authenticated orchestrator health and the topic/experiment API lifecycle, including retention after failure and destruction after success. The isolated smoke does not exercise those HTTPS routes or prove a challenge reward path.
CI builds distributions and tests both supported Python versions. Image
publication is automatic for main, but it does not edit deployment pins or
touch a host. Run .github/workflows/approve-image-update.yml from main with
the candidate, source commit, expected digest and the exact approval phrase.
The protected production environment verifies provenance and emits
update.json; it still changes no host. Promotion consists of reviewing that
evidence, changing CORTEX_IMAGE to its immutable digest, rendering Compose,
backing up state, pulling that digest and recreating one role.
The Cortex image itself has deliberately no git pull, Docker-socket watcher,
CI SSH deployment or unattended host updater; only challenge containers
auto-update, through the supervisor gates above, and they never hold a leaf
seed. Safe automation of the Cortex image must preserve the same operator gate,
take a consistent SQLite backup, verify role health plus a fresh sealed bundle,
and restore both the prior digest and schema-compatible state on failure.
Rollback uses the prior reviewed digest and a schema-compatible state backup. Never roll a database backward by deleting rows or replacing the latest seal. Trust-root rotations are separate signed ceremonies and do not happen as a side effect of an image rollback.