Architecture
This page explains how CORE is built: which code lives where, what runs on the cluster, and how
trust, data and traffic move through it. It follows the repository’s own CLAUDE.md and the code it
points to.
What CORE is, and what it is not
CORE is the shared platform for the Runink applications (FACE, PULSE, LUNA and FORGE). Platform concerns live here: self-healing, the event mesh, secrets, the object store, inference serving, MCP, PKI and mTLS, telemetry, the Kubernetes operators and the infrastructure as code.
The apps do not import CORE. Code that more than one app runs lives in a shared library that
CORE and the apps both import. FACE, PULSE, LUNA and FORGE each carry a test that fails if their
go.mod or any import names github.com/org-runink/core.
The code, by module
| Module | Where | Contents |
|---|---|---|
github.com/org-runink/core | grpc/ | the core CLI (grpc/cmd/core), the other grpc/cmd/* binaries, grpc/infrastructure/*, the agent fleet under grpc/agents/ |
github.com/org-runink/face-operator | grpc/operators/runink | the FACE Kubernetes operator |
github.com/org-runink/pulse-operator | grpc/operators/pulse | the PULSE Kubernetes operator |
github.com/org-runink/core-operator | grpc/operators/core | the generic ClientInstance CRD and its controller, the provisioner, and the CORE console |
github.com/org-runink/placement | grpc/operators/placement | shared pod placement and envcontract, the declared set of required env vars per app and role |
The operators are separate modules on purpose, to keep controller-runtime’s dependency tree out of
the root module. go build ./... at the root does not cover them.
The shared libraries are separate repositories (inference, security, mesh, store, ml, and
ui for the shared Flutter and gRPC components). CORE consumes them through local replace
directives. For image builds it also carries committed copies of some of them (grpc/vendor/ and
.github/*-snapshot/), which one guard test,
grpc/operators/runink/internal/controller/sibling_copies_test.go, keeps in step with their
upstream.
Binaries
grpc/cmd/ holds ambassador, appbench, certrotator, certsync, compliance-gate, core,
ddnssync, devgateway, dns_sync, edgerelay, forgecheck, ghapp-token,
inference-activator, install_server, knowledge, mcphost, modelfetch, modelrouter,
runner-dispatcher, runner-entrypoint and ttsd. The ones you meet most often:
| Binary | Role |
|---|---|
core | the single control surface: dev stack, deploys, images, security scans, sessions, the install wizard. See the CLI reference. |
devgateway | the edge: a DaemonSet on host ports 80 and 443 that terminates TLS with a leaf from CORE’s own CA and routes each host to its frontend. |
ambassador | a small static mTLS TCP proxy built on security/pki, which replaced HAProxy. Its image is FROM scratch. |
modelrouter | the one admission queue in front of the inference engines. |
forgecheck | the verifier central CI runs on FORGE projects. |
core-runner | the self-hosted data-access runner (grpc/operators/core/cmd/core-runner). |
What runs on the cluster
Deployments use k0s with pure kustomize overlays (no Helm). The operators are the
authoritative source of each pod’s spec and env, not the kustomize base YAML: to change what a pod
gets, change the controller in operators/{runink,pulse,core}/internal/controller/.
The console tracks these namespaces: core-system (CORE), runink-system (FACE), pulse-system
(PULSE), luna-system (LUNA), forge-system (FORGE), inference-system, github-runners (CI) and
registry-system.
ClientInstance
ClientInstance (core.runink.org/v1alpha1) is the generic tenant instance the platform places on a
shared cluster (grpc/operators/core/api/v1alpha1/clientinstance_types.go):
| Field | Meaning |
|---|---|
app | the application identity. Drives the core.runink.org/app label and the dedicated-taint toleration. |
tenant | the tenant key. The core.runink.org/tenant label plus soft node affinity. |
role | controlplane or runner (default runner): selects the PriorityClass. |
image | the container image to run. |
port | the container and service port (default 8080). |
replicas | default 1; ignored when suspend is true. |
computeUnits | the CU budget, mapped to real CPU and memory limits. |
tags | client labels added to the pod template and ServiceAccount. |
placement | nodeSelector, tolerations (added to the defaults), affinity (replaces the default soft affinity). |
env | extra environment variables. |
users | access grants: email, role (admin, writer or reader, default reader), optional phone, optional expiresAt. Projected as AUTH_ALLOWED_EMAILS and AUTH_SEED_USERS. |
subscription | tier (lite, dedicated or enterprise), seats, status (active, suspended or past_due). |
suspend | scale the instance down. |
DeepCopy is written by hand (there is no controller-gen), and the CRD YAML is the authority. The
CRD schema prunes unknown fields, so a field that is not in the schema is silently dropped (this is
how a subscription.credits field was lost on every write before it was removed). The old
spec.variant field was removed in core#903, because the env vars it projected had no reader.
Every operator gives each pod the same isolation labels and scheduling (operators/placement):
core.runink.org/{app,tenant,role}, runink.org/{app,tenant,role}, the toleration
core.runink.org/dedicated=<app>, soft (anti-)affinity and a priorityClassName. Namespace-level
policy (ResourceQuota, LimitRange, PriorityClass, NetworkPolicy, PDB) is static kustomize under
grpc/infrastructure/apps/*/kustomize/governance/.
Trust: the security spine
Everything internal is zero-trust:
- PKI.
security/pkiis an in-memory ECDSA CA that issues short-lived leaves off a shared cluster CA. Verification is chain-only, so peers dial by service DNS or pod IP.core mesh camints and distributes that CA. - Encryption at rest.
security/secrets/envelopeis AES-GCM envelope encryption, applied unconditionally bystore/objectstoreandstore/connections. There is no switch to turn it off. Anything that opens either store needsCORE_ENVELOPE_KEKand refuses to start without it. - The object store. CORE runs its own S3-compatible
objectd(from thestorelibrary), not the MinIO server. objectd verifies SigV4, and core-operator’s provisioner mints every objectd key as anobjectd-key-<id>Secret in the consumer’s namespace. - App databases. FACE and PULSE seal database values per column before they are written. That is a narrower claim than “the database is encrypted”: schema, table and column names, row counts and unsealed columns stay readable. Whole-database encryption is an open decision.
- Committed key material: none.
core dev upgenerates its local KEK withcrypto/randon first run, and a test flags any 32-byte key literal in that package.
Where the console keeps its data
The console does not keep its metadata in ConfigMaps. It mounts its own encrypted store/appfs root
for the app core-console (appfsroot.go, metastore.go, metadoc.go):
- on objectd, when
OBJECTSTORE_ENDPOINTand both credentials are set. If objectd is configured but cannot be opened, the console fails closed instead of falling back; - otherwise on a local encrypted fallback under
CONSOLE_APPFS_DIR(default/var/lib/core/appfs), on its PVC. There is no in-memory mode.
Each store (agent health, schedules, runs, lineage, governance findings, judgements, rules
reconciliation, session commands and runs, GitHub config, cloud providers, connections,
subscriptions) is a table in that root. Sessions are run/sessions. The audit chain is
var/log/audit: a hash chain that continues across restarts and that GET /api/audit/verify walks.
Only three ConfigMaps are still written, because agent workflows read them:
core-agent-schedules, core-judgement-submissions and core-console-connections. The console
republishes them from its tables after every commit, so editing one by hand changes nothing: the
next write replaces it.
Traffic: the edge and the apps
Each app’s web UI is served by its own small Go frontend (the “sleeve”, in the app repo), which
serves the Flutter bundle and reverse-proxies gRPC-web, /auth/* and /api/* to the backend. The
devgateway edge routes / on each host to that frontend.
The devgateway has its own sign-in gate. When edge auth is on, an empty
DEVGW_AUTH_ALLOWED_EMAILS now refuses every sign-in. Admitting any verified Google account
needs the explicit DEVGW_AUTH_ALLOW_ANY_VERIFIED=true. Edge sessions are server-side, in the
devgateway’s own appfs root.
FORGE has no domain and no sign-in of its own. CORE serves it at /forge/ through its own reverse
proxy, under CORE’s session, and vouches for the person with the X-Core-Identity header.
Inference
The model plane is sovereign: no hosted model is called. mistral.rs is the only engine. The
llama.cpp image was retired on 2026-09-27. cmd/modelrouter is the one admission queue in front of
the engines. Budgets across the agents derive from one configured decode rate,
INFERENCE_DECODE_TOK_S. See Models and inference.
Networking
The decided pod network is dual-stack, IPv6 primary (owner decision 2026-09-26). Until the
River/distro k0s change ships, the running box is still IPv6 single-stack, and the local
multipass clusters are k0s’s default IPv4 single-stack. Manifests must therefore work on all three.
The IPv4 ranges are written in one place (grpc/infrastructure/apps/_shared/cluster-cidrs/), and
dual_stack_manifests_test.go enforces the rules: bind [::], build URLs with
net.JoinHostPort, never pin a Service to one family, and give every IPv6 egress catch-all its IPv4
twin.
Images
core build builds local podman images only. The images the cluster runs come from
.github/workflows/core-images-build.yml: werf and buildah in a privileged, ephemeral in-cluster
Job, pushed to the node-local registry. Every Dockerfile takes its base image through
ARG BASE_REGISTRY and pins an @sha256: digest. The base images are declared in
grpc/infrastructure/images/baseimages.yaml and mirrored with core images mirror.
After a CD roll (core-cd.yml), workloads run a digest-pinned :latest@sha256:…. A bare
kubectl rollout restart then restarts onto the same digest. To move a workload onto new bits,
dispatch core-cd.yml with images=<img>.