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. |
And one with no default at all: cluster.name, the cluster label on this cluster's metrics. Rendering fails without it.
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/truerelevance/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: ""
# -----------------------------------------------------------------------------------------
# The cluster this agent runs in - REQUIRED, and there is no default
# -----------------------------------------------------------------------------------------
# The value of the `cluster` label on this cluster's metrics and logs. It goes into every signal
# fingerprint, it is the cluster an alert without a `cluster` label is taken to be about, and the
# model is told to put it in every query it writes. Rendering fails without it.
#
# It used to be three settings the chart never set, all defaulting to the development machine's
# name - so every install fingerprinted with somebody else's cluster and told its model to filter
# on a label that matched nothing (backlog #139). No default can be right for a cluster the chart
# has never seen, so it has none.
#
# Treat it as immutable once the cluster has reported: changing it re-keys every future signal,
# and nothing new dedupes against what came before.
cluster:
name: ""
# -----------------------------------------------------------------------------------------
# Where things live
# -----------------------------------------------------------------------------------------
# The namespace your Prometheus/Alertmanager/Grafana run in. Used by the NetworkPolicy, which
# is the webhook's entire protection unless secrets.webhookToken is set - 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: []
# THE TWO WINDOWS THAT DECIDE WHETHER A ROLLBACK MAY RUN UNATTENDED.
#
# Both are read by the policy engine's rollback_deployment gate, and BOTH must hold: the
# current revision has to be younger than freshRevisionWindow (this is a fresh deploy, so
# rolling back is the obvious move rather than a guess), and the previous revision has to
# have been healthy for at least previousHealthyMinimum (there is somewhere known-good to
# go back TO). Fail either and the action is refused with the reason recorded - which is a
# working gate, not a bug, and is the most likely explanation for a rollback you expected
# and did not get.
#
# They were code defaults with no chart value until v0.7.0, which was defensible only while
# rollback_deployment was unimplemented. The moment the executor arm shipped these became
# the two knobs an operator most needs to reach.
#
# .NET TimeSpan format - "hh:mm:ss", NOT Go/Prometheus duration. "30m" does not parse and
# the agent would fail to start with a binding error. values.schema.json enforces the shape
# so `helm template` catches it first.
#
# WIDENING freshRevisionWindow is the dangerous direction: it lets the agent roll back a
# revision that has been serving for hours, where "it started at the deploy" is a much
# weaker claim. Shortening previousHealthyMinimum is the other one - it permits a rollback
# onto a revision that was never proven good.
rollbackFreshRevisionWindow: "00:30:00"
rollbackPreviousHealthyMinimum: "01:00:00"
# Above this fraction of unhealthy pods, EVERYTHING escalates: the agent treats it as a
# cluster-wide event and refuses to act, because a fleet-wide fault is not something to
# start restarting pods into.
#
# Also a code default with no chart value until v0.7.0. Note before raising it: the e2e
# release gate crosses this ceiling on purpose when it applies eleven chaos fixtures at
# once, and the correct response to "no action was taken in the --full run" is a separate
# single-fixture acting run, NOT a wider ceiling. See backlog #97.
clusterUnhealthyCeiling: 0.3
# -----------------------------------------------------------------------------------------
# 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.
# -----------------------------------------------------------------------------------------
# The webhook's own port - what makes exposing the console safe
# -----------------------------------------------------------------------------------------
# The console, the JSON API and /webhooks/alertmanager used to answer on one port. The console
# has OIDC (auth below); the receiver cannot use it, because Alertmanager cannot sign in. It can
# send a bearer token (secrets.webhookToken), but without one the NetworkPolicy is the receiver's
# entire protection, and one port forced a choice on every install: reach the console only by
# port-forward, or widen the policy for the console and admit forged alerts with it.
#
# With this set, /webhooks answers ONLY here and the console and API answer ONLY on 8080. Point
# your NetworkPolicy at this port and expose 8080 however you like.
#
# 0 KEEPS EVERYTHING ON 8080, AND IS THE DEFAULT. Turning this on is a BREAKING change to your
# Alertmanager configuration: the receiver URL must move to this port in the same change, or
# /webhooks stops answering where Alertmanager posts and every alert vanishes silently. The
# watchdog is what tells you - its absence is the signal - and the e2e gate caught exactly this
# when the split was briefly defaulted on.
#
# receivers:
# - name: hephaisto
# webhook_configs:
# - url: http://hephaisto.hephaisto:8081/webhooks/alertmanager # <- the webhook port
webhookPort: 0
# -----------------------------------------------------------------------------------------
# The MCP endpoint (#157) - OFF by default
# -----------------------------------------------------------------------------------------
# Lets a coding agent, or an MCP gateway in front of many of them, ask about incidents: search,
# one incident in full, its investigation and evidence, the history of an alert, code fixes, the
# agent's status - and acknowledge, assign, add a note entry, give feedback, and (approver role)
# close or re-investigate. Nothing here approves an action or a code-fix plan, re-arms or sets a
# mode: those tools do not exist.
#
# MCP over stateless streamable HTTP at /mcp, on this port only. Nothing else answers here, and
# /mcp answers nowhere else. Never without a credential: a token below, or with auth.enabled the
# identity provider's bearer token (acceptIdentityProviderTokens).
#
# One token per consumer. Its value lives in the Secret named by secrets.mcp under the token's
# name, at least 32 characters (`openssl rand -hex 24`); a shorter one refuses to start.
#
# kind: shared a gateway's token, sent for every one of its users. It acts as itself -
# the audit trail says mcp/<name> - and a person a model names is recorded as
# claimed, unverified. `assignedTo: me` means nobody for it.
# kind: person one person's token; acts as `subject`.
# role reader, or approver (adds closing and re-investigating)
# mayWrite false makes it read-only
#
# An MCP gateway registers it like any HTTP MCP server, with the token as its bearer:
#
# hephaisto:
# url: http://hephaisto.hephaisto:8083/mcp
# transport: http
# auth_type: bearer_token
# auth_value: os.environ/HEPHAISTO_MCP_TOKEN
mcp:
enabled: false
port: 8083
acceptIdentityProviderTokens: true
tokens: []
# - name: gateway
# kind: shared
# role: reader
# - name: flo
# kind: person
# subject: flo
# role: approver
# Who may reach the port, when networkPolicy.enabled. Nobody by default: list the gateway's
# namespace. Never the coder's - the chart refuses it, the coder gets no route to the agent.
networkPolicy:
fromNamespaces: []
fromCIDRs: []
# -----------------------------------------------------------------------------------------
# Authentication - OFF by default, and it fails CLOSED when on
# -----------------------------------------------------------------------------------------
# With this off there is no authentication of any kind: anyone who can reach the console port
# can read every incident - which carries pod logs and ConfigMap contents the agent gathered
# from every namespace - and can approve an action. An identity proxy in front is then the only
# control.
#
# FAILS CLOSED, deliberately: if the IdP is unreachable, Hephaisto is unavailable. Dropping to
# an unauthenticated console whenever the IdP is down would make the control decorative and hand
# an attacker its timing. The cost is accepted rather than overlooked - reading the console
# during an outage now depends on the IdP being up.
#
# The two roles are separate on purpose. "Anyone who can log in may authorise a change to the
# cluster" is not a default worth shipping, and the set of people who should watch an incident is
# much larger than the set who should approve a `kubectl delete`. Leaving either empty means any
# authenticated user, which is logged at startup.
auth:
enabled: false
# e.g. https://keycloak.example.com/realms/hephaisto
authority: ""
clientId: hephaisto
# Keycloak nests realm roles under realm_access.roles; the agent flattens them onto this claim.
rolesClaim: roles
readerRole: ""
approverRole: ""
mode: Observe
# -----------------------------------------------------------------------------------------
# When an incident ends without anybody touching it
# -----------------------------------------------------------------------------------------
# An alert's incident closes when Alertmanager resolves it. These are for the rest.
incidents:
# The Kubernetes watcher only reports that something is wrong, so an incident it opens needs
# another way to end. Once every pod of the workload has been running, ready and without a
# restart for this long, the watcher reports it healed and the incident closes, as
# hephaisto/watcher. "00:00:00" turns that off. Keep it above five minutes: a crash loop's
# back-off is that long, and a container between two crashes is not a healed one.
healedAfter: "00:10:00"
# The backstop for what cannot heal: a workload that was deleted, a failed Job. An open
# incident with no signal for expireAfter and nobody's acknowledgement is expired - never
# closed, because nobody dealt with it - and an approval nobody answered within
# approvalTimeout escalates. Off by default: it changes what the console shows unasked.
sweep:
enabled: false
expireAfter: "3.00:00:00"
approvalTimeout: "1.00:00:00"
# -----------------------------------------------------------------------------------------
# What the model is told about this cluster
# -----------------------------------------------------------------------------------------
investigation:
# The namespaces worth investigating at all, stated on the model's environment card. Empty
# says nothing about scope - which is right for an install that takes every alert - rather
# than naming a namespace that exists on one development machine.
inScopeNamespaces: []
# Investigation in a Job (v0.12.0 F5). The investigation's model loop runs as Claude Code in a
# Job next to the coder - on the subscription token in secrets.codeFix - while Hephaisto serves
# it the same read-only tools over an internal port and grounds its conclusion as it always has.
# Off renders nothing: every investigation runs in-process, as in v0.11.
#
# Needs codeFix.enabled: the Job borrows the coder's namespace, image, ServiceAccount, Secret
# and egress proxy. It needs no mode of the code-fix stage - codeFix.mode off is fine.
job:
enabled: false
# inprocess | job - the env arm. The switch ConfigMap's investigationExecutor key is the other,
# most restrictive winning, so enabling the infrastructure and switching investigations over
# are two separate acts, and either arm takes it back to inprocess.
executor: inprocess
# The investigator port: /investigate and nothing else, admitted only from investigator pods.
port: 8084
# The Claude model the investigator runs (an id, or a CLI alias such as opus). Independent of
# codeFix.model. Empty lets the CLI choose the account default.
model: ""
# real | fake. fake runs scripted, $0 - dev and CI only.
sdk: real
# Per run. On a subscription the cost is notional, but it still caps what one run may use.
maxCostUsd: 2
maxTurns: 60
deadline: "00:15:00"
# Storm control. What a run that cannot have a slot does is overflow, below.
concurrentJobs: 1
jobsPerHour: 20
# With every slot taken or the hourly cap reached: "inprocess" investigates with the in-process
# model, "wait" holds the investigation until a Job slot frees, so the in-process model is
# never called (set fallbackToInProcess: false too). "wait" also sends incidents about another
# cluster to a Job.
overflow: inprocess
# A refused, failed, vanished or rate-limited Job falls back to the in-process investigation.
fallbackToInProcess: true
# A read-only clone of the running revision, when the workload maps through
# codeFix.repositories. Adds GitHub egress and a clone to every such investigation.
source:
enabled: false
# -----------------------------------------------------------------------------------------
# Model prices - the cost caps bind only on a priced model
# -----------------------------------------------------------------------------------------
llm:
# A model with no price bills as $0, so every cost cap silently stops binding while the
# console reports 0.0% utilisation. The agent therefore REFUSES TO START when a cost cap is
# set (they all are, by default) and Llm:Model or Llm:PlanningModel has no price (backlog
# #140). The built-in table covers the Gemini models and a few openai-compatible ones; a model
# behind a gateway, under a name of your choosing, needs an entry here.
#
# A price of 0 is accepted, for a model that really is free - a local one. Saying so here is
# the acknowledgement the refusal asks for. Prices are USD per million tokens. An entry here
# replaces a built-in price of the same name.
#
# pricing:
# - model: my-gateway-model
# inputPerMillionUsd: 0.30
# outputPerMillionUsd: 2.50
pricing: []
# -----------------------------------------------------------------------------------------
# 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 bearer token Hephaisto PRESENTS TO grafana-mcp: key `token`.
#
# NOT a Grafana credential, and the distinction cost a day on the first production install.
# mcp-grafana >= 1.1.0 has two separate tokens: one it validates callers against
# (MCP_GRAFANA_SERVER_TOKEN, which is this one) and its OWN Grafana service-account token,
# which Hephaisto never sees. So this must match whatever your grafana-mcp was started with -
# generate a fresh one and every tool call comes back 401, and the agent silently degrades to
# Kubernetes-only investigation.
#
# It follows that scoping THIS token cannot limit what the model reaches in Grafana. That is
# decided by grafana-mcp's own credential, on the MCP server, out of this chart's reach.
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 bearer token the INBOUND webhook requires: key `token`, at least 16 characters. Empty
# means /webhooks requires none, and the NetworkPolicy is then its whole protection.
#
# Alertmanager sends it with `http_config.authorization.credentials_file` on the receiver
# (backlog #138 - the comments here used to say it could not). Set both in ONE change: a
# token the receiver does not send refuses every alert, and no retry fixes that.
#
# receivers:
# - name: hephaisto
# webhook_configs:
# - url: http://hephaisto.hephaisto:8080/webhooks/alertmanager
# send_resolved: true
# http_config:
# authorization:
# credentials_file: /etc/alertmanager/secrets/hephaisto-webhook-token/token
webhookToken: ""
# The MCP endpoint's tokens: one key per mcp.tokens[].name. Only read when mcp.enabled.
mcp: ""
# The HMAC key the outbound webhook signs with: key `signingSecret`. Only read when
# notifications.webhook.signed is true.
notificationWebhook: hephaisto-notification-webhook
# The OIDC client secret: key `clientSecret`. Only read when auth.enabled is true. A client
# secret in a values file is a client secret in `helm get values` and in whatever git repo
# holds your Application, forever.
auth: hephaisto-auth
# 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
# The Teams bot's client secret: key `clientSecret`. Only referenced when
# notifications.teamsBot.enabled is true. It expires - an app registration's secret lasts two
# years at most - so whoever creates it owns a date in a calendar.
notificationTeamsBot: hephaisto-notification-teams-bot
# The code-fix coder's credentials. Only referenced when codeFix.enabled is true, and never
# read by Hephaisto at all: the Secret lives in codeFix.namespace - NOT in this release's
# namespace - and each coder Job receives it by secretKeyRef. Hephaisto can name it and holds
# no RBAC that could read it. Every key is optional; which ones a coder needs depends on you:
#
# CLAUDE_CODE_OAUTH_TOKEN the Claude subscription token, from `claude setup-token`
# ANTHROPIC_API_KEY the API-key alternative to the OAuth token
# GITHUB_TOKEN a FINE-GRAINED PAT: contents:write + pull_requests:write on the
# mapped repositories ONLY. Never a classic token - a classic
# `repo` scope is every repository the account can see.
# NUGET_GITHUB_TOKEN read:packages, for a private GitHub Packages NuGet feed
#
# One Secret, but no container of a Job is handed all of it (v0.13.0). The two model keys go to
# the `coder` container, where the model runs, and to no other; GITHUB_TOKEN and
# NUGET_GITHUB_TOKEN go to `prepare`, which has ended before the model starts; GITHUB_TOKEN
# alone goes to `publish`, which starts after it has ended. The containers share no process
# namespace, so the model's shell cannot read a token out of another one's environment.
codeFix: hephaisto-codefix
# The AGENT's GitHub token: key `GITHUB_TOKEN`. Only referenced when github.enabled is true.
# A Secret in THIS release's namespace, and deliberately not the one above: that one lives in
# codeFix.namespace, holds a token that may push, and the agent has no way to read it. This
# one reads issues and writes comments:
#
# repositories owned by an organisation a FINE-GRAINED token of the bot account, limited to
# the listed repositories: Issues read and write,
# Pull requests read, Metadata read
# repositories owned by a user a fine-grained token only reaches repositories its
# own account or organisation owns, so a bot that is a
# collaborator there needs a classic token (`repo`)
#
# The chart refuses the same name as secrets.codeFix: one token for both is the coder's write
# access held by the process that reads every issue.
github: ""
# -----------------------------------------------------------------------------------------
# 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: an unauthenticated receiver
# accepts anything that can reach it, which is why Hephaisto's own inbound webhook takes a
# token too (secrets.webhookToken).
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
# Microsoft Teams, through a registered BOT. A different thing from `teams` above, and the
# two can run side by side. A Workflows trigger can post and nothing else; a bot can EDIT
# what it posted, which is what makes one living board possible instead of a card per event.
#
# - The CHANNEL holds one message: a board of every open incident, edited in place. An
# incident that is closed disappears from it.
# - An ALERT goes to each recipient as a personal chat message from the bot, and is edited
# to say how the incident ended.
# - NOTHING IS EVER DELETED. Teams leaves "This message has been deleted." behind, for a
# channel post and for a reply alike, and there is no setting that stops it.
#
# Every button is a link, unless `actions.enabled` below: then an open alert also carries
# buttons that act - Acknowledge, Assign to me, Reinvestigate, Close for the approvers you
# name, and, behind a switch of their own, Approve and Deny - which Microsoft delivers by
# calling this pod.
#
# You need, in the SAME Microsoft tenant as your Teams: a single-tenant app registration, an
# Azure Bot resource (free tier) with the Teams channel on, and a Teams app that names the bot
# with scopes `team` and `personal`, installed in the team and for each recipient. The client
# secret comes from secrets.notificationTeamsBot.
teamsBot:
enabled: false
# The Microsoft Entra tenant id, and the bot's Microsoft App ID. Neither is a secret - the
# app id is printed in the Teams app manifest.
tenantId: ""
appId: ""
# The channel that holds the board, as `19:[email protected]`. Take it from "Get link to
# channel" and URL-decode it.
channelId: ""
# The team's group id, the `groupId=` value of that same link. Optional: it is what the
# "Open the board" button on an alert is built from.
teamId: ""
# Work email addresses that receive alerts. Each must belong to a member of the team and
# have the Teams app installed for themselves, or the bot may not write to them.
recipients: []
board:
# How many incidents the board lists before it says "and N more". At most 40. The board
# also trims itself to 100 KB whatever this says, because a count is not a size.
maxIncidents: 20
# Buttons that ACT on an alert.
# Any member of the team: Acknowledge, Assign to me, and - on an escalated incident no
# investigation found anything for - Reinvestigate. All three are read-level in the
# console too.
# An approver only: Close, which asks for a reason first. See `approvers` below.
# An approver only, and only with `approvals.enabled`: Approve and Deny of a proposed
# action. Without it they stay links into the console.
#
# Turning this on creates the ONE inbound route Microsoft calls: POST /api/teams/messages,
# on its own port and nowhere else, and nothing else answers on that port. Set your Azure
# Bot's messaging endpoint to https://<a host that reaches this port>/api/teams/messages.
# Every request must carry a Bot Framework token for this bot's app id, signed by a key
# endorsed for Teams, for this tenant, from somebody in the team - anything else changes
# nothing. The NetworkPolicy opens this port from anywhere: the token is its authentication,
# and Microsoft's addresses are not yours to list.
actions:
enabled: false
# Must differ from 8080 and from webhookPort.
port: 8082
# Who holds the approver role in Teams, as Microsoft Entra OBJECT IDS - GUIDs, not
# addresses. A click arrives as an Entra identity, and the console's approver role lives
# in whatever auth.authority names, so the mapping between the two is said here, in a
# reviewed file, and nothing in somebody else's directory can grant it.
# az ad user show --id [email protected] --query id -o tsv
# Empty means nobody: Close is then not drawn, and a click that asks for it is refused. A
# card is the same for everybody, so with somebody named here everybody SEES Close; who
# may press it is decided at the click. An approver must also be a member of the team.
approvers: []
# Approve and Deny, on an alert whose incident has an action awaiting approval. The card
# then says what is proposed - the action, its target, its arguments, its risk - with the
# two buttons under it, and an approver's click is recorded as that person and runs the
# action exactly as approving in the console does: through admission, under the mode,
# the budgets and the kill switch.
#
# Its own switch, off by default, because it is the one click here that can change your
# cluster: you can let the team acknowledge and close from Teams and keep every approval
# in the console. Refused without `enabled` above, and without at least one approver.
approvals:
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: IncidentOpened | IncidentEscalated | ApprovalRequired | IncidentResolved | VerificationFailed |
# ModeChanged | PolicyChanged |
# CodeFixPlanReady | CodeFixPrOpened | CodeFixFailed (see codeFix below)
# 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.
#
# channel: webhook | teams | teamsBot
#
# Since v0.10.0 a route can own only some incidents, and name who it tells:
# name: a label for the route, recorded on every delivery it made. Unique.
# matchers: [{label, values: [...]}] - all must hold. The alert's own labels, as the rule set
# them (team, product, channel). An incident opened by a Kubernetes signal has none.
# clusters: the `cluster` label values this route owns. kinds: SignalKind names.
# recipients: people on a channel that tells people (teamsBot). Empty means the channel's own.
# steps: [{after: "00:20:00", recipients: [...], minSeverity, channel, toAssignee}] - when
# nobody has acknowledged the incident that long after it opened or reopened, tell
# these people too. Each step once per outage; an acknowledgement stops them.
# fallback: true - owns what no SCOPED route owns (namespaces, matchers, clusters, kinds), so
# an alert nobody labelled still reaches somebody. A fallback has no scope itself.
# Ownership first, then events and minSeverity. An unscoped route hears everything.
#
# routes:
# - name: payments
# channel: teamsBot
# events: [IncidentOpened, IncidentEscalated]
# matchers: [{ label: team, values: [payments] }]
# recipients: [[email protected]]
# - name: everything-else
# channel: teamsBot
# fallback: true
# events: [IncidentOpened, IncidentEscalated]
# recipients: [[email protected]]
#
# A route to teamsBot decides which events become a personal ALERT. The board needs no route:
# it shows every open incident as soon as teamsBot is enabled.
#
# routes:
# - channel: teams
# events: [IncidentOpened, IncidentEscalated, ApprovalRequired, VerificationFailed, ModeChanged]
# minSeverity: Warning
# namespaces: []
routes: []
# Messages per channel per hour, after which the rest are suppressed and counted. The cap is
# what keeps a storm from becoming a flood; a suppressed opening or escalation step is logged
# at Error, because that is a person not being told (#150).
maxPerChannelPerHour: 60
# -----------------------------------------------------------------------------------------
# Code fixes - a SECOND, separately gated stage. Off, and not even rendered, by default.
# -----------------------------------------------------------------------------------------
# When an investigation's grounded primary finding is a bug in application code, nothing in the
# cluster can fix it: the planner says "a human must fix this" and the incident escalates. This
# stage hands that finding to a coder - Claude Code, in a Kubernetes Job of its own - which
# clones the repository the workload is mapped to and writes a fix PLAN (automatically,
# read-only). After a human approves the plan, a second Job implements it on a branch, runs the
# build and tests, and opens a pull request - a DRAFT, unless the context repository's
# repos.yaml says `defaults.pr.draft: unless-ready`; then it is a draft only when the runner had
# no check to run or a deviation from the approved plan was reported. A human reviews, merges
# and deploys. Nothing here merges, deploys or touches the workload.
#
# WHAT THE CODER CAN REACH, which is the security argument in full:
#
# - NO cluster identity. Its ServiceAccount is bound to nothing and its token is never
# mounted. It cannot read a Secret, a log, a pod or anything else in the cluster.
# - NO Hephaisto credential and NO inbound surface. It answers by printing a framed block to
# its own log; Hephaisto pulls it with the `get pods/log` it already holds. A callback
# endpoint would be a second write surface reachable by a shell that just read
# attacker-influenceable logs.
# - Egress ONLY through the allowlist proxy below (when networkPolicy and egressProxy are on,
# which is the default): DNS, and the proxy's port. Every request it makes is on the proxy's
# stdout - that log is the per-attempt outbound audit.
# - Its write target is a git BRANCH and a pull request, with a token scoped to the mapped
# repositories. Branch protection on each default branch is YOUR prerequisite - the chart
# cannot check it, NOTES.txt reminds you.
#
# WHAT HEPHAISTO GAINS: `create jobs` - in exactly ONE namespace, this one, which is refused at
# render time if it is also a namespace Hephaisto may act in, its own, the observability one,
# `default` or kube-*. The agent's RbacSelfCheck refuses to boot if it holds `create jobs`
# anywhere else. See templates/rbac-codefix.yaml.
#
# TWO SWITCHES, deliberately:
# enabled renders the infrastructure (namespace objects, RBAC, proxy, policies). Off renders
# NOTHING code-fix related, except `codeFixMode: "off"` in the switch ConfigMap.
# mode off | plan | pr - what the stage may DO. Its own axis, independent of the agent's
# `mode`: an Observe-only agent may still plan code fixes, because planning writes
# nothing anywhere. The agent's kill switch (mode Off, the emergency stop, the
# runaway latch) still overrides it, and so does the codeFixMode ConfigMap key -
# the most restrictive arm wins, and silence is Off.
#
# PROMOTE IN ORDER: enabled + plan, then watch the plans for a while, then pr - and pr only once
# auth.enabled is true. Approving a repository write on the strength of an unauthenticated click
# is refused at render time AND at startup.
codeFix:
enabled: false
# off | plan | pr. QUOTE IT: YAML 1.1 reads a bare `off` as the boolean false, which the
# schema then refuses with a type error that does not mention quoting.
# off evaluate every escalation and record "would have started a code fix" - no Job, $0.
# plan start a read-only plan Job for each eligible incident.
# pr additionally allow an approved plan to be implemented and opened as a Draft PR.
mode: "off"
# Where coder Jobs run. Must exist (or set createNamespace), and must never be a namespace
# Hephaisto may act in - see the header. The Secret named by secrets.codeFix lives HERE.
namespace: hephaisto-coder
# Render the Namespace object. Off by default because most GitOps setups own namespaces
# elsewhere, and a chart that deletes a namespace on uninstall takes the Secret with it.
createNamespace: false
# The coder image: Claude Code + the Agent SDK, git, gh, the .NET SDK, bun, python. Pinned by
# the chart's appVersion like the agent image, so chart, agent and coder speak one contract.
image:
repository: ghcr.io/truerelevance/hephaisto-coder
# Empty means .Chart.AppVersion.
tag: ""
pullPolicy: IfNotPresent
# The dev-context repository: the coder's CLAUDE.md, rules, skills, curated memory and
# repos.yaml. Cloned at `ref` at the start of EVERY Job, so a merged change is in effect for the
# next attempt with no restart. Pin a tag instead of a branch if you want a review gate between
# "merged" and "used". Needs to be readable with GITHUB_TOKEN.
contextRepository:
url: ""
ref: main
# THE AUTHORIZATION LIST: which workload maps to which repository. Empty maps nothing, so no
# Job can start however eligible an incident is. This is the operator's half of a double
# opt-in; the other half is dev-context's repos.yaml (coderEnabled: true), which the coder
# itself enforces. Same posture as policy.actionableNamespaces.
#
# workload namespace/Kind/name of the CONTROLLER, e.g. shop/Deployment/shop-api
# url the repository to clone and open the PR against
# defaultBranch the PR's base, and the branch that must be protected
# path subdirectory the workload is built from; empty for the root
#
# repositories:
# - workload: shop/Deployment/shop-api
# url: https://github.com/example/shop
# defaultBranch: main
# path: ""
repositories: []
# Every mapped repository's host must be in here, which the chart checks at render time.
# Hephaisto re-checks the PR URL the coder reports against it before recording a PR. Only
# GitHub is supported in this release: the coder opens PRs with `gh`.
allowedRepositoryHosts:
- github.com
# Which primary-finding categories may start a coder. `application` is the whole point;
# `config` is an opt-in (a wrong key in code vs a wrong value in a manifest is often a judgment
# call). `image` is deliberately NEVER a default - a bad image is a build or deploy problem.
# Also never, regardless of this list: Unschedulable, ImagePullBackOff, NodePressure and
# PvcNearlyFull incidents, which are infrastructure by construction.
#
# This list and allowedRepositoryHosts are the WHOLE list: the agent has no built-in entries
# for either (backlog #120), so an empty list here permits nothing.
eligibleCategories:
- application
# A primary finding below this confidence starts nothing.
confidenceFloor: 0.7
# Money and rate. cost is NOMINAL on a subscription (OAuth) token - it is what the same run
# would have cost on the API - so these bound behaviour rather than a bill. A crashed runner is
# charged its phase cap. Coder spend also enters the agent's global LLM budget windows.
budgets:
perPlanUsd: 5
perImplementUsd: 15
perDayUsd: 50
# Per mapped repository per day, so one flapping service cannot open a PR an hour.
attemptsPerRepositoryPerDay: 3
# Coder Jobs in flight at once. 1 is right for a subscription token, which shares a
# rolling usage window with its owner's interactive use - and the NuGet cache below is RWO.
concurrentJobs: 1
# .NET TimeSpan - "hh:mm:ss" or "d.hh:mm:ss" - NOT Go durations: "30m" does not parse and the
# agent would fail to start. Plan and implement deadlines must exceed four minutes; the runner
# reserves three to report. They become the Job's activeDeadlineSeconds.
deadlines:
plan: "00:30:00"
implement: "01:00:00"
# A plan nobody decided on expires. Stale advice is not advice.
approvalTimeout: "3.00:00:00"
# Finished Jobs and their logs are garbage-collected after this - the result is in Postgres.
jobTtl: "01:00:00"
pollInterval: "00:00:15"
# The coder's scratch space (an emptyDir): the clones, the build output, the NuGet packages
# when nugetCache is off.
workspaceSize: 8Gi
# Per container of a coder pod, and so per pod: its containers run one after the other, never
# side by side. A .NET restore + build + test of a mid-sized service wants the memory.
resources:
cpuRequest: "1"
memoryRequest: 2Gi
cpuLimit: "4"
memoryLimit: 6Gi
# A PR is accepted only when the runner's OWN build and tests were green. The runner pushes
# nothing red either way; off stops the agent from refusing a result that reports no green
# build - a repository with no build or test command at all.
requireGreenBuild: true
# THROWAWAY E2E CLUSTERS ONLY. Lets mode: pr run with auth.enabled false, so the approval that
# authorises a repository write is an unauthenticated click. The agent logs a warning at every
# start while this is on.
allowUnauthenticatedApproval: false
# real | fake. fake runs scripted, $0 plumbing (dev and CI): no model is called, and the coder
# refuses to start if a real token is present.
sdk: real
# The Claude model the coder runs (CODEFIX_MODEL): a model id such as claude-haiku-4-5-20251001,
# or a Claude Code alias (haiku, sonnet, opus). Empty lets the CLI pick its default for the
# account - on a subscription usually the most capable and most expensive one. Pin it to bound
# cost; the per-phase budgets below still cap every run whichever model it is.
model: ""
# "" | shim. shim answers `gh` locally instead of calling GitHub - dev and CI only.
gh: ""
# The allowlist egress proxy (squid), in codeFix.namespace. Calico OSS and most CNIs have no
# FQDN NetworkPolicy, so a domain allowlist has to be a proxy: the NetworkPolicy lets coder pods
# reach DNS and this proxy and nothing else, and the proxy lets them reach these domains and
# nothing else. CONNECT only to 443; plain HTTP only to 80. Every request is logged to its
# stdout - the per-attempt outbound audit.
#
# Domain syntax is squid `dstdomain`: `.example.com` matches example.com AND every subdomain;
# `example.com` matches exactly. Squid REFUSES TO START on an entry covered by another one
# (api.nuget.org beside .nuget.org), so the chart drops covered entries when it renders.
egressProxy:
enabled: true
image:
repository: ubuntu/squid
# Ubuntu 24.04 LTS base, squid 6.x. Pinned: a proxy that is the only way out of the pod
# is not something to let float.
tag: 6.6-24.04_edge
pullPolicy: IfNotPresent
allowlist:
# The model (api.anthropic.com) and the OAuth token's endpoints.
- .anthropic.com
- .claude.ai
- platform.claude.com
# git clone/push, gh, release assets.
- github.com
- api.github.com
- codeload.github.com
- objects.githubusercontent.com
- .githubusercontent.com
# NuGet: GitHub Packages and nuget.org.
- nuget.pkg.github.com
- api.nuget.org
- .nuget.org
# npm, PyPI, and GHCR for container base-image lookups a build may make.
- registry.npmjs.org
- pypi.org
- files.pythonhosted.org
- ghcr.io
# Added to `allowlist` rather than replacing it, so admitting one internal package mirror
# does not mean restating the whole list.
extraAllowlist: []
# A PVC for NuGet packages shared across coder Jobs, so every attempt does not start with a
# cold 2-4 minute restore. ReadWriteOnce, which is why concurrentJobs defaults to 1: two Jobs
# scheduled on different nodes cannot both mount it.
#
# Only the `prepare` container writes to it - the restore of the untouched default branch. The
# `coder` container, where the model and its build run, mounts it READ-ONLY (v0.13.0). So with
# the cache on, a fix that adds a package cannot be built in the Job, not even one from
# nuget.org, and the attempt says so; with it off, packages live in the pod's own workspace
# and a public one can still be fetched.
nugetCache:
enabled: false
size: 10Gi
# Empty means the cluster's default StorageClass.
storageClass: ""
# The coder NetworkPolicies. INDEPENDENT of the top-level networkPolicy.enabled, on purpose:
# an install that runs the agent without a policy (the webhook protected some other way) must
# still not run a shell-holding model with open egress. Turning this off is a deliberate act:
# without it the coder can reach anything the cluster can, and the proxy is advisory.
networkPolicy:
enabled: true
# -----------------------------------------------------------------------------------------
# GitHub issues as work (v0.14.0) - OFF by default
# -----------------------------------------------------------------------------------------
# An issue assigned to Hephaisto's account, in a repository listed here, is taken as work. The
# agent ASKS GitHub - a poll per repository, nothing reaches in, no webhook to expose - and an
# unchanged list is answered with a 304 that costs nothing against the rate limit.
#
# The account is an ordinary GitHub account (a machine user), not a GitHub App: an App cannot
# be an assignee. Make it a collaborator on each listed repository, create its token signed in
# as it, and put the token in the Secret secrets.github names.
#
# A taken issue is planned by a coder Job (codeFix has to be enabled; see issues.repositories
# below), the plan is posted on the issue, an approver answers it there, and the pull request the
# implementing Job opens is followed until it is merged or closed. GET /api/workitems shows them,
# and `github` is among the connections of GET /api/status. Unassigning the account, or closing
# the issue, takes it back at any point. With the agent's mode Off nothing is asked and nothing
# is taken.
github:
enabled: false
# The REST API's root. GitHub Enterprise Server's is https://<host>/api/v3.
apiBaseUrl: https://api.github.com
# The account issues are assigned to. Empty means whoever the token belongs to, which is
# right unless the token is somebody else's.
botLogin: ""
# How often each repository is asked. .NET TimeSpan (hh:mm:ss), at least five seconds;
# GitHub's own advice for a poller is a minute.
pollInterval: "00:01:00"
issues:
# THE AUTHORIZATION LIST: owner/repo. An issue anywhere else is never asked about, however
# it is assigned. Required when enabled - an empty list would poll nothing and look fine.
#
# An issue taken from one of these is planned by a coder Job, under codeFix.mode and every
# codeFix cap and switch (with the mode off, the issue is told so and nothing starts). Where
# the code is cloned from: the FIRST codeFix.repositories entry whose url names the
# repository - it ends in /owner/repo or /owner/repo.git - with that entry's defaultBranch
# and path; with no such entry, https://github.com/owner/repo on the default branch GitHub
# reports. Either way the host has to be in codeFix.allowedRepositoryHosts, and the
# repository has to be enabled in the context repository's repos.yaml.
#
# repositories:
# - example/shop
repositories: []
# The board's date field "tomorrow" and "next week" plan an issue by. On that day the issue
# is planned for today; a day that passed with it open makes it left over. Create a field of
# type Date with this name on the project; without one, only today / not today work.
dayField: "Planned for"
# The Status option "backlog" sets.
backlogStatus: "Backlog"
# Who may answer a plan ON THE ISSUE, by account NUMBER - a login can be renamed and taken by
# somebody else. `gh api users/<login> --jq .id`.
#
# An approver answers with a comment whose first line is `/approve`, `/reject` followed by a
# reason, or `/replan`. Nothing else is a command, and nobody else's comment is one: an
# account that is not listed is told so once per plan and changes nothing. What is approved
# is the plan as Hephaisto stored it, never the comment's text. An approval still needs
# codeFix.mode: pr - at plan or off it is refused, and the issue is told which.
#
# `/replan` has the issue planned again, and then what an approver - and the issue's author -
# wrote on the issue since it was handed over is given to the planning Job, as untrusted text.
# Nobody else's comment reaches a Job.
#
# Empty: nobody can answer by comment, no comment is read, and the plan says that it is
# answered in the console. The API is there either way, for whoever the console's approver
# policy admits: POST /api/workitems/{id}/codefix/{attemptId}/approve and .../deny.
#
# These people can make Hephaisto push a branch to every repository listed above. List
# maintainers, not everybody who may comment.
#
# approvers:
# - 1234567
approvers: []
# Send the agent's GitHub calls through codeFix.egressProxy when that proxy is rendered
# (codeFix.enabled and codeFix.egressProxy.enabled): its allowlist already holds
# api.github.com, its log then shows the agent's requests beside the coder's, and with
# networkPolicy.egress.enabled it is the agent's only way out to GitHub - the chart adds the
# rule that lets the agent reach the proxy, and the one that lets the proxy accept it.
#
# Without the proxy and with networkPolicy.egress.enabled, GitHub has to be reachable through
# networkPolicy.egress.extraEgressCIDRs on 443. A GitHub Enterprise host has to be added to
# codeFix.egressProxy.extraAllowlist, and the proxy tunnels to port 443 only.
useEgressProxy: true
# ---------------------------------------------------------------------------------------
# The planner (v0.15.0) - OFF by default
# ---------------------------------------------------------------------------------------
# /planner in the console: the open issues of ONE project of an organisation, walked through
# one at a time, with the day's plan and the whole board grouped by its top-level issues.
# Needs github.enabled: it uses the agent's token, through the same proxy.
#
# It reads the project on a person's page load, never on a timer. Every write is one click
# of somebody who holds the console's approver role, recorded in the audit trail under their
# name and done on GitHub by the token's account: set one of the board's single-select
# fields, close an issue, put the label `today` or `not today` on it, assign it.
#
# THE TOKEN NEEDS MORE THAN ISSUES AS WORK DOES. A fine-grained token: organisation
# permission "Projects: read and write", and "Issues: read and write" on EVERY repository
# whose issues are on the board - not only the ones listed in issues.repositories. A classic
# token: `project` and `repo`. Without the first the page says that the board could not be
# read; without the second a click on an issue of that repository is refused and says so.
#
# Assigning an issue to the agent's account in a repository of issues.repositories hands it
# over: that is what github.enabled means. The planner offers the button; the rule is above.
planner:
enabled: false
# The organisation and the project's number: github.com/orgs/<owner>/projects/<number>.
# A project of a user account is not supported.
owner: ""
number: 0
# Names of the board's fields an item should not leave empty. An item with one of them
# empty is put in front of a person. Empty: no field is asked for.
#
# requiredFields:
# - Priority
# - Area
requiredFields: []
# An issue with an open pull request on which nothing happened for this many days is put in
# front of a person.
staleAfterDays: 5
# The logins an issue can be assigned to from the planner, besides the agent's own account.
#
# assignees:
# - octocat
assignees: []
# Where a new issue can be opened from the planner, as owner/repo; the first is chosen
# unless somebody picks another. The repositories of the board's issues follow, the one
# with the most first - and are all there is when this is empty.
#
# repositories:
# - example/planning
repositories: []
# Claude labels an issue and writes an analysis and a plan of it, when a person asks - a
# button on the planner, or a tick when an issue is opened there. It runs as a coder Job
# (needs codeFix.enabled): the coder's image, namespace, Secret, egress proxy, model and
# dev-context. Labels are only ever added, and only the repository's own; the analysis is
# posted as a comment on the issue.
triage:
enabled: false
maxCostUsd: 2
maxTurns: 40
# hh:mm:ss, between 2 minutes and 2 hours.
deadline: "00:15:00"
maxConcurrentJobs: 2
# -----------------------------------------------------------------------------------------
# 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.
#
# WHICH SERVER. The agent hands the model eighteen of the server's tools, by name, and the
# names are those of mcp-grafana 2.0.1 (chart grafana-mcp 0.27.1), which is what they were
# read from. Traces need a server that has Tempo tools of its own - `search_tempo_traces`,
# `get_tempo_trace`, `list_tempo_attribute_names`, `list_tempo_attribute_values` - and the
# alert's rule is read with `alerting_rules_read`. A server without one of them still works:
# the tool is not offered to the model, the `grafana-mcp` row of /api/status reads
# `Degraded - 13 of 18 tools; missing ...` with the names, and the agent's log says at
# Warning what an investigation can no longer do. Nothing else tells you.
#
# The agent does not use `grafana_api_request`, the server's raw door to Grafana's HTTP API:
# a server started with --disable-api, as the tested one was, loses nothing the agent needs.
grafanaMcp:
# INCLUDE THE PATH. The value is used verbatim as the streamable-http endpoint, so
# `http://grafana-mcp:8000` connects to nothing and every Grafana tool is lost - which
# presents as the agent simply being worse at diagnosis, with no error anywhere.
#
# url: http://grafana-mcp:8000/mcp
#
# Empty disables it, and the agent says so at startup rather than starting silently toolless.
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.
#
# Three names are worth setting. Every trace tool requires `datasourceUid`, so `tempo` saves
# the discovery call on each investigation that reads a trace; and `prometheus` is what the
# model must pass to `alerting_rules_read` to see PrometheusRule rules at all - without a
# datasource that tool answers for Grafana-managed rules only, which is none of the shipped
# ones. Name the Prometheus that EVALUATES the rules, if Grafana has several.
datasourceUids: {}
# prometheus: abc123
# loki: def456
# tempo: ghi789
# Where one pod's logs are in Loki: a LogQL stream selector with <namespace> and <pod> where
# those go. Set, the model is told to read pod logs in Loki first - every container of the
# pod and what it logged before a restart, in one query - and to keep get_pod_logs for what
# Loki does not have. Empty, it is told nothing and reads logs from the Kubernetes API.
#
# The label names are your log shipper's. Include the cluster label if Loki holds several.
#
# promtail / alloy: '{namespace="<namespace>", pod="<pod>"}'
# OTel k8sattributes: '{k8s_namespace_name="<namespace>", k8s_pod_name="<pod>"}'
podLogSelector: ""
# -----------------------------------------------------------------------------------------
# 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
# OFF by default. `absent()` needs a concrete series to assert the absence of, so this
# rule names ONE service - and on any install not running that exact service the series
# is permanently absent, so it fires after 5m and never stops, feeding the agent an
# incident about a workload that does not exist. Edit files/alerts/no-traffic.yaml to
# name your own continuously-served services before turning this on. See backlog #103.
noTraffic: false
# 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
# The agent's own absence: not running, alerts not reaching it, or unable to write what
# arrives (files/alerts/agent-presence.yaml). Every rule carries hephaisto_route=external, and
# your Alertmanager must route that label to a receiver that is NOT this agent - an agent that
# is down cannot report being down. Leave it on, and add that route.
agentPresence: 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.
# The one value with no default: the value of the `cluster` label on this cluster's metrics.
cluster:
name: ci-minimal
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/truerelevance/hephaisto
tag: "0.0.1"
pullPolicy: IfNotPresent
imagePullSecrets:
- name: ghcr-pull
serviceAccount:
name: hephaisto
annotations:
example.com/owner: sre
cluster:
name: ci-full
investigation:
inScopeNamespaces: [app]
llm:
pricing:
- model: my-gateway-model
inputPerMillionUsd: 0.30
outputPerMillionUsd: 2.50
observabilityNamespace: hephaisto-obs
prometheusOperator:
selectorLabels:
release: kube-prometheus-stack
example.com/extra: "yes"
policy:
actionableNamespaces:
- hephaisto-chaos
- some-app
mode: DryRun
incidents:
healedAfter: "00:15:00"
sweep:
enabled: true
expireAfter: "7.00:00:00"
approvalTimeout: "12:00:00"
secrets:
postgres: hephaisto-postgres
llm: hephaisto-llm
grafanaMcp: grafana-mcp-caller-token
mcp: hephaisto-mcp
github: hephaisto-github
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:
podLogSelector: '{namespace="<namespace>", pod="<pod>"}'
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
# OIDC on, so codeFix.mode below can be pr: approving a repository write needs an
# authenticated human, and the chart refuses pr without it.
auth:
enabled: true
authority: https://keycloak.example.com/realms/hephaisto
approverRole: hephaisto-approver
notifications:
baseUrl: https://hephaisto.example.com
webhook:
url: https://hooks.example.com/hephaisto
teamsBot:
enabled: true
tenantId: f4447a03-0000-0000-0000-000000000000
appId: 58b00500-0000-0000-0000-000000000000
channelId: "19:[email protected]"
teamId: 11111111-0000-0000-0000-000000000000
recipients: [[email protected], [email protected]]
board:
maxIncidents: 15
# The one inbound route Microsoft calls, so the render test covers its port, Service
# entry and NetworkPolicy rule.
actions:
enabled: true
port: 8082
# Entra object ids: who may close from a card.
approvers:
- 5f0c0000-0000-0000-0000-000000000001
- 5f0c0000-0000-0000-0000-000000000002
approvals:
enabled: true
routes:
- channel: webhook
events: [IncidentEscalated, CodeFixPlanReady, CodeFixPrOpened, CodeFixFailed]
- channel: teamsBot
events: [IncidentEscalated, ApprovalRequired, VerificationFailed, CodeFixPlanReady]
minSeverity: Warning
# Every code-fix template at once: its own namespace object, the Role, the unbound
# ServiceAccount, both NetworkPolicies, the proxy with an extra allowlist entry, and the NuGet
# cache. mode pr, because that is the widest thing the chart can be asked to render.
codeFix:
enabled: true
mode: pr
namespace: hephaisto-coder
createNamespace: true
image:
repository: ghcr.io/truerelevance/hephaisto-coder
tag: "0.0.1"
pullPolicy: IfNotPresent
contextRepository:
url: https://github.com/example/dev-context
ref: main
repositories:
- workload: shop/Deployment/shop-api
url: https://github.com/example/shop
defaultBranch: main
path: ""
- workload: shop/StatefulSet/shop-worker
url: https://github.com/example/shop
defaultBranch: main
path: src/Worker
allowedRepositoryHosts: [github.com]
eligibleCategories: [application, config]
confidenceFloor: 0.75
budgets:
perPlanUsd: 4
perImplementUsd: 12.5
perDayUsd: 40
attemptsPerRepositoryPerDay: 2
concurrentJobs: 1
deadlines:
plan: "00:25:00"
implement: "00:50:00"
approvalTimeout: "2.00:00:00"
jobTtl: "02:00:00"
pollInterval: "00:00:20"
workspaceSize: 10Gi
resources:
cpuRequest: 500m
memoryRequest: 2Gi
cpuLimit: "4"
memoryLimit: 6Gi
requireGreenBuild: true
allowUnauthenticatedApproval: false
sdk: real
gh: ""
egressProxy:
enabled: true
extraAllowlist:
- nuget.example.com
nugetCache:
enabled: true
size: 20Gi
storageClass: standard
networkPolicy:
enabled: true
# The MCP endpoint (#157), with one token of each kind and a gateway's namespace admitted.
mcp:
enabled: true
port: 8083
tokens:
- name: gateway
kind: shared
role: reader
- name: oncall
kind: person
subject: oncall
role: approver
mayWrite: false
networkPolicy:
fromNamespaces: [gateway]
# GitHub issues as work (v0.14.0): two repositories, an approver whose number has eight digits,
# and - because the proxy above is on - the agent's GitHub calls through it.
github:
enabled: true
apiBaseUrl: https://api.github.com
botLogin: hephaisto-bot
pollInterval: "00:01:00"
issues:
repositories:
- example/shop
- example/dev-context
approvers:
- 1001
- 12345678
useEgressProxy: true