This file provides guidance to All Agents when working with code in this repository.
This is a GitOps-managed home Kubernetes cluster using FluxCD and Talos Linux. Runtime application secrets normally come from 1Password via External Secrets (op-secret-store). SOPS is still used for Flux cluster variables, Talos/bootstrap secrets, and existing SOPS-managed resources.
# List all available tasks
task
# Apply a Flux Kustomization manually
task kubernetes:apply-ks cluster=main path=<category>/<app>/app
# Browse a PVC interactively
task kubernetes:browse-pvc ns=<namespace> claim=<pvc-name>
# Delete failed/evicted pods
task kubernetes:delete-failed-pods
# Sync ExternalSecrets
task kubernetes:sync-secrets ns=<namespace> secret=<name>
# Validate a GitOps app without applying it
task kubernetes:validate-app app=<category>/<app>
# Run focused schema validation on YAML files or directories
task kubernetes:validate-yaml path=<path>
# Resolve a mutable image tag to an immutable digest reference
task kubernetes:pin-image image=docker.io/library/nginx:latest
# Check effective workload images for missing digest pins
task kubernetes:check-image-pins
# Resolve the image and record explicit health, storage, database, and auth decisions first
task kubernetes:preflight-new-app app=tools/example args="--image docker.io/library/nginx:latest --port 8080 --no-health-endpoint --persistence none --database none --auth internal"
# Scaffold from the pinned image and probe decision returned by the preflight
task kubernetes:new-app category=tools app=example args="--image docker.io/library/nginx:latest@sha256:<resolved-digest> --port 8080 --no-health-endpoint --internal"
# Upsert one Homarr app and tile. Pipe credentials as JSON; never write them to disk.
credential_provider | task kubernetes:homarr-upsert-app args="--name Example --href https://example.${HOME_DOMAIN} --icon-url https://example/icon.svg"
# Follow updated PR heads, required checks, and merge through an explicit repository target
task github:deliver-pr pr=<number> args="--merge"
# Watch Flux converge a pushed app
task kubernetes:reconcile-app app=tools/searxng
# Start a verified, bounded Alertmanager silence. Routine app deployments and
# internal cluster work stay off the public status page.
export MAINTENANCE_KUBECONFIG="$HOME/.kube/config"
scripts/cluster-maintenance begin \
--reason "planned cluster maintenance" --duration 4h
# Add --public-status only when user-visible service impact is expected.
# That opt-in requires STATUS_API_KEY from the TheZoo Status item in 1Password.
export STATUS_API_URL="https://status.thezoo.house"
scripts/cluster-maintenance begin \
--reason "planned public service interruption" --duration 4h --public-status
# Admit the exact known persistent SMART interface-speed baseline while still
# failing closed on every other warning or critical alert.
scripts/cluster-maintenance begin \
--reason "planned cluster maintenance" --duration 4h \
--allow-active-alert \
alertname=SmartDeviceInterfaceSlow,kubernetes_node=k8s-rhea,device=sda
# Renew, inspect, or end the exact silence returned by begin
scripts/cluster-maintenance renew --id <silence-id> --duration 4h
scripts/cluster-maintenance status --id <silence-id>
scripts/cluster-maintenance end --id <silence-id>
kubernetes/apps/<category>/<app>/- Application deployments organized by category (arrs, media, observability, etc.)kubernetes/flux/- Flux configuration, repositories, and cluster variableskubernetes/templates/- Reusable templates referenced by app kustomizationskubernetes/bootstrap/- Initial cluster bootstrap resourcestalos/- Talos Linux cluster configuration (talconfig.yaml, patches, secrets).taskfiles/- Task automation scripts
kubernetes/flux/apps.yamlis the root Kustomization that applies everything underkubernetes/apps/- It enables SOPS decryption via
sops-agesecret - Variable substitution from:
cluster-settings(ConfigMap),cluster-secrets(Secret),cluster-ipam(ConfigMap),volsync-schedules(ConfigMap) - Child Kustomizations inherit decryption and substitution unless labeled
substitution.flux.home.arpa/disabled: "true"
Each app follows: kubernetes/apps/<category>/<app>/app/
kustomization.yaml- References resources and templateshelmrelease.yaml- HelmRelease with chart configurationexternalsecret.yaml- Runtime app secrets from 1Password via External Secrets when needed*.sops.yaml- Only for Flux/Talos/bootstrap secrets or existing areas that intentionally use SOPS directly
tools- agent/admin/operator utilities and small cluster helper UIsservices- user-facing internal apps that do not fit a narrower domainnetwork- ingress, DNS, routing, SMTP, tunnels, network controllersdatabase- database operators, shared datastores, caches, and database cluster resourcesobservability- monitoring, alerting, logging, dashboards, status checksmedia- media library, playback, download, or metadata appsarrs- Arr-stack apps and download automationgames- game servers and game-specific routerssecurity- identity, auth, policy, and security services
- Use the tools declared in
.mise.tomlfor this repo:kubectl,sops,age,task,flux2,talhelper,talosctl,krew, Renovate,yayamlls, and Python. - Python is pinned through Mise for standard-library-only validation helpers. The repository does not declare third-party Python dependencies such as PyYAML.
- For YAML inspection and transformation, prefer
yq,kubectl kustomize,helm template, and the scripts underscripts/. - Use
yayamllsfor focused schema and language-server validation. It does not format YAML; preserve the repository's existing YAML style when editing. - If a helper script uses Python, keep it Python standard-library only unless a Python environment and dependency file are added deliberately.
- Do not install Python packages globally/Homebrew just to inspect YAML. If a one-off Python dependency is unavoidable, use an isolated temporary environment and do not make it part of the repo workflow without adding explicit project config.
# HelmRelease
# yaml-language-server: $schema=https://k8s-schemas.pages.dev/helm.toolkit.fluxcd.io/helmrelease_v2.json
# Flux Kustomization
# yaml-language-server: $schema=https://k8s-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json
# Plain kustomization.yaml
# yaml-language-server: $schema=https://json.schemastore.org/kustomizationspec.interval: 10mspec.install.remediation.retries: 3spec.upgrade.cleanupOnFail: truespec.upgrade.remediation.strategy: rollback,retries: 3- Skip CRD management unless chart requires it
- 2-space indentation, no tabs
- Top-level key order:
apiVersion,kind,metadata,spec - Use YAML anchors for repeated values (
&probes,&cephBucket) - Variable substitution uses
${VAR_NAME}syntax (processed by Flux postBuild) - End files with single newline
- Runtime app secrets should use 1Password plus External Secrets by default:
- Create or update an item in the
Kubernetesvault. - Add an
ExternalSecretusingsecretStoreRef.kind: ClusterSecretStoreandsecretStoreRef.name: op-secret-store. - Target the app secret as
<app>-secretunless the chart expects a different name. - Consume the generated Secret with
envFrom.secretRef, explicit env refs, orvaluesFromas appropriate.
- Create or update an item in the
- Use SOPS (
*.sops.yaml) for Flux cluster variables, Talos/bootstrap secrets, and existing SOPS-managed resources, not as the default for new app runtime secrets. - Never commit plaintext secrets.
Prefer existing shared data platforms before adding embedded sidecars or app-local singleton databases.
- Use the existing CloudNativePG/Postgres 17 service for Postgres-compatible apps.
- Default app connection host:
postgres17-pooler.database.svc.cluster.local. - Direct writer host is available as
postgres17-rw.database.svc.cluster.localwhen an app requires direct session behavior that the pooler breaks. - Initialize app databases/users through GitOps-owned init resources or established app patterns, not manual psql mutations.
- Store app DB credentials in 1Password and surface them with
ExternalSecretviaop-secret-store. - Common env pattern:
DATABASE_URL=postgresql://...@postgres17-pooler.database.svc.cluster.local:5432/<db>INIT_POSTGRES_HOST=postgres17-pooler.database.svc.cluster.localINIT_POSTGRES_DBNAME,INIT_POSTGRES_USER,INIT_POSTGRES_PASS,INIT_POSTGRES_SUPER_PASS
- Use the MariaDB Operator in the
databasenamespace for MariaDB/MySQL-compatible apps. - Prefer operator-managed app-specific MariaDB resources through GitOps over embedded database sidecars.
- Use
ceph-blockstorage unless the app has a documented reason to use a different storage backend. - Only use same-pod database sidecars for small apps when no suitable shared/operator path exists, and document why.
- Use shared Dragonfly for Redis-compatible needs unless isolation requires otherwise.
- In-cluster URL pattern:
redis://dragonfly.database.svc.cluster.local:6379/<db>. - If a workload in another namespace uses Dragonfly and network policy is enforced, update the Dragonfly client NetworkPolicy in GitOps before rollout.
- Prefer existing Rook-Ceph object storage/RGW patterns for S3-compatible needs.
- Keep browser-facing presigned URLs on a hostname the browser can reach, not an in-cluster service DNS name.
- Store S3 credentials in 1Password and expose through
ExternalSecret.
- Main config:
talos/talconfig.yaml - Per-node configs:
talos/clusterconfig/ - Patches:
talos/patches/global/andtalos/patches/controlplane/ - Encrypted secrets:
talos/talsecret.sops.yaml
Any planned operation that can roll, drain, reboot, reconcile, or otherwise disrupt
live cluster resources must create a bounded maintenance silence immediately before
the first live mutation. Use scripts/cluster-maintenance begin; do not construct
ad hoc Alertmanager payloads. The helper coordinates concurrent starts with a
continuously renewed Kubernetes Lease, then releases it through resource-versioned
expiration so an earlier operator cannot delete a successor's lock. Release failures
warn without hiding an already verified silence ID, and the Lease ages out safely.
The helper fails closed when Alertmanager HA is not ready, an unrelated unsilenced
warning or critical alert already exists, another owned maintenance window is active,
or both concrete Alertmanager pods do not report the same active silence and expected
alert coverage. A known persistent baseline may be admitted with a repeatable exact
--allow-active-alert selector containing alertname plus at least one scoping label.
The established Renovate baseline is
alertname=SmartDeviceInterfaceSlow,kubernetes_node=k8s-rhea,device=sda. This is an
admission exception only: keep the alert visible, verify its identity has not changed,
and continue to reject every additional warning or critical alert.
The standard matcher covers normal named alerts but deliberately excludes Watchdog
and InfoInhibitor, preserving the dead-man heartbeat and null-routed helper path.
The default window is four hours and the hard maximum is 24 hours. Routine begin
creates only the Alertmanager silence. Do not publish internal implementation work,
new app deployments, rehearsals, or changes with no expected user-visible impact.
Add --public-status only when users are expected to see an outage or degraded
service; that opt-in requires STATUS_API_KEY and creates an identified maintenance
incident before the silence. The helper atomically persists its state under
~/.local/state/home-k8s/cluster-maintenance.json (override with
MAINTENANCE_STATE_FILE). For a public window, if either side fails, it compensates
the side that already succeeded. Renew only the returned silence ID. Call end only after the merged
revision, Flux/Tuppr convergence, affected workloads, cluster health, and
component-relevant routes or APIs are verified. end refuses to restore alerting or
resolve any public notice while unexpected warning or critical alerts remain. It
expires the silence before resolving an attached notice. A failed rollout keeps the window
active only while a protected GitOps rollback is being executed and verified. If the
operator dies, the finite Alertmanager expiry restores alerting automatically while
the persisted state preserves any public notice for explicit resolution.
Run scripts/cluster-maintenance probe to exercise Alertmanager's create/read/delete
lifecycle with a unique, preflighted alert name. The probe rejects any collision,
verifies both members, and expires itself in cleanup even when verification fails.
When using app-template charts:
- Use
controllerswith per-controller containers, probes (anchor&probes), and restrictivesecurityContext defaultPodOptions.securityContext:runAsNonRoot,runAsUser,runAsGroup,fsGroup,seccompProfile: RuntimeDefault- External ingress:
ingressClassName: externalwith external-dns - Internal ingress:
ingressClassName: internal - LoadBalancers: Use Cilium annotation
io.cilium/lb-ipam-ips: "${IPAM_IP_*}"
Use an app-local OCIRepository plus same-namespace
HelmRelease.spec.chartRef whenever upstream publishes the exact chart through
a reliable public OCI registry. The source and release use the app name, and
the OCI source must select the Helm chart content layer:
spec:
layerSelector:
mediaType: application/vnd.cncf.helm.chart.content.v1.tar+gzip
operation: copy
ref:
tag: 1.2.3
url: oci://registry.example/charts/exampleTreat HTTP HelmRepository-to-OCIRepository conversion as a source-only handoff, never as a chart upgrade:
- Add and reconcile the OCIRepository before changing the HelmRelease.
- Preserve the exact chart version, release identity, values, strategies, hooks, post-renderers, target namespace, and every non-source field.
- Keep the shared HelmRepository through the observation and rollback window.
- Run
scripts/check-oci-source-handoffs <base-git-revision>before commit. The guard requires matching source versions, no non-source HelmRelease changes, the app-local OCI house pattern, and byte-identical HTTP and OCI chart packages by default. For an explicitly approved rollout-bearing migration, annotate the OCIRepository withoci.home.arpa/allow-rollout: "true"; this is required for OCI-backed HelmRepository conversions and exact-version package mismatches. - Unless a rollout is explicitly approved, require no rendered workload change, pod restart, resource replacement, delete/prune operation, or application downtime. An approved rollout still preserves chart versions, release identity, Services, PVCs, data, and every non-source HelmRelease field.
- Keep HTTP HelmRepository sources when upstream has no reliable exact OCI publication, and document that exception rather than creating a mirror by default.
- Do not treat an OCI-backed
HelmRepositorytoOCIRepositoryconversion as source-only. Flux decorates OCIRepository chart versions with OCI digest build metadata, which can change templates that reference.Chart.Versionand roll workloads even when the chart archive is identical. Leave these releases on the OCI HelmRepository abstraction unless a separately approved rollout is intended and validated.
Firecrawl chart promotion is intentionally separate from Renovate. The private,
signed downstream publisher dispatches .github/workflows/firecrawl-chart-promotion.yaml
with an immutable version and digest. That workflow verifies the package and exact
OIDC identity, updates only the OCI tag plus its digest comment, opens a ShamuBot
pull request, and enables automatic merge behind the protected Static Analysis and
Flate checks. A GitHub push webhook then asks Hermes to run
scripts/verify-firecrawl-live <main-commit> and close the matching
firecrawl-promotion issue only after Flux, Helm, all workloads, the public route,
and shared Dragonfly, PostgreSQL, and RabbitMQ connections pass.
Before committing a new or changed app, run:
scripts/validate-app <category>/<app>
# or
task kubernetes:validate-app app=<category>/<app>This renders the app kustomization, derives its effective namespace from the owning Flux Kustomization, server-dry-runs non-Secret manifests, resolves declared HelmRepository, OCI-backed HelmRepository, GitRepository, and same-namespace OCIRepository chartRef sources, merges ConfigMap and Secret valuesFrom inputs in Flux order, renders HelmReleases with common Flux substitutions, rejects malformed chart documents, checks effective PodSpec images for immutable digests, and server-dry-runs the rendered chart output. Raw Secret manifests are skipped during local dry-run because SOPS material may only be available to Flux. Use scripts/validate-app --offline <category>/<app> for credential-free rendering; required valuesFrom resources must then be present in the rendered app context.
Repository-wide image validation uses both scripts/check-image-pins kubernetes/apps for rendered Kustomize workloads and scripts/check-helmrelease-images for Helm-rendered workloads. The Helm check freezes existing legacy tag-only chart defaults in scripts/helm-image-pin-baseline.txt; new entries should be digest-pinned rather than added to the baseline. Offline render gaps must be explicit and reasoned in scripts/helm-image-validation-skips.txt.
For public images, pin mutable tags before committing:
scripts/pin-image docker.io/searxng/searxng:latest
# or
task kubernetes:pin-image image=docker.io/searxng/searxng:latestBefore scaffolding a new standard app-template app, make the runtime contract explicit and resolve the mutable image to an immutable digest:
scripts/preflight-new-app tools/example \
--image docker.io/library/nginx:latest \
--port 8080 \
--no-health-endpoint \
--persistence none \
--database none \
--auth internalWhen upstream source is available, pass --source <checkout> --strict-evidence to require source evidence for the declared port, health endpoint, persistence paths, and database choice. The report includes image platforms, entrypoint, command, exposed ports, declared volumes, and environment variable names when crane is available. Unknowns are warnings rather than guessed values.
Then scaffold the app with the pinned image returned by the preflight:
scripts/new-app tools example --image docker.io/library/nginx:latest@sha256:<resolved-digest> --port 8080 --health-path / --internal
# or
task kubernetes:new-app category=tools app=example args="--image docker.io/library/nginx:latest@sha256:<resolved-digest> --port 8080 --health-path / --internal"For focused YAML validation, scripts/validate-yaml automatically recognizes a single kubernetes/apps/<category>/<app> root and runs the canonical scripts/validate-app --offline workflow. This avoids reading whichever Kubernetes context happens to be active. Set VALIDATE_YAML_APP_MODE=online only when cluster-backed substitution and server-side dry-run are intentional. Exit code 201 from yayamlls is explained as a schema failure or unsupported schema instead of being returned without context.
For Homarr v1 dashboard updates, use scripts/homarr-upsert-app. It reads { "username": "...", "password": "..." } from stdin, owns and closes its local port-forward unless --base-url is supplied, locally serializes upserts matching either app name or URL, deduplicates both the app and board tile, and verifies the result by read-back. Explicit remote base URLs must use HTTPS; plaintext HTTP is accepted only on loopback. Do not put Homarr credentials in arguments, environment variables, or temporary files.
For PR delivery, use scripts/pr-deliver <pr> [--repo owner/name] [--merge]. It keeps one watcher per repository/PR, follows a changed head SHA instead of reporting stale checks, and always passes an explicit repository to gh pr merge, avoiding worktree checkout conflicts.
After pushing app changes, watch Flux converge with:
scripts/reconcile-app tools/searxng
# or
task kubernetes:reconcile-app app=tools/searxngDo not force Flux reconciliation as the default agent workflow. Push Git changes, then watch the existing Flux controllers converge. Manual flux reconcile ... is for explicit break-glass/debug use, not normal app rollout.