Skip to content

Helm values

This page is charts/hephaisto/values.yaml, transcluded from the chart rather than described. Its comments are the reference; a copy would drift from the chart within a release.

Every value is also validated by charts/hephaisto/values.schema.json, which enforces the enums for mode, image.pullPolicy and otel.protocol, and the TimeSpan shape of alertmanager.maxDuration. An invalid value is a render failure, not a runtime surprise.

The four that decide what the agent can do

Read these before the rest.

ValueDefaultWhy it matters
modeObserveOff, Observe, DryRun, Auto. The env arm and database arm can only make this more restrictive.
policy.actionableNamespaces[]Empty means act nowhere — and renders no write Role at all.
policy.autoEnabledActionTypes[]Empty means everything waits for a human.
prometheusOperator.selectorLabels.releasekube-prometheus-stackWrong value fails silently: rules exist, Prometheus selects none, agent reports healthy.

The file

yaml
# =========================================================================================
# Hephaisto
# =========================================================================================
# Defaults are for a consumer who is NOT this repo's dev cluster. Where the safe default and
# the convenient default disagree, this file picks the safe one and says why. The dev cluster
# gets its own overrides in values-dev.yaml.
#
# What this chart does NOT install: Prometheus, Alertmanager, Grafana, Loki, Tempo or an OTel
# collector. You already run those, wired to your own storage and retention. It installs the
# agent, its RBAC, the alert rules that are the agent's INPUT, and a dashboard.
# =========================================================================================

nameOverride: ""
fullnameOverride: ""

image:
  repository: ghcr.io/flou21/hephaisto
  # Defaults to .Chart.AppVersion, which CI stamps from the git tag - so the chart and the
  # image it asks for can never drift. Override only to pin something else deliberately.
  tag: ""
  pullPolicy: IfNotPresent

imagePullSecrets: []

serviceAccount:
  name: ""
  annotations: {}

# -----------------------------------------------------------------------------------------
# AGPL section 13: where YOUR source lives
# -----------------------------------------------------------------------------------------
# Hephaisto is AGPL-3.0 and serves a web console, so the people who use it do so over a
# network and never receive a copy of the binary. Section 13 entitles them to the source of
# the build they are talking to, and the console footer links here to discharge that.
#
# Empty means upstream, which is correct ONLY if you run an unmodified Hephaisto. If you build
# from a fork and anyone else can reach the UI or the API, point this at your own source -
# offering users the upstream source of a program they are not running is the one thing
# section 13 exists to prevent.
sourceUrl: ""

# -----------------------------------------------------------------------------------------
# Where things live
# -----------------------------------------------------------------------------------------
# The namespace your Prometheus/Alertmanager/Grafana run in. Used by the NetworkPolicy, which
# is the webhook's ENTIRE authentication - see networkPolicy below.
observabilityNamespace: hephaisto-obs

# -----------------------------------------------------------------------------------------
# The Prometheus Operator selector - read the comment in _helpers.tpl before changing
# -----------------------------------------------------------------------------------------
# Must match `podMonitorSelector` / `ruleSelector` on your Prometheus resource. The default is
# what kube-prometheus-stack uses when installed under its own name, which is right for most
# consumers and wrong for anyone who named the release something else.
#
# Deliberately NOT defaulted to this repo's own value: that would be correct for exactly one
# cluster and wrong-but-plausible everywhere else, which is the worst kind of default for a
# setting whose failure mode is silence.
prometheusOperator:
  selectorLabels:
    release: kube-prometheus-stack

# -----------------------------------------------------------------------------------------
# Policy - what the agent is allowed to touch
# -----------------------------------------------------------------------------------------
policy:
  # THE NAMESPACES THE AGENT MAY ACT IN. Empty means act nowhere, which is the correct
  # default for a process whose action set ends in `kubectl delete pod`.
  #
  # This one value drives BOTH the RBAC write Role/RoleBinding and the app's own
  # Policy__AllowedNamespaces configuration, so the grant and the policy stop being two
  # independent facts that nothing reconciles. An empty list renders no Role at all.
  #
  # Refused outright: kube-*, default, the release namespace, observabilityNamespace.
  #
  # EACH NAMESPACE MUST ALSO CARRY THE LABEL `hephaisto.dev/destructive-actions-allowed: "true"`.
  # That is a second, independent confirmation and it is checked by the policy engine, not
  # just documented: this list is the operator's authority, the label is the namespace
  # owner's, and both have to agree before anything in it can be touched. A namespace named
  # here and left unlabelled is denied, and the reason on the action says so.
  actionableNamespaces: []

  # ACTION TYPES THAT MAY RUN WITHOUT A HUMAN, and only when `mode` is Auto. Empty means every
  # action waits for someone to press approve, which is the correct default and the state most
  # installs should stay in.
  #
  # Promote ONE TYPE AT A TIME, after watching that type propose correctly for a while and
  # need no human correction. Promoting globally is how you find out that the action set
  # contains something you had not thought about, all at once, in production.
  #
  # restart_pod is the sane first and often only entry: it is the most common correct
  # remediation there is, and it is self-healing - the controller restores the pod, so the
  # worst sustained case at the budget caps is roughly ten restarts an hour, which is
  # indistinguishable from a badly tuned HPA.
  #
  # Valid values are ActionType members: RestartPod, RolloutRestart, RollbackDeployment,
  # ScaleWorkload, DeleteStuckJob, DeleteFailedJobPods, SilenceAlert, PatchResources,
  # CordonNode, DrainNode. DeletePvc and DeleteWorkload are permanently denied and naming
  # them here does nothing.
  autoEnabledActionTypes: []

# -----------------------------------------------------------------------------------------
# Mode
# -----------------------------------------------------------------------------------------
# Off | Observe | DryRun | Auto. The env arm; the ConfigMap and the database row are the other
# two, and THE MOST RESTRICTIVE OF THE THREE WINS. Observe is the only defensible default:
# it detects, investigates and reports, and mutates nothing.
mode: Observe

# -----------------------------------------------------------------------------------------
# Secrets - referenced, never created
# -----------------------------------------------------------------------------------------
# This chart has no Secret template and will not grow one. A value passed to a chart ends up
# in `helm get values`, in the release Secret, and in whatever git repo holds your Argo
# Application - forever, and readable by anyone with get on Secrets in that namespace.
#
# Each name below is wrapped in `required`, so `helm template` fails loudly rather than
# rendering a dangling secretKeyRef that surfaces twenty minutes later as
# CreateContainerConfigError on a pod nobody is watching yet.
secrets:
  # Postgres credentials: keys POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB, and
  # POSTGRES_APP_PASSWORD for the non-owner role the agent serves as.
  postgres: hephaisto-postgres
  # The LLM API keys. Both keys are optional and which one is needed depends on
  # Llm:Provider: GEMINI_API_KEY for the gemini provider and for embeddings while
  # Llm:EmbeddingProvider is gemini (its default); LLM_API_KEY for the openai-compatible
  # provider (DeepSeek, OpenRouter, a local Ollama or LM Studio server). A Secret carrying
  # only one is a normal install, and one carrying neither is a valid self-hosted install
  # that points both Llm__Endpoint and Llm__EmbeddingEndpoint at local servers.
  llm: hephaisto-llm
  # The grafana-mcp bearer token: key `token`. Cannot exist before install - only Grafana can
  # mint it - so this is necessarily a reference to something you create first.
  grafanaMcp: grafana-mcp-caller-token
  # A Grafana service-account token that may WRITE annotations: key `token`. Editor is enough,
  # and is deliberately less than grafana-mcp's Admin. Only read when grafana.url is set.
  grafanaAnnotation: hephaisto-grafana-annotation
  # The HMAC key the outbound webhook signs with: key `signingSecret`. Only read when
  # notifications.webhook.signed is true.
  notificationWebhook: hephaisto-notification-webhook
  # The Teams Power Automate Workflows trigger URL: key `url`. It is a SECRET rather than a
  # value because the URL *is* the credential - its `sig` query parameter is the entire
  # authentication - so it must never reach `helm get values` or a git repo.
  notificationTeams: hephaisto-notification-teams

# -----------------------------------------------------------------------------------------
# Alertmanager - for the SilenceAlert action, and nothing else
# -----------------------------------------------------------------------------------------
# Empty means SilenceAlert is refused by the executor before any call is made, which is the
# honest answer for an install with no Alertmanager rather than a 404 that reads like a bug.
#
# Note what this is NOT: the agent does not read alerts from here - they arrive by webhook.
# This is write-only, and the only thing it writes is a silence.
#
# SilenceAlert ALWAYS requires approval, whatever policy.autoEnabledActionTypes says. It is
# the one action whose failure mode is that everything looks fine.
alertmanager:
  url: ""
  # Hard ceiling on a silence, whatever the model asks for. The dangerous silence is not the
  # wrong one, it is the LONG one: a silence nobody remembers is a monitoring gap that looks
  # like a quiet system.
  #
  # .NET TimeSpan format - "hh:mm:ss", NOT Go/Prometheus duration. "2h" is what everything
  # else in a Kubernetes values file looks like and it does NOT parse here: the agent would
  # fail to start with a binding error naming a key most people would not connect to this
  # line. values.schema.json enforces the shape so `helm template` catches it first.
  maxDuration: "02:00:00"

# -----------------------------------------------------------------------------------------
# Outbound notifications
# -----------------------------------------------------------------------------------------
# Nothing here is on by default, and `routes` being empty is what makes that true: with no
# routes the agent queues nothing and delivers nowhere, exactly as an empty
# policy.actionableNamespaces means it may act nowhere.
#
# Two independent things have to change to be told anything - configure a channel, and add a
# route that names it - and a route naming a channel you did not configure is refused at
# startup rather than discovered the first time something escalates.
notifications:
  # The externally reachable URL of THIS Hephaisto. Every message exists to make somebody open
  # a link, and the pod cannot work this out for itself: it knows the address it binds, not the
  # one a person reaches it on. Required as soon as any route exists, and validated at startup.
  baseUrl: ""

  # Grafana's external URL, for a "look at the graphs" link beside the diagnosis. Optional -
  # a message without it is thinner, not broken. This is the address a HUMAN uses, which is not
  # the in-cluster grafana.url above.
  grafanaUrl: ""

  # The generic outbound HTTP channel. Deliberately not called "webhook" in code: in this
  # chart a webhook is the thing Alertmanager posts INTO the agent.
  webhook:
    # Where to POST. Empty means the channel is not registered at all.
    url: ""
    # Sign each request with HMAC-SHA256 over the exact body, in X-Hephaisto-Signature, so the
    # receiver can prove it came from this agent. Worth turning on: Hephaisto's own INBOUND
    # webhook cannot be authenticated at all, because Alertmanager has no field for a header,
    # and a receiver of yours does not have to accept that trade.
    signed: false

  # Microsoft Teams, via a Power Automate Workflows trigger. NOT the retired Office 365
  # "Incoming Webhook" connector. The trigger URL comes from secrets.notificationTeams.
  teams:
    enabled: false

  # Which events, at which severity, in which namespaces, go where. Additive only - there is
  # no deny rule, because a subtractive routing table is one nobody can reason about and the
  # thing being routed is the message that says the agent needs help.
  #
  # events: IncidentEscalated | ApprovalRequired | IncidentResolved | VerificationFailed |
  #         ModeChanged | PolicyChanged
  # minSeverity: Info | Warning | Critical   (inclusive; defaults to Info)
  # namespaces: empty means "not scoped by namespace", which is the only way a route can carry
  #             ModeChanged or PolicyChanged - those are about the agent and have no namespace.
  #
  # routes:
  #   - channel: teams
  #     events: [IncidentEscalated, ApprovalRequired, VerificationFailed, ModeChanged]
  #     minSeverity: Warning
  #     namespaces: []
  routes: []

# -----------------------------------------------------------------------------------------
# Database
# -----------------------------------------------------------------------------------------
postgres:
  # Host the agent connects to. Ignored when embedded.enabled is true.
  host: postgres
  port: 5432

  # The role the agent SERVES as. It is created and granted by the agent itself on the owner
  # connection at startup, and holds INSERT but NOT UPDATE, DELETE or TRUNCATE on audit_events.
  #
  # It must not be the owner named by the POSTGRES_USER key of secrets.postgres: Postgres
  # cannot restrain a table's owner, which may always grant itself back, so pointing this at
  # the owner enforces nothing and the agent refuses to start rather than pretend otherwise.
  #
  # Needs POSTGRES_APP_PASSWORD in secrets.postgres. Without it the agent serves as the owner
  # and logs a warning - see scripts/bootstrap-secrets.sh.
  appUser: hephaisto_app

  # A single-replica StatefulSet with a local PVC, for evaluating the chart. It is NOT a
  # production database: no backups, no replication, no connection pooling. Default off, so
  # nobody gets one by accident and then discovers it during an incident.
  embedded:
    enabled: false
    image: pgvector/pgvector:pg17
    storage: 10Gi
    storageClassName: ""

# -----------------------------------------------------------------------------------------
# Telemetry
# -----------------------------------------------------------------------------------------
otel:
  # Empty means no OTLP export: console logging plus the /metrics scrape endpoint, never a
  # crash and never silence. The agent has to start in a broken cluster, because a broken
  # cluster is when someone needs it.
  endpoint: ""
  protocol: http/protobuf
  # Free-form; ends up as resource attributes on every span, metric and log.
  environment: ""

# grafana-mcp gives the agent PromQL, LogQL and trace tools. Without it the agent degrades to
# Kubernetes reads only - which works, and is much worse at diagnosis. Empty url disables it,
# and the agent logs a warning saying so rather than starting silently toolless.
grafanaMcp:
  url: ""

  # Datasource name -> uid, put straight into the agent's environment card so it does not have
  # to spend a tool call on list_datasources before it can query anything (backlog #36).
  #
  # This is the cheapest available reduction in steps: a fact the agent cannot look up is
  # exactly what the environment card is FOR, and the measured baseline is 7.5 steps and
  # $0.080 per investigation. The uids are stable per cluster:
  #
  #   curl -sH "Authorization: Bearer $TOKEN" $GRAFANA/api/datasources | jq '.[] | {name, uid}'
  #
  # Empty renders no section at all rather than an empty one, so leaving it unset costs a
  # discovery call and nothing else.
  datasourceUids: {}
    # prometheus: abc123
    # loki: def456
    # tempo: ghi789

# -----------------------------------------------------------------------------------------
# Grafana itself - annotations
# -----------------------------------------------------------------------------------------
# Separate from grafanaMcp above: that is the MCP server the model queries THROUGH, this is
# Grafana's own HTTP API, which the agent posts incident annotations to so an operator reads a
# diagnosis against the graph it came from.
#
# Needs secrets.grafanaAnnotation, and that token is the only Grafana credential in this
# system that may write. Empty url disables annotation entirely; the agent says so once at
# startup rather than failing per incident.
grafana:
  url: ""

# -----------------------------------------------------------------------------------------
# Alert rules - the agent's INPUT, not its telemetry
# -----------------------------------------------------------------------------------------
# These are in the chart because a Hephaisto installed without them detects nothing and
# reports itself perfectly healthy. They are what turns your cluster's symptoms into the
# signals it investigates.
alerts:
  kubernetes: true
  slo: true
  # The dead-man's switch. An always-firing alert whose ABSENCE is the signal: if it stops
  # arriving, the whole alert path between Prometheus and the agent is broken. Leave it on.
  watchdog: true
  # OFF by default, and it should stay off unless you run this repo's exact collector
  # topology. Its alerts assert facts about OUR pipeline; on a different stack they fire
  # permanently - and are then fed to the agent as incidents, which is worse than useless.
  observabilitySelfcheck: false

dashboard:
  # A ConfigMap labelled for the Grafana sidecar to pick up.
  enabled: true
  # The label the sidecar watches for. kube-prometheus-stack's default is grafana_dashboard.
  sidecarLabel: grafana_dashboard
  # Grafana folder annotation; empty means the sidecar's default folder.
  folder: ""

# -----------------------------------------------------------------------------------------
# NetworkPolicy - this IS the webhook's authentication
# -----------------------------------------------------------------------------------------
# The Alertmanager webhook is unauthenticated, because Alertmanager cannot send credentials to
# a receiver. Anything that can reach port 8080 can create incidents and, in Auto mode, cause
# the agent to act on them. The only thing standing in front of it is this policy.
networkPolicy:
  enabled: true

  # Extra CIDRs allowed to reach 8080. SHIPS EMPTY, and that is not caution for its own sake:
  # `0.0.0.0/0` here means every pod in the cluster can post forged alerts.
  #
  # You may still need it. Kubelet probes arrive from the node's address, which no
  # namespaceSelector can match, so on some clusters an empty list means the liveness probe
  # fails and the pod is killed in a loop by its own protection. If that happens, prefer the
  # node CIDR over 0.0.0.0/0.
  extraIngressCIDRs: []

  # ---------------------------------------------------------------------------------------
  # Egress - OFF by default, and the default is load-bearing
  # ---------------------------------------------------------------------------------------
  # Adding Egress to a NetworkPolicy denies EVERYTHING not explicitly listed. For this pod
  # that means DNS, the Kubernetes API server, Postgres, the observability stack, grafana-mcp
  # and the LLM API - all of which it needs. Turning this on with an incomplete allow list
  # produces an agent that starts cleanly, reports healthy, and does nothing, which is one of
  # the harder failures to diagnose because every symptom points somewhere else.
  #
  # So it is opt-in, and switching it on is a deliberate act with a checklist. Work through
  # every entry below before enabling it, and expect to need apiServerCIDRs.
  egress:
    enabled: false

    # DNS. Almost nothing works without it, so it is allowed to the whole cluster rather than
    # to a namespace: CoreDNS lives in kube-system on most clusters and somewhere else on
    # enough of them that pinning it is how this breaks on the cluster you did not test.
    dns: true

    # The Kubernetes API server. THIS IS THE ONE THAT CATCHES PEOPLE. The API server is not a
    # pod in a namespace you can select - it is an endpoint, typically the control-plane node
    # address on 6443, or a load balancer outside the cluster entirely. There is no portable
    # selector for it, so you must supply the CIDR:
    #
    #   kubectl get endpoints kubernetes -o jsonpath='{.subsets[*].addresses[*].ip}'
    #
    # Left empty the agent cannot watch pods, cannot read events and cannot act - which is to
    # say it cannot do anything at all.
    apiServerCIDRs: []
    apiServerPorts: [443, 6443]

    # Everything outside the cluster the agent talks to: the LLM API, and whatever your
    # notification channels point at. Ships empty, which means investigations fail and
    # notifications never arrive - both loudly, at least.
    #
    # `0.0.0.0/0` on 443 is the honest starting point for most people, and is still a
    # meaningful restriction: it stops the agent reaching anything in-cluster it was not
    # granted above.
    extraEgressCIDRs: []
    extraEgressPorts: [443]

# -----------------------------------------------------------------------------------------
# RBAC
# -----------------------------------------------------------------------------------------
rbac:
  # The cluster-wide read ClusterRole and its binding. Reads cannot break anything, and an SRE
  # cannot diagnose a namespace in isolation.
  #
  # There is deliberately no `extraRules` value. A chart whose permission set is a
  # values-driven loop is a chart where `--set rbac.extraRules[0].resources[0]='*'` grants
  # cluster-wide Secret access and the `helm diff` looks unremarkable. Widening this stays a
  # code change and a commit.
  create: true

  # The cordon/drain ClusterRole. It is CREATED but NEVER BOUND, and there is no value here to
  # bind it: doing so would turn a recorded human decision into a line buried in an Argo
  # Application. Binding it stays a hand-written ClusterRoleBinding in its own commit. The
  # friction is the feature.
  nodeRole: true

securityContext:
  # TRUE, because this chart runs the PUBLISHED image: a self-contained app that writes
  # nothing to its own filesystem. Only /tmp is writable, via an emptyDir.
  #
  # The one thing that legitimately needs this false is a development image whose entrypoint
  # is a compiler - `dotnet watch` has to write bin/, obj/, a NuGet cache and a CLI home, and
  # with a read-only root it dies at startup with
  #   System.IO.IOException: Read-only file system : '/app/src/.../obj'
  # which reads like a permissions bug and is actually a design mismatch. See values-dev.yaml.
  #
  # Turning this off for the published image buys nothing: the boundary that actually contains
  # this pod is RBAC.
  readOnlyRootFilesystem: true

resources:
  requests:
    cpu: 100m
    memory: 512Mi
  limits:
    # No CPU limit: CFS throttling on a latency-sensitive process produces stalls that look
    # exactly like the dependency slowness the agent is meant to be diagnosing. Memory IS
    # limited, to bound a leak.
    memory: 1Gi

nodeSelector: {}
tolerations: []
affinity: {}
podAnnotations: {}

# -----------------------------------------------------------------------------------------
# Extra environment - the seam for everything this chart does not expose as a value
# -----------------------------------------------------------------------------------------
# The agent binds far more configuration than lives above: `Llm:Budget:MaxCostUsdPerHour`,
# `Llm:Investigation:MaxSteps`, `Llm:Model`, and so on - every one of them settable as a
# `Section__Key` environment variable. Promoting each to a first-class value would duplicate
# the options classes in YAML and drift the first time one is renamed.
#
# Appended LAST in the container spec, so these override anything the chart set. Standard
# Kubernetes env entries - `value` or `valueFrom` both work:
#
#   extraEnv:
#     - name: Llm__Budget__MaxCostUsdPerHour
#       value: "1.00"
#     - name: Llm__Model
#       valueFrom:
#         configMapKeyRef: { name: my-config, key: model }
#
# Do NOT put a secret in `value`. It lands in `helm get values`, in the release Secret, and in
# whatever git repo holds your Argo Application, forever. Use `valueFrom.secretKeyRef`.
extraEnv: []

# Whole ConfigMaps or Secrets projected as environment. Same caveat, same ordering.
extraEnvFrom: []

# NOTE: there is deliberately no `replicaCount`.
#
# The budget check, cooldown check, kill-switch check and the action INSERT are one
# Serializable transaction. Two replicas make that a distributed TOCTOU race on the one code
# path that ends in `kubectl delete pod` - and a kill switch a second replica can race is not
# a kill switch. The Deployment is a documented singleton with strategy: Recreate.

Worked examples

A minimal install, and an everything-on install, both of which CI renders on every push:

yaml
# The smallest input that must render: nothing enabled beyond the defaults, no namespace the
# agent may act in, no observability wiring. This is what a first `helm install` looks like.
prometheusOperator:
  selectorLabels:
    release: kube-prometheus-stack
yaml
# Everything on at once. Exists so that a template only exercised by an unusual combination
# still gets rendered in CI.
image:
  repository: ghcr.io/flou21/hephaisto
  tag: "0.0.1"
  pullPolicy: IfNotPresent

imagePullSecrets:
  - name: ghcr-pull

serviceAccount:
  name: hephaisto
  annotations:
    example.com/owner: sre

observabilityNamespace: hephaisto-obs

prometheusOperator:
  selectorLabels:
    release: kube-prometheus-stack
    example.com/extra: "yes"

policy:
  actionableNamespaces:
    - hephaisto-chaos
    - some-app

mode: DryRun

secrets:
  postgres: hephaisto-postgres
  llm: hephaisto-llm
  grafanaMcp: grafana-mcp-caller-token

postgres:
  host: postgres
  port: 5432
  embedded:
    enabled: true
    image: pgvector/pgvector:pg17
    storage: 20Gi
    storageClassName: standard

otel:
  endpoint: http://otel-collector.hephaisto-obs:4318
  protocol: http/protobuf
  environment: ci

grafanaMcp:
  url: http://grafana-mcp.hephaisto-obs:8000/mcp

alerts:
  kubernetes: true
  slo: true
  watchdog: true
  observabilitySelfcheck: true

dashboard:
  enabled: true
  sidecarLabel: grafana_dashboard
  folder: Hephaisto

networkPolicy:
  enabled: true
  extraIngressCIDRs:
    - 10.42.0.0/16

rbac:
  create: true
  nodeRole: true

podAnnotations:
  example.com/note: "rendered by CI"

nodeSelector:
  kubernetes.io/os: linux

tolerations:
  - key: example.com/dedicated
    operator: Exists
    effect: NoSchedule

# The seam for configuration the chart does not expose as a value. Exercised here so
# kubeconform validates the rendered env entries, and so the reserved-name guard in
# _helpers.tpl is rendered against a values file that actually uses it.
extraEnv:
  - name: Llm__Budget__MaxCostUsdPerHour
    value: "1.00"
  - name: Llm__Investigation__MaxSteps
    value: "8"
  - name: Llm__Model
    valueFrom:
      configMapKeyRef:
        name: hephaisto-model
        key: model
extraEnvFrom:
  - configMapRef:
      name: hephaisto-extra-config

AGPL-3.0-only · Source