# Catalyst Blueprint scratch chart for NewAPI.
#
# Per docs/INVIOLABLE-PRINCIPLES.md #4 (never hardcode), every
# operationally-meaningful value is configurable; cluster overlays in
# clusters/<sovereign>/bootstrap-kit/ may override any of these without
# rebuilding the Blueprint OCI artifact. The Sovereign FQDN, Keycloak
# issuer, channel endpoints, channel keys, and TLS issuer are all
# operator-supplied at install time.

catalystBlueprint:
  upstream:
    chart: "" # scratch chart — no upstream Helm chart
    version: ""
    repo: ""
    images:
      newapi: "ghcr.io/openova-io/openova/newapi-mirror"
# ─── Image pull secrets (issue #952) ─────────────────────────────────────
# Both the NewAPI mirror image and the metering sidecar live in PRIVATE
# ghcr.io/openova-io/openova/* repositories. Without an imagePullSecret
# on the Pod, kubelet cannot authenticate to pull them and the Pod stalls
# in ImagePullBackOff with a 403 Forbidden — blocking alice signup gate
# 5 (LLM) on every fresh Sovereign.
#
# `ghcr-pull` is the canonical name created in flux-system at cloud-init
# (infra/hetzner/cloudinit-control-plane.tftpl) and reflected into every
# bootstrap-kit namespace listed in the source Secret's
# `reflector.v1.k8s.emberstack.com/reflection-{allowed,auto}-namespaces`
# annotation. The newapi namespace is included in that list so this
# default works on every Sovereign without operator intervention. Per
# Inviolable Principle #4 the list is overrideable from per-Sovereign
# overlays (set to `[]` for clusters that pull from public repos only).
imagePullSecrets:
  - name: ghcr-pull
# ─── NewAPI application ──────────────────────────────────────────────────
newapi:
  enabled: true
  # Solo-Sovereign minimum — single replica. Per-Sovereign overlays bump
  # to 2+ once HA is needed (NewAPI is stateless once Postgres + Valkey
  # carry the persistent state).
  replicas: 1
  # Pin upstream image — DO NOT use floating tags per
  # docs/INVIOLABLE-PRINCIPLES.md #4. The chart references a
  # GHCR-mirrored image (operator's GHCR or OpenOva's mirror namespace)
  # to avoid pulling from Docker Hub at runtime. The mirror is built by
  # `.github/workflows/build-bp-newapi.yaml` — every push to
  # `platform/newapi/chart/**` triggers the mirror Job which captures the
  # upstream repo digest and re-tags into
  # `ghcr.io/openova-io/openova/newapi-mirror:<UPSTREAM_VER>`.
  #
  # Pre-2026-05-10 the chart referenced `:v0.4.5` — a tag that never
  # existed in any upstream OR in our GHCR (the original commit
  # 44d0200a invented the tag with no matching CI build). Every fresh
  # Sovereign's NewAPI Pod ImagePullBackOff'd, blocking alice signup
  # gate 5 (LLM). Bounded-cycle audit prov #7 Gap F.
  #
  # The tag is the upstream Calcium-Ion/new-api git tag (verbatim,
  # including the leading `v`). Bump in lockstep with
  # `.github/workflows/build-bp-newapi.yaml`'s `DEFAULT_UPSTREAM_VERSION`
  # AND `Chart.yaml` `appVersion` (which strips the leading `v` per
  # Helm convention).
  image:
    repository: ghcr.io/openova-io/openova/newapi-mirror
    tag: "v0.13.2"
    pullPolicy: IfNotPresent
  # NewAPI HTTP listener — port 3000 is the upstream default.
  port: 3000
  resources:
    requests:
      cpu: 100m
      memory: 256Mi
    limits:
      cpu: 2
      memory: 1Gi
  # SecurityContext — non-root.
  securityContext:
    runAsNonRoot: true
    runAsUser: 1000
    readOnlyRootFilesystem: false
    allowPrivilegeEscalation: false
    capabilities:
      drop: ["ALL"]
  # Liveness / readiness — NewAPI exposes /api/status (verified via
  # upstream code path controller/misc.go:GetStatus).
  probes:
    liveness:
      path: /api/status
      port: 3000
      initialDelaySeconds: 30
      periodSeconds: 10
      timeoutSeconds: 5
      failureThreshold: 3
    readiness:
      path: /api/status
      port: 3000
      initialDelaySeconds: 10
      periodSeconds: 10
      timeoutSeconds: 5
      failureThreshold: 3
# ─── Postgres backend (bp-cnpg) ──────────────────────────────────────────
# NewAPI persists users, credits, channels, tokens, audit log to
# Postgres. Catalyst routes this to a managed Postgres provisioned via a
# Crossplane PostgresqlInstance claim against bp-cnpg. The DSN is
# operator-supplied via ExternalSecret; never hardcoded.
database:
  # ExternalSecret name carrying the full DSN. Required key: SQL_DSN
  # (e.g. "postgres://user:pass@host:5432/newapi?sslmode=require").
  # Pre-#943 this was REQUIRED — the deployment.yaml gate skipped Pod
  # render when empty, blocking alice signup gate 5 (LLM) on every
  # fresh Sovereign. Issue #943 (2026-05-05): when empty, the chart
  # auto-provisions via .Values.cnpg.* below and the Pod reads from
  # the chart-emitted DSN Secret. Operator may still set this to
  # bypass auto-provision and wire NewAPI at an external Postgres
  # (the contabo path historically did this via Crossplane).
  existingSecret: "" # operator-supplied (e.g. "newapi-db-dsn")
  existingSecretKey: "SQL_DSN"
# ─── CNPG-backed auto-provisioned Postgres (issue #943) ──────────────────
# When enabled (DEFAULT true), the chart renders templates/cnpg-cluster.yaml:
#   - postgresql.cnpg.io/v1.Cluster `<release>-newapi-pg`
#   - Secret `<release>-newapi-db-dsn` (key: SQL_DSN) read by the Pod
# The Cluster's auto-emitted basic-auth Secret is read via Helm `lookup`
# and the DSN string is composed at render time. Per Inviolable Principle
# #4 the instance count, image, storage, and SSL mode are all operator-
# tunable via per-Sovereign overlay.
#
# Capabilities-gated on postgresql.cnpg.io/v1 — when bp-cnpg is not yet
# Ready the Cluster CR is skipped and chart install does not fail.
cnpg:
  enabled: true
  # Override the auto-emitted DSN Secret name. Default empty ⇒ derived
  # from release name (`<release>-newapi-db-dsn`).
  dsnSecretName: ""
  # Database name created by CNPG initdb.
  database: newapi
  # SSL mode component of the DSN string. CNPG enforces TLS by default
  # so `require` matches the cluster's posture; operators may relax to
  # `prefer` on dev clusters by overlaying this value.
  sslMode: "require"
  cluster:
    instances: 1
    imageName: "ghcr.io/cloudnative-pg/postgresql:16.4"
    storage: "5Gi"
    storageClassName: "" # empty ⇒ default StorageClass
    owner: "newapi"
    # Optional bootstrap SQL — empty by default. Operators may add
    # CREATE EXTENSION / additional databases here without forking
    # the chart.
    postInitApplicationSQL: []
    resources:
      requests:
        cpu: 50m
        memory: 128Mi
      limits:
        cpu: 500m
        memory: 512Mi
# ─── Valkey cache (bp-valkey) ────────────────────────────────────────────
# Session store and rate-limit counters. Optional but recommended
# (NewAPI falls back to in-memory; in-memory loses state on restart).
valkey:
  enabled: true
  url: "redis://valkey.valkey.svc.cluster.local:6379"
  # If the operator's Valkey requires auth, supply via ExternalSecret.
  # Required key: REDIS_CONN_STRING (full URL with credentials).
  existingSecret: "" # e.g. "newapi-valkey-conn"
  existingSecretKey: "REDIS_CONN_STRING"
# ─── App credentials (NewAPI-internal) ───────────────────────────────────
# SESSION_SECRET (random 32+ bytes) and CRYPTO_SECRET (random 32+ bytes)
# are NewAPI-internal cryptographic material.
#
# Issue #943 (2026-05-05): the chart auto-provisions these via
# templates/credentials-secret.yaml when autoProvision=true (DEFAULT)
# AND existingSecret is empty. Persistence across reconciles via Helm
# `lookup` — same load-bearing pattern as gitea-admin-secret (issue
# #830 Bug 2). Without this, every reconcile would invalidate every
# active session cookie and every encrypted token across NewAPI's user
# base. helm.sh/resource-policy: keep so a `helm uninstall` + reinstall
# recovers the same bytes.
#
# Operator may bring their own bytes via per-Sovereign overlay setting
# `existingSecret` — when non-empty it takes precedence and the chart-
# emitted Secret is NOT rendered.
credentials:
  existingSecret: "" # operator-supplied (e.g. "newapi-app-creds")
  autoProvision: true
  # Override the auto-emitted credentials Secret name. Default empty
  # ⇒ derived from release name (`<release>-app-creds`).
  autoSecretName: ""
# ─── Auth: ops-staff admin UI ────────────────────────────────────────────
# The admin UI at admin.<host> is gated by the operator's IdP. Default
# mode is `keycloak` (OIDC against bp-keycloak). `masterKey` is allowed
# only for bootstrap-time access before Keycloak realm is ready.
auth:
  adminUI:
    mode: "keycloak" # keycloak | masterKey
    keycloak:
      issuer: "" # operator-supplied (e.g. "https://keycloak.<sovereign>/realms/ops")
      clientId: "newapi-admin"
      callbackPath: "/oauth/callback"
      # ExternalSecret carrying client_secret. Required key:
      # OIDC_CLIENT_SECRET.
      existingSecret: "" # e.g. "newapi-oidc"
    # Bootstrap-only master key. Set via ExternalSecret. Required key:
    # MASTER_KEY. The blueprint's smoke test ASSERTS this is unset on
    # production overlays.
    masterKeySecret: ""
  customerAPI:
    # `catalyst` (DEFAULT): customer-facing portal UI is disabled at
    # ingress. Catalyst issues per-user keys via NewAPI's admin API.
    # `self-serve`: customer-facing portal is enabled (NOT recommended
    # in Catalyst Sovereigns).
    keyIssuer: "catalyst"
# ─── Channels ────────────────────────────────────────────────────────────
# Operator-defined LLM provider channels. Every channel MUST carry a
# verifiable `attestation` block; render fails with a helpful error if
# any enabled channel lacks attestation. See blueprint.yaml configSchema
# and platform/newapi/README.md for the channel-tier model.
#
# Example (operator overrides this list in their per-Sovereign overlay):
#
# channels:
#   - name: qwen
#     type: vllm
#     endpoint: http://vllm.vllm.svc.cluster.local:8000/v1
#     models: ["Qwen/Qwen2.5-72B-Instruct"]
#     attestation:
#       kind: in-cluster
#       owner: "<sovereign-operator>"
#   - name: claude
#     type: anthropic
#     endpoint: https://api.anthropic.com
#     existingSecret: "newapi-channel-anthropic"
#     models: ["claude-sonnet-4-6", "claude-haiku-4-5"]
#     attestation:
#       kind: commercial-contract
#       accountId: "<anthropic-account-id>"
#       contractRef: "<internal-doc-link>"
#   - name: byok
#     type: byok-passthrough
#     attestation:
#       kind: byok
channels: []
# First-otech default vLLM channel. Disabled by default — opt-in per
# Sovereign via the bootstrap-kit overlay (slot 80-newapi.yaml). When
# `enabled: true` AND `endpoint != ""`, the chart composes a `vllm`-typed
# channel into the rendered channels list.
#
# Per docs/INVIOLABLE-PRINCIPLES.md #4 (never hardcode), the endpoint and
# model identifiers are operator-supplied at install time. The Catalyst
# repo references the OpenOva marketing site's wired upstream
# (`https://llm-api.omtd.bankdhofar.com`, model `qwen3-coder`, used by
# Axon at `openova-private/clusters/contabo-mkt/apps/axon/helmrelease.yaml`)
# as the canonical first-otech default ONLY for documentation; the
# chart's defaults here are empty so an unconfigured deploy does not
# silently wire customers to a third-party endpoint.
defaultChannels:
  vllm:
    # Set true in the per-Sovereign bootstrap-kit overlay. When false,
    # this block is ignored and the channel is NOT composed.
    enabled: false
    # Customer-visible model prefix.
    name: "qwen"
    # Upstream vLLM endpoint URL. Operator-supplied. The chart never
    # bakes a default endpoint — `helm template` with
    # `defaultChannels.vllm.enabled: true` and an empty `endpoint` is a
    # configuration error caught at render time.
    endpoint: ""
    # Models exposed via this channel.
    models:
      - "qwen3-coder"
    # ExternalSecret name holding the upstream API key (omit for truly
    # in-cluster vLLM where no key is required).
    existingSecret: ""
    # Attestation block. Default kind = `in-cluster` (operator owns the
    # weights and inference hardware behind the relay). Overlay the kind
    # to `commercial-contract` if the relay terminates at a third-party
    # provider; in that case `accountId` + `contractRef` become required.
    attestation:
      kind: "in-cluster"
      owner: ""
  # ─── Qwen3.6 @ BankDhofar (canonical first channel — issue #915) ─────
  # First-class default channel: Qwen3.6 hosted at BankDhofar
  # (`https://llm-api.omtd.bankdhofar.com`, model `qwen3-coder` / alias
  # `qwen3.6`). When `qwenBankDhofar.enabled: true`, the chart:
  #   1. Composes this entry into the EFFECTIVE channels list as
  #      channel #1 (BEFORE any operator-supplied `.Values.channels`
  #      and BEFORE the generic `defaultChannels.vllm` block) — so a
  #      fresh customer landing on a fresh Sovereign gets routed here
  #      by default.
  #   2. Triggers the `channel-seed` post-install/post-upgrade Helm
  #      hook Job that probes NewAPI's admin API for the channel by
  #      name and POSTs it if missing (idempotent — re-runs after an
  #      upgrade are no-ops).
  #
  # Per docs/INVIOLABLE-PRINCIPLES.md #4 (never hardcode), every knob
  # below is configurable via the per-Sovereign bootstrap-kit overlay.
  # The endpoint default is empty so a `helm template` with default
  # values does not silently wire customers to a third-party host;
  # operators set `endpoint: https://llm-api.omtd.bankdhofar.com` in
  # their per-Sovereign overlay (matches the canonical wiring in
  # openova-private/clusters/contabo-mkt/apps/axon/helmrelease.yaml,
  # the SAME relay the OpenOva marketing site uses).
  qwenBankDhofar:
    # Toggle the channel composition + seed-Job render. Default false
    # so a CI smoke render produces no Job; per-Sovereign overlay flips
    # this true with the canonical endpoint + a wired ExternalSecret.
    enabled: false
    # Customer-visible channel name. Channel name is the human-readable
    # row label in NewAPI's admin UI AND is the idempotency key for
    # the seed Job's existence-probe (GET /api/channel/?keyword=<name>).
    name: "qwen3.6-bankdhofar"
    # Upstream OpenAI-compatible endpoint URL. The chart leaves this
    # empty so an unconfigured deploy never silently dials a third-
    # party host. Per-Sovereign overlay sets it to
    # `https://llm-api.omtd.bankdhofar.com` (canonical first-otech
    # default; matches the existing Axon helmrelease).
    endpoint: ""
    # Customer-facing model identifiers exposed via this channel. NewAPI
    # routes incoming requests whose `model` field matches any string
    # here to this channel. The default list contains both the
    # human-friendly `qwen3.6` alias (used in product docs) and the
    # upstream-canonical `qwen3-coder` (matches the Axon helmrelease at
    # https://llm-api.omtd.bankdhofar.com — DO NOT delete this alias
    # or existing OpenClaw/Axon deployments stop routing).
    models:
      - "qwen3.6"
      - "qwen3-coder"
    # ExternalSecret name carrying the upstream API key under the
    # canonical key `API_KEY`. Operator overlay creates this Secret
    # (or wires an ExternalSecret pulling from OpenBao) before
    # enabling the channel; the seed Job mounts it and POSTs the
    # value as the channel's `key` field to NewAPI's admin API. The
    # canonical OpenBao path is
    # `sovereign/<sovereign-fqdn>/newapi/channel-qwen-bankdhofar` with
    # property `API_KEY` (operator overlay supplies the ExternalSecret).
    existingSecret: "newapi-channel-qwen-bankdhofar"
    # Secret key name holding the upstream API key.
    existingSecretKey: "API_KEY"
    # Channel attestation. The Qwen3.6@BankDhofar relay is operated by
    # Bank Dhofar (a commercial counterparty); the attestation kind is
    # `commercial-contract` per platform/newapi/README.md compliance
    # posture. Operator overlay supplies `accountId` + `contractRef`
    # (legal-team-owned doc link) before flipping `enabled: true`.
    attestation:
      kind: "commercial-contract"
      accountId: ""
      contractRef: ""
# ─── Channel-seed Job (issue #915) ───────────────────────────────────────
# Post-install / post-upgrade Helm hook Job that POSTs default channels
# (currently `defaultChannels.qwenBankDhofar`) into NewAPI's admin API
# so the FIRST customer request lands on a fully-wired channel, not a
# 404 / 400. The Job is idempotent: it probes existence via
# GET `/api/channel/?keyword=<name>` and skips channels that already
# exist; only newly-added entries POST against NewAPI.
#
# Authorization: NewAPI's admin API requires either a logged-in admin
# session cookie OR the bootstrap MASTER_KEY in the
# `Authorization: Bearer <master-key>` header. The seed Job mounts the
# operator-supplied master-key Secret (`auth.adminUI.masterKeySecret`)
# and uses it ONCE at install time. After bootstrap completes, the
# operator may rotate the master key out — channels persist in
# Postgres independent of the master key.
#
# Defaults to enabled=true so a Sovereign with `qwenBankDhofar.enabled`
# also seeds the channel. Operators that load channels via an
# out-of-band controller flip this off to suppress the Job entirely.
channelSeed:
  enabled: true
  # Image for the curl-based seed loop. Pinned at the upstream-mutable
  # default tag; production overlays SHA-pin per
  # docs/INVIOLABLE-PRINCIPLES.md #4a.
  image:
    repository: docker.io/curlimages/curl
    tag: "8.10.1"
    pullPolicy: IfNotPresent
  # Resource budget — small. Each channel is one GET (probe) + zero
  # or one POST against the in-cluster NewAPI Service.
  resources:
    requests:
      cpu: 10m
      memory: 32Mi
    limits:
      cpu: 100m
      memory: 64Mi
  # Hook delete policy — keep the most recent Job around for operator
  # forensics (`kubectl logs job/<release>-channel-seed`) but delete
  # the previous one before each install/upgrade so we don't pile up
  # immutable Jobs across upgrades.
  hookDeletePolicy: "before-hook-creation"
  # backoffLimit — retries on transient errors (NewAPI Pod still
  # warming up post-rollout) before the Job marks itself Failed.
  backoffLimit: 6
  # activeDeadlineSeconds — upper bound on Job runtime. 5 minutes
  # is generous for a single-channel seed loop that retries through
  # NewAPI's startup window.
  activeDeadlineSeconds: 300
# ─── Geographic AUP enforcement ──────────────────────────────────────────
# Blocks requests from sanctioned regions on commercial-provider
# channels. US/EU export-control baseline by default. Operator extends
# per their jurisdiction's requirements. Reduction below baseline
# requires `geo.sanctionedRegions.allowRelaxation: true` AND a recorded
# justification reviewed by counsel.
geo:
  sanctionedRegions:
    block:
      - IR
      - KP
      - SY
      - CU
      - "RU-occupied-UA"
    allowRelaxation: false
# ─── Provider AUP enforcement ────────────────────────────────────────────
aup:
  enforcement: "provider-strictest" # provider-strictest | per-channel | off
# ─── Audit log ───────────────────────────────────────────────────────────
audit:
  enabled: true
  retentionDays: 730
  # Default false — metadata only. Operator may opt in per privacy
  # posture; MUST be disclosed in customer ToS if enabled.
  logPromptContent: false
# ─── BYOK ────────────────────────────────────────────────────────────────
byok:
  scope: "request-scoped" # request-scoped | never
# ─── Reseller disclosure ─────────────────────────────────────────────────
reseller:
  disclosureRequired: true
# ─── Catalyst integration ───────────────────────────────────────────────
# When enabled, the chart provisions a Kubernetes secret
# `catalyst-newapi-admin-token` consumed by Catalyst's signup hook to
# issue per-user API keys against NewAPI's admin API.
#
# ADR-0003 §3.2 + §6 binds this Secret. The unified-rbac service in the
# OTECH control plane reads it and `Authorization: Bearer <ADMIN_API_TOKEN>`
# against `http://newapi.newapi.svc/api/v1/admin/users` to issue per-user
# keys. Idempotency on `external_id` (return-existing api_key on 409) is
# the responsibility of the NewAPI admin API itself, not this chart.
catalystIntegration:
  enabled: true
  # ExternalSecret name carrying the admin API token NewAPI generates at
  # bootstrap. Required key: ADMIN_API_TOKEN. The token is rotated by
  # the operator on a schedule documented in
  # docs/CATALYST-CLI-AGENT.md.
  existingSecret: "catalyst-newapi-admin-token"
  # ExternalSecret render — gated, default true. Sources the admin token
  # from the operator's OpenBao via a ClusterSecretStore (default
  # `vault-region1`, the bp-external-secrets-stores default). The remote
  # path is operator-supplied; the canonical convention per
  # docs/INVIOLABLE-PRINCIPLES.md #4 is
  # `sovereign/<sovereign-fqdn>/newapi/admin-token` (under the store's
  # `secret/` mount) with property `ADMIN_API_TOKEN`. The cluster overlay
  # at `clusters/<sovereign>/bootstrap-kit/80-newapi.yaml` sets
  # `externalSecret.remoteRef.key` per Sovereign.
  externalSecret:
    enabled: true
    refreshInterval: "1h"
    secretStoreRef:
      # ClusterSecretStore (Catalyst default = `vault-region1`, shipped
      # by bp-external-secrets-stores). Overlays may swap to a
      # SecretStore by switching kind.
      kind: "ClusterSecretStore"
      name: "vault-region1"
    # Remote OpenBao path. Per ADR-0001 the secret-store backend is
    # OpenBao via the vault provider; the path uses the operator's
    # convention. Operator overlay supplies the per-Sovereign value.
    remoteRef:
      # `key` is the OpenBao path under the ClusterSecretStore's base
      # path (`secret/` in the Catalyst default). Operator overlay
      # supplies the per-Sovereign value, e.g.:
      #   sovereign/omantel.omani.works/newapi/admin-token
      key: ""
      # `property` is the JSON field inside the OpenBao secret holding
      # the bearer token used by the Catalyst signup hook (ADR-0003 §3.2).
      property: "ADMIN_API_TOKEN"
# ─── External-Secrets webhook readiness gate (Fix #138, prov #20) ────────
# Pre-install/pre-upgrade hook that polls the external-secrets validating-
# admission webhook until it serves a structured response (HTTP 200/400/
# 405/415/422). Closes the race between bp-external-secrets reaching
# HR Ready=True (Pods Ready) and the apiserver-side EndpointSlice for
# the webhook Service being observable by the admission controller.
#
# Without this gate the chart's templates/external-secret.yaml apply
# attempt fails with:
#   Internal error occurred: failed calling webhook
#   "validate.externalsecret.external-secrets.io": ... no endpoints
#   available for service "external-secrets-webhook"
#
# Per docs/INVIOLABLE-PRINCIPLES.md #4 every knob is operator-tunable
# via per-Sovereign overlay (e.g. swap to a different webhook namespace
# if external-secrets is provisioned via a non-Catalyst distribution).
# Per #3 the gate is event-driven (probe-then-proceed), NOT a blanket
# `helm install --timeout`.
externalSecretsWebhookGate:
  # Default ON. Set false ONLY on Sovereigns where external-secrets is
  # provisioned outside the Catalyst bootstrap-kit and the operator has
  # confirmed the webhook is already serving by the time bp-newapi
  # installs (rare; recommended to leave true).
  enabled: true
  # Webhook Service name (default = upstream external-secrets chart's
  # `<release>-webhook` Service when releaseName is `external-secrets`).
  service: "external-secrets-webhook"
  # Namespace the webhook Service lives in. Catalyst default per
  # clusters/_template/bootstrap-kit/15-external-secrets.yaml.
  namespace: "external-secrets-system"
  # HTTPS port the webhook serves on (upstream chart default is 443).
  port: 443
  # Validation path for ExternalSecret resources. Sourced from upstream
  # chart's templates/validatingwebhook.yaml — different from the
  # ClusterSecretStore path (used by bp-external-secrets-stores in Fix
  # #137); each consumer probes its own validation endpoint.
  path: "/validate-external-secrets-io-v1beta1-externalsecret"
  # Wall-clock budget. 60s comfortably exceeds the typical 5–15s
  # EndpointSlice propagation window observed across fresh provisions.
  timeoutSeconds: 60
  # Probe interval inside the wall-clock budget.
  intervalSeconds: 2
# ─── Service ─────────────────────────────────────────────────────────────
service:
  type: ClusterIP
  port: 3000
  targetPort: 3000
# ─── ServiceAccount ──────────────────────────────────────────────────────
serviceAccount:
  create: true
  name: ""
# ─── Ingress + cert-manager TLS ──────────────────────────────────────────
# Two virtual hosts:
#   - `host`      : customer-facing OpenAI-compatible API at /v1/*
#   - `adminHost` : ops-staff admin UI (IdP-gated)
#
# The upstream's customer portal UI is disabled at ingress: only /v1/*
# and /api/* paths are exposed on `host`. Path-based deny rules block
# the upstream's React portal routes.
ingress:
  enabled: true
  className: "traefik"
  host: "" # operator-supplied (e.g. api.<sovereign-fqdn>)
  adminHost: "" # operator-supplied (e.g. admin.<sovereign-fqdn>)
  tls:
    enabled: true
    issuer: "letsencrypt-prod" # cert-manager ClusterIssuer
    apiSecretName: "newapi-api-tls"
    adminSecretName: "newapi-admin-tls"
  # Path allowlist for the customer-facing host. Anything not on this
  # list returns 404. Locks down the upstream portal UI surface.
  customerAPIAllowedPaths:
    - "/v1/"
    - "/api/v1/"
    - "/api/status"
  # Traefik Middleware (path allowlist) is OFF by default. Sovereigns run
  # Cilium Gateway API which does not register the traefik.io/v1alpha1
  # Middleware CRD; rendering this resource on a Cilium Sovereign causes
  # a hard "no matches for kind Middleware" install failure. Enable only
  # on contabo-style Traefik clusters; on Cilium Sovereigns, path-level
  # locking down lives in the HTTPRoute layer (future work).
  middleware:
    enabled: false
  annotations: {}
# ─── NetworkPolicy ───────────────────────────────────────────────────────
# Default-allow ingress from the platform's gateway namespace; egress
# to Postgres, Valkey, Keycloak, in-cluster vLLM, DNS, and the operator-
# configured commercial-provider endpoints (egress for those is
# enforced by the cluster's egress gateway, not this NetworkPolicy).
networkPolicy:
  enabled: true
  ingress:
    fromNamespaceLabels:
      kubernetes.io/metadata.name: traefik
  egress:
    # bp-cnpg
    - namespaceLabel: cnpg-system
      port: 5432
    # bp-valkey
    - namespaceLabel: valkey
      port: 6379
    # bp-keycloak (OIDC discovery + token)
    - namespaceLabel: keycloak
      port: 8443
    # bp-vllm (in-cluster cheap-tier channel)
    - namespaceLabel: vllm
      port: 8000
# ─── ServiceMonitor ──────────────────────────────────────────────────────
# Default false per docs/BLUEPRINT-AUTHORING.md §11.2 (Observability
# toggles must default false). Operator opts in via per-cluster overlay
# once kube-prometheus-stack reconciles.
serviceMonitor:
  enabled: false
  interval: "30s"
  scrapeTimeout: "10s"
  path: "/metrics"
  labels: {}
# ─── HorizontalPodAutoscaler ─────────────────────────────────────────────
# Default false. Multi-tenant Sovereigns enable HPA via the per-cluster
# overlay once kube-prometheus-stack + metrics-server reconcile.
hpa:
  enabled: false
  minReplicas: 1
  maxReplicas: 8
  targetCPUUtilizationPercentage: 70
  targetMemoryUtilizationPercentage: 80
# ─── Metering sidecar (#798) ─────────────────────────────────────────────
# Per-pod sidecar that:
#   1. Front-ends NewAPI as a transparent reverse proxy on its own port.
#   2. Observes successful /v1/* responses for OpenAI-style usage data.
#   3. Emits one envelope per completed LLM request on NATS subject
#      `catalyst.usage.recorded` (per #795 [Q-mine-3] + ADR-0001 §6).
#   4. Persists envelopes to disk + retries on NATS unreachable; the
#      customer-facing LLM call is NEVER blocked on metering.
#
# Architecture decision: NewAPI is consumed as upstream
# `ghcr.io/openova-io/openova/newapi-mirror` — a pinned mirror, NOT a
# fork we own. Patching the upstream binary to emit NATS would create
# rebase debt for every release. Observing the request/response pair at
# the network edge produces the same envelopes without forking; the
# OpenAI-compatible /v1/* response carries `usage.{prompt_tokens,
# completion_tokens, total_tokens}` that we project into the canonical
# envelope shape per `core/services/shared/events/nats.go`.
#
# The sidecar is enabled by default — a deployed bp-newapi without
# metering would silently drop billing data. Operators that explicitly
# do NOT want metering set `meteringSidecar.enabled: false` in the per-
# Sovereign overlay (only valid in regulated environments where every
# channel carries its own out-of-band ledger).
meteringSidecar:
  enabled: true
  image:
    # services-build CI publishes this image alongside the SME services
    # on every push to core/services/metering-sidecar/**.
    repository: ghcr.io/openova-io/openova/services-metering-sidecar
    tag: "latest"
    pullPolicy: IfNotPresent
  # The sidecar listens on this port; the Service routes traffic
  # (port 3000) → sidecar → newapi:3000-on-127.0.0.1. We expose the
  # sidecar at the same external port (3000) and have it forward
  # locally to NewAPI on a per-pod loopback address.
  port: 8086
  # Per-token price in micro-OMR. 1 OMR = 1,000,000 micro-OMR;
  # priceMicroOMRPerToken=156 means each token costs 0.000156 OMR
  # (~ 0.156 baisa per token). Operator overlays per region/channel.
  priceMicroOMRPerToken: 156
  # NATS broker URL. Empty → in-cluster default
  # (`nats://nats-jetstream.nats-system.svc.cluster.local:4222`). Per
  # docs/INVIOLABLE-PRINCIPLES.md #4 the sidecar accepts an explicit
  # override for cross-namespace or external-broker deployments.
  natsURL: ""
  natsPublishTimeout: "5s"
  # Spool directory for envelopes generated during a NATS outage. The
  # sidecar's background drain loop republishes once NATS recovers, so
  # persistence across pod restarts is unnecessary; an emptyDir is the
  # right shape (a pod restart during a NATS outage is rare, and on
  # the rare occasion it happens, NewAPI's own ledger backstops
  # reconciliation per #795 [Q-mine-4]).
  spoolDir: "/var/lib/metering-sidecar/spool"
  # HTTP header names the proxy reads for customer + tenant
  # identification. The cluster ingress / NewAPI's customer-API admin
  # layer is expected to inject these on every authenticated request.
  customerIDHeader: "X-Customer-Id"
  tenantIDHeader: "X-Tenant-Id"
  resources:
    requests:
      cpu: 25m
      memory: 64Mi
    limits:
      cpu: 500m
      memory: 256Mi
  securityContext:
    runAsNonRoot: true
    runAsUser: 65534
    readOnlyRootFilesystem: true
    allowPrivilegeEscalation: false
    capabilities:
      drop: ["ALL"]
