Declared-State Files

The files in this repo that declare state living somewhere else (the forge, a host, a vault, a cluster), each with the one task that applies it and the --check that catches drift. This is the convention; the table is the inventory. Add a row when you add a file.

The convention

  • One file, one owner. A declared-state file has exactly one task that applies it. Other consumers may read it, but only the owner writes the live system from it.
  • --check is the drift gate. The owner task has a --check mode that reads no vault, compares live state against the file, and exits non-zero on drift. It runs on the weekly schedule and, where the file is in the diff, as a PR check. A check that cannot see is a failure, never a pass (UNKNOWN counts as red).
  • Apply reads back. Every mutation is read back and compared, because some APIs answer 200 without applying (Forgejo’s hook edit, and Tailscale SSH on indri returns exit 0 for everything).
  • Metadata only. A declared-state file names things; it never holds a secret value. Secrets are referenced by op:// path or by on-host file path, and the public repo stays clean.
  • Only blumeops names this instance. talos and horkos carry no config or state specific to this deployment. Anything that says eblume, eblu.me, a vault name or a host name belongs here, and reaches them as a mounted file or an environment variable (repos.json is the model). A default in their code that falls back to this instance’s hostname counts as instance config and belongs here too.
  • Nix reads JSON. When a nix configuration consumes the file, write it as JSON so builtins.fromJSON (builtins.readFile ./file.json) works without a converter. YAML is fine when nothing in nix reads it.

The inventory

FileDeclaresOwner taskCheckOther readers
argocd/manifests/talos/repos.jsonwhich repos the talos bot may touch, the pod clone pool, which repos get the forge webhooksmise run forge-reconcileforge-reconcile --check (weekly, PR)the talos pod entrypoint, via a kustomize ConfigMap
forge/branch-protections.jsonbranch protection rules on main — the single source, so an undeclared live rule is drift that forge-reconcile apply deletesmise run forge-reconcilesamenone
forge/tag-protections.jsontag protection rules (Forgejo 16: name_pattern + push whitelist) — the single source, so an undeclared live rule is drift that forge-reconcile apply deletesmise run forge-reconcilesamenone
forge/runners.jsonrunner topology: name, host, labels, scope, capacity, unitmise run forge-reconcileforge-reconcile --checknixos/ringtail/configuration.nix, darwin/indri/configuration.nix
warrant-policy.yamlwhich workflows are requestable, their class, input schemas, SHA bindingnone; it is read at request and dispatch timerequest-run refuses at request timemise run request-run, horkos
service-versions.yamlper-service review freshness and current versionmise run service-review (human, updates the file)container-version-check for container pinsforge-name-lint, one ringtail nix pin
mise-tasks/forge-name-lint.allowlist.yamlexceptions to the forge-name lintmise run forge-name-lintthe lint itselfnone
ntfy/topics.jsonntfy topics: purpose, producers, notification budget (volume, priority, actionable)mise run ntfy-lintthe lint itself (Ntfy Lint PR check + prek hook)the Topics table in docs/reference/services/ntfy.md (hand-mirrored); the ringtail and indri apply-watcher NTFY_TOPIC via ntfy/topics.nix; the two auto-apply workflows’ refusal topic at runtime (raw fetch at main, falling back to the ops-approvals literal)
forge/actions-secrets.jsonthe Actions secret names per repomise run forge-reconcile (values half: human-only PUT/DELETE from the in-code 1Password sources)forge-reconcile --check compares namesnone
darwin/indri/secrets.json, nixos/ringtail/secrets.jsonon-host secret files by path, op:// source, owner, mode, unit to restartmise run seed-secretsseed-secrets --check (vault-free; weekly host systemd timer on ringtail)the per-host flake invariant (invariants.nix lockstep)

One glob-semantics difference from protected_file_patterns: tag name patterns are compiled by Gitea’s glob without separators, so * matches dots (v* matches v0.1.0), while protected_file_patterns compiles . and / as separators; a tag pattern of the form /regex/ (leading and trailing slash) is a raw regex.

forge/tag-protections.json stays empty for eblume/release-kit by design: the kit’s own release workflow creates tags as the synthetic forgejo-actions user (id -2), which Forgejo v16 tag-protection whitelists cannot resolve (routers/api/v1/repo/tag.go, models/git/protected_tag.go — DB users and teams only), so a v* rule would 422 the kit’s own release (#1661). Instead the tag invariant is enforced over live writers: forge-reconcile --check fails on any write-or-above collaborator on release-kit other than eblume and on any write deploy key; the main branch-protection half is already declared in forge/branch-protections.json (eblume-only whitelists) and drift-checked by the same --check.

The same mechanism rules out a tag rule on eblume/hephaestus as well: its release workflow git pushes its tags with the Actions task token, which authenticates as the same synthetic user, so a v* rule would block the repo’s own release. Its tag fence is therefore the same as release-kit’s — no bot write (the bot stays read/fork, agent-containerization); tags are cut by the release workflow on eblume-merged main pushes only.

Planned, filed as issues on 2026-10-12:

FileDeclaresIssue
talos instance documentforge URL, bot login, creator allowlist, horkos and argocd endpointseblume/talos#314

What is not a declared-state file

  • ArgoCD manifests under argocd/: ArgoCD itself is the owner and the reconciler, and argocd app diff is the check. See argocd.
  • The nix configurations under darwin/ and nixos/: the generation switch is the owner, indri-flake-check and ringtail-flake-check the pre-apply checks, invariants.nix the guards.
  • Changelog fragments, prek and mise config: tooling, not state.