Skip to content

Agent options ​

Every option class the agent binds, its config section, and its defaults. Most are not exposed as Helm values — set those through extraEnv.

Durations are .NET TimeSpan strings ("00:10:00", not "10m").

Cluster ​

KeyTypeDefault
Cluster:Namestringnone — required, chart value cluster.name

The value of the cluster label on this cluster's metrics and logs. It fills Ingest:ClusterName, Kubernetes:ClusterName and Investigation:Environment:ClusterName, which used to be set separately and all defaulted to this repository's development cluster. An empty name refuses to start; a legacy key that disagrees with it is overridden and logged.

Llm ​

The chat model, the embedding model, and what a single investigation is allowed to spend.

KeyTypeDefault
Llm:Providerstringgemini
Llm:Modelstringgemini-3.7-flash
Llm:PlanningModelstring?null — falls back to Model
Llm:EmbeddingProviderstringgemini
Llm:EmbeddingModelstringgemini-embedding-001
Llm:EmbeddingDimensionsint768
Llm:ApiKeystring?null — from the secret
Llm:EmbeddingApiKeystring?null
Llm:EmbeddingEndpointstring?null
Llm:Endpointstring?null
Llm:ApiVersionstring?null
Llm:PlanningStructuredOutputJsonSchema | JsonObjectJsonSchema
Llm:Temperaturedouble0.2
Llm:MaxOutputTokensint?8192
Llm:Pricingmap of id → pricethe Gemini models and a few openai-compatible ones
Llm:Priceslist of {Model, InputPerMillionUsd, OutputPerMillionUsd}[] — chart value llm.pricing

An unpriced model does not start

Llm:Pricing maps a model id to InputPerMillionUsd / OutputPerMillionUsd; Llm:Prices adds to it as a list, because a model id like gpt-oss:120b cannot be a configuration key in an environment variable. A model with no price costs nothing as far as the budget is concerned, which would switch the cost budget off rather than approximate it - so with any cost cap set, the agent refuses to start on a model or planning model it cannot price. A price of 0 is accepted. A response under a model id the gateway chose is priced as the configured model.

The seeded Gemini prices are promotional and double on 2027-01-01. Nothing in the code knows that date.

JsonSchema is not universally supported

DeepSeek answers 400 "This response_format type is unavailable now". Phase 1 is unaffected, so the agent diagnoses correctly and then proposes nothing — indistinguishable in a run summary from an agent that considered acting and declined. JsonObject moves the schema into the prompt. It is the weaker mode and stays off by default, and it is safe only because nothing downstream takes the model's word for it: every cited finding id is verified, and an action missing a namespace, kind or name is dropped before an executor sees it.

Llm:Investigation — one investigation's budget ​

KeyTypeDefault
MaxStepsint12
MaxToolCallsint20
MaxWallClockTimeSpan00:10:00
MaxInputTokenslong400000
MaxCostUsddecimal0.50
MaxConsecutiveNoToolTurnsint2

A reserved concluding step lets a run that hits MaxSteps still write a finding. It releases two calls rather than one, because the conclusion is taken through the conclude tool and a tool call is two model round trips; reserving a single step paid for the first and refused the second, which is #78.

That rescue used not to be able to land when the exhausted ceiling was tokens, because the concluding call resends the conversation. That limitation was closed by #82 — the conversation is trimmed for the concluding call — and this page went on describing it for two releases after.

Llm:Budget — the rolling global budget, counted in Postgres ​

KeyTypeDefault
MaxTokensPerHourlong2000000
MaxCostUsdPerHourdecimal3.00
MaxCostUsdPerDaydecimal20.00
MaxCostUsdPerIncidentdecimal0.50
WarnAtUtilizationdouble0.80
RunawayHourlyHitsBeforeLatchint3
RunawayWindowTimeSpan1.00:00:00

Llm:Retry ​

KeyTypeDefault
Attemptsint5
InitialDelayTimeSpan00:00:01
MaxDelayTimeSpan00:00:30
ExpBasedouble2.0
Jitterdouble1.0

Llm:Tools — what a tool call may return ​

KeyTypeDefault
TimeoutTimeSpan00:00:20
MaxResultBytesint8192
MaxRawBytesint1000000
MaxQueryRangeTimeSpan7.00:00:00
RequireTimeRangebooltrue
RedactedArgumentNameslistseeded

Redaction covers arguments, not results

RedactedArgumentNames redacts tool arguments in logs and traces. It does not redact tool results — raw describe_pod and get_pod_logs output carries env vars, hostnames and log contents verbatim into evidence blobs. That is why recorded cassettes are not committed to the repository.

Policy ​

The default-deny policy engine. A pure function over facts passed in by the caller, which is what makes it exhaustively unit-testable.

KeyTypeDefault
AllowedNamespacesset[] — act nowhere
ProtectedNamespacessetseeded, never actionable
AutoEnabledActionTypesset[]
ProtectedLabelsmapseeded
RequiredNamespaceLabelstringhephaisto.dev/destructive-actions-allowed
AllowSingleReplicaRestartLabelstringhephaisto.dev/allow-single-replica-restart
MaxPodsPerActionint10
MaxWorkloadFractiondouble0.5
MaxActionsPerIncidentint3
MaxActionsPerWorkloadPerHourint2
MaxActionsPerHourint10
MaxActionsPerDayint20
WorkloadCooldownTimeSpan00:15:00
MinPodAgeBeforeActionTimeSpan00:02:00
ClusterUnhealthyCeilingdouble0.3
RollbackFreshRevisionWindowTimeSpan00:30:00
RollbackPreviousHealthyMinimumTimeSpan01:00:00
MaintenanceWindowslist[]

ClusterUnhealthyCeiling is the circuit breaker: above that fraction of unhealthy workloads the agent stops acting, on the reasoning that a cluster-wide problem is not one a pod restart fixes.

Kubernetes ​

KeyTypeDefault
Enabledbooltrue
ClusterNamestringfrom Cluster:Name
KubeconfigPath / KubeconfigContextstring?null
ReadableNamespaces / DeniedNamespacesset{}
SignalQueueCapacityint2048
RelistIntervalTimeSpan00:10:00
WatchTimeoutTimeSpan00:05:00
ReconnectBaseDelay / ReconnectMaxDelayTimeSpan00:00:01 / 00:01:00
StormThresholdint50
StormWindowTimeSpan00:00:30
StormAggregateIntervalTimeSpan00:01:00
RestartStormThresholdint3
RestartStormWindowTimeSpan00:10:00
IgnoredKindsSignalKind[][]
HealedAfterTimeSpan00:10:00 (chart: incidents.healedAfter; zero turns it off)
HealedCheckIntervalTimeSpan00:02:00
ReadinessFlapThresholdint4
ReadinessFlapWindowTimeSpan00:10:00
LogTailLinesint2000
LogLimitBytesint4194304
MaxRowsint200
RbacModeEnforce | WarnOnlyEnforce

Kubernetes:Enabled=false is a demo setting

It skips the RBAC self-check and the watchers, and leaves the executor that refuses everything in place. An agent that watches nothing while reporting itself healthy is the worst failure mode this project has, so it logs at warning level on every start. It exists so the console can boot on a laptop with no kubeconfig.

Persistence ​

KeyTypeDefault
ConnectionStringstring?null
ConnectionStringNamestringhephaisto
AppConnectionStringNamestringhephaisto_app
ApplyMigrationsOnStartupboolfalse
CommandTimeoutTimeSpan00:00:30
EvidenceBlobRetentionTimeSpan30.00:00:00
LlmUsageRetentionTimeSpan30.00:00:00
NotificationRetentionTimeSpan30.00:00:00
RetentionSweepIntervalTimeSpan01:00:00
RetentionBatchSizeint1000
MaxAdmissionRetriesint3
SearchPoolFactorint8
SearchMinPoolint100

Two connection strings, deliberately. The serving role holds INSERT but not UPDATE or DELETE on audit_events — Postgres cannot restrain a table's owner, so audit immutability depends on the agent not being one.

Investigation ​

KeyTypeDefault
MaxOuterTurnsint8
MinConfidenceForPlandouble0.5
PlanningCostUsddecimal0.10
PlanningTimeoutTimeSpan00:01:30
PlanningMaxInputTokenslong200000
EvidenceBlobRetentionTimeSpan30.00:00:00
StallNudge, FinalConclusionNudge, OpeningMessagestringsee prompts

Investigation:Environment — the environment card ​

What the agent is told about your cluster.

KeyTypeDefault
ClusterNamestringfrom Cluster:Name
InScopeNamespaceslist[] — chart value investigation.inScopeNamespaces; empty omits the line
ProtectedNamespaceslist[] — empty names Policy:ProtectedNamespaces, the enforced list
DatasourceUidsmap{}
PodLogSelectorstringempty (chart: grafanaMcp.podLogSelector)
WorkloadOwnersmap{}
Noteslist[]

Ingest ​

KeyTypeDefault
ClusterNamestringfrom Cluster:Name
BurstWindowTimeSpan00:05:00
FlapThresholdint3
FlapWindowTimeSpan01:00:00
FlapCooldownTimeSpan04:00:00
CorrelationWindowTimeSpan00:10:00
SelfNamespacesset["hephaisto","hephaisto-obs"]

Web ​

KeyTypeDefault
WebhookPortint0 — chart value webhookPort; 0 keeps /webhooks on 8080
MainPortint8080
TrustForwardedHeadersboolfalse
WebhookTokenstring?null — chart value secrets.webhookToken; at least 16 characters

With WebhookToken set, /webhooks/* answers 401 to a request without Authorization: Bearer <token>. Alertmanager sends it with http_config.authorization.credentials_file.

Notifications ​

KeyTypeDefault
BaseUrlstring?null — required once any route exists
Routeslist[]
Webhook:Url, Webhook:SigningSecretstring?null
Teams:WorkflowUrlstring?null
GrafanaUrlstring?null
MaxPerChannelPerHourint60
CorrelationCooldownTimeSpan00:15:00
MaxAttemptsint8
FirstRetryDelayTimeSpan00:00:30
MaxRetryDelayTimeSpan00:30:00
DispatchIntervalTimeSpan00:00:10
DispatchBatchSizeint20
SendTimeoutTimeSpan00:00:10

A route is Channel, Events, MinSeverity (default Info) and Namespaces. Routing is additive only — there is no deny rule. See Notifications.

Grafana ​

KeyTypeDefault
McpUrlstring?null — no PromQL/LogQL tools without it
ServiceAccountTokenstring?null
Urlstring?null
AnnotationTokenstring?null
AnnotationTimeoutTimeSpan00:00:05
ToolCacheDurationTimeSpan00:00:30
ConnectTimeoutTimeSpan00:00:10
AllowedToolslistthe eighteen tools of the server handed to the model - see requirements

AllowedTools can be added to and not replaced: a configured entry is appended to the default list, as .NET configuration does for every list with a default. A name the server does not offer is not handed to the model and turns the grafana-mcp connection Degraded.

Alertmanager ​

Write-only, and the only thing written is a silence.

KeyTypeDefault
Urlstring?null
MaxDurationTimeSpan02:00:00
DefaultDurationTimeSpan00:30:00
TimeoutTimeSpan00:00:10

KillSwitch ​

KeyTypeDefault
SwitchDirectorystring?null
ModeFileNamestringmode
EmergencyStopFileNamestringkillSwitch
ModeEnvironmentVariablestringHEPHAISTO_MODE

Demo ​

KeyTypeDefault
Seedboolfalse
TranscriptPathstringDemo/transcripts

Refuses outright if the database already holds an incident, so it cannot overwrite anything.

AGPL-3.0-only · Source