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,uiandbilling. The Go build resolves them through relativereplacedirectives.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/coreExport 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=768Bring 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.
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=$?"| Exit | Meaning |
|---|---|
0 | Populated: traces, impacts and scenarios came back. |
1 | It could not complete: an unexpected RPC error, or the time budget (--timeout, default 10m) ran out. |
2 | A 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). |
3 | The call ran, but every payload field is empty. |
4 | TOON_PARSE_FAILURE: the model’s reply did not match the template’s contract. |
Minutes per simulation are normal on CPU-only inference.