Skip to content

Local development and validation

This page is for Runink operators and contributors who have the FACE source and its sibling repositories checked out side by side. The FACE repository does not stand up a stack by itself. CORE’s core CLI owns the development stack.

Prerequisites

  • Go 1.26, buf, grpcurl, Flutter and a Chrome or Chromium browser.

  • The sibling repositories next to face/: core, inference, mesh, security, store, ml, web, ui and billing. The Go build resolves them through relative replace directives.

  • The generated protobuf code. It is not committed, so generate it first:

    cd grpc && buf generate && cd ..
    cd grpc && go build ./... && cd ..

Start the stack with core dev up face

Build the core CLI

cd ../core/grpc
go build -o core ./cmd/core

Export the backend’s configuration

core dev up passes its environment through to the backend, and it creates the mesh CA and the envelope KEK as dev files. You supply everything else:

export FACE_REPO=/path/to/face                    # the checkout to build; --face-repo alone is not enough
export ADMIN_EMAIL='devadmin@localhost.test'
export ADMIN_PASSWORD='<any local password>'
export AUTH_JWT_SECRET='<any local value>'        # required: the backend exits without it
export RUNINK_VARIANT=demo                        # local instance card + demo connections
export INFERENCE_REMOTE_URL='http://127.0.0.1:<chat-port>'   # required
export EMBEDDING_URL='http://127.0.0.1:<embed-port>'
export MODEL_EMBEDDING_DIM=768

Bring up an OpenAI-compatible model server first, as described in Model plane. A dead loopback port is enough for the process to boot, but then it has no model.

Start it

./core dev up face --face-repo "$FACE_REPO" > /tmp/face-devup.log 2>&1 &
until grep -qE 'backend ready on ephemeral|❌' /tmp/face-devup.log; do sleep 5; done
PORT=$(grep -oE 'ephemeral gRPC port [0-9]+' /tmp/face-devup.log | grep -oE '[0-9]+$')

The backend’s gRPC port is chosen fresh on every start. Read it from the log line ✅ [face] backend ready on ephemeral gRPC port <n>. core dev up face also starts a second FACE process with SERVICE_ROLE=runner, and points the control plane at it, so fetches work.

If you run the two processes by hand instead, start the runner with SERVICE_ROLE=runner, and give the control plane MANAGED_RUNNER_ENDPOINT=127.0.0.1:<runner port>. If the two processes use separate data directories, they must still share one FACE_APPFS_DIR. Sessions live in the appfs root, so a runner with its own root rejects every forwarded fetch with invalid or expired session token. AUTH_JWT_SECRET and ADMIN_PASSWORD can come from owner-only files instead of the environment (SECRET_BACKEND=file SECRETS_DIR=<dir>).

Build and serve the cockpit (release)

core dev up runs the Flutter app in debug mode, which often never paints. For anything you intend to look at, build the release bundle and point it at the local backend:

cd flutter
flutter build web --release --no-tree-shake-icons \
  --dart-define=BACKEND_URL=http://127.0.0.1:$PORT \
  --dart-define=SERVER_HOST=127.0.0.1 \
  --dart-define=SERVER_PORT=$PORT \
  --dart-define=WEB_PORT=$PORT \
  --dart-define=SERVER_SECURE=false \
  --dart-define=DEBUG_AUTO_LOGIN=false
cd ..
go run .claude/skills/run-face/smoke.go serve flutter/build/web 3001 &

These --dart-define values are for local use only. The production image bakes in no backend host: the cockpit uses the origin it was loaded from.

The smoke harnesses

Both harnesses are single-file Go programs using only the standard library, run from the repository root. Build them before use. go run turns every non-zero exit code into 1, and here the exit code is the result.

smoke.go signs in through the real cockpit over the Chrome DevTools Protocol, picks the instance, and screenshots the Fetch, AI Center, Rules, Lab, Twins and Schedules pages:

go build -o /tmp/face-smoke .claude/skills/run-face/smoke.go
/tmp/face-smoke "$ADMIN_EMAIL" "$ADMIN_PASSWORD" /tmp/face-shots; echo "exit=$?"

Exit 1 means the login never left the login route. Exit 2 means it logged in, but a route bounced back to the login page, so the screenshot shows the login card. Look at the PNGs: a blank white image means the Flutter engine never painted.

simulate-smoke.go proves the backend returns real data. It signs in, checks that the inference plane answers, lists the scenarios, runs one simulation, and turns the result into an exit code:

go build -o /tmp/simulate-smoke .claude/skills/run-face/simulate-smoke.go
/tmp/simulate-smoke "$PORT"; echo "exit=$?"
ExitMeaning
0Populated: traces, impacts and scenarios came back.
1It could not complete: an unexpected RPC error, or the time budget (--timeout, default 10m) ran out.
2A precondition failed, and the message names it: nothing listening, login refused, inference plane unreachable, or no scenarios yet (which needs a completed fetch or a demo instance).
3The call ran, but every payload field is empty.
4TOON_PARSE_FAILURE: the model’s reply did not match the template’s contract.

Minutes per simulation are normal on CPU-only inference.