Skip to content
Data-access runners

Data-access runners

A data-access runner is the process that dials a tenant source on CORE’s behalf. It is not a CI runner; GitHub Actions runners are a separate thing (docs/runners/cloud-runners.md). Design: docs/design/core-data-runners.md. Operator guide: docs/runners/self-hosted-data-runner.md.

The page is DataEx › Runners (?tab=runners). It was born under DevEx › Cluster and moved to DataEx on 2026-09-26, keeping its tab name. It is the shared org-runink/ui datasources RunnersService, mounted by datasources_ui.go.

Until rollout step 6, a connected self-hosted runner receives no work. RunnerService/Execute is declared in runners.proto and core-runner answers UNIMPLEMENTED. Every Resolve action and CapEx pull routed to a self-hosted runner is refused with runner_offline, and the detail says “connected; dispatch arrives in rollout step 6”. Use the Runink managed runner in the meantime.

The two kinds

Kind on the wireDisplayWhat it is
managedRunink managed, id consolethe console process itself. It is never stored and is synthesized on every read. It can never be revoked, re-addressed or enrolled.
self_hostedthe name you gave ita core-runner process on a host you run, stored as kind enrolled in the registry (var/lib/core-runners)

Registry limits (runners_store.go): 100 runners, 100 tokens, 8 labels per runner, and a 512 KiB budget. Writes over a limit are refused, never truncated.

CORE dials the runner

The owner reversed decision D3 on 2026-09-26: CORE connects out to each self-hosted runner at the address an admin entered. There is no runner listener, port, DNS record or devgateway pass-through on CORE’s side, and no CORE_RUNNER_ENDPOINT. The link is mutual TLS on the runner CA (core-system/core-runner-ca, a separate trust root from the mesh CA). The core-operator mints it once, and it is mounted at CORE_RUNNER_CA_DIR. Identity is the certificate, never the address, so IPv4, IPv6 and FQDN addresses all work, and changing an address is only a reconnect.

The core-operator pod network on the platform box is IPv6 single-stack today, so a runner must be reachable from the pod over IPv6, an AAAA FQDN, or NAT64.

Address grammar

host:port, where the host is an IPv4 address, a [bracketed] IPv6 address or an RFC 1123 DNS name, and the port is 1–65535 (runners_address.go). The console refuses URL schemes, userinfo (@), IPv6 zone ids, unbracketed IPv6, the unspecified and multicast addresses, and host labels that look like a token or key.

Enrol a self-hosted runner

You must be allowed by CORE_RUNNER_ADMINS. When it is unset, every signed-in identity is allowed.

Create the runner

Add runner on the page, or POST /api/runners {name, address, zone, labels}. The name is a DNS label. The response is {runner, ticket, command}. The runner starts PENDING.

Copy the ticket, which is shown once

The ticket is crt1. followed by base64url of the enrolment ticket: the token, the runner id and name, the expiry, and the runner CA’s SHA-256. It carries no endpoint. It is valid for 15 minutes, works once, and enrols only this runner. CORE stores only the token’s SHA-256, and holds the enrolment proof key in memory only. If the console restarts before the runner is reachable, issue a new token.

Start the runner on its host

CORE_RUNNER_TICKET='crt1.…' core-runner serve --listen :7443

The ticket goes in the environment. A ticket on the command line is refused, and the variable is unset as soon as it has been read. core-runner flags: --listen (env CORE_RUNNER_LISTEN, default :7443) and --state-dir (env CORE_RUNNER_STATE_DIR, default /var/lib/core-runner, mode 0700). A runner that CORE deployed reads its ticket from CORE_RUNNER_TICKET_FILE instead. That file must be regular and 0600 or 0400, and setting both variables is refused.

CORE enrols it

CORE dials the address, then retries with backoff. Connect now (POST /api/runners/{id}/connect) tries at once. Both sides prove they hold the token with an HMAC bound to that TLS connection’s exporter, so a relay fails. The runner sends a CSR for a key it just generated. CORE signs a 24-hour runner server certificate that names only spiffe://core.runink/runner/<id>, with no DNS or IP name, and consumes the token in one step. The runner is ACTIVE.

State on the runner host: identity.pem (certificate and private key, 0600), ca.pem and runner.json, key.pending.pem (present only during an enrolment or renewal), and revoked (written on revocation, after which the runner refuses to start).

Operating a runner

ActionRouteNotes
Healthpolled every 15 s (RunnerService/Health)every figure is a runner claim and is labelled so. Health never touches a source
Renewalautomaticbefore a third of the leaf’s life is left, CORE asks for a CSR over a fresh key, records it as the NEXT fingerprint, installs it, then promotes it. The old leaf is then refused
Edit addressPATCH /api/runners/{id}audited with the old and new address. The certificate stays valid. A zone or label change on a PENDING runner revokes its live token
New ticketPOST /api/runners/{id}/tokenPENDING runners only. The old token is revoked
Revoke tokenDELETE /api/runner-tokens/{id}an unused token
Revoke runnerPOST /api/runners/{id}/revokesends Goodbye(REVOKED) best-effort and never dials again. The runner writes revoked and exits 0

GET /api/runners is session-gated and never dials. For each runner it serves id, name, displayName, kind, address, zone, labels, state and health.{connected, lastSeen, version, certNotAfter, inFlight, limit}. The concurrency limit is 4 (decision D9), reported with limitEnforced:false until step 6. An unreadable registry answers 503, never “only console”.

Choosing the runner per action

Every Resolve action and CapEx PullFeed accepts an optional runner:

  • absent: use the connection’s binding, whose empty value is console;
  • console: Runink managed, explicitly, even for a connection bound to a self-hosted runner;
  • a name: that runner, with no fallback.

The runner is picked at audit time on Intelligence › Data audit. A connection’s default binding is set in the Connections wizard with the same picker. The effective runner and whether it was “chosen for this action” or “the connection’s binding” are recorded in the audit detail.

Refusal codes

From atlas_res.go. None of them opens a credential bundle or calls the engine, and none falls back to another runner:

ConditionOutcome
name not registeredrunner_unknown
runner REVOKEDrunner_revoked
runner PENDINGrunner_offline (not enrolled yet)
ACTIVE, not connectedrunner_offline
ACTIVE and connectedrunner_offline, detail “connected; dispatch arrives in rollout step 6”
registry unreadablefailed, “runner registry unavailable” (never runner_unknown)

runner_busy is reserved for step 6, when the concurrency cap is enforced.

Runners CORE deploys, and the outbound channel

runners_deploy.go lets CORE provision a runner itself: on an on-premise host over SSH, on a Compute Engine VM in your GCP project, as a Snowpark Container Services service, or on a Databricks job cluster. The ticket is handed over inside the target. Deploying is admin-only with a stricter rule than the rest of the page: an unset CORE_RUNNER_ADMINS refuses everyone. Credentials are referenced by id under CORE_DEPLOY_SECRETS_DIR/<id>/<field>, never typed. The operator guide says Snowflake and Databricks are listed but not yet deployable.

A runner on Snowflake SPCS or a Databricks job cluster cannot be dialled. For those targets only, and pending owner confirmation, the direction is reversed (runners_channel.go). CORE listens on CORE_RUNNER_CHANNEL_LISTEN and hands deployed runners CORE_RUNNER_CHANNEL_ADDR. Both must be set, or those targets are unavailable. Such a runner is registered with no address and runs core-runner serve with CORE_RUNNER_CHANNEL set. It gets a runner client certificate, and CORE runs Health and renewal over that connection. A runner that has an address can neither enrol nor serve over the channel. On-premise and GCP runners are unchanged.

What a runner never does

It never dials CORE, except over the outbound channel above. It holds no source configuration and no stored credentials, schedules nothing, and its Health never touches a source.