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.
--checkis the drift gate. The owner task has a--checkmode 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 (UNKNOWNcounts 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.jsonis 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
| File | Declares | Owner task | Check | Other readers |
|---|---|---|---|---|
argocd/manifests/talos/repos.json | which repos the talos bot may touch, the pod clone pool, which repos get the forge webhooks | mise run forge-reconcile | forge-reconcile --check (weekly, PR) | the talos pod entrypoint, via a kustomize ConfigMap |
forge/branch-protections.json | branch protection rules on main — the single source, so an undeclared live rule is drift that forge-reconcile apply deletes | mise run forge-reconcile | same | none |
forge/tag-protections.json | tag protection rules (Forgejo 16: name_pattern + push whitelist) — the single source, so an undeclared live rule is drift that forge-reconcile apply deletes | mise run forge-reconcile | same | none |
forge/runners.json | runner topology: name, host, labels, scope, capacity, unit | mise run forge-reconcile | forge-reconcile --check | nixos/ringtail/configuration.nix, darwin/indri/configuration.nix |
warrant-policy.yaml | which workflows are requestable, their class, input schemas, SHA binding | none; it is read at request and dispatch time | request-run refuses at request time | mise run request-run, horkos |
service-versions.yaml | per-service review freshness and current version | mise run service-review (human, updates the file) | container-version-check for container pins | forge-name-lint, one ringtail nix pin |
mise-tasks/forge-name-lint.allowlist.yaml | exceptions to the forge-name lint | mise run forge-name-lint | the lint itself | none |
ntfy/topics.json | ntfy topics: purpose, producers, notification budget (volume, priority, actionable) | mise run ntfy-lint | the 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.json | the Actions secret names per repo | mise run forge-reconcile (values half: human-only PUT/DELETE from the in-code 1Password sources) | forge-reconcile --check compares names | none |
darwin/indri/secrets.json, nixos/ringtail/secrets.json | on-host secret files by path, op:// source, owner, mode, unit to restart | mise run seed-secrets | seed-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:
| File | Declares | Issue |
|---|---|---|
| talos instance document | forge URL, bot login, creator allowlist, horkos and argocd endpoints | eblume/talos#314 |
What is not a declared-state file
- ArgoCD manifests under
argocd/: ArgoCD itself is the owner and the reconciler, andargocd app diffis the check. See argocd. - The nix configurations under
darwin/andnixos/: the generation switch is the owner,indri-flake-checkandringtail-flake-checkthe pre-apply checks,invariants.nixthe guards. - Changelog fragments, prek and mise config: tooling, not state.
Related
- warrant-approval-gated-runs for why the policy file is reviewed data rather than tribal knowledge.
- principals and add-a-freestanding-repo for
repos.jsonin practice. - seed-secrets for seating the on-host secret files the per-host
secrets.jsonmanifests declare. - security-model for the vault tiers the
op://references point into.