Skip to content

Helm values ​

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

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

The four that decide what the agent can do ​

Read these before the rest.

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

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-stack
yaml
# Everything on at once. Exists so that a template only exercised by an unusual combination
# still gets rendered in CI.
image:
  repository: ghcr.io/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

AGPL-3.0-only · Source