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.
| Value | Default | Why it matters |
|---|---|---|
mode | Observe | Off, 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.release | kube-prometheus-stack | Wrong 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-stackyaml
# 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