Skip to content

Configuration

The chart is the supported surface; the environment variables are what the binary actually reads. Both are listed because you will meet both — the chart when you install, and the env vars when you read a pod spec at three in the morning.

The machine-checkable version of this page is charts/bosun/values.schema.json, which helm install validates against before it renders anything.

ValueEnvDefault
git.providerGIT_PROVIDERgithubgithub, gitea implemented; gitlab, bitbucket are extension points
git.ownerGIT_OWNERREQUIRED
git.repoGIT_REPOREQUIRED
git.repoURLGIT_REPO_URLREQUIRED. Clone URL, reachable from the cluster
git.existingSecretREQUIRED. Existing Secret holding the credential; the chart creates none
git.tokenKeyGIT_TOKENtokenKey within that Secret. Its value becomes GIT_TOKEN, which is the name the startup error uses
git.apiBaseGIT_API_BASE(unset)See below — it means different things per host
git.insecureSkipTLSVerifyGIT_INSECURE_SKIP_TLS_VERIFYfalseScoped to the agent’s git client and its push clone, never global
git.author.nameGIT_AUTHOR_NAME(derived)See below — leave empty unless you have a reason
git.author.emailGIT_AUTHOR_EMAIL(derived)
git.app.appIdGITHUB_APP_ID(unset)Set to authenticate as a GitHub App instead of a token
git.app.installationIdGITHUB_APP_INSTALLATION_ID(discovered)Optional; discovered from the repository when unset
git.app.existingSecretgit.existingSecretSecret holding the App private key
git.app.privateKeyKeyGITHUB_APP_PRIVATE_KEYprivate-keyKey within it. Its value becomes GITHUB_APP_PRIVATE_KEY

When git.app.appId is set, GIT_TOKEN is not set at all — installation tokens are minted from the key at runtime and live about an hour, so there is nothing static to hold.

The hosts differ, so the value does:

  • github — the API root: https://ghe.example.com/api/v3
  • gitea — the instance root: https://gitea.example.com. The client appends /api/v1 itself, and also needs that root to build a push remote.

Empty means the agent derives it. As a GitHub App that is its own bot identity (<slug>[bot] <id+slug[bot]@users.noreply.github.com>), which is what makes the pushed commits attribute to the App’s avatar rather than to a stranger.

If you do set an email, never use a users.noreply.github.com address that is not your bot’s: that namespace belongs to GitHub accounts. An earlier default of bosun@users.noreply.github.com attributed every commit the first live repair pushed — avatar and all — to an unrelated account named bosun own.

ValueEnvDefault
llm.providerLLM_PROVIDERREQUIRED. openai or anthropic
llm.modelLLM_MODELREQUIRED
llm.baseURLLLM_BASE_URL(unset)Required for openai; optional for anthropic
llm.reasoningEffortLLM_REASONING_EFFORT(unset)Passed through where supported; leave unset otherwise
llm.timeoutLLM_TIMEOUT10m
llm.existingSecret(unset)Omit entirely for an unauthenticated local endpoint
llm.apiKeyKeyLLM_API_KEYapi-keyKey within that Secret. Its value becomes LLM_API_KEY

There is no default provider. openai reaches OpenAI, Azure OpenAI, LM Studio, Ollama, vLLM, llama.cpp and LiteLLM; anthropic reaches Anthropic and gateways presenting the Messages API. See Model providers for how to choose a model, and why the score to optimise is unsafe actions = 0 rather than accuracy.

ValueEnvDefault
gate.modeGATE_MODEclustercluster — the agent is the gate. ci — the gate runs in CI and the agent waits on the check
gate.checkNameGATE_CHECK_NAMEaddons-gateMust match your branch protection rule
gate.forkPRsGATE_FORK_PRSfalseRender pull requests whose head is in another repository
gate.waitGATE_WAIT10mHow long to wait for a verdict. CI mode only
gate.pollGATE_POLL30sPaces the cluster-mode sweep for new pull requests
gate.reportAuthorGATE_REPORT_AUTHOR(per-host)Whose gate report the agent will believe — see below
gate.inventorySourceINVENTORY_SOURCEsecretsWhere cluster mode reads the live inventory — see below
gate.argocd.baseURLARGOCD_BASE_URLhttps://argocd-server.argocd.svcThe ArgoCD API. inventorySource: argocd only
gate.argocd.port(NetworkPolicy only)443The port the chart’s egress rule opens to the ArgoCD namespace
gate.argocd.existingSecretARGOCD_TOKEN(none)Secret holding the ArgoCD account token, key tokenKey
gate.argocd.caSecretARGOCD_CA_FILE(none)Secret holding the CA that verifies argocd-server, key caKey
gate.argocd.insecureSkipTLSVerifyARGOCD_INSECURE_SKIP_TLS_VERIFYfalseAccept any certificate from argocd-server

gate.inventorySource is where the uncomfortable grant lives

Section titled “gate.inventorySource is where the uncomfortable grant lives”

Cluster mode reads four fields — name, server, labels, annotations — and by default it reads them from the ArgoCD cluster Secrets, which also carry cluster credentials. That grant cannot be made smaller. Kubernetes RBAC has no predicate for “the labels but not the data”: there are no deny rules, resourceNames does not apply to list (a list request carries no name for the authorizer to match), and the label selector the gate sends is a filter the apiserver applies after authorising — so a token holding the Role can drop it and read every Secret in the namespace.

gate.inventorySource: argocd reads the same four fields from GET /api/v1/clusters on the ArgoCD API, which serves them with the credential block redacted. The Role stops being created. Mint the token with

Terminal window
argocd account generate-token --account bosun

and give it one line in argocd-rbac-cmp, bosun, clusters, get, *, allow — and nothing else, or it is a bigger credential than the Secret read it replaced.

It is a trade rather than a win, which is why it is not the default: a second credential to rotate, a second component that can be down on its own (the Secrets are readable whenever the apiserver is; argocd-server is not), and a second TLS story, because argocd-server serves its own certificate rather than the one the kubelet mounts into every pod. The chart adds the NetworkPolicy egress rule for the ArgoCD namespace itself — argocd-server is a ClusterIP, and forgetting that hangs with zero bytes.

In cluster mode the render runs helm over the pull request’s content, inside your cluster. Whose content that is should be an operator’s decision. Off, a fork pull request gets an error status naming this value — a refusal you can see, rather than a required check that never reports.

gate.reportAuthor is an authenticity check

Section titled “gate.reportAuthor is an authenticity check”

The gate publishes its verdict as a pull-request comment carrying a marker. Anyone who can comment can write that marker — and the report under it is what the agent reads to decide which manifests to rewrite and which version strings it will accept as corroborated. A forged report is not a wrong opinion; it is an instruction wearing the gate’s authority.

Empty means the per-host default, because the answer is a fact about the host:

  • githubgithub-actions[bot]. A gate running in GitHub Actions comments through github.token and therefore as that account, every time.
  • gitea — unchecked. Gitea Actions has no equivalent fixed identity; set this to whichever user minted the CI token.

Set it to "*" to read the report whoever wrote it. That is the pre-existing behaviour and there are deployments where it is the only expressible answer — but it should be a decision in your values file rather than an absence.

If your gate comments as a bot user or a PAT’s owner rather than through Actions, name that account here. The symptom of getting it wrong is the agent saying it ignored a report and naming the author it saw.

ValueEnvDefault
triage.allowPathsALLOW_PATHS[]Where the agent may ever write. Empty refuses everything, and the process refuses to start with it
triage.denyPathsDENY_PATHS[]Added to the built-in deny-list; cannot subtract from it
triage.maxAttemptsMAX_ATTEMPTS2Attempt cap, tracked by pull-request label
triage.explainGreenEXPLAIN_GREENtrueExplain green gates on held pull requests
triage.migrateDroppedVersionsMIGRATE_DROPPED_VERSIONStrueThe deterministic apiVersion repair. No model involved
triage.structuralMigrationSTRUCTURAL_MIGRATIONtrueThe document-reshape path, for bumps where swapping the version is not the whole job
triage.migrateMaxDocsMIGRATE_MAX_DOCS5Cap on documents reshaped in one pass
triage.egressDenyEGRESS_DENY[]Hosts the upstream lookup must never reach
triage.upstreamNotes.enabledUPSTREAM_NOTEStrueFetch publisher release notes for the explain and escalate paths
triage.upstreamNotes.maxReleasesUPSTREAM_MAX_RELEASES5
triage.upstreamNotes.maxCommitsUPSTREAM_MAX_COMMITS10
triage.upstreamNotes.maxBodyCharsUPSTREAM_MAX_BODY_CHARS4000

triage.allowPaths is the whole write surface

Section titled “triage.allowPaths is the whole write surface”

An empty allowlist refuses everything and the process refuses to start with one — a service that can write nowhere and does not say so is a service that looks broken later, for a reason nobody will find.

Set it to the tree the agent may repair, typically [addons/**]. It is a standing grant and deliberately coarse; the per-request bound is Scope, set from the promotion’s own file list, and both must pass.

triage.denyPaths adds to a built-in list that configuration cannot remove from. Every entry is a way to make a red gate green without fixing anything:

.github/** the workflows that run the gate
.gitops-gate.yaml what the gate renders, and how
.gitops-gate/** the cluster inventory it compares against
delivery/** the kit itself, including this agent and its prompt
.gitlab-ci.yml the GitLab and Bitbucket equivalents
bitbucket-pipelines.yml
**/kargo-projects/** the merge policy and version constraints
**/kargo-pipelines/** the promotion pipelines themselves

The matcher understands ** at the start of a pattern, at the end, or at both — not in the middle. A wildcard inside a directory name (**/kargo-*/**) is not something the deny-list can express.

triage.upstreamNotes never feeds the write path

Section titled “triage.upstreamNotes never feeds the write path”

Release notes and upstream commits are fetched only on the paths that produce prose — the green-gate explanation and an escalation. The mechanical path, the one that writes files, does not fetch them, so they are not in the evidence string the applier corroborates against.

Without that rule, a commit message containing v1.5.0 would make v1.5.0 a corroborated value to write. See ADR 0005.

ValueEnvDefault
liveReads.enabledLIVE_READSfalseOff by default: everything else the agent reads is public or already in the pull request
liveReads.scope(RBAC only)groupsgroups or wide
liveReads.apiGroups(RBAC only)[]The groups granted under groups scope
liveReads.argocdNamespaceLIVE_READS_ARGOCD_NSargocdAlso the cluster-mode gate’s inventory namespace

Live reads are get and list only. The chart’s ClusterRole has no create, update, patch or delete verb anywhere.

ValueEnvDefault
supervise.enabledSUPERVISE_PIPELINEtrue
supervise.intervalSUPERVISE_INTERVAL10m
metrics.serviceMonitor.enabledfalseScrape /metrics

Read-only: three LISTs and a shallow clone, using the Kargo read the ClusterRole already grants. Both /pipeline and /metrics answer 503 before the first sweep completes, deliberately — a scraper reading zeroes from a supervisor that has not looked yet would record “nothing is wrong” as a measurement.

See The pipeline supervisor for what it looks for and the two alert rules worth having, including the one that fires when the supervisor itself goes quiet.

ValueDefault
networkPolicy.enabledtrue
networkPolicy.flavorstandard
networkPolicy.kargoNamespacekargoWhich namespace may call the triage hook
networkPolicy.egress.dnsNamespacekube-system
networkPolicy.egress.namespaces[]
networkPolicy.egress.ipBlocks[]Your model endpoint goes here
networkPolicy.egress.apiServer.ipBlocks[]The apiserver’s real endpoints
networkPolicy.egress.fqdns[]Registries the upstream lookup may reach
networkPolicy.egress.fqdnPatterns[]
networkPolicy.egress.allowPublicHTTPSfalseYour git host
networkPolicy.egress.allowInternetfalse
ValueEnvDefault
image.repositoryREQUIRED
image.tag / image.digest(appVersion) — prefer a digest
image.pullPolicyIfNotPresent
replicaCount1
service.portAGENT_ADDR8080
branding.nameAGENT_BRANDBosun
serviceAccount.create / .nametrue / (fullname)
rbac.createtrue
resources25m CPU / 64Mi requested, 512Mi memory limit
nodeSelector, tolerations, affinity, podAnnotations, priorityClassNamestandard

The pod spec also carries CLONE_ROOT=/work, which is not a chart value. It is where the agent and the gate clone a pull request’s branch, backed by an emptyDir so the root filesystem can stay read-only. Nothing there is worth persisting — a restart mid-triage starts clean rather than resuming.

branding.mark is deprecated and ignored since 0.17.0. Comments no longer carry an identity header at all — authenticating as a GitHub App puts the name and avatar above every comment already, and a bold header under that was the agent introducing itself twice. Still accepted so setting it does not fail an upgrade.

.gitops-gate.yaml lives in the repository being gated, not in this chart. It is documented separately in the .gitops-gate.yaml reference.