Skip to content

Running it

With CORRALAI_OIDC_ISSUER unset, the brain runs with auth disabled — any caller is trusted. This is for local development only. Setting CORRALAI_OIDC_ISSUER to any OIDC provider (Keycloak, Auth0, Okta, Dex, Authentik, or others) turns auth on: callers must present a valid token for that issuer and the configured CORRALAI_OIDC_AUDIENCE. Dev mode and auth-on run the exact same code paths, so “works on my machine” and “works with auth” don’t drift apart. See the full env reference on corral’s CLI page — every variable there is generated straight from the binary’s own source comment, not hand-copied.

corral certify --local (see Getting started and Your first audit, in detail) is the fastest way to see the audit itself — no daemon required. deploy/demo/ is the separate, optional compose stack for the coordination substrate underneath a running brain: it brings up the brain plus a small herd of agents against a bundled local model, no API keys required. make demo is the base coordinated herd, make demo-clobber runs the same herd with claims turned off (a demo of what NOT coordinating looks like), make demo-observe adds a read-only observer console, and make demo-models runs a two-model comparison so model_comparison has real A-vs-B data to report on.

The scorecard + critic precision (brain-path only)

Section titled “The scorecard + critic precision (brain-path only)”

corral scorecard shows which model actually catches bugs, execution-proven (recall from mutants a model’s own generated tests killed — never a self-report) and, in a C-PREC column, how often that model’s test-critic findings turn out to be real once a human adjudicates them. Both columns read the running brain’s DuckDB stores over its HTTP API (GET /api/bugcatch) — set CORRAL_BRAIN (and a token via corral secret) to point at one; without it, corral scorecard falls back to opening the local CORRALAI_BUGCATCH_DB file directly, which shows recall/precision but not the C-PREC column (that join needs CORRALAI_CRITICSCORE_DB, and the offline path never has both single-process files open at once).

The critic’s own findings get flagged automatically the same way the rest of the audit works — by execution, not opinion — but the human gate has final say: corral criticscore list shows findings still awaiting adjudication, corral criticscore show <id> prints one in full (model, target test, evidence), and corral criticscore confirm <id> / refute <id> record a human verdict that permanently overrides the auto-adjudication — a later automated pass can never claw a human verdict back. All four subcommands require a brain (CORRAL_BRAIN); there’s no offline mode, because confirm/refute are admin-gated MCP tools that need the caller’s authenticated identity for the audit trail.

Honesty caveat: the critic-precision metric and its human gate are brain-path features only, like the rest of the bug-catching scorecard. corral certify --local still shows the critic’s auto-adjudicated verdict on the run’s own tape (nothing is hidden from you locally), but it persists nothing to a scorecard — there’s no server-side store to write to without a brain running.

The tests×mutants matrix (--matrix, opt-in, brain-path only)

Section titled “The tests×mutants matrix (--matrix, opt-in, brain-path only)”

corral certify --local --matrix opts a run into a second, more expensive pass: after the primary mutant pass converges, it re-scores every one of the dev suite’s own tests, alone, against the same mutants — a per-test adequacy number instead of one suite-wide kill-rate. The verdict prints a matrix: summary (tests scored, delete-candidate count) and each delete-candidate selector, and the run’s --record tape carries a pool_matrix beat with the same data.

The cost is real and it’s why this is opt-in. The matrix is T tests × M mutants extra jail runs, on top of the primary pass — a suite with 40 tests and 40 mutants is up to 1,600 additional sandboxed executions. Leave it off for routine runs; turn it on when you actually want the per-test signal (e.g. before a cleanup pass on a suite an agentic dev grew fast).

The safe-to-delete list is a signal, not a verdict. A delete-candidate is a test that was scored (compiled and ran clean against the compliant code) but killed zero of the run’s planted mutants. Every delete-candidate line carries the same honest caveat, verbatim: “Relative to this mutant set; a test may still guard behavior no mutant probed.” Zero kills against this run’s mutants is not proof the test is vacuous — it means this run’s mutant generator never happened to seed a violation that test would catch. Review before deleting; don’t script deletions off the list unattended.

Persistence is brain-path only. A --matrix-opted run’s per-test rows land in the running brain’s internal/matrixstore DuckDB store (CORRALAI_MATRIX_DB) — the same store the hosted adversarial pool’s start_adversarial_run writes to when its own matrix param is set. corral matrix list [--json] reads it back over the brain’s HTTP API (GET /api/matrix, mirroring /api/bugcatch); like criticscore, there is no offline fallback — set CORRAL_BRAIN (and a token via corral secret). A --local --matrix run with no CORRAL_BRAIN configured prints its own tape/verdict summary same as always, but that run’s rows are never persisted anywhere corral matrix list can see them.

Language support: go and python only today, the same two languages the underlying test-enumeration/single-test-selector plumbing (ListTestsCmd/ SingleTestCmd) currently implements — an unsupported language silently skips the matrix phase (the primary audit still runs and gates normally).

Python single-file caveat: in single-file --test mode (no repo dir), enumeration runs pytest --collect-only against the operator-supplied --test path itself, not the synthetic path the dev test is actually written to in the jail — so on Python, the --test filename must follow pytest’s discovery convention (matching the synthetic test_<code>.py path) or collection finds nothing and the matrix is silently skipped (fail-soft, no crash). Repo-dir mode and go are unaffected.

Dependency dirs — bound read-only, never copied (--repo-dir)

Section titled “Dependency dirs — bound read-only, never copied (--repo-dir)”

corral certify --local --repo-dir <path> seeds the whole cloned tree into the jail workspace so a real multi-file project’s own tests (which import the package) resolve. Dependency dirs are the usual bulk of that checkout and are irrelevant to the mutant/text seed, so instead of copying them in, corral auto-detects and bind-mounts them read-only: node_modules, vendor, .venv, venv, and .bundle. This keeps them off the workspace’s 64 MiB text cap and off the mutant/test-writer’s context.

Dependencies must already be present — vendored or installed, the same way CI expects a checkout to arrive. Corral binds what’s already there; it never installs anything on your behalf.

  • --bind-dir <path> (repeatable, repo-relative) binds additional dirs the same way, for dependency trees the auto-detected set doesn’t cover.
  • --no-bind-deps opts back into the pre-bind behavior: every dep dir is copied into the workspace seed instead, subject to the same size cap.
  • When anything is bound, the run prints deps: bound N dir(s) read-only (<names>) — not copied into the jail seed.

The honest backend caveat. Binding a dep dir requires the backend to relocate it into the jail workspace — only bwrap (Linux default, via --ro-bind Host Target) and --jail container (via -v Host:Target:ro) can do that. bwrap binds cleanly with no size limit and no copy. --jail container is more restricted: containers remap the host uid, so a container process can’t read a dep dir it doesn’t have world-read permission on — --jail container binds only world-readable dep dirs; anything more restrictively permissioned is copied into the seed instead (and so is subject to the workspace size cap) — a loud fallback, not a silent one.

macOS sandbox-exec cannot bind dep dirs at all. sandbox-exec only grants read access at a dir’s original host path — it has no primitive to relocate a dir into the jail workspace, which is where the test toolchain (cwd = the workspace) needs to find it. So on macOS (and on any other/unknown backend), every dep dir is copied into the workspace seed, the same as --no-bind-deps, and is subject to the 64 MiB workspace size cap.

The verify gate needs a sandbox (Ubuntu 24.04+)

Section titled “The verify gate needs a sandbox (Ubuntu 24.04+)”

The brain certifies a gated task by running its verify command itself — in a jail, against its own working copy, reading the real exit code — rather than trusting what a worker reports (see Trust & security). That jail is bubblewrap by default, and bubblewrap needs unprivileged user namespaces.

On Ubuntu 24.04+, unprivileged user namespaces are restricted by default (kernel.apparmor_restrict_unprivileged_userns=1). If the brain can’t get a sandbox it does not crash and does not silently trust the worker without saying so — it logs, loudly, and falls back to the recorded-execution lookup:

verify-gate: NO isolation backend (bwrap ... Permission denied);
gated completion falls back to worker-reported executions — set CORRALAI_GATE_EXEC_BACKEND

To get the independent gate, install bubblewrap and grant it (and it alone) the userns permission with a small AppArmor profile — this keeps the rest of the host hardened:

Terminal window
sudo apt install -y bubblewrap
sudo tee /etc/apparmor.d/bwrap >/dev/null <<'PROF'
abi <abi/4.0>,
include <tunables/global>
profile bwrap /usr/bin/bwrap flags=(unconfined) {
userns,
include if exists <local/bwrap>
}
PROF
sudo apparmor_parser -r /etc/apparmor.d/bwrap

Restart the brain and confirm the log now reads verify-gate: independent verification enabled (backend bwrap). The profile in /etc/apparmor.d/ reloads automatically on boot.

Alternatives to bubblewrap: set CORRALAI_GATE_EXEC_BACKEND=container to run the verify command in a container instead (needs a runtime and an image carrying the project’s toolchain), or — only on a host that is already a disposable sandbox — CORRALAI_GATE_EXEC_BACKEND=none with CORRALAI_GATE_EXEC_UNSAFE_HOST=1. Lifting the restriction globally (kernel.apparmor_restrict_unprivileged_userns=0) also works but weakens every process on the host, so prefer the per-binary profile.

The brain listens on CORRALAI_ADDR (default 127.0.0.1:9019) and expects to be reached through a reverse tunnel or proxy that terminates TLS and forwards to that local address — the general pattern is: run corral bound to loopback, run a tunnel client (any provider) pointed at that same loopback address, and set CORRALAI_ALLOWED_HOSTS to the public hostname(s) the tunnel exposes so the brain’s Host-header check accepts them. Corralai supports built-in TLS via CORRALAI_TLS_CERT/CORRALAI_TLS_KEY, or CORRALAI_TLS_AUTOCERT_DOMAINS for automatic Let’s Encrypt certificates, as alternatives to a tunnel for a brain with a public IP. This page describes the pattern only — specific tunnel providers, hostnames, and ports are a deployment’s own operational detail, not part of this doc.