Running the Local Service Stack
This guide stands up the whole Test Cabinet as a deployment runs it — on your own machine, on a throwaway Kubernetes cluster — so you can drive runs the way the web console does in the cloud rather than one at a time from the CLI.
It is the service-driven counterpart to First Time Setup. Where that guide gets you to a single CLI run (the CLI embeds the core runner directly, so it needs no backend or cluster), this one brings up the backend (which owns the run queue), the auth service, the dispatcher, and the artifact service in the cluster, and runs them exactly as staging and prod do. The web console you drive them with is the exception: locally you run it from source (a Vite dev server) rather than in-cluster, so UI edits hot-reload without an image rebuild.
Running is the developer reference this guide sits on top of — it holds the authoritative list of every variable each service reads. Reach for it when you need a value this guide doesn’t spell out.
How the service-driven flow differs
Section titled “How the service-driven flow differs”A single CLI or Tauri desktop run executes
in-process: the binary builds a RunEngine and runs
the container itself. There is no queue and no second process.
The service-driven flow splits that apart so it can scale across a cluster:
- A console enqueues a run by posting it to the backend’s queue — the one URL a console talks to.
- The in-cluster dispatcher claims the queued run and creates one Kubernetes Job running the driver.
- The driver executes that single run — under the Kubernetes runtime it creates one ephemeral, untrusted sandbox pod — streams its event timeline and asset preview back to the backend (which relays them to the console live), then uploads the produced source, build, and media to the artifact service and reports terminal status with the run record.
- The console reads the live stream from the backend and the produced build and
media from the artifact service (the backend reports the artifact service’s URL
via
GET /config).
Because execution is now a cluster concern, the local mirror of it runs on a real (local) Kubernetes cluster — k3d, k3s-in-Docker — from the same manifests a deployment uses. There is no worker process on the host; a run you enqueue schedules as a Job inside the cluster, exactly as in the cloud.
Prerequisites
Section titled “Prerequisites”You do not need to build the service binaries by hand — the bring-up builds
each service’s container image from deployments/images/. In the
devcontainer
the container-runtime, k3d, and kubectl requirements below are already
provided (the host Docker daemon is bound in over a socket — Docker-outside-of-
Docker — and k3d/kubectl ship in the image); on a bare host, install them
yourself. Either way you need:
-
A container runtime (Docker, or a Docker-compatible one such as Podman). k3d runs the cluster nodes as containers, and the bring-up builds the images through this runtime — it uses
podmanwhen present and falls back todockerautomatically (override withCONTAINER_TOOL=… make …). -
k3dandkubectlonPATH. -
make(the bring-up is driven by a Makefile). -
A harness API key exported in your shell — the run injects it into the sandbox. At least one of:
Harness Variable claudeANTHROPIC_API_KEYcodexOPENAI_API_KEYcline,goose,kilo,opencode,piOPENROUTER_API_KEYTerminal window export ANTHROPIC_API_KEY=… # the harness you intend to runThe Makefile reads this from your environment and creates the cluster Secret from it, so no key is ever written to a tracked file.
-
(Optional) Subscription auth. The
secretstarget also wires subscription mode into the local stack: if your host has signed-in harness CLI credential files —~/.claude/.credentials.json(+~/.claude.json),$CODEX_HOME/auth.json(default~/.codex/auth.json), and/or the Antigravity OAuth token at~/.gemini/antigravity-cli/antigravity-oauth-token— it builds atcab-driver-subscriptionSecret from whichever exist and the dispatcher mounts it (read-only,optional) into each driver Job. This is opt-in and never errors when the files are absent, so an API-key-only setup is unaffected. It is the only way to run the subscription-only Antigravity harness locally. With the creds present and no API key, the engine prefers subscription on its own; lock it withTCAB_AUTH_MODEor the dispatcher’sTCAB_DISPATCHER_DRIVER_AUTH_MODEif needed. See Set Up Authentication.
The harness run-container image is pulled from the registry the first time a run needs it, so there is nothing to pre-build for it.
1. Bring the stack up
Section titled “1. Bring the stack up”From anywhere in the checkout (the Makefile resolves the repository root itself):
make -C deployments/local local-upThis creates a throwaway k3d cluster, builds the backend, auth,
dispatcher, driver, artifact, and arena images and
loads them into the cluster with k3d image import (no registry needed), creates the
cluster Secrets from your environment (the harness key above, plus a fixed dev
service token the dispatcher claims jobs with), applies the
deployments/k8s/overlays/local
kustomize overlay, and force-ingests the test-case catalog from a read-only mount
of the repository. It finishes once every service is rolled out.
The web console does not run in-cluster locally (only prod does — via the
internal-ingress component). Baking it into a pod would force a full image
rebuild + re-import on every UI edit, so instead you run it from source in step 3 —
edits hot-reload against the same forwarded backend. There is nothing to configure:
its backend/auth URLs are pre-set to the forwarded addresses in the committed
apps/web/.env.development.
If you forgot to export a harness key, the bring-up stops before applying with a message naming the variables it accepts — export one and re-run.
Check what came up:
make -C deployments/local local-status # pods, services, and volumes in the namespace2. Expose the stack on localhost
Section titled “2. Expose the stack on localhost”The browser runs outside the cluster, so reach the in-cluster services over a port-forward. Hold them open in their own terminal:
make -C deployments/local local-forward # backend → :8787, auth → :8789, artifacts → :8790, arena → :8791Leave this running (Ctrl-C stops it). It forwards the data plane the browser talks
to directly: the backend (live run stream), the auth service, the artifact
service (each run’s playable build and proof/asset media, as ordinary
<img>/<iframe> requests), and the arena (matches/tournaments). The local overlay
points the artifact and arena URLs the backend advertises (GET /config) at these
forwarded 127.0.0.1 ports; in a real deployment they are the data-plane hosts the
console reaches over the network instead.
3. Start the web console
Section titled “3. Start the web console”The console runs from source, not in-cluster. In its own terminal:
npm run -w apps/web devThen open http://127.0.0.1:1430. There is nothing to configure — its backend
and auth URLs are pre-set to the forwarded 127.0.0.1:8787 / :8789 in the
committed apps/web/.env.development, so the catalog loads on first visit (the
backend/auth CORS layers are permissive, so the cross-origin requests are allowed).
The backend is the one URL the console talks to for runs — there is no worker to
register. (You can still override the backend URL in the UI’s settings, or via
VITE_BACKEND_URL in a gitignored .env.local, to point at a different stack.)
Now register an account. Sign-in is required to launch a
run as well as to push, review, and publish — the backend gates POST /jobs
on the launching account and attributes the run to it, so every mutation
(enqueue included) needs a token. Reads still work signed-out, but those
mutations are rejected 401. You can register in the UI, or with the CLI against
the forwarded auth service:
tcab register --username dev --display-name "Dev"4. Enqueue a run and watch it execute
Section titled “4. Enqueue a run and watch it execute”From the console — signed in (see step 3; the launch button stays disabled with a sign-in prompt otherwise) — start a run: pick a test case, a model, and the harness whose key you exported. The console posts it to the backend’s queue, authenticated as your account, and immediately begins streaming.
Watch it schedule as a Job in the cluster:
kubectl -n tcab-local get jobs,pods -wYou should see a driver Job appear, and — for the Kubernetes runtime — the driver create a sandbox pod for the model’s work. In the console, the event stream flows live; for an asset-generation case you also see the live drawing preview, forwarded from the sandbox through the driver and backend.
When the run finishes, the driver uploads its artifacts and reports terminal status. The run becomes reviewable in the console, and its playable build loads from the artifact service — the control-plane backend never carries the artifact bytes. Enqueue several runs at once and each schedules as its own Job, with no per-worker registration — that is the scaling property the whole topology is built for.
5. Iterate and tear down
Section titled “5. Iterate and tear down”make -C deployments/local local-rebuild # after a code or manifest change: rebuild images, re-apply the overlay, restart servicesmake -C deployments/local local-reapply # after a manifest-only change (RBAC, env, volumes): re-apply the overlay, no image rebuildmake -C deployments/local local-ingest # after editing a test case: force re-ingest the catalogmake -C deployments/local secrets # after rotating a key: re-create the Secrets from the environmentmake -C deployments/local local-down # delete the cluster and everything in itDriver Jobs are created fresh per run, so they pick up a rebuilt tcab-driver
image (or a rotated key) on the next run with no restart. A re-ingest is required
after editing a case for the same reason it is for the bare backend: the store is
immutable per (slug, version), so a backend-driven run keeps serving the
previous definition until you force the overwrite (see
Running → re-ingest).
Adversarial arena
Section titled “Adversarial arena”Adversarial-arena execution (quick matches and tournaments) runs on the
tcab-arena service in the stack — the dedicated, CPU-bound execution host for
head-to-head controller matches. The local overlay brings it up alongside the other
services; reach it locally via make -C deployments/local local-forward, which adds
arena → 127.0.0.1:8791. The backend reports the arena’s URL at GET /config
(TCAB_ARENA_PUBLIC_URL), and the console fetches it for its match/tournament run
actions; arena reads (published tournaments + stored replays) stay on the
backend. See the adversarial type for what the
arena covers.
Next steps
Section titled “Next steps”- Reviewing Test Run Results — assess a run the stack produced.
- Publishing a Test Run Result — release a reviewed run.
- Deployment — put the same images on real staging and prod hosts; what changes is the namespace, not how the flow is wired.