Skip to content

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

ModuleWhereContents
github.com/org-runink/coregrpc/the core CLI (grpc/cmd/core), the other grpc/cmd/* binaries, grpc/infrastructure/*, the agent fleet under grpc/agents/
github.com/org-runink/face-operatorgrpc/operators/runinkthe FACE Kubernetes operator
github.com/org-runink/pulse-operatorgrpc/operators/pulsethe PULSE Kubernetes operator
github.com/org-runink/core-operatorgrpc/operators/corethe generic ClientInstance CRD and its controller, the provisioner, and the CORE console
github.com/org-runink/placementgrpc/operators/placementshared 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:

BinaryRole
corethe single control surface: dev stack, deploys, images, security scans, sessions, the install wizard. See the CLI reference.
devgatewaythe 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.
ambassadora small static mTLS TCP proxy built on security/pki, which replaced HAProxy. Its image is FROM scratch.
modelrouterthe one admission queue in front of the inference engines.
forgecheckthe verifier central CI runs on FORGE projects.
core-runnerthe 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):

FieldMeaning
appthe application identity. Drives the core.runink.org/app label and the dedicated-taint toleration.
tenantthe tenant key. The core.runink.org/tenant label plus soft node affinity.
rolecontrolplane or runner (default runner): selects the PriorityClass.
imagethe container image to run.
portthe container and service port (default 8080).
replicasdefault 1; ignored when suspend is true.
computeUnitsthe CU budget, mapped to real CPU and memory limits.
tagsclient labels added to the pod template and ServiceAccount.
placementnodeSelector, tolerations (added to the defaults), affinity (replaces the default soft affinity).
envextra environment variables.
usersaccess grants: email, role (admin, writer or reader, default reader), optional phone, optional expiresAt. Projected as AUTH_ALLOWED_EMAILS and AUTH_SEED_USERS.
subscriptiontier (lite, dedicated or enterprise), seats, status (active, suspended or past_due).
suspendscale 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/pki is 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 ca mints and distributes that CA.
  • Encryption at rest. security/secrets/envelope is AES-GCM envelope encryption, applied unconditionally by store/objectstore and store/connections. There is no switch to turn it off. Anything that opens either store needs CORE_ENVELOPE_KEK and refuses to start without it.
  • The object store. CORE runs its own S3-compatible objectd (from the store library), not the MinIO server. objectd verifies SigV4, and core-operator’s provisioner mints every objectd key as an objectd-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 up generates its local KEK with crypto/rand on 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_ENDPOINT and 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>.