# Configuration reference

`config.yaml` is deliberately minimal: **bootstrap only.** It says how to
find and secure Runku's own admin database, which target databases to
protect, and how to verify OIDC tokens. It does **not** say which tables
are exposed, what's masked, what writes are allowed, or who's allowed to
do what — that all lives in Runku's own admin database (see
[auth-and-permissions.md](auth-and-permissions.md) and
[policy.md](policy.md)), managed live via the `runku auth`/`runku
policy` CLI, never by hand-editing this file and redeploying.

Parsing is strict: unknown fields are a hard error at startup, not a
silently-ignored typo. `${VAR}` anywhere in the file is expanded from
the environment before parsing.

## Minimal example

```yaml
# HTTP listener shared by the portal and MCP endpoints.
server:
  # TCP port exposed by the runku process.
  port: 8080

# Private Runku state: policy, identities, roles and API-key hashes.
# This database is never published as a connector.
store:
  # PostgreSQL is recommended for production and multiple instances.
  driver: postgres
  # Dedicated admin-store DSN, injected instead of committed to this file.
  dsn: ${RUNKU_MCP_STORE_DSN}

# External sources published through governed MCP endpoints.
connectors:
  # Stable identifier for this independently governed source.
  - id: main
    database:
      # External source engine: postgres, mysql or sqlserver.
      driver: postgres
      # Source credentials; deliberately separate from RUNKU_MCP_STORE_DSN.
      dsn: ${DATABASE_URL}
```

## `store` and `connectors` are different boundaries

They may both use PostgreSQL, but they must not be treated as the same
database or credential:

| Configuration | Purpose | Typical contents | Accessed by agents |
|---|---|---|---|
| `store` | Runku's private control state | Policies, roles, identities, API-key hashes and connector metadata | Never |
| `connectors[]` | External sources governed by Runku | Your application or business data | Only through generated tools allowed by policy |

```mermaid
flowchart LR
  Operator[Operator] -->|manages policy and identities| Store[(Runku admin store)]
  Agent[MCP client] -->|typed tool call| Runku[Runku MCP Forge]
  Store -->|policy and authorization| Runku
  Runku -->|validated, parameterized operation| Source[(Connected source)]
  Source -->|allowed result| Runku
  Runku -->|masked result| Agent
```

The admin store belongs to the Runku deployment. A connector belongs to the
operator and represents a source Runku protects. Use a dedicated database and
credentials for the PostgreSQL admin store, even when a connected source also
uses PostgreSQL.

## `server`

| Field | Type | Default | Notes |
|---|---|---|---|
| `port` | int | `8080` | |
| `tls` | object, optional | disabled | see below |

```yaml
# Listener and optional transport encryption.
server:
  # TCP port used by the service.
  port: 8080
  tls:
    # Serve HTTPS directly. Disable when a trusted reverse proxy terminates TLS.
    enabled: true
    # Mounted PEM certificate chain and matching private key.
    cert_file: /etc/runku/tls/cert.pem
    key_file: /etc/runku/tls/key.pem
```

`tls.enabled: true` switches the listener to `ListenAndServeTLS` —
`cert_file`/`key_file` are then required. Without it, Runku serves plain
HTTP, same as every version before TLS support existed — the common case
being a reverse proxy in front of it that already terminates TLS.

## `store`

Runku's own admin database — roles, identities, API key hashes, role
bindings, and each connector's policy. **Never** one of the databases
Runku protects; a completely separate engine and connection.

| Field | Type | Notes |
|---|---|---|
| `driver` | string | `"sqlite"` or `"postgres"`; both are implemented. |
| `path` | string | SQLite only. Required when `driver: sqlite`; forbidden otherwise. |
| `dsn` | string | Postgres only. Required when `driver: postgres`; forbidden otherwise. |

PostgreSQL is the recommended production backend and supports shared or
multi-instance deployments. Keep its DSN in the environment or a secrets
manager:

```yaml
# Dedicated database for Runku's internal operational state.
store:
  # Recommended for production, HA and multi-instance deployments.
  driver: postgres
  # Secret DSN for the admin database; not a connector DSN.
  dsn: ${RUNKU_MCP_STORE_DSN}
```

SQLite is fully supported for evaluation, local operation and small
single-instance deployments:

```yaml
# Simple single-instance admin store for evaluation or small installations.
store:
  # Embedded storage; no external database server is required.
  driver: sqlite
  # Persistent file containing Runku policy, roles and identities.
  path: data/runku.db
```

Exactly one engine, never a hybrid — `store` is unrelated to
`connectors[].database`, which is the database(s) being protected.

## `connectors`

At least one required. Each connector is one target database, exposed on
its own MCP endpoint — a single `runku serve` process can serve several
simultaneously.

| Field | Type | Default | Notes |
|---|---|---|---|
| `id` | string | `"default"` for the first connector if omitted | Must be unique. |
| `path` | string | `/mcp/<id>` | Must start with `/`; must be unique. |
| `database.driver` | string | — | `postgres` \| `mysql` \| `sqlserver` |
| `database.dsn` | string | — | Required |

```yaml
# Each connector is an external source with an independent MCP URL and policy.
connectors:
  # First source: a PostgreSQL business database.
  - id: postgres-main
    # Explicit MCP route; defaults to /mcp/<id> when omitted.
    path: /mcp/postgres-main
    database:
      # Driver used to introspect and query this external source.
      driver: postgres
      # Credentials belong to the source, not Runku's admin store.
      dsn: ${DATABASE_URL}
  # Second source: a separate MySQL business database.
  - id: mysql-secondary
    database:
      driver: mysql
      dsn: ${MYSQL_DSN}
```

DSN formats per engine:

- **postgres**: `postgres://user:pass@host:5432/db?sslmode=disable`
- **mysql**: `user:pass@tcp(host:3306)/db?parseTime=true&multiStatements=true`
- **sqlserver**: `sqlserver://user:pass@host:1433?database=db&encrypt=disable`

What each connector actually exposes (tables, columns, mutations,
procedures) is **not** configured here — see
[policy.md](policy.md).

## `audit`

Local audit buffering, optional outbound shipping, optional rotated file
logging. The stdout audit line (one JSON object per tool call) is always
on, unconditionally, regardless of anything in this section — this
section only adds sinks on top of it.

| Field | Type | Default | Notes |
|---|---|---|---|
| `sqlite_path` | string | disabled if empty | Local SQLite buffer — powers `/reports/*` and the portal. |
| `shipper` | object | disabled | See below. |
| `runtime_metrics` | object | disabled | Same shape as `shipper`, a separate channel for runtime/tool-analysis metrics. |
| `log_file` | object, optional | disabled | Writes the same trail to a rotated file, in *addition* to stdout. |

```yaml
# Optional audit sinks in addition to the always-enabled stdout audit stream.
audit:
  # Local outbox and portal/reporting buffer; separate from the admin store.
  sqlite_path: data/runku-audit.db
  shipper:
    # HTTPS receiver for batched audit events.
    endpoint: ${RUNKU_SHIPPER_ENDPOINT}
    # Delivery cadence and maximum events per request.
    interval: 30s
    batch_size: 200
    headers:
      # Optional static authentication header.
      X-Api-Key: ${SHIPPER_API_KEY}
    oauth2:
      # Optional OAuth2 client-credentials authentication for the receiver.
      token_url: https://idp.example.com/oauth2/token
      client_id: ${SHIPPER_CLIENT_ID}
      client_secret: ${SHIPPER_CLIENT_SECRET}
      scopes: [audit:write]
  log_file:
    # Optional rotated local JSON audit file.
    path: data/runku-audit.log
    # Rotation size, retained files, age and compression behavior.
    max_size_mb: 100
    max_backups: 5
    max_age_days: 30
    compress: false
```

`shipper` is a plain webhook POST — it has no idea any particular
backend exists; point it at whatever ingests JSON over HTTPS. Entries
are only deleted from the local buffer **after a confirmed successful
delivery** (an outbox pattern — a failing shipper never loses data, it
just accumulates locally). `headers` are static, sent on every request.
`oauth2` is the standard client-credentials grant (RFC 6749 §4.4) —
token fetch, caching and refresh are handled entirely by the underlying
OAuth2 library. Both may be combined; both are optional; with neither
set, the shipper POSTs unauthenticated.

Leaving `shipper.endpoint` empty disables shipping. Entries remain in the local SQLite buffer for reports and the portal until retention removes them.

## `auth`

Authenticates every request to `/mcp` and `/reports/*` — who's calling,
verified cryptographically, not self-reported. Disabled by default.

| Field | Type | Default | Notes |
|---|---|---|---|
| `enabled` | bool | `false` | |
| `oidc` | object, optional | unset | Only how to *verify* incoming tokens — issuing/storing credentials is never Runku's job. |

```yaml
# Request authentication. Authorization remains in roles/policy in the store.
auth:
  # Require a valid API key or verified OIDC bearer token.
  enabled: true
  oidc:
    # Trusted issuer discovered through its OIDC metadata endpoint.
    issuer: https://idp.example.com/realms/runku
    # Audience that incoming access tokens must contain.
    audience: runku-mcp
    # Public PKCE client used only by browser portal login.
    portal_client_id: runku-portal
```

- `issuer` / `audience` — standard OIDC discovery
  (`/.well-known/openid-configuration`) + JWKS verification. `audience`
  must match a claim your provider actually issues (with Keycloak, this
  means the client needs an audience protocol mapper).
- `portal_client_id`, optional — a **public** OIDC client (Authorization
  Code + PKCE, no secret) the browser-based portal uses for its "log in
  with SSO" button. Distinct from `audience`, which is only ever used to
  verify tokens on incoming requests. Unset just disables that button —
  the token-paste login path still works regardless.

API keys are **not** configured here — see
[auth-and-permissions.md](auth-and-permissions.md); they live in the
admin store and are managed with `runku auth apikey`.

## Configuration checklist

Before starting the service, validate the listen address, persistent store
path or DSN, connector DSNs, audit retention, TLS and authentication settings.
Unknown fields fail startup so configuration mistakes are visible immediately.
