Coco

Self-hosted

The same gate as a container image, for a team whose data cannot leave its own infrastructure.

Running the gate yourself

The gate ships as a container image, for the team whose data cannot leave its own infrastructure. It is the same gate file, the same contracts and the same ledger the hosted service runs.

The hosted service is the faster road and most teams start there. This page is for the ones that cannot.

The image

Two build stages, for one reason. Compiling contracts from YAML into the JSON the gate reads needs Node. Answering a call needs nothing but Python, because the gate is standard library only. So Node does its work at build time and does not ship, and what you run is a small Python image with no dependency tree to audit.

docker build -t coco-gate:0.1.0 .
docker build -t coco-gate:0.1.0-kyc --build-arg COCO_MANDATE=kyc .

COCO_MANDATE pins a contract set to an image. The runtime carries no compiler, so whatever a container is going to enforce has to be compiled in at build time. Put the mandate name in the tag, so an auditor can tell two images apart.

The contracts are baked in and the receipts are not. Everything under /coco/home is the contract as shipped and never changes at runtime.

That makes a contract change a rebuild, on purpose, because an image an operator can edit in place is an image an auditor cannot pin. The urgent brake is the mode rather than the contract. Set COCO_MODE=observe and restart, and the gate records without stopping anything while the corrected image builds and ships.

Run it

docker run -d \
  -p 8787:8787 \
  -v coco-data:/data \
  -e COCO_MODE=observe \
  -e COCO_API_KEY="$(openssl rand -hex 24)" \
  coco-gate:0.1.0

Mount /data, or the hash chain restarts every time the container does. That volume is the only state on the machine worth a backup.

The container runs as an unprivileged user and carries a healthcheck that probes /health on its own port.

Environment

VariableDefaultWhat it does
COCO_MODEobserveobserve, assist or enforce
COCO_API_KEYnoneRequires a bearer token on every route except health
COCO_ON_ERRORblockWhat a gate that cannot evaluate does
COCO_PORT8787The port the gate listens on
COCO_LEDGER_PATH/data/ledger.dbWhere receipts are written
COCO_TIMEOUT30Seconds before the gate is treated as not having answered
COCO_CASE_ROOT/dataWhere case state is read from when a call carries no working directory
COCO_DASH_KEYnoneThe dashboard's browser password

Observe is the only honest default for an image. A gate that arrived enforcing would block calls against contracts the customer has not read yet.

The mode and the key are read once at boot rather than per request, so neither can change underneath a call that is already being decided. Changing the mode means restarting the container.

The dashboard

The same image carries the dashboard as a second role. Run it as its own container sharing the /data volume, so it reads the ledger the gate wrote.

docker run -d \
  -p 8788:8788 \
  -v coco-data:/data \
  -e COCO_DASH_KEY="$(openssl rand -hex 24)" \
  coco-gate:0.1.0 python3 /coco/dashboard.py

The dashboard opens on a live feed of verdicts as the gate writes them, allows included. Clicking a row opens the receipt whole, with the contract that fired, every check that ran, the state the gate read and the contract source scrolled to the failing line. Contracts, the escalation queue and the full receipts table are their own views, and the table filters by verdict, searches by reason and exports as CSV.

The dashboard verifies the receipt chain on every load.

More than one customer on one host

deploy/compose.yaml runs the whole plane behind Caddy. Each customer is two containers from the same image, a gate and a dashboard, sharing one volume.

The gate takes a path prefix on a shared host name. The dashboard needs a host of its own, because its shell asks for /static and /api at the root and a stripped prefix leaves every one of those requests outside it. The Caddyfile says why at length.

Keys live in an .env file on the host and never in the compose file.

deploy/gcp.sh stands the same stack up on Google Cloud, in Sydney, because data residency is the point. Every create in it tolerates already existing, so re-running after a failure is safe. Two things it cannot do for you, and it stops to say so. Linking a billing account, and pointing DNS at the machine.

Next

  • SDK for the client and the API
  • Modes for what to run before you enforce
  • The ledger for what the receipts hold

On this page